feat: 发薪批次表格动态化 - 根据薪酬模板动态生成列

- schema: BatchEntry 增加 extraItems Json 字段存储自定义模板项
- calcBatchEntry: 新增 extraItems 参数,合并自定义 INPUT 项取值
- 后端编辑接口: 动态校验自定义字段,预置字段走固定列,自定义走 extraItems
- 所有 calcBatchEntry 调用处传入并写回 extraItems
- 前端表头/单元格从 templateItems 动态生成,排除 netPay 重复列
- 前端 saveEdit 动态化: 预置字段走固定列,自定义字段走 extraItems
- renderCell 支持从 extraItems 取值
- 表格列左右锁定: 员工/合同类型左锁,实发/递延/风险/操作右锁
- 合理列宽 + 横向滚动 + hover 效果
This commit is contained in:
freedakgmail
2026-08-19 09:15:54 +08:00
parent 64ae633857
commit 20f920686e
6 changed files with 171 additions and 121 deletions
+14 -3
View File
@@ -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<string, number> | 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<string, number> = {}
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,
}
}