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 */}
+
+
+ );
+}
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?.length === 0 && (
+
+ )}
+
+ {/* Pagination */}
+ {data && data.totalPages > 1 && (
+
+
+
+
+ Showing page {filters.page || 1} of{' '}
+ {data.totalPages} ({data.total} total)
+
+
+
+
+
+
+
+
+ )}
+
+
+ {/* Reject Modal */}
+ {rejectModal && (
+
+
+
Reject Entry
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/pages/dashboard/commissions/MyEarningsPage.tsx b/src/pages/dashboard/commissions/MyEarningsPage.tsx
new file mode 100644
index 0000000..76805b3
--- /dev/null
+++ b/src/pages/dashboard/commissions/MyEarningsPage.tsx
@@ -0,0 +1,214 @@
+import { useMyEarnings } from '@/hooks/useCommissions';
+import type { CommissionEntry, EntryStatus } from '@/services/commissions';
+
+export default function MyEarningsPage() {
+ const { data: earnings, isLoading } = useMyEarnings();
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ 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';
+ };
+
+ return (
+
+
+
My Earnings
+
+
+ {/* Summary Cards */}
+
+
+
+
+
+
+
+ - Total Earned
+ -
+ ${(earnings?.total ?? 0).toLocaleString()}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - Pending
+ -
+ ${(earnings?.pending ?? 0).toLocaleString()}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - Approved
+ -
+ ${(earnings?.approved ?? 0).toLocaleString()}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - Paid
+ -
+ ${(earnings?.paid ?? 0).toLocaleString()}
+
+
+
+
+
+
+
+
+ {/* Additional Stats */}
+
+
Summary
+
+
+ Current Period
+
+ ${(earnings?.current_period_earnings ?? 0).toLocaleString()}
+
+
+
+ Total Entries
+ {earnings?.entries_count ?? 0}
+
+
+ Last Paid
+
+ {earnings?.last_paid_date
+ ? new Date(earnings.last_paid_date).toLocaleDateString()
+ : 'Never'}
+
+
+
+
+
+ {/* Recent Entries */}
+
+
+
Recent Commission Entries
+
+
+
+
+ | Date |
+ Reference |
+ Base Amount |
+ Commission |
+ Status |
+
+
+
+ {earnings?.recent_entries?.map((entry: CommissionEntry) => (
+
+ |
+ {new Date(entry.created_at).toLocaleDateString()}
+ |
+
+ {entry.reference_type}
+ #{entry.reference_id.substring(0, 8)}
+ |
+
+ ${entry.base_amount.toLocaleString()}
+ |
+
+ ${entry.commission_amount.toLocaleString()}
+ |
+
+
+ {entry.status}
+
+ |
+
+ ))}
+
+
+
+ {(!earnings?.recent_entries || earnings.recent_entries.length === 0) && (
+
+
No commission entries yet
+
+ )}
+
+
+ {/* Quick Actions */}
+
+
+ );
+}
diff --git a/src/pages/dashboard/commissions/PeriodsPage.tsx b/src/pages/dashboard/commissions/PeriodsPage.tsx
new file mode 100644
index 0000000..1941110
--- /dev/null
+++ b/src/pages/dashboard/commissions/PeriodsPage.tsx
@@ -0,0 +1,261 @@
+import { useState } from 'react';
+import { usePeriods, useClosePeriod, useMarkPeriodPaid } from '@/hooks/useCommissions';
+import type { PeriodFilters, CommissionPeriod, PeriodStatus } from '@/services/commissions';
+
+export default function PeriodsPage() {
+ const [filters, setFilters] = useState({ page: 1, limit: 20 });
+ const [payModal, setPayModal] = useState<{ id: string; reference: string; notes: string } | null>(null);
+ const { data, isLoading, error } = usePeriods(filters);
+ const closePeriod = useClosePeriod();
+ const markPaid = useMarkPeriodPaid();
+
+ const handleClose = async (id: string) => {
+ if (window.confirm('Are you sure you want to close this period? This action cannot be undone.')) {
+ await closePeriod.mutateAsync(id);
+ }
+ };
+
+ const handleMarkPaid = async () => {
+ if (!payModal) return;
+ await markPaid.mutateAsync({
+ id: payModal.id,
+ data: {
+ payment_reference: payModal.reference || undefined,
+ payment_notes: payModal.notes || undefined,
+ }
+ });
+ setPayModal(null);
+ };
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
Error loading periods
+
+ );
+ }
+
+ const getStatusColor = (status: PeriodStatus) => {
+ const colors: Record = {
+ open: 'bg-blue-100 text-blue-800',
+ closed: 'bg-yellow-100 text-yellow-800',
+ processing: 'bg-orange-100 text-orange-800',
+ paid: 'bg-green-100 text-green-800',
+ };
+ return colors[status] || 'bg-gray-100 text-gray-800';
+ };
+
+ return (
+
+
+
+ {/* 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"
+ />
+
+
+
+
+ {/* Periods List */}
+
+
+
+
+ | Period |
+ Date Range |
+ Entries |
+ Total Amount |
+ Status |
+ Actions |
+
+
+
+ {data?.data?.map((period: CommissionPeriod) => (
+
+ |
+ {period.name}
+ |
+
+
+ {new Date(period.starts_at).toLocaleDateString()} - {new Date(period.ends_at).toLocaleDateString()}
+
+ |
+
+ {period.total_entries || 0}
+ |
+
+
+ ${(period.total_amount || 0).toLocaleString()}
+
+ |
+
+
+ {period.status}
+
+ |
+
+ {period.status === 'open' && (
+
+ )}
+ {period.status === 'closed' && (
+
+ )}
+ {(period.status === 'paid' || period.status === 'processing') && (
+
+ View
+
+ )}
+ |
+
+ ))}
+
+
+
+ {data?.data?.length === 0 && (
+
+ )}
+
+ {/* Pagination */}
+ {data && data.totalPages > 1 && (
+
+
+
+
+ Showing page {filters.page || 1} of{' '}
+ {data.totalPages} ({data.total} total)
+
+
+
+
+
+
+
+
+ )}
+
+
+ {/* Pay Modal */}
+ {payModal && (
+
+
+
Mark Period as Paid
+
+
+
+ setPayModal({ ...payModal, reference: e.target.value })}
+ placeholder="e.g., Wire transfer #12345"
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/pages/dashboard/commissions/SchemesPage.tsx b/src/pages/dashboard/commissions/SchemesPage.tsx
new file mode 100644
index 0000000..77cd961
--- /dev/null
+++ b/src/pages/dashboard/commissions/SchemesPage.tsx
@@ -0,0 +1,187 @@
+import { useState } from 'react';
+import { useSchemes, useDeleteScheme, useToggleSchemeActive } from '@/hooks/useCommissions';
+import type { SchemeFilters, CommissionScheme } from '@/services/commissions';
+
+export default function SchemesPage() {
+ const [filters, setFilters] = useState({ page: 1, limit: 20 });
+ const { data, isLoading, error } = useSchemes(filters);
+ const deleteScheme = useDeleteScheme();
+ const toggleActive = useToggleSchemeActive();
+
+ const handleDelete = async (id: string) => {
+ if (window.confirm('Are you sure you want to delete this scheme?')) {
+ await deleteScheme.mutateAsync(id);
+ }
+ };
+
+ const handleToggleActive = async (id: string) => {
+ await toggleActive.mutateAsync(id);
+ };
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
Error loading schemes
+
+ );
+ }
+
+ const getTypeLabel = (type: string) => {
+ const labels: Record = {
+ percentage: 'Percentage',
+ fixed: 'Fixed Amount',
+ tiered: 'Tiered',
+ };
+ return labels[type] || type;
+ };
+
+ return (
+
+
+
+ {/* Filters */}
+
+
+
+
+
+
+
+
+
+
+
+
+ setFilters((prev) => ({ ...prev, search: e.target.value || undefined, page: 1 }))}
+ placeholder="Search by name..."
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
+ />
+
+
+
+
+ {/* Schemes Grid */}
+
+ {data?.data?.map((scheme: CommissionScheme) => (
+
+
+
+
{scheme.name}
+
{getTypeLabel(scheme.type)}
+
+
+
+
+ {scheme.description && (
+
{scheme.description}
+ )}
+
+
+ {scheme.type === 'percentage' && (
+
+ Rate
+ {scheme.rate}%
+
+ )}
+ {scheme.type === 'fixed' && (
+
+ Amount
+ ${scheme.fixed_amount?.toLocaleString()}
+
+ )}
+ {scheme.min_amount > 0 && (
+
+ Min Amount
+ ${scheme.min_amount.toLocaleString()}
+
+ )}
+ {scheme.max_amount && (
+
+ Max Amount
+ ${scheme.max_amount.toLocaleString()}
+
+ )}
+
+
+
+
+ View Details
+
+
+
+
+ ))}
+
+
+ {data?.data?.length === 0 && (
+
+ )}
+
+ );
+}
diff --git a/src/pages/dashboard/commissions/index.ts b/src/pages/dashboard/commissions/index.ts
new file mode 100644
index 0000000..1a4021e
--- /dev/null
+++ b/src/pages/dashboard/commissions/index.ts
@@ -0,0 +1,5 @@
+export { default as CommissionsPage } from './CommissionsPage';
+export { default as SchemesPage } from './SchemesPage';
+export { default as EntriesPage } from './EntriesPage';
+export { default as PeriodsPage } from './PeriodsPage';
+export { default as MyEarningsPage } from './MyEarningsPage';
diff --git a/src/pages/dashboard/sales/ActivitiesPage.tsx b/src/pages/dashboard/sales/ActivitiesPage.tsx
new file mode 100644
index 0000000..39a46ff
--- /dev/null
+++ b/src/pages/dashboard/sales/ActivitiesPage.tsx
@@ -0,0 +1,188 @@
+import { useState } from 'react';
+import { useActivities, useDeleteActivity } from '@/hooks/useSales';
+import type { ActivityFilters, Activity, ActivityType, ActivityStatus } from '@/services/sales';
+
+export default function ActivitiesPage() {
+ const [filters, setFilters] = useState({ page: 1, limit: 20 });
+ const { data, isLoading, error } = useActivities(filters);
+ const deleteActivity = useDeleteActivity();
+
+ const handleDelete = async (id: string) => {
+ if (window.confirm('Are you sure you want to delete this activity?')) {
+ await deleteActivity.mutateAsync(id);
+ }
+ };
+
+ const handlePageChange = (page: number) => {
+ setFilters((prev) => ({ ...prev, page }));
+ };
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
Error loading activities
+
+ );
+ }
+
+ const getTypeIcon = (type: ActivityType) => {
+ const icons: Record = {
+ call: '📞',
+ email: '📧',
+ meeting: '📅',
+ task: '✅',
+ note: '📝',
+ };
+ return icons[type] || '📋';
+ };
+
+ const getStatusColor = (status: ActivityStatus) => {
+ const colors: Record = {
+ pending: 'bg-blue-100 text-blue-800',
+ completed: 'bg-green-100 text-green-800',
+ cancelled: 'bg-red-100 text-red-800',
+ };
+ return colors[status] || 'bg-gray-100 text-gray-800';
+ };
+
+ return (
+
+
+
+ {/* Filters */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Activities List */}
+
+
+ {data?.data?.map((activity: Activity) => (
+ -
+
+
{getTypeIcon(activity.type)}
+
+
+
{activity.subject}
+
+ {activity.status}
+
+
+ {activity.description && (
+
{activity.description}
+ )}
+
+ {activity.type}
+ {activity.due_date && (
+ Due: {new Date(activity.due_date).toLocaleDateString()}
+ )}
+ {activity.lead_id && Lead Activity}
+ {activity.opportunity_id && Opportunity Activity}
+
+
+
+
+ View
+
+
+
+
+
+ ))}
+
+
+ {data?.data?.length === 0 && (
+
+ )}
+
+ {/* Pagination */}
+ {data && data.totalPages > 1 && (
+
+
+
+
+ Showing page {filters.page || 1} of{' '}
+ {data.totalPages} ({data.total} total)
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/pages/dashboard/sales/LeadDetailPage.tsx b/src/pages/dashboard/sales/LeadDetailPage.tsx
new file mode 100644
index 0000000..8827ac9
--- /dev/null
+++ b/src/pages/dashboard/sales/LeadDetailPage.tsx
@@ -0,0 +1,376 @@
+import { useState } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { useLead, useUpdateLead, useConvertLead, useActivities, useCreateActivity } from '@/hooks/useSales';
+import type { UpdateLeadDto, CreateActivityDto } from '@/services/sales';
+
+export default function LeadDetailPage() {
+ const { id } = useParams<{ id: string }>();
+ const navigate = useNavigate();
+ const { data: lead, isLoading, error } = useLead(id || '');
+ const { data: activities } = useActivities({ lead_id: id, limit: 10 });
+ const updateLead = useUpdateLead();
+ const convertLead = useConvertLead();
+ const createActivity = useCreateActivity();
+
+ const [isEditing, setIsEditing] = useState(false);
+ const [formData, setFormData] = useState({});
+ const [showConvertModal, setShowConvertModal] = useState(false);
+ const [convertData, setConvertData] = useState({ opportunity_name: '', amount: 0, expected_close_date: '' });
+ const [showActivityModal, setShowActivityModal] = useState(false);
+ const [activityData, setActivityData] = useState({
+ lead_id: id || '',
+ type: 'note',
+ subject: '',
+ });
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (error || !lead) {
+ return (
+
+ );
+ }
+
+ const handleUpdate = async () => {
+ if (!id) return;
+ await updateLead.mutateAsync({ id, data: formData });
+ setIsEditing(false);
+ };
+
+ const handleConvert = async () => {
+ if (!id) return;
+ const result = await convertLead.mutateAsync({
+ id,
+ data: {
+ opportunity_name: convertData.opportunity_name || `${lead.first_name} ${lead.last_name} - Opportunity`,
+ amount: convertData.amount,
+ expected_close_date: convertData.expected_close_date || undefined,
+ },
+ });
+ navigate(`/dashboard/sales/opportunities/${result.opportunity_id}`);
+ };
+
+ const handleCreateActivity = async () => {
+ await createActivity.mutateAsync(activityData);
+ setShowActivityModal(false);
+ setActivityData({ lead_id: id || '', type: 'note', subject: '' });
+ };
+
+ const getStatusColor = (status: string) => {
+ const colors: Record = {
+ new: 'bg-blue-100 text-blue-800',
+ contacted: 'bg-yellow-100 text-yellow-800',
+ qualified: 'bg-green-100 text-green-800',
+ unqualified: 'bg-red-100 text-red-800',
+ converted: 'bg-purple-100 text-purple-800',
+ };
+ return colors[status] || 'bg-gray-100 text-gray-800';
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+ {lead.status !== 'converted' && (
+
+ )}
+
+
+
+
+
+ {/* Lead Info */}
+
+
+
+
Lead Information
+
+ {lead.status}
+
+
+
+ {isEditing ? (
+
+
+
+
+ setFormData({ ...formData, email: e.target.value })}
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
+ />
+
+
+
+
+
+
+
+ ) : (
+
+
+
- Email
+ - {lead.email}
+
+
+
- Phone
+ - {lead.phone || '-'}
+
+
+
- Company
+ - {lead.company || '-'}
+
+
+
- Job Title
+ - {lead.job_title || '-'}
+
+
+
- Source
+ - {lead.source.replace('_', ' ')}
+
+
+
- Score
+ - {lead.score}
+
+
+
- Notes
+ - {lead.notes || 'No notes'}
+
+
+ )}
+
+
+ {/* Activities */}
+
+
+
Recent Activities
+
+
+ {activities?.data?.length ? (
+
+ {activities.data.map((activity) => (
+ -
+
+
{activity.subject}
+
{activity.type.replace('_', ' ')}
+
+
+ {new Date(activity.created_at).toLocaleDateString()}
+
+
+ ))}
+
+ ) : (
+
No activities yet
+ )}
+
+
+
+ {/* Sidebar */}
+
+
+
Details
+
+
+
- Created
+ - {new Date(lead.created_at).toLocaleDateString()}
+
+
+
- Last Updated
+ - {new Date(lead.updated_at).toLocaleDateString()}
+
+ {lead.assigned_to && (
+
+
- Assigned To
+ - User #{lead.assigned_to}
+
+ )}
+
+
+
+
+
+ {/* Convert Modal */}
+ {showConvertModal && (
+
+
+
Convert to Opportunity
+
+
+
+
+
+
+
+ )}
+
+ {/* Activity Modal */}
+ {showActivityModal && (
+
+
+
Add Activity
+
+
+
+
+
+
+
+ setActivityData({ ...activityData, subject: e.target.value })}
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/pages/dashboard/sales/LeadsPage.tsx b/src/pages/dashboard/sales/LeadsPage.tsx
new file mode 100644
index 0000000..a4f11a9
--- /dev/null
+++ b/src/pages/dashboard/sales/LeadsPage.tsx
@@ -0,0 +1,184 @@
+import { useState } from 'react';
+import { useLeads, useDeleteLead } from '@/hooks/useSales';
+import type { LeadFilters, LeadStatus, LeadSource, Lead } from '@/services/sales';
+
+export default function LeadsPage() {
+ const [filters, setFilters] = useState({ page: 1, limit: 20 });
+ const { data, isLoading, error } = useLeads(filters);
+ const deleteLead = useDeleteLead();
+
+ const handleDelete = async (id: string) => {
+ if (window.confirm('Are you sure you want to delete this lead?')) {
+ await deleteLead.mutateAsync(id);
+ }
+ };
+
+ const handlePageChange = (page: number) => {
+ setFilters((prev) => ({ ...prev, page }));
+ };
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ const getStatusColor = (status: LeadStatus) => {
+ const colors: Record = {
+ new: 'bg-blue-100 text-blue-800',
+ contacted: 'bg-yellow-100 text-yellow-800',
+ qualified: 'bg-green-100 text-green-800',
+ unqualified: 'bg-red-100 text-red-800',
+ converted: 'bg-purple-100 text-purple-800',
+ };
+ return colors[status] || 'bg-gray-100 text-gray-800';
+ };
+
+ return (
+
+
+
+ {/* Filters */}
+
+
+
+
+
+
+
+
+
+
+
+
+ setFilters((prev) => ({ ...prev, search: e.target.value || undefined, page: 1 }))}
+ placeholder="Search by name, email, company..."
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
+ />
+
+
+
+
+ {/* Leads Table */}
+
+
+
+
+ | Name |
+ Company |
+ Status |
+ Source |
+ Score |
+ Actions |
+
+
+
+ {data?.data?.map((lead: Lead) => (
+
+
+
+
+ {lead.first_name} {lead.last_name}
+
+ {lead.email}
+
+ |
+ {lead.company || '-'} |
+
+
+ {lead.status}
+
+ |
+ {lead.source.replace('_', ' ')} |
+ {lead.score} |
+
+
+ View
+
+
+ |
+
+ ))}
+
+
+
+ {/* Pagination */}
+ {data && data.totalPages > 1 && (
+
+
+
+
+ Showing page {filters.page || 1} of{' '}
+ {data.totalPages} ({data.total} total)
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/pages/dashboard/sales/OpportunitiesPage.tsx b/src/pages/dashboard/sales/OpportunitiesPage.tsx
new file mode 100644
index 0000000..dba0e89
--- /dev/null
+++ b/src/pages/dashboard/sales/OpportunitiesPage.tsx
@@ -0,0 +1,105 @@
+import { useState } from 'react';
+import { useOpportunities, usePipelineStages } from '@/hooks/useSales';
+import type { OpportunityFilters, Opportunity, OpportunityStage } from '@/services/sales';
+
+export default function OpportunitiesPage() {
+ const [filters, setFilters] = useState({ page: 1, limit: 20 });
+ const { data: opportunities, isLoading } = useOpportunities(filters);
+ const { data: stages } = usePipelineStages();
+
+ const getStageColor = (stage: OpportunityStage) => {
+ const colors: Record = {
+ prospecting: 'bg-gray-100 text-gray-800',
+ qualification: 'bg-blue-100 text-blue-800',
+ proposal: 'bg-purple-100 text-purple-800',
+ negotiation: 'bg-yellow-100 text-yellow-800',
+ closed_won: 'bg-green-100 text-green-800',
+ closed_lost: 'bg-red-100 text-red-800',
+ };
+ return colors[stage] || 'bg-gray-100 text-gray-800';
+ };
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ {/* Stage Filters */}
+
+
+ {stages?.map((stage) => (
+
+ ))}
+
+
+ {/* Opportunities Grid */}
+
+ {opportunities?.data?.map((opp: Opportunity) => (
+
window.location.href = `/dashboard/sales/opportunities/${opp.id}`}
+ >
+
+
{opp.name}
+
+ {opp.stage.replace('_', ' ')}
+
+
+
{opp.company_name || 'No company'}
+
+
+ ${opp.amount.toLocaleString()}
+
+ {opp.probability}% probability
+
+ {opp.expected_close_date && (
+
+ Expected close: {new Date(opp.expected_close_date).toLocaleDateString()}
+
+ )}
+
+ ))}
+
+
+ {opportunities?.data?.length === 0 && (
+
+ )}
+
+ );
+}
diff --git a/src/pages/dashboard/sales/OpportunityDetailPage.tsx b/src/pages/dashboard/sales/OpportunityDetailPage.tsx
new file mode 100644
index 0000000..83bbf3e
--- /dev/null
+++ b/src/pages/dashboard/sales/OpportunityDetailPage.tsx
@@ -0,0 +1,379 @@
+import { useState } from 'react';
+import { useParams } from 'react-router-dom';
+import { useOpportunity, useUpdateOpportunity, usePipelineStages, useActivities, useCreateActivity } from '@/hooks/useSales';
+import type { UpdateOpportunityDto, CreateActivityDto } from '@/services/sales';
+
+export default function OpportunityDetailPage() {
+ const { id } = useParams<{ id: string }>();
+ const { data: opportunity, isLoading, error } = useOpportunity(id || '');
+ const { data: stages } = usePipelineStages();
+ const { data: activities } = useActivities({ opportunity_id: id, limit: 10 });
+ const updateOpportunity = useUpdateOpportunity();
+ const createActivity = useCreateActivity();
+
+ const [isEditing, setIsEditing] = useState(false);
+ const [formData, setFormData] = useState({});
+ const [showActivityModal, setShowActivityModal] = useState(false);
+ const [activityData, setActivityData] = useState({
+ opportunity_id: id || '',
+ type: 'note',
+ subject: '',
+ });
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (error || !opportunity) {
+ return (
+
+ );
+ }
+
+ const handleUpdate = async () => {
+ if (!id) return;
+ await updateOpportunity.mutateAsync({ id, data: formData });
+ setIsEditing(false);
+ };
+
+ const handleStageChange = async (stageId: string) => {
+ if (!id) return;
+ await updateOpportunity.mutateAsync({ id, data: { stage_id: stageId } });
+ };
+
+ const handleCreateActivity = async () => {
+ await createActivity.mutateAsync(activityData);
+ setShowActivityModal(false);
+ setActivityData({ opportunity_id: id || '', type: 'note', subject: '' });
+ };
+
+ const getStageColor = (stage: string) => {
+ const colors: Record = {
+ prospecting: 'bg-gray-100 text-gray-800',
+ qualification: 'bg-blue-100 text-blue-800',
+ proposal: 'bg-purple-100 text-purple-800',
+ negotiation: 'bg-yellow-100 text-yellow-800',
+ closed_won: 'bg-green-100 text-green-800',
+ closed_lost: 'bg-red-100 text-red-800',
+ };
+ return colors[stage] || 'bg-gray-100 text-gray-800';
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
+ {/* Stage Pipeline */}
+
+
Stage
+
+ {stages?.map((stage) => (
+
+ ))}
+
+
+
+
+ {/* Opportunity Info */}
+
+
+
+
Opportunity Information
+
+ {opportunity.stage.replace('_', ' ')}
+
+
+
+ {isEditing ? (
+
+
+
+ setFormData({ ...formData, name: e.target.value })}
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
+ />
+
+
+
+
+ setFormData({ ...formData, expected_close_date: e.target.value })}
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
+ />
+
+
+
+
+
+
+ ) : (
+
+
+
- Amount
+ - ${opportunity.amount.toLocaleString()}
+
+
+
- Probability
+ - {opportunity.probability}%
+
+
+
- Weighted Value
+ -
+ ${((opportunity.amount * opportunity.probability) / 100).toLocaleString()}
+
+
+
+
- Expected Close
+ -
+ {opportunity.expected_close_date
+ ? new Date(opportunity.expected_close_date).toLocaleDateString()
+ : 'Not set'}
+
+
+ {opportunity.company_name && (
+
+
- Company
+ - {opportunity.company_name}
+
+ )}
+ {opportunity.contact_name && (
+
+
- Contact
+ - {opportunity.contact_name}
+
+ )}
+ {opportunity.description && (
+
+
- Description
+ - {opportunity.description}
+
+ )}
+
+ )}
+
+
+ {/* Activities */}
+
+
+
Recent Activities
+
+
+ {activities?.data?.length ? (
+
+ {activities.data.map((activity) => (
+ -
+
+
{activity.subject}
+
{activity.type.replace('_', ' ')}
+
+
+ {new Date(activity.created_at).toLocaleDateString()}
+
+
+ ))}
+
+ ) : (
+
No activities yet
+ )}
+
+
+
+ {/* Sidebar */}
+
+
+
Details
+
+
+
- Created
+ - {new Date(opportunity.created_at).toLocaleDateString()}
+
+
+
- Last Updated
+ - {new Date(opportunity.updated_at).toLocaleDateString()}
+
+ {opportunity.actual_close_date && (
+
+
- Closed At
+ - {new Date(opportunity.actual_close_date).toLocaleDateString()}
+
+ )}
+ {opportunity.assigned_to && (
+
+
- Assigned To
+ - User #{opportunity.assigned_to}
+
+ )}
+ {opportunity.lead_id && (
+
+ )}
+
+
+
+ {/* Quick Stats */}
+
+
Summary
+
+
+ Deal Value
+ ${opportunity.amount.toLocaleString()}
+
+
+ Win Probability
+ {opportunity.probability}%
+
+
+ Expected Value
+
+ ${((opportunity.amount * opportunity.probability) / 100).toLocaleString()}
+
+
+
+
+
+
+
+ {/* Activity Modal */}
+ {showActivityModal && (
+
+
+
Add Activity
+
+
+
+
+
+
+
+ setActivityData({ ...activityData, subject: e.target.value })}
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/pages/dashboard/sales/SalesPage.tsx b/src/pages/dashboard/sales/SalesPage.tsx
new file mode 100644
index 0000000..646f1b7
--- /dev/null
+++ b/src/pages/dashboard/sales/SalesPage.tsx
@@ -0,0 +1,163 @@
+import { useSalesDashboard, usePipelineView, useLeadStats } from '@/hooks/useSales';
+
+export default function SalesPage() {
+ const { data: dashboard, isLoading } = useSalesDashboard();
+ const { data: pipelineView } = usePipelineView();
+ const { data: leadStats } = useLeadStats();
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
Sales Dashboard
+
+
+ {/* Summary Cards */}
+
+
+
+
+
+
+
+ - Total Leads
+ - {dashboard?.leads?.total ?? 0}
+
+
+
+
+
+
+
+
+
+
+
+
+ - Total Opportunities
+ - {dashboard?.opportunities?.total ?? 0}
+
+
+
+
+
+
+
+
+
+
+
+
+ - Pipeline Value
+ -
+ ${(dashboard?.pipeline?.totalValue ?? 0).toLocaleString()}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - Won Deals
+ - {dashboard?.opportunities?.won ?? 0}
+
+
+
+
+
+
+
+ {/* Quick Stats */}
+
+ {/* Leads by Status */}
+
+
Leads by Status
+
+ {leadStats && Object.entries(leadStats.byStatus).map(([status, count]) => (
+
+ {status.replace('_', ' ')}
+ {count}
+
+ ))}
+
+
+
+ {/* Opportunities by Stage */}
+
+
Opportunities by Stage
+
+ {pipelineView?.map((stage) => (
+
+
{stage.stageName}
+
+ {stage.count}
+ (${stage.totalAmount.toLocaleString()})
+
+
+ ))}
+
+
+
+
+ {/* Quick Links */}
+
+
+ );
+}
diff --git a/src/pages/dashboard/sales/index.ts b/src/pages/dashboard/sales/index.ts
new file mode 100644
index 0000000..2079e03
--- /dev/null
+++ b/src/pages/dashboard/sales/index.ts
@@ -0,0 +1,6 @@
+export { default as SalesPage } from './SalesPage';
+export { default as LeadsPage } from './LeadsPage';
+export { default as LeadDetailPage } from './LeadDetailPage';
+export { default as OpportunitiesPage } from './OpportunitiesPage';
+export { default as OpportunityDetailPage } from './OpportunityDetailPage';
+export { default as ActivitiesPage } from './ActivitiesPage';
diff --git a/src/router/index.tsx b/src/router/index.tsx
index 63fd8d6..bb78e64 100644
--- a/src/router/index.tsx
+++ b/src/router/index.tsx
@@ -31,6 +31,21 @@ const WebhooksPage = lazy(() => import('@/pages/dashboard/WebhooksPage').then(m
const AuditLogsPage = lazy(() => import('@/pages/dashboard/AuditLogsPage').then(m => ({ default: m.AuditLogsPage })));
const FeatureFlagsPage = lazy(() => import('@/pages/dashboard/FeatureFlagsPage').then(m => ({ default: m.FeatureFlagsPage })));
+// Lazy loaded pages - Sales
+const SalesPage = lazy(() => import('@/pages/dashboard/sales').then(m => ({ default: m.SalesPage })));
+const LeadsPage = lazy(() => import('@/pages/dashboard/sales').then(m => ({ default: m.LeadsPage })));
+const LeadDetailPage = lazy(() => import('@/pages/dashboard/sales').then(m => ({ default: m.LeadDetailPage })));
+const OpportunitiesPage = lazy(() => import('@/pages/dashboard/sales').then(m => ({ default: m.OpportunitiesPage })));
+const OpportunityDetailPage = lazy(() => import('@/pages/dashboard/sales').then(m => ({ default: m.OpportunityDetailPage })));
+const ActivitiesPage = lazy(() => import('@/pages/dashboard/sales').then(m => ({ default: m.ActivitiesPage })));
+
+// Lazy loaded pages - Commissions
+const CommissionsPage = lazy(() => import('@/pages/dashboard/commissions').then(m => ({ default: m.CommissionsPage })));
+const SchemesPage = lazy(() => import('@/pages/dashboard/commissions').then(m => ({ default: m.SchemesPage })));
+const EntriesPage = lazy(() => import('@/pages/dashboard/commissions').then(m => ({ default: m.EntriesPage })));
+const PeriodsPage = lazy(() => import('@/pages/dashboard/commissions').then(m => ({ default: m.PeriodsPage })));
+const MyEarningsPage = lazy(() => import('@/pages/dashboard/commissions').then(m => ({ default: m.MyEarningsPage })));
+
// Lazy loaded pages - Admin
const WhatsAppSettings = lazy(() => import('@/pages/admin/WhatsAppSettings').then(m => ({ default: m.WhatsAppSettings })));
const AnalyticsDashboardPage = lazy(() => import('@/pages/admin/AnalyticsDashboardPage').then(m => ({ default: m.AnalyticsDashboardPage })));
@@ -133,6 +148,21 @@ export function AppRouter() {
} />
} />
} />
+
+ {/* Sales routes */}
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ {/* Commissions routes */}
+ } />
+ } />
+ } />
+ } />
+ } />
{/* Superadmin routes */}
diff --git a/src/services/commissions/assignments.api.ts b/src/services/commissions/assignments.api.ts
new file mode 100644
index 0000000..6324c16
--- /dev/null
+++ b/src/services/commissions/assignments.api.ts
@@ -0,0 +1,104 @@
+import api from '../api';
+import { CommissionScheme } from './schemes.api';
+
+export interface CommissionAssignment {
+ id: string;
+ tenant_id: string;
+ user_id: string;
+ scheme_id: string;
+ starts_at: string;
+ ends_at: string | null;
+ custom_rate: number | null;
+ is_active: boolean;
+ created_at: string;
+ created_by: string | null;
+ scheme?: CommissionScheme;
+ user?: {
+ id: string;
+ first_name: string;
+ last_name: string;
+ email: string;
+ };
+}
+
+export interface CreateAssignmentDto {
+ user_id: string;
+ scheme_id: string;
+ starts_at?: string;
+ ends_at?: string;
+ custom_rate?: number;
+ is_active?: boolean;
+}
+
+export interface UpdateAssignmentDto {
+ ends_at?: string;
+ custom_rate?: number;
+ is_active?: boolean;
+}
+
+export interface AssignmentFilters {
+ user_id?: string;
+ scheme_id?: string;
+ is_active?: boolean;
+ page?: number;
+ limit?: number;
+ sortBy?: string;
+ sortOrder?: 'ASC' | 'DESC';
+}
+
+export interface PaginatedAssignments {
+ data: CommissionAssignment[];
+ total: number;
+ page: number;
+ limit: number;
+ totalPages: number;
+}
+
+export const assignmentsApi = {
+ list: async (params?: AssignmentFilters): Promise => {
+ const response = await api.get('/commissions/assignments', { params });
+ return response.data;
+ },
+
+ get: async (id: string): Promise => {
+ const response = await api.get(`/commissions/assignments/${id}`);
+ return response.data;
+ },
+
+ getByUser: async (userId: string): Promise => {
+ const response = await api.get(`/commissions/assignments/user/${userId}`);
+ return response.data;
+ },
+
+ getActiveScheme: async (userId: string): Promise => {
+ const response = await api.get(`/commissions/assignments/user/${userId}/active`);
+ if ('message' in response.data) {
+ return null;
+ }
+ return response.data;
+ },
+
+ getSchemeAssignees: async (schemeId: string): Promise => {
+ const response = await api.get(`/commissions/assignments/scheme/${schemeId}/users`);
+ return response.data;
+ },
+
+ assign: async (data: CreateAssignmentDto): Promise => {
+ const response = await api.post('/commissions/assignments', data);
+ return response.data;
+ },
+
+ update: async (id: string, data: UpdateAssignmentDto): Promise => {
+ const response = await api.patch(`/commissions/assignments/${id}`, data);
+ return response.data;
+ },
+
+ remove: async (id: string): Promise => {
+ await api.delete(`/commissions/assignments/${id}`);
+ },
+
+ deactivate: async (id: string): Promise => {
+ const response = await api.post(`/commissions/assignments/${id}/deactivate`);
+ return response.data;
+ },
+};
diff --git a/src/services/commissions/dashboard.api.ts b/src/services/commissions/dashboard.api.ts
new file mode 100644
index 0000000..b8629a3
--- /dev/null
+++ b/src/services/commissions/dashboard.api.ts
@@ -0,0 +1,101 @@
+import api from '../api';
+import { CommissionEntry } from './entries.api';
+import { PeriodStatus } from './periods.api';
+
+export interface DashboardSummary {
+ total_pending: number;
+ total_approved: number;
+ total_paid: number;
+ pending_count: number;
+ approved_count: number;
+ paid_count: number;
+ active_schemes: number;
+ active_assignments: number;
+ open_periods: number;
+ currency: string;
+}
+
+export interface UserEarnings {
+ user_id: string;
+ user_name: string;
+ user_email: string;
+ pending: number;
+ approved: number;
+ paid: number;
+ total: number;
+ entries_count: number;
+}
+
+export interface PeriodEarnings {
+ period_id: string;
+ period_name: string;
+ starts_at: string;
+ ends_at: string;
+ status: PeriodStatus;
+ total_amount: number;
+ entries_count: number;
+}
+
+export interface TopEarner {
+ user_id: string;
+ user_name: string;
+ total_earned: number;
+ entries_count: number;
+ rank: number;
+}
+
+export interface MyEarnings {
+ pending: number;
+ approved: number;
+ paid: number;
+ total: number;
+ current_period_earnings: number;
+ last_paid_date: string | null;
+ entries_count: number;
+ recent_entries: CommissionEntry[];
+}
+
+export interface SchemePerformance {
+ total_generated: number;
+ total_paid: number;
+ entries_count: number;
+ avg_commission: number;
+ active_users: number;
+}
+
+export const dashboardApi = {
+ getSummary: async (): Promise => {
+ const response = await api.get('/commissions/dashboard/summary');
+ return response.data;
+ },
+
+ getEarningsByUser: async (params?: { date_from?: string; date_to?: string }): Promise => {
+ const response = await api.get('/commissions/dashboard/by-user', { params });
+ return response.data;
+ },
+
+ getEarningsByPeriod: async (): Promise => {
+ const response = await api.get('/commissions/dashboard/by-period');
+ return response.data;
+ },
+
+ getTopEarners: async (params?: { limit?: number; date_from?: string; date_to?: string }): Promise => {
+ const response = await api.get('/commissions/dashboard/top-earners', { params });
+ return response.data;
+ },
+
+ getMyEarnings: async (): Promise => {
+ const response = await api.get('/commissions/dashboard/my-earnings');
+ return response.data;
+ },
+
+ getUserEarnings: async (userId: string): Promise => {
+ const response = await api.get(`/commissions/dashboard/user/${userId}/earnings`);
+ return response.data;
+ },
+
+ getSchemePerformance: async (schemeId: string): Promise => {
+ const response = await api.get(`/commissions/dashboard/scheme/${schemeId}/performance`);
+ return response.data;
+ },
+};
diff --git a/src/services/commissions/entries.api.ts b/src/services/commissions/entries.api.ts
new file mode 100644
index 0000000..08fa3d2
--- /dev/null
+++ b/src/services/commissions/entries.api.ts
@@ -0,0 +1,147 @@
+import api from '../api';
+import { CommissionScheme } from './schemes.api';
+
+export type EntryStatus = 'pending' | 'approved' | 'rejected' | 'paid' | 'cancelled';
+
+export interface CommissionEntry {
+ id: string;
+ tenant_id: string;
+ user_id: string;
+ scheme_id: string;
+ assignment_id: string | null;
+ reference_type: string;
+ reference_id: string;
+ base_amount: number;
+ rate_applied: number;
+ commission_amount: number;
+ currency: string;
+ status: EntryStatus;
+ period_id: string | null;
+ paid_at: string | null;
+ payment_reference: string | null;
+ notes: string | null;
+ metadata: Record;
+ created_at: string;
+ updated_at: string;
+ approved_by: string | null;
+ approved_at: string | null;
+ scheme?: CommissionScheme;
+ user?: {
+ id: string;
+ first_name: string;
+ last_name: string;
+ email: string;
+ };
+}
+
+export interface CalculateCommissionDto {
+ user_id: string;
+ reference_type: string;
+ reference_id: string;
+ base_amount: number;
+ currency?: string;
+ notes?: string;
+ metadata?: Record;
+}
+
+export interface UpdateEntryStatusDto {
+ status: EntryStatus;
+ notes?: string;
+}
+
+export interface CalculationResult {
+ rate_applied: number;
+ commission_amount: number;
+ scheme_id: string;
+ assignment_id: string | null;
+}
+
+export interface EntryFilters {
+ user_id?: string;
+ scheme_id?: string;
+ period_id?: string;
+ status?: EntryStatus;
+ reference_type?: string;
+ date_from?: string;
+ date_to?: string;
+ page?: number;
+ limit?: number;
+ sortBy?: string;
+ sortOrder?: 'ASC' | 'DESC';
+}
+
+export interface PaginatedEntries {
+ data: CommissionEntry[];
+ total: number;
+ page: number;
+ limit: number;
+ totalPages: number;
+}
+
+export const entriesApi = {
+ list: async (params?: EntryFilters): Promise => {
+ const response = await api.get('/commissions/entries', { params });
+ return response.data;
+ },
+
+ listPending: async (params?: { page?: number; limit?: number }): Promise => {
+ const response = await api.get('/commissions/entries/pending', { params });
+ return response.data;
+ },
+
+ get: async (id: string): Promise => {
+ const response = await api.get(`/commissions/entries/${id}`);
+ return response.data;
+ },
+
+ getByUser: async (userId: string, params?: { page?: number; limit?: number }): Promise => {
+ const response = await api.get(`/commissions/entries/user/${userId}`, { params });
+ return response.data;
+ },
+
+ getByPeriod: async (periodId: string, params?: { page?: number; limit?: number }): Promise => {
+ const response = await api.get(`/commissions/entries/period/${periodId}`, { params });
+ return response.data;
+ },
+
+ getByReference: async (type: string, refId: string): Promise => {
+ const response = await api.get(`/commissions/entries/reference/${type}/${refId}`);
+ if ('message' in response.data) {
+ return null;
+ }
+ return response.data;
+ },
+
+ calculate: async (data: CalculateCommissionDto): Promise => {
+ const response = await api.post('/commissions/entries/calculate', data);
+ return response.data;
+ },
+
+ simulate: async (data: CalculateCommissionDto): Promise => {
+ const response = await api.post('/commissions/entries/simulate', data);
+ return response.data;
+ },
+
+ updateStatus: async (id: string, data: UpdateEntryStatusDto): Promise => {
+ const response = await api.patch(`/commissions/entries/${id}/status`, data);
+ return response.data;
+ },
+
+ bulkApprove: async (entryIds: string[]): Promise<{ approved: number; failed: number }> => {
+ const response = await api.post<{ approved: number; failed: number }>('/commissions/entries/bulk-approve', { entry_ids: entryIds });
+ return response.data;
+ },
+
+ bulkReject: async (entryIds: string[], reason?: string): Promise<{ rejected: number; failed: number }> => {
+ const response = await api.post<{ rejected: number; failed: number }>('/commissions/entries/bulk-reject', { entry_ids: entryIds, reason });
+ return response.data;
+ },
+
+ cancelByReference: async (type: string, refId: string, reason?: string): Promise => {
+ const response = await api.post(`/commissions/entries/cancel/${type}/${refId}`, { reason });
+ if ('message' in response.data) {
+ return null;
+ }
+ return response.data;
+ },
+};
diff --git a/src/services/commissions/index.ts b/src/services/commissions/index.ts
new file mode 100644
index 0000000..232dfe5
--- /dev/null
+++ b/src/services/commissions/index.ts
@@ -0,0 +1,5 @@
+export * from './schemes.api';
+export * from './assignments.api';
+export * from './entries.api';
+export * from './periods.api';
+export * from './dashboard.api';
diff --git a/src/services/commissions/periods.api.ts b/src/services/commissions/periods.api.ts
new file mode 100644
index 0000000..df670f9
--- /dev/null
+++ b/src/services/commissions/periods.api.ts
@@ -0,0 +1,121 @@
+import api from '../api';
+
+export type PeriodStatus = 'open' | 'closed' | 'processing' | 'paid';
+
+export interface CommissionPeriod {
+ id: string;
+ tenant_id: string;
+ name: string;
+ starts_at: string;
+ ends_at: string;
+ total_entries: number;
+ total_amount: number;
+ currency: string;
+ status: PeriodStatus;
+ closed_at: string | null;
+ closed_by: string | null;
+ paid_at: string | null;
+ paid_by: string | null;
+ payment_reference: string | null;
+ payment_notes: string | null;
+ created_at: string;
+ created_by: string | null;
+}
+
+export interface CreatePeriodDto {
+ name: string;
+ starts_at: string;
+ ends_at: string;
+ currency?: string;
+}
+
+export interface MarkPeriodPaidDto {
+ payment_reference?: string;
+ payment_notes?: string;
+}
+
+export interface PeriodFilters {
+ status?: PeriodStatus;
+ date_from?: string;
+ date_to?: string;
+ page?: number;
+ limit?: number;
+ sortBy?: string;
+ sortOrder?: 'ASC' | 'DESC';
+}
+
+export interface PaginatedPeriods {
+ data: CommissionPeriod[];
+ total: number;
+ page: number;
+ limit: number;
+ totalPages: number;
+}
+
+export interface PeriodSummary {
+ total_entries: number;
+ total_amount: number;
+ by_status: {
+ pending: number;
+ approved: number;
+ rejected: number;
+ paid: number;
+ cancelled: number;
+ };
+ by_user: Array<{
+ user_id: string;
+ user_name: string;
+ entries_count: number;
+ total_amount: number;
+ }>;
+}
+
+export const periodsApi = {
+ list: async (params?: PeriodFilters): Promise => {
+ const response = await api.get('/commissions/periods', { params });
+ return response.data;
+ },
+
+ getOpen: async (): Promise => {
+ const response = await api.get('/commissions/periods/open');
+ if ('message' in response.data) {
+ return null;
+ }
+ return response.data;
+ },
+
+ get: async (id: string): Promise => {
+ const response = await api.get(`/commissions/periods/${id}`);
+ return response.data;
+ },
+
+ getSummary: async (id: string): Promise => {
+ const response = await api.get(`/commissions/periods/${id}/summary`);
+ return response.data;
+ },
+
+ create: async (data: CreatePeriodDto): Promise => {
+ const response = await api.post('/commissions/periods', data);
+ return response.data;
+ },
+
+ close: async (id: string): Promise => {
+ const response = await api.post(`/commissions/periods/${id}/close`);
+ return response.data;
+ },
+
+ reopen: async (id: string): Promise => {
+ const response = await api.post(`/commissions/periods/${id}/reopen`);
+ return response.data;
+ },
+
+ markAsProcessing: async (id: string): Promise => {
+ const response = await api.post(`/commissions/periods/${id}/processing`);
+ return response.data;
+ },
+
+ markAsPaid: async (id: string, data?: MarkPeriodPaidDto): Promise => {
+ const response = await api.post(`/commissions/periods/${id}/pay`, data || {});
+ return response.data;
+ },
+};
diff --git a/src/services/commissions/schemes.api.ts b/src/services/commissions/schemes.api.ts
new file mode 100644
index 0000000..fd98a58
--- /dev/null
+++ b/src/services/commissions/schemes.api.ts
@@ -0,0 +1,107 @@
+import api from '../api';
+
+// Types
+export type SchemeType = 'percentage' | 'fixed' | 'tiered';
+export type AppliesTo = 'all' | 'products' | 'categories';
+
+export interface TierConfig {
+ from: number;
+ to: number | null;
+ rate: number;
+}
+
+export interface CommissionScheme {
+ id: string;
+ tenant_id: string;
+ name: string;
+ description: string | null;
+ type: SchemeType;
+ rate: number;
+ fixed_amount: number;
+ tiers: TierConfig[];
+ applies_to: AppliesTo;
+ product_ids: string[];
+ category_ids: string[];
+ min_amount: number;
+ max_amount: number | null;
+ is_active: boolean;
+ created_at: string;
+ updated_at: string;
+ created_by: string | null;
+}
+
+export interface CreateSchemeDto {
+ name: string;
+ description?: string;
+ type: SchemeType;
+ rate?: number;
+ fixed_amount?: number;
+ tiers?: TierConfig[];
+ applies_to?: AppliesTo;
+ product_ids?: string[];
+ category_ids?: string[];
+ min_amount?: number;
+ max_amount?: number;
+ is_active?: boolean;
+}
+
+export type UpdateSchemeDto = Partial;
+
+export interface SchemeFilters {
+ type?: SchemeType;
+ is_active?: boolean;
+ search?: string;
+ page?: number;
+ limit?: number;
+ sortBy?: string;
+ sortOrder?: 'ASC' | 'DESC';
+}
+
+export interface PaginatedSchemes {
+ data: CommissionScheme[];
+ total: number;
+ page: number;
+ limit: number;
+ totalPages: number;
+}
+
+export const schemesApi = {
+ list: async (params?: SchemeFilters): Promise => {
+ const response = await api.get('/commissions/schemes', { params });
+ return response.data;
+ },
+
+ listActive: async (): Promise => {
+ const response = await api.get('/commissions/schemes/active');
+ return response.data;
+ },
+
+ get: async (id: string): Promise => {
+ const response = await api.get(`/commissions/schemes/${id}`);
+ return response.data;
+ },
+
+ create: async (data: CreateSchemeDto): Promise => {
+ const response = await api.post('/commissions/schemes', data);
+ return response.data;
+ },
+
+ update: async (id: string, data: UpdateSchemeDto): Promise => {
+ const response = await api.patch(`/commissions/schemes/${id}`, data);
+ return response.data;
+ },
+
+ delete: async (id: string): Promise => {
+ await api.delete(`/commissions/schemes/${id}`);
+ },
+
+ duplicate: async (id: string, name?: string): Promise => {
+ const response = await api.post(`/commissions/schemes/${id}/duplicate`, { name });
+ return response.data;
+ },
+
+ toggleActive: async (id: string): Promise => {
+ const response = await api.post(`/commissions/schemes/${id}/toggle-active`);
+ return response.data;
+ },
+};
diff --git a/src/services/sales/activities.api.ts b/src/services/sales/activities.api.ts
new file mode 100644
index 0000000..0b1933c
--- /dev/null
+++ b/src/services/sales/activities.api.ts
@@ -0,0 +1,156 @@
+import api from '../api';
+
+// Types
+export type ActivityType = 'call' | 'meeting' | 'task' | 'email' | 'note';
+export type ActivityStatus = 'pending' | 'completed' | 'cancelled';
+
+export interface Activity {
+ id: string;
+ tenant_id: string;
+ type: ActivityType;
+ status: ActivityStatus;
+ subject: string;
+ description: string | null;
+ lead_id: string | null;
+ opportunity_id: string | null;
+ due_date: string | null;
+ due_time: string | null;
+ duration_minutes: number | null;
+ completed_at: string | null;
+ outcome: string | null;
+ assigned_to: string | null;
+ created_by: string | null;
+ call_direction: 'inbound' | 'outbound' | null;
+ call_recording_url: string | null;
+ location: string | null;
+ meeting_url: string | null;
+ attendees: Array<{
+ name: string;
+ email: string;
+ status?: 'accepted' | 'declined' | 'tentative' | 'pending';
+ }>;
+ reminder_at: string | null;
+ reminder_sent: boolean;
+ custom_fields: Record;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface CreateActivityDto {
+ type: ActivityType;
+ subject: string;
+ description?: string;
+ lead_id?: string;
+ opportunity_id?: string;
+ due_date?: string;
+ due_time?: string;
+ duration_minutes?: number;
+ assigned_to?: string;
+ call_direction?: 'inbound' | 'outbound';
+ location?: string;
+ meeting_url?: string;
+ attendees?: Array<{
+ name: string;
+ email: string;
+ status?: 'accepted' | 'declined' | 'tentative' | 'pending';
+ }>;
+ reminder_at?: string;
+ custom_fields?: Record;
+}
+
+export type UpdateActivityDto = Partial;
+
+export interface ActivityFilters {
+ type?: ActivityType;
+ status?: ActivityStatus;
+ lead_id?: string;
+ opportunity_id?: string;
+ assigned_to?: string;
+ page?: number;
+ limit?: number;
+ sortBy?: string;
+ sortOrder?: 'ASC' | 'DESC';
+}
+
+export interface PaginatedActivities {
+ data: Activity[];
+ total: number;
+ page: number;
+ limit: number;
+ totalPages: number;
+}
+
+export interface ActivityStats {
+ total: number;
+ pending: number;
+ completed: number;
+ overdue: number;
+ byType: Record;
+}
+
+export const activitiesApi = {
+ list: async (params?: ActivityFilters): Promise => {
+ const response = await api.get('/sales/activities', { params });
+ return response.data;
+ },
+
+ get: async (id: string): Promise => {
+ const response = await api.get(`/sales/activities/${id}`);
+ return response.data;
+ },
+
+ create: async (data: CreateActivityDto): Promise => {
+ const response = await api.post('/sales/activities', data);
+ return response.data;
+ },
+
+ update: async (id: string, data: UpdateActivityDto): Promise => {
+ const response = await api.patch(`/sales/activities/${id}`, data);
+ return response.data;
+ },
+
+ delete: async (id: string): Promise => {
+ await api.delete(`/sales/activities/${id}`);
+ },
+
+ complete: async (id: string, outcome?: string): Promise => {
+ const response = await api.post(`/sales/activities/${id}/complete`, { outcome });
+ return response.data;
+ },
+
+ cancel: async (id: string): Promise => {
+ const response = await api.post(`/sales/activities/${id}/cancel`);
+ return response.data;
+ },
+
+ getByLead: async (leadId: string): Promise => {
+ const response = await api.get(`/sales/activities/lead/${leadId}`);
+ return response.data;
+ },
+
+ getByOpportunity: async (opportunityId: string): Promise => {
+ const response = await api.get(`/sales/activities/opportunity/${opportunityId}`);
+ return response.data;
+ },
+
+ getUpcoming: async (days?: number, userId?: string): Promise => {
+ const response = await api.get('/sales/activities/upcoming', {
+ params: { days, user_id: userId },
+ });
+ return response.data;
+ },
+
+ getOverdue: async (userId?: string): Promise => {
+ const response = await api.get('/sales/activities/overdue', {
+ params: { user_id: userId },
+ });
+ return response.data;
+ },
+
+ getStats: async (userId?: string): Promise => {
+ const response = await api.get('/sales/activities/stats', {
+ params: { user_id: userId },
+ });
+ return response.data;
+ },
+};
diff --git a/src/services/sales/dashboard.api.ts b/src/services/sales/dashboard.api.ts
new file mode 100644
index 0000000..9cbd904
--- /dev/null
+++ b/src/services/sales/dashboard.api.ts
@@ -0,0 +1,108 @@
+import api from '../api';
+import { OpportunityStage } from './opportunities.api';
+
+// Types
+export interface SalesSummary {
+ leads: {
+ total: number;
+ new: number;
+ qualified: number;
+ converted: number;
+ conversionRate: number;
+ };
+ opportunities: {
+ total: number;
+ open: number;
+ won: number;
+ lost: number;
+ totalValue: number;
+ wonValue: number;
+ avgDealSize: number;
+ winRate: number;
+ };
+ activities: {
+ total: number;
+ pending: number;
+ completed: number;
+ overdue: number;
+ };
+ pipeline: {
+ totalValue: number;
+ weightedValue: number;
+ byStage: Array<{
+ stage: OpportunityStage;
+ count: number;
+ value: number;
+ }>;
+ };
+}
+
+export interface ConversionRates {
+ leadToOpportunity: number;
+ opportunityToWon: number;
+ overall: number;
+ bySource: Array<{
+ source: string;
+ leads: number;
+ converted: number;
+ rate: number;
+ }>;
+ byMonth: Array<{
+ month: string;
+ leads: number;
+ opportunities: number;
+ won: number;
+ }>;
+}
+
+export interface RevenueReport {
+ total: number;
+ byMonth: Array<{
+ month: string;
+ revenue: number;
+ deals: number;
+ }>;
+ byUser: Array<{
+ userId: string;
+ userName: string;
+ revenue: number;
+ deals: number;
+ }>;
+}
+
+export interface TopSeller {
+ userId: string;
+ userName: string;
+ revenue: number;
+ deals: number;
+ avgDealSize: number;
+ winRate: number;
+}
+
+export const salesDashboardApi = {
+ getSummary: async (): Promise => {
+ const response = await api.get('/sales/dashboard');
+ return response.data;
+ },
+
+ getConversionRates: async (startDate?: string, endDate?: string): Promise => {
+ const response = await api.get('/sales/dashboard/conversion', {
+ params: { start_date: startDate, end_date: endDate },
+ });
+ return response.data;
+ },
+
+ getRevenue: async (startDate: string, endDate: string): Promise => {
+ const response = await api.get('/sales/dashboard/revenue', {
+ params: { start_date: startDate, end_date: endDate },
+ });
+ return response.data;
+ },
+
+ getTopSellers: async (limit?: number, startDate?: string, endDate?: string): Promise => {
+ const response = await api.get('/sales/dashboard/top-sellers', {
+ params: { limit, start_date: startDate, end_date: endDate },
+ });
+ return response.data;
+ },
+};
diff --git a/src/services/sales/index.ts b/src/services/sales/index.ts
new file mode 100644
index 0000000..db0c5da
--- /dev/null
+++ b/src/services/sales/index.ts
@@ -0,0 +1,5 @@
+export * from './leads.api';
+export * from './opportunities.api';
+export * from './activities.api';
+export * from './pipeline.api';
+export * from './dashboard.api';
diff --git a/src/services/sales/leads.api.ts b/src/services/sales/leads.api.ts
new file mode 100644
index 0000000..4360986
--- /dev/null
+++ b/src/services/sales/leads.api.ts
@@ -0,0 +1,137 @@
+import api from '../api';
+
+// Types
+export type LeadStatus = 'new' | 'contacted' | 'qualified' | 'unqualified' | 'converted';
+export type LeadSource = 'website' | 'referral' | 'cold_call' | 'event' | 'advertisement' | 'social_media' | 'other';
+
+export interface Lead {
+ id: string;
+ tenant_id: string;
+ first_name: string;
+ last_name: string;
+ email: string | null;
+ phone: string | null;
+ company: string | null;
+ job_title: string | null;
+ website: string | null;
+ source: LeadSource;
+ status: LeadStatus;
+ score: number;
+ assigned_to: string | null;
+ notes: string | null;
+ converted_at: string | null;
+ converted_to_opportunity_id: string | null;
+ address_line1: string | null;
+ address_line2: string | null;
+ city: string | null;
+ state: string | null;
+ postal_code: string | null;
+ country: string | null;
+ custom_fields: Record;
+ created_at: string;
+ updated_at: string;
+ created_by: string | null;
+}
+
+export interface CreateLeadDto {
+ first_name: string;
+ last_name: string;
+ email?: string;
+ phone?: string;
+ company?: string;
+ job_title?: string;
+ website?: string;
+ source?: LeadSource;
+ status?: LeadStatus;
+ score?: number;
+ assigned_to?: string;
+ notes?: string;
+ address_line1?: string;
+ address_line2?: string;
+ city?: string;
+ state?: string;
+ postal_code?: string;
+ country?: string;
+ custom_fields?: Record;
+}
+
+export type UpdateLeadDto = Partial;
+
+export interface ConvertLeadDto {
+ opportunity_name?: string;
+ amount?: number;
+ currency?: string;
+ expected_close_date?: string;
+}
+
+export interface LeadFilters {
+ status?: LeadStatus;
+ source?: LeadSource;
+ assigned_to?: string;
+ search?: string;
+ page?: number;
+ limit?: number;
+ sortBy?: string;
+ sortOrder?: 'ASC' | 'DESC';
+}
+
+export interface PaginatedLeads {
+ data: Lead[];
+ total: number;
+ page: number;
+ limit: number;
+ totalPages: number;
+}
+
+export interface LeadStats {
+ total: number;
+ byStatus: Record;
+ bySource: Record;
+ avgScore: number;
+}
+
+export const leadsApi = {
+ list: async (params?: LeadFilters): Promise => {
+ const response = await api.get('/sales/leads', { params });
+ return response.data;
+ },
+
+ get: async (id: string): Promise => {
+ const response = await api.get(`/sales/leads/${id}`);
+ return response.data;
+ },
+
+ create: async (data: CreateLeadDto): Promise => {
+ const response = await api.post('/sales/leads', data);
+ return response.data;
+ },
+
+ update: async (id: string, data: UpdateLeadDto): Promise => {
+ const response = await api.patch(`/sales/leads/${id}`, data);
+ return response.data;
+ },
+
+ delete: async (id: string): Promise => {
+ await api.delete(`/sales/leads/${id}`);
+ },
+
+ convert: async (id: string, data: ConvertLeadDto): Promise => {
+ const response = await api.post(`/sales/leads/${id}/convert`, data);
+ return response.data;
+ },
+
+ assign: async (id: string, userId: string): Promise => {
+ const response = await api.patch(`/sales/leads/${id}/assign`, { user_id: userId });
+ return response.data;
+ },
+
+ updateScore: async (id: string, score: number): Promise => {
+ const response = await api.patch(`/sales/leads/${id}/score`, { score });
+ return response.data;
+ },
+
+ getStats: async (): Promise => {
+ const response = await api.get('/sales/leads/stats');
+ return response.data;
+ },
+};
diff --git a/src/services/sales/opportunities.api.ts b/src/services/sales/opportunities.api.ts
new file mode 100644
index 0000000..c8f3dd9
--- /dev/null
+++ b/src/services/sales/opportunities.api.ts
@@ -0,0 +1,165 @@
+import api from '../api';
+
+// Types
+export type OpportunityStage = 'prospecting' | 'qualification' | 'proposal' | 'negotiation' | 'closed_won' | 'closed_lost';
+
+export interface Opportunity {
+ id: string;
+ tenant_id: string;
+ name: string;
+ description: string | null;
+ lead_id: string | null;
+ stage: OpportunityStage;
+ stage_id: string | null;
+ amount: number;
+ currency: string;
+ probability: number;
+ expected_close_date: string | null;
+ actual_close_date: string | null;
+ assigned_to: string | null;
+ won_at: string | null;
+ lost_at: string | null;
+ lost_reason: string | null;
+ contact_name: string | null;
+ contact_email: string | null;
+ contact_phone: string | null;
+ company_name: string | null;
+ notes: string | null;
+ custom_fields: Record;
+ created_at: string;
+ updated_at: string;
+ created_by: string | null;
+}
+
+export interface CreateOpportunityDto {
+ name: string;
+ description?: string;
+ lead_id?: string;
+ stage?: OpportunityStage;
+ stage_id?: string;
+ amount?: number;
+ currency?: string;
+ probability?: number;
+ expected_close_date?: string;
+ assigned_to?: string;
+ contact_name?: string;
+ contact_email?: string;
+ contact_phone?: string;
+ company_name?: string;
+ notes?: string;
+ custom_fields?: Record;
+}
+
+export type UpdateOpportunityDto = Partial;
+
+export interface MoveOpportunityDto {
+ stage: OpportunityStage;
+ stage_id?: string;
+ notes?: string;
+}
+
+export interface OpportunityFilters {
+ stage?: OpportunityStage;
+ stage_id?: string;
+ assigned_to?: string;
+ min_amount?: number;
+ max_amount?: number;
+ is_open?: boolean;
+ search?: string;
+ page?: number;
+ limit?: number;
+ sortBy?: string;
+ sortOrder?: 'ASC' | 'DESC';
+}
+
+export interface PaginatedOpportunities {
+ data: Opportunity[];
+ total: number;
+ page: number;
+ limit: number;
+ totalPages: number;
+}
+
+export interface PipelineView {
+ stage: OpportunityStage;
+ stageName: string;
+ opportunities: Opportunity[];
+ count: number;
+ totalAmount: number;
+}
+
+export interface OpportunityStats {
+ total: number;
+ open: number;
+ won: number;
+ lost: number;
+ totalValue: number;
+ wonValue: number;
+ avgDealSize: number;
+ winRate: number;
+}
+
+export interface ForecastData {
+ totalPipeline: number;
+ weightedPipeline: number;
+ expectedRevenue: number;
+ byMonth: Array<{ month: string; amount: number; weighted: number }>;
+}
+
+export const opportunitiesApi = {
+ list: async (params?: OpportunityFilters): Promise => {
+ const response = await api.get('/sales/opportunities', { params });
+ return response.data;
+ },
+
+ get: async (id: string): Promise => {
+ const response = await api.get(`/sales/opportunities/${id}`);
+ return response.data;
+ },
+
+ create: async (data: CreateOpportunityDto): Promise => {
+ const response = await api.post('/sales/opportunities', data);
+ return response.data;
+ },
+
+ update: async (id: string, data: UpdateOpportunityDto): Promise => {
+ const response = await api.patch(`/sales/opportunities/${id}`, data);
+ return response.data;
+ },
+
+ delete: async (id: string): Promise => {
+ await api.delete(`/sales/opportunities/${id}`);
+ },
+
+ move: async (id: string, data: MoveOpportunityDto): Promise => {
+ const response = await api.post(`/sales/opportunities/${id}/move`, data);
+ return response.data;
+ },
+
+ markAsWon: async (id: string, notes?: string): Promise => {
+ const response = await api.post(`/sales/opportunities/${id}/won`, { notes });
+ return response.data;
+ },
+
+ markAsLost: async (id: string, reason?: string): Promise => {
+ const response = await api.post(`/sales/opportunities/${id}/lost`, { reason });
+ return response.data;
+ },
+
+ getByStage: async (): Promise => {
+ const response = await api.get('/sales/opportunities/pipeline');
+ return response.data;
+ },
+
+ getStats: async (): Promise => {
+ const response = await api.get('/sales/opportunities/stats');
+ return response.data;
+ },
+
+ getForecast: async (startDate: string, endDate: string): Promise => {
+ const response = await api.get('/sales/opportunities/forecast', {
+ params: { start_date: startDate, end_date: endDate },
+ });
+ return response.data;
+ },
+};
diff --git a/src/services/sales/pipeline.api.ts b/src/services/sales/pipeline.api.ts
new file mode 100644
index 0000000..e5f6c85
--- /dev/null
+++ b/src/services/sales/pipeline.api.ts
@@ -0,0 +1,67 @@
+import api from '../api';
+
+// Types
+export interface PipelineStage {
+ id: string;
+ tenant_id: string;
+ name: string;
+ position: number;
+ color: string;
+ is_won: boolean;
+ is_lost: boolean;
+ is_active: boolean;
+ created_at: string;
+ updated_at: string;
+ opportunityCount?: number;
+ totalAmount?: number;
+}
+
+export interface CreatePipelineStageDto {
+ name: string;
+ position?: number;
+ color?: string;
+ is_won?: boolean;
+ is_lost?: boolean;
+}
+
+export interface UpdatePipelineStageDto {
+ name?: string;
+ color?: string;
+ is_active?: boolean;
+}
+
+export const pipelineApi = {
+ getStages: async (): Promise => {
+ const response = await api.get('/sales/pipeline/stages');
+ return response.data;
+ },
+
+ getStage: async (id: string): Promise => {
+ const response = await api.get(`/sales/pipeline/stages/${id}`);
+ return response.data;
+ },
+
+ createStage: async (data: CreatePipelineStageDto): Promise => {
+ const response = await api.post('/sales/pipeline/stages', data);
+ return response.data;
+ },
+
+ updateStage: async (id: string, data: UpdatePipelineStageDto): Promise => {
+ const response = await api.patch(`/sales/pipeline/stages/${id}`, data);
+ return response.data;
+ },
+
+ deleteStage: async (id: string): Promise => {
+ await api.delete(`/sales/pipeline/stages/${id}`);
+ },
+
+ reorderStages: async (stageIds: string[]): Promise => {
+ const response = await api.post('/sales/pipeline/reorder', { stage_ids: stageIds });
+ return response.data;
+ },
+
+ initializeDefaults: async (): Promise => {
+ const response = await api.post('/sales/pipeline/initialize');
+ return response.data;
+ },
+};