- 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>
220 lines
7.0 KiB
TypeScript
220 lines
7.0 KiB
TypeScript
import { GHLClient } from '../client';
|
|
import {
|
|
GHLOpportunity,
|
|
GHLPipeline,
|
|
GHLPipelineStage,
|
|
GHLPaginatedResponse
|
|
} from '@/types/ghl';
|
|
|
|
export interface CreateOpportunityDTO {
|
|
name: string;
|
|
pipelineId: string;
|
|
pipelineStageId: string;
|
|
contactId: string;
|
|
monetaryValue?: number;
|
|
assignedTo?: string;
|
|
status?: 'open' | 'won' | 'lost' | 'abandoned';
|
|
customFields?: { key: string; value: any }[];
|
|
}
|
|
|
|
export interface UpdateOpportunityDTO {
|
|
name?: string;
|
|
pipelineStageId?: string;
|
|
monetaryValue?: number;
|
|
status?: 'open' | 'won' | 'lost' | 'abandoned';
|
|
assignedTo?: string;
|
|
customFields?: { key: string; value: any }[];
|
|
}
|
|
|
|
export interface OpportunitySearchParams {
|
|
locationId?: string;
|
|
pipelineId?: string;
|
|
pipelineStageId?: string;
|
|
contactId?: string;
|
|
status?: 'open' | 'won' | 'lost' | 'abandoned' | 'all';
|
|
assignedTo?: string;
|
|
limit?: number;
|
|
startAfterId?: string;
|
|
query?: string;
|
|
}
|
|
|
|
export class OpportunitiesService {
|
|
constructor(private client: GHLClient) {}
|
|
|
|
// Get all opportunities
|
|
async getAll(params?: OpportunitySearchParams): Promise<GHLPaginatedResponse<GHLOpportunity>> {
|
|
const searchParams: Record<string, string> = {
|
|
locationId: params?.locationId || this.client.locationID,
|
|
};
|
|
|
|
if (params?.pipelineId) searchParams.pipelineId = params.pipelineId;
|
|
if (params?.pipelineStageId) searchParams.pipelineStageId = params.pipelineStageId;
|
|
if (params?.contactId) searchParams.contactId = params.contactId;
|
|
if (params?.status) searchParams.status = params.status;
|
|
if (params?.assignedTo) searchParams.assignedTo = params.assignedTo;
|
|
if (params?.limit) searchParams.limit = String(params.limit);
|
|
if (params?.startAfterId) searchParams.startAfterId = params.startAfterId;
|
|
if (params?.query) searchParams.query = params.query;
|
|
|
|
return this.client.get('/opportunities/search', searchParams);
|
|
}
|
|
|
|
// Get a single opportunity by ID
|
|
async getById(opportunityId: string): Promise<GHLOpportunity> {
|
|
return this.client.get(`/opportunities/${opportunityId}`);
|
|
}
|
|
|
|
// Create a new opportunity
|
|
async create(data: CreateOpportunityDTO): Promise<GHLOpportunity> {
|
|
return this.client.post('/opportunities/', {
|
|
...data,
|
|
locationId: this.client.locationID,
|
|
});
|
|
}
|
|
|
|
// Update an existing opportunity
|
|
async update(opportunityId: string, data: UpdateOpportunityDTO): Promise<GHLOpportunity> {
|
|
return this.client.put(`/opportunities/${opportunityId}`, data);
|
|
}
|
|
|
|
// Delete an opportunity
|
|
async delete(opportunityId: string): Promise<void> {
|
|
await this.client.delete(`/opportunities/${opportunityId}`);
|
|
}
|
|
|
|
// Move opportunity to a different stage
|
|
async moveToStage(opportunityId: string, stageId: string): Promise<GHLOpportunity> {
|
|
return this.update(opportunityId, { pipelineStageId: stageId });
|
|
}
|
|
|
|
// Mark opportunity as won
|
|
async markAsWon(opportunityId: string): Promise<GHLOpportunity> {
|
|
return this.update(opportunityId, { status: 'won' });
|
|
}
|
|
|
|
// Mark opportunity as lost
|
|
async markAsLost(opportunityId: string): Promise<GHLOpportunity> {
|
|
return this.update(opportunityId, { status: 'lost' });
|
|
}
|
|
|
|
// Mark opportunity as abandoned
|
|
async markAsAbandoned(opportunityId: string): Promise<GHLOpportunity> {
|
|
return this.update(opportunityId, { status: 'abandoned' });
|
|
}
|
|
|
|
// Reopen an opportunity
|
|
async reopen(opportunityId: string): Promise<GHLOpportunity> {
|
|
return this.update(opportunityId, { status: 'open' });
|
|
}
|
|
|
|
// Get opportunities by contact
|
|
async getByContact(contactId: string): Promise<GHLOpportunity[]> {
|
|
const result = await this.getAll({ contactId });
|
|
return result.data || [];
|
|
}
|
|
|
|
// Get opportunities by pipeline
|
|
async getByPipeline(pipelineId: string): Promise<GHLOpportunity[]> {
|
|
const result = await this.getAll({ pipelineId });
|
|
return result.data || [];
|
|
}
|
|
|
|
// Get opportunities by stage
|
|
async getByStage(pipelineStageId: string): Promise<GHLOpportunity[]> {
|
|
const result = await this.getAll({ pipelineStageId });
|
|
return result.data || [];
|
|
}
|
|
|
|
// Update monetary value
|
|
async updateValue(opportunityId: string, value: number): Promise<GHLOpportunity> {
|
|
return this.update(opportunityId, { monetaryValue: value });
|
|
}
|
|
|
|
// Assign opportunity to a user
|
|
async assign(opportunityId: string, userId: string): Promise<GHLOpportunity> {
|
|
return this.update(opportunityId, { assignedTo: userId });
|
|
}
|
|
}
|
|
|
|
export class PipelinesService {
|
|
constructor(private client: GHLClient) {}
|
|
|
|
// Get all pipelines
|
|
async getAll(): Promise<GHLPipeline[]> {
|
|
const response = await this.client.get('/opportunities/pipelines', {
|
|
locationId: this.client.locationID,
|
|
});
|
|
return (response as any).pipelines || [];
|
|
}
|
|
|
|
// Get a single pipeline by ID
|
|
async getById(pipelineId: string): Promise<GHLPipeline> {
|
|
return this.client.get(`/opportunities/pipelines/${pipelineId}`);
|
|
}
|
|
|
|
// Get stages for a pipeline
|
|
async getStages(pipelineId: string): Promise<GHLPipelineStage[]> {
|
|
const pipeline = await this.getById(pipelineId);
|
|
return pipeline.stages || [];
|
|
}
|
|
|
|
// Create a new pipeline
|
|
async create(data: { name: string; stages: { name: string }[] }): Promise<GHLPipeline> {
|
|
return this.client.post('/opportunities/pipelines/', {
|
|
...data,
|
|
locationId: this.client.locationID,
|
|
});
|
|
}
|
|
|
|
// Update a pipeline
|
|
async update(pipelineId: string, data: { name?: string }): Promise<GHLPipeline> {
|
|
return this.client.put(`/opportunities/pipelines/${pipelineId}`, data);
|
|
}
|
|
|
|
// Delete a pipeline
|
|
async delete(pipelineId: string): Promise<void> {
|
|
await this.client.delete(`/opportunities/pipelines/${pipelineId}`);
|
|
}
|
|
|
|
// Add a stage to a pipeline
|
|
async addStage(pipelineId: string, stageName: string, position?: number): Promise<GHLPipelineStage> {
|
|
return this.client.post(`/opportunities/pipelines/${pipelineId}/stages`, {
|
|
name: stageName,
|
|
...(position !== undefined && { position }),
|
|
});
|
|
}
|
|
|
|
// Update a stage
|
|
async updateStage(pipelineId: string, stageId: string, data: { name?: string; position?: number }): Promise<GHLPipelineStage> {
|
|
return this.client.put(`/opportunities/pipelines/${pipelineId}/stages/${stageId}`, data);
|
|
}
|
|
|
|
// Delete a stage
|
|
async deleteStage(pipelineId: string, stageId: string): Promise<void> {
|
|
await this.client.delete(`/opportunities/pipelines/${pipelineId}/stages/${stageId}`);
|
|
}
|
|
|
|
// Get pipeline statistics
|
|
async getStats(pipelineId: string): Promise<{
|
|
totalOpportunities: number;
|
|
totalValue: number;
|
|
byStage: { stageId: string; count: number; value: number }[];
|
|
}> {
|
|
// This would need to aggregate from opportunities
|
|
// Implementing a basic version
|
|
const pipeline = await this.getById(pipelineId);
|
|
const stages = pipeline.stages || [];
|
|
|
|
// This is a simplified version - actual implementation would aggregate from opportunities
|
|
return {
|
|
totalOpportunities: 0,
|
|
totalValue: 0,
|
|
byStage: stages.map(stage => ({
|
|
stageId: stage.id,
|
|
count: 0,
|
|
value: 0,
|
|
})),
|
|
};
|
|
}
|
|
}
|