import { QRCodeSVG } from "qrcode.react" import { useState, useRef } from "react" import { toast } from "sonner" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { attachmentApi, employeeApi, socialInsuranceApi } from '../../lib/api-services' import { useConfirm } from '../../hooks/useConfirm' import { copyToClipboard } from '../../lib/clipboard' import Card from "../../components/ui/Card" import Button from "../../components/ui/Button" import { Input, Label, Select } from "../../components/ui/Input" import { AlertTriangle, Paperclip, Trash2, Eye, Download, Copy } from "lucide-react" import { fmt } from "./shared" export default function BasicInfo({ profile, employeeId, attachments }: { profile: any; employeeId: string; attachments: any[] }) { const queryClient = useQueryClient() const confirm = useConfirm() const [editing, setEditing] = useState(false) const fileInputRef = useRef(null) const [fileType, setFileType] = useState<'ID_CARD' | 'BANK_CARD' | 'EDUCATION' | 'CERTIFICATE' | 'CONTRACT' | 'PHOTO' | 'OTHER'>('ID_CARD') const addAttachmentMutation = useMutation({ mutationFn: (data: any) => attachmentApi.add(data), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile', employeeId] }), }) const deleteAttachmentMutation = useMutation({ mutationFn: (id: string) => attachmentApi.remove(id), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile', employeeId] }), }) const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return const allowedExts = ['.pdf', '.jpg', '.jpeg', '.png', '.heic', '.gif', '.bmp', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.tiff', '.tif'] const ext = file.name.toLowerCase().substring(file.name.lastIndexOf('.')) if (!allowedExts.includes(ext)) { toast.error('不支持的文件格式,支持 PDF、图片、Word、Excel 等常见格式') return } if (file.size > 10 * 1024 * 1024) { toast.error('文件过大,请上传小于 10MB 的文件') return } const reader = new FileReader() reader.onload = (event) => { const fileUrl = event.target?.result as string addAttachmentMutation.mutate({ employeeId, fileName: file.name, fileType, fileUrl, fileSize: file.size }) } reader.readAsDataURL(file) } const fileTypeLabels: Record = { ID_CARD: '身份证', BANK_CARD: '银行卡', EDUCATION: '学历证书', CERTIFICATE: '职业资格证书', CONTRACT: '合同扫描件', PHOTO: '员工照片', OTHER: '其他' } const fileTypeColors: Record = { ID_CARD: 'bg-blue-50 text-blue-600', BANK_CARD: 'bg-green-50 text-safe', EDUCATION: 'bg-amber-50 text-amber-600', CERTIFICATE: 'bg-purple-50 text-purple-600', CONTRACT: 'bg-cyan-50 text-cyan-600', PHOTO: 'bg-pink-50 text-pink-600', OTHER: 'bg-gray-100 text-gray-500' } const formatSize = (bytes: number) => { if (!bytes) return '-' if (bytes < 1024) return `${bytes}B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB` return `${(bytes / 1024 / 1024).toFixed(1)}MB` } const [form, setForm] = useState({ department: profile.department || '', position: profile.position || '', gender: profile.gender || '男', femaleWorkerType: profile.femaleWorkerType || '', phone: profile.phone || '', hireDate: profile.hireDate?.toString().slice(0, 10) || '', monthlySalary: profile.monthlySalary || '', emergencyContact: profile.emergencyContact || '', emergencyPhone: profile.emergencyPhone || '', address: profile.address || '', bankName: profile.bankName || '', bankAccount: profile.bankAccount || '', isPregnant: profile.isPregnant || false, isInMedicalPeriod: profile.isInMedicalPeriod || false, isWorkInjured: profile.isWorkInjured || false, socialInsBase: profile.socialInsBase ?? '', housingFundBase: profile.housingFundBase ?? '', specialDeduction: profile.specialDeduction ?? 0, city: profile.city || '', education: profile.education || '', cityChangeReason: '', }) const updateMutation = useMutation({ mutationFn: (data: any) => employeeApi.update(profile.id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }) queryClient.invalidateQueries({ queryKey: ['roster'] }) setEditing(false) }, onError: (err: any) => { const details = err?.response?.data?.error?.details if (details?.length > 0) { toast.error(details.map((d: any) => `${d.path}: ${d.message}`).join(';')) } else { toast.error(err?.response?.data?.error?.message || '保存失败') } }, }) // 查询社保费用明细(按险种分别计算) const { data: socialDetail } = useQuery({ queryKey: ['social-calc', profile.id, profile.socialInsBase, profile.city], queryFn: async () => { if (!profile.socialInsBase || !profile.city) return null try { return await socialInsuranceApi.calculate(Number(profile.socialInsBase), profile.city) } catch { return null } }, enabled: !editing && !!profile.socialInsBase && !!profile.city, }) const handleSave = async () => { if (form.city !== (profile.city || '') && !form.cityChangeReason.trim()) { toast.error('参保城市变更必须填写变更原因') return } // 入职日期变更联动状态确认 const originalHireDate = profile.hireDate?.toString().slice(0, 10) || '' let statusUpdate: string | undefined if (form.hireDate && form.hireDate !== originalHireDate) { const today = new Date() today.setHours(0, 0, 0, 0) const newHireDate = new Date(form.hireDate) const currentStatus = profile.status || 'ACTIVE' let newStatus = currentStatus if (newHireDate > today) { newStatus = 'PENDING_ONBOARD' } else { newStatus = 'ACTIVE' } if (newStatus !== currentStatus) { const statusLabel = newStatus === 'PENDING_ONBOARD' ? '待入职' : '在职' const currentLabel = currentStatus === 'PENDING_ONBOARD' ? '待入职' : currentStatus === 'ACTIVE' ? '在职' : currentStatus const confirmed = await confirm({ title: '入职日期变更确认', message: `入职日期从 ${originalHireDate} 变更为 ${form.hireDate},员工状态将从"${currentLabel}"变更为"${statusLabel}",是否继续?`, }) if (!confirmed) return statusUpdate = newStatus } } const data: any = { department: form.department, gender: form.gender, femaleWorkerType: form.femaleWorkerType || undefined, phone: form.phone || undefined, hireDate: new Date(form.hireDate).toISOString(), monthlySalary: String(form.monthlySalary), emergencyContact: form.emergencyContact || undefined, emergencyPhone: form.emergencyPhone || undefined, address: form.address || undefined, bankName: form.bankName || undefined, bankAccount: form.bankAccount || undefined, isPregnant: form.isPregnant, isInMedicalPeriod: form.isInMedicalPeriod, isWorkInjured: form.isWorkInjured, socialInsBase: form.socialInsBase === '' ? null : Number(form.socialInsBase), housingFundBase: form.housingFundBase === '' ? null : Number(form.housingFundBase), specialDeduction: Number(form.specialDeduction) || 0, city: form.city || undefined, education: form.education || undefined, position: form.position || undefined, cityChangeReason: form.city !== profile.city ? form.cityChangeReason || undefined : undefined, } if (statusUpdate) data.status = statusUpdate updateMutation.mutate(data) } const personalFields = [ { label: '姓名', value: profile.name }, { label: '部门', value: profile.department }, { label: '性别', value: profile.gender || '未填写' }, ...(profile.gender === '女' ? [{ label: '女性岗位', value: profile.femaleWorkerType === 'CADRE' ? '干部/管理岗' : profile.femaleWorkerType === 'WORKER' ? '工人/操作岗' : '未填写' }] : []), { label: '证件号码', value: profile.idCardNumber || '未填写' }, { label: '手机号', value: profile.phone || '未填写' }, { label: '学历', value: profile.education || '未填写' }, { label: '职务/岗位', value: profile.position || '未填写' }, { label: '入职日期', value: profile.hireDate?.toString().slice(0, 10) }, { label: '状态', value: profile.status === 'ACTIVE' ? '在职' : '离职' }, ...(profile.retirementDaysLeft != null ? (() => { if (profile.retirementDaysLeft <= 0) return [{ label: '距退休', value: '已到退休年龄' }] if (profile.birthDate) { const bd = new Date(profile.birthDate) const gender = profile.gender || '男' const fwt = profile.femaleWorkerType || null const baseAge = gender === '男' ? 60 : (fwt === 'WORKER' ? 50 : 55) const delayInterval = gender === '男' ? 4 : (fwt === 'WORKER' ? 2 : 4) const maxDelay = gender === '男' ? 36 : (fwt === 'WORKER' ? 60 : 36) const baseRetireDate = new Date(bd) baseRetireDate.setFullYear(baseRetireDate.getFullYear() + baseAge) const reformStart = new Date(2025, 0, 1) const monthsSince = Math.max(0, (baseRetireDate.getFullYear() - reformStart.getFullYear()) * 12 + (baseRetireDate.getMonth() - reformStart.getMonth())) const delayMonths = Math.min(maxDelay, Math.floor(monthsSince / delayInterval)) const retireDate = new Date(baseRetireDate) retireDate.setMonth(retireDate.getMonth() + delayMonths) return [{ label: '退休日期', value: `${retireDate.getFullYear()}年${retireDate.getMonth() + 1}月${retireDate.getDate()}日` }] } return [{ label: '距退休', value: `${profile.retirementDaysLeft}天` }] })() : []), ...(profile.status !== 'ACTIVE' && profile.terminations && profile.terminations.length > 0 ? [{ label: '离职日期', value: profile.terminations .map((t: any) => t.terminationDate?.toString().slice(0, 10)) .filter(Boolean) .sort() .reverse()[0] || '未记录' }] : []), ] const salaryFields = [ { label: '月工资', value: `¥${fmt(profile.monthlySalary)}` }, { label: '紧急联系人', value: profile.emergencyContact || '未填写' }, { label: '紧急联系电话', value: profile.emergencyPhone || '未填写' }, { label: '住址', value: profile.address || '未填写' }, { label: '开户行', value: profile.bankName || '未填写' }, { label: '银行账号', value: profile.bankAccount || '未填写' }, ] const special = [ { label: '孕期', value: profile.isPregnant }, { label: '医疗期', value: profile.isInMedicalPeriod }, { label: '工伤', value: profile.isWorkInjured }, ] return (

基本信息

{!editing ? ( ) : (
)}
{!editing ? (

个人信息

{personalFields.map((f) => (
{f.label} {f.value} {f.label === '证件号码' && profile.idCardNumber && ( )}
))}

薪酬与银行

{salaryFields.map((f) => (
{f.label} {f.value}
))}
{profile.socialInsBase != null && (

社保与公积金明细

社保缴纳基数 ¥{fmt(profile.socialInsBase)}
公积金缴纳基数 ¥{fmt(profile.housingFundBase || 0)}
个人养老(8%) ¥{fmt((profile.socialInsBase || 0) * 0.08)}
单位养老(16%) ¥{fmt((profile.socialInsBase || 0) * 0.16)}
个人医疗(2%) ¥{fmt((profile.socialInsBase || 0) * 0.02)}
单位医疗(8%) ¥{fmt((profile.socialInsBase || 0) * 0.08)}
个人失业(0.5%) ¥{fmt((profile.socialInsBase || 0) * 0.005)}
单位失业(0.5%) ¥{fmt((profile.socialInsBase || 0) * 0.005)}
单位工伤(0.2%) ¥{fmt((profile.socialInsBase || 0) * 0.002)}
单位生育(0.8%) ¥{fmt((profile.socialInsBase || 0) * 0.008)}
个人公积金(7%) ¥{fmt((profile.housingFundBase || 0) * 0.07)}
单位公积金(7%) ¥{fmt((profile.housingFundBase || 0) * 0.07)}
个人合计 ¥{fmt((profile.socialInsBase || 0) * 0.105 + (profile.housingFundBase || 0) * 0.07)}
单位合计 ¥{fmt((profile.socialInsBase || 0) * 0.255 + (profile.housingFundBase || 0) * 0.07)}
注:比例为通用参考值,实际比例以当地政策为准
)}
) : (
setForm({ ...form, department: e.target.value })} />
{form.gender === '女' && (
)}
setForm({ ...form, phone: e.target.value })} placeholder="选填" maxLength={11} />
setForm({ ...form, position: e.target.value })} placeholder="如:前端工程师" />
setForm({ ...form, hireDate: e.target.value })} />
setForm({ ...form, monthlySalary: Number(e.target.value) })} />
setForm({ ...form, emergencyContact: e.target.value })} placeholder="选填" />
setForm({ ...form, emergencyPhone: e.target.value })} placeholder="选填" />
setForm({ ...form, address: e.target.value })} placeholder="选填" />
setForm({ ...form, bankName: e.target.value })} placeholder="选填" />
setForm({ ...form, bankAccount: e.target.value })} placeholder="选填" />
)} {/* 薪税信息 */}

薪税信息

{!editing ? (
参保城市 {profile.city || '未设置'}
社保缴费基数 {profile.socialInsBase ? `¥${fmt(profile.socialInsBase)}` : '未设置'}
公积金缴费基数 {profile.housingFundBase ? `¥${fmt(profile.housingFundBase)}` : '未设置'}
专项附加扣除 {profile.specialDeduction ? `¥${fmt(profile.specialDeduction)}/月` : '¥0.00/月'}
{socialDetail?.items?.length > 0 && (
社保费用明细(按险种分别计算)
{socialDetail.items.map((item: any) => (
{item.name}
企业 ¥{fmt(item.orgAmount)}({item.orgRate}%)
个人 ¥{fmt(item.empAmount)}({item.empRate}%)
))}
{socialDetail.capped &&
提示:社保基数已封顶(上限 ¥{fmt(socialDetail.actualBase)})
} {socialDetail.floored &&
提示:社保基数已保底(下限 ¥{fmt(socialDetail.actualBase)})
} {socialDetail.medicalBase && socialDetail.medicalBase !== socialDetail.actualBase && (
医保基数:¥{fmt(socialDetail.medicalBase)}(与养老基数不同)
)}
)}
) : (
setForm({ ...form, city: e.target.value })} />
setForm({ ...form, socialInsBase: e.target.value })} onFocus={(e) => e.target.select()} />
setForm({ ...form, housingFundBase: e.target.value })} onFocus={(e) => e.target.select()} />
setForm({ ...form, specialDeduction: Number(e.target.value) || 0 })} />
{form.city !== (profile.city || '') && (
setForm({ ...form, cityChangeReason: e.target.value })} />
)}
)}

社保/公积金基数按上年度月均工资核定,每年7月调整。专项附加扣除由员工在portal端填报,无则为0。

{!editing && (!profile.socialInsBase || !profile.housingFundBase) && (
该员工{!profile.socialInsBase ? '社保' : '公积金'}基数未设置,发薪时社保/公积金将按0计算。请点击右上角「编辑」填写缴费基数。
)}
{/* 特殊状态 */}

特殊状态

{!editing ? (
{special.map((s) => ( {s.label}:{s.value ? '是' : '否'} ))}
) : (
)} {(profile.isPregnant || profile.isInMedicalPeriod || profile.isWorkInjured) && !editing && (

⚠️ 该员工处于特殊保护期,解聘操作将触发法律风险预警

)}
{/* 员工端二维码 */} {!editing && profile.phone && (

员工端入口

员工扫码进入员工端,使用手机号登录

可查看工资条、合同信息、确认签署

链接:{window.location.origin}/portal/login

)} {/* 附件管理 */}

附件管理({attachments?.length || 0}个)

{!editing && (
)}
{!editing && attachments?.length ? (
{attachments.map((att) => (
{att.fileName}
{fileTypeLabels[att.fileType] || att.fileType} {formatSize(att.fileSize)}
))}
) : !editing ?
暂无附件
: null}
) }