import { QRCodeSVG } from "qrcode.react" import { useUnsavedChanges } from "../../hooks/useUnsavedChanges" 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 { fmt } from "./shared" export default function BasicInfo({ profile, employeeId, attachments }: { profile: any; employeeId: string; attachments: any[] }) { const queryClient = useQueryClient() const [editing, setEditing] = useState(false) const fileInputRef = useRef(null) const [fileType, setFileType] = useState<'ID_CARD' | 'BANK_CARD' | 'EDUCATION' | 'OTHER'>('ID_CARD') const addAttachmentMutation = useMutation({ mutationFn: (data: any) => api.post('/attachments', data), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile', employeeId] }), }) const deleteAttachmentMutation = useMutation({ mutationFn: (id: string) => api.delete(`/attachments/${id}`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile', employeeId] }), }) const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return const allowedTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png', 'image/heic'] const allowedExts = ['.pdf', '.jpg', '.jpeg', '.png', '.heic'] const ext = file.name.toLowerCase().substring(file.name.lastIndexOf('.')) if (!allowedTypes.includes(file.type) && !allowedExts.includes(ext)) { toast.error('不支持的文件格式,请上传 PDF、JPG、PNG 或 HEIC 格式') 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: '学历证书', 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', 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 || '', 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 || '', cityChangeReason: '', }) const updateMutation = useMutation({ mutationFn: (data: any) => api.put(`/employees/${profile.id}`, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }) queryClient.invalidateQueries({ queryKey: ['roster'] }) setEditing(false) }, }) const handleSave = () => { if (form.city !== (profile.city || '') && !form.cityChangeReason.trim()) { toast.error('参保城市变更必须填写变更原因') return } 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, cityChangeReason: form.city !== profile.city ? form.cityChangeReason || undefined : undefined, } 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.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}
))}

薪酬与银行

{salaryFields.map((f) => (
{f.label} {f.value}
))}
) : (
setForm({ ...form, department: e.target.value })} />
{form.gender === '女' && (
)}
setForm({ ...form, phone: e.target.value })} placeholder="选填" maxLength={11} />
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/月'}
) : (
setForm({ ...form, city: e.target.value })} />
setForm({ ...form, socialInsBase: e.target.value })} />
setForm({ ...form, housingFundBase: e.target.value })} />
setForm({ ...form, specialDeduction: Number(e.target.value) || 0 })} />
{form.city !== (profile.city || '') && (
setForm({ ...form, cityChangeReason: e.target.value })} />
)}
)}

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