feat: 社保公积金独立配置+版本化缴费记录+月度增减员+补偿金批次

- Schema: 拆分社保/公积金配置,新增EmployeeSocialInsRecord/EmployeeHousingFundRecord/DepartmentRecord模型,扩展SalaryChangeRecord,增加SEVERANCE批次类型
- 后端: createEmployee/rehireEmployee接收社保公积金字段并创建缴费记录版本;createTermination/createResignation接收截止年月并关闭缴费记录;调薪/调部门API+版本记录;月度增减员API;公积金独立CRUD/计算/调基;SEVERANCE批次calcBatchEntry
- 前端: AddEmployeeModal/RehireModal增加社保公积金输入;ResignModal/Termination增加截止年月+日期不一致提醒;花名册增加调薪/调部门弹窗;SocialInsurance.tsx Tab拆分(社保/公积金/月度增减员)+CSV导出;Money.tsx增加补偿金批次类型
- 修复: seed.ts移除housingOrg/housingEmp;risk.service.ts从HousingFundConfig获取公积金费率
This commit is contained in:
freedakgmail
2026-07-23 20:02:59 +08:00
parent ad6800b63c
commit 4f125d309b
15 changed files with 2227 additions and 335 deletions
+101 -7
View File
@@ -55,6 +55,7 @@ enum PayrollBatchType {
REGULAR // 常规发薪
TERMINATION // 离职结算
BONUS // 年终奖/奖金
SEVERANCE // 补偿金按月发放(无社保,个税按政策处理)
}
enum PayrollBatchStatus {
@@ -131,6 +132,10 @@ model Organization {
onboardingLinks OnboardingLink[]
confirmLinks ContractConfirmLink[]
socialInsuranceConfig SocialInsuranceConfig[]
housingFundConfigs HousingFundConfig[]
socialInsRecords EmployeeSocialInsRecord[]
housingFundRecords EmployeeHousingFundRecord[]
departmentRecords EmployeeDepartmentRecord[]
notificationSetting NotificationSetting?
overtimeConfig OvertimeConfig?
notificationLogs NotificationLog[]
@@ -178,8 +183,12 @@ model Employee {
isInMedicalPeriod Boolean @default(false)
isWorkInjured Boolean @default(false)
// 薪税扩展
socialInsBase Float? // 社保缴费基数(按人核定
housingFundBase Float? // 公积金缴费基数(按人核定
socialInsBase Float? // 社保缴费基数(便捷字段,由Record同步
housingFundBase Float? // 公积金缴费基数(便捷字段,由Record同步
socialInsStartMonth String? // 当前社保开始年月(便捷字段)
socialInsEndMonth String? // 当前社保截止年月(便捷字段,null=在保)
housingFundStartMonth String? // 当前公积金开始年月(便捷字段)
housingFundEndMonth String? // 当前公积金截止年月(便捷字段)
specialDeduction Float @default(0) // 专项附加扣除(子女教育、赡养老人等,员工portal端填报)
createdBy String
createdAt DateTime @default(now())
@@ -197,6 +206,9 @@ model Employee {
attendanceRecords AttendanceRecord[]
trainingRecords TrainingRecord[]
performanceRecords PerformanceRecord[]
socialInsRecords EmployeeSocialInsRecord[]
housingFundRecords EmployeeHousingFundRecord[]
departmentRecords EmployeeDepartmentRecord[]
}
model LaborContract {
@@ -256,6 +268,8 @@ model TerminationRecord {
terminationDate DateTime
resignationReason String? // 主动离职原因(type=RESIGNATION时使用)
compensation Float @default(0)
socialInsEndMonth String? // 社保截止缴费年月 YYYY-MM
housingFundEndMonth String? // 公积金截止缴费年月 YYYY-MM
riskLevel RiskAssessment @default(SAFE)
checklist Json
remark String?
@@ -315,14 +329,33 @@ model SocialInsuranceConfig {
unemploymentEmp Float @default(0.5) // 失业保险 个人比例 %
injuryOrg Float @default(0.2) // 工伤保险 企业比例 %
maternityOrg Float @default(0.8) // 生育保险 企业比例 %
housingOrg Float @default(12) // 公积金 企业比例 %
housingEmp Float @default(12) // 公积金 个人比例 %
baseMin Float @default(6326) // 缴费基数下限
baseMax Float @default(33891) // 缴费基数上限
baseMin Float @default(6326) // 社保缴费基数下限
baseMax Float @default(33891) // 社保缴费基数上限
effectiveFrom String // 生效月份 YYYY-MM
effectiveTo String? // 失效月份 YYYY-MMnull=当前有效)
isCurrent Boolean @default(true) // 是否当前生效版本
adjustmentDone Boolean @default(false) // 是否已执行过员工基数调整
adjustmentDone Boolean @default(false) // 是否已执行过社保基数调整
createdBy String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([orgId, effectiveFrom])
@@index([orgId, isCurrent])
}
model HousingFundConfig {
id String @id @default(cuid())
orgId String
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
city String @default("北京")
housingOrg Float @default(12) // 公积金 企业比例 %
housingEmp Float @default(12) // 公积金 个人比例 %
baseMin Float @default(6326) // 公积金缴费基数下限
baseMax Float @default(33891) // 公积金缴费基数上限
effectiveFrom String // 生效月份 YYYY-MM
effectiveTo String? // 失效月份 YYYY-MMnull=当前有效)
isCurrent Boolean @default(true) // 是否当前生效版本
adjustmentDone Boolean @default(false) // 是否已执行过公积金基数调整
createdBy String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -613,11 +646,72 @@ model SalaryChangeRecord {
oldSalary Float
newSalary Float
effectiveDate DateTime // 生效日期
effectiveMonth String // 生效年月 YYYY-MM(从 effectiveDate 转换)
endMonth String? // 失效年月 YYYY-MM(null=至今有效,被新版本覆盖时设置)
changeType String @default("SALARY_CHANGE") // ONBOARDING=入职, REHIRE=重新入职, SALARY_CHANGE=调薪
reason String?
createdBy String
createdAt DateTime @default(now())
@@index([orgId, employeeId])
@@index([employeeId, effectiveMonth, endMonth])
}
model EmployeeSocialInsRecord {
id String @id @default(cuid())
orgId String
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
employeeId String
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
startMonth String // 开始缴费年月 YYYY-MM
endMonth String? // 截止缴费年月 YYYY-MMnull=至今有效)
base Float // 缴费基数
changeType String // ONBOARDING=入职, REHIRE=重新入职, ADJUST=调基, TERMINATION=离职/解聘
changeRefId String? // 关联的 TerminationRecord ID(离职/解聘时)
remark String?
createdBy String
createdAt DateTime @default(now())
@@index([orgId, employeeId])
@@index([employeeId, startMonth, endMonth])
}
model EmployeeHousingFundRecord {
id String @id @default(cuid())
orgId String
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
employeeId String
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
startMonth String // 开始缴费年月 YYYY-MM
endMonth String? // 截止缴费年月 YYYY-MMnull=至今有效)
base Float // 缴费基数
changeType String // ONBOARDING=入职, REHIRE=重新入职, ADJUST=调基, TERMINATION=离职/解聘
changeRefId String? // 关联的 TerminationRecord ID(离职/解聘时)
remark String?
createdBy String
createdAt DateTime @default(now())
@@index([orgId, employeeId])
@@index([employeeId, startMonth, endMonth])
}
model EmployeeDepartmentRecord {
id String @id @default(cuid())
orgId String
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
employeeId String
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
oldDepartment String // 调整前部门
newDepartment String // 调整后部门
effectiveMonth String // 生效年月 YYYY-MM
endMonth String? // 失效年月 YYYY-MMnull=至今有效)
reason String? // 调部门原因
changeType String // ONBOARDING=入职, REHIRE=重新入职, TRANSFER=调部门
createdBy String
createdAt DateTime @default(now())
@@index([orgId, employeeId])
@@index([employeeId, effectiveMonth, endMonth])
}
model OnboardingLink {
-2
View File
@@ -109,8 +109,6 @@ async function main() {
unemploymentEmp: 0.5,
injuryOrg: 0.2,
maternityOrg: 0.8,
housingOrg: 7,
housingEmp: 7,
baseMin: 7384,
baseMax: 36921,
effectiveFrom: '2025-07',
+150
View File
@@ -0,0 +1,150 @@
/**
* 一次性迁移脚本:为现有员工创建初始版本记录
* 运行方式:npx tsx scripts/migrate-records.ts
*/
import prisma from '../src/lib/prisma.js'
import { decrypt } from '../src/lib/crypto.js'
function dateToMonth(date: Date): string {
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
return `${y}-${m}`
}
function prevMonth(month: string): string {
const [y, m] = month.split('-').map(Number)
const d = new Date(y, m - 2, 1)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
}
async function main() {
const employees = await prisma.employee.findMany({
include: {
terminations: { orderBy: { terminationDate: 'desc' }, take: 1 },
salaryChanges: { orderBy: { createdAt: 'desc' }, take: 1 },
socialInsRecords: { take: 1 },
housingFundRecords: { take: 1 },
departmentRecords: { take: 1 },
},
})
console.log(`Found ${employees.length} employees to migrate`)
for (const emp of employees) {
const hireMonth = dateToMonth(emp.hireDate)
const termination = emp.terminations[0]
const endMonth = termination ? dateToMonth(termination.terminationDate) : null
// 解密月薪获取数值
let salaryNum = 0
try {
salaryNum = parseFloat(decrypt(emp.monthlySalary)) || 0
} catch {
salaryNum = parseFloat(emp.monthlySalary) || 0
}
const socialInsBase = emp.socialInsBase ?? salaryNum
const housingFundBase = emp.housingFundBase ?? salaryNum
// 1. 社保缴费记录(仅当尚无记录时创建)
if (emp.socialInsRecords.length === 0) {
await prisma.employeeSocialInsRecord.create({
data: {
orgId: emp.orgId,
employeeId: emp.id,
startMonth: emp.socialInsStartMonth || hireMonth,
endMonth: endMonth || emp.socialInsEndMonth || null,
base: socialInsBase,
changeType: 'ONBOARDING',
createdBy: emp.createdBy,
},
})
}
// 2. 公积金缴费记录
if (emp.housingFundRecords.length === 0) {
await prisma.employeeHousingFundRecord.create({
data: {
orgId: emp.orgId,
employeeId: emp.id,
startMonth: emp.housingFundStartMonth || hireMonth,
endMonth: endMonth || emp.housingFundEndMonth || null,
base: housingFundBase,
changeType: 'ONBOARDING',
createdBy: emp.createdBy,
},
})
}
// 3. 薪资变更记录(仅当尚无记录时创建)
if (emp.salaryChanges.length === 0) {
await prisma.salaryChangeRecord.create({
data: {
orgId: emp.orgId,
employeeId: emp.id,
oldSalary: 0,
newSalary: salaryNum,
effectiveDate: emp.hireDate,
effectiveMonth: hireMonth,
endMonth: null,
changeType: 'ONBOARDING',
createdBy: emp.createdBy,
},
})
} else {
// 已有记录但缺少 effectiveMonth/endMonth/changeType,补充
const latest = emp.salaryChanges[0]
if (!latest.effectiveMonth || !latest.changeType) {
await prisma.salaryChangeRecord.update({
where: { id: latest.id },
data: {
effectiveMonth: dateToMonth(latest.effectiveDate),
changeType: latest.changeType || 'SALARY_CHANGE',
},
})
}
}
// 4. 部门变更记录
if (emp.departmentRecords.length === 0) {
await prisma.employeeDepartmentRecord.create({
data: {
orgId: emp.orgId,
employeeId: emp.id,
oldDepartment: '',
newDepartment: emp.department,
effectiveMonth: hireMonth,
endMonth: null,
changeType: 'ONBOARDING',
createdBy: emp.createdBy,
},
})
}
// 5. 同步 Employee 便捷字段
await prisma.employee.update({
where: { id: emp.id },
data: {
socialInsStartMonth: emp.socialInsStartMonth || hireMonth,
socialInsEndMonth: endMonth || emp.socialInsEndMonth || null,
socialInsBase,
housingFundStartMonth: emp.housingFundStartMonth || hireMonth,
housingFundEndMonth: endMonth || emp.housingFundEndMonth || null,
housingFundBase,
},
})
console.log(`${emp.name} (${emp.department}) — records created/synced`)
}
console.log('\nMigration complete!')
}
main()
.catch((e) => {
console.error('Migration failed:', e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})
+4 -4
View File
@@ -182,7 +182,7 @@ router.get('/batches/:id', async (req: AuthRequest, res: Response, next: NextFun
// 创建批次
const createBatchSchema = z.object({
month: z.string().regex(/^\d{4}-\d{2}$/),
type: z.enum(['REGULAR', 'TERMINATION', 'BONUS']).default('REGULAR'),
type: z.enum(['REGULAR', 'TERMINATION', 'BONUS', 'SEVERANCE']).default('REGULAR'),
mode: z.enum(['copy_last', 'blank_employees', 'blank_all', 'copy_batch']).default('copy_last'),
sourceBatchId: z.string().optional(),
name: z.string().optional(),
@@ -211,7 +211,7 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
const prevMonth = new Date(monthStart.getFullYear(), monthStart.getMonth() - 1, 1)
const prevMonthStr = `${prevMonth.getFullYear()}-${String(prevMonth.getMonth() + 1).padStart(2, '0')}`
const batchName = name || `${month}${batchNo}${type === 'BONUS' ? '奖金' : type === 'TERMINATION' ? '离职结算' : '发薪'}`
const batchName = name || `${month}${batchNo}${type === 'BONUS' ? '奖金' : type === 'TERMINATION' ? '离职结算' : type === 'SEVERANCE' ? '补偿金' : '发薪'}`
// 根据模式确定员工列表和数据来源
let employees: any[] = []
@@ -236,7 +236,7 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
})
} else {
// copy_last 或 blank_employees:拉入员工
if (type === 'TERMINATION') {
if (type === 'TERMINATION' || type === 'SEVERANCE') {
const terminations = await prisma.terminationRecord.findMany({
where: { orgId, terminationDate: { gte: monthStart, lte: monthEnd } },
include: { employee: { include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 } } } },
@@ -317,7 +317,7 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
// 计算社保、个税等
// 同月已有归档常规批次时,新批次跳过社保(避免重复扣缴),但用户可手动编辑覆盖
const skipSocial = type !== 'BONUS' && hasArchivedRegularBatch > 0
const skipSocial = type !== 'BONUS' && type !== 'SEVERANCE' && hasArchivedRegularBatch > 0
const calcResult = await calcBatchEntry(orgId, emp.id, month, { baseSalary, overtimePay, allowance, deduction, bonus }, type, { skipSocial })
// 风险提示
+132 -1
View File
@@ -2,7 +2,7 @@ import { Router, Request, Response, NextFunction } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import { auditLog } from '../middleware/auditLog'
import prisma from '../lib/prisma'
import { decrypt } from '../lib/crypto'
import { decrypt, encrypt } from '../lib/crypto'
import { getContractStatus } from '../services/contract.service'
const router = Router()
@@ -570,4 +570,135 @@ router.delete('/:employeeId/performance/:recordId', authMiddleware, async (req:
} catch (err) { next(err) }
})
// ========== 调薪/调部门 API ==========
function dateToMonth(date: Date): string {
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
return `${y}-${m}`
}
function prevMonth(month: string): string {
const [y, m] = month.split('-').map(Number)
const d = new Date(y, m - 2, 1)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
}
// 调薪
router.post('/:id/salary-change', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const { newSalary, effectiveMonth, reason } = req.body
const employee = await prisma.employee.findFirst({
where: { id: req.params.id, orgId: req.user!.orgId },
})
if (!employee) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } })
}
const oldSalary = safeDecrypt(employee.monthlySalary)
const effMonth = effectiveMonth || dateToMonth(new Date())
const prevEffMonth = prevMonth(effMonth)
// 关闭之前有效记录
await prisma.salaryChangeRecord.updateMany({
where: { employeeId: req.params.id, endMonth: null },
data: { endMonth: prevEffMonth },
})
// 创建新薪资记录
const record = await prisma.salaryChangeRecord.create({
data: {
orgId: req.user!.orgId,
employeeId: req.params.id,
oldSalary,
newSalary: Number(newSalary),
effectiveDate: new Date(`${effMonth}-01`),
effectiveMonth: effMonth,
endMonth: null,
changeType: 'SALARY_CHANGE',
reason: reason || null,
createdBy: req.user!.id,
},
})
// 同步 Employee 便捷字段
await prisma.employee.update({
where: { id: req.params.id },
data: { monthlySalary: encrypt(String(newSalary)) },
})
await auditLog(req, 'CREATE', 'SALARY_CHANGE', record.id, { employeeId: req.params.id, oldSalary, newSalary })
res.json({ success: true, data: record })
} catch (err) { next(err) }
})
// 调薪历史
router.get('/:id/salary-records', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const records = await prisma.salaryChangeRecord.findMany({
where: { employeeId: req.params.id, orgId: req.user!.orgId },
orderBy: { effectiveDate: 'desc' },
})
res.json({ success: true, data: records })
} catch (err) { next(err) }
})
// 调部门
router.post('/:id/department-change', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const { newDepartment, effectiveMonth, reason } = req.body
const employee = await prisma.employee.findFirst({
where: { id: req.params.id, orgId: req.user!.orgId },
})
if (!employee) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } })
}
const oldDepartment = employee.department
const effMonth = effectiveMonth || dateToMonth(new Date())
const prevEffMonth = prevMonth(effMonth)
// 关闭之前有效记录
await prisma.employeeDepartmentRecord.updateMany({
where: { employeeId: req.params.id, endMonth: null },
data: { endMonth: prevEffMonth },
})
// 创建新部门记录
const record = await prisma.employeeDepartmentRecord.create({
data: {
orgId: req.user!.orgId,
employeeId: req.params.id,
oldDepartment,
newDepartment,
effectiveMonth: effMonth,
endMonth: null,
changeType: 'TRANSFER',
reason: reason || null,
createdBy: req.user!.id,
},
})
// 同步 Employee 便捷字段
await prisma.employee.update({
where: { id: req.params.id },
data: { department: newDepartment },
})
await auditLog(req, 'CREATE', 'DEPARTMENT_CHANGE', record.id, { employeeId: req.params.id, oldDepartment, newDepartment })
res.json({ success: true, data: record })
} catch (err) { next(err) }
})
// 调部门历史
router.get('/:id/department-records', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const records = await prisma.employeeDepartmentRecord.findMany({
where: { employeeId: req.params.id, orgId: req.user!.orgId },
orderBy: { effectiveMonth: 'desc' },
})
res.json({ success: true, data: records })
} catch (err) { next(err) }
})
export default router
+465 -21
View File
@@ -7,7 +7,7 @@ import { z } from 'zod'
const router = Router()
router.use(authMiddleware)
const configFields = {
const socialConfigFields = {
city: z.string().optional(),
pensionOrg: z.number().optional(),
pensionEmp: z.number().optional(),
@@ -17,6 +17,12 @@ const configFields = {
unemploymentEmp: z.number().optional(),
injuryOrg: z.number().optional(),
maternityOrg: z.number().optional(),
baseMin: z.number().optional(),
baseMax: z.number().optional(),
}
const housingConfigFields = {
city: z.string().optional(),
housingOrg: z.number().optional(),
housingEmp: z.number().optional(),
baseMin: z.number().optional(),
@@ -85,7 +91,7 @@ router.get('/config/by-month/:month', async (req: AuthRequest, res: Response, ne
// 新建版本(年度调基/比例变更)
const createVersionSchema = z.object({
...configFields,
...socialConfigFields,
effectiveFrom: z.string().regex(/^\d{4}-\d{2}$/),
})
@@ -147,7 +153,7 @@ router.get('/config/:id/adjust-preview', async (req: AuthRequest, res: Response,
const employees = await prisma.employee.findMany({
where: { orgId, status: 'ACTIVE' },
select: { id: true, name: true, department: true, socialInsBase: true, housingFundBase: true, monthlySalary: true },
select: { id: true, name: true, department: true, socialInsBase: true, monthlySalary: true },
orderBy: { name: 'asc' },
})
@@ -180,21 +186,16 @@ router.get('/config/:id/adjust-preview', async (req: AuthRequest, res: Response,
let monthlyWage = 0
try { monthlyWage = Number(decrypt(emp.monthlySalary)) } catch { monthlyWage = Number(emp.monthlySalary) || 0 }
const oldSocialBase = emp.socialInsBase ?? monthlyWage
const oldHousingBase = emp.housingFundBase ?? monthlyWage
const avgSalary = avgSalaryMap.get(emp.id) ?? monthlyWage
// 建议基数 = 上年月均工资按上下限裁剪
const suggestedSocialBase = Math.min(Math.max(avgSalary, config.baseMin), config.baseMax)
const suggestedHousingBase = Math.min(Math.max(avgSalary, config.baseMin), config.baseMax)
return {
employeeId: emp.id,
name: emp.name,
department: emp.department,
oldSocialBase,
oldHousingBase,
avgSalary,
monthlyWage,
suggestedSocialBase,
suggestedHousingBase,
}
})
@@ -209,7 +210,6 @@ const adjustApplySchema = z.object({
items: z.array(z.object({
employeeId: z.string(),
newSocialBase: z.number(),
newHousingBase: z.number(),
})),
})
@@ -225,19 +225,40 @@ router.post('/config/:id/adjust-apply', async (req: AuthRequest, res: Response,
if (config.adjustmentDone) return res.status(400).json({ success: false, message: '该版本已执行过基数调整' })
const { items } = adjustApplySchema.parse(req.body)
const adjustMonth = config.effectiveFrom
const prevAdjustMonth = (() => {
const [y, m] = adjustMonth.split('-').map(Number)
const d = new Date(y, m - 2, 1)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
})()
let adjusted = 0
for (const item of items) {
// 裁剪到上下限范围内
const socialBase = Math.min(Math.max(item.newSocialBase, config.baseMin), config.baseMax)
const housingBase = Math.min(Math.max(item.newHousingBase, config.baseMin), config.baseMax)
// 关闭旧社保记录
await prisma.employeeSocialInsRecord.updateMany({
where: { employeeId: item.employeeId, endMonth: null },
data: { endMonth: prevAdjustMonth },
})
// 创建新社保记录
await prisma.employeeSocialInsRecord.create({
data: {
orgId,
employeeId: item.employeeId,
startMonth: adjustMonth,
endMonth: null,
base: socialBase,
changeType: 'ADJUST',
createdBy: req.user!.id,
},
})
// 同步 Employee 便捷字段
await prisma.employee.update({
where: { id: item.employeeId },
data: {
socialInsBase: socialBase,
housingFundBase: housingBase,
},
data: { socialInsBase: socialBase, socialInsStartMonth: adjustMonth },
})
adjusted++
}
@@ -296,11 +317,8 @@ router.post('/calculate', async (req: AuthRequest, res: Response, next: NextFunc
const unemploymentEmp = actualBase * config.unemploymentEmp / 100
const injuryOrg = actualBase * config.injuryOrg / 100
const maternityOrg = actualBase * config.maternityOrg / 100
const housingOrg = actualBase * config.housingOrg / 100
const housingEmp = actualBase * config.housingEmp / 100
const totalOrg = pensionOrg + medicalOrg + unemploymentOrg + injuryOrg + maternityOrg + housingOrg
const totalEmp = pensionEmp + medicalEmp + unemploymentEmp + housingEmp
const totalOrg = pensionOrg + medicalOrg + unemploymentOrg + injuryOrg + maternityOrg
const totalEmp = pensionEmp + medicalEmp + unemploymentEmp
const total = totalOrg + totalEmp
res.json({
@@ -317,7 +335,6 @@ router.post('/calculate', async (req: AuthRequest, res: Response, next: NextFunc
{ name: '失业保险', orgRate: config.unemploymentOrg, empRate: config.unemploymentEmp, orgAmount: unemploymentOrg, empAmount: unemploymentEmp },
{ name: '工伤保险', orgRate: config.injuryOrg, empRate: 0, orgAmount: injuryOrg, empAmount: 0 },
{ name: '生育保险', orgRate: config.maternityOrg, empRate: 0, orgAmount: maternityOrg, empAmount: 0 },
{ name: '住房公积金', orgRate: config.housingOrg, empRate: config.housingEmp, orgAmount: housingOrg, empAmount: housingEmp },
],
totalOrg,
totalEmp,
@@ -329,4 +346,431 @@ router.post('/calculate', async (req: AuthRequest, res: Response, next: NextFunc
}
})
// ========== 公积金配置 ==========
// 获取当前公积金配置
router.get('/housing-config', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
let config = await prisma.housingFundConfig.findFirst({
where: { orgId: req.user!.orgId, isCurrent: true },
orderBy: { effectiveFrom: 'desc' },
})
if (!config) {
config = await prisma.housingFundConfig.create({
data: {
orgId: req.user!.orgId,
effectiveFrom: new Date().toISOString().slice(0, 7),
createdBy: req.user!.id,
},
})
}
res.json({ success: true, data: config })
} catch (err) {
next(err)
}
})
// 公积金配置版本列表
router.get('/housing-config/versions', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const versions = await prisma.housingFundConfig.findMany({
where: { orgId: req.user!.orgId },
orderBy: { effectiveFrom: 'desc' },
})
res.json({ success: true, data: versions })
} catch (err) {
next(err)
}
})
// 新建公积金配置版本
const createHousingVersionSchema = z.object({
...housingConfigFields,
effectiveFrom: z.string().regex(/^\d{4}-\d{2}$/),
})
router.post('/housing-config/versions', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const data = createHousingVersionSchema.parse(req.body)
const orgId = req.user!.orgId
const existing = await prisma.housingFundConfig.findUnique({
where: { orgId_effectiveFrom: { orgId, effectiveFrom: data.effectiveFrom } },
})
if (existing) {
return res.status(400).json({ success: false, message: `${data.effectiveFrom} 已有公积金配置版本` })
}
const prevCurrent = await prisma.housingFundConfig.findFirst({
where: { orgId, isCurrent: true },
})
if (prevCurrent) {
const [year, mon] = data.effectiveFrom.split('-').map(Number)
const prevMonth = mon === 1
? `${year - 1}-12`
: `${year}-${String(mon - 1).padStart(2, '0')}`
await prisma.housingFundConfig.update({
where: { id: prevCurrent.id },
data: { isCurrent: false, effectiveTo: prevMonth },
})
}
const version = await prisma.housingFundConfig.create({
data: {
orgId,
...data,
isCurrent: true,
createdBy: req.user!.id,
},
})
res.json({ success: true, data: version })
} catch (err) {
next(err)
}
})
// 公积金计算
router.post('/housing-calculate', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { base, month } = calcSchema.parse(req.body)
const orgId = req.user!.orgId
let config
if (month) {
config = await prisma.housingFundConfig.findFirst({
where: {
orgId,
effectiveFrom: { lte: month },
OR: [{ effectiveTo: null }, { effectiveTo: { gte: month } }],
},
orderBy: { effectiveFrom: 'desc' },
})
}
if (!config) {
config = await prisma.housingFundConfig.findFirst({
where: { orgId, isCurrent: true },
})
}
if (!config) {
config = await prisma.housingFundConfig.create({
data: { orgId, effectiveFrom: new Date().toISOString().slice(0, 7), createdBy: req.user!.id },
})
}
const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax)
const housingOrg = actualBase * config.housingOrg / 100
const housingEmp = actualBase * config.housingEmp / 100
res.json({
success: true,
data: {
actualBase,
originalBase: base,
capped: base > config.baseMax,
floored: base < config.baseMin,
configVersion: config.effectiveFrom,
housingOrg,
housingEmp,
total: housingOrg + housingEmp,
},
})
} catch (err) {
next(err)
}
})
// 公积金调基预览
router.get('/housing-config/:id/adjust-preview', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params
const orgId = req.user!.orgId
const config = await prisma.housingFundConfig.findFirst({
where: { id, orgId },
})
if (!config) return res.status(404).json({ success: false, message: '公积金配置版本不存在' })
if (config.adjustmentDone) return res.status(400).json({ success: false, message: '该版本已执行过公积金基数调整' })
const employees = await prisma.employee.findMany({
where: { orgId, status: 'ACTIVE' },
select: { id: true, name: true, department: true, housingFundBase: true, monthlySalary: true },
orderBy: { name: 'asc' },
})
const now = new Date()
const lastYearStart = `${now.getFullYear() - 1}-01`
const lastYearEnd = `${now.getFullYear() - 1}-12`
const lastYearPayslips = await prisma.payslip.findMany({
where: { orgId, month: { gte: lastYearStart, lte: lastYearEnd } },
select: { employeeId: true, totalPay: true },
})
const empPayslipMap = new Map<string, number[]>()
for (const p of lastYearPayslips) {
if (!empPayslipMap.has(p.employeeId)) empPayslipMap.set(p.employeeId, [])
empPayslipMap.get(p.employeeId)!.push(p.totalPay)
}
const items = employees.map((emp) => {
let monthlyWage = 0
try { monthlyWage = Number(decrypt(emp.monthlySalary)) } catch { monthlyWage = Number(emp.monthlySalary) || 0 }
const oldBase = emp.housingFundBase ?? monthlyWage
const payslips = empPayslipMap.get(emp.id)
const avgSalary = payslips && payslips.length > 0 ? payslips.reduce((s, v) => s + v, 0) / payslips.length : monthlyWage
const suggestedBase = Math.min(Math.max(avgSalary, config.baseMin), config.baseMax)
return {
employeeId: emp.id,
name: emp.name,
department: emp.department,
oldBase,
avgSalary,
monthlyWage,
suggestedBase,
}
})
res.json({ success: true, data: { items, total: items.length, baseMin: config.baseMin, baseMax: config.baseMax } })
} catch (err) {
next(err)
}
})
// 执行公积金调基
const adjustHousingSchema = z.object({
items: z.array(z.object({
employeeId: z.string(),
newBase: z.number(),
})),
})
router.post('/housing-config/:id/adjust-apply', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params
const orgId = req.user!.orgId
const config = await prisma.housingFundConfig.findFirst({
where: { id, orgId },
})
if (!config) return res.status(404).json({ success: false, message: '公积金配置版本不存在' })
if (config.adjustmentDone) return res.status(400).json({ success: false, message: '该版本已执行过公积金基数调整' })
const { items } = adjustHousingSchema.parse(req.body)
const adjustMonth = config.effectiveFrom
const prevAdjustMonth = (() => {
const [y, m] = adjustMonth.split('-').map(Number)
const d = new Date(y, m - 2, 1)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
})()
let adjusted = 0
for (const item of items) {
const base = Math.min(Math.max(item.newBase, config.baseMin), config.baseMax)
// 关闭旧记录
await prisma.employeeHousingFundRecord.updateMany({
where: { employeeId: item.employeeId, endMonth: null },
data: { endMonth: prevAdjustMonth },
})
// 创建新记录
await prisma.employeeHousingFundRecord.create({
data: {
orgId,
employeeId: item.employeeId,
startMonth: adjustMonth,
endMonth: null,
base,
changeType: 'ADJUST',
createdBy: req.user!.id,
},
})
// 同步 Employee 便捷字段
await prisma.employee.update({
where: { id: item.employeeId },
data: { housingFundBase: base, housingFundStartMonth: adjustMonth },
})
adjusted++
}
await prisma.housingFundConfig.update({
where: { id },
data: { adjustmentDone: true },
})
res.json({ success: true, data: { adjusted, total: items.length } })
} catch (err) {
next(err)
}
})
// ========== 月度增减员 ==========
// 社保月度增减员
router.get('/monthly-changes', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const month = (req.query.month as string) || new Date().toISOString().slice(0, 7)
const orgId = req.user!.orgId
// 增员:startMonth == month
const additions = await prisma.employeeSocialInsRecord.findMany({
where: { orgId, startMonth: month },
include: { employee: { select: { name: true, department: true, idCardNumber: true } } },
orderBy: { createdAt: 'asc' },
})
// 减员:endMonth == month 且 changeType == TERMINATION
const reductions = await prisma.employeeSocialInsRecord.findMany({
where: { orgId, endMonth: month, changeType: 'TERMINATION' },
include: { employee: { select: { name: true, department: true, idCardNumber: true } } },
orderBy: { createdAt: 'asc' },
})
res.json({
success: true,
data: {
month,
additions: additions.map((r) => ({
employeeId: r.employeeId,
name: r.employee.name,
department: r.employee.department,
base: r.base,
startMonth: r.startMonth,
changeType: r.changeType,
})),
reductions: reductions.map((r) => ({
employeeId: r.employeeId,
name: r.employee.name,
department: r.employee.department,
base: r.base,
endMonth: r.endMonth,
changeType: r.changeType,
})),
},
})
} catch (err) {
next(err)
}
})
// 公积金月度增减员
router.get('/housing/monthly-changes', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const month = (req.query.month as string) || new Date().toISOString().slice(0, 7)
const orgId = req.user!.orgId
const additions = await prisma.employeeHousingFundRecord.findMany({
where: { orgId, startMonth: month },
include: { employee: { select: { name: true, department: true, idCardNumber: true } } },
orderBy: { createdAt: 'asc' },
})
const reductions = await prisma.employeeHousingFundRecord.findMany({
where: { orgId, endMonth: month, changeType: 'TERMINATION' },
include: { employee: { select: { name: true, department: true, idCardNumber: true } } },
orderBy: { createdAt: 'asc' },
})
res.json({
success: true,
data: {
month,
additions: additions.map((r) => ({
employeeId: r.employeeId,
name: r.employee.name,
department: r.employee.department,
base: r.base,
startMonth: r.startMonth,
changeType: r.changeType,
})),
reductions: reductions.map((r) => ({
employeeId: r.employeeId,
name: r.employee.name,
department: r.employee.department,
base: r.base,
endMonth: r.endMonth,
changeType: r.changeType,
})),
},
})
} catch (err) {
next(err)
}
})
// ========== 在职申报 ==========
// 社保在保人员
router.get('/active-declaration', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const month = (req.query.month as string) || new Date().toISOString().slice(0, 7)
const orgId = req.user!.orgId
const records = await prisma.employeeSocialInsRecord.findMany({
where: {
orgId,
startMonth: { lte: month },
OR: [{ endMonth: null }, { endMonth: { gte: month } }],
},
include: { employee: { select: { name: true, department: true, idCardNumber: true, hireDate: true } } },
orderBy: { createdAt: 'asc' },
})
res.json({
success: true,
data: {
month,
items: records.map((r) => ({
employeeId: r.employeeId,
name: r.employee.name,
department: r.employee.department,
base: r.base,
startMonth: r.startMonth,
endMonth: r.endMonth,
changeType: r.changeType,
})),
},
})
} catch (err) {
next(err)
}
})
// 公积金在保人员
router.get('/housing/active-declaration', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const month = (req.query.month as string) || new Date().toISOString().slice(0, 7)
const orgId = req.user!.orgId
const records = await prisma.employeeHousingFundRecord.findMany({
where: {
orgId,
startMonth: { lte: month },
OR: [{ endMonth: null }, { endMonth: { gte: month } }],
},
include: { employee: { select: { name: true, department: true, idCardNumber: true, hireDate: true } } },
orderBy: { createdAt: 'asc' },
})
res.json({
success: true,
data: {
month,
items: records.map((r) => ({
employeeId: r.employeeId,
name: r.employee.name,
department: r.employee.department,
base: r.base,
startMonth: r.startMonth,
endMonth: r.endMonth,
changeType: r.changeType,
})),
},
})
} catch (err) {
next(err)
}
})
export default router
+184 -2
View File
@@ -6,6 +6,18 @@ function daysBetween(a: Date, b: Date): number {
return Math.floor((a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24))
}
function dateToMonth(date: Date): string {
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
return `${y}-${m}`
}
function prevMonth(month: string): string {
const [y, m] = month.split('-').map(Number)
const d = new Date(y, m - 2, 1)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
}
export function getContractStatus(contract: {
signDate: Date | null
startDate: Date
@@ -155,12 +167,20 @@ export async function getEmployeeDetail(orgId: string, id: string) {
}
export async function createEmployee(orgId: string, userId: string, data: any) {
const hireDate = new Date(data.hireDate)
const hireMonth = dateToMonth(hireDate)
const salaryNum = Number(data.monthlySalary) || 0
const socialInsBase = data.socialInsBase != null ? Number(data.socialInsBase) : salaryNum
const housingFundBase = data.housingFundBase != null ? Number(data.housingFundBase) : salaryNum
const socialInsStartMonth = data.socialInsStartMonth || hireMonth
const housingFundStartMonth = data.housingFundStartMonth || hireMonth
const employee = await prisma.employee.create({
data: {
orgId,
name: data.name,
department: data.department,
hireDate: new Date(data.hireDate),
hireDate,
monthlySalary: encrypt(data.monthlySalary),
gender: data.gender,
phone: data.phone,
@@ -168,6 +188,65 @@ export async function createEmployee(orgId: string, userId: string, data: any) {
isPregnant: data.isPregnant || false,
isInMedicalPeriod: data.isInMedicalPeriod || false,
isWorkInjured: data.isWorkInjured || false,
socialInsBase,
housingFundBase,
socialInsStartMonth,
housingFundStartMonth,
createdBy: userId,
},
})
// 创建社保缴费记录
await prisma.employeeSocialInsRecord.create({
data: {
orgId,
employeeId: employee.id,
startMonth: socialInsStartMonth,
endMonth: null,
base: socialInsBase,
changeType: 'ONBOARDING',
createdBy: userId,
},
})
// 创建公积金缴费记录
await prisma.employeeHousingFundRecord.create({
data: {
orgId,
employeeId: employee.id,
startMonth: housingFundStartMonth,
endMonth: null,
base: housingFundBase,
changeType: 'ONBOARDING',
createdBy: userId,
},
})
// 创建初始薪资变更记录
await prisma.salaryChangeRecord.create({
data: {
orgId,
employeeId: employee.id,
oldSalary: 0,
newSalary: salaryNum,
effectiveDate: hireDate,
effectiveMonth: hireMonth,
endMonth: null,
changeType: 'ONBOARDING',
createdBy: userId,
},
})
// 创建初始部门记录
await prisma.employeeDepartmentRecord.create({
data: {
orgId,
employeeId: employee.id,
oldDepartment: '',
newDepartment: data.department,
effectiveMonth: hireMonth,
endMonth: null,
changeType: 'ONBOARDING',
createdBy: userId,
},
})
@@ -227,6 +306,38 @@ export async function rehireEmployee(orgId: string, userId: string, id: string,
throw { code: 'VALIDATION_ERROR', message: '新入职日期必须晚于上次离职/解聘日期' }
}
const newHireMonth = dateToMonth(newHireDate)
const salaryNum = Number(decrypt(employee.monthlySalary)) || 0
const socialInsBase = data.socialInsBase != null ? Number(data.socialInsBase) : salaryNum
const housingFundBase = data.housingFundBase != null ? Number(data.housingFundBase) : salaryNum
const socialInsStartMonth = data.socialInsStartMonth || newHireMonth
const housingFundStartMonth = data.housingFundStartMonth || newHireMonth
const prevHireMonth = prevMonth(newHireMonth)
// 关闭旧社保缴费记录
await prisma.employeeSocialInsRecord.updateMany({
where: { employeeId: id, endMonth: null },
data: { endMonth: prevHireMonth },
})
// 关闭旧公积金缴费记录
await prisma.employeeHousingFundRecord.updateMany({
where: { employeeId: id, endMonth: null },
data: { endMonth: prevHireMonth },
})
// 关闭旧薪资记录
await prisma.salaryChangeRecord.updateMany({
where: { employeeId: id, endMonth: null },
data: { endMonth: prevHireMonth },
})
// 关闭旧部门记录
await prisma.employeeDepartmentRecord.updateMany({
where: { employeeId: id, endMonth: null },
data: { endMonth: prevHireMonth },
})
await prisma.employee.update({
where: { id },
data: {
@@ -236,6 +347,67 @@ export async function rehireEmployee(orgId: string, userId: string, id: string,
isPregnant: false,
isInMedicalPeriod: false,
isWorkInjured: false,
socialInsBase,
housingFundBase,
socialInsStartMonth,
socialInsEndMonth: null,
housingFundStartMonth,
housingFundEndMonth: null,
},
})
// 创建新社保缴费记录
await prisma.employeeSocialInsRecord.create({
data: {
orgId,
employeeId: id,
startMonth: socialInsStartMonth,
endMonth: null,
base: socialInsBase,
changeType: 'REHIRE',
createdBy: userId,
},
})
// 创建新公积金缴费记录
await prisma.employeeHousingFundRecord.create({
data: {
orgId,
employeeId: id,
startMonth: housingFundStartMonth,
endMonth: null,
base: housingFundBase,
changeType: 'REHIRE',
createdBy: userId,
},
})
// 创建新薪资记录
await prisma.salaryChangeRecord.create({
data: {
orgId,
employeeId: id,
oldSalary: salaryNum,
newSalary: salaryNum,
effectiveDate: newHireDate,
effectiveMonth: newHireMonth,
endMonth: null,
changeType: 'REHIRE',
createdBy: userId,
},
})
// 创建新部门记录
await prisma.employeeDepartmentRecord.create({
data: {
orgId,
employeeId: id,
oldDepartment: employee.department,
newDepartment: data.department || employee.department,
effectiveMonth: newHireMonth,
endMonth: null,
changeType: 'REHIRE',
createdBy: userId,
},
})
@@ -287,13 +459,23 @@ export async function updateEmployee(orgId: string, id: string, data: any) {
updateData.monthlySalary = encrypt(data.monthlySalary)
// 记录薪资变更
if (oldSalary !== newSalary) {
const now = new Date()
const nowMonth = dateToMonth(now)
// 关闭之前有效记录
await prisma.salaryChangeRecord.updateMany({
where: { employeeId: id, endMonth: null },
data: { endMonth: prevMonth(nowMonth) },
})
await prisma.salaryChangeRecord.create({
data: {
orgId,
employeeId: id,
oldSalary,
newSalary,
effectiveDate: new Date(),
effectiveDate: now,
effectiveMonth: nowMonth,
endMonth: null,
changeType: 'SALARY_CHANGE',
reason: data.salaryChangeReason || '手动调整',
createdBy: '',
},
+15 -5
View File
@@ -117,7 +117,7 @@ export async function calcBatchEntry(
batchType: string = 'REGULAR',
options?: { skipSocial?: boolean; overrideSocial?: { socialEmp?: number; socialOrg?: number; housingEmp?: number; housingOrg?: number } },
) {
const [employee, socialConfig] = await Promise.all([
const [employee, socialConfig, housingConfig] = await Promise.all([
prisma.employee.findFirst({ where: { id: employeeId, orgId } }),
prisma.socialInsuranceConfig.findFirst({
where: {
@@ -127,6 +127,14 @@ export async function calcBatchEntry(
},
orderBy: { effectiveFrom: 'desc' },
}),
prisma.housingFundConfig.findFirst({
where: {
orgId,
effectiveFrom: { lte: month },
OR: [{ effectiveTo: null }, { effectiveTo: { gte: month } }],
},
orderBy: { effectiveFrom: 'desc' },
}),
])
if (!employee) throw { code: 'NOT_FOUND', message: '员工不存在' }
@@ -136,13 +144,15 @@ export async function calcBatchEntry(
let socialEmp = 0, socialOrg = 0, housingEmp = 0, housingOrg = 0
// 年终奖/奖金批次:不扣社保公积金
if (batchType !== 'BONUS' && !options?.skipSocial) {
// 年终奖/奖金批次、补偿金批次:不扣社保公积金
if (batchType !== 'BONUS' && batchType !== 'SEVERANCE' && !options?.skipSocial) {
if (socialConfig) {
const social = calcSocialInsurance(socialBase, socialConfig)
const housing = calcHousingFund(housingBase, socialConfig)
socialEmp = social.socialEmp
socialOrg = social.socialOrg
}
if (housingConfig) {
const housing = calcHousingFund(housingBase, housingConfig)
housingEmp = housing.housingEmp
housingOrg = housing.housingOrg
}
@@ -164,7 +174,7 @@ export async function calcBatchEntry(
// 年终奖单独计税
tax = calcBonusTax(inputs.bonus)
} else {
// 累计预扣法
// 累计预扣法(补偿金也走累计预扣,但无社保公积金扣除)
const year = month.slice(0, 4)
const prevPayslips = await prisma.payslip.findMany({
where: {
+4 -3
View File
@@ -282,7 +282,7 @@ export async function getDashboardData(orgId: string) {
const [
employeeCount, highRisks, pendingRisks, riskItems, resolvedItems,
overtimeRecords, payslips, batchEntries, socialConfig,
overtimeRecords, payslips, batchEntries, socialConfig, housingConfig,
monthContracts, monthTerminations, monthDisciplinary, monthAttendance,
monthSeverancePay,
] = await Promise.all([
@@ -315,6 +315,7 @@ export async function getDashboardData(orgId: string) {
select: { baseSalary: true, overtimePay: true, allowance: true, deduction: true, bonus: true, totalPay: true, socialEmp: true, socialOrg: true, housingEmp: true, housingOrg: true, tax: true, netPay: true, employeeId: true },
}),
prisma.socialInsuranceConfig.findFirst({ where: { orgId, isCurrent: true } }),
prisma.housingFundConfig.findFirst({ where: { orgId, isCurrent: true } }),
prisma.laborContract.count({
where: { orgId, createdAt: { gte: monthStart, lte: monthEnd } },
}),
@@ -408,8 +409,8 @@ export async function getDashboardData(orgId: string) {
const avgBase = employeeCount > 0 ? Math.max(socialConfig.baseMin, Math.min(socialConfig.baseMax, totalBaseSalary / Math.max(employeeCount, 1))) : socialConfig.baseMin
socialOrgTotal = avgBase * (socialConfig.pensionOrg + socialConfig.medicalOrg + socialConfig.unemploymentOrg + socialConfig.injuryOrg + socialConfig.maternityOrg) / 100 * employeeCount
socialEmpTotal = avgBase * (socialConfig.pensionEmp + socialConfig.medicalEmp + socialConfig.unemploymentEmp) / 100 * employeeCount
housingOrgTotal = avgBase * socialConfig.housingOrg / 100 * employeeCount
housingEmpTotal = avgBase * socialConfig.housingEmp / 100 * employeeCount
housingOrgTotal = avgBase * (housingConfig?.housingOrg ?? 0) / 100 * employeeCount
housingEmpTotal = avgBase * (housingConfig?.housingEmp ?? 0) / 100 * employeeCount
}
// 个税:优先用归档批次的实际计算值,否则估算
+56 -6
View File
@@ -6,6 +6,12 @@ function daysBetween(a: Date, b: Date): number {
return Math.floor((a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24))
}
function dateToMonth(date: Date): string {
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
return `${y}-${m}`
}
export interface ChecklistItem {
key: string
label: string
@@ -152,14 +158,21 @@ export async function createTermination(orgId: string, userId: string, data: any
const { level } = assessRisk(employee, data.reason)
const termDate = new Date(data.terminationDate)
const termMonth = dateToMonth(termDate)
const socialInsEndMonth = data.socialInsEndMonth || termMonth
const housingFundEndMonth = data.housingFundEndMonth || termMonth
const record = await prisma.terminationRecord.create({
data: {
orgId,
employeeId: data.employeeId,
type: 'TERMINATION',
reason: data.reason,
terminationDate: new Date(data.terminationDate),
terminationDate: termDate,
compensation: data.compensation || 0,
socialInsEndMonth,
housingFundEndMonth,
riskLevel: level,
checklist: data.checklist || {},
remark: data.remark,
@@ -167,15 +180,30 @@ export async function createTermination(orgId: string, userId: string, data: any
},
})
// 关闭社保缴费记录(设置 endMonth)
await prisma.employeeSocialInsRecord.updateMany({
where: { employeeId: data.employeeId, endMonth: null },
data: { endMonth: socialInsEndMonth, changeRefId: record.id },
})
// 关闭公积金缴费记录
await prisma.employeeHousingFundRecord.updateMany({
where: { employeeId: data.employeeId, endMonth: null },
data: { endMonth: housingFundEndMonth, changeRefId: record.id },
})
// 根据解聘日期判断在职/离职状态
const termDate = new Date(data.terminationDate)
const today = new Date()
today.setHours(0, 0, 0, 0)
const isResigned = termDate <= today
await prisma.employee.update({
where: { id: data.employeeId },
data: { status: isResigned ? 'RESIGNED' : 'ACTIVE' },
data: {
status: isResigned ? 'RESIGNED' : 'ACTIVE',
socialInsEndMonth,
housingFundEndMonth,
},
})
await prisma.riskItem.updateMany({
@@ -202,15 +230,22 @@ export async function createResignation(orgId: string, userId: string, data: any
throw { code: 'CONFLICT', message: '该员工已有离职/解聘记录,如需再次办理请先重新雇佣' }
}
const termDate = new Date(data.terminationDate)
const termMonth = dateToMonth(termDate)
const socialInsEndMonth = data.socialInsEndMonth || termMonth
const housingFundEndMonth = data.housingFundEndMonth || termMonth
const record = await prisma.terminationRecord.create({
data: {
orgId,
employeeId: data.employeeId,
type: 'RESIGNATION',
reason: 'RESIGNATION',
terminationDate: new Date(data.terminationDate),
terminationDate: termDate,
resignationReason: data.resignationReason || null,
compensation: 0,
socialInsEndMonth,
housingFundEndMonth,
riskLevel: 'SAFE',
checklist: {},
remark: data.remark || null,
@@ -218,15 +253,30 @@ export async function createResignation(orgId: string, userId: string, data: any
},
})
// 关闭社保缴费记录
await prisma.employeeSocialInsRecord.updateMany({
where: { employeeId: data.employeeId, endMonth: null },
data: { endMonth: socialInsEndMonth, changeRefId: record.id },
})
// 关闭公积金缴费记录
await prisma.employeeHousingFundRecord.updateMany({
where: { employeeId: data.employeeId, endMonth: null },
data: { endMonth: housingFundEndMonth, changeRefId: record.id },
})
// 根据离职日期判断在职/离职状态
const termDate = new Date(data.terminationDate)
const today = new Date()
today.setHours(0, 0, 0, 0)
const isResigned = termDate <= today
await prisma.employee.update({
where: { id: data.employeeId },
data: { status: isResigned ? 'RESIGNED' : 'ACTIVE' },
data: {
status: isResigned ? 'RESIGNED' : 'ACTIVE',
socialInsEndMonth,
housingFundEndMonth,
},
})
await prisma.riskItem.updateMany({