La pagina Gestione Rischi mostra come tabella primaria (read-only) i requisiti del framework Lex&ISO come rischi, codice RSK-<misura>Com.N via /api/risks/derived, filtrati per classe (importante All.1 / essenziale All.2). Framing normativo (validato nis2-expert): ogni voce = "rischio derivante dal mancato/parziale soddisfacimento del requisito", mai "requisito = rischio"; RSK e "92" NON come codici/numeri ufficiali ACN; matrice = strumento di rappresentazione (art.24). - Rischi CUSTOM mantenuti in sezione "Rischi aggiuntivi (all-hazard)". - Tasti "Nuovo Rischio"/"AI Suggerisci" limitati a super_admin/org_admin (non rimossi): l'inserimento libero serve al multirischio (art.24 D.Lgs.138/2024). - api.js derivedRisks(); help.js + i18n aggiornati; version 1.25.4; SW v1.25.4. - Cache-buster api/help/i18n ?v=20260727r. Backend/DB non toccati. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
309 lines
24 KiB
HTML
309 lines
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<title>Controlli periodici · NIS2 Agile</title>
|
|
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia CDN). -->
|
|
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
|
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
|
<style>
|
|
/* ── Compat V3: token + bottoni emessi dal JS (ex style.css, niente CDN) ── */
|
|
:root{
|
|
--primary:#1F5FA8;--gray-100:#F1F5F9;--gray-200:#E3E9F0;--gray-400:#94A3B8;--gray-500:#64748B;
|
|
}
|
|
.btn{display:inline-flex;align-items:center;gap:7px;border:1px solid transparent;border-radius:8px;padding:8px 13px;font:inherit;font-weight:600;font-size:12.5px;cursor:pointer;line-height:1.2}
|
|
.btn:disabled{opacity:.55;cursor:not-allowed}
|
|
.btn-primary{background:var(--c-blue);color:#fff}.btn-primary:hover:not(:disabled){background:#1A5494}
|
|
.btn-outline{background:#fff;color:var(--ink);border-color:var(--line)}.btn-outline:hover:not(:disabled){background:#F5F8FC;border-color:#c7d4e4}
|
|
.btn-sm{padding:5px 11px;font-size:11.5px}
|
|
a.btn{text-decoration:none}
|
|
.pc-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; }
|
|
.pc-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
|
|
.pc-toolbar { display:flex; justify-content:flex-end; margin-bottom:12px; }
|
|
.pc-table { width:100%; border-collapse:collapse; font-size:.88rem; }
|
|
.pc-table th, .pc-table td { text-align:left; padding:9px 12px; border-top:1px solid var(--gray-100,#f3f4f6); vertical-align:top; }
|
|
.pc-table th { font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
|
|
.pc-badge { display:inline-block; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; white-space:nowrap; }
|
|
.pc-badge.code { background:#f1f5f9; color:#475569; font-family:monospace; }
|
|
.d-overdue { background:#fee2e2; color:#991b1b; } .d-due_soon { background:#fef3c7; color:#92400e; }
|
|
.d-upcoming { background:#dcfce7; color:#166534; } .d-suspended { background:#f3f4f6; color:#6b7280; } .d-none { background:#f3f4f6; color:#9ca3af; }
|
|
.o-conforme { background:#dcfce7; color:#166534; } .o-non_conforme { background:#fee2e2; color:#991b1b; }
|
|
.o-parziale { background:#fef3c7; color:#92400e; } .o-non_applicabile { background:#f3f4f6; color:#6b7280; }
|
|
.pc-empty { text-align:center; padding:22px 16px; color:var(--gray-500,#6b7280); font-size:.86rem; }
|
|
.sm-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:30px 16px; }
|
|
.sm-overlay.open { display:flex; }
|
|
.sm-dialog { background:#fff; border-radius:12px; width:100%; max-width:600px; box-shadow:0 20px 50px rgba(0,0,0,.25); }
|
|
.sm-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--gray-100,#f3f4f6); }
|
|
.sm-head h3 { margin:0; font-size:1.05rem; }
|
|
.sm-body { padding:18px 20px; } .sm-foot { padding:14px 20px; border-top:1px solid var(--gray-100,#f3f4f6); display:flex; justify-content:flex-end; gap:10px; }
|
|
.sm-field { margin-bottom:14px; } .sm-field label { display:block; font-weight:600; font-size:.86rem; margin-bottom:4px; }
|
|
.sm-field .hint { font-weight:400; color:var(--gray-500,#6b7280); font-size:.78rem; }
|
|
.sm-field input[type=text], .sm-field input[type=date], .sm-field input[type=number], .sm-field select, .sm-field textarea { width:100%; padding:9px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.9rem; background:#fff; }
|
|
.sm-field textarea { min-height:60px; resize:vertical; }
|
|
.sm-row { display:flex; gap:12px; flex-wrap:wrap; } .sm-row > .sm-field { flex:1 1 0; min-width:140px; }
|
|
.sm-err { color:#b91c1c; font-size:.82rem; min-height:18px; }
|
|
.sm-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--gray-400,#9ca3af); }
|
|
.hidden { display:none !important; }
|
|
</style>
|
|
<!-- PWA:start -->
|
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
<meta name="theme-color" content="#0066CC">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32.png">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16.png">
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
|
<meta name="apple-mobile-web-app-title" content="NIS2 Agile">
|
|
<meta name="application-name" content="NIS2 Agile">
|
|
<script src="/js/pwa.js?v=20260614" defer></script>
|
|
<!-- PWA:end -->
|
|
</head>
|
|
<body>
|
|
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
|
<div id="topnav"></div>
|
|
|
|
<main class="wrap">
|
|
<nav class="crumb" aria-label="breadcrumb">
|
|
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
|
<i data-lucide="chevron-right" class="ic"></i><span>Monitoraggio</span>
|
|
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="pctl.title">Controlli periodici</span>
|
|
</nav>
|
|
<div class="page-head">
|
|
<div>
|
|
<h1 data-i18n="pctl.title">Controlli periodici</h1>
|
|
<p class="sub">Controlli ricorrenti, esecuzioni con evidenza ed esiti (ISO/IEC 27001 §9.1 / A.8.16)</p>
|
|
</div>
|
|
<button class="btn-primary" onclick="pcOpen()"><i data-lucide="plus" class="ic"></i>Nuovo controllo</button>
|
|
</div>
|
|
<section class="card card-pad">
|
|
<div class="pc-intro">
|
|
<strong>Controlli periodici (monitoraggio).</strong>
|
|
Definisci i controlli ricorrenti (con responsabile, frequenza e metodo) e registra ogni esecuzione con esito ed evidenze:
|
|
lo storico è la prova del monitoraggio nel tempo. Da un esito non conforme puoi generare una non conformità o un'azione correttiva.
|
|
</div>
|
|
<div class="pc-note">
|
|
Ancoraggio: <strong>ISO/IEC 27001 §9.1</strong> (monitoraggio e misurazione) e <strong>A.8.16</strong> (attività di monitoraggio) — <strong>buona prassi</strong>, non un obbligo di legge. Per NIS2 supporta GV.PO-02/DE.CM. Strumento di supporto, non un parere legale.
|
|
</div>
|
|
<div id="pc-list" aria-live="polite"></div>
|
|
</section>
|
|
</main>
|
|
|
|
<!-- Modale controllo (crea/modifica) -->
|
|
<div class="sm-overlay" id="pc-modal" role="dialog" aria-modal="true" aria-labelledby="pc-modal-title">
|
|
<div class="sm-dialog"><div class="sm-head"><h3 id="pc-modal-title">Nuovo controllo periodico</h3><button class="sm-close" type="button" aria-label="Chiudi" onclick="pcClose()">×</button></div>
|
|
<div class="sm-body">
|
|
<input type="hidden" id="pc-id">
|
|
<div class="sm-field"><label for="pc-title">Titolo *</label><input type="text" id="pc-title" maxlength="255" placeholder="es. Verifica mensile dei backup"></div>
|
|
<div class="sm-row">
|
|
<div class="sm-field"><label for="pc-cat">Categoria</label><input type="text" id="pc-cat" maxlength="100" placeholder="es. Backup, Accessi, Rete"></div>
|
|
<div class="sm-field"><label for="pc-ref">Controllo collegato <span class="hint">(SoA/NIS2)</span></label><input type="text" id="pc-ref" maxlength="32" placeholder="es. A.8.13"></div>
|
|
</div>
|
|
<div class="sm-row">
|
|
<div class="sm-field"><label for="pc-owner">Responsabile</label><select id="pc-owner"><option value="">— Nessuno —</option></select></div>
|
|
<div class="sm-field"><label for="pc-freq">Frequenza *</label>
|
|
<select id="pc-freq" onchange="pcFreqChange()">
|
|
<option value="giornaliero">Giornaliera</option><option value="settimanale">Settimanale</option>
|
|
<option value="mensile" selected>Mensile</option><option value="trimestrale">Trimestrale</option>
|
|
<option value="semestrale">Semestrale</option><option value="annuale">Annuale</option><option value="custom">Personalizzata (giorni)</option>
|
|
</select>
|
|
</div>
|
|
<div class="sm-field hidden" id="pc-fdays-wrap"><label for="pc-fdays">Ogni N giorni</label><input type="number" id="pc-fdays" min="1" value="30"></div>
|
|
</div>
|
|
<div class="sm-row">
|
|
<div class="sm-field"><label for="pc-next">Prossima scadenza <span class="hint">(vuoto = calcolata)</span></label><input type="date" id="pc-next"></div>
|
|
<div class="sm-field"><label for="pc-status">Stato</label><select id="pc-status"><option value="active">Attivo</option><option value="suspended">Sospeso</option></select></div>
|
|
</div>
|
|
<div class="sm-field"><label for="pc-method">Metodo di verifica</label><textarea id="pc-method" maxlength="2000" placeholder="Come si esegue il controllo e quali evidenze si raccolgono"></textarea></div>
|
|
<div class="sm-err" id="pc-err" role="alert"></div>
|
|
</div>
|
|
<div class="sm-foot"><button class="btn btn-outline" type="button" onclick="pcClose()">Annulla</button><button class="btn btn-primary" type="button" id="pc-save" onclick="pcSave()">Salva</button></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modale dettaglio + esecuzioni -->
|
|
<div class="sm-overlay" id="det-modal" role="dialog" aria-modal="true" aria-labelledby="det-title">
|
|
<div class="sm-dialog"><div class="sm-head"><h3 id="det-title">Dettaglio controllo</h3><button class="sm-close" type="button" aria-label="Chiudi" onclick="detClose()">×</button></div>
|
|
<div class="sm-body" id="det-body"></div>
|
|
<div class="sm-foot"><button class="btn btn-outline" type="button" onclick="detClose()">Chiudi</button><a class="btn btn-outline" id="det-report" href="#" target="_blank" rel="noopener">Report</a><button class="btn btn-primary" type="button" onclick="execOpen()">Registra esecuzione</button></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modale registra esecuzione -->
|
|
<div class="sm-overlay" id="exec-modal" role="dialog" aria-modal="true" aria-labelledby="exec-title">
|
|
<div class="sm-dialog" style="max-width:520px;"><div class="sm-head"><h3 id="exec-title">Registra esecuzione</h3><button class="sm-close" type="button" aria-label="Chiudi" onclick="execClose()">×</button></div>
|
|
<div class="sm-body">
|
|
<div class="sm-row">
|
|
<div class="sm-field"><label for="ex-date">Data esecuzione</label><input type="date" id="ex-date"></div>
|
|
<div class="sm-field"><label for="ex-outcome">Esito *</label>
|
|
<select id="ex-outcome" onchange="execOutcomeChange()">
|
|
<option value="conforme">Conforme</option><option value="parziale">Parziale</option>
|
|
<option value="non_conforme">Non conforme</option><option value="non_applicabile">Non applicabile</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="sm-field"><label for="ex-notes">Note / evidenza</label><textarea id="ex-notes" maxlength="2000"></textarea></div>
|
|
<div class="sm-field hidden" id="ex-gen-wrap"><label for="ex-gen">Su esito negativo</label>
|
|
<select id="ex-gen"><option value="none">Nessuna azione</option><option value="nc">Genera non conformità (NC)</option><option value="ac">Genera NC + azione correttiva (AC)</option></select>
|
|
</div>
|
|
<div class="sm-err" id="ex-err" role="alert"></div>
|
|
</div>
|
|
<div class="sm-foot"><button class="btn btn-outline" type="button" onclick="execClose()">Annulla</button><button class="btn btn-primary" type="button" id="ex-save" onclick="execSave()">Registra</button></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
|
|
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
|
|
<script src="/js/api.js?v=20260727r"></script>
|
|
<script src="/js/common.js?v=20260627t"></script>
|
|
<script src="/vendor/lucide/lucide.min.js"></script>
|
|
<script src="/js/topnav-v3.js?v=20260627t"></script>
|
|
<script src="/js/i18n.js?v=20260727r"></script>
|
|
<script src="/js/help.js?v=20260727r"></script>
|
|
<script>
|
|
'use strict';
|
|
/* Controlli periodici (control monitoring §9.1/A.8.16): controlli ricorrenti + esecuzioni (evidenza) + NC/AC. */
|
|
let PC = { list: [], roles: [], detail: null };
|
|
const FREQ_LABEL = { giornaliero:'Giornaliera', settimanale:'Settimanale', mensile:'Mensile', trimestrale:'Trimestrale', semestrale:'Semestrale', annuale:'Annuale', custom:'Personalizzata' };
|
|
const DUE_LABEL = { overdue:'In ritardo', due_soon:'In scadenza', upcoming:'Pianificato', suspended:'Sospeso', none:'—' };
|
|
const OUT_LABEL = { conforme:'Conforme', non_conforme:'Non conforme', parziale:'Parziale', non_applicabile:'N.A.' };
|
|
|
|
function el(id) { return document.getElementById(id); }
|
|
function esc(s) { const d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
|
|
|
|
document.addEventListener('DOMContentLoaded', async function () {
|
|
if (typeof checkAuth === 'function' && !checkAuth()) return;
|
|
if (typeof loadSidebar === 'function') loadSidebar();
|
|
if (window.I18n && I18n.init) I18n.init('it');
|
|
if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
|
|
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') { pcClose(); detClose(); execClose(); } });
|
|
['pc-modal','det-modal','exec-modal'].forEach(m => { const o = el(m); if (o) o.addEventListener('click', e => { if (e.target === o) o.classList.remove('open'); }); });
|
|
try { const r = await api.orgRolesList(); PC.roles = (r && (r.roles || r)) || []; } catch (e) { PC.roles = []; }
|
|
el('pc-owner').innerHTML = '<option value="">— Nessuno —</option>' + (Array.isArray(PC.roles) ? PC.roles : []).map(x => '<option value="' + x.id + '">' + esc(x.role_name || x.name) + '</option>').join('');
|
|
await pcLoad();
|
|
});
|
|
|
|
async function pcLoad() {
|
|
el('pc-list').innerHTML = '<div class="pc-empty">Caricamento…</div>';
|
|
try {
|
|
const d = await api.pctlList(); PC.list = (d && d.controls) || [];
|
|
pcRender();
|
|
} catch (e) { el('pc-list').innerHTML = '<div class="pc-empty">Errore: ' + esc(e.message || e) + '</div>'; }
|
|
}
|
|
|
|
function pcRender() {
|
|
if (!PC.list.length) { el('pc-list').innerHTML = '<div class="pc-empty">Nessun controllo periodico. Creane uno con "+ Nuovo controllo".</div>'; return; }
|
|
const rows = PC.list.map(c => {
|
|
const due = c.due_status || 'none';
|
|
const lo = c.last_outcome ? '<span class="pc-badge o-' + c.last_outcome + '">' + esc(OUT_LABEL[c.last_outcome] || c.last_outcome) + '</span>' : '<span style="color:#9ca3af">—</span>';
|
|
return '<tr>'
|
|
+ '<td><span class="pc-badge code">' + esc(c.code || '—') + '</span></td>'
|
|
+ '<td><strong>' + esc(c.title) + '</strong>' + (c.control_ref ? ' <span class="pc-badge code">' + esc(c.control_ref) + '</span>' : '') + (c.category ? '<div style="font-size:.78rem;color:#6b7280;">' + esc(c.category) + '</div>' : '') + '</td>'
|
|
+ '<td style="font-size:.82rem;">' + esc(c.owner_role || '—') + '</td>'
|
|
+ '<td style="font-size:.82rem;">' + esc(FREQ_LABEL[c.frequency] || c.frequency) + (c.frequency === 'custom' && c.frequency_days ? ' (' + c.frequency_days + 'gg)' : '') + '</td>'
|
|
+ '<td><span class="pc-badge d-' + due + '">' + esc(c.next_due_date || '—') + '</span><div style="font-size:.72rem;color:#6b7280;">' + esc(DUE_LABEL[due] || '') + '</div></td>'
|
|
+ '<td>' + lo + '</td>'
|
|
+ '<td style="white-space:nowrap;">'
|
|
+ '<button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;" onclick="detOpen(' + c.id + ')">Dettaglio</button> '
|
|
+ '<button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;" onclick="pcOpen(' + c.id + ')">Modifica</button> '
|
|
+ '<button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;color:#b91c1c;border-color:#fecaca;" onclick="pcDelete(' + c.id + ')">Elimina</button>'
|
|
+ '</td></tr>';
|
|
}).join('');
|
|
el('pc-list').innerHTML = '<table class="pc-table"><thead><tr><th>Codice</th><th>Controllo</th><th>Responsabile</th><th>Frequenza</th><th>Prossima scadenza</th><th>Ultimo esito</th><th>Azioni</th></tr></thead><tbody>' + rows + '</tbody></table>';
|
|
}
|
|
|
|
function pcFreqChange() { el('pc-fdays-wrap').classList.toggle('hidden', el('pc-freq').value !== 'custom'); }
|
|
|
|
function pcOpen(id) {
|
|
const c = id ? PC.list.find(x => x.id === id) : null;
|
|
el('pc-modal-title').textContent = c ? 'Modifica controllo periodico' : 'Nuovo controllo periodico';
|
|
el('pc-id').value = c ? c.id : '';
|
|
el('pc-title').value = c ? (c.title || '') : '';
|
|
el('pc-cat').value = c ? (c.category || '') : '';
|
|
el('pc-ref').value = c ? (c.control_ref || '') : '';
|
|
el('pc-owner').value = c && c.owner_role_id ? String(c.owner_role_id) : '';
|
|
el('pc-freq').value = c ? (c.frequency || 'mensile') : 'mensile';
|
|
el('pc-fdays').value = c && c.frequency_days ? c.frequency_days : 30;
|
|
el('pc-next').value = c ? (c.next_due_date || '') : '';
|
|
el('pc-status').value = c ? (c.status || 'active') : 'active';
|
|
el('pc-method').value = c ? (c.method || '') : '';
|
|
el('pc-err').textContent = ''; pcFreqChange();
|
|
el('pc-modal').classList.add('open');
|
|
setTimeout(() => el('pc-title').focus(), 50);
|
|
}
|
|
function pcClose() { el('pc-modal').classList.remove('open'); }
|
|
|
|
async function pcSave() {
|
|
const id = el('pc-id').value;
|
|
const title = el('pc-title').value.trim();
|
|
if (!title) { el('pc-err').textContent = 'Il titolo è obbligatorio.'; return; }
|
|
const freq = el('pc-freq').value;
|
|
const payload = {
|
|
title: title, category: el('pc-cat').value.trim(), control_ref: el('pc-ref').value.trim(),
|
|
owner_role_id: el('pc-owner').value ? parseInt(el('pc-owner').value, 10) : null,
|
|
frequency: freq, frequency_days: freq === 'custom' ? parseInt(el('pc-fdays').value, 10) || 30 : null,
|
|
next_due_date: el('pc-next').value || null, status: el('pc-status').value, method: el('pc-method').value.trim(),
|
|
};
|
|
const btn = el('pc-save'); btn.disabled = true;
|
|
try {
|
|
if (id) await api.pctlUpdate(parseInt(id, 10), payload); else await api.pctlCreate(payload);
|
|
showNotification(id ? 'Controllo aggiornato.' : 'Controllo creato.', 'success'); pcClose(); await pcLoad();
|
|
} catch (e) { el('pc-err').textContent = e.message || 'Errore nel salvataggio.'; } finally { btn.disabled = false; }
|
|
}
|
|
|
|
async function pcDelete(id) {
|
|
const c = PC.list.find(x => x.id === id);
|
|
if (!confirm('Eliminare il controllo "' + (c ? c.title : '') + '" e il suo storico?')) return;
|
|
try { await api.pctlDelete(id); showNotification('Eliminato.', 'success'); await pcLoad(); }
|
|
catch (e) { showNotification(e.message || 'Errore', 'error'); }
|
|
}
|
|
|
|
async function detOpen(id) {
|
|
el('det-body').innerHTML = '<div class="pc-empty">Caricamento…</div>';
|
|
el('det-modal').classList.add('open');
|
|
try { const c = await api.pctlGet(id); PC.detail = c; detRender(c); }
|
|
catch (e) { el('det-body').innerHTML = '<div class="pc-empty">Errore: ' + esc(e.message) + '</div>'; }
|
|
}
|
|
function detClose() { el('det-modal').classList.remove('open'); PC.detail = null; }
|
|
|
|
function detRender(c) {
|
|
el('det-title').textContent = 'Controllo ' + (c.code || '');
|
|
const tok = (window.localStorage ? localStorage.getItem('nis2_access_token') : '') || '';
|
|
const org = (window.localStorage ? localStorage.getItem('nis2_org_id') : '') || '';
|
|
el('det-report').href = (api.baseUrl || '/api') + '/periodic-controls/' + c.id + '/report?token=' + encodeURIComponent(tok) + (org ? '&org_id=' + encodeURIComponent(org) : '');
|
|
const ex = c.executions || [];
|
|
let html = '<div style="font-size:.9rem;margin-bottom:8px;"><strong>' + esc(c.title) + '</strong>' + (c.control_ref ? ' <span class="pc-badge code">' + esc(c.control_ref) + '</span>' : '') + '</div>'
|
|
+ '<div style="font-size:.82rem;color:#374151;margin-bottom:12px;">Frequenza: ' + esc(FREQ_LABEL[c.frequency] || c.frequency) + ' · Responsabile: ' + esc(c.owner_role || '—') + ' · Prossima scadenza: ' + esc(c.next_due_date || '—') + '</div>';
|
|
if (c.method) html += '<div style="font-size:.82rem;color:#475569;margin-bottom:12px;"><em>Metodo:</em> ' + esc(c.method) + '</div>';
|
|
html += '<h4 style="margin:8px 0 6px;font-size:.82rem;text-transform:uppercase;letter-spacing:.04em;color:#6b7280;">Storico esecuzioni (evidenza)</h4>';
|
|
if (!ex.length) { html += '<div class="pc-empty">Nessuna esecuzione registrata.</div>'; }
|
|
else {
|
|
html += '<table class="pc-table"><thead><tr><th>Data</th><th>Esito</th><th>Eseguito da</th><th>NC</th><th>Note</th></tr></thead><tbody>'
|
|
+ ex.map(e => '<tr><td>' + esc(e.executed_at) + '</td><td><span class="pc-badge o-' + e.outcome + '">' + esc(OUT_LABEL[e.outcome] || e.outcome) + '</span></td>'
|
|
+ '<td style="font-size:.8rem;">' + esc(e.executed_by_name || '') + '</td><td style="font-size:.8rem;">' + esc(e.ncr_code || '') + '</td><td style="font-size:.82rem;">' + esc(e.notes || '') + '</td></tr>').join('')
|
|
+ '</tbody></table>';
|
|
}
|
|
el('det-body').innerHTML = html;
|
|
}
|
|
|
|
function execOpen() { if (!PC.detail) return; el('ex-date').value = new Date().toISOString().slice(0, 10); el('ex-outcome').value = 'conforme'; el('ex-notes').value = ''; el('ex-gen').value = 'none'; el('ex-err').textContent = ''; execOutcomeChange(); el('exec-modal').classList.add('open'); setTimeout(() => el('ex-outcome').focus(), 50); }
|
|
function execClose() { el('exec-modal').classList.remove('open'); }
|
|
function execOutcomeChange() { const neg = ['non_conforme', 'parziale'].indexOf(el('ex-outcome').value) !== -1; el('ex-gen-wrap').classList.toggle('hidden', !neg); }
|
|
|
|
async function execSave() {
|
|
if (!PC.detail) return;
|
|
const payload = { executed_at: el('ex-date').value || null, outcome: el('ex-outcome').value, notes: el('ex-notes').value.trim(),
|
|
generate: (['non_conforme', 'parziale'].indexOf(el('ex-outcome').value) !== -1) ? el('ex-gen').value : 'none' };
|
|
const btn = el('ex-save'); btn.disabled = true;
|
|
try {
|
|
const r = await api.pctlAddExecution(PC.detail.id, payload);
|
|
showNotification('Esecuzione registrata' + (r && r.ncr ? ' · ' + (r.ncr.ncr_code || 'NC creata') : '') + '.', 'success');
|
|
execClose(); const c = await api.pctlGet(PC.detail.id); PC.detail = c; detRender(c); await pcLoad();
|
|
} catch (e) { el('ex-err').textContent = e.message || 'Errore.'; } finally { btn.disabled = false; }
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|