feat: 移除ROI,新增待处理风险列表
- 移除ROI横幅和说明,改为年度总价值概览 - 后端新增 pendingRisks 查询,返回PENDING状态风险 - 前端新增待处理风险表格,按员工去重展示 - summary 改为包含待处理风险数量
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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>:双倍工资≤11个月(第82条)、经济补偿N≤12个月月薪≤社平3倍(第47条)、违法解除2N≤24个月(第87条)</span>
|
||||
|
||||
Reference in New Issue
Block a user