feat: 社保配置回退SocialYearStandard、月度办理tab切换、员工参保筛选分页
- 后端:dotenv/config 加载 ENCRYPTION_KEY,修复薪资数据解密 - 后端:社保/公积金 calculate API 回退到 SocialYearStandard(通过默认账户) - 后端:getSocialConfigByMonth/getHousingConfigByMonth 回退到 SocialYearStandard - 后端:active-declaration 查询条件改为 lte,当月办理的增员也显示在在保列表 - 后端:employee-enrollment API 增加部门/参保状态筛选和分页 - 后端:updateEmployeeSchema 增加 baseSalary/performanceSalary 字段 - 后端:payroll2.routes 批次详情包含 contracts 合同类型 - 前端:月度办理社保/公积金改为 tab 切换(不再同时展开) - 前端:员工参保列表增加筛选(部门/社保状态/公积金状态)和分页 - 前端:花名册基本信息增加月度工资只读显示(基本+绩效自动计算) - 前端:薪资批次详情表增加合同类型列 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Generated
+13
@@ -14,6 +14,7 @@
|
||||
"bcryptjs": "^2.4.3",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.4.2",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^4.19.0",
|
||||
"express-rate-limit": "^7.4.0",
|
||||
@@ -1695,6 +1696,18 @@
|
||||
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.4.2.tgz",
|
||||
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/duck": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmmirror.com/duck/-/duck-0.1.12.tgz",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"bcryptjs": "^2.4.3",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.4.2",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^4.19.0",
|
||||
"express-rate-limit": "^7.4.0",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dotenv/config'
|
||||
import app from './app'
|
||||
import { validateEnv } from './lib/config'
|
||||
import logger from './lib/logger'
|
||||
|
||||
@@ -1023,9 +1023,9 @@ router.post('/batches/:batchId/fetch-disciplinary', async (req: AuthRequest, res
|
||||
}
|
||||
|
||||
// 查询批次月份内的违纪扣款记录(按 violationDate 落在批次月份内)
|
||||
const monthStart = new Date(batch.month + '-01')
|
||||
const monthStart = new Date(batch.month + '-01T00:00:00.000Z')
|
||||
const monthEnd = new Date(monthStart)
|
||||
monthEnd.setMonth(monthEnd.getMonth() + 1)
|
||||
monthEnd.setUTCMonth(monthEnd.getUTCMonth() + 1)
|
||||
|
||||
const disciplinaryRecords = await prisma.disciplinaryRecord.findMany({
|
||||
where: {
|
||||
@@ -1037,6 +1037,7 @@ router.post('/batches/:batchId/fetch-disciplinary', async (req: AuthRequest, res
|
||||
},
|
||||
select: { employeeId: true, deductionAmount: true },
|
||||
})
|
||||
console.log(`[fetch-disciplinary] batch.month=${batch.month} monthStart=${monthStart.toISOString()} monthEnd=${monthEnd.toISOString()} entries=${entries.length} records=${disciplinaryRecords.length}`)
|
||||
|
||||
// 按员工汇总扣款金额
|
||||
const deductionMap = new Map<string, number>()
|
||||
|
||||
@@ -819,6 +819,41 @@ router.post('/calculate', async (req: AuthRequest, res: Response, next: NextFunc
|
||||
where: { ...whereBase, isCurrent: true },
|
||||
})
|
||||
}
|
||||
// 回退到 SocialYearStandard(通过默认社保账户关联)
|
||||
if (!config) {
|
||||
const socialAccount = await prisma.socialAccount.findFirst({
|
||||
where: { orgId, type: 'SOCIAL', isDefault: true },
|
||||
})
|
||||
if (socialAccount) {
|
||||
const standard = await prisma.socialYearStandard.findFirst({
|
||||
where: {
|
||||
accountId: socialAccount.id,
|
||||
effectiveFrom: { lte: month || '9999-12' },
|
||||
OR: [{ effectiveTo: null }, { effectiveTo: { gte: month || '0000-01' } }],
|
||||
},
|
||||
orderBy: { effectiveFrom: 'desc' },
|
||||
})
|
||||
if (standard) {
|
||||
config = {
|
||||
baseMin: standard.baseMin,
|
||||
baseMax: standard.baseMax,
|
||||
medicalBaseMin: standard.medicalBaseMin,
|
||||
medicalBaseMax: standard.medicalBaseMax,
|
||||
pensionOrg: standard.pensionOrg,
|
||||
pensionEmp: standard.pensionEmp,
|
||||
medicalOrg: standard.medicalOrg,
|
||||
medicalEmp: standard.medicalEmp,
|
||||
medicalOrgExtra: standard.medicalOrgExtra,
|
||||
medicalEmpExtra: standard.medicalEmpExtra,
|
||||
unemploymentOrg: standard.unemploymentOrg,
|
||||
unemploymentEmp: standard.unemploymentEmp,
|
||||
injuryOrg: standard.injuryOrg,
|
||||
maternityOrg: standard.maternityOrg,
|
||||
extraInsurances: standard.extraInsurances,
|
||||
} as any
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!config) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: `未找到${city || ''}的社保配置,请先在社保管理中创建` } })
|
||||
}
|
||||
@@ -1004,6 +1039,30 @@ router.post('/housing-calculate', async (req: AuthRequest, res: Response, next:
|
||||
where: { ...whereBase, isCurrent: true },
|
||||
})
|
||||
}
|
||||
// 回退到 SocialYearStandard(通过默认公积金账户关联)
|
||||
if (!config) {
|
||||
const housingAccount = await prisma.socialAccount.findFirst({
|
||||
where: { orgId, type: 'HOUSING', isDefault: true },
|
||||
})
|
||||
if (housingAccount) {
|
||||
const standard = await prisma.socialYearStandard.findFirst({
|
||||
where: {
|
||||
accountId: housingAccount.id,
|
||||
effectiveFrom: { lte: month || '9999-12' },
|
||||
OR: [{ effectiveTo: null }, { effectiveTo: { gte: month || '0000-01' } }],
|
||||
},
|
||||
orderBy: { effectiveFrom: 'desc' },
|
||||
})
|
||||
if (standard) {
|
||||
config = {
|
||||
baseMin: standard.housingBaseMin || standard.baseMin,
|
||||
baseMax: standard.housingBaseMax || standard.baseMax,
|
||||
housingOrg: standard.housingOrg,
|
||||
housingEmp: standard.housingEmp,
|
||||
} as any
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!config) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: `未找到${city || ''}的公积金配置,请先在公积金管理中创建` } })
|
||||
}
|
||||
@@ -1251,7 +1310,7 @@ function calcHousingDetail(base: number, config: any) {
|
||||
return { actualBase, orgAmount, empAmount, total: orgAmount + empAmount }
|
||||
}
|
||||
|
||||
/** 按月份匹配社保配置版本 */
|
||||
/** 按月份匹配社保配置版本(回退到 SocialYearStandard) */
|
||||
async function getSocialConfigByMonth(orgId: string, month: string, city?: string) {
|
||||
const where: any = { orgId }
|
||||
if (city) where.city = city
|
||||
@@ -1262,10 +1321,73 @@ async function getSocialConfigByMonth(orgId: string, month: string, city?: strin
|
||||
if (!config) {
|
||||
config = await prisma.socialInsuranceConfig.findFirst({ where: { ...where, isCurrent: true } })
|
||||
}
|
||||
// 回退到 SocialYearStandard(通过默认社保账户关联)
|
||||
if (!config) {
|
||||
const socialAccount = await prisma.socialAccount.findFirst({
|
||||
where: { orgId, type: 'SOCIAL', isDefault: true },
|
||||
})
|
||||
if (socialAccount) {
|
||||
const standard = await prisma.socialYearStandard.findFirst({
|
||||
where: {
|
||||
accountId: socialAccount.id,
|
||||
effectiveFrom: { lte: month },
|
||||
OR: [{ effectiveTo: null }, { effectiveTo: { gte: month } }],
|
||||
},
|
||||
orderBy: { effectiveFrom: 'desc' },
|
||||
})
|
||||
if (!standard) {
|
||||
const fallback = await prisma.socialYearStandard.findFirst({
|
||||
where: { accountId: socialAccount.id, isCurrent: true },
|
||||
orderBy: { effectiveFrom: 'desc' },
|
||||
})
|
||||
if (fallback) {
|
||||
config = {
|
||||
city: city || '北京',
|
||||
effectiveFrom: fallback.effectiveFrom,
|
||||
baseMin: fallback.baseMin,
|
||||
baseMax: fallback.baseMax,
|
||||
medicalBaseMin: fallback.medicalBaseMin,
|
||||
medicalBaseMax: fallback.medicalBaseMax,
|
||||
pensionOrg: fallback.pensionOrg,
|
||||
pensionEmp: fallback.pensionEmp,
|
||||
medicalOrg: fallback.medicalOrg,
|
||||
medicalEmp: fallback.medicalEmp,
|
||||
medicalOrgExtra: fallback.medicalOrgExtra,
|
||||
medicalEmpExtra: fallback.medicalEmpExtra,
|
||||
unemploymentOrg: fallback.unemploymentOrg,
|
||||
unemploymentEmp: fallback.unemploymentEmp,
|
||||
injuryOrg: fallback.injuryOrg,
|
||||
maternityOrg: fallback.maternityOrg,
|
||||
extraInsurances: fallback.extraInsurances,
|
||||
} as any
|
||||
}
|
||||
} else {
|
||||
config = {
|
||||
city: city || '北京',
|
||||
effectiveFrom: standard.effectiveFrom,
|
||||
baseMin: standard.baseMin,
|
||||
baseMax: standard.baseMax,
|
||||
medicalBaseMin: standard.medicalBaseMin,
|
||||
medicalBaseMax: standard.medicalBaseMax,
|
||||
pensionOrg: standard.pensionOrg,
|
||||
pensionEmp: standard.pensionEmp,
|
||||
medicalOrg: standard.medicalOrg,
|
||||
medicalEmp: standard.medicalEmp,
|
||||
medicalOrgExtra: standard.medicalOrgExtra,
|
||||
medicalEmpExtra: standard.medicalEmpExtra,
|
||||
unemploymentOrg: standard.unemploymentOrg,
|
||||
unemploymentEmp: standard.unemploymentEmp,
|
||||
injuryOrg: standard.injuryOrg,
|
||||
maternityOrg: standard.maternityOrg,
|
||||
extraInsurances: standard.extraInsurances,
|
||||
} as any
|
||||
}
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
/** 按月份匹配公积金配置版本 */
|
||||
/** 按月份匹配公积金配置版本(回退到 SocialYearStandard) */
|
||||
async function getHousingConfigByMonth(orgId: string, month: string, city?: string) {
|
||||
const where: any = { orgId }
|
||||
if (city) where.city = city
|
||||
@@ -1276,6 +1398,47 @@ async function getHousingConfigByMonth(orgId: string, month: string, city?: stri
|
||||
if (!config) {
|
||||
config = await prisma.housingFundConfig.findFirst({ where: { ...where, isCurrent: true } })
|
||||
}
|
||||
// 回退到 SocialYearStandard(通过默认公积金账户关联)
|
||||
if (!config) {
|
||||
const housingAccount = await prisma.socialAccount.findFirst({
|
||||
where: { orgId, type: 'HOUSING', isDefault: true },
|
||||
})
|
||||
if (housingAccount) {
|
||||
const standard = await prisma.socialYearStandard.findFirst({
|
||||
where: {
|
||||
accountId: housingAccount.id,
|
||||
effectiveFrom: { lte: month },
|
||||
OR: [{ effectiveTo: null }, { effectiveTo: { gte: month } }],
|
||||
},
|
||||
orderBy: { effectiveFrom: 'desc' },
|
||||
})
|
||||
if (!standard) {
|
||||
const fallback = await prisma.socialYearStandard.findFirst({
|
||||
where: { accountId: housingAccount.id, isCurrent: true },
|
||||
orderBy: { effectiveFrom: 'desc' },
|
||||
})
|
||||
if (fallback) {
|
||||
config = {
|
||||
city: city || '北京',
|
||||
effectiveFrom: fallback.effectiveFrom,
|
||||
baseMin: fallback.housingBaseMin || fallback.baseMin,
|
||||
baseMax: fallback.housingBaseMax || fallback.baseMax,
|
||||
housingOrg: fallback.housingOrg,
|
||||
housingEmp: fallback.housingEmp,
|
||||
} as any
|
||||
}
|
||||
} else {
|
||||
config = {
|
||||
city: city || '北京',
|
||||
effectiveFrom: standard.effectiveFrom,
|
||||
baseMin: standard.housingBaseMin || standard.baseMin,
|
||||
baseMax: standard.housingBaseMax || standard.baseMax,
|
||||
housingOrg: standard.housingOrg,
|
||||
housingEmp: standard.housingEmp,
|
||||
} as any
|
||||
}
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -1547,8 +1710,8 @@ router.get('/active-declaration', async (req: AuthRequest, res: Response, next:
|
||||
const records = await prisma.employeeSocialInsRecord.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
startMonth: { lt: month },
|
||||
OR: [{ endMonth: null }, { endMonth: { gt: month } }],
|
||||
startMonth: { lte: month },
|
||||
OR: [{ endMonth: null }, { endMonth: { gte: month } }],
|
||||
employee: { contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } } },
|
||||
},
|
||||
include: { employee: { select: { name: true, department: true, idCardNumber: true, hireDate: true, contracts: { orderBy: { createdAt: 'desc' }, take: 1, select: { contractType: true } } } } },
|
||||
@@ -1715,8 +1878,8 @@ router.get('/housing/active-declaration', async (req: AuthRequest, res: Response
|
||||
const records = await prisma.employeeHousingFundRecord.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
startMonth: { lt: month },
|
||||
OR: [{ endMonth: null }, { endMonth: { gt: month } }],
|
||||
startMonth: { lte: month },
|
||||
OR: [{ endMonth: null }, { endMonth: { gte: month } }],
|
||||
employee: { contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } } },
|
||||
},
|
||||
include: { employee: { select: { name: true, department: true, idCardNumber: true, hireDate: true, contracts: { orderBy: { createdAt: 'desc' }, take: 1, select: { contractType: true } } } } },
|
||||
@@ -2180,13 +2343,19 @@ router.get('/employee-enrollment', async (req: AuthRequest, res: Response, next:
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const keyword = (req.query.keyword as string) || ''
|
||||
const department = (req.query.department as string) || ''
|
||||
const socialStatus = (req.query.socialStatus as string) || '' // INSURED | UNINSURED
|
||||
const housingStatus = (req.query.housingStatus as string) || '' // INSURED | UNINSURED
|
||||
const page = Math.max(1, parseInt(req.query.page as string) || 1)
|
||||
const pageSize = Math.min(200, Math.max(1, parseInt(req.query.pageSize as string) || 20))
|
||||
|
||||
// 查询所有在职员工
|
||||
// 查询所有在职员工(先全量查,再在内存中按参保状态过滤,最后分页)
|
||||
const employees = await prisma.employee.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
status: 'ACTIVE',
|
||||
...(keyword ? { name: { contains: keyword, mode: 'insensitive' } } : {}),
|
||||
...(department ? { department } : {}),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -2217,7 +2386,7 @@ router.get('/employee-enrollment', async (req: AuthRequest, res: Response, next:
|
||||
const socialMap = new Map(socialRecords.map(r => [r.employeeId, r]))
|
||||
const housingMap = new Map(housingRecords.map(r => [r.employeeId, r]))
|
||||
|
||||
const list = employees.map(emp => {
|
||||
let list = employees.map(emp => {
|
||||
const social = socialMap.get(emp.id)
|
||||
const housing = housingMap.get(emp.id)
|
||||
return {
|
||||
@@ -2236,7 +2405,14 @@ router.get('/employee-enrollment', async (req: AuthRequest, res: Response, next:
|
||||
}
|
||||
})
|
||||
|
||||
res.json({ success: true, data: list })
|
||||
// 按参保状态过滤
|
||||
if (socialStatus) list = list.filter(e => e.socialInsStatus === socialStatus)
|
||||
if (housingStatus) list = list.filter(e => e.housingFundStatus === housingStatus)
|
||||
|
||||
const total = list.length
|
||||
const paged = list.slice((page - 1) * pageSize, page * pageSize)
|
||||
|
||||
res.json({ success: true, data: paged, total, page, pageSize })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user