diff --git a/backend/src/routes/payroll2.routes.ts b/backend/src/routes/payroll2.routes.ts index 9cb42c3..661abc0 100644 --- a/backend/src/routes/payroll2.routes.ts +++ b/backend/src/routes/payroll2.routes.ts @@ -162,6 +162,8 @@ router.get('/template', async (req: AuthRequest, res: Response, next: NextFuncti const updateTemplateItemSchema = z.object({ name: z.string().min(1).optional(), formula: z.string().nullable().optional(), + calcType: z.enum(['FORMULA', 'SYSTEM']).optional(), + dependencies: z.string().optional(), order: z.number().int().optional(), isEditable: z.boolean().optional(), }) @@ -179,6 +181,8 @@ router.put('/template/:id', async (req: AuthRequest, res: Response, next: NextFu if (data.formula !== undefined) updateData.formula = data.formula if (data.order !== undefined) updateData.order = data.order if (data.isEditable !== undefined) updateData.isEditable = data.isEditable + if (data.calcType !== undefined) updateData.calcType = data.calcType + if (data.dependencies !== undefined) updateData.dependencies = data.dependencies const updated = await prisma.payslipItem.update({ where: { id: req.params.id }, data: updateData }) res.json({ success: true, data: updated }) @@ -193,6 +197,8 @@ const createTemplateItemSchema = z.object({ code: z.string().min(1), type: z.enum(['INPUT', 'CALCULATED']), formula: z.string().nullable().optional(), + calcType: z.enum(['FORMULA', 'SYSTEM']).default('FORMULA'), + dependencies: z.string().default('[]'), order: z.number().int().default(99), isEditable: z.boolean().default(true), }) diff --git a/backend/src/services/payroll.service.ts b/backend/src/services/payroll.service.ts index cc9f31a..f829586 100644 --- a/backend/src/services/payroll.service.ts +++ b/backend/src/services/payroll.service.ts @@ -205,6 +205,177 @@ export function calcBonusTax(bonusAmount: number): number { return Math.max(0, Math.round(tax * 100) / 100) } +// ========== 系统计算器注册表 ========== + +interface SystemCalcContext { + orgId: string + employeeId: string + month: string + batchType: string + employee: any + socialConfig: any + housingConfig: any + supplementaryHousingConfig: any + socialBase: number + housingBase: number + isNoSocialContract: boolean + options: any + values: Record + // Side-effect outputs (org-side and supplementary amounts not in template) + socialOrg: number + housingOrg: number + supplementaryHousingEmp: number + supplementaryHousingOrg: number + taxBreakdown: any + // System values (before override) + systemSocialEmp: number + systemSocialOrg: number + systemHousingEmp: number + systemHousingOrg: number + systemSuppHousingEmp: number + systemSuppHousingOrg: number + // Internal cache flag + _socialCalculated: boolean +} + +type SystemCalcFn = (ctx: SystemCalcContext) => Promise + +/** + * 社保+公积金联合计算(缓存结果,避免重复查询数据库) + */ +async function ensureSocialHousingCalculated(ctx: SystemCalcContext) { + if (ctx._socialCalculated) return + ctx._socialCalculated = true + + const { orgId, employeeId, month, batchType, socialConfig, housingConfig, supplementaryHousingConfig, socialBase, housingBase, isNoSocialContract, options } = ctx + + let socialEmp = 0, socialOrg = 0, housingEmp = 0, housingOrg = 0 + let suppHousingEmp = 0, suppHousingOrg = 0 + + if (batchType !== 'BONUS' && batchType !== 'SEVERANCE' && !options?.skipSocial && !isNoSocialContract) { + let fullSocialEmp = 0, fullSocialOrg = 0, fullHousingEmp = 0, fullHousingOrg = 0 + let fullSuppHousingEmp = 0, fullSuppHousingOrg = 0 + if (socialConfig) { + const social = calcSocialInsurance(socialBase, socialConfig) + fullSocialEmp = social.socialEmp + fullSocialOrg = social.socialOrg + } + if (housingConfig) { + const housing = calcHousingFund(housingBase, housingConfig) + fullHousingEmp = housing.housingEmp + fullHousingOrg = housing.housingOrg + } + if (supplementaryHousingConfig) { + const suppHousing = calcHousingFund(housingBase, supplementaryHousingConfig) + fullSuppHousingEmp = suppHousing.housingEmp + fullSuppHousingOrg = suppHousing.housingOrg + } + + const archivedEntries = await prisma.batchEntry.findMany({ + where: { orgId, employeeId, batch: { month, status: 'ARCHIVED', type: { in: ['REGULAR', 'TERMINATION'] } } }, + select: { socialEmp: true, socialOrg: true, housingEmp: true, housingOrg: true, supplementaryHousingEmp: true, supplementaryHousingOrg: true }, + }) + const deductedSocialEmp = archivedEntries.reduce((s, e) => s + e.socialEmp, 0) + const deductedSocialOrg = archivedEntries.reduce((s, e) => s + e.socialOrg, 0) + const deductedHousingEmp = archivedEntries.reduce((s, e) => s + e.housingEmp, 0) + const deductedHousingOrg = archivedEntries.reduce((s, e) => s + e.housingOrg, 0) + const deductedSuppHousingEmp = archivedEntries.reduce((s, e) => s + (e.supplementaryHousingEmp || 0), 0) + const deductedSuppHousingOrg = archivedEntries.reduce((s, e) => s + (e.supplementaryHousingOrg || 0), 0) + + socialEmp = Math.max(0, fullSocialEmp - deductedSocialEmp) + socialOrg = Math.max(0, fullSocialOrg - deductedSocialOrg) + housingEmp = Math.max(0, fullHousingEmp - deductedHousingEmp) + housingOrg = Math.max(0, fullHousingOrg - deductedHousingOrg) + suppHousingEmp = Math.max(0, fullSuppHousingEmp - deductedSuppHousingEmp) + suppHousingOrg = Math.max(0, fullSuppHousingOrg - deductedSuppHousingOrg) + + if (options?.prevDeferred) { + socialEmp += options.prevDeferred.socialEmp || 0 + housingEmp += options.prevDeferred.housingEmp || 0 + } + } + + ctx.values.socialEmp = socialEmp + ctx.values.housingEmp = housingEmp + ctx.socialOrg = socialOrg + ctx.housingOrg = housingOrg + ctx.supplementaryHousingEmp = suppHousingEmp + ctx.supplementaryHousingOrg = suppHousingOrg + ctx.systemSocialEmp = socialEmp + ctx.systemSocialOrg = socialOrg + ctx.systemHousingEmp = housingEmp + ctx.systemHousingOrg = housingOrg + ctx.systemSuppHousingEmp = suppHousingEmp + ctx.systemSuppHousingOrg = suppHousingOrg +} + +/** + * 个税系统计算函数 + */ +async function calcTaxSystem(ctx: SystemCalcContext): Promise { + const { orgId, employeeId, month, batchType, employee, values } = ctx + const totalPay = values.totalPay || 0 + const socialEmp = values.socialEmp || 0 + const housingEmp = values.housingEmp || 0 + const suppHousingEmp = ctx.supplementaryHousingEmp + + if (batchType === 'BONUS') { + const tax = calcBonusTax(values.bonus || 0) + ctx.taxBreakdown = { method: '单独计税(年终奖)', bonus: values.bonus || 0, tax } + return tax + } + + const year = month.slice(0, 4) + const archivedEntries = await prisma.batchEntry.findMany({ + where: { orgId, employeeId, batch: { month: { startsWith: year }, status: 'ARCHIVED' } }, + select: { totalPay: true, socialEmp: true, housingEmp: true, supplementaryHousingEmp: true, tax: true }, + }) + const ytdIncome = archivedEntries.reduce((s, e) => s + e.totalPay, 0) + totalPay + const ytdSocialEmp = archivedEntries.reduce((s, e) => s + e.socialEmp, 0) + socialEmp + const ytdHousingEmp = archivedEntries.reduce((s, e) => s + e.housingEmp + (e.supplementaryHousingEmp || 0), 0) + housingEmp + suppHousingEmp + + const deductionRecords = await prisma.specialDeductionRecord.findMany({ + where: { orgId, employeeId, month: { startsWith: year, lte: month } }, + select: { amount: true, month: true }, + }) + let ytdSpecialDeduction: number + if (deductionRecords.length > 0) { + ytdSpecialDeduction = deductionRecords.reduce((s, r) => s + r.amount, 0) + } else { + ytdSpecialDeduction = employee.specialDeduction * Number(month.slice(5, 7)) + } + + const ytdTaxDeducted = archivedEntries.reduce((s, e) => s + e.tax, 0) + const deductionAmount = 5000 * Number(month.slice(5, 7)) + const ytdTaxableIncome = Math.max(0, ytdIncome - deductionAmount - ytdSocialEmp - ytdHousingEmp - ytdSpecialDeduction) + const tax = calcCumulativeTax(ytdTaxableIncome, ytdTaxDeducted) + ctx.taxBreakdown = { + method: '累计预扣法', + month: Number(month.slice(5, 7)), + ytdIncome, deductionAmount, ytdSocialEmp, ytdHousingEmp, ytdSpecialDeduction, + specialDeductionSource: deductionRecords.length > 0 ? '按月记录' : '便捷字段', + specialDeductionRecords: deductionRecords.length, + ytdTaxableIncome, ytdTaxDeducted, currentMonthTax: tax, archivedCount: archivedEntries.length, + } + return tax +} + +/** + * 系统计算器注册表 + * key 对应 PayslipItem.formula 中的 SYSTEM 标识 + */ +const systemCalculators: Record = { + SOCIAL_EMP: async (ctx) => { + await ensureSocialHousingCalculated(ctx) + return ctx.values.socialEmp + }, + HOUSING_EMP: async (ctx) => { + await ensureSocialHousingCalculated(ctx) + return ctx.values.housingEmp + }, + TAX: calcTaxSystem, +} + // ========== 批次计算 ========== export async function calcBatchEntry( @@ -261,179 +432,72 @@ export async function calcBatchEntry( const socialBase = isNoSocialContract ? 0 : (employee.socialInsBase != null ? employee.socialInsBase : inputs.baseSalary) const housingBase = isNoSocialContract ? 0 : (employee.housingFundBase != null ? employee.housingFundBase : inputs.baseSalary) - let socialEmp = 0, socialOrg = 0, housingEmp = 0, housingOrg = 0 - let suppHousingEmp = 0, suppHousingOrg = 0 - - // 年终奖/奖金批次、补偿金批次、劳务协议/实习协议人员:不扣社保公积金 - // 其他批次:计算当月应缴全额,减去已归档批次已扣金额,差额为本批次应扣 - if (batchType !== 'BONUS' && batchType !== 'SEVERANCE' && !options?.skipSocial && !isNoSocialContract) { - // 1. 计算当月应缴社保公积金全额 - let fullSocialEmp = 0, fullSocialOrg = 0, fullHousingEmp = 0, fullHousingOrg = 0 - let fullSuppHousingEmp = 0, fullSuppHousingOrg = 0 - if (socialConfig) { - const social = calcSocialInsurance(socialBase, socialConfig) - fullSocialEmp = social.socialEmp - fullSocialOrg = social.socialOrg - } - if (housingConfig) { - const housing = calcHousingFund(housingBase, housingConfig) - fullHousingEmp = housing.housingEmp - fullHousingOrg = housing.housingOrg - } - // 补充公积金(用同样的公积金基数) - if (supplementaryHousingConfig) { - const suppHousing = calcHousingFund(housingBase, supplementaryHousingConfig) - fullSuppHousingEmp = suppHousing.housingEmp - fullSuppHousingOrg = suppHousing.housingOrg - } - - // 2. 查询该员工当月已归档批次中已扣的社保公积金金额 - const archivedEntries = await prisma.batchEntry.findMany({ - where: { - orgId, - employeeId, - batch: { month, status: 'ARCHIVED', type: { in: ['REGULAR', 'TERMINATION'] } }, - }, - select: { socialEmp: true, socialOrg: true, housingEmp: true, housingOrg: true, supplementaryHousingEmp: true, supplementaryHousingOrg: true }, - }) - const deductedSocialEmp = archivedEntries.reduce((s, e) => s + e.socialEmp, 0) - const deductedSocialOrg = archivedEntries.reduce((s, e) => s + e.socialOrg, 0) - const deductedHousingEmp = archivedEntries.reduce((s, e) => s + e.housingEmp, 0) - const deductedHousingOrg = archivedEntries.reduce((s, e) => s + e.housingOrg, 0) - const deductedSuppHousingEmp = archivedEntries.reduce((s, e) => s + (e.supplementaryHousingEmp || 0), 0) - const deductedSuppHousingOrg = archivedEntries.reduce((s, e) => s + (e.supplementaryHousingOrg || 0), 0) - - // 3. 本批次应扣 = 应缴全额 - 已扣金额(不足额补扣,足额为0) - socialEmp = Math.max(0, fullSocialEmp - deductedSocialEmp) - socialOrg = Math.max(0, fullSocialOrg - deductedSocialOrg) - housingEmp = Math.max(0, fullHousingEmp - deductedHousingEmp) - housingOrg = Math.max(0, fullHousingOrg - deductedHousingOrg) - suppHousingEmp = Math.max(0, fullSuppHousingEmp - deductedSuppHousingEmp) - suppHousingOrg = Math.max(0, fullSuppHousingOrg - deductedSuppHousingOrg) - - // 4. 叠加上月递延的社保/公积金(入职当月未扣完的部分,本月补扣) - if (options?.prevDeferred) { - socialEmp += options.prevDeferred.socialEmp || 0 - housingEmp += options.prevDeferred.housingEmp || 0 - } - } - - // 保存系统计算值(覆盖前,含递延) - const systemSocialEmp = socialEmp - const systemSocialOrg = socialOrg - const systemHousingEmp = housingEmp - const systemHousingOrg = housingOrg - const systemSupplementaryHousingEmp = suppHousingEmp - const systemSupplementaryHousingOrg = suppHousingOrg - - // 手动覆盖社保值 - if (options?.overrideSocial) { - if (options.overrideSocial.socialEmp !== undefined) socialEmp = options.overrideSocial.socialEmp - if (options.overrideSocial.socialOrg !== undefined) socialOrg = options.overrideSocial.socialOrg - if (options.overrideSocial.housingEmp !== undefined) housingEmp = options.overrideSocial.housingEmp - if (options.overrideSocial.housingOrg !== undefined) housingOrg = options.overrideSocial.housingOrg - } - - // 应发合计:从模板公式计算(默认公式与之前硬编码一致) + // ── 模板驱动计算 ── const templateItems = await getTemplate(orgId) - const totalPayItem = templateItems.find(i => i.code === 'totalPay') - let totalPay: number - if (totalPayItem && totalPayItem.formula) { - const formulaVars: Record = { - baseSalary: inputs.baseSalary, - positionSalary: inputs.positionSalary || 0, - performanceSalary: inputs.performanceSalary || 0, - senioritySalary: inputs.senioritySalary || 0, - overtimePay: inputs.overtimePay, - transportAllowance: inputs.transportAllowance || 0, - mealAllowance: inputs.mealAllowance || 0, - housingAllowance: inputs.housingAllowance || 0, - communicationAllowance: inputs.communicationAllowance || 0, - allowance: inputs.allowance, - bonus: inputs.bonus, - deduction: inputs.deduction, - otherDeduction: inputs.otherDeduction || 0, - } - totalPay = evalFormula(totalPayItem.formula, formulaVars) - } else { - // 回退到硬编码(兼容模板未配置的情况) - totalPay = inputs.baseSalary - + (inputs.positionSalary || 0) - + (inputs.performanceSalary || 0) - + (inputs.senioritySalary || 0) - + inputs.overtimePay - + (inputs.transportAllowance || 0) - + (inputs.mealAllowance || 0) - + (inputs.housingAllowance || 0) - + (inputs.communicationAllowance || 0) - + inputs.allowance - + inputs.bonus - - inputs.deduction - - (inputs.otherDeduction || 0) + + // 构建系统计算上下文 + const ctx: SystemCalcContext = { + orgId, employeeId, month, batchType, employee, + socialConfig, housingConfig, supplementaryHousingConfig, + socialBase, housingBase, isNoSocialContract, options, + values: {}, + socialOrg: 0, housingOrg: 0, + supplementaryHousingEmp: 0, supplementaryHousingOrg: 0, + taxBreakdown: null, + systemSocialEmp: 0, systemSocialOrg: 0, + systemHousingEmp: 0, systemHousingOrg: 0, + systemSuppHousingEmp: 0, systemSuppHousingOrg: 0, + _socialCalculated: false, } - // 个税计算 - let tax = 0 - let taxBreakdown: any = null - if (batchType === 'BONUS') { - // 年终奖单独计税 - tax = calcBonusTax(inputs.bonus) - taxBreakdown = { method: '单独计税(年终奖)', bonus: inputs.bonus, tax } - } else { - // 累计预扣法(补偿金也走累计预扣,但无社保公积金扣除) - const year = month.slice(0, 4) - // 从已归档批次的 BatchEntry 获取当年历史数据(不依赖工资条是否已生成) - const archivedEntries = await prisma.batchEntry.findMany({ - where: { - orgId, - employeeId, - batch: { - month: { startsWith: year }, - status: 'ARCHIVED', - }, - }, - select: { totalPay: true, socialEmp: true, housingEmp: true, supplementaryHousingEmp: true, tax: true }, - }) - const ytdIncome = archivedEntries.reduce((s, e) => s + e.totalPay, 0) + totalPay - const ytdSocialEmp = archivedEntries.reduce((s, e) => s + e.socialEmp, 0) + socialEmp - const ytdHousingEmp = archivedEntries.reduce((s, e) => s + e.housingEmp + (e.supplementaryHousingEmp || 0), 0) + housingEmp + suppHousingEmp - - // 专项附加扣除:按月读取 SpecialDeductionRecord 实际填报金额累加 - // 优先使用按月记录;若无按月记录,回退到员工便捷字段 × 月数(兼容旧数据) - const deductionRecords = await prisma.specialDeductionRecord.findMany({ - where: { orgId, employeeId, month: { startsWith: year, lte: month } }, - select: { amount: true, month: true }, - }) - let ytdSpecialDeduction: number - if (deductionRecords.length > 0) { - // 按月实际填报金额累加 - ytdSpecialDeduction = deductionRecords.reduce((s, r) => s + r.amount, 0) - } else { - // 回退:员工便捷字段 × 月数(兼容未填报按月记录的旧数据) - ytdSpecialDeduction = employee.specialDeduction * Number(month.slice(5, 7)) - } - - const ytdTaxDeducted = archivedEntries.reduce((s, e) => s + e.tax, 0) - const deductionAmount = 5000 * Number(month.slice(5, 7)) - const ytdTaxableIncome = Math.max(0, ytdIncome - deductionAmount - ytdSocialEmp - ytdHousingEmp - ytdSpecialDeduction) - tax = calcCumulativeTax(ytdTaxableIncome, ytdTaxDeducted) - taxBreakdown = { - method: '累计预扣法', - month: Number(month.slice(5, 7)), - ytdIncome, - deductionAmount, - ytdSocialEmp, - ytdHousingEmp, - ytdSpecialDeduction, - specialDeductionSource: deductionRecords.length > 0 ? '按月记录' : '便捷字段', - specialDeductionRecords: deductionRecords.length, - ytdTaxableIncome, - ytdTaxDeducted, - currentMonthTax: tax, - archivedCount: archivedEntries.length, + // 1. 初始化输入项 + for (const item of templateItems) { + if (item.type === 'INPUT') { + ctx.values[item.code] = (inputs as any)[item.code] || 0 } } + // 2. 按拓扑排序计算所有项 + const sorted = topologicalSort(templateItems) + for (const item of sorted) { + if (item.type === 'INPUT') continue + + // 在计算 tax 前应用手动覆盖(覆盖社保值) + if (item.code === 'tax' && options?.overrideSocial) { + if (options.overrideSocial.socialEmp !== undefined) ctx.values.socialEmp = options.overrideSocial.socialEmp + if (options.overrideSocial.housingEmp !== undefined) ctx.values.housingEmp = options.overrideSocial.housingEmp + if (options.overrideSocial.socialOrg !== undefined) ctx.socialOrg = options.overrideSocial.socialOrg + if (options.overrideSocial.housingOrg !== undefined) ctx.housingOrg = options.overrideSocial.housingOrg + } + + if (item.calcType === 'SYSTEM' && item.formula) { + const fn = systemCalculators[item.formula] + if (fn) { + ctx.values[item.code] = await fn(ctx) + } + } else if (item.formula) { + ctx.values[item.code] = evalFormula(item.formula, ctx.values) + } + } + + // 3. 提取计算结果 + let socialEmp = ctx.values.socialEmp || 0 + let socialOrg = ctx.socialOrg + let housingEmp = ctx.values.housingEmp || 0 + let housingOrg = ctx.housingOrg + let suppHousingEmp = ctx.supplementaryHousingEmp + let suppHousingOrg = ctx.supplementaryHousingOrg + const systemSocialEmp = ctx.systemSocialEmp + const systemSocialOrg = ctx.systemSocialOrg + const systemHousingEmp = ctx.systemHousingEmp + const systemHousingOrg = ctx.systemHousingOrg + const systemSupplementaryHousingEmp = ctx.systemSuppHousingEmp + const systemSupplementaryHousingOrg = ctx.systemSuppHousingOrg + let tax = ctx.values.tax || 0 + const taxBreakdown = ctx.taxBreakdown + let totalPay = ctx.values.totalPay || 0 + let netPay = ctx.values.netPay || 0 + // ── 最低工资保护 + 递延扣款逻辑 ── // 读取最低工资标准(从年度标准或旧配置表) let minWage = 0 @@ -444,21 +508,8 @@ export async function calcBatchEntry( // 上月递延的最低工资补齐差额(本批次需扣回) const prevDeferredMinWage = options?.prevDeferred?.minWage || 0 - // 初始实发:优先从模板公式计算,再减去补充公积金和递延项(模板公式不含这两个系统内部字段) - const netPayItem = templateItems.find(i => i.code === 'netPay') - let netPay: number - if (netPayItem && netPayItem.formula) { - const formulaVars: Record = { - totalPay, - socialEmp, - housingEmp, - tax, - } - netPay = evalFormula(netPayItem.formula, formulaVars) - suppHousingEmp - prevDeferredMinWage - } else { - // 回退到硬编码 - netPay = totalPay - socialEmp - housingEmp - suppHousingEmp - tax - prevDeferredMinWage - } + // netPay 已由模板公式计算(公式中不含补充公积金和递延项),此处补扣 + netPay = netPay - suppHousingEmp - prevDeferredMinWage // 递延金额(本批次扣不动、递延到次月的部分) let deferredSocialEmp = 0 diff --git a/frontend/src/pages/money/BatchTab.tsx b/frontend/src/pages/money/BatchTab.tsx index b12efcb..d3fa294 100644 --- a/frontend/src/pages/money/BatchTab.tsx +++ b/frontend/src/pages/money/BatchTab.tsx @@ -497,6 +497,13 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void }, }) + const { data: templateItems } = useQuery({ + queryKey: ['payslip-template'], + queryFn: async () => { + return await payrollApi.template() + }, + }) + const updateEntryMutation = useMutation({ mutationFn: ({ employeeId, data }: { employeeId: string; data: any }) => payrollApi.updateBatchEntry(batchId, employeeId, data), @@ -729,10 +736,19 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void } } - // 可编辑的输入项字段 - const editableFields = isBonus - ? ['bonus'] - : ['baseSalary', 'performanceSalary', 'overtimePay', 'allowance', 'deduction', 'bonus', 'socialEmp', 'housingEmp', 'socialOrg', 'housingOrg'] + // 可编辑的输入项字段:从模板动态获取,回退到硬编码 + const editableFields = (() => { + if (templateItems && templateItems.length > 0) { + const editable = templateItems.filter((i: any) => i.isEditable).map((i: any) => i.code) + // 奖金批次只允许编辑 bonus + if (isBonus) return ['bonus'] + // 社保/公积金允许手动覆盖 + return [...editable, 'socialEmp', 'housingEmp', 'socialOrg', 'housingOrg'] + } + return isBonus + ? ['bonus'] + : ['baseSalary', 'performanceSalary', 'overtimePay', 'allowance', 'deduction', 'bonus', 'socialEmp', 'housingEmp', 'socialOrg', 'housingOrg'] + })() // 点击单元格进入编辑 const startEdit = (employeeId: string, field: string, currentValue: number) => { diff --git a/frontend/src/pages/money/TemplateTab.tsx b/frontend/src/pages/money/TemplateTab.tsx index 4b87c80..30ccb4e 100644 --- a/frontend/src/pages/money/TemplateTab.tsx +++ b/frontend/src/pages/money/TemplateTab.tsx @@ -23,6 +23,7 @@ export function TemplateManager() { code: '', type: 'INPUT' as 'INPUT' | 'CALCULATED', formula: '', + calcType: 'FORMULA' as 'FORMULA' | 'SYSTEM', order: 99, isEditable: true, }) @@ -67,7 +68,7 @@ export function TemplateManager() { /** 打开新增表单 */ const handleAdd = () => { setEditingItem(null) - setForm({ name: '', code: '', type: 'INPUT', formula: '', order: items?.length ? items.length + 1 : 99, isEditable: true }) + setForm({ name: '', code: '', type: 'INPUT', formula: '', calcType: 'FORMULA', order: items?.length ? items.length + 1 : 99, isEditable: true }) setShowForm(true) } @@ -79,6 +80,7 @@ export function TemplateManager() { code: item.code, type: item.type, formula: item.formula || '', + calcType: item.calcType || 'FORMULA', order: item.order, isEditable: item.isEditable, }) @@ -91,10 +93,11 @@ export function TemplateManager() { toast.error('名称和字段代码不能为空') return } - const payload = { + const payload: any = { name: form.name.trim(), type: form.type, formula: form.type === 'CALCULATED' ? form.formula.trim() || null : null, + calcType: form.type === 'CALCULATED' ? form.calcType : 'FORMULA', order: Number(form.order), isEditable: form.isEditable, } @@ -128,6 +131,7 @@ export function TemplateManager() { 名称 字段代码 类型 + 计算方式 计算公式 可编辑 操作 @@ -144,6 +148,13 @@ export function TemplateManager() { {item.type === 'INPUT' ? '输入项' : '计算项'} + + {item.type === 'CALCULATED' && ( + + {(item.calcType || 'FORMULA') === 'SYSTEM' ? '系统计算' : '公式计算'} + + )} + {item.formula || '—'} @@ -240,15 +251,36 @@ export function TemplateManager() { {form.type === 'CALCULATED' && ( -
- - setForm({ ...form, formula: e.target.value })} - placeholder="如:baseSalary + overtimePay + allowance - deduction" - /> -

可引用其他字段代码进行加减乘除运算

+
+
+ + + {editingItem && editingItem.isDefault && ( +

预置项的计算方式不可修改

+ )} +
+
+ + setForm({ ...form, formula: e.target.value })} + placeholder={form.calcType === 'SYSTEM' ? '如:SOCIAL_EMP' : '如:baseSalary + overtimePay + allowance - deduction'} + disabled={!!editingItem && editingItem.isDefault && form.calcType === 'SYSTEM'} + /> +

+ {form.calcType === 'SYSTEM' + ? '系统计算标识:SOCIAL_EMP(个人社保)、HOUSING_EMP(个人公积金)、TAX(个税)' + : '可引用其他字段代码进行加减乘除运算,支持 min()、max()、round() 函数'} +

+
)}