diff --git a/backend/src/routes/dashboard.routes.ts b/backend/src/routes/dashboard.routes.ts index 8b601e2..20fb636 100644 --- a/backend/src/routes/dashboard.routes.ts +++ b/backend/src/routes/dashboard.routes.ts @@ -99,10 +99,10 @@ router.get('/cost-analysis', authMiddleware, async (req: AuthRequest, res: Respo } }) -// 合规健康度评分 + AI 建议卡片流 +// 合规健康度评分 — 已统一为 getHealthCheck 口径 router.get('/compliance-score', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => { try { - const data = await getComplianceScore(req.user!.orgId) + const data = await getHealthCheck(req.user!.orgId) res.json({ success: true, data }) } catch (err) { next(err) diff --git a/backend/src/routes/employee.routes.ts b/backend/src/routes/employee.routes.ts index 95166a1..ef9d770 100644 --- a/backend/src/routes/employee.routes.ts +++ b/backend/src/routes/employee.routes.ts @@ -37,14 +37,49 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => { }) /** - * 轻量级全量员工列表(不分页,仅返回 id/name/department/gender/status) + * 轻量级全量员工列表(不分页,仅返回 id/name/department/position/gender/status) * 用于特殊状态台账、发薪批次等需要选择全部员工的场景 + * 支持 status 筛选参数,默认返回 ACTIVE */ router.get('/all-lite', authMiddleware, async (req: AuthRequest, res, next) => { try { + const status = req.query.status + ? String(req.query.status).split(',') as any[] + : ['ACTIVE', 'RESIGNED'] as any[] + const department = req.query.department as string | undefined const employees = await prisma.employee.findMany({ - where: { orgId: req.user!.orgId, status: { in: ['ACTIVE', 'RESIGNED'] } }, - select: { id: true, name: true, department: true, gender: true, status: true }, + where: { + orgId: req.user!.orgId, + status: { in: status }, + ...(department && { department }), + }, + select: { id: true, name: true, department: true, position: true, gender: true, status: true }, + orderBy: { name: 'asc' }, + }) + res.json({ success: true, data: employees }) + } catch (err) { + next(err) + } +}) + +/** + * 标准员工列表(不分页,供各页面下拉选择、引用等场景) + * 与 /roster 分离,/roster 专用于花名册分页列表 + * 支持 status、department 筛选 + */ +router.get('/list', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const status = req.query.status + ? String(req.query.status).split(',') as any[] + : ['ACTIVE'] as any[] + const department = req.query.department as string | undefined + const employees = await prisma.employee.findMany({ + where: { + orgId: req.user!.orgId, + status: { in: status }, + ...(department && { department }), + }, + select: { id: true, name: true, department: true, position: true, phone: true, status: true }, orderBy: { name: 'asc' }, }) res.json({ success: true, data: employees }) diff --git a/docs/api-consistency-refactor.md b/docs/api-consistency-refactor.md new file mode 100644 index 0000000..3b4bbae --- /dev/null +++ b/docs/api-consistency-refactor.md @@ -0,0 +1,235 @@ +# TurboHR API 口径统一重构方案 + +> 2026-08-01 · 全量梳理 + 分步执行 + +## 一、问题总览 + +### 1.1 后端统计函数重复(口径不一致) + +| 函数 | 端点 | 维度 | 问题 | +|------|------|------|------| +| `getDashboardData` | `/dashboard` | todos 去重 | 基础数据源,其他应复用 | +| `getComplianceScore` | `/dashboard/compliance-score` | 5 维度(旧) | 与评分标准说明不一致,应废弃 | +| `getHealthCheck` | `/dashboard/health-check` | 6 维度(新) | 与评分标准说明一致 | + +**重复查询的指标**:`unsignedContracts`、`expiringContracts`、`policiesWithoutPublish`、`socialConfig`、`housingConfig`、`totalEmployees` — 三个函数各自独立查数据库,口径不完全一致。 + +### 1.2 `/roster` API 被滥用为通用员工列表 + +| 页面 | 调用方式 | 用途 | 问题 | +|------|---------|------|------| +| `Roster.tsx` | `/roster` + 分页参数 | 花名册列表 + `globalRiskStats` | ✓ 正确用法 | +| `Contracts.tsx` | `/roster` + contractStatus | 合同管理列表 | ❌ 应有专用合同 API | +| `Attendance.tsx` | `/roster?pageSize=200` | 考勤页选员工 | ❌ pageSize 硬编码 | +| `AIAssistant.tsx` | `/roster?pageSize=999` | AI 对话引用员工 | ❌ pageSize 硬编码 | +| `Compensation.tsx` | `/roster` | 薪酬管理选员工 | ❌ 无分页参数 | +| `Termination.tsx` | `/roster` | 解聘选员工 | ❌ 无分页参数 | +| `Money.tsx` | `/roster` + status=ACTIVE | 薪酬看板 | ❌ 无分页参数 | +| `PortalQRModal.tsx` | `/roster?pageSize=999` | 二维码模态框 | ❌ pageSize 硬编码 | + +### 1.3 前端响应解析不统一 + +`/roster` 返回 `{ success, data, pagination, globalRiskStats }`,但各页面解析方式不同: + +| 页面 | 解析方式 | +|------|---------| +| `Roster.tsx` | `res`(完整响应) | +| `AIAssistant.tsx` | `res.data?.items \|\| res.data \|\| []` | +| `Attendance.tsx` | `res.data` | +| `Compensation.tsx` | `res.data` | +| `Money.tsx` | `res.data \|\| []` | +| `PortalQRModal.tsx` | `res.data?.data \|\| res.data \|\| []` | + +### 1.4 后端路由职责重叠 + +| 路由 | 职责 | 重叠 | +|------|------|------| +| `roster.routes.ts` | 花名册 CRUD + 合同状态 + 部门列表 + 风险统计 | 合同管理、员工列表 | +| `employee.routes.ts` | 员工 CRUD | 与 roster 的员工创建/编辑重叠 | +| `dashboard.routes.ts` | 仪表盘 + 风险中心 + 合规评分 + 体检诊断 + 工作台 | 统计逻辑分散 | + +--- + +## 二、重构方案 + +### Phase 1: 后端统计函数统一(低风险) + +**目标**:废弃 `getComplianceScore`,Dashboard 合规评分改用 `getHealthCheck`。 + +#### 1.1 废弃 `getComplianceScore` + +- `/dashboard/compliance-score` 端点改为调用 `getHealthCheck`,返回相同结构 +- 前端 `Dashboard.tsx` 的 `complianceScore` 查询改为使用 `health-check` 数据 +- 保留 `getComplianceScore` 函数体但标记 `@deprecated`,后续删除 + +#### 1.2 提取共享统计基础函数 + +```typescript +// backend/src/services/stats.service.ts +/** 获取组织级基础统计数据(所有统计函数共享) */ +export async function getOrgBaseStats(orgId: string) { + const now = new Date() + const [ + totalEmployees, + unsignedEmployees, // 无合同员工 + expiringContracts, // 30天内到期 + policiesWithoutPublish, // 未公示制度 + totalPolicies, + socialConfig, + housingConfig, + employeesNoSocial, // 社保异常(按员工去重) + overtimeExcessive, // 超时加班 + unconfirmedPayslips, + totalPayslips, + totalTerminations, + completedTerminations, + terminationsWithChecklist, + disciplinaryRecords, + attendanceRecords, + trainingRecords, + ] = await Promise.all([...]) + return { totalEmployees, unsignedEmployees, expiringContracts, ... } +} +``` + +- `getDashboardData`、`getHealthCheck` 均调用 `getOrgBaseStats`,确保口径一致 + +### Phase 2: 新建员工列表专用 API(中风险) + +**目标**:`/roster` 专用于花名册分页列表,新建 `/employees/list` 供其他页面获取不分页员工列表。 + +#### 2.1 新增 `/employees/list` 端点 + +```typescript +// employee.routes.ts +// 获取员工列表(不分页,供下拉选择、引用等场景) +// 支持 status、department 筛选 +router.get('/list', authMiddleware, async (req, res) => { + const { status, department } = req.query + const employees = await prisma.employee.findMany({ + where: { orgId: req.user!.orgId, ...(status && { status: String(status) }) }, + select: { id: true, name: true, department: true, position: true, status: true }, + orderBy: { name: 'asc' }, + }) + res.json({ success: true, data: employees }) +}) +``` + +#### 2.2 前端迁移 + +| 页面 | 原调用 | 新调用 | +|------|--------|--------| +| `Attendance.tsx` | `/roster?pageSize=200` | `/employees/list?status=ACTIVE` | +| `AIAssistant.tsx` | `/roster?pageSize=999` | `/employees/list?status=ACTIVE` | +| `Compensation.tsx` | `/roster` | `/employees/list?status=ACTIVE` | +| `Termination.tsx` | `/roster` | `/employees/list?status=ACTIVE` | +| `Money.tsx` | `/roster?status=ACTIVE` | `/employees/list?status=ACTIVE` | +| `PortalQRModal.tsx` | `/roster?pageSize=999` | `/employees/list?status=ACTIVE` | + +### Phase 3: 前端 API 层统一封装(低风险) + +**目标**:建立类型安全的 API 调用层,统一响应解析。 + +#### 3.1 创建 API 服务模块 + +```typescript +// frontend/src/lib/api-services.ts + +// 统一响应解析 +function unwrap(res: any): T { + return res.data?.data ?? res.data ?? res +} + +export const rosterApi = { + list: (params: RosterParams) => api.get('/roster', { params }).then(unwrap), + departments: () => api.get('/roster/departments').then(unwrap()), + contractTypes: () => api.get('/roster/contract-types').then(unwrap()), +} + +export const employeeApi = { + list: (params?: { status?: string; department?: string }) => + api.get('/employees/list', { params }).then(unwrap()), + profile: (id: string) => api.get(`/roster/${id}/profile`).then(unwrap()), +} + +export const dashboardApi = { + data: () => api.get('/dashboard').then(unwrap()), + healthCheck: () => api.get('/dashboard/health-check').then(unwrap()), + risks: () => api.get('/dashboard/risks').then(unwrap()), + calendar: (month: string) => api.get(`/dashboard/calendar?month=${month}`).then(unwrap()), +} +``` + +#### 3.2 各页面迁移调用 + +逐步将各页面从 `api.get('/xxx')` 改为 `xxxApi.method()`,确保响应解析统一。 + +### Phase 4: 后端路由职责清理(中风险) + +**目标**:消除路由间职责重叠。 + +| 调整 | 说明 | +|------|------| +| `roster.routes.ts` 移除员工 CRUD | 员工创建/编辑/删除统一走 `employee.routes.ts` | +| `Contracts.tsx` 改用 `/employees` + 合同子资源 | 合同管理不再复用花名册列表 | +| `dashboard.routes.ts` 统计逻辑提取到 service | 路由层只做参数校验和响应封装 | + +--- + +## 三、执行优先级 + +| 优先级 | Phase | 风险 | 预计改动 | +|--------|-------|------|---------| +| P0 | Phase 1.1 废弃 getComplianceScore | 低 | 后端 2 文件 + 前端 1 文件 | +| P1 | Phase 2 新建 /employees/list | 中 | 后端 1 文件 + 前端 6 文件 | +| P2 | Phase 1.2 提取共享统计函数 | 低 | 后端 1 新文件 + 2 改动 | +| P3 | Phase 3 前端 API 服务层 | 低 | 前端 1 新文件 + 逐步迁移 | +| P4 | Phase 4 路由职责清理 | 中 | 后端多文件重构 | + +--- + +## 四、统一响应规范 + +### 4.1 所有 API 响应格式 + +```typescript +// 列表类(分页) +{ + success: true, + data: T[], + pagination: { page, pageSize, total, totalPages }, + // 可选附加字段 + globalRiskStats?: { expiring, expired, unsigned } +} + +// 列表类(不分页) +{ + success: true, + data: T[] +} + +// 单对象 +{ + success: true, + data: T +} + +// 错误 +{ + success: false, + error: { code, message, trace_id } +} +``` + +### 4.2 前端统一解析 + +所有 API 调用通过 `unwrap()` 解析,取 `data` 字段,不再各页面自行判断 `res.data?.items || res.data || []`。 + +--- + +## 五、验收标准 + +1. `/dashboard`、`/dashboard/compliance-score`、`/dashboard/health-check` 三个端点的统计数据口径完全一致 +2. `/roster` 仅用于花名册分页列表,其他页面获取员工列表统一用 `/employees/list` +3. 前端所有 API 调用通过 `lib/api-services.ts` 统一封装,无裸 `api.get()` 调用 +4. 所有 API 响应遵循统一格式,前端解析统一 diff --git a/frontend/src/components/PortalQRModal.tsx b/frontend/src/components/PortalQRModal.tsx index bf2bf4d..eea1d11 100644 --- a/frontend/src/components/PortalQRModal.tsx +++ b/frontend/src/components/PortalQRModal.tsx @@ -7,6 +7,7 @@ import { QRCodeSVG } from 'qrcode.react' import { Smartphone, Copy, Check, Search, Loader2, User, ChevronRight } from 'lucide-react' import Modal from './ui/Modal' import api from '../lib/api' +import { employeeApi } from '../lib/api-services' interface Employee { id: string @@ -30,8 +31,7 @@ export default function PortalQRModal({ open, onClose }: { open: boolean; onClos useEffect(() => { if (!open) return setLoading(true) - api.get('/roster', { params: { pageSize: 999 } }).then((res: any) => { - const list = res.data?.data || res.data || [] + employeeApi.list({ status: 'ACTIVE' }).then((list: any) => { setEmployees(list.map((e: any) => ({ id: e.id, name: e.name, diff --git a/frontend/src/lib/api-services.ts b/frontend/src/lib/api-services.ts new file mode 100644 index 0000000..a5883b2 --- /dev/null +++ b/frontend/src/lib/api-services.ts @@ -0,0 +1,143 @@ +/** + * 统一 API 服务层 + * 所有页面应通过此模块调用 API,确保口径统一 + */ + +import api from './api' + +/** axios 拦截器已返回 response.data,但 TS 类型仍是 AxiosResponse,用 as any 绕过 */ +const get = ((url: string, config?: any) => api.get(url, config)) as any +const post = ((url: string, data?: any, config?: any) => api.post(url, data, config)) as any +const put = ((url: string, data?: any, config?: any) => api.put(url, data, config)) as any + +/** 统一响应解析:返回一个回调函数,用于 .then() 中取 data 字段 */ +function unwrap(): (res: any) => T { + return (res: any) => (res?.data ?? res) as T +} + +// ========== 员工相关 ========== + +export interface EmployeeOption { + id: string + name: string + department?: string + position?: string + phone?: string + gender?: string + status?: string +} + +export const employeeApi = { + /** 标准员工列表(不分页,供下拉选择等) */ + list: (params?: { status?: string; department?: string }) => + get('/employees/list', { params }).then(unwrap()), + /** 轻量级全量员工列表(含 RESIGNED) */ + allLite: (params?: { status?: string; department?: string }) => + get('/employees/all-lite', { params }).then(unwrap()), + /** 员工详情 */ + detail: (id: string) => + get(`/employees/${id}`).then(unwrap()), + /** 员工档案(花名册) */ + profile: (id: string) => + get(`/roster/${id}/profile`).then(unwrap()), +} + +// ========== 花名册相关 ========== + +export interface RosterParams { + page?: number + pageSize?: number + search?: string + status?: string + department?: string + contractStatus?: string +} + +export interface RosterResponse { + data: any[] + pagination: { page: number; pageSize: number; total: number; totalPages: number } + globalRiskStats?: { expiring: number; expired: number; unsigned: number } +} + +export const rosterApi = { + /** 花名册分页列表 */ + list: (params: RosterParams) => + get('/roster', { params }) as Promise, + /** 部门列表 */ + departments: () => + get('/roster/departments').then(unwrap()), + /** 合同类型 */ + contractTypes: () => + get('/roster/contract-types').then(unwrap()), + /** 即将到期合同 */ + expiringContracts: () => + get('/roster/contracts/expiring').then(unwrap()), + /** 违纪记录 */ + disciplinary: (employeeId: string) => + get(`/roster/${employeeId}/disciplinary`).then(unwrap()), + /** 证据链 */ + evidenceChain: (employeeId: string) => + get(`/roster/${employeeId}/evidence-chain`).then(unwrap()), +} + +// ========== 仪表盘相关 ========== + +export const dashboardApi = { + /** 仪表盘主数据 */ + data: () => + get('/dashboard').then(unwrap()), + /** 用工体检诊断(6 维度,与评分标准一致) */ + healthCheck: () => + get('/dashboard/health-check').then(unwrap()), + /** 体检诊断历史 */ + healthCheckHistory: () => + get('/dashboard/health-check/history').then(unwrap()), + /** 风险中心(与工作台同口径) */ + risks: () => + get('/dashboard/risks').then(unwrap()), + /** 月度日历 */ + calendar: (month: string) => + get(`/dashboard/calendar?month=${month}`).then(unwrap()), + /** 成本分析 */ + costAnalysis: (month: string) => + get(`/dashboard/cost-analysis?month=${month}`).then(unwrap()), + /** 人力信息总览 */ + workforceStats: () => + get('/dashboard/workforce-stats').then(unwrap()), + /** 下一步行动 */ + nextActions: () => + get('/dashboard/workspace/next-actions').then(unwrap()), + /** 入离职统计 */ + turnoverStats: (months = 12) => + get(`/dashboard/turnover-stats?months=${months}`).then(unwrap()), + /** 绩效统计 */ + performanceStats: (period: string) => + get(`/dashboard/performance-stats?period=${period}`).then(unwrap()), + /** 年度价值报告 */ + annualValue: (year: number) => + get(`/dashboard/annual-value?year=${year}`).then(unwrap()), + /** 年度价值报告历史 */ + annualValueHistory: () => + get('/dashboard/annual-value/history').then(unwrap()), +} + +// ========== 考勤相关 ========== + +export const attendanceApi = { + list: (params: { month: string; department?: string; status?: string }) => + get('/attendance', { params }).then(unwrap()), + stats: (month: string) => + get(`/attendance/stats?month=${month}`).then(unwrap()), + shifts: () => + get('/attendance/shifts').then(unwrap()), + shiftAssignments: (date: string) => + get(`/attendance/shift-assignments?date=${date}`).then(unwrap()), + daily: (date: string) => + get(`/attendance/daily?date=${date}`).then(unwrap()), + monthlyReport: (month: string) => + get(`/attendance/monthly-report?month=${month}`).then(unwrap()), + leaves: () => + get('/attendance/leaves').then(unwrap()), + publishRecords: () => + get('/attendance/publish-records').then(unwrap()), +} diff --git a/frontend/src/pages/AIAssistant.tsx b/frontend/src/pages/AIAssistant.tsx index 431e77d..8776bbb 100644 --- a/frontend/src/pages/AIAssistant.tsx +++ b/frontend/src/pages/AIAssistant.tsx @@ -8,6 +8,7 @@ import rehypeRaw from 'rehype-raw' import { Document, Packer, Paragraph, HeadingLevel, TextRun, Table, TableRow, TableCell, WidthType, BorderStyle, AlignmentType } from 'docx' import { saveAs } from 'file-saver' import api from '../lib/api' +import { employeeApi } from '../lib/api-services' import { useAuthStore } from '../store/authStore' import Card from '../components/ui/Card' import Button from '../components/ui/Button' @@ -633,11 +634,8 @@ function PredictTab() { const [disciplinarySummary, setDisciplinarySummary] = useState('') const { data: employees } = useQuery({ - queryKey: ['roster-list'], - queryFn: async () => { - const res = await api.get('/roster?pageSize=999') as any - return res.data?.items || res.data || [] - }, + queryKey: ['employee-list'], + queryFn: () => employeeApi.list({ status: 'ACTIVE' }), }) const departments = [...new Set((employees || []).map((e: any) => e.department).filter(Boolean))] @@ -1384,11 +1382,8 @@ function ReviewTab() { } const { data: employees } = useQuery({ - queryKey: ['roster-list'], - queryFn: async () => { - const res = await api.get('/roster') as any - return res.data?.items || res.data || [] - }, + queryKey: ['employee-list'], + queryFn: () => employeeApi.list({ status: 'ACTIVE' }), }) const handleReview = async () => { @@ -1580,11 +1575,8 @@ function CaseTab() { const { history, saveMutation, deleteMutation, loadHistory } = useAIHistory('case') const { data: employees } = useQuery({ - queryKey: ['roster-list'], - queryFn: async () => { - const res = await api.get('/roster') as any - return res.data?.items || res.data || [] - }, + queryKey: ['employee-list'], + queryFn: () => employeeApi.list({ status: 'ACTIVE' }), }) const handleMatch = async () => { diff --git a/frontend/src/pages/Attendance.tsx b/frontend/src/pages/Attendance.tsx index 13a6e95..f0e352b 100644 --- a/frontend/src/pages/Attendance.tsx +++ b/frontend/src/pages/Attendance.tsx @@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { CalendarCheck, CheckCircle, Clock, AlertCircle, Plus, Trash2, Calendar, Users, BarChart3, Plane, Upload, Download, X, Send, Loader2, CheckCheck } from 'lucide-react' import api from '../lib/api' +import { employeeApi, rosterApi } from '../lib/api-services' import { useAuthStore } from '../store/authStore' import Card from '../components/ui/Card' import Button from '../components/ui/Button' @@ -124,10 +125,7 @@ function ConfirmTab() { const { data: departmentList } = useQuery({ queryKey: ['roster-departments'], - queryFn: async () => { - const res = await api.get('/roster/departments') as any - return res.data || [] - }, + queryFn: () => rosterApi.departments(), }) const { data: publishRecords } = useQuery({ @@ -982,11 +980,8 @@ function LeavesTab() { }) const { data: rosterData } = useQuery({ - queryKey: ['roster-employees'], - queryFn: async () => { - const res = await api.get('/roster?pageSize=200') as any - return res.data - }, + queryKey: ['employee-list'], + queryFn: () => employeeApi.list({ status: 'ACTIVE' }), }) const createMutation = useMutation({ diff --git a/frontend/src/pages/Compensation.tsx b/frontend/src/pages/Compensation.tsx index 086e50f..259ff11 100644 --- a/frontend/src/pages/Compensation.tsx +++ b/frontend/src/pages/Compensation.tsx @@ -2,6 +2,7 @@ import { useState, useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import { Calculator, Info, AlertCircle } from 'lucide-react' import api from '../lib/api' +import { rosterApi } from '../lib/api-services' import Card from '../components/ui/Card' import Button from '../components/ui/Button' import { Input, Label, Select } from '../components/ui/Input' diff --git a/frontend/src/pages/Contracts.tsx b/frontend/src/pages/Contracts.tsx index 4801729..c0e5bf0 100644 --- a/frontend/src/pages/Contracts.tsx +++ b/frontend/src/pages/Contracts.tsx @@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Plus, Search, Paperclip, Trash2, X, FileText, Download, Eye } from 'lucide-react' import { toast } from 'sonner' import api from '../lib/api' +import { rosterApi } from '../lib/api-services' import Card from '../components/ui/Card' import Button from '../components/ui/Button' import { Input, Label, Select } from '../components/ui/Input' @@ -66,10 +67,7 @@ export default function Contracts() { const { data: departmentList } = useQuery({ queryKey: ['roster-departments'], - queryFn: async () => { - const res = await api.get('/roster/departments') as any - return res.data || [] - }, + queryFn: () => rosterApi.departments(), }) return ( diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 3cadd00..a1e2bf8 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -5,6 +5,7 @@ import { Link } from 'react-router-dom' import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, RadialBarChart, RadialBar, PolarAngleAxis } from 'recharts' import { Users, AlertTriangle, CheckSquare, DollarSign, ArrowRight, RefreshCw, FileText, Calendar, TrendingUp, Briefcase, Calculator, Wallet, Building2, Receipt, Check, X, Clock, LayoutDashboard, ListTodo, ShieldAlert, UserPlus, AlertCircle, Download, ChevronRight, TrendingDown, ShieldCheck, Lightbulb, BookOpen, Sparkles, Repeat, XCircle, Loader2 } from 'lucide-react' import api from '../lib/api' +import { dashboardApi, rosterApi } from '../lib/api-services' import { useAuthStore } from '../store/authStore' import Card from '../components/ui/Card' import Button from '../components/ui/Button' @@ -57,10 +58,7 @@ export default function Dashboard() { const { data: expiringContracts } = useQuery({ queryKey: ['expiring-contracts'], - queryFn: async () => { - const res = await api.get('/roster/contracts/expiring') as any - return res.data - }, + queryFn: () => rosterApi.expiringContracts(), }) const currentMonth = new Date().toISOString().slice(0, 7) @@ -75,18 +73,12 @@ export default function Dashboard() { const { data: complianceScore } = useQuery({ queryKey: ['compliance-score'], - queryFn: async () => { - const res = await api.get('/dashboard/compliance-score') as any - return res.data - }, + queryFn: () => dashboardApi.healthCheck(), }) const { data: workforceStats } = useQuery({ queryKey: ['workforce-stats'], - queryFn: async () => { - const res = await api.get('/dashboard/workforce-stats') as any - return res.data - }, + queryFn: () => dashboardApi.workforceStats(), }) const resolveMutation = useMutation({ diff --git a/frontend/src/pages/Money.tsx b/frontend/src/pages/Money.tsx index c6a916d..c60d4ff 100644 --- a/frontend/src/pages/Money.tsx +++ b/frontend/src/pages/Money.tsx @@ -7,6 +7,7 @@ import { Calculator, AlertCircle, Info, Check, Upload, Layers, Settings as Setti import { Stepper, type Step } from '../components/ui/Stepper' import { InlineAlert } from '../components/ui/InlineAlert' import api from '../lib/api' +import { rosterApi } from '../lib/api-services' import { useAuthStore } from '../store/authStore' import Card from '../components/ui/Card' import Button from '../components/ui/Button' @@ -72,21 +73,14 @@ function CustomEmployeeSelector({ selectedIds, onChange }: { selectedIds: string const { data: employees } = useQuery({ queryKey: ['roster-for-batch', search, filterDept], queryFn: async () => { - const params: any = { pageSize: 999 } - if (search) params.search = search - if (filterDept) params.department = filterDept - params.status = 'ACTIVE' - const res = await api.get('/roster', { params }) as any + const res = await rosterApi.list({ pageSize: 999, search, department: filterDept, status: 'ACTIVE' }) return res.data || [] }, }) const { data: deptList } = useQuery({ queryKey: ['roster-departments'], - queryFn: async () => { - const res = await api.get('/roster/departments') as any - return res.data || [] - }, + queryFn: () => rosterApi.departments(), }) const toggle = (id: string) => { diff --git a/frontend/src/pages/Termination.tsx b/frontend/src/pages/Termination.tsx index 4f22136..5d59b8d 100644 --- a/frontend/src/pages/Termination.tsx +++ b/frontend/src/pages/Termination.tsx @@ -6,6 +6,7 @@ import { Stepper } from '../components/ui/Stepper' import { InlineAlert } from '../components/ui/InlineAlert' import jsPDF from 'jspdf' import api from '../lib/api' +import { rosterApi } from '../lib/api-services' import { useAuthStore } from '../store/authStore' import Card from '../components/ui/Card' import Button from '../components/ui/Button' @@ -217,10 +218,10 @@ export default function Termination() { const [draftPage, setDraftPage] = useState(1) const [draftPageSize, setDraftPageSize] = useState(20) - const { data: employees } = useQuery({ + const { data: employees } = useQuery({ queryKey: ['roster-for-termination'], queryFn: async () => { - const res = await api.get('/roster') as any + const res = await rosterApi.list({ pageSize: 999 }) return res.data }, }) @@ -229,10 +230,7 @@ export default function Termination() { const { data: departmentList } = useQuery({ queryKey: ['roster-departments'], - queryFn: async () => { - const res = await api.get('/roster/departments') as any - return res.data || [] - }, + queryFn: () => rosterApi.departments(), }) const { data: profile } = useQuery({