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