优化: 添加员工弹窗合同和试用期智能计算

1. 缺省入职日期和合同开始日期为今天,签订日期默认空(留空表示尚未签订)
2. 签约时长与合同结束日期双向自动计算:改年限→算结束日期,改结束日期→算年限
3. 开始日期变更时自动重算结束日期
4. 试用期长度按劳动合同法第19条校验(3月以下不得约定,1年以下≤1月,3年以下≤2月,3年以上≤6月)
5. 有试用期时试用期工资必填,且不得低于转正工资80%(劳动合同法第20条)
6. 校验不通过时禁止保存并显示错误提示
This commit is contained in:
freedakgmail
2026-07-23 18:22:29 +08:00
parent 1eeca663a0
commit 43186d3dd8
+125 -10
View File
@@ -922,15 +922,96 @@ function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
loading: boolean
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({
name: '', department: '', hireDate: '', monthlySalary: '',
name: '', department: '', hireDate: todayStr, monthlySalary: '',
gender: '男' as '男' | '女', phone: '',
isPregnant: false, isInMedicalPeriod: false, isWorkInjured: false,
contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED',
signDate: '', startDate: '', endDate: '',
signDate: '', startDate: todayStr, endDate: defaultEndDate,
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 data: any = {
name: form.name, department: form.department,
@@ -951,6 +1032,10 @@ function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
onSubmit(data)
}
const canSubmit = form.name && form.department && form.hireDate && form.monthlySalary
&& (form.contractType === 'UNSIGNED' || form.startDate)
&& !probationError && !probationSalaryError
return (
<Modal open onClose={onClose} title="添加员工">
<div className="space-y-3">
@@ -978,28 +1063,58 @@ function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
</div>
<div className="border-t pt-3">
<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>
</Select>
</div>
{form.contractType !== 'UNSIGNED' && (
<div className="space-y-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><Label> *</Label><Input type="date" value={form.startDate} onChange={(e) => setForm({ ...form, startDate: e.target.value })} /></div>
<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>
{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><Label>()</Label><Input type="number" value={form.probationMonths} onChange={(e) => setForm({ ...form, probationMonths: parseInt(e.target.value) || 0 })} min={0} max={6} /></div>
<div><Label></Label><Input type="number" value={form.probationSalary} onChange={(e) => setForm({ ...form, probationSalary: parseInt(e.target.value) || 0 })} /></div>
<>
<div className="grid grid-cols-2 gap-3">
<div>
<Label></Label>
<Input type="number" value={form.contractYears} onChange={(e) => handleContractYearsChange(parseInt(e.target.value) || 0)} min={1} />
</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 className="flex justify-end gap-2 pt-2">
<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>
</Modal>