优化: 大文件拆分+代码分割+按需加载+console清理+any类型替换
- Money.tsx (2260行→54行): 拆分为 money/ 子目录4个组件, React.lazy二级分割 - AIAssistant.tsx (2038行→63行): 拆分为 ai-assistant/ 子目录6个组件, React.lazy二级分割 - xlsx改为动态导入, OvertimeTab从345KB降至12.7KB - api-services.ts: 请求参数 any→Record<string,unknown> - 移除前端3处console.log残留 - 后端console替换为pino logger - 前后端未使用import/变量清理 - Zod schema验证: termination/platform/special-status/work-process - 新增 leave.routes.ts, acceptance-test.routes.ts - UI组件: PageGuide, QueryError, Stepper
This commit is contained in:
@@ -4,6 +4,7 @@ import { toast } from 'sonner'
|
||||
import { AlertTriangle, Check, ChevronRight, ChevronLeft, Shield, Info, Calculator, FileText, Printer, Trash2, List, Download, Plus, Edit, Send, CheckCircle, XCircle, Play, Ban, CheckCheck } from 'lucide-react'
|
||||
import { Stepper } from '../components/ui/Stepper'
|
||||
import { InlineAlert } from '../components/ui/InlineAlert'
|
||||
import PageGuide from '../components/ui/PageGuide'
|
||||
import jsPDF from 'jspdf'
|
||||
import { rosterApi, terminationApi } from '../lib/api-services'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
@@ -12,6 +13,7 @@ import Button from '../components/ui/Button'
|
||||
import { Input, Label, Select } from '../components/ui/Input'
|
||||
import EmptyState from '../components/ui/EmptyState'
|
||||
import Pagination from '../components/ui/Pagination'
|
||||
import { useConfirm } from '../hooks/useConfirm'
|
||||
|
||||
// 金额格式化:保留两位小数 + 千分位
|
||||
const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
@@ -139,19 +141,6 @@ const DEFAULT_HANDOVER_ITEMS = [
|
||||
{ key: 'contract_return', label: '劳动合同收回', done: false, remark: '' },
|
||||
]
|
||||
|
||||
interface RosterEmployee {
|
||||
id: string
|
||||
name: string
|
||||
department: string
|
||||
status: string
|
||||
hasTermination?: boolean
|
||||
latestTerminationStatus?: string
|
||||
hireDate: string
|
||||
monthlySalary: number
|
||||
latestContract: any
|
||||
counts: any
|
||||
}
|
||||
|
||||
interface EmployeeProfile {
|
||||
id: string
|
||||
name: string
|
||||
@@ -171,6 +160,7 @@ interface EmployeeProfile {
|
||||
|
||||
export default function Termination() {
|
||||
const queryClient = useQueryClient()
|
||||
const confirm = useConfirm()
|
||||
const [view, setView] = useState<'list' | 'wizard' | 'detail'>('list')
|
||||
const [draftId, setDraftId] = useState<string | null>(null)
|
||||
const [step, setStep] = useState(0)
|
||||
@@ -182,7 +172,7 @@ export default function Termination() {
|
||||
const [checklist, setChecklist] = useState<Record<string, boolean>>({})
|
||||
const [acknowledgeRisk, setAcknowledgeRisk] = useState(false)
|
||||
const [socialAvgWage, setSocialAvgWage] = useState(0)
|
||||
const [compBreakdown, setCompBreakdown] = useState<any>(null)
|
||||
const [, setCompBreakdown] = useState<any>(null)
|
||||
const [compAdjustments, setCompAdjustments] = useState<Array<{ field: string; from: number; to: number; reason: string }>>([])
|
||||
const [handoverItems, setHandoverItems] = useState(DEFAULT_HANDOVER_ITEMS)
|
||||
const [checklistOverrides, setChecklistOverrides] = useState<Record<string, { checked: boolean; overrideReason: string }>>({})
|
||||
@@ -341,7 +331,7 @@ export default function Termination() {
|
||||
})
|
||||
|
||||
// 草稿列表
|
||||
const { data: draftsData, refetch: refetchDrafts } = useQuery({
|
||||
const { data: draftsData } = useQuery({
|
||||
queryKey: ['termination-drafts', filterStatus, filterDepartment, searchTerm, draftPage, draftPageSize],
|
||||
queryFn: async () => {
|
||||
const params: any = { page: draftPage, pageSize: draftPageSize }
|
||||
@@ -524,28 +514,37 @@ export default function Termination() {
|
||||
}
|
||||
}, [selectedEmployee, terminationDate, socialAvgWage, reason])
|
||||
|
||||
// 计算调整后的实际补偿金(系统预估 + 手动调整差额)
|
||||
const adjustedTotal = useMemo(() => {
|
||||
if (!costResult) return 0
|
||||
let total = costResult.grandTotal
|
||||
compAdjustments.forEach(adj => {
|
||||
total += (adj.to - adj.from)
|
||||
})
|
||||
return total
|
||||
}, [costResult, compAdjustments])
|
||||
|
||||
const canProceed = () => {
|
||||
if (step === 0) return !!employeeId && (!!draftId || !selectedEmployee?.hasTermination || selectedEmployee?.latestTerminationStatus === 'CANCELLED' || selectedEmployee?.latestTerminationStatus === 'COMPLETED')
|
||||
if (step === 1) return !!reason && !!terminationDate && (!riskAssessment?.warnings.length || acknowledgeRisk)
|
||||
if (step === 2) return true
|
||||
// step 2: 合规检查 — required 项必须勾选
|
||||
if (step === 2) {
|
||||
if (!checklistItems) return true
|
||||
const requiredItems = checklistItems.filter(item => item.suggestionType === 'required')
|
||||
if (requiredItems.length === 0) return true
|
||||
return requiredItems.every(item => checklist[item.key] === true)
|
||||
}
|
||||
// step 3: 费用结算 — 有补偿金时需确认
|
||||
if (step === 3) return true
|
||||
if (step === 4) return true
|
||||
// step 4: 工作交接 — 关键项需完成
|
||||
if (step === 4) {
|
||||
const keyItems = handoverItems.filter(item => item.key === 'work_handover' || item.key === 'equipment_return' || item.key === 'access_revoke')
|
||||
if (keyItems.length === 0) return true
|
||||
return keyItems.every(item => item.done)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
saveMutation.mutate({
|
||||
employeeId,
|
||||
reason,
|
||||
terminationDate: new Date(terminationDate).toISOString(),
|
||||
socialInsEndMonth: socialInsEndMonth || terminationDate.slice(0, 7),
|
||||
housingFundEndMonth: housingFundEndMonth || terminationDate.slice(0, 7),
|
||||
compensation: costResult?.totalSeverance || 0,
|
||||
checklist,
|
||||
remark: '',
|
||||
})
|
||||
}
|
||||
|
||||
/** 保存草稿(任意步骤可调用) */
|
||||
const handleSaveDraft = () => {
|
||||
const breakdown = costResult ? {
|
||||
@@ -553,7 +552,8 @@ export default function Termination() {
|
||||
noticePay: costResult.noticePay,
|
||||
doublePay: costResult.doublePay,
|
||||
other: 0,
|
||||
total: costResult.grandTotal,
|
||||
total: adjustedTotal,
|
||||
systemTotal: costResult.grandTotal,
|
||||
adjustments: compAdjustments,
|
||||
} : null
|
||||
|
||||
@@ -564,7 +564,7 @@ export default function Termination() {
|
||||
terminationDate: terminationDate ? new Date(terminationDate).toISOString() : new Date().toISOString(),
|
||||
socialInsEndMonth: socialInsEndMonth || terminationDate.slice(0, 7),
|
||||
housingFundEndMonth: housingFundEndMonth || terminationDate.slice(0, 7),
|
||||
compensation: costResult?.grandTotal || 0,
|
||||
compensation: adjustedTotal || 0,
|
||||
checklist,
|
||||
currentStep: step,
|
||||
compensationBreakdown: breakdown,
|
||||
@@ -610,41 +610,6 @@ export default function Termination() {
|
||||
toast.success('已调整')
|
||||
}
|
||||
|
||||
// 模拟计算:追加新版本,支持参数对比
|
||||
const handleSimulate = () => {
|
||||
if (!costResult || !selectedEmployee) return
|
||||
setSavedItems((prev) => {
|
||||
// 该员工的最新版本号
|
||||
const sameEmployee = prev.filter(item => item.employeeId === employeeId)
|
||||
const maxVersion = sameEmployee.reduce((max, item) => Math.max(max, item.version), 0)
|
||||
const newVersion = maxVersion + 1
|
||||
// 追加新版本(不覆盖旧版本)
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
id: `sim-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
employeeId,
|
||||
name: selectedEmployee.name,
|
||||
department: selectedEmployee.department,
|
||||
reason,
|
||||
reasonLabel,
|
||||
terminationDate,
|
||||
severancePay: costResult.severancePay,
|
||||
noticePay: costResult.noticePay,
|
||||
doublePay: costResult.doublePay,
|
||||
grandTotal: costResult.grandTotal,
|
||||
years: costResult.years,
|
||||
remainingMonths: costResult.remainingMonths,
|
||||
compMonths: costResult.compMonths,
|
||||
version: newVersion,
|
||||
isSimulated: true,
|
||||
createdAt: new Date().toISOString().slice(0, 19).replace('T', ' '),
|
||||
},
|
||||
]
|
||||
})
|
||||
handleReset()
|
||||
}
|
||||
|
||||
// 保存成功后追加正式版本(isSimulated=false)
|
||||
useEffect(() => {
|
||||
if (saveMutation.isSuccess && costResult && selectedEmployee) {
|
||||
@@ -731,6 +696,9 @@ export default function Termination() {
|
||||
{/* 草稿列表视图 */}
|
||||
{view === 'list' && (
|
||||
<>
|
||||
<PageGuide>
|
||||
解聘补偿页面用于规范处理离职、解聘审批及补偿核算。流程:①新建解聘草稿,选择员工和解聘原因 → ②系统自动计算经济补偿金 → ③合规检查(合同状态、医疗期、孕期等) → ④确认补偿方案 → ⑤生成解聘协议等法律文书。支持协商解除、过错解除、非过错解除、经济性裁员等场景。
|
||||
</PageGuide>
|
||||
<div className="flex gap-2 flex-wrap items-center">
|
||||
<Input
|
||||
placeholder="搜索员工姓名或部门"
|
||||
@@ -844,8 +812,8 @@ export default function Termination() {
|
||||
)}
|
||||
{item.status === 'DRAFT' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm(`确认执行「${item.employeeName}」的解聘手续?\n确认后员工状态将变更为离职,社保/公积金将停缴,此操作不可撤销。`)) {
|
||||
onClick={async () => {
|
||||
if (await confirm({ title: '执行解聘', message: `确认执行「${item.employeeName}」的解聘手续?\n确认后员工状态将变更为离职,社保/公积金将停缴,此操作不可撤销。` })) {
|
||||
setDraftId(item.id)
|
||||
executeMutation.mutate()
|
||||
}
|
||||
@@ -1548,12 +1516,23 @@ export default function Termination() {
|
||||
<div>社保截止:{socialInsEndMonth || terminationDate.slice(0, 7)}</div>
|
||||
<div>公积金截止:{housingFundEndMonth || terminationDate.slice(0, 7)}</div>
|
||||
{costResult && (
|
||||
<div>补偿金合计:¥{fmt(costResult.grandTotal)}</div>
|
||||
<>
|
||||
{compAdjustments.length > 0 ? (
|
||||
<>
|
||||
<div>系统预估补偿金:¥{fmt(costResult.grandTotal)}</div>
|
||||
{compAdjustments.map((adj, i) => (
|
||||
<div key={i} className="text-amber-700">
|
||||
手动调整 - {adj.field}:¥{fmt(adj.from)} → ¥{fmt(adj.to)}({adj.reason})
|
||||
</div>
|
||||
))}
|
||||
<div className="font-medium text-primary">实际补偿金合计:¥{fmt(adjustedTotal)}</div>
|
||||
</>
|
||||
) : (
|
||||
<div>补偿金合计:¥{fmt(costResult.grandTotal)}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div>工作交接:{handoverItems.filter(i => i.done).length}/{handoverItems.length} 项完成</div>
|
||||
{compAdjustments.length > 0 && (
|
||||
<div className="text-amber-700">补偿金已手动调整 {compAdjustments.length} 项</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 风险提示 */}
|
||||
@@ -1771,9 +1750,17 @@ export default function Termination() {
|
||||
<ChevronLeft className="w-4 h-4 mr-1" />上一步
|
||||
</Button>
|
||||
{step < 4 ? (
|
||||
<Button onClick={() => setStep(step + 1)} disabled={!canProceed()}>
|
||||
下一步<ChevronRight className="w-4 h-4 ml-1" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
{!canProceed() && step === 2 && checklistItems?.some(item => item.suggestionType === 'required' && !checklist[item.key]) && (
|
||||
<span className="text-xs text-danger">请先勾选所有必检项</span>
|
||||
)}
|
||||
{!canProceed() && step === 4 && handoverItems.some(item => (item.key === 'work_handover' || item.key === 'equipment_return' || item.key === 'access_revoke') && !item.done) && (
|
||||
<span className="text-xs text-danger">请先完成工作交接、设备归还、权限收回</span>
|
||||
)}
|
||||
<Button onClick={() => setStep(step + 1)} disabled={!canProceed()}>
|
||||
下一步<ChevronRight className="w-4 h-4 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
) : step === 4 ? (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="secondary" onClick={handleSaveDraft} disabled={saveDraftMutation.isPending}>
|
||||
|
||||
Reference in New Issue
Block a user