feat(portal): add business risk timeline

This commit is contained in:
igor04091968
2026-06-04 13:23:39 +03:00
parent 0341e114a9
commit 3c7e1de96c
5 changed files with 546 additions and 29 deletions
@@ -343,6 +343,27 @@ h1 {
min-height: 132px;
}
.mini-kpi {
min-height: 96px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel-soft);
}
.mini-kpi span,
.mini-kpi small {
display: block;
color: var(--muted);
}
.mini-kpi strong {
display: block;
margin: 8px 0 4px;
font-size: 24px;
line-height: 1.1;
}
.kpi-value {
margin: 10px 0 6px;
font-size: 28px;
@@ -830,6 +830,7 @@ function renderOperator(data, report) {
${renderAgentQualityNodes(report?.agent_quality_nodes, report?.agent_quality_nodes_summary)}
${renderAgentCoverageSla(report?.agent_coverage_sla)}
${renderBusinessRisk(report?.business_risk)}
${renderBusinessRiskTimeline(report?.business_risk_history, report?.business_risk_history_summary)}
${renderOverviewAnalytics(report)}
<section class="dashboard-band">
<div class="band-head"><h3>Рабочая активность сотрудников</h3><span class="muted">загрузка, простои, перегруз и дисциплина процессов</span></div>
@@ -1650,6 +1651,69 @@ function businessRiskReasons(item) {
return `${reasons}. Trust ${item?.trust_score ?? 0}%, активность ${item?.activity_score ?? 0}%, тренд ${businessTrendText(item?.trend)}, проблемных узлов ${item?.problem_nodes_count ?? 0}`;
}
function renderBusinessRiskTimeline(history, summary) {
const s = summary || {};
const rows = Array.isArray(history) ? history.slice(-10).reverse() : [];
const status = Number(s.stable_high_risk || 0) > 0 || Number(s.new_high_risk || 0) > 0
? "WARN"
: "OK";
const counters = [
["Ухудшились", s.departments_worsened ?? 0, "рост уровня риска за период"],
["Улучшились", s.departments_improved ?? 0, "снижение уровня риска"],
["Стабильно высокий риск", s.stable_high_risk ?? 0, "3+ дня HIGH/CRITICAL"],
["Новый высокий риск", s.new_high_risk ?? 0, "последняя точка стала HIGH/CRITICAL"],
];
return `
<section class="card business-risk-timeline-card">
<div class="section-head">
<div>
<h3>Динамика рисков</h3>
<p class="muted">Как менялся бизнес-риск подразделений по накопленной daily history.</p>
</div>
<span class="badge ${statusClass(status)}">${ui(status)}</span>
</div>
<div class="summary-grid kpi-grid compact-kpis">
${counters.map(([label, value, hint]) => `
<article class="mini-kpi">
<span>${ui(label)}</span>
<strong>${escapeHtml(value)}</strong>
<small>${ui(hint)}</small>
</article>
`).join("")}
</div>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th>Дата</th>
<th>Подразделение</th>
<th>Риск</th>
<th>Причины</th>
</tr>
</thead>
<tbody>
${rows.length ? rows.map(item => `
<tr>
<td>${ui(item.date || "-")}</td>
<td><strong>${ui(item.department || "Без подразделения")}</strong></td>
<td><span class="badge ${statusClass(item.risk_level)}">${ui(item.risk_level || "UNKNOWN")}</span></td>
<td>${ui(Array.isArray(item.reasons) && item.reasons.length ? item.reasons.join("; ") : "нет существенных причин")}</td>
</tr>
`).join("") : `
<tr>
<td>-</td>
<td>История не накоплена</td>
<td><span class="badge status-unknown">UNKNOWN</span></td>
<td>Нужно дождаться daily history.</td>
</tr>
`}
</tbody>
</table>
</div>
</section>
`;
}
function businessTrendText(value) {
const trend = String(value || "UNKNOWN").toUpperCase();
if (trend === "FALLING") return "падает";
@@ -1697,6 +1761,7 @@ function renderReports(data) {
${renderAgentQualityNodes(data.agent_quality_nodes, data.agent_quality_nodes_summary)}
${renderAgentCoverageSla(data.agent_coverage_sla)}
${renderBusinessRisk(data.business_risk)}
${renderBusinessRiskTimeline(data.business_risk_history, data.business_risk_history_summary)}
${renderUebaRisk(data.ueba_risk)}
${renderWorkforceIndexExplanation(data.workforce_policy)}
<h3 class="section-title">Срезы отчета</h3>