From 1b05facc9239be14d4c0b47555c1a9ec1e3ab614 Mon Sep 17 00:00:00 2001 From: freedakgmail Date: Thu, 23 Jul 2026 16:56:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=A3=E8=81=98Step3=E5=90=88?= =?UTF-8?q?=E8=A7=84=E6=A3=80=E6=9F=A5=E5=A2=9E=E5=BC=BA=20=E2=80=94=20?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E8=87=AA=E5=8A=A8=E9=A2=84=E5=A1=AB+?= =?UTF-8?q?=E5=BB=BA=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端: - getChecklistForReason 接收 employee 数据,支持自动检查 - 医疗期是否届满:根据 isInMedicalPeriod 自动判断 - 是否经过培训/调岗:根据 trainingRecords 自动判断 - 经济补偿金/代通知金:系统给建议(suggestion + suggestionType) - checklist 接口改为 async,获取员工 profile 传入 前端: - checklist 查询传入 employeeId - useEffect 自动预填系统判断结果到 checklist state - Step 3 UI 重写:显示系统判断标签、来源说明、建议提示 - 建议按类型显示不同颜色(warning/required/info) --- backend/src/routes/termination.routes.ts | 28 ++++- backend/src/services/termination.service.ts | 111 ++++++++++++++++---- frontend/src/pages/Termination.tsx | 75 ++++++++++--- 3 files changed, 175 insertions(+), 39 deletions(-) diff --git a/backend/src/routes/termination.routes.ts b/backend/src/routes/termination.routes.ts index 7153d94..ac405f5 100644 --- a/backend/src/routes/termination.routes.ts +++ b/backend/src/routes/termination.routes.ts @@ -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) => { diff --git a/backend/src/services/termination.service.ts b/backend/src/services/termination.service.ts index fccb1a0..9e869ab 100644 --- a/backend/src/services/termination.service.ts +++ b/backend/src/services/termination.service.ts @@ -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 [] diff --git a/frontend/src/pages/Termination.tsx b/frontend/src/pages/Termination.tsx index e139b20..ef42de3 100644 --- a/frontend/src/pages/Termination.tsx +++ b/frontend/src/pages/Termination.tsx @@ -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 = {} + 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 && (
- {checklistItems?.map((item) => ( - - ))} +
+ +
系统已根据员工记录自动预填部分检查项,您可逐项调整后继续。
+
+ {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 ( +
+ + {item.autoSource && ( +
+ + {item.autoSource} +
+ )} + {item.suggestion && ( +
+ + {item.suggestion} +
+ )} +
+ ) + })}
)}