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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user