feat: 工作日历/考勤管理重构/AI人力报告/工作台员工分布/筛选优化/导入导出增强

- 新增工作日历页面(月历视图、事件管理、自定义事件)
- 考勤管理重构为6 Tab模块(班次/排班/每日出勤/月度报表/休假记录)
- AI顾问新增人力报告Tab,支持流式生成+Word导出
- 工作台总览新增员工分布统计(性别/年龄/学历/司龄饼图)+部门成本拆分
- 花名册/合同/解聘补偿新增部门和状态筛选
- 薪税管理新增工资表导入模板下载、银行代发CSV导出
- 社保公积金支持多公积金账户类型显示
- 数据导出新增花名册/解聘记录导出,中文文件名编码修复
- 数据导入新增模板下载(员工/增减员/工资表)+错误日志导出
- 移除工作台日历卡片(已迁移至独立工作日历页面)
- 新增20260728/20260729更新测试指导文档
This commit is contained in:
freedakgmail
2026-07-29 08:35:29 +08:00
parent d020d04a8a
commit fb36b10402
45 changed files with 3756 additions and 169 deletions
+68
View File
@@ -171,4 +171,72 @@ router.get('/annual-value/history', authMiddleware, async (req: AuthRequest, res
}
})
// 人力信息总览 — 员工分布统计
router.get('/workforce-stats', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const orgId = req.user!.orgId
const employees = await prisma.employee.findMany({
where: { orgId, status: 'ACTIVE' },
select: { gender: true, birthDate: true, hireDate: true, education: true },
})
const now = new Date()
// 性别分布
const genderDist: Record<string, number> = {}
for (const e of employees) {
const g = e.gender || '未知'
genderDist[g] = (genderDist[g] || 0) + 1
}
// 年龄段分布
const ageRanges = ['<25', '25-30', '31-35', '36-40', '41-50', '>50']
const ageDist: Record<string, number> = {}
for (const r of ageRanges) ageDist[r] = 0
for (const e of employees) {
if (!e.birthDate) continue
const age = now.getFullYear() - e.birthDate.getFullYear()
if (age < 25) ageDist['<25']++
else if (age <= 30) ageDist['25-30']++
else if (age <= 35) ageDist['31-35']++
else if (age <= 40) ageDist['36-40']++
else if (age <= 50) ageDist['41-50']++
else ageDist['>50']++
}
// 学历分布
const eduDist: Record<string, number> = {}
for (const e of employees) {
const edu = e.education || '未知'
eduDist[edu] = (eduDist[edu] || 0) + 1
}
// 司龄分布
const tenureRanges = ['<1年', '1-3年', '3-5年', '5-10年', '>10年']
const tenureDist: Record<string, number> = {}
for (const r of tenureRanges) tenureDist[r] = 0
for (const e of employees) {
const years = (now.getTime() - e.hireDate.getTime()) / (365.25 * 24 * 3600 * 1000)
if (years < 1) tenureDist['<1年']++
else if (years < 3) tenureDist['1-3年']++
else if (years < 5) tenureDist['3-5年']++
else if (years < 10) tenureDist['5-10年']++
else tenureDist['>10年']++
}
res.json({
success: true,
data: {
total: employees.length,
gender: Object.entries(genderDist).map(([name, value]) => ({ name, value })),
age: Object.entries(ageDist).map(([name, value]) => ({ name, value })),
education: Object.entries(eduDist).map(([name, value]) => ({ name, value })),
tenure: Object.entries(tenureDist).map(([name, value]) => ({ name, value })),
},
})
} catch (err) {
next(err)
}
})
export default router