feat: 20260805 系统优化 - 身份证复制fallback/薪税日期筛选/社保版本修复/证据链导出/违纪证明/医疗期政策/绩效类型评级/合同作废/帮助更新
This commit is contained in:
@@ -18,6 +18,16 @@ function safeDecrypt(encrypted: string): number {
|
||||
}
|
||||
}
|
||||
|
||||
function safeDecryptStr(encrypted: string | null): string | null {
|
||||
if (!encrypted) return null
|
||||
try {
|
||||
if (!encrypted.includes(':')) return encrypted
|
||||
return decrypt(encrypted)
|
||||
} catch {
|
||||
return encrypted
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 花名册聚合 API ==========
|
||||
|
||||
// 获取部门列表(去重)
|
||||
@@ -161,7 +171,7 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
gender: e.gender,
|
||||
phone: e.phone,
|
||||
idCardMasked,
|
||||
idCardNumber: e.idCardNumber,
|
||||
idCardNumber: safeDecryptStr(e.idCardNumber),
|
||||
monthlySalary: safeDecrypt(e.monthlySalary),
|
||||
isPregnant: e.isPregnant,
|
||||
isInMedicalPeriod: e.isInMedicalPeriod,
|
||||
@@ -199,16 +209,11 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
result = result.filter((e) => e.contractStatus === contractStatus)
|
||||
}
|
||||
|
||||
// 身份证号后N位搜索:在内存中过滤(解密完整身份证号后匹配)
|
||||
// 身份证号后N位搜索:在内存中过滤(idCardNumber 已解密为明文)
|
||||
if (isIdCardSearch) {
|
||||
result = result.filter((e: any) => {
|
||||
if (!e.idCardNumber) return false
|
||||
try {
|
||||
const fullIdCard = decrypt(e.idCardNumber)
|
||||
return fullIdCard.endsWith(search!)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return String(e.idCardNumber).endsWith(search!)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -312,7 +317,7 @@ router.get('/:id/profile', authMiddleware, async (req: AuthRequest, res, next) =
|
||||
status: dynamicStatus,
|
||||
monthlySalary: safeDecrypt(monthlySalary),
|
||||
bankAccount: bankAccount ? safeDecrypt(bankAccount).toString() : null,
|
||||
idCardNumber: idCardNumber ? safeDecrypt(idCardNumber).toString() : null,
|
||||
idCardNumber: safeDecryptStr(idCardNumber),
|
||||
monthlyProcessRecords,
|
||||
},
|
||||
})
|
||||
@@ -736,13 +741,20 @@ router.get('/:id/evidence-chain/export', authMiddleware, async (req: AuthRequest
|
||||
wsRisk.getRow(1).font = { bold: true }
|
||||
risks.forEach((r, i) => wsRisk.addRow({ no: i + 1, ...r }))
|
||||
|
||||
const encodedName = encodeURIComponent(empName)
|
||||
const fullFileName = `${empName}_证据链.xlsx`
|
||||
const encodedName = encodeURIComponent(fullFileName)
|
||||
const asciiFallback = `evidence_chain_${employee.id.slice(-8)}.xlsx`
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${encodedName}_证据链.xlsx"; filename*=UTF-8''${encodedName}_证据链.xlsx`)
|
||||
await workbook.xlsx.write(res)
|
||||
res.end()
|
||||
} catch (err) {
|
||||
next(err)
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${asciiFallback}"; filename*=UTF-8''${encodedName}`)
|
||||
const buffer = await workbook.xlsx.writeBuffer()
|
||||
res.send(Buffer.from(buffer))
|
||||
} catch (err: any) {
|
||||
console.error('证据链导出失败:', err?.message || err)
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ success: false, error: { code: 'EXPORT_FAILED', message: `导出失败:${err?.message || '服务器错误'}` } })
|
||||
} else {
|
||||
res.end()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -919,6 +931,55 @@ router.delete('/:employeeId/disciplinary/:recordId', authMiddleware, async (req:
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 违纪确认证明导出
|
||||
router.get('/:employeeId/disciplinary/:recordId/certificate', authMiddleware, async (req: AuthRequest, res: Response, next) => {
|
||||
try {
|
||||
const record = await prisma.disciplinaryRecord.findFirst({
|
||||
where: { id: req.params.recordId, orgId: req.user!.orgId },
|
||||
include: { employee: true },
|
||||
})
|
||||
if (!record) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } })
|
||||
}
|
||||
const org = await prisma.organization.findUnique({ where: { id: req.user!.orgId } })
|
||||
const typeMap: Record<string, string> = { LATE: '迟到', ABSENT: '旷工', INSUBORDINATION: '不服从管理', MISCONDUCT: '违纪', VIOLATE_POLICY: '违反规章制度', OTHER: '其他' }
|
||||
const actionMap: Record<string, string> = { ORAL_WARNING: '口头警告', WRITTEN_WARNING: '书面警告', DEDUCTION: '扣款', DEMOTION: '降职', TERMINATION: '解除劳动合同' }
|
||||
const severityMap: Record<string, string> = { WARNING: '警告', SERIOUS: '严重', SEVERE: '重度' }
|
||||
|
||||
let idCard = ''
|
||||
try { if (record.employee.idCardNumber) idCard = decrypt(record.employee.idCardNumber) } catch { idCard = record.employee.idCardNumber || '' }
|
||||
|
||||
const content = `违纪确认证明
|
||||
|
||||
兹证明 ${record.employee.name}(身份证号:${idCard || '___'})系我单位员工,于 ${record.violationDate.toISOString().slice(0, 10)} 发生以下违纪行为:
|
||||
|
||||
违纪类型:${typeMap[record.violationType] || record.violationType}
|
||||
严重程度:${severityMap[record.severity] || record.severity}
|
||||
违纪事实:${record.description}
|
||||
处理结果:${actionMap[record.action] || record.action}${record.actionDetail ? `(${record.actionDetail})` : ''}
|
||||
|
||||
${record.employeeAck ? `该员工已于 ${record.ackDate ? new Date(record.ackDate).toISOString().slice(0, 10) : '___'} 签字确认上述违纪事实及处理结果。${record.witness ? `见证人:${record.witness}。` : ''}` : '该员工尚未签字确认。'}
|
||||
|
||||
特此证明。
|
||||
|
||||
${org?.name || ''}
|
||||
${new Date().toLocaleDateString('zh-CN')}`
|
||||
|
||||
const blob = Buffer.from('\ufeff' + content, 'utf8')
|
||||
const certFileName = `${record.employee.name}_违纪确认证明.doc`
|
||||
const encodedCertName = encodeURIComponent(certFileName)
|
||||
const asciiCertFallback = `disciplinary_cert_${record.id.slice(-8)}.doc`
|
||||
res.setHeader('Content-Type', 'application/msword;charset=utf-8')
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${asciiCertFallback}"; filename*=UTF-8''${encodedCertName}`)
|
||||
res.send(blob)
|
||||
} catch (err: any) {
|
||||
console.error('违纪确认证明导出失败:', err?.message || err)
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ success: false, error: { code: 'EXPORT_FAILED', message: `导出失败:${err?.message || '服务器错误'}` } })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// ========== 考勤记录 CRUD ==========
|
||||
|
||||
router.get('/:employeeId/attendance', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
|
||||
Reference in New Issue
Block a user