From 4486adc957c5c2891b75ee7ed48ccf6de96f3360 Mon Sep 17 00:00:00 2001 From: selfrelease Date: Sat, 15 Aug 2026 14:41:37 +0800 Subject: [PATCH] =?UTF-8?q?ux:=20=E8=8A=B1=E5=90=8D=E5=86=8C=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E4=BC=98=E5=8C=96=20-=20=E5=8E=BB=E6=8E=89=E5=8F=91?= =?UTF-8?q?=E8=96=AA=E3=80=81=E5=BC=80=E5=85=B7=E8=AF=81=E6=98=8E/?= =?UTF-8?q?=E7=BB=AD=E7=AD=BE=E5=90=88=E5=90=8C=E7=9B=B4=E6=8E=A5=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E3=80=81=E5=88=A0=E9=99=A4=E7=94=A8=E5=B7=A5=E5=8A=9E?= =?UTF-8?q?=E7=90=86=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 去掉花名册操作栏的"发薪"按钮 - 开具证明改为弹窗直接创建+提交,不再跳转用工办理页面 - 续签合同改为弹窗直接创建+提交,自动推导新合同开始日期 - 批量开具证明改为直接API调用,不再跳转用工办理页面 - 删除用工办理页面(WorkProcess.tsx)和路由 - 去掉工具栏"用工办理"按钮 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- frontend/src/App.tsx | 2 - frontend/src/pages/Roster.tsx | 216 ++++++- frontend/src/pages/WorkProcess.tsx | 943 ----------------------------- 3 files changed, 188 insertions(+), 973 deletions(-) delete mode 100644 frontend/src/pages/WorkProcess.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fb61aa3..107f59c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -41,7 +41,6 @@ const MedicalPeriodCalculator = lazy(() => import('./pages/tools/MedicalPeriodCa const HealthCheck = lazy(() => import('./pages/tools/HealthCheck')) const AnnualValueReport = lazy(() => import('./pages/tools/AnnualValueReport')) const CalendarPage = lazy(() => import('./pages/Calendar')) -const WorkProcess = lazy(() => import('./pages/WorkProcess')) const MyAttendance = lazy(() => import('./pages/portal/MyAttendance')) const MyLeave = lazy(() => import('./pages/portal/MyLeave')) const SpecialStatus = lazy(() => import('./pages/SpecialStatus')) @@ -204,7 +203,6 @@ export default function App() { } /> } /> } /> - } /> } /> } /> } /> diff --git a/frontend/src/pages/Roster.tsx b/frontend/src/pages/Roster.tsx index b8bac7c..b01cbf9 100644 --- a/frontend/src/pages/Roster.tsx +++ b/frontend/src/pages/Roster.tsx @@ -5,7 +5,7 @@ import { toast } from 'sonner' import { toastError } from '../lib/errorToast' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useConfirm } from '../hooks/useConfirm' -import { Users, Plus, Check, UserX, UserPlus, DollarSign, Building2, RotateCcw, Upload, Wallet, Download, Phone, MapPin, Search, Settings2, CheckCircle, FileText } from 'lucide-react' +import { Users, Plus, Check, UserX, UserPlus, DollarSign, Building2, RotateCcw, Upload, Download, Phone, MapPin, Search, Settings2, CheckCircle, FileText } from 'lucide-react' import { rosterApi, employeeApi, terminationApi, workProcessApi } from '../lib/api-services' import { copyToClipboard } from '../lib/clipboard' import { useAuthStore } from '../store/authStore' @@ -79,6 +79,16 @@ export default function Roster() { const [deptEmployee, setDeptEmployee] = useState(null) const [showConfirmModal, setShowConfirmModal] = useState(false) const [confirmEmployee, setConfirmEmployee] = useState(null) + // 开具证明弹窗 + const [showCertModal, setShowCertModal] = useState(false) + const [certEmployee, setCertEmployee] = useState(null) + const [certPurpose, setCertPurpose] = useState('') + // 续签合同弹窗 + const [showRenewModal, setShowRenewModal] = useState(false) + const [renewEmployee, setRenewEmployee] = useState(null) + const [renewNewStartDate, setRenewNewStartDate] = useState('') + const [renewNewEndDate, setRenewNewEndDate] = useState('') + const [renewNewSalary, setRenewNewSalary] = useState('') const pageSize = usePageSize() const [page, setPage] = useState(1) const [selectedIds, setSelectedIds] = useState>(new Set()) @@ -141,9 +151,7 @@ export default function Roster() { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }) localStorage.removeItem('add-employee-draft') setShowAddModal(false) - toast.success('员工已添加', { - action: { label: '前往用工办理', onClick: () => navigate('/work-process') }, - }) + toast.success('员工已添加') }, onError: (err: any) => toastError(err, '创建失败'), }) @@ -281,6 +289,47 @@ export default function Roster() { }, }) + /** 开具收入证明:直接创建并提交工单 */ + const certMutation = useMutation({ + mutationFn: async (data: { employeeId: string; formData: Record }) => { + const created: any = await workProcessApi.create({ type: 'INCOME_CERT', title: '开具收入证明', employeeId: data.employeeId, formData: data.formData, status: 'DRAFT' }) + if (created?.id) { + await workProcessApi.submit(created.id) + } + return created + }, + onSuccess: () => { + toast.success('收入证明已开具,可在「证据管理」中查看和下载') + queryClient.invalidateQueries({ queryKey: ['work-processes'] }) + setShowCertModal(false) + setCertEmployee(null) + setCertPurpose('') + }, + onError: (err: any) => toastError(err, '开具证明失败'), + }) + + /** 续签合同:直接创建并提交工单 */ + const renewMutation = useMutation({ + mutationFn: async (data: { employeeId: string; formData: Record }) => { + const created: any = await workProcessApi.create({ type: 'RENEW', title: '合同续签', employeeId: data.employeeId, formData: data.formData, status: 'DRAFT' }) + if (created?.id) { + await workProcessApi.submit(created.id) + } + return created + }, + onSuccess: () => { + toast.success('合同续签已完成') + queryClient.invalidateQueries({ queryKey: ['work-processes'] }) + queryClient.invalidateQueries({ queryKey: ['roster'] }) + setShowRenewModal(false) + setRenewEmployee(null) + setRenewNewStartDate('') + setRenewNewEndDate('') + setRenewNewSalary('') + }, + onError: (err: any) => toastError(err, '续签合同失败'), + }) + /** 批量转正:为选中的试用期员工创建转正草稿 */ const handleBatchConfirm = async () => { const probationEmployees = employees.filter((e: any) => selectedIds.has(e.id) && e.probationInfo?.isProbation) @@ -314,14 +363,44 @@ export default function Roster() { setSelectedIds(new Set()) } - /** 批量开具证明:跳转到用工办理批量开具收入证明 */ - const handleBatchCert = () => { + /** 批量开具证明:直接为选中员工创建并提交收入证明 */ + const handleBatchCert = async () => { if (selectedIds.size === 0) { toast.error('请至少选择一名员工') return } - const ids = Array.from(selectedIds).join(',') - window.location.href = `/work-process?type=INCOME_CERT&employeeIds=${ids}` + let success = 0 + let failed = 0 + for (const id of selectedIds) { + const emp = employees.find((e: any) => e.id === id) + if (!emp) continue + try { + const created: any = await workProcessApi.create({ + type: 'INCOME_CERT', + title: '开具收入证明', + employeeId: id, + formData: { + employeeId: id, + employeeName: emp.name, + idCardNumber: emp.idCardMasked || '', + position: emp.position || '', + monthlyIncome: emp.monthlySalary ? `¥${emp.monthlySalary}` : '', + purpose: '批量开具', + }, + status: 'DRAFT', + }) + if (created?.id) { + await workProcessApi.submit(created.id) + } + success++ + } catch { + failed++ + } + } + queryClient.invalidateQueries({ queryKey: ['work-processes'] }) + if (success > 0) toast.success(`已为 ${success} 名员工开具收入证明`) + if (failed > 0) toast.error(`${failed} 名员工开具证明失败`) + setSelectedIds(new Set()) } const toggleSelect = (id: string) => { @@ -448,9 +527,6 @@ export default function Roster() { - @@ -779,8 +855,8 @@ export default function Roster() { > - {/* 试用期员工显示转正按钮,其他员工显示发薪按钮 */} - {e.probationInfo?.isProbation ? ( + {/* 试用期员工显示转正按钮 */} + {e.probationInfo?.isProbation && ( - ) : ( - )} + + + + + )} + + {/* 续签合同弹窗 */} + {showRenewModal && renewEmployee && ( + { setShowRenewModal(false); setRenewEmployee(null) }} title={`为 ${renewEmployee.name} 续签合同`}> +
+
+ 原合同到期日:{renewEmployee.latestContract?.endDate ? new Date(renewEmployee.latestContract.endDate).toISOString().slice(0, 10) : '无'} +
+
+ + setRenewNewStartDate(e.target.value)} /> +
+
+ + setRenewNewEndDate(e.target.value)} placeholder="留空表示无固定期限" /> +
+
+ + setRenewNewSalary(e.target.value)} placeholder="续签后月薪" /> +
+
+ + +
+
+
+ )} + {showBatchRenewModal && ( { setShowBatchRenewModal(false); setPreviewData(null); }} title="批量续签">
diff --git a/frontend/src/pages/WorkProcess.tsx b/frontend/src/pages/WorkProcess.tsx deleted file mode 100644 index eed1d7a..0000000 --- a/frontend/src/pages/WorkProcess.tsx +++ /dev/null @@ -1,943 +0,0 @@ -import { useState, useEffect } from 'react' -import { useNavigate } from 'react-router-dom' -import { usePageSize } from '../hooks/usePageSize' -import { useUnsavedChanges } from '../hooks/useUnsavedChanges' -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { toast } from 'sonner' -import { toastError } from '../lib/errorToast' -import { - UserPlus, LogIn, FileSignature, Edit, CheckCircle, RefreshCw, - Repeat, Pause, FileText, Briefcase, - Loader2, ChevronRight, Trash2, Send, X, Eye, Search, Download, Users, -} from 'lucide-react' -import { workProcessApi, templatesApi, employeeApi } from '../lib/api-services' -import Card from '../components/ui/Card' -import Button from '../components/ui/Button' -import { Input, Label, Select } from '../components/ui/Input' -import Modal from '../components/ui/Modal' -import Pagination from '../components/ui/Pagination' -import PageGuide from '../components/ui/PageGuide' -import QueryError from '../components/ui/QueryError' - -const PROCESS_ICONS: Record = { - HIRE: UserPlus, ONBOARD: LogIn, CUSTOM_CONTRACT: FileSignature, - INFO_SUBMIT: Edit, CONFIRM: CheckCircle, CHANGE: RefreshCw, - RENEW: Repeat, SUSPEND: Pause, INCOME_CERT: FileText, - FLEXIBLE: Briefcase, -} - -const PROCESS_TYPES: Record = { - HIRE: { label: '员工录用', description: '录用新员工并起草劳动合同' }, - ONBOARD: { label: '员工入职', description: '办理员工入职手续' }, - CUSTOM_CONTRACT: { label: '自定义合同签署', description: '自定义合同内容并签署' }, - INFO_SUBMIT: { label: '员工信息提交', description: '提交员工信息变更' }, - CONFIRM: { label: '员工转正', description: '试用期员工转正' }, - CHANGE: { label: '合同变更', description: '变更合同内容' }, - RENEW: { label: '合同续签', description: '到期合同续签' }, - SUSPEND: { label: '合同中止', description: '中止履行合同' }, - INCOME_CERT: { label: '开具收入证明', description: '为员工开具收入证明' }, - FLEXIBLE: { label: '灵活用工', description: '灵活用工协议签署' }, -} - -const STATUS_CONFIG: Record = { - DRAFT: { label: '草稿', color: 'bg-gray-100 text-gray-600' }, - PENDING_APPROVAL: { label: '待审批', color: 'bg-amber-50 text-amber-700' }, - APPROVED: { label: '已审批', color: 'bg-blue-50 text-blue-700' }, - REJECTED: { label: '已驳回', color: 'bg-red-50 text-red-700' }, - EXECUTING: { label: '执行中', color: 'bg-purple-50 text-purple-700' }, - COMPLETED: { label: '已完成', color: 'bg-green-50 text-safe' }, - CANCELLED: { label: '已撤销', color: 'bg-gray-100 text-gray-400' }, -} - -// 各流程类型的表单字段配置(required 标记必填项) -const FORM_FIELDS: Record = { - HIRE: [ - { key: 'name', label: '员工姓名', type: 'text', required: true }, - { key: 'department', label: '部门', type: 'text', required: true }, - { key: 'hireDate', label: '入职日期', type: 'date', required: true }, - { key: 'monthlySalary', label: '月薪', type: 'number' }, - { key: 'phone', label: '手机号', type: 'text', required: true }, - { key: 'idCardNumber', label: '证件号码', type: 'text', required: true }, - { key: 'gender', label: '性别', type: 'select', options: ['男', '女'] }, - { key: 'contractStartDate', label: '合同开始日期', type: 'date' }, - { key: 'contractEndDate', label: '合同结束日期', type: 'date' }, - ], - ONBOARD: [ - { key: 'employeeId', label: '选择员工', type: 'employee-select', required: true }, - { key: 'hireDate', label: '入职日期', type: 'date', required: true }, - ], - CUSTOM_CONTRACT: [ - { key: 'employeeId', label: '选择员工', type: 'employee-select', required: true }, - { key: 'contractStartDate', label: '合同开始日期', type: 'date', required: true }, - { key: 'contractEndDate', label: '合同结束日期', type: 'date' }, - { key: 'contractType', label: '合同类型', type: 'select', options: ['FIXED', 'UNFIXED', 'INTERNSHIP'] }, - ], - INFO_SUBMIT: [ - { key: 'employeeId', label: '选择员工', type: 'employee-select', required: true }, - { key: 'department', label: '部门', type: 'text' }, - { key: 'phone', label: '手机号', type: 'text' }, - { key: 'address', label: '地址', type: 'text' }, - { key: 'emergencyContact', label: '紧急联系人', type: 'text' }, - { key: 'emergencyPhone', label: '紧急联系电话', type: 'text' }, - ], - CONFIRM: [ - { key: 'employeeId', label: '选择员工', type: 'employee-select', required: true }, - { key: 'confirmDate', label: '转正日期', type: 'date', required: true }, - { key: 'regularSalary', label: '转正薪资', type: 'number' }, - ], - CHANGE: [ - { key: 'employeeId', label: '选择员工', type: 'employee-select', required: true }, - { key: 'contractId', label: '选择合同', type: 'contract-select', required: true }, - { key: 'newEndDate', label: '新到期日期', type: 'date', required: true }, - ], - RENEW: [ - { key: 'employeeId', label: '选择员工', type: 'employee-select', required: true }, - { key: 'oldContractId', label: '原合同', type: 'contract-select' }, - { key: 'newStartDate', label: '新合同开始日期', type: 'date', required: true }, - { key: 'newEndDate', label: '新合同结束日期', type: 'date' }, - { key: 'newSalary', label: '新薪资', type: 'number' }, - ], - SUSPEND: [ - { key: 'employeeId', label: '选择员工', type: 'employee-select', required: true }, - { key: 'contractId', label: '选择合同', type: 'contract-select', required: true }, - { key: 'suspendDate', label: '中止日期', type: 'date', required: true }, - ], - INCOME_CERT: [ - { key: 'enterpriseTemplateId', label: '关联企业模板(选填)', type: 'enterprise-template' }, - { key: 'employeeId', label: '选择员工', type: 'employee-select', required: true }, - { key: 'employeeName', label: '员工姓名', type: 'text' }, - { key: 'idCardNumber', label: '证件号码', type: 'text' }, - { key: 'position', label: '职务', type: 'text' }, - { key: 'monthlyIncome', label: '月收入', type: 'text' }, - { key: 'purpose', label: '用途', type: 'text' }, - ], - TERMINATE: [ - { key: 'employeeId', label: '选择员工', type: 'employee-select', required: true }, - { key: 'contractId', label: '选择合同', type: 'contract-select' }, - { key: 'terminateDate', label: '终止日期', type: 'date', required: true }, - { key: 'reason', label: '终止原因', type: 'select', options: ['EXPIRED', 'NEGOTIATED', 'FAULT', 'NONFAULT', 'LAYOFF'], required: true }, - { key: 'compensation', label: '经济补偿金', type: 'number' }, - ], - RESCIND: [ - { key: 'employeeId', label: '选择员工', type: 'employee-select', required: true }, - { key: 'contractId', label: '选择合同', type: 'contract-select' }, - { key: 'rescindDate', label: '解除日期', type: 'date', required: true }, - { key: 'reason', label: '解除原因', type: 'select', options: ['NEGOTIATED', 'FAULT', 'NONFAULT', 'LAYOFF'], required: true }, - { key: 'compensation', label: '经济补偿金', type: 'number' }, - ], - LEAVING_CERT: [ - { key: 'enterpriseTemplateId', label: '关联企业模板(选填)', type: 'enterprise-template' }, - { key: 'employeeId', label: '选择员工', type: 'employee-select', required: true }, - { key: 'employeeName', label: '员工姓名', type: 'text' }, - { key: 'idCardNumber', label: '证件号码', type: 'text' }, - { key: 'position', label: '职务', type: 'text' }, - { key: 'hireDate', label: '入职日期', type: 'date' }, - { key: 'leaveDate', label: '离职日期', type: 'date', required: true }, - ], - FLEXIBLE: [ - { key: 'name', label: '姓名', type: 'text', required: true }, - { key: 'phone', label: '手机号', type: 'text' }, - { key: 'idCardNumber', label: '证件号码', type: 'text', required: true }, - { key: 'department', label: '部门', type: 'text' }, - { key: 'agreementStartDate', label: '协议开始日期', type: 'date', required: true }, - { key: 'agreementEndDate', label: '协议结束日期', type: 'date' }, - { key: 'payMethod', label: '计酬方式', type: 'text' }, - ], -} - -// 字段 key → 中文 label 映射(用于展示已保存的表单数据) -const FIELD_LABEL_MAP: Record = Object.values(FORM_FIELDS).flat().reduce((acc, f) => { - acc[f.key] = f.label - return acc -}, {} as Record) - -/** 日期字段对配置:各流程类型的开始/结束日期字段对 */ -const DATE_RANGE_FIELDS: Record> = { - HIRE: [{ start: 'contractStartDate', end: 'contractEndDate', startLabel: '合同开始日期', endLabel: '合同结束日期' }], - CUSTOM_CONTRACT: [{ start: 'contractStartDate', end: 'contractEndDate', startLabel: '合同开始日期', endLabel: '合同结束日期' }], - RENEW: [{ start: 'newStartDate', end: 'newEndDate', startLabel: '新合同开始日期', endLabel: '新合同结束日期' }], - FLEXIBLE: [{ start: 'agreementStartDate', end: 'agreementEndDate', startLabel: '协议开始日期', endLabel: '协议结束日期' }], -} - -/** 校验日期前后关系:结束日期不能早于开始日期 */ -function validateDateRange(type: string, data: Record): string | null { - const pairs = DATE_RANGE_FIELDS[type] - if (!pairs) return null - for (const pair of pairs) { - const start = data[pair.start] - const end = data[pair.end] - if (start && end && new Date(end) < new Date(start)) { - return `${pair.endLabel}不能早于${pair.startLabel}` - } - } - return null -} - -export default function WorkProcess() { - const navigate = useNavigate() - const queryClient = useQueryClient() - const [showCreate, setShowCreate] = useState(false) - const [selectedType, setSelectedType] = useState(() => { - try { return localStorage.getItem('workprocess-draft-type') || '' } catch { return '' } - }) - const [formData, setFormData] = useState>(() => { - try { - const saved = localStorage.getItem('workprocess-draft-data') - return saved ? JSON.parse(saved) : {} - } catch { return {} } - }) - const [filterType, setFilterType] = useState('') - const [filterStatus, setFilterStatus] = useState('') - const [detailId, setDetailId] = useState(null) - const [previewContent, setPreviewContent] = useState(null) - const [showBatch, setShowBatch] = useState(false) - const [batchType, setBatchType] = useState('INCOME_CERT') - const [batchEmployees, setBatchEmployees] = useState([]) - const [batchSearch, setBatchSearch] = useState('') - const pageSize = usePageSize() - const [page, setPage] = useState(1) - - // 持久化草稿到 localStorage,防止录入数据丢失 - useEffect(() => { - try { - if (selectedType && Object.keys(formData).length > 0) { - localStorage.setItem('workprocess-draft-type', selectedType) - localStorage.setItem('workprocess-draft-data', JSON.stringify(formData)) - } else { - localStorage.removeItem('workprocess-draft-type') - localStorage.removeItem('workprocess-draft-data') - } - } catch {} - }, [selectedType, formData]) - - const isDirty = selectedType && Object.keys(formData).length > 0 - useUnsavedChanges(!!isDirty) - - const { data: listData, isLoading, isError, error, refetch } = useQuery({ - queryKey: ['work-processes', filterType, filterStatus, page, pageSize], - queryFn: async () => { - const params: any = { page, pageSize } - if (filterType) params.type = filterType - if (filterStatus) params.status = filterStatus - return await workProcessApi.list({ page, pageSize, type: filterType || undefined, status: filterStatus || undefined } as any) - }, - }) - - const createMutation = useMutation({ - mutationFn: async (data: any) => { - return await workProcessApi.create(data) - }, - onSuccess: () => { - toast.success('已创建草稿,可在列表中查看详情并提交', { - action: { label: '去提交', onClick: () => {} }, - }) - queryClient.invalidateQueries({ queryKey: ['work-processes'] }) - setShowCreate(false) - setFormData({}) - setSelectedType('') - }, - onError: (err: any) => toastError(err, '创建失败'), - }) - - const submitMutation = useMutation({ - mutationFn: async (id: string) => { - return await workProcessApi.submit(id) - }, - onSuccess: () => { - toast.success('已提交并执行', { - action: { label: '查看文书', onClick: () => navigate('/evidence') }, - }) - toast.info('文书已生成,可在「证据管理」中查看和下载', { duration: 5000 }) - queryClient.invalidateQueries({ queryKey: ['work-processes'] }) - setDetailId(null) - }, - onError: (err: any) => toastError(err, '提交失败'), - }) - - const cancelMutation = useMutation({ - mutationFn: async (id: string) => { - return await workProcessApi.cancel(id) - }, - onSuccess: () => { - toast.success('已撤销') - queryClient.invalidateQueries({ queryKey: ['work-processes'] }) - setDetailId(null) - }, - onError: (err: any) => toastError(err, '撤销失败'), - }) - - const deleteMutation = useMutation({ - mutationFn: async (id: string) => { - await workProcessApi.remove(id) - }, - onSuccess: () => { - toast.success('已删除') - queryClient.invalidateQueries({ queryKey: ['work-processes'] }) - }, - onError: (err: any) => toastError(err, '删除失败'), - }) - - const previewMutation = useMutation({ - mutationFn: async (id: string) => { - return await workProcessApi.preview(id) - }, - onSuccess: (data) => { - setPreviewContent(data.content) - }, - onError: (err: any) => toastError(err, '预览失败'), - }) - - const handleCreate = () => { - if (!selectedType) { - toast.error('请选择流程类型') - return - } - // 校验必填项 - const requiredFields = (FORM_FIELDS[selectedType] || []).filter(f => f.required) - const missingFields = requiredFields.filter(f => !formData[f.key] || String(formData[f.key]).trim() === '') - if (missingFields.length > 0) { - toast.error(`请填写必填项:${missingFields.map(f => f.label).join('、')}`) - return - } - // 校验日期前后关系 - const dateError = validateDateRange(selectedType, formData) - if (dateError) { - toast.error(dateError) - return - } - createMutation.mutate({ - type: selectedType, - title: PROCESS_TYPES[selectedType].label, - employeeId: formData.employeeId || undefined, - formData, - status: 'DRAFT', - }) - } - - const handleCreateAndSubmit = () => { - if (!selectedType) { - toast.error('请选择流程类型') - return - } - // 校验日期前后关系 - const dateError = validateDateRange(selectedType, formData) - if (dateError) { - toast.error(dateError) - return - } - createMutation.mutate( - { type: selectedType, title: PROCESS_TYPES[selectedType].label, employeeId: formData.employeeId || undefined, formData, status: 'DRAFT' }, - { - onSuccess: (data: any) => { - const newId = data?.id - if (newId) { - submitMutation.mutate(newId) - } else { - toast.success('草稿已创建,请手动提交') - queryClient.invalidateQueries({ queryKey: ['work-processes'] }) - } - setShowCreate(false) - setFormData({}) - setSelectedType('') - }, - } - ) - } - - const handleFieldChange = (key: string, value: any) => { - setFormData(prev => ({ ...prev, [key]: value })) - // 选择员工后自动填充相关字段 - if (key === 'employeeId' && value) { - employeeApi.detail(value).then((emp: any) => { - setFormData(prev => ({ - ...prev, - employeeName: emp.name || prev.employeeName, - idCardNumber: emp.idCardNumber || prev.idCardNumber, - position: emp.position || prev.position, - monthlyIncome: emp.monthlySalary ? String(emp.monthlySalary) : prev.monthlyIncome, - hireDate: emp.hireDate ? emp.hireDate.slice(0, 10) : prev.hireDate, - department: emp.department || prev.department, - phone: emp.phone || prev.phone, - })) - // RENEW 类型:自动推导新合同开始日期 = 原合同结束日期 + 1天 - if (selectedType === 'RENEW' && emp.contracts?.length > 0) { - const latestContract = emp.contracts[0] - if (latestContract?.endDate) { - const endDate = new Date(latestContract.endDate) - endDate.setDate(endDate.getDate() + 1) - setFormData(prev => ({ - ...prev, - oldContractId: latestContract.id, - newStartDate: endDate.toISOString().slice(0, 10), - })) - } - } - }).catch(() => {}) - } - } - - const handleBatchSubmit = () => { - if (batchEmployees.length === 0) { - toast.error('请至少选择一名员工') - return - } - let success = 0 - let failed = 0 - Promise.all( - batchEmployees.map(async (empId) => { - try { - const emp = allEmployees.find((e: any) => e.id === empId) - if (!emp) return - const data: any = { - type: batchType, - title: PROCESS_TYPES[batchType].label, - employeeId: empId, - formData: { - employeeName: emp.name, - idCardNumber: emp.idCardNumber || '', - position: emp.position || '', - }, - status: 'DRAFT', - } - const res: any = await workProcessApi.create(data) - if (res?.id) { - await workProcessApi.submit(res.id) - success++ - } - } catch { - failed++ - } - }) - ).then(() => { - toast.success(`批量开具完成:成功 ${success} 个${failed > 0 ? ',失败 ' + failed + ' 个' : ''}`) - queryClient.invalidateQueries({ queryKey: ['work-processes'] }) - setShowBatch(false) - setBatchEmployees([]) - setBatchSearch('') - }) - } - - const { data: allEmployees = [] } = useQuery({ - queryKey: ['employee-list-batch'], - queryFn: async () => { - return await employeeApi.allLite() - }, - }) - - const filteredEmployees = allEmployees.filter((e: any) => { - if (!batchSearch) return true - return e.name.includes(batchSearch) || (e.department || '').includes(batchSearch) - }) - - const items = listData?.items || [] - const total = listData?.total || 0 - - return ( -
- - 用工办理用于管理员工入职、转正、调岗、离职等全生命周期手续。选择办理类型后填写相关信息并提交,系统自动生成对应文书并更新员工档案。支持批量办理和流程跟踪。关联:入职登记后请前往「花名册」查看档案;离职办理后请前往「离职管理」计算补偿。 - - {/* 发起办理 */} - -
-

用工办理

-
- {isDirty && ( - - - 有未完成的草稿:{PROCESS_TYPES[selectedType]?.label} - - - )} - - -
-
- - {/* 13类流程卡片 */} -
- {Object.entries(PROCESS_TYPES).map(([key, config]) => { - const Icon = PROCESS_ICONS[key] || FileText - return ( - - ) - })} -
-
- - {/* 办理记录 */} - -
-

办理记录

- - -
- - {isLoading ? ( -
- ) : isError ? ( - - ) : items.length === 0 ? ( -
暂无办理记录
- ) : ( -
- {items.map((item: any) => { - const Icon = PROCESS_ICONS[item.type] || FileText - const statusCfg = STATUS_CONFIG[item.status] || STATUS_CONFIG.DRAFT - return ( -
setDetailId(item.id)} - > - -
-
- {item.title} - {statusCfg.label} -
-
- {item.employee ? `${item.employee.name} · ${item.employee.department}` : (item.formData?.employeeName || item.formData?.name || '未关联员工')} - {' · '}{new Date(item.createdAt).toLocaleDateString('zh-CN')} -
-
- {(item.status === 'COMPLETED' || item.status === 'EXECUTING') && item.documents && item.documents.length > 0 && ( - - )} - -
- ) - })} -
- )} - setPage(1)} - /> -
- - {/* 创建/编辑弹窗 */} - setShowCreate(false)} title={selectedType ? `发起:${PROCESS_TYPES[selectedType]?.label}` : '发起办理'} size="lg"> - {!selectedType ? ( -
- {Object.entries(PROCESS_TYPES).map(([key, config]) => { - const Icon = PROCESS_ICONS[key] || FileText - return ( - - ) - })} -
- ) : ( -
-
{PROCESS_TYPES[selectedType]?.description}
- {(FORM_FIELDS[selectedType] || []).map(field => ( -
- - {field.type === 'select' ? ( - - ) : field.type === 'textarea' ? ( -