feat: 完成优化-4全部任务 + 多城市社保 + pgvector修复
- AIAssistant: 会话历史保存/加载/删除,风险预测支持范围筛选,审查结果保存到员工档案 - Dashboard: 待办批量操作,风险分布可下钻,刷新按钮Tab级联,薪税tab导出Excel - SocialInsurance: 多城市社保/公积金配置支持,城市选择器 - Roster: 新增参保城市字段 - risk.service: 修复风险项去重逻辑(用employeeId:type:actionUrl替代含动态天数的title) - payroll.routes: 修复OvertimeRecord/Payslip字段名错误 - pgvector: 从源码编译安装x86_64版本兼容postgresql@15 - 优化-4文档: 全部8项标记为已完成
This commit is contained in:
@@ -2,6 +2,7 @@ import { Router, Response } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import prisma from '../lib/prisma'
|
||||
import { decrypt } from '../lib/crypto'
|
||||
import ExcelJS from 'exceljs'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -49,4 +50,87 @@ router.get('/all', authMiddleware, async (req: AuthRequest, res: Response, next)
|
||||
}
|
||||
})
|
||||
|
||||
// 导出本月薪税汇总 Excel
|
||||
router.get('/payroll', authMiddleware, async (req: AuthRequest, res: Response, next) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const month = (req.query.month as string) || new Date().toISOString().slice(0, 7)
|
||||
|
||||
const entries = await prisma.batchEntry.findMany({
|
||||
where: { orgId, batch: { month, status: 'ARCHIVED' } },
|
||||
include: { employee: true, batch: true },
|
||||
orderBy: { employee: { name: 'asc' } },
|
||||
})
|
||||
|
||||
const workbook = new ExcelJS.Workbook()
|
||||
const ws = workbook.addWorksheet('薪税汇总')
|
||||
|
||||
ws.columns = [
|
||||
{ header: '员工姓名', key: 'name', width: 12 },
|
||||
{ header: '部门', key: 'department', width: 15 },
|
||||
{ header: '基本工资', key: 'baseSalary', width: 12 },
|
||||
{ header: '加班费', key: 'overtimePay', width: 12 },
|
||||
{ header: '津贴补贴', key: 'allowance', width: 12 },
|
||||
{ header: '奖金', key: 'bonus', width: 12 },
|
||||
{ header: '扣款', key: 'deduction', width: 12 },
|
||||
{ header: '应发合计', key: 'totalPay', width: 12 },
|
||||
{ header: '个人社保', key: 'socialEmp', width: 12 },
|
||||
{ header: '个人公积金', key: 'housingEmp', width: 12 },
|
||||
{ header: '个人所得税', key: 'tax', width: 12 },
|
||||
{ header: '实发工资', key: 'netPay', width: 12 },
|
||||
{ header: '企业社保', key: 'socialOrg', width: 12 },
|
||||
{ header: '企业公积金', key: 'housingOrg', width: 12 },
|
||||
{ header: '企业总成本', key: 'orgCost', width: 12 },
|
||||
]
|
||||
|
||||
ws.getRow(1).font = { bold: true }
|
||||
|
||||
for (const e of entries) {
|
||||
ws.addRow({
|
||||
name: e.employee.name,
|
||||
department: e.employee.department,
|
||||
baseSalary: e.baseSalary,
|
||||
overtimePay: e.overtimePay,
|
||||
allowance: e.allowance,
|
||||
bonus: e.bonus,
|
||||
deduction: e.deduction,
|
||||
totalPay: e.totalPay,
|
||||
socialEmp: e.socialEmp,
|
||||
housingEmp: e.housingEmp,
|
||||
tax: e.tax,
|
||||
netPay: e.netPay,
|
||||
socialOrg: e.socialOrg,
|
||||
housingOrg: e.housingOrg,
|
||||
orgCost: e.totalPay + e.socialOrg + e.housingOrg,
|
||||
})
|
||||
}
|
||||
|
||||
// 汇总行
|
||||
const totalRow = ws.addRow({
|
||||
name: '合计',
|
||||
baseSalary: { formula: `SUM(C2:C${entries.length + 1})` },
|
||||
overtimePay: { formula: `SUM(D2:D${entries.length + 1})` },
|
||||
allowance: { formula: `SUM(E2:E${entries.length + 1})` },
|
||||
bonus: { formula: `SUM(F2:F${entries.length + 1})` },
|
||||
deduction: { formula: `SUM(G2:G${entries.length + 1})` },
|
||||
totalPay: { formula: `SUM(H2:H${entries.length + 1})` },
|
||||
socialEmp: { formula: `SUM(I2:I${entries.length + 1})` },
|
||||
housingEmp: { formula: `SUM(J2:J${entries.length + 1})` },
|
||||
tax: { formula: `SUM(K2:K${entries.length + 1})` },
|
||||
netPay: { formula: `SUM(L2:L${entries.length + 1})` },
|
||||
socialOrg: { formula: `SUM(M2:M${entries.length + 1})` },
|
||||
housingOrg: { formula: `SUM(N2:N${entries.length + 1})` },
|
||||
orgCost: { formula: `SUM(O2:O${entries.length + 1})` },
|
||||
})
|
||||
totalRow.font = { bold: true }
|
||||
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
res.setHeader('Content-Disposition', `attachment; filename="payroll-${month}.xlsx"`)
|
||||
await workbook.xlsx.write(res)
|
||||
res.end()
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
Reference in New Issue
Block a user