fix: portal端token过期后自动跳转登录页,避免显示空数据
- portalAxios 401 响应拦截器:清除 token 并跳转 /portal/login - PortalLayoutWrapper:检查 token 是否存在,不存在则跳转登录页 - 排除 /portal/login 和 /portal/auto-login 页面避免重复跳转 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -67,6 +67,7 @@ router.post('/batch', authMiddleware, async (req: AuthRequest, res: Response, ne
|
||||
weekendHours: z.number().min(0),
|
||||
holidayHours: z.number().min(0),
|
||||
overtimePay: z.number().min(0),
|
||||
deductionAmount: z.number().min(0).optional(),
|
||||
})),
|
||||
})
|
||||
const { month, items } = schema.parse(req.body)
|
||||
@@ -126,6 +127,22 @@ router.post('/batch-confirm', authMiddleware, async (req: AuthRequest, res: Resp
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
/** 更新考勤确认的扣款金额 */
|
||||
router.put('/confirmations/:id/deduction', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { deductionAmount } = req.body
|
||||
const record = await prisma.attendanceConfirmation.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '考勤确认记录不存在' } })
|
||||
const updated = await prisma.attendanceConfirmation.update({
|
||||
where: { id: req.params.id },
|
||||
data: { deductionAmount: Number(deductionAmount) || 0 },
|
||||
})
|
||||
res.json({ success: true, data: updated })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// ========== 班次管理 ==========
|
||||
|
||||
router.get('/shifts', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
|
||||
@@ -366,16 +366,28 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
|
||||
const failedEmployees: { employeeId: string; name: string; error: string }[] = []
|
||||
for (const emp of employees) {
|
||||
let baseSalary = 0
|
||||
let performanceSalary = 0
|
||||
let overtimePay = 0
|
||||
let allowance = 0
|
||||
let deduction = 0
|
||||
let bonus = 0
|
||||
|
||||
// 解密员工工资结构
|
||||
let empBaseSalary = 0
|
||||
let empPerformanceSalary = 0
|
||||
if (emp.baseSalary) {
|
||||
try { empBaseSalary = Number(decrypt(emp.baseSalary)) || 0 } catch { empBaseSalary = Number(emp.baseSalary) || 0 }
|
||||
}
|
||||
if (emp.performanceSalary) {
|
||||
try { empPerformanceSalary = Number(decrypt(emp.performanceSalary)) || 0 } catch { empPerformanceSalary = Number(emp.performanceSalary) || 0 }
|
||||
}
|
||||
|
||||
if (mode === 'copy_batch' && sourceEntries) {
|
||||
// 复制指定批次:从源条目复制数据
|
||||
const srcEntry = sourceEntries.find(e => e.employeeId === emp.id)
|
||||
if (srcEntry) {
|
||||
baseSalary = srcEntry.baseSalary
|
||||
performanceSalary = srcEntry.performanceSalary || 0
|
||||
overtimePay = srcEntry.overtimePay
|
||||
allowance = srcEntry.allowance
|
||||
deduction = srcEntry.deduction
|
||||
@@ -397,17 +409,40 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
|
||||
const inProbation = isInProbation(latestContract, batchMonthEnd)
|
||||
if (inProbation && latestContract.probationSalary > 0) {
|
||||
baseSalary = latestContract.probationSalary
|
||||
performanceSalary = 0 // 试用期一般不发绩效
|
||||
} else if (prevPayslip) {
|
||||
// 非试用期:优先用上月工资条的基本工资(保持薪资连续性)
|
||||
baseSalary = prevPayslip.baseSalary
|
||||
performanceSalary = prevPayslip.performanceSalary || 0
|
||||
} else if (empBaseSalary > 0 || empPerformanceSalary > 0) {
|
||||
// 新数据:有工资结构
|
||||
baseSalary = empBaseSalary
|
||||
performanceSalary = empPerformanceSalary
|
||||
} else if (emp.monthlySalary) {
|
||||
// 旧数据兼容:回退到 monthlySalary
|
||||
try { baseSalary = Number(decrypt(emp.monthlySalary)) || 0 } catch { baseSalary = Number(emp.monthlySalary) || 0 }
|
||||
}
|
||||
overtimePay = overtime?.totalPay || 0
|
||||
allowance = prevPayslip?.allowance || 0
|
||||
deduction = prevPayslip?.deduction || 0
|
||||
} else if (mode === 'custom' || mode === 'blank_employees') {
|
||||
// 自定义/空白员工模式:取员工工资结构(非试用期)
|
||||
const batchMonthEnd = new Date(`${month}-28T23:59:59`)
|
||||
const latestContract = emp.contracts?.[0]
|
||||
const inProbation = isInProbation(latestContract, batchMonthEnd)
|
||||
if (inProbation && latestContract?.probationSalary > 0) {
|
||||
baseSalary = latestContract.probationSalary
|
||||
performanceSalary = 0
|
||||
} else if (empBaseSalary > 0 || empPerformanceSalary > 0) {
|
||||
baseSalary = empBaseSalary
|
||||
performanceSalary = empPerformanceSalary
|
||||
} else if (emp.monthlySalary && mode === 'custom') {
|
||||
// custom 模式旧数据兼容
|
||||
try { baseSalary = Number(decrypt(emp.monthlySalary)) || 0 } catch { baseSalary = Number(emp.monthlySalary) || 0 }
|
||||
}
|
||||
// blank_employees 模式:金额默认0,不自动带出
|
||||
}
|
||||
// blank_employees 和 blank_all: 所有金额默认 0(不自动带出基本工资,不取试用期工资)
|
||||
// blank_all: 所有金额默认 0
|
||||
|
||||
// 统一试用期判定(仅 copy_last / copy_batch / custom 模式适用,SEVERANCE 除外)
|
||||
// blank_employees / blank_all 模式下所有金额应为 0,不覆盖试用期工资
|
||||
@@ -416,6 +451,7 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
|
||||
const latestContract = emp.contracts?.[0]
|
||||
if (isInProbation(latestContract, batchMonthEnd) && latestContract?.probationSalary > 0) {
|
||||
baseSalary = latestContract.probationSalary
|
||||
performanceSalary = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,7 +478,7 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
|
||||
try {
|
||||
// 查询上月递延数据(上月已归档批次中该员工的递延金额)
|
||||
const prevDeferred = await getPrevDeferred(orgId, emp.id, month)
|
||||
calcResult = await calcBatchEntry(orgId, emp.id, month, { baseSalary, overtimePay, allowance, deduction, bonus }, type, { prevDeferred })
|
||||
calcResult = await calcBatchEntry(orgId, emp.id, month, { baseSalary, performanceSalary, overtimePay, allowance, deduction, bonus }, type, { prevDeferred })
|
||||
} catch (calcErr: any) {
|
||||
// 单个员工计算失败不阻塞整个批次,记录错误并使用零值
|
||||
failedEmployees.push({ employeeId: emp.id, name: emp.name, error: calcErr?.message || '计算失败' })
|
||||
@@ -459,6 +495,7 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
|
||||
orgId,
|
||||
employeeId: emp.id,
|
||||
baseSalary,
|
||||
performanceSalary,
|
||||
overtimePay,
|
||||
allowance,
|
||||
deduction,
|
||||
@@ -692,6 +729,299 @@ router.post('/batches/:batchId/fetch-bonus', async (req: AuthRequest, res: Respo
|
||||
}
|
||||
})
|
||||
|
||||
// 获取绩效工资:按批次月份从 PerformanceRecord 查考核等级,结合 Employee.performanceSalary 计算绩效工资
|
||||
// 绩效系数:A=1.2, B=1.0, C=0.8, D=0.6;无绩效记录则系数=1.0
|
||||
router.post('/batches/:batchId/fetch-performance', 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, error: { code: 'NOT_FOUND', message: '批次不存在' } })
|
||||
if (batch.status === 'ARCHIVED') return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '已归档批次不可操作' } })
|
||||
|
||||
// 获取批次所有条目
|
||||
const entries = await prisma.batchEntry.findMany({
|
||||
where: { batchId },
|
||||
select: { id: true, employeeId: true, performanceSalary: true },
|
||||
})
|
||||
|
||||
if (entries.length === 0) {
|
||||
return res.json({ success: true, data: { filled: 0, totalAmount: 0, message: '批次无员工条目' } })
|
||||
}
|
||||
|
||||
// 查询批次月份的绩效记录(按 period = YYYY-MM 匹配)
|
||||
const performanceRecords = await prisma.performanceRecord.findMany({
|
||||
where: { orgId, period: batch.month, periodType: 'MONTHLY' },
|
||||
select: { employeeId: true, grade: true, score: true, result: true },
|
||||
})
|
||||
const perfMap = new Map<string, { grade: string; score: number }>()
|
||||
for (const r of performanceRecords) {
|
||||
// 同一员工同月可能有多条记录,取最新一条(按创建时间倒序已由 DB 保证)
|
||||
if (!perfMap.has(r.employeeId)) {
|
||||
perfMap.set(r.employeeId, { grade: r.grade, score: r.score })
|
||||
}
|
||||
}
|
||||
|
||||
// 查询员工绩效工资基数
|
||||
const employees = await prisma.employee.findMany({
|
||||
where: { id: { in: entries.map(e => e.employeeId) } },
|
||||
select: { id: true, performanceSalary: true },
|
||||
})
|
||||
const empPerfSalaryMap = new Map<string, number>()
|
||||
for (const emp of employees) {
|
||||
if (emp.performanceSalary) {
|
||||
try { empPerfSalaryMap.set(emp.id, Number(decrypt(emp.performanceSalary)) || 0) } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
// 绩效系数映射
|
||||
const gradeCoefficients: Record<string, number> = { A: 1.2, B: 1.0, C: 0.8, D: 0.6 }
|
||||
|
||||
let filled = 0
|
||||
let totalAmount = 0
|
||||
for (const entry of entries) {
|
||||
const basePerfSalary = empPerfSalaryMap.get(entry.employeeId) || 0
|
||||
if (basePerfSalary <= 0) continue // 绩效工资基数为0则跳过
|
||||
|
||||
const perfRecord = perfMap.get(entry.employeeId)
|
||||
const coefficient = perfRecord ? (gradeCoefficients[perfRecord.grade] || 1.0) : 1.0
|
||||
const calculatedPerfSalary = Math.round(basePerfSalary * coefficient * 100) / 100
|
||||
|
||||
await prisma.batchEntry.update({
|
||||
where: { id: entry.id },
|
||||
data: { performanceSalary: calculatedPerfSalary },
|
||||
})
|
||||
filled++
|
||||
totalAmount += calculatedPerfSalary
|
||||
}
|
||||
|
||||
// 重算批次汇总
|
||||
const allEntries = await prisma.batchEntry.findMany({ where: { batchId } })
|
||||
const totals = allEntries.reduce((acc, e) => ({
|
||||
totalPay: acc.totalPay + e.baseSalary + (e.performanceSalary || 0) + (e.positionSalary || 0) + (e.senioritySalary || 0) + e.overtimePay + (e.transportAllowance || 0) + (e.mealAllowance || 0) + (e.housingAllowance || 0) + (e.communicationAllowance || 0) + e.allowance + e.bonus - e.deduction - (e.otherDeduction || 0),
|
||||
totalNetPay: acc.totalNetPay + e.netPay,
|
||||
totalSocialOrg: acc.totalSocialOrg + e.socialOrg,
|
||||
totalSocialEmp: acc.totalSocialEmp + e.socialEmp,
|
||||
totalHousingOrg: acc.totalHousingOrg + e.housingOrg,
|
||||
totalHousingEmp: acc.totalHousingEmp + e.housingEmp,
|
||||
totalTax: acc.totalTax + e.tax,
|
||||
}), { totalPay: 0, totalNetPay: 0, totalSocialOrg: 0, totalSocialEmp: 0, totalHousingOrg: 0, totalHousingEmp: 0, totalTax: 0 })
|
||||
|
||||
await prisma.payrollBatch.update({
|
||||
where: { id: batchId },
|
||||
data: {
|
||||
totalPay: Math.round(totals.totalPay * 100) / 100,
|
||||
totalNetPay: Math.round(totals.totalNetPay * 100) / 100,
|
||||
totalSocialOrg: Math.round(totals.totalSocialOrg * 100) / 100,
|
||||
totalSocialEmp: Math.round(totals.totalSocialEmp * 100) / 100,
|
||||
totalHousingOrg: Math.round(totals.totalHousingOrg * 100) / 100,
|
||||
totalHousingEmp: Math.round(totals.totalHousingEmp * 100) / 100,
|
||||
totalTax: Math.round(totals.totalTax * 100) / 100,
|
||||
},
|
||||
})
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
filled,
|
||||
totalAmount: Math.round(totalAmount * 100) / 100,
|
||||
message: filled > 0
|
||||
? `已填充 ${filled} 人绩效工资,合计 ¥${Math.round(totalAmount * 100) / 100}${performanceRecords.length > 0 ? `(${performanceRecords.length} 人有考核记录)` : '(无考核记录,按系数1.0计算)'}`
|
||||
: '无绩效工资数据(员工未设置绩效工资基数)',
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取违纪扣款:按批次月份从 DisciplinaryRecord 查 action=DEDUCTION 的记录,汇总扣款金额填充到 entries.deduction
|
||||
router.post('/batches/:batchId/fetch-disciplinary', 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, error: { code: 'NOT_FOUND', message: '批次不存在' } })
|
||||
if (batch.status === 'ARCHIVED') return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '已归档批次不可操作' } })
|
||||
|
||||
// 获取批次所有条目
|
||||
const entries = await prisma.batchEntry.findMany({
|
||||
where: { batchId },
|
||||
select: { id: true, employeeId: true, deduction: true },
|
||||
})
|
||||
|
||||
if (entries.length === 0) {
|
||||
return res.json({ success: true, data: { filled: 0, totalAmount: 0, message: '批次无员工条目' } })
|
||||
}
|
||||
|
||||
// 查询批次月份内的违纪扣款记录(按 violationDate 落在批次月份内)
|
||||
const monthStart = new Date(batch.month + '-01')
|
||||
const monthEnd = new Date(monthStart)
|
||||
monthEnd.setMonth(monthEnd.getMonth() + 1)
|
||||
|
||||
const disciplinaryRecords = await prisma.disciplinaryRecord.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
employeeId: { in: entries.map(e => e.employeeId) },
|
||||
action: 'DEDUCTION',
|
||||
deductionAmount: { gt: 0 },
|
||||
violationDate: { gte: monthStart, lt: monthEnd },
|
||||
},
|
||||
select: { employeeId: true, deductionAmount: true },
|
||||
})
|
||||
|
||||
// 按员工汇总扣款金额
|
||||
const deductionMap = new Map<string, number>()
|
||||
for (const r of disciplinaryRecords) {
|
||||
deductionMap.set(r.employeeId, (deductionMap.get(r.employeeId) || 0) + r.deductionAmount)
|
||||
}
|
||||
|
||||
let filled = 0
|
||||
let totalAmount = 0
|
||||
for (const entry of entries) {
|
||||
const deduction = deductionMap.get(entry.employeeId)
|
||||
if (deduction && deduction > 0) {
|
||||
await prisma.batchEntry.update({
|
||||
where: { id: entry.id },
|
||||
data: { deduction: Math.round(deduction * 100) / 100 },
|
||||
})
|
||||
filled++
|
||||
totalAmount += deduction
|
||||
}
|
||||
}
|
||||
|
||||
// 重算批次汇总
|
||||
const allEntries = await prisma.batchEntry.findMany({ where: { batchId } })
|
||||
const totals = allEntries.reduce((acc, e) => ({
|
||||
totalPay: acc.totalPay + e.baseSalary + (e.performanceSalary || 0) + (e.positionSalary || 0) + (e.senioritySalary || 0) + e.overtimePay + (e.transportAllowance || 0) + (e.mealAllowance || 0) + (e.housingAllowance || 0) + (e.communicationAllowance || 0) + e.allowance + e.bonus - e.deduction - (e.otherDeduction || 0),
|
||||
totalNetPay: acc.totalNetPay + e.netPay,
|
||||
totalSocialOrg: acc.totalSocialOrg + e.socialOrg,
|
||||
totalSocialEmp: acc.totalSocialEmp + e.socialEmp,
|
||||
totalHousingOrg: acc.totalHousingOrg + e.housingOrg,
|
||||
totalHousingEmp: acc.totalHousingEmp + e.housingEmp,
|
||||
totalTax: acc.totalTax + e.tax,
|
||||
}), { totalPay: 0, totalNetPay: 0, totalSocialOrg: 0, totalSocialEmp: 0, totalHousingOrg: 0, totalHousingEmp: 0, totalTax: 0 })
|
||||
|
||||
await prisma.payrollBatch.update({
|
||||
where: { id: batchId },
|
||||
data: {
|
||||
totalPay: Math.round(totals.totalPay * 100) / 100,
|
||||
totalNetPay: Math.round(totals.totalNetPay * 100) / 100,
|
||||
totalSocialOrg: Math.round(totals.totalSocialOrg * 100) / 100,
|
||||
totalSocialEmp: Math.round(totals.totalSocialEmp * 100) / 100,
|
||||
totalHousingOrg: Math.round(totals.totalHousingOrg * 100) / 100,
|
||||
totalHousingEmp: Math.round(totals.totalHousingEmp * 100) / 100,
|
||||
totalTax: Math.round(totals.totalTax * 100) / 100,
|
||||
},
|
||||
})
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
filled,
|
||||
totalAmount: Math.round(totalAmount * 100) / 100,
|
||||
message: filled > 0
|
||||
? `已填充 ${filled} 人违纪扣款,合计 ¥${Math.round(totalAmount * 100) / 100}`
|
||||
: `${batch.month} 无违纪扣款数据`,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取考勤扣款:按批次月份从 AttendanceConfirmation 查 deductionAmount > 0 的记录,填充到 entries.deduction
|
||||
router.post('/batches/:batchId/fetch-attendance-deduction', 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, error: { code: 'NOT_FOUND', message: '批次不存在' } })
|
||||
if (batch.status === 'ARCHIVED') return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '已归档批次不可操作' } })
|
||||
|
||||
// 获取批次所有条目
|
||||
const entries = await prisma.batchEntry.findMany({
|
||||
where: { batchId },
|
||||
select: { id: true, employeeId: true, deduction: true },
|
||||
})
|
||||
|
||||
if (entries.length === 0) {
|
||||
return res.json({ success: true, data: { filled: 0, totalAmount: 0, message: '批次无员工条目' } })
|
||||
}
|
||||
|
||||
// 查询批次月份的考勤确认记录
|
||||
const confirmations = await prisma.attendanceConfirmation.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
employeeId: { in: entries.map(e => e.employeeId) },
|
||||
month: batch.month,
|
||||
deductionAmount: { gt: 0 },
|
||||
},
|
||||
select: { employeeId: true, deductionAmount: true },
|
||||
})
|
||||
|
||||
const deductionMap = new Map<string, number>()
|
||||
for (const c of confirmations) {
|
||||
deductionMap.set(c.employeeId, c.deductionAmount)
|
||||
}
|
||||
|
||||
let filled = 0
|
||||
let totalAmount = 0
|
||||
for (const entry of entries) {
|
||||
const deduction = deductionMap.get(entry.employeeId)
|
||||
if (deduction && deduction > 0) {
|
||||
await prisma.batchEntry.update({
|
||||
where: { id: entry.id },
|
||||
data: { deduction: Math.round(deduction * 100) / 100 },
|
||||
})
|
||||
filled++
|
||||
totalAmount += deduction
|
||||
}
|
||||
}
|
||||
|
||||
// 重算批次汇总
|
||||
const allEntries = await prisma.batchEntry.findMany({ where: { batchId } })
|
||||
const totals = allEntries.reduce((acc, e) => ({
|
||||
totalPay: acc.totalPay + e.baseSalary + (e.performanceSalary || 0) + (e.positionSalary || 0) + (e.senioritySalary || 0) + e.overtimePay + (e.transportAllowance || 0) + (e.mealAllowance || 0) + (e.housingAllowance || 0) + (e.communicationAllowance || 0) + e.allowance + e.bonus - e.deduction - (e.otherDeduction || 0),
|
||||
totalNetPay: acc.totalNetPay + e.netPay,
|
||||
totalSocialOrg: acc.totalSocialOrg + e.socialOrg,
|
||||
totalSocialEmp: acc.totalSocialEmp + e.socialEmp,
|
||||
totalHousingOrg: acc.totalHousingOrg + e.housingOrg,
|
||||
totalHousingEmp: acc.totalHousingEmp + e.housingEmp,
|
||||
totalTax: acc.totalTax + e.tax,
|
||||
}), { totalPay: 0, totalNetPay: 0, totalSocialOrg: 0, totalSocialEmp: 0, totalHousingOrg: 0, totalHousingEmp: 0, totalTax: 0 })
|
||||
|
||||
await prisma.payrollBatch.update({
|
||||
where: { id: batchId },
|
||||
data: {
|
||||
totalPay: Math.round(totals.totalPay * 100) / 100,
|
||||
totalNetPay: Math.round(totals.totalNetPay * 100) / 100,
|
||||
totalSocialOrg: Math.round(totals.totalSocialOrg * 100) / 100,
|
||||
totalSocialEmp: Math.round(totals.totalSocialEmp * 100) / 100,
|
||||
totalHousingOrg: Math.round(totals.totalHousingOrg * 100) / 100,
|
||||
totalHousingEmp: Math.round(totals.totalHousingEmp * 100) / 100,
|
||||
totalTax: Math.round(totals.totalTax * 100) / 100,
|
||||
},
|
||||
})
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
filled,
|
||||
totalAmount: Math.round(totalAmount * 100) / 100,
|
||||
message: filled > 0
|
||||
? `已填充 ${filled} 人考勤扣款,合计 ¥${Math.round(totalAmount * 100) / 100}`
|
||||
: `${batch.month} 无考勤扣款数据(需先在考勤确认中填写扣款金额)`,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取条目个税计算明细
|
||||
router.get('/batches/:batchId/entries/:employeeId/tax-detail', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
|
||||
@@ -220,6 +220,8 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
idCardMasked,
|
||||
idCardNumber: safeDecryptStr(e.idCardNumber),
|
||||
monthlySalary: safeDecrypt(e.monthlySalary),
|
||||
baseSalary: e.baseSalary ? safeDecrypt(e.baseSalary) : null,
|
||||
performanceSalary: e.performanceSalary ? safeDecrypt(e.performanceSalary) : null,
|
||||
socialInsBase: e.socialInsBase,
|
||||
housingFundBase: e.housingFundBase,
|
||||
socialInsCalc: (() => {
|
||||
@@ -373,7 +375,7 @@ router.get('/:id/profile', authMiddleware, async (req: AuthRequest, res, next) =
|
||||
if (found) monthlyProcessRecords.push(recordData)
|
||||
}
|
||||
|
||||
const { monthlySalary, bankAccount, idCardNumber, ...rest } = employee
|
||||
const { monthlySalary, baseSalary, performanceSalary, bankAccount, idCardNumber, ...rest } = employee
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const dynamicStatus = employee.terminations.some((t) => t.status === 'COMPLETED' && t.terminationDate <= today) ? 'RESIGNED' : 'ACTIVE'
|
||||
@@ -383,6 +385,8 @@ router.get('/:id/profile', authMiddleware, async (req: AuthRequest, res, next) =
|
||||
...rest,
|
||||
status: dynamicStatus,
|
||||
monthlySalary: safeDecrypt(monthlySalary),
|
||||
baseSalary: baseSalary ? safeDecrypt(baseSalary) : null,
|
||||
performanceSalary: performanceSalary ? safeDecrypt(performanceSalary) : null,
|
||||
bankAccount: bankAccount ? safeDecrypt(bankAccount).toString() : null,
|
||||
idCardNumber: safeDecryptStr(idCardNumber),
|
||||
monthlyProcessRecords,
|
||||
@@ -961,7 +965,7 @@ router.get('/:employeeId/disciplinary', authMiddleware, async (req: AuthRequest,
|
||||
|
||||
router.post('/:employeeId/disciplinary', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { violationDate, violationType, description, severity, action, actionDetail, employeeAck, ackDate, ackMethod, witness, attachmentUrl } = req.body
|
||||
const { violationDate, violationType, description, severity, action, actionDetail, deductionAmount, employeeAck, ackDate, ackMethod, witness, attachmentUrl } = req.body
|
||||
const record = await prisma.disciplinaryRecord.create({
|
||||
data: {
|
||||
orgId: req.user!.orgId,
|
||||
@@ -972,6 +976,7 @@ router.post('/:employeeId/disciplinary', authMiddleware, async (req: AuthRequest
|
||||
severity: severity || 'WARNING',
|
||||
action: action || 'ORAL_WARNING',
|
||||
actionDetail,
|
||||
deductionAmount: Number(deductionAmount) || 0,
|
||||
employeeAck: employeeAck || false,
|
||||
ackDate: ackDate ? new Date(ackDate) : null,
|
||||
ackMethod,
|
||||
@@ -995,7 +1000,7 @@ router.post('/:employeeId/disciplinary', authMiddleware, async (req: AuthRequest
|
||||
|
||||
router.put('/:employeeId/disciplinary/:recordId', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { violationDate, violationType, description, severity, action, actionDetail, employeeAck, ackDate, ackMethod, witness, attachmentUrl } = req.body
|
||||
const { violationDate, violationType, description, severity, action, actionDetail, deductionAmount, employeeAck, ackDate, ackMethod, witness, attachmentUrl } = req.body
|
||||
const record = await prisma.disciplinaryRecord.findFirst({
|
||||
where: { id: req.params.recordId, orgId: req.user!.orgId },
|
||||
})
|
||||
@@ -1009,6 +1014,7 @@ router.put('/:employeeId/disciplinary/:recordId', authMiddleware, async (req: Au
|
||||
severity,
|
||||
action,
|
||||
actionDetail,
|
||||
deductionAmount: deductionAmount !== undefined ? Number(deductionAmount) || 0 : undefined,
|
||||
employeeAck,
|
||||
ackDate: ackDate ? new Date(ackDate) : null,
|
||||
ackMethod,
|
||||
@@ -1412,7 +1418,7 @@ function prevMonth(month: string): string {
|
||||
// 调薪
|
||||
router.post('/:id/salary-change', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { newSalary, effectiveMonth, reason } = req.body
|
||||
const { newSalary, baseSalary, performanceSalary, effectiveMonth, reason } = req.body
|
||||
const employee = await prisma.employee.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
@@ -1446,10 +1452,13 @@ router.post('/:id/salary-change', authMiddleware, async (req: AuthRequest, res,
|
||||
},
|
||||
})
|
||||
|
||||
// 同步 Employee 便捷字段
|
||||
// 同步 Employee 便捷字段(含工资结构)
|
||||
const updateData: any = { monthlySalary: encrypt(String(newSalary)) }
|
||||
if (baseSalary !== undefined) updateData.baseSalary = encrypt(String(Number(baseSalary) || 0))
|
||||
if (performanceSalary !== undefined) updateData.performanceSalary = encrypt(String(Number(performanceSalary) || 0))
|
||||
await prisma.employee.update({
|
||||
where: { id: req.params.id },
|
||||
data: { monthlySalary: encrypt(String(newSalary)) },
|
||||
data: updateData,
|
||||
})
|
||||
|
||||
await auditLog(req, 'CREATE', 'SALARY_CHANGE', record.id, { employeeId: req.params.id, oldSalary, newSalary })
|
||||
|
||||
Reference in New Issue
Block a user