feat: 系统优化Phase2 - 面包屑导航/侧边栏间距/制度公示阅读签收/模板变量中文化/通知类型补全
- 面包屑导航组件,集成至TopNav header - 侧边栏菜单分组间距增大,分组间分隔线 - 制度公示员工阅读签收:PolicyReadRecord模型、portal路由、管理端阅读统计 - 修复Policies.tsx民主程序推进bug(字段名/API路径/参数) - 用工文本模板变量名英文转中文显示 - 通知类型TYPE_LABELS补全(RISK_ALERT/SOCIAL_INS/OVERTIME_ALERT/PAYSLIP_READY) - 通知示例数据补充 - h2标题统一为text-sm font-medium - 新增run.md
This commit is contained in:
@@ -4,6 +4,7 @@ import { chat, chatStream, reviewContract, matchCase, predictRisks, predictRisks
|
||||
import { seedKnowledgeBase, addKnowledge, searchKnowledge, ensureRAGTable } from '../services/rag.service'
|
||||
import prisma from '../lib/prisma'
|
||||
import { z } from 'zod'
|
||||
import { decrypt } from '../lib/crypto'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -542,4 +543,140 @@ router.delete('/rag/:id', authMiddleware, async (req: AuthRequest, res, next) =>
|
||||
}
|
||||
})
|
||||
|
||||
// ========== 合同到期决策助手 ==========
|
||||
|
||||
router.post('/contract-decision', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { employeeId } = req.body as { employeeId: string }
|
||||
if (!employeeId) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 employeeId' } })
|
||||
}
|
||||
|
||||
const emp = await prisma.employee.findFirst({
|
||||
where: { id: employeeId, orgId: req.user!.orgId },
|
||||
include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 } },
|
||||
})
|
||||
if (!emp) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } })
|
||||
}
|
||||
|
||||
const contract = emp.contracts[0]
|
||||
if (!contract || !contract.endDate) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '该员工无固定期限合同或未签合同,不适用到期决策' } })
|
||||
}
|
||||
|
||||
// 解密薪资
|
||||
let salary = 0
|
||||
try { salary = Number(decrypt(emp.monthlySalary)) || 0 } catch { salary = Number(emp.monthlySalary) || 0 }
|
||||
|
||||
const today = new Date()
|
||||
const hireDate = emp.hireDate
|
||||
const endDate = contract.endDate
|
||||
const totalMonths = (endDate.getFullYear() - hireDate.getFullYear()) * 12 + (endDate.getMonth() - hireDate.getMonth())
|
||||
const years = Math.floor(totalMonths / 12)
|
||||
const remainingMonths = totalMonths % 12
|
||||
let n = years
|
||||
if (remainingMonths >= 6) n = years + 1
|
||||
else if (remainingMonths > 0) n = years + 0.5
|
||||
if (n <= 0) n = 0.5
|
||||
|
||||
const daysToExpiry = Math.ceil((endDate.getTime() - today.getTime()) / 86400000)
|
||||
|
||||
// 三选项成本对比
|
||||
const options = [
|
||||
{
|
||||
key: 'RENEW',
|
||||
title: '续签合同',
|
||||
cost: 0,
|
||||
description: '与员工续签劳动合同,保持劳动关系延续',
|
||||
legalRisk: '低',
|
||||
details: {
|
||||
compensation: 0,
|
||||
noticePeriod: '无需通知',
|
||||
notes: '续签时如维持或提高条件,员工拒绝则无需补偿;降低条件员工拒绝需支付 N',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'EXPIRE_NO_RENEW',
|
||||
title: '到期不续签',
|
||||
cost: salary * n,
|
||||
description: '合同到期后公司决定不续签,需支付经济补偿金 N',
|
||||
legalRisk: '中',
|
||||
details: {
|
||||
compensation: salary * n,
|
||||
n,
|
||||
monthlySalary: salary,
|
||||
noticePeriod: '建议提前30天书面通知',
|
||||
notes: '公司提出不续签需支付经济补偿金(N);员工主动提出不续签则无需支付',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'EXPIRE_WAIT',
|
||||
title: '逾期不处理(风险最高)',
|
||||
cost: salary * (n + 1),
|
||||
description: '合同到期后继续用工但不签新合同,可能被认定为事实劳动关系',
|
||||
legalRisk: '高',
|
||||
details: {
|
||||
compensation: salary * (n + 1),
|
||||
n,
|
||||
monthlySalary: salary,
|
||||
notes: '逾期超过1个月未续签,员工可主张双倍工资;满1年视为已订立无固定期限劳动合同',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// 构建 AI 建议请求
|
||||
const decisionContext = `员工合同到期决策分析:
|
||||
- 姓名:${emp.name}
|
||||
- 部门:${emp.department}
|
||||
- 入职日期:${hireDate.toISOString().slice(0, 10)}
|
||||
- 合同到期日:${endDate.toISOString().slice(0, 10)}(距今${daysToExpiry}天)
|
||||
- 月薪:¥${salary.toFixed(2)}
|
||||
- 工龄:${years}年${remainingMonths}月(经济补偿月数 N=${n})
|
||||
- 特殊状态:${emp.isPregnant ? '孕期/哺乳期 ' : ''}${emp.isInMedicalPeriod ? '医疗期 ' : ''}${emp.isWorkInjured ? '工伤' : '无'}
|
||||
|
||||
三选项成本对比:
|
||||
1. 续签:成本 ¥0
|
||||
2. 到期不续签:成本 ¥${(salary * n).toFixed(2)}(经济补偿 N=${n})
|
||||
3. 逾期不处理:风险成本 ¥${(salary * (n + 1)).toFixed(2)}(双倍工资风险)
|
||||
|
||||
请给出专业建议,分析每个选项的法律风险和实际影响,推荐最优方案。`
|
||||
|
||||
let aiAdvice = ''
|
||||
try {
|
||||
await checkUsageLimit(req.user!.orgId, 'chat')
|
||||
const result = await chat([{ role: 'user', content: decisionContext }], '')
|
||||
aiAdvice = result
|
||||
await recordUsage(req.user!.orgId, req.user!.id, 'chat')
|
||||
} catch {
|
||||
aiAdvice = 'AI 建议生成失败,请参考以上成本对比数据自行判断。'
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
employee: {
|
||||
id: emp.id,
|
||||
name: emp.name,
|
||||
department: emp.department,
|
||||
hireDate: hireDate.toISOString().slice(0, 10),
|
||||
contractEndDate: endDate.toISOString().slice(0, 10),
|
||||
daysToExpiry,
|
||||
monthlySalary: salary,
|
||||
workYears: years,
|
||||
workMonths: remainingMonths,
|
||||
n,
|
||||
isPregnant: emp.isPregnant,
|
||||
isInMedicalPeriod: emp.isInMedicalPeriod,
|
||||
isWorkInjured: emp.isWorkInjured,
|
||||
},
|
||||
options,
|
||||
aiAdvice,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
Reference in New Issue
Block a user