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