feat: 批量获取操作跨批次重复检查 + 加班费改为获取 + 排除预入职
后端: - 5个批量获取接口(加班费/绩效/奖金/违纪/考勤)加 force 参数 - 非force模式下检查同月其他批次是否已有非零值,有则返回 needConfirm - 加班费接口改用 calcBatchEntry 重算(含税/社保),不再手动算 totalPay - 批次创建用当前日期判断是否已入职,排除预入职人员 前端: - 5个 mutation 支持 force 参数,needConfirm 时弹确认框 - 加班费按钮文字从'导入加班费'改为'获取加班费'
This commit is contained in:
@@ -702,6 +702,7 @@ router.post('/overtime/import-to-batch/:batchId', async (req: AuthRequest, res:
|
|||||||
try {
|
try {
|
||||||
const { batchId } = req.params
|
const { batchId } = req.params
|
||||||
const orgId = req.user!.orgId
|
const orgId = req.user!.orgId
|
||||||
|
const force = (req.body as any)?.force === true
|
||||||
|
|
||||||
const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
|
const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
|
||||||
if (!batch) return res.status(404).json({ success: false, message: '批次不存在' })
|
if (!batch) return res.status(404).json({ success: false, message: '批次不存在' })
|
||||||
@@ -718,9 +719,33 @@ router.post('/overtime/import-to-batch/:batchId', async (req: AuthRequest, res:
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (overtimeRecords.length === 0) {
|
if (overtimeRecords.length === 0) {
|
||||||
return res.json({ success: false, message: '没有可导入的加班记录(所有记录已关联批次或无数据)' })
|
return res.json({ success: false, message: '没有可获取的加班记录(所有记录已关联批次或无数据)' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查跨批次重复(非 force 模式)
|
||||||
|
const toFillEmployeeIds = new Set(overtimeRecords.map(ot => ot.employeeId))
|
||||||
|
if (!force && toFillEmployeeIds.size > 0) {
|
||||||
|
const otherEntries = await prisma.batchEntry.findMany({
|
||||||
|
where: { orgId, batch: { month: batch.month, id: { not: batchId } }, overtimePay: { gt: 0 } },
|
||||||
|
select: { employeeId: true },
|
||||||
|
})
|
||||||
|
const dupIds = new Set(otherEntries.map(e => e.employeeId))
|
||||||
|
const overlap = [...toFillEmployeeIds].filter(id => dupIds.has(id))
|
||||||
|
if (overlap.length > 0) {
|
||||||
|
const overlapNames = await prisma.employee.findMany({ where: { id: { in: overlap } }, select: { name: true } })
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
needConfirm: true,
|
||||||
|
duplicateCount: overlap.length,
|
||||||
|
duplicateNames: overlapNames.map(e => e.name),
|
||||||
|
message: `${overlap.length} 人在其他批次中已有加班费(${overlapNames.map(e => e.name).join('、')}),确认是否再次获取?`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { calcBatchEntry } = await import('../services/payroll.service')
|
||||||
const results: any[] = []
|
const results: any[] = []
|
||||||
for (const ot of overtimeRecords) {
|
for (const ot of overtimeRecords) {
|
||||||
// 获取员工月工资
|
// 获取员工月工资
|
||||||
@@ -745,26 +770,61 @@ router.post('/overtime/import-to-batch/:batchId', async (req: AuthRequest, res:
|
|||||||
data: { weekdayPay, weekendPay, holidayPay, totalPay, batchId },
|
data: { weekdayPay, weekendPay, holidayPay, totalPay, batchId },
|
||||||
})
|
})
|
||||||
|
|
||||||
// 更新批次条目的加班费
|
// 更新批次条目的加班费并重算
|
||||||
const entry = await prisma.batchEntry.findUnique({
|
const entry = await prisma.batchEntry.findUnique({
|
||||||
where: { batchId_employeeId: { batchId, employeeId: ot.employeeId } },
|
where: { batchId_employeeId: { batchId, employeeId: ot.employeeId } },
|
||||||
})
|
})
|
||||||
if (entry) {
|
if (entry) {
|
||||||
|
const inputs = {
|
||||||
|
baseSalary: entry.baseSalary,
|
||||||
|
performanceSalary: entry.performanceSalary || undefined,
|
||||||
|
overtimePay: totalPay,
|
||||||
|
allowance: entry.allowance,
|
||||||
|
deduction: entry.deduction,
|
||||||
|
bonus: entry.bonus,
|
||||||
|
positionSalary: entry.positionSalary || undefined,
|
||||||
|
senioritySalary: entry.senioritySalary || undefined,
|
||||||
|
transportAllowance: entry.transportAllowance || undefined,
|
||||||
|
mealAllowance: entry.mealAllowance || undefined,
|
||||||
|
housingAllowance: entry.housingAllowance || undefined,
|
||||||
|
communicationAllowance: entry.communicationAllowance || undefined,
|
||||||
|
otherDeduction: entry.otherDeduction || undefined,
|
||||||
|
}
|
||||||
|
const calcResult = await calcBatchEntry(orgId, ot.employeeId, batch.month, inputs, batch.type)
|
||||||
|
const { systemSocialEmp, systemSocialOrg, systemHousingEmp, systemHousingOrg, systemSupplementaryHousingEmp, systemSupplementaryHousingOrg, taxBreakdown, ...entryData } = calcResult
|
||||||
await prisma.batchEntry.update({
|
await prisma.batchEntry.update({
|
||||||
where: { id: entry.id },
|
where: { id: entry.id },
|
||||||
data: { overtimePay: totalPay },
|
data: entryData,
|
||||||
})
|
|
||||||
// 重新计算条目
|
|
||||||
const newTotalPay = entry.baseSalary + totalPay + entry.allowance + entry.bonus - entry.deduction
|
|
||||||
await prisma.batchEntry.update({
|
|
||||||
where: { id: entry.id },
|
|
||||||
data: { totalPay: newTotalPay },
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push({ employeeId: ot.employeeId, employeeName: ot.employee.name, totalPay })
|
results.push({ employeeId: ot.employeeId, employeeName: ot.employee.name, totalPay })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重算批次汇总
|
||||||
|
const allEntries = await prisma.batchEntry.findMany({ where: { batchId } })
|
||||||
|
const totals = allEntries.reduce((acc, e) => ({
|
||||||
|
totalPay: acc.totalPay + e.totalPay,
|
||||||
|
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: { imported: results.length, details: results } })
|
res.json({ success: true, data: { imported: results.length, details: results } })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err)
|
next(err)
|
||||||
|
|||||||
@@ -118,7 +118,26 @@ async function recalcBatchTotals(batchId: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// RFC 5987 编码中文文件名
|
/**
|
||||||
|
* 检查同月其他批次中哪些员工已有非零值
|
||||||
|
* 返回有重复的员工ID集合
|
||||||
|
*/
|
||||||
|
async function checkOtherBatches(orgId: string, currentBatchId: string, month: string, field: string): Promise<Set<string>> {
|
||||||
|
const otherEntries = await prisma.batchEntry.findMany({
|
||||||
|
where: {
|
||||||
|
orgId,
|
||||||
|
batch: { month, id: { not: currentBatchId } },
|
||||||
|
},
|
||||||
|
select: { employeeId: true, [field]: true },
|
||||||
|
})
|
||||||
|
const duplicateIds = new Set<string>()
|
||||||
|
for (const e of otherEntries) {
|
||||||
|
if ((e as any)[field] > 0) duplicateIds.add(e.employeeId)
|
||||||
|
}
|
||||||
|
return duplicateIds
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function contentDisposition(filename: string): string {
|
function contentDisposition(filename: string): string {
|
||||||
const encoded = encodeURIComponent(filename)
|
const encoded = encodeURIComponent(filename)
|
||||||
return `attachment; filename="${encoded}"; filename*=UTF-8''${encoded}`
|
return `attachment; filename="${encoded}"; filename*=UTF-8''${encoded}`
|
||||||
@@ -430,12 +449,17 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
|
|||||||
employees = terminations.map(t => t.employee)
|
employees = terminations.map(t => t.employee)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// 预入职判定:用当前日期而非批次月末,避免预入职人员被拉入
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
const todayEnd = new Date(today)
|
||||||
|
todayEnd.setDate(todayEnd.getDate() + 1)
|
||||||
employees = await prisma.employee.findMany({
|
employees = await prisma.employee.findMany({
|
||||||
where: {
|
where: {
|
||||||
orgId,
|
orgId,
|
||||||
OR: [
|
OR: [
|
||||||
// 在职且已入职(hireDate <= 批次月末,排除预入职)
|
// 在职且已入职(hireDate < 明天,排除预入职)
|
||||||
{ status: 'ACTIVE', hireDate: { lte: monthEnd } },
|
{ status: 'ACTIVE', hireDate: { lt: todayEnd } },
|
||||||
// 本月离职的员工(离职当月仍需结算)
|
// 本月离职的员工(离职当月仍需结算)
|
||||||
{ status: 'RESIGNED', updatedAt: { gte: monthStart, lte: monthEnd } },
|
{ status: 'RESIGNED', updatedAt: { gte: monthStart, lte: monthEnd } },
|
||||||
],
|
],
|
||||||
@@ -760,6 +784,7 @@ router.post('/batches/:batchId/fetch-bonus', async (req: AuthRequest, res: Respo
|
|||||||
try {
|
try {
|
||||||
const { batchId } = req.params
|
const { batchId } = req.params
|
||||||
const orgId = req.user!.orgId
|
const orgId = req.user!.orgId
|
||||||
|
const force = (req.body as any)?.force === true
|
||||||
|
|
||||||
const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, 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) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
|
||||||
@@ -778,6 +803,25 @@ router.post('/batches/:batchId/fetch-bonus', async (req: AuthRequest, res: Respo
|
|||||||
// 按批次月份查询提成奖金
|
// 按批次月份查询提成奖金
|
||||||
const bonusMap = await getBonusByMonthAndEmployeeIds(orgId, batch.month, entries.map((e) => e.employeeId))
|
const bonusMap = await getBonusByMonthAndEmployeeIds(orgId, batch.month, entries.map((e) => e.employeeId))
|
||||||
|
|
||||||
|
// 检查跨批次重复(非 force 模式)
|
||||||
|
const toFillEmployeeIds = new Set(entries.filter(e => bonusMap.has(e.employeeId)).map(e => e.employeeId))
|
||||||
|
if (!force && toFillEmployeeIds.size > 0) {
|
||||||
|
const dupIds = await checkOtherBatches(orgId, batchId, batch.month, 'bonus')
|
||||||
|
const overlap = [...toFillEmployeeIds].filter(id => dupIds.has(id))
|
||||||
|
if (overlap.length > 0) {
|
||||||
|
const overlapNames = await prisma.employee.findMany({ where: { id: { in: overlap } }, select: { name: true } })
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
needConfirm: true,
|
||||||
|
duplicateCount: overlap.length,
|
||||||
|
duplicateNames: overlapNames.map(e => e.name),
|
||||||
|
message: `${overlap.length} 人在其他批次中已有提成奖金(${overlapNames.map(e => e.name).join('、')}),确认是否再次获取?`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let filled = 0
|
let filled = 0
|
||||||
let totalAmount = 0
|
let totalAmount = 0
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@@ -815,6 +859,7 @@ router.post('/batches/:batchId/fetch-performance', async (req: AuthRequest, res:
|
|||||||
try {
|
try {
|
||||||
const { batchId } = req.params
|
const { batchId } = req.params
|
||||||
const orgId = req.user!.orgId
|
const orgId = req.user!.orgId
|
||||||
|
const force = (req.body as any)?.force === true
|
||||||
|
|
||||||
const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, 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) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
|
||||||
@@ -855,6 +900,25 @@ router.post('/batches/:batchId/fetch-performance', async (req: AuthRequest, res:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查跨批次重复(非 force 模式)
|
||||||
|
const toFillEmployeeIds = new Set(entries.filter(e => (empPerfSalaryMap.get(e.employeeId) || 0) > 0).map(e => e.employeeId))
|
||||||
|
if (!force && toFillEmployeeIds.size > 0) {
|
||||||
|
const dupIds = await checkOtherBatches(orgId, batchId, batch.month, 'performanceSalary')
|
||||||
|
const overlap = [...toFillEmployeeIds].filter(id => dupIds.has(id))
|
||||||
|
if (overlap.length > 0) {
|
||||||
|
const overlapNames = await prisma.employee.findMany({ where: { id: { in: overlap } }, select: { name: true } })
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
needConfirm: true,
|
||||||
|
duplicateCount: overlap.length,
|
||||||
|
duplicateNames: overlapNames.map(e => e.name),
|
||||||
|
message: `${overlap.length} 人在其他批次中已有绩效工资(${overlapNames.map(e => e.name).join('、')}),确认是否再次获取?`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 绩效系数映射
|
// 绩效系数映射
|
||||||
const gradeCoefficients: Record<string, number> = { A: 1.2, B: 1.0, C: 0.8, D: 0.6 }
|
const gradeCoefficients: Record<string, number> = { A: 1.2, B: 1.0, C: 0.8, D: 0.6 }
|
||||||
|
|
||||||
@@ -904,6 +968,7 @@ router.post('/batches/:batchId/fetch-disciplinary', async (req: AuthRequest, res
|
|||||||
try {
|
try {
|
||||||
const { batchId } = req.params
|
const { batchId } = req.params
|
||||||
const orgId = req.user!.orgId
|
const orgId = req.user!.orgId
|
||||||
|
const force = (req.body as any)?.force === true
|
||||||
|
|
||||||
const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, 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) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
|
||||||
@@ -941,6 +1006,25 @@ router.post('/batches/:batchId/fetch-disciplinary', async (req: AuthRequest, res
|
|||||||
deductionMap.set(r.employeeId, (deductionMap.get(r.employeeId) || 0) + r.deductionAmount)
|
deductionMap.set(r.employeeId, (deductionMap.get(r.employeeId) || 0) + r.deductionAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查跨批次重复(非 force 模式)
|
||||||
|
const toFillEmployeeIds = new Set([...deductionMap.keys()])
|
||||||
|
if (!force && toFillEmployeeIds.size > 0) {
|
||||||
|
const dupIds = await checkOtherBatches(orgId, batchId, batch.month, 'deduction')
|
||||||
|
const overlap = [...toFillEmployeeIds].filter(id => dupIds.has(id))
|
||||||
|
if (overlap.length > 0) {
|
||||||
|
const overlapNames = await prisma.employee.findMany({ where: { id: { in: overlap } }, select: { name: true } })
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
needConfirm: true,
|
||||||
|
duplicateCount: overlap.length,
|
||||||
|
duplicateNames: overlapNames.map(e => e.name),
|
||||||
|
message: `${overlap.length} 人在其他批次中已有扣款(${overlapNames.map(e => e.name).join('、')}),确认是否再次获取?`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let filled = 0
|
let filled = 0
|
||||||
let totalAmount = 0
|
let totalAmount = 0
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@@ -979,6 +1063,7 @@ router.post('/batches/:batchId/fetch-attendance-deduction', async (req: AuthRequ
|
|||||||
try {
|
try {
|
||||||
const { batchId } = req.params
|
const { batchId } = req.params
|
||||||
const orgId = req.user!.orgId
|
const orgId = req.user!.orgId
|
||||||
|
const force = (req.body as any)?.force === true
|
||||||
|
|
||||||
const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, 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) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
|
||||||
@@ -1010,6 +1095,25 @@ router.post('/batches/:batchId/fetch-attendance-deduction', async (req: AuthRequ
|
|||||||
deductionMap.set(c.employeeId, c.deductionAmount)
|
deductionMap.set(c.employeeId, c.deductionAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查跨批次重复(非 force 模式)
|
||||||
|
const toFillEmployeeIds = new Set([...deductionMap.keys()])
|
||||||
|
if (!force && toFillEmployeeIds.size > 0) {
|
||||||
|
const dupIds = await checkOtherBatches(orgId, batchId, batch.month, 'deduction')
|
||||||
|
const overlap = [...toFillEmployeeIds].filter(id => dupIds.has(id))
|
||||||
|
if (overlap.length > 0) {
|
||||||
|
const overlapNames = await prisma.employee.findMany({ where: { id: { in: overlap } }, select: { name: true } })
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
needConfirm: true,
|
||||||
|
duplicateCount: overlap.length,
|
||||||
|
duplicateNames: overlapNames.map(e => e.name),
|
||||||
|
message: `${overlap.length} 人在其他批次中已有扣款(${overlapNames.map(e => e.name).join('、')}),确认是否再次获取?`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let filled = 0
|
let filled = 0
|
||||||
let totalAmount = 0
|
let totalAmount = 0
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
|
|||||||
@@ -488,20 +488,20 @@ export const payrollApi = {
|
|||||||
syncOvertimeFromAttendance: (month: string) =>
|
syncOvertimeFromAttendance: (month: string) =>
|
||||||
post('/payroll/overtime/sync-from-attendance', { month }).then(unwrap<any>()),
|
post('/payroll/overtime/sync-from-attendance', { month }).then(unwrap<any>()),
|
||||||
/** 导入加班费到批次 */
|
/** 导入加班费到批次 */
|
||||||
importOvertimeToBatch: (batchId: string) =>
|
importOvertimeToBatch: (batchId: string, force?: boolean) =>
|
||||||
post(`/payroll/overtime/import-to-batch/${batchId}`).then(unwrap<any>()),
|
post(`/payroll/overtime/import-to-batch/${batchId}`, force ? { force: true } : {}).then(unwrap<any>()),
|
||||||
/** 获取提成奖金到批次 */
|
/** 获取提成奖金到批次 */
|
||||||
fetchBonusToBatch: (batchId: string) =>
|
fetchBonusToBatch: (batchId: string, force?: boolean) =>
|
||||||
post(`/payroll2/batches/${batchId}/fetch-bonus`).then(unwrap<any>()),
|
post(`/payroll2/batches/${batchId}/fetch-bonus`, force ? { force: true } : {}).then(unwrap<any>()),
|
||||||
/** 获取绩效工资到批次(按考核系数计算) */
|
/** 获取绩效工资到批次(按考核系数计算) */
|
||||||
fetchPerformanceToBatch: (batchId: string) =>
|
fetchPerformanceToBatch: (batchId: string, force?: boolean) =>
|
||||||
post(`/payroll2/batches/${batchId}/fetch-performance`).then(unwrap<any>()),
|
post(`/payroll2/batches/${batchId}/fetch-performance`, force ? { force: true } : {}).then(unwrap<any>()),
|
||||||
/** 获取违纪扣款到批次 */
|
/** 获取违纪扣款到批次 */
|
||||||
fetchDisciplinaryToBatch: (batchId: string) =>
|
fetchDisciplinaryToBatch: (batchId: string, force?: boolean) =>
|
||||||
post(`/payroll2/batches/${batchId}/fetch-disciplinary`).then(unwrap<any>()),
|
post(`/payroll2/batches/${batchId}/fetch-disciplinary`, force ? { force: true } : {}).then(unwrap<any>()),
|
||||||
/** 获取考勤扣款到批次 */
|
/** 获取考勤扣款到批次 */
|
||||||
fetchAttendanceDeductionToBatch: (batchId: string) =>
|
fetchAttendanceDeductionToBatch: (batchId: string, force?: boolean) =>
|
||||||
post(`/payroll2/batches/${batchId}/fetch-attendance-deduction`).then(unwrap<any>()),
|
post(`/payroll2/batches/${batchId}/fetch-attendance-deduction`, force ? { force: true } : {}).then(unwrap<any>()),
|
||||||
/** 加班费配置 */
|
/** 加班费配置 */
|
||||||
overtimeConfig: () =>
|
overtimeConfig: () =>
|
||||||
get('/payroll/overtime/config').then(unwrap<any>()),
|
get('/payroll/overtime/config').then(unwrap<any>()),
|
||||||
|
|||||||
@@ -562,25 +562,37 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
})
|
})
|
||||||
|
|
||||||
const importOvertimeMutation = useMutation({
|
const importOvertimeMutation = useMutation({
|
||||||
mutationFn: () => payrollApi.importOvertimeToBatch(batchId),
|
mutationFn: (force?: boolean) => payrollApi.importOvertimeToBatch(batchId, force),
|
||||||
onSuccess: (res: any) => {
|
onSuccess: async (res: any) => {
|
||||||
|
if (res.data?.needConfirm) {
|
||||||
|
if (await confirm({ title: '跨批次重复提醒', message: res.data.message, variant: 'primary' })) {
|
||||||
|
importOvertimeMutation.mutate(true)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
|
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['batches'] })
|
queryClient.invalidateQueries({ queryKey: ['batches'] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['overtime-records'] })
|
queryClient.invalidateQueries({ queryKey: ['overtime-records'] })
|
||||||
if (res.data?.imported > 0) {
|
if (res.data?.imported > 0) {
|
||||||
toast.success(`成功导入 ${res.data.imported} 条加班费记录`)
|
toast.success(`成功获取 ${res.data.imported} 条加班费记录`)
|
||||||
} else {
|
} else {
|
||||||
toast.info(res.data?.message || '没有可导入的加班记录')
|
toast.info(res.data?.message || '没有可获取的加班记录')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast.error('导入失败,请重试')
|
toast.error('获取失败,请重试')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const fetchBonusMutation = useMutation({
|
const fetchBonusMutation = useMutation({
|
||||||
mutationFn: () => payrollApi.fetchBonusToBatch(batchId),
|
mutationFn: (force?: boolean) => payrollApi.fetchBonusToBatch(batchId, force),
|
||||||
onSuccess: (res: any) => {
|
onSuccess: async (res: any) => {
|
||||||
|
if (res.data?.needConfirm) {
|
||||||
|
if (await confirm({ title: '跨批次重复提醒', message: res.data.message, variant: 'primary' })) {
|
||||||
|
fetchBonusMutation.mutate(true)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
|
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['batches'] })
|
queryClient.invalidateQueries({ queryKey: ['batches'] })
|
||||||
if (res.data?.filled > 0) {
|
if (res.data?.filled > 0) {
|
||||||
@@ -595,8 +607,14 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
})
|
})
|
||||||
|
|
||||||
const fetchPerformanceMutation = useMutation({
|
const fetchPerformanceMutation = useMutation({
|
||||||
mutationFn: () => payrollApi.fetchPerformanceToBatch(batchId),
|
mutationFn: (force?: boolean) => payrollApi.fetchPerformanceToBatch(batchId, force),
|
||||||
onSuccess: (res: any) => {
|
onSuccess: async (res: any) => {
|
||||||
|
if (res.data?.needConfirm) {
|
||||||
|
if (await confirm({ title: '跨批次重复提醒', message: res.data.message, variant: 'primary' })) {
|
||||||
|
fetchPerformanceMutation.mutate(true)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
|
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['batches'] })
|
queryClient.invalidateQueries({ queryKey: ['batches'] })
|
||||||
if (res.data?.filled > 0) {
|
if (res.data?.filled > 0) {
|
||||||
@@ -611,8 +629,14 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
})
|
})
|
||||||
|
|
||||||
const fetchDisciplinaryMutation = useMutation({
|
const fetchDisciplinaryMutation = useMutation({
|
||||||
mutationFn: () => payrollApi.fetchDisciplinaryToBatch(batchId),
|
mutationFn: (force?: boolean) => payrollApi.fetchDisciplinaryToBatch(batchId, force),
|
||||||
onSuccess: (res: any) => {
|
onSuccess: async (res: any) => {
|
||||||
|
if (res.data?.needConfirm) {
|
||||||
|
if (await confirm({ title: '跨批次重复提醒', message: res.data.message, variant: 'primary' })) {
|
||||||
|
fetchDisciplinaryMutation.mutate(true)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
|
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['batches'] })
|
queryClient.invalidateQueries({ queryKey: ['batches'] })
|
||||||
if (res.data?.filled > 0) {
|
if (res.data?.filled > 0) {
|
||||||
@@ -627,8 +651,14 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
})
|
})
|
||||||
|
|
||||||
const fetchAttendanceDeductionMutation = useMutation({
|
const fetchAttendanceDeductionMutation = useMutation({
|
||||||
mutationFn: () => payrollApi.fetchAttendanceDeductionToBatch(batchId),
|
mutationFn: (force?: boolean) => payrollApi.fetchAttendanceDeductionToBatch(batchId, force),
|
||||||
onSuccess: (res: any) => {
|
onSuccess: async (res: any) => {
|
||||||
|
if (res.data?.needConfirm) {
|
||||||
|
if (await confirm({ title: '跨批次重复提醒', message: res.data.message, variant: 'primary' })) {
|
||||||
|
fetchAttendanceDeductionMutation.mutate(true)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
|
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['batches'] })
|
queryClient.invalidateQueries({ queryKey: ['batches'] })
|
||||||
if (res.data?.filled > 0) {
|
if (res.data?.filled > 0) {
|
||||||
@@ -854,16 +884,16 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => importOvertimeMutation.mutate()}
|
onClick={() => importOvertimeMutation.mutate(undefined)}
|
||||||
disabled={importOvertimeMutation.isPending}
|
disabled={importOvertimeMutation.isPending}
|
||||||
>
|
>
|
||||||
<Calculator className="w-4 h-4 mr-1" />
|
<Calculator className="w-4 h-4 mr-1" />
|
||||||
{importOvertimeMutation.isPending ? '导入中...' : '导入加班费'}
|
{importOvertimeMutation.isPending ? '获取中...' : '获取加班费'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => fetchBonusMutation.mutate()}
|
onClick={() => fetchBonusMutation.mutate(undefined)}
|
||||||
disabled={fetchBonusMutation.isPending || isArchived}
|
disabled={fetchBonusMutation.isPending || isArchived}
|
||||||
title="从提成奖金模块按月拉取填充奖金字段"
|
title="从提成奖金模块按月拉取填充奖金字段"
|
||||||
>
|
>
|
||||||
@@ -873,7 +903,7 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => fetchPerformanceMutation.mutate()}
|
onClick={() => fetchPerformanceMutation.mutate(undefined)}
|
||||||
disabled={fetchPerformanceMutation.isPending || isArchived}
|
disabled={fetchPerformanceMutation.isPending || isArchived}
|
||||||
title="按考核等级系数计算绩效工资(A=1.2/B=1.0/C=0.8/D=0.6),无考核记录按1.0"
|
title="按考核等级系数计算绩效工资(A=1.2/B=1.0/C=0.8/D=0.6),无考核记录按1.0"
|
||||||
>
|
>
|
||||||
@@ -883,7 +913,7 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => fetchDisciplinaryMutation.mutate()}
|
onClick={() => fetchDisciplinaryMutation.mutate(undefined)}
|
||||||
disabled={fetchDisciplinaryMutation.isPending || isArchived}
|
disabled={fetchDisciplinaryMutation.isPending || isArchived}
|
||||||
title="按批次月份从违纪记录中汇总扣款金额(action=DEDUCTION)"
|
title="按批次月份从违纪记录中汇总扣款金额(action=DEDUCTION)"
|
||||||
>
|
>
|
||||||
@@ -893,7 +923,7 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => fetchAttendanceDeductionMutation.mutate()}
|
onClick={() => fetchAttendanceDeductionMutation.mutate(undefined)}
|
||||||
disabled={fetchAttendanceDeductionMutation.isPending || isArchived}
|
disabled={fetchAttendanceDeductionMutation.isPending || isArchived}
|
||||||
title="按批次月份从考勤确认中获取扣款金额(需先在考勤确认中填写)"
|
title="按批次月份从考勤确认中获取扣款金额(需先在考勤确认中填写)"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user