Files
TurboHR/backend/src/routes/payroll.routes.ts
T
freedakgmail 442063fb14 fix: 后端统一非劳动合同类型不缴社保公积金
- payroll.service.ts: isNoSocialContract 补充 PARTTIME/OUTSOURCING/UNSIGNED
- payroll.routes.ts tax-preview: 增加 contract 类型判断,非劳动合同 socialBase/housingBase 强制 0
- roster.routes.ts: 之前已修复
2026-08-17 20:25:11 +08:00

761 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Router, Response, NextFunction } from 'express'
import prisma from '../lib/prisma'
import { decrypt } from '../lib/crypto'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import { z } from 'zod'
import { isInProbation } from '../services/contract.service'
const router = Router()
router.use(authMiddleware)
// ========== 加班费记录 ==========
const overtimeSchema = z.object({
employeeId: z.string().min(1),
month: z.string().regex(/^\d{4}-\d{2}$/),
monthlyWage: z.number().positive(),
weekdayHours: z.number().min(0).default(0),
weekendHours: z.number().min(0).default(0),
holidayHours: z.number().min(0).default(0),
})
// 获取加班费记录列表
router.get('/overtime', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { employeeId, month } = req.query
const records = await prisma.overtimeRecord.findMany({
where: {
orgId: req.user!.orgId,
...(employeeId ? { employeeId: String(employeeId) } : {}),
...(month ? { month: String(month) } : {}),
},
include: { employee: { select: { id: true, name: true, department: true } } },
orderBy: { createdAt: 'desc' },
})
res.json({ success: true, data: records })
} catch (err) {
next(err)
}
})
// 保存加班费记录
router.post('/overtime', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const data = overtimeSchema.parse(req.body)
const otConfig = await prisma.overtimeConfig.findUnique({ where: { orgId: req.user!.orgId } }) ?? { weekdayRate: 1.5, weekendRate: 2.0, holidayRate: 3.0, monthlyDays: 21.75, dailyHours: 8 }
const hourlyWage = data.monthlyWage / otConfig.monthlyDays / otConfig.dailyHours
const weekdayPay = hourlyWage * otConfig.weekdayRate * data.weekdayHours
const weekendPay = hourlyWage * otConfig.weekendRate * data.weekendHours
const holidayPay = hourlyWage * otConfig.holidayRate * data.holidayHours
const totalPay = weekdayPay + weekendPay + holidayPay
const record = await prisma.overtimeRecord.upsert({
where: {
employeeId_month: { employeeId: data.employeeId, month: data.month },
},
update: {
weekdayHours: data.weekdayHours,
weekendHours: data.weekendHours,
holidayHours: data.holidayHours,
weekdayPay,
weekendPay,
holidayPay,
totalPay,
},
create: {
orgId: req.user!.orgId,
employeeId: data.employeeId,
month: data.month,
weekdayHours: data.weekdayHours,
weekendHours: data.weekendHours,
holidayHours: data.holidayHours,
weekdayPay,
weekendPay,
holidayPay,
totalPay,
},
})
res.json({ success: true, data: record })
} catch (err) {
next(err)
}
})
// 更新加班记录(按ID
const overtimeUpdateSchema = z.object({
weekdayHours: z.number().min(0).optional(),
weekendHours: z.number().min(0).optional(),
holidayHours: z.number().min(0).optional(),
monthlyWage: z.number().positive().optional(),
})
router.put('/overtime/:id', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params
const data = overtimeUpdateSchema.parse(req.body)
const existing = await prisma.overtimeRecord.findUnique({ where: { id } })
if (!existing) {
res.status(404).json({ success: false, message: '记录不存在' })
return
}
const monthlyWage = data.monthlyWage ?? 0
const weekdayHours = data.weekdayHours ?? existing.weekdayHours
const weekendHours = data.weekendHours ?? existing.weekendHours
const holidayHours = data.holidayHours ?? existing.holidayHours
const otConfig = await prisma.overtimeConfig.findUnique({ where: { orgId: req.user!.orgId } }) ?? { weekdayRate: 1.5, weekendRate: 2.0, holidayRate: 3.0, monthlyDays: 21.75, dailyHours: 8 }
const hourlyWage = monthlyWage / otConfig.monthlyDays / otConfig.dailyHours
const weekdayPay = hourlyWage * otConfig.weekdayRate * weekdayHours
const weekendPay = hourlyWage * otConfig.weekendRate * weekendHours
const holidayPay = hourlyWage * otConfig.holidayRate * holidayHours
const totalPay = weekdayPay + weekendPay + holidayPay
const record = await prisma.overtimeRecord.update({
where: { id },
data: {
weekdayHours,
weekendHours,
holidayHours,
weekdayPay,
weekendPay,
holidayPay,
totalPay,
},
})
res.json({ success: true, data: record })
} catch (err) {
next(err)
}
})
// 从考勤记录同步加班工时
router.post('/overtime/sync-from-attendance', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const orgId = req.user!.orgId
const { month } = req.body as { month: string }
if (!month || !/^\d{4}-\d{2}$/.test(month)) {
return res.status(400).json({ success: false, message: '请提供有效的月份(YYYY-MM' })
}
const monthStart = new Date(month + '-01')
const monthEnd = new Date(monthStart)
monthEnd.setMonth(monthEnd.getMonth() + 1)
// 获取该月所有考勤记录(含加班工时)
const records = await prisma.attendanceRecord.findMany({
where: { orgId, date: { gte: monthStart, lt: monthEnd }, overtimeHours: { gt: 0 } },
})
if (records.length === 0) {
return res.json({ success: false, message: '该月考勤记录中无加班工时' })
}
// 按员工汇总加班工时,按日期类型分类
const empMap = new Map<string, { weekday: number; weekend: number; holiday: number }>()
for (const r of records) {
const day = new Date(r.date)
const dayOfWeek = day.getDay() // 0=周日, 6=周六
let type: 'weekday' | 'weekend' | 'holiday' = 'weekday'
if (dayOfWeek === 0 || dayOfWeek === 6) {
type = 'weekend'
}
// 简单判断法定节假日:这里使用周末判断,实际法定节假日需要额外配置
// 如果有 holidayHours 字段在 attendanceRecord 中,优先使用
if (!empMap.has(r.employeeId)) {
empMap.set(r.employeeId, { weekday: 0, weekend: 0, holiday: 0 })
}
const entry = empMap.get(r.employeeId)!
entry[type] += r.overtimeHours || 0
}
// 获取员工月工资用于计算加班费
let config = await prisma.overtimeConfig.findUnique({ where: { orgId } })
if (!config) config = await prisma.overtimeConfig.create({ data: { orgId } })
let synced = 0
for (const [employeeId, hours] of empMap) {
const emp = await prisma.employee.findFirst({ where: { id: employeeId }, select: { monthlySalary: true } })
let monthlyWage = 0
try { monthlyWage = emp?.monthlySalary ? Number(decrypt(emp.monthlySalary)) : 0 } catch { monthlyWage = Number(emp?.monthlySalary) || 0 }
const hourlyWage = monthlyWage / config.monthlyDays / config.dailyHours
const weekdayPay = hourlyWage * config.weekdayRate * hours.weekday
const weekendPay = hourlyWage * config.weekendRate * hours.weekend
const holidayPay = hourlyWage * config.holidayRate * hours.holiday
const totalPay = weekdayPay + weekendPay + holidayPay
await prisma.overtimeRecord.upsert({
where: { employeeId_month: { employeeId, month } },
update: {
weekdayHours: hours.weekday,
weekendHours: hours.weekend,
holidayHours: hours.holiday,
weekdayPay, weekendPay, holidayPay, totalPay,
},
create: {
orgId, employeeId, month,
weekdayHours: hours.weekday,
weekendHours: hours.weekend,
holidayHours: hours.holiday,
weekdayPay, weekendPay, holidayPay, totalPay,
},
})
synced++
}
res.json({ success: true, data: { synced, totalEmployees: empMap.size } })
} catch (err) {
next(err)
}
})
// ========== 工资条管理 ==========
const payslipSchema = z.object({
employeeId: z.string().min(1),
month: z.string().regex(/^\d{4}-\d{2}$/),
baseSalary: z.number().min(0).default(0),
overtimePay: z.number().min(0).default(0),
weekdayOvertimePay: z.number().min(0).default(0),
weekendOvertimePay: z.number().min(0).default(0),
holidayOvertimePay: z.number().min(0).default(0),
allowance: z.number().min(0).default(0),
deduction: z.number().min(0).default(0),
})
// 获取工资条列表
router.get('/payslip', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { month, employeeId } = req.query
const payslips = await prisma.payslip.findMany({
where: {
orgId: req.user!.orgId,
...(month ? { month: String(month) } : {}),
...(employeeId ? { employeeId: String(employeeId) } : {}),
},
include: { employee: { select: { id: true, name: true, department: true } } },
orderBy: [{ month: 'desc' }, { employee: { name: 'asc' } }],
})
res.json({ success: true, data: payslips })
} catch (err) {
next(err)
}
})
// 创建/更新工资条
router.post('/payslip', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const data = payslipSchema.parse(req.body)
const totalPay = data.baseSalary + data.overtimePay + data.allowance - data.deduction
const payslip = await prisma.payslip.upsert({
where: {
employeeId_month: { employeeId: data.employeeId, month: data.month },
},
update: {
baseSalary: data.baseSalary,
overtimePay: data.overtimePay,
weekdayOvertimePay: data.weekdayOvertimePay,
weekendOvertimePay: data.weekendOvertimePay,
holidayOvertimePay: data.holidayOvertimePay,
allowance: data.allowance,
deduction: data.deduction,
totalPay,
},
create: {
orgId: req.user!.orgId,
employeeId: data.employeeId,
month: data.month,
baseSalary: data.baseSalary,
overtimePay: data.overtimePay,
weekdayOvertimePay: data.weekdayOvertimePay,
weekendOvertimePay: data.weekendOvertimePay,
holidayOvertimePay: data.holidayOvertimePay,
allowance: data.allowance,
deduction: data.deduction,
totalPay,
},
})
res.json({ success: true, data: payslip })
} catch (err) {
next(err)
}
})
// 从加班费记录自动生成工资条
router.post('/payslip/generate', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { month, employeeId, baseSalary, allowance, deduction } = req.body as {
month: string
employeeId: string
baseSalary: number
allowance?: number
deduction?: number
}
const overtime = await prisma.overtimeRecord.findUnique({
where: { employeeId_month: { employeeId, month } },
})
const overtimePay = overtime?.totalPay || 0
const totalPay = baseSalary + overtimePay + (allowance || 0) - (deduction || 0)
const payslip = await prisma.payslip.upsert({
where: { employeeId_month: { employeeId, month } },
update: {
baseSalary,
overtimePay,
weekdayOvertimePay: overtime?.weekdayPay || 0,
weekendOvertimePay: overtime?.weekendPay || 0,
holidayOvertimePay: overtime?.holidayPay || 0,
allowance: allowance || 0,
deduction: deduction || 0,
totalPay,
},
create: {
orgId: req.user!.orgId,
employeeId,
month,
baseSalary,
overtimePay,
weekdayOvertimePay: overtime?.weekdayPay || 0,
weekendOvertimePay: overtime?.weekendPay || 0,
holidayOvertimePay: overtime?.holidayPay || 0,
allowance: allowance || 0,
deduction: deduction || 0,
totalPay,
},
})
res.json({ success: true, data: payslip })
} catch (err) {
next(err)
}
})
// 删除工资条
router.delete('/payslip/:id', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
await prisma.payslip.delete({
where: { id: req.params.id, orgId: req.user!.orgId },
})
res.json({ success: true })
} catch (err) {
next(err)
}
})
// ========== 批量生成工资条 ==========
const batchGenerateSchema = z.object({
month: z.string().regex(/^\d{4}-\d{2}$/),
allowances: z.record(z.string(), z.number().default(0)).optional(),
deductions: z.record(z.string(), z.number().default(0)).optional(),
})
// 批量生成全员工资条
router.post('/payslip/batch-generate', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { month, allowances = {}, deductions = {} } = batchGenerateSchema.parse(req.body)
const employees = await prisma.employee.findMany({
where: { orgId: req.user!.orgId, status: 'ACTIVE' },
include: {
contracts: { orderBy: { createdAt: 'desc' }, take: 1 },
},
})
const results: any[] = []
for (const emp of employees) {
const overtime = await prisma.overtimeRecord.findUnique({
where: { employeeId_month: { employeeId: emp.id, month } },
})
const overtimePay = overtime?.totalPay || 0
const allowance = allowances[emp.id] || 0
const deduction = deductions[emp.id] || 0
let baseSalary = 0
// 按月份判定是否仍在试用期
const monthEnd = new Date(`${month}-28T23:59:59`)
const latestContract = emp.contracts[0]
if (isInProbation(latestContract, monthEnd) && latestContract?.probationSalary > 0) {
baseSalary = latestContract.probationSalary
} else if (emp.monthlySalary) {
try {
baseSalary = Number(decrypt(emp.monthlySalary)) || 0
} catch {
baseSalary = Number(emp.monthlySalary) || 0
}
}
const totalPay = baseSalary + overtimePay + allowance - deduction
const payslip = await prisma.payslip.upsert({
where: { employeeId_month: { employeeId: emp.id, month } },
update: { baseSalary, overtimePay, allowance, deduction, totalPay },
create: {
orgId: req.user!.orgId,
employeeId: emp.id,
month,
baseSalary,
overtimePay,
weekdayOvertimePay: overtime?.weekdayPay || 0,
weekendOvertimePay: overtime?.weekendPay || 0,
holidayOvertimePay: overtime?.holidayPay || 0,
allowance,
deduction,
totalPay,
},
})
results.push(payslip)
}
res.json({ success: true, data: { generated: results.length, payslips: results } })
} catch (err) {
next(err)
}
})
// ========== 加班费计算规则配置 ==========
const overtimeConfigSchema = z.object({
weekdayRate: z.number().min(1).default(1.5),
weekendRate: z.number().min(1).default(2.0),
holidayRate: z.number().min(1).default(3.0),
monthlyDays: z.number().min(1).default(21.75),
dailyHours: z.number().min(1).default(8),
})
// 获取加班费计算规则
router.get('/overtime/config', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
let config = await prisma.overtimeConfig.findUnique({ where: { orgId: req.user!.orgId } })
if (!config) {
config = await prisma.overtimeConfig.create({ data: { orgId: req.user!.orgId } })
}
res.json({ success: true, data: config })
} catch (err) {
next(err)
}
})
// 保存加班费计算规则
router.post('/overtime/config', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const data = overtimeConfigSchema.parse(req.body)
const config = await prisma.overtimeConfig.upsert({
where: { orgId: req.user!.orgId },
update: data,
create: { orgId: req.user!.orgId, ...data },
})
res.json({ success: true, data: config })
} catch (err) {
next(err)
}
})
// ========== 批量导入加班工时 ==========
const batchOvertimeSchema = z.array(
z.object({
employeeId: z.string().min(1),
month: z.string().regex(/^\d{4}-\d{2}$/),
weekdayHours: z.number().min(0).default(0),
weekendHours: z.number().min(0).default(0),
holidayHours: z.number().min(0).default(0),
}),
)
router.post('/overtime/batch', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const items = batchOvertimeSchema.parse(req.body)
const results: any[] = []
for (const data of items) {
const record = await prisma.overtimeRecord.upsert({
where: { employeeId_month: { employeeId: data.employeeId, month: data.month } },
update: {
weekdayHours: data.weekdayHours,
weekendHours: data.weekendHours,
holidayHours: data.holidayHours,
weekdayPay: 0, weekendPay: 0, holidayPay: 0, totalPay: 0,
},
create: {
orgId: req.user!.orgId,
employeeId: data.employeeId,
month: data.month,
weekdayHours: data.weekdayHours,
weekendHours: data.weekendHours,
holidayHours: data.holidayHours,
},
})
results.push(record)
}
res.json({ success: true, data: { imported: results.length } })
} catch (err) {
next(err)
}
})
// ========== 批次导入加班费 ==========
router.post('/overtime/import-to-batch/:batchId', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { batchId } = req.params
const orgId = req.user!.orgId
const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
if (!batch) return res.status(404).json({ success: false, message: '批次不存在' })
if (batch.status === 'ARCHIVED') return res.status(400).json({ success: false, message: '已归档批次不可操作' })
// 获取加班费计算规则
let config = await prisma.overtimeConfig.findUnique({ where: { orgId } })
if (!config) config = await prisma.overtimeConfig.create({ data: { orgId } })
// 获取该月未关联批次的加班记录
const overtimeRecords = await prisma.overtimeRecord.findMany({
where: { orgId, month: batch.month, batchId: null },
include: { employee: { select: { id: true, name: true, monthlySalary: true } } },
})
if (overtimeRecords.length === 0) {
return res.json({ success: false, message: '没有可导入的加班记录(所有记录已关联批次或无数据)' })
}
const results: any[] = []
for (const ot of overtimeRecords) {
// 获取员工月工资
let monthlyWage = 0
try {
monthlyWage = ot.employee.monthlySalary ? Number(decrypt(ot.employee.monthlySalary)) : 0
} catch {
monthlyWage = Number(ot.employee.monthlySalary) || 0
}
if (!monthlyWage) continue
// 根据规则计算加班费
const hourlyWage = monthlyWage / config.monthlyDays / config.dailyHours
const weekdayPay = hourlyWage * config.weekdayRate * ot.weekdayHours
const weekendPay = hourlyWage * config.weekendRate * ot.weekendHours
const holidayPay = hourlyWage * config.holidayRate * ot.holidayHours
const totalPay = weekdayPay + weekendPay + holidayPay
// 更新加班记录:计算金额并锁定到批次
await prisma.overtimeRecord.update({
where: { id: ot.id },
data: { weekdayPay, weekendPay, holidayPay, totalPay, batchId },
})
// 更新批次条目的加班费
const entry = await prisma.batchEntry.findUnique({
where: { batchId_employeeId: { batchId, employeeId: ot.employeeId } },
})
if (entry) {
await prisma.batchEntry.update({
where: { id: entry.id },
data: { overtimePay: totalPay },
})
// 重新计算条目
const newTotalPay = entry.baseSalary + totalPay + entry.allowance + entry.bonus - entry.deduction
await prisma.batchEntry.update({
where: { id: entry.id },
data: { totalPay: newTotalPay },
})
}
results.push({ employeeId: ot.employeeId, employeeName: ot.employee.name, totalPay })
}
res.json({ success: true, data: { imported: results.length, details: results } })
} catch (err) {
next(err)
}
})
// ========== 税率试算 ==========
router.post('/tax-preview', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { employeeId, month, baseSalary, overtimePay, allowance, deduction, bonus, specialDeduction } = req.body
const orgId = req.user!.orgId
// 获取员工和配置
const [employee, socialConfig, housingConfig] = await Promise.all([
employeeId ? prisma.employee.findFirst({ where: { id: employeeId, orgId }, include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 } } }) : null,
prisma.socialInsuranceConfig.findFirst({
where: { orgId, effectiveFrom: { lte: month }, OR: [{ effectiveTo: null }, { effectiveTo: { gte: month } }] },
orderBy: { effectiveFrom: 'desc' },
}),
prisma.housingFundConfig.findFirst({
where: { orgId, effectiveFrom: { lte: month }, OR: [{ effectiveTo: null }, { effectiveTo: { gte: month } }] },
orderBy: { effectiveFrom: 'desc' },
}),
])
const emp = employee || { socialInsBase: baseSalary, housingFundBase: baseSalary } as any
// 非劳动合同类型不缴纳社保公积金
const contractType = (employee as any)?.contracts?.[0]?.contractType
const isNoSocialContract = contractType && ['LABOR', 'INTERNSHIP', 'PARTTIME', 'OUTSOURCING', 'UNSIGNED'].includes(contractType)
const socialBase = isNoSocialContract ? 0 : (emp.socialInsBase != null ? emp.socialInsBase : baseSalary)
const housingBase = isNoSocialContract ? 0 : (emp.housingFundBase != null ? emp.housingFundBase : baseSalary)
// 计算社保公积金
let socialEmp = 0, housingEmp = 0
if (socialConfig) {
const { calcSocialInsurance } = await import('../services/payroll.service')
const social = calcSocialInsurance(socialBase, socialConfig)
socialEmp = social.socialEmp
}
if (housingConfig) {
const { calcHousingFund } = await import('../services/payroll.service')
const housing = calcHousingFund(housingBase, housingConfig)
housingEmp = housing.housingEmp
}
// 获取 YTD 数据计算累计个税
const year = month.slice(0, 4)
const ytdPayslips = employeeId
? await prisma.payslip.findMany({
where: { employeeId, month: { startsWith: year }, status: 'PUBLISHED' },
orderBy: { month: 'asc' },
})
: []
const ytdTaxableIncome = ytdPayslips.reduce((sum, p) => sum + (p.totalPay - p.deduction - socialEmp - housingEmp - (specialDeduction || 0)), 0)
const ytdTaxDeducted = ytdPayslips.reduce((sum, p) => sum + (p.tax || 0), 0)
const { calcCumulativeTax } = await import('../services/payroll.service')
const totalPay = (baseSalary || 0) + (overtimePay || 0) + (allowance || 0) - (deduction || 0) + (bonus || 0)
const taxableIncome = totalPay - socialEmp - housingEmp - (specialDeduction || 0)
const tax = calcCumulativeTax(ytdTaxableIncome + taxableIncome, ytdTaxDeducted)
const netPay = totalPay - socialEmp - housingEmp - tax
res.json({
success: true,
data: {
baseSalary: baseSalary || 0,
overtimePay: overtimePay || 0,
allowance: allowance || 0,
deduction: deduction || 0,
bonus: bonus || 0,
totalPay,
socialEmp,
housingEmp,
specialDeduction: specialDeduction || 0,
taxableIncome,
estimatedTax: tax,
netPay,
ytdPayslipCount: ytdPayslips.length,
breakdown: [
{ label: '应发合计', value: totalPay },
{ label: '个人社保', value: -socialEmp },
{ label: '个人公积金', value: -housingEmp },
{ label: '专项附加扣除', value: -(specialDeduction || 0) },
{ label: '应纳税所得额', value: taxableIncome },
{ label: '当月个税', value: -tax },
{ label: '实发工资', value: netPay },
],
},
})
} catch (err) {
next(err)
}
})
// ========== 薪资汇总表 & 明细表 ==========
// 薪资汇总表(按部门维度统计)
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