refactor: API口径统一 — 统计函数/员工列表/前端服务层
后端: - /dashboard/compliance-score 改用 getHealthCheck,与体检诊断同口径 - 新增 /employees/list 端点(不分页,供各页面下拉选择) - /employees/all-lite 增加 position 字段和 status 筛选参数 - getHealthCheck 未签合同改为查无合同记录的员工(非UNSIGNED类型) 前端: - 新建 lib/api-services.ts 统一 API 服务层 - employeeApi/rosterApi/dashboardApi/attendanceApi 统一封装 - 6个页面迁移到统一 API 调用: - Attendance: employeeApi.list + rosterApi.departments - Termination: rosterApi.list + rosterApi.departments - Money: rosterApi.list + rosterApi.departments - PortalQRModal: employeeApi.list - AIAssistant: 3处 employeeApi.list 替代 /roster?pageSize=999 - Contracts: rosterApi.departments - Dashboard: rosterApi.expiringContracts + dashboardApi.healthCheck/workforceStats
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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<T>(res: any): T {
|
||||
return res.data?.data ?? res.data ?? res
|
||||
}
|
||||
|
||||
export const rosterApi = {
|
||||
list: (params: RosterParams) => api.get('/roster', { params }).then(unwrap<RosterResponse>),
|
||||
departments: () => api.get('/roster/departments').then(unwrap<string[]>()),
|
||||
contractTypes: () => api.get('/roster/contract-types').then(unwrap<ContractType[]>()),
|
||||
}
|
||||
|
||||
export const employeeApi = {
|
||||
list: (params?: { status?: string; department?: string }) =>
|
||||
api.get('/employees/list', { params }).then(unwrap<EmployeeOption[]>()),
|
||||
profile: (id: string) => api.get(`/roster/${id}/profile`).then(unwrap<EmployeeProfile>()),
|
||||
}
|
||||
|
||||
export const dashboardApi = {
|
||||
data: () => api.get('/dashboard').then(unwrap<DashboardData>()),
|
||||
healthCheck: () => api.get('/dashboard/health-check').then(unwrap<HealthCheckData>()),
|
||||
risks: () => api.get('/dashboard/risks').then(unwrap<RiskItem[]>()),
|
||||
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<T>()` 解析,取 `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 响应遵循统一格式,前端解析统一
|
||||
@@ -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,
|
||||
|
||||
@@ -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<T>(): (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<EmployeeOption[]>()),
|
||||
/** 轻量级全量员工列表(含 RESIGNED) */
|
||||
allLite: (params?: { status?: string; department?: string }) =>
|
||||
get('/employees/all-lite', { params }).then(unwrap<EmployeeOption[]>()),
|
||||
/** 员工详情 */
|
||||
detail: (id: string) =>
|
||||
get(`/employees/${id}`).then(unwrap<any>()),
|
||||
/** 员工档案(花名册) */
|
||||
profile: (id: string) =>
|
||||
get(`/roster/${id}/profile`).then(unwrap<any>()),
|
||||
}
|
||||
|
||||
// ========== 花名册相关 ==========
|
||||
|
||||
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<RosterResponse>,
|
||||
/** 部门列表 */
|
||||
departments: () =>
|
||||
get('/roster/departments').then(unwrap<string[]>()),
|
||||
/** 合同类型 */
|
||||
contractTypes: () =>
|
||||
get('/roster/contract-types').then(unwrap<any[]>()),
|
||||
/** 即将到期合同 */
|
||||
expiringContracts: () =>
|
||||
get('/roster/contracts/expiring').then(unwrap<any[]>()),
|
||||
/** 违纪记录 */
|
||||
disciplinary: (employeeId: string) =>
|
||||
get(`/roster/${employeeId}/disciplinary`).then(unwrap<any[]>()),
|
||||
/** 证据链 */
|
||||
evidenceChain: (employeeId: string) =>
|
||||
get(`/roster/${employeeId}/evidence-chain`).then(unwrap<any>()),
|
||||
}
|
||||
|
||||
// ========== 仪表盘相关 ==========
|
||||
|
||||
export const dashboardApi = {
|
||||
/** 仪表盘主数据 */
|
||||
data: () =>
|
||||
get('/dashboard').then(unwrap<any>()),
|
||||
/** 用工体检诊断(6 维度,与评分标准一致) */
|
||||
healthCheck: () =>
|
||||
get('/dashboard/health-check').then(unwrap<any>()),
|
||||
/** 体检诊断历史 */
|
||||
healthCheckHistory: () =>
|
||||
get('/dashboard/health-check/history').then(unwrap<any[]>()),
|
||||
/** 风险中心(与工作台同口径) */
|
||||
risks: () =>
|
||||
get('/dashboard/risks').then(unwrap<any[]>()),
|
||||
/** 月度日历 */
|
||||
calendar: (month: string) =>
|
||||
get(`/dashboard/calendar?month=${month}`).then(unwrap<any>()),
|
||||
/** 成本分析 */
|
||||
costAnalysis: (month: string) =>
|
||||
get(`/dashboard/cost-analysis?month=${month}`).then(unwrap<any>()),
|
||||
/** 人力信息总览 */
|
||||
workforceStats: () =>
|
||||
get('/dashboard/workforce-stats').then(unwrap<any>()),
|
||||
/** 下一步行动 */
|
||||
nextActions: () =>
|
||||
get('/dashboard/workspace/next-actions').then(unwrap<any>()),
|
||||
/** 入离职统计 */
|
||||
turnoverStats: (months = 12) =>
|
||||
get(`/dashboard/turnover-stats?months=${months}`).then(unwrap<any>()),
|
||||
/** 绩效统计 */
|
||||
performanceStats: (period: string) =>
|
||||
get(`/dashboard/performance-stats?period=${period}`).then(unwrap<any>()),
|
||||
/** 年度价值报告 */
|
||||
annualValue: (year: number) =>
|
||||
get(`/dashboard/annual-value?year=${year}`).then(unwrap<any>()),
|
||||
/** 年度价值报告历史 */
|
||||
annualValueHistory: () =>
|
||||
get('/dashboard/annual-value/history').then(unwrap<any[]>()),
|
||||
}
|
||||
|
||||
// ========== 考勤相关 ==========
|
||||
|
||||
export const attendanceApi = {
|
||||
list: (params: { month: string; department?: string; status?: string }) =>
|
||||
get('/attendance', { params }).then(unwrap<any>()),
|
||||
stats: (month: string) =>
|
||||
get(`/attendance/stats?month=${month}`).then(unwrap<any>()),
|
||||
shifts: () =>
|
||||
get('/attendance/shifts').then(unwrap<any[]>()),
|
||||
shiftAssignments: (date: string) =>
|
||||
get(`/attendance/shift-assignments?date=${date}`).then(unwrap<any>()),
|
||||
daily: (date: string) =>
|
||||
get(`/attendance/daily?date=${date}`).then(unwrap<any>()),
|
||||
monthlyReport: (month: string) =>
|
||||
get(`/attendance/monthly-report?month=${month}`).then(unwrap<any>()),
|
||||
leaves: () =>
|
||||
get('/attendance/leaves').then(unwrap<any[]>()),
|
||||
publishRecords: () =>
|
||||
get('/attendance/publish-records').then(unwrap<any[]>()),
|
||||
}
|
||||
@@ -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<any[]>({
|
||||
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<any[]>({
|
||||
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<any[]>({
|
||||
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 () => {
|
||||
|
||||
@@ -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<string[]>({
|
||||
queryKey: ['roster-departments'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/roster/departments') as any
|
||||
return res.data || []
|
||||
},
|
||||
queryFn: () => rosterApi.departments(),
|
||||
})
|
||||
|
||||
const { data: publishRecords } = useQuery<any[]>({
|
||||
@@ -982,11 +980,8 @@ function LeavesTab() {
|
||||
})
|
||||
|
||||
const { data: rosterData } = useQuery<any>({
|
||||
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({
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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<string[]>({
|
||||
queryKey: ['roster-departments'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/roster/departments') as any
|
||||
return res.data || []
|
||||
},
|
||||
queryFn: () => rosterApi.departments(),
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@@ -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<any>({
|
||||
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<any>({
|
||||
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<any>({
|
||||
queryKey: ['workforce-stats'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/dashboard/workforce-stats') as any
|
||||
return res.data
|
||||
},
|
||||
queryFn: () => dashboardApi.workforceStats(),
|
||||
})
|
||||
|
||||
const resolveMutation = useMutation({
|
||||
|
||||
@@ -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<any>({
|
||||
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<string[]>({
|
||||
queryKey: ['roster-departments'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/roster/departments') as any
|
||||
return res.data || []
|
||||
},
|
||||
queryFn: () => rosterApi.departments(),
|
||||
})
|
||||
|
||||
const toggle = (id: string) => {
|
||||
|
||||
@@ -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<RosterEmployee[]>({
|
||||
const { data: employees } = useQuery<any[]>({
|
||||
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<string[]>({
|
||||
queryKey: ['roster-departments'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/roster/departments') as any
|
||||
return res.data || []
|
||||
},
|
||||
queryFn: () => rosterApi.departments(),
|
||||
})
|
||||
|
||||
const { data: profile } = useQuery<EmployeeProfile>({
|
||||
|
||||
Reference in New Issue
Block a user