fix: HR系统优化批次1 - P0/P1问题修复

P0-3: 修复合同状态判断逻辑,有合同记录但signDate为null时不再误判未签
P0-5: 修复参保城市默认北京问题,导入和预览均改为null
P0-11: 添加全局ErrorBoundary防止白屏,三处布局均包裹
P1-2: 合同附件改为可选,允许先保存再补充上传
P1-7.2: 排班弹窗增加员工搜索(姓名/部门)
P1-8.2: 加班费导入支持Excel(xlsx/xls)格式,兼容中英文列名
P1-9: 社保/公积金基数月度办理支持逐人修改,后端返回recordId
This commit is contained in:
selfrelease
2026-07-30 18:34:36 +08:00
parent 411adb7ddc
commit 55286819ae
11 changed files with 480 additions and 39 deletions
+2 -2
View File
@@ -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, '参保城市')) || '北京', 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('入职日期格式错误') }
@@ -285,7 +285,7 @@ router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async
socialInsBase: num(getField(r, '社保基数')) || num(salary),
housingFundBase: num(getField(r, '公积金基数')) || num(salary),
specialDeduction: num(getField(r, '专项附加扣除')) || 0,
city: val(getField(r, '参保城市')) || '北京',
city: val(getField(r, '参保城市')) || null,
isPregnant: val(getField(r, '孕期')) === '是',
isInMedicalPeriod: val(getField(r, '医疗期')) === '是',
isWorkInjured: val(getField(r, '工伤')) === '是',
+2
View File
@@ -119,6 +119,7 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
endDate: latestContract.endDate,
contractType: latestContract.contractType,
hireDate: e.hireDate,
hasRecord: true,
})
: getContractStatus({
signDate: null,
@@ -126,6 +127,7 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
endDate: null,
contractType: 'UNSIGNED',
hireDate: e.hireDate,
hasRecord: false,
})
const isResigned = e.terminations.some((t) => t.status === 'COMPLETED' && t.terminationDate <= today)
const isPreHire = !isResigned && e.hireDate > todayEnd
+4
View File
@@ -878,6 +878,7 @@ router.get('/monthly-changes', async (req: AuthRequest, res: Response, next: Nex
const config = await getConfigForCity(r.city)
const detail = config ? calcSocialDetail(r.base, config) : null
return {
recordId: r.id,
employeeId: r.employeeId,
name: r.employee.name,
department: r.employee.department,
@@ -948,6 +949,7 @@ router.get('/housing/monthly-changes', async (req: AuthRequest, res: Response, n
const config = await getConfigForCity(r.city)
const detail = config ? calcHousingDetail(r.base, config) : null
return {
recordId: r.id,
employeeId: r.employeeId,
name: r.employee.name,
department: r.employee.department,
@@ -1012,6 +1014,7 @@ router.get('/active-declaration', async (req: AuthRequest, res: Response, next:
const config = await getConfigForCity(r.city)
const detail = config ? calcSocialDetail(r.base, config) : null
return {
recordId: r.id,
employeeId: r.employeeId,
name: r.employee.name,
department: r.employee.department,
@@ -1073,6 +1076,7 @@ router.get('/housing/active-declaration', async (req: AuthRequest, res: Response
const config = await getConfigForCity(r.city)
const detail = config ? calcHousingDetail(r.base, config) : null
return {
recordId: r.id,
employeeId: r.employeeId,
name: r.employee.name,
department: r.employee.department,
+17 -2
View File
@@ -24,6 +24,8 @@ export function getContractStatus(contract: {
endDate: Date | null
contractType: string
hireDate: Date
/** hasRecord: 是否存在合同记录(区分"有合同但未填签订日期"和"完全无合同" */
hasRecord?: boolean
}): { status: string; statusText: string; riskLevel: 'high' | 'medium' | 'low' | 'safe' } {
const today = new Date()
const typeLabelMap: Record<string, string> = {
@@ -35,7 +37,11 @@ export function getContractStatus(contract: {
}
const typeLabel = typeLabelMap[contract.contractType] || ''
if (!contract.signDate || contract.contractType === 'UNSIGNED') {
// 只有真正没有合同记录(hasRecord=false)或类型为 UNSIGNED 时,才判定为"未签合同"
// 有合同记录但 signDate 为 null 时,不再判定为"未签合同"
const isUnsigned = contract.contractType === 'UNSIGNED' || (contract.hasRecord === false && !contract.signDate)
if (isUnsigned) {
const days = daysBetween(today, contract.hireDate)
if (days > 365) {
return { status: 'unsigned_over_year', statusText: '未签合同(已视为无固定期限)', riskLevel: 'high' }
@@ -45,6 +51,7 @@ export function getContractStatus(contract: {
return { status: 'unsigned', statusText: `未签合同(${days}天)`, riskLevel: 'medium' }
}
// 有合同记录(FIXED/UNFIXED/LABOR/INTERNSHIP),即使 signDate 为 null 也按正常合同处理
if (contract.endDate) {
const daysToExpire = daysBetween(contract.endDate, today)
if (daysToExpire < 0) {
@@ -55,7 +62,13 @@ export function getContractStatus(contract: {
return { status: 'active', statusText: `${typeLabel}·正常`, riskLevel: 'safe' }
}
return { status: 'unfixed', statusText: '无固定期限·正常', riskLevel: 'safe' }
// 无固定期限或有合同但无结束日期
if (contract.contractType === 'UNFIXED') {
return { status: 'unfixed', statusText: '无固定期限·正常', riskLevel: 'safe' }
}
// 有合同记录但未填结束日期(如 FIXED 但 endDate 为 null),视为正常
return { status: 'active', statusText: `${typeLabel}·正常`, riskLevel: 'safe' }
}
export function validateProbation(contractMonths: number, probationMonths: number): { valid: boolean; max: number; message?: string } {
@@ -112,6 +125,7 @@ export async function getEmployees(orgId: string, params: { page?: number; pageS
endDate: latestContract.endDate,
contractType: latestContract.contractType,
hireDate: emp.hireDate,
hasRecord: true,
})
: getContractStatus({
signDate: null,
@@ -119,6 +133,7 @@ export async function getEmployees(orgId: string, params: { page?: number; pageS
endDate: null,
contractType: 'UNSIGNED',
hireDate: emp.hireDate,
hasRecord: false,
})
let decryptedSalary = 0