diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 9425986..d1dc38b 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -952,6 +952,8 @@ model BatchEntry { prevDeferredMinWage Float @default(0) // 从上月继承的递延最低工资(本批次已补扣) // 风险提示 riskWarnings Json? + // 自定义模板项的值(非预置项),如 {"heatAllowance": 300} + extraItems Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/backend/src/routes/import.routes.ts b/backend/src/routes/import.routes.ts index 8d0196e..b4cd27c 100644 --- a/backend/src/routes/import.routes.ts +++ b/backend/src/routes/import.routes.ts @@ -890,9 +890,10 @@ router.post('/payroll', authMiddleware, upload.single('file'), async (req: AuthR } // 重新计算税费 - const calcResult = await calcBatchEntry(orgId, empId, batch.month, inputs, batch.type) - const { systemSocialEmp: _sse, systemSocialOrg: _sso, systemHousingEmp: _she, systemHousingOrg: _sho, taxBreakdown: _tb, ...entryData } = calcResult - await prisma.batchEntry.update({ where: { id: entryId }, data: { ...inputs, ...entryData } }) + const entry = await prisma.batchEntry.findUnique({ where: { id: entryId } }) + const calcResult = await calcBatchEntry(orgId, empId, batch.month, inputs, batch.type, undefined, (entry?.extraItems as Record) || null) + const { systemSocialEmp: _sse, systemSocialOrg: _sso, systemHousingEmp: _she, systemHousingOrg: _sho, taxBreakdown: _tb, extraItems: _ei, ...entryData } = calcResult + await prisma.batchEntry.update({ where: { id: entryId }, data: { ...inputs, ...entryData, extraItems: calcResult.extraItems && Object.keys(calcResult.extraItems).length > 0 ? calcResult.extraItems : undefined } }) result.updated++ } catch (e: any) { result.errors.push(`第${i + 2}行:${e?.message || '导入失败'}`) diff --git a/backend/src/routes/payroll.routes.ts b/backend/src/routes/payroll.routes.ts index 0fd5ca4..b6f024d 100644 --- a/backend/src/routes/payroll.routes.ts +++ b/backend/src/routes/payroll.routes.ts @@ -790,11 +790,11 @@ router.post('/overtime/import-to-batch/:batchId', async (req: AuthRequest, res: 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 + const calcResult = await calcBatchEntry(orgId, ot.employeeId, batch.month, inputs, batch.type, undefined, (entry.extraItems as Record) || null) + const { systemSocialEmp, systemSocialOrg, systemHousingEmp, systemHousingOrg, systemSupplementaryHousingEmp, systemSupplementaryHousingOrg, taxBreakdown, extraItems: _ei, ...entryData } = calcResult await prisma.batchEntry.update({ where: { id: entry.id }, - data: entryData, + data: { ...entryData, extraItems: calcResult.extraItems && Object.keys(calcResult.extraItems).length > 0 ? calcResult.extraItems : undefined }, }) } diff --git a/backend/src/routes/payroll2.routes.ts b/backend/src/routes/payroll2.routes.ts index ec09c7d..29ab467 100644 --- a/backend/src/routes/payroll2.routes.ts +++ b/backend/src/routes/payroll2.routes.ts @@ -80,12 +80,12 @@ async function recalcEntry(orgId: string, batchId: string, batchMonth: string, b } const prevDeferred = await getPrevDeferred(orgId, employeeId, batchMonth) - const calcResult = await calcBatchEntry(orgId, employeeId, batchMonth, inputs, batchType as any, { prevDeferred }) + const calcResult = await calcBatchEntry(orgId, employeeId, batchMonth, inputs, batchType as any, { prevDeferred }, (entry.extraItems as Record) || null) - const { systemSocialEmp, systemSocialOrg, systemHousingEmp, systemHousingOrg, systemSupplementaryHousingEmp, systemSupplementaryHousingOrg, taxBreakdown, ...entryData } = calcResult + const { systemSocialEmp, systemSocialOrg, systemHousingEmp, systemHousingOrg, systemSupplementaryHousingEmp, systemSupplementaryHousingOrg, taxBreakdown, extraItems: _ei, ...entryData } = calcResult await prisma.batchEntry.update({ where: { id: entryId }, - data: entryData, + data: { ...entryData, extraItems: calcResult.extraItems && Object.keys(calcResult.extraItems).length > 0 ? calcResult.extraItems : undefined }, }) } @@ -652,6 +652,7 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti prevDeferredHousingEmp: calcResult.prevDeferredHousingEmp || 0, prevDeferredMinWage: calcResult.prevDeferredMinWage || 0, riskWarnings, + extraItems: calcResult.extraItems && Object.keys(calcResult.extraItems).length > 0 ? calcResult.extraItems : undefined, }, }) entries.push(entry) @@ -688,7 +689,8 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti } }) -// 编辑批次条目(计算依据项 + 社保公积金手动覆盖) +// 编辑批次条目(计算依据项 + 社保公积金手动覆盖 + 自定义模板项) +// 预置字段通过固定 schema 校验,自定义字段通过模板 isEditable 动态校验 const updateEntrySchema = z.object({ baseSalary: z.number().optional(), performanceSalary: z.number().optional(), @@ -700,7 +702,16 @@ const updateEntrySchema = z.object({ socialOrg: z.number().optional(), housingEmp: z.number().optional(), housingOrg: z.number().optional(), -}) + positionSalary: z.number().optional(), + senioritySalary: z.number().optional(), + transportAllowance: z.number().optional(), + mealAllowance: z.number().optional(), + housingAllowance: z.number().optional(), + communicationAllowance: z.number().optional(), + otherDeduction: z.number().optional(), + // 自定义字段通过 extraItems 传递 + extraItems: z.record(z.string(), z.number()).optional(), +}).passthrough() router.put('/batches/:batchId/entries/:employeeId', async (req: AuthRequest, res: Response, next: NextFunction) => { try { @@ -717,6 +728,22 @@ router.put('/batches/:batchId/entries/:employeeId', async (req: AuthRequest, res }) if (!entry) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '条目不存在' } }) + // 获取模板,确定哪些自定义字段可编辑 + const templateItems = await getTemplate(orgId) + const presetCodes = new Set(['baseSalary', 'performanceSalary', 'overtimePay', 'allowance', 'deduction', 'bonus', 'socialEmp', 'socialOrg', 'housingEmp', 'housingOrg', 'positionSalary', 'senioritySalary', 'transportAllowance', 'mealAllowance', 'housingAllowance', 'communicationAllowance', 'otherDeduction']) + const customEditableCodes = new Set(templateItems.filter(i => i.isEditable && !presetCodes.has(i.code)).map(i => i.code)) + + // 合并 extraItems:已有值 + 新传入的可编辑自定义字段 + const existingExtraItems = (entry.extraItems as Record) || {} + const newExtraItems = { ...existingExtraItems } + if (data.extraItems) { + for (const [code, val] of Object.entries(data.extraItems)) { + if (customEditableCodes.has(code)) { + newExtraItems[code] = val + } + } + } + // 合并输入项(包含细化薪资字段,避免编辑时丢失岗位工资/绩效工资等) const inputs = { baseSalary: data.baseSalary ?? entry.baseSalary, @@ -724,14 +751,14 @@ router.put('/batches/:batchId/entries/:employeeId', async (req: AuthRequest, res allowance: data.allowance ?? entry.allowance, deduction: data.deduction ?? entry.deduction, bonus: data.bonus ?? entry.bonus, - positionSalary: (data as any).positionSalary ?? entry.positionSalary ?? undefined, + positionSalary: data.positionSalary ?? entry.positionSalary ?? undefined, performanceSalary: data.performanceSalary ?? entry.performanceSalary ?? undefined, - senioritySalary: (data as any).senioritySalary ?? entry.senioritySalary ?? undefined, - transportAllowance: entry.transportAllowance || undefined, - mealAllowance: entry.mealAllowance || undefined, - housingAllowance: entry.housingAllowance || undefined, - communicationAllowance: entry.communicationAllowance || undefined, - otherDeduction: entry.otherDeduction || undefined, + senioritySalary: data.senioritySalary ?? entry.senioritySalary ?? undefined, + transportAllowance: data.transportAllowance ?? (entry.transportAllowance || undefined), + mealAllowance: data.mealAllowance ?? (entry.mealAllowance || undefined), + housingAllowance: data.housingAllowance ?? (entry.housingAllowance || undefined), + communicationAllowance: data.communicationAllowance ?? (entry.communicationAllowance || undefined), + otherDeduction: data.otherDeduction ?? (entry.otherDeduction || undefined), } // 构建社保覆盖参数(如果请求中包含社保字段) @@ -748,13 +775,15 @@ router.put('/batches/:batchId/entries/:employeeId', async (req: AuthRequest, res const options: any = { prevDeferred } if (Object.keys(overrideSocial).length > 0) options.overrideSocial = overrideSocial - // 重新计算 - const calcResult = await calcBatchEntry(orgId, employeeId, batch.month, inputs, batch.type, options) + // 重新计算(传入 extraItems 供自定义 INPUT 项取值) + const calcResult = await calcBatchEntry(orgId, employeeId, batch.month, inputs, batch.type, options, newExtraItems) - const { systemSocialEmp, systemSocialOrg, systemHousingEmp, systemHousingOrg, systemSupplementaryHousingEmp, systemSupplementaryHousingOrg, taxBreakdown, ...entryData } = calcResult + const { systemSocialEmp, systemSocialOrg, systemHousingEmp, systemHousingOrg, systemSupplementaryHousingEmp, systemSupplementaryHousingOrg, taxBreakdown, extraItems: calcExtraItems, ...entryData } = calcResult + // 合并计算结果中的 extraItems(包含自定义计算项的值) + const finalExtraItems = { ...newExtraItems, ...(calcExtraItems || {}) } const updated = await prisma.batchEntry.update({ where: { id: entry.id }, - data: { ...inputs, ...entryData }, + data: { ...inputs, ...entryData, extraItems: Object.keys(finalExtraItems).length > 0 ? finalExtraItems : undefined }, }) // 更新批次汇总 @@ -1242,12 +1271,13 @@ router.post('/batches/:batchId/employees', async (req: AuthRequest, res: Respons const calcResult = await calcBatchEntry(orgId, employeeId, batch.month, { baseSalary, overtimePay, allowance: 0, deduction: 0, bonus: 0 }, batch.type) const riskWarnings = await getPayrollRiskWarnings(orgId, employeeId) - const { systemSocialEmp: _sse, systemSocialOrg: _sso, systemHousingEmp: _she, systemHousingOrg: _sho, systemSupplementaryHousingEmp: _sshe, systemSupplementaryHousingOrg: _ssho, taxBreakdown: _tb, ...entryData } = calcResult + const { systemSocialEmp: _sse, systemSocialOrg: _sso, systemHousingEmp: _she, systemHousingOrg: _sho, systemSupplementaryHousingEmp: _sshe, systemSupplementaryHousingOrg: _ssho, taxBreakdown: _tb, extraItems: _ei, ...entryData } = calcResult const entry = await prisma.batchEntry.create({ data: { batchId, orgId, employeeId, baseSalary, overtimePay, allowance: 0, deduction: 0, bonus: 0, ...entryData, riskWarnings, + extraItems: calcResult.extraItems && Object.keys(calcResult.extraItems).length > 0 ? calcResult.extraItems : undefined, }, }) results.push(entry) @@ -1384,11 +1414,11 @@ router.post('/batches/:batchId/archive', async (req: AuthRequest, res: Response, const prevDeferred = await getPrevDeferred(orgId, entry.employeeId, batch.month) const options: any = { prevDeferred } - const calcResult = await calcBatchEntry(orgId, entry.employeeId, batch.month, inputs, batch.type, options) - const { systemSocialEmp: _sse, systemSocialOrg: _sso, systemHousingEmp: _she, systemHousingOrg: _sho, systemSupplementaryHousingEmp: _sshe, systemSupplementaryHousingOrg: _ssho, taxBreakdown: _tb, ...entryData } = calcResult + const calcResult = await calcBatchEntry(orgId, entry.employeeId, batch.month, inputs, batch.type, options, (entry.extraItems as Record) || null) + const { systemSocialEmp: _sse, systemSocialOrg: _sso, systemHousingEmp: _she, systemHousingOrg: _sho, systemSupplementaryHousingEmp: _sshe, systemSupplementaryHousingOrg: _ssho, taxBreakdown: _tb, extraItems: _ei, ...entryData } = calcResult await prisma.batchEntry.update({ where: { id: entry.id }, - data: { ...entryData }, + data: { ...entryData, extraItems: calcResult.extraItems && Object.keys(calcResult.extraItems).length > 0 ? calcResult.extraItems : undefined }, }) } catch (e: any) { recalcErrors.push(`${entry.employeeId}: ${e?.message || '重算失败'}`) diff --git a/backend/src/services/payroll.service.ts b/backend/src/services/payroll.service.ts index 517c9db..85107e2 100644 --- a/backend/src/services/payroll.service.ts +++ b/backend/src/services/payroll.service.ts @@ -401,9 +401,10 @@ export async function calcBatchEntry( orgId: string, employeeId: string, month: string, - inputs: { baseSalary: number; overtimePay: number; allowance: number; deduction: number; bonus: number; positionSalary?: number; performanceSalary?: number; senioritySalary?: number; transportAllowance?: number; mealAllowance?: number; housingAllowance?: number; communicationAllowance?: number; otherDeduction?: number }, + inputs: { baseSalary: number; overtimePay: number; allowance: number; deduction: number; bonus: number; positionSalary?: number; performanceSalary?: number; senioritySalary?: number; transportAllowance?: number; mealAllowance?: number; housingAllowance?: number; communicationAllowance?: number; otherDeduction?: number; [key: string]: any }, batchType: string = 'REGULAR', options?: { skipSocial?: boolean; overrideSocial?: { socialEmp?: number; socialOrg?: number; housingEmp?: number; housingOrg?: number }; prevDeferred?: { socialEmp?: number; housingEmp?: number; minWage?: number } }, + extraItems?: Record | null, ) { const employee = await prisma.employee.findFirst({ where: { id: employeeId, orgId }, @@ -469,10 +470,10 @@ export async function calcBatchEntry( _socialCalculated: false, } - // 1. 初始化输入项 + // 1. 初始化输入项:先从固定列 inputs 取,再从 extraItems 补充自定义项 for (const item of templateItems) { if (item.type === 'INPUT') { - ctx.values[item.code] = (inputs as any)[item.code] || 0 + ctx.values[item.code] = (inputs as any)[item.code] ?? (extraItems?.[item.code] ?? 0) } } @@ -601,6 +602,15 @@ export async function calcBatchEntry( } } + // 提取非预置项的计算结果到 extraItems + const presetCodes = new Set(DEFAULT_ITEMS.map(i => i.code)) + const resultExtraItems: Record = {} + for (const item of templateItems) { + if (!presetCodes.has(item.code) && ctx.values[item.code] !== undefined) { + resultExtraItems[item.code] = Math.round((ctx.values[item.code] || 0) * 100) / 100 + } + } + return { socialEmp: Math.round(socialEmp * 100) / 100, socialOrg: Math.round(socialOrg * 100) / 100, @@ -627,6 +637,7 @@ export async function calcBatchEntry( prevDeferredSocialEmp: options?.prevDeferred?.socialEmp || 0, prevDeferredHousingEmp: options?.prevDeferred?.housingEmp || 0, prevDeferredMinWage, + extraItems: resultExtraItems, } } diff --git a/frontend/src/pages/money/BatchTab.tsx b/frontend/src/pages/money/BatchTab.tsx index 7e806a3..93eb967 100644 --- a/frontend/src/pages/money/BatchTab.tsx +++ b/frontend/src/pages/money/BatchTab.tsx @@ -750,6 +750,9 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void : ['baseSalary', 'performanceSalary', 'overtimePay', 'allowance', 'deduction', 'bonus', 'socialEmp', 'housingEmp', 'socialOrg', 'housingOrg'] })() + // 预置字段集合(固定列存储) + const PRESET_FIELDS = new Set(['baseSalary', 'performanceSalary', 'overtimePay', 'allowance', 'deduction', 'bonus', 'socialEmp', 'socialOrg', 'housingEmp', 'housingOrg', 'positionSalary', 'senioritySalary', 'transportAllowance', 'mealAllowance', 'housingAllowance', 'communicationAllowance', 'otherDeduction']) + // 点击单元格进入编辑 const startEdit = (employeeId: string, field: string, currentValue: number) => { if (isArchived || !editableFields.includes(field)) return @@ -765,15 +768,20 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void const entry = batch.entries.find((e: any) => e.employeeId === employeeId) if (!entry) { setEditCell(null); return } const data: any = {} - // 输入项字段一起提交 - const inputFields = isBonus ? ['bonus'] : ['baseSalary', 'performanceSalary', 'overtimePay', 'allowance', 'deduction', 'bonus'] - inputFields.forEach(f => { - data[f] = f === field ? numValue : entry[f] - }) // 社保公积金字段:编辑哪个提交哪个 - const socialFields = ['socialEmp', 'housingEmp', 'socialOrg', 'housingOrg'] + const socialFields = ['socialEmp', 'socialOrg', 'housingEmp', 'housingOrg'] if (socialFields.includes(field)) { data[field] = numValue + } else if (PRESET_FIELDS.has(field)) { + // 预置输入项字段一起提交 + const inputFields = isBonus ? ['bonus'] : (templateItems || []).filter((t: any) => t.type === 'INPUT' && t.isEditable && PRESET_FIELDS.has(t.code)).map((t: any) => t.code) + inputFields.forEach(f => { + data[f] = f === field ? numValue : entry[f] + }) + } else { + // 自定义字段走 extraItems + const existingExtra = entry.extraItems || {} + data.extraItems = { ...existingExtra, [field]: numValue } } updateEntryMutation.mutate({ employeeId, data }) setEditCell(null) @@ -793,7 +801,7 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void const isEditing = editCell?.employeeId === entry.employeeId && editCell?.field === field const canEdit = !isArchived && editableFields.includes(field) - const value = entry[field] || 0 + const value = PRESET_FIELDS.has(field) ? (entry[field] || 0) : ((entry.extraItems || {})[field] || 0) const displayValue = field === 'deduction' && value > 0 ? '-' + fmt(value) : fmt(value) if (isEditing) { @@ -1226,33 +1234,28 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void {/* 人员表格 */} -
- +
+
- - - - - - - - {isBonus && } - {!isBonus && } - - - - - - - - - {!isArchived && } + + + + {/* 动态生成模板项列(排除 netPay,由固定实发列显示) */} + {(templateItems || []).filter((t: any) => (!isBonus || t.code === 'bonus') && t.code !== 'netPay').map((t: any) => ( + + ))} + + + + {!isArchived && } {batch.entries.slice((page - 1) * pageSize, page * pageSize).map((entry: any) => ( - - + - - {renderCell(entry, 'baseSalary')} - {(() => { - const perfVal = entry.performanceSalary || 0 - const grade = entry.perfGrade || null - const coef = entry.perfCoefficient != null ? entry.perfCoefficient : null - // 颜色:A(1.2)=绿色, C(0.8)=橙色, D(0.6)=红色, B(1.0)/无=默认 - let colorClass = '' - if (coef != null && coef !== 1.0) { - if (coef > 1.0) colorClass = 'text-green-600' - else if (coef >= 0.8) colorClass = 'text-orange-600' - else colorClass = 'text-red-600' - } - const isEditing = editCell?.employeeId === entry.employeeId && editCell?.field === 'performanceSalary' - const canEdit = !isArchived && editableFields.includes('performanceSalary') - if (isEditing) { + {/* 动态生成模板项单元格(排除 netPay) */} + {(templateItems || []).filter((t: any) => (!isBonus || t.code === 'bonus') && t.code !== 'netPay').map((t: any) => { + // 绩效工资:保留特殊渲染(显示绩效等级) + if (t.code === 'performanceSalary') { + const perfVal = entry.performanceSalary || 0 + const grade = entry.perfGrade || null + const coef = entry.perfCoefficient != null ? entry.perfCoefficient : null + let colorClass = '' + if (coef != null && coef !== 1.0) { + if (coef > 1.0) colorClass = 'text-green-600' + else if (coef >= 0.8) colorClass = 'text-orange-600' + else colorClass = 'text-red-600' + } + const isEditing = editCell?.employeeId === entry.employeeId && editCell?.field === 'performanceSalary' + const canEdit = !isArchived && editableFields.includes('performanceSalary') + if (isEditing) { + return ( + + ) + } return ( - ) } - return ( - - ) - })()} - {renderCell(entry, 'overtimePay')} - {renderCell(entry, 'allowance')} - {renderCell(entry, 'bonus')} - {renderCell(entry, 'deduction')} - {/* 计算项 - 灰显 */} - - {renderCell(entry, 'socialEmp')} - {renderCell(entry, 'housingEmp')} - + // 个税:保留点击查看明细 + if (t.code === 'tax') { + return ( + + ) + } + // 其他项:通用渲染 + return renderCell(entry, t.code, t.type === 'CALCULATED' ? 'font-medium text-gray-600' : '') + })} {/* 实发:最低工资保护触发时高亮显示 */} - {/* 递延扣款明细 */} - - {!isArchived && ( -
员工合同类型基本工资绩效工资加班费津贴奖金奖金扣款应发社保(个人)公积金(个人)个税实发递延扣款风险操作
员工合同类型 + {t.name} + 实发递延扣款风险操作
+
{entry.employee.name}
{entry.employee.idCardNumber && {entry.employee.idCardNumber}} @@ -1262,7 +1265,7 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void )}
+ {(() => { const ct = entry.employee.contracts?.[0]?.contractType const cfg: Record = { @@ -1285,75 +1288,78 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void ) })()} + setEditValue(e.target.value)} + onBlur={handleBlur} + onKeyDown={handleKeyDown} + /> + - setEditValue(e.target.value)} - onBlur={handleBlur} - onKeyDown={handleKeyDown} - /> + canEdit && startEdit(entry.employeeId, 'performanceSalary', perfVal)} + > +
+ {canEdit ? ( + + {fmt(perfVal)} + + ) : ( + {fmt(perfVal)} + )} + {grade && ( + + {grade}·{coef != null ? coef.toFixed(1) : '1.0'} + + )} +
canEdit && startEdit(entry.employeeId, 'performanceSalary', perfVal)} - > -
- {canEdit ? ( - - {fmt(perfVal)} - - ) : ( - {fmt(perfVal)} - )} - {grade && ( - - {grade}·{coef != null ? coef.toFixed(1) : '1.0'} - - )} -
-
{fmt(entry.totalPay)} handleTaxDetailClick(entry.employeeId, entry.employee.name)} title="点击查看个税计算明细">{fmt(entry.tax)} handleTaxDetailClick(entry.employeeId, entry.employee.name)} title="点击查看个税计算明细"> + {fmt(entry.tax)} + 0 ? 'text-amber-600' : 'text-safe'}`} title={(entry.minWageApplied || 0) > 0 ? `最低工资保护已触发\n应发 ¥${fmt(entry.totalPay)} → 实发补齐到 ¥${fmt(entry.minWage)}\n免扣社保 ¥${fmt(entry.deferredSocialEmp || 0)} + 免扣公积金 ¥${fmt(entry.deferredHousingEmp || 0)} + 额外补齐 ¥${fmt(entry.deferredMinWage || 0)} = 递延 ¥${fmt((entry.deferredSocialEmp || 0) + (entry.deferredHousingEmp || 0) + (entry.deferredMinWage || 0))}(次月补扣)` : ''}> + 0 ? 'text-amber-600' : 'text-safe'}`} style={{ width: 110, right: isArchived ? 160 : 210 }} title={(entry.minWageApplied || 0) > 0 ? `最低工资保护已触发\n应发 ¥${fmt(entry.totalPay)} → 实发补齐到 ¥${fmt(entry.minWage)}\n免扣社保 ¥${fmt(entry.deferredSocialEmp || 0)} + 免扣公积金 ¥${fmt(entry.deferredHousingEmp || 0)} + 额外补齐 ¥${fmt(entry.deferredMinWage || 0)} = 递延 ¥${fmt((entry.deferredSocialEmp || 0) + (entry.deferredHousingEmp || 0) + (entry.deferredMinWage || 0))}(次月补扣)` : ''}> {fmt(entry.netPay)} {(entry.minWageApplied || 0) > 0 && } + {(() => { const deferred = (entry.deferredSocialEmp || 0) + (entry.deferredHousingEmp || 0) + (entry.deferredMinWage || 0) const prevDeferred = (entry.prevDeferredSocialEmp || 0) + (entry.prevDeferredHousingEmp || 0) + (entry.prevDeferredMinWage || 0) @@ -1366,7 +1372,7 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void return })()} + {entry.riskWarnings && entry.riskWarnings.length > 0 ? ( @@ -1380,7 +1386,7 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void )} +