import { Router, Request, Response, NextFunction } from 'express' import { authMiddleware, AuthRequest } from '../middleware/auth' import { auditLog } from '../middleware/auditLog' import prisma from '../lib/prisma' import { decrypt } from '../lib/crypto' import { getContractStatus } from '../services/contract.service' const router = Router() function safeDecrypt(encrypted: string): number { try { if (!encrypted || !encrypted.includes(':')) return Number(encrypted) || 0 return Number(decrypt(encrypted)) } catch { return Number(encrypted) || 0 } } // ========== 花名册聚合 API ========== // 花名册列表(含汇总信息) router.get('/', authMiddleware, async (req: AuthRequest, res, next) => { try { const employees = await prisma.employee.findMany({ where: { orgId: req.user!.orgId }, orderBy: { createdAt: 'desc' }, include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 }, _count: { select: { disciplinaryRecords: true, attendanceRecords: true, trainingRecords: true, performanceRecords: true, payslips: true, overtimeRecords: true, }, }, }, }) const result = employees.map((e) => { const latestContract = e.contracts[0] || null const contractInfo = latestContract ? getContractStatus({ signDate: latestContract.signDate, startDate: latestContract.startDate, endDate: latestContract.endDate, contractType: latestContract.contractType, hireDate: e.hireDate, }) : getContractStatus({ signDate: null, startDate: e.hireDate, endDate: null, contractType: 'UNSIGNED', hireDate: e.hireDate, }) return { id: e.id, name: e.name, department: e.department, status: e.status, hireDate: e.hireDate, gender: e.gender, phone: e.phone, monthlySalary: safeDecrypt(e.monthlySalary), latestContract, contractStatus: contractInfo.status, contractStatusText: contractInfo.statusText, riskLevel: contractInfo.riskLevel, counts: e._count, } }) res.json({ success: true, data: result }) } catch (err) { next(err) } }) // 员工完整档案(花名册详情) router.get('/:id/profile', authMiddleware, async (req: AuthRequest, res, next) => { try { const employee = await prisma.employee.findFirst({ where: { id: req.params.id, orgId: req.user!.orgId }, include: { contracts: { orderBy: { createdAt: 'desc' } }, payslips: { orderBy: { month: 'desc' } }, overtimeRecords: { orderBy: { month: 'desc' } }, disciplinaryRecords: { orderBy: { violationDate: 'desc' } }, attendanceRecords: { orderBy: { date: 'desc' }, take: 90 }, trainingRecords: { orderBy: { trainingDate: 'desc' } }, performanceRecords: { orderBy: { period: 'desc' } }, terminations: { orderBy: { createdAt: 'desc' } }, attachments: true, }, }) if (!employee) { return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } }) } const { monthlySalary, bankAccount, idCardNumber, ...rest } = employee res.json({ success: true, data: { ...rest, monthlySalary: safeDecrypt(monthlySalary), bankAccount: bankAccount ? safeDecrypt(bankAccount).toString() : null, idCardNumber: idCardNumber ? safeDecrypt(idCardNumber).toString() : null, }, }) } catch (err) { next(err) } }) // 仲裁证据链导出 router.get('/:id/evidence-chain', authMiddleware, async (req: AuthRequest, res, next) => { try { const employee = await prisma.employee.findFirst({ where: { id: req.params.id, orgId: req.user!.orgId }, include: { contracts: { orderBy: { createdAt: 'desc' } }, payslips: { orderBy: { month: 'desc' } }, overtimeRecords: { orderBy: { month: 'desc' } }, disciplinaryRecords: { orderBy: { violationDate: 'desc' } }, attendanceRecords: { orderBy: { date: 'desc' } }, trainingRecords: { orderBy: { trainingDate: 'desc' } }, performanceRecords: { orderBy: { period: 'desc' } }, terminations: true, }, }) if (!employee) { return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } }) } const evidence: any[] = [] const empName = employee.name const empDept = employee.department const hireDate = employee.hireDate.toISOString().slice(0, 10) // 1. 劳动关系证据 evidence.push({ category: '劳动关系', title: '入职登记', date: hireDate, description: `${empName}于${hireDate}入职${empDept},建立劳动关系。`, evidenceType: 'EMPLOYMENT', }) employee.contracts.forEach((c) => { evidence.push({ category: '劳动关系', title: `劳动合同(${c.contractType === 'FIXED' ? '固定期限' : c.contractType === 'UNFIXED' ? '无固定期限' : '未签订'})`, date: c.signDate ? c.signDate.toISOString().slice(0, 10) : c.startDate.toISOString().slice(0, 10), description: `合同期限:${c.startDate.toISOString().slice(0, 10)} 至 ${c.endDate ? c.endDate.toISOString().slice(0, 10) : '无固定期限'},试用期${c.probationMonths}个月,试用期工资¥${c.probationSalary}。`, evidenceType: 'CONTRACT', signed: !!c.signDate, }) }) // 2. 薪酬证据 employee.payslips.forEach((p) => { evidence.push({ category: '薪酬发放', title: `${p.month}月工资条`, date: p.month, description: `基本工资¥${p.baseSalary.toFixed(2)},加班费¥${p.overtimePay.toFixed(2)},津贴¥${p.allowance.toFixed(2)},扣款¥${p.deduction.toFixed(2)},应发合计¥${p.totalPay.toFixed(2)}。${p.confirmedAt ? '员工已确认。' : '员工未确认。'}`, evidenceType: 'PAYSLIP', confirmed: !!p.confirmedAt, }) }) employee.overtimeRecords.forEach((o) => { if (o.totalPay > 0) { evidence.push({ category: '薪酬发放', title: `${o.month}月加班费记录`, date: o.month, description: `工作日加班${o.weekdayHours}h,休息日加班${o.weekendHours}h,节假日加班${o.holidayHours}h,加班费合计¥${o.totalPay.toFixed(2)}。`, evidenceType: 'OVERTIME', }) } }) // 3. 考勤证据 const abnormalAttendance = employee.attendanceRecords.filter((a) => a.status !== 'NORMAL') abnormalAttendance.forEach((a) => { const statusMap: Record = { LATE: '迟到', EARLY_LEAVE: '早退', ABSENT: '旷工', LEAVE: '请假', BUSINESS_TRIP: '出差' } evidence.push({ category: '考勤记录', title: `${a.date.toISOString().slice(0, 10)} 考勤异常`, date: a.date.toISOString().slice(0, 10), description: `状态:${statusMap[a.status] || a.status}${a.lateMinutes ? `,迟到${a.lateMinutes}分钟` : ''}${a.earlyMinutes ? `,早退${a.earlyMinutes}分钟` : ''}。${a.remark || ''}`, evidenceType: 'ATTENDANCE', }) }) // 4. 违纪证据 employee.disciplinaryRecords.forEach((d) => { const typeMap: Record = { LATE: '迟到', ABSENT: '旷工', INSUBORDINATION: '不服从管理', MISCONDUCT: '违纪', VIOLATE_POLICY: '违反规章制度', OTHER: '其他' } const actionMap: Record = { ORAL_WARNING: '口头警告', WRITTEN_WARNING: '书面警告', DEDUCTION: '扣款', DEMOTION: '降职', TERMINATION: '解除劳动合同' } evidence.push({ category: '违纪处理', title: `${d.violationDate.toISOString().slice(0, 10)} ${typeMap[d.violationType] || d.violationType}`, date: d.violationDate.toISOString().slice(0, 10), description: `违纪事实:${d.description}。处理结果:${actionMap[d.action] || d.action}。${d.employeeAck ? `员工已签字确认(${d.ackDate ? d.ackDate.toISOString().slice(0, 10) : ''})。` : '员工未签字。'}${d.witness ? `见证人:${d.witness}。` : ''}`, evidenceType: 'DISCIPLINARY', acknowledged: d.employeeAck, }) }) // 5. 培训签收证据 employee.trainingRecords.forEach((t) => { const ackMap: Record = { PENDING: '待签收', SIGNED: '已签收', REFUSED: '拒绝签收' } evidence.push({ category: '培训签收', title: `${t.trainingDate.toISOString().slice(0, 10)} ${t.topic}`, date: t.trainingDate.toISOString().slice(0, 10), description: `培训主题:${t.topic}。时长:${t.duration}小时。${t.content ? `内容:${t.content}。` : ''}签收状态:${ackMap[t.ackStatus] || t.ackStatus}。`, evidenceType: 'TRAINING', acknowledged: t.ackStatus === 'SIGNED', }) }) // 6. 绩效证据 employee.performanceRecords.forEach((p) => { const resultMap: Record = { EXCELLENT: '优秀', QUALIFIED: '合格', NEED_IMPROVE: '需改进', UNQUALIFIED: '不胜任' } evidence.push({ category: '绩效考核', title: `${p.period} 绩效考核`, date: p.period, description: `得分:${p.score},等级:${p.grade},结果:${resultMap[p.result] || p.result}。${p.summary ? `评语:${p.summary}。` : ''}${p.improvementPlan ? `改进计划:${p.improvementPlan}。` : ''}${p.employeeAck ? '员工已签字确认。' : '员工未签字。'}`, evidenceType: 'PERFORMANCE', acknowledged: p.employeeAck, }) }) // 7. 解聘证据 employee.terminations.forEach((t) => { const reasonMap: Record = { NEGOTIATED: '协商解除', FAULT: '员工过错', NONFAULT: '非过错解除', LAYOFF: '经济性裁员', EXPIRED: '合同到期' } evidence.push({ category: '解聘记录', title: `${t.terminationDate.toISOString().slice(0, 10)} 解聘记录`, date: t.terminationDate.toISOString().slice(0, 10), description: `解聘原因:${reasonMap[t.reason] || t.reason}。经济补偿金:¥${t.compensation.toFixed(2)}。${t.remark || ''}`, evidenceType: 'TERMINATION', }) }) res.json({ success: true, data: { employee: { name: empName, department: empDept, hireDate, status: employee.status, gender: employee.gender, phone: employee.phone, }, evidence, summary: { total: evidence.length, signed: evidence.filter((e) => e.acknowledged === true).length, unsigned: evidence.filter((e) => e.acknowledged === false).length, }, }, }) } catch (err) { next(err) } }) // ========== 违纪记录 CRUD ========== router.get('/:employeeId/disciplinary', authMiddleware, async (req: AuthRequest, res, next) => { try { const records = await prisma.disciplinaryRecord.findMany({ where: { employeeId: req.params.employeeId, orgId: req.user!.orgId }, orderBy: { violationDate: 'desc' }, }) res.json({ success: true, data: records }) } catch (err) { next(err) } }) 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 record = await prisma.disciplinaryRecord.create({ data: { orgId: req.user!.orgId, employeeId: req.params.employeeId, violationDate: new Date(violationDate), violationType, description, severity: severity || 'WARNING', action: action || 'ORAL_WARNING', actionDetail, employeeAck: employeeAck || false, ackDate: ackDate ? new Date(ackDate) : null, ackMethod, witness, attachmentUrl, createdBy: req.user!.id, }, }) await auditLog(req, 'CREATE', 'DISCIPLINARY', record.id, { employeeId: req.params.employeeId }) res.json({ success: true, data: record }) } catch (err) { next(err) } }) 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 record = await prisma.disciplinaryRecord.findFirst({ where: { id: req.params.recordId, orgId: req.user!.orgId }, }) if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } }) const updated = await prisma.disciplinaryRecord.update({ where: { id: req.params.recordId }, data: { violationDate: violationDate ? new Date(violationDate) : undefined, violationType, description, severity, action, actionDetail, employeeAck, ackDate: ackDate ? new Date(ackDate) : null, ackMethod, witness, attachmentUrl, }, }) res.json({ success: true, data: updated }) } catch (err) { next(err) } }) router.delete('/:employeeId/disciplinary/:recordId', authMiddleware, async (req: AuthRequest, res, next) => { try { const record = await prisma.disciplinaryRecord.findFirst({ where: { id: req.params.recordId, orgId: req.user!.orgId }, }) if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } }) await prisma.disciplinaryRecord.delete({ where: { id: req.params.recordId } }) res.json({ success: true }) } catch (err) { next(err) } }) // ========== 考勤记录 CRUD ========== router.get('/:employeeId/attendance', authMiddleware, async (req: AuthRequest, res, next) => { try { const records = await prisma.attendanceRecord.findMany({ where: { employeeId: req.params.employeeId, orgId: req.user!.orgId }, orderBy: { date: 'desc' }, take: 90, }) res.json({ success: true, data: records }) } catch (err) { next(err) } }) router.post('/:employeeId/attendance', authMiddleware, async (req: AuthRequest, res, next) => { try { const { date, checkInTime, checkOutTime, status, lateMinutes, earlyMinutes, workHours, overtimeHours, remark } = req.body const record = await prisma.attendanceRecord.upsert({ where: { employeeId_date: { employeeId: req.params.employeeId, date: new Date(date) } }, create: { orgId: req.user!.orgId, employeeId: req.params.employeeId, date: new Date(date), checkInTime, checkOutTime, status: status || 'NORMAL', lateMinutes: lateMinutes || 0, earlyMinutes: earlyMinutes || 0, workHours: workHours || 0, overtimeHours: overtimeHours || 0, remark, createdBy: req.user!.id, }, update: { checkInTime, checkOutTime, status, lateMinutes, earlyMinutes, workHours, overtimeHours, remark, }, }) res.json({ success: true, data: record }) } catch (err) { next(err) } }) router.delete('/:employeeId/attendance/:recordId', authMiddleware, async (req: AuthRequest, res, next) => { try { const record = await prisma.attendanceRecord.findFirst({ where: { id: req.params.recordId, orgId: req.user!.orgId }, }) if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } }) await prisma.attendanceRecord.delete({ where: { id: req.params.recordId } }) res.json({ success: true }) } catch (err) { next(err) } }) // ========== 培训签收记录 CRUD ========== router.get('/:employeeId/training', authMiddleware, async (req: AuthRequest, res, next) => { try { const records = await prisma.trainingRecord.findMany({ where: { employeeId: req.params.employeeId, orgId: req.user!.orgId }, orderBy: { trainingDate: 'desc' }, }) res.json({ success: true, data: records }) } catch (err) { next(err) } }) router.post('/:employeeId/training', authMiddleware, async (req: AuthRequest, res, next) => { try { const { trainingDate, topic, content, trainer, duration, ackStatus, ackDate, attachmentUrl, remark } = req.body const record = await prisma.trainingRecord.create({ data: { orgId: req.user!.orgId, employeeId: req.params.employeeId, trainingDate: new Date(trainingDate), topic, content, trainer, duration: duration || 0, ackStatus: ackStatus || 'PENDING', ackDate: ackDate ? new Date(ackDate) : null, attachmentUrl, remark, createdBy: req.user!.id, }, }) await auditLog(req, 'CREATE', 'TRAINING', record.id, { employeeId: req.params.employeeId }) res.json({ success: true, data: record }) } 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 const record = await prisma.trainingRecord.findFirst({ where: { id: req.params.recordId, orgId: req.user!.orgId }, }) if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } }) const updated = await prisma.trainingRecord.update({ where: { id: req.params.recordId }, data: { trainingDate: trainingDate ? new Date(trainingDate) : undefined, topic, content, trainer, duration, ackStatus, ackDate: ackDate ? new Date(ackDate) : null, attachmentUrl, remark, }, }) res.json({ success: true, data: updated }) } catch (err) { next(err) } }) router.delete('/:employeeId/training/:recordId', authMiddleware, async (req: AuthRequest, res, next) => { try { const record = await prisma.trainingRecord.findFirst({ where: { id: req.params.recordId, orgId: req.user!.orgId }, }) if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } }) await prisma.trainingRecord.delete({ where: { id: req.params.recordId } }) res.json({ success: true }) } catch (err) { next(err) } }) // ========== 绩效记录 CRUD ========== router.get('/:employeeId/performance', authMiddleware, async (req: AuthRequest, res, next) => { try { const records = await prisma.performanceRecord.findMany({ where: { employeeId: req.params.employeeId, orgId: req.user!.orgId }, orderBy: { period: 'desc' }, }) res.json({ success: true, data: records }) } catch (err) { next(err) } }) router.post('/:employeeId/performance', authMiddleware, async (req: AuthRequest, res, next) => { try { const { period, score, grade, result, summary, improvementPlan, employeeAck, ackDate, reviewer } = 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, score: score || 0, grade: grade || 'B', result: result || 'QUALIFIED', summary, improvementPlan, employeeAck: employeeAck || false, ackDate: ackDate ? new Date(ackDate) : null, reviewer, createdBy: req.user!.id, }, update: { score, grade, result, summary, improvementPlan, employeeAck, ackDate: ackDate ? new Date(ackDate) : null, reviewer, }, }) await auditLog(req, 'CREATE', 'PERFORMANCE', record.id, { employeeId: req.params.employeeId }) res.json({ success: true, data: record }) } catch (err) { next(err) } }) 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 record = await prisma.performanceRecord.findFirst({ where: { id: req.params.recordId, orgId: req.user!.orgId }, }) if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } }) const updated = await prisma.performanceRecord.update({ where: { id: req.params.recordId }, data: { period, score, grade, result, summary, improvementPlan, employeeAck, ackDate: ackDate ? new Date(ackDate) : null, reviewer, }, }) res.json({ success: true, data: updated }) } catch (err) { next(err) } }) router.delete('/:employeeId/performance/:recordId', authMiddleware, async (req: AuthRequest, res, next) => { try { const record = await prisma.performanceRecord.findFirst({ where: { id: req.params.recordId, orgId: req.user!.orgId }, }) if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } }) await prisma.performanceRecord.delete({ where: { id: req.params.recordId } }) res.json({ success: true }) } catch (err) { next(err) } }) export default router