feat: Phase 1-3 优化全部完成

Phase 1 紧急修复(8项):
- 社保城市选择改为可输入
- 社保上下限拆分(三险/医保独立基数)
- 公积金试算结果展示修复
- 花名册合同保存修复(日期ISO格式)
- 薪酬批次创建失败修复(城市过滤+错误处理)
- 证据链查看修复
- 个税计算修复(blank_employees读取基本工资)
- 加班费倍率读取配置

Phase 2 功能完善(3项):
- 批量导入per-row异常捕获+导入按钮
- 单人发薪UI入口优化
- 解除协议模板补充(员工提出离职版)

Phase 3 后期规划(4项):
- 工资表导入功能(POST /import/payroll + 前端入口)
- 大病险/长护险附加险种(extraInsurances JSON + 计算适配)
- 专项附加扣除按月录入(SpecialDeductionRecord模型 + 前端Tab)
- 预置河北省社保政策(seed数据)
This commit is contained in:
selfrelease
2026-07-27 18:55:08 +08:00
parent 034fcc4111
commit 255af519d2
16 changed files with 1464 additions and 77 deletions
+35 -8
View File
@@ -35,10 +35,35 @@ export async function getTemplate(orgId: string) {
// ========== 社保计算 ==========
export function calcSocialInsurance(base: number, config: any) {
// 养老/失业/工伤保险基数
const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax)
const socialEmp = actualBase * (config.pensionEmp + config.medicalEmp + config.unemploymentEmp) / 100
const socialOrg = actualBase * (config.pensionOrg + config.medicalOrg + config.unemploymentOrg + config.injuryOrg + config.maternityOrg) / 100
return { actualBase, socialEmp, socialOrg }
// 医疗/生育保险基数(独立上下限,为 0 时 fallback 到统一基数)
const medMin = config.medicalBaseMin && config.medicalBaseMin > 0 ? config.medicalBaseMin : config.baseMin
const medMax = config.medicalBaseMax && config.medicalBaseMax > 0 ? config.medicalBaseMax : config.baseMax
const medicalBase = Math.min(Math.max(base, medMin), medMax)
let socialEmp = actualBase * (config.pensionEmp + config.unemploymentEmp) / 100 + medicalBase * config.medicalEmp / 100
let socialOrg = actualBase * (config.pensionOrg + config.unemploymentOrg + config.injuryOrg) / 100 + medicalBase * (config.medicalOrg + config.maternityOrg) / 100
// 附加险种(大病险/长护险等)
const extraItems: any[] = []
if (config.extraInsurances && Array.isArray(config.extraInsurances)) {
for (const ins of config.extraInsurances) {
const insBase = ins.baseType === 'medical' ? medicalBase : ins.baseType === 'fixed' ? 1 : actualBase
if (ins.baseType === 'fixed' && ins.fixedAmount) {
const orgAmt = ins.fixedAmount
const empAmt = ins.empFixedAmount || 0
socialOrg += orgAmt
socialEmp += empAmt
extraItems.push({ name: ins.name, orgAmount: orgAmt, empAmount: empAmt })
} else {
const orgAmt = insBase * (ins.orgRate || 0) / 100
const empAmt = insBase * (ins.empRate || 0) / 100
socialOrg += orgAmt
socialEmp += empAmt
extraItems.push({ name: ins.name, orgAmount: orgAmt, empAmount: empAmt })
}
}
}
return { actualBase, medicalBase, socialEmp, socialOrg, extraItems }
}
export function calcHousingFund(base: number, config: any) {
@@ -106,11 +131,14 @@ export async function calcBatchEntry(
batchType: string = 'REGULAR',
options?: { skipSocial?: boolean; overrideSocial?: { socialEmp?: number; socialOrg?: number; housingEmp?: number; housingOrg?: number } },
) {
const [employee, socialConfig, housingConfig] = await Promise.all([
prisma.employee.findFirst({ where: { id: employeeId, orgId } }),
const employee = await prisma.employee.findFirst({ where: { id: employeeId, orgId } })
if (!employee) throw { code: 'NOT_FOUND', message: '员工不存在' }
const cityWhere = employee.city ? { orgId, city: employee.city } : { orgId }
const [socialConfig, housingConfig] = await Promise.all([
prisma.socialInsuranceConfig.findFirst({
where: {
orgId,
...cityWhere,
effectiveFrom: { lte: month },
OR: [{ effectiveTo: null }, { effectiveTo: { gte: month } }],
},
@@ -118,14 +146,13 @@ export async function calcBatchEntry(
}),
prisma.housingFundConfig.findFirst({
where: {
orgId,
...cityWhere,
effectiveFrom: { lte: month },
OR: [{ effectiveTo: null }, { effectiveTo: { gte: month } }],
},
orderBy: { effectiveFrom: 'desc' },
}),
])
if (!employee) throw { code: 'NOT_FOUND', message: '员工不存在' }
// 社保基数:优先用员工核定基数,否则用基本工资
const socialBase = employee.socialInsBase || inputs.baseSalary