feat: 工作日历/考勤管理重构/AI人力报告/工作台员工分布/筛选优化/导入导出增强
- 新增工作日历页面(月历视图、事件管理、自定义事件) - 考勤管理重构为6 Tab模块(班次/排班/每日出勤/月度报表/休假记录) - AI顾问新增人力报告Tab,支持流式生成+Word导出 - 工作台总览新增员工分布统计(性别/年龄/学历/司龄饼图)+部门成本拆分 - 花名册/合同/解聘补偿新增部门和状态筛选 - 薪税管理新增工资表导入模板下载、银行代发CSV导出 - 社保公积金支持多公积金账户类型显示 - 数据导出新增花名册/解聘记录导出,中文文件名编码修复 - 数据导入新增模板下载(员工/增减员/工资表)+错误日志导出 - 移除工作台日历卡片(已迁移至独立工作日历页面) - 新增20260728/20260729更新测试指导文档
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 详情视图 */}
|
||||
|
||||
Reference in New Issue
Block a user