feat: 工作日历/考勤管理重构/AI人力报告/工作台员工分布/筛选优化/导入导出增强
- 新增工作日历页面(月历视图、事件管理、自定义事件) - 考勤管理重构为6 Tab模块(班次/排班/每日出勤/月度报表/休假记录) - AI顾问新增人力报告Tab,支持流式生成+Word导出 - 工作台总览新增员工分布统计(性别/年龄/学历/司龄饼图)+部门成本拆分 - 花名册/合同/解聘补偿新增部门和状态筛选 - 薪税管理新增工资表导入模板下载、银行代发CSV导出 - 社保公积金支持多公积金账户类型显示 - 数据导出新增花名册/解聘记录导出,中文文件名编码修复 - 数据导入新增模板下载(员工/增减员/工资表)+错误日志导出 - 移除工作台日历卡片(已迁移至独立工作日历页面) - 新增20260728/20260729更新测试指导文档
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user