Files
nis2-agile/public/stakeholders.html
T
DevEnv nis2-agileandClaude Opus 4.8 42529043d3 [FEAT] ARIA puntatore guida "Mostrami dov'è" — spotlight verso il menu (Fase D)
Porting del pattern TRPG (CHANGE_aria_voice_pointer.md) sulla sidebar V2:
- public/js/aria-guide.js (nuovo): DESTINATIONS (26 voci menu → keyword IT),
  matchDestination(question), pointTo(nav,label) = overlay + spotlight pulsante +
  puntatore animato + coach-mark "Vai →"; Esc/click per chiudere; fallback navigate
  se la voce non è visibile; rispetta prefers-reduced-motion; self-contained, no inline-CSS.
- common-bi.js: data-nav="<href senza .html>" sulle voci sidebar (aggancio puntatore).
- common.js: inietta aria-guide.js dopo login; dopo ogni risposta ARIA, se la domanda
  combacia con una destinazione mostra il bottone "📍 Mostrami dov'è: <voce>".
Frontend-only, additivo. Cache-buster common.js/common-bi.js -> 20260624f.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 08:39:47 +02:00

497 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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=20260618">
<style>
.stk-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; }
.stk-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
.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:5px 12px; font-size:.82rem; }
/* Matrice Mendelow */
.mx-card { border:1px solid var(--gray-200,#e5e7eb); border-radius:12px; padding:16px 18px; margin-bottom:18px; background:#fff; }
.mx-card h3 { margin:0 0 4px; font-size:1.02rem; }
.mx-sub { font-size:.8rem; color:var(--gray-500,#6b7280); margin-bottom:12px; }
.mx-wrap { display:flex; gap:20px; flex-wrap:wrap; align-items:flex-start; }
.mx-chart { flex:1 1 460px; min-width:300px; max-width:560px; }
.mx-side { flex:1 1 220px; min-width:200px; }
.mx-svg { width:100%; height:auto; display:block; }
.mx-dot { cursor:pointer; }
.mx-dot:focus { outline:2px solid #1d4ed8; outline-offset:2px; }
.mx-legend { display:flex; flex-direction:column; gap:6px; margin-top:6px; }
.mx-legend .it { display:flex; align-items:center; gap:8px; font-size:.8rem; }
.mx-sw { width:14px; height:14px; border-radius:4px; flex:0 0 auto; }
.mx-unrated { margin-top:6px; }
.mx-unrated h4 { font-size:.82rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); margin:0 0 6px; }
.mx-unrated ul { list-style:none; margin:0; padding:0; max-height:240px; overflow:auto; }
.mx-unrated li { font-size:.84rem; padding:5px 8px; border:1px solid var(--gray-100,#f3f4f6); border-radius:7px; margin-bottom:5px; cursor:pointer; display:flex; justify-content:space-between; gap:8px; }
.mx-unrated li:hover { background:var(--gray-50,#f9fafb); }
/* Registro */
.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:.72rem; 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; white-space:nowrap; }
.stk-badge.code { background:#f1f5f9; color:#475569; font-family:monospace; }
.stk-badge.q1 { background:#fef3c7; color:#92400e; }
.stk-badge.q2 { background:#fee2e2; color:#991b1b; }
.stk-badge.q3 { background:#dbeafe; color:#1e40af; }
.stk-badge.q4 { background:#f3f4f6; color:#374151; }
.stk-badge.na { background:#f3f4f6; color:#9ca3af; }
.stk-pi { font-family:monospace; font-size:.82rem; white-space:nowrap; }
.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: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=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; background:#fff; }
.sm-field select[multiple] { min-height:108px; }
.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>
<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-outline btn-sm" onclick="stkTypeOpen()">+ Aggiungi tipo</button>
<button class="btn btn-primary btn-sm" onclick="stkOpen()">+ Aggiungi stakeholder</button>
</div>
</header>
<div class="content-body">
<div class="stk-intro">
<strong>Mappatura e gestione degli stakeholder.</strong>
Registra gli stakeholder interni ed esterni scegliendo il tipo dall'elenco (puoi aggiungerne di nuovi),
collegali all'organigramma (interni) o a un fornitore della Supply Chain (esterni) e alle procedure,
e valuta <em>potere</em> e <em>interesse</em> (0–5): il sistema li colloca automaticamente sulla matrice.
</div>
<div class="stk-note">
Ancoraggio: <strong>GV.SC-02</strong> (ruoli e responsabilità verso fornitori, clienti e partner) —
obbligo di gestione del rischio della catena di fornitura ai sensi dell'<strong>art. 24 D.Lgs. 138/2024</strong>.
La <strong>matrice di Mendelow</strong> (potere/interesse) è una <strong>buona prassi</strong> di gestione,
<strong>non un obbligo NIS2</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">Gestisci fornitori in Supply Chain</a>
</div>
<!-- Matrice di Mendelow -->
<div class="mx-card" id="mx-card">
<h3>Matrice di Mendelow <span style="font-weight:400;font-size:.8rem;color:#6b7280;">(buona prassi · x = interesse, y = potere)</span></h3>
<div class="mx-sub">Ogni stakeholder valutato è collocato nel quadrante in base a potere e interesse. Clicca un punto per modificarlo.</div>
<div class="mx-wrap">
<div class="mx-chart" id="mx-chart"></div>
<div class="mx-side">
<div class="mx-legend" id="mx-legend"></div>
<div class="mx-unrated" id="mx-unrated"></div>
</div>
</div>
</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">&times;</button></div>
<div class="sm-body">
<input type="hidden" id="stk-id">
<div class="sm-row">
<div class="sm-field"><label for="stk-f-type">Tipo stakeholder *</label>
<select id="stk-f-type" onchange="stkOnTypeChange()"></select>
</div>
<div class="sm-field"><label for="stk-f-name">Nome / denominazione *</label><input type="text" id="stk-f-name" maxlength="255" placeholder="es. Mario Rossi · Acme S.p.A."></div>
</div>
<div class="sm-field" id="stk-wrap-role">
<label for="stk-f-role">Collegamento all'organigramma <span class="hint">(interni)</span></label>
<select id="stk-f-role"><option value="">— Nessuno —</option></select>
</div>
<div class="sm-field hidden" id="stk-wrap-supplier">
<label for="stk-f-supplier">Collegamento a un fornitore <span class="hint">(esterni · dalla Supply Chain)</span></label>
<select id="stk-f-supplier"><option value="">— Nessuno —</option></select>
</div>
<div class="sm-row">
<div class="sm-field"><label for="stk-f-power">Potere sull'azienda (0–5)</label>
<select id="stk-f-power"></select>
</div>
<div class="sm-field"><label for="stk-f-interest">Interesse sull'azienda (0–5)</label>
<select id="stk-f-interest"></select>
</div>
</div>
<div class="sm-field">
<label for="stk-f-procs">Procedure collegate <span class="hint">(Ctrl/Cmd per selezione multipla)</span></label>
<select id="stk-f-procs" multiple></select>
</div>
<div class="sm-row">
<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 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>
<div class="sm-field"><label for="stk-f-notes">Note</label><textarea id="stk-f-notes" maxlength="2000"></textarea></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>
<!-- Modale aggiungi tipo -->
<div class="sm-overlay" id="stk-type-modal" role="dialog" aria-modal="true" aria-labelledby="stk-type-title">
<div class="sm-dialog" style="max-width:480px;">
<div class="sm-head"><h3 id="stk-type-title">Aggiungi tipo stakeholder</h3><button class="sm-close" type="button" onclick="stkTypeClose()" aria-label="Chiudi">&times;</button></div>
<div class="sm-body">
<p style="font-size:.84rem;color:#6b7280;margin:0 0 12px;">Il nuovo tipo riceverà automaticamente il codice successivo (proseguendo da <code>Stak.30</code>). I 30 tipi di sistema non sono modificabili.</p>
<div class="sm-field"><label for="stk-t-tipo">Categoria *</label>
<select id="stk-t-tipo"><option value="Interno">Interno</option><option value="Esterno" selected>Esterno</option></select>
</div>
<div class="sm-field"><label for="stk-t-descr">Descrizione *</label><textarea id="stk-t-descr" maxlength="1000" placeholder="es. Comunità accademica: università e centri di ricerca partner."></textarea></div>
<div class="sm-err" id="stk-t-err" role="alert"></div>
</div>
<div class="sm-foot"><button class="btn btn-outline" type="button" onclick="stkTypeClose()">Annulla</button><button class="btn btn-primary" type="button" id="stk-t-save" onclick="stkTypeSave()">Salva</button></div>
</div>
</div>
<script src="/js/api.js?v=20260631"></script>
<script src="/js/common.js?v=20260624f"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script src="/js/common-bi.js?v=20260624f"></script>
<script src="/js/i18n.js?v=20260631"></script>
<script src="/js/help.js?v=20260624e"></script>
<script>
'use strict';
/*
* NIS2 Agile - Registro Stakeholder + matrice di Mendelow (Epic C / C5.1)
* Tabella dedicata `stakeholders` (NON suppliers): interni -> org_roles,
* esterni -> link opzionale a un supplier. Doppia valutazione potere/interesse
* 0-5 -> quadrante calcolato lato API (cfg_stakeholder_quadrants). Procedure m2m.
* Ancoraggio GV.SC-02 (obbligo). Matrice di Mendelow = buona prassi, non obbligo.
*/
let STK = { list: [], quadrants: [], types: [], pickers: { org_roles: [], policies: [], suppliers: [] } };
// Stile per quadrante (codici dal seed: Q1 keep satisfied, Q2 manage closely, Q3 keep informed, Q4 monitor)
const QSTYLE = {
Q1: { fill:'#fef3c7', stroke:'#f59e0b', dot:'#d97706', cls:'q1' },
Q2: { fill:'#fee2e2', stroke:'#ef4444', dot:'#dc2626', cls:'q2' },
Q3: { fill:'#dbeafe', stroke:'#3b82f6', dot:'#2563eb', cls:'q3' },
Q4: { fill:'#f3f4f6', stroke:'#9ca3af', dot:'#6b7280', cls:'q4' },
};
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;'); }
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(); stkTypeClose(); } });
['stk-modal','stk-type-modal'].forEach(function (mid) {
const ov = el(mid);
if (ov) ov.addEventListener('click', function (e) { if (e.target === ov) ov.classList.remove('open'); });
});
// Popola le select dei punteggi 0-5
['stk-f-power','stk-f-interest'].forEach(function (sid) {
let html = '<option value="">— non valutato —</option>';
for (let i = 0; i <= 5; i++) html += '<option value="' + i + '">' + i + '</option>';
el(sid).innerHTML = html;
});
await stkLoadAll();
});
async function stkLoadAll() {
el('stk-groups').innerHTML = '<div class="stk-empty">Caricamento…</div>';
try {
const [list, types, pickers] = await Promise.all([api.stkList(), api.stkTypes(), api.stkPickers()]);
STK.list = (list && list.stakeholders) || [];
STK.quadrants = (list && list.quadrants) || [];
STK.types = (types && types.types) || [];
STK.pickers = (pickers) || { org_roles: [], policies: [], suppliers: [] };
stkFillFormSelects();
renderMatrix();
renderRegistry();
} catch (e) {
el('stk-groups').innerHTML = '<div class="stk-empty">Errore nel caricamento: ' + esc(e.message || e) + '</div>';
}
}
// ── Selettori del form (tipo / organigramma / fornitori / procedure) ──
function stkFillFormSelects() {
// Tipo (optgroup Interni/Esterni)
const interni = STK.types.filter(t => t.tipo === 'Interno');
const esterni = STK.types.filter(t => t.tipo === 'Esterno');
const optsFor = arr => arr.map(t => '<option value="' + escAttr(t.code) + '">' + esc(t.code) + ' — ' + esc((t.descr || '').slice(0, 70)) + '</option>').join('');
el('stk-f-type').innerHTML =
'<optgroup label="Interni">' + optsFor(interni) + '</optgroup>' +
'<optgroup label="Esterni">' + optsFor(esterni) + '</optgroup>';
el('stk-f-role').innerHTML = '<option value="">— Nessuno —</option>' +
(STK.pickers.org_roles || []).map(r => '<option value="' + r.id + '">' + esc(r.role_name) + '</option>').join('');
el('stk-f-supplier').innerHTML = '<option value="">— Nessuno —</option>' +
(STK.pickers.suppliers || []).map(s => '<option value="' + s.id + '">' + esc(s.name) + '</option>').join('');
el('stk-f-procs').innerHTML =
(STK.pickers.policies || []).map(p => '<option value="' + p.id + '">' + esc(p.title) + '</option>').join('') ||
'<option value="" disabled>Nessuna procedura disponibile</option>';
}
function tipoOfCode(code) {
const t = STK.types.find(x => x.code === code);
return t ? t.tipo : null;
}
// ── Matrice di Mendelow (SVG dipendency-free) ──
function renderMatrix() {
const PADX = 64, PADTOP = 16, S = 360, W = PADX + S + 24, H = PADTOP + S + 52;
const quads = STK.quadrants.length ? STK.quadrants
: [{code:'Q1',power_min:3,power_max:5,interest_min:0,interest_max:2},
{code:'Q2',power_min:3,power_max:5,interest_min:3,interest_max:5},
{code:'Q3',power_min:0,power_max:2,interest_min:3,interest_max:5},
{code:'Q4',power_min:0,power_max:2,interest_min:0,interest_max:2}];
const px = v => PADX + (v / 5) * S; // interesse -> x
const py = v => PADTOP + (1 - v / 5) * S; // potere -> y (invertito)
let svg = '<svg class="mx-svg" viewBox="0 0 ' + W + ' ' + H + '" role="img" aria-label="Matrice di Mendelow potere/interesse">';
// Rettangoli dei quadranti
quads.forEach(function (q) {
const x0 = q.interest_min <= 2 ? PADX : PADX + S / 2;
const x1 = q.interest_max <= 2 ? PADX + S / 2 : PADX + S;
const y0 = q.power_max >= 3 ? PADTOP : PADTOP + S / 2; // alto
const y1 = q.power_min >= 3 ? PADTOP + S / 2 : PADTOP + S; // basso
const st = QSTYLE[q.code] || QSTYLE.Q4;
svg += '<rect x="' + x0 + '" y="' + y0 + '" width="' + (x1 - x0) + '" height="' + (y1 - y0) + '" fill="' + st.fill + '" stroke="' + st.stroke + '" stroke-width="1" opacity="0.55"/>';
const lbl = (q.label || q.code).replace(/\s*\(.*\)/, '');
svg += '<text x="' + (x0 + 8) + '" y="' + (y0 + 16) + '" font-size="11" font-weight="700" fill="#374151">' + esc(q.code) + '</text>';
svg += '<text x="' + (x0 + 8) + '" y="' + (y0 + 30) + '" font-size="10" fill="#4b5563">' + esc(lbl.slice(0, 26)) + '</text>';
});
// Assi
svg += '<line x1="' + PADX + '" y1="' + PADTOP + '" x2="' + PADX + '" y2="' + (PADTOP + S) + '" stroke="#9ca3af" stroke-width="1.5"/>';
svg += '<line x1="' + PADX + '" y1="' + (PADTOP + S) + '" x2="' + (PADX + S) + '" y2="' + (PADTOP + S) + '" stroke="#9ca3af" stroke-width="1.5"/>';
// Etichette assi
svg += '<text x="' + (PADX + S / 2) + '" y="' + (PADTOP + S + 40) + '" font-size="12" font-weight="700" fill="#374151" text-anchor="middle">Interesse →</text>';
svg += '<text transform="translate(16,' + (PADTOP + S / 2) + ') rotate(-90)" font-size="12" font-weight="700" fill="#374151" text-anchor="middle">Potere →</text>';
svg += '<text x="' + PADX + '" y="' + (PADTOP + S + 18) + '" font-size="10" fill="#6b7280" text-anchor="middle">0</text>';
svg += '<text x="' + (PADX + S) + '" y="' + (PADTOP + S + 18) + '" font-size="10" fill="#6b7280" text-anchor="middle">5</text>';
svg += '<text x="' + (PADX - 10) + '" y="' + (PADTOP + S) + '" font-size="10" fill="#6b7280" text-anchor="end">0</text>';
svg += '<text x="' + (PADX - 10) + '" y="' + (PADTOP + 6) + '" font-size="10" fill="#6b7280" text-anchor="end">5</text>';
// Punti (con jitter deterministico per celle affollate)
const rated = STK.list.filter(s => s.rated);
const cellCount = {}, cellIdx = {};
rated.forEach(s => { const k = s.power + '_' + s.interest; cellCount[k] = (cellCount[k] || 0) + 1; });
rated.forEach(function (s) {
const k = s.power + '_' + s.interest;
const n = cellCount[k]; const i = (cellIdx[k] = (cellIdx[k] || 0) + 1) - 1;
let ox = 0, oy = 0;
if (n > 1) { const ang = (2 * Math.PI * i) / n; const rad = 9; ox = Math.cos(ang) * rad; oy = Math.sin(ang) * rad; }
const cx = px(s.interest) + ox, cy = py(s.power) + oy;
const st = QSTYLE[s.quadrant_code] || QSTYLE.Q4;
const tip = s.name + ' (' + s.stak_code + ') · Potere ' + s.power + ' / Interesse ' + s.interest + ' · ' + (s.quadrant_label || '');
svg += '<circle class="mx-dot" tabindex="0" role="button" cx="' + cx.toFixed(1) + '" cy="' + cy.toFixed(1) + '" r="7" fill="' + st.dot + '" stroke="#fff" stroke-width="1.5" '
+ 'onclick="stkOpen(' + s.id + ')" onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();stkOpen(' + s.id + ');}">'
+ '<title>' + escAttr(tip) + '</title></circle>';
});
svg += '</svg>';
el('mx-chart').innerHTML = svg;
// Legenda
el('mx-legend').innerHTML = (STK.quadrants.length ? STK.quadrants : quads).map(function (q) {
const st = QSTYLE[q.code] || QSTYLE.Q4;
const n = STK.list.filter(s => s.quadrant_code === q.code).length;
return '<div class="it"><span class="mx-sw" style="background:' + st.dot + '"></span><span><strong>' + esc(q.code) + '</strong> ' + esc((q.label || '')) + ' · ' + n + '</span></div>';
}).join('');
// Non valutati
const unrated = STK.list.filter(s => !s.rated);
el('mx-unrated').innerHTML = unrated.length
? '<h4>Da valutare (' + unrated.length + ')</h4><ul>' + unrated.map(s =>
'<li onclick="stkOpen(' + s.id + ')"><span>' + esc(s.name) + '</span><span class="stk-badge code">' + esc(s.stak_code) + '</span></li>').join('') + '</ul>'
: '';
}
// ── Registro per categoria ──
function renderRegistry() {
if (!STK.list.length) {
el('stk-groups').innerHTML = '<div class="stk-empty">Nessuno stakeholder registrato. Aggiungine uno con "+ Aggiungi stakeholder".</div>';
return;
}
const groups = [
{ tipo: 'Interno', title: 'Stakeholder interni' },
{ tipo: 'Esterno', title: 'Stakeholder esterni' },
];
el('stk-groups').innerHTML = groups.map(function (g) {
const items = STK.list.filter(s => s.tipo === g.tipo);
let inner;
if (!items.length) {
inner = '<div class="stk-empty">Nessuno stakeholder ' + (g.tipo === 'Interno' ? 'interno' : 'esterno') + ' registrato.</div>';
} else {
const rows = items.map(function (s) {
const q = s.quadrant_code ? '<span class="stk-badge ' + (QSTYLE[s.quadrant_code] || QSTYLE.Q4).cls + '">' + esc(s.quadrant_code) + ' · ' + esc((s.quadrant_label || '').replace(/\s*\(.*\)/, '')) + '</span>' : '<span class="stk-badge na">da valutare</span>';
const pi = s.rated ? '<span class="stk-pi">P' + s.power + ' · I' + s.interest + '</span>' : '<span class="stk-pi" style="color:#9ca3af">—</span>';
const link = s.org_role_name ? ('👤 ' + esc(s.org_role_name)) : (s.supplier_name ? ('🏢 ' + esc(s.supplier_name)) : '—');
const procs = (s.policy_ids && s.policy_ids.length) ? (s.policy_ids.length + ' proc.') : '—';
return '<tr>'
+ '<td><span class="stk-badge code">' + esc(s.stak_code) + '</span></td>'
+ '<td><strong>' + esc(s.name) + '</strong>'
+ (s.contact_name || s.contact_email ? '<div style="font-size:.78rem;color:#6b7280;margin-top:2px;">' + esc(s.contact_name || '') + (s.contact_email ? ' · ' + esc(s.contact_email) : '') + '</div>' : '') + '</td>'
+ '<td>' + pi + '</td>'
+ '<td>' + q + '</td>'
+ '<td style="font-size:.82rem;">' + link + '</td>'
+ '<td style="font-size:.82rem;">' + procs + '</td>'
+ '<td style="white-space:nowrap;">'
+ '<button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;" onclick="stkOpen(' + s.id + ')">Modifica</button> '
+ '<button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;color:#b91c1c;border-color:#fecaca;" onclick="stkDelete(' + s.id + ')">Elimina</button>'
+ '</td></tr>';
}).join('');
inner = '<table class="stk-table"><thead><tr>'
+ '<th>Codice</th><th>Nome</th><th>Pot./Int.</th><th>Quadrante</th><th>Collegamento</th><th>Procedure</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(g.title)
+ '<span class="stk-count">' + items.length + '</span></div></div>' + inner + '</div>';
}).join('');
}
// ── Modale stakeholder ──
function stkOpen(id) {
const s = id ? STK.list.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-type').value = s ? s.stak_code : (el('stk-f-type').querySelector('option') ? el('stk-f-type').querySelector('option').value : '');
el('stk-f-name').value = s ? (s.name || '') : '';
el('stk-f-role').value = s && s.org_role_id ? String(s.org_role_id) : '';
el('stk-f-supplier').value = s && s.supplier_id ? String(s.supplier_id) : '';
el('stk-f-power').value = s && s.power != null ? String(s.power) : '';
el('stk-f-interest').value = s && s.interest != null ? String(s.interest) : '';
el('stk-f-contact-name').value = s ? (s.contact_name || '') : '';
el('stk-f-contact-email').value = s ? (s.contact_email || '') : '';
el('stk-f-notes').value = s ? (s.notes || '') : '';
// procedure multi-select
const sel = el('stk-f-procs'); const have = (s && s.policy_ids) || [];
Array.prototype.forEach.call(sel.options, o => { o.selected = have.indexOf(parseInt(o.value, 10)) !== -1; });
el('stk-err').textContent = '';
stkOnTypeChange();
el('stk-modal').classList.add('open');
setTimeout(() => el('stk-f-name').focus(), 50);
}
function stkClose() { el('stk-modal').classList.remove('open'); }
// Mostra organigramma per interni, fornitore per esterni
function stkOnTypeChange() {
const tipo = tipoOfCode(el('stk-f-type').value);
const isInt = tipo === 'Interno';
el('stk-wrap-role').classList.toggle('hidden', !isInt);
el('stk-wrap-supplier').classList.toggle('hidden', isInt);
if (isInt) el('stk-f-supplier').value = ''; else el('stk-f-role').value = '';
}
async function stkSave() {
const id = el('stk-id').value;
const code = el('stk-f-type').value;
const name = el('stk-f-name').value.trim();
if (!code) { el('stk-err').textContent = 'Seleziona un tipo.'; return; }
if (!name) { el('stk-err').textContent = 'Il nome è obbligatorio.'; return; }
const tipo = tipoOfCode(code);
const procs = Array.prototype.filter.call(el('stk-f-procs').options, o => o.selected).map(o => parseInt(o.value, 10)).filter(Boolean);
const payload = {
stak_code: code,
name: name,
org_role_id: tipo === 'Interno' && el('stk-f-role').value ? parseInt(el('stk-f-role').value, 10) : null,
supplier_id: tipo === 'Esterno' && el('stk-f-supplier').value ? parseInt(el('stk-f-supplier').value, 10) : null,
power: el('stk-f-power').value !== '' ? parseInt(el('stk-f-power').value, 10) : null,
interest: el('stk-f-interest').value !== '' ? parseInt(el('stk-f-interest').value, 10) : null,
policy_ids: procs,
contact_name: el('stk-f-contact-name').value.trim(),
contact_email: el('stk-f-contact-email').value.trim(),
notes: el('stk-f-notes').value.trim(),
};
const btn = el('stk-save'); btn.disabled = true;
try {
if (id) await api.stkUpdate(parseInt(id, 10), payload); else await api.stkCreate(payload);
showNotification(id ? 'Stakeholder aggiornato.' : 'Stakeholder aggiunto.', 'success');
stkClose();
await stkLoadAll();
} catch (e) {
el('stk-err').textContent = (e && e.message) ? e.message : 'Errore nel salvataggio.';
} finally { btn.disabled = false; }
}
async function stkDelete(id) {
const s = STK.list.find(x => x.id === id);
if (!confirm('Eliminare lo stakeholder "' + (s ? s.name : '') + '"?')) return;
try {
await api.stkDelete(id);
showNotification('Stakeholder eliminato.', 'success');
await stkLoadAll();
} catch (e) { showNotification((e && e.message) || 'Errore nell\'eliminazione.', 'error'); }
}
// ── Modale aggiungi tipo ──
function stkTypeOpen() { el('stk-t-err').textContent = ''; el('stk-t-descr').value = ''; el('stk-type-modal').classList.add('open'); setTimeout(() => el('stk-t-descr').focus(), 50); }
function stkTypeClose() { el('stk-type-modal').classList.remove('open'); }
async function stkTypeSave() {
const tipo = el('stk-t-tipo').value;
const descr = el('stk-t-descr').value.trim();
if (!descr) { el('stk-t-err').textContent = 'La descrizione è obbligatoria.'; return; }
const btn = el('stk-t-save'); btn.disabled = true;
try {
const r = await api.stkAddType({ tipo: tipo, descr: descr });
showNotification('Tipo aggiunto: ' + (r && r.code ? r.code : ''), 'success');
stkTypeClose();
const types = await api.stkTypes();
STK.types = (types && types.types) || STK.types;
stkFillFormSelects();
} catch (e) {
el('stk-t-err').textContent = (e && e.message) ? e.message : 'Errore nel salvataggio.';
} finally { btn.disabled = false; }
}
</script>
</body>
</html>