feat: 系统优化Phase2 - 面包屑导航/侧边栏间距/制度公示阅读签收/模板变量中文化/通知类型补全

- 面包屑导航组件,集成至TopNav header
- 侧边栏菜单分组间距增大,分组间分隔线
- 制度公示员工阅读签收:PolicyReadRecord模型、portal路由、管理端阅读统计
- 修复Policies.tsx民主程序推进bug(字段名/API路径/参数)
- 用工文本模板变量名英文转中文显示
- 通知类型TYPE_LABELS补全(RISK_ALERT/SOCIAL_INS/OVERTIME_ALERT/PAYSLIP_READY)
- 通知示例数据补充
- h2标题统一为text-sm font-medium
- 新增run.md
This commit is contained in:
selfrelease
2026-07-26 20:32:38 +08:00
parent 9cb0d1f63b
commit d79e3baa34
71 changed files with 18561 additions and 3230 deletions
+769
View File
@@ -0,0 +1,769 @@
import { useState, useRef } from "react"
import { toast } from "sonner"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import api from "../../lib/api"
import Card from "../../components/ui/Card"
import Button from "../../components/ui/Button"
import { Input, Label, Select } from "../../components/ui/Input"
import Modal from "../../components/ui/Modal"
import { Users, FileText, AlertTriangle, Calendar, GraduationCap, TrendingUp, Scale, X, Plus, Paperclip, Trash2, Printer, Calculator, Shield, Info, Check, Eye, Download, UserX, UserPlus, Briefcase, FileSignature, DollarSign, Building2, RotateCcw, History } 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({
newDepartment: employee.department || '',
effectiveDate: todayStr,
reason: '',
})
const handleSubmit = () => {
onSubmit({
newDepartment: form.newDepartment,
effectiveDate: new Date(form.effectiveDate).toISOString(),
reason: form.reason || undefined,
})
}
const canSubmit = form.newDepartment && form.effectiveDate && form.newDepartment !== employee.department
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}</div>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label> *</Label>
<Input value={form.newDepartment} onChange={(e) => setForm({ ...form, newDepartment: 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 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 terminationMonth = form.terminationDate ? form.terminationDate.slice(0, 7) : ''
const reasons = ['个人原因', '职业发展', '薪资不满意', '家庭原因', '身体原因', '其他']
const handleSubmit = () => {
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,
})
}
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.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>
{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 () => {
const res = await api.get('/roster/contract-types') as any
return res.data || []
},
staleTime: Infinity,
})
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>
<Input value={form.department} onChange={(e) => setForm({ ...form, department: e.target.value })} placeholder="如:技术部" />
</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 || employee?.monthlySalary || ''} onChange={(e) => setForm({ ...form, socialInsBase: e.target.value })} placeholder="默认为月工资" />
</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 || employee?.monthlySalary || ''} onChange={(e) => setForm({ ...form, housingFundBase: e.target.value })} placeholder="默认为月工资" />
</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: cities = ['北京'] } = useQuery<string[]>({
queryKey: ['social-config-cities'],
queryFn: async () => {
const res = await api.get('/social/config/cities') as any
return res.data?.length ? res.data : ['北京']
},
})
const { data: contractTypes = [] } = useQuery<Array<{ value: string; label: string; hasEndDate: boolean }>>({
queryKey: ['contract-types'],
queryFn: async () => {
const res = await api.get('/roster/contract-types') as any
return res.data || []
},
staleTime: Infinity,
})
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: todayStr, monthlySalary: '',
idCardNumber: '', gender: '男' as '男' | '女', femaleWorkerType: '' as '' | 'CADRE' | 'WORKER', phone: '',
city: '北京',
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 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 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 })
}
// 计算合同月数
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 = () => {
const data: any = {
name: form.name, department: form.department,
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,
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.name && form.department && form.hireDate && form.monthlySalary
&& form.idCardNumber.length >= 18
&& (form.contractType === 'UNSIGNED' || form.startDate)
&& !probationError && !probationSalaryError
const isDirty = !!(form.name || form.department || form.idCardNumber || form.monthlySalary || form.phone)
useUnsavedChanges(isDirty)
return (
<Modal open onClose={onClose} title="添加员工" size="xl">
<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?.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><Input value={form.department} onChange={(e) => setForm({ ...form, department: e.target.value })} placeholder="如:技术部" /></div>
<div><Label> *</Label><Input value={form.idCardNumber} onChange={(e) => handleIdCardChange(e.target.value)} placeholder="18位" maxLength={18} /></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><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) => setForm({ ...form, phone: e.target.value })} placeholder="选填" maxLength={11} /></div>
</div>
<div className="grid grid-cols-4 gap-4">
<div><Label></Label><Select value={form.city} onChange={(e) => setForm({ ...form, city: e.target.value })}>{cities.map((c) => <option key={c} value={c}>{c}</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>
<span className="text-xs text-gray-500"></span>
</div>
<div className="grid grid-cols-4 gap-4">
<div>
<Label></Label>
<Input type="number" value={form.socialInsBase || form.monthlySalary} onChange={(e) => setForm({ ...form, socialInsBase: e.target.value })} placeholder="默认为月工资" />
</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 || form.monthlySalary} onChange={(e) => setForm({ ...form, housingFundBase: e.target.value })} placeholder="默认为月工资" />
</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 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)
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-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>
)
}