[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:
Adrian Flores Cortes 2026-01-25 06:26:47 -06:00
parent a4253a8ce9
commit 69a6a2c4ea
5 changed files with 587 additions and 0 deletions

View File

@ -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
View 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(),
});
}

View 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;
},
};

View 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}`);
},
};

View File

@ -0,0 +1,2 @@
export * from './definitions.api';
export * from './assignments.api';