ux: 花名册操作优化 - 去掉发薪、开具证明/续签合同直接操作、删除用工办理页面

- 去掉花名册操作栏的"发薪"按钮
- 开具证明改为弹窗直接创建+提交,不再跳转用工办理页面
- 续签合同改为弹窗直接创建+提交,自动推导新合同开始日期
- 批量开具证明改为直接API调用,不再跳转用工办理页面
- 删除用工办理页面(WorkProcess.tsx)和路由
- 去掉工具栏"用工办理"按钮

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-08-15 14:41:37 +08:00
parent 757cbc8740
commit 4486adc957
3 changed files with 188 additions and 973 deletions
+188 -28
View File
@@ -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<any>(null)
const [showConfirmModal, setShowConfirmModal] = useState(false)
const [confirmEmployee, setConfirmEmployee] = useState<any>(null)
// 开具证明弹窗
const [showCertModal, setShowCertModal] = useState(false)
const [certEmployee, setCertEmployee] = useState<any>(null)
const [certPurpose, setCertPurpose] = useState('')
// 续签合同弹窗
const [showRenewModal, setShowRenewModal] = useState(false)
const [renewEmployee, setRenewEmployee] = useState<any>(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<Set<string>>(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<string, unknown> }) => {
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<string, unknown> }) => {
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() {
<Button onClick={() => setShowAddModal(true)} className="h-9 shrink-0">
<Plus className="mr-1.5 h-4 w-4" />
</Button>
<Button variant="secondary" onClick={() => navigate('/work-process')} className="h-9 shrink-0" title="完整的入职办理流程">
<UserPlus className="mr-1.5 h-4 w-4" />
</Button>
<Button variant="secondary" onClick={() => setShowImportModal(true)} className="h-9 shrink-0">
<Upload className="mr-1.5 h-4 w-4" />
</Button>
@@ -779,8 +855,8 @@ export default function Roster() {
>
<DollarSign className="h-4 w-4" />
</button>
{/* 试用期员工显示转正按钮,其他员工显示发薪按钮 */}
{e.probationInfo?.isProbation ? (
{/* 试用期员工显示转正按钮 */}
{e.probationInfo?.isProbation && (
<button
type="button"
title="转正"
@@ -794,19 +870,6 @@ export default function Roster() {
>
<CheckCircle className="h-4 w-4" />
</button>
) : (
<button
type="button"
title="发薪"
aria-label={`${e.name}发薪`}
className="rounded-md p-1.5 text-gray-500 transition hover:bg-primary/10 hover:text-primary"
onClick={(ev) => {
ev.stopPropagation()
navigate('/money')
}}
>
<Wallet className="h-4 w-4" />
</button>
)}
<button
type="button"
@@ -828,7 +891,9 @@ export default function Roster() {
className="rounded-md p-1.5 text-gray-500 transition hover:bg-primary/10 hover:text-primary"
onClick={(ev) => {
ev.stopPropagation()
window.location.href = `/work-process?type=INCOME_CERT&employeeId=${e.id}`
setCertEmployee(e)
setCertPurpose('')
setShowCertModal(true)
}}
>
<FileText className="h-4 w-4" />
@@ -840,7 +905,19 @@ export default function Roster() {
className="rounded-md p-1.5 text-gray-500 transition hover:bg-primary/10 hover:text-primary"
onClick={(ev) => {
ev.stopPropagation()
window.location.href = `/work-process?type=RENEW&employeeId=${e.id}`
// 自动推导新合同开始日期:原合同结束日 + 1天
const oldEndDate = e.latestContract?.endDate
let newStart = new Date().toISOString().slice(0, 10)
if (oldEndDate) {
const d = new Date(oldEndDate)
d.setDate(d.getDate() + 1)
newStart = d.toISOString().slice(0, 10)
}
setRenewEmployee(e)
setRenewNewStartDate(newStart)
setRenewNewEndDate('')
setRenewNewSalary(e.monthlySalary ? String(e.monthlySalary) : '')
setShowRenewModal(true)
}}
>
<RotateCcw className="h-4 w-4" />
@@ -982,6 +1059,89 @@ export default function Roster() {
/>
)}
{/* 开具收入证明弹窗 */}
{showCertModal && certEmployee && (
<Modal open={showCertModal} onClose={() => { setShowCertModal(false); setCertEmployee(null) }} title={`${certEmployee.name} 开具收入证明`}>
<div className="space-y-3">
<div className="text-xs text-gray-500">
</div>
<div>
<Label></Label>
<Input value={certPurpose} onChange={(e) => setCertPurpose(e.target.value)} placeholder="如:办理签证、租房、贷款等" />
</div>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={() => { setShowCertModal(false); setCertEmployee(null) }}></Button>
<Button
disabled={certMutation.isPending}
onClick={() => {
certMutation.mutate({
employeeId: certEmployee.id,
formData: {
employeeId: certEmployee.id,
employeeName: certEmployee.name,
idCardNumber: certEmployee.idCardMasked || '',
position: certEmployee.position || '',
monthlyIncome: certEmployee.monthlySalary ? `¥${certEmployee.monthlySalary}` : '',
purpose: certPurpose || '通用',
},
})
}}
></Button>
</div>
</div>
</Modal>
)}
{/* 续签合同弹窗 */}
{showRenewModal && renewEmployee && (
<Modal open={showRenewModal} onClose={() => { setShowRenewModal(false); setRenewEmployee(null) }} title={`${renewEmployee.name} 续签合同`}>
<div className="space-y-3">
<div className="text-xs text-gray-500">
{renewEmployee.latestContract?.endDate ? new Date(renewEmployee.latestContract.endDate).toISOString().slice(0, 10) : '无'}
</div>
<div>
<Label> *</Label>
<Input type="date" value={renewNewStartDate} onChange={(e) => setRenewNewStartDate(e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="date" value={renewNewEndDate} onChange={(e) => setRenewNewEndDate(e.target.value)} placeholder="留空表示无固定期限" />
</div>
<div>
<Label></Label>
<Input type="number" value={renewNewSalary} onChange={(e) => setRenewNewSalary(e.target.value)} placeholder="续签后月薪" />
</div>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={() => { setShowRenewModal(false); setRenewEmployee(null) }}></Button>
<Button
disabled={renewMutation.isPending}
onClick={() => {
if (!renewNewStartDate) {
toast.error('请填写新合同开始日期')
return
}
if (renewNewEndDate && renewNewEndDate < renewNewStartDate) {
toast.error('结束日期不能早于开始日期')
return
}
renewMutation.mutate({
employeeId: renewEmployee.id,
formData: {
employeeId: renewEmployee.id,
oldContractId: renewEmployee.latestContract?.id || '',
newStartDate: renewNewStartDate,
newEndDate: renewNewEndDate || undefined,
newSalary: renewNewSalary || undefined,
},
})
}}
></Button>
</div>
</div>
</Modal>
)}
{showBatchRenewModal && (
<Modal open={showBatchRenewModal} onClose={() => { setShowBatchRenewModal(false); setPreviewData(null); }} title="批量续签">
<div className="space-y-3">