From 5a9d44033942eb9a3c0bda383bd4f79b863d34c7 Mon Sep 17 00:00:00 2001 From: selfrelease Date: Sun, 16 Aug 2026 12:26:13 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BA=BF=E4=B8=8B=E7=AD=BE=E7=BD=B2?= =?UTF-8?q?=E7=99=BB=E8=AE=B0=E6=A0=87=E9=A2=98=E8=8B=B1=E6=96=87=E8=BD=AC?= =?UTF-8?q?=E4=B8=AD=E6=96=87+=E5=AE=8C=E6=88=90=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=97=B6=E5=8C=BA+=E7=AD=BE=E7=BD=B2=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E8=A7=92=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. documentTitle 中 contractType 枚举值转中文 (LABOR→劳务协议、FIXED→固定期限劳动合同等) 2. 签署日期解析改为本地时区构造(new Date(y,m-1,d,12)), 避免 new Date('2026-08-16') 被解析为 UTC 00:00 导致 本地 +8 显示为 08:00:00 3. 签署记录 Tab 增加数字角标显示总记录数 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- 20260816-优化.md | 51 +++ backend/src/routes/esign.routes.ts | 19 +- backend/src/routes/payroll2.routes.ts | 76 +++++ frontend/src/App.tsx | 2 + frontend/src/components/layout/SidebarNav.tsx | 2 + frontend/src/lib/api-services.ts | 3 + frontend/src/pages/CommissionBonus.tsx | 306 ++++++++++++++++++ frontend/src/pages/ESign.tsx | 1 + frontend/src/pages/money/BatchTab.tsx | 28 +- 9 files changed, 485 insertions(+), 3 deletions(-) create mode 100644 frontend/src/pages/CommissionBonus.tsx diff --git a/20260816-优化.md b/20260816-优化.md index d0a85fe..7704b89 100644 --- a/20260816-优化.md +++ b/20260816-优化.md @@ -346,3 +346,54 @@ - **修复**:Dashboard 待办列表渲染时,`actionUrl` 为空或 '/' 时不渲染 Link 和"立刻办理"按钮,改为纯展示 div;有有效 URL 时才渲染可跳转链接。问题 10A 已修正三期/医疗期/工伤的反向引导 URL,其余 actionUrl(`/roster?employee=xxx`、`/special-status`、`/money`)均指向有效路由,Roster 已处理 `?employee` 参数自动定位。 - **涉及文件**:`frontend/src/pages/Dashboard.tsx`(待办列表渲染 1136-1233 行) +### ✅ 已完成 13:风险提醒规则优化(问题 10B) + +- **问题**:合同到期提醒仅 30 天预警,HR 反应时间不足;去重后的待办列表未按优先级排序。 +- **修复**: + 1. 合同到期预警从 30 天提前到 60 天,分级:≤30 天 HIGH(需立即处理),31-60 天 MEDIUM(提前预警)。 + 2. `dedupedTodos` 按优先级排序(URGENT > HIGH > MEDIUM > LOW),确保最紧急的待办排在最前。 +- **涉及文件**:`backend/src/services/risk.service.ts`(`detectContractRisks` 216-248 行、`getDashboardData` dedupedTodos 排序 941-952 行) + +### ✅ 已完成 14:提成奖金新模块(问题 12) + +- **问题**:无独立提成奖金管理模块,`BONUS` 批次是年终奖发放,非按月提成奖金管理。 +- **实现**: + 1. **数据模型**:新增 `CommissionBonus` 表(`orgId`、`employeeId`、`month` YYYY-MM、`amount` 支持正负值、`remark`、`createdBy`),唯一索引 `(orgId, employeeId, month)`。 + 2. **后端**:新增 `commission-bonus.service.ts` + `commission-bonus.routes.ts`: + - `GET /commission-bonus?month=YYYY-MM` 按月列表 + 汇总(总奖金/总扣款/净额) + - `POST /commission-bonus` 新增单条(upsert,同员工同月自动覆盖) + - `PUT /commission-bonus/:id` 编辑单条 + - `DELETE /commission-bonus/:id` 删除单条 + - `POST /commission-bonus/import` 批量导入 Excel(字段:员工姓名/证件号码/金额/备注) + - `GET /commission-bonus/template` 下载导入模板 + 3. **前端**:新增 `CommissionBonus.tsx` 页面: + - 月份选择器(默认当月) + - 汇总卡片(记录数/总奖金/总扣款/净额) + - 列表展示(员工/部门/金额/备注),支持搜索、分页 + - 单条新增/编辑/删除 + - 批量导入 Excel + 模板下载 + - 金额支持正负值(正=奖金,负=扣款) + 4. **菜单**:`SidebarNav.tsx` "团队"分组新增"提成奖金"入口,路由 `/commission-bonus`。 + 5. **DB 同步**:`prisma db push` 已执行,Prisma Client 已重新生成。 +- **涉及文件**: + - `backend/prisma/schema.prisma`(新增 `CommissionBonus` 模型 + Employee/Organization 反向关联) + - `backend/src/services/commission-bonus.service.ts`(新增) + - `backend/src/routes/commission-bonus.routes.ts`(新增) + - `backend/src/app.ts`(路由注册) + - `frontend/src/pages/CommissionBonus.tsx`(新增) + - `frontend/src/components/layout/SidebarNav.tsx`(菜单项) + - `frontend/src/App.tsx`(路由注册) + - `frontend/src/lib/api-services.ts`(`commissionBonusApi`) + +### ✅ 已完成 15:获取提成奖金(问题 13) + +- **问题**:薪资批次编辑薪资时无"获取提成奖金"按钮,无法从提成奖金模块按月拉取填充 `bonus` 字段。 +- **实现**: + 1. 后端新增 `POST /payroll2/batches/:batchId/fetch-bonus`:按批次月份从 `CommissionBonus` 表拉取,填充到对应 `entries.bonus`,重算批次汇总。 + 2. 前端 `BatchTab.tsx` 编辑薪资步骤新增"获取提成奖金"按钮(在"导入加班费"旁),调用后展示结果摘要(X 人提成奖金已填充,合计 ¥Y)。 + 3. 已归档批次禁用按钮。 +- **涉及文件**: + - `backend/src/routes/payroll2.routes.ts`(`fetch-bonus` 接口 552-628 行) + - `frontend/src/lib/api-services.ts`(`fetchBonusToBatch`) + - `frontend/src/pages/money/BatchTab.tsx`(`fetchBonusMutation` + 按钮 784-798 行) + diff --git a/backend/src/routes/esign.routes.ts b/backend/src/routes/esign.routes.ts index a1f2e25..5df77d0 100644 --- a/backend/src/routes/esign.routes.ts +++ b/backend/src/routes/esign.routes.ts @@ -214,7 +214,10 @@ router.post('/sign-date', async (req: AuthRequest, res: Response, next: NextFunc if (contract.signMethod === 'ELECTRONIC') { return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '电子签合同的签署日期由电签系统自动回写,不可手动修改' } }) } - const parsedDate = new Date(signDate) + // 解析日期字符串(YYYY-MM-DD),手动构造本地中午时间避免时区偏移 + const dateStr = String(signDate).slice(0, 10) + const [y, m, d] = dateStr.split('-').map(Number) + const parsedDate = new Date(y, (m || 1) - 1, d || 1, 12, 0, 0, 0) if (isNaN(parsedDate.getTime())) { return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '签署日期格式无效' } }) } @@ -229,6 +232,18 @@ router.post('/sign-date', async (req: AuthRequest, res: Response, next: NextFunc select: { id: true }, }) if (!existingRecord) { + // 合同类型枚举转中文 + const CONTRACT_TYPE_LABEL: Record = { + FIXED: '固定期限劳动合同', + UNFIXED: '无固定期限劳动合同', + UNSIGNED: '未签合同', + LABOR: '劳务协议', + INTERNSHIP: '实习协议', + DISPATCH: '劳务派遣合同', + OUTSOURCING: '外包合同', + PARTTIME: '非全日制合同', + } + const contractLabel = CONTRACT_TYPE_LABEL[contract.contractType] || '合同' await prisma.eSignRecord.create({ data: { orgId: req.user!.orgId, @@ -236,7 +251,7 @@ router.post('/sign-date', async (req: AuthRequest, res: Response, next: NextFunc employeeId: contract.employeeId, scene: 'CONTRACT', signMethod: 'PAPER', - documentTitle: `${contract.contractType}合同线下签署登记`, + documentTitle: `${contractLabel}线下签署登记`, status: 'COMPLETED', completedAt: parsedDate, signedAt: parsedDate, diff --git a/backend/src/routes/payroll2.routes.ts b/backend/src/routes/payroll2.routes.ts index 8d44914..fa9bd2f 100644 --- a/backend/src/routes/payroll2.routes.ts +++ b/backend/src/routes/payroll2.routes.ts @@ -11,6 +11,7 @@ import { prePayrollCheck, } from '../services/payroll.service' import { isInProbation } from '../services/contract.service' +import { getBonusByMonthAndEmployeeIds } from '../services/commission-bonus.service' // RFC 5987 编码中文文件名 function contentDisposition(filename: string): string { @@ -548,6 +549,81 @@ router.put('/batches/:batchId/entries/:employeeId', async (req: AuthRequest, res } }) +// 获取提成奖金:按批次月份从 CommissionBonus 表拉取,填充到 entries.bonus +router.post('/batches/:batchId/fetch-bonus', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const { batchId } = req.params + const orgId = req.user!.orgId + + const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } }) + if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } }) + if (batch.status === 'ARCHIVED') return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '已归档批次不可操作' } }) + + // 获取批次所有条目 + const entries = await prisma.batchEntry.findMany({ + where: { batchId }, + select: { id: true, employeeId: true, bonus: true }, + }) + + if (entries.length === 0) { + return res.json({ success: true, data: { filled: 0, totalAmount: 0, message: '批次无员工条目' } }) + } + + // 按批次月份查询提成奖金 + const bonusMap = await getBonusByMonthAndEmployeeIds(orgId, batch.month, entries.map((e) => e.employeeId)) + + let filled = 0 + let totalAmount = 0 + for (const entry of entries) { + const bonus = bonusMap.get(entry.employeeId) + if (bonus) { + await prisma.batchEntry.update({ + where: { id: entry.id }, + data: { bonus: bonus.amount }, + }) + filled++ + totalAmount += bonus.amount + } + } + + // 重算批次汇总 + const allEntries = await prisma.batchEntry.findMany({ where: { batchId } }) + const totals = allEntries.reduce((acc, e) => ({ + totalPay: acc.totalPay + e.baseSalary + e.overtimePay + e.allowance + e.bonus - e.deduction, + totalNetPay: acc.totalNetPay + e.netPay, + totalSocialOrg: acc.totalSocialOrg + e.socialOrg, + totalSocialEmp: acc.totalSocialEmp + e.socialEmp, + totalHousingOrg: acc.totalHousingOrg + e.housingOrg, + totalHousingEmp: acc.totalHousingEmp + e.housingEmp, + totalTax: acc.totalTax + e.tax, + }), { totalPay: 0, totalNetPay: 0, totalSocialOrg: 0, totalSocialEmp: 0, totalHousingOrg: 0, totalHousingEmp: 0, totalTax: 0 }) + + await prisma.payrollBatch.update({ + where: { id: batchId }, + data: { + totalPay: Math.round(totals.totalPay * 100) / 100, + totalNetPay: Math.round(totals.totalNetPay * 100) / 100, + totalSocialOrg: Math.round(totals.totalSocialOrg * 100) / 100, + totalSocialEmp: Math.round(totals.totalSocialEmp * 100) / 100, + totalHousingOrg: Math.round(totals.totalHousingOrg * 100) / 100, + totalHousingEmp: Math.round(totals.totalHousingEmp * 100) / 100, + totalTax: Math.round(totals.totalTax * 100) / 100, + }, + }) + + res.json({ + success: true, + data: { + filled, + totalAmount: Math.round(totalAmount * 100) / 100, + message: filled > 0 ? `已填充 ${filled} 人提成奖金,合计 ¥${Math.round(totalAmount * 100) / 100}` : `${batch.month} 无提成奖金数据`, + }, + }) + } catch (err) { + next(err) + } +}) + // 获取条目个税计算明细 router.get('/batches/:batchId/entries/:employeeId/tax-detail', async (req: AuthRequest, res: Response, next: NextFunction) => { try { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 107f59c..058db8d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -61,6 +61,7 @@ const SalaryDashboard = lazy(() => import('./pages/SalaryDashboard')) const CommercialInsurance = lazy(() => import('./pages/CommercialInsurance')) const EmployeeBenefits = lazy(() => import('./pages/EmployeeBenefits')) const ESign = lazy(() => import('./pages/ESign')) +const CommissionBonus = lazy(() => import('./pages/CommissionBonus')) // 平台管理端 const PlatformLogin = lazy(() => import('./pages/platform/PlatformLogin')) @@ -204,6 +205,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/layout/SidebarNav.tsx b/frontend/src/components/layout/SidebarNav.tsx index 0c596a4..c026b6e 100644 --- a/frontend/src/components/layout/SidebarNav.tsx +++ b/frontend/src/components/layout/SidebarNav.tsx @@ -16,6 +16,7 @@ import { ChevronDown, ChevronRight, Building2, CalendarDays, ClipboardList, Heart, CalendarClock, Gift, PenTool, Umbrella, GraduationCap, TrendingUp, AlertTriangle, + DollarSign, } from 'lucide-react' import Logo from '../ui/Logo' import { settingsApi } from '../../lib/api-services' @@ -50,6 +51,7 @@ const navGroups: NavGroup[] = [ { path: '/performance-records', label: '绩效考核', icon: TrendingUp }, { path: '/disciplinary-records', label: '违纪记录', icon: AlertTriangle }, { path: '/special-status', label: '特殊员工', icon: Heart }, + { path: '/commission-bonus', label: '提成奖金', icon: DollarSign }, ], }, { diff --git a/frontend/src/lib/api-services.ts b/frontend/src/lib/api-services.ts index 65fd6f4..d49c5e7 100644 --- a/frontend/src/lib/api-services.ts +++ b/frontend/src/lib/api-services.ts @@ -477,6 +477,9 @@ export const payrollApi = { /** 导入加班费到批次 */ importOvertimeToBatch: (batchId: string) => post(`/payroll/overtime/import-to-batch/${batchId}`).then(unwrap()), + /** 获取提成奖金到批次 */ + fetchBonusToBatch: (batchId: string) => + post(`/payroll2/batches/${batchId}/fetch-bonus`).then(unwrap()), /** 加班费配置 */ overtimeConfig: () => get('/payroll/overtime/config').then(unwrap()), diff --git a/frontend/src/pages/CommissionBonus.tsx b/frontend/src/pages/CommissionBonus.tsx new file mode 100644 index 0000000..25fe7f1 --- /dev/null +++ b/frontend/src/pages/CommissionBonus.tsx @@ -0,0 +1,306 @@ +import { useState, useRef } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { Plus, Edit2, Trash2, Upload, Download, Search } from 'lucide-react' +import { toast } from 'sonner' +import { commissionBonusApi, employeeApi } from '../lib/api-services' +import { Input, Label, Select } from '../components/ui/Input' +import Button from '../components/ui/Button' +import Modal from '../components/ui/Modal' +import PageGuide from '../components/ui/PageGuide' +import Pagination from '../components/ui/Pagination' +import { usePageSize } from '../hooks/usePageSize' + +export default function CommissionBonus() { + const queryClient = useQueryClient() + const pageSize = usePageSize() + const [page, setPage] = useState(1) + const [month, setMonth] = useState(new Date().toISOString().slice(0, 7)) + const [search, setSearch] = useState('') + const [showAdd, setShowAdd] = useState(false) + const [editRecord, setEditRecord] = useState(null) + const fileRef = useRef(null) + + // 列表 + 汇总 + const { data, isLoading } = useQuery({ + queryKey: ['commission-bonus', month], + queryFn: () => commissionBonusApi.list(month), + }) + + // 在职员工列表(新增用) + const { data: employees } = useQuery({ + queryKey: ['employees-active'], + queryFn: () => employeeApi.list({ status: 'ACTIVE' }), + enabled: showAdd, + }) + + const records = data?.records || [] + const summary = data?.summary || { count: 0, totalBonus: 0, totalDeduction: 0, netAmount: 0 } + + // 搜索过滤 + const filtered = records.filter((r: any) => + !search || r.employee?.name?.includes(search) || r.employee?.department?.includes(search) + ) + + // 分页 + const paged = filtered.slice((page - 1) * pageSize, page * pageSize) + + // 新增 + const addMutation = useMutation({ + mutationFn: (data: { employeeId: string; month: string; amount: number; remark?: string }) => + commissionBonusApi.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['commission-bonus'] }) + setShowAdd(false) + toast.success('已添加') + }, + onError: (err: any) => toast.error(err?.response?.data?.error?.message || '添加失败'), + }) + + // 更新 + const updateMutation = useMutation({ + mutationFn: (data: { id: string; amount?: number; remark?: string }) => + commissionBonusApi.update(data.id, { amount: data.amount, remark: data.remark }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['commission-bonus'] }) + setEditRecord(null) + toast.success('已更新') + }, + onError: (err: any) => toast.error(err?.response?.data?.error?.message || '更新失败'), + }) + + // 删除 + const deleteMutation = useMutation({ + mutationFn: (id: string) => commissionBonusApi.remove(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['commission-bonus'] }) + toast.success('已删除') + }, + onError: (err: any) => toast.error(err?.response?.data?.error?.message || '删除失败'), + }) + + // 导入 + const importMutation = useMutation({ + mutationFn: (file: File) => commissionBonusApi.import(file, month), + onSuccess: (data: any) => { + queryClient.invalidateQueries({ queryKey: ['commission-bonus'] }) + toast.success(`导入完成:新增 ${data.created},更新 ${data.updated},跳过 ${data.skipped}`) + if (data.errors?.length > 0) { + toast.error(`错误明细:${data.errors.slice(0, 3).map((e: any) => `第${e.row}行: ${e.message}`).join(';')}`) + } + }, + onError: (err: any) => toast.error(err?.response?.data?.error?.message || '导入失败'), + }) + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (file) importMutation.mutate(file) + if (fileRef.current) fileRef.current.value = '' + } + + const fmt = (n: number) => `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` + + return ( +
+ +

按月管理员工提成奖金/扣款。金额正数为奖金,负数为扣款。数据可用于薪资批次"获取提成奖金"自动填充。

+
+ + {/* 筛选栏 */} +
+
+ + { setMonth(e.target.value); setPage(1) }} className="w-40" /> +
+
+ +
+ + { setSearch(e.target.value); setPage(1) }} placeholder="员工姓名/部门" className="pl-9" /> +
+
+
+ + + + +
+
+ + {/* 汇总卡片 */} +
+
+
记录数
+
{summary.count}
+
+
+
总奖金
+
{fmt(summary.totalBonus)}
+
+
+
总扣款
+
{fmt(summary.totalDeduction)}
+
+
+
净额
+
= 0 ? 'text-gray-900' : 'text-danger'}`}>{fmt(summary.netAmount)}
+
+
+ + {/* 列表 */} +
+ + + + + + + + + + + + {isLoading ? ( + + ) : paged.length === 0 ? ( + + ) : paged.map((r: any) => ( + + + + + + + + ))} + +
员工部门金额备注操作
加载中...
{month} 无提成奖金记录
{r.employee?.name || '-'}{r.employee?.department || '-'}= 0 ? 'text-safe' : 'text-danger'}`}> + {r.amount >= 0 ? '+' : ''}{fmt(r.amount)} + {r.remark || '-'} + + +
+
+ + {filtered.length > pageSize && ( + setPage(1)} /> + )} + + {/* 新增弹窗 */} + {showAdd && ( + setShowAdd(false)} + onSubmit={(data) => addMutation.mutate(data)} + saving={addMutation.isPending} + /> + )} + + {/* 编辑弹窗 */} + {editRecord && ( + setEditRecord(null)} + onSubmit={(data) => updateMutation.mutate({ id: editRecord.id, ...data })} + saving={updateMutation.isPending} + /> + )} +
+ ) +} + +function AddModal({ employees, month, onClose, onSubmit, saving }: { + employees: any[] + month: string + onClose: () => void + onSubmit: (data: { employeeId: string; month: string; amount: number; remark?: string }) => void + saving: boolean +}) { + const [employeeId, setEmployeeId] = useState('') + const [amount, setAmount] = useState('') + const [remark, setRemark] = useState('') + + return ( + +
+
+ + +
+
+ + +
+
+ + setAmount(e.target.value)} placeholder="如 5000 或 -200" /> +
+
+ + setRemark(e.target.value)} placeholder="选填" /> +
+
+ + +
+
+
+ ) +} + +function EditModal({ record, onClose, onSubmit, saving }: { + record: any + onClose: () => void + onSubmit: (data: { amount?: number; remark?: string }) => void + saving: boolean +}) { + const [amount, setAmount] = useState(String(record.amount)) + const [remark, setRemark] = useState(record.remark || '') + + return ( + +
+
+ + setAmount(e.target.value)} /> +
+
+ + setRemark(e.target.value)} /> +
+
+ + +
+
+
+ ) +} diff --git a/frontend/src/pages/ESign.tsx b/frontend/src/pages/ESign.tsx index 9f7d8ca..89604b4 100644 --- a/frontend/src/pages/ESign.tsx +++ b/frontend/src/pages/ESign.tsx @@ -183,6 +183,7 @@ export default function ESign() { className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${activeTab === 'records' ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'}`} > 签署记录 + {records.length > 0 && {records.length}}
diff --git a/frontend/src/pages/money/BatchTab.tsx b/frontend/src/pages/money/BatchTab.tsx index bdc8739..ac0a7d3 100644 --- a/frontend/src/pages/money/BatchTab.tsx +++ b/frontend/src/pages/money/BatchTab.tsx @@ -3,7 +3,7 @@ import { usePageSize } from '../../hooks/usePageSize' import { toast } from 'sonner' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useConfirm } from '../../hooks/useConfirm' -import { Calculator, AlertCircle, Info, Upload, Layers, Settings as SettingsIcon, Archive, Plus, Trash2, AlertTriangle, Download, FileText, X, ChevronLeft, Clock, Users, TrendingDown, TrendingUp, BadgeCheck } from 'lucide-react' +import { Calculator, AlertCircle, Info, Upload, Layers, Settings as SettingsIcon, Archive, Plus, Trash2, AlertTriangle, Download, FileText, X, ChevronLeft, Clock, Users, TrendingDown, TrendingUp, BadgeCheck, DollarSign } from 'lucide-react' import { Stepper, type Step } from '../../components/ui/Stepper' import { InlineAlert } from '../../components/ui/InlineAlert' import PageGuide from '../../components/ui/PageGuide' @@ -556,6 +556,22 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void }, }) + const fetchBonusMutation = useMutation({ + mutationFn: () => payrollApi.fetchBonusToBatch(batchId), + onSuccess: (res: any) => { + queryClient.invalidateQueries({ queryKey: ['batch-detail'] }) + queryClient.invalidateQueries({ queryKey: ['batches'] }) + if (res.data?.filled > 0) { + toast.success(res.data.message) + } else { + toast.info(res.data?.message || '无提成奖金数据') + } + }, + onError: () => { + toast.error('获取提成奖金失败') + }, + }) + const importPayrollMutation = useMutation({ mutationFn: async (file: File) => { const token = useAuthStore.getState().accessToken @@ -774,6 +790,16 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void {importOvertimeMutation.isPending ? '导入中...' : '导入加班费'} +