feat: 移除ROI,新增待处理风险列表

- 移除ROI横幅和说明,改为年度总价值概览
- 后端新增 pendingRisks 查询,返回PENDING状态风险
- 前端新增待处理风险表格,按员工去重展示
- summary 改为包含待处理风险数量
This commit is contained in:
selfrelease
2026-07-31 10:04:04 +08:00
parent de73d20d68
commit 8014c9a139
2 changed files with 109 additions and 17 deletions
+48 -7
View File
@@ -1553,6 +1553,7 @@ export async function getAnnualValueReport(orgId: string, year: number) {
policiesPublished,
monthlyRisks,
employeeRiskDetails,
pendingRisks,
] = await Promise.all([
prisma.riskItem.count({
where: {
@@ -1631,6 +1632,20 @@ export async function getAnnualValueReport(orgId: string, year: number) {
},
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
@@ -1763,16 +1778,42 @@ export async function getAnnualValueReport(orgId: string, year: number) {
policiesPublished,
}
const summary = roi >= 1000
? `${year} 年系统创造价值约 ${fmtMoney(totalValue)}ROI 达 ${roi}%,规避潜在损失 ${fmtMoney(lossAvoided)},节约 HR 工时 ${timeSaved} 小时`
: roi >= 300
? `${year} 年系统创造价值约 ${fmtMoney(totalValue)}ROI ${roi}%,处理风险 ${risksResolved} 项,AI 辅助 ${aiConversations + aiReviews}`
: `${year} 年系统已运行,处理风险 ${risksResolved} 项,AI 辅助 ${aiConversations + aiReviews} 次,建议加强使用深度`
const summary = `${year} 年规避潜在损失 ${fmtMoney(lossAvoided)},节约 HR 工时 ${timeSaved} 小时,仍有 ${pendingRisks.length} 项风险待处理`
// 未解决风险按员工去重+风险类别去重
const pendingBreakdown: Record<string, {
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 {
year,
totalValue,
roi,
metrics,
timeline,
costSaved,
@@ -1780,6 +1821,7 @@ export async function getAnnualValueReport(orgId: string, year: number) {
lossAvoided,
adjustedLossAvoided,
employeeDetails,
pendingRiskDetails,
summary,
}
}
@@ -1795,7 +1837,6 @@ export async function saveAnnualValueReport(orgId: string, userId: string, year:
orgId,
year: result.year,
totalValue: result.totalValue,
roi: result.roi,
metrics: result.metrics as any,
timeline: result.timeline as any,
costSaved: result.costSaved,
+61 -10
View File
@@ -117,7 +117,7 @@ export default function AnnualValueReport() {
</div>
</div>
{/* ROI 横幅 */}
{/* 年度总价值概览 */}
<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 gap-4">
@@ -125,13 +125,13 @@ export default function AnnualValueReport() {
<TrendingUp className="w-8 h-8 text-primary" />
</div>
<div>
<div className="text-xs text-gray-500"> (ROI)</div>
<div className="text-3xl font-bold text-primary">{report.roi}%</div>
<div className="text-xs text-gray-500"></div>
<div className="text-3xl font-bold text-primary">{fmtMoney(report.totalValue)}</div>
</div>
</div>
<div className="text-right">
<div className="text-xs text-gray-500"></div>
<div className="text-2xl font-bold text-amber-600">{fmtMoney(report.totalValue)}</div>
<div className="text-xs text-gray-500"></div>
<div className="text-2xl font-bold text-safe">{fmtMoney(report.adjustedLossAvoided || 0)}</div>
</div>
</div>
<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 className="flex items-center gap-2">
<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>
</div>
<span className="text-gray-400">{new Date(r.createdAt).toLocaleDateString('zh-CN')}</span>
@@ -270,6 +270,61 @@ export default function AnnualValueReport() {
</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">
<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><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% 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>