feat: 工作日历/考勤管理重构/AI人力报告/工作台员工分布/筛选优化/导入导出增强

- 新增工作日历页面(月历视图、事件管理、自定义事件)
- 考勤管理重构为6 Tab模块(班次/排班/每日出勤/月度报表/休假记录)
- AI顾问新增人力报告Tab,支持流式生成+Word导出
- 工作台总览新增员工分布统计(性别/年龄/学历/司龄饼图)+部门成本拆分
- 花名册/合同/解聘补偿新增部门和状态筛选
- 薪税管理新增工资表导入模板下载、银行代发CSV导出
- 社保公积金支持多公积金账户类型显示
- 数据导出新增花名册/解聘记录导出,中文文件名编码修复
- 数据导入新增模板下载(员工/增减员/工资表)+错误日志导出
- 移除工作台日历卡片(已迁移至独立工作日历页面)
- 新增20260728/20260729更新测试指导文档
This commit is contained in:
freedakgmail
2026-07-29 08:35:29 +08:00
parent d020d04a8a
commit fb36b10402
45 changed files with 3756 additions and 169 deletions
+73 -2
View File
@@ -4,6 +4,7 @@ import { toast } from 'sonner'
import { AlertTriangle, Check, ChevronRight, ChevronLeft, Shield, Info, Calculator, FileText, Printer, Trash2, List, Download, Plus, Edit, Send, CheckCircle, XCircle, Play, Ban, CheckCheck } from 'lucide-react'
import jsPDF from 'jspdf'
import api from '../lib/api'
import { useAuthStore } from '../store/authStore'
import Card from '../components/ui/Card'
import Button from '../components/ui/Button'
import { Input, Label, Select } from '../components/ui/Input'
@@ -115,6 +116,9 @@ export default function Termination() {
}>>([])
// 是否展开对比
const [showCompare, setShowCompare] = useState(false)
const [filterStatus, setFilterStatus] = useState('')
const [filterDepartment, setFilterDepartment] = useState('')
const [searchTerm, setSearchTerm] = useState('')
const { data: employees } = useQuery<RosterEmployee[]>({
queryKey: ['roster-for-termination'],
@@ -126,6 +130,14 @@ export default function Termination() {
const selectedEmployee = employees?.find((e) => e.id === employeeId)
const { data: departmentList } = useQuery<string[]>({
queryKey: ['roster-departments'],
queryFn: async () => {
const res = await api.get('/roster/departments') as any
return res.data || []
},
})
const { data: profile } = useQuery<EmployeeProfile>({
queryKey: ['employee-profile', employeeId],
queryFn: async () => {
@@ -239,9 +251,13 @@ export default function Termination() {
// 草稿列表
const { data: drafts, refetch: refetchDrafts } = useQuery({
queryKey: ['termination-drafts'],
queryKey: ['termination-drafts', filterStatus, filterDepartment, searchTerm],
queryFn: async () => {
const res = await api.get('/termination/drafts') as any
const params: any = {}
if (filterStatus) params.status = filterStatus
if (filterDepartment) params.department = filterDepartment
if (searchTerm) params.search = searchTerm
const res = await api.get('/termination/drafts', { params }) as any
return res.data
},
enabled: view === 'list',
@@ -624,6 +640,60 @@ export default function Termination() {
{/* 草稿列表视图 */}
{view === 'list' && (
<>
<div className="flex gap-2 flex-wrap items-center">
<Input
placeholder="搜索员工姓名或部门"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="!w-48"
/>
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="h-9 rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-700 shadow-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/10"
>
<option value=""></option>
<option value="DRAFT">稿</option>
<option value="PENDING_APPROVAL"></option>
<option value="APPROVED"></option>
<option value="EXECUTING"></option>
<option value="COMPLETED"></option>
<option value="CANCELLED"></option>
</select>
<select
value={filterDepartment}
onChange={(e) => setFilterDepartment(e.target.value)}
className="h-9 rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-700 shadow-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/10"
>
<option value=""></option>
{departmentList?.map((d: string) => <option key={d} value={d}>{d}</option>)}
</select>
{(searchTerm || filterStatus || filterDepartment) && (
<button onClick={() => { setSearchTerm(''); setFilterStatus(''); setFilterDepartment('') }} className="text-xs text-gray-500 hover:text-primary"></button>
)}
<Button variant="secondary" size="sm" onClick={async () => {
try {
const params = new URLSearchParams()
if (searchTerm) params.set('search', searchTerm)
if (filterStatus) params.set('status', filterStatus)
if (filterDepartment) params.set('department', filterDepartment)
const token = useAuthStore.getState().accessToken
const baseURL = import.meta.env.DEV ? 'http://localhost:3000/api/v1' : '/api/v1'
const res = await fetch(`${baseURL}/export/terminations?${params}`, { headers: { Authorization: `Bearer ${token}` } })
if (!res.ok) throw new Error('导出失败')
const blob = await res.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `解聘记录-${new Date().toISOString().slice(0, 10)}.xlsx`
a.click()
URL.revokeObjectURL(url)
} catch { toast.error('导出失败') }
}}>
<Download className="w-4 h-4 mr-1" />
</Button>
</div>
<Card>
{(!drafts || drafts.length === 0) ? (
<EmptyState
@@ -754,6 +824,7 @@ export default function Termination() {
</div>
)}
</Card>
</>
)}
{/* 详情视图 */}