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 (
{employee.name} - {employee.department}
¥{fmt(employee.monthlySalary)}
setForm({ ...form, newSalary: e.target.value })} placeholder="元" />
setForm({ ...form, effectiveDate: e.target.value })} />
setForm({ ...form, reason: e.target.value })} placeholder="如:年度调薪、晋升加薪" />
{error && (
{(error as any)?.response?.data?.error?.message || '操作失败,请重试'}
)}
) } 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 (
{employee.name}
{employee.department} / {employee.position || '—'}
setForm({ ...form, effectiveDate: e.target.value })} />
setForm({ ...form, reason: e.target.value })} placeholder="如:组织架构调整、岗位轮换" />
{error && (
{(error as any)?.response?.data?.error?.message || '操作失败,请重试'}
)}
) } 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 (
{/* 解聘类型选择 */}
{termType === 'RESIGNATION' ? ( <>
员工主动离职,不涉及经济补偿金。离职日期可在未来(提前办理),到日期后状态自动变为离职。
{employee.name} - {employee.department}
setForm({ ...form, terminationDate: e.target.value })} />
默认与离职日期同月,可手动修改
setForm({ ...form, socialInsEndMonth: e.target.value })} />
setForm({ ...form, housingFundEndMonth: e.target.value })} />
{((form.socialInsEndMonth && form.socialInsEndMonth !== terminationMonth) || (form.housingFundEndMonth && form.housingFundEndMonth !== terminationMonth)) && (
截止缴费年月与离职日期不在同月,请确认是否为多缴/少缴月份。
)}
setForm({ ...form, remark: e.target.value })} placeholder="补充说明" />
) : (
解聘流程

选择"{termType === 'NEGOTIATED' ? '协商解除' : termType === 'FAULT' ? '过错解除' : termType === 'NONFAULT' ? '非过错解除' : termType === 'LAYOFF' ? '裁员' : termType === 'EXPIRED' ? '到期不续签' : '违法解除'}"将进入解聘向导,支持补偿金计算、合规检查、工作交接等完整流程。

点击"进入解聘向导"跳转到解聘补偿页面。

)} {error && (
{(error as any)?.response?.data?.error?.message || '操作失败,请重试'}
)}
) } /** 转正确认弹窗 */ 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 (
员工试用期结束,确认转正。转正后薪资将更新到花名册。
{employee.name} - {employee.department}
setForm({ ...form, confirmDate: e.target.value })} />
setForm({ ...form, regularSalary: e.target.value })} placeholder={originalSalary ? `不填则保持原薪资 ¥${originalSalary}` : '请输入转正薪资'} />
{originalSalary ? (salaryChanged ? ⚠ 转正薪资与原薪资不同,提交后将发起薪酬调整确认书签署 : '填写后将更新员工月工资并记录薪资变更') : '填写后将更新员工月工资并记录薪资变更'}
{error && (
{(error as any)?.response?.data?.error?.message || '操作失败,请重试'}
)}
) } 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>({ 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 (
复用已有基本信息,只需填写新入职日期和劳动合同。
{employee.name}
handleHireDateChange(e.target.value)} />
默认与月工资一致,可手动修改
setForm({ ...form, socialInsBase: e.target.value })} onFocus={(e) => e.target.select()} placeholder={employee?.monthlySalary || '默认为月工资'} />
setForm({ ...form, socialInsStartMonth: e.target.value })} />
setForm({ ...form, housingFundBase: e.target.value })} onFocus={(e) => e.target.select()} placeholder={employee?.monthlySalary || '默认为月工资'} />
setForm({ ...form, housingFundStartMonth: e.target.value })} />
{form.contractType !== 'UNSIGNED' && (
setForm({ ...form, signDate: e.target.value })} />
留空表示尚未签订
{form.startDate || '随入职日期'}
{form.contractType === 'FIXED' && (
handleContractYearsChange(parseInt(e.target.value) || 0)} min={1} />
)} {form.contractType === 'FIXED' && (
handleEndDateChange(e.target.value)} />
)}
{form.contractType === 'FIXED' && (
修改签约时长自动计算结束日期,修改结束日期自动计算签约时长
)}
setForm({ ...form, probationMonths: parseInt(e.target.value) || 0 })} min={0} max={6} /> {contractMonths > 0 && (
法定上限:{probationMax}个月
)} {probationError &&
{probationError}
}
setForm({ ...form, probationSalary: parseFloat(e.target.value) || 0 })} disabled={form.probationMonths <= 0} /> {monthlySalaryNum > 0 && form.probationMonths > 0 && (
不低于转正工资80%(≥¥{(monthlySalaryNum * 0.8).toFixed(0)})
)} {probationSalaryError &&
{probationSalaryError}
}
)} {error && (
{(error as any)?.response?.data?.error?.message || '操作失败,请重试'}
)}
) } 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>({ 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('') const [manualHousingAccountId, setManualHousingAccountId] = useState('') // 部门变化时查询适用账户 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({ queryKey: ['social-accounts', 'SOCIAL'], queryFn: () => socialAccountApi.list('SOCIAL'), enabled: !!form.departmentId, }) const { data: allHousingAccounts = [] } = useQuery({ 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 (
{error && (
{error.response?.data?.error?.details?.length > 0 ? error.response.data.error.details.map((d: any, i: number) => (
• {d.path}: {d.message}
)) : (error.response?.data?.error?.message || '操作失败')}
)} {/* 基本信息 */}
setForm({ ...form, name: e.target.value })} placeholder="员工姓名" />
setForm({ ...form, position: e.target.value })} placeholder="如:前端工程师" />
handleIdCardChange(e.target.value)} placeholder="18位" maxLength={18} />
{idCardDuplicate?.exists && (
该证件号码已存在:{idCardDuplicate.employee?.name}({idCardDuplicate.employee?.department}),请确认是否重复录入
)} {ageWarning && (
{ageWarning.message}
)}
{form.idCardNumber.length >= 17 ? form.gender : '自动识别'}
{form.gender === '女' && (
)}
handleHireDateChange(e.target.value)} />
setForm({ ...form, monthlySalary: e.target.value })} placeholder="元" />
{ 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} />
{phoneDuplicate?.exists && (
该手机号已存在:{phoneDuplicate.employee?.name}({phoneDuplicate.employee?.department}),请确认是否重复录入
)}
{autoAccounts?.socialAccount?.city || form.city || '—'}
从社保账户自动继承
{/* 社保公积金 */}
社保公积金 {isNoSocialContract ? ( 劳务协议/实习协议人员不缴纳社保公积金 ) : ( 选定部门后自动带出账户,可手动调整。缴费基数默认与月工资一致。 )}
{/* 账户选择(选部门后自动带出,可手动调整) */} {!isNoSocialContract && (
{autoAccounts?.socialStandard && manualSocialAccountId === autoAccounts.socialAccount?.id && (
当前标准:基数 {autoAccounts.socialStandard.baseMin}~{autoAccounts.socialStandard.baseMax},养老 {autoAccounts.socialStandard.pensionOrg}%/{autoAccounts.socialStandard.pensionEmp}%
)}
{autoAccounts?.housingStandard && manualHousingAccountId === autoAccounts.housingAccount?.id && (
当前标准:基数 {autoAccounts.housingStandard.baseMin}~{autoAccounts.housingStandard.baseMax},公积金 {autoAccounts.housingStandard.housingOrg}%/{autoAccounts.housingStandard.housingEmp}%
)}
)}
setForm({ ...form, socialInsBase: e.target.value })} onFocus={(e) => e.target.select()} placeholder={form.monthlySalary || '默认为月工资'} disabled={isNoSocialContract} />
setForm({ ...form, socialInsStartMonth: e.target.value })} disabled={isNoSocialContract} />
setForm({ ...form, housingFundBase: e.target.value })} onFocus={(e) => e.target.select()} placeholder={form.monthlySalary || '默认为月工资'} disabled={isNoSocialContract} />
setForm({ ...form, housingFundStartMonth: e.target.value })} disabled={isNoSocialContract} />
{/* 合同信息 */}
合同信息
{isOverage && (
超龄人员不可签订劳动合同,仅可选劳务协议/实习协议/未签
)}
{form.contractType !== 'UNSIGNED' && (
setForm({ ...form, signDate: e.target.value })} />
留空表示尚未签订
{form.startDate || '随入职日期'}
{form.contractType === 'FIXED' && (
handleContractYearsChange(parseInt(e.target.value) || 0)} min={1} />
)} {form.contractType === 'FIXED' && (
handleEndDateChange(e.target.value)} />
)}
{form.contractType === 'FIXED' && (
修改签约时长自动计算结束日期,修改结束日期自动计算签约时长
)}
setForm({ ...form, probationMonths: parseInt(e.target.value) || 0 })} min={0} max={6} /> {contractMonths > 0 && (
法定上限:{probationMax}个月
)} {probationError &&
{probationError}
}
setForm({ ...form, probationSalary: parseFloat(e.target.value) || 0 })} disabled={form.probationMonths <= 0} /> {monthlySalaryNum > 0 && form.probationMonths > 0 && (
不低于转正工资80%(≥¥{(monthlySalaryNum * 0.8).toFixed(0)})
)} {probationSalaryError &&
{probationSalaryError}
}
)}
) }