feat: AI历史记录、企业信息seed、员工状态自动刷新、风险预测数据准确性优化
- 智能问答/风险预测/合同审查/案例匹配 均支持历史记录保存、加载、删除 - 后端 predict/predict-stream 补充离职记录详情和预计算工龄/天数,避免AI计算错误 - Organization 增加 contactName/contactPhone 字段,seed 企业信息 - Settings 页面修复 orgData/usersData 取值路径错误 - Roster/Termination/Contracts mutation 增加 roster cache invalidation - 员工档案页面 tab 固定、内容区域滚动到窗口底部 - 解聘证据链显示逻辑修复(协商一致离职 vs 员工主动离职)
This commit is contained in:
@@ -105,7 +105,7 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
contractType: 'UNSIGNED',
|
||||
hireDate: e.hireDate,
|
||||
})
|
||||
const isResigned = e.terminations.some((t) => t.terminationDate <= today)
|
||||
const isResigned = e.terminations.some((t) => t.status === 'COMPLETED' && t.terminationDate <= today)
|
||||
const isPreHire = !isResigned && e.hireDate > todayEnd
|
||||
const dynamicStatus = isResigned ? 'RESIGNED' : (isPreHire ? 'PRE_HIRE' : 'ACTIVE')
|
||||
return {
|
||||
@@ -179,7 +179,7 @@ router.get('/:id/profile', authMiddleware, async (req: AuthRequest, res, next) =
|
||||
const { monthlySalary, bankAccount, idCardNumber, ...rest } = employee
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const dynamicStatus = employee.terminations.some((t) => t.terminationDate <= today) ? 'RESIGNED' : 'ACTIVE'
|
||||
const dynamicStatus = employee.terminations.some((t) => t.status === 'COMPLETED' && t.terminationDate <= today) ? 'RESIGNED' : 'ACTIVE'
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
@@ -220,6 +220,11 @@ router.get('/:id/evidence-chain', authMiddleware, async (req: AuthRequest, res,
|
||||
const empDept = employee.department
|
||||
const hireDate = employee.hireDate.toISOString().slice(0, 10)
|
||||
|
||||
// 风险检测
|
||||
const risks: any[] = []
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
// 1. 劳动关系证据
|
||||
evidence.push({
|
||||
category: '劳动关系',
|
||||
@@ -228,17 +233,86 @@ router.get('/:id/evidence-chain', authMiddleware, async (req: AuthRequest, res,
|
||||
description: `${empName}于${hireDate}入职${empDept},建立劳动关系。`,
|
||||
evidenceType: 'EMPLOYMENT',
|
||||
})
|
||||
|
||||
// 无合同风险检测
|
||||
if (employee.contracts.length === 0) {
|
||||
const daysSinceHire = Math.floor((today.getTime() - employee.hireDate.getTime()) / (86400000))
|
||||
if (daysSinceHire > 30) {
|
||||
risks.push({
|
||||
level: daysSinceHire > 365 ? 'DANGER' : 'HIGH',
|
||||
category: '劳动关系',
|
||||
title: '未签订书面劳动合同',
|
||||
description: `入职已${daysSinceHire}天仍未签订书面劳动合同,超过30天未签合同将面临双倍工资赔偿风险(《劳动合同法》第82条)。${daysSinceHire > 365 ? '已满一年未签合同,视为已订立无固定期限劳动合同。' : ''}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
employee.contracts.forEach((c) => {
|
||||
const contractTypeText = ({ FIXED: '固定期限', UNFIXED: '无固定期限', LABOR: '劳务协议', INTERNSHIP: '实习协议', UNSIGNED: '未签订' } as Record<string, string>)[c.contractType] || '未签订'
|
||||
const isSigned = !!c.signDate
|
||||
evidence.push({
|
||||
category: '劳动关系',
|
||||
title: `合同(${({ FIXED: '固定期限', UNFIXED: '无固定期限', LABOR: '劳务协议', INTERNSHIP: '实习协议', UNSIGNED: '未签订' } as Record<string, string>)[c.contractType] || '未签订'})`,
|
||||
title: `合同(${contractTypeText})`,
|
||||
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}。`,
|
||||
description: `合同期限:${c.startDate.toISOString().slice(0, 10)} 至 ${c.endDate ? c.endDate.toISOString().slice(0, 10) : '无固定期限'},试用期${c.probationMonths}个月,试用期工资¥${c.probationSalary}。${isSigned ? '' : '⚠ 该合同尚未签订。'}`,
|
||||
evidenceType: 'CONTRACT',
|
||||
signed: !!c.signDate,
|
||||
signed: isSigned,
|
||||
riskLevel: !isSigned ? 'HIGH' : undefined,
|
||||
})
|
||||
|
||||
// 合同未签字风险
|
||||
if (!isSigned) {
|
||||
risks.push({
|
||||
level: 'HIGH',
|
||||
category: '劳动关系',
|
||||
title: '合同未签字',
|
||||
description: `合同(${contractTypeText})期限${c.startDate.toISOString().slice(0, 10)}至${c.endDate ? c.endDate.toISOString().slice(0, 10) : '无固定期限'},尚未签订。未签合同在仲裁中无法证明劳动关系约定内容。`,
|
||||
})
|
||||
}
|
||||
|
||||
// 合同过期风险
|
||||
if (c.endDate && c.endDate < today) {
|
||||
const expiredDays = Math.floor((today.getTime() - c.endDate.getTime()) / 86400000)
|
||||
risks.push({
|
||||
level: expiredDays > 30 ? 'HIGH' : 'MEDIUM',
|
||||
category: '劳动关系',
|
||||
title: '合同已过期',
|
||||
description: `合同已于${c.endDate.toISOString().slice(0, 10)}过期,过期${expiredDays}天。过期后继续用工满一个月未续签的,面临双倍工资风险。`,
|
||||
})
|
||||
} else if (c.endDate) {
|
||||
const daysToExpire = Math.floor((c.endDate.getTime() - today.getTime()) / 86400000)
|
||||
if (daysToExpire <= 30 && daysToExpire >= 0) {
|
||||
risks.push({
|
||||
level: 'MEDIUM',
|
||||
category: '劳动关系',
|
||||
title: '合同即将到期',
|
||||
description: `合同将于${c.endDate.toISOString().slice(0, 10)}到期,剩余${daysToExpire}天。请及时办理续签或终止手续。`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 试用期工资为0风险
|
||||
if (c.probationMonths > 0 && c.probationSalary === 0) {
|
||||
risks.push({
|
||||
level: 'MEDIUM',
|
||||
category: '劳动关系',
|
||||
title: '试用期工资为0',
|
||||
description: `合同约定试用期${c.probationMonths}个月但试用期工资为0,违反《劳动合同法》第20条(试用期工资不得低于本单位相同岗位最低档工资的80%或劳动合同约定工资的80%)。`,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 无工资条风险检测(入职超过1个月但无工资条)
|
||||
const daysSinceHire = Math.floor((today.getTime() - employee.hireDate.getTime()) / 86400000)
|
||||
if (daysSinceHire > 30 && employee.payslips.length === 0) {
|
||||
risks.push({
|
||||
level: 'MEDIUM',
|
||||
category: '薪酬发放',
|
||||
title: '无工资条记录',
|
||||
description: `入职已${daysSinceHire}天但无任何工资条记录,在仲裁中难以证明已按时足额支付工资。建议尽快创建发薪批次并归档。`,
|
||||
})
|
||||
}
|
||||
|
||||
// 2. 薪酬证据
|
||||
employee.payslips.forEach((p) => {
|
||||
evidence.push({
|
||||
@@ -317,14 +391,39 @@ router.get('/:id/evidence-chain', authMiddleware, async (req: AuthRequest, res,
|
||||
|
||||
// 7. 解聘证据
|
||||
employee.terminations.forEach((t) => {
|
||||
const reasonMap: Record<string, string> = { NEGOTIATED: '协商解除', FAULT: '员工过错', NONFAULT: '非过错解除', LAYOFF: '经济性裁员', EXPIRED: '合同到期' }
|
||||
const reasonMap: Record<string, string> = { NEGOTIATED: '协商一致', FAULT: '员工过错', NONFAULT: '非过错解除', LAYOFF: '经济性裁员', EXPIRED: '合同到期', RESIGNATION: '员工主动离职' }
|
||||
const typeLabel = t.type === 'RESIGNATION' && t.reason === 'NEGOTIATED'
|
||||
? '协商一致离职'
|
||||
: t.type === 'RESIGNATION' ? '员工主动离职' : '公司解聘'
|
||||
const statusMap: Record<string, string> = { DRAFT: '草稿', PENDING_APPROVAL: '待审批', APPROVED: '已审批', EXECUTING: '执行中', COMPLETED: '已完成', REJECTED: '已驳回', CANCELLED: '已撤销' }
|
||||
evidence.push({
|
||||
category: '解聘记录',
|
||||
title: `${t.terminationDate.toISOString().slice(0, 10)} 解聘记录`,
|
||||
title: `${t.terminationDate.toISOString().slice(0, 10)} ${typeLabel}记录`,
|
||||
date: t.terminationDate.toISOString().slice(0, 10),
|
||||
description: `解聘原因:${reasonMap[t.reason] || t.reason}。经济补偿金:¥${t.compensation.toFixed(2)}。${t.remark || ''}`,
|
||||
description: `类型:${typeLabel}。原因:${reasonMap[t.reason] || t.reason}。经济补偿金:¥${t.compensation.toFixed(2)}。流程状态:${statusMap[t.status] || t.status}。${t.resignationReason ? `离职原因:${t.resignationReason}。` : ''}${t.remark || ''}`,
|
||||
evidenceType: 'TERMINATION',
|
||||
acknowledged: t.status === 'COMPLETED',
|
||||
})
|
||||
|
||||
// 离职流程未完成风险
|
||||
if (t.status !== 'COMPLETED') {
|
||||
risks.push({
|
||||
level: 'HIGH',
|
||||
category: '解聘记录',
|
||||
title: '离职流程未完成',
|
||||
description: `${typeLabel}流程当前状态为「${statusMap[t.status] || t.status}」,尚未完成闭环。离职日期${t.terminationDate.toISOString().slice(0, 10)},若未签署解除协议/离职确认书、未完成工作交接、未结清工资、未办理社保减员,在仲裁中无法证明离职的合法性与完整性,面临继续履行合同或违法解除赔偿(2N)风险。`,
|
||||
})
|
||||
}
|
||||
|
||||
// 协商解除但补偿金为0
|
||||
if (t.reason === 'NEGOTIATED' && t.compensation === 0) {
|
||||
risks.push({
|
||||
level: 'MEDIUM',
|
||||
category: '解聘记录',
|
||||
title: '协商解除但补偿金为0',
|
||||
description: `解聘原因为协商解除但经济补偿金为0,若员工事后主张非自愿离职,企业需举证协商一致且员工自愿放弃补偿,否则可能被认定为单方违法解除,面临2N赔偿风险。`,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
res.json({
|
||||
@@ -334,15 +433,17 @@ router.get('/:id/evidence-chain', authMiddleware, async (req: AuthRequest, res,
|
||||
name: empName,
|
||||
department: empDept,
|
||||
hireDate,
|
||||
status: employee.terminations.some((t) => t.terminationDate <= new Date()) ? 'RESIGNED' : 'ACTIVE',
|
||||
status: employee.terminations.some((t) => t.status === 'COMPLETED' && t.terminationDate <= new Date()) ? 'RESIGNED' : 'ACTIVE',
|
||||
gender: employee.gender,
|
||||
phone: employee.phone,
|
||||
},
|
||||
evidence,
|
||||
risks,
|
||||
summary: {
|
||||
total: evidence.length,
|
||||
signed: evidence.filter((e) => e.acknowledged === true).length,
|
||||
unsigned: evidence.filter((e) => e.acknowledged === false).length,
|
||||
riskCount: risks.length,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user