[CLEANUP] Unica UI: rimossi 28 duplicati *-bi.html (orfani post-swap Bootstrap Italia, 0 riferimenti). Le pagine ufficiali (nomi senza -bi) SONO la UI V2 BI

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-15 17:17:34 +02:00
co-authored by Claude Opus 4.8
parent 717f9ac396
commit 3c5184bbae
28 changed files with 0 additions and 18692 deletions
-411
View File
@@ -1,411 +0,0 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Pannello Amministrazione (BI) - NIS2 Agile</title>
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
cosi' style.css resta autorevole per i widget renderizzati via JS -> parita' funzionale.
Pagina in /admin/: gli asset usano path ASSOLUTI (/vendor, /js, /css) serviti dal web root. -->
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="/css/style.css?v=20260613b">
<style>
.admin-stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.admin-stat-card {
background: var(--card-bg);
border-radius: var(--border-radius-lg);
box-shadow: var(--card-shadow);
padding: 20px;
transition: box-shadow var(--transition), transform var(--transition);
}
.admin-stat-card:hover {
box-shadow: var(--card-shadow-hover);
transform: translateY(-2px);
}
.admin-stat-label {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--gray-500);
margin-bottom: 8px;
}
.admin-stat-value {
font-size: 2rem;
font-weight: 800;
color: var(--gray-900);
line-height: 1;
}
.admin-stat-icon {
width: 40px;
height: 40px;
border-radius: var(--border-radius);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
}
.admin-stat-icon svg {
width: 20px;
height: 20px;
}
.admin-stat-icon.blue { background: var(--primary-bg); color: var(--primary); }
.admin-stat-icon.green { background: var(--secondary-bg); color: var(--secondary); }
.admin-stat-icon.orange { background: var(--warning-bg); color: #d97706; }
.admin-stat-icon.red { background: var(--danger-bg); color: var(--danger); }
.admin-stat-icon.purple { background: #f3e8ff; color: #7c3aed; }
/* Le icone BI usano fill=currentColor: garantiamo che il colore del wrapper sia ereditato. */
.admin-stat-icon svg.icon { fill: currentColor; width: 20px; height: 20px; }
.chart-container {
padding: 24px;
}
.bar-chart {
display: flex;
align-items: flex-end;
gap: 32px;
height: 200px;
padding: 0 20px;
border-bottom: 2px solid var(--gray-200);
}
.bar-chart-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
height: 100%;
justify-content: flex-end;
}
.bar-chart-bar {
width: 60px;
border-radius: 6px 6px 0 0;
transition: height 0.8s ease;
min-height: 4px;
position: relative;
}
.bar-chart-bar.free { background: var(--gray-400); }
.bar-chart-bar.professional { background: var(--primary); }
.bar-chart-bar.enterprise { background: #7c3aed; }
.bar-chart-value {
font-size: 0.875rem;
font-weight: 700;
color: var(--gray-700);
margin-bottom: 8px;
}
.bar-chart-label {
font-size: 0.75rem;
color: var(--gray-500);
margin-top: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.quick-link-card {
display: flex;
align-items: center;
gap: 16px;
padding: 20px 24px;
background: var(--card-bg);
border: 1px solid var(--gray-200);
border-radius: var(--border-radius-lg);
cursor: pointer;
transition: all var(--transition-fast);
text-decoration: none;
color: inherit;
}
.quick-link-card:hover {
border-color: var(--primary);
background: var(--primary-bg);
color: var(--primary);
transform: translateY(-2px);
box-shadow: var(--card-shadow-hover);
}
.quick-link-card:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.quick-link-icon {
width: 48px;
height: 48px;
border-radius: var(--border-radius);
background: var(--primary-bg);
color: var(--primary);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.quick-link-icon svg {
width: 24px;
height: 24px;
fill: currentColor;
}
.quick-link-info h4 {
font-size: 0.9375rem;
font-weight: 600;
color: var(--gray-800);
margin-bottom: 2px;
}
.quick-link-info p {
font-size: 0.8125rem;
color: var(--gray-500);
}
.quick-link-card:hover .quick-link-info h4 {
color: var(--primary);
}
.quick-links-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
@media (max-width: 1024px) {
.admin-stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.admin-stats-grid {
grid-template-columns: 1fr;
}
.quick-links-grid {
grid-template-columns: 1fr;
}
.bar-chart {
gap: 16px;
}
.bar-chart-bar {
width: 40px;
}
}
</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">
<h1 class="h2">Pannello Amministrazione</h1>
<div class="content-header-actions">
<span class="badge badge-danger">Super Admin</span>
</div>
</header>
<div class="content-body">
<!-- Stats Row 1 -->
<div class="admin-stats-grid" id="stats-row-1">
<div class="admin-stat-card">
<div class="admin-stat-icon blue" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-pa"></use></svg>
</div>
<div class="admin-stat-label" id="lbl-total-orgs">Totale Organizzazioni</div>
<div class="admin-stat-value" id="stat-total-orgs" aria-labelledby="lbl-total-orgs">--</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-icon green" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-check-circle"></use></svg>
</div>
<div class="admin-stat-label" id="lbl-active-orgs">Organizzazioni Attive</div>
<div class="admin-stat-value" id="stat-active-orgs" aria-labelledby="lbl-active-orgs">--</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-icon blue" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg>
</div>
<div class="admin-stat-label" id="lbl-total-users">Totale Utenti</div>
<div class="admin-stat-value" id="stat-total-users" aria-labelledby="lbl-total-users">--</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-icon green" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg>
</div>
<div class="admin-stat-label" id="lbl-active-users">Utenti Attivi</div>
<div class="admin-stat-value" id="stat-active-users" aria-labelledby="lbl-active-users">--</div>
</div>
</div>
<!-- Stats Row 2 -->
<div class="admin-stats-grid" id="stats-row-2">
<div class="admin-stat-card">
<div class="admin-stat-icon purple" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-check"></use></svg>
</div>
<div class="admin-stat-label" id="lbl-assessments">Assessment Completati</div>
<div class="admin-stat-value" id="stat-assessments" aria-labelledby="lbl-assessments">--</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-icon orange" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-warning-circle"></use></svg>
</div>
<div class="admin-stat-label" id="lbl-incidents">Incidenti Aperti</div>
<div class="admin-stat-value" id="stat-incidents" aria-labelledby="lbl-incidents">--</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-icon red" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-error"></use></svg>
</div>
<div class="admin-stat-label" id="lbl-risks">Totale Rischi</div>
<div class="admin-stat-value" id="stat-risks" aria-labelledby="lbl-risks">--</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-icon purple" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-software"></use></svg>
</div>
<div class="admin-stat-label" id="lbl-ai">Interazioni AI</div>
<div class="admin-stat-value" id="stat-ai" aria-labelledby="lbl-ai">--</div>
</div>
</div>
<!-- Plans Distribution -->
<div class="card mb-24">
<div class="card-header">
<h3>Distribuzione Piani</h3>
</div>
<div class="chart-container">
<div class="bar-chart" id="plans-chart" role="img" aria-label="Grafico a barre: distribuzione delle organizzazioni per piano di sottoscrizione">
<div class="spinner" style="margin:40px auto;"></div>
</div>
</div>
</div>
<!-- Quick Links -->
<div class="card">
<div class="card-header">
<h3>Gestione Piattaforma</h3>
</div>
<div class="card-body">
<nav class="quick-links-grid" aria-label="Collegamenti rapidi gestione piattaforma">
<a href="/admin/organizations-bi.html" class="quick-link-card">
<div class="quick-link-icon" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-pa"></use></svg>
</div>
<div class="quick-link-info">
<h4>Gestisci Organizzazioni</h4>
<p>Visualizza e gestisci tutte le organizzazioni registrate sulla piattaforma.</p>
</div>
</a>
<a href="/admin/users-bi.html" class="quick-link-card">
<div class="quick-link-icon" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg>
</div>
<div class="quick-link-info">
<h4>Gestisci Utenti</h4>
<p>Visualizza e gestisci tutti gli utenti registrati sulla piattaforma.</p>
</div>
</a>
</nav>
</div>
</div>
</div>
</main>
</div>
<script src="/js/api.js?v=20260613"></script>
<script src="/js/common.js?v=20260615"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). -->
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script>
if (window.bootstrap && bootstrap.loadFonts) {
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
}
</script>
<script src="/js/common-bi.js?v=20260615"></script>
<script src="/js/i18n.js?v=20260615"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
I18n.init();
// ── Admin check ──────────────────────────────────────────
(async function checkAdmin() {
try {
const me = await api.getMe();
if (!me.success || !me.data || me.data.role !== 'super_admin') {
showNotification('Accesso non autorizzato. Solo gli amministratori possono accedere a questa pagina.', 'error');
setTimeout(() => { window.location.href = '/dashboard.html'; }, 1500);
return;
}
loadAdminStats();
} catch (e) {
window.location.href = '/dashboard.html';
}
})();
// ── Load Stats ───────────────────────────────────────────
async function loadAdminStats() {
try {
const result = await api.request('GET', '/admin/stats');
if (result.success && result.data) {
const d = result.data;
document.getElementById('stat-total-orgs').textContent = d.total_organizations ?? 0;
document.getElementById('stat-active-orgs').textContent = d.active_organizations ?? 0;
document.getElementById('stat-total-users').textContent = d.total_users ?? 0;
document.getElementById('stat-active-users').textContent = d.active_users ?? 0;
document.getElementById('stat-assessments').textContent = d.completed_assessments ?? 0;
document.getElementById('stat-incidents').textContent = d.open_incidents ?? 0;
document.getElementById('stat-risks').textContent = d.total_risks ?? 0;
document.getElementById('stat-ai').textContent = d.ai_interactions ?? 0;
renderPlansChart(d.plans_distribution || []);
} else {
showNotification('Impossibile caricare le statistiche.', 'error');
}
} catch (e) {
console.error('Errore caricamento stats:', e);
showNotification('Errore di connessione.', 'error');
}
}
// ── Plans Chart ──────────────────────────────────────────
function renderPlansChart(distribution) {
const container = document.getElementById('plans-chart');
const planLabels = { free: 'Free', professional: 'Professional', enterprise: 'Enterprise' };
const plans = { free: 0, professional: 0, enterprise: 0 };
distribution.forEach(p => {
const key = (p.subscription_plan || 'free').toLowerCase();
if (plans.hasOwnProperty(key)) {
plans[key] = parseInt(p.count) || 0;
}
});
const maxVal = Math.max(1, ...Object.values(plans));
let html = '';
for (const [key, count] of Object.entries(plans)) {
const heightPct = (count / maxVal) * 100;
html += `
<div class="bar-chart-item">
<div class="bar-chart-value">${count}</div>
<div class="bar-chart-bar ${key}" style="height:${Math.max(heightPct, 3)}%;"></div>
<div class="bar-chart-label">${planLabels[key]}</div>
</div>`;
}
container.innerHTML = html;
}
</script>
</body>
</html>
-328
View File
@@ -1,328 +0,0 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Gestione Organizzazioni (BI) - NIS2 Agile</title>
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
via JS (tabella org/paginazione/badge) -> parita' funzionale assoluta. BI veste la chrome.
Path relativi ../ perche' la pagina vive sotto public/admin/. -->
<link rel="stylesheet" href="../vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="../css/style.css?v=20260613b">
<style>
.entity-type-badge {
display: inline-flex;
padding: 3px 10px;
border-radius: 100px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.entity-type-badge.essential { background: var(--danger-bg); color: var(--danger); }
.entity-type-badge.important { background: var(--warning-bg); color: #a16207; }
.entity-type-badge.not_applicable { background: var(--gray-100); color: var(--gray-600); }
.plan-badge {
display: inline-flex;
padding: 3px 10px;
border-radius: 100px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.plan-badge.free { background: var(--gray-100); color: var(--gray-600); }
.plan-badge.professional { background: var(--primary-bg); color: var(--primary); }
.plan-badge.enterprise { background: #f3e8ff; color: #7c3aed; }
.status-dot {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.8125rem;
}
.status-dot::before {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.status-dot.active::before { background: var(--secondary); }
.status-dot.inactive::before { background: var(--gray-400); }
.pagination {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-top: 1px solid var(--gray-100);
background: var(--gray-50);
}
.pagination-info {
font-size: 0.8125rem;
color: var(--gray-500);
}
.pagination-controls {
display: flex;
gap: 8px;
}
.score-inline {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.8125rem;
font-weight: 600;
}
.score-inline .score-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.admin-breadcrumb {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8125rem;
color: var(--gray-500);
margin-bottom: 20px;
}
.admin-breadcrumb a {
color: var(--primary);
text-decoration: none;
font-weight: 500;
}
.admin-breadcrumb a:hover {
text-decoration: underline;
}
.admin-breadcrumb .icon {
width: 14px;
height: 14px;
color: var(--gray-400);
fill: currentColor;
}
</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">
<h1 class="h2">Gestione Organizzazioni</h1>
<div class="content-header-actions">
<span class="badge badge-danger">Super Admin</span>
</div>
</header>
<div class="content-body">
<!-- Breadcrumb -->
<nav class="admin-breadcrumb" aria-label="Percorso di navigazione">
<a href="../admin/index.html">Amministrazione</a>
<svg class="icon icon-sm" aria-hidden="true"><use href="../vendor/bootstrap-italia/dist/svg/sprites.svg#it-chevron-right"></use></svg>
<span aria-current="page">Organizzazioni</span>
</nav>
<!-- Table -->
<div class="card">
<div class="card-body" style="padding:0;">
<div id="orgs-container">
<div class="spinner" style="margin:60px auto;" role="status" aria-label="Caricamento in corso"></div>
</div>
</div>
</div>
</div>
</main>
</div>
<script src="../js/api.js?v=20260613"></script>
<script src="../js/common.js?v=20260615"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). -->
<script src="../vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script>
if (window.bootstrap && bootstrap.loadFonts) {
bootstrap.loadFonts('../vendor/bootstrap-italia/dist/fonts');
}
</script>
<script src="../js/common-bi.js?v=20260615"></script>
<script src="../js/i18n.js?v=20260615"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
I18n.init();
let currentPage = 1;
// ── Admin check ──────────────────────────────────────────
(async function checkAdmin() {
try {
const me = await api.getMe();
if (!me.success || !me.data || me.data.role !== 'super_admin') {
showNotification('Accesso non autorizzato.', 'error');
setTimeout(() => { window.location.href = '/dashboard.html'; }, 1500);
return;
}
loadOrganizations(1);
} catch (e) {
window.location.href = '/dashboard.html';
}
})();
// ── Mappature ────────────────────────────────────────────
const sectorLabels = {
energy: 'Energia', transport: 'Trasporti', banking: 'Banche',
health: 'Sanita\'', water: 'Acqua', digital_infra: 'Infrastrutture Digitali',
public_admin: 'Pubblica Amministrazione', manufacturing: 'Manifatturiero',
postal: 'Servizi Postali', chemical: 'Chimico', food: 'Alimentare',
waste: 'Rifiuti', ict_services: 'Servizi ICT',
digital_providers: 'Provider Digitali', space: 'Spazio',
research: 'Ricerca', other: 'Altro'
};
const entityTypeLabels = {
essential: 'Essenziale',
important: 'Importante',
not_applicable: 'N/A'
};
const planLabels = { free: 'Free', professional: 'Professional', enterprise: 'Enterprise' };
// ── Caricamento ──────────────────────────────────────────
async function loadOrganizations(page) {
const container = document.getElementById('orgs-container');
container.innerHTML = '<div class="spinner" style="margin:60px auto;" role="status" aria-label="Caricamento in corso"></div>';
currentPage = page;
try {
const result = await api.request('GET', '/admin/organizations?page=' + page);
if (result.success) {
renderOrganizations(result.data || [], result.pagination || {});
} else {
container.innerHTML = `
<div class="empty-state">
<h4>Errore nel caricamento</h4>
<p>${escapeHtml(result.message || 'Errore sconosciuto')}</p>
</div>`;
}
} catch (e) {
container.innerHTML = '<div class="empty-state"><h4>Errore di connessione</h4></div>';
}
}
function renderOrganizations(orgs, pagination) {
const container = document.getElementById('orgs-container');
if (!orgs || orgs.length === 0) {
container.innerHTML = `
<div class="empty-state">
<svg class="icon" aria-hidden="true" style="width:48px;height:48px;fill:var(--gray-300);"><use href="../vendor/bootstrap-italia/dist/svg/sprites.svg#it-pa"></use></svg>
<h4>Nessuna organizzazione trovata</h4>
<p>Le organizzazioni registrate appariranno qui.</p>
</div>`;
return;
}
let html = `
<div class="table-container">
<table>
<thead>
<tr>
<th scope="col">Nome</th>
<th scope="col">Settore</th>
<th scope="col">Tipo Entita'</th>
<th scope="col">Membri</th>
<th scope="col">Ultimo Score</th>
<th scope="col">Piano</th>
<th scope="col">Stato</th>
<th scope="col">Data Creazione</th>
</tr>
</thead>
<tbody>`;
orgs.forEach(org => {
const entityType = org.entity_type || 'not_applicable';
const sector = sectorLabels[org.sector] || org.sector || '-';
const entityLabel = entityTypeLabels[entityType] || entityType;
const plan = (org.subscription_plan || 'free').toLowerCase();
const planLabel = planLabels[plan] || plan;
const isActive = org.is_active == 1 || org.is_active === true;
const score = org.last_score;
let scoreHtml = '<span class="text-muted">-</span>';
if (score !== null && score !== undefined) {
const scoreColor = getScoreColor(score);
scoreHtml = `
<span class="score-inline">
<span class="score-dot" style="background:${scoreColor}"></span>
${Math.round(score)}%
</span>`;
}
html += `
<tr>
<td><strong>${escapeHtml(org.name || '-')}</strong></td>
<td>${escapeHtml(sector)}</td>
<td><span class="entity-type-badge ${entityType}">${escapeHtml(entityLabel)}</span></td>
<td>${org.member_count ?? '-'}</td>
<td>${scoreHtml}</td>
<td><span class="plan-badge ${plan}">${escapeHtml(planLabel)}</span></td>
<td><span class="status-dot ${isActive ? 'active' : 'inactive'}">${isActive ? 'Attiva' : 'Inattiva'}</span></td>
<td>${formatDate(org.created_at)}</td>
</tr>`;
});
html += '</tbody></table></div>';
// Paginazione
const total = pagination.total || orgs.length;
const perPage = pagination.per_page || 20;
const page = pagination.page || currentPage;
const totalPages = Math.ceil(total / perPage);
const start = ((page - 1) * perPage) + 1;
const end = Math.min(page * perPage, total);
if (totalPages > 1) {
html += `
<div class="pagination">
<div class="pagination-info">
Visualizzati ${start}-${end} di ${total} organizzazioni
</div>
<div class="pagination-controls">
<button class="btn btn-secondary btn-sm" onclick="loadOrganizations(${page - 1})" ${page <= 1 ? 'disabled' : ''}>
&laquo; Precedente
</button>
<button class="btn btn-secondary btn-sm" onclick="loadOrganizations(${page + 1})" ${page >= totalPages ? 'disabled' : ''}>
Successivo &raquo;
</button>
</div>
</div>`;
} else if (total > 0) {
html += `
<div class="pagination">
<div class="pagination-info">
Totale: ${total} organizzazion${total === 1 ? 'e' : 'i'}
</div>
<div></div>
</div>`;
}
container.innerHTML = html;
}
</script>
</body>
</html>
-400
View File
@@ -1,400 +0,0 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Gestione Utenti (BI) - NIS2 Agile</title>
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
via JS (tabella utenti/azioni) -> parita' funzionale assoluta. BI veste la chrome.
Path ../ perche' questa pagina vive in /admin/. -->
<link rel="stylesheet" href="../vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="../css/style.css?v=20260613b">
<style>
.role-badge {
display: inline-flex;
padding: 3px 10px;
border-radius: 100px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.role-badge.super_admin { background: var(--danger-bg); color: var(--danger); }
.role-badge.org_admin { background: var(--primary-bg); color: var(--primary); }
.role-badge.compliance_manager { background: var(--secondary-bg); color: #15803d; }
.role-badge.board_member { background: #f3e8ff; color: #7c3aed; }
.role-badge.auditor { background: var(--info-bg); color: var(--primary); }
.role-badge.employee { background: var(--gray-100); color: var(--gray-600); }
.active-toggle {
position: relative;
display: inline-flex;
align-items: center;
cursor: pointer;
}
.active-toggle input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
width: 36px;
height: 20px;
background: var(--gray-300);
border-radius: 10px;
transition: background var(--transition-fast);
position: relative;
}
.toggle-slider::after {
content: '';
position: absolute;
width: 16px;
height: 16px;
background: #fff;
border-radius: 50%;
top: 2px;
left: 2px;
transition: transform var(--transition-fast);
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.active-toggle input:checked + .toggle-slider {
background: var(--secondary);
}
.active-toggle input:checked + .toggle-slider::after {
transform: translateX(16px);
}
/* Focus visibile WCAG su toggle keyboard-operabile */
.active-toggle input:focus-visible + .toggle-slider {
outline: 2px solid var(--primary, #06c);
outline-offset: 2px;
}
.pagination {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-top: 1px solid var(--gray-100);
background: var(--gray-50);
}
.pagination-info {
font-size: 0.8125rem;
color: var(--gray-500);
}
.pagination-controls {
display: flex;
gap: 8px;
}
.org-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.org-tag {
display: inline-flex;
padding: 2px 8px;
font-size: 0.7rem;
font-weight: 500;
background: var(--gray-100);
color: var(--gray-600);
border-radius: var(--border-radius-sm);
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-breadcrumb {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8125rem;
color: var(--gray-500);
margin-bottom: 20px;
}
.admin-breadcrumb a {
color: var(--primary);
text-decoration: none;
font-weight: 500;
}
.admin-breadcrumb a:hover {
text-decoration: underline;
}
.admin-breadcrumb .icon {
color: var(--gray-400);
}
</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">
<h1 class="h2">Gestione Utenti</h1>
<div class="content-header-actions">
<span class="badge badge-danger">Super Admin</span>
</div>
</header>
<div class="content-body">
<!-- Breadcrumb -->
<nav class="admin-breadcrumb" aria-label="Percorso di navigazione">
<a href="../admin/index.html">Amministrazione</a>
<svg class="icon icon-sm" aria-hidden="true"><use href="../vendor/bootstrap-italia/dist/svg/sprites.svg#it-chevron-right"></use></svg>
<span aria-current="page">Utenti</span>
</nav>
<!-- Table -->
<div class="card">
<div class="card-body" style="padding:0;">
<div id="users-container" aria-live="polite">
<div class="spinner" style="margin:60px auto;" role="status" aria-label="Caricamento utenti"></div>
</div>
</div>
</div>
</div>
</main>
</div>
<script src="../js/api.js?v=20260613"></script>
<script src="../js/common.js?v=20260615"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). -->
<script src="../vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script>
if (window.bootstrap && bootstrap.loadFonts) {
bootstrap.loadFonts('../vendor/bootstrap-italia/dist/fonts');
}
</script>
<script src="../js/common-bi.js?v=20260615"></script>
<script src="../js/i18n.js?v=20260615"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
I18n.init();
let currentPage = 1;
// ── Admin check ──────────────────────────────────────────
(async function checkAdmin() {
try {
const me = await api.getMe();
if (!me.success || !me.data || me.data.role !== 'super_admin') {
showNotification('Accesso non autorizzato.', 'error');
setTimeout(() => { window.location.href = '../dashboard.html'; }, 1500);
return;
}
loadUsers(1);
} catch (e) {
window.location.href = '../dashboard.html';
}
})();
// ── Mappature ────────────────────────────────────────────
const roleLabels = {
super_admin: 'Super Admin',
org_admin: 'Amministratore',
compliance_manager: 'Compliance Manager',
board_member: 'Membro CDA',
auditor: 'Auditor',
employee: 'Dipendente'
};
// ── Caricamento ──────────────────────────────────────────
async function loadUsers(page) {
const container = document.getElementById('users-container');
container.innerHTML = '<div class="spinner" style="margin:60px auto;" role="status" aria-label="Caricamento utenti"></div>';
currentPage = page;
try {
const result = await api.request('GET', '/admin/users?page=' + page);
if (result.success) {
renderUsers(result.data || [], result.pagination || {});
} else {
container.innerHTML = `
<div class="empty-state">
<h4>Errore nel caricamento</h4>
<p>${escapeHtml(result.message || 'Errore sconosciuto')}</p>
</div>`;
}
} catch (e) {
container.innerHTML = '<div class="empty-state"><h4>Errore di connessione</h4></div>';
}
}
function renderUsers(users, pagination) {
const container = document.getElementById('users-container');
if (!users || users.length === 0) {
container.innerHTML = `
<div class="empty-state">
<svg class="icon icon-lg" aria-hidden="true" style="margin:0 auto;"><use href="../vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg>
<h4>Nessun utente trovato</h4>
<p>Gli utenti registrati appariranno qui.</p>
</div>`;
return;
}
let html = `
<div class="table-container">
<table>
<thead>
<tr>
<th scope="col">Nome</th>
<th scope="col">Email</th>
<th scope="col">Ruolo</th>
<th scope="col">Organizzazioni</th>
<th scope="col">Ultimo Login</th>
<th scope="col">Attivo</th>
<th scope="col">Data Creazione</th>
<th scope="col">Azioni</th>
</tr>
</thead>
<tbody>`;
users.forEach(user => {
const role = user.role || 'employee';
const roleLabel = roleLabels[role] || role;
const isActive = user.is_active == 1 || user.is_active === true;
// Organizzazioni
let orgsHtml = '<span class="text-muted">-</span>';
if (user.organizations) {
const orgNames = user.organizations.split(',').filter(n => n.trim());
if (orgNames.length > 0) {
orgsHtml = '<div class="org-tags">';
orgNames.forEach(name => {
orgsHtml += `<span class="org-tag" title="${escapeHtml(name.trim())}">${escapeHtml(name.trim())}</span>`;
});
orgsHtml += '</div>';
}
}
// Ultimo login
const lastLogin = user.last_login_at ? formatDateTime(user.last_login_at) : '<span class="text-muted">Mai</span>';
html += `
<tr>
<td><strong>${escapeHtml(user.full_name || '-')}</strong></td>
<td>${escapeHtml(user.email || '-')}</td>
<td><span class="role-badge ${role}">${escapeHtml(roleLabel)}</span></td>
<td>${orgsHtml}</td>
<td>${lastLogin}</td>
<td>
<label class="active-toggle" title="${isActive ? 'Attivo' : 'Inattivo'}">
<span class="visually-hidden">Stato attivo utente ${escapeHtml(user.email || '')}</span>
<input type="checkbox" ${isActive ? 'checked' : ''} onchange="toggleUserActive(${user.id}, this.checked)" ${role === 'super_admin' ? 'disabled' : ''} aria-label="${isActive ? 'Disattiva' : 'Attiva'} utente ${escapeHtml(user.email || '')}">
<span class="toggle-slider" aria-hidden="true"></span>
</label>
</td>
<td>${formatDate(user.created_at)}</td>
<td>
${role !== 'super_admin' && isActive
? `<button class="btn btn-secondary btn-sm" onclick="impersonateUser(${user.id}, '${escapeHtml(user.email)}')" title="Entra come questo utente per 1 ora">
<svg class="icon icon-sm" aria-hidden="true" style="vertical-align:-2px"><use href="../vendor/bootstrap-italia/dist/svg/sprites.svg#it-exchange-circle"></use></svg>
Impersonate
</button>`
: '<span class="text-muted">-</span>'}
</td>
</tr>`;
});
html += '</tbody></table></div>';
// Paginazione
const total = pagination.total || users.length;
const perPage = pagination.per_page || 20;
const page = pagination.page || currentPage;
const totalPages = Math.ceil(total / perPage);
const start = ((page - 1) * perPage) + 1;
const end = Math.min(page * perPage, total);
if (totalPages > 1) {
html += `
<div class="pagination">
<div class="pagination-info">
Visualizzati ${start}-${end} di ${total} utenti
</div>
<div class="pagination-controls">
<button class="btn btn-secondary btn-sm" onclick="loadUsers(${page - 1})" ${page <= 1 ? 'disabled' : ''}>
&laquo; Precedente
</button>
<button class="btn btn-secondary btn-sm" onclick="loadUsers(${page + 1})" ${page >= totalPages ? 'disabled' : ''}>
Successivo &raquo;
</button>
</div>
</div>`;
} else if (total > 0) {
html += `
<div class="pagination">
<div class="pagination-info">
Totale: ${total} utent${total === 1 ? 'e' : 'i'}
</div>
<div></div>
</div>`;
}
container.innerHTML = html;
}
// ── Toggle Attivo/Inattivo ───────────────────────────────
async function toggleUserActive(userId, isActive) {
try {
const result = await api.request('PUT', '/admin/users/' + userId, {
is_active: isActive ? 1 : 0
});
if (result.success) {
showNotification(
isActive ? 'Utente attivato con successo.' : 'Utente disattivato con successo.',
'success'
);
} else {
showNotification(result.message || 'Errore nell\'aggiornamento.', 'error');
// Ricarica per resettare lo stato
loadUsers(currentPage);
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
loadUsers(currentPage);
}
}
// ── Impersonate (Fase 4 / G11 — UI) ──
async function impersonateUser(userId, email) {
if (!confirm('Entrare come ' + email + ' per 1 ora?\n\nSarai loggato come quell\'utente. Tornerai alla tua sessione facendo logout.')) return;
try {
const res = await api.post('/auth/impersonate', { user_id: userId });
if (res.success && res.data && res.data.access_token) {
// Salva sessione originale per "Exit impersonate"
sessionStorage.setItem('nis2_impersonate_origin_token', api.token);
sessionStorage.setItem('nis2_impersonate_origin_refresh', api.refreshToken || '');
sessionStorage.setItem('nis2_impersonate_target_email', email);
// Sostituisce token con quello impersonate (no refresh per default)
api.setTokens(res.data.access_token, '');
window.location.href = '../dashboard.html';
} else {
showNotification(res.message || 'Impersonate fallito.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
}
</script>
</body>
</html>