Sprint 3: 员工目录优化 + Profile Shell + 离职工作流重构 + 合同类型扩充 + 月度社保重构 + 商险管理Tab

S3-1: Roster.tsx 添加 InlineAlert 合同风险提示 + 快捷筛选标签
S3-2: 新建 EmployeeProfileShell.tsx 统一员工详情布局,重构 EmployeeProfile.tsx
S3-3: Termination.tsx 集成 Stepper 步骤条 + InlineAlert 风险提示
S3-4: schema.prisma ContractType 枚举扩充 DISPATCH/OUTSOURCING/PARTTIME + 后端接口 + 前端选择器
S3-5: SocialInsurance.tsx 月度办理 Tab 使用 InlineAlert 替换原始提示
S3-6: SocialInsurance.tsx 新增商险管理 Tab(方案CRUD + 参保人员列表)
This commit is contained in:
selfrelease
2026-07-31 18:07:49 +08:00
parent 55b6867c9c
commit 9946197d20
7 changed files with 636 additions and 81 deletions
+60 -1
View File
@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useState, useMemo } from 'react'
import { toast } from 'sonner'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useConfirm } from '../hooks/useConfirm'
@@ -13,6 +13,7 @@ import Modal from '../components/ui/Modal'
import Pagination from '../components/ui/Pagination'
import { fmt, terminateReasonMap, DetailTab, TAB_GROUPS, TAB_COUNT_KEYS } from './roster/shared'
import EmployeeProfile from './roster/EmployeeProfile'
import { InlineAlert } from '../components/ui/InlineAlert'
import { AddEmployeeModal, ResignModal, RehireModal, SalaryChangeModal, DeptChangeModal } from './roster/modals'
import { ImportSettings } from './Settings'
@@ -219,6 +220,23 @@ export default function Roster() {
const hasActiveFilters = search || filterStatus || filterContractStatus || filterDepartment
/** 计算合同风险统计(基于当前页数据) */
const riskStats = useMemo(() => {
const expiring = employees.filter((e: any) => e.contractStatus === 'expiring').length
const expired = employees.filter((e: any) => e.contractStatus === 'expired').length
const unsigned = employees.filter((e: any) => ['unsigned', 'unsigned_over_30', 'unsigned_over_year'].includes(e.contractStatus)).length
const probation = employees.filter((e: any) => e.probationInfo?.isProbation && e.probationInfo?.isExpiring).length
return { expiring, expired, unsigned, probation }
}, [employees])
/** 快捷筛选标签 */
const quickFilters = [
{ key: 'expiring', label: '即将到期', count: riskStats.expiring, color: 'amber', filter: { status: '', contractStatus: 'expiring', department: '' } },
{ key: 'expired', label: '已到期', count: riskStats.expired, color: 'rose', filter: { status: '', contractStatus: 'expired', department: '' } },
{ key: 'unsigned', label: '未签合同', count: riskStats.unsigned, color: 'rose', filter: { status: '', contractStatus: 'unsigned', department: '' } },
{ key: 'probation', label: '试用期即将到期', count: riskStats.probation, color: 'amber', filter: { status: 'PROBATION', contractStatus: '', department: '' } },
].filter(f => f.count > 0)
const { data: departmentList } = useQuery<string[]>({
queryKey: ['roster-departments'],
queryFn: async () => {
@@ -332,6 +350,44 @@ export default function Roster() {
</div>
)}
{/* 合同风险提示 */}
{!isLoading && (riskStats.expired > 0 || riskStats.unsigned > 0) && (
<InlineAlert
type="warning"
title="合同风险提醒"
closable
>
{riskStats.expired > 0 && <span> <strong className="text-danger">{riskStats.expired}</strong> </span>}
{riskStats.unsigned > 0 && <span><strong className="text-danger">{riskStats.unsigned}</strong> </span>}
<span className="text-gray-500"></span>
</InlineAlert>
)}
{/* 快捷筛选标签 */}
{!isLoading && quickFilters.length > 0 && (
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs text-gray-400"></span>
{quickFilters.map((f) => (
<button
key={f.key}
onClick={() => {
setFilterStatus(f.filter.status)
setFilterContractStatus(f.filter.contractStatus)
setFilterDepartment(f.filter.department)
setPage(1)
}}
className={`px-2.5 py-1 rounded-full text-xs font-medium border transition-all hover:shadow-sm ${
f.color === 'rose'
? 'border-rose-200 bg-rose-50 text-rose-700 hover:bg-rose-100'
: 'border-amber-200 bg-amber-50 text-amber-700 hover:bg-amber-100'
}`}
>
{f.label}{f.count}
</button>
))}
</div>
)}
{isLoading ? (
<div className="rounded-lg border border-gray-200 bg-white py-16 text-center text-sm text-gray-400">...</div>
) : filtered.length === 0 ? (
@@ -422,6 +478,9 @@ export default function Roster() {
UNFIXED: { label: '劳动合同-无固定期', style: 'bg-purple-50 text-purple-700 border border-purple-200' },
LABOR: { label: '劳务协议', style: 'bg-amber-50 text-amber-700 border border-amber-200' },
INTERNSHIP: { label: '实习协议', style: 'bg-teal-50 text-teal-700 border border-teal-200' },
DISPATCH: { label: '劳务派遣', style: 'bg-cyan-50 text-cyan-700 border border-cyan-200' },
OUTSOURCING: { label: '业务外包', style: 'bg-slate-50 text-slate-700 border border-slate-200' },
PARTTIME: { label: '兼职协议', style: 'bg-indigo-50 text-indigo-700 border border-indigo-200' },
UNSIGNED: { label: '未签合同', style: 'bg-gray-100 text-gray-500 border border-gray-200' },
}
const ct = e.latestContract?.contractType