[SAAS-022] feat: Implement Goals module frontend
- Added API services for definitions and assignments - Added 24 React Query hooks in useGoals.ts - Updated hooks/index.ts to export useGoals Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a4253a8ce9
commit
69a6a2c4ea
@ -14,3 +14,4 @@ export * from './useAnalytics';
|
||||
export * from './useMfa';
|
||||
export * from './useWhatsApp';
|
||||
export * from './usePortfolio';
|
||||
export * from './useGoals';
|
||||
|
||||
242
src/hooks/useGoals.ts
Normal file
242
src/hooks/useGoals.ts
Normal file
@ -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(),
|
||||
});
|
||||
}
|
||||
205
src/services/goals/assignments.api.ts
Normal file
205
src/services/goals/assignments.api.ts
Normal file
@ -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<Omit<CreateAssignmentDto, 'definitionId'>>;
|
||||
|
||||
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<PaginatedAssignments> => {
|
||||
const response = await api.get<PaginatedAssignments>('/goals/assignments', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
listByGoal: async (goalId: string): Promise<Assignment[]> => {
|
||||
const response = await api.get<Assignment[]>(`/goals/${goalId}/assignments`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
get: async (id: string): Promise<Assignment> => {
|
||||
const response = await api.get<Assignment>(`/goals/assignments/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: CreateAssignmentDto): Promise<Assignment> => {
|
||||
const response = await api.post<Assignment>('/goals/assignments', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: UpdateAssignmentDto): Promise<Assignment> => {
|
||||
const response = await api.patch<Assignment>(`/goals/assignments/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateStatus: async (id: string, data: AssignmentStatusDto): Promise<Assignment> => {
|
||||
const response = await api.patch<Assignment>(`/goals/assignments/${id}/status`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateProgress: async (id: string, data: UpdateProgressDto): Promise<Assignment> => {
|
||||
const response = await api.post<Assignment>(`/goals/assignments/${id}/progress`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getHistory: async (id: string): Promise<ProgressLog[]> => {
|
||||
const response = await api.get<ProgressLog[]>(`/goals/assignments/${id}/history`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`/goals/assignments/${id}`);
|
||||
},
|
||||
|
||||
// My Goals
|
||||
getMyGoals: async (): Promise<Assignment[]> => {
|
||||
const response = await api.get<Assignment[]>('/goals/my');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getMyGoalsSummary: async (): Promise<MyGoalsSummary> => {
|
||||
const response = await api.get<MyGoalsSummary>('/goals/my/summary');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateMyProgress: async (id: string, data: UpdateProgressDto): Promise<Assignment> => {
|
||||
const response = await api.post<Assignment>(`/goals/my/${id}/update`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Reports
|
||||
getCompletionReport: async (startDate?: string, endDate?: string): Promise<CompletionReport> => {
|
||||
const response = await api.get<CompletionReport>('/goals/reports/completion', {
|
||||
params: { startDate, endDate },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getUserReport: async (): Promise<UserReport[]> => {
|
||||
const response = await api.get<UserReport[]>('/goals/reports/by-user');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
137
src/services/goals/definitions.api.ts
Normal file
137
src/services/goals/definitions.api.ts
Normal file
@ -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<string, unknown>;
|
||||
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<CreateDefinitionDto>;
|
||||
|
||||
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<PaginatedDefinitions> => {
|
||||
const response = await api.get<PaginatedDefinitions>('/goals', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
get: async (id: string): Promise<GoalDefinition> => {
|
||||
const response = await api.get<GoalDefinition>(`/goals/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: CreateDefinitionDto): Promise<GoalDefinition> => {
|
||||
const response = await api.post<GoalDefinition>('/goals', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: UpdateDefinitionDto): Promise<GoalDefinition> => {
|
||||
const response = await api.patch<GoalDefinition>(`/goals/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateStatus: async (id: string, data: DefinitionStatusDto): Promise<GoalDefinition> => {
|
||||
const response = await api.patch<GoalDefinition>(`/goals/${id}/status`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
activate: async (id: string): Promise<GoalDefinition> => {
|
||||
const response = await api.post<GoalDefinition>(`/goals/${id}/activate`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
duplicate: async (id: string): Promise<GoalDefinition> => {
|
||||
const response = await api.post<GoalDefinition>(`/goals/${id}/duplicate`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await api.delete(`/goals/${id}`);
|
||||
},
|
||||
};
|
||||
2
src/services/goals/index.ts
Normal file
2
src/services/goals/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './definitions.api';
|
||||
export * from './assignments.api';
|
||||
Loading…
Reference in New Issue
Block a user