Files
TurboHR/frontend/src/pages/roster/modals.tsx
T
selfrelease 8391c123fc refactor: 社保公积金Tab重构为账户卡片列表+展开年度标准管理
1. 社保/公积金Tab从"选账户→展示配置"改为"账户卡片列表+展开管理"
2. 每个账户卡片可展开显示:当前标准/最低工资编辑/新建年度标准/版本历史/调基/试算
3. 账户新建/编辑/删除/设默认从设置页面迁移到社保公积金菜单
4. 设置页面去掉"社保公积金账户"Tab
5. "新建版本"改名为"新建年度标准"
6. 新增AccountCard组件独立管理每个账户的展开状态和数据查询

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-16 15:05:06 +08:00

1193 lines
56 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from "react"
import { useQuery } from "@tanstack/react-query"
import { toast } from "sonner"
import { rosterApi, socialInsuranceApi, employeeApi, socialAccountApi } from '../../lib/api-services'
import api from '../../lib/api'
import Button from "../../components/ui/Button"
import { Input, Label, Select } from "../../components/ui/Input"
import Modal from "../../components/ui/Modal"
import { AlertTriangle, Briefcase, FileSignature } from "lucide-react"
import { useUnsavedChanges } from "../../hooks/useUnsavedChanges"
import { fmt } from "./shared"
export function SalaryChangeModal({ employee, onClose, onSubmit, loading, error }: {
employee: any
onClose: () => void
onSubmit: (data: any) => void
loading: boolean
error: any
}) {
const todayStr = new Date().toISOString().slice(0, 10)
const [form, setForm] = useState({
newSalary: '',
effectiveDate: todayStr,
reason: '',
})
const handleSubmit = () => {
onSubmit({
newSalary: parseFloat(form.newSalary),
effectiveDate: new Date(form.effectiveDate).toISOString(),
reason: form.reason || undefined,
})
}
const canSubmit = form.newSalary && parseFloat(form.newSalary) > 0 && form.effectiveDate
return (
<Modal open onClose={onClose} title={`调薪 - ${employee.name}`}>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<Label></Label>
<div className="text-xs text-gray-600 py-1.5">{employee.name} - {employee.department}</div>
</div>
<div>
<Label></Label>
<div className="text-xs text-gray-600 py-1.5">¥{fmt(employee.monthlySalary)}</div>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label> *</Label>
<Input type="number" value={form.newSalary} onChange={(e) => setForm({ ...form, newSalary: e.target.value })} placeholder="元" />
</div>
<div>
<Label> *</Label>
<Input type="date" value={form.effectiveDate} onChange={(e) => setForm({ ...form, effectiveDate: e.target.value })} />
</div>
</div>
<div>
<Label></Label>
<Input value={form.reason} onChange={(e) => setForm({ ...form, reason: e.target.value })} placeholder="如:年度调薪、晋升加薪" />
</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 || !canSubmit}>{loading ? '保存中...' : '确认调薪'}</Button>
</div>
</div>
</Modal>
)
}
export function DeptChangeModal({ employee, onClose, onSubmit, loading, error }: {
employee: any
onClose: () => void
onSubmit: (data: any) => void
loading: boolean
error: any
}) {
const todayStr = new Date().toISOString().slice(0, 10)
const [form, setForm] = useState({
departmentId: employee.departmentId || '',
newPosition: employee.position || '',
effectiveDate: todayStr,
reason: '',
})
// 拉取组织架构部门和职务列表
const { data: departments = [] } = useQuery({
queryKey: ['departments'],
queryFn: () => api.get('/departments').then(r => r.data),
})
const { data: positions = [] } = useQuery({
queryKey: ['positions'],
queryFn: () => api.get('/positions').then(r => r.data),
})
// 构建部门树形下拉选项(带层级缩进)
const deptOptions: { id: string; label: string; level: number }[] = []
const buildDeptOptions = (items: any[], parentId: string | null, level: number) => {
items.filter(d => d.parentId === parentId).sort((a, b) => a.sortOrder - b.sortOrder).forEach(d => {
deptOptions.push({ id: d.id, label: d.name, level })
buildDeptOptions(items, d.id, level + 1)
})
}
buildDeptOptions(departments, null, 0)
// 选中新部门后,过滤该部门下的职务:
// 1. 通用职务(departmentId 为空)所有部门可用
// 2. 该部门专属职务
// 3. 父部门专属职务(子部门继承父部门职务,如铁矿继承项目运营中心的工人/施工员等)
const getDeptAncestorIds = (deptId: string): string[] => {
const ids: string[] = []
let cur: any = departments.find((d: any) => d.id === deptId)
while (cur) {
ids.push(cur.id)
cur = cur.parentId ? departments.find((d: any) => d.id === cur.parentId) : null
}
return ids
}
const ancestorIds = form.departmentId ? getDeptAncestorIds(form.departmentId) : []
const filteredPositions = form.departmentId
? positions.filter((p: any) => !p.departmentId || ancestorIds.includes(p.departmentId))
: positions
const selectedDept = departments.find((d: any) => d.id === form.departmentId)
const hasChange = (form.departmentId && form.departmentId !== employee.departmentId) ||
(form.newPosition && form.newPosition !== employee.position)
const handleSubmit = () => {
onSubmit({
departmentId: form.departmentId || undefined,
newDepartment: selectedDept?.name || employee.department,
newPosition: form.newPosition || undefined,
effectiveMonth: form.effectiveDate.slice(0, 7),
reason: form.reason || undefined,
})
}
const canSubmit = form.effectiveDate && hasChange
return (
<Modal open onClose={onClose} title={`调动 - ${employee.name}`}>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<Label></Label>
<div className="text-xs text-gray-600 py-1.5">{employee.name}</div>
</div>
<div>
<Label> / </Label>
<div className="text-xs text-gray-600 py-1.5">{employee.department} / {employee.position || '—'}</div>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label> *</Label>
<Select value={form.departmentId} onChange={(e) => setForm({ ...form, departmentId: e.target.value })}>
<option value=""></option>
{deptOptions.map(d => (
<option key={d.id} value={d.id}>{' '.repeat(d.level)}{d.label}</option>
))}
</Select>
</div>
<div>
<Label></Label>
<Select value={form.newPosition} onChange={(e) => setForm({ ...form, newPosition: e.target.value })}>
<option value=""></option>
{filteredPositions.map((p: any) => (
<option key={p.id} value={p.name}>{p.name}{p.level ? `${p.level}` : ''}</option>
))}
</Select>
</div>
</div>
<div>
<Label> *</Label>
<Input type="date" value={form.effectiveDate} onChange={(e) => setForm({ ...form, effectiveDate: e.target.value })} />
</div>
<div>
<Label></Label>
<Input value={form.reason} onChange={(e) => setForm({ ...form, reason: e.target.value })} placeholder="如:组织架构调整、岗位轮换" />
</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 || !canSubmit}>{loading ? '保存中...' : '确认调动'}</Button>
</div>
</div>
</Modal>
)
}
export function ResignModal({ employee, onClose, onSubmit, loading, error }: {
employee: any
onClose: () => void
onSubmit: (data: any) => void
loading: boolean
error: any
}) {
const [form, setForm] = useState({
terminationDate: new Date().toISOString().slice(0, 10),
resignationReason: '个人原因',
remark: '',
socialInsEndMonth: '',
housingFundEndMonth: '',
})
/** 离职/解聘类型:主动离职走快速流程,其他类型跳转解聘向导 */
const [termType, setTermType] = useState<'RESIGNATION' | 'NEGOTIATED' | 'FAULT' | 'NONFAULT' | 'LAYOFF' | 'EXPIRED' | 'ILLEGAL'>('RESIGNATION')
const terminationMonth = form.terminationDate ? form.terminationDate.slice(0, 7) : ''
const reasons = ['个人原因', '职业发展', '薪资不满意', '家庭原因', '身体原因', '其他']
const handleSubmit = () => {
if (termType === 'RESIGNATION') {
onSubmit({
employeeId: employee.id,
terminationDate: new Date(form.terminationDate).toISOString(),
resignationReason: form.resignationReason,
remark: form.remark || undefined,
socialInsEndMonth: form.socialInsEndMonth || terminationMonth,
housingFundEndMonth: form.housingFundEndMonth || terminationMonth,
})
} else {
// 非主动离职:跳转 Termination 向导,通过 URL 参数传递
const params = new URLSearchParams({
employeeId: employee.id,
reason: termType,
})
window.location.href = `/termination?${params.toString()}`
}
}
return (
<Modal open onClose={onClose} title={`办理离职/解聘 - ${employee.name}`}>
<div className="space-y-3">
{/* 解聘类型选择 */}
<div>
<Label>/</Label>
<Select value={termType} onChange={(e) => setTermType(e.target.value as any)}>
<option value="RESIGNATION"></option>
<option value="NEGOTIATED"></option>
<option value="FAULT"></option>
<option value="NONFAULT"></option>
<option value="LAYOFF"></option>
<option value="EXPIRED"></option>
<option value="ILLEGAL"></option>
</Select>
</div>
{termType === 'RESIGNATION' ? (
<>
<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.terminationDate}
onChange={(e) => setForm({ ...form, terminationDate: e.target.value })}
/>
</div>
<div>
<Label></Label>
<Select value={form.resignationReason} onChange={(e) => setForm({ ...form, resignationReason: e.target.value })}>
{reasons.map((r) => <option key={r} value={r}>{r}</option>)}
</Select>
</div>
<div className="border-t pt-3">
<Label></Label>
<div className="text-xs text-gray-400 mb-2"></div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label></Label>
<Input type="month" value={form.socialInsEndMonth || terminationMonth} onChange={(e) => setForm({ ...form, socialInsEndMonth: e.target.value })} />
</div>
<div>
<Label></Label>
<Input type="month" value={form.housingFundEndMonth || terminationMonth} onChange={(e) => setForm({ ...form, housingFundEndMonth: e.target.value })} />
</div>
</div>
{((form.socialInsEndMonth && form.socialInsEndMonth !== terminationMonth) || (form.housingFundEndMonth && form.housingFundEndMonth !== terminationMonth)) && (
<div className="flex items-center gap-2 px-3 py-2 rounded-md bg-amber-50 text-warning text-xs mt-2">
<AlertTriangle className="w-4 h-4 shrink-0" />
/
</div>
)}
</div>
<div>
<Label></Label>
<Input value={form.remark} onChange={(e) => setForm({ ...form, remark: e.target.value })} placeholder="补充说明" />
</div>
</>
) : (
<div className="bg-amber-50 text-amber-700 text-xs px-3 py-2 rounded-md space-y-1">
<div className="font-medium"></div>
<p>"{termType === 'NEGOTIATED' ? '协商解除' : termType === 'FAULT' ? '过错解除' : termType === 'NONFAULT' ? '非过错解除' : termType === 'LAYOFF' ? '裁员' : termType === 'EXPIRED' ? '到期不续签' : '违法解除'}"</p>
<p>"进入解聘向导"</p>
</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}>
{termType === 'RESIGNATION' ? (loading ? '提交中...' : '确认离职') : '进入解聘向导'}
</Button>
</div>
</div>
</Modal>
)
}
/** 转正确认弹窗 */
export function ConfirmModal({ employee, onClose, onSubmit, loading, error }: {
employee: any
onClose: () => void
onSubmit: (data: any) => void
loading: boolean
error: any
}) {
const originalSalary = employee.monthlySalary ? Number(employee.monthlySalary) : null
const [form, setForm] = useState({
confirmDate: new Date().toISOString().slice(0, 10),
regularSalary: originalSalary ? String(originalSalary) : '',
})
/** 转正薪资与原薪资是否不同 */
const salaryChanged = form.regularSalary !== '' && Number(form.regularSalary) !== originalSalary
const handleSubmit = () => {
onSubmit({
employeeId: employee.id,
confirmDate: new Date(form.confirmDate).toISOString(),
regularSalary: form.regularSalary ? Number(form.regularSalary) : undefined,
})
}
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.confirmDate} onChange={(e) => setForm({ ...form, confirmDate: e.target.value })} />
</div>
<div>
<Label>{originalSalary ? `(原薪资 ¥${originalSalary}` : ''}</Label>
<Input type="number" value={form.regularSalary} onChange={(e) => setForm({ ...form, regularSalary: e.target.value })} placeholder={originalSalary ? `不填则保持原薪资 ¥${originalSalary}` : '请输入转正薪资'} />
<div className="text-xs text-gray-400 mt-1">
{originalSalary
? (salaryChanged
? <span className="text-amber-600"> </span>
: '填写后将更新员工月工资并记录薪资变更')
: '填写后将更新员工月工资并记录薪资变更'}
</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>
)
}
export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
employee: any
onClose: () => void
onSubmit: (data: any) => void
loading: boolean
error: any
}) {
const todayStr = new Date().toISOString().slice(0, 10)
const { data: contractTypes = [] } = useQuery<Array<{ value: string; label: string; hasEndDate: boolean }>>({
queryKey: ['contract-types'],
queryFn: async () => {
return await rosterApi.contractTypes()
},
staleTime: Infinity,
})
// 拉取组织架构部门列表,用于部门下拉选择
const { data: departments = [] } = useQuery({
queryKey: ['departments'],
queryFn: () => api.get('/departments').then(r => r.data),
})
// 构建部门树形下拉选项(带层级缩进)
const deptOptions: { id: string; label: string; level: number }[] = []
const buildDeptOptions = (items: any[], parentId: string | null, level: number) => {
items.filter(d => d.parentId === parentId).sort((a, b) => a.sortOrder - b.sortOrder).forEach(d => {
deptOptions.push({ id: d.id, label: d.name, level })
buildDeptOptions(items, d.id, level + 1)
})
}
buildDeptOptions(departments, null, 0)
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({
hireDate: todayStr,
department: employee.department || '',
contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED',
signDate: '',
startDate: todayStr,
endDate: defaultEndDate,
contractYears: 3,
probationMonths: 0,
probationSalary: 0,
socialInsBase: '', socialInsStartMonth: '',
housingFundBase: '', housingFundStartMonth: '',
})
const hireMonth = form.hireDate ? form.hireDate.slice(0, 7) : ''
// 计算合同月数
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 = employee?.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 handleHireDateChange = (hireDate: string) => {
if (form.contractType === 'FIXED' && form.contractYears > 0 && hireDate) {
const start = new Date(hireDate)
const end = new Date(start)
end.setFullYear(end.getFullYear() + form.contractYears)
end.setDate(end.getDate() - 1)
setForm({ ...form, hireDate, startDate: hireDate, endDate: end.toISOString().slice(0, 10) })
} else {
setForm({ ...form, hireDate, startDate: hireDate })
}
}
const handleSubmit = () => {
const data: any = {
hireDate: new Date(form.hireDate).toISOString(),
department: form.department,
socialInsBase: form.socialInsBase ? parseFloat(form.socialInsBase) : undefined,
socialInsStartMonth: form.socialInsStartMonth || undefined,
housingFundBase: form.housingFundBase ? parseFloat(form.housingFundBase) : undefined,
housingFundStartMonth: form.housingFundStartMonth || undefined,
}
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)
}
const canSubmit = form.hireDate
&& (form.contractType === 'UNSIGNED' || form.startDate)
&& !probationError && !probationSalaryError
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 className="grid grid-cols-2 gap-3">
<div>
<Label></Label>
<div className="text-xs text-gray-600 py-1.5">{employee.name}</div>
</div>
<div>
<Label> *</Label>
<Select value={form.department} onChange={(e) => setForm({ ...form, department: e.target.value })}>
<option value=""></option>
{deptOptions.map(d => (
<option key={d.id} value={d.label}>{' '.repeat(d.level)}{d.label}</option>
))}
</Select>
</div>
</div>
<div>
<Label> *</Label>
<Input type="date" value={form.hireDate} onChange={(e) => handleHireDateChange(e.target.value)} />
</div>
<div className="border-t pt-3">
<Label></Label>
<div className="text-xs text-gray-400 mb-2"></div>
<div className="grid grid-cols-4 gap-3">
<div>
<Label></Label>
<Input type="number" value={form.socialInsBase} onChange={(e) => setForm({ ...form, socialInsBase: e.target.value })} onFocus={(e) => e.target.select()} placeholder={employee?.monthlySalary || '默认为月工资'} />
</div>
<div>
<Label></Label>
<Input type="month" value={form.socialInsStartMonth || hireMonth} onChange={(e) => setForm({ ...form, socialInsStartMonth: e.target.value })} />
</div>
<div>
<Label></Label>
<Input type="number" value={form.housingFundBase} onChange={(e) => setForm({ ...form, housingFundBase: e.target.value })} onFocus={(e) => e.target.select()} placeholder={employee?.monthlySalary || '默认为月工资'} />
</div>
<div>
<Label></Label>
<Input type="month" value={form.housingFundStartMonth || hireMonth} onChange={(e) => setForm({ ...form, housingFundStartMonth: e.target.value })} />
</div>
</div>
</div>
<div className="border-t pt-3">
<div className="grid grid-cols-4 gap-3">
<div className="col-span-1">
<Label></Label>
<Select value={form.contractType} onChange={(e) => {
const ct = contractTypes.find(t => t.value === e.target.value)
setForm({ ...form, contractType: e.target.value as any, endDate: ct && !ct.hasEndDate ? '' : form.endDate })
}}>
{contractTypes.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
</Select>
</div>
</div>
</div>
{form.contractType !== 'UNSIGNED' && (
<div className="space-y-3">
<div className="grid grid-cols-4 gap-3">
<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><div className="text-xs text-gray-600 py-1.5">{form.startDate || '随入职日期'}</div></div>
{form.contractType === 'FIXED' && (
<div>
<Label></Label>
<Input type="number" value={form.contractYears} onChange={(e) => handleContractYearsChange(parseInt(e.target.value) || 0)} min={1} />
</div>
)}
{form.contractType === 'FIXED' && (
<div>
<Label></Label>
<Input type="date" value={form.endDate} onChange={(e) => handleEndDateChange(e.target.value)} />
</div>
)}
</div>
{form.contractType === 'FIXED' && (
<div className="text-xs text-gray-400"></div>
)}
<div className="grid grid-cols-4 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>
)}
{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 || !canSubmit}>{loading ? '提交中...' : '确认入职'}</Button>
</div>
</div>
</Modal>
)
}
export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
onClose: () => void
onSubmit: (data: any) => void
loading: boolean
error: any
}) {
const todayStr = new Date().toISOString().slice(0, 10)
const { data: contractTypes = [] } = useQuery<Array<{ value: string; label: string; hasEndDate: boolean }>>({
queryKey: ['contract-types'],
queryFn: async () => {
return await rosterApi.contractTypes()
},
staleTime: Infinity,
})
// 拉取组织架构部门列表,用于部门下拉选择
const { data: departments = [] } = useQuery({
queryKey: ['departments'],
queryFn: () => api.get('/departments').then(r => r.data),
})
// 构建部门树形下拉选项(带层级缩进)
const deptOptions: { id: string; label: string; level: number }[] = []
const buildDeptOptions = (items: any[], parentId: string | null, level: number) => {
items.filter(d => d.parentId === parentId).sort((a, b) => a.sortOrder - b.sortOrder).forEach(d => {
deptOptions.push({ id: d.id, label: d.name, level })
buildDeptOptions(items, d.id, level + 1)
})
}
buildDeptOptions(departments, null, 0)
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(() => {
try {
const saved = localStorage.getItem('add-employee-draft')
if (saved) return JSON.parse(saved)
} catch {}
return {
name: '', department: '', departmentId: '', position: '', hireDate: todayStr, monthlySalary: '',
idCardNumber: '', gender: '男' as '男' | '女', femaleWorkerType: '' as '' | 'CADRE' | 'WORKER', phone: '',
city: '北京', education: '', status: 'ACTIVE' as 'ACTIVE' | 'PRE_ONBOARD',
contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED',
signDate: '', startDate: todayStr, endDate: defaultEndDate,
contractYears: 3, probationMonths: 0, probationSalary: 0,
socialInsBase: '', socialInsStartMonth: '',
housingFundBase: '', housingFundStartMonth: '',
}
})
// 持久化草稿到 localStorage,防止录入数据丢失
useEffect(() => {
try {
const isDirty = !!(form.name || form.department || form.idCardNumber || form.monthlySalary || form.phone)
if (isDirty) {
localStorage.setItem('add-employee-draft', JSON.stringify(form))
} else {
localStorage.removeItem('add-employee-draft')
}
} catch {}
}, [form])
const hireMonth = form.hireDate ? form.hireDate.slice(0, 7) : ''
// 选定部门后自动带出社保公积金账户
const [autoAccounts, setAutoAccounts] = useState<{ socialAccount: any; housingAccount: any; socialStandard: any; housingStandard: any } | null>(null)
const [manualSocialAccountId, setManualSocialAccountId] = useState<string>('')
const [manualHousingAccountId, setManualHousingAccountId] = useState<string>('')
// 部门变化时查询适用账户
useEffect(() => {
if (form.departmentId) {
socialAccountApi.departmentAccount(form.departmentId).then((res: any) => {
setAutoAccounts(res)
setManualSocialAccountId(res.socialAccount?.id || '')
setManualHousingAccountId(res.housingAccount?.id || '')
}).catch(() => {})
} else {
setAutoAccounts(null)
}
}, [form.departmentId])
// 加载所有账户列表(供手动调整)
const { data: allSocialAccounts = [] } = useQuery<any[]>({
queryKey: ['social-accounts', 'SOCIAL'],
queryFn: () => socialAccountApi.list('SOCIAL'),
enabled: !!form.departmentId,
})
const { data: allHousingAccounts = [] } = useQuery<any[]>({
queryKey: ['social-accounts', 'HOUSING'],
queryFn: () => socialAccountApi.list('HOUSING'),
enabled: !!form.departmentId,
})
// 入职日期变更 → 同步合同开始日期 + 重算结束日期
const handleHireDateChange = (hireDate: string) => {
if (form.contractType === 'FIXED' && form.contractYears > 0 && hireDate) {
const start = new Date(hireDate)
const end = new Date(start)
end.setFullYear(end.getFullYear() + form.contractYears)
end.setDate(end.getDate() - 1)
setForm({ ...form, hireDate, startDate: hireDate, endDate: end.toISOString().slice(0, 10) })
} else {
setForm({ ...form, hireDate, startDate: hireDate })
}
}
// 根据证件号码自动计算性别(第17位:奇数=男,偶数=女)+ 查重 + 年龄合规筛查 + 有效性校验
const [idCardDuplicate, setIdCardDuplicate] = useState<{ exists: boolean; employee?: any } | null>(null)
const [ageWarning, setAgeWarning] = useState<{ type: 'BLOCK' | 'WARN'; message: string } | null>(null)
const [phoneDuplicate, setPhoneDuplicate] = useState<{ exists: boolean; employee?: any } | null>(null)
// 身份证校验位算法(GB 11643-1999
const ID_WEIGHTS = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
const ID_CHECK_CODES = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
const handleIdCardChange = (idCard: string) => {
let gender = form.gender
if (idCard.length >= 17) {
const digit = parseInt(idCard[16])
if (!isNaN(digit)) gender = digit % 2 === 1 ? '男' : '女'
}
setForm({ ...form, idCardNumber: idCard, gender })
setIdCardDuplicate(null)
setAgeWarning(null)
if (idCard.length === 18) {
// 1. 基本格式校验:前17位必须为数字
if (!/^\d{17}[\dXx]$/.test(idCard)) {
setAgeWarning({ type: 'BLOCK', message: '证件号码格式错误:前17位必须为数字,第18位为数字或X' })
return
}
// 2. 校验位验证
const sum = idCard.substring(0, 17).split('').reduce((s, c, i) => s + parseInt(c) * ID_WEIGHTS[i], 0)
const expectedCheck = ID_CHECK_CODES[sum % 11]
if (idCard[17].toUpperCase() !== expectedCheck) {
setAgeWarning({ type: 'BLOCK', message: '证件号码校验位错误,请检查输入是否正确' })
return
}
// 3. 出生日期合法性验证
const birthYear = parseInt(idCard.substring(6, 10))
const birthMonth = parseInt(idCard.substring(10, 12))
const birthDay = parseInt(idCard.substring(12, 14))
if (birthMonth < 1 || birthMonth > 12 || birthDay < 1 || birthDay > 31) {
setAgeWarning({ type: 'BLOCK', message: '证件号码出生日期非法(月份或日期超出范围)' })
return
}
const birthDate = new Date(birthYear, birthMonth - 1, birthDay)
if (isNaN(birthDate.getTime()) || birthDate.getFullYear() !== birthYear || birthDate.getMonth() !== birthMonth - 1 || birthDate.getDate() !== birthDay) {
setAgeWarning({ type: 'BLOCK', message: '证件号码出生日期不存在(如2月30日)' })
return
}
if (birthDate > new Date()) {
setAgeWarning({ type: 'BLOCK', message: '证件号码出生日期晚于今天,不可录入' })
return
}
employeeApi.checkIdCard(idCard).then((data: { exists: boolean; employee?: any }) => {
setIdCardDuplicate(data)
}).catch(() => {})
// 4. 年龄合规筛查
const today = new Date()
let age = today.getFullYear() - birthYear
const monthDiff = today.getMonth() - (birthMonth - 1)
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDay)) {
age--
}
if (age < 16) {
setAgeWarning({ type: 'BLOCK', message: `该员工年龄 ${age} 岁,未满16周岁,禁止招用童工(《劳动法》第15条)` })
} else if (age < 18) {
setAgeWarning({ type: 'WARN', message: `该员工年龄 ${age} 岁,未满18周岁,属于未成年工,需遵守特殊保护规定(《劳动法》第58条)` })
} else if (gender === '男' && age >= 60) {
setAgeWarning({ type: 'WARN', message: `该员工年龄 ${age} 岁,已达法定退休年龄(男60岁),不可签订劳动合同,请选择劳务协议` })
} else if (gender === '女' && age >= 50) {
setAgeWarning({ type: 'WARN', message: `该员工年龄 ${age} 岁,已达或接近法定退休年龄(女工人50岁/干部55岁),不可签订劳动合同,请选择劳务协议` })
}
}
}
// 派生:是否超龄(用于限制合同类型)
const isOverage = (() => {
if (form.idCardNumber.length !== 18) return false
const birthYear = parseInt(form.idCardNumber.substring(6, 10))
const birthMonth = parseInt(form.idCardNumber.substring(10, 12))
const birthDay = parseInt(form.idCardNumber.substring(12, 14))
if (isNaN(birthYear) || isNaN(birthMonth) || isNaN(birthDay)) return false
const today = new Date()
let age = today.getFullYear() - birthYear
const monthDiff = today.getMonth() - (birthMonth - 1)
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDay)) age--
if (form.gender === '男') return age >= 60
// 女:干部55,工人50,未选类型按50
return age >= (form.femaleWorkerType === 'CADRE' ? 55 : 50)
})()
// 劳务协议/实习协议:不缴纳社保公积金
const isNoSocialContract = form.contractType === 'LABOR' || form.contractType === 'INTERNSHIP'
// 超龄时若当前合同类型非法(草稿恢复场景),自动切到 UNSIGNED
useEffect(() => {
if (isOverage && !['LABOR', 'INTERNSHIP', 'UNSIGNED'].includes(form.contractType)) {
setForm((prev: any) => ({ ...prev, contractType: 'UNSIGNED' }))
}
}, [isOverage, form.contractType])
// 计算合同月数
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 handleSubmit = () => {
// 童工阻断:未满16周岁禁止录入
if (ageWarning?.type === 'BLOCK') {
toast.error(ageWarning.message)
return
}
const data: any = {
name: form.name, department: form.department, departmentId: form.departmentId || undefined,
position: form.position || undefined,
hireDate: new Date(form.hireDate).toISOString(),
monthlySalary: form.monthlySalary, gender: form.gender,
femaleWorkerType: form.gender === '女' && form.femaleWorkerType ? form.femaleWorkerType : undefined,
idCardNumber: form.idCardNumber || undefined,
phone: form.phone || undefined,
education: form.education || undefined,
city: autoAccounts?.socialAccount?.city || form.city || '北京',
status: form.status || 'ACTIVE',
socialInsBase: form.socialInsBase ? parseFloat(form.socialInsBase) : undefined,
socialInsStartMonth: form.socialInsStartMonth || undefined,
housingFundBase: form.housingFundBase ? parseFloat(form.housingFundBase) : undefined,
housingFundStartMonth: form.housingFundStartMonth || undefined,
socialAccountId: manualSocialAccountId || undefined,
housingAccountId: manualHousingAccountId || undefined,
}
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)
}
const canSubmit = form.name && form.department && form.hireDate && form.monthlySalary
&& form.idCardNumber.length >= 18
&& ageWarning?.type !== 'BLOCK'
&& (form.contractType === 'UNSIGNED' || form.startDate)
&& !probationError && !probationSalaryError
// 超龄人员不得签订劳动合同
&& (!isOverage || ['LABOR', 'INTERNSHIP', 'UNSIGNED'].includes(form.contractType))
const isDirty = !!(form.name || form.department || form.idCardNumber || form.monthlySalary || form.phone)
useUnsavedChanges(isDirty)
return (
<Modal open onClose={onClose} title="添加员工" size="xl" closeOnOverlayClick={false}>
<div className="space-y-4">
{error && (
<div className="px-3 py-2 rounded-md bg-red-50 text-red-700 text-sm">
{error.response?.data?.error?.details?.length > 0
? error.response.data.error.details.map((d: any, i: number) => (
<div key={i}> {d.path}: {d.message}</div>
))
: (error.response?.data?.error?.message || '操作失败')}
</div>
)}
{/* 基本信息 */}
<div className="grid grid-cols-4 gap-4">
<div><Label> *</Label><Input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="员工姓名" /></div>
<div><Label> *</Label><Select value={form.departmentId} onChange={(e) => {
const opt = deptOptions.find(d => d.id === e.target.value)
setForm({ ...form, departmentId: e.target.value, department: opt?.label || '' })
}}><option value=""></option>{deptOptions.map(d => <option key={d.id} value={d.id}>{' '.repeat(d.level)}{d.label}</option>)}</Select></div>
<div><Label>/</Label><Input value={form.position} onChange={(e) => setForm({ ...form, position: e.target.value })} placeholder="如:前端工程师" /></div>
<div><Label> *</Label><Input value={form.idCardNumber} onChange={(e) => handleIdCardChange(e.target.value)} placeholder="18位" maxLength={18} /></div>
{idCardDuplicate?.exists && (
<div className="col-span-4 px-3 py-2 rounded-md bg-amber-50 text-amber-700 text-xs flex items-center gap-2">
<AlertTriangle className="w-4 h-4 shrink-0" />
<span>{idCardDuplicate.employee?.name}{idCardDuplicate.employee?.department}</span>
</div>
)}
{ageWarning && (
<div className={`col-span-4 px-3 py-2 rounded-md text-xs flex items-center gap-2 ${ageWarning.type === 'BLOCK' ? 'bg-red-50 text-danger' : 'bg-amber-50 text-amber-700'}`}>
<AlertTriangle className="w-4 h-4 shrink-0" />
<span>{ageWarning.message}</span>
</div>
)}
<div><Label></Label><div className="text-sm text-gray-600 py-2">{form.idCardNumber.length >= 17 ? form.gender : '自动识别'}</div></div>
{form.gender === '女' && (
<div><Label></Label><Select value={form.femaleWorkerType} onChange={(e) => setForm({ ...form, femaleWorkerType: e.target.value as '' | 'CADRE' | 'WORKER' })}><option value=""></option><option value="CADRE">/</option><option value="WORKER">/</option></Select></div>
)}
</div>
<div className="grid grid-cols-4 gap-4">
<div><Label> *</Label><Input type="date" value={form.hireDate} onChange={(e) => handleHireDateChange(e.target.value)} /></div>
<div><Label></Label><Select value={form.status} onChange={(e) => setForm({ ...form, status: e.target.value as 'ACTIVE' | 'PRE_ONBOARD' })}><option value="ACTIVE"></option><option value="PRE_ONBOARD"></option></Select></div>
<div><Label> *</Label><Input type="number" value={form.monthlySalary} onChange={(e) => setForm({ ...form, monthlySalary: e.target.value })} placeholder="元" /></div>
<div><Label></Label><Input value={form.phone} onChange={(e) => {
const phone = e.target.value.replace(/\D/g, '').slice(0, 11)
setForm({ ...form, phone })
setPhoneDuplicate(null)
if (phone.length === 11) {
employeeApi.checkPhone(phone).then((data: { exists: boolean; employee?: any }) => {
setPhoneDuplicate(data)
}).catch(() => {})
}
}} placeholder="选填" maxLength={11} /></div>
</div>
{phoneDuplicate?.exists && (
<div className="px-3 py-2 rounded-md bg-amber-50 text-amber-700 text-xs flex items-center gap-2">
<AlertTriangle className="w-4 h-4 shrink-0" />
<span>{phoneDuplicate.employee?.name}{phoneDuplicate.employee?.department}</span>
</div>
)}
<div className="grid grid-cols-4 gap-4">
<div><Label></Label><div className="text-xs text-gray-600 py-1.5">{autoAccounts?.socialAccount?.city || form.city || '—'}</div><div className="text-xs text-gray-400"></div></div>
<div><Label></Label><Select value={form.education} onChange={(e) => setForm({ ...form, education: e.target.value })}><option value=""></option><option value="博士"></option><option value="硕士"></option><option value="本科"></option><option value="大专"></option><option value="高中"></option><option value="其他"></option></Select></div>
</div>
{/* 社保公积金 */}
<div className="border-t border-gray-200 pt-4">
<div className="flex items-center gap-2 mb-3">
<Briefcase className="w-4 h-4 text-gray-500" />
<span className="text-sm font-medium text-gray-700"></span>
{isNoSocialContract ? (
<span className="text-xs text-amber-600">/</span>
) : (
<span className="text-xs text-gray-500"></span>
)}
</div>
{/* 账户选择(选部门后自动带出,可手动调整) */}
{!isNoSocialContract && (
<div className="grid grid-cols-2 gap-4 mb-3">
<div>
<Label></Label>
<Select value={manualSocialAccountId} onChange={(e) => setManualSocialAccountId(e.target.value)} disabled={!form.departmentId}>
<option value="">{form.departmentId ? '未选择' : '请先选择部门'}</option>
{allSocialAccounts.map((a: any) => (
<option key={a.id} value={a.id}>{a.name}{a.city}</option>
))}
</Select>
{autoAccounts?.socialStandard && manualSocialAccountId === autoAccounts.socialAccount?.id && (
<div className="text-xs text-gray-400 mt-1">
{autoAccounts.socialStandard.baseMin}~{autoAccounts.socialStandard.baseMax} {autoAccounts.socialStandard.pensionOrg}%/{autoAccounts.socialStandard.pensionEmp}%
</div>
)}
</div>
<div>
<Label></Label>
<Select value={manualHousingAccountId} onChange={(e) => setManualHousingAccountId(e.target.value)} disabled={!form.departmentId}>
<option value="">{form.departmentId ? '未选择' : '请先选择部门'}</option>
{allHousingAccounts.map((a: any) => (
<option key={a.id} value={a.id}>{a.name}{a.city}</option>
))}
</Select>
{autoAccounts?.housingStandard && manualHousingAccountId === autoAccounts.housingAccount?.id && (
<div className="text-xs text-gray-400 mt-1">
{autoAccounts.housingStandard.baseMin}~{autoAccounts.housingStandard.baseMax} {autoAccounts.housingStandard.housingOrg}%/{autoAccounts.housingStandard.housingEmp}%
</div>
)}
</div>
</div>
)}
<div className={`grid grid-cols-4 gap-4 ${isNoSocialContract ? 'opacity-50' : ''}`}>
<div>
<Label></Label>
<Input type="number" value={form.socialInsBase} onChange={(e) => setForm({ ...form, socialInsBase: e.target.value })} onFocus={(e) => e.target.select()} placeholder={form.monthlySalary || '默认为月工资'} disabled={isNoSocialContract} />
</div>
<div>
<Label></Label>
<Input type="month" value={form.socialInsStartMonth || hireMonth} onChange={(e) => setForm({ ...form, socialInsStartMonth: e.target.value })} disabled={isNoSocialContract} />
</div>
<div>
<Label></Label>
<Input type="number" value={form.housingFundBase} onChange={(e) => setForm({ ...form, housingFundBase: e.target.value })} onFocus={(e) => e.target.select()} placeholder={form.monthlySalary || '默认为月工资'} disabled={isNoSocialContract} />
</div>
<div>
<Label></Label>
<Input type="month" value={form.housingFundStartMonth || hireMonth} onChange={(e) => setForm({ ...form, housingFundStartMonth: e.target.value })} disabled={isNoSocialContract} />
</div>
</div>
</div>
{/* 合同信息 */}
<div className="border-t border-gray-200 pt-4">
<div className="flex items-center gap-2 mb-3">
<FileSignature className="w-4 h-4 text-gray-500" />
<span className="text-sm font-medium text-gray-700"></span>
</div>
<div className="grid grid-cols-4 gap-4">
<div className="col-span-1">
<Label></Label>
<Select value={form.contractType} onChange={(e) => {
const ct = contractTypes.find(t => t.value === e.target.value)
const newType = e.target.value as any
// 选劳务协议/实习协议 → 社保公积金置 0 并清空起始月
if (newType === 'LABOR' || newType === 'INTERNSHIP') {
setForm({
...form,
contractType: newType,
endDate: ct && !ct.hasEndDate ? '' : form.endDate,
socialInsBase: '0', socialInsStartMonth: '',
housingFundBase: '0', housingFundStartMonth: '',
})
} else {
setForm({ ...form, contractType: newType, endDate: ct && !ct.hasEndDate ? '' : form.endDate })
}
}}>
{contractTypes
.filter(t => !isOverage || ['LABOR', 'INTERNSHIP', 'UNSIGNED'].includes(t.value))
.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
</Select>
{isOverage && (
<div className="text-xs text-amber-600 mt-1">//</div>
)}
</div>
</div>
</div>
{form.contractType !== 'UNSIGNED' && (
<div className="space-y-4">
<div className="grid grid-cols-4 gap-4">
<div>
<Label></Label>
<Input type="date" value={form.signDate} onChange={(e) => setForm({ ...form, signDate: e.target.value })} />
<div className="text-xs text-gray-500 mt-1"></div>
</div>
<div><Label></Label><div className="text-sm text-gray-600 py-2">{form.startDate || '随入职日期'}</div></div>
{form.contractType === 'FIXED' && (
<div>
<Label></Label>
<Input type="number" value={form.contractYears} onChange={(e) => handleContractYearsChange(parseInt(e.target.value) || 0)} min={1} />
</div>
)}
{form.contractType === 'FIXED' && (
<div>
<Label></Label>
<Input type="date" value={form.endDate} onChange={(e) => handleEndDateChange(e.target.value)} />
</div>
)}
</div>
{form.contractType === 'FIXED' && (
<div className="text-xs text-gray-500"></div>
)}
<div className="grid grid-cols-4 gap-4">
<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-500 mt-1">{probationMax}</div>
)}
{probationError && <div className="text-xs text-danger mt-1">{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-500 mt-1">80%¥{(monthlySalaryNum * 0.8).toFixed(0)}</div>
)}
{probationSalaryError && <div className="text-xs text-danger mt-1">{probationSalaryError}</div>}
</div>
</div>
</div>
)}
<div className="flex justify-end gap-3 pt-3 border-t border-gray-200">
<Button variant="secondary" onClick={onClose}></Button>
<Button onClick={handleSubmit} disabled={loading || !canSubmit}>{loading ? '保存中...' : '保存'}</Button>
</div>
</div>
</Modal>
)
}