feat: 20260809 系统优化 - 全部28项问题修复(P0×6+P1×16+P2×6)
P0: 福利批量参保/离职证明下载防乱码/考勤模板合并Sheet/补卡修改/附件在线查看删除 P1: 分页pageSize修复/离职导出筛选/撤回删除草稿/加班费自动计算/考勤加班汇总/证据链异常详情/制度催办/模板导入Word/社保封顶保底/校验字段提示/职务字段/社保费用明细/弹窗防误关/身份证查重/证明员工下拉/培训批量 P2: 离职流程去重/社保基数覆盖输入/薪税入口改名/添加员工引导/绩效模板清理
This commit is contained in:
@@ -3,6 +3,7 @@ import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { auditLog } from '../middleware/auditLog'
|
||||
import { createEvidence } from '../services/evidence.service'
|
||||
import prisma from '../lib/prisma'
|
||||
import { sha256 } from '../lib/crypto'
|
||||
import {
|
||||
createEmployeeSchema,
|
||||
updateEmployeeSchema,
|
||||
@@ -97,6 +98,24 @@ router.get('/:id', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 身份证查重
|
||||
router.get('/check-id-card', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const idCard = req.query.idCard as string
|
||||
if (!idCard || idCard.length < 18) {
|
||||
return res.json({ success: true, data: { exists: false } })
|
||||
}
|
||||
const hash = sha256(idCard)
|
||||
const employee = await prisma.employee.findFirst({
|
||||
where: { orgId: req.user!.orgId, idCardHash: hash },
|
||||
select: { id: true, name: true, department: true, status: true },
|
||||
})
|
||||
res.json({ success: true, data: { exists: !!employee, employee } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const data = createEmployeeSchema.parse(req.body)
|
||||
|
||||
@@ -146,10 +146,17 @@ router.get('/:id/download', authMiddleware, async (req: AuthRequest, res: Respon
|
||||
if (!template) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模板不存在' } })
|
||||
}
|
||||
// 包装为 HTML 格式以确保 Word 正确打开
|
||||
const htmlContent = `<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40">
|
||||
<head><meta charset="utf-8"><title>${template.name}</title>
|
||||
<style>
|
||||
body { font-family: SimSun, serif; font-size: 14pt; line-height: 2; }
|
||||
</style></head>
|
||||
<body>${template.content}</body></html>`
|
||||
const encoded = encodeURIComponent(template.name + '.doc')
|
||||
res.setHeader('Content-Type', 'application/msword')
|
||||
res.setHeader('Content-Type', 'application/msword; charset=utf-8')
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${encoded}"; filename*=UTF-8''${encoded}`)
|
||||
res.send(template.content)
|
||||
res.send(htmlContent)
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
|
||||
@@ -359,9 +359,16 @@ router.get('/terminations', authMiddleware, async (req: AuthRequest, res: Respon
|
||||
const status = req.query.status as string | undefined
|
||||
const department = req.query.department as string | undefined
|
||||
const search = req.query.search as string | undefined
|
||||
const dateFrom = req.query.dateFrom as string | undefined
|
||||
const dateTo = req.query.dateTo as string | undefined
|
||||
|
||||
const where: any = { orgId }
|
||||
if (status) where.status = status
|
||||
if (dateFrom || dateTo) {
|
||||
where.terminationDate = {}
|
||||
if (dateFrom) where.terminationDate.gte = new Date(dateFrom)
|
||||
if (dateTo) where.terminationDate.lte = new Date(dateTo + 'T23:59:59')
|
||||
}
|
||||
if (department || search) {
|
||||
where.employee = {}
|
||||
if (department) where.employee.department = department
|
||||
|
||||
@@ -541,12 +541,22 @@ router.post('/monthly', authMiddleware, requireAdmin, upload.single('file'), asy
|
||||
}
|
||||
|
||||
const wb = XLSX.read(req.file.buffer, { type: 'buffer', cellDates: true })
|
||||
const result: any = { month, attendance: 0, overtime: 0, salaryChanges: 0, socialInsChanges: 0, housingFundChanges: 0, errors: [] as string[], strategies: { '考勤记录': '覆盖(同员工同日覆盖)', '加班记录': '累加(同员工同月累加)', '薪资调整': '覆盖(关闭旧记录,新建新记录)', '社保变动': '覆盖(关闭旧记录,新建新记录)', '公积金变动': '覆盖(关闭旧记录,新建新记录)' } }
|
||||
const result: any = { month, attendance: 0, overtime: 0, discipline: 0, salaryChanges: 0, socialInsChanges: 0, housingFundChanges: 0, errors: [] as string[], strategies: { '考勤记录': '覆盖(同员工同日覆盖)', '加班记录': '累加(同员工同月累加)', '违纪记录': '追加(同员工同日可多条)', '薪资调整': '覆盖(关闭旧记录,新建新记录)', '社保变动': '覆盖(关闭旧记录,新建新记录)', '公积金变动': '覆盖(关闭旧记录,新建新记录)' } }
|
||||
|
||||
const employees = await prisma.employee.findMany({ where: { orgId }, select: { id: true, name: true, monthlySalary: true, department: true, idCardHash: true } })
|
||||
const empByHash = new Map(employees.filter(e => e.idCardHash).map(e => [e.idCardHash, e]))
|
||||
const empByName = new Map(employees.map(e => [e.name, e]))
|
||||
|
||||
// 获取加班费配置,用于自动计算 totalPay
|
||||
const otConfig = await prisma.overtimeConfig.findUnique({ where: { orgId } }) ?? { weekdayRate: 1.5, weekendRate: 2.0, holidayRate: 3.0, monthlyDays: 21.75, dailyHours: 8 }
|
||||
function calcOvertimePay(monthlyWage: number, wdHours: number, weHours: number, hoHours: number) {
|
||||
const hourlyWage = (monthlyWage || 0) / otConfig.monthlyDays / otConfig.dailyHours
|
||||
const weekdayPay = hourlyWage * otConfig.weekdayRate * wdHours
|
||||
const weekendPay = hourlyWage * otConfig.weekendRate * weHours
|
||||
const holidayPay = hourlyWage * otConfig.holidayRate * hoHours
|
||||
return Math.round((weekdayPay + weekendPay + holidayPay) * 100) / 100
|
||||
}
|
||||
|
||||
function findEmp(r: any) {
|
||||
const idCard = val(getField(r, '身份证号'))
|
||||
if (idCard) {
|
||||
@@ -556,56 +566,109 @@ router.post('/monthly', authMiddleware, requireAdmin, upload.single('file'), asy
|
||||
return empByName.get(val(getField(r, '姓名')))
|
||||
}
|
||||
|
||||
// 考勤记录
|
||||
// 考勤记录 + 加班记录(支持合并Sheet"考勤与加班"或独立Sheet)
|
||||
const mergedSheet = wb.Sheets['考勤与加班']
|
||||
const attSheet = wb.Sheets['考勤记录']
|
||||
if (attSheet) {
|
||||
const rows = XLSX.utils.sheet_to_json(attSheet)
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
try {
|
||||
const emp = findEmp(r)
|
||||
if (!emp) { result.errors.push(`考勤第${i + 2}行:找不到员工「${val(getField(r, '姓名'))}」`); continue }
|
||||
const date = parseDate(getField(r, '日期'))
|
||||
if (!date) { result.errors.push(`考勤第${i + 2}行:日期格式错误`); continue }
|
||||
const statusMap: any = { '正常': 'NORMAL', '迟到': 'LATE', '早退': 'EARLY_LEAVE', '缺勤': 'ABSENT', '请假': 'LEAVE', '出差': 'BUSINESS_TRIP' }
|
||||
await prisma.attendanceRecord.upsert({
|
||||
where: { employeeId_date: { employeeId: emp.id, date } },
|
||||
create: { orgId, employeeId: emp.id, date, status: statusMap[val(getField(r, '考勤状态'))] || 'NORMAL', checkInTime: val(getField(r, '上班时间')) || null, checkOutTime: val(getField(r, '下班时间')) || null, remark: val(getField(r, '备注')) || null, createdBy: userId },
|
||||
update: { status: statusMap[val(getField(r, '考勤状态'))] || 'NORMAL', checkInTime: val(getField(r, '上班时间')) || null, checkOutTime: val(getField(r, '下班时间')) || null, remark: val(getField(r, '备注')) || null },
|
||||
})
|
||||
result.attendance++
|
||||
} catch (e: any) { result.errors.push(`考勤第${i + 2}行:${e?.message || '导入失败'}`) }
|
||||
}
|
||||
}
|
||||
|
||||
// 加班记录
|
||||
const otSheet = wb.Sheets['加班记录']
|
||||
if (otSheet) {
|
||||
const rows = XLSX.utils.sheet_to_json(otSheet)
|
||||
const statusMap: any = { '正常': 'NORMAL', '迟到': 'LATE', '早退': 'EARLY_LEAVE', '缺勤': 'ABSENT', '请假': 'LEAVE', '出差': 'BUSINESS_TRIP' }
|
||||
|
||||
if (mergedSheet) {
|
||||
// 合并Sheet:每行同时处理考勤和加班
|
||||
const rows = XLSX.utils.sheet_to_json(mergedSheet)
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
try {
|
||||
const emp = findEmp(r)
|
||||
if (!emp) { result.errors.push(`加班第${i + 2}行:找不到员工「${val(getField(r, '姓名'))}」`); continue }
|
||||
if (!emp) { result.errors.push(`第${i + 2}行:找不到员工「${val(getField(r, '姓名'))}」`); continue }
|
||||
const date = parseDate(getField(r, '日期'))
|
||||
if (!date) { result.errors.push(`加班第${i + 2}行:日期格式错误`); continue }
|
||||
const otMonth = dateToMonth(date)
|
||||
const hours = num(getField(r, '加班时长'))
|
||||
const otType = val(getField(r, '加班类型')) || '工作日加班'
|
||||
const wdHours = num(getField(r, '工作日加班时长')) || (otType.includes('工作日') ? hours : 0)
|
||||
const weHours = num(getField(r, '休息日加班时长')) || (otType.includes('休息日') ? hours : 0)
|
||||
const hoHours = num(getField(r, '法定节假日加班时长')) || (otType.includes('法定') ? hours : 0)
|
||||
await prisma.overtimeRecord.upsert({
|
||||
where: { employeeId_month: { employeeId: emp.id, month: otMonth } },
|
||||
create: { orgId, employeeId: emp.id, month: otMonth, weekdayHours: wdHours, weekendHours: weHours, holidayHours: hoHours } as any,
|
||||
update: {
|
||||
weekdayHours: { increment: wdHours },
|
||||
weekendHours: { increment: weHours },
|
||||
holidayHours: { increment: hoHours },
|
||||
},
|
||||
})
|
||||
result.overtime++
|
||||
} catch (e: any) { result.errors.push(`加班第${i + 2}行:${e?.message || '导入失败'}`) }
|
||||
if (!date) { result.errors.push(`第${i + 2}行:日期格式错误`); continue }
|
||||
|
||||
// 考勤部分
|
||||
const attStatus = val(getField(r, '考勤状态'))
|
||||
if (attStatus || val(getField(r, '上班时间')) || val(getField(r, '下班时间'))) {
|
||||
await prisma.attendanceRecord.upsert({
|
||||
where: { employeeId_date: { employeeId: emp.id, date } },
|
||||
create: { orgId, employeeId: emp.id, date, status: statusMap[attStatus] || 'NORMAL', checkInTime: val(getField(r, '上班时间')) || null, checkOutTime: val(getField(r, '下班时间')) || null, remark: val(getField(r, '备注')) || null, createdBy: userId },
|
||||
update: { status: statusMap[attStatus] || 'NORMAL', checkInTime: val(getField(r, '上班时间')) || null, checkOutTime: val(getField(r, '下班时间')) || null, remark: val(getField(r, '备注')) || null },
|
||||
})
|
||||
result.attendance++
|
||||
}
|
||||
|
||||
// 加班部分
|
||||
const wdHours = num(getField(r, '工作日加班时长'))
|
||||
const weHours = num(getField(r, '休息日加班时长'))
|
||||
const hoHours = num(getField(r, '法定节假日加班时长'))
|
||||
if (wdHours > 0 || weHours > 0 || hoHours > 0) {
|
||||
const otMonth = dateToMonth(date)
|
||||
let monthlyWage = 0
|
||||
try { monthlyWage = Number(decrypt(emp.monthlySalary)) || 0 } catch { monthlyWage = Number(emp.monthlySalary) || 0 }
|
||||
const totalPay = calcOvertimePay(monthlyWage, wdHours, weHours, hoHours)
|
||||
await prisma.overtimeRecord.upsert({
|
||||
where: { employeeId_month: { employeeId: emp.id, month: otMonth } },
|
||||
create: { orgId, employeeId: emp.id, month: otMonth, weekdayHours: wdHours, weekendHours: weHours, holidayHours: hoHours, totalPay } as any,
|
||||
update: {
|
||||
weekdayHours: { increment: wdHours },
|
||||
weekendHours: { increment: weHours },
|
||||
holidayHours: { increment: hoHours },
|
||||
totalPay: { increment: totalPay },
|
||||
},
|
||||
})
|
||||
result.overtime++
|
||||
}
|
||||
} catch (e: any) { result.errors.push(`第${i + 2}行:${e?.message || '导入失败'}`) }
|
||||
}
|
||||
} else {
|
||||
// 向后兼容:独立Sheet
|
||||
if (attSheet) {
|
||||
const rows = XLSX.utils.sheet_to_json(attSheet)
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
try {
|
||||
const emp = findEmp(r)
|
||||
if (!emp) { result.errors.push(`考勤第${i + 2}行:找不到员工「${val(getField(r, '姓名'))}」`); continue }
|
||||
const date = parseDate(getField(r, '日期'))
|
||||
if (!date) { result.errors.push(`考勤第${i + 2}行:日期格式错误`); continue }
|
||||
await prisma.attendanceRecord.upsert({
|
||||
where: { employeeId_date: { employeeId: emp.id, date } },
|
||||
create: { orgId, employeeId: emp.id, date, status: statusMap[val(getField(r, '考勤状态'))] || 'NORMAL', checkInTime: val(getField(r, '上班时间')) || null, checkOutTime: val(getField(r, '下班时间')) || null, remark: val(getField(r, '备注')) || null, createdBy: userId },
|
||||
update: { status: statusMap[val(getField(r, '考勤状态'))] || 'NORMAL', checkInTime: val(getField(r, '上班时间')) || null, checkOutTime: val(getField(r, '下班时间')) || null, remark: val(getField(r, '备注')) || null },
|
||||
})
|
||||
result.attendance++
|
||||
} catch (e: any) { result.errors.push(`考勤第${i + 2}行:${e?.message || '导入失败'}`) }
|
||||
}
|
||||
}
|
||||
|
||||
if (otSheet) {
|
||||
const rows = XLSX.utils.sheet_to_json(otSheet)
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
try {
|
||||
const emp = findEmp(r)
|
||||
if (!emp) { result.errors.push(`加班第${i + 2}行:找不到员工「${val(getField(r, '姓名'))}」`); continue }
|
||||
const date = parseDate(getField(r, '日期'))
|
||||
if (!date) { result.errors.push(`加班第${i + 2}行:日期格式错误`); continue }
|
||||
const otMonth = dateToMonth(date)
|
||||
const hours = num(getField(r, '加班时长'))
|
||||
const otType = val(getField(r, '加班类型')) || '工作日加班'
|
||||
const wdHours = num(getField(r, '工作日加班时长')) || (otType.includes('工作日') ? hours : 0)
|
||||
const weHours = num(getField(r, '休息日加班时长')) || (otType.includes('休息日') ? hours : 0)
|
||||
const hoHours = num(getField(r, '法定节假日加班时长')) || (otType.includes('法定') ? hours : 0)
|
||||
let monthlyWage = 0
|
||||
try { monthlyWage = Number(decrypt(emp.monthlySalary)) || 0 } catch { monthlyWage = Number(emp.monthlySalary) || 0 }
|
||||
const totalPay = calcOvertimePay(monthlyWage, wdHours, weHours, hoHours)
|
||||
await prisma.overtimeRecord.upsert({
|
||||
where: { employeeId_month: { employeeId: emp.id, month: otMonth } },
|
||||
create: { orgId, employeeId: emp.id, month: otMonth, weekdayHours: wdHours, weekendHours: weHours, holidayHours: hoHours, totalPay } as any,
|
||||
update: {
|
||||
weekdayHours: { increment: wdHours },
|
||||
weekendHours: { increment: weHours },
|
||||
holidayHours: { increment: hoHours },
|
||||
totalPay: { increment: totalPay },
|
||||
},
|
||||
})
|
||||
result.overtime++
|
||||
} catch (e: any) { result.errors.push(`加班第${i + 2}行:${e?.message || '导入失败'}`) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,6 +748,27 @@ router.post('/monthly', authMiddleware, requireAdmin, upload.single('file'), asy
|
||||
}
|
||||
}
|
||||
|
||||
// 违纪记录
|
||||
const discSheet = wb.Sheets['违纪记录']
|
||||
if (discSheet) {
|
||||
const rows = XLSX.utils.sheet_to_json(discSheet)
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
try {
|
||||
const emp = findEmp(r)
|
||||
if (!emp) { result.errors.push(`违纪第${i + 2}行:找不到员工「${val(getField(r, '姓名'))}」`); continue }
|
||||
const date = parseDate(getField(r, '日期'))
|
||||
if (!date) { result.errors.push(`违纪第${i + 2}行:日期格式错误`); continue }
|
||||
const typeMap: any = { '迟到': 'LATE', '旷工': 'ABSENT', '不服从': 'INSUBORDINATION', '违纪': 'MISCONDUCT', '违规': 'VIOLATE_POLICY', '其他': 'OTHER' }
|
||||
const actMap: any = { '口头警告': 'ORAL_WARNING', '书面警告': 'WRITTEN_WARNING', '扣款': 'DEDUCTION', '降级': 'DEMOTION', '辞退': 'TERMINATION' }
|
||||
await prisma.disciplinaryRecord.create({
|
||||
data: { orgId, employeeId: emp.id, violationDate: date, violationType: typeMap[val(getField(r, '违纪类型'))] || 'OTHER', description: val(getField(r, '描述')) || '', action: actMap[val(getField(r, '处罚'))] || 'ORAL_WARNING', createdBy: userId },
|
||||
})
|
||||
result.discipline++
|
||||
} catch (e: any) { result.errors.push(`违纪第${i + 2}行:${e?.message || '导入失败'}`) }
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
@@ -694,11 +778,25 @@ router.post('/monthly', authMiddleware, requireAdmin, upload.single('file'), asy
|
||||
router.get('/monthly-template', authMiddleware, async (_req: AuthRequest, res: Response) => {
|
||||
const wb = XLSX.utils.book_new()
|
||||
|
||||
const attData = [{ '姓名': '张三', '身份证号': '110101199001011234', '日期': '2024-06-01', '考勤状态': '正常', '上班时间': '09:00', '下班时间': '18:00', '备注': '' }]
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(attData), '考勤记录')
|
||||
|
||||
const otData = [{ '姓名': '张三', '身份证号': '110101199001011234', '日期': '2024-06-15', '工作日加班时长': 2, '休息日加班时长': 0, '法定节假日加班时长': 0, '加班时长': 2, '加班类型': '工作日加班' }]
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(otData), '加班记录')
|
||||
// 合并考勤+加班为一个Sheet,减少重复录入姓名身份证号
|
||||
const attOtData = [{
|
||||
'姓名': '张三',
|
||||
'身份证号': '110101199001011234',
|
||||
'日期': '2024-06-01',
|
||||
'考勤状态': '正常',
|
||||
'上班时间': '09:00',
|
||||
'下班时间': '18:00',
|
||||
'工作日加班时长': 0,
|
||||
'休息日加班时长': 0,
|
||||
'法定节假日加班时长': 0,
|
||||
'备注': '',
|
||||
}]
|
||||
const attOtWs = XLSX.utils.json_to_sheet(attOtData)
|
||||
attOtWs['!cols'] = [
|
||||
{ wch: 10 }, { wch: 20 }, { wch: 12 }, { wch: 10 }, { wch: 8 }, { wch: 8 },
|
||||
{ wch: 14 }, { wch: 14 }, { wch: 16 }, { wch: 12 },
|
||||
]
|
||||
XLSX.utils.book_append_sheet(wb, attOtWs, '考勤与加班')
|
||||
|
||||
const salaryData = [{ '姓名': '张三', '身份证号': '110101199001011234', '调整后月薪': 12000, '生效日期': '2024-06-01', '调薪原因': '年度调薪' }]
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(salaryData), '薪资调整')
|
||||
@@ -709,9 +807,12 @@ router.get('/monthly-template', authMiddleware, async (_req: AuthRequest, res: R
|
||||
const hfData = [{ '姓名': '张三', '身份证号': '110101199001011234', '变动类型': '调基', '缴费基数': 12000 }]
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(hfData), '公积金变动')
|
||||
|
||||
const discData = [{ '姓名': '张三', '身份证号': '110101199001011234', '日期': '2024-06-10', '违纪类型': '警告', '描述': '迟到', '处罚': '口头警告' }]
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(discData), '违纪记录')
|
||||
|
||||
const buf = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' })
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
res.setHeader('Content-Disposition', contentDisposition('月度增减员导入模板.xlsx'))
|
||||
res.setHeader('Content-Disposition', contentDisposition('考勤月度导入模板.xlsx'))
|
||||
res.send(buf)
|
||||
})
|
||||
|
||||
|
||||
@@ -129,6 +129,88 @@ router.put('/overtime/:id', async (req: AuthRequest, res: Response, next: NextFu
|
||||
}
|
||||
})
|
||||
|
||||
// 从考勤记录同步加班工时
|
||||
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({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { requireAdmin } from '../middleware/rbac'
|
||||
import { z } from 'zod'
|
||||
import prisma from '../lib/prisma'
|
||||
import {
|
||||
@@ -90,24 +91,26 @@ router.delete('/:id', authMiddleware, async (req: AuthRequest, res: Response, ne
|
||||
router.get('/:id/read-stats', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const policy = await prisma.policyDocument.findFirst({ where: { id: req.params.id, orgId }, select: { id: true } })
|
||||
const policy = await prisma.policyDocument.findFirst({ where: { id: req.params.id, orgId }, select: { id: true, title: true } })
|
||||
if (!policy) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '制度不存在' } })
|
||||
}
|
||||
const [totalEmployees, readRecords] = await Promise.all([
|
||||
prisma.employee.count({ where: { orgId, status: 'ACTIVE' } }),
|
||||
const [allEmployees, readRecords] = await Promise.all([
|
||||
prisma.employee.findMany({ where: { orgId, status: 'ACTIVE' }, select: { id: true, name: true, department: true }, orderBy: { name: 'asc' } }),
|
||||
prisma.policyReadRecord.findMany({
|
||||
where: { policyId: req.params.id, orgId },
|
||||
include: { employee: { select: { id: true, name: true, department: true } } },
|
||||
orderBy: { readAt: 'desc' },
|
||||
}),
|
||||
])
|
||||
const readEmpIds = new Set(readRecords.map(r => r.employeeId))
|
||||
const unreadEmployees = allEmployees.filter(e => !readEmpIds.has(e.id))
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total: totalEmployees,
|
||||
total: allEmployees.length,
|
||||
readCount: readRecords.length,
|
||||
unreadCount: totalEmployees - readRecords.length,
|
||||
unreadCount: unreadEmployees.length,
|
||||
records: readRecords.map(r => ({
|
||||
employeeId: r.employeeId,
|
||||
employeeName: r.employee.name,
|
||||
@@ -115,6 +118,11 @@ router.get('/:id/read-stats', authMiddleware, async (req: AuthRequest, res: Resp
|
||||
readAt: r.readAt.toISOString(),
|
||||
ip: r.ip,
|
||||
})),
|
||||
unreadEmployees: unreadEmployees.map(e => ({
|
||||
employeeId: e.id,
|
||||
employeeName: e.name,
|
||||
department: e.department,
|
||||
})),
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
@@ -122,4 +130,43 @@ router.get('/:id/read-stats', authMiddleware, async (req: AuthRequest, res: Resp
|
||||
}
|
||||
})
|
||||
|
||||
/** 催办未签收员工 */
|
||||
router.post('/:id/remind', authMiddleware, requireAdmin, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const policy = await prisma.policyDocument.findFirst({ where: { id: req.params.id, orgId }, select: { id: true, title: true } })
|
||||
if (!policy) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '制度不存在' } })
|
||||
}
|
||||
const { employeeIds } = req.body as { employeeIds?: string[] }
|
||||
const readRecords = await prisma.policyReadRecord.findMany({ where: { policyId: req.params.id, orgId }, select: { employeeId: true } })
|
||||
const readEmpIds = new Set(readRecords.map(r => r.employeeId))
|
||||
const targetEmployees = await prisma.employee.findMany({
|
||||
where: {
|
||||
orgId, status: 'ACTIVE',
|
||||
id: employeeIds && employeeIds.length > 0 ? { in: employeeIds } : undefined,
|
||||
},
|
||||
select: { id: true, name: true },
|
||||
})
|
||||
const unreadEmployees = targetEmployees.filter(e => !readEmpIds.has(e.id))
|
||||
// 创建催办通知
|
||||
for (const emp of unreadEmployees) {
|
||||
await prisma.notificationLog.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: emp.id,
|
||||
type: 'POLICY_REMIND',
|
||||
title: `制度签收提醒:${policy.title}`,
|
||||
content: `您有一项制度「${policy.title}」尚未签收,请尽快完成阅读确认。`,
|
||||
channel: 'IN_APP',
|
||||
status: 'SENT',
|
||||
},
|
||||
}).catch(() => {})
|
||||
}
|
||||
res.json({ success: true, data: { reminded: unreadEmployees.length } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createEvidence } from '../services/evidence.service'
|
||||
import prisma from '../lib/prisma'
|
||||
import { decrypt, encrypt } from '../lib/crypto'
|
||||
import { getContractStatus } from '../services/contract.service'
|
||||
import { calcSocialInsurance, calcHousingFund } from '../services/payroll.service'
|
||||
import ExcelJS from 'exceljs'
|
||||
|
||||
const router = Router()
|
||||
@@ -120,6 +121,32 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
}),
|
||||
])
|
||||
|
||||
// 获取社保和公积金配置(按城市缓存)
|
||||
const currentMonth = new Date().toISOString().slice(0, 7)
|
||||
const configCache = new Map<string, { social?: any; housing?: any }>()
|
||||
const getConfigsForCity = async (city?: string) => {
|
||||
const key = city || '_default'
|
||||
if (configCache.has(key)) return configCache.get(key)!
|
||||
const cityWhere = city ? { orgId: req.user!.orgId, city } : { orgId: req.user!.orgId }
|
||||
const [socialCfg, housingCfg] = await Promise.all([
|
||||
prisma.socialInsuranceConfig.findFirst({
|
||||
where: { ...cityWhere, effectiveFrom: { lte: currentMonth }, OR: [{ effectiveTo: null }, { effectiveTo: { gte: currentMonth } }] },
|
||||
orderBy: { effectiveFrom: 'desc' },
|
||||
}),
|
||||
prisma.housingFundConfig.findFirst({
|
||||
where: { ...cityWhere, effectiveFrom: { lte: currentMonth }, OR: [{ effectiveTo: null }, { effectiveTo: { gte: currentMonth } }] },
|
||||
orderBy: { effectiveFrom: 'desc' },
|
||||
}),
|
||||
])
|
||||
const result = { social: socialCfg, housing: housingCfg }
|
||||
configCache.set(key, result)
|
||||
return result
|
||||
}
|
||||
|
||||
// 预加载所有涉及城市的配置
|
||||
const cities = [...new Set(employees.map((e) => e.city).filter(Boolean))] as string[]
|
||||
await Promise.all(cities.map((c) => getConfigsForCity(c)))
|
||||
|
||||
// 计算动态状态和合同状态
|
||||
let result = employees.map((e) => {
|
||||
const latestContract = e.contracts[0] || null
|
||||
@@ -173,6 +200,20 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
idCardMasked,
|
||||
idCardNumber: safeDecryptStr(e.idCardNumber),
|
||||
monthlySalary: safeDecrypt(e.monthlySalary),
|
||||
socialInsBase: e.socialInsBase,
|
||||
housingFundBase: e.housingFundBase,
|
||||
socialInsCalc: (() => {
|
||||
const cfgs = configCache.get(e.city || '_default')
|
||||
if (!cfgs?.social || !e.socialInsBase) return null
|
||||
const r = calcSocialInsurance(e.socialInsBase, cfgs.social)
|
||||
return { socialEmp: r.socialEmp, socialOrg: r.socialOrg }
|
||||
})(),
|
||||
housingFundCalc: (() => {
|
||||
const cfgs = configCache.get(e.city || '_default')
|
||||
if (!cfgs?.housing || !e.housingFundBase) return null
|
||||
const r = calcHousingFund(e.housingFundBase, cfgs.housing)
|
||||
return { housingEmp: r.housingEmp, housingOrg: r.housingOrg }
|
||||
})(),
|
||||
isPregnant: e.isPregnant,
|
||||
isInMedicalPeriod: e.isInMedicalPeriod,
|
||||
isWorkInjured: e.isWorkInjured,
|
||||
@@ -767,7 +808,11 @@ router.get('/training/list', authMiddleware, async (req: AuthRequest, res, next)
|
||||
const page = parseInt(req.query.page as string) || 1
|
||||
const pageSize = parseInt(req.query.pageSize as string) || 20
|
||||
const keyword = (req.query.keyword as string) || ''
|
||||
const ackStatus = (req.query.ackStatus as string) || ''
|
||||
const where: any = { orgId }
|
||||
if (ackStatus) {
|
||||
where.ackStatus = ackStatus
|
||||
}
|
||||
if (keyword) {
|
||||
const employees = await prisma.employee.findMany({
|
||||
where: { orgId, name: { contains: keyword } },
|
||||
@@ -789,6 +834,35 @@ router.get('/training/list', authMiddleware, async (req: AuthRequest, res, next)
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 培训记录催办(发送通知给未签收员工)
|
||||
router.post('/training/remind/:recordId', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const record = await prisma.trainingRecord.findFirst({
|
||||
where: { id: req.params.recordId, orgId },
|
||||
include: { employee: { select: { id: true, name: true, department: true, phone: true } } },
|
||||
})
|
||||
if (!record) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '培训记录不存在' } })
|
||||
}
|
||||
if (record.ackStatus !== 'PENDING') {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '仅待签收记录可催办' } })
|
||||
}
|
||||
// 记录催办通知日志
|
||||
await prisma.notificationLog.create({
|
||||
data: {
|
||||
orgId,
|
||||
type: 'TRAINING_REMIND',
|
||||
title: `培训签收催办:${record.topic}`,
|
||||
content: `员工 ${record.employee.name}(${record.employee.department})的培训记录「${record.topic}」尚未签收,请尽快完成签收。`,
|
||||
channel: 'SYSTEM',
|
||||
status: 'SENT',
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: { message: `已催办 ${record.employee.name} 签收「${record.topic}」` } })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 绩效记录列表(全员)
|
||||
router.get('/performance/list', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
@@ -1074,6 +1148,33 @@ router.post('/:employeeId/training', authMiddleware, async (req: AuthRequest, re
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 批量创建培训记录
|
||||
router.post('/training/batch', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { employeeIds, trainingDate, topic, content, trainer, duration, remark } = req.body
|
||||
if (!employeeIds || !Array.isArray(employeeIds) || employeeIds.length === 0) {
|
||||
return res.json({ success: false, error: { code: 'VALIDATION_ERROR', message: '请至少选择一名员工' } })
|
||||
}
|
||||
const results = await Promise.all(employeeIds.map((empId: string) =>
|
||||
prisma.trainingRecord.create({
|
||||
data: {
|
||||
orgId: req.user!.orgId,
|
||||
employeeId: empId,
|
||||
trainingDate: new Date(trainingDate),
|
||||
topic,
|
||||
content,
|
||||
trainer,
|
||||
duration: duration || 0,
|
||||
ackStatus: 'PENDING',
|
||||
remark,
|
||||
createdBy: req.user!.id,
|
||||
},
|
||||
})
|
||||
))
|
||||
res.json({ success: true, data: { count: results.length } })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.put('/:employeeId/training/:recordId', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { trainingDate, topic, content, trainer, duration, ackStatus, ackDate, attachmentUrl, remark } = req.body
|
||||
@@ -1124,29 +1225,35 @@ router.get('/:employeeId/performance', authMiddleware, async (req: AuthRequest,
|
||||
|
||||
router.post('/:employeeId/performance', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { period, score, grade, result, summary, improvementPlan, employeeAck, ackDate, reviewer } = req.body
|
||||
const { period, periodType, score, grade, result, summary, improvementPlan, employeeAck, ackDate, reviewer, templateId, dimensionScores } = req.body
|
||||
const record = await prisma.performanceRecord.upsert({
|
||||
where: { employeeId_period: { employeeId: req.params.employeeId, period } },
|
||||
create: {
|
||||
orgId: req.user!.orgId,
|
||||
employeeId: req.params.employeeId,
|
||||
period,
|
||||
periodType: periodType || 'MONTHLY',
|
||||
score: score || 0,
|
||||
grade: grade || 'B',
|
||||
result: result || 'QUALIFIED',
|
||||
summary,
|
||||
improvementPlan,
|
||||
templateId: templateId || null,
|
||||
dimensionScores: dimensionScores || undefined,
|
||||
employeeAck: employeeAck || false,
|
||||
ackDate: ackDate ? new Date(ackDate) : null,
|
||||
reviewer,
|
||||
createdBy: req.user!.id,
|
||||
},
|
||||
update: {
|
||||
periodType,
|
||||
score,
|
||||
grade,
|
||||
result,
|
||||
summary,
|
||||
improvementPlan,
|
||||
templateId: templateId || null,
|
||||
dimensionScores: dimensionScores || undefined,
|
||||
employeeAck,
|
||||
ackDate: ackDate ? new Date(ackDate) : null,
|
||||
reviewer,
|
||||
@@ -1159,7 +1266,7 @@ router.post('/:employeeId/performance', authMiddleware, async (req: AuthRequest,
|
||||
|
||||
router.put('/:employeeId/performance/:recordId', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { period, score, grade, result, summary, improvementPlan, employeeAck, ackDate, reviewer } = req.body
|
||||
const { period, periodType, score, grade, result, summary, improvementPlan, employeeAck, ackDate, reviewer, templateId, dimensionScores } = req.body
|
||||
const record = await prisma.performanceRecord.findFirst({
|
||||
where: { id: req.params.recordId, orgId: req.user!.orgId },
|
||||
})
|
||||
@@ -1168,11 +1275,14 @@ router.put('/:employeeId/performance/:recordId', authMiddleware, async (req: Aut
|
||||
where: { id: req.params.recordId },
|
||||
data: {
|
||||
period,
|
||||
periodType,
|
||||
score,
|
||||
grade,
|
||||
result,
|
||||
summary,
|
||||
improvementPlan,
|
||||
templateId: templateId || null,
|
||||
dimensionScores: dimensionScores || undefined,
|
||||
employeeAck,
|
||||
ackDate: ackDate ? new Date(ackDate) : null,
|
||||
reviewer,
|
||||
@@ -1193,6 +1303,72 @@ router.delete('/:employeeId/performance/:recordId', authMiddleware, async (req:
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// ========== 绩效模板 CRUD ==========
|
||||
|
||||
// 获取模板列表
|
||||
router.get('/performance/templates', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const templates = await prisma.performanceTemplate.findMany({
|
||||
where: { orgId: req.user!.orgId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
res.json({ success: true, data: templates })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 创建模板
|
||||
router.post('/performance/templates', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { name, description, dimensions, gradeRules, isDefault } = req.body
|
||||
if (!name || !dimensions || !Array.isArray(dimensions)) {
|
||||
return res.json({ success: false, error: { code: 'VALIDATION_ERROR', message: '模板名称和考核维度为必填' } })
|
||||
}
|
||||
// 如果设为默认,先取消其他默认
|
||||
if (isDefault) {
|
||||
await prisma.performanceTemplate.updateMany({ where: { orgId: req.user!.orgId, isDefault: true }, data: { isDefault: false } })
|
||||
}
|
||||
const template = await prisma.performanceTemplate.create({
|
||||
data: {
|
||||
orgId: req.user!.orgId,
|
||||
name,
|
||||
description,
|
||||
dimensions,
|
||||
gradeRules: gradeRules || undefined,
|
||||
isDefault: isDefault || false,
|
||||
createdBy: req.user!.id,
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: template })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 更新模板
|
||||
router.put('/performance/templates/:id', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { name, description, dimensions, gradeRules, isDefault } = req.body
|
||||
const existing = await prisma.performanceTemplate.findFirst({ where: { id: req.params.id, orgId: req.user!.orgId } })
|
||||
if (!existing) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模板不存在' } })
|
||||
if (isDefault) {
|
||||
await prisma.performanceTemplate.updateMany({ where: { orgId: req.user!.orgId, isDefault: true, id: { not: req.params.id } }, data: { isDefault: false } })
|
||||
}
|
||||
const updated = await prisma.performanceTemplate.update({
|
||||
where: { id: req.params.id },
|
||||
data: { name, description, dimensions, gradeRules: gradeRules || undefined, isDefault },
|
||||
})
|
||||
res.json({ success: true, data: updated })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 删除模板
|
||||
router.delete('/performance/templates/:id', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const existing = await prisma.performanceTemplate.findFirst({ where: { id: req.params.id, orgId: req.user!.orgId } })
|
||||
if (!existing) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模板不存在' } })
|
||||
await prisma.performanceTemplate.delete({ where: { id: req.params.id } })
|
||||
res.json({ success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// ========== 调薪/调部门 API ==========
|
||||
|
||||
function dateToMonth(date: Date): string {
|
||||
|
||||
@@ -49,10 +49,16 @@ router.get('/:id/download', authMiddleware, async (req: AuthRequest, res: Respon
|
||||
if (!template) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模板不存在' } })
|
||||
}
|
||||
const htmlContent = `<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40">
|
||||
<head><meta charset="utf-8"><title>${template.name}</title>
|
||||
<style>
|
||||
body { font-family: SimSun, serif; font-size: 14pt; line-height: 2; }
|
||||
</style></head>
|
||||
<body>${template.content}</body></html>`
|
||||
const encoded = encodeURIComponent(template.name + '.doc')
|
||||
res.setHeader('Content-Type', 'application/msword')
|
||||
res.setHeader('Content-Type', 'application/msword; charset=utf-8')
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${encoded}"; filename*=UTF-8''${encoded}`)
|
||||
res.send(template.content)
|
||||
res.send(htmlContent)
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
|
||||
@@ -340,4 +340,24 @@ router.get('/draft/:id/validate-step', authMiddleware, async (req: AuthRequest,
|
||||
}
|
||||
})
|
||||
|
||||
// 删除草稿(仅允许 DRAFT 和 CANCELLED 状态)
|
||||
router.delete('/draft/:id', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const record = await prisma.terminationRecord.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!record) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } })
|
||||
}
|
||||
if (record.status !== 'DRAFT' && record.status !== 'CANCELLED') {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '仅草稿或已撤销的记录可以删除' } })
|
||||
}
|
||||
await prisma.terminationRecord.delete({ where: { id: req.params.id } })
|
||||
await auditLog(req, 'DELETE_DRAFT', 'TERMINATION_RECORD', req.params.id, { employeeId: record.employeeId })
|
||||
res.json({ success: true })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
Reference in New Issue
Block a user