feat: TurboHR 14项优化与功能增强
- #1 Dashboard风险提醒增加立刻办理按钮 - #2 Calendar月份选择器改为input month - #3 Termination增加7种解聘原因法律依据和操作步骤 - #5 合同审查支持PDF TXT格式 - #6 AI合同审查prompt优化为具体修改建议 - #7 知识库添加更新机制说明 - #9 SpecialStatus员工选择改用all-lite接口 - #10 Termination增加详细法律条款引用 - #11 Money发薪批次增加社保公积金合计列 - #12 EmployeeAttachment扩展文件类型 - #13 花名册增加女职工干部工人选项加退休提醒 - #14 新增公司备用文件上传模块
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import { Building2, Upload, Trash2, FileText, AlertCircle, Calendar } from 'lucide-react'
|
||||
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 EmptyState from '../components/ui/EmptyState'
|
||||
|
||||
const FILE_TYPES = [
|
||||
{ value: 'BUSINESS_LICENSE', label: '营业执照' },
|
||||
{ value: 'WORK_HOURS', label: '工时备案' },
|
||||
{ value: 'HR_POLICY', label: '制度文件' },
|
||||
{ value: 'LABOR_CONTRACT_TEMPLATE', label: '合同模板' },
|
||||
{ value: 'OTHER', label: '其他' },
|
||||
]
|
||||
|
||||
const fileTypeLabels: Record<string, string> = Object.fromEntries(FILE_TYPES.map(t => [t.value, t.label]))
|
||||
|
||||
export default function CompanyFiles() {
|
||||
const queryClient = useQueryClient()
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [fileType, setFileType] = useState('BUSINESS_LICENSE')
|
||||
const [remark, setRemark] = useState('')
|
||||
const [expiryDate, setExpiryDate] = useState('')
|
||||
const [filterType, setFilterType] = useState('')
|
||||
|
||||
const { data: files, isLoading } = useQuery<any[]>({
|
||||
queryKey: ['company-files', filterType],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/company-files', { params: filterType ? { fileType: filterType } : {} }) as any
|
||||
return res.data || []
|
||||
},
|
||||
})
|
||||
|
||||
const addMutation = useMutation({
|
||||
mutationFn: (data: any) => api.post('/company-files', data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['company-files'] })
|
||||
toast.success('文件上传成功')
|
||||
setRemark('')
|
||||
setExpiryDate('')
|
||||
},
|
||||
onError: () => toast.error('上传失败'),
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => api.delete(`/company-files/${id}`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['company-files'] })
|
||||
toast.success('已删除')
|
||||
},
|
||||
})
|
||||
|
||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
toast.error('文件不能超过 10MB')
|
||||
return
|
||||
}
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
addMutation.mutate({
|
||||
fileName: file.name,
|
||||
fileType,
|
||||
fileUrl: reader.result as string,
|
||||
fileSize: file.size,
|
||||
remark: remark || undefined,
|
||||
expiryDate: expiryDate || undefined,
|
||||
})
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
const fmtSize = (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes}B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)}MB`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 className="h-5 w-5 text-primary" />
|
||||
<h1 className="text-lg font-semibold">公司备用文件</h1>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="text-sm text-gray-500">上传营业执照、工时备案文件、公司制度文件、合同模板等公司级文件</div>
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
<div>
|
||||
<Label>文件类型</Label>
|
||||
<Select value={fileType} onChange={(e) => setFileType(e.target.value)} className="w-36">
|
||||
{FILE_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>备注(选填)</Label>
|
||||
<Input value={remark} onChange={(e) => setRemark(e.target.value)} placeholder="如:2024年营业执照" className="w-48" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>有效期(选填)</Label>
|
||||
<Input type="date" value={expiryDate} onChange={(e) => setExpiryDate(e.target.value)} className="w-40" />
|
||||
</div>
|
||||
<input ref={fileInputRef} type="file" className="hidden" onChange={handleFileUpload} />
|
||||
<Button onClick={() => fileInputRef.current?.click()} disabled={addMutation.isPending}>
|
||||
<Upload className="w-4 h-4 mr-1" />
|
||||
{addMutation.isPending ? '上传中...' : '上传文件'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-medium">文件列表</h2>
|
||||
<Select value={filterType} onChange={(e) => setFilterType(e.target.value)} className="w-32 text-xs">
|
||||
<option value="">全部类型</option>
|
||||
{FILE_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
|
||||
</Select>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-gray-500">加载中...</div>
|
||||
) : !files || files.length === 0 ? (
|
||||
<EmptyState title="暂无文件" description="点击上方上传按钮添加公司文件" />
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{files.map((f: any) => {
|
||||
const isExpired = f.expiryDate && new Date(f.expiryDate) < new Date()
|
||||
const isExpiringSoon = f.expiryDate && !isExpired && (new Date(f.expiryDate).getTime() - new Date().getTime()) < 30 * 24 * 60 * 60 * 1000
|
||||
return (
|
||||
<div key={f.id} className="flex items-center gap-3 p-3 rounded-md border hover:bg-gray-50">
|
||||
<FileText className="w-5 h-5 text-gray-400 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium truncate">{f.fileName}</span>
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-100 text-gray-600">{fileTypeLabels[f.fileType] || f.fileType}</span>
|
||||
{isExpired && <span className="text-xs px-1.5 py-0.5 rounded bg-red-50 text-danger flex items-center gap-0.5"><AlertCircle className="w-3 h-3" />已过期</span>}
|
||||
{isExpiringSoon && <span className="text-xs px-1.5 py-0.5 rounded bg-amber-50 text-warning flex items-center gap-0.5"><Calendar className="w-3 h-3" />即将到期</span>}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-0.5">
|
||||
{fmtSize(f.fileSize)}
|
||||
{f.remark && <span className="ml-2">· {f.remark}</span>}
|
||||
{f.expiryDate && <span className="ml-2">· 有效期至 {f.expiryDate.slice(0, 10)}</span>}
|
||||
<span className="ml-2">· {new Date(f.createdAt).toLocaleDateString('zh-CN')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href={f.fileUrl} download={f.fileName} className="text-xs text-primary hover:underline flex items-center gap-1">
|
||||
<FileText className="w-3.5 h-3.5" />下载
|
||||
</a>
|
||||
<button
|
||||
onClick={() => deleteMutation.mutate(f.id)}
|
||||
className="p-1.5 rounded hover:bg-red-50 text-gray-400 hover:text-danger"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user