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
+92
View File
@@ -576,4 +576,96 @@ router.post('/tax-preview', async (req: AuthRequest, res: Response, next: NextFu
}
})
// ========== 薪资汇总表 & 明细表 ==========
// 薪资汇总表(按部门维度统计)
router.get('/batch/:id/summary', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const batch = await prisma.payrollBatch.findFirst({
where: { id: req.params.id, orgId: req.user!.orgId },
})
if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
const entries = await prisma.batchEntry.findMany({
where: { batchId: req.params.id },
include: { employee: { select: { id: true, name: true, department: true } } },
})
// 按部门汇总
const deptMap = new Map<string, any>()
for (const e of entries) {
const dept = e.employee.department || '未分配'
if (!deptMap.has(dept)) {
deptMap.set(dept, { department: dept, headcount: 0, totalPay: 0, totalNetPay: 0, totalSocialEmp: 0, totalSocialOrg: 0, totalHousingEmp: 0, totalHousingOrg: 0, totalTax: 0 })
}
const d = deptMap.get(dept)
d.headcount++
d.totalPay += e.totalPay
d.totalNetPay += e.netPay
d.totalSocialEmp += e.socialEmp
d.totalSocialOrg += e.socialOrg
d.totalHousingEmp += e.housingEmp
d.totalHousingOrg += e.housingOrg
d.totalTax += e.tax
}
const departments = Array.from(deptMap.values())
const grandTotal = {
headcount: entries.length,
totalPay: entries.reduce((s, e) => s + e.totalPay, 0),
totalNetPay: entries.reduce((s, e) => s + e.netPay, 0),
totalSocialEmp: entries.reduce((s, e) => s + e.socialEmp, 0),
totalSocialOrg: entries.reduce((s, e) => s + e.socialOrg, 0),
totalHousingEmp: entries.reduce((s, e) => s + e.housingEmp, 0),
totalHousingOrg: entries.reduce((s, e) => s + e.housingOrg, 0),
totalTax: entries.reduce((s, e) => s + e.tax, 0),
}
res.json({ success: true, data: { batch, departments, grandTotal } })
} catch (err) { next(err) }
})
// 薪资明细表(全员明细)
router.get('/batch/:id/detail', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const batch = await prisma.payrollBatch.findFirst({
where: { id: req.params.id, orgId: req.user!.orgId },
})
if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
const entries = await prisma.batchEntry.findMany({
where: { batchId: req.params.id },
include: { employee: { select: { id: true, name: true, department: true, phone: true } } },
orderBy: { employee: { department: 'asc' } },
})
const details = entries.map(e => ({
employeeId: e.employeeId,
name: e.employee.name,
department: e.employee.department,
phone: e.employee.phone,
baseSalary: e.baseSalary,
positionSalary: e.positionSalary,
performanceSalary: e.performanceSalary,
senioritySalary: e.senioritySalary,
overtimePay: e.overtimePay,
transportAllowance: e.transportAllowance,
mealAllowance: e.mealAllowance,
housingAllowance: e.housingAllowance,
communicationAllowance: e.communicationAllowance,
allowance: e.allowance,
bonus: e.bonus,
deduction: e.deduction,
otherDeduction: e.otherDeduction,
socialEmp: e.socialEmp,
housingEmp: e.housingEmp,
tax: e.tax,
totalPay: e.totalPay,
netPay: e.netPay,
}))
res.json({ success: true, data: { batch, details } })
} catch (err) { next(err) }
})
export default router