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 || '操作失败,请重试'} )} 取消 {loading ? '保存中...' : '确认调薪'} ) } 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, departmentId: e.target.value })}> 请选择部门 {deptOptions.map(d => ( {' '.repeat(d.level)}{d.label} ))} 新职务 setForm({ ...form, newPosition: e.target.value })}> 保持不变 {filteredPositions.map((p: any) => ( {p.name}{p.level ? `(${p.level})` : ''} ))} 生效日期 * setForm({ ...form, effectiveDate: e.target.value })} /> 调动原因(选填) setForm({ ...form, reason: e.target.value })} placeholder="如:组织架构调整、岗位轮换" /> {error && ( {(error as any)?.response?.data?.error?.message || '操作失败,请重试'} )} 取消 {loading ? '保存中...' : '确认调动'} ) } 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 ( {/* 解聘类型选择 */} 离职/解聘类型 setTermType(e.target.value as any)}> 主动离职(快速办理) 协商解除 过错解除 非过错解除 裁员 到期不续签 违法解除 {termType === 'RESIGNATION' ? ( <> 员工主动离职,不涉及经济补偿金。离职日期可在未来(提前办理),到日期后状态自动变为离职。 员工 {employee.name} - {employee.department} 离职日期 setForm({ ...form, terminationDate: e.target.value })} /> 离职原因 setForm({ ...form, resignationReason: e.target.value })}> {reasons.map((r) => {r})} 社保公积金截止缴费年月 默认与离职日期同月,可手动修改 社保截止年月 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 || '操作失败,请重试'} )} 取消 {termType === 'RESIGNATION' ? (loading ? '提交中...' : '确认离职') : '进入解聘向导'} ) } /** 转正确认弹窗 */ 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 })} /> 转正薪资{originalSalary ? `(原薪资 ¥${originalSalary})` : ''} setForm({ ...form, regularSalary: e.target.value })} placeholder={originalSalary ? `不填则保持原薪资 ¥${originalSalary}` : '请输入转正薪资'} /> {originalSalary ? (salaryChanged ? ⚠ 转正薪资与原薪资不同,提交后将发起薪酬调整确认书签署 : '填写后将更新员工月工资并记录薪资变更') : '填写后将更新员工月工资并记录薪资变更'} {error && ( {(error as any)?.response?.data?.error?.message || '操作失败,请重试'} )} 取消 {loading ? '提交中...' : '确认转正'} ) } 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} 部门 * setForm({ ...form, department: e.target.value })}> 请选择部门 {deptOptions.map(d => ( {' '.repeat(d.level)}{d.label} ))} 新入职日期 * 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 })} /> 合同类型 { 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 => {t.label})} {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}} 试用期工资{form.probationMonths > 0 ? ' *' : ''} 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 || '操作失败,请重试'} )} 取消 {loading ? '提交中...' : '确认入职'} ) } 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({ queryKey: ['social-config-cities'], queryFn: async () => { const res = await socialInsuranceApi.cities() as any return res?.length ? res : ['北京'] }, }) 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: '', 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, 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="员工姓名" /> 部门 * { const opt = deptOptions.find(d => d.id === e.target.value) setForm({ ...form, departmentId: e.target.value, department: opt?.label || '' }) }}>请选择部门{deptOptions.map(d => {' '.repeat(d.level)}{d.label})} 职务/岗位 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 === '女' && ( 女性岗位类型 setForm({ ...form, femaleWorkerType: e.target.value as '' | 'CADRE' | 'WORKER' })}>未选择干部/管理岗工人/操作岗 )} 入职日期 * 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}),请确认是否重复录入 )} 参保城市 { const city = e.target.value setForm({ ...form, city }) const salary = Number(form.socialInsBase === '' ? form.monthlySalary : form.socialInsBase) || 0 const hfBase = Number(form.housingFundBase === '' ? form.monthlySalary : form.housingFundBase) || 0 if (salary > 0) { try { const res = await socialInsuranceApi.calculate(salary, city) if (res?.capped || res?.floored) { setForm((prev: any) => ({ ...prev, socialInsBase: String(res.actualBase) })) } } catch {} } if (hfBase > 0) { try { const res = await socialInsuranceApi.housingCalculate(hfBase, city) if (res?.capped || res?.floored) { setForm((prev: any) => ({ ...prev, housingFundBase: String(res.actualBase) })) } } catch {} } }}>{cities.map((c) => {c})} 学历 setForm({ ...form, education: e.target.value })}>未选择博士硕士本科大专高中其他 {/* 社保公积金 */} 社保公积金 {isNoSocialContract ? ( 劳务协议/实习协议人员不缴纳社保公积金 ) : ( 选定部门后自动带出账户,可手动调整。缴费基数默认与月工资一致。 )} {/* 账户选择(选部门后自动带出,可手动调整) */} {!isNoSocialContract && ( 社保账户 setManualSocialAccountId(e.target.value)} disabled={!form.departmentId}> {form.departmentId ? '未选择' : '请先选择部门'} {allSocialAccounts.map((a: any) => ( {a.name}({a.city}) ))} {autoAccounts?.socialStandard && manualSocialAccountId === autoAccounts.socialAccount?.id && ( 当前标准:基数 {autoAccounts.socialStandard.baseMin}~{autoAccounts.socialStandard.baseMax},养老 {autoAccounts.socialStandard.pensionOrg}%/{autoAccounts.socialStandard.pensionEmp}% )} 公积金账户 setManualHousingAccountId(e.target.value)} disabled={!form.departmentId}> {form.departmentId ? '未选择' : '请先选择部门'} {allHousingAccounts.map((a: any) => ( {a.name}({a.city}) ))} {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} /> {/* 合同信息 */} 合同信息 合同类型 { 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 => {t.label})} {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}} 试用期工资{form.probationMonths > 0 ? ' *' : ''} 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}} )} 取消 {loading ? '保存中...' : '保存'} ) }
选择"{termType === 'NEGOTIATED' ? '协商解除' : termType === 'FAULT' ? '过错解除' : termType === 'NONFAULT' ? '非过错解除' : termType === 'LAYOFF' ? '裁员' : termType === 'EXPIRED' ? '到期不续签' : '违法解除'}"将进入解聘向导,支持补偿金计算、合规检查、工作交接等完整流程。
点击"进入解聘向导"跳转到解聘补偿页面。