feat: Sprint 2 — Stepper/InlineAlert组件 + 首页TaskCenter任务中心 + /workspace/next-actions API + Money.tsx BatchDetail集成工作流步骤条和质量门禁
This commit is contained in:
@@ -239,6 +239,122 @@ router.get('/workforce-stats', authMiddleware, async (req: AuthRequest, res: Res
|
||||
}
|
||||
})
|
||||
|
||||
// 工作台下一步行动 — 聚合待办任务、草稿批次、到期合同、特殊状态
|
||||
router.get('/workspace/next-actions', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const now = new Date()
|
||||
const in30Days = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000)
|
||||
|
||||
// 1. 待办风险项
|
||||
const riskItems = await prisma.riskItem.findMany({
|
||||
where: { orgId, status: 'PENDING' },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 10,
|
||||
select: { id: true, title: true, type: true, level: true, deadline: true, actionUrl: true, employeeId: true },
|
||||
})
|
||||
|
||||
// 2. 草稿发薪批次
|
||||
const draftBatches = await prisma.payrollBatch.findMany({
|
||||
where: { orgId, status: 'DRAFT' },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 5,
|
||||
select: { id: true, name: true, month: true, type: true, employeeCount: true, totalPay: true },
|
||||
})
|
||||
|
||||
// 3. 即将到期合同(30天内)
|
||||
const expiringContracts = await prisma.laborContract.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
endDate: { gte: now, lte: in30Days },
|
||||
employee: { status: 'ACTIVE' },
|
||||
},
|
||||
include: { employee: { select: { id: true, name: true, department: true } } },
|
||||
orderBy: { endDate: 'asc' },
|
||||
take: 10,
|
||||
})
|
||||
|
||||
// 4. 特殊状态员工
|
||||
const specialStatusEmployees = await prisma.employee.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
status: 'ACTIVE',
|
||||
OR: [
|
||||
{ isPregnant: true },
|
||||
{ isInMedicalPeriod: true },
|
||||
{ isWorkInjured: true },
|
||||
],
|
||||
},
|
||||
select: { id: true, name: true, department: true, isPregnant: true, isInMedicalPeriod: true, isWorkInjured: true },
|
||||
take: 10,
|
||||
})
|
||||
|
||||
// 按优先级分组
|
||||
const actions: Array<{ category: string; priority: 'high' | 'medium' | 'low'; items: any[] }> = [
|
||||
{
|
||||
category: '待办事项',
|
||||
priority: 'high',
|
||||
items: riskItems.map(r => ({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
type: r.type,
|
||||
level: r.level,
|
||||
dueDate: r.deadline?.toISOString().slice(0, 10),
|
||||
link: r.actionUrl || '/',
|
||||
})),
|
||||
},
|
||||
{
|
||||
category: '发薪批次',
|
||||
priority: 'high',
|
||||
items: draftBatches.map(b => ({
|
||||
id: b.id,
|
||||
title: `${b.name}(${b.month})`,
|
||||
subtitle: `${b.employeeCount}人 · 应发 ¥${(b.totalPay || 0).toLocaleString()}`,
|
||||
link: '/money',
|
||||
})),
|
||||
},
|
||||
{
|
||||
category: '合同到期',
|
||||
priority: 'medium',
|
||||
items: expiringContracts.map(c => ({
|
||||
id: c.id,
|
||||
title: `${c.employee?.name} 的合同将于 ${c.endDate?.toISOString().slice(0, 10)} 到期`,
|
||||
subtitle: c.employee?.department || '',
|
||||
link: '/roster',
|
||||
})),
|
||||
},
|
||||
{
|
||||
category: '特殊状态',
|
||||
priority: 'medium',
|
||||
items: specialStatusEmployees.map(e => ({
|
||||
id: e.id,
|
||||
title: e.name,
|
||||
subtitle: [
|
||||
e.isPregnant ? '孕期' : '',
|
||||
e.isInMedicalPeriod ? '医疗期' : '',
|
||||
e.isWorkInjured ? '工伤' : '',
|
||||
].filter(Boolean).join('、'),
|
||||
link: '/special-status',
|
||||
})),
|
||||
},
|
||||
]
|
||||
|
||||
// 过滤空分类
|
||||
const filteredActions = actions.filter(a => a.items.length > 0)
|
||||
const totalCount = filteredActions.reduce((s, a) => s + a.items.length, 0)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
actions: filteredActions,
|
||||
totalCount,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 入离职统计看板 — 按月聚合入职和离职人数
|
||||
router.get('/turnover-stats', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user