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:
@@ -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, '工伤')) === '是',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user