feat: 20260815 系统优化 - 全部31项问题修复(P0×6+P1×14+P2×9+P3×2)
P0紧急修复(6项): - 草稿保存完整恢复所有字段(含socialAvgWage) - 补偿金批次从compensationBreakdown读取 - 违法解除风险确认UI - 合同结束日期前后校验(前后端双保险) P1高优先级(14项): - 离职日期联动社保/公积金截止月(15号规则) - 合规检查+工作交接改为软阻断(生成待办) - 补偿月数(N/N+1/2N/自定义)+计算基数(近12月/合同/自定义) - 解聘并入花名册操作栏(类型选择跳转向导) - 合同续签开始日期自动推导(原合同结束日+1天) - 年龄合规筛查(童工阻断/未成年工/退休警告) - 编辑入职日期后状态联动(待入职↔在职) - 转正移植到花名册操作栏+薪资回写 - 男职工无法选择三期 P2体验优化(9项): - "劳动合同"调整为"用工关系" - 费用结算新增剩余年假折算(300%日工资) - 身份证号全域改为"证件号码"(前后端18个文件) - 手机号查重 - 开具证明+合同续签移植到花名册操作栏 - 批量转正+批量开具证明 - 去掉用工办理模块 P3规划(2项): - 组织架构+审批流(Department/Position/ApprovalFlow/ApprovalInstance) - 客服工作台(Ticket/ChatSession+SUPPORT角色) 新增模型: Department/Position/ApprovalFlow/ApprovalInstance/Ticket/TicketMessage/ChatSession/ChatMessage 新增字段: Employee.departmentId/supervisorId 新增角色: SUPPORT Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -18,17 +18,17 @@ function contentDisposition(filename: string): string {
|
||||
const router = Router()
|
||||
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } })
|
||||
|
||||
// 身份证号格式校验(18位正则 + 校验位算法)
|
||||
// 证件号码格式校验(18位正则 + 校验位算法)
|
||||
function validateIdCard(idCard: string): { valid: boolean; upgraded?: string; error?: string } {
|
||||
if (!idCard) return { valid: true }
|
||||
const s = idCard.trim()
|
||||
// 15位身份证号升级为18位
|
||||
// 15位证件号码升级为18位
|
||||
if (/^\d{15}$/.test(s)) {
|
||||
const upgraded = upgrade15To18(s)
|
||||
return { valid: true, upgraded }
|
||||
}
|
||||
if (!/^\d{17}[\dXx]$/.test(s)) {
|
||||
return { valid: false, error: '身份证号格式错误(应为18位)' }
|
||||
return { valid: false, error: '证件号码格式错误(应为18位)' }
|
||||
}
|
||||
// 校验位算法
|
||||
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
|
||||
@@ -36,7 +36,7 @@ function validateIdCard(idCard: string): { valid: boolean; upgraded?: string; er
|
||||
const sum = s.substring(0, 17).split('').reduce((acc, ch, i) => acc + parseInt(ch) * weights[i], 0)
|
||||
const expected = checkCodes[sum % 11]
|
||||
if (s.charAt(17).toUpperCase() !== expected) {
|
||||
return { valid: false, error: '身份证号校验位错误' }
|
||||
return { valid: false, error: '证件号码校验位错误' }
|
||||
}
|
||||
return { valid: true }
|
||||
}
|
||||
@@ -124,7 +124,7 @@ router.post('/excel/preview', authMiddleware, requireAdmin, upload.single('file'
|
||||
const rows = XLSX.utils.sheet_to_json(empSheet)
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
const row: any = { rowNo: i + 2, name: val(getField(r, '姓名')), department: val(getField(r, '部门')) || '未分配', hireDate: getField(r, '入职日期'), salary: num(getField(r, '月工资')), phone: val(getField(r, '手机号')), idCard: val(getField(r, '身份证号')), city: val(getField(r, '参保城市')) || null, status: 'normal', errors: [] as string[], warnings: [] as string[] }
|
||||
const row: any = { rowNo: i + 2, name: val(getField(r, '姓名')), department: val(getField(r, '部门')) || '未分配', hireDate: getField(r, '入职日期'), salary: num(getField(r, '月工资')), phone: val(getField(r, '手机号')), idCard: val(getField(r, '证件号码')), city: val(getField(r, '参保城市')) || null, status: 'normal', errors: [] as string[], warnings: [] as string[] }
|
||||
if (!row.name) { row.status = 'error'; row.errors.push('姓名为空') }
|
||||
const hireDate = parseDate(getField(r, '入职日期'))
|
||||
if (!hireDate) { row.status = 'error'; row.errors.push('入职日期格式错误') }
|
||||
@@ -144,8 +144,8 @@ router.post('/excel/preview', authMiddleware, requireAdmin, upload.single('file'
|
||||
const rows = XLSX.utils.sheet_to_json(contractSheet)
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
const row: any = { rowNo: i + 2, name: val(getField(r, '姓名')), idCard: val(getField(r, '身份证号')), contractType: val(getField(r, '合同类型')), startDate: getField(r, '合同开始日期'), endDate: getField(r, '合同结束日期'), status: 'normal', errors: [] as string[] }
|
||||
if (!row.name && !row.idCard) { row.status = 'error'; row.errors.push('姓名和身份证号都为空') }
|
||||
const row: any = { rowNo: i + 2, name: val(getField(r, '姓名')), idCard: val(getField(r, '证件号码')), contractType: val(getField(r, '合同类型')), startDate: getField(r, '合同开始日期'), endDate: getField(r, '合同结束日期'), status: 'normal', errors: [] as string[] }
|
||||
if (!row.name && !row.idCard) { row.status = 'error'; row.errors.push('姓名和证件号码都为空') }
|
||||
const sd = parseDate(getField(r, '合同开始日期'))
|
||||
if (!sd) { row.status = 'error'; row.errors.push('开始日期格式错误') }
|
||||
if (row.status === 'error') preview.errors.push({ sheet: '劳动合同', row: i + 2, name: row.name, errors: row.errors })
|
||||
@@ -159,8 +159,8 @@ router.post('/excel/preview', authMiddleware, requireAdmin, upload.single('file'
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
const otType = val(getField(r, '加班类型')) || '工作日加班'
|
||||
const row: any = { rowNo: i + 2, name: val(getField(r, '姓名')), idCard: val(getField(r, '身份证号')), date: getField(r, '日期'), hours: num(getField(r, '加班时长')), otType, status: 'normal', errors: [] as string[] }
|
||||
if (!row.name && !row.idCard) { row.status = 'error'; row.errors.push('姓名和身份证号都为空') }
|
||||
const row: any = { rowNo: i + 2, name: val(getField(r, '姓名')), idCard: val(getField(r, '证件号码')), date: getField(r, '日期'), hours: num(getField(r, '加班时长')), otType, status: 'normal', errors: [] as string[] }
|
||||
if (!row.name && !row.idCard) { row.status = 'error'; row.errors.push('姓名和证件号码都为空') }
|
||||
const dt = parseDate(getField(r, '日期'))
|
||||
if (!dt) { row.status = 'error'; row.errors.push('日期格式错误') }
|
||||
if (row.status === 'error') preview.errors.push({ sheet: '加班记录', row: i + 2, name: row.name, errors: row.errors })
|
||||
@@ -173,8 +173,8 @@ router.post('/excel/preview', authMiddleware, requireAdmin, upload.single('file'
|
||||
const rows = XLSX.utils.sheet_to_json(discSheet)
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
const row: any = { rowNo: i + 2, name: val(getField(r, '姓名')), idCard: val(getField(r, '身份证号')), date: getField(r, '日期'), violationType: val(getField(r, '违纪类型')), description: val(getField(r, '描述')), status: 'normal', errors: [] as string[] }
|
||||
if (!row.name && !row.idCard) { row.status = 'error'; row.errors.push('姓名和身份证号都为空') }
|
||||
const row: any = { rowNo: i + 2, name: val(getField(r, '姓名')), idCard: val(getField(r, '证件号码')), date: getField(r, '日期'), violationType: val(getField(r, '违纪类型')), description: val(getField(r, '描述')), status: 'normal', errors: [] as string[] }
|
||||
if (!row.name && !row.idCard) { row.status = 'error'; row.errors.push('姓名和证件号码都为空') }
|
||||
if (row.status === 'error') preview.errors.push({ sheet: '违纪记录', row: i + 2, name: row.name, errors: row.errors })
|
||||
preview.disciplinary.push(row)
|
||||
}
|
||||
@@ -185,8 +185,8 @@ router.post('/excel/preview', authMiddleware, requireAdmin, upload.single('file'
|
||||
const rows = XLSX.utils.sheet_to_json(attSheet)
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
const row: any = { rowNo: i + 2, name: val(getField(r, '姓名')), idCard: val(getField(r, '身份证号')), date: getField(r, '日期'), attStatus: val(getField(r, '考勤状态')), status: 'normal', errors: [] as string[] }
|
||||
if (!row.name && !row.idCard) { row.status = 'error'; row.errors.push('姓名和身份证号都为空') }
|
||||
const row: any = { rowNo: i + 2, name: val(getField(r, '姓名')), idCard: val(getField(r, '证件号码')), date: getField(r, '日期'), attStatus: val(getField(r, '考勤状态')), status: 'normal', errors: [] as string[] }
|
||||
if (!row.name && !row.idCard) { row.status = 'error'; row.errors.push('姓名和证件号码都为空') }
|
||||
const dt = parseDate(getField(r, '日期'))
|
||||
if (!dt) { row.status = 'error'; row.errors.push('日期格式错误') }
|
||||
if (row.status === 'error') preview.errors.push({ sheet: '考勤记录', row: i + 2, name: row.name, errors: row.errors })
|
||||
@@ -259,8 +259,8 @@ router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async
|
||||
const salary = String(num(getField(r, '月工资')))
|
||||
if (salary === '0') { result.skipped++; result.errors.push(`员工第${i + 2}行:月工资为空`); result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'skipped', message: '月工资为空' }); continue }
|
||||
|
||||
let idCard = val(getField(r, '身份证号'))
|
||||
if (!idCard) { result.skipped++; result.errors.push(`员工第${i + 2}行:身份证号为空,跳过`); result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'skipped', message: '身份证号为空' }); continue }
|
||||
let idCard = val(getField(r, '证件号码'))
|
||||
if (!idCard) { result.skipped++; result.errors.push(`员工第${i + 2}行:证件号码为空,跳过`); result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'skipped', message: '证件号码为空' }); continue }
|
||||
if (idCard) {
|
||||
const idCheck = validateIdCard(idCard)
|
||||
if (!idCheck.valid) { result.skipped++; result.errors.push(`员工第${i + 2}行:${idCheck.error}`); result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'skipped', message: idCheck.error }); continue }
|
||||
@@ -329,8 +329,8 @@ router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async
|
||||
const msg = e?.message || ''
|
||||
if (msg.includes('Unique constraint')) {
|
||||
result.duplicates++
|
||||
result.errors.push(`员工第${i + 2}行:该员工已存在(身份证号重复),跳过`)
|
||||
result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'duplicate', message: '身份证号重复' })
|
||||
result.errors.push(`员工第${i + 2}行:该员工已存在(证件号码重复),跳过`)
|
||||
result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'duplicate', message: '证件号码重复' })
|
||||
} else if (msg.includes('invalid') || msg.includes('validation')) {
|
||||
result.skipped++
|
||||
result.errors.push(`员工第${i + 2}行:数据格式不正确,请检查各项填写`)
|
||||
@@ -353,7 +353,7 @@ router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
try {
|
||||
const idCard = val(getField(r, '身份证号'))
|
||||
const idCard = val(getField(r, '证件号码'))
|
||||
const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(val(getField(r, '姓名')))
|
||||
if (!empId) { result.errors.push(`合同第${i + 2}行:找不到员工「${val(getField(r, '姓名'))}」`); continue }
|
||||
const startDate = parseDate(getField(r, '合同开始日期'))
|
||||
@@ -399,7 +399,7 @@ router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
try {
|
||||
const idCard = val(getField(r, '身份证号'))
|
||||
const idCard = val(getField(r, '证件号码'))
|
||||
const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(val(getField(r, '姓名')))
|
||||
if (!empId) { result.errors.push(`加班第${i + 2}行:找不到员工「${val(getField(r, '姓名'))}」`); continue }
|
||||
const date = parseDate(getField(r, '日期'))
|
||||
@@ -429,7 +429,7 @@ router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
try {
|
||||
const idCard = val(getField(r, '身份证号'))
|
||||
const idCard = val(getField(r, '证件号码'))
|
||||
const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(val(getField(r, '姓名')))
|
||||
if (!empId) { result.errors.push(`违纪第${i + 2}行:找不到员工「${val(getField(r, '姓名'))}」`); continue }
|
||||
const date = parseDate(getField(r, '日期'))
|
||||
@@ -462,7 +462,7 @@ router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
try {
|
||||
const idCard = val(getField(r, '身份证号'))
|
||||
const idCard = val(getField(r, '证件号码'))
|
||||
const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(val(getField(r, '姓名')))
|
||||
if (!empId) { result.errors.push(`考勤第${i + 2}行:找不到员工「${val(getField(r, '姓名'))}」`); continue }
|
||||
const date = parseDate(getField(r, '日期'))
|
||||
@@ -495,30 +495,30 @@ router.get('/template', authMiddleware, async (_req: AuthRequest, res: Response)
|
||||
const wb = XLSX.utils.book_new()
|
||||
|
||||
const empData = [
|
||||
{ '姓名*': '张三', '部门': '技术部', '性别(选填,留空自动识别)': '男', '手机号': '13800138000', '身份证号': '110101199001011234', '入职日期*': '2023-03-01', '月工资*': 10000, '社保基数': 10000, '公积金基数': 10000, '专项附加扣除': 1000, '参保城市': '北京', '紧急联系人': '李四', '紧急联系电话': '13900139000', '住址': '北京市朝阳区', '开户行': '工商银行', '银行账号': '6222021234567890', '孕期': '否', '医疗期': '否', '工伤': '否' },
|
||||
{ '姓名*': '张三', '部门': '技术部', '性别(选填,留空自动识别)': '男', '手机号': '13800138000', '证件号码': '110101199001011234', '入职日期*': '2023-03-01', '月工资*': 10000, '社保基数': 10000, '公积金基数': 10000, '专项附加扣除': 1000, '参保城市': '北京', '紧急联系人': '李四', '紧急联系电话': '13900139000', '住址': '北京市朝阳区', '开户行': '工商银行', '银行账号': '6222021234567890', '孕期': '否', '医疗期': '否', '工伤': '否' },
|
||||
]
|
||||
const empWs = XLSX.utils.json_to_sheet(empData, { header: ['姓名*', '部门', '性别(选填,留空自动识别)', '手机号', '身份证号', '入职日期*', '月工资*', '社保基数', '公积金基数', '专项附加扣除', '参保城市', '紧急联系人', '紧急联系电话', '住址', '开户行', '银行账号', '孕期', '医疗期', '工伤'] })
|
||||
const empWs = XLSX.utils.json_to_sheet(empData, { header: ['姓名*', '部门', '性别(选填,留空自动识别)', '手机号', '证件号码', '入职日期*', '月工资*', '社保基数', '公积金基数', '专项附加扣除', '参保城市', '紧急联系人', '紧急联系电话', '住址', '开户行', '银行账号', '孕期', '医疗期', '工伤'] })
|
||||
// 设置示例行样式(灰色背景)
|
||||
empWs['!cols'] = [{ wch: 10 }, { wch: 12 }, { wch: 22 }, { wch: 13 }, { wch: 20 }, { wch: 12 }, { wch: 10 }, { wch: 10 }, { wch: 10 }, { wch: 12 }, { wch: 10 }, { wch: 10 }, { wch: 13 }, { wch: 18 }, { wch: 10 }, { wch: 18 }, { wch: 6 }, { wch: 6 }, { wch: 6 }]
|
||||
XLSX.utils.book_append_sheet(wb, empWs, '员工信息')
|
||||
|
||||
const contractData = [
|
||||
{ '姓名*': '张三', '身份证号': '110101199001011234', '合同类型': '固定期限', '签订日期': '2023-03-01', '合同开始日期*': '2023-03-01', '合同结束日期': '2026-03-01', '合同年限': 3, '签订方式': '纸质', '试用期月数': 3, '试用期工资': 8000 },
|
||||
{ '姓名*': '张三', '证件号码': '110101199001011234', '合同类型': '固定期限', '签订日期': '2023-03-01', '合同开始日期*': '2023-03-01', '合同结束日期': '2026-03-01', '合同年限': 3, '签订方式': '纸质', '试用期月数': 3, '试用期工资': 8000 },
|
||||
]
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(contractData), '劳动合同')
|
||||
|
||||
const otData = [
|
||||
{ '姓名*': '张三', '身份证号': '110101199001011234', '日期*': '2024-01-15', '工作日加班时长': 2, '休息日加班时长': 0, '法定节假日加班时长': 0, '加班类型': '工作日加班', '加班时长': 2, '倍率': 1.5, '是否审批': '是' },
|
||||
{ '姓名*': '张三', '证件号码': '110101199001011234', '日期*': '2024-01-15', '工作日加班时长': 2, '休息日加班时长': 0, '法定节假日加班时长': 0, '加班类型': '工作日加班', '加班时长': 2, '倍率': 1.5, '是否审批': '是' },
|
||||
]
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(otData), '加班记录')
|
||||
|
||||
const discData = [
|
||||
{ '姓名*': '张三', '身份证号': '110101199001011234', '日期*': '2024-01-10', '违纪类型': '警告', '描述': '迟到', '处罚': '口头警告' },
|
||||
{ '姓名*': '张三', '证件号码': '110101199001011234', '日期*': '2024-01-10', '违纪类型': '警告', '描述': '迟到', '处罚': '口头警告' },
|
||||
]
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(discData), '违纪记录')
|
||||
|
||||
const attData = [
|
||||
{ '姓名*': '张三', '身份证号': '110101199001011234', '日期*': '2024-01-15', '考勤状态': '正常', '上班时间': '09:00', '下班时间': '18:00', '备注': '' },
|
||||
{ '姓名*': '张三', '证件号码': '110101199001011234', '日期*': '2024-01-15', '考勤状态': '正常', '上班时间': '09:00', '下班时间': '18:00', '备注': '' },
|
||||
]
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(attData), '考勤记录')
|
||||
|
||||
@@ -558,7 +558,7 @@ router.post('/monthly', authMiddleware, requireAdmin, upload.single('file'), asy
|
||||
}
|
||||
|
||||
function findEmp(r: any) {
|
||||
const idCard = val(getField(r, '身份证号'))
|
||||
const idCard = val(getField(r, '证件号码'))
|
||||
if (idCard) {
|
||||
const emp = empByHash.get(sha256(idCard))
|
||||
if (emp) return emp
|
||||
@@ -778,10 +778,10 @@ 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()
|
||||
|
||||
// 合并考勤+加班为一个Sheet,减少重复录入姓名身份证号
|
||||
// 合并考勤+加班为一个Sheet,减少重复录入姓名证件号码
|
||||
const attOtData = [{
|
||||
'姓名': '张三',
|
||||
'身份证号': '110101199001011234',
|
||||
'证件号码': '110101199001011234',
|
||||
'日期': '2024-06-01',
|
||||
'考勤状态': '正常',
|
||||
'上班时间': '09:00',
|
||||
@@ -798,16 +798,16 @@ router.get('/monthly-template', authMiddleware, async (_req: AuthRequest, res: R
|
||||
]
|
||||
XLSX.utils.book_append_sheet(wb, attOtWs, '考勤与加班')
|
||||
|
||||
const salaryData = [{ '姓名': '张三', '身份证号': '110101199001011234', '调整后月薪': 12000, '生效日期': '2024-06-01', '调薪原因': '年度调薪' }]
|
||||
const salaryData = [{ '姓名': '张三', '证件号码': '110101199001011234', '调整后月薪': 12000, '生效日期': '2024-06-01', '调薪原因': '年度调薪' }]
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(salaryData), '薪资调整')
|
||||
|
||||
const socialData = [{ '姓名': '张三', '身份证号': '110101199001011234', '变动类型': '调基', '缴费基数': 12000 }]
|
||||
const socialData = [{ '姓名': '张三', '证件号码': '110101199001011234', '变动类型': '调基', '缴费基数': 12000 }]
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(socialData), '社保变动')
|
||||
|
||||
const hfData = [{ '姓名': '张三', '身份证号': '110101199001011234', '变动类型': '调基', '缴费基数': 12000 }]
|
||||
const hfData = [{ '姓名': '张三', '证件号码': '110101199001011234', '变动类型': '调基', '缴费基数': 12000 }]
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(hfData), '公积金变动')
|
||||
|
||||
const discData = [{ '姓名': '张三', '身份证号': '110101199001011234', '日期': '2024-06-10', '违纪类型': '警告', '描述': '迟到', '处罚': '口头警告' }]
|
||||
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' })
|
||||
@@ -847,7 +847,7 @@ router.post('/payroll', authMiddleware, upload.single('file'), async (req: AuthR
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
try {
|
||||
const idCard = val(getField(r, '身份证号'))
|
||||
const idCard = val(getField(r, '证件号码'))
|
||||
const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(val(getField(r, '姓名')))
|
||||
if (!empId) { result.errors.push(`第${i + 2}行:找不到员工「${val(getField(r, '姓名'))}」`); continue }
|
||||
const entryId = entryByEmp.get(empId)
|
||||
@@ -906,8 +906,8 @@ router.post('/payroll', authMiddleware, upload.single('file'), async (req: AuthR
|
||||
router.get('/payroll-template', authMiddleware, (_req: AuthRequest, res: Response) => {
|
||||
const wb = XLSX.utils.book_new()
|
||||
const data = [
|
||||
{ '姓名*': '张三', '身份证号*': '110101199001011234', '基本工资': 10000, '加班费': 500, '津贴': 800, '扣款': 0, '奖金': 2000 },
|
||||
{ '姓名*': '李四', '身份证号*': '110101199002021234', '基本工资': 12000, '加班费': 0, '津贴': 600, '扣款': 100, '奖金': 0 },
|
||||
{ '姓名*': '张三', '证件号码*': '110101199001011234', '基本工资': 10000, '加班费': 500, '津贴': 800, '扣款': 0, '奖金': 2000 },
|
||||
{ '姓名*': '李四', '证件号码*': '110101199002021234', '基本工资': 12000, '加班费': 0, '津贴': 600, '扣款': 100, '奖金': 0 },
|
||||
]
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data), '工资表')
|
||||
const buf = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' })
|
||||
@@ -921,8 +921,8 @@ router.get('/payroll-template', authMiddleware, (_req: AuthRequest, res: Respons
|
||||
router.get('/special-deduction/template', authMiddleware, (_req: AuthRequest, res: Response) => {
|
||||
const wb = XLSX.utils.book_new()
|
||||
const data = [
|
||||
{ '姓名*': '张三', '身份证号': '110101199001011234', '子女教育': 1000, '赡养老人': 2000, '住房': 1500, '继续教育': 0, '婴幼儿照护': 0, '备注': '' },
|
||||
{ '姓名*': '李四', '身份证号': '110101199002021234', '子女教育': 0, '赡养老人': 1000, '住房': 0, '继续教育': 400, '婴幼儿照护': 1000, '备注': '继续教育证书' },
|
||||
{ '姓名*': '张三', '证件号码': '110101199001011234', '子女教育': 1000, '赡养老人': 2000, '住房': 1500, '继续教育': 0, '婴幼儿照护': 0, '备注': '' },
|
||||
{ '姓名*': '李四', '证件号码': '110101199002021234', '子女教育': 0, '赡养老人': 1000, '住房': 0, '继续教育': 400, '婴幼儿照护': 1000, '备注': '继续教育证书' },
|
||||
]
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data), '专项附加扣除')
|
||||
const buf = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' })
|
||||
@@ -953,7 +953,7 @@ router.post('/special-deduction', authMiddleware, requireAdmin, upload.single('f
|
||||
const r = rows[i] as any
|
||||
try {
|
||||
const name = val(getField(r, '姓名'))
|
||||
const idCard = val(getField(r, '身份证号'))
|
||||
const idCard = val(getField(r, '证件号码'))
|
||||
const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(name)
|
||||
if (!empId) { result.skipped++; result.errors.push(`第${i + 2}行:找不到员工「${name}」`); result.details.push({ row: i + 2, name, status: 'skipped', message: '找不到员工' }); continue }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user