feat: 移除ROI,新增待处理风险列表
- 移除ROI横幅和说明,改为年度总价值概览 - 后端新增 pendingRisks 查询,返回PENDING状态风险 - 前端新增待处理风险表格,按员工去重展示 - summary 改为包含待处理风险数量
This commit is contained in:
@@ -1553,6 +1553,7 @@ export async function getAnnualValueReport(orgId: string, year: number) {
|
|||||||
policiesPublished,
|
policiesPublished,
|
||||||
monthlyRisks,
|
monthlyRisks,
|
||||||
employeeRiskDetails,
|
employeeRiskDetails,
|
||||||
|
pendingRisks,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
prisma.riskItem.count({
|
prisma.riskItem.count({
|
||||||
where: {
|
where: {
|
||||||
@@ -1631,6 +1632,20 @@ export async function getAnnualValueReport(orgId: string, year: number) {
|
|||||||
},
|
},
|
||||||
orderBy: { estimatedLoss: 'desc' },
|
orderBy: { estimatedLoss: 'desc' },
|
||||||
}),
|
}),
|
||||||
|
// 未解决的风险列表(PENDING状态),按预估损失降序,最多100条
|
||||||
|
prisma.riskItem.findMany({
|
||||||
|
where: {
|
||||||
|
orgId, status: 'PENDING',
|
||||||
|
estimatedLoss: { gt: 0 },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
employee: {
|
||||||
|
select: { id: true, name: true, department: true, idCardHash: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { estimatedLoss: 'desc' },
|
||||||
|
take: 100,
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
// 按身份证号去重(同一人可能有多条 Employee 记录),无身份证号时回退到 employeeId
|
// 按身份证号去重(同一人可能有多条 Employee 记录),无身份证号时回退到 employeeId
|
||||||
@@ -1763,16 +1778,42 @@ export async function getAnnualValueReport(orgId: string, year: number) {
|
|||||||
policiesPublished,
|
policiesPublished,
|
||||||
}
|
}
|
||||||
|
|
||||||
const summary = roi >= 1000
|
const summary = `${year} 年规避潜在损失 ${fmtMoney(lossAvoided)},节约 HR 工时 ${timeSaved} 小时,仍有 ${pendingRisks.length} 项风险待处理`
|
||||||
? `${year} 年系统创造价值约 ${fmtMoney(totalValue)},ROI 达 ${roi}%,规避潜在损失 ${fmtMoney(lossAvoided)},节约 HR 工时 ${timeSaved} 小时`
|
|
||||||
: roi >= 300
|
// 未解决风险按员工去重+风险类别去重
|
||||||
? `${year} 年系统创造价值约 ${fmtMoney(totalValue)},ROI ${roi}%,处理风险 ${risksResolved} 项,AI 辅助 ${aiConversations + aiReviews} 次`
|
const pendingBreakdown: Record<string, {
|
||||||
: `${year} 年系统已运行,处理风险 ${risksResolved} 项,AI 辅助 ${aiConversations + aiReviews} 次,建议加强使用深度`
|
name: string
|
||||||
|
department: string
|
||||||
|
riskCount: number
|
||||||
|
totalLoss: number
|
||||||
|
details: { title: string; estimatedLoss: number; legalBasis: string }[]
|
||||||
|
}> = {}
|
||||||
|
for (const r of pendingRisks) {
|
||||||
|
const personKey = r.employee?.idCardHash || r.employee?.name || r.employeeId || '_unknown'
|
||||||
|
if (!pendingBreakdown[personKey]) {
|
||||||
|
pendingBreakdown[personKey] = {
|
||||||
|
name: r.employee?.name || '未知员工',
|
||||||
|
department: r.employee?.department || '-',
|
||||||
|
riskCount: 0,
|
||||||
|
totalLoss: 0,
|
||||||
|
details: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingBreakdown[personKey].riskCount++
|
||||||
|
pendingBreakdown[personKey].totalLoss += r.estimatedLoss || 0
|
||||||
|
pendingBreakdown[personKey].details.push({
|
||||||
|
title: r.title,
|
||||||
|
estimatedLoss: r.estimatedLoss || 0,
|
||||||
|
legalBasis: getLegalBasis(r.title),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const pendingRiskDetails = Object.values(pendingBreakdown)
|
||||||
|
.map(p => ({ ...p, details: p.details.sort((a, b) => b.estimatedLoss - a.estimatedLoss) }))
|
||||||
|
.sort((a, b) => b.totalLoss - a.totalLoss)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
year,
|
year,
|
||||||
totalValue,
|
totalValue,
|
||||||
roi,
|
|
||||||
metrics,
|
metrics,
|
||||||
timeline,
|
timeline,
|
||||||
costSaved,
|
costSaved,
|
||||||
@@ -1780,6 +1821,7 @@ export async function getAnnualValueReport(orgId: string, year: number) {
|
|||||||
lossAvoided,
|
lossAvoided,
|
||||||
adjustedLossAvoided,
|
adjustedLossAvoided,
|
||||||
employeeDetails,
|
employeeDetails,
|
||||||
|
pendingRiskDetails,
|
||||||
summary,
|
summary,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1795,7 +1837,6 @@ export async function saveAnnualValueReport(orgId: string, userId: string, year:
|
|||||||
orgId,
|
orgId,
|
||||||
year: result.year,
|
year: result.year,
|
||||||
totalValue: result.totalValue,
|
totalValue: result.totalValue,
|
||||||
roi: result.roi,
|
|
||||||
metrics: result.metrics as any,
|
metrics: result.metrics as any,
|
||||||
timeline: result.timeline as any,
|
timeline: result.timeline as any,
|
||||||
costSaved: result.costSaved,
|
costSaved: result.costSaved,
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export default function AnnualValueReport() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ROI 横幅 */}
|
{/* 年度总价值概览 */}
|
||||||
<Card className="bg-gradient-to-r from-primary/10 to-amber-50 border-primary/20">
|
<Card className="bg-gradient-to-r from-primary/10 to-amber-50 border-primary/20">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -125,13 +125,13 @@ export default function AnnualValueReport() {
|
|||||||
<TrendingUp className="w-8 h-8 text-primary" />
|
<TrendingUp className="w-8 h-8 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-gray-500">投资回报率 (ROI)</div>
|
<div className="text-xs text-gray-500">年度总价值</div>
|
||||||
<div className="text-3xl font-bold text-primary">{report.roi}%</div>
|
<div className="text-3xl font-bold text-primary">{fmtMoney(report.totalValue)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-xs text-gray-500">年度总价值</div>
|
<div className="text-xs text-gray-500">规避损失</div>
|
||||||
<div className="text-2xl font-bold text-amber-600">{fmtMoney(report.totalValue)}</div>
|
<div className="text-2xl font-bold text-safe">{fmtMoney(report.adjustedLossAvoided || 0)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-3 text-sm text-gray-700">{report.summary}</p>
|
<p className="mt-3 text-sm text-gray-700">{report.summary}</p>
|
||||||
@@ -168,7 +168,7 @@ export default function AnnualValueReport() {
|
|||||||
<div key={r.id} className="flex items-center justify-between p-2 rounded-md bg-gray-50 text-xs">
|
<div key={r.id} className="flex items-center justify-between p-2 rounded-md bg-gray-50 text-xs">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium">{r.year} 年</span>
|
<span className="font-medium">{r.year} 年</span>
|
||||||
<span className="font-bold text-primary">ROI {r.roi}%</span>
|
<span className="font-bold text-primary">总价值 {fmtMoney(r.totalValue)}</span>
|
||||||
<span className="text-gray-500">{r.summary}</span>
|
<span className="text-gray-500">{r.summary}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-gray-400">{new Date(r.createdAt).toLocaleDateString('zh-CN')}</span>
|
<span className="text-gray-400">{new Date(r.createdAt).toLocaleDateString('zh-CN')}</span>
|
||||||
@@ -270,6 +270,61 @@ export default function AnnualValueReport() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 未解决风险列表 */}
|
||||||
|
{report.pendingRiskDetails && report.pendingRiskDetails.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<h2 className="text-sm font-medium mb-3 flex items-center gap-1.5">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-danger" />
|
||||||
|
待处理风险 ({report.pendingRiskDetails.length} 人)
|
||||||
|
</h2>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-xs">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-gray-200 text-gray-500">
|
||||||
|
<th className="text-left py-2 px-2 font-medium">员工</th>
|
||||||
|
<th className="text-left py-2 px-2 font-medium">部门</th>
|
||||||
|
<th className="text-center py-2 px-2 font-medium">风险数</th>
|
||||||
|
<th className="text-right py-2 px-2 font-medium">潜在损失</th>
|
||||||
|
<th className="text-left py-2 px-2 font-medium">风险明细</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{report.pendingRiskDetails.map((emp: any, idx: number) => (
|
||||||
|
<tr key={idx} className="border-b border-gray-50">
|
||||||
|
<td className="py-2 px-2 font-medium">{emp.name}</td>
|
||||||
|
<td className="py-2 px-2 text-gray-600">{emp.department}</td>
|
||||||
|
<td className="py-2 px-2 text-center">{emp.riskCount}</td>
|
||||||
|
<td className="py-2 px-2 text-right font-bold text-danger">¥{emp.totalLoss.toLocaleString()}</td>
|
||||||
|
<td className="py-2 px-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{emp.details.map((d: any, i: number) => (
|
||||||
|
<div key={i} className="flex items-center gap-1.5">
|
||||||
|
<span className="text-gray-700">{d.title}</span>
|
||||||
|
<span className="text-gray-400">·</span>
|
||||||
|
<span className="text-gray-500">{d.legalBasis}</span>
|
||||||
|
<span className="text-gray-400">·</span>
|
||||||
|
<span className="text-gray-600">¥{d.estimatedLoss.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr className="border-t-2 border-gray-200 font-bold">
|
||||||
|
<td className="py-2 px-2" colSpan={3}>合计</td>
|
||||||
|
<td className="py-2 px-2 text-right text-danger">
|
||||||
|
¥{report.pendingRiskDetails.reduce((s: number, e: any) => s + e.totalLoss, 0).toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td className="py-2 px-2"></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 价值计算说明 */}
|
{/* 价值计算说明 */}
|
||||||
<Card className="bg-gray-50">
|
<Card className="bg-gray-50">
|
||||||
<h2 className="text-sm font-medium mb-2">价值计算说明</h2>
|
<h2 className="text-sm font-medium mb-2">价值计算说明</h2>
|
||||||
@@ -290,10 +345,6 @@ export default function AnnualValueReport() {
|
|||||||
<span className="w-1.5 h-1.5 rounded-full bg-amber-500 mt-1 flex-shrink-0" />
|
<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>
|
||||||
<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%,上限 9999%</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-1.5">
|
<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 className="w-1.5 h-1.5 rounded-full bg-gray-300 mt-1 flex-shrink-0" />
|
||||||
<span><b>法定上限</b>:双倍工资≤11个月(第82条)、经济补偿N≤12个月月薪≤社平3倍(第47条)、违法解除2N≤24个月(第87条)</span>
|
<span><b>法定上限</b>:双倍工资≤11个月(第82条)、经济补偿N≤12个月月薪≤社平3倍(第47条)、违法解除2N≤24个月(第87条)</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user