- common.js: aggiunto i18nKey a navItems, data-i18n su sezioni e voci sidebar → toggle IT/EN ora traduce la navigazione in tempo reale - Tutte e 10 le pagine HTML: aggiunto data-i18n="*.title" agli h2 (dashboard, assessment, risks, incidents, policies, supply-chain, training, assets, reports, settings) - FIX BUG: sidebar puntava ad audit.html (inesistente) → corretto in reports.html - HelpSystem: funziona correttamente in tutte le 10 pagine (content-header-actions presente, init() chiamato) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
730 lines
32 KiB
HTML
730 lines
32 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Formazione - NIS2 Agile</title>
|
|
<link rel="stylesheet" href="/css/style.css">
|
|
<style>
|
|
/* ── Tab Navigation ─────────────────────────────────────── */
|
|
.tab-nav {
|
|
display: flex;
|
|
gap: 0;
|
|
border-bottom: 2px solid var(--gray-200);
|
|
margin-bottom: 24px;
|
|
}
|
|
.tab-nav button {
|
|
padding: 12px 24px;
|
|
background: none;
|
|
border: none;
|
|
border-bottom: 2px solid transparent;
|
|
margin-bottom: -2px;
|
|
color: var(--gray-500);
|
|
font-size: 0.9rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
.tab-nav button:hover {
|
|
color: var(--gray-700);
|
|
background: var(--gray-50);
|
|
}
|
|
.tab-nav button.active {
|
|
color: var(--primary);
|
|
border-bottom-color: var(--primary);
|
|
}
|
|
.tab-panel { display: none; }
|
|
.tab-panel.active { display: block; }
|
|
|
|
/* ── Course Cards Grid ──────────────────────────────────── */
|
|
.courses-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
.course-card {
|
|
background: var(--card-bg);
|
|
border-radius: var(--border-radius);
|
|
box-shadow: var(--card-shadow);
|
|
padding: 20px;
|
|
transition: box-shadow var(--transition);
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.course-card:hover {
|
|
box-shadow: var(--card-shadow-hover);
|
|
}
|
|
.course-card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 12px;
|
|
}
|
|
.course-card-title {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: var(--gray-800);
|
|
margin: 0;
|
|
}
|
|
.course-card-desc {
|
|
font-size: 0.85rem;
|
|
color: var(--gray-500);
|
|
margin-bottom: 16px;
|
|
flex-grow: 1;
|
|
line-height: 1.5;
|
|
}
|
|
.course-card-meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
align-items: center;
|
|
font-size: 0.8rem;
|
|
}
|
|
.course-meta-item {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
color: var(--gray-500);
|
|
background: var(--gray-100);
|
|
padding: 4px 10px;
|
|
border-radius: 20px;
|
|
}
|
|
.course-meta-item svg { width: 14px; height: 14px; }
|
|
.badge-mandatory {
|
|
background: var(--danger-bg);
|
|
color: var(--danger);
|
|
font-weight: 600;
|
|
padding: 3px 10px;
|
|
border-radius: 20px;
|
|
font-size: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
/* ── Assignments Table ──────────────────────────────────── */
|
|
.data-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
background: var(--card-bg);
|
|
border-radius: var(--border-radius);
|
|
overflow: hidden;
|
|
box-shadow: var(--card-shadow);
|
|
}
|
|
.data-table thead {
|
|
background: var(--gray-50);
|
|
}
|
|
.data-table th {
|
|
text-align: left;
|
|
padding: 12px 16px;
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
color: var(--gray-500);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
border-bottom: 1px solid var(--gray-200);
|
|
}
|
|
.data-table td {
|
|
padding: 12px 16px;
|
|
font-size: 0.875rem;
|
|
color: var(--gray-700);
|
|
border-bottom: 1px solid var(--gray-100);
|
|
}
|
|
.data-table tr:last-child td { border-bottom: none; }
|
|
.data-table tr:hover td { background: var(--gray-50); }
|
|
|
|
/* ── Status Badges ──────────────────────────────────────── */
|
|
.status-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 4px 12px;
|
|
border-radius: 20px;
|
|
font-size: 0.78rem;
|
|
font-weight: 500;
|
|
gap: 4px;
|
|
}
|
|
.status-badge.assigned { background: var(--gray-100); color: var(--gray-600); }
|
|
.status-badge.in_progress { background: var(--info-bg); color: var(--info); }
|
|
.status-badge.completed { background: var(--secondary-bg); color: var(--secondary); }
|
|
.status-badge.overdue { background: var(--danger-bg); color: var(--danger); }
|
|
|
|
/* ── Compliance Percentage Bar ──────────────────────────── */
|
|
.compliance-bar-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.compliance-bar {
|
|
flex-grow: 1;
|
|
height: 8px;
|
|
background: var(--gray-200);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
.compliance-bar-fill {
|
|
height: 100%;
|
|
border-radius: 4px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
.compliance-pct {
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
min-width: 45px;
|
|
text-align: right;
|
|
}
|
|
.compliance-pct.high { color: var(--secondary); }
|
|
.compliance-pct.medium { color: var(--warning); }
|
|
.compliance-pct.low { color: var(--danger); }
|
|
|
|
/* ── Spinner/Loading ────────────────────────────────────── */
|
|
.loading-state {
|
|
text-align: center;
|
|
padding: 48px 20px;
|
|
color: var(--gray-400);
|
|
}
|
|
.loading-state .spinner {
|
|
width: 32px;
|
|
height: 32px;
|
|
border: 3px solid var(--gray-200);
|
|
border-top-color: var(--primary);
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
margin: 0 auto 12px;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
/* ── Empty State ────────────────────────────────────────── */
|
|
.empty-state-box {
|
|
text-align: center;
|
|
padding: 48px 20px;
|
|
color: var(--gray-400);
|
|
}
|
|
.empty-state-box svg {
|
|
width: 48px;
|
|
height: 48px;
|
|
margin-bottom: 12px;
|
|
opacity: 0.5;
|
|
}
|
|
.empty-state-box h4 {
|
|
color: var(--gray-600);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
/* ── Responsive ─────────────────────────────────────────── */
|
|
@media (max-width: 768px) {
|
|
.courses-grid { grid-template-columns: 1fr; }
|
|
.tab-nav button { padding: 10px 14px; font-size: 0.82rem; }
|
|
.data-table { font-size: 0.8rem; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app-layout">
|
|
<aside class="sidebar" id="sidebar"></aside>
|
|
|
|
<main class="main-content">
|
|
<header class="content-header">
|
|
<h2 data-i18n="training.title">Formazione e Awareness</h2>
|
|
<div class="content-header-actions">
|
|
<span class="text-muted" style="font-size:0.8rem; margin-right:8px;">Art. 20 NIS2</span>
|
|
<button class="btn btn-primary" onclick="showCreateCourseModal()">+ Nuovo Corso</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="content-body">
|
|
<!-- Tab Navigation -->
|
|
<div class="tab-nav">
|
|
<button class="active" onclick="switchTab('courses', this)">Corsi</button>
|
|
<button onclick="switchTab('assignments', this)">Le Mie Assegnazioni</button>
|
|
<button onclick="switchTab('compliance', this)">Compliance Formativa</button>
|
|
</div>
|
|
|
|
<!-- Tab: Corsi -->
|
|
<div class="tab-panel active" id="tab-courses">
|
|
<div class="courses-grid" id="courses-grid">
|
|
<div class="loading-state">
|
|
<div class="spinner"></div>
|
|
<p>Caricamento corsi...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab: Le Mie Assegnazioni -->
|
|
<div class="tab-panel" id="tab-assignments">
|
|
<div id="assignments-container">
|
|
<div class="loading-state">
|
|
<div class="spinner"></div>
|
|
<p>Caricamento assegnazioni...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab: Compliance Formativa -->
|
|
<div class="tab-panel" id="tab-compliance">
|
|
<div style="display:flex; justify-content:flex-end; margin-bottom:16px;">
|
|
<button class="btn btn-primary" onclick="showAssignCourseModal()">Assegna Corso</button>
|
|
</div>
|
|
<div id="compliance-container">
|
|
<div class="loading-state">
|
|
<div class="spinner"></div>
|
|
<p>Caricamento dati compliance...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script src="/js/api.js"></script>
|
|
<script src="/js/common.js"></script>
|
|
<script src="/js/i18n.js"></script>
|
|
<script src="/js/help.js"></script>
|
|
<script>
|
|
// ── Auth & Init ─────────────────────────────────────────
|
|
if (!checkAuth()) throw new Error('Not authenticated');
|
|
loadSidebar();
|
|
I18n.init();
|
|
HelpSystem.init();
|
|
|
|
// ── Labels ──────────────────────────────────────────────
|
|
const roleLabels = {
|
|
all: 'Tutti',
|
|
board_member: 'Organi di Gestione',
|
|
compliance_manager: 'Compliance Manager',
|
|
employee: 'Dipendente',
|
|
technical: 'Tecnico'
|
|
};
|
|
|
|
const statusLabels = {
|
|
assigned: 'Assegnato',
|
|
in_progress: 'In Corso',
|
|
completed: 'Completato',
|
|
overdue: 'Scaduto'
|
|
};
|
|
|
|
// ── State ───────────────────────────────────────────────
|
|
let allCourses = [];
|
|
let allAssignments = [];
|
|
let allCompliance = [];
|
|
|
|
// ── Tab Switch ──────────────────────────────────────────
|
|
function switchTab(tabId, btn) {
|
|
document.querySelectorAll('.tab-nav button').forEach(b => b.classList.remove('active'));
|
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
document.getElementById('tab-' + tabId).classList.add('active');
|
|
|
|
if (tabId === 'courses' && allCourses.length === 0) loadCourses();
|
|
if (tabId === 'assignments') loadAssignments();
|
|
if (tabId === 'compliance') loadCompliance();
|
|
}
|
|
|
|
// ── Load Courses ────────────────────────────────────────
|
|
async function loadCourses() {
|
|
const container = document.getElementById('courses-grid');
|
|
container.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Caricamento corsi...</p></div>';
|
|
|
|
try {
|
|
const result = await api.listCourses();
|
|
if (result.success && result.data) {
|
|
allCourses = result.data;
|
|
renderCourses(allCourses);
|
|
} else {
|
|
container.innerHTML = '<div class="empty-state-box"><h4>Errore nel caricamento</h4><p>' + escapeHtml(result.message || '') + '</p></div>';
|
|
}
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state-box"><h4>Errore di connessione</h4><p>Impossibile caricare i corsi.</p></div>';
|
|
}
|
|
}
|
|
|
|
function renderCourses(courses) {
|
|
const container = document.getElementById('courses-grid');
|
|
|
|
if (!courses || courses.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state-box" style="grid-column: 1/-1;">
|
|
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838L7.667 9.088l1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3z"/></svg>
|
|
<h4>Nessun corso disponibile</h4>
|
|
<p>Crea il primo corso di formazione per iniziare.</p>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
courses.forEach(course => {
|
|
const mandatory = course.is_mandatory == 1;
|
|
html += `
|
|
<div class="course-card">
|
|
<div class="course-card-header">
|
|
<h4 class="course-card-title">${escapeHtml(course.title)}</h4>
|
|
${mandatory ? '<span class="badge-mandatory">Obbligatorio</span>' : ''}
|
|
</div>
|
|
<p class="course-card-desc">${escapeHtml(course.description || 'Nessuna descrizione disponibile.')}</p>
|
|
<div class="course-card-meta">
|
|
${course.duration_minutes ? `
|
|
<span class="course-meta-item">
|
|
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>
|
|
${course.duration_minutes} min
|
|
</span>
|
|
` : ''}
|
|
<span class="course-meta-item">
|
|
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z"/></svg>
|
|
${roleLabels[course.target_role] || course.target_role || 'Tutti'}
|
|
</span>
|
|
${course.nis2_article ? `
|
|
<span class="course-meta-item">
|
|
<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-2V4z" clip-rule="evenodd"/></svg>
|
|
Art. ${escapeHtml(course.nis2_article)}
|
|
</span>
|
|
` : ''}
|
|
${course.passing_score ? `
|
|
<span class="course-meta-item">
|
|
Soglia: ${course.passing_score}%
|
|
</span>
|
|
` : ''}
|
|
</div>
|
|
</div>`;
|
|
});
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
// ── Create Course Modal ─────────────────────────────────
|
|
function showCreateCourseModal() {
|
|
const roleOptions = Object.entries(roleLabels).map(([val, label]) =>
|
|
`<option value="${val}">${label}</option>`
|
|
).join('');
|
|
|
|
showModal('Nuovo Corso di Formazione', `
|
|
<div class="form-group">
|
|
<label class="form-label">Titolo *</label>
|
|
<input type="text" class="form-input" id="course-title" placeholder="Es: Fondamenti Cybersecurity NIS2" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Descrizione</label>
|
|
<textarea class="form-input" id="course-description" rows="3" placeholder="Descrizione del corso..."></textarea>
|
|
</div>
|
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px;">
|
|
<div class="form-group">
|
|
<label class="form-label">Ruolo Destinatario</label>
|
|
<select class="form-select" id="course-target-role">${roleOptions}</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Durata (minuti)</label>
|
|
<input type="number" class="form-input" id="course-duration" min="1" placeholder="60">
|
|
</div>
|
|
</div>
|
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px;">
|
|
<div class="form-group">
|
|
<label class="form-label">Articolo NIS2</label>
|
|
<input type="text" class="form-input" id="course-nis2-article" placeholder="Es: Art. 20">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Punteggio Minimo (%)</label>
|
|
<input type="number" class="form-input" id="course-passing-score" min="0" max="100" value="70">
|
|
</div>
|
|
</div>
|
|
<div class="form-group" style="margin-top:8px;">
|
|
<label style="display:flex; align-items:center; gap:8px; cursor:pointer;">
|
|
<input type="checkbox" id="course-mandatory">
|
|
<span class="form-label" style="margin:0;">Corso obbligatorio</span>
|
|
</label>
|
|
</div>
|
|
`, {
|
|
size: 'lg',
|
|
footer: `
|
|
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
|
|
<button class="btn btn-primary" onclick="createCourse()">Crea Corso</button>
|
|
`
|
|
});
|
|
}
|
|
|
|
async function createCourse() {
|
|
const title = document.getElementById('course-title').value.trim();
|
|
if (!title) {
|
|
showNotification('Il titolo e\' obbligatorio.', 'warning');
|
|
return;
|
|
}
|
|
|
|
const data = {
|
|
title: title,
|
|
description: document.getElementById('course-description').value.trim(),
|
|
target_role: document.getElementById('course-target-role').value,
|
|
is_mandatory: document.getElementById('course-mandatory').checked ? 1 : 0,
|
|
duration_minutes: parseInt(document.getElementById('course-duration').value) || null,
|
|
nis2_article: document.getElementById('course-nis2-article').value.trim() || null,
|
|
passing_score: parseInt(document.getElementById('course-passing-score').value) || 70,
|
|
};
|
|
|
|
closeModal();
|
|
showNotification('Creazione corso in corso...', 'info');
|
|
|
|
try {
|
|
const result = await api.post('/training/courses', data);
|
|
if (result.success) {
|
|
showNotification('Corso creato con successo!', 'success');
|
|
loadCourses();
|
|
} else {
|
|
showNotification(result.message || 'Errore nella creazione del corso.', 'error');
|
|
}
|
|
} catch (e) {
|
|
showNotification('Errore di connessione.', 'error');
|
|
}
|
|
}
|
|
|
|
// ── Load Assignments ────────────────────────────────────
|
|
async function loadAssignments() {
|
|
const container = document.getElementById('assignments-container');
|
|
container.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Caricamento assegnazioni...</p></div>';
|
|
|
|
try {
|
|
const result = await api.getMyTraining();
|
|
if (result.success && result.data) {
|
|
allAssignments = result.data;
|
|
renderAssignments(allAssignments);
|
|
} else {
|
|
container.innerHTML = '<div class="empty-state-box"><h4>Errore nel caricamento</h4></div>';
|
|
}
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state-box"><h4>Errore di connessione</h4></div>';
|
|
}
|
|
}
|
|
|
|
function renderAssignments(assignments) {
|
|
const container = document.getElementById('assignments-container');
|
|
|
|
if (!assignments || assignments.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state-box">
|
|
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/><path fill-rule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5z" clip-rule="evenodd"/></svg>
|
|
<h4>Nessuna assegnazione</h4>
|
|
<p>Non hai ancora corsi assegnati.</p>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
let html = `
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Corso</th>
|
|
<th>Stato</th>
|
|
<th>Scadenza</th>
|
|
<th>Punteggio Quiz</th>
|
|
<th>Azioni</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>`;
|
|
|
|
assignments.forEach(a => {
|
|
const isOverdue = a.due_date && new Date(a.due_date) < new Date() && a.status !== 'completed';
|
|
const displayStatus = isOverdue ? 'overdue' : a.status;
|
|
|
|
let actionBtn = '';
|
|
if (displayStatus === 'assigned' || displayStatus === 'overdue') {
|
|
actionBtn = `<button class="btn btn-primary btn-sm" onclick="updateAssignment(${a.id}, 'in_progress')">Inizia</button>`;
|
|
} else if (displayStatus === 'in_progress') {
|
|
actionBtn = `<button class="btn btn-primary btn-sm" onclick="updateAssignment(${a.id}, 'completed')">Completa</button>`;
|
|
} else {
|
|
actionBtn = `<span style="color:var(--secondary); font-weight:500; font-size:0.85rem;">Completato</span>`;
|
|
}
|
|
|
|
html += `
|
|
<tr>
|
|
<td>
|
|
<div style="font-weight:500;">${escapeHtml(a.title || '')}</div>
|
|
${a.is_mandatory == 1 ? '<span style="font-size:0.75rem; color:var(--danger);">Obbligatorio</span>' : ''}
|
|
</td>
|
|
<td><span class="status-badge ${displayStatus}">${statusLabels[displayStatus] || displayStatus}</span></td>
|
|
<td>${a.due_date ? formatDate(a.due_date) : '-'}</td>
|
|
<td>${a.quiz_score != null ? a.quiz_score + '%' : '-'}</td>
|
|
<td>${actionBtn}</td>
|
|
</tr>`;
|
|
});
|
|
|
|
html += '</tbody></table>';
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
async function updateAssignment(id, status) {
|
|
try {
|
|
const result = await api.put(`/training/assignments/${id}`, { status: status });
|
|
if (result.success) {
|
|
const msg = status === 'in_progress' ? 'Corso iniziato!' : 'Corso completato!';
|
|
showNotification(msg, 'success');
|
|
loadAssignments();
|
|
} else {
|
|
showNotification(result.message || 'Errore nell\'aggiornamento.', 'error');
|
|
}
|
|
} catch (e) {
|
|
showNotification('Errore di connessione.', 'error');
|
|
}
|
|
}
|
|
|
|
// ── Load Compliance Status ──────────────────────────────
|
|
async function loadCompliance() {
|
|
const container = document.getElementById('compliance-container');
|
|
container.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Caricamento dati compliance...</p></div>';
|
|
|
|
try {
|
|
const result = await api.getTrainingCompliance();
|
|
if (result.success && result.data) {
|
|
allCompliance = result.data;
|
|
renderCompliance(allCompliance);
|
|
} else {
|
|
container.innerHTML = '<div class="empty-state-box"><h4>Errore nel caricamento</h4></div>';
|
|
}
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state-box"><h4>Errore di connessione</h4></div>';
|
|
}
|
|
}
|
|
|
|
function renderCompliance(members) {
|
|
const container = document.getElementById('compliance-container');
|
|
|
|
if (!members || members.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state-box">
|
|
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z"/></svg>
|
|
<h4>Nessun membro trovato</h4>
|
|
<p>Nessun membro dell\'organizzazione trovato.</p>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
let html = `
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Nome</th>
|
|
<th>Email</th>
|
|
<th>Ruolo</th>
|
|
<th>Corsi Completati</th>
|
|
<th>% Compliance Obbligatoria</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>`;
|
|
|
|
members.forEach(m => {
|
|
const assignments = m.assignments || [];
|
|
const completed = assignments.filter(a => a.status === 'completed').length;
|
|
const total = assignments.length;
|
|
const pct = m.mandatory_compliance != null ? m.mandatory_compliance : 100;
|
|
const pctClass = pct >= 80 ? 'high' : pct >= 50 ? 'medium' : 'low';
|
|
const barColor = pct >= 80 ? 'var(--secondary)' : pct >= 50 ? 'var(--warning)' : 'var(--danger)';
|
|
|
|
html += `
|
|
<tr>
|
|
<td style="font-weight:500;">${escapeHtml(m.full_name || '-')}</td>
|
|
<td>${escapeHtml(m.email || '-')}</td>
|
|
<td>${escapeHtml(m.role || '-')}</td>
|
|
<td>${completed} / ${total}</td>
|
|
<td>
|
|
<div class="compliance-bar-container">
|
|
<div class="compliance-bar">
|
|
<div class="compliance-bar-fill" style="width:${pct}%; background:${barColor};"></div>
|
|
</div>
|
|
<span class="compliance-pct ${pctClass}">${pct}%</span>
|
|
</div>
|
|
</td>
|
|
</tr>`;
|
|
});
|
|
|
|
html += '</tbody></table>';
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
// ── Assign Course Modal ─────────────────────────────────
|
|
async function showAssignCourseModal() {
|
|
// Load courses if not already loaded
|
|
if (allCourses.length === 0) {
|
|
try {
|
|
const result = await api.listCourses();
|
|
if (result.success && result.data) {
|
|
allCourses = result.data;
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
|
|
if (allCourses.length === 0) {
|
|
showNotification('Nessun corso disponibile. Crea prima un corso.', 'warning');
|
|
return;
|
|
}
|
|
|
|
const courseOptions = allCourses.map(c =>
|
|
`<option value="${c.id}">${escapeHtml(c.title)}</option>`
|
|
).join('');
|
|
|
|
// Load members for selection
|
|
let membersHtml = '<p class="text-muted">Caricamento membri...</p>';
|
|
if (allCompliance.length > 0) {
|
|
membersHtml = allCompliance.map(m => `
|
|
<label style="display:flex; align-items:center; gap:8px; padding:6px 0; cursor:pointer;">
|
|
<input type="checkbox" class="assign-user-cb" value="${m.id}">
|
|
<span>${escapeHtml(m.full_name || m.email)}</span>
|
|
<span style="color:var(--gray-400); font-size:0.8rem;">(${escapeHtml(m.role || '')})</span>
|
|
</label>
|
|
`).join('');
|
|
}
|
|
|
|
showModal('Assegna Corso', `
|
|
<div class="form-group">
|
|
<label class="form-label">Corso</label>
|
|
<select class="form-select" id="assign-course-id">${courseOptions}</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Scadenza</label>
|
|
<input type="date" class="form-input" id="assign-due-date">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Seleziona Utenti</label>
|
|
<div style="max-height:200px; overflow-y:auto; border:1px solid var(--gray-200); border-radius:var(--border-radius-sm); padding:8px 12px;">
|
|
${membersHtml}
|
|
</div>
|
|
</div>
|
|
`, {
|
|
size: 'lg',
|
|
footer: `
|
|
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
|
|
<button class="btn btn-primary" onclick="doAssignCourse()">Assegna</button>
|
|
`
|
|
});
|
|
}
|
|
|
|
async function doAssignCourse() {
|
|
const courseId = document.getElementById('assign-course-id').value;
|
|
const dueDate = document.getElementById('assign-due-date').value || null;
|
|
const checkboxes = document.querySelectorAll('.assign-user-cb:checked');
|
|
const userIds = Array.from(checkboxes).map(cb => parseInt(cb.value));
|
|
|
|
if (userIds.length === 0) {
|
|
showNotification('Seleziona almeno un utente.', 'warning');
|
|
return;
|
|
}
|
|
|
|
closeModal();
|
|
showNotification('Assegnazione in corso...', 'info');
|
|
|
|
try {
|
|
const result = await api.post('/training/assign', {
|
|
course_id: parseInt(courseId),
|
|
user_ids: userIds,
|
|
due_date: dueDate
|
|
});
|
|
if (result.success) {
|
|
showNotification(result.message || 'Corso assegnato con successo!', 'success');
|
|
loadCompliance();
|
|
} else {
|
|
showNotification(result.message || 'Errore nell\'assegnazione.', 'error');
|
|
}
|
|
} catch (e) {
|
|
showNotification('Errore di connessione.', 'error');
|
|
}
|
|
}
|
|
|
|
// ── Initial Load ────────────────────────────────────────
|
|
loadCourses();
|
|
</script>
|
|
</body>
|
|
</html>
|