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
+86 -11
View File
@@ -1,8 +1,9 @@
import { useState } from 'react'
import { usePageSize } from '../../hooks/usePageSize'
import { toast } from 'sonner'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useConfirm } from '../../hooks/useConfirm'
import { Calculator, Check, Layers, Settings as X } from 'lucide-react'
import { Calculator, Check, Layers, Settings as X, Download, Eye } from 'lucide-react'
import PageGuide from '../../components/ui/PageGuide'
import { payrollApi } from '../../lib/api-services'
import Card from '../../components/ui/Card'
@@ -17,8 +18,8 @@ export function PayslipManager() {
const queryClient = useQueryClient()
const confirm = useConfirm()
const [month, setMonth] = useState(new Date().toISOString().slice(0, 7))
const pageSize = usePageSize()
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [showTaxPreview, setShowTaxPreview] = useState(false)
const [previewData, setPreviewData] = useState({
baseSalary: 0,
@@ -67,20 +68,86 @@ export function PayslipManager() {
return (
<div className="space-y-3">
<PageGuide>
PDF
PDF
</PageGuide>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Input type="month" value={month} onChange={(e) => setMonth(e.target.value)} className="w-48" />
{payslips && payslips.length > 0 && (
<div className="flex gap-2 text-xs">
<span className="px-2 py-0.5 rounded bg-gray-100 text-gray-600"> {payslips.length} </span>
<span className="px-2 py-0.5 rounded bg-green-50 text-safe"> {confirmedCount}</span>
<span className="px-2 py-0.5 rounded bg-amber-50 text-warning"> {unconfirmedCount}</span>
<div className="flex items-center gap-2 text-xs flex-nowrap">
<span className="px-2 py-0.5 rounded bg-gray-100 text-gray-600 whitespace-nowrap"> {payslips.length} </span>
<span className="px-2 py-0.5 rounded bg-green-50 text-safe whitespace-nowrap"> {confirmedCount}</span>
<span className="px-2 py-0.5 rounded bg-amber-50 text-warning whitespace-nowrap"> {unconfirmedCount}</span>
</div>
)}
</div>
<div className="flex gap-2">
<Button
variant="secondary"
onClick={() => {
if (!payslips || payslips.length === 0) {
toast.error('暂无工资条可导出')
return
}
const headers = ['员工', '部门', '基本工资', '加班费', '津贴', '奖金', '扣款', '应发合计', '个税', '实发', '确认状态']
const rows = payslips.map((p: any) => [
p.employee?.name || '',
p.employee?.department || '',
p.baseSalary || 0,
p.overtimePay || 0,
p.allowance || 0,
p.bonus || 0,
p.deduction || 0,
p.totalPay || 0,
p.tax || 0,
p.netPay || 0,
p.confirmedAt ? '已确认' : '未确认',
])
const csv = [headers, ...rows].map(r => r.join(',')).join('\n')
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `工资表-${month}.csv`
a.click()
URL.revokeObjectURL(url)
toast.success('已导出工资表')
}}
>
<Download className="w-4 h-4 mr-1" />
</Button>
<Button
variant="secondary"
onClick={() => {
if (!payslips || payslips.length === 0) {
toast.error('暂无工资条可导出')
return
}
const headers = ['序号', '收款人姓名', '收款账号', '开户行', '金额', '用途', '备注']
const rows = payslips.map((p: any, i: number) => [
i + 1,
p.employee?.name || '',
p.employee?.bankAccount || '',
p.employee?.bankName || '',
(p.netPay || 0).toFixed(2),
`${month}月工资`,
'',
])
const csv = [headers, ...rows].map(r => r.join(',')).join('\n')
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `工资流水-${month}.csv`
a.click()
URL.revokeObjectURL(url)
toast.success('已导出工资流水')
}}
>
<Download className="w-4 h-4 mr-1" />
</Button>
<Button
onClick={() => setShowTaxPreview(true)}
>
@@ -107,7 +174,6 @@ export function PayslipManager() {
<Card><div className="text-center py-8 text-gray-500"></div></Card>
) : (
<Card>
<Pagination page={page} pageSize={pageSize} total={payslips.length} onPageChange={setPage} onPageSizeChange={(s) => { setPageSize(s); setPage(1) }} />
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
@@ -144,8 +210,16 @@ export function PayslipManager() {
<span className="inline-flex items-center gap-1 text-safe text-xs">
<Check className="w-3 h-3" />
</span>
) : p.viewedAt ? (
<span className="inline-flex items-center gap-1 text-blue-600 text-xs" title={`查看于 ${new Date(p.viewedAt).toLocaleString('zh-CN')}`}>
<Eye className="w-3 h-3" />
</span>
) : p.publishedAt ? (
<span className="inline-flex items-center gap-1 text-gray-400 text-xs">
<Check className="w-3 h-3" />
</span>
) : (
<span className="text-warning text-xs"></span>
<span className="text-gray-400 text-xs"></span>
)}
</td>
<td className="py-2 px-2">
@@ -161,6 +235,7 @@ export function PayslipManager() {
</tbody>
</table>
</div>
<Pagination page={page} pageSize={pageSize} total={payslips.length} onPageChange={setPage} onPageSizeChange={() => setPage(1)} />
</Card>
)}