Files
nis2-agile/public/internal-audits.html
T

413 lines
26 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>Audit interni · 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}
.ia-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; }
.ia-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
.ia-toolbar { display:flex; justify-content:flex-end; margin-bottom:12px; }
.ia-table { width:100%; border-collapse:collapse; font-size:.88rem; }
.ia-table th, .ia-table td { text-align:left; padding:9px 12px; border-top:1px solid var(--gray-100,#f3f4f6); vertical-align:top; }
.ia-table th { font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
.ia-badge { display:inline-block; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; white-space:nowrap; }
.ia-badge.s-planned { background:#e0f2fe; color:#075985; }
.ia-badge.s-in_progress { background:#fef3c7; color:#92400e; }
.ia-badge.s-completed { background:#dcfce7; color:#166534; }
.ia-badge.s-cancelled { background:#fee2e2; color:#991b1b; }
.ia-empty { text-align:center; padding:22px 16px; color:var(--gray-500,#6b7280); font-size:.86rem; }
.ia-overdue { color:#991b1b; font-weight:600; }
.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:760px; 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; flex-wrap:wrap; }
.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 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:150px; }
.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; }
.ck-group { margin-top:16px; }
.ck-group h4 { font-size:.82rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); margin:14px 0 6px; }
.ck-row { display:flex; gap:8px; align-items:flex-start; padding:8px 0; border-top:1px solid var(--gray-100,#f3f4f6); }
.ck-row:first-of-type { border-top:none; }
.ck-ref { flex:0 0 64px; font-size:.78rem; font-weight:700; color:#374151; padding-top:8px; }
.ck-main { flex:1 1 auto; }
.ck-cp { font-size:.86rem; }
.ck-controls { display:flex; gap:6px; align-items:center; margin-top:5px; flex-wrap:wrap; }
.ck-controls select { padding:5px 8px; border:1px solid var(--gray-200,#e5e7eb); border-radius:7px; font-size:.8rem; }
.ck-controls input[type=text] { padding:5px 8px; border:1px solid var(--gray-200,#e5e7eb); border-radius:7px; font-size:.8rem; flex:1 1 180px; }
.ck-nc { font-size:.74rem; background:none; border:1px solid #fecaca; color:#b91c1c; border-radius:6px; padding:4px 9px; cursor:pointer; }
.ck-nc[disabled] { opacity:.6; cursor:default; }
.ck-ncbadge { font-size:.72rem; color:#991b1b; font-weight:700; }
.det-meta { font-size:.85rem; color:#374151; margin-bottom:6px; }
.det-meta strong { color:#111827; }
</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="iaud.title">Audit interni</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="iaud.title">Audit interni</h1>
<p class="sub">Programma, checklist ed esiti degli audit interni del SGSI (ISO/IEC 27001 §9.2)</p>
</div>
<button class="btn-primary" onclick="auditOpen()"><i data-lucide="plus" class="ic"></i>Nuovo audit</button>
</div>
<section class="card card-pad">
<div class="ia-intro">
<strong>Audit interni del SGSI (ISO/IEC 27001 §9.2).</strong>
Programma gli audit, conduci la <em>checklist</em> (clausole 4-10 ISO 27001 + controlli Annex A applicabili dal tuo SoA),
registra gli esiti riga per riga, apri una <em>non conformità</em> sui punti non conformi e genera il <em>report</em> stampabile.
</div>
<div class="ia-note">
Ancoraggio: <strong>ISO/IEC 27001 §9.2</strong> (audit interni) — buona prassi di governance anche per NIS2
(<strong>GV.PO-02</strong>, monitoraggio del programma di sicurezza). Strumento di supporto organizzativo, non un parere legale.
</div>
<div id="aud-list" aria-live="polite"></div>
</section>
</main>
<!-- Modale crea/modifica audit -->
<div class="sm-overlay" id="aud-modal" role="dialog" aria-modal="true" aria-labelledby="aud-modal-title">
<div class="sm-dialog">
<div class="sm-head"><h3 id="aud-modal-title">Nuovo audit</h3><button class="sm-close" type="button" aria-label="Chiudi" onclick="auditClose()">&times;</button></div>
<div class="sm-body">
<input type="hidden" id="aud-id">
<div class="sm-field"><label for="aud-title">Titolo *</label><input type="text" id="aud-title" maxlength="255" placeholder="Es. Audit interno SGSI 2026"></div>
<div class="sm-row">
<div class="sm-field"><label for="aud-status">Stato</label>
<select id="aud-status">
<option value="planned">Pianificato</option>
<option value="in_progress">In corso</option>
<option value="completed">Completato</option>
<option value="cancelled">Annullato</option>
</select>
</div>
<div class="sm-field"><label for="aud-planned">Data pianificata <span class="hint">(→ calendario)</span></label><input type="date" id="aud-planned"></div>
<div class="sm-field"><label for="aud-executed">Data esecuzione</label><input type="date" id="aud-executed"></div>
</div>
<div class="sm-field"><label for="aud-scope">Ambito (scope)</label><textarea id="aud-scope" maxlength="2000" placeholder="Es. SGSI, sede principale, processi IT"></textarea></div>
<div class="sm-field"><label for="aud-criteria">Criteri</label><textarea id="aud-criteria" maxlength="2000" placeholder="Es. ISO/IEC 27001:2022 cl.4-10 + Annex A">ISO/IEC 27001:2022 cl.4-10 + Annex A</textarea>
<span class="hint">Alla creazione la checklist viene pre-popolata con le clausole 4-10 e i controlli Annex A applicabili dal tuo SoA.</span>
</div>
<div class="sm-row">
<div class="sm-field"><label for="aud-lead-user">Auditor capo <span class="hint">(utente)</span></label><select id="aud-lead-user"><option value="">— Nessuno —</option></select></div>
<div class="sm-field"><label for="aud-lead-role">Auditor capo <span class="hint">(ruolo)</span></label><select id="aud-lead-role"><option value="">— Nessuno —</option></select></div>
</div>
<div class="sm-field" id="aud-wrap-conclusion"><label for="aud-conclusion">Conclusioni</label><textarea id="aud-conclusion" maxlength="2000"></textarea></div>
<div class="sm-err" id="aud-err" role="alert"></div>
</div>
<div class="sm-foot"><button class="btn btn-outline" type="button" onclick="auditClose()">Annulla</button><button class="btn btn-primary" type="button" id="aud-save" onclick="auditSave()">Salva</button></div>
</div>
</div>
<!-- Modale dettaglio audit (checklist) -->
<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 audit</h3><button class="sm-close" type="button" aria-label="Chiudi" onclick="detClose()">&times;</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>
<button class="btn btn-outline" type="button" onclick="detAddItem()">+ Voce custom</button>
<button class="btn btn-outline" type="button" onclick="detEdit()">Modifica audit</button>
<a class="btn btn-primary" id="det-report" href="#" target="_blank" rel="noopener">Report</a>
</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=20260730a"></script>
<script src="/js/common.js?v=20260729b"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260729"></script>
<script src="/js/i18n.js?v=20260731"></script>
<script src="/js/help.js?v=20260806b"></script>
<script>
'use strict';
/*
* NIS2 Agile - Audit interni (ISO 27001 §9.2) — MODULO A
* Programma audit + checklist (clausole 4-10 + Annex A dal SoA) + esiti per riga
* + apertura NC sui non conformi + report HTML stampabile.
*/
let IA = { audits: [], users: [], roles: [], detail: null };
const ST_LABELS = { planned: 'Pianificato', in_progress: 'In corso', completed: 'Completato', cancelled: 'Annullato' };
const RES_LABELS = { da_verificare: 'Da verificare', conforme: 'Conforme', non_conforme: 'Non conforme', osservazione: 'Osservazione', opportunita: 'Opportunità', non_applicabile: 'Non applicabile' };
const TYPE_LABELS = { clause: 'Clausole ISO 27001', annex_control: 'Controlli Annex A', nis2_measure: 'Misure NIS2', custom: 'Voci aggiuntive' };
const TYPE_ORDER = ['clause', 'annex_control', 'nis2_measure', 'custom'];
function el(id) { return document.getElementById(id); }
function esc(s) { const d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
function escAttr(s) { return esc(s).replace(/"/g, '&quot;'); }
// Il report è HTML servito dall'API: token+org passati in query (no header su <a target=_blank>).
function reportUrl(id) {
const base = (api && api.baseUrl) ? api.baseUrl : '/api';
const qs = [];
if (api && api.token) qs.push('token=' + encodeURIComponent(api.token));
if (api && api.orgId) qs.push('org_id=' + encodeURIComponent(api.orgId));
return base + '/internal-audits/' + id + '/report' + (qs.length ? '?' + qs.join('&') : '');
}
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') { auditClose(); detClose(); } });
['aud-modal', 'det-modal'].forEach(m => { const o = el(m); if (o) o.addEventListener('click', e => { if (e.target === o) o.classList.remove('open'); }); });
await iaLoadAll();
});
async function iaLoadAll() {
el('aud-list').innerHTML = '<div class="ia-empty">Caricamento…</div>';
try {
const [auds, users, roles] = await Promise.all([
api.intAuditList(),
api.orgRolesAssignableUsers().catch(() => ({ users: [] })),
api.orgRolesList().catch(() => ({ roles: [] }))
]);
IA.audits = (auds && auds.audits) || [];
IA.users = (users && (users.users || users.assignable_users)) || [];
IA.roles = (roles && (roles.roles || roles.org_roles)) || [];
fillSelects();
renderAudits();
} catch (e) {
el('aud-list').innerHTML = '<div class="ia-empty">Errore: ' + esc(e.message || e) + '</div>';
}
}
function fillSelects() {
el('aud-lead-user').innerHTML = '<option value="">— Nessuno —</option>'
+ IA.users.map(u => '<option value="' + u.id + '">' + esc(u.full_name || u.name || ('Utente ' + u.id)) + '</option>').join('');
el('aud-lead-role').innerHTML = '<option value="">— Nessuno —</option>'
+ IA.roles.map(r => '<option value="' + r.id + '">' + esc(r.role_name || r.name || ('Ruolo ' + r.id)) + '</option>').join('');
}
function todayStr() { return new Date().toISOString().slice(0, 10); }
function isOverdue(a) { return a.planned_date && a.planned_date < todayStr() && a.status !== 'completed' && a.status !== 'cancelled'; }
function renderAudits() {
if (!IA.audits.length) { el('aud-list').innerHTML = '<div class="ia-empty">Nessun audit. Creane uno con "+ Nuovo audit".</div>'; return; }
const rows = IA.audits.map(a => {
const dl = a.planned_date ? ('<span class="' + (isOverdue(a) ? 'ia-overdue' : '') + '">' + esc(a.planned_date) + '</span>') : '—';
const lead = a.lead_auditor_name ? esc(a.lead_auditor_name) : (a.lead_auditor_role_name ? esc(a.lead_auditor_role_name) : '—');
const ncTag = a.n_nc > 0 ? ' <span class="ia-badge s-cancelled">' + a.n_nc + ' NC</span>' : '';
return '<tr>'
+ '<td><strong>' + esc(a.code || '—') + '</strong></td>'
+ '<td><a href="#" onclick="detOpen(' + a.id + ');return false;"><strong>' + esc(a.title) + '</strong></a><div style="font-size:.78rem;color:#6b7280;">' + a.n_items + ' voci' + ncTag + '</div></td>'
+ '<td><span class="ia-badge s-' + a.status + '">' + (ST_LABELS[a.status] || a.status) + '</span></td>'
+ '<td>' + lead + '</td>'
+ '<td>' + dl + '</td>'
+ '<td style="white-space:nowrap;">'
+ '<button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;" onclick="detOpen(' + a.id + ')">Apri</button> '
+ '<button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;color:#b91c1c;border-color:#fecaca;" onclick="auditDelete(' + a.id + ')">Elimina</button>'
+ '</td></tr>';
}).join('');
el('aud-list').innerHTML = '<table class="ia-table"><thead><tr><th>Codice</th><th>Titolo</th><th>Stato</th><th>Auditor capo</th><th>Scadenza</th><th>Azioni</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
// ── Crea / modifica audit ──
function auditOpen(id) {
el('aud-err').textContent = '';
el('aud-id').value = id || '';
el('aud-modal-title').textContent = id ? 'Modifica audit' : 'Nuovo audit';
el('aud-wrap-conclusion').classList.toggle('hidden', !id);
if (!id) {
el('aud-title').value = ''; el('aud-status').value = 'planned';
el('aud-planned').value = ''; el('aud-executed').value = '';
el('aud-scope').value = ''; el('aud-criteria').value = 'ISO/IEC 27001:2022 cl.4-10 + Annex A';
el('aud-lead-user').value = ''; el('aud-lead-role').value = ''; el('aud-conclusion').value = '';
} else {
const a = IA.detail || IA.audits.find(x => x.id === id) || {};
el('aud-title').value = a.title || ''; el('aud-status').value = a.status || 'planned';
el('aud-planned').value = a.planned_date || ''; el('aud-executed').value = a.executed_date || '';
el('aud-scope').value = a.scope || ''; el('aud-criteria').value = a.criteria || '';
el('aud-lead-user').value = a.lead_auditor_user_id || ''; el('aud-lead-role').value = a.lead_auditor_role_id || '';
el('aud-conclusion').value = a.conclusion || '';
}
el('aud-modal').classList.add('open');
setTimeout(function () { var fe = el('aud-modal').querySelector('input,select,textarea'); if (fe) fe.focus(); }, 50);
}
function auditClose() { el('aud-modal').classList.remove('open'); }
async function auditSave() {
const id = el('aud-id').value;
const title = el('aud-title').value.trim();
if (!title) { el('aud-err').textContent = 'Il titolo è obbligatorio.'; return; }
const payload = {
title: title,
status: el('aud-status').value,
planned_date: el('aud-planned').value || null,
executed_date: el('aud-executed').value || null,
scope: el('aud-scope').value.trim() || null,
criteria: el('aud-criteria').value.trim() || null,
lead_auditor_user_id: el('aud-lead-user').value || null,
lead_auditor_role_id: el('aud-lead-role').value || null
};
if (id) { payload.conclusion = el('aud-conclusion').value.trim() || null; }
el('aud-save').disabled = true;
try {
if (id) {
await api.intAuditUpdate(id, payload);
} else {
const res = await api.intAuditCreate(payload);
if (res && res.items_seeded != null) { /* checklist pre-popolata */ }
}
auditClose();
await iaLoadAll();
} catch (e) {
el('aud-err').textContent = e.message || 'Errore di salvataggio.';
} finally {
el('aud-save').disabled = false;
}
}
async function auditDelete(id) {
if (!confirm('Eliminare questo audit e tutta la sua checklist?')) return;
try { await api.intAuditDelete(id); await iaLoadAll(); }
catch (e) { alert(e.message || 'Errore eliminazione.'); }
}
// ── Dettaglio (checklist) ──
async function detOpen(id) {
el('det-body').innerHTML = '<div class="ia-empty">Caricamento…</div>';
el('det-modal').classList.add('open');
setTimeout(function () { var fe = el('det-modal').querySelector('.sm-close, button'); if (fe) fe.focus(); }, 50);
try {
const a = await api.intAuditGet(id);
IA.detail = a;
el('det-report').href = reportUrl(id);
renderDetail(a);
} catch (e) {
el('det-body').innerHTML = '<div class="ia-empty">Errore: ' + esc(e.message || e) + '</div>';
}
}
function detClose() { el('det-modal').classList.remove('open'); IA.detail = null; }
function detEdit() { if (IA.detail) auditOpen(IA.detail.id); }
function renderDetail(a) {
el('det-title').textContent = (a.code ? a.code + ' — ' : '') + a.title;
const lead = a.lead_auditor_name || a.lead_auditor_role_name || '—';
let h = '<div class="det-meta"><strong>Stato:</strong> ' + esc(ST_LABELS[a.status] || a.status)
+ ' &middot; <strong>Auditor capo:</strong> ' + esc(lead)
+ ' &middot; <strong>Pianificata:</strong> ' + esc(a.planned_date || '—') + '</div>';
if (a.scope) h += '<div class="det-meta"><strong>Ambito:</strong> ' + esc(a.scope) + '</div>';
if (a.criteria) h += '<div class="det-meta"><strong>Criteri:</strong> ' + esc(a.criteria) + '</div>';
const items = a.items || [];
if (!items.length) { h += '<div class="ia-empty">Nessuna voce di checklist. Aggiungine una con "+ Voce custom".</div>'; el('det-body').innerHTML = h; return; }
const groups = {};
items.forEach(it => { (groups[it.ref_type] = groups[it.ref_type] || []).push(it); });
TYPE_ORDER.forEach(type => {
if (!groups[type] || !groups[type].length) return;
h += '<div class="ck-group"><h4>' + esc(TYPE_LABELS[type] || type) + ' (' + groups[type].length + ')</h4>';
groups[type].forEach(it => { h += renderItemRow(it); });
h += '</div>';
});
el('det-body').innerHTML = h;
}
function renderItemRow(it) {
const opts = Object.keys(RES_LABELS).map(k => '<option value="' + k + '"' + (it.result === k ? ' selected' : '') + '>' + esc(RES_LABELS[k]) + '</option>').join('');
let ncCtl;
if (it.ncr) {
ncCtl = '<span class="ck-ncbadge">NC ' + esc(it.ncr.ncr_code) + '</span>';
} else {
const dis = it.result === 'non_conforme' ? '' : ' disabled title="Apri NC disponibile sulle voci non conformi"';
ncCtl = '<button class="ck-nc" onclick="itemRaiseNcr(' + it.id + ')"' + dis + '>Apri NC</button>';
}
return '<div class="ck-row" id="ck-' + it.id + '">'
+ '<div class="ck-ref">' + esc(it.ref_code || '') + '</div>'
+ '<div class="ck-main">'
+ '<div class="ck-cp">' + esc(it.checkpoint) + '</div>'
+ '<div class="ck-controls">'
+ '<select onchange="itemSetResult(' + it.id + ', this.value)">' + opts + '</select>'
+ '<input type="text" placeholder="Note / evidenze" value="' + escAttr(it.note || '') + '" onchange="itemSetNote(' + it.id + ', this.value)">'
+ ncCtl
+ '</div></div></div>';
}
async function itemSetResult(itemId, value) {
try {
await api.intAuditUpdateItem(itemId, { result: value });
const it = findItem(itemId); if (it) { it.result = value; }
// ridisegna la riga per aggiornare il pulsante "Apri NC"
const row = el('ck-' + itemId); if (row && it) row.outerHTML = renderItemRow(it);
} catch (e) { alert(e.message || 'Errore aggiornamento esito.'); }
}
async function itemSetNote(itemId, value) {
try { await api.intAuditUpdateItem(itemId, { note: value }); const it = findItem(itemId); if (it) it.note = value; }
catch (e) { alert(e.message || 'Errore aggiornamento nota.'); }
}
async function itemRaiseNcr(itemId) {
if (!IA.detail) return;
if (!confirm('Aprire una non conformità (NC) collegata a questa voce?')) return;
try {
const res = await api.intAuditRaiseNcr(IA.detail.id, { item_id: itemId });
const it = findItem(itemId);
if (it) { it.ncr = { id: res.id, ncr_code: res.ncr_code }; const row = el('ck-' + itemId); if (row) row.outerHTML = renderItemRow(it); }
alert('Non conformità ' + (res.ncr_code || '') + (res.already ? ' (già esistente)' : ' creata') + '. La trovi nel modulo NCR/CAPA.');
await iaLoadAll();
} catch (e) { alert(e.message || 'Errore apertura NC.'); }
}
function findItem(itemId) { return (IA.detail && IA.detail.items || []).find(x => x.id === itemId); }
async function detAddItem() {
if (!IA.detail) return;
const cp = prompt('Testo del punto di verifica (voce custom):');
if (!cp || !cp.trim()) return;
try {
await api.intAuditAddItem({ audit_id: IA.detail.id, checkpoint: cp.trim(), ref_type: 'custom' });
await detOpen(IA.detail.id);
await iaLoadAll();
} catch (e) { alert(e.message || 'Errore aggiunta voce.'); }
}
</script>
</body>
</html>