feat: 已离职/解聘员工可重新入职

1. 后端新增 rehireEmployee 函数和 POST /employees/:id/rehire 接口
2. 校验:仅已离职员工可重新入职,新入职日期须晚于上次离职日期
3. 复用员工已有基本信息,只需填写新入职日期和劳动合同
4. 前端花名册已离职员工显示「重新入职」按钮+弹窗(RehireModal)
This commit is contained in:
freedakgmail
2026-07-23 18:11:01 +08:00
parent 2896ba77aa
commit 1eeca663a0
3 changed files with 252 additions and 1 deletions
+169 -1
View File
@@ -1,6 +1,6 @@
import { useState, useRef } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Users, FileText, AlertTriangle, Calendar, GraduationCap, TrendingUp, Scale, X, Plus, Paperclip, Trash2, Printer, Calculator, Shield, Info, Check, Eye, Download, UserX } from 'lucide-react'
import { Users, FileText, AlertTriangle, Calendar, GraduationCap, TrendingUp, Scale, X, Plus, Paperclip, Trash2, Printer, Calculator, Shield, Info, Check, Eye, Download, UserX, UserPlus } from 'lucide-react'
import api from '../lib/api'
import Card from '../components/ui/Card'
import Button from '../components/ui/Button'
@@ -21,6 +21,8 @@ export default function Roster() {
const [showAddModal, setShowAddModal] = useState(false)
const [showResignModal, setShowResignModal] = useState(false)
const [resignEmployee, setResignEmployee] = useState<any>(null)
const [showRehireModal, setShowRehireModal] = useState(false)
const [rehireEmployee, setRehireEmployee] = useState<any>(null)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
@@ -59,6 +61,16 @@ export default function Roster() {
},
})
const rehireMutation = useMutation({
mutationFn: (data: any) => api.post(`/employees/${rehireEmployee?.id}/rehire`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['roster'] })
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
setShowRehireModal(false)
setRehireEmployee(null)
},
})
const filtered = employees?.filter((e: any) =>
!search || e.name.includes(search) || e.department.includes(search)
) || []
@@ -181,6 +193,18 @@ export default function Roster() {
</button>
</div>
)}
{e.status === 'RESIGNED' && (
<button
className="text-xs text-primary hover:text-primary/80 flex items-center gap-0.5"
onClick={(ev) => {
ev.stopPropagation()
setRehireEmployee(e)
setShowRehireModal(true)
}}
>
<UserPlus className="w-3.5 h-3.5" />
</button>
)}
</td>
</tr>
))}
@@ -208,6 +232,16 @@ export default function Roster() {
error={resignMutation.error as any}
/>
)}
{showRehireModal && rehireEmployee && (
<RehireModal
employee={rehireEmployee}
onClose={() => { setShowRehireModal(false); setRehireEmployee(null) }}
onSubmit={(data) => rehireMutation.mutate(data)}
loading={rehireMutation.isPending}
error={rehireMutation.error as any}
/>
)}
</div>
)
}
@@ -748,6 +782,140 @@ function ResignModal({ employee, onClose, onSubmit, loading, error }: {
)
}
function RehireModal({ employee, onClose, onSubmit, loading, error }: {
employee: any
onClose: () => void
onSubmit: (data: any) => void
loading: boolean
error: any
}) {
const [form, setForm] = useState({
hireDate: new Date().toISOString().slice(0, 10),
isPregnant: false,
isInMedicalPeriod: false,
isWorkInjured: false,
contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED',
signDate: '',
startDate: '',
endDate: '',
contractYears: 3,
probationMonths: 0,
probationSalary: 0,
})
const handleSubmit = () => {
const data: any = {
hireDate: new Date(form.hireDate).toISOString(),
isPregnant: form.isPregnant,
isInMedicalPeriod: form.isInMedicalPeriod,
isWorkInjured: form.isWorkInjured,
}
if (form.contractType !== 'UNSIGNED' && form.startDate) {
data.contract = {
signDate: form.signDate ? new Date(form.signDate).toISOString() : null,
startDate: new Date(form.startDate).toISOString(),
endDate: form.endDate ? new Date(form.endDate).toISOString() : null,
contractType: form.contractType,
contractYears: form.contractYears,
probationMonths: form.probationMonths,
probationSalary: form.probationSalary,
}
}
onSubmit(data)
}
return (
<Modal open onClose={onClose} title={`重新入职 - ${employee.name}`}>
<div className="space-y-3">
<div className="bg-blue-50 text-blue-700 text-xs px-3 py-2 rounded-md">
</div>
<div>
<Label></Label>
<div className="text-xs text-gray-600">{employee.name} - {employee.department}</div>
</div>
<div>
<Label></Label>
<Input
type="date"
value={form.hireDate}
onChange={(e) => setForm({ ...form, hireDate: e.target.value })}
/>
</div>
<div className="flex gap-4 text-xs">
<label className="flex items-center gap-1">
<input type="checkbox" checked={form.isPregnant} onChange={(e) => setForm({ ...form, isPregnant: e.target.checked })} />
</label>
<label className="flex items-center gap-1">
<input type="checkbox" checked={form.isInMedicalPeriod} onChange={(e) => setForm({ ...form, isInMedicalPeriod: e.target.checked })} />
</label>
<label className="flex items-center gap-1">
<input type="checkbox" checked={form.isWorkInjured} onChange={(e) => setForm({ ...form, isWorkInjured: e.target.checked })} />
</label>
</div>
<div className="border-t pt-3">
<Label></Label>
<Select value={form.contractType} onChange={(e) => setForm({ ...form, contractType: e.target.value as any })}>
<option value="FIXED"></option>
<option value="UNFIXED"></option>
<option value="UNSIGNED"></option>
</Select>
</div>
{form.contractType !== 'UNSIGNED' && (
<div className="space-y-2">
<div className="grid grid-cols-2 gap-2">
<div>
<Label></Label>
<Input type="date" value={form.signDate} onChange={(e) => setForm({ ...form, signDate: e.target.value })} />
</div>
<div>
<Label></Label>
<Input type="date" value={form.startDate} onChange={(e) => setForm({ ...form, startDate: e.target.value })} />
</div>
</div>
{form.contractType === 'FIXED' && (
<div className="grid grid-cols-2 gap-2">
<div>
<Label></Label>
<Input type="date" value={form.endDate} onChange={(e) => setForm({ ...form, endDate: e.target.value })} />
</div>
<div>
<Label></Label>
<Input type="number" value={form.contractYears} onChange={(e) => setForm({ ...form, contractYears: parseInt(e.target.value) || 3 })} />
</div>
</div>
)}
<div className="grid grid-cols-2 gap-2">
<div>
<Label></Label>
<Input type="number" value={form.probationMonths} onChange={(e) => setForm({ ...form, probationMonths: parseInt(e.target.value) || 0 })} />
</div>
<div>
<Label></Label>
<Input type="number" value={form.probationSalary} onChange={(e) => setForm({ ...form, probationSalary: parseFloat(e.target.value) || 0 })} />
</div>
</div>
</div>
)}
{error && (
<div className="text-xs text-danger">
{(error as any)?.response?.data?.error?.message || '操作失败,请重试'}
</div>
)}
<div className="flex justify-end gap-2 pt-2">
<Button variant="secondary" onClick={onClose}></Button>
<Button onClick={handleSubmit} disabled={loading}>
{loading ? '提交中...' : '确认入职'}
</Button>
</div>
</div>
</Modal>
)
}
function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
onClose: () => void
onSubmit: (data: any) => void