[FEAT] Pagina Rischi (Simon): matrice a 4 fasce + toggle inerente/residuo; trasversali 12 col in fondo
Richieste Simon sulla finestra Rischi:
1) Matrice 5×5: eliminata la fascia 10-14; fasce = 1-2 basso (verde), 3-8 medio
(arancione), 9-12 alto (rosso tenue), 15-25 critico (rosso). Unificate con le
bande Alert. (matrixBand + CSS band-low/med/high/crit; rimosse level-1..10).
2) La matrice riepiloga i giudizi INERENTI o RESIDUI con un tasto di selezione
(setMatrixView; matrixPoints legge likelihood/impact o residual_*).
3) Rischi trasversali:
3.1 sezione spostata in FONDO (matrice dai requisiti risalita subito dopo la
tabella derivata);
3.2 tabella trasversali convertita alle STESSE 12 colonne (inerente+residuo+
alert+esito), editing inline P/I via api.updateRisk (onTransversalScoreChange);
3.3 tasti "AI Suggerisci" e "Nuovo Rischio" spostati in testa alla tabella;
3.4 seconda matrice 5×5 dei trasversali con lo stesso toggle inerente/residuo;
3.5 tasto Elimina per ogni rischio trasversale (confirmDeleteRisk).
- Solo frontend/statico. version 1.27.0 + SW cache bump.
- Verificato puppeteer (996002): 4 bande, 2 toggle funzionanti, 12 col, 6 delete, 0 errori.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b8c7d3c9b9
commit
4809d8e895
+235
-139
@@ -134,16 +134,16 @@
|
|||||||
transform: scale(1.04);
|
transform: scale(1.04);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
.matrix-cell.level-1 { background: #dcfce7; }
|
/* Bande di rischio (spec Simon): 1-2 basso verde · 3-8 medio arancione · 9-12 alto rosso tenue · 15-25 critico rosso */
|
||||||
.matrix-cell.level-2 { background: #bbf7d0; }
|
.matrix-cell.band-low { background: #bbf7d0; }
|
||||||
.matrix-cell.level-3 { background: #fef9c3; }
|
.matrix-cell.band-med { background: #fdba74; }
|
||||||
.matrix-cell.level-4 { background: #fde68a; }
|
.matrix-cell.band-high { background: #f87171; }
|
||||||
.matrix-cell.level-5 { background: #fed7aa; }
|
.matrix-cell.band-crit { background: #dc2626; }
|
||||||
.matrix-cell.level-6 { background: #fdba74; }
|
/* Toggle Inerente/Residuo sopra ciascuna matrice */
|
||||||
.matrix-cell.level-7 { background: #fb923c; }
|
.mx-toggle { display:inline-flex; border:1px solid var(--gray-300); border-radius:8px; overflow:hidden; }
|
||||||
.matrix-cell.level-8 { background: #fca5a5; }
|
.mx-toggle button { border:0; background:#fff; padding:6px 14px; font-size:0.8rem; font-weight:700; color:var(--gray-600); cursor:pointer; }
|
||||||
.matrix-cell.level-9 { background: #f87171; }
|
.mx-toggle button.on { background:var(--primary); color:#fff; }
|
||||||
.matrix-cell.level-10 { background: #ef4444; }
|
.mx-head { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-bottom:10px; }
|
||||||
|
|
||||||
.matrix-dot {
|
.matrix-dot {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
@@ -300,11 +300,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Requisiti del framework: riquadro a scorrimento verticale (ticket #501/1) */
|
/* Requisiti del framework: riquadro a scorrimento verticale (ticket #501/1) */
|
||||||
#view-derived .table-container.scroll-box {
|
#view-derived .table-container.scroll-box,
|
||||||
|
#view-table .table-container.scroll-box {
|
||||||
max-height: 60vh;
|
max-height: 60vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
#view-derived .table-container.scroll-box thead th {
|
#view-derived .table-container.scroll-box thead th,
|
||||||
|
#view-table .table-container.scroll-box thead th {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
background: var(--gray-50);
|
background: var(--gray-50);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@@ -391,12 +393,7 @@
|
|||||||
<h1 data-i18n="risks.title">Gestione Rischi</h1>
|
<h1 data-i18n="risks.title">Gestione Rischi</h1>
|
||||||
<p class="sub">Registro rischi e matrice 5×5</p>
|
<p class="sub">Registro rischi e matrice 5×5</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-header-actions" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
<!-- I tasti AI Suggerisci / Nuovo Rischio sono in testa alla tabella "Rischi trasversali" (3.3) -->
|
||||||
<button class="btn-ghost" id="btn-ai-suggest" onclick="aiSuggest()">
|
|
||||||
<i data-lucide="sparkles" class="ic"></i>AI Suggerisci
|
|
||||||
</button>
|
|
||||||
<button class="btn-primary" id="btn-new-risk" onclick="showRiskModal()"><i data-lucide="plus" class="ic"></i>Nuovo Rischio</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-body">
|
<div class="content-body">
|
||||||
@@ -452,7 +449,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── SEZIONE 2: Rischi trasversali (multirischio / all-hazards) — accordion (#501 p3) ── -->
|
<!-- ── Matrice 5×5 dei RISCHI DAI REQUISITI (toggle Inerente/Residuo — punto 2) ── -->
|
||||||
|
<div id="view-matrix" style="margin-top:16px;">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="mx-head" style="width:100%;">
|
||||||
|
<div>
|
||||||
|
<h3 style="margin:0;">Matrice di Rischio 5×5 — Rischi dai requisiti</h3>
|
||||||
|
<span class="text-muted" style="font-size:0.8rem;">Esclusi i requisiti non applicabili alla classe</span>
|
||||||
|
</div>
|
||||||
|
<div class="mx-toggle" role="group" aria-label="Vista matrice rischi dai requisiti">
|
||||||
|
<button type="button" id="mxd-inh" class="on" onclick="setMatrixView('derived','inherent')">Inerente</button>
|
||||||
|
<button type="button" id="mxd-res" onclick="setMatrixView('derived','residual')">Residuo</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="risk-matrix-container">
|
||||||
|
<div class="risk-matrix-wrapper">
|
||||||
|
<div class="risk-matrix-y-label">Probabilita'</div>
|
||||||
|
<div class="risk-matrix-grid-wrapper">
|
||||||
|
<div class="risk-matrix" id="risk-matrix-grid-derived">
|
||||||
|
<div class="spinner" style="margin:60px auto; grid-column: span 6;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="risk-matrix-x-label">Impatto</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="matrix-legend">
|
||||||
|
<div class="matrix-legend-item"><div class="matrix-legend-color" style="background:#bbf7d0;"></div>Basso (1-2)</div>
|
||||||
|
<div class="matrix-legend-item"><div class="matrix-legend-color" style="background:#fdba74;"></div>Medio (3-8)</div>
|
||||||
|
<div class="matrix-legend-item"><div class="matrix-legend-color" style="background:#f87171;"></div>Alto (9-12)</div>
|
||||||
|
<div class="matrix-legend-item"><div class="matrix-legend-color" style="background:#dc2626;"></div>Critico (15-25)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted" style="font-size:0.78rem;text-align:center;margin:16px auto 0;max-width:640px;line-height:1.5;">La matrice 5×5 è uno strumento di rappresentazione, non un obbligo normativo (art. 24 D.Lgs. 138/2024). Il tasto <b>Inerente/Residuo</b> mostra i giudizi prima o dopo il trattamento.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── SEZIONE 2 (in fondo): Rischi trasversali (multirischio / all-hazards) — accordion (#501 p3) ── -->
|
||||||
<details id="allhazard-section" class="allhazard" open>
|
<details id="allhazard-section" class="allhazard" open>
|
||||||
<summary class="allhazard-summary">
|
<summary class="allhazard-summary">
|
||||||
<span class="ah-caret" aria-hidden="true">
|
<span class="ah-caret" aria-hidden="true">
|
||||||
@@ -463,9 +498,14 @@
|
|||||||
</summary>
|
</summary>
|
||||||
<p class="text-muted" style="font-size:0.82rem;margin:10px 0 16px;line-height:1.5;" data-i18n="risks.custom_help">Qui si registrano le minacce non riconducibili a un singolo requisito (ambiente fisico, catena di fornitura, eventi naturali, errore umano), come richiesto dall'approccio multirischio/all-hazards (art. 24 D.Lgs. 138/2024, che recepisce l'art. 21 §2 della Direttiva (UE) 2022/2555). Si distinguono dai «rischi di non conformità» della tabella qui sopra, che misurano il mancato o parziale soddisfacimento dei singoli requisiti.</p>
|
<p class="text-muted" style="font-size:0.82rem;margin:10px 0 16px;line-height:1.5;" data-i18n="risks.custom_help">Qui si registrano le minacce non riconducibili a un singolo requisito (ambiente fisico, catena di fornitura, eventi naturali, errore umano), come richiesto dall'approccio multirischio/all-hazards (art. 24 D.Lgs. 138/2024, che recepisce l'art. 21 §2 della Direttiva (UE) 2022/2555). Si distinguono dai «rischi di non conformità» della tabella qui sopra, che misurano il mancato o parziale soddisfacimento dei singoli requisiti.</p>
|
||||||
|
|
||||||
<!-- Table View (rischi trasversali) -->
|
<!-- Table View (rischi trasversali) — tasti in testa (3.3) + tabella 12 colonne (3.2) -->
|
||||||
<div id="view-table">
|
<div id="view-table">
|
||||||
<div class="filters-bar">
|
<div class="mx-head" style="margin-bottom:12px;">
|
||||||
|
<div class="content-header-actions" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||||
|
<button class="btn-ghost" id="btn-ai-suggest" onclick="aiSuggest()"><i data-lucide="sparkles" class="ic"></i>AI Suggerisci</button>
|
||||||
|
<button class="btn-primary" id="btn-new-risk" onclick="showRiskModal()"><i data-lucide="plus" class="ic"></i>Nuovo Rischio</button>
|
||||||
|
</div>
|
||||||
|
<div class="filters-bar" style="margin:0;">
|
||||||
<select class="form-select" id="filter-category" onchange="loadRisks()" aria-label="Filtra per categoria">
|
<select class="form-select" id="filter-category" onchange="loadRisks()" aria-label="Filtra per categoria">
|
||||||
<option value="">Tutte le categorie</option>
|
<option value="">Tutte le categorie</option>
|
||||||
<option value="cyber">Cyber</option>
|
<option value="cyber">Cyber</option>
|
||||||
@@ -484,25 +524,34 @@
|
|||||||
<option value="closed">Chiuso</option>
|
<option value="closed">Chiuso</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="table-container">
|
<div class="table-container scroll-box">
|
||||||
<table class="table-clickable">
|
<table class="derived-12">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Codice</th>
|
<th scope="col" rowspan="2">Codice rischio</th>
|
||||||
<th scope="col">Titolo</th>
|
<th scope="col" rowspan="2">Descrizione rischio</th>
|
||||||
<th scope="col">Categoria</th>
|
<th scope="colgroup" colspan="3" class="grp grp-inh">Rischio Inerente</th>
|
||||||
<th scope="col">Probabilita'</th>
|
<th scope="col" rowspan="2">Alert</th>
|
||||||
|
<th scope="col" rowspan="2">Rischio Inerente</th>
|
||||||
|
<th scope="colgroup" colspan="3" class="grp grp-res">Rischio Residuo</th>
|
||||||
|
<th scope="col" rowspan="2">Esito residuo</th>
|
||||||
|
<th scope="col" rowspan="2">Rischio Residuo</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Probabilità</th>
|
||||||
<th scope="col">Impatto</th>
|
<th scope="col">Impatto</th>
|
||||||
<th scope="col">Score</th>
|
<th scope="col">Valutazione</th>
|
||||||
<th scope="col">Stato</th>
|
<th scope="col">Probabilità</th>
|
||||||
<th scope="col">Azioni</th>
|
<th scope="col">Impatto</th>
|
||||||
|
<th scope="col">Valutazione</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="risks-table-body">
|
<tbody id="risks-table-body">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8">
|
<td colspan="12">
|
||||||
<div class="spinner" style="margin:40px auto;"></div>
|
<div class="spinner" style="margin:40px auto;"></div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -511,53 +560,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
|
||||||
|
|
||||||
<!-- Matrix View (sempre visibile) -->
|
<!-- ── Matrice 5×5 dei RISCHI TRASVERSALI (toggle Inerente/Residuo — punto 3.4) ── -->
|
||||||
<div id="view-matrix" style="margin-top:16px;">
|
<div id="view-matrix-trans" style="margin-top:18px;">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3>Matrice di Rischio 5x5</h3>
|
<div class="mx-head" style="width:100%;">
|
||||||
<span class="text-muted" style="font-size:0.8rem;" data-i18n="risks.matrix_subtitle">Rischi da requisiti (valutati) + aggiuntivi — esclusi i non applicabili</span>
|
<div>
|
||||||
|
<h3 style="margin:0;">Matrice di Rischio 5×5 — Rischi trasversali</h3>
|
||||||
|
<span class="text-muted" style="font-size:0.8rem;">Solo i rischi trasversali registrati sopra</span>
|
||||||
|
</div>
|
||||||
|
<div class="mx-toggle" role="group" aria-label="Vista matrice rischi trasversali">
|
||||||
|
<button type="button" id="mxt-inh" class="on" onclick="setMatrixView('trans','inherent')">Inerente</button>
|
||||||
|
<button type="button" id="mxt-res" onclick="setMatrixView('trans','residual')">Residuo</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="risk-matrix-container">
|
<div class="risk-matrix-container">
|
||||||
<div class="risk-matrix-wrapper">
|
<div class="risk-matrix-wrapper">
|
||||||
<div class="risk-matrix-y-label">Probabilita'</div>
|
<div class="risk-matrix-y-label">Probabilita'</div>
|
||||||
<div class="risk-matrix-grid-wrapper">
|
<div class="risk-matrix-grid-wrapper">
|
||||||
<div class="risk-matrix" id="risk-matrix-grid">
|
<div class="risk-matrix" id="risk-matrix-grid-transversal">
|
||||||
<div class="spinner" style="margin:60px auto; grid-column: span 6;"></div>
|
<div class="spinner" style="margin:60px auto; grid-column: span 6;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="risk-matrix-x-label">Impatto</div>
|
<div class="risk-matrix-x-label">Impatto</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="matrix-legend">
|
<div class="matrix-legend">
|
||||||
<div class="matrix-legend-item">
|
<div class="matrix-legend-item"><div class="matrix-legend-color" style="background:#bbf7d0;"></div>Basso (1-2)</div>
|
||||||
<div class="matrix-legend-color" style="background:#bbf7d0;"></div>
|
<div class="matrix-legend-item"><div class="matrix-legend-color" style="background:#fdba74;"></div>Medio (3-8)</div>
|
||||||
Basso (1-4)
|
<div class="matrix-legend-item"><div class="matrix-legend-color" style="background:#f87171;"></div>Alto (9-12)</div>
|
||||||
</div>
|
<div class="matrix-legend-item"><div class="matrix-legend-color" style="background:#dc2626;"></div>Critico (15-25)</div>
|
||||||
<div class="matrix-legend-item">
|
|
||||||
<div class="matrix-legend-color" style="background:#fde68a;"></div>
|
|
||||||
Medio (5-9)
|
|
||||||
</div>
|
|
||||||
<div class="matrix-legend-item">
|
|
||||||
<div class="matrix-legend-color" style="background:#fdba74;"></div>
|
|
||||||
Alto (10-14)
|
|
||||||
</div>
|
|
||||||
<div class="matrix-legend-item">
|
|
||||||
<div class="matrix-legend-color" style="background:#fb923c;"></div>
|
|
||||||
Elevato (15-19)
|
|
||||||
</div>
|
|
||||||
<div class="matrix-legend-item">
|
|
||||||
<div class="matrix-legend-color" style="background:#ef4444;"></div>
|
|
||||||
Critico (20-25)
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-muted" style="font-size:0.78rem;text-align:center;margin:16px auto 0;max-width:640px;line-height:1.5;" data-i18n="risks.matrix_caption">La matrice 5×5 è uno strumento di rappresentazione, non un obbligo normativo (art. 24 D.Lgs. 138/2024).</p>
|
<p class="text-muted" style="font-size:0.78rem;text-align:center;margin:16px auto 0;max-width:640px;line-height:1.5;">La matrice 5×5 è uno strumento di rappresentazione, non un obbligo normativo (art. 24 D.Lgs. 138/2024).</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<!-- Detail View -->
|
<!-- Detail View -->
|
||||||
<div id="view-detail" class="hidden"></div>
|
<div id="view-detail" class="hidden"></div>
|
||||||
@@ -1045,6 +1086,7 @@
|
|||||||
risksData = (result.data && result.data.items) || [];
|
risksData = (result.data && result.data.items) || [];
|
||||||
renderRisksTable(risksData);
|
renderRisksTable(risksData);
|
||||||
updateAllhazardHeader(risksData.length);
|
updateAllhazardHeader(risksData.length);
|
||||||
|
loadTransversalMatrix();
|
||||||
} else {
|
} else {
|
||||||
showNotification(result.message || 'Errore nel caricamento rischi', 'error');
|
showNotification(result.message || 'Errore nel caricamento rischi', 'error');
|
||||||
}
|
}
|
||||||
@@ -1076,46 +1118,99 @@
|
|||||||
|
|
||||||
if (!risks || risks.length === 0) {
|
if (!risks || risks.length === 0) {
|
||||||
tbody.innerHTML = `
|
tbody.innerHTML = `
|
||||||
<tr><td colspan="8">
|
<tr><td colspan="12">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2.001A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clip-rule="evenodd"/></svg>
|
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2.001A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clip-rule="evenodd"/></svg>
|
||||||
<h4>Nessun rischio registrato</h4>
|
<h4>Nessun rischio trasversale registrato</h4>
|
||||||
<p>Clicca "Nuovo Rischio" per aggiungere il primo rischio, oppure usa "AI Suggerisci".</p>
|
<p>Usa "Nuovo Rischio" o "AI Suggerisci" qui sopra per aggiungere minacce non legate a un singolo requisito.</p>
|
||||||
</div>
|
</div>
|
||||||
</td></tr>`;
|
</td></tr>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const editable = canEvaluate;
|
||||||
let html = '';
|
let html = '';
|
||||||
risks.forEach(r => {
|
risks.forEach(r => {
|
||||||
const score = r.inherent_risk_score || 0;
|
const id = r.id;
|
||||||
const scoreClass = getScoreClassForRisk(score);
|
const iL = (r.likelihood >= 1 && r.likelihood <= 5) ? parseInt(r.likelihood) : null;
|
||||||
const statusBadge = getStatusBadge(r.status);
|
const iI = (r.impact >= 1 && r.impact <= 5) ? parseInt(r.impact) : null;
|
||||||
const categoryLabel = CATEGORY_LABELS[r.category] || r.category;
|
const iP = (iL != null && iI != null) ? iL * iI : null;
|
||||||
|
const rL = (r.residual_likelihood >= 1 && r.residual_likelihood <= 5) ? parseInt(r.residual_likelihood) : null;
|
||||||
|
const rI = (r.residual_impact >= 1 && r.residual_impact <= 5) ? parseInt(r.residual_impact) : null;
|
||||||
|
const rP = (rL != null && rI != null) ? rL * rI : null;
|
||||||
|
|
||||||
|
const iEval = iP != null ? `<span class="rk-eval ${evalClass(iP)}">${iP}</span>` : '<span class="rk-empty">—</span>';
|
||||||
|
const rEval = rP != null ? `<span class="rk-eval ${evalClass(rP)}">${rP}</span>` : '<span class="rk-empty">—</span>';
|
||||||
|
const ab = alertBand(iP);
|
||||||
|
const alertCell = ab
|
||||||
|
? `<button class="alert-btn ${ab.cls}" onclick="viewRiskDetail(${id})" title="Apri il rischio (trattamenti/azioni)">${ab.label}</button>`
|
||||||
|
: '<span class="rk-empty">—</span>';
|
||||||
|
const inhOutcome = iP != null ? '<span class="rk-valutato">Valutato</span>' : '<span class="rk-empty">—</span>';
|
||||||
|
const ro = residualOutcome(rP);
|
||||||
|
const resOutcomeCell = ro ? `<span class="${ro.cls}">${ro.label}</span>` : '<span class="rk-empty">—</span>';
|
||||||
|
const catLabel = CATEGORY_LABELS[r.category] || r.category || '';
|
||||||
|
const safeTitle = (r.title || '').replace(/['\\]/g, '');
|
||||||
|
const actBtns = editable
|
||||||
|
? `<div style="display:flex;gap:5px;margin-top:5px;">
|
||||||
|
<button class="btn btn-ghost btn-sm btn-icon" onclick="showRiskModal(${id})" title="Modifica"><svg viewBox="0 0 20 20" width="14" height="14" fill="currentColor"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg></button>
|
||||||
|
<button class="btn btn-ghost btn-sm btn-icon" onclick="confirmDeleteRisk(${id}, '${escapeHtml(safeTitle)}')" title="Elimina rischio trasversale"><svg viewBox="0 0 20 20" width="14" height="14" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/></svg></button>
|
||||||
|
</div>` : '';
|
||||||
|
const resCell = (rP != null ? '<span class="rk-valutato">Valutato</span>' : '<span class="rk-empty">—</span>') + actBtns;
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<tr onclick="viewRiskDetail(${r.id})">
|
<tr>
|
||||||
<td><code style="font-size:0.8rem; color:var(--primary);">${escapeHtml(r.risk_code || '-')}</code></td>
|
<td class="cell-code">
|
||||||
<td><strong>${escapeHtml(r.title)}</strong></td>
|
<a href="javascript:void(0)" onclick="viewRiskDetail(${id})" title="Apri il rischio"><code style="font-size:0.75rem;color:var(--primary);">${escapeHtml(r.risk_code || '-')}</code></a>
|
||||||
<td><span class="tag">${escapeHtml(categoryLabel)}</span></td>
|
|
||||||
<td>${r.likelihood || '-'}/5</td>
|
|
||||||
<td>${r.impact || '-'}/5</td>
|
|
||||||
<td><span class="risk-score ${scoreClass}">${score}</span></td>
|
|
||||||
<td>${statusBadge}</td>
|
|
||||||
<td>
|
|
||||||
<div class="btn-group" onclick="event.stopPropagation()">
|
|
||||||
<button class="btn btn-ghost btn-sm btn-icon" onclick="showRiskModal(${r.id})" title="Modifica">
|
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-ghost btn-sm btn-icon" onclick="confirmDeleteRisk(${r.id}, '${escapeHtml(r.title)}')" title="Elimina">
|
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
|
<td class="cell-desc">
|
||||||
|
<div style="font-size:0.64rem;text-transform:uppercase;letter-spacing:0.04em;font-weight:600;color:var(--gray-500);">${escapeHtml(catLabel)}</div>
|
||||||
|
<div style="font-size:0.82rem;color:var(--gray-800);">${escapeHtml(r.title || '')}</div>
|
||||||
|
</td>
|
||||||
|
<td>${scoreSelT('til-' + id, iL, editable)}</td>
|
||||||
|
<td>${scoreSelT('tii-' + id, iI, editable)}</td>
|
||||||
|
<td>${iEval}</td>
|
||||||
|
<td>${alertCell}</td>
|
||||||
|
<td>${inhOutcome}</td>
|
||||||
|
<td>${scoreSelT('trl-' + id, rL, editable)}</td>
|
||||||
|
<td>${scoreSelT('tri-' + id, rI, editable)}</td>
|
||||||
|
<td>${rEval}</td>
|
||||||
|
<td>${resOutcomeCell}</td>
|
||||||
|
<td>${resCell}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
tbody.innerHTML = html;
|
tbody.innerHTML = html;
|
||||||
|
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select 1-5 inline per i rischi trasversali (salva su tabella risks via api.updateRisk).
|
||||||
|
function scoreSelT(id, val, editable) {
|
||||||
|
if (!editable) return (val >= 1 && val <= 5) ? (val + '/5') : '<span class="rk-empty">—</span>';
|
||||||
|
let o = `<option value="">—</option>`;
|
||||||
|
for (let v = 1; v <= 5; v++) o += `<option value="${v}" ${v == val ? 'selected' : ''}>${v}</option>`;
|
||||||
|
const rid = id.substring(id.indexOf('-') + 1);
|
||||||
|
return `<select class="rk-sel" id="${id}" onchange="onTransversalScoreChange(${rid})">${o}</select>`;
|
||||||
|
}
|
||||||
|
async function onTransversalScoreChange(riskId) {
|
||||||
|
const r = (risksData || []).find(x => String(x.id) === String(riskId));
|
||||||
|
if (!r) return;
|
||||||
|
const rd = elId => { const el = document.getElementById(elId); if (!el) return null; const v = el.value; return v === '' ? null : parseInt(v); };
|
||||||
|
r.likelihood = rd('til-' + riskId);
|
||||||
|
r.impact = rd('tii-' + riskId);
|
||||||
|
r.inherent_risk_score = (r.likelihood && r.impact) ? r.likelihood * r.impact : 0;
|
||||||
|
r.residual_likelihood = rd('trl-' + riskId);
|
||||||
|
r.residual_impact = rd('tri-' + riskId);
|
||||||
|
r.residual_risk_score = (r.residual_likelihood && r.residual_impact) ? r.residual_likelihood * r.residual_impact : 0;
|
||||||
|
renderRisksTable(risksData);
|
||||||
|
loadTransversalMatrix();
|
||||||
|
try {
|
||||||
|
await api.updateRisk(riskId, {
|
||||||
|
likelihood: r.likelihood || 0,
|
||||||
|
impact: r.impact || 0,
|
||||||
|
residual_likelihood: r.residual_likelihood,
|
||||||
|
residual_impact: r.residual_impact,
|
||||||
|
});
|
||||||
|
} catch (e) { showNotification(e.message || 'Errore nel salvataggio', 'error'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getScoreClassForRisk(score) {
|
function getScoreClassForRisk(score) {
|
||||||
@@ -1148,76 +1243,77 @@
|
|||||||
// ESCLUSI i "non applicabili" (stato non_applicabile o non applicabili alla classe);
|
// ESCLUSI i "non applicabili" (stato non_applicabile o non applicabili alla classe);
|
||||||
// - rischi AGGIUNTIVI custom (che portano già likelihood/impact).
|
// - rischi AGGIUNTIVI custom (che portano già likelihood/impact).
|
||||||
// Legge dagli array in memoria (derivedData/risksData): niente fetch qui.
|
// Legge dagli array in memoria (derivedData/risksData): niente fetch qui.
|
||||||
function loadMatrix() {
|
// ── Matrici: vista (inerente/residuo) indipendente per ciascuna matrice (punti 2 e 3.4) ──
|
||||||
const points = [];
|
let matrixViewDerived = 'inherent';
|
||||||
|
let matrixViewTrans = 'inherent';
|
||||||
(derivedData || []).forEach(r => {
|
function setMatrixView(scope, view) {
|
||||||
if (!isDerivedApplicable(r)) return; // esclude i non applicabili alla classe
|
if (scope === 'derived') {
|
||||||
if (r.stato === 'non_applicabile') return; // esclude i non applicabili
|
matrixViewDerived = view;
|
||||||
const l = parseInt(r.likelihood), i = parseInt(r.impact);
|
const a = document.getElementById('mxd-inh'), b = document.getElementById('mxd-res');
|
||||||
if (l >= 1 && l <= 5 && i >= 1 && i <= 5) {
|
if (a) a.classList.toggle('on', view === 'inherent');
|
||||||
points.push({ likelihood: l, impact: i, title: ((r.risk_code || '') + ' ' + (r.requisito_code || '')).trim() });
|
if (b) b.classList.toggle('on', view === 'residual');
|
||||||
}
|
loadDerivedMatrix();
|
||||||
});
|
} else {
|
||||||
|
matrixViewTrans = view;
|
||||||
(risksData || []).forEach(r => {
|
const a = document.getElementById('mxt-inh'), b = document.getElementById('mxt-res');
|
||||||
const l = parseInt(r.likelihood), i = parseInt(r.impact);
|
if (a) a.classList.toggle('on', view === 'inherent');
|
||||||
if (l >= 1 && l <= 5 && i >= 1 && i <= 5) {
|
if (b) b.classList.toggle('on', view === 'residual');
|
||||||
points.push({ likelihood: l, impact: i, title: r.title || (r.risk_code || 'Rischio') });
|
loadTransversalMatrix();
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
renderMatrix(points);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMatrix(risks) {
|
|
||||||
const grid = document.getElementById('risk-matrix-grid');
|
|
||||||
|
|
||||||
// Build a map: `${likelihood}-${impact}` -> count
|
|
||||||
const cellMap = {};
|
|
||||||
risks.forEach(r => {
|
|
||||||
const key = `${r.likelihood || 0}-${r.impact || 0}`;
|
|
||||||
if (!cellMap[key]) cellMap[key] = [];
|
|
||||||
cellMap[key].push(r);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Color level mapping for cell score (likelihood * impact)
|
|
||||||
function getCellLevel(score) {
|
|
||||||
if (score <= 1) return 'level-1';
|
|
||||||
if (score <= 2) return 'level-2';
|
|
||||||
if (score <= 4) return 'level-3';
|
|
||||||
if (score <= 6) return 'level-4';
|
|
||||||
if (score <= 8) return 'level-5';
|
|
||||||
if (score <= 10) return 'level-6';
|
|
||||||
if (score <= 14) return 'level-7';
|
|
||||||
if (score <= 16) return 'level-8';
|
|
||||||
if (score <= 19) return 'level-9';
|
|
||||||
return 'level-10';
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
function matrixPoints(list, view, titleFn, filterFn) {
|
||||||
|
const pts = [];
|
||||||
|
(list || []).forEach(r => {
|
||||||
|
if (filterFn && !filterFn(r)) return;
|
||||||
|
const l = view === 'residual' ? parseInt(r.residual_likelihood) : parseInt(r.likelihood);
|
||||||
|
const i = view === 'residual' ? parseInt(r.residual_impact) : parseInt(r.impact);
|
||||||
|
if (l >= 1 && l <= 5 && i >= 1 && i <= 5) pts.push({ likelihood: l, impact: i, title: titleFn(r) });
|
||||||
|
});
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
function loadDerivedMatrix() {
|
||||||
|
const pts = matrixPoints(derivedData, matrixViewDerived,
|
||||||
|
r => ((r.risk_code || '') + ' ' + (r.requisito_code || '')).trim(),
|
||||||
|
r => isDerivedApplicable(r) && r.stato !== 'non_applicabile');
|
||||||
|
renderMatrix(pts, 'risk-matrix-grid-derived');
|
||||||
|
}
|
||||||
|
function loadTransversalMatrix() {
|
||||||
|
const pts = matrixPoints(risksData, matrixViewTrans, r => r.title || (r.risk_code || 'Rischio'));
|
||||||
|
renderMatrix(pts, 'risk-matrix-grid-transversal');
|
||||||
|
}
|
||||||
|
// Compat: le vecchie chiamate loadMatrix() aggiornano entrambe le matrici
|
||||||
|
function loadMatrix() { loadDerivedMatrix(); loadTransversalMatrix(); }
|
||||||
|
|
||||||
|
// Bande (spec Simon): 1-2 basso, 3-8 medio, 9-12 alto, 15-25 critico
|
||||||
|
function matrixBand(score) {
|
||||||
|
if (score >= 15) return 'band-crit';
|
||||||
|
if (score >= 9) return 'band-high';
|
||||||
|
if (score >= 3) return 'band-med';
|
||||||
|
return 'band-low';
|
||||||
|
}
|
||||||
|
function renderMatrix(risks, gridId) {
|
||||||
|
const grid = document.getElementById(gridId);
|
||||||
|
if (!grid) return;
|
||||||
|
const cellMap = {};
|
||||||
|
(risks || []).forEach(r => {
|
||||||
|
const key = `${r.likelihood || 0}-${r.impact || 0}`;
|
||||||
|
(cellMap[key] = cellMap[key] || []).push(r);
|
||||||
|
});
|
||||||
let html = '';
|
let html = '';
|
||||||
// Rows from top (likelihood=5) to bottom (likelihood=1)
|
|
||||||
for (let li = 5; li >= 1; li--) {
|
for (let li = 5; li >= 1; li--) {
|
||||||
// Y-axis header
|
|
||||||
html += `<div class="matrix-y-header">${li}</div>`;
|
html += `<div class="matrix-y-header">${li}</div>`;
|
||||||
// 5 cells per row (impact 1-5)
|
|
||||||
for (let imp = 1; imp <= 5; imp++) {
|
for (let imp = 1; imp <= 5; imp++) {
|
||||||
const score = li * imp;
|
const score = li * imp;
|
||||||
const level = getCellLevel(score);
|
|
||||||
const key = `${li}-${imp}`;
|
const key = `${li}-${imp}`;
|
||||||
const risksInCell = cellMap[key] || [];
|
const risksInCell = cellMap[key] || [];
|
||||||
const dot = risksInCell.length > 0
|
const dot = risksInCell.length > 0
|
||||||
? `<div class="matrix-dot" title="${risksInCell.map(r => escapeHtml(r.title)).join(', ')}">${risksInCell.length}</div>`
|
? `<div class="matrix-dot" title="${risksInCell.map(r => escapeHtml(r.title)).join(', ')}">${risksInCell.length}</div>`
|
||||||
: '';
|
: '';
|
||||||
html += `<div class="matrix-cell ${level}" title="P:${li} x I:${imp} = ${score}">${dot}</div>`;
|
html += `<div class="matrix-cell ${matrixBand(score)}" title="P:${li} x I:${imp} = ${score}">${dot}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Bottom row: corner + x-axis headers
|
|
||||||
html += `<div class="matrix-corner"></div>`;
|
html += `<div class="matrix-corner"></div>`;
|
||||||
for (let imp = 1; imp <= 5; imp++) {
|
for (let imp = 1; imp <= 5; imp++) html += `<div class="matrix-x-header">${imp}</div>`;
|
||||||
html += `<div class="matrix-x-header">${imp}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
grid.innerHTML = html;
|
grid.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1237,8 +1333,8 @@
|
|||||||
|
|
||||||
function renderDetail(risk) {
|
function renderDetail(risk) {
|
||||||
document.getElementById('view-derived').classList.add('hidden');
|
document.getElementById('view-derived').classList.add('hidden');
|
||||||
document.getElementById('view-table').classList.add('hidden');
|
|
||||||
document.getElementById('view-matrix').classList.add('hidden');
|
document.getElementById('view-matrix').classList.add('hidden');
|
||||||
|
['allhazard-section', 'view-matrix-trans'].forEach(id => { const el = document.getElementById(id); if (el) el.classList.add('hidden'); });
|
||||||
const container = document.getElementById('view-detail');
|
const container = document.getElementById('view-detail');
|
||||||
container.classList.remove('hidden');
|
container.classList.remove('hidden');
|
||||||
|
|
||||||
@@ -1412,8 +1508,8 @@
|
|||||||
document.getElementById('view-detail').classList.add('hidden');
|
document.getElementById('view-detail').classList.add('hidden');
|
||||||
// Sezione derivati, tabella custom e matrice sono sempre visibili.
|
// Sezione derivati, tabella custom e matrice sono sempre visibili.
|
||||||
document.getElementById('view-derived').classList.remove('hidden');
|
document.getElementById('view-derived').classList.remove('hidden');
|
||||||
document.getElementById('view-table').classList.remove('hidden');
|
|
||||||
document.getElementById('view-matrix').classList.remove('hidden');
|
document.getElementById('view-matrix').classList.remove('hidden');
|
||||||
|
['allhazard-section', 'view-matrix-trans'].forEach(id => { const el = document.getElementById(id); if (el) el.classList.remove('hidden'); });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Create/Edit Risk Modal ───────────────────────────────────
|
// ── Create/Edit Risk Modal ───────────────────────────────────
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@
|
|||||||
* Il nome cache include la release: va BUMPATO ad ogni rilascio (vedi version.json),
|
* Il nome cache include la release: va BUMPATO ad ogni rilascio (vedi version.json),
|
||||||
* cosi' activate elimina automaticamente la shell vecchia.
|
* cosi' activate elimina automaticamente la shell vecchia.
|
||||||
*/
|
*/
|
||||||
const CACHE = 'nis2-shell-v1.26.0';
|
const CACHE = 'nis2-shell-v1.27.0';
|
||||||
|
|
||||||
// Shell V3 (top-nav): niente più style.css/common-bi.js/bootstrap-italia (UI V2).
|
// Shell V3 (top-nav): niente più style.css/common-bi.js/bootstrap-italia (UI V2).
|
||||||
const PRECACHE = [
|
const PRECACHE = [
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version": "1.26.0", "build": "2026-07-30-v1.26.0", "date": "2026-07-30", "changelog": "Gestione Rischi rinnovata: la tabella dei rischi dai requisiti ora ha 12 colonne con DOPPIA valutazione — rischio INERENTE (prima del trattamento) e RESIDUO (dopo). Probabilita e Impatto si compilano direttamente in tabella (1-5). La colonna Alert propone un'azione in base al punteggio (Prioritaria/Critica/Necessaria/Suggerita) e da li puoi creare azioni sul rischio. Codice cliccabile verso Misure e Requisiti. NB: il punteggio di rischio residuo NON cambia da solo la conformita del requisito (che dipende da attuazione ed evidenze): la aggiorni in modo esplicito."}
|
{"version": "1.27.0", "build": "2026-08-01-v1.27.0", "date": "2026-08-01", "changelog": "Pagina Rischi rinnovata (richieste Simon): la matrice 5x5 ora ha 4 fasce di rischio (1-2 basso verde, 3-8 medio arancione, 9-12 alto rosso tenue, 15-25 critico rosso) e un tasto per passare tra i giudizi INERENTI e RESIDUI. La sezione 'Rischi trasversali' e' stata spostata in fondo, ora ha le stesse 12 colonne dei rischi dai requisiti (valutazione inerente e residua, alert, esito), i tasti 'AI Suggerisci' e 'Nuovo Rischio' in testa, una propria matrice 5x5 con lo stesso tasto Inerente/Residuo, e un tasto per eliminare ogni rischio trasversale."}
|
||||||
|
|||||||
Reference in New Issue
Block a user