九鸿:薪酬核算预览所有数值列均可手工修改
- 标准工资、岗位工资、加班工资(标准)、安全环保奖(标准)、绩效(标准)、出勤天数/时长、
各项计算结果、合计、税后工资全部改为可编辑,自动计算只作为初值
- 修改某个字段时只重算其影响的合计,不覆盖正在编辑的列
- 出勤天数按整数、时长按 1 位小数、金额按 2 位小数录入
Co-Authored-By: Claude Code <noreply@anthropic.com>
| | |
| | | ]; |
| | | } |
| | | |
| | | /** 预览表格里允许手工调整的字段(其余均为自动计算) */ |
| | | /** |
| | | * 预览表格里可手工调整的字段:所有薪酬数值字段都可在自动计算结果上修改。 |
| | | * 自动计算只负责生成初值,保存时以页面上看到的值为准。 |
| | | */ |
| | | export const PREVIEW_EDITABLE_FIELDS = [ |
| | | 'workDays', |
| | | 'paidAttendanceDays', |
| | | 'standardSalary', |
| | | 'baseSalary', |
| | | 'postSalary', |
| | | 'overtimeHourlyRate', |
| | | 'safetyAwardStandard', |
| | | 'performanceStandard', |
| | | 'standardWorkHours', |
| | | 'overtimeHours', |
| | | 'legalHolidayHours', |
| | | 'standardAttendancePay', |
| | | 'overtimePay', |
| | | 'legalHolidayPay', |
| | | 'safetyAward', |
| | | 'floatingPerformancePay', |
| | | 'payableSalary', |
| | | 'seniorityPay', |
| | | 'nightShiftAllowance', |
| | | 'educationAllowance', |
| | |
| | | 'excellentEmployeeAward', |
| | | 'otherSubsidy', |
| | | 'deptPerformance', |
| | | 'additionTotal', |
| | | 'missedPunchDeduction', |
| | | 'lateEarlyDeduction', |
| | | 'otherDeduction', |
| | | 'deductionTotal', |
| | | 'socialSecurityDeduction', |
| | | 'housingFundDeduction', |
| | | 'taxDeduction', |
| | | 'actualSalary', |
| | | ] as const; |
| | | |
| | | /** 应付工资的构成字段(任一变化都重算应付工资) */ |
| | | export const PREVIEW_PAYABLE_COMPONENTS = [ |
| | | 'standardAttendancePay', |
| | | 'overtimePay', |
| | | 'legalHolidayPay', |
| | | 'safetyAward', |
| | | 'floatingPerformancePay', |
| | | ] as const; |
| | | |
| | | /** 各字段的录入精度(未列出的按金额 2 位小数) */ |
| | | export const PREVIEW_FIELD_PRECISION: Record<string, number> = { |
| | | workDays: 0, |
| | | paidAttendanceDays: 1, |
| | | standardWorkHours: 1, |
| | | overtimeHours: 1, |
| | | legalHolidayHours: 1, |
| | | }; |
| | | |
| | | /** 加项字段(汇总为「加项合计」) */ |
| | | export const PREVIEW_ADDITION_FIELDS = [ |
| | | 'seniorityPay', |
| | |
| | | PREVIEW_ADDITION_FIELDS, |
| | | PREVIEW_DEDUCTION_FIELDS, |
| | | PREVIEW_EDITABLE_FIELDS, |
| | | PREVIEW_FIELD_PRECISION, |
| | | PREVIEW_PAYABLE_COMPONENTS, |
| | | useGridColumns, |
| | | useGridFormSchema, |
| | | usePreviewColumns, |
| | |
| | | } |
| | | } |
| | | |
| | | /** 预览表中按原样展示的字段(非金额) */ |
| | | const PLAIN_FIELDS = [ |
| | | 'userName', |
| | | 'hireDate', |
| | | 'postName', |
| | | 'deptName', |
| | | 'workDays', |
| | | 'paidAttendanceDays', |
| | | 'standardWorkHours', |
| | | 'overtimeHours', |
| | | 'legalHolidayHours', |
| | | ]; |
| | | /** 各字段的录入精度 */ |
| | | function fieldPrecision(field: string) { |
| | | return PREVIEW_FIELD_PRECISION[field] ?? 2; |
| | | } |
| | | |
| | | /** 是否为可手工调整的列 */ |
| | | function isEditableField(field: string) { |
| | |
| | | return (record as unknown as Record<string, number | string | undefined>)[ |
| | | field |
| | | ]; |
| | | } |
| | | |
| | | /** 金额展示:保留两位小数 */ |
| | | function formatAmount(value?: number | string) { |
| | | if (value === null || value === undefined || value === '' || Number.isNaN(Number(value))) { |
| | | return '-'; |
| | | } |
| | | return Number(value).toFixed(2); |
| | | } |
| | | |
| | | /** 求和(空值按 0 计) */ |
| | |
| | | Number(record.floatingPerformancePay ?? 0); |
| | | record.additionTotal = sumFields(record, PREVIEW_ADDITION_FIELDS); |
| | | record.deductionTotal = sumFields(record, PREVIEW_DEDUCTION_FIELDS); |
| | | record.actualSalary = |
| | | record.payableSalary + |
| | | record.additionTotal - |
| | | record.deductionTotal - |
| | | Number(record.socialSecurityDeduction ?? 0) - |
| | | Number(record.housingFundDeduction ?? 0) - |
| | | Number(record.taxDeduction ?? 0); |
| | | record.actualSalary = calculateActualSalary(record); |
| | | } |
| | | |
| | | /** 更新行数据并重算合计 */ |
| | | /** 按当前行数据重算税后工资 */ |
| | | function calculateActualSalary( |
| | | record: HrmSalaryCalculationApi.SalaryCalculation, |
| | | ) { |
| | | return ( |
| | | Number(record.payableSalary ?? 0) + |
| | | Number(record.additionTotal ?? 0) - |
| | | Number(record.deductionTotal ?? 0) - |
| | | Number(record.socialSecurityDeduction ?? 0) - |
| | | Number(record.housingFundDeduction ?? 0) - |
| | | Number(record.taxDeduction ?? 0) |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * 手工修改某个字段后,只重算它影响的合计,不覆盖刚改的那一列: |
| | | * - 改应付工资构成项 → 重算应付工资 |
| | | * - 改加项 → 重算加项合计 |
| | | * - 改扣款 → 重算扣款合计 |
| | | * - 改其余任何金额 → 重算税后工资(改税后工资本身时不再回写) |
| | | */ |
| | | function updateFieldValue( |
| | | record: HrmSalaryCalculationApi.SalaryCalculation, |
| | | field: string, |
| | | value: number, |
| | | ) { |
| | | (record as Record<string, unknown>)[field] = value; |
| | | recalculateRecord(record); |
| | | const payableComponents = PREVIEW_PAYABLE_COMPONENTS as readonly string[]; |
| | | const additionFields = PREVIEW_ADDITION_FIELDS as readonly string[]; |
| | | const deductionFields = PREVIEW_DEDUCTION_FIELDS as readonly string[]; |
| | | if (payableComponents.includes(field)) { |
| | | record.payableSalary = sumFields(record, PREVIEW_PAYABLE_COMPONENTS); |
| | | } |
| | | if (additionFields.includes(field)) { |
| | | record.additionTotal = sumFields(record, PREVIEW_ADDITION_FIELDS); |
| | | } |
| | | if (deductionFields.includes(field)) { |
| | | record.deductionTotal = sumFields(record, PREVIEW_DEDUCTION_FIELDS); |
| | | } |
| | | if (field !== 'actualSalary') { |
| | | record.actualSalary = calculateActualSalary(record); |
| | | } |
| | | } |
| | | |
| | | /** 提交薪酬核算(手动调整后保存) */ |
| | |
| | | :maskClosable="false" |
| | | > |
| | | <Alert |
| | | message="核算流程:选好核算周期与部门后点「预览数据」→ 核对自动计算结果,可手工调整浮动绩效、加项与扣款 → 点「保存核算」落库。标准工资/岗位工资/加班工资(标准)/安全环保奖(标准)/绩效(标准) 在员工档案中维护。" |
| | | message="核算流程:选好核算周期与部门后点「预览数据」→ 系统按员工档案自动算出各项金额,所有数值都可在此基础上手工修改(改某一项会自动重算它影响到的合计)→ 点「保存核算」按页面上的值落库。标准工资/岗位工资/加班工资(标准)/安全环保奖(标准)/绩效(标准) 默认取员工档案。" |
| | | type="info" |
| | | show-icon |
| | | style="margin-bottom: 16px" |
| | |
| | | <InputNumber |
| | | :value="fieldValue(record, String(column.dataIndex)) as number" |
| | | :min="0" |
| | | :precision="2" |
| | | :precision="fieldPrecision(String(column.dataIndex))" |
| | | size="small" |
| | | style="width: 90px" |
| | | style="width: 96px" |
| | | @change=" |
| | | (val: number | string | null) => |
| | | updateFieldValue( |
| | |
| | | " |
| | | /> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'actualSalary'"> |
| | | <span class="font-bold text-green-600">{{ |
| | | formatAmount(record.actualSalary) |
| | | }}</span> |
| | | </template> |
| | | <template v-else-if="PLAIN_FIELDS.includes(String(column.dataIndex))"> |
| | | {{ fieldValue(record, String(column.dataIndex)) ?? '-' }} |
| | | </template> |
| | | <template v-else> |
| | | {{ formatAmount(fieldValue(record, String(column.dataIndex))) }} |
| | | {{ fieldValue(record, String(column.dataIndex)) ?? '-' }} |
| | | </template> |
| | | </template> |
| | | </Table> |