feat: 绩效系数配置化 + 工作台社保公积金评分修复
- Organization 新增 defaultGradeCoefficients 字段,系统设置可配置缺省绩效系数 - 绩效记录编辑时自动带出系数(优先模板 gradeRules,回退组织缺省配置) - 薪资计算优先用记录 coefficient,回退模板/组织缺省/硬编码默认值 - 绩效模板表单新增等级系数设置区域 - 修复工作台社保公积金评分为0:查询从旧表 SocialInsuranceConfig 改为 SocialYearStandard - 修复 employeesNoSocial 计算逻辑:只统计有正式劳动合同(FIXED/UNFIXED)但无社保记录的员工 - 社保账户未设置 paymentDay 时回退到 Organization.socialInsCutoffDay 生成月度任务 - 合规健康度各维度补充 todoCount 字段 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -141,6 +141,7 @@ model Organization {
|
||||
esignPerformanceEnabled Boolean @default(false) // 绩效考核电子签
|
||||
esignDisciplinaryEnabled Boolean @default(false) // 违纪记录电子签
|
||||
socialInsCutoffDay Int @default(15) // 社保/公积金截止日:每月该日前离职→截止月=离职月-1,该日后→截止月=离职月
|
||||
defaultGradeCoefficients Json? @default("{\"A\":1.2,\"B\":1.0,\"C\":0.8,\"D\":0.6}") // 缺省绩效系数(不选模板时使用)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -801,6 +802,7 @@ model PerformanceRecord {
|
||||
periodType String @default("MONTHLY") // MONTHLY/QUARTERLY/YEARLY
|
||||
score Float @default(0) // 考核得分
|
||||
grade String @default("B") // A/B/C/D
|
||||
coefficient Float @default(1.0) // 绩效系数(A=1.2, B=1.0, C=0.8, D=0.6,可手动调整)
|
||||
result String @default("QUALIFIED") // EXCELLENT/QUALIFIED/NEED_IMPROVE/UNQUALIFIED
|
||||
summary String? // 考核评语
|
||||
improvementPlan String? // 改进计划(不胜任时)
|
||||
|
||||
@@ -324,13 +324,17 @@ router.get('/batches/:id', async (req: AuthRequest, res: Response, next: NextFun
|
||||
// 查询批次月份的绩效记录,附加到 entry 上
|
||||
const perfRecords = await prisma.performanceRecord.findMany({
|
||||
where: { orgId: req.user!.orgId, period: rawBatch.month, periodType: 'MONTHLY' },
|
||||
select: { employeeId: true, grade: true, score: true },
|
||||
select: { employeeId: true, grade: true, score: true, coefficient: true },
|
||||
})
|
||||
const gradeCoefficients: Record<string, number> = { A: 1.2, B: 1.0, C: 0.8, D: 0.6 }
|
||||
// 等级默认系数:优先从组织缺省配置取,回退到硬编码
|
||||
const orgForCoeff = await prisma.organization.findUnique({ where: { id: req.user!.orgId }, select: { defaultGradeCoefficients: true } })
|
||||
const DEFAULT_GRADE_COEFFICIENTS: Record<string, number> = (orgForCoeff?.defaultGradeCoefficients as Record<string, number>) || { A: 1.2, B: 1.0, C: 0.8, D: 0.6 }
|
||||
const perfMap = new Map<string, { grade: string; coefficient: number }>()
|
||||
for (const r of perfRecords) {
|
||||
if (!perfMap.has(r.employeeId)) {
|
||||
perfMap.set(r.employeeId, { grade: r.grade, coefficient: gradeCoefficients[r.grade] || 1.0 })
|
||||
// 优先用记录中保存的 coefficient,回退到等级默认值
|
||||
const coeff = r.coefficient != null ? r.coefficient : (DEFAULT_GRADE_COEFFICIENTS[r.grade] || 1.0)
|
||||
perfMap.set(r.employeeId, { grade: r.grade, coefficient: coeff })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -916,13 +920,29 @@ router.post('/batches/:batchId/fetch-performance', async (req: AuthRequest, res:
|
||||
// 查询批次月份的绩效记录(按 period = YYYY-MM 匹配)
|
||||
const performanceRecords = await prisma.performanceRecord.findMany({
|
||||
where: { orgId, period: batch.month, periodType: 'MONTHLY' },
|
||||
select: { employeeId: true, grade: true, score: true, result: true },
|
||||
select: { employeeId: true, grade: true, score: true, result: true, coefficient: true, templateId: true },
|
||||
})
|
||||
const perfMap = new Map<string, { grade: string; score: number }>()
|
||||
// 查询关联的绩效模板(用于回退取 gradeRules 中的 coefficient)
|
||||
const templateIds = [...new Set(performanceRecords.map(r => r.templateId).filter(Boolean))] as string[]
|
||||
const templates = templateIds.length > 0 ? await prisma.performanceTemplate.findMany({
|
||||
where: { id: { in: templateIds } },
|
||||
select: { id: true, gradeRules: true },
|
||||
}) : []
|
||||
const templateCoeffMap = new Map<string, Record<string, number>>()
|
||||
for (const t of templates) {
|
||||
const coeffs: Record<string, number> = {}
|
||||
if (t.gradeRules && Array.isArray(t.gradeRules)) {
|
||||
for (const rule of t.gradeRules as any[]) {
|
||||
if (rule.grade && rule.coefficient != null) coeffs[rule.grade] = Number(rule.coefficient)
|
||||
}
|
||||
}
|
||||
templateCoeffMap.set(t.id, coeffs)
|
||||
}
|
||||
const perfMap = new Map<string, { grade: string; score: number; coefficient: number | null }>()
|
||||
for (const r of performanceRecords) {
|
||||
// 同一员工同月可能有多条记录,取最新一条(按创建时间倒序已由 DB 保证)
|
||||
if (!perfMap.has(r.employeeId)) {
|
||||
perfMap.set(r.employeeId, { grade: r.grade, score: r.score })
|
||||
perfMap.set(r.employeeId, { grade: r.grade, score: r.score, coefficient: r.coefficient })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -957,8 +977,9 @@ router.post('/batches/:batchId/fetch-performance', async (req: AuthRequest, res:
|
||||
}
|
||||
}
|
||||
|
||||
// 绩效系数映射
|
||||
const gradeCoefficients: Record<string, number> = { A: 1.2, B: 1.0, C: 0.8, D: 0.6 }
|
||||
// 绩效系数:优先用记录中保存的 coefficient,回退到组织缺省配置,再回退到硬编码
|
||||
const orgForCoeff = await prisma.organization.findUnique({ where: { id: orgId }, select: { defaultGradeCoefficients: true } })
|
||||
const DEFAULT_GRADE_COEFFICIENTS: Record<string, number> = (orgForCoeff?.defaultGradeCoefficients as Record<string, number>) || { A: 1.2, B: 1.0, C: 0.8, D: 0.6 }
|
||||
|
||||
let filled = 0
|
||||
let totalAmount = 0
|
||||
@@ -969,7 +990,18 @@ router.post('/batches/:batchId/fetch-performance', async (req: AuthRequest, res:
|
||||
|
||||
const perfRecord = perfMap.get(entry.employeeId)
|
||||
const grade = perfRecord?.grade || null
|
||||
const coefficient = perfRecord ? (gradeCoefficients[perfRecord.grade] || 1.0) : 1.0
|
||||
// 系数优先级:记录中保存的 coefficient > 模板 gradeRules 中的 coefficient > 系统默认值
|
||||
let coefficient = 1.0
|
||||
if (perfRecord) {
|
||||
if (perfRecord.coefficient != null) {
|
||||
coefficient = perfRecord.coefficient
|
||||
} else {
|
||||
// 查绩效记录关联的模板
|
||||
const perfRecWithTemplate = performanceRecords.find(r => r.employeeId === entry.employeeId && r.templateId)
|
||||
const templateCoeffs = perfRecWithTemplate?.templateId ? templateCoeffMap.get(perfRecWithTemplate.templateId) : null
|
||||
coefficient = (templateCoeffs && perfRecord.grade in templateCoeffs) ? templateCoeffs[perfRecord.grade] : (DEFAULT_GRADE_COEFFICIENTS[perfRecord.grade] || 1.0)
|
||||
}
|
||||
}
|
||||
const calculatedPerfSalary = Math.round(basePerfSalary * coefficient * 100) / 100
|
||||
|
||||
await prisma.batchEntry.update({
|
||||
|
||||
@@ -1271,7 +1271,7 @@ router.get('/:employeeId/performance', authMiddleware, async (req: AuthRequest,
|
||||
|
||||
router.post('/:employeeId/performance', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { period, periodType, score, grade, result, summary, improvementPlan, employeeAck, ackDate, reviewer, templateId, dimensionScores } = req.body
|
||||
const { period, periodType, score, grade, coefficient, result, summary, improvementPlan, employeeAck, ackDate, reviewer, templateId, dimensionScores } = req.body
|
||||
const record = await prisma.performanceRecord.upsert({
|
||||
where: { employeeId_period: { employeeId: req.params.employeeId, period } },
|
||||
create: {
|
||||
@@ -1281,6 +1281,7 @@ router.post('/:employeeId/performance', authMiddleware, async (req: AuthRequest,
|
||||
periodType: periodType || 'MONTHLY',
|
||||
score: score || 0,
|
||||
grade: grade || 'B',
|
||||
coefficient: coefficient != null ? Number(coefficient) : 1.0,
|
||||
result: result || 'QUALIFIED',
|
||||
summary,
|
||||
improvementPlan,
|
||||
@@ -1295,6 +1296,7 @@ router.post('/:employeeId/performance', authMiddleware, async (req: AuthRequest,
|
||||
periodType,
|
||||
score,
|
||||
grade,
|
||||
coefficient: coefficient != null ? Number(coefficient) : undefined,
|
||||
result,
|
||||
summary,
|
||||
improvementPlan,
|
||||
@@ -1312,7 +1314,7 @@ router.post('/:employeeId/performance', authMiddleware, async (req: AuthRequest,
|
||||
|
||||
router.put('/:employeeId/performance/:recordId', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { period, periodType, score, grade, result, summary, improvementPlan, employeeAck, ackDate, reviewer, templateId, dimensionScores } = req.body
|
||||
const { period, periodType, score, grade, coefficient, result, summary, improvementPlan, employeeAck, ackDate, reviewer, templateId, dimensionScores } = req.body
|
||||
const record = await prisma.performanceRecord.findFirst({
|
||||
where: { id: req.params.recordId, orgId: req.user!.orgId },
|
||||
})
|
||||
@@ -1324,6 +1326,7 @@ router.put('/:employeeId/performance/:recordId', authMiddleware, async (req: Aut
|
||||
periodType,
|
||||
score,
|
||||
grade,
|
||||
coefficient: coefficient != null ? Number(coefficient) : undefined,
|
||||
result,
|
||||
summary,
|
||||
improvementPlan,
|
||||
|
||||
@@ -28,7 +28,7 @@ router.get('/org', async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const org = await prisma.organization.findUnique({
|
||||
where: { id: req.user!.orgId },
|
||||
select: { id: true, name: true, plan: true, maxEmployees: true, city: true, contactName: true, contactPhone: true, payrollDays: true, payrollReminderDays: true, retirementReminderEnabled: true, esignPolicyEnabled: true, esignPayslipEnabled: true, esignOnboardingEnabled: true, createdAt: true },
|
||||
select: { id: true, name: true, plan: true, maxEmployees: true, city: true, contactName: true, contactPhone: true, payrollDays: true, payrollReminderDays: true, retirementReminderEnabled: true, esignPolicyEnabled: true, esignPayslipEnabled: true, esignOnboardingEnabled: true, defaultGradeCoefficients: true, createdAt: true },
|
||||
})
|
||||
res.json({ success: true, data: org })
|
||||
} catch (err) {
|
||||
@@ -39,7 +39,7 @@ router.get('/org', async (req: AuthRequest, res, next) => {
|
||||
// 更新企业信息
|
||||
router.put('/org', requireAdmin, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { name, payrollDays, payrollReminderDays, city, contactName, contactPhone, retirementReminderEnabled, esignPolicyEnabled, esignPayslipEnabled, esignOnboardingEnabled } = req.body as { name?: string; payrollDays?: number[]; payrollReminderDays?: number; city?: string; contactName?: string; contactPhone?: string; retirementReminderEnabled?: boolean; esignPolicyEnabled?: boolean; esignPayslipEnabled?: boolean; esignOnboardingEnabled?: boolean }
|
||||
const { name, payrollDays, payrollReminderDays, city, contactName, contactPhone, retirementReminderEnabled, esignPolicyEnabled, esignPayslipEnabled, esignOnboardingEnabled, defaultGradeCoefficients } = req.body as { name?: string; payrollDays?: number[]; payrollReminderDays?: number; city?: string; contactName?: string; contactPhone?: string; retirementReminderEnabled?: boolean; esignPolicyEnabled?: boolean; esignPayslipEnabled?: boolean; esignOnboardingEnabled?: boolean; defaultGradeCoefficients?: Record<string, number> }
|
||||
const updateData: any = {}
|
||||
if (name) updateData.name = name
|
||||
if (payrollDays !== undefined) updateData.payrollDays = payrollDays
|
||||
@@ -51,10 +51,11 @@ router.put('/org', requireAdmin, async (req: AuthRequest, res, next) => {
|
||||
if (esignPolicyEnabled !== undefined) updateData.esignPolicyEnabled = esignPolicyEnabled
|
||||
if (esignPayslipEnabled !== undefined) updateData.esignPayslipEnabled = esignPayslipEnabled
|
||||
if (esignOnboardingEnabled !== undefined) updateData.esignOnboardingEnabled = esignOnboardingEnabled
|
||||
if (defaultGradeCoefficients !== undefined) updateData.defaultGradeCoefficients = defaultGradeCoefficients
|
||||
const org = await prisma.organization.update({
|
||||
where: { id: req.user!.orgId },
|
||||
data: updateData,
|
||||
select: { id: true, name: true, plan: true, maxEmployees: true, city: true, contactName: true, contactPhone: true, payrollDays: true, payrollReminderDays: true, retirementReminderEnabled: true, esignPolicyEnabled: true, esignPayslipEnabled: true, esignOnboardingEnabled: true },
|
||||
select: { id: true, name: true, plan: true, maxEmployees: true, city: true, contactName: true, contactPhone: true, payrollDays: true, payrollReminderDays: true, retirementReminderEnabled: true, esignPolicyEnabled: true, esignPayslipEnabled: true, esignOnboardingEnabled: true, defaultGradeCoefficients: true },
|
||||
})
|
||||
res.json({ success: true, data: org })
|
||||
} catch (err) {
|
||||
|
||||
@@ -429,12 +429,14 @@ export async function detectMonthlyTasks(orgId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 社保和公积金按账户 paymentDay 生成提醒
|
||||
// 社保和公积金按账户 paymentDay 生成提醒,未设置时回退到组织 socialInsCutoffDay
|
||||
const org = await prisma.organization.findUnique({ where: { id: orgId }, select: { socialInsCutoffDay: true } })
|
||||
const defaultCutoffDay = org?.socialInsCutoffDay || 15
|
||||
const accounts = await prisma.socialAccount.findMany({
|
||||
where: { orgId, status: 'ACTIVE', paymentDay: { not: null } },
|
||||
where: { orgId, status: 'ACTIVE' },
|
||||
})
|
||||
for (const acc of accounts) {
|
||||
const day = acc.paymentDay!
|
||||
const day = acc.paymentDay || defaultCutoffDay
|
||||
if (today >= day) {
|
||||
const deadline = new Date(now.getFullYear(), now.getMonth(), day)
|
||||
const typeLabel = acc.type === 'SOCIAL' ? '社保' : '公积金'
|
||||
@@ -721,8 +723,9 @@ export async function getDashboardData(orgId: string) {
|
||||
where: { orgId, batch: { month: currentMonth, status: 'ARCHIVED' } },
|
||||
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 } }),
|
||||
// 社保/公积金配置:优先查 SocialYearStandard(新架构),回退到旧表
|
||||
prisma.socialYearStandard.findFirst({ where: { orgId, isCurrent: true, account: { type: 'SOCIAL' } } }).then(async r => r as any || (await prisma.socialInsuranceConfig.findFirst({ where: { orgId, isCurrent: true } }) as any)),
|
||||
prisma.socialYearStandard.findFirst({ where: { orgId, isCurrent: true, account: { type: 'HOUSING' } } }).then(async r => r as any || (await prisma.housingFundConfig.findFirst({ where: { orgId, isCurrent: true } }) as any)),
|
||||
prisma.laborContract.count({
|
||||
where: { orgId, createdAt: { gte: monthStart, lte: monthEnd } },
|
||||
}),
|
||||
@@ -1498,8 +1501,9 @@ export async function getComplianceScore(orgId: string) {
|
||||
prisma.attendanceConfirmation.count({ where: { orgId, status: 'PENDING' } }),
|
||||
prisma.payslip.count({ where: { orgId, confirmedAt: null } }),
|
||||
prisma.payslip.count({ where: { orgId } }),
|
||||
prisma.socialInsuranceConfig.findFirst({ where: { orgId, isCurrent: true } }),
|
||||
prisma.housingFundConfig.findFirst({ where: { orgId, isCurrent: true } }),
|
||||
// 社保/公积金配置:优先查 SocialYearStandard(新架构),回退到旧表
|
||||
prisma.socialYearStandard.findFirst({ where: { orgId, isCurrent: true, account: { type: 'SOCIAL' } } }).then(async r => r ? true : !!(await prisma.socialInsuranceConfig.findFirst({ where: { orgId, isCurrent: true } }))) as Promise<boolean>,
|
||||
prisma.socialYearStandard.findFirst({ where: { orgId, isCurrent: true, account: { type: 'HOUSING' } } }).then(async r => r ? true : !!(await prisma.housingFundConfig.findFirst({ where: { orgId, isCurrent: true } }))) as Promise<boolean>,
|
||||
prisma.riskItem.count({
|
||||
where: {
|
||||
orgId,
|
||||
@@ -1519,13 +1523,18 @@ export async function getComplianceScore(orgId: string) {
|
||||
? Math.max(0, 100 - Math.round((unconfirmedPayslips / totalPayslips) * 100))
|
||||
: 100
|
||||
const socialScore = (socialConfig ? 50 : 0) + (housingConfig ? 50 : 0)
|
||||
// 社保公积金待办:当月未完成的缴纳任务
|
||||
const currentMonthStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
|
||||
const socialTodoCount = await prisma.riskItem.count({
|
||||
where: { orgId, status: 'PENDING', type: 'MONTHLY', title: { startsWith: `${currentMonthStr}月 缴纳` } },
|
||||
})
|
||||
|
||||
const dimensions = [
|
||||
{ key: 'contract', name: '合同管理', score: contractScore, todoCount: unsignedContracts + pendingSignContracts + expiringContracts },
|
||||
{ key: 'policy', name: '规章制度', score: policyScore, todoCount: policiesWithoutPublish },
|
||||
{ key: 'attendance', name: '考勤工时', score: attendanceScore, todoCount: attendanceUnconfirmed },
|
||||
{ key: 'salary', name: '薪酬工资', score: salaryScore, todoCount: unconfirmedPayslips },
|
||||
{ key: 'social', name: '社保公积金', score: socialScore, todoCount: 0 },
|
||||
{ key: 'social', name: '社保公积金', score: socialScore, todoCount: socialTodoCount },
|
||||
]
|
||||
|
||||
const overallScore = Math.round(
|
||||
@@ -1679,10 +1688,17 @@ export async function getHealthCheck(orgId: string) {
|
||||
// democracyProgress 是 JSONB,用 status 字段判断未完成
|
||||
prisma.policyDocument.count({ where: { orgId, status: { not: 'PUBLISHED' } } }),
|
||||
prisma.policyDocument.count({ where: { orgId } }),
|
||||
prisma.socialInsuranceConfig.findFirst({ where: { orgId, isCurrent: true } }),
|
||||
prisma.housingFundConfig.findFirst({ where: { orgId, isCurrent: true } }),
|
||||
// 社保记录按员工去重
|
||||
prisma.employeeSocialInsRecord.groupBy({ by: ['employeeId'], where: { orgId, endMonth: null }, _count: true }).then(r => r.length),
|
||||
// 社保/公积金配置:优先查 SocialYearStandard(新架构),回退到旧表
|
||||
prisma.socialYearStandard.findFirst({ where: { orgId, isCurrent: true, account: { type: 'SOCIAL' } } }).then(async r => r ? true : !!(await prisma.socialInsuranceConfig.findFirst({ where: { orgId, isCurrent: true } }))) as Promise<boolean>,
|
||||
prisma.socialYearStandard.findFirst({ where: { orgId, isCurrent: true, account: { type: 'HOUSING' } } }).then(async r => r ? true : !!(await prisma.housingFundConfig.findFirst({ where: { orgId, isCurrent: true } }))) as Promise<boolean>,
|
||||
// 社保记录:计算有正式劳动合同但无社保记录的在职员工数(实习/劳务/外包等不缴社保)
|
||||
Promise.all([
|
||||
prisma.employee.findMany({
|
||||
where: { orgId, status: 'ACTIVE', contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } } },
|
||||
select: { id: true },
|
||||
}),
|
||||
prisma.employeeSocialInsRecord.groupBy({ by: ['employeeId'], where: { orgId, endMonth: null }, _count: true }).then(r => new Set(r.map(x => x.employeeId))),
|
||||
]).then(([emps, socialEmpIds]) => emps.filter(e => !socialEmpIds.has(e.id)).length),
|
||||
prisma.overtimeRecord.count({ where: { orgId, weekdayHours: { gt: 36 } } }),
|
||||
// 解聘合规
|
||||
prisma.terminationRecord.count({ where: { orgId } }),
|
||||
@@ -1703,6 +1719,7 @@ export async function getHealthCheck(orgId: string) {
|
||||
key: 'contract',
|
||||
name: '合同管理',
|
||||
score: Math.max(0, 100 - unsignedContracts * 15 - pendingSignContracts * 10 - expiringContracts * 5),
|
||||
todoCount: unsignedContracts + pendingSignContracts + expiringContracts,
|
||||
findings: [
|
||||
unsignedContracts > 0 ? `${unsignedContracts} 名员工未签书面合同,存在双倍工资风险` : '全员已有合同记录',
|
||||
pendingSignContracts > 0 ? `${pendingSignContracts} 份合同待签署(已创建未完成签署)` : '',
|
||||
@@ -1720,6 +1737,7 @@ export async function getHealthCheck(orgId: string) {
|
||||
score: totalPayslips > 0
|
||||
? Math.max(0, 100 - Math.round((unconfirmedPayslips / totalPayslips) * 100))
|
||||
: 100,
|
||||
todoCount: unconfirmedPayslips,
|
||||
findings: [
|
||||
totalPayslips > 0
|
||||
? `${unconfirmedPayslips}/${totalPayslips} 张工资条未确认`
|
||||
@@ -1737,6 +1755,9 @@ export async function getHealthCheck(orgId: string) {
|
||||
(housingConfig ? 30 : 0) +
|
||||
(employeesNoSocial === 0 ? 20 : Math.max(0, 20 - employeesNoSocial * 2))
|
||||
),
|
||||
todoCount: await prisma.riskItem.count({
|
||||
where: { orgId, status: 'PENDING', type: 'MONTHLY', title: { startsWith: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}月 缴纳` } },
|
||||
}),
|
||||
findings: [
|
||||
socialConfig ? '社保配置已设置' : '未配置社保比例,建议尽快设置',
|
||||
housingConfig ? '公积金配置已设置' : '未配置公积金比例',
|
||||
@@ -1751,6 +1772,7 @@ export async function getHealthCheck(orgId: string) {
|
||||
key: 'attendance',
|
||||
name: '考勤加班',
|
||||
score: Math.max(0, 100 - overtimeExcessive * 10),
|
||||
todoCount: overtimeExcessive,
|
||||
findings: [
|
||||
overtimeExcessive > 0 ? `${overtimeExcessive} 条加班记录月超时36小时,存在违法风险` : '加班时长合规',
|
||||
attendanceRecords > 0 ? `已记录 ${attendanceRecords} 条考勤数据` : '暂无考勤记录,建议建立考勤制度',
|
||||
@@ -1766,6 +1788,7 @@ export async function getHealthCheck(orgId: string) {
|
||||
score: totalPolicies > 0
|
||||
? Math.max(0, 100 - policiesWithoutPublish * 12)
|
||||
: 60,
|
||||
todoCount: policiesWithoutPublish,
|
||||
findings: [
|
||||
totalPolicies === 0 ? '尚未上传任何规章制度,建议建立基础制度体系' : `已管理 ${totalPolicies} 项制度`,
|
||||
policiesWithoutPublish > 0 ? `${policiesWithoutPublish} 项制度未完成民主程序,仲裁中可能无效` : '所有制度已完成民主公示',
|
||||
@@ -1784,6 +1807,7 @@ export async function getHealthCheck(orgId: string) {
|
||||
(terminationsWithChecklist / Math.max(1, completedTerminations)) * 40
|
||||
)
|
||||
: 100,
|
||||
todoCount: totalTerminations - completedTerminations,
|
||||
findings: [
|
||||
totalTerminations > 0
|
||||
? `共 ${totalTerminations} 起解聘,${completedTerminations} 起已完成流程`
|
||||
|
||||
Reference in New Issue
Block a user