cre-sync/lib/ghl/services/conversations.ts
BusyBee3333 4e6467ffb0 Add CRESync CRM application with Setup page
- Build complete Next.js CRM for commercial real estate
- Add authentication with JWT sessions and role-based access
- Add GoHighLevel API integration for contacts, conversations, opportunities
- Add AI-powered Control Center with tool calling
- Add Setup page with onboarding checklist (/setup)
- Add sidebar navigation with Setup menu item
- Fix type errors in onboarding API, GHL services, and control center tools
- Add Prisma schema with SQLite for local development
- Add UI components with clay morphism design system

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 17:30:55 -05:00

184 lines
5.8 KiB
TypeScript

import { GHLClient } from '../client';
import {
GHLConversation,
GHLMessage,
SendMessageDTO,
GHLPaginatedResponse
} from '@/types/ghl';
export interface ConversationSearchParams {
locationId?: string;
contactId?: string;
limit?: number;
startAfterId?: string;
status?: 'all' | 'read' | 'unread' | 'starred';
}
export interface SendSMSParams {
contactId: string;
message: string;
fromNumber?: string;
}
export interface SendEmailParams {
contactId: string;
subject: string;
htmlBody: string;
fromEmail?: string;
fromName?: string;
replyTo?: string;
cc?: string[];
bcc?: string[];
attachments?: { url: string; name: string }[];
}
export class ConversationsService {
constructor(private client: GHLClient) {}
// Get all conversations
async getAll(params?: ConversationSearchParams): Promise<GHLPaginatedResponse<GHLConversation>> {
const searchParams: Record<string, string> = {
locationId: params?.locationId || this.client.locationID,
};
if (params?.limit) searchParams.limit = String(params.limit);
if (params?.startAfterId) searchParams.startAfterId = params.startAfterId;
if (params?.contactId) searchParams.contactId = params.contactId;
if (params?.status) searchParams.status = params.status;
// v2 API returns { conversations: [...] } - normalize to { data: [...] }
const response = await this.client.get<{ conversations: GHLConversation[]; total?: number }>('/conversations/search', searchParams);
return {
data: response.conversations || [],
meta: { total: response.total || response.conversations?.length || 0, currentPage: 1 },
};
}
// Get a single conversation by ID
async getById(conversationId: string): Promise<GHLConversation> {
return this.client.get(`/conversations/${conversationId}`);
}
// Get conversation by contact ID
async getByContactId(contactId: string): Promise<GHLConversation | null> {
const result = await this.getAll({ contactId, limit: 1 });
return result.data?.[0] || null;
}
// Get messages in a conversation
async getMessages(conversationId: string, params?: {
limit?: number;
startAfterId?: string;
}): Promise<GHLPaginatedResponse<GHLMessage>> {
const searchParams: Record<string, string> = {};
if (params?.limit) searchParams.limit = String(params.limit);
if (params?.startAfterId) searchParams.startAfterId = params.startAfterId;
// v2 API returns { messages: [...] } - normalize to { data: [...] }
const response = await this.client.get<{ messages: GHLMessage[]; total?: number }>(`/conversations/${conversationId}/messages`, searchParams);
return {
data: response.messages || [],
meta: { total: response.total || response.messages?.length || 0, currentPage: 1 },
};
}
// Send an SMS message
async sendSMS(params: SendSMSParams): Promise<GHLMessage> {
return this.client.post('/conversations/messages', {
type: 'SMS',
contactId: params.contactId,
message: params.message,
...(params.fromNumber && { fromNumber: params.fromNumber }),
});
}
// Send an email message
async sendEmail(params: SendEmailParams): Promise<GHLMessage> {
return this.client.post('/conversations/messages', {
type: 'Email',
contactId: params.contactId,
subject: params.subject,
html: params.htmlBody,
...(params.fromEmail && { from: params.fromEmail }),
...(params.fromName && { fromName: params.fromName }),
...(params.replyTo && { replyTo: params.replyTo }),
...(params.cc?.length && { cc: params.cc }),
...(params.bcc?.length && { bcc: params.bcc }),
...(params.attachments?.length && { attachments: params.attachments }),
});
}
// Generic send message (SMS or Email)
async send(params: SendMessageDTO): Promise<GHLMessage> {
if (params.type === 'SMS') {
return this.sendSMS({
contactId: params.contactId,
message: params.message || '',
});
} else {
return this.sendEmail({
contactId: params.contactId,
subject: params.subject || '',
htmlBody: params.message || '',
});
}
}
// Mark conversation as read
async markAsRead(conversationId: string): Promise<void> {
await this.client.put(`/conversations/${conversationId}/status`, {
status: 'read',
});
}
// Mark conversation as unread
async markAsUnread(conversationId: string): Promise<void> {
await this.client.put(`/conversations/${conversationId}/status`, {
status: 'unread',
});
}
// Star a conversation
async star(conversationId: string): Promise<void> {
await this.client.put(`/conversations/${conversationId}/status`, {
starred: true,
});
}
// Unstar a conversation
async unstar(conversationId: string): Promise<void> {
await this.client.put(`/conversations/${conversationId}/status`, {
starred: false,
});
}
// Delete a conversation (archive)
async delete(conversationId: string): Promise<void> {
await this.client.delete(`/conversations/${conversationId}`);
}
// Get unread count
async getUnreadCount(): Promise<number> {
const result = await this.getAll({ status: 'unread', limit: 1 });
return result.meta?.total || 0;
}
// Schedule a message (if supported)
async scheduleMessage(params: SendMessageDTO & { scheduledAt: Date }): Promise<GHLMessage> {
return this.client.post('/conversations/messages', {
type: params.type,
contactId: params.contactId,
message: params.message,
...(params.subject && { subject: params.subject }),
scheduledTimestamp: params.scheduledAt.toISOString(),
});
}
// Cancel a scheduled message
async cancelScheduledMessage(messageId: string): Promise<void> {
await this.client.delete(`/conversations/messages/${messageId}/schedule`);
}
}