- help.js 'misure-requisiti': sezione 'Valutazione e stato di conformità' (stato per requisito, valutazione del rischio che parte vuota, copia procedure di default) + nota. - KB AI: doc SYSTEM su valutazione conformità + procedure/rischi di default (uuid 92979c05). - Cache-buster ?v=20260622 (35 pagine), version.json 1.18.1, SW cache nis2-shell-v1.18.1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
279 lines
17 KiB
HTML
279 lines
17 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>Stakeholder - NIS2 Agile</title>
|
|
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css. -->
|
|
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
|
<link rel="stylesheet" href="/css/style.css?v=20260617">
|
|
<style>
|
|
.stk-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:16px; font-size:.92rem; line-height:1.6; }
|
|
.stk-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 18px; }
|
|
.stk-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
|
|
.stk-toolbar .grow { flex:1 1 auto; }
|
|
.stk-toolbar .btn { padding:4px 12px; font-size:.8rem; }
|
|
.stk-group { border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; margin-bottom:14px; overflow:hidden; }
|
|
.stk-group-head { background:var(--gray-50,#f9fafb); padding:11px 14px; display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:6px; }
|
|
.stk-group-title { font-weight:700; font-size:.98rem; }
|
|
.stk-count { display:inline-block; font-size:.72rem; font-weight:800; padding:2px 10px; border-radius:20px; background:#e0f2fe; color:#075985; margin-left:8px; }
|
|
.stk-table { width:100%; border-collapse:collapse; font-size:.88rem; }
|
|
.stk-table th, .stk-table td { text-align:left; padding:9px 12px; border-top:1px solid var(--gray-100,#f3f4f6); vertical-align:top; }
|
|
.stk-table th { font-size:.74rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
|
|
.stk-badge { display:inline-block; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; }
|
|
.stk-badge.crit-low { background:#dcfce7; color:#166534; }
|
|
.stk-badge.crit-medium { background:#fef3c7; color:#92400e; }
|
|
.stk-badge.crit-high { background:#ffedd5; color:#9a3412; }
|
|
.stk-badge.crit-critical { background:#fee2e2; color:#991b1b; }
|
|
.stk-badge.st { background:#f1f5f9; color:#475569; }
|
|
.stk-empty { text-align:center; padding:22px 16px; color:var(--gray-500,#6b7280); font-size:.86rem; }
|
|
.stk-link { font-size:.78rem; }
|
|
/* Modale */
|
|
.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:40px 16px; }
|
|
.sm-overlay.open { display:flex; }
|
|
.sm-dialog { background:#fff; border-radius:12px; width:100%; max-width:560px; 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 input[type=text], .sm-field input[type=email], .sm-field select, .sm-field textarea { width:100%; padding:9px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.9rem; }
|
|
.sm-field textarea { min-height:64px; resize:vertical; }
|
|
.sm-row { display:flex; gap:12px; }
|
|
.sm-row > .sm-field { flex:1 1 0; }
|
|
.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); }
|
|
</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>
|
|
<div class="app-layout">
|
|
<aside class="sidebar" id="sidebar"></aside>
|
|
<main class="main-content">
|
|
<header class="content-header">
|
|
<h2 data-i18n="stk.title">Stakeholder</h2>
|
|
<div class="content-header-actions">
|
|
<button class="btn btn-primary btn-sm" onclick="stkOpen()">+ Aggiungi stakeholder</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="content-body">
|
|
<div class="stk-intro">
|
|
<strong>Mappa degli stakeholder.</strong>
|
|
Vista di sintesi di fornitori, clienti e partner della tua organizzazione, raggruppati per tipo. Riusa i dati della Supply Chain: il dettaglio e la valutazione dei fornitori restano nel modulo dedicato.
|
|
</div>
|
|
<div class="stk-note">
|
|
Ancoraggio: <strong>GV.SC-02</strong> (ruoli e responsabilità definiti verso fornitori, clienti e partner) con <strong>GV.SC-04/05/07</strong>. Gli obblighi di gestione del rischio della catena di fornitura fanno capo all'<strong>art. 24 D.Lgs. 138/2024</strong>. Strumento di supporto, non un parere legale.
|
|
</div>
|
|
|
|
<div class="stk-toolbar">
|
|
<span class="grow"></span>
|
|
<a class="btn btn-outline btn-sm" href="supply-chain.html" data-i18n="stk.manage_suppliers">Gestisci fornitori in Supply Chain</a>
|
|
</div>
|
|
|
|
<div id="stk-groups" aria-live="polite"></div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Modale stakeholder (crea/modifica) -->
|
|
<div class="sm-overlay" id="stk-modal" role="dialog" aria-modal="true" aria-labelledby="stk-modal-title">
|
|
<div class="sm-dialog">
|
|
<div class="sm-head"><h3 id="stk-modal-title">Aggiungi stakeholder</h3><button class="sm-close" type="button" onclick="stkClose()" aria-label="Chiudi">×</button></div>
|
|
<div class="sm-body">
|
|
<input type="hidden" id="stk-id">
|
|
<div class="sm-field"><label for="stk-f-name">Nome *</label><input type="text" id="stk-f-name" maxlength="255" placeholder="es. Acme Cloud S.p.A."></div>
|
|
<div class="sm-row">
|
|
<div class="sm-field"><label for="stk-f-type">Tipo</label>
|
|
<select id="stk-f-type" onchange="stkOnTypeChange()">
|
|
<option value="supplier">Fornitore</option>
|
|
<option value="customer">Cliente</option>
|
|
<option value="partner">Partner</option>
|
|
</select>
|
|
</div>
|
|
<div class="sm-field"><label for="stk-f-service">Servizio / relazione *</label><input type="text" id="stk-f-service" maxlength="255" placeholder="es. Hosting, consulenza, contratto quadro"></div>
|
|
</div>
|
|
<div class="sm-row">
|
|
<div class="sm-field"><label for="stk-f-crit">Criticità</label>
|
|
<select id="stk-f-crit">
|
|
<option value="low">Bassa</option>
|
|
<option value="medium" selected>Media</option>
|
|
<option value="high">Alta</option>
|
|
<option value="critical">Critica</option>
|
|
</select>
|
|
</div>
|
|
<div class="sm-field"><label for="stk-f-contact-name">Nome contatto</label><input type="text" id="stk-f-contact-name" maxlength="255"></div>
|
|
</div>
|
|
<div class="sm-field"><label for="stk-f-contact-email">Email contatto</label><input type="email" id="stk-f-contact-email" maxlength="255" placeholder="referente@esempio.it"></div>
|
|
<div class="sm-err" id="stk-err" role="alert"></div>
|
|
</div>
|
|
<div class="sm-foot"><button class="btn btn-outline" type="button" onclick="stkClose()">Annulla</button><button class="btn btn-primary" type="button" id="stk-save" onclick="stkSave()">Salva</button></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/js/api.js?v=20260622"></script>
|
|
<script src="/js/common.js?v=20260622"></script>
|
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
|
<script src="/js/common-bi.js?v=20260618"></script>
|
|
<script src="/js/i18n.js?v=20260622"></script>
|
|
<script src="/js/help.js?v=20260622"></script>
|
|
<script>
|
|
'use strict';
|
|
/*
|
|
* NIS2 Agile - Mappa stakeholder (A4 Fase 4.5)
|
|
* Vista di sintesi fornitori/clienti/partner: riusa la tabella suppliers (stakeholder_type).
|
|
* Lettura via api.stakeholderMap() (_acn). Creazione/etichettatura via api.createSupplier/updateSupplier
|
|
* (NON _acn -> ritornano {success,...}). Ancoraggio GV.SC-02 + GV.SC-04/05/07. Obblighi: art. 24 D.Lgs. 138/2024.
|
|
*/
|
|
let STK = { groups: { supplier: [], customer: [], partner: [] }, counts: {} };
|
|
|
|
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 TYPE_TITLE = { supplier: 'Fornitori', customer: 'Clienti', partner: 'Partner' };
|
|
const TYPE_ORDER = ['supplier', 'customer', 'partner'];
|
|
const CRIT_LABELS = { low: 'Bassa', medium: 'Media', high: 'Alta', critical: 'Critica' };
|
|
const STATUS_LABELS = { active: 'Attivo', under_review: 'In revisione', suspended: 'Sospeso', terminated: 'Cessato' };
|
|
|
|
document.addEventListener('DOMContentLoaded', async function () {
|
|
if (typeof checkAuth === 'function' && !checkAuth()) return;
|
|
if (window.I18n && I18n.init) I18n.init('it');
|
|
if (typeof loadSidebar === 'function') loadSidebar();
|
|
if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
|
|
|
|
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') stkClose(); });
|
|
const ov = el('stk-modal');
|
|
if (ov) ov.addEventListener('click', function (e) { if (e.target === ov) stkClose(); });
|
|
|
|
await stkLoad();
|
|
});
|
|
|
|
async function stkLoad() {
|
|
el('stk-groups').innerHTML = '<div class="stk-empty">Caricamento…</div>';
|
|
try {
|
|
const data = await api.stakeholderMap();
|
|
STK.groups = (data && data.groups) || { supplier: [], customer: [], partner: [] };
|
|
STK.counts = (data && data.counts) || {};
|
|
stkRender();
|
|
} catch (e) {
|
|
el('stk-groups').innerHTML = '<div class="stk-empty">Errore nel caricamento: ' + esc(e.message || e) + '</div>';
|
|
}
|
|
}
|
|
|
|
function stkRender() {
|
|
const total = TYPE_ORDER.reduce((n, t) => n + ((STK.groups[t] || []).length), 0);
|
|
if (!total) {
|
|
el('stk-groups').innerHTML = '<div class="stk-empty">Nessuno stakeholder registrato. Aggiungine uno con "+ Aggiungi stakeholder". I fornitori inseriti dalla Supply Chain compaiono automaticamente qui.</div>';
|
|
return;
|
|
}
|
|
el('stk-groups').innerHTML = TYPE_ORDER.map(stkGroupHtml).join('');
|
|
}
|
|
|
|
function stkGroupHtml(type) {
|
|
const items = STK.groups[type] || [];
|
|
const count = STK.counts[type] != null ? STK.counts[type] : items.length;
|
|
let inner;
|
|
if (!items.length) {
|
|
inner = '<div class="stk-empty">Nessun ' + esc(TYPE_TITLE[type].toLowerCase().replace(/i$/, 'e')) + ' registrato.</div>';
|
|
} else {
|
|
const rows = items.map(function (s) {
|
|
const crit = s.criticality || 'medium';
|
|
const isSupplier = (s.stakeholder_type || 'supplier') === 'supplier';
|
|
const actions = '<button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;" onclick="stkOpen(' + s.id + ')">Modifica</button>'
|
|
+ (isSupplier ? ' <a class="stk-link" href="supply-chain.html">Dettaglio in Supply Chain</a>' : '');
|
|
return '<tr>'
|
|
+ '<td><strong>' + esc(s.name) + '</strong>'
|
|
+ (s.contact_name || s.contact_email ? '<div style="font-size:.78rem;color:var(--gray-500,#6b7280);margin-top:2px;">' + esc(s.contact_name || '') + (s.contact_email ? ' · ' + esc(s.contact_email) : '') + '</div>' : '') + '</td>'
|
|
+ '<td>' + (s.service_type ? esc(s.service_type) : '—') + '</td>'
|
|
+ '<td><span class="stk-badge crit-' + esc(crit) + '">' + esc(CRIT_LABELS[crit] || crit) + '</span></td>'
|
|
+ '<td><span class="stk-badge st">' + esc(STATUS_LABELS[s.status] || s.status || '—') + '</span></td>'
|
|
+ '<td>' + actions + '</td>'
|
|
+ '</tr>';
|
|
}).join('');
|
|
inner = '<table class="stk-table"><thead><tr>'
|
|
+ '<th>Nome</th><th>Servizio / relazione</th><th>Criticità</th><th>Stato</th><th>Azioni</th>'
|
|
+ '</tr></thead><tbody>' + rows + '</tbody></table>';
|
|
}
|
|
return '<div class="stk-group">'
|
|
+ '<div class="stk-group-head"><div class="stk-group-title">' + esc(TYPE_TITLE[type]) + '<span class="stk-count">' + count + '</span></div></div>'
|
|
+ inner + '</div>';
|
|
}
|
|
|
|
function stkAllItems() {
|
|
return TYPE_ORDER.reduce((acc, t) => acc.concat(STK.groups[t] || []), []);
|
|
}
|
|
|
|
function stkOpen(id) {
|
|
const s = id ? stkAllItems().find(x => x.id === id) : null;
|
|
el('stk-modal-title').textContent = s ? 'Modifica stakeholder' : 'Aggiungi stakeholder';
|
|
el('stk-id').value = s ? s.id : '';
|
|
el('stk-f-name').value = s ? (s.name || '') : '';
|
|
el('stk-f-type').value = s ? (s.stakeholder_type || 'supplier') : 'supplier';
|
|
el('stk-f-service').value = s ? (s.service_type || '') : '';
|
|
el('stk-f-crit').value = s ? (s.criticality || 'medium') : 'medium';
|
|
el('stk-f-contact-name').value = s ? (s.contact_name || '') : '';
|
|
el('stk-f-contact-email').value = s ? (s.contact_email || '') : '';
|
|
el('stk-err').textContent = '';
|
|
el('stk-modal').classList.add('open');
|
|
setTimeout(() => el('stk-f-name').focus(), 50);
|
|
}
|
|
|
|
function stkClose() { el('stk-modal').classList.remove('open'); }
|
|
|
|
// Per clienti/partner il servizio è meno ovvio: precompila un default ragionevole se vuoto.
|
|
function stkOnTypeChange() {
|
|
const t = el('stk-f-type').value;
|
|
const svc = el('stk-f-service');
|
|
if (!svc.value.trim()) {
|
|
if (t === 'customer') svc.value = 'Cliente';
|
|
else if (t === 'partner') svc.value = 'Partner';
|
|
}
|
|
}
|
|
|
|
async function stkSave() {
|
|
const id = el('stk-id').value;
|
|
const name = el('stk-f-name').value.trim();
|
|
let service = el('stk-f-service').value.trim();
|
|
const type = el('stk-f-type').value;
|
|
if (!name) { el('stk-err').textContent = 'Il nome è obbligatorio.'; return; }
|
|
// service_type è obbligatorio lato backend: forziamo un default coerente per clienti/partner.
|
|
if (!service) {
|
|
service = type === 'customer' ? 'Cliente' : (type === 'partner' ? 'Partner' : '');
|
|
}
|
|
if (!service) { el('stk-err').textContent = 'Indica il servizio o la relazione.'; return; }
|
|
const payload = {
|
|
name: name,
|
|
stakeholder_type: type,
|
|
service_type: service,
|
|
criticality: el('stk-f-crit').value,
|
|
contact_name: el('stk-f-contact-name').value.trim(),
|
|
contact_email: el('stk-f-contact-email').value.trim()
|
|
};
|
|
const btn = el('stk-save'); btn.disabled = true;
|
|
try {
|
|
const r = id ? await api.updateSupplier(parseInt(id, 10), payload) : await api.createSupplier(payload);
|
|
if (r && r.success === false) { el('stk-err').textContent = r.message || 'Errore nel salvataggio.'; return; }
|
|
showNotification(id ? 'Stakeholder aggiornato.' : 'Stakeholder aggiunto.', 'success');
|
|
stkClose();
|
|
await stkLoad();
|
|
} catch (e) {
|
|
el('stk-err').textContent = (e && e.message) ? e.message : 'Errore nel salvataggio.';
|
|
} finally { btn.disabled = false; }
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|