feat: TurboHR 14项优化与功能增强
- #1 Dashboard风险提醒增加立刻办理按钮 - #2 Calendar月份选择器改为input month - #3 Termination增加7种解聘原因法律依据和操作步骤 - #5 合同审查支持PDF TXT格式 - #6 AI合同审查prompt优化为具体修改建议 - #7 知识库添加更新机制说明 - #9 SpecialStatus员工选择改用all-lite接口 - #10 Termination增加详细法律条款引用 - #11 Money发薪批次增加社保公积金合计列 - #12 EmployeeAttachment扩展文件类型 - #13 花名册增加女职工干部工人选项加退休提醒 - #14 新增公司备用文件上传模块
This commit is contained in:
@@ -84,28 +84,33 @@ export async function* chatStream(messages: { role: 'user' | 'assistant'; conten
|
||||
}
|
||||
|
||||
export async function reviewContract(contractText: string): Promise<{ text: string; structured: { riskItems: { level: string; title: string; description: string; suggestion: string }[]; score: number; summary: string } }> {
|
||||
const prompt = `请审查以下劳动合同文本的合法性,逐条检查并标注风险等级(红/黄/绿),给出修改建议,最后给出合规评分(0-100分)。
|
||||
const prompt = `请审查以下劳动合同文本的合法性和合规性,逐条检查并标注风险等级(红/黄/绿),对每个风险点必须给出:
|
||||
1. 问题说明:具体哪一条款存在什么问题
|
||||
2. 法律依据:引用《劳动合同法》具体条款
|
||||
3. 具体修改建议:给出可以直接替换的修改后条款文本
|
||||
|
||||
最后给出合规评分(0-100分)。
|
||||
|
||||
合同文本:
|
||||
${contractText}
|
||||
|
||||
请按以下格式输出:
|
||||
请严格按以下格式输出:
|
||||
【风险项】
|
||||
🔴/🟡/🟢 [问题标题] - [说明] - [修改建议]
|
||||
🔴/🟡/🟢 [问题标题] - [问题说明+法律依据] - [具体修改建议:应将xxx修改为yyy]
|
||||
|
||||
【合规评分】XX/100
|
||||
|
||||
【总体建议】
|
||||
一段话总结`
|
||||
一段话总结,指出最需要优先修改的3个问题`
|
||||
|
||||
const response = await client.chat.completions.create({
|
||||
model: 'qwen-max',
|
||||
messages: [
|
||||
{ role: 'system', content: '你是劳动法合同审查专家,精通劳动合同法。' },
|
||||
{ role: 'system', content: '你是劳动法合同审查专家,精通劳动合同法。对每个风险点必须给出法律依据和可直接替换的具体修改建议文本。' },
|
||||
{ role: 'user', content: prompt },
|
||||
],
|
||||
temperature: 0.3,
|
||||
max_tokens: 3000,
|
||||
max_tokens: 4000,
|
||||
})
|
||||
|
||||
const text = response.choices[0]?.message?.content || ''
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import prisma from '../lib/prisma'
|
||||
import type { RiskLevel, RiskType } from '@prisma/client'
|
||||
import { decrypt } from '../lib/crypto'
|
||||
import { calcIndividualRetireAge, calcRetirementDaysLeft } from './retirement.service'
|
||||
|
||||
function daysBetween(a: Date, b: Date): number {
|
||||
return Math.floor((a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24))
|
||||
@@ -119,6 +120,17 @@ function estimateRiskCost(
|
||||
}
|
||||
}
|
||||
|
||||
// 退休提醒:未及时办理退休可能导致多缴社保公积金
|
||||
if (title.includes('退休')) {
|
||||
const daysMatch = title.match(/(\d+)天/)
|
||||
const days = daysMatch ? parseInt(daysMatch[1]) : 0
|
||||
return {
|
||||
estimatedLoss: days > 0 ? salary * (days / 30) : salary,
|
||||
lossRange: [500, salary * 6],
|
||||
deadline: new Date(today.getTime() + (days > 0 ? days : 7) * 86400000),
|
||||
}
|
||||
}
|
||||
|
||||
// 默认
|
||||
return {
|
||||
estimatedLoss: 0,
|
||||
@@ -452,6 +464,49 @@ export async function detectMonthlyTasks(orgId: string) {
|
||||
return risks
|
||||
}
|
||||
|
||||
/**
|
||||
* 退休提醒:检测即将退休的员工(距退休180天内)
|
||||
*/
|
||||
export async function detectRetirementRisks(orgId: string) {
|
||||
const employees = await prisma.employee.findMany({
|
||||
where: { orgId, status: 'ACTIVE', birthDate: { not: null } },
|
||||
select: { id: true, name: true, gender: true, birthDate: true, femaleWorkerType: true },
|
||||
})
|
||||
|
||||
const risks: { employeeId: string; type: RiskType; level: RiskLevel; title: string; description: string; actionUrl: string }[] = []
|
||||
|
||||
for (const emp of employees) {
|
||||
if (!emp.birthDate) continue
|
||||
const gender = emp.gender || '男'
|
||||
const fwt = emp.femaleWorkerType
|
||||
const bd = new Date(emp.birthDate)
|
||||
const { retireDate } = calcIndividualRetireAge(bd, gender, fwt)
|
||||
const daysLeft = calcRetirementDaysLeft(bd, gender, fwt) ?? 0
|
||||
|
||||
if (daysLeft <= 180 && daysLeft > 0) {
|
||||
risks.push({
|
||||
employeeId: emp.id,
|
||||
type: 'RETIREMENT',
|
||||
level: daysLeft <= 30 ? 'HIGH' : 'MEDIUM',
|
||||
title: `${emp.name}距退休仅剩${daysLeft}天`,
|
||||
description: `${gender === '女' ? (fwt === 'WORKER' ? '女工人' : '女干部') : '男'},出生日期 ${emp.birthDate.toISOString().slice(0, 10)},预计退休日期 ${retireDate.toISOString().slice(0, 10)}。请提前准备退休手续。`,
|
||||
actionUrl: `/roster?employee=${encodeURIComponent(emp.name)}`,
|
||||
})
|
||||
} else if (daysLeft <= 0) {
|
||||
risks.push({
|
||||
employeeId: emp.id,
|
||||
type: 'RETIREMENT',
|
||||
level: 'HIGH',
|
||||
title: `${emp.name}已达退休年龄`,
|
||||
description: `${gender === '女' ? (fwt === 'WORKER' ? '女工人' : '女干部') : '男'},出生日期 ${emp.birthDate.toISOString().slice(0, 10)},已超过退休日期 ${retireDate.toISOString().slice(0, 10)}。请尽快办理退休手续。`,
|
||||
actionUrl: `/roster?employee=${encodeURIComponent(emp.name)}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return risks
|
||||
}
|
||||
|
||||
export async function runRiskDetection(orgId: string) {
|
||||
// 非月度风险去重:检查所有状态(含 RESOLVED/IGNORED),避免已处理的风险被重新创建
|
||||
// 按 employeeId:type 归并,不依赖 actionUrl(actionUrl 可能因天数变化而不同)
|
||||
@@ -481,9 +536,10 @@ export async function runRiskDetection(orgId: string) {
|
||||
const onboardingRisks = await detectOnboardingRisks(orgId)
|
||||
const monthlyTasks = await detectMonthlyTasks(orgId)
|
||||
const specialStatusRisks = await detectSpecialStatusRisks(orgId)
|
||||
const retirementRisks = await detectRetirementRisks(orgId)
|
||||
|
||||
// 获取所有相关员工数据用于风险量化
|
||||
const allEmployeeIds = [...contractRisks, ...terminationRisks, ...onboardingRisks, ...specialStatusRisks]
|
||||
const allEmployeeIds = [...contractRisks, ...terminationRisks, ...onboardingRisks, ...specialStatusRisks, ...retirementRisks]
|
||||
.map(r => r.employeeId)
|
||||
.filter(Boolean) as string[]
|
||||
const employees = allEmployeeIds.length > 0
|
||||
@@ -492,7 +548,7 @@ export async function runRiskDetection(orgId: string) {
|
||||
const empMap = new Map(employees.map(e => [e.id, e]))
|
||||
|
||||
// 月度任务用 monthlyKeys 去重,其他任务用 existingKeys 去重
|
||||
const nonMonthlyRisks = [...contractRisks, ...terminationRisks, ...onboardingRisks, ...specialStatusRisks]
|
||||
const nonMonthlyRisks = [...contractRisks, ...terminationRisks, ...onboardingRisks, ...specialStatusRisks, ...retirementRisks]
|
||||
const toCreate = [
|
||||
...nonMonthlyRisks.filter((r) => !existingKeys.has(`${r.employeeId}:${r.type}`)),
|
||||
...monthlyTasks.filter((r) => !monthlyKeys.has(`${r.employeeId}:${r.title}`)),
|
||||
@@ -790,7 +846,7 @@ export async function getDashboardData(orgId: string) {
|
||||
const daysUntilDeadline = r.deadline ? daysBetween(r.deadline, new Date()) : null
|
||||
return {
|
||||
id: r.id,
|
||||
type: r.type as 'CONTRACT' | 'SALARY' | 'TERMINATION' | 'MONTHLY',
|
||||
type: r.type as 'CONTRACT' | 'SALARY' | 'TERMINATION' | 'MONTHLY' | 'ONBOARDING' | 'RETIREMENT',
|
||||
level: r.level.toLowerCase() as 'high' | 'medium' | 'low',
|
||||
priority,
|
||||
title: r.title,
|
||||
@@ -821,6 +877,7 @@ export async function getDashboardData(orgId: string) {
|
||||
if (title.includes('公积金')) return 'MONTHLY_HOUSING'
|
||||
if (title.includes('工资')) return 'MONTHLY_PAYROLL'
|
||||
if (title.includes('个税')) return 'MONTHLY_TAX'
|
||||
if (title.includes('退休')) return 'RETIREMENT'
|
||||
return title.replace(/\d+/g, '').trim()
|
||||
}
|
||||
|
||||
@@ -872,7 +929,7 @@ export async function getDashboardData(orgId: string) {
|
||||
|
||||
const resolvedTodos = resolvedItems.map((r: typeof resolvedItems[number]) => ({
|
||||
id: r.id,
|
||||
type: r.type as 'CONTRACT' | 'SALARY' | 'TERMINATION' | 'MONTHLY',
|
||||
type: r.type as 'CONTRACT' | 'SALARY' | 'TERMINATION' | 'MONTHLY' | 'ONBOARDING' | 'RETIREMENT',
|
||||
level: r.level.toLowerCase() as 'high' | 'medium' | 'low',
|
||||
title: r.title,
|
||||
description: r.description,
|
||||
@@ -906,7 +963,7 @@ export async function getDashboardData(orgId: string) {
|
||||
greeting,
|
||||
stats: {
|
||||
employeeCount,
|
||||
highRiskCount: dedupedTodos.filter((t) => t.level === 'high' && (t.type === 'CONTRACT' || t.type === 'TERMINATION')).length,
|
||||
highRiskCount: dedupedTodos.filter((t) => t.level === 'high' && (t.type === 'CONTRACT' || t.type === 'TERMINATION' || t.type === 'RETIREMENT')).length,
|
||||
todoCount: todos.length,
|
||||
monthlyOvertimePay,
|
||||
},
|
||||
@@ -1818,6 +1875,7 @@ export async function getAnnualValueReport(orgId: string, year: number) {
|
||||
if (title.includes('公积金')) return 'MONTHLY_HOUSING'
|
||||
if (title.includes('工资')) return 'MONTHLY_PAYROLL'
|
||||
if (title.includes('个税')) return 'MONTHLY_TAX'
|
||||
if (title.includes('退休')) return 'RETIREMENT'
|
||||
return title.replace(/\d+/g, '').trim()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user