diff --git a/src/hooks/index.ts b/src/hooks/index.ts index fb19021..25cf94f 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -14,3 +14,4 @@ export * from './useAnalytics'; export * from './useMfa'; export * from './useWhatsApp'; export * from './usePortfolio'; +export * from './useGoals'; diff --git a/src/hooks/useGoals.ts b/src/hooks/useGoals.ts new file mode 100644 index 0000000..6f6e895 --- /dev/null +++ b/src/hooks/useGoals.ts @@ -0,0 +1,242 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { + definitionsApi, + assignmentsApi, + type DefinitionFilters, + type CreateDefinitionDto, + type UpdateDefinitionDto, + type GoalStatus, + type AssignmentFilters, + type CreateAssignmentDto, + type UpdateAssignmentDto, + type AssignmentStatus, + type UpdateProgressDto, +} from '@/services/goals'; + +// ============================================ +// Goal Definitions Hooks +// ============================================ + +export function useGoalDefinitions(filters?: DefinitionFilters) { + return useQuery({ + queryKey: ['goals', 'definitions', filters], + queryFn: () => definitionsApi.list(filters), + }); +} + +export function useGoalDefinition(id: string) { + return useQuery({ + queryKey: ['goals', 'definitions', id], + queryFn: () => definitionsApi.get(id), + enabled: !!id, + }); +} + +export function useCreateGoalDefinition() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateDefinitionDto) => definitionsApi.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['goals', 'definitions'] }); + }, + }); +} + +export function useUpdateGoalDefinition() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateDefinitionDto }) => + definitionsApi.update(id, data), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['goals', 'definitions'] }); + queryClient.invalidateQueries({ queryKey: ['goals', 'definitions', id] }); + }, + }); +} + +export function useUpdateGoalStatus() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, status }: { id: string; status: GoalStatus }) => + definitionsApi.updateStatus(id, { status }), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['goals', 'definitions'] }); + queryClient.invalidateQueries({ queryKey: ['goals', 'definitions', id] }); + }, + }); +} + +export function useActivateGoal() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => definitionsApi.activate(id), + onSuccess: (_, id) => { + queryClient.invalidateQueries({ queryKey: ['goals', 'definitions'] }); + queryClient.invalidateQueries({ queryKey: ['goals', 'definitions', id] }); + }, + }); +} + +export function useDuplicateGoalDefinition() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => definitionsApi.duplicate(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['goals', 'definitions'] }); + }, + }); +} + +export function useDeleteGoalDefinition() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => definitionsApi.delete(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['goals', 'definitions'] }); + }, + }); +} + +// ============================================ +// Goal Assignments Hooks +// ============================================ + +export function useGoalAssignments(filters?: AssignmentFilters) { + return useQuery({ + queryKey: ['goals', 'assignments', filters], + queryFn: () => assignmentsApi.list(filters), + }); +} + +export function useGoalAssignmentsByGoal(goalId: string) { + return useQuery({ + queryKey: ['goals', 'definitions', goalId, 'assignments'], + queryFn: () => assignmentsApi.listByGoal(goalId), + enabled: !!goalId, + }); +} + +export function useGoalAssignment(id: string) { + return useQuery({ + queryKey: ['goals', 'assignments', id], + queryFn: () => assignmentsApi.get(id), + enabled: !!id, + }); +} + +export function useCreateGoalAssignment() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateAssignmentDto) => assignmentsApi.create(data), + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey: ['goals', 'assignments'] }); + queryClient.invalidateQueries({ queryKey: ['goals', 'definitions', result.definitionId, 'assignments'] }); + queryClient.invalidateQueries({ queryKey: ['goals', 'my'] }); + }, + }); +} + +export function useUpdateGoalAssignment() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateAssignmentDto }) => + assignmentsApi.update(id, data), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['goals', 'assignments'] }); + queryClient.invalidateQueries({ queryKey: ['goals', 'assignments', id] }); + }, + }); +} + +export function useUpdateAssignmentStatus() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, status }: { id: string; status: AssignmentStatus }) => + assignmentsApi.updateStatus(id, { status }), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['goals', 'assignments'] }); + queryClient.invalidateQueries({ queryKey: ['goals', 'assignments', id] }); + queryClient.invalidateQueries({ queryKey: ['goals', 'my'] }); + }, + }); +} + +export function useUpdateGoalProgress() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateProgressDto }) => + assignmentsApi.updateProgress(id, data), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['goals', 'assignments'] }); + queryClient.invalidateQueries({ queryKey: ['goals', 'assignments', id] }); + queryClient.invalidateQueries({ queryKey: ['goals', 'my'] }); + }, + }); +} + +export function useGoalProgressHistory(assignmentId: string) { + return useQuery({ + queryKey: ['goals', 'assignments', assignmentId, 'history'], + queryFn: () => assignmentsApi.getHistory(assignmentId), + enabled: !!assignmentId, + }); +} + +export function useDeleteGoalAssignment() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => assignmentsApi.delete(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['goals', 'assignments'] }); + queryClient.invalidateQueries({ queryKey: ['goals', 'my'] }); + }, + }); +} + +// ============================================ +// My Goals Hooks +// ============================================ + +export function useMyGoals() { + return useQuery({ + queryKey: ['goals', 'my'], + queryFn: () => assignmentsApi.getMyGoals(), + }); +} + +export function useMyGoalsSummary() { + return useQuery({ + queryKey: ['goals', 'my', 'summary'], + queryFn: () => assignmentsApi.getMyGoalsSummary(), + }); +} + +export function useUpdateMyGoalProgress() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateProgressDto }) => + assignmentsApi.updateMyProgress(id, data), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['goals', 'my'] }); + queryClient.invalidateQueries({ queryKey: ['goals', 'assignments', id] }); + }, + }); +} + +// ============================================ +// Reports Hooks +// ============================================ + +export function useGoalCompletionReport(startDate?: string, endDate?: string) { + return useQuery({ + queryKey: ['goals', 'reports', 'completion', startDate, endDate], + queryFn: () => assignmentsApi.getCompletionReport(startDate, endDate), + }); +} + +export function useGoalUserReport() { + return useQuery({ + queryKey: ['goals', 'reports', 'by-user'], + queryFn: () => assignmentsApi.getUserReport(), + }); +} diff --git a/src/services/goals/assignments.api.ts b/src/services/goals/assignments.api.ts new file mode 100644 index 0000000..b7cdcf3 --- /dev/null +++ b/src/services/goals/assignments.api.ts @@ -0,0 +1,205 @@ +import api from '../api'; + +// ───────────────────────────────────────────── +// Types +// ───────────────────────────────────────────── + +export type AssigneeType = 'user' | 'team' | 'tenant'; +export type AssignmentStatus = 'active' | 'achieved' | 'failed' | 'cancelled'; +export type ProgressSource = 'manual' | 'automatic' | 'import' | 'api'; + +export interface Assignment { + id: string; + tenantId: string; + definitionId: string; + assigneeType: AssigneeType; + userId: string | null; + teamId: string | null; + customTarget: number | null; + currentValue: number; + progressPercentage: number; + lastUpdatedAt: string | null; + status: AssignmentStatus; + achievedAt: string | null; + notes: string | null; + createdAt: string; + updatedAt: string; + definition?: { + id: string; + name: string; + targetValue: number; + unit: string | null; + startsAt: string; + endsAt: string; + }; + user?: { + id: string; + email: string; + firstName: string | null; + lastName: string | null; + }; +} + +export interface CreateAssignmentDto { + definitionId: string; + assigneeType?: AssigneeType; + userId?: string; + teamId?: string; + customTarget?: number; + notes?: string; +} + +export type UpdateAssignmentDto = Partial>; + +export interface AssignmentStatusDto { + status: AssignmentStatus; +} + +export interface UpdateProgressDto { + value: number; + source?: ProgressSource; + sourceReference?: string; + notes?: string; +} + +export interface AssignmentFilters { + definitionId?: string; + userId?: string; + status?: AssignmentStatus; + assigneeType?: AssigneeType; + minProgress?: number; + maxProgress?: number; + sortBy?: string; + sortOrder?: 'ASC' | 'DESC'; + page?: number; + limit?: number; +} + +export interface PaginatedAssignments { + items: Assignment[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + +export interface ProgressLog { + id: string; + assignmentId: string; + previousValue: number | null; + newValue: number; + changeAmount: number | null; + source: ProgressSource; + sourceReference: string | null; + notes: string | null; + loggedAt: string; + loggedBy: string | null; +} + +export interface MyGoalsSummary { + totalAssignments: number; + activeAssignments: number; + achievedAssignments: number; + failedAssignments: number; + averageProgress: number; + atRiskCount: number; +} + +export interface CompletionReport { + totalGoals: number; + achievedGoals: number; + failedGoals: number; + activeGoals: number; + completionRate: number; + averageProgress: number; +} + +export interface UserReport { + userId: string; + userName: string | null; + totalAssignments: number; + achieved: number; + failed: number; + active: number; + averageProgress: number; +} + +// ───────────────────────────────────────────── +// API Service +// ───────────────────────────────────────────── + +export const assignmentsApi = { + // Assignments CRUD + list: async (params?: AssignmentFilters): Promise => { + const response = await api.get('/goals/assignments', { params }); + return response.data; + }, + + listByGoal: async (goalId: string): Promise => { + const response = await api.get(`/goals/${goalId}/assignments`); + return response.data; + }, + + get: async (id: string): Promise => { + const response = await api.get(`/goals/assignments/${id}`); + return response.data; + }, + + create: async (data: CreateAssignmentDto): Promise => { + const response = await api.post('/goals/assignments', data); + return response.data; + }, + + update: async (id: string, data: UpdateAssignmentDto): Promise => { + const response = await api.patch(`/goals/assignments/${id}`, data); + return response.data; + }, + + updateStatus: async (id: string, data: AssignmentStatusDto): Promise => { + const response = await api.patch(`/goals/assignments/${id}/status`, data); + return response.data; + }, + + updateProgress: async (id: string, data: UpdateProgressDto): Promise => { + const response = await api.post(`/goals/assignments/${id}/progress`, data); + return response.data; + }, + + getHistory: async (id: string): Promise => { + const response = await api.get(`/goals/assignments/${id}/history`); + return response.data; + }, + + delete: async (id: string): Promise => { + await api.delete(`/goals/assignments/${id}`); + }, + + // My Goals + getMyGoals: async (): Promise => { + const response = await api.get('/goals/my'); + return response.data; + }, + + getMyGoalsSummary: async (): Promise => { + const response = await api.get('/goals/my/summary'); + return response.data; + }, + + updateMyProgress: async (id: string, data: UpdateProgressDto): Promise => { + const response = await api.post(`/goals/my/${id}/update`, data); + return response.data; + }, + + // Reports + getCompletionReport: async (startDate?: string, endDate?: string): Promise => { + const response = await api.get('/goals/reports/completion', { + params: { startDate, endDate }, + }); + return response.data; + }, + + getUserReport: async (): Promise => { + const response = await api.get('/goals/reports/by-user'); + return response.data; + }, +}; diff --git a/src/services/goals/definitions.api.ts b/src/services/goals/definitions.api.ts new file mode 100644 index 0000000..18fa457 --- /dev/null +++ b/src/services/goals/definitions.api.ts @@ -0,0 +1,137 @@ +import api from '../api'; + +// ───────────────────────────────────────────── +// Types +// ───────────────────────────────────────────── + +export type GoalType = 'target' | 'limit' | 'maintain'; +export type MetricType = 'number' | 'currency' | 'percentage' | 'boolean' | 'count'; +export type PeriodType = 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly' | 'custom'; +export type DataSource = 'manual' | 'sales' | 'billing' | 'commissions' | 'custom'; +export type GoalStatus = 'draft' | 'active' | 'paused' | 'completed' | 'cancelled'; + +export interface SourceConfig { + module?: string; + entity?: string; + filter?: Record; + aggregation?: 'sum' | 'count' | 'avg'; + field?: string; +} + +export interface Milestone { + percentage: number; + notify: boolean; +} + +export interface GoalDefinition { + id: string; + tenantId: string; + name: string; + description: string | null; + category: string | null; + type: GoalType; + metric: MetricType; + targetValue: number; + unit: string | null; + period: PeriodType; + startsAt: string; + endsAt: string; + source: DataSource; + sourceConfig: SourceConfig; + milestones: Milestone[]; + status: GoalStatus; + tags: string[]; + createdAt: string; + updatedAt: string; + createdBy: string | null; + assignmentCount?: number; +} + +export interface CreateDefinitionDto { + name: string; + description?: string; + category?: string; + type?: GoalType; + metric?: MetricType; + targetValue: number; + unit?: string; + period?: PeriodType; + startsAt: string; + endsAt: string; + source?: DataSource; + sourceConfig?: SourceConfig; + milestones?: Milestone[]; + status?: GoalStatus; + tags?: string[]; +} + +export type UpdateDefinitionDto = Partial; + +export interface DefinitionStatusDto { + status: GoalStatus; +} + +export interface DefinitionFilters { + status?: GoalStatus; + period?: PeriodType; + category?: string; + search?: string; + activeOn?: string; + sortBy?: string; + sortOrder?: 'ASC' | 'DESC'; + page?: number; + limit?: number; +} + +export interface PaginatedDefinitions { + items: GoalDefinition[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + +// ───────────────────────────────────────────── +// API Service +// ───────────────────────────────────────────── + +export const definitionsApi = { + list: async (params?: DefinitionFilters): Promise => { + const response = await api.get('/goals', { params }); + return response.data; + }, + + get: async (id: string): Promise => { + const response = await api.get(`/goals/${id}`); + return response.data; + }, + + create: async (data: CreateDefinitionDto): Promise => { + const response = await api.post('/goals', data); + return response.data; + }, + + update: async (id: string, data: UpdateDefinitionDto): Promise => { + const response = await api.patch(`/goals/${id}`, data); + return response.data; + }, + + updateStatus: async (id: string, data: DefinitionStatusDto): Promise => { + const response = await api.patch(`/goals/${id}/status`, data); + return response.data; + }, + + activate: async (id: string): Promise => { + const response = await api.post(`/goals/${id}/activate`); + return response.data; + }, + + duplicate: async (id: string): Promise => { + const response = await api.post(`/goals/${id}/duplicate`); + return response.data; + }, + + delete: async (id: string): Promise => { + await api.delete(`/goals/${id}`); + }, +}; diff --git a/src/services/goals/index.ts b/src/services/goals/index.ts new file mode 100644 index 0000000..3ba8efb --- /dev/null +++ b/src/services/goals/index.ts @@ -0,0 +1,2 @@ +export * from './definitions.api'; +export * from './assignments.api';