feat: 完成23项系统优化 - 花名册社保状态列/身份证复制/附件类型扩展, 用工办理姓名检索/直接提交/文书查看/批量证明, 风险中心跳转筛选+批量处理, 日历7/15/35天分组+逾期统计, 考勤单条编辑+按人导出, 工资条查看状态+工资流水导出, 辞职申请附件上传, 交接清单PDF下载, 操作完成下一步引导, 休假审批入口, 人效成本分部门

This commit is contained in:
freedakgmail
2026-08-04 22:55:03 +08:00
parent 338af5eee9
commit 5604d02de9
41 changed files with 2104 additions and 482 deletions
+4 -4
View File
@@ -10,7 +10,7 @@ import { Paperclip, Trash2, Eye, Download } from "lucide-react"
export default function AttachmentInfo({ employeeId, attachments }: { employeeId: string; attachments: any[] }) {
const queryClient = useQueryClient()
const fileInputRef = useRef<HTMLInputElement>(null)
const [fileType, setFileType] = useState<'ID_CARD' | 'BANK_CARD' | 'EDUCATION' | 'OTHER'>('ID_CARD')
const [fileType, setFileType] = useState<'ID_CARD' | 'BANK_CARD' | 'EDUCATION' | 'CERTIFICATE' | 'CONTRACT' | 'PHOTO' | 'OTHER'>('ID_CARD')
const addAttachmentMutation = useMutation({
mutationFn: (data: any) => attachmentApi.add(data),
@@ -49,8 +49,8 @@ export default function AttachmentInfo({ employeeId, attachments }: { employeeId
reader.readAsDataURL(file)
}
const fileTypeLabels: Record<string, string> = { ID_CARD: '身份证', BANK_CARD: '银行卡', EDUCATION: '学历证书', OTHER: '其他' }
const fileTypeColors: Record<string, string> = { 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 fileTypeLabels: Record<string, string> = { ID_CARD: '身份证', BANK_CARD: '银行卡', EDUCATION: '学历证书', CERTIFICATE: '职业资格证书', CONTRACT: '合同扫描件', PHOTO: '员工照片', OTHER: '其他' }
const fileTypeColors: Record<string, string> = { 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 '-'
@@ -65,7 +65,7 @@ export default function AttachmentInfo({ employeeId, attachments }: { employeeId
<Card>
<div className="flex gap-2 mb-3 flex-nowrap items-center">
<Select value={fileType} onChange={(e) => setFileType(e.target.value as any)} className="text-xs !w-32 shrink-0">
<option value="ID_CARD"></option><option value="BANK_CARD"></option><option value="EDUCATION"></option><option value="OTHER"></option>
<option value="ID_CARD"></option><option value="BANK_CARD"></option><option value="EDUCATION"></option><option value="CERTIFICATE"></option><option value="CONTRACT"></option><option value="PHOTO"></option><option value="OTHER"></option>
</Select>
<input ref={fileInputRef} type="file" className="hidden" onChange={handleFileUpload} />
<Button size="sm" variant="secondary" onClick={() => fileInputRef.current?.click()} disabled={addAttachmentMutation.isPending} className="shrink-0 whitespace-nowrap">
+86 -6
View File
@@ -6,14 +6,14 @@ import { attachmentApi, employeeApi } from '../../lib/api-services'
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 } from "lucide-react"
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 [editing, setEditing] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const [fileType, setFileType] = useState<'ID_CARD' | 'BANK_CARD' | 'EDUCATION' | 'OTHER'>('ID_CARD')
const [fileType, setFileType] = useState<'ID_CARD' | 'BANK_CARD' | 'EDUCATION' | 'CERTIFICATE' | 'CONTRACT' | 'PHOTO' | 'OTHER'>('ID_CARD')
const addAttachmentMutation = useMutation({
mutationFn: (data: any) => attachmentApi.add(data),
@@ -46,8 +46,8 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
reader.readAsDataURL(file)
}
const fileTypeLabels: Record<string, string> = { ID_CARD: '身份证', BANK_CARD: '银行卡', EDUCATION: '学历证书', OTHER: '其他' }
const fileTypeColors: Record<string, string> = { 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 fileTypeLabels: Record<string, string> = { ID_CARD: '身份证', BANK_CARD: '银行卡', EDUCATION: '学历证书', CERTIFICATE: '职业资格证书', CONTRACT: '合同扫描件', PHOTO: '员工照片', OTHER: '其他' }
const fileTypeColors: Record<string, string> = { 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`
@@ -195,7 +195,23 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
{personalFields.map((f) => (
<div key={f.label} className="flex justify-between border-b pb-1.5 text-xs">
<span className="text-gray-500 shrink-0">{f.label}</span>
<span className="font-medium text-right truncate ml-2">{f.value}</span>
<span className="font-medium text-right truncate ml-2 flex items-center gap-1">
{f.value}
{f.label === '身份证号' && profile.idCardNumber && (
<button
type="button"
className="text-gray-400 hover:text-primary transition-colors shrink-0"
title="复制身份证号"
onClick={() => {
navigator.clipboard.writeText(profile.idCardNumber)
.then(() => toast.success('已复制身份证号'))
.catch(() => toast.error('复制失败'))
}}
>
<Copy className="w-3 h-3" />
</button>
)}
</span>
</div>
))}
</div>
@@ -211,6 +227,70 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
))}
</div>
</div>
{profile.socialInsBase != null && (
<div className="pt-3 border-t">
<h3 className="text-xs font-medium text-gray-600 mb-2"></h3>
<div className="grid md:grid-cols-2 gap-x-6 gap-y-1 text-xs">
<div className="flex justify-between border-b pb-1">
<span className="text-gray-500"></span>
<span className="font-medium">¥{fmt(profile.socialInsBase)}</span>
</div>
<div className="flex justify-between border-b pb-1">
<span className="text-gray-500"></span>
<span className="font-medium">¥{fmt(profile.housingFundBase || 0)}</span>
</div>
<div className="flex justify-between border-b pb-1">
<span className="text-gray-500">8%</span>
<span>¥{fmt((profile.socialInsBase || 0) * 0.08)}</span>
</div>
<div className="flex justify-between border-b pb-1">
<span className="text-gray-500">16%</span>
<span>¥{fmt((profile.socialInsBase || 0) * 0.16)}</span>
</div>
<div className="flex justify-between border-b pb-1">
<span className="text-gray-500">2%</span>
<span>¥{fmt((profile.socialInsBase || 0) * 0.02)}</span>
</div>
<div className="flex justify-between border-b pb-1">
<span className="text-gray-500">8%</span>
<span>¥{fmt((profile.socialInsBase || 0) * 0.08)}</span>
</div>
<div className="flex justify-between border-b pb-1">
<span className="text-gray-500">0.5%</span>
<span>¥{fmt((profile.socialInsBase || 0) * 0.005)}</span>
</div>
<div className="flex justify-between border-b pb-1">
<span className="text-gray-500">0.5%</span>
<span>¥{fmt((profile.socialInsBase || 0) * 0.005)}</span>
</div>
<div className="flex justify-between border-b pb-1">
<span className="text-gray-500">0.2%</span>
<span>¥{fmt((profile.socialInsBase || 0) * 0.002)}</span>
</div>
<div className="flex justify-between border-b pb-1">
<span className="text-gray-500">0.8%</span>
<span>¥{fmt((profile.socialInsBase || 0) * 0.008)}</span>
</div>
<div className="flex justify-between border-b pb-1">
<span className="text-gray-500">7%</span>
<span>¥{fmt((profile.housingFundBase || 0) * 0.07)}</span>
</div>
<div className="flex justify-between border-b pb-1">
<span className="text-gray-500">7%</span>
<span>¥{fmt((profile.housingFundBase || 0) * 0.07)}</span>
</div>
<div className="flex justify-between font-medium pt-1">
<span></span>
<span className="text-primary">¥{fmt((profile.socialInsBase || 0) * 0.105 + (profile.housingFundBase || 0) * 0.07)}</span>
</div>
<div className="flex justify-between font-medium pt-1">
<span></span>
<span className="text-primary">¥{fmt((profile.socialInsBase || 0) * 0.255 + (profile.housingFundBase || 0) * 0.07)}</span>
</div>
</div>
<div className="text-xs text-gray-400 mt-1"></div>
</div>
)}
</div>
) : (
<div className="grid md:grid-cols-2 gap-3">
@@ -358,7 +438,7 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
{!editing && (
<div className="flex gap-2 items-center">
<Select value={fileType} onChange={(e) => setFileType(e.target.value as any)} className="text-xs !w-28">
<option value="ID_CARD"></option><option value="BANK_CARD"></option><option value="EDUCATION"></option><option value="OTHER"></option>
<option value="ID_CARD"></option><option value="BANK_CARD"></option><option value="EDUCATION"></option><option value="CERTIFICATE"></option><option value="CONTRACT"></option><option value="PHOTO"></option><option value="OTHER"></option>
</Select>
<input ref={fileInputRef} type="file" className="hidden" onChange={handleFileUpload} />
<Button size="sm" variant="secondary" onClick={() => fileInputRef.current?.click()} disabled={addAttachmentMutation.isPending} className="shrink-0 whitespace-nowrap">