[FEAT] Epic C / C1 operativo — valutazione/stato di conformità per requisito (mig.047)
org_requisito_state (org x requisito -> stato + valutazione del rischio + note), la valutazione 'parte vuota'. È da qui che emerge lo stato di conformità (sostituirà il Gap ACN, C3). - FrameworkController: catalog ora include lo stato per-org di ogni requisito (risoluzione org OPZIONALE con verifica accesso = anti-IDOR); nuovo POST /framework/state (upsert, requireOrgRole org_admin/compliance_manager, validazione stato + requisito esistente). - misure-requisiti.html: colonna Stato/Valutazione (se org) + modale di valutazione (stato, valutazione del rischio, note). api.frameworkSetState. Smoke prod: catalog has_org/org_class OK; upsert non_conforme persistito e riletto; ISOLAMENTO multi-tenant verificato (org 152 non visibile a org 151). Additivo. 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
92d101b6f4
commit
b34bf3f9f6
@@ -255,6 +255,7 @@ $actionMap = [
|
||||
// rischio di default per requisito). Nessuna scrittura (codifica non variabile).
|
||||
'framework' => [
|
||||
'GET:catalog' => 'catalog',
|
||||
'POST:state' => 'setState', // upsert valutazione/stato per requisito (org-scoped)
|
||||
],
|
||||
|
||||
// ── AssessmentController ────────────────────────
|
||||
|
||||
@@ -294,6 +294,7 @@ class NIS2API {
|
||||
// Catalogo cfg_nis2_* (43 misure / 116 requisiti + procedura+rischio default).
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
frameworkCatalog() { return this._acn(this.get('/framework/catalog')); }
|
||||
frameworkSetState(d) { return this._acn(this.post('/framework/state', d || {})); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Stakeholder estesi (A4 Fase 4.5) — riusa suppliers (GV.SC-02).
|
||||
|
||||
@@ -37,6 +37,30 @@
|
||||
.mr-row.spento { opacity:.4; }
|
||||
.mr-row.spento td { font-style:italic; }
|
||||
.mr-empty { text-align:center; padding:34px 16px; color:var(--gray-500,#6b7280); }
|
||||
.sv-badge { display:inline-block; font-size:.72rem; font-weight:800; padding:3px 10px; border-radius:20px; border:none; cursor:pointer; }
|
||||
.sv-badge.sv-todo { background:#f1f5f9; color:#475569; }
|
||||
.sv-badge.sv-na { background:#e5e7eb; color:#6b7280; }
|
||||
.sv-badge.sv-ko { background:#fee2e2; color:#991b1b; }
|
||||
.sv-badge.sv-mid { background:#fef3c7; color:#92400e; }
|
||||
.sv-badge.sv-ok { background:#dcfce7; color:#166534; }
|
||||
.sv-val { font-size:.76rem; color:#64748b; margin-top:4px; max-width:300px; }
|
||||
/* Modale valutazione */
|
||||
.rm-overlay { display:none; position:fixed; inset:0; background:rgba(15,23,42,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; }
|
||||
.rm-overlay.open { display:flex; }
|
||||
.rm-dialog { background:#fff; border-radius:12px; width:100%; max-width:600px; box-shadow:0 20px 50px rgba(0,0,0,.25); }
|
||||
.rm-head { display:flex; justify-content:space-between; align-items:flex-start; padding:16px 20px; border-bottom:1px solid var(--gray-100,#f3f4f6); }
|
||||
.rm-head h3 { margin:0; font-size:1.02rem; }
|
||||
.rm-head .sub { font-size:.8rem; color:var(--gray-500,#6b7280); margin-top:4px; }
|
||||
.rm-body { padding:18px 20px; }
|
||||
.rm-foot { padding:14px 20px; border-top:1px solid var(--gray-100,#f3f4f6); display:flex; justify-content:flex-end; gap:10px; }
|
||||
.rm-field { margin-bottom:14px; }
|
||||
.rm-field label { display:block; font-weight:600; font-size:.86rem; margin-bottom:4px; }
|
||||
.rm-field select, .rm-field textarea { width:100%; padding:9px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.9rem; }
|
||||
.rm-field textarea { min-height:80px; resize:vertical; }
|
||||
.rm-ctx { background:#f8fafc; border:1px solid var(--gray-100,#f3f4f6); border-radius:8px; padding:10px 12px; font-size:.83rem; margin-bottom:14px; line-height:1.5; }
|
||||
.rm-ctx .pill { display:inline-block; font-size:.7rem; font-weight:700; color:#3730a3; background:#eef2ff; border-radius:6px; padding:1px 6px; margin-right:4px; }
|
||||
.rm-err { color:#b91c1c; font-size:.82rem; min-height:18px; }
|
||||
.rm-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--gray-400,#9ca3af); }
|
||||
</style>
|
||||
<!-- PWA:start -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
@@ -104,6 +128,40 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Modale valutazione del requisito (org-scoped) -->
|
||||
<div class="rm-overlay" id="mr-modal" role="dialog" aria-modal="true" aria-labelledby="mr-modal-title">
|
||||
<div class="rm-dialog">
|
||||
<div class="rm-head">
|
||||
<div><h3 id="mr-modal-title">Valutazione del requisito</h3><div class="sub" id="mr-modal-sub"></div></div>
|
||||
<button class="rm-close" type="button" onclick="mrCloseEval()" aria-label="Chiudi">×</button>
|
||||
</div>
|
||||
<div class="rm-body">
|
||||
<input type="hidden" id="mr-eval-id">
|
||||
<div class="rm-ctx" id="mr-eval-ctx"></div>
|
||||
<div class="rm-field"><label for="mr-eval-stato">Stato di conformità</label>
|
||||
<select id="mr-eval-stato">
|
||||
<option value="da_valutare">Da valutare</option>
|
||||
<option value="non_applicabile">Non applicabile</option>
|
||||
<option value="non_conforme">Non conforme</option>
|
||||
<option value="parziale">Parziale</option>
|
||||
<option value="conforme">Conforme</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="rm-field"><label for="mr-eval-val">Valutazione del rischio</label>
|
||||
<textarea id="mr-eval-val" maxlength="4000" placeholder="Descrivi la valutazione del rischio per questo requisito (probabilità, impatto, misure in essere, gap…)."></textarea>
|
||||
</div>
|
||||
<div class="rm-field"><label for="mr-eval-note">Note</label>
|
||||
<textarea id="mr-eval-note" maxlength="2000" placeholder="Note operative, riferimenti, evidenze…"></textarea>
|
||||
</div>
|
||||
<div class="rm-err" id="mr-eval-err" role="alert"></div>
|
||||
</div>
|
||||
<div class="rm-foot">
|
||||
<button class="btn btn-outline" type="button" onclick="mrCloseEval()">Annulla</button>
|
||||
<button class="btn btn-primary" type="button" id="mr-eval-save" onclick="mrSaveEval()">Salva</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260621"></script>
|
||||
<script src="/js/common.js?v=20260621"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
@@ -117,11 +175,20 @@
|
||||
* Dati da api.frameworkCatalog() (tabelle cfg_nis2_*). Codifica non modificabile.
|
||||
* Le righe si "spengono" per la classe del soggetto (importante/essenziale).
|
||||
*/
|
||||
let MR = { measures: [], org_class: null };
|
||||
let MR = { measures: [], org_class: null, has_org: false, stati: [], byId: {} };
|
||||
|
||||
function el(id) { return document.getElementById(id); }
|
||||
function esc(s) { const d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
|
||||
|
||||
const STATO_META = {
|
||||
da_valutare: { label: 'Da valutare', cls: 'sv-todo' },
|
||||
non_applicabile: { label: 'Non applicabile', cls: 'sv-na' },
|
||||
non_conforme: { label: 'Non conforme', cls: 'sv-ko' },
|
||||
parziale: { label: 'Parziale', cls: 'sv-mid' },
|
||||
conforme: { label: 'Conforme', cls: 'sv-ok' }
|
||||
};
|
||||
function statoMeta(s) { return STATO_META[s] || STATO_META.da_valutare; }
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
if (typeof checkAuth === 'function' && !checkAuth()) return;
|
||||
if (window.I18n && I18n.init) I18n.init('it');
|
||||
@@ -135,6 +202,11 @@
|
||||
const data = await api.frameworkCatalog();
|
||||
MR.measures = (data && data.measures) || [];
|
||||
MR.org_class = (data && data.org_class) || null;
|
||||
MR.has_org = !!(data && data.has_org);
|
||||
MR.stati = (data && data.stati) || Object.keys(STATO_META);
|
||||
// indicizza i requisiti per id (per la modale)
|
||||
MR.byId = {};
|
||||
MR.measures.forEach(function (m) { (m.requisiti || []).forEach(function (q) { MR.byId[q.id] = q; }); });
|
||||
// se conosco la classe dell'org, parto in modalità "auto"; altrimenti mostro tutto
|
||||
if (!MR.org_class) el('mr-class').value = 'all';
|
||||
el('mr-stat-mis').textContent = (data && data.totals && data.totals.misure) || MR.measures.length;
|
||||
@@ -212,7 +284,8 @@
|
||||
html += '</div>';
|
||||
html += '<table class="mr-table"><thead><tr>' +
|
||||
'<th>Requisito</th><th>Descrizione</th><th>Procedura (default)</th><th>Rischio (default)</th>' +
|
||||
'<th style="text-align:center">Imp.</th><th style="text-align:center">Ess.</th></tr></thead><tbody>';
|
||||
'<th style="text-align:center">Imp.</th><th style="text-align:center">Ess.</th>' +
|
||||
(MR.has_org ? '<th>Stato / Valutazione</th>' : '') + '</tr></thead><tbody>';
|
||||
g.rows.forEach(function (r) {
|
||||
const req = r.req, m = r.m;
|
||||
html += '<tr class="mr-row' + (r.active ? '' : ' spento') + '">';
|
||||
@@ -226,12 +299,58 @@
|
||||
html += '<td>' + (req.risk_code ? '<span class="mr-code-pill">' + esc(req.risk_code) + '</span> ' + esc(req.risk_descr || '') : '—') + '</td>';
|
||||
html += '<td class="mr-yn ' + (req.applies_important ? 'y' : 'n') + '">' + (req.applies_important ? '✓' : '—') + '</td>';
|
||||
html += '<td class="mr-yn ' + (req.applies_essential ? 'y' : 'n') + '">' + (req.applies_essential ? '✓' : '—') + '</td>';
|
||||
if (MR.has_org) {
|
||||
const sm = statoMeta(req.stato);
|
||||
const val = req.valutazione_rischio ? esc(req.valutazione_rischio).slice(0, 90) + (req.valutazione_rischio.length > 90 ? '…' : '') : '';
|
||||
html += '<td><button type="button" class="sv-badge ' + sm.cls + '" onclick="mrEdit(' + req.id + ')" title="Modifica valutazione">' + esc(sm.label) + '</button>' +
|
||||
(val ? '<div class="sv-val">' + val + '</div>' : '') + '</td>';
|
||||
}
|
||||
html += '</tr>';
|
||||
});
|
||||
html += '</tbody></table></div>';
|
||||
});
|
||||
el('mr-list').innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Valutazione per requisito (org-scoped) ──────────────────────────
|
||||
function mrEdit(reqId) {
|
||||
const q = MR.byId[reqId];
|
||||
if (!q) return;
|
||||
el('mr-eval-id').value = reqId;
|
||||
el('mr-modal-sub').textContent = (q.requisito_code || '') + ' · ' + (q.proc_code || '') + ' / ' + (q.risk_code || '');
|
||||
let ctx = '<div>' + esc(q.requisito_descr || '') + '</div>';
|
||||
if (q.risk_code) ctx += '<div style="margin-top:6px"><span class="pill">' + esc(q.risk_code) + '</span> ' + esc(q.risk_descr || '') + '</div>';
|
||||
el('mr-eval-ctx').innerHTML = ctx;
|
||||
el('mr-eval-stato').value = q.stato || 'da_valutare';
|
||||
el('mr-eval-val').value = q.valutazione_rischio || '';
|
||||
el('mr-eval-note').value = q.note || '';
|
||||
el('mr-eval-err').textContent = '';
|
||||
el('mr-modal').classList.add('open');
|
||||
}
|
||||
function mrCloseEval() { el('mr-modal').classList.remove('open'); }
|
||||
async function mrSaveEval() {
|
||||
const reqId = parseInt(el('mr-eval-id').value, 10);
|
||||
const btn = el('mr-eval-save');
|
||||
el('mr-eval-err').textContent = '';
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const res = await api.frameworkSetState({
|
||||
requisito_id: reqId,
|
||||
stato: el('mr-eval-stato').value,
|
||||
valutazione_rischio: el('mr-eval-val').value.trim(),
|
||||
note: el('mr-eval-note').value.trim()
|
||||
});
|
||||
const q = MR.byId[reqId];
|
||||
if (q && res) { q.stato = res.stato; q.valutazione_rischio = res.valutazione_rischio; q.note = res.note; }
|
||||
mrCloseEval();
|
||||
mrRender();
|
||||
const hint = el('mr-hint'); if (hint) { hint.textContent = 'Valutazione salvata'; setTimeout(function () { hint.textContent = ''; }, 2500); }
|
||||
} catch (e) {
|
||||
el('mr-eval-err').textContent = (e && e.message) ? e.message : 'Errore nel salvataggio.';
|
||||
} finally { btn.disabled = false; }
|
||||
}
|
||||
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') mrCloseEval(); });
|
||||
(function () { const ov = el('mr-modal'); if (ov) ov.addEventListener('click', function (e) { if (e.target === ov) mrCloseEval(); }); })();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user