feat: 已离职/解聘员工可重新入职
1. 后端新增 rehireEmployee 函数和 POST /employees/:id/rehire 接口 2. 校验:仅已离职员工可重新入职,新入职日期须晚于上次离职日期 3. 复用员工已有基本信息,只需填写新入职日期和劳动合同 4. 前端花名册已离职员工显示「重新入职」按钮+弹窗(RehireModal)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user