优化: 添加员工弹窗合同和试用期智能计算
1. 缺省入职日期和合同开始日期为今天,签订日期默认空(留空表示尚未签订) 2. 签约时长与合同结束日期双向自动计算:改年限→算结束日期,改结束日期→算年限 3. 开始日期变更时自动重算结束日期 4. 试用期长度按劳动合同法第19条校验(3月以下不得约定,1年以下≤1月,3年以下≤2月,3年以上≤6月) 5. 有试用期时试用期工资必填,且不得低于转正工资80%(劳动合同法第20条) 6. 校验不通过时禁止保存并显示错误提示
This commit is contained in:
+125
-10
@@ -922,15 +922,96 @@ function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
|
|||||||
loading: boolean
|
loading: boolean
|
||||||
error: any
|
error: any
|
||||||
}) {
|
}) {
|
||||||
|
const todayStr = new Date().toISOString().slice(0, 10)
|
||||||
|
const defaultEndDate = (() => {
|
||||||
|
const d = new Date()
|
||||||
|
d.setFullYear(d.getFullYear() + 3)
|
||||||
|
d.setDate(d.getDate() - 1)
|
||||||
|
return d.toISOString().slice(0, 10)
|
||||||
|
})()
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
name: '', department: '', hireDate: '', monthlySalary: '',
|
name: '', department: '', hireDate: todayStr, monthlySalary: '',
|
||||||
gender: '男' as '男' | '女', phone: '',
|
gender: '男' as '男' | '女', phone: '',
|
||||||
isPregnant: false, isInMedicalPeriod: false, isWorkInjured: false,
|
isPregnant: false, isInMedicalPeriod: false, isWorkInjured: false,
|
||||||
contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED',
|
contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED',
|
||||||
signDate: '', startDate: '', endDate: '',
|
signDate: '', startDate: todayStr, endDate: defaultEndDate,
|
||||||
contractYears: 3, probationMonths: 0, probationSalary: 0,
|
contractYears: 3, probationMonths: 0, probationSalary: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 计算合同月数
|
||||||
|
const contractMonths = (() => {
|
||||||
|
if (form.contractType !== 'FIXED' || !form.startDate) return 0
|
||||||
|
if (form.endDate) {
|
||||||
|
const start = new Date(form.startDate)
|
||||||
|
const end = new Date(form.endDate)
|
||||||
|
return Math.round((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24 * 30.44))
|
||||||
|
}
|
||||||
|
return form.contractYears * 12
|
||||||
|
})()
|
||||||
|
|
||||||
|
// 试用期上限(劳动合同法第19条)
|
||||||
|
const probationMax = (() => {
|
||||||
|
if (contractMonths >= 36) return 6
|
||||||
|
if (contractMonths >= 12) return 2
|
||||||
|
if (contractMonths >= 3) return 1
|
||||||
|
return 0
|
||||||
|
})()
|
||||||
|
|
||||||
|
const probationError = (() => {
|
||||||
|
if (form.probationMonths <= 0) return ''
|
||||||
|
if (contractMonths > 0 && contractMonths < 3) return '合同不足3个月,不得约定试用期'
|
||||||
|
if (form.probationMonths > probationMax) return `合同${contractMonths}个月,试用期最多${probationMax}个月`
|
||||||
|
return ''
|
||||||
|
})()
|
||||||
|
|
||||||
|
const monthlySalaryNum = parseFloat(form.monthlySalary) || 0
|
||||||
|
const probationSalaryError = (() => {
|
||||||
|
if (form.probationMonths <= 0) return ''
|
||||||
|
if (form.probationSalary <= 0) return '有试用期时试用期工资必填'
|
||||||
|
if (monthlySalaryNum > 0 && form.probationSalary < monthlySalaryNum * 0.8) {
|
||||||
|
return `试用期工资不得低于转正工资的80%(最低¥${(monthlySalaryNum * 0.8).toFixed(0)})`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})()
|
||||||
|
|
||||||
|
// 合同结束日期自动计算
|
||||||
|
const handleContractYearsChange = (years: number) => {
|
||||||
|
if (!form.startDate || years <= 0) {
|
||||||
|
setForm({ ...form, contractYears: years, endDate: '' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const start = new Date(form.startDate)
|
||||||
|
const end = new Date(start)
|
||||||
|
end.setFullYear(end.getFullYear() + years)
|
||||||
|
end.setDate(end.getDate() - 1)
|
||||||
|
setForm({ ...form, contractYears: years, endDate: end.toISOString().slice(0, 10) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合同结束日期变更 → 自动计算签约年限
|
||||||
|
const handleEndDateChange = (endDate: string) => {
|
||||||
|
if (!form.startDate || !endDate) {
|
||||||
|
setForm({ ...form, endDate, contractYears: 0 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const start = new Date(form.startDate)
|
||||||
|
const end = new Date(endDate)
|
||||||
|
const months = Math.round((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24 * 30.44))
|
||||||
|
setForm({ ...form, endDate, contractYears: Math.max(1, Math.round(months / 12)) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始日期变更 → 重新计算结束日期
|
||||||
|
const handleStartDateChange = (startDate: string) => {
|
||||||
|
if (form.contractType === 'FIXED' && form.contractYears > 0 && startDate) {
|
||||||
|
const start = new Date(startDate)
|
||||||
|
const end = new Date(start)
|
||||||
|
end.setFullYear(end.getFullYear() + form.contractYears)
|
||||||
|
end.setDate(end.getDate() - 1)
|
||||||
|
setForm({ ...form, startDate, endDate: end.toISOString().slice(0, 10) })
|
||||||
|
} else {
|
||||||
|
setForm({ ...form, startDate })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
const data: any = {
|
const data: any = {
|
||||||
name: form.name, department: form.department,
|
name: form.name, department: form.department,
|
||||||
@@ -951,6 +1032,10 @@ function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
|
|||||||
onSubmit(data)
|
onSubmit(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canSubmit = form.name && form.department && form.hireDate && form.monthlySalary
|
||||||
|
&& (form.contractType === 'UNSIGNED' || form.startDate)
|
||||||
|
&& !probationError && !probationSalaryError
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open onClose={onClose} title="添加员工">
|
<Modal open onClose={onClose} title="添加员工">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -978,28 +1063,58 @@ function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
|
|||||||
</div>
|
</div>
|
||||||
<div className="border-t pt-3">
|
<div className="border-t pt-3">
|
||||||
<Label>合同类型</Label>
|
<Label>合同类型</Label>
|
||||||
<Select value={form.contractType} onChange={(e) => setForm({ ...form, contractType: e.target.value as any })}>
|
<Select value={form.contractType} onChange={(e) => setForm({ ...form, contractType: e.target.value as any, endDate: e.target.value === 'UNFIXED' ? '' : form.endDate })}>
|
||||||
<option value="FIXED">固定期限</option><option value="UNFIXED">无固定期限</option><option value="UNSIGNED">未签合同</option>
|
<option value="FIXED">固定期限</option><option value="UNFIXED">无固定期限</option><option value="UNSIGNED">未签合同</option>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
{form.contractType !== 'UNSIGNED' && (
|
{form.contractType !== 'UNSIGNED' && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div><Label>签订日期</Label><Input type="date" value={form.signDate} onChange={(e) => setForm({ ...form, signDate: e.target.value })} /></div>
|
<div>
|
||||||
<div><Label>合同开始日期 *</Label><Input type="date" value={form.startDate} onChange={(e) => setForm({ ...form, startDate: e.target.value })} /></div>
|
<Label>签订日期</Label>
|
||||||
|
<Input type="date" value={form.signDate} onChange={(e) => setForm({ ...form, signDate: e.target.value })} />
|
||||||
|
<div className="text-xs text-gray-400 mt-0.5">留空表示尚未签订</div>
|
||||||
|
</div>
|
||||||
|
<div><Label>合同开始日期 *</Label><Input type="date" value={form.startDate} onChange={(e) => handleStartDateChange(e.target.value)} /></div>
|
||||||
</div>
|
</div>
|
||||||
{form.contractType === 'FIXED' && (
|
{form.contractType === 'FIXED' && (
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<>
|
||||||
<div><Label>合同结束日期</Label><Input type="date" value={form.endDate} onChange={(e) => setForm({ ...form, endDate: e.target.value })} /></div>
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div><Label>试用期(月)</Label><Input type="number" value={form.probationMonths} onChange={(e) => setForm({ ...form, probationMonths: parseInt(e.target.value) || 0 })} min={0} max={6} /></div>
|
<div>
|
||||||
<div><Label>试用期工资</Label><Input type="number" value={form.probationSalary} onChange={(e) => setForm({ ...form, probationSalary: parseInt(e.target.value) || 0 })} /></div>
|
<Label>签约时长(年)</Label>
|
||||||
|
<Input type="number" value={form.contractYears} onChange={(e) => handleContractYearsChange(parseInt(e.target.value) || 0)} min={1} />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>合同结束日期</Label>
|
||||||
|
<Input type="date" value={form.endDate} onChange={(e) => handleEndDateChange(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400">修改签约时长自动计算结束日期,修改结束日期自动计算签约时长</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<Label>试用期(月)</Label>
|
||||||
|
<Input type="number" value={form.probationMonths} onChange={(e) => setForm({ ...form, probationMonths: parseInt(e.target.value) || 0 })} min={0} max={6} />
|
||||||
|
{contractMonths > 0 && (
|
||||||
|
<div className="text-xs text-gray-400 mt-0.5">法定上限:{probationMax}个月</div>
|
||||||
|
)}
|
||||||
|
{probationError && <div className="text-xs text-danger mt-0.5">{probationError}</div>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>试用期工资{form.probationMonths > 0 ? ' *' : ''}</Label>
|
||||||
|
<Input type="number" value={form.probationSalary} onChange={(e) => setForm({ ...form, probationSalary: parseFloat(e.target.value) || 0 })} disabled={form.probationMonths <= 0} />
|
||||||
|
{monthlySalaryNum > 0 && form.probationMonths > 0 && (
|
||||||
|
<div className="text-xs text-gray-400 mt-0.5">不低于转正工资80%(≥¥{(monthlySalaryNum * 0.8).toFixed(0)})</div>
|
||||||
|
)}
|
||||||
|
{probationSalaryError && <div className="text-xs text-danger mt-0.5">{probationSalaryError}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
<Button variant="secondary" onClick={onClose}>取消</Button>
|
<Button variant="secondary" onClick={onClose}>取消</Button>
|
||||||
<Button onClick={handleSubmit} disabled={loading || !form.name || !form.department || !form.hireDate || !form.monthlySalary}>{loading ? '保存中...' : '保存'}</Button>
|
<Button onClick={handleSubmit} disabled={loading || !canSubmit}>{loading ? '保存中...' : '保存'}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
Reference in New Issue
Block a user