From 36ee5213c53ed1783f0e2f0e90f357d6c26af0c1 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sat, 24 Jan 2026 22:50:11 -0600 Subject: [PATCH] [SAAS-018/020] feat: Add Sales and Commissions frontend modules Sales Frontend (Sprint 2): - Add services: leads, opportunities, activities, pipeline, dashboard APIs - Add hooks: useSales with React Query integration - Add pages: SalesPage, LeadsPage, LeadDetailPage, OpportunitiesPage, OpportunityDetailPage, ActivitiesPage - Integrate routes in main router Commissions Frontend (Sprint 4): - Add services: schemes, entries, periods, assignments, dashboard APIs - Add hooks: useCommissions with React Query integration - Add pages: CommissionsPage, SchemesPage, EntriesPage, PeriodsPage, MyEarningsPage - Integrate routes in main router Co-Authored-By: Claude Opus 4.5 --- src/hooks/useCommissions.ts | 402 ++++++++++++++++++ src/hooks/useSales.ts | 398 +++++++++++++++++ .../dashboard/commissions/CommissionsPage.tsx | 211 +++++++++ .../dashboard/commissions/EntriesPage.tsx | 307 +++++++++++++ .../dashboard/commissions/MyEarningsPage.tsx | 214 ++++++++++ .../dashboard/commissions/PeriodsPage.tsx | 261 ++++++++++++ .../dashboard/commissions/SchemesPage.tsx | 187 ++++++++ src/pages/dashboard/commissions/index.ts | 5 + src/pages/dashboard/sales/ActivitiesPage.tsx | 188 ++++++++ src/pages/dashboard/sales/LeadDetailPage.tsx | 376 ++++++++++++++++ src/pages/dashboard/sales/LeadsPage.tsx | 184 ++++++++ .../dashboard/sales/OpportunitiesPage.tsx | 105 +++++ .../dashboard/sales/OpportunityDetailPage.tsx | 379 +++++++++++++++++ src/pages/dashboard/sales/SalesPage.tsx | 163 +++++++ src/pages/dashboard/sales/index.ts | 6 + src/router/index.tsx | 30 ++ src/services/commissions/assignments.api.ts | 104 +++++ src/services/commissions/dashboard.api.ts | 101 +++++ src/services/commissions/entries.api.ts | 147 +++++++ src/services/commissions/index.ts | 5 + src/services/commissions/periods.api.ts | 121 ++++++ src/services/commissions/schemes.api.ts | 107 +++++ src/services/sales/activities.api.ts | 156 +++++++ src/services/sales/dashboard.api.ts | 108 +++++ src/services/sales/index.ts | 5 + src/services/sales/leads.api.ts | 137 ++++++ src/services/sales/opportunities.api.ts | 165 +++++++ src/services/sales/pipeline.api.ts | 67 +++ 28 files changed, 4639 insertions(+) create mode 100644 src/hooks/useCommissions.ts create mode 100644 src/hooks/useSales.ts create mode 100644 src/pages/dashboard/commissions/CommissionsPage.tsx create mode 100644 src/pages/dashboard/commissions/EntriesPage.tsx create mode 100644 src/pages/dashboard/commissions/MyEarningsPage.tsx create mode 100644 src/pages/dashboard/commissions/PeriodsPage.tsx create mode 100644 src/pages/dashboard/commissions/SchemesPage.tsx create mode 100644 src/pages/dashboard/commissions/index.ts create mode 100644 src/pages/dashboard/sales/ActivitiesPage.tsx create mode 100644 src/pages/dashboard/sales/LeadDetailPage.tsx create mode 100644 src/pages/dashboard/sales/LeadsPage.tsx create mode 100644 src/pages/dashboard/sales/OpportunitiesPage.tsx create mode 100644 src/pages/dashboard/sales/OpportunityDetailPage.tsx create mode 100644 src/pages/dashboard/sales/SalesPage.tsx create mode 100644 src/pages/dashboard/sales/index.ts create mode 100644 src/services/commissions/assignments.api.ts create mode 100644 src/services/commissions/dashboard.api.ts create mode 100644 src/services/commissions/entries.api.ts create mode 100644 src/services/commissions/index.ts create mode 100644 src/services/commissions/periods.api.ts create mode 100644 src/services/commissions/schemes.api.ts create mode 100644 src/services/sales/activities.api.ts create mode 100644 src/services/sales/dashboard.api.ts create mode 100644 src/services/sales/index.ts create mode 100644 src/services/sales/leads.api.ts create mode 100644 src/services/sales/opportunities.api.ts create mode 100644 src/services/sales/pipeline.api.ts diff --git a/src/hooks/useCommissions.ts b/src/hooks/useCommissions.ts new file mode 100644 index 0000000..ba8fb45 --- /dev/null +++ b/src/hooks/useCommissions.ts @@ -0,0 +1,402 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { + schemesApi, + assignmentsApi, + entriesApi, + periodsApi, + dashboardApi, + type SchemeFilters, + type CreateSchemeDto, + type UpdateSchemeDto, + type AssignmentFilters, + type CreateAssignmentDto, + type UpdateAssignmentDto, + type EntryFilters, + type CalculateCommissionDto, + type UpdateEntryStatusDto, + type PeriodFilters, + type CreatePeriodDto, + type MarkPeriodPaidDto, +} from '@/services/commissions'; + +// Schemes Hooks +export function useSchemes(filters?: SchemeFilters) { + return useQuery({ + queryKey: ['commissions', 'schemes', filters], + queryFn: () => schemesApi.list(filters), + }); +} + +export function useActiveSchemes() { + return useQuery({ + queryKey: ['commissions', 'schemes', 'active'], + queryFn: () => schemesApi.listActive(), + }); +} + +export function useScheme(id: string) { + return useQuery({ + queryKey: ['commissions', 'schemes', id], + queryFn: () => schemesApi.get(id), + enabled: !!id, + }); +} + +export function useCreateScheme() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateSchemeDto) => schemesApi.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['commissions', 'schemes'] }); + }, + }); +} + +export function useUpdateScheme() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateSchemeDto }) => + schemesApi.update(id, data), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['commissions', 'schemes'] }); + queryClient.invalidateQueries({ queryKey: ['commissions', 'schemes', id] }); + }, + }); +} + +export function useDeleteScheme() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => schemesApi.delete(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['commissions', 'schemes'] }); + }, + }); +} + +export function useDuplicateScheme() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, name }: { id: string; name?: string }) => + schemesApi.duplicate(id, name), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['commissions', 'schemes'] }); + }, + }); +} + +export function useToggleSchemeActive() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => schemesApi.toggleActive(id), + onSuccess: (_, id) => { + queryClient.invalidateQueries({ queryKey: ['commissions', 'schemes'] }); + queryClient.invalidateQueries({ queryKey: ['commissions', 'schemes', id] }); + }, + }); +} + +// Assignments Hooks +export function useAssignments(filters?: AssignmentFilters) { + return useQuery({ + queryKey: ['commissions', 'assignments', filters], + queryFn: () => assignmentsApi.list(filters), + }); +} + +export function useAssignment(id: string) { + return useQuery({ + queryKey: ['commissions', 'assignments', id], + queryFn: () => assignmentsApi.get(id), + enabled: !!id, + }); +} + +export function useUserAssignments(userId: string) { + return useQuery({ + queryKey: ['commissions', 'assignments', 'user', userId], + queryFn: () => assignmentsApi.getByUser(userId), + enabled: !!userId, + }); +} + +export function useUserActiveScheme(userId: string) { + return useQuery({ + queryKey: ['commissions', 'assignments', 'user', userId, 'active'], + queryFn: () => assignmentsApi.getActiveScheme(userId), + enabled: !!userId, + }); +} + +export function useSchemeAssignees(schemeId: string) { + return useQuery({ + queryKey: ['commissions', 'assignments', 'scheme', schemeId], + queryFn: () => assignmentsApi.getSchemeAssignees(schemeId), + enabled: !!schemeId, + }); +} + +export function useCreateAssignment() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateAssignmentDto) => assignmentsApi.assign(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['commissions', 'assignments'] }); + }, + }); +} + +export function useUpdateAssignment() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateAssignmentDto }) => + assignmentsApi.update(id, data), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['commissions', 'assignments'] }); + queryClient.invalidateQueries({ queryKey: ['commissions', 'assignments', id] }); + }, + }); +} + +export function useRemoveAssignment() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => assignmentsApi.remove(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['commissions', 'assignments'] }); + }, + }); +} + +export function useDeactivateAssignment() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => assignmentsApi.deactivate(id), + onSuccess: (_, id) => { + queryClient.invalidateQueries({ queryKey: ['commissions', 'assignments'] }); + queryClient.invalidateQueries({ queryKey: ['commissions', 'assignments', id] }); + }, + }); +} + +// Entries Hooks +export function useEntries(filters?: EntryFilters) { + return useQuery({ + queryKey: ['commissions', 'entries', filters], + queryFn: () => entriesApi.list(filters), + }); +} + +export function usePendingEntries(params?: { page?: number; limit?: number }) { + return useQuery({ + queryKey: ['commissions', 'entries', 'pending', params], + queryFn: () => entriesApi.listPending(params), + }); +} + +export function useEntry(id: string) { + return useQuery({ + queryKey: ['commissions', 'entries', id], + queryFn: () => entriesApi.get(id), + enabled: !!id, + }); +} + +export function useUserEntries(userId: string, params?: { page?: number; limit?: number }) { + return useQuery({ + queryKey: ['commissions', 'entries', 'user', userId, params], + queryFn: () => entriesApi.getByUser(userId, params), + enabled: !!userId, + }); +} + +export function usePeriodEntries(periodId: string, params?: { page?: number; limit?: number }) { + return useQuery({ + queryKey: ['commissions', 'entries', 'period', periodId, params], + queryFn: () => entriesApi.getByPeriod(periodId, params), + enabled: !!periodId, + }); +} + +export function useCalculateCommission() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CalculateCommissionDto) => entriesApi.calculate(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['commissions', 'entries'] }); + queryClient.invalidateQueries({ queryKey: ['commissions', 'dashboard'] }); + }, + }); +} + +export function useSimulateCommission() { + return useMutation({ + mutationFn: (data: CalculateCommissionDto) => entriesApi.simulate(data), + }); +} + +export function useUpdateEntryStatus() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateEntryStatusDto }) => + entriesApi.updateStatus(id, data), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['commissions', 'entries'] }); + queryClient.invalidateQueries({ queryKey: ['commissions', 'entries', id] }); + queryClient.invalidateQueries({ queryKey: ['commissions', 'dashboard'] }); + }, + }); +} + +export function useBulkApproveEntries() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (entryIds: string[]) => entriesApi.bulkApprove(entryIds), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['commissions', 'entries'] }); + queryClient.invalidateQueries({ queryKey: ['commissions', 'dashboard'] }); + }, + }); +} + +export function useBulkRejectEntries() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ entryIds, reason }: { entryIds: string[]; reason?: string }) => + entriesApi.bulkReject(entryIds, reason), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['commissions', 'entries'] }); + queryClient.invalidateQueries({ queryKey: ['commissions', 'dashboard'] }); + }, + }); +} + +// Periods Hooks +export function usePeriods(filters?: PeriodFilters) { + return useQuery({ + queryKey: ['commissions', 'periods', filters], + queryFn: () => periodsApi.list(filters), + }); +} + +export function useOpenPeriod() { + return useQuery({ + queryKey: ['commissions', 'periods', 'open'], + queryFn: () => periodsApi.getOpen(), + }); +} + +export function usePeriod(id: string) { + return useQuery({ + queryKey: ['commissions', 'periods', id], + queryFn: () => periodsApi.get(id), + enabled: !!id, + }); +} + +export function usePeriodSummary(id: string) { + return useQuery({ + queryKey: ['commissions', 'periods', id, 'summary'], + queryFn: () => periodsApi.getSummary(id), + enabled: !!id, + }); +} + +export function useCreatePeriod() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreatePeriodDto) => periodsApi.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['commissions', 'periods'] }); + }, + }); +} + +export function useClosePeriod() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => periodsApi.close(id), + onSuccess: (_, id) => { + queryClient.invalidateQueries({ queryKey: ['commissions', 'periods'] }); + queryClient.invalidateQueries({ queryKey: ['commissions', 'periods', id] }); + queryClient.invalidateQueries({ queryKey: ['commissions', 'dashboard'] }); + }, + }); +} + +export function useReopenPeriod() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => periodsApi.reopen(id), + onSuccess: (_, id) => { + queryClient.invalidateQueries({ queryKey: ['commissions', 'periods'] }); + queryClient.invalidateQueries({ queryKey: ['commissions', 'periods', id] }); + }, + }); +} + +export function useMarkPeriodPaid() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: string; data?: MarkPeriodPaidDto }) => + periodsApi.markAsPaid(id, data), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['commissions', 'periods'] }); + queryClient.invalidateQueries({ queryKey: ['commissions', 'periods', id] }); + queryClient.invalidateQueries({ queryKey: ['commissions', 'entries'] }); + queryClient.invalidateQueries({ queryKey: ['commissions', 'dashboard'] }); + }, + }); +} + +// Dashboard Hooks +export function useCommissionsDashboard() { + return useQuery({ + queryKey: ['commissions', 'dashboard'], + queryFn: () => dashboardApi.getSummary(), + }); +} + +export function useEarningsByUser(params?: { date_from?: string; date_to?: string }) { + return useQuery({ + queryKey: ['commissions', 'dashboard', 'byUser', params], + queryFn: () => dashboardApi.getEarningsByUser(params), + }); +} + +export function useEarningsByPeriod() { + return useQuery({ + queryKey: ['commissions', 'dashboard', 'byPeriod'], + queryFn: () => dashboardApi.getEarningsByPeriod(), + }); +} + +export function useTopEarners(params?: { limit?: number; date_from?: string; date_to?: string }) { + return useQuery({ + queryKey: ['commissions', 'dashboard', 'topEarners', params], + queryFn: () => dashboardApi.getTopEarners(params), + }); +} + +export function useMyEarnings() { + return useQuery({ + queryKey: ['commissions', 'dashboard', 'myEarnings'], + queryFn: () => dashboardApi.getMyEarnings(), + }); +} + +export function useUserEarnings(userId: string) { + return useQuery({ + queryKey: ['commissions', 'dashboard', 'user', userId, 'earnings'], + queryFn: () => dashboardApi.getUserEarnings(userId), + enabled: !!userId, + }); +} + +export function useSchemePerformance(schemeId: string) { + return useQuery({ + queryKey: ['commissions', 'dashboard', 'scheme', schemeId, 'performance'], + queryFn: () => dashboardApi.getSchemePerformance(schemeId), + enabled: !!schemeId, + }); +} diff --git a/src/hooks/useSales.ts b/src/hooks/useSales.ts new file mode 100644 index 0000000..48b90df --- /dev/null +++ b/src/hooks/useSales.ts @@ -0,0 +1,398 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { + leadsApi, + opportunitiesApi, + activitiesApi, + pipelineApi, + salesDashboardApi, + type LeadFilters, + type CreateLeadDto, + type UpdateLeadDto, + type ConvertLeadDto, + type OpportunityFilters, + type CreateOpportunityDto, + type UpdateOpportunityDto, + type MoveOpportunityDto, + type ActivityFilters, + type CreateActivityDto, + type UpdateActivityDto, + type CreatePipelineStageDto, + type UpdatePipelineStageDto, +} from '@/services/sales'; + +// Leads Hooks +export function useLeads(filters?: LeadFilters) { + return useQuery({ + queryKey: ['sales', 'leads', filters], + queryFn: () => leadsApi.list(filters), + }); +} + +export function useLead(id: string) { + return useQuery({ + queryKey: ['sales', 'leads', id], + queryFn: () => leadsApi.get(id), + enabled: !!id, + }); +} + +export function useLeadStats() { + return useQuery({ + queryKey: ['sales', 'leads', 'stats'], + queryFn: () => leadsApi.getStats(), + }); +} + +export function useCreateLead() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateLeadDto) => leadsApi.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['sales', 'leads'] }); + }, + }); +} + +export function useUpdateLead() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateLeadDto }) => + leadsApi.update(id, data), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['sales', 'leads'] }); + queryClient.invalidateQueries({ queryKey: ['sales', 'leads', id] }); + }, + }); +} + +export function useDeleteLead() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => leadsApi.delete(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['sales', 'leads'] }); + }, + }); +} + +export function useConvertLead() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: ConvertLeadDto }) => + leadsApi.convert(id, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['sales', 'leads'] }); + queryClient.invalidateQueries({ queryKey: ['sales', 'opportunities'] }); + }, + }); +} + +export function useAssignLead() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, userId }: { id: string; userId: string }) => + leadsApi.assign(id, userId), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['sales', 'leads'] }); + queryClient.invalidateQueries({ queryKey: ['sales', 'leads', id] }); + }, + }); +} + +// Opportunities Hooks +export function useOpportunities(filters?: OpportunityFilters) { + return useQuery({ + queryKey: ['sales', 'opportunities', filters], + queryFn: () => opportunitiesApi.list(filters), + }); +} + +export function useOpportunity(id: string) { + return useQuery({ + queryKey: ['sales', 'opportunities', id], + queryFn: () => opportunitiesApi.get(id), + enabled: !!id, + }); +} + +export function usePipelineView() { + return useQuery({ + queryKey: ['sales', 'opportunities', 'pipeline'], + queryFn: () => opportunitiesApi.getByStage(), + }); +} + +export function useOpportunityStats() { + return useQuery({ + queryKey: ['sales', 'opportunities', 'stats'], + queryFn: () => opportunitiesApi.getStats(), + }); +} + +export function useCreateOpportunity() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateOpportunityDto) => opportunitiesApi.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['sales', 'opportunities'] }); + }, + }); +} + +export function useUpdateOpportunity() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateOpportunityDto }) => + opportunitiesApi.update(id, data), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['sales', 'opportunities'] }); + queryClient.invalidateQueries({ queryKey: ['sales', 'opportunities', id] }); + }, + }); +} + +export function useMoveOpportunity() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: MoveOpportunityDto }) => + opportunitiesApi.move(id, data), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['sales', 'opportunities'] }); + queryClient.invalidateQueries({ queryKey: ['sales', 'opportunities', id] }); + queryClient.invalidateQueries({ queryKey: ['sales', 'pipeline'] }); + }, + }); +} + +export function useDeleteOpportunity() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => opportunitiesApi.delete(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['sales', 'opportunities'] }); + }, + }); +} + +export function useMarkOpportunityWon() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, notes }: { id: string; notes?: string }) => + opportunitiesApi.markAsWon(id, notes), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['sales', 'opportunities'] }); + queryClient.invalidateQueries({ queryKey: ['sales', 'opportunities', id] }); + queryClient.invalidateQueries({ queryKey: ['sales', 'dashboard'] }); + }, + }); +} + +export function useMarkOpportunityLost() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, reason }: { id: string; reason?: string }) => + opportunitiesApi.markAsLost(id, reason), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['sales', 'opportunities'] }); + queryClient.invalidateQueries({ queryKey: ['sales', 'opportunities', id] }); + queryClient.invalidateQueries({ queryKey: ['sales', 'dashboard'] }); + }, + }); +} + +// Activities Hooks +export function useActivities(filters?: ActivityFilters) { + return useQuery({ + queryKey: ['sales', 'activities', filters], + queryFn: () => activitiesApi.list(filters), + }); +} + +export function useActivity(id: string) { + return useQuery({ + queryKey: ['sales', 'activities', id], + queryFn: () => activitiesApi.get(id), + enabled: !!id, + }); +} + +export function useLeadActivities(leadId: string) { + return useQuery({ + queryKey: ['sales', 'activities', 'lead', leadId], + queryFn: () => activitiesApi.getByLead(leadId), + enabled: !!leadId, + }); +} + +export function useOpportunityActivities(opportunityId: string) { + return useQuery({ + queryKey: ['sales', 'activities', 'opportunity', opportunityId], + queryFn: () => activitiesApi.getByOpportunity(opportunityId), + enabled: !!opportunityId, + }); +} + +export function useUpcomingActivities(days?: number, userId?: string) { + return useQuery({ + queryKey: ['sales', 'activities', 'upcoming', days, userId], + queryFn: () => activitiesApi.getUpcoming(days, userId), + }); +} + +export function useOverdueActivities(userId?: string) { + return useQuery({ + queryKey: ['sales', 'activities', 'overdue', userId], + queryFn: () => activitiesApi.getOverdue(userId), + }); +} + +export function useActivityStats(userId?: string) { + return useQuery({ + queryKey: ['sales', 'activities', 'stats', userId], + queryFn: () => activitiesApi.getStats(userId), + }); +} + +export function useCreateActivity() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateActivityDto) => activitiesApi.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['sales', 'activities'] }); + }, + }); +} + +export function useUpdateActivity() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateActivityDto }) => + activitiesApi.update(id, data), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['sales', 'activities'] }); + queryClient.invalidateQueries({ queryKey: ['sales', 'activities', id] }); + }, + }); +} + +export function useDeleteActivity() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => activitiesApi.delete(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['sales', 'activities'] }); + }, + }); +} + +export function useCompleteActivity() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, outcome }: { id: string; outcome?: string }) => + activitiesApi.complete(id, outcome), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['sales', 'activities'] }); + queryClient.invalidateQueries({ queryKey: ['sales', 'activities', id] }); + }, + }); +} + +export function useCancelActivity() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => activitiesApi.cancel(id), + onSuccess: (_, id) => { + queryClient.invalidateQueries({ queryKey: ['sales', 'activities'] }); + queryClient.invalidateQueries({ queryKey: ['sales', 'activities', id] }); + }, + }); +} + +// Pipeline Hooks +export function usePipelineStages() { + return useQuery({ + queryKey: ['sales', 'pipeline', 'stages'], + queryFn: () => pipelineApi.getStages(), + }); +} + +export function useCreatePipelineStage() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreatePipelineStageDto) => pipelineApi.createStage(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['sales', 'pipeline'] }); + }, + }); +} + +export function useUpdatePipelineStage() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdatePipelineStageDto }) => + pipelineApi.updateStage(id, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['sales', 'pipeline'] }); + }, + }); +} + +export function useDeletePipelineStage() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => pipelineApi.deleteStage(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['sales', 'pipeline'] }); + }, + }); +} + +export function useReorderPipelineStages() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (stageIds: string[]) => pipelineApi.reorderStages(stageIds), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['sales', 'pipeline'] }); + }, + }); +} + +export function useInitializeDefaultStages() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: () => pipelineApi.initializeDefaults(), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['sales', 'pipeline'] }); + }, + }); +} + +// Dashboard Hooks +export function useSalesDashboard() { + return useQuery({ + queryKey: ['sales', 'dashboard'], + queryFn: () => salesDashboardApi.getSummary(), + }); +} + +export function useConversionRates(startDate?: string, endDate?: string) { + return useQuery({ + queryKey: ['sales', 'dashboard', 'conversion', startDate, endDate], + queryFn: () => salesDashboardApi.getConversionRates(startDate, endDate), + }); +} + +export function useRevenueReport(startDate: string, endDate: string) { + return useQuery({ + queryKey: ['sales', 'dashboard', 'revenue', startDate, endDate], + queryFn: () => salesDashboardApi.getRevenue(startDate, endDate), + enabled: !!startDate && !!endDate, + }); +} + +export function useTopSellers(limit?: number, startDate?: string, endDate?: string) { + return useQuery({ + queryKey: ['sales', 'dashboard', 'topSellers', limit, startDate, endDate], + queryFn: () => salesDashboardApi.getTopSellers(limit, startDate, endDate), + }); +} diff --git a/src/pages/dashboard/commissions/CommissionsPage.tsx b/src/pages/dashboard/commissions/CommissionsPage.tsx new file mode 100644 index 0000000..ae8e79e --- /dev/null +++ b/src/pages/dashboard/commissions/CommissionsPage.tsx @@ -0,0 +1,211 @@ +import { useCommissionsDashboard, useTopEarners, useEarningsByPeriod } from '@/hooks/useCommissions'; + +export default function CommissionsPage() { + const { data: dashboard, isLoading } = useCommissionsDashboard(); + const { data: topEarners } = useTopEarners({ limit: 5 }); + const { data: earningsByPeriod } = useEarningsByPeriod(); + + if (isLoading) { + return ( +
+
+
+ ); + } + + return ( +
+
+

Commissions Dashboard

+
+ + {/* Summary Cards */} +
+
+
+
+
+ + + +
+
+
+
Pending Commissions
+
+ ${(dashboard?.total_pending ?? 0).toLocaleString()} +
+
+
+
+
+
+ +
+
+
+
+ + + +
+
+
+
Pending Count
+
+ {dashboard?.pending_count ?? 0} +
+
+
+
+
+
+ +
+
+
+
+ + + +
+
+
+
Approved
+
+ ${(dashboard?.total_approved ?? 0).toLocaleString()} +
+
+
+
+
+
+ +
+
+
+
+ + + +
+
+
+
Paid Out
+
+ ${(dashboard?.total_paid ?? 0).toLocaleString()} +
+
+
+
+
+
+
+ + {/* Stats Grid */} +
+ {/* Top Earners */} +
+

Top Earners

+
+ {topEarners?.map((earner, index) => ( +
+
+ + {earner.rank || index + 1} + + {earner.user_name} +
+ ${earner.total_earned.toLocaleString()} +
+ ))} + {(!topEarners || topEarners.length === 0) && ( +

No earnings data yet

+ )} +
+
+ + {/* Earnings by Period */} +
+

Recent Periods

+
+ {earningsByPeriod?.slice(0, 5).map((period) => ( +
+
+ {period.period_name} + + {period.status} + +
+ ${period.total_amount.toLocaleString()} +
+ ))} + {(!earningsByPeriod || earningsByPeriod.length === 0) && ( +

No periods yet

+ )} +
+
+
+ + {/* Quick Stats */} +
+

System Status

+
+
+ Active Schemes + {dashboard?.active_schemes ?? 0} +
+
+ Active Assignments + {dashboard?.active_assignments ?? 0} +
+
+ Open Periods + {dashboard?.open_periods ?? 0} +
+
+
+ + {/* Quick Links */} +
+

Quick Actions

+
+ + Schemes + + + Entries + + + Periods + + + My Earnings + + + + New Scheme + +
+
+
+ ); +} diff --git a/src/pages/dashboard/commissions/EntriesPage.tsx b/src/pages/dashboard/commissions/EntriesPage.tsx new file mode 100644 index 0000000..213f368 --- /dev/null +++ b/src/pages/dashboard/commissions/EntriesPage.tsx @@ -0,0 +1,307 @@ +import { useState } from 'react'; +import { useEntries, useUpdateEntryStatus, useBulkApproveEntries } from '@/hooks/useCommissions'; +import type { EntryFilters, CommissionEntry, EntryStatus } from '@/services/commissions'; + +export default function EntriesPage() { + const [filters, setFilters] = useState({ page: 1, limit: 20 }); + const [selectedEntries, setSelectedEntries] = useState([]); + const [rejectModal, setRejectModal] = useState<{ id: string; notes: string } | null>(null); + const { data, isLoading, error } = useEntries(filters); + const updateStatus = useUpdateEntryStatus(); + const bulkApprove = useBulkApproveEntries(); + + const handleApprove = async (id: string) => { + await updateStatus.mutateAsync({ id, data: { status: 'approved' } }); + }; + + const handleReject = async () => { + if (!rejectModal) return; + await updateStatus.mutateAsync({ + id: rejectModal.id, + data: { status: 'rejected', notes: rejectModal.notes || undefined } + }); + setRejectModal(null); + }; + + const handleBulkApprove = async () => { + if (selectedEntries.length === 0) return; + await bulkApprove.mutateAsync(selectedEntries); + setSelectedEntries([]); + }; + + const toggleSelection = (id: string) => { + setSelectedEntries((prev) => + prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id] + ); + }; + + const toggleSelectAll = () => { + const pendingEntries = data?.data?.filter((e: CommissionEntry) => e.status === 'pending') || []; + if (selectedEntries.length === pendingEntries.length) { + setSelectedEntries([]); + } else { + setSelectedEntries(pendingEntries.map((e: CommissionEntry) => e.id)); + } + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+

Error loading entries

+
+ ); + } + + const getStatusColor = (status: EntryStatus) => { + const colors: Record = { + pending: 'bg-yellow-100 text-yellow-800', + approved: 'bg-green-100 text-green-800', + rejected: 'bg-red-100 text-red-800', + paid: 'bg-blue-100 text-blue-800', + cancelled: 'bg-gray-100 text-gray-800', + }; + return colors[status] || 'bg-gray-100 text-gray-800'; + }; + + const getUserName = (entry: CommissionEntry) => { + if (entry.user) { + return `${entry.user.first_name} ${entry.user.last_name}`; + } + return `User #${entry.user_id}`; + }; + + const getSchemeName = (entry: CommissionEntry) => { + return entry.scheme?.name || 'N/A'; + }; + + return ( +
+
+

Commission Entries

+ {selectedEntries.length > 0 && ( + + )} +
+ + {/* Filters */} +
+
+
+ + +
+
+ + setFilters((prev) => ({ ...prev, date_from: e.target.value || undefined, page: 1 }))} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" + /> +
+
+ + setFilters((prev) => ({ ...prev, date_to: e.target.value || undefined, page: 1 }))} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" + /> +
+
+ + setFilters((prev) => ({ ...prev, reference_type: e.target.value || undefined, page: 1 }))} + placeholder="e.g., opportunity" + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" + /> +
+
+
+ + {/* Entries Table */} +
+ + + + + + + + + + + + + + + {data?.data?.map((entry: CommissionEntry) => ( + + + + + + + + + + + ))} + +
+ 0 && selectedEntries.length === data?.data?.filter((e: CommissionEntry) => e.status === 'pending').length} + onChange={toggleSelectAll} + className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" + /> + UserSchemeBase AmountCommissionStatusDateActions
+ {entry.status === 'pending' && ( + toggleSelection(entry.id)} + className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" + /> + )} + +
{getUserName(entry)}
+
+
{getSchemeName(entry)}
+
+ ${entry.base_amount.toLocaleString()} + + ${entry.commission_amount.toLocaleString()} + + + {entry.status} + + + {new Date(entry.created_at).toLocaleDateString()} + + {entry.status === 'pending' && ( + <> + + + + )} + {entry.status !== 'pending' && ( + + View + + )} +
+ + {data?.data?.length === 0 && ( +
+

No entries found

+
+ )} + + {/* Pagination */} + {data && data.totalPages > 1 && ( +
+
+
+

+ Showing page {filters.page || 1} of{' '} + {data.totalPages} ({data.total} total) +

+
+
+ + +
+
+
+ )} +
+ + {/* Reject Modal */} + {rejectModal && ( +
+
+

Reject Entry

+
+ +