Files
nis2-agile/public/js/common-bi.js
T
DevEnv nis2-agileandClaude Opus 4.8 5368b0a61c [FEAT] A4 Fase 4.2 — Competenze (skill/requisiti ruolo/gap) + alert→azione NCR+CAPA
Secondo modulo del modello relazionale A4 (docs/DESIGN_A4_RELATIONAL.md).

Backend:
- Migration 042 (docs/sql/042_competences.sql) + runner aggiornato (scripts/migrate-a4.php).
  4 tabelle additive/idempotenti: skills (org-owned o globali), role_skills
  (competenze richieste dal ruolo, UNIQUE role+skill), user_skills (possedute,
  UNIQUE org+user+skill), skill_course_map. APPLICATA su prod (nis2-db v8.0.45,
  TLSv1.3).
- CompetenceController (route 'competences'): catalog, skills CRUD, roleSkills,
  userSkills, skillCourses (map/unmap), gapGrid (richiesto−posseduto per ruolo
  con titolare), openAction. Multi-tenancy + anti-IDOR + livelli 1-5; competenze
  globali in sola lettura per gli utenti org.
- alert→azione: openAction RIUSA il workflow NCR/CAPA reale (la tabella azioni è
  capa_actions figlia di non_conformities, NON 'corrective_actions' che non
  esiste): crea NCR (source='management_review', source_entity_type='competence_gap',
  source_entity_id=role_skills.id) + capa_actions figlia. Anti-duplicato sul gap.
  Zero ALTER alle tabelle esistenti. Ancoraggio PR.AT-01/02, GV.RR-04, art.24 D.Lgs.

Frontend:
- competenze.html (4 schede: Catalogo, Requisiti per ruolo, Competenze persone,
  Gap & azioni) + js/competenze.js. Bootstrap Italia V2. CTA "Assegna corso"
  (riuso /training/assign) e "Apri non conformità".
- Voce sidebar "Competenze" (common.js + common-bi.js + BI_PAGES) + nav.competences
  i18n IT/EN. api.js: metodi comp* + assignTraining.

Help/KB/PWA:
- help.js: guida contestuale 'competences' (schede, calcolo gap, PR.AT-01/02,
  GV.RR-04, art.24 D.Lgs., disclaimer no-parere-legale) + mappa pagina.
- sw.js: nome cache nis2-shell-v1.17.0 (allineamento PWA).
- Design doc corretto (rettifica capa_actions vs corrective_actions) + avanzamento 4.1/4.2.

Cache-buster: ?v=20260618 dei 5 JS condivisi su 32 HTML. version.json 1.16.0 -> 1.17.0.
Smoke E2E su prod (fpm reale): catalogo, requisito, competenza, gap=2, openAction
(NCR+CAPA), anti-dup 409, mappatura corso — tutti verdi; dati di test ripuliti.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:01:32 +02:00

212 lines
16 KiB
JavaScript

/* ═══════════════════════════════════════════════════════════════════
* common-bi.js — Override Bootstrap Italia per la chrome (sidebar)
* ───────────────────────────────────────────────────────────────────
* PILOTA "App-pages onda B" (T5).
*
* Regole rispettate:
* - ADDITIVO: NON sovrascrive common.js. Va caricato DOPO common.js e
* ridefinisce SOLO loadSidebar() con markup Bootstrap Italia.
* - ZERO backend: riusa tutti gli helper di common.js (_loadUserInfo,
* _setupMobileToggle, _loadVersionFooter, _loadFirmBranding,
* _renderImpersonateBanner, switchLang, _getSavedLang, icon*()).
* - Stessi ID DOM della sidebar originale (sidebar-user-name,
* sidebar-user-avatar, sidebar-user-role, sidebar-org-switcher,
* sidebar-version, .lang-btn) → gli helper esistenti funzionano
* invariati senza toccare api.js.
* - Accessibilità: <nav aria-label>, link active con aria-current,
* bottoni con aria-label, focus nativo BI.
* - I link puntano alle varianti ".html" quando esistono, con
* fallback all'originale per le pagine non ancora convertite.
* ═══════════════════════════════════════════════════════════════════ */
(function () {
'use strict';
// Pagine già convertite a Bootstrap Italia in questa onda (+ login).
// Per queste la navigazione resta in contesto "-bi". Le altre puntano
// ancora all'originale (parità funzionale, nessun link rotto).
var BI_PAGES = {
'login.html': 'login.html',
'dashboard.html': 'dashboard.html',
'assessment.html': 'assessment.html',
'isms.html': 'isms.html',
'risks.html': 'risks.html',
'incidents.html': 'incidents.html',
'policies.html': 'policies.html',
'supply-chain.html': 'supply-chain.html',
'training.html': 'training.html',
'assets.html': 'assets.html',
'reports.html': 'reports.html',
'settings.html': 'settings.html',
'whistleblowing.html': 'whistleblowing.html',
'organigramma.html': 'organigramma.html',
'competenze.html': 'competenze.html'
};
function biHref(href) {
return BI_PAGES[href] || href;
}
function loadSidebarBI() {
var container = document.getElementById('sidebar');
if (!container) return;
// La pagina corrente può essere sia "x.html" sia "x.html":
// normalizziamo togliendo il suffisso -bi per l'evidenziazione active.
var raw = window.location.pathname.split('/').pop() || 'dashboard.html';
var currentBase = raw.replace(/-bi\.html$/, '.html');
// Stesso albero di navigazione di common.js (icone riusate dagli
// helper globali icon*()). label/href/i18nKey identici.
var navItems = [
{
label: 'Principale', i18nKey: 'nav.main',
items: [
{ name: 'Dashboard', href: 'dashboard.html', icon: iconGrid(), i18nKey: 'nav.dashboard' },
{ name: 'Compliance Journey', href: 'workflow.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/></svg>' },
{ name: 'Gap Analysis', href: 'assessment.html', icon: iconClipboardCheck(), i18nKey: 'nav.gap_analysis' },
{ name: 'Gap Analysis ACN', href: 'acn-gap.html', icon: iconClipboardCheck(), i18nKey: 'nav.acn_gap' },
{ name: 'Modello SGSI (ISO 27001)', href: 'isms.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>', i18nKey: 'nav.isms' }
]
},
{
label: 'Gestione', i18nKey: 'nav.management',
items: [
{ name: 'Organigramma', href: 'organigramma.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M8 2.5h4a.5.5 0 01.5.5v2a.5.5 0 01-.5.5h-1.25v2.5H15a.5.5 0 01.5.5v1.25H17a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-4a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h1.25V9.5H6v1.25H7.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-4a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H4.5V9.5a.5.5 0 01.5-.5h4.75V6H8a.5.5 0 01-.5-.5V3a.5.5 0 01.5-.5z"/></svg>', i18nKey: 'nav.org_chart' },
{ name: 'Competenze', href: 'competenze.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 11-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zM12 2a1 1 0 01.967.744L14.146 7.2 17.5 8.134a1 1 0 010 1.732l-3.354.934-1.18 4.455a1 1 0 01-1.933 0L9.854 10.8 6.5 9.866a1 1 0 010-1.732l3.354-.934 1.179-4.456A1 1 0 0112 2z"/></svg>', i18nKey: 'nav.competences' },
{ name: 'Rischi', href: 'risks.html', icon: iconShieldExclamation(), i18nKey: 'nav.risks' },
{ name: 'Incidenti', href: 'incidents.html', icon: iconBell(), i18nKey: 'nav.incidents' },
{ name: 'Policy', href: 'policies.html', icon: iconDocumentText(), i18nKey: 'nav.policies' },
{ name: 'Supply Chain', href: 'supply-chain.html', icon: iconLink(), i18nKey: 'nav.supply_chain' },
{ name: 'Segnalazioni', href: 'whistleblowing.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/></svg>' },
{ name: 'Normative', href: 'normative.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/></svg>' },
{ name: 'AI Cross-Analysis', href: 'cross-analysis.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z"/></svg>' },
{ name: 'Knowledge Base', href: 'kb.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 4.804A7.968 7.968 0 005.5 4c-1.255 0-2.443.29-3.5.804v10A7.969 7.969 0 015.5 14c1.669 0 3.218.51 4.5 1.385A7.962 7.962 0 0114.5 14c1.255 0 2.443.29 3.5.804v-10A7.968 7.968 0 0014.5 4c-1.255 0-2.443.29-3.5.804V12a1 1 0 11-2 0V4.804z"/></svg>' }
]
},
{
label: 'Operativo', i18nKey: 'nav.operations',
items: [
{ name: 'Formazione', href: 'training.html', icon: iconAcademicCap(), i18nKey: 'nav.training' },
{ name: 'Inventario', href: 'assets.html', icon: iconServer(), i18nKey: 'nav.assets' },
{ name: 'Audit & Report', href: 'reports.html', icon: iconChartBar(), i18nKey: 'nav.audit' }
]
},
{
label: 'Sistema', i18nKey: 'nav.system',
items: [
{ name: 'Guida all\'uso', href: 'guida.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg>' },
{ name: 'Impostazioni', href: 'settings.html', icon: iconCog(), i18nKey: 'nav.settings' },
{ name: 'Architettura', href: 'architecture.html', icon: iconCubeTransparent() },
{ name: 'Simulazione Demo', href: 'simulate.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd"/></svg>' },
{ name: 'Integrazioni Esterne', href: 'integrazioniext.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"/><path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"/></svg>' }
]
}
];
var html = '';
// Brand
html += '' +
'<div class="sidebar-brand">' +
'<div class="sidebar-brand-icon">' +
'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.18l7 3.12v4.7c0 4.83-3.23 9.36-7 10.57-3.77-1.21-7-5.74-7-10.57V6.3l7-3.12z"/><path d="M10 12.5l-2-2-1.41 1.41L10 15.32l5.41-5.41L14 8.5l-4 4z"/></svg>' +
'</div>' +
'<div>' +
'<h1 class="sidebar-logo-text">NIS2 Agile</h1>' +
'<span>Compliance Platform</span>' +
'</div>' +
'</div>';
// Org-switcher (popolato async per consulenti — stesso id)
html += '<div id="sidebar-org-switcher" style="display:none;padding:8px 12px;border-bottom:1px solid var(--gray-100);"></div>';
// Nav — markup BI: <nav> con aria-label, link con aria-current
html += '<nav class="sidebar-nav" aria-label="Navigazione principale">';
navItems.forEach(function (section) {
var sectionI18n = section.i18nKey ? ' data-i18n="' + section.i18nKey + '"' : '';
html += '<div class="sidebar-nav-label"' + sectionI18n + '>' + section.label + '</div>';
section.items.forEach(function (item) {
var isActive = currentBase === item.href;
var cls = isActive ? 'active' : '';
var aria = isActive ? ' aria-current="page"' : '';
var spanI18n = item.i18nKey ? ' data-i18n="' + item.i18nKey + '"' : '';
html += '<a href="' + biHref(item.href) + '" class="' + cls + '"' + aria + '>' +
item.icon + '<span' + spanI18n + '>' + item.name + '</span>' +
'</a>';
});
});
html += '</nav>';
// Footer: lang toggle + user info + version (stessi id degli helper)
html += '' +
'<div class="sidebar-footer">' +
'<div class="sidebar-lang-toggle" id="sidebar-lang-toggle" role="group" aria-label="Lingua">' +
'<button class="lang-btn ' + (_getSavedLang() === 'it' ? 'active' : '') + '" onclick="switchLang(\'it\')" title="Italiano" aria-label="Italiano">IT</button>' +
'<button class="lang-btn ' + (_getSavedLang() === 'en' ? 'active' : '') + '" onclick="switchLang(\'en\')" title="English" aria-label="English">EN</button>' +
'</div>' +
'<div class="sidebar-user">' +
'<div class="sidebar-user-avatar" id="sidebar-user-avatar" aria-hidden="true">--</div>' +
'<div class="sidebar-user-info">' +
'<div class="sidebar-user-name" id="sidebar-user-name">Caricamento...</div>' +
'<div class="sidebar-user-role" id="sidebar-user-role">Utente</div>' +
'</div>' +
'<button class="sidebar-logout-btn" onclick="api.logout()" title="Esci" aria-label="Esci dall\'applicazione">' +
'<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 001 1h6a1 1 0 100-2H4V5h5a1 1 0 100-2H3zm11.707 3.293a1 1 0 010 1.414L12.414 10l2.293 2.293a1 1 0 01-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0z" clip-rule="evenodd"/><path fill-rule="evenodd" d="M16 10a1 1 0 00-1-1H8a1 1 0 100 2h7a1 1 0 001-1z" clip-rule="evenodd"/></svg>' +
'</button>' +
'</div>' +
'<div class="sidebar-version" id="sidebar-version" title="Versione applicazione" tabindex="0" role="button"' +
' style="text-align:center; padding:6px 12px; font-size:.68rem; color:var(--gray-400); cursor:pointer; border-top:1px solid var(--gray-100); margin-top:4px;"' +
' onclick="_showVersionChangelog()" onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();_showVersionChangelog();}">' +
'v—' +
'</div>' +
'</div>';
container.innerHTML = html;
// Riuso integrale degli helper di common.js
if (typeof _loadUserInfo === 'function') _loadUserInfo();
if (typeof _setupMobileToggle === 'function') _setupMobileToggle();
if (typeof _loadVersionFooter === 'function') _loadVersionFooter();
if (typeof _loadFirmBranding === 'function') _loadFirmBranding();
if (typeof _renderImpersonateBanner === 'function') _renderImpersonateBanner();
}
// Override globale: le pagine -bi chiamano loadSidebar() come le originali.
window.loadSidebar = loadSidebarBI;
})();
// ── Avatar di prodotto: hook demo-mode (product-demo-protocol v1.0.1) ──
// Solo in presenza di ?demo=<session_id> inietta gli asset demo (no-op per utenti normali).
(function () {
try {
if (!new URLSearchParams(window.location.search).get('demo')) return;
// Auto-auth SINCRONA: il demo_jwt arriva nel fragment (#djwt=) dello spa_iframe_url.
// Lo impostiamo come token API PRIMA che la pagina chiami checkAuth() (common-bi.js gira
// prima dello script inline della pagina). Origin demo separato → nessuna pollution di sessioni reali.
try {
var h = new URLSearchParams((window.location.hash || '').replace(/^#/, ''));
var djwt = h.get('djwt');
if (djwt) {
localStorage.setItem('nis2_access_token', djwt);
localStorage.setItem('nis2_user_role', 'compliance_manager');
var orgv = null;
try {
var pl = JSON.parse(atob(djwt.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
if (pl && pl.org_id) { orgv = String(pl.org_id); localStorage.setItem('nis2_org_id', orgv); }
} catch (e2) { /* jwt opaco: org via credentials */ }
// CRITICO: api.js ha già creato l'istanza `api` leggendo localStorage VUOTO al load
// (api.js è incluso PRIMA di common-bi.js). Senza questo sync in-memory, checkAuth()
// vede api.token=null e redirige a login. Aggiorniamo l'istanza viva.
try { if (typeof api !== 'undefined' && api) { api.token = djwt; if (orgv) api.orgId = orgv; } } catch (e3) { /* noop */ }
}
} catch (e1) { /* noop */ }
var css = document.createElement('link'); css.rel = 'stylesheet'; css.href = '/css/demo.css?v=20260613'; document.head.appendChild(css);
var inject = function () {
var s1 = document.createElement('script'); s1.src = '/js/demo-selectors.js?v=20260613';
s1.onload = function () { var s2 = document.createElement('script'); s2.src = '/js/demo-mode.js?v=20260613'; document.body.appendChild(s2); };
document.body.appendChild(s1);
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', inject); else inject();
} catch (e) { /* noop */ }
})();