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, Download, Eye } from 'lucide-react' import PageGuide from '../../components/ui/PageGuide' import { payrollApi } from '../../lib/api-services' import Card from '../../components/ui/Card' import Button from '../../components/ui/Button' import { Input, Label } from '../../components/ui/Input' import Modal from '../../components/ui/Modal' import Pagination from '../../components/ui/Pagination' // 金额格式化:保留两位小数 + 千分位 const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) export function PayslipManager({ filterEmployeeId }: { filterEmployeeId?: string }) { 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 [showTaxPreview, setShowTaxPreview] = useState(false) const [previewData, setPreviewData] = useState({ baseSalary: 0, overtimePay: 0, allowance: 0, deduction: 0, bonus: 0, specialDeduction: 0, }) const { data: payslips, isLoading } = useQuery({ queryKey: ['payslips', month, filterEmployeeId], queryFn: async () => { const all = await payrollApi.payslips({ month }) if (!filterEmployeeId) return all return all.filter((p: any) => p.employeeId === filterEmployeeId) }, }) const deleteMutation = useMutation({ mutationFn: (id: string) => payrollApi.removePayslip(id), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['payslips'] }), }) const generateFromBatchMutation = useMutation({ mutationFn: (data: any) => payrollApi.generatePayslips(data?.month || data), onSuccess: (res: any) => { queryClient.invalidateQueries({ queryKey: ['payslips'] }) queryClient.invalidateQueries({ queryKey: ['dashboard'] }) const n = res?.data?.generated || 0 toast.success(`已从归档批次汇总生成 ${n} 条工资条并发布。`) }, }) const taxPreviewMutation = useMutation({ mutationFn: (data: any) => payrollApi.taxPreview(data), onSuccess: (res: any) => { setTaxResult(res.data) setShowTaxPreview(true) }, }) const [taxResult, setTaxResult] = useState(null) const confirmedCount = payslips?.filter((p: any) => p.confirmedAt).length || 0 const unconfirmedCount = payslips ? payslips.length - confirmedCount : 0 return (
工资条管理用于查看、编辑和发布已生成的工资明细。支持按月份筛选、批量确认发送、导出个人工资条PDF。员工可在手机端查看已确认的工资条。关联:发薪批次请在「批次管理」中创建;社保公积金基数请在「社保管理」中设置,影响工资计算。
setMonth(e.target.value)} className="w-48" /> {payslips && payslips.length > 0 && (
共 {payslips.length} 条 已确认 {confirmedCount} 未确认 {unconfirmedCount}
)}
{isLoading ? (
加载中...
) : !payslips || payslips.length === 0 ? (
该月份暂无工资条记录
) : (
{payslips.slice((page - 1) * pageSize, page * pageSize).map((p: any) => ( ))}
员工 部门 基本工资 加班费 津贴 奖金 扣款 应发合计 个税 实发 确认状态
{p.employee?.name} {p.employee?.department} ¥{fmt(p.baseSalary)} ¥{fmt(p.overtimePay)} ¥{fmt(p.allowance)} ¥{fmt(p.bonus)} {p.deduction > 0 ? '-¥' + fmt(p.deduction) : '¥0'} ¥{fmt(p.totalPay)} ¥{fmt(p.tax)} ¥{fmt(p.netPay)} {p.confirmedAt ? ( 已确认 ) : p.viewedAt ? ( 已查看 ) : p.publishedAt ? ( 已发送 ) : ( 未发送 )}
setPage(1)} />
)} {/* 税率试算 Modal */} {showTaxPreview && ( { setShowTaxPreview(false); setTaxResult(null) }}>

工资条税率试算

setPreviewData({ ...previewData, baseSalary: Number(e.target.value) })} placeholder="请输入" />
setPreviewData({ ...previewData, overtimePay: Number(e.target.value) })} placeholder="请输入" />
setPreviewData({ ...previewData, allowance: Number(e.target.value) })} placeholder="请输入" />
setPreviewData({ ...previewData, bonus: Number(e.target.value) })} placeholder="请输入" />
setPreviewData({ ...previewData, deduction: Number(e.target.value) })} placeholder="请输入" />
setPreviewData({ ...previewData, specialDeduction: Number(e.target.value) })} placeholder="请输入" />
{taxResult && (
计算结果
{taxResult.breakdown.map((item: any, i: number) => (
0 && i < taxResult.breakdown.length - 1 ? 'text-gray-500' : ''}`}> {item.label} {item.value < 0 ? `-¥${fmt(Math.abs(item.value))}` : `¥${fmt(item.value)}`}
))} {taxResult.ytdPayslipCount > 0 && (
注:已累计{taxResult.ytdPayslipCount}条工资条计算个税
)}
)}
)}
) }