EPIC-P1-004: Frontend API clients - Add sales.api.ts (orders, quotations, returns) - Add purchases.api.ts (purchase orders, suppliers, receipts) - Add financial.api.ts (accounts, journals, payments) - Add crm.api.ts (leads, opportunities, campaigns) - Add projects.api.ts (projects, tasks, timesheets) - Update api/index.ts with new exports Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
403 lines
11 KiB
TypeScript
403 lines
11 KiB
TypeScript
import api from './axios-instance';
|
|
import { API_ENDPOINTS } from '@shared/constants/api-endpoints';
|
|
import type { BaseEntity } from '@shared/types/entities.types';
|
|
import type { ApiResponse, PaginatedResponse, PaginationParams } from '@shared/types/api.types';
|
|
|
|
// ============================================================================
|
|
// Interfaces
|
|
// ============================================================================
|
|
|
|
export interface Lead extends BaseEntity {
|
|
name: string;
|
|
contactName?: string;
|
|
email?: string;
|
|
phone?: string;
|
|
mobile?: string;
|
|
companyName?: string;
|
|
title?: string;
|
|
street?: string;
|
|
city?: string;
|
|
stateId?: string;
|
|
countryId?: string;
|
|
website?: string;
|
|
description?: string;
|
|
source?: string;
|
|
medium?: string;
|
|
campaign?: string;
|
|
priority: 'low' | 'medium' | 'high' | 'very_high';
|
|
status: 'new' | 'contacted' | 'qualified' | 'proposition' | 'won' | 'lost';
|
|
stageId?: string;
|
|
stageName?: string;
|
|
userId?: string;
|
|
userName?: string;
|
|
teamId?: string;
|
|
expectedRevenue?: number;
|
|
probability?: number;
|
|
lostReason?: string;
|
|
tags?: string[];
|
|
tenantId: string;
|
|
}
|
|
|
|
export interface Opportunity extends BaseEntity {
|
|
name: string;
|
|
partnerId: string;
|
|
partnerName?: string;
|
|
contactName?: string;
|
|
email?: string;
|
|
phone?: string;
|
|
description?: string;
|
|
priority: 'low' | 'medium' | 'high' | 'very_high';
|
|
status: 'new' | 'qualified' | 'proposition' | 'negotiation' | 'won' | 'lost';
|
|
stageId: string;
|
|
stageName?: string;
|
|
userId?: string;
|
|
userName?: string;
|
|
teamId?: string;
|
|
expectedRevenue: number;
|
|
probability: number;
|
|
expectedCloseDate?: string;
|
|
lostReason?: string;
|
|
tags?: string[];
|
|
leadId?: string;
|
|
tenantId: string;
|
|
}
|
|
|
|
export interface CrmStage extends BaseEntity {
|
|
name: string;
|
|
sequence: number;
|
|
probability: number;
|
|
isFolded: boolean;
|
|
requirements?: string;
|
|
teamId?: string;
|
|
tenantId: string;
|
|
}
|
|
|
|
export interface PipelineSummary {
|
|
stages: {
|
|
id: string;
|
|
name: string;
|
|
sequence: number;
|
|
count: number;
|
|
totalRevenue: number;
|
|
weightedRevenue: number;
|
|
}[];
|
|
totals: {
|
|
leads: number;
|
|
opportunities: number;
|
|
totalRevenue: number;
|
|
weightedRevenue: number;
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// DTOs
|
|
// ============================================================================
|
|
|
|
export interface CreateLeadDto {
|
|
name: string;
|
|
contactName?: string;
|
|
email?: string;
|
|
phone?: string;
|
|
mobile?: string;
|
|
companyName?: string;
|
|
title?: string;
|
|
street?: string;
|
|
city?: string;
|
|
stateId?: string;
|
|
countryId?: string;
|
|
website?: string;
|
|
description?: string;
|
|
source?: string;
|
|
medium?: string;
|
|
campaign?: string;
|
|
priority?: Lead['priority'];
|
|
stageId?: string;
|
|
userId?: string;
|
|
teamId?: string;
|
|
expectedRevenue?: number;
|
|
probability?: number;
|
|
tags?: string[];
|
|
}
|
|
|
|
export interface UpdateLeadDto {
|
|
name?: string;
|
|
contactName?: string;
|
|
email?: string;
|
|
phone?: string;
|
|
mobile?: string;
|
|
companyName?: string;
|
|
title?: string;
|
|
street?: string;
|
|
city?: string;
|
|
stateId?: string;
|
|
countryId?: string;
|
|
website?: string;
|
|
description?: string;
|
|
source?: string;
|
|
medium?: string;
|
|
campaign?: string;
|
|
priority?: Lead['priority'];
|
|
status?: Lead['status'];
|
|
stageId?: string;
|
|
userId?: string;
|
|
teamId?: string;
|
|
expectedRevenue?: number;
|
|
probability?: number;
|
|
lostReason?: string;
|
|
tags?: string[];
|
|
}
|
|
|
|
export interface CreateOpportunityDto {
|
|
name: string;
|
|
partnerId: string;
|
|
contactName?: string;
|
|
email?: string;
|
|
phone?: string;
|
|
description?: string;
|
|
priority?: Opportunity['priority'];
|
|
stageId: string;
|
|
userId?: string;
|
|
teamId?: string;
|
|
expectedRevenue: number;
|
|
probability?: number;
|
|
expectedCloseDate?: string;
|
|
tags?: string[];
|
|
leadId?: string;
|
|
}
|
|
|
|
export interface UpdateOpportunityDto {
|
|
name?: string;
|
|
partnerId?: string;
|
|
contactName?: string;
|
|
email?: string;
|
|
phone?: string;
|
|
description?: string;
|
|
priority?: Opportunity['priority'];
|
|
status?: Opportunity['status'];
|
|
stageId?: string;
|
|
userId?: string;
|
|
teamId?: string;
|
|
expectedRevenue?: number;
|
|
probability?: number;
|
|
expectedCloseDate?: string;
|
|
lostReason?: string;
|
|
tags?: string[];
|
|
}
|
|
|
|
export interface ConvertLeadDto {
|
|
createPartner: boolean;
|
|
partnerId?: string;
|
|
createOpportunity: boolean;
|
|
opportunityName?: string;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Filters
|
|
// ============================================================================
|
|
|
|
export interface LeadFilters extends PaginationParams {
|
|
status?: Lead['status'];
|
|
priority?: Lead['priority'];
|
|
stageId?: string;
|
|
userId?: string;
|
|
teamId?: string;
|
|
source?: string;
|
|
dateFrom?: string;
|
|
dateTo?: string;
|
|
}
|
|
|
|
export interface OpportunityFilters extends PaginationParams {
|
|
status?: Opportunity['status'];
|
|
priority?: Opportunity['priority'];
|
|
stageId?: string;
|
|
partnerId?: string;
|
|
userId?: string;
|
|
teamId?: string;
|
|
dateFrom?: string;
|
|
dateTo?: string;
|
|
minRevenue?: number;
|
|
maxRevenue?: number;
|
|
}
|
|
|
|
// ============================================================================
|
|
// API Client
|
|
// ============================================================================
|
|
|
|
export const crmApi = {
|
|
// Leads
|
|
getLeads: async (filters?: LeadFilters): Promise<PaginatedResponse<Lead>> => {
|
|
const response = await api.get<PaginatedResponse<Lead>>(
|
|
API_ENDPOINTS.CRM.LEADS,
|
|
{ params: filters }
|
|
);
|
|
return response.data;
|
|
},
|
|
|
|
getLeadById: async (id: string): Promise<Lead> => {
|
|
const response = await api.get<ApiResponse<Lead>>(
|
|
`${API_ENDPOINTS.CRM.LEADS}/${id}`
|
|
);
|
|
if (!response.data.success || !response.data.data) {
|
|
throw new Error(response.data.error || 'Lead no encontrado');
|
|
}
|
|
return response.data.data;
|
|
},
|
|
|
|
createLead: async (data: CreateLeadDto): Promise<Lead> => {
|
|
const response = await api.post<ApiResponse<Lead>>(
|
|
API_ENDPOINTS.CRM.LEADS,
|
|
data
|
|
);
|
|
if (!response.data.success || !response.data.data) {
|
|
throw new Error(response.data.error || 'Error al crear lead');
|
|
}
|
|
return response.data.data;
|
|
},
|
|
|
|
updateLead: async (id: string, data: UpdateLeadDto): Promise<Lead> => {
|
|
const response = await api.patch<ApiResponse<Lead>>(
|
|
`${API_ENDPOINTS.CRM.LEADS}/${id}`,
|
|
data
|
|
);
|
|
if (!response.data.success || !response.data.data) {
|
|
throw new Error(response.data.error || 'Error al actualizar lead');
|
|
}
|
|
return response.data.data;
|
|
},
|
|
|
|
convertLead: async (id: string, data: ConvertLeadDto): Promise<{ partner?: { id: string }; opportunity?: Opportunity }> => {
|
|
const response = await api.post<ApiResponse<{ partner?: { id: string }; opportunity?: Opportunity }>>(
|
|
`${API_ENDPOINTS.CRM.LEADS}/${id}/convert`,
|
|
data
|
|
);
|
|
if (!response.data.success || !response.data.data) {
|
|
throw new Error(response.data.error || 'Error al convertir lead');
|
|
}
|
|
return response.data.data;
|
|
},
|
|
|
|
markLeadAsWon: async (id: string): Promise<Lead> => {
|
|
const response = await api.post<ApiResponse<Lead>>(
|
|
`${API_ENDPOINTS.CRM.LEADS}/${id}/won`
|
|
);
|
|
if (!response.data.success || !response.data.data) {
|
|
throw new Error(response.data.error || 'Error al marcar lead como ganado');
|
|
}
|
|
return response.data.data;
|
|
},
|
|
|
|
markLeadAsLost: async (id: string, reason?: string): Promise<Lead> => {
|
|
const response = await api.post<ApiResponse<Lead>>(
|
|
`${API_ENDPOINTS.CRM.LEADS}/${id}/lost`,
|
|
{ reason }
|
|
);
|
|
if (!response.data.success || !response.data.data) {
|
|
throw new Error(response.data.error || 'Error al marcar lead como perdido');
|
|
}
|
|
return response.data.data;
|
|
},
|
|
|
|
deleteLead: async (id: string): Promise<void> => {
|
|
const response = await api.delete<ApiResponse>(
|
|
`${API_ENDPOINTS.CRM.LEADS}/${id}`
|
|
);
|
|
if (!response.data.success) {
|
|
throw new Error(response.data.error || 'Error al eliminar lead');
|
|
}
|
|
},
|
|
|
|
// Opportunities
|
|
getOpportunities: async (filters?: OpportunityFilters): Promise<PaginatedResponse<Opportunity>> => {
|
|
const response = await api.get<PaginatedResponse<Opportunity>>(
|
|
API_ENDPOINTS.CRM.OPPORTUNITIES,
|
|
{ params: filters }
|
|
);
|
|
return response.data;
|
|
},
|
|
|
|
getOpportunityById: async (id: string): Promise<Opportunity> => {
|
|
const response = await api.get<ApiResponse<Opportunity>>(
|
|
`${API_ENDPOINTS.CRM.OPPORTUNITIES}/${id}`
|
|
);
|
|
if (!response.data.success || !response.data.data) {
|
|
throw new Error(response.data.error || 'Oportunidad no encontrada');
|
|
}
|
|
return response.data.data;
|
|
},
|
|
|
|
createOpportunity: async (data: CreateOpportunityDto): Promise<Opportunity> => {
|
|
const response = await api.post<ApiResponse<Opportunity>>(
|
|
API_ENDPOINTS.CRM.OPPORTUNITIES,
|
|
data
|
|
);
|
|
if (!response.data.success || !response.data.data) {
|
|
throw new Error(response.data.error || 'Error al crear oportunidad');
|
|
}
|
|
return response.data.data;
|
|
},
|
|
|
|
updateOpportunity: async (id: string, data: UpdateOpportunityDto): Promise<Opportunity> => {
|
|
const response = await api.patch<ApiResponse<Opportunity>>(
|
|
`${API_ENDPOINTS.CRM.OPPORTUNITIES}/${id}`,
|
|
data
|
|
);
|
|
if (!response.data.success || !response.data.data) {
|
|
throw new Error(response.data.error || 'Error al actualizar oportunidad');
|
|
}
|
|
return response.data.data;
|
|
},
|
|
|
|
markOpportunityAsWon: async (id: string): Promise<Opportunity> => {
|
|
const response = await api.post<ApiResponse<Opportunity>>(
|
|
`${API_ENDPOINTS.CRM.OPPORTUNITIES}/${id}/won`
|
|
);
|
|
if (!response.data.success || !response.data.data) {
|
|
throw new Error(response.data.error || 'Error al marcar oportunidad como ganada');
|
|
}
|
|
return response.data.data;
|
|
},
|
|
|
|
markOpportunityAsLost: async (id: string, reason?: string): Promise<Opportunity> => {
|
|
const response = await api.post<ApiResponse<Opportunity>>(
|
|
`${API_ENDPOINTS.CRM.OPPORTUNITIES}/${id}/lost`,
|
|
{ reason }
|
|
);
|
|
if (!response.data.success || !response.data.data) {
|
|
throw new Error(response.data.error || 'Error al marcar oportunidad como perdida');
|
|
}
|
|
return response.data.data;
|
|
},
|
|
|
|
deleteOpportunity: async (id: string): Promise<void> => {
|
|
const response = await api.delete<ApiResponse>(
|
|
`${API_ENDPOINTS.CRM.OPPORTUNITIES}/${id}`
|
|
);
|
|
if (!response.data.success) {
|
|
throw new Error(response.data.error || 'Error al eliminar oportunidad');
|
|
}
|
|
},
|
|
|
|
// Pipeline
|
|
getPipeline: async (teamId?: string): Promise<PipelineSummary> => {
|
|
const response = await api.get<ApiResponse<PipelineSummary>>(
|
|
API_ENDPOINTS.CRM.PIPELINE,
|
|
{ params: teamId ? { teamId } : undefined }
|
|
);
|
|
if (!response.data.success || !response.data.data) {
|
|
throw new Error(response.data.error || 'Error al obtener pipeline');
|
|
}
|
|
return response.data.data;
|
|
},
|
|
|
|
// Stages
|
|
getStages: async (): Promise<CrmStage[]> => {
|
|
const response = await api.get<ApiResponse<CrmStage[]>>(
|
|
API_ENDPOINTS.CRM.STAGES
|
|
);
|
|
if (!response.data.success || !response.data.data) {
|
|
throw new Error(response.data.error || 'Error al obtener etapas');
|
|
}
|
|
return response.data.data;
|
|
},
|
|
};
|