## Backend (NestJS) - Entities: Lead, Opportunity, PipelineStage, Activity with TypeORM - Services: LeadsService, OpportunitiesService, PipelineService, ActivitiesService, SalesDashboardService - Controllers: LeadsController, OpportunitiesController, PipelineController, ActivitiesController, DashboardController - DTOs: Full set of Create/Update/Convert DTOs with validation - Tests: 5 test suites with comprehensive coverage ## Frontend (React) - Pages: /sales, /sales/leads, /sales/leads/[id], /sales/opportunities, /sales/opportunities/[id], /sales/activities - Components: SalesDashboard, ConversionFunnel, LeadsList, LeadForm, LeadCard, PipelineBoard, OpportunityCard, OpportunityForm, ActivityTimeline, ActivityForm - Hooks: useLeads, useOpportunities, usePipeline, useActivities, useSalesDashboard - Services: leads.api, opportunities.api, activities.api, pipeline.api, dashboard.api ## Documentation - Updated SAAS-018-sales.md with implementation details - Updated MASTER_INVENTORY.yml - status changed from specified to completed Story Points: 21 Sprint: 6 - Sales Foundation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
140 lines
4.3 KiB
TypeScript
140 lines
4.3 KiB
TypeScript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import {
|
|
opportunitiesApi,
|
|
Opportunity,
|
|
CreateOpportunityDto,
|
|
UpdateOpportunityDto,
|
|
MoveOpportunityDto,
|
|
OpportunityFilters,
|
|
} from '../../services/sales/opportunities.api';
|
|
|
|
const QUERY_KEYS = {
|
|
opportunities: ['sales', 'opportunities'] as const,
|
|
opportunity: (id: string) => ['sales', 'opportunities', id] as const,
|
|
pipeline: ['sales', 'opportunities', 'pipeline'] as const,
|
|
stats: ['sales', 'opportunities', 'stats'] as const,
|
|
forecast: (start: string, end: string) => ['sales', 'opportunities', 'forecast', start, end] as const,
|
|
};
|
|
|
|
export function useOpportunities(filters?: OpportunityFilters) {
|
|
return useQuery({
|
|
queryKey: [...QUERY_KEYS.opportunities, filters],
|
|
queryFn: () => opportunitiesApi.list(filters),
|
|
staleTime: 30 * 1000,
|
|
});
|
|
}
|
|
|
|
export function useOpportunity(id: string) {
|
|
return useQuery({
|
|
queryKey: QUERY_KEYS.opportunity(id),
|
|
queryFn: () => opportunitiesApi.get(id),
|
|
enabled: !!id,
|
|
});
|
|
}
|
|
|
|
export function usePipeline() {
|
|
return useQuery({
|
|
queryKey: QUERY_KEYS.pipeline,
|
|
queryFn: opportunitiesApi.getByStage,
|
|
staleTime: 30 * 1000,
|
|
});
|
|
}
|
|
|
|
export function useOpportunityStats() {
|
|
return useQuery({
|
|
queryKey: QUERY_KEYS.stats,
|
|
queryFn: opportunitiesApi.getStats,
|
|
staleTime: 60 * 1000,
|
|
});
|
|
}
|
|
|
|
export function useForecast(startDate: string, endDate: string) {
|
|
return useQuery({
|
|
queryKey: QUERY_KEYS.forecast(startDate, endDate),
|
|
queryFn: () => opportunitiesApi.getForecast(startDate, endDate),
|
|
enabled: !!startDate && !!endDate,
|
|
staleTime: 5 * 60 * 1000,
|
|
});
|
|
}
|
|
|
|
export function useCreateOpportunity() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (data: CreateOpportunityDto) => opportunitiesApi.create(data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.opportunities });
|
|
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.pipeline });
|
|
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.stats });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateOpportunity() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: UpdateOpportunityDto }) =>
|
|
opportunitiesApi.update(id, data),
|
|
onSuccess: (updatedOpportunity) => {
|
|
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.opportunities });
|
|
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.pipeline });
|
|
queryClient.setQueryData(QUERY_KEYS.opportunity(updatedOpportunity.id), updatedOpportunity);
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteOpportunity() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (id: string) => opportunitiesApi.delete(id),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.opportunities });
|
|
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.pipeline });
|
|
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.stats });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useMoveOpportunity() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: MoveOpportunityDto }) =>
|
|
opportunitiesApi.move(id, data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.opportunities });
|
|
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.pipeline });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useMarkAsWon() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({ id, notes }: { id: string; notes?: string }) =>
|
|
opportunitiesApi.markAsWon(id, notes),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.opportunities });
|
|
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.pipeline });
|
|
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.stats });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useMarkAsLost() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({ id, reason }: { id: string; reason?: string }) =>
|
|
opportunitiesApi.markAsLost(id, reason),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.opportunities });
|
|
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.pipeline });
|
|
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.stats });
|
|
},
|
|
});
|
|
}
|