fix: 年度价值报告风险量化按劳动法设定法定上限

- 单条风险estimatedLoss封顶:双倍工资≤11个月(第82条)、经济补偿N≤12个月月薪≤社平3倍(第47条)、违法解除2N≤24个月(第87条)
- 年度规避损失按30%规避率打折(非每项风险都演变为实际仲裁)
- ROI上限9999%
- 前端计算说明同步更新
This commit is contained in:
selfrelease
2026-07-30 19:17:22 +08:00
parent 55286819ae
commit aced7929e1
2 changed files with 34 additions and 13 deletions
+27 -10
View File
@@ -15,8 +15,15 @@ function safeDecryptSalary(encrypted: string): number {
}
}
/**
* 社平工资3倍封顶(参照上海2024年度社平工资约12307元/月,3倍≈36921元)
* 《劳动合同法》第47条:月工资高于社平3倍的按3倍计,且补偿年限最高12年
*/
const SOCIAL_AVG_SALARY_3X = 36921
/**
* 风险量化:根据风险类型和员工数据估算预估损失金额
* 依据《劳动合同法》《劳动法》设定法定上限
* @returns { estimatedLoss, lossRange, deadline }
*/
function estimateRiskCost(
@@ -24,21 +31,26 @@ function estimateRiskCost(
employee?: { hireDate: Date; monthlySalary?: string } | null,
): { estimatedLoss: number; lossRange: [number, number]; deadline?: Date } {
const today = new Date()
const salary = employee ? safeDecryptSalary(employee.monthlySalary || '0') : 0
const rawSalary = employee ? safeDecryptSalary(employee.monthlySalary || '0') : 0
// 第47条:高收入者月薪封顶为社平3倍
const salary = Math.min(rawSalary, SOCIAL_AVG_SALARY_3X)
const workYears = employee
? Math.max(0, (today.getFullYear() - employee.hireDate.getFullYear()) +
(today.getMonth() - employee.hireDate.getMonth()) / 12)
: 0
const n = Math.max(0.5, Math.round(workYears * 2) / 2) // 经济补偿月数,不满半年算0.5
// 经济补偿月数:每满1年1个月,不满半年算0.5个月,第47条上限12年
const n = Math.min(12, Math.max(0.5, Math.round(workYears * 2) / 2))
// 根据风险标题关键词判断类型并估算
const title = risk.title
// 未签合同(>30天):双倍工资
// 《劳动合同法》第82条:超过1个月不满1年未签书面合同,每月支付2倍工资
// 超过1年视为已订立无固定期限合同,不再适用双倍工资,故最多罚11个月
if (title.includes('未签合同') && title.includes('天')) {
const daysMatch = title.match(/(\d+)天/)
const days = daysMatch ? parseInt(daysMatch[1]) : 0
const months = Math.ceil(days / 30)
const months = Math.min(11, Math.ceil(days / 30)) // 法定上限11个月
const loss = salary * months
return {
estimatedLoss: loss,
@@ -48,8 +60,9 @@ function estimateRiskCost(
}
// 合同到期未续签:经济补偿 N × 月薪
// 《劳动合同法》第47条:N≤12个月,月薪≤社平3倍
if (title.includes('到期') && title.includes('未续签')) {
const loss = salary * n
const loss = salary * n // n已封顶12
return {
estimatedLoss: loss,
lossRange: [loss, loss * 2], // 可能被认定为违法解除 2N
@@ -79,8 +92,9 @@ function estimateRiskCost(
}
// 三期/工伤/医疗期:违法解除风险 2N
// 《劳动合同法》第87条:违法解除按第47条标准的2倍赔偿,2N≤24个月
if (title.includes('孕期') || title.includes('哺乳期') || title.includes('工伤') || title.includes('医疗期')) {
const loss = salary * n * 2
const loss = salary * n * 2 // n已封顶12,故2N≤24
return {
estimatedLoss: loss,
lossRange: [loss, loss * 2],
@@ -1609,9 +1623,11 @@ export async function getAnnualValueReport(orgId: string, year: number) {
}))
// 价值计算
// 1. 规避损失 = 已解决风险的预估损失之和
// 1. 规避损失 = 已解决风险的预估损失之和 × 0.3(规避率30%,非每项风险都会演变为实际仲裁)
// 2. 节约工时 = AI 查询 * 0.5h + 合同审查 * 1h + 算薪批次 * 2h + 制度公示 * 1h
// 3. 节约成本 = 节约工时 * 100 元/h(HR 平均时薪)
const RISK_AVOIDANCE_RATE = 0.3 // 规避率:约30%的未处理风险会演变为实际劳动争议
const adjustedLossAvoided = Math.round(lossAvoided * RISK_AVOIDANCE_RATE)
const aiTimeSaved = aiConversations * 0.5 + aiReviews * 1.0
const payrollTimeSaved = payrollBatches * 2.0
const policyTimeSaved = policiesPublished * 1.0
@@ -1619,12 +1635,13 @@ export async function getAnnualValueReport(orgId: string, year: number) {
const timeSaved = Math.round(aiTimeSaved + payrollTimeSaved + policyTimeSaved + contractTimeSaved)
const costSaved = timeSaved * 100
// 总价值 = 规避损失 + 节约成本
const totalValue = lossAvoided + costSaved
// 总价值 = 规避损失(打折后) + 节约成本
const totalValue = adjustedLossAvoided + costSaved
// ROI = 总价值 / 系统成本(假设年费 12000 元)
// ROI = 总价值 / 系统成本(年费 12000 元),上限 9999%
const systemCost = 12000
const roi = systemCost > 0 ? Math.round((totalValue / systemCost) * 100) : 0
const rawRoi = systemCost > 0 ? Math.round((totalValue / systemCost) * 100) : 0
const roi = Math.min(rawRoi, 9999)
const metrics = {
risksResolved,
@@ -226,7 +226,7 @@ export default function AnnualValueReport() {
<div className="space-y-1 text-xs text-gray-600">
<div className="flex items-start gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-safe mt-1 flex-shrink-0" />
<span><b></b></span>
<span><b></b> × 30%</span>
</div>
<div className="flex items-start gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-blue-500 mt-1 flex-shrink-0" />
@@ -238,11 +238,15 @@ export default function AnnualValueReport() {
</div>
<div className="flex items-start gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-amber-500 mt-1 flex-shrink-0" />
<span><b></b> + </span>
<span><b></b> + </span>
</div>
<div className="flex items-start gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-gray-400 mt-1 flex-shrink-0" />
<span><b>ROI</b> ÷ ¥12,000× 100%</span>
<span><b>ROI</b> ÷ ¥12,000× 100% 9999%</span>
</div>
<div className="flex items-start gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-gray-300 mt-1 flex-shrink-0" />
<span><b></b>1182N123472N2487</span>
</div>
</div>
</Card>