feat: 解聘Step3合规检查增强 — 系统自动预填+建议
后端: - getChecklistForReason 接收 employee 数据,支持自动检查 - 医疗期是否届满:根据 isInMedicalPeriod 自动判断 - 是否经过培训/调岗:根据 trainingRecords 自动判断 - 经济补偿金/代通知金:系统给建议(suggestion + suggestionType) - checklist 接口改为 async,获取员工 profile 传入 前端: - checklist 查询传入 employeeId - useEffect 自动预填系统判断结果到 checklist state - Step 3 UI 重写:显示系统判断标签、来源说明、建议提示 - 建议按类型显示不同颜色(warning/required/info)
This commit is contained in:
@@ -19,9 +19,31 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/checklist/:reason', authMiddleware, (req: AuthRequest, res) => {
|
||||
const checklist = getChecklistForReason(req.params.reason)
|
||||
res.json({ success: true, data: checklist })
|
||||
router.get('/checklist/:reason', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const employeeId = req.query.employeeId as string
|
||||
let employee: any = undefined
|
||||
|
||||
if (employeeId) {
|
||||
const emp = await prisma.employee.findFirst({
|
||||
where: { id: employeeId, orgId: req.user!.orgId },
|
||||
include: {
|
||||
trainingRecords: true,
|
||||
},
|
||||
})
|
||||
if (emp) {
|
||||
employee = {
|
||||
isInMedicalPeriod: emp.isInMedicalPeriod,
|
||||
trainingRecords: emp.trainingRecords,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const checklist = getChecklistForReason(req.params.reason, employee)
|
||||
res.json({ success: true, data: checklist })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/assess/:employeeId', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
|
||||
@@ -6,39 +6,108 @@ function daysBetween(a: Date, b: Date): number {
|
||||
return Math.floor((a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
export function getChecklistForReason(reason: string): { key: string; label: string }[] {
|
||||
export interface ChecklistItem {
|
||||
key: string
|
||||
label: string
|
||||
autoChecked?: boolean | null // null=无法自动判断,true/false=系统判断结果
|
||||
autoSource?: string // 系统判断依据说明
|
||||
suggestion?: string // 系统建议说明
|
||||
suggestionType?: 'info' | 'warning' | 'required'
|
||||
}
|
||||
|
||||
export function getChecklistForReason(reason: string, employee?: any): ChecklistItem[] {
|
||||
switch (reason) {
|
||||
case 'NEGOTIATED':
|
||||
return [
|
||||
{ key: 'compensation_paid', label: '是否已支付经济补偿金' },
|
||||
{ key: 'agreement_signed', label: '是否签署协商解除协议' },
|
||||
{ key: 'final_pay_ready', label: '是否结清最后工资' },
|
||||
{
|
||||
key: 'compensation_paid', label: '是否已支付经济补偿金',
|
||||
autoChecked: null,
|
||||
suggestion: '协商解除需支付经济补偿金(N),建议在协商协议中明确金额',
|
||||
suggestionType: 'required',
|
||||
},
|
||||
{ key: 'agreement_signed', label: '是否签署协商解除协议', autoChecked: null },
|
||||
{ key: 'final_pay_ready', label: '是否结清最后工资', autoChecked: null },
|
||||
]
|
||||
case 'FAULT':
|
||||
return [
|
||||
{ key: 'has_rules', label: '是否有规章制度依据' },
|
||||
{ key: 'has_evidence', label: '是否有违纪证据' },
|
||||
{ key: 'notify_union', label: '是否事先通知工会' },
|
||||
{ key: 'written_notice', label: '是否出具书面解除通知' },
|
||||
]
|
||||
case 'NONFAULT':
|
||||
return [
|
||||
{ key: 'medical_period_end', label: '医疗期是否已届满' },
|
||||
{ key: 'training_given', label: '是否经过培训或调岗' },
|
||||
{ key: 'compensation_paid', label: '是否支付经济补偿金' },
|
||||
{ key: 'advance_notice', label: '是否提前30天通知或支付代通知金' },
|
||||
{ key: 'has_rules', label: '是否有规章制度依据', autoChecked: null },
|
||||
{ key: 'has_evidence', label: '是否有违纪证据', autoChecked: null },
|
||||
{ key: 'notify_union', label: '是否事先通知工会', autoChecked: null },
|
||||
{ key: 'written_notice', label: '是否出具书面解除通知', autoChecked: null },
|
||||
]
|
||||
case 'NONFAULT': {
|
||||
const items: ChecklistItem[] = []
|
||||
|
||||
// 医疗期是否已届满 — 系统自动判断
|
||||
if (employee?.isInMedicalPeriod) {
|
||||
items.push({
|
||||
key: 'medical_period_end', label: '医疗期是否已届满',
|
||||
autoChecked: false,
|
||||
autoSource: '系统记录显示该员工正处于医疗期内,医疗期未届满',
|
||||
suggestion: '医疗期内不得以非过错理由解除,需等待医疗期届满',
|
||||
suggestionType: 'warning',
|
||||
})
|
||||
} else {
|
||||
items.push({
|
||||
key: 'medical_period_end', label: '医疗期是否已届满',
|
||||
autoChecked: null,
|
||||
autoSource: '系统未记录该员工处于医疗期,如实际已届满请勾选确认',
|
||||
})
|
||||
}
|
||||
|
||||
// 是否经过培训或调岗 — 系统自动判断
|
||||
const hasTraining = employee?.trainingRecords?.length > 0
|
||||
items.push({
|
||||
key: 'training_given', label: '是否经过培训或调岗',
|
||||
autoChecked: hasTraining ? true : null,
|
||||
autoSource: hasTraining
|
||||
? `系统记录显示该员工有${employee.trainingRecords.length}条培训记录`
|
||||
: '系统未找到培训或调岗记录,请人工确认',
|
||||
suggestion: hasTraining
|
||||
? '已有培训记录,满足"不胜任工作经培训或调岗"的前提条件'
|
||||
: '以不胜任工作为由解除前,必须先经过培训或调岗,否则违法解除风险极高',
|
||||
suggestionType: hasTraining ? 'info' : 'warning',
|
||||
})
|
||||
|
||||
// 是否支付经济补偿金 — 系统建议
|
||||
items.push({
|
||||
key: 'compensation_paid', label: '是否支付经济补偿金',
|
||||
autoChecked: null,
|
||||
suggestion: '非过错解除需支付经济补偿金(N),并在Step 4费用结算中确认金额',
|
||||
suggestionType: 'required',
|
||||
})
|
||||
|
||||
// 是否提前30天通知或支付代通知金 — 系统建议
|
||||
items.push({
|
||||
key: 'advance_notice', label: '是否提前30天通知或支付代通知金',
|
||||
autoChecked: null,
|
||||
suggestion: '非过错解除需提前30天书面通知,或额外支付1个月工资作为代通知金(N+1)',
|
||||
suggestionType: 'required',
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
case 'LAYOFF':
|
||||
return [
|
||||
{ key: 'advance_notice_30', label: '是否提前30天向工会或全体职工说明' },
|
||||
{ key: 'listen_opinions', label: '是否听取工会或职工意见' },
|
||||
{ key: 'report_labor_dept', label: '是否向劳动行政部门报告' },
|
||||
{ key: 'compensation_paid', label: '是否支付经济补偿金' },
|
||||
{ key: 'advance_notice_30', label: '是否提前30天向工会或全体职工说明', autoChecked: null },
|
||||
{ key: 'listen_opinions', label: '是否听取工会或职工意见', autoChecked: null },
|
||||
{ key: 'report_labor_dept', label: '是否向劳动行政部门报告', autoChecked: null },
|
||||
{
|
||||
key: 'compensation_paid', label: '是否支付经济补偿金',
|
||||
autoChecked: null,
|
||||
suggestion: '裁员需支付经济补偿金(N)',
|
||||
suggestionType: 'required',
|
||||
},
|
||||
]
|
||||
case 'EXPIRED':
|
||||
return [
|
||||
{ key: 'compensation_paid', label: '是否支付经济补偿金(如需)' },
|
||||
{ key: 'written_notice', label: '是否提前通知员工不续签' },
|
||||
{
|
||||
key: 'compensation_paid', label: '是否支付经济补偿金(如需)',
|
||||
autoChecked: null,
|
||||
suggestion: '公司提出不续签需支付经济补偿金(N);员工主动提出不续签则无需支付',
|
||||
suggestionType: 'info',
|
||||
},
|
||||
{ key: 'written_notice', label: '是否提前通知员工不续签', autoChecked: null },
|
||||
]
|
||||
default:
|
||||
return []
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { AlertTriangle, Check, ChevronRight, ChevronLeft, Shield, Info, Calculator, FileText, Printer } from 'lucide-react'
|
||||
import api from '../lib/api'
|
||||
@@ -136,15 +136,29 @@ export default function Termination() {
|
||||
return list
|
||||
}, [profile])
|
||||
|
||||
const { data: checklistItems } = useQuery<{ key: string; label: string }[]>({
|
||||
queryKey: ['checklist', reason],
|
||||
const { data: checklistItems } = useQuery<{
|
||||
key: string; label: string; autoChecked?: boolean | null; autoSource?: string; suggestion?: string; suggestionType?: string
|
||||
}[]>({
|
||||
queryKey: ['checklist', reason, employeeId],
|
||||
queryFn: async () => {
|
||||
const res = await api.get(`/termination/checklist/${reason}`) as any
|
||||
const res = await api.get(`/termination/checklist/${reason}`, { params: { employeeId } }) as any
|
||||
return res.data
|
||||
},
|
||||
enabled: !!reason && step >= 2,
|
||||
enabled: !!reason && !!employeeId && step >= 2,
|
||||
})
|
||||
|
||||
// checklist 加载后自动预填系统判断结果
|
||||
useEffect(() => {
|
||||
if (checklistItems) {
|
||||
const prefilled: Record<string, boolean> = {}
|
||||
checklistItems.forEach((item) => {
|
||||
if (item.autoChecked === true) prefilled[item.key] = true
|
||||
else if (item.autoChecked === false) prefilled[item.key] = false
|
||||
})
|
||||
setChecklist(prefilled)
|
||||
}
|
||||
}, [checklistItems])
|
||||
|
||||
const { data: riskAssessment } = useQuery<{ level: string; warnings: string[] }>({
|
||||
queryKey: ['assess', employeeId, reason],
|
||||
queryFn: async () => {
|
||||
@@ -409,16 +423,47 @@ export default function Termination() {
|
||||
{/* Step 3: 合规检查 */}
|
||||
{step === 2 && (
|
||||
<div className="space-y-3">
|
||||
{checklistItems?.map((item) => (
|
||||
<label key={item.key} className="flex items-center gap-3 p-3 rounded-md border cursor-pointer hover:bg-gray-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checklist[item.key] || false}
|
||||
onChange={(e) => setChecklist({ ...checklist, [item.key]: e.target.checked })}
|
||||
/>
|
||||
<span className="text-xs">{item.label}</span>
|
||||
</label>
|
||||
))}
|
||||
<div className="bg-blue-50 text-blue-700 text-xs px-3 py-2 rounded-md flex items-start gap-2">
|
||||
<Info className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
<div>系统已根据员工记录自动预填部分检查项,您可逐项调整后继续。</div>
|
||||
</div>
|
||||
{checklistItems?.map((item) => {
|
||||
const checked = checklist[item.key] || false
|
||||
const isAutoChecked = item.autoChecked !== null && item.autoChecked !== undefined
|
||||
const suggestionColor =
|
||||
item.suggestionType === 'warning' ? 'bg-amber-50 text-amber-700' :
|
||||
item.suggestionType === 'required' ? 'bg-red-50 text-red-700' :
|
||||
'bg-gray-50 text-gray-600'
|
||||
return (
|
||||
<div key={item.key} className="border rounded-md p-3 space-y-2">
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={(e) => setChecklist({ ...checklist, [item.key]: e.target.checked })}
|
||||
/>
|
||||
<span className="text-xs font-medium">{item.label}</span>
|
||||
{isAutoChecked && (
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${item.autoChecked ? 'bg-green-50 text-safe' : 'bg-red-50 text-danger'}`}>
|
||||
系统判断:{item.autoChecked ? '是' : '否'}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
{item.autoSource && (
|
||||
<div className="text-xs text-gray-500 pl-7 flex items-start gap-1">
|
||||
<Info className="w-3 h-3 mt-0.5 shrink-0" />
|
||||
<span>{item.autoSource}</span>
|
||||
</div>
|
||||
)}
|
||||
{item.suggestion && (
|
||||
<div className={`text-xs px-2 py-1.5 rounded-md ml-7 flex items-start gap-1 ${suggestionColor}`}>
|
||||
<AlertTriangle className="w-3 h-3 mt-0.5 shrink-0" />
|
||||
<span>{item.suggestion}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user