- common.js: idle session timeout 30min con avviso countdown 5min prima del logout - common.js: checkAuth() attiva automaticamente il monitor di inattività - api.js: messaggi errore connessione usano i18n (IT/EN) tramite I18n.t() - risks.html: saveRisk() e aiSuggest() con setButtonLoading durante salvataggio - risks.html: deleteRisk() ricarica la matrice se si è in matrix view - incidents.html: createIncident() con setButtonLoading durante registrazione - policies.html: savePolicy() e saveAIGeneratedPolicy() con setButtonLoading - policies.html: banner AI-draft con pulsante X per dismissione Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
955 lines
45 KiB
HTML
955 lines
45 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Policy - NIS2 Agile</title>
|
|
<link rel="stylesheet" href="/css/style.css">
|
|
<style>
|
|
/* ── Policy Page Styles ──────────────────────────────────── */
|
|
.filters-bar {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 20px;
|
|
}
|
|
.filters-bar .form-select {
|
|
min-width: 180px;
|
|
padding: 8px 12px;
|
|
font-size: 0.85rem;
|
|
}
|
|
.filters-bar .btn {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.policy-actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
.policy-actions .btn {
|
|
padding: 5px 10px;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
gap: 0;
|
|
border-bottom: 2px solid var(--gray-200);
|
|
margin-bottom: 24px;
|
|
}
|
|
.tab-btn {
|
|
padding: 10px 20px;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
color: var(--gray-500);
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
border-bottom: 2px solid transparent;
|
|
margin-bottom: -2px;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
.tab-btn:hover {
|
|
color: var(--gray-700);
|
|
}
|
|
.tab-btn.active {
|
|
color: var(--primary);
|
|
border-bottom-color: var(--primary);
|
|
}
|
|
.tab-content {
|
|
display: none;
|
|
}
|
|
.tab-content.active {
|
|
display: block;
|
|
}
|
|
|
|
.template-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
.template-card {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--gray-200);
|
|
border-radius: var(--border-radius);
|
|
padding: 20px;
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
.template-card:hover {
|
|
border-color: var(--primary);
|
|
box-shadow: var(--card-shadow-hover);
|
|
transform: translateY(-2px);
|
|
}
|
|
.template-card h4 {
|
|
font-size: 0.9rem;
|
|
color: var(--gray-800);
|
|
margin-bottom: 8px;
|
|
}
|
|
.template-card .template-meta {
|
|
font-size: 0.78rem;
|
|
color: var(--gray-500);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
.template-card .template-meta .tag {
|
|
font-size: 0.7rem;
|
|
}
|
|
|
|
.detail-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 24px;
|
|
gap: 16px;
|
|
}
|
|
.detail-header h3 {
|
|
font-size: 1.25rem;
|
|
color: var(--gray-900);
|
|
}
|
|
.detail-header-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.detail-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 300px;
|
|
gap: 24px;
|
|
}
|
|
@media (max-width: 900px) {
|
|
.detail-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.policy-content-box {
|
|
background: var(--gray-50);
|
|
border: 1px solid var(--gray-200);
|
|
border-radius: var(--border-radius);
|
|
padding: 24px;
|
|
white-space: pre-wrap;
|
|
font-size: 0.875rem;
|
|
line-height: 1.7;
|
|
color: var(--gray-700);
|
|
min-height: 200px;
|
|
max-height: 600px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.meta-card {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--gray-200);
|
|
border-radius: var(--border-radius);
|
|
padding: 20px;
|
|
}
|
|
.meta-card h4 {
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
color: var(--gray-800);
|
|
margin-bottom: 16px;
|
|
padding-bottom: 12px;
|
|
border-bottom: 1px solid var(--gray-100);
|
|
}
|
|
.meta-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 8px 0;
|
|
font-size: 0.82rem;
|
|
border-bottom: 1px solid var(--gray-50);
|
|
}
|
|
.meta-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.meta-label {
|
|
color: var(--gray-500);
|
|
font-weight: 500;
|
|
}
|
|
.meta-value {
|
|
color: var(--gray-800);
|
|
font-weight: 600;
|
|
text-align: right;
|
|
}
|
|
|
|
.ai-preview {
|
|
background: var(--gray-50);
|
|
border: 1px solid var(--gray-200);
|
|
border-radius: var(--border-radius);
|
|
padding: 20px;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
white-space: pre-wrap;
|
|
font-size: 0.85rem;
|
|
line-height: 1.6;
|
|
margin-top: 12px;
|
|
}
|
|
.ai-preview-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 12px;
|
|
color: var(--primary);
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
}
|
|
.ai-preview-header svg {
|
|
width: 20px;
|
|
height: 20px;
|
|
}
|
|
|
|
.loading-overlay {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 60px 20px;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
color: var(--gray-500);
|
|
}
|
|
.loading-overlay .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;
|
|
}
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.text-link {
|
|
color: var(--primary);
|
|
cursor: pointer;
|
|
font-weight: 500;
|
|
}
|
|
.text-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.content-header-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app-layout">
|
|
<aside class="sidebar" id="sidebar"></aside>
|
|
|
|
<main class="main-content">
|
|
<header class="content-header">
|
|
<h2 data-i18n="policies.title">Gestione Policy</h2>
|
|
<div class="content-header-actions">
|
|
<button class="btn btn-outline" onclick="openAIGenerateModal()">
|
|
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-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-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 8.512a1 1 0 010 1.836l-3.354 1.311-1.18 4.456a1 1 0 01-1.932 0L9.854 11.66 6.5 10.348a1 1 0 010-1.836l3.354-1.311 1.18-4.456A1 1 0 0112 2z"/></svg>
|
|
Genera con AI
|
|
</button>
|
|
<button class="btn btn-primary" onclick="openCreateModal()">+ Nuova Policy</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="content-body" id="page-content">
|
|
<!-- Tabs -->
|
|
<div class="tabs">
|
|
<button class="tab-btn active" onclick="switchTab('list')">Elenco Policy</button>
|
|
<button class="tab-btn" onclick="switchTab('templates')">Template</button>
|
|
</div>
|
|
|
|
<!-- Tab: Lista Policy -->
|
|
<div class="tab-content active" id="tab-list">
|
|
<div class="filters-bar">
|
|
<select class="form-select" id="filter-category" onchange="loadPolicies()">
|
|
<option value="">Tutte le Categorie</option>
|
|
<option value="information_security">Sicurezza Informazioni</option>
|
|
<option value="access_control">Controllo Accessi</option>
|
|
<option value="incident_response">Risposta Incidenti</option>
|
|
<option value="business_continuity">Continuita' Operativa</option>
|
|
<option value="supply_chain">Supply Chain</option>
|
|
<option value="encryption">Crittografia</option>
|
|
<option value="hr_security">Sicurezza HR</option>
|
|
<option value="asset_management">Gestione Asset</option>
|
|
<option value="network_security">Sicurezza Rete</option>
|
|
<option value="vulnerability_management">Gestione Vulnerabilita'</option>
|
|
</select>
|
|
<select class="form-select" id="filter-status" onchange="loadPolicies()">
|
|
<option value="">Tutti gli Stati</option>
|
|
<option value="draft">Bozza</option>
|
|
<option value="review">In Revisione</option>
|
|
<option value="approved">Approvata</option>
|
|
<option value="published">Pubblicata</option>
|
|
<option value="archived">Archiviata</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="table-container" id="policies-table">
|
|
<div class="loading-overlay">
|
|
<div class="spinner"></div>
|
|
<span>Caricamento policy...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab: Template -->
|
|
<div class="tab-content" id="tab-templates">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>Template Policy NIS2</h3>
|
|
</div>
|
|
<div class="card-body" id="templates-grid">
|
|
<div class="loading-overlay">
|
|
<div class="spinner"></div>
|
|
<span>Caricamento template...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Detail View (hidden by default) -->
|
|
<div id="policy-detail-view" style="display:none;"></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 CATEGORY_LABELS = {
|
|
information_security: 'Sicurezza Informazioni',
|
|
access_control: 'Controllo Accessi',
|
|
incident_response: 'Risposta Incidenti',
|
|
business_continuity: 'Continuita\' Operativa',
|
|
supply_chain: 'Supply Chain',
|
|
encryption: 'Crittografia',
|
|
hr_security: 'Sicurezza HR',
|
|
asset_management: 'Gestione Asset',
|
|
network_security: 'Sicurezza Rete',
|
|
vulnerability_management: 'Gestione Vulnerabilita\''
|
|
};
|
|
|
|
const STATUS_LABELS = {
|
|
draft: 'Bozza',
|
|
review: 'In Revisione',
|
|
approved: 'Approvata',
|
|
published: 'Pubblicata',
|
|
archived: 'Archiviata'
|
|
};
|
|
|
|
const STATUS_BADGE_CLASS = {
|
|
draft: 'badge-neutral',
|
|
review: 'badge-info',
|
|
approved: 'badge-success',
|
|
published: 'badge-primary',
|
|
archived: 'badge-neutral'
|
|
};
|
|
|
|
// ── State ───────────────────────────────────────────────
|
|
let currentView = 'list'; // 'list' | 'detail'
|
|
let policiesData = [];
|
|
|
|
// ── Tab switching ───────────────────────────────────────
|
|
function switchTab(tab) {
|
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
|
|
if (tab === 'list') {
|
|
document.querySelectorAll('.tab-btn')[0].classList.add('active');
|
|
document.getElementById('tab-list').classList.add('active');
|
|
} else if (tab === 'templates') {
|
|
document.querySelectorAll('.tab-btn')[1].classList.add('active');
|
|
document.getElementById('tab-templates').classList.add('active');
|
|
loadTemplates();
|
|
}
|
|
|
|
// Hide detail view when switching tabs
|
|
document.getElementById('policy-detail-view').style.display = 'none';
|
|
document.querySelector('.tabs').style.display = '';
|
|
document.getElementById('tab-list').style.display = '';
|
|
document.getElementById('tab-templates').style.display = '';
|
|
currentView = 'list';
|
|
}
|
|
|
|
// ── Load Policies ───────────────────────────────────────
|
|
async function loadPolicies() {
|
|
const params = {};
|
|
const category = document.getElementById('filter-category').value;
|
|
const status = document.getElementById('filter-status').value;
|
|
if (category) params.category = category;
|
|
if (status) params.status = status;
|
|
|
|
const container = document.getElementById('policies-table');
|
|
container.innerHTML = '<div class="loading-overlay"><div class="spinner"></div><span>Caricamento policy...</span></div>';
|
|
|
|
try {
|
|
const result = await api.listPolicies(params);
|
|
if (result.success) {
|
|
policiesData = result.data || [];
|
|
renderPoliciesTable(policiesData);
|
|
} 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><p>Impossibile caricare le policy.</p></div>';
|
|
}
|
|
}
|
|
|
|
function renderPoliciesTable(policies) {
|
|
const container = document.getElementById('policies-table');
|
|
|
|
if (!policies || policies.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state" style="padding:40px 20px;">
|
|
<svg viewBox="0 0 20 20" fill="currentColor" style="width:48px;height:48px;color:var(--gray-300);">
|
|
<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>
|
|
<h4>Nessuna policy trovata</h4>
|
|
<p>Crea la tua prima policy o utilizza un template per iniziare.</p>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
let html = `
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Titolo</th>
|
|
<th>Categoria</th>
|
|
<th>Art. NIS2</th>
|
|
<th>Versione</th>
|
|
<th>Stato</th>
|
|
<th>Prossima Revisione</th>
|
|
<th>Azioni</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>`;
|
|
|
|
policies.forEach(p => {
|
|
const catLabel = CATEGORY_LABELS[p.category] || p.category || '-';
|
|
const statusLabel = STATUS_LABELS[p.status] || p.status || 'Bozza';
|
|
const badgeClass = STATUS_BADGE_CLASS[p.status] || 'badge-neutral';
|
|
|
|
html += `
|
|
<tr>
|
|
<td>
|
|
<span class="text-link" onclick="viewPolicy(${p.id})">${escapeHtml(p.title)}</span>
|
|
</td>
|
|
<td>${escapeHtml(catLabel)}</td>
|
|
<td>${escapeHtml(p.nis2_article || '-')}</td>
|
|
<td>${escapeHtml(p.version || '1.0')}</td>
|
|
<td><span class="badge ${badgeClass}">${escapeHtml(statusLabel)}</span></td>
|
|
<td>${formatDate(p.next_review_date)}</td>
|
|
<td>
|
|
<div class="policy-actions">
|
|
<button class="btn btn-sm btn-ghost" onclick="viewPolicy(${p.id})" title="Visualizza">
|
|
<svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14"><path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/><path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/></svg>
|
|
</button>
|
|
<button class="btn btn-sm btn-ghost" onclick="openEditModal(${p.id})" title="Modifica">
|
|
<svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg>
|
|
</button>
|
|
${p.status === 'draft' || p.status === 'review' ? `
|
|
<button class="btn btn-sm btn-success" onclick="approvePolicy(${p.id})" title="Approva">
|
|
<svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
|
|
</button>` : ''}
|
|
<button class="btn btn-sm btn-ghost" onclick="confirmDeletePolicy(${p.id}, '${escapeHtml(p.title).replace(/'/g, "\\'")}')" title="Elimina" style="color:var(--danger);">
|
|
<svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>`;
|
|
});
|
|
|
|
html += '</tbody></table>';
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
// ── View Policy Detail ──────────────────────────────────
|
|
async function viewPolicy(id) {
|
|
// Hide tabs and list
|
|
document.querySelector('.tabs').style.display = 'none';
|
|
document.getElementById('tab-list').classList.remove('active');
|
|
document.getElementById('tab-templates').classList.remove('active');
|
|
|
|
const detailView = document.getElementById('policy-detail-view');
|
|
detailView.style.display = 'block';
|
|
detailView.innerHTML = '<div class="loading-overlay"><div class="spinner"></div><span>Caricamento dettagli...</span></div>';
|
|
currentView = 'detail';
|
|
|
|
try {
|
|
const result = await api.getPolicy(id);
|
|
if (result.success && result.data) {
|
|
renderPolicyDetail(result.data);
|
|
} else {
|
|
detailView.innerHTML = '<div class="empty-state"><h4>Policy non trovata</h4></div>';
|
|
}
|
|
} catch (e) {
|
|
detailView.innerHTML = '<div class="empty-state"><h4>Errore di connessione</h4></div>';
|
|
}
|
|
}
|
|
|
|
function renderPolicyDetail(policy) {
|
|
const detailView = document.getElementById('policy-detail-view');
|
|
const catLabel = CATEGORY_LABELS[policy.category] || policy.category || '-';
|
|
const statusLabel = STATUS_LABELS[policy.status] || policy.status || 'Bozza';
|
|
const badgeClass = STATUS_BADGE_CLASS[policy.status] || 'badge-neutral';
|
|
|
|
detailView.innerHTML = `
|
|
<div class="detail-header">
|
|
<div>
|
|
<div style="margin-bottom:8px;">
|
|
<span class="text-link" onclick="backToList()" style="font-size:0.82rem;">
|
|
← Torna all'elenco
|
|
</span>
|
|
</div>
|
|
<h3>${escapeHtml(policy.title)}</h3>
|
|
<div style="margin-top:8px;">
|
|
<span class="badge ${badgeClass}">${escapeHtml(statusLabel)}</span>
|
|
${policy.ai_generated ? '<span class="badge badge-info" style="margin-left:6px;">AI Generata</span>' : ''}
|
|
</div>
|
|
${policy.ai_generated && (policy.status === 'draft' || policy.status === 'review') ? `
|
|
<div id="ai-draft-warning-${policy.id}" style="background:#fff7ed; border:1px solid #fed7aa; border-radius:var(--border-radius); padding:12px 16px; margin-top:12px; display:flex; gap:10px; align-items:flex-start; position:relative;">
|
|
<svg viewBox="0 0 20 20" fill="#c2410c" width="18" height="18" style="flex-shrink:0; margin-top:2px;"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
|
<div style="flex:1;">
|
|
<strong style="color:#c2410c; font-size:0.875rem;">Bozza AI — Revisione obbligatoria prima dell'approvazione</strong>
|
|
<p style="font-size:0.78rem; color:#92400e; margin:4px 0 0;">Questo documento e' stato generato automaticamente da un sistema AI. Prima di approvarlo, verificare che sia conforme alle procedure interne, al quadro normativo applicabile e alla realta' specifica dell'organizzazione. L'AI puo' produrre contenuti imprecisi o incompleti.</p>
|
|
</div>
|
|
<button onclick="document.getElementById('ai-draft-warning-${policy.id}').remove();" title="Chiudi" aria-label="Chiudi avviso"
|
|
style="flex-shrink:0;background:none;border:none;cursor:pointer;color:#c2410c;padding:2px;line-height:1;opacity:0.7;" onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.7">
|
|
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
|
|
</button>
|
|
</div>` : ''}
|
|
</div>
|
|
<div class="detail-header-actions">
|
|
${policy.status === 'draft' || policy.status === 'review' ? `
|
|
<button class="btn btn-success" onclick="approvePolicy(${policy.id})">
|
|
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
|
|
Approva
|
|
</button>` : ''}
|
|
<button class="btn btn-secondary" onclick="openEditModal(${policy.id})">
|
|
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg>
|
|
Modifica
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="detail-grid">
|
|
<div>
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>Contenuto Policy</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="policy-content-box">${escapeHtml(policy.content || 'Nessun contenuto disponibile.')}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div class="meta-card">
|
|
<h4>Informazioni</h4>
|
|
<div class="meta-row">
|
|
<span class="meta-label">Categoria</span>
|
|
<span class="meta-value">${escapeHtml(catLabel)}</span>
|
|
</div>
|
|
<div class="meta-row">
|
|
<span class="meta-label">Art. NIS2</span>
|
|
<span class="meta-value">${escapeHtml(policy.nis2_article || '-')}</span>
|
|
</div>
|
|
<div class="meta-row">
|
|
<span class="meta-label">Versione</span>
|
|
<span class="meta-value">${escapeHtml(policy.version || '1.0')}</span>
|
|
</div>
|
|
<div class="meta-row">
|
|
<span class="meta-label">Stato</span>
|
|
<span class="meta-value"><span class="badge ${badgeClass}">${escapeHtml(statusLabel)}</span></span>
|
|
</div>
|
|
<div class="meta-row">
|
|
<span class="meta-label">Creata il</span>
|
|
<span class="meta-value">${formatDate(policy.created_at)}</span>
|
|
</div>
|
|
<div class="meta-row">
|
|
<span class="meta-label">Aggiornata il</span>
|
|
<span class="meta-value">${formatDate(policy.updated_at)}</span>
|
|
</div>
|
|
<div class="meta-row">
|
|
<span class="meta-label">Prossima Revisione</span>
|
|
<span class="meta-value">${formatDate(policy.next_review_date)}</span>
|
|
</div>
|
|
${policy.approved_by_name ? `
|
|
<div class="meta-row">
|
|
<span class="meta-label">Approvata da</span>
|
|
<span class="meta-value">${escapeHtml(policy.approved_by_name)}</span>
|
|
</div>
|
|
<div class="meta-row">
|
|
<span class="meta-label">Approvata il</span>
|
|
<span class="meta-value">${formatDate(policy.approved_at)}</span>
|
|
</div>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function backToList() {
|
|
document.getElementById('policy-detail-view').style.display = 'none';
|
|
document.querySelector('.tabs').style.display = '';
|
|
switchTab('list');
|
|
currentView = 'list';
|
|
}
|
|
|
|
// ── Create / Edit Modal ─────────────────────────────────
|
|
function openCreateModal() {
|
|
showPolicyFormModal(null);
|
|
}
|
|
|
|
async function openEditModal(id) {
|
|
try {
|
|
const result = await api.getPolicy(id);
|
|
if (result.success && result.data) {
|
|
showPolicyFormModal(result.data);
|
|
} else {
|
|
showNotification('Impossibile caricare la policy.', 'error');
|
|
}
|
|
} catch (e) {
|
|
showNotification('Errore di connessione.', 'error');
|
|
}
|
|
}
|
|
|
|
function showPolicyFormModal(policy) {
|
|
const isEdit = !!policy;
|
|
const title = isEdit ? 'Modifica Policy' : 'Nuova Policy';
|
|
|
|
const categoryOptions = Object.entries(CATEGORY_LABELS).map(([val, label]) =>
|
|
`<option value="${val}" ${policy && policy.category === val ? 'selected' : ''}>${label}</option>`
|
|
).join('');
|
|
|
|
const content = `
|
|
<form id="policy-form" onsubmit="return false;">
|
|
<div class="form-group">
|
|
<label class="form-label">Titolo <span class="required">*</span></label>
|
|
<input type="text" class="form-input" id="form-title" value="${isEdit ? escapeHtml(policy.title) : ''}" required placeholder="Inserisci il titolo della policy">
|
|
</div>
|
|
<div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
|
|
<div class="form-group">
|
|
<label class="form-label">Categoria <span class="required">*</span></label>
|
|
<select class="form-select" id="form-category" required>
|
|
<option value="">Seleziona...</option>
|
|
${categoryOptions}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Articolo NIS2</label>
|
|
<input type="text" class="form-input" id="form-nis2-article" value="${isEdit && policy.nis2_article ? escapeHtml(policy.nis2_article) : ''}" placeholder="es. 21.2.a">
|
|
</div>
|
|
</div>
|
|
<div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
|
|
<div class="form-group">
|
|
<label class="form-label">Versione</label>
|
|
<input type="text" class="form-input" id="form-version" value="${isEdit && policy.version ? escapeHtml(policy.version) : '1.0'}" placeholder="1.0">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Prossima Revisione</label>
|
|
<input type="date" class="form-input" id="form-review-date" value="${isEdit && policy.next_review_date ? policy.next_review_date.substring(0, 10) : ''}">
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Contenuto</label>
|
|
<textarea class="form-textarea" id="form-content" rows="12" placeholder="Inserisci il contenuto della policy... Puoi strutturare il documento con sezioni: 1. Scopo 2. Ambito 3. Responsabilita' 4. Procedure 5. Revisione">${isEdit && policy.content ? escapeHtml(policy.content) : ''}</textarea>
|
|
</div>
|
|
</form>
|
|
`;
|
|
|
|
showModal(title, content, {
|
|
size: 'lg',
|
|
footer: `
|
|
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
|
|
<button class="btn btn-primary" onclick="savePolicy(${isEdit ? policy.id : 'null'})">${isEdit ? 'Salva Modifiche' : 'Crea Policy'}</button>
|
|
`
|
|
});
|
|
}
|
|
|
|
async function savePolicy(id) {
|
|
const title = document.getElementById('form-title').value.trim();
|
|
const category = document.getElementById('form-category').value;
|
|
|
|
if (!title) {
|
|
showNotification('Il titolo e\' obbligatorio.', 'warning');
|
|
return;
|
|
}
|
|
if (!category) {
|
|
showNotification('La categoria e\' obbligatoria.', 'warning');
|
|
return;
|
|
}
|
|
|
|
const data = {
|
|
title: title,
|
|
category: category,
|
|
nis2_article: document.getElementById('form-nis2-article').value.trim(),
|
|
version: document.getElementById('form-version').value.trim() || '1.0',
|
|
content: document.getElementById('form-content').value,
|
|
next_review_date: document.getElementById('form-review-date').value || null,
|
|
};
|
|
|
|
const btn = document.querySelector('#modal-overlay .btn-primary');
|
|
setButtonLoading(btn, true);
|
|
try {
|
|
let result;
|
|
if (id) {
|
|
result = await api.updatePolicy(id, data);
|
|
} else {
|
|
result = await api.createPolicy(data);
|
|
}
|
|
|
|
if (result.success) {
|
|
closeModal();
|
|
showNotification(id ? 'Policy aggiornata con successo.' : 'Policy creata con successo.', 'success');
|
|
loadPolicies();
|
|
if (currentView === 'detail' && id) {
|
|
viewPolicy(id);
|
|
}
|
|
} else {
|
|
setButtonLoading(btn, false);
|
|
showNotification(result.message || 'Errore nel salvataggio.', 'error');
|
|
}
|
|
} catch (e) {
|
|
setButtonLoading(btn, false);
|
|
showNotification('Errore di connessione.', 'error');
|
|
}
|
|
}
|
|
|
|
// ── Approve Policy ──────────────────────────────────────
|
|
async function approvePolicy(id) {
|
|
showModal('Conferma Approvazione', `
|
|
<p>Sei sicuro di voler approvare questa policy?</p>
|
|
<p style="margin-top:8px;font-size:0.85rem;color:var(--gray-500);">Lo stato verra' modificato in "Approvata" e non sara' piu' possibile annullare l'operazione.</p>
|
|
`, {
|
|
size: 'sm',
|
|
footer: `
|
|
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
|
|
<button class="btn btn-success" onclick="doApprovePolicy(${id})">Approva</button>
|
|
`
|
|
});
|
|
}
|
|
|
|
async function doApprovePolicy(id) {
|
|
closeModal();
|
|
try {
|
|
const result = await api.approvePolicy(id);
|
|
if (result.success) {
|
|
showNotification('Policy approvata con successo.', 'success');
|
|
loadPolicies();
|
|
if (currentView === 'detail') {
|
|
viewPolicy(id);
|
|
}
|
|
} else {
|
|
showNotification(result.message || 'Errore nell\'approvazione.', 'error');
|
|
}
|
|
} catch (e) {
|
|
showNotification('Errore di connessione.', 'error');
|
|
}
|
|
}
|
|
|
|
// ── Delete Policy ───────────────────────────────────────
|
|
function confirmDeletePolicy(id, title) {
|
|
showModal('Conferma Eliminazione', `
|
|
<p>Sei sicuro di voler eliminare la policy <strong>${title}</strong>?</p>
|
|
<p style="margin-top:8px;font-size:0.85rem;color:var(--danger);">Questa azione e' irreversibile.</p>
|
|
`, {
|
|
size: 'sm',
|
|
footer: `
|
|
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
|
|
<button class="btn btn-danger" onclick="doDeletePolicy(${id})">Elimina</button>
|
|
`
|
|
});
|
|
}
|
|
|
|
async function doDeletePolicy(id) {
|
|
closeModal();
|
|
try {
|
|
const result = await api.del(`/policies/${id}`);
|
|
if (result.success) {
|
|
showNotification('Policy eliminata.', 'success');
|
|
loadPolicies();
|
|
if (currentView === 'detail') {
|
|
backToList();
|
|
}
|
|
} else {
|
|
showNotification(result.message || 'Errore nell\'eliminazione.', 'error');
|
|
}
|
|
} catch (e) {
|
|
showNotification('Errore di connessione.', 'error');
|
|
}
|
|
}
|
|
|
|
// ── AI Generate ─────────────────────────────────────────
|
|
function openAIGenerateModal() {
|
|
const categoryOptions = Object.entries(CATEGORY_LABELS).map(([val, label]) =>
|
|
`<option value="${val}">${label}</option>`
|
|
).join('');
|
|
|
|
showModal('Genera Policy con AI', `
|
|
<p style="margin-bottom:16px;color:var(--gray-600);font-size:0.9rem;">
|
|
Seleziona la categoria per generare automaticamente una bozza di policy tramite intelligenza artificiale.
|
|
Il contenuto generato potra' essere rivisto e modificato prima della pubblicazione.
|
|
</p>
|
|
<div class="form-group">
|
|
<label class="form-label">Categoria Policy <span class="required">*</span></label>
|
|
<select class="form-select" id="ai-category">
|
|
${categoryOptions}
|
|
</select>
|
|
</div>
|
|
<div id="ai-preview-container" style="display:none;"></div>
|
|
`, {
|
|
footer: `
|
|
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
|
|
<button class="btn btn-primary" id="ai-generate-btn" onclick="doAIGenerate()">
|
|
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-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-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 8.512a1 1 0 010 1.836l-3.354 1.311-1.18 4.456a1 1 0 01-1.932 0L9.854 11.66 6.5 10.348a1 1 0 010-1.836l3.354-1.311 1.18-4.456A1 1 0 0112 2z"/></svg>
|
|
Genera
|
|
</button>
|
|
`
|
|
});
|
|
}
|
|
|
|
let aiGeneratedData = null;
|
|
|
|
async function doAIGenerate() {
|
|
const category = document.getElementById('ai-category').value;
|
|
const btn = document.getElementById('ai-generate-btn');
|
|
const previewContainer = document.getElementById('ai-preview-container');
|
|
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<div class="spinner" style="width:16px;height:16px;border-width:2px;margin:0 auto;"></div> Generazione...';
|
|
|
|
try {
|
|
const result = await api.aiGeneratePolicy(category);
|
|
if (result.success && result.data) {
|
|
aiGeneratedData = result.data;
|
|
previewContainer.style.display = 'block';
|
|
previewContainer.innerHTML = `
|
|
<div class="ai-preview-header">
|
|
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-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-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 8.512a1 1 0 010 1.836l-3.354 1.311-1.18 4.456a1 1 0 01-1.932 0L9.854 11.66 6.5 10.348a1 1 0 010-1.836l3.354-1.311 1.18-4.456A1 1 0 0112 2z"/></svg>
|
|
Policy Generata: ${escapeHtml(aiGeneratedData.title || '')}
|
|
</div>
|
|
<div class="ai-preview">${escapeHtml(aiGeneratedData.content || '')}</div>
|
|
`;
|
|
|
|
// Change footer buttons
|
|
const footer = btn.closest('.modal-footer');
|
|
footer.innerHTML = `
|
|
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
|
|
<button class="btn btn-primary" onclick="saveAIGeneratedPolicy()">Salva come Bozza</button>
|
|
`;
|
|
} else {
|
|
showNotification(result.message || 'Errore nella generazione AI.', 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = 'Genera';
|
|
}
|
|
} catch (e) {
|
|
showNotification('Errore di connessione.', 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = 'Genera';
|
|
}
|
|
}
|
|
|
|
async function saveAIGeneratedPolicy() {
|
|
if (!aiGeneratedData) return;
|
|
|
|
const data = {
|
|
title: aiGeneratedData.title || 'Policy Generata AI',
|
|
category: aiGeneratedData.category || document.getElementById('ai-category')?.value || 'information_security',
|
|
nis2_article: aiGeneratedData.nis2_article || '',
|
|
content: aiGeneratedData.content || '',
|
|
version: '1.0',
|
|
ai_generated: 1,
|
|
};
|
|
|
|
const btn = document.querySelector('#modal-overlay .btn-primary');
|
|
setButtonLoading(btn, true);
|
|
try {
|
|
const result = await api.createPolicy(data);
|
|
if (result.success) {
|
|
closeModal();
|
|
showNotification('Policy AI salvata come bozza.', 'success');
|
|
aiGeneratedData = null;
|
|
loadPolicies();
|
|
} else {
|
|
setButtonLoading(btn, false);
|
|
showNotification(result.message || 'Errore nel salvataggio.', 'error');
|
|
}
|
|
} catch (e) {
|
|
setButtonLoading(btn, false);
|
|
showNotification('Errore di connessione.', 'error');
|
|
}
|
|
}
|
|
|
|
// ── Templates ───────────────────────────────────────────
|
|
async function loadTemplates() {
|
|
const container = document.getElementById('templates-grid');
|
|
container.innerHTML = '<div class="loading-overlay"><div class="spinner"></div><span>Caricamento template...</span></div>';
|
|
|
|
try {
|
|
const result = await api.getPolicyTemplates();
|
|
if (result.success && result.data) {
|
|
renderTemplates(result.data);
|
|
} else {
|
|
container.innerHTML = '<div class="empty-state"><h4>Nessun template disponibile</h4></div>';
|
|
}
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state"><h4>Errore di connessione</h4></div>';
|
|
}
|
|
}
|
|
|
|
function renderTemplates(templates) {
|
|
const container = document.getElementById('templates-grid');
|
|
|
|
if (!templates || templates.length === 0) {
|
|
container.innerHTML = '<div class="empty-state"><h4>Nessun template disponibile</h4></div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="template-grid">';
|
|
templates.forEach(t => {
|
|
const catLabel = CATEGORY_LABELS[t.category] || t.category;
|
|
html += `
|
|
<div class="template-card" onclick="createFromTemplate('${escapeHtml(t.category)}', '${escapeHtml(t.title).replace(/'/g, "\\'")}', '${escapeHtml(t.nis2_article || '').replace(/'/g, "\\'")}')">
|
|
<h4>${escapeHtml(t.title)}</h4>
|
|
<div class="template-meta">
|
|
<span class="tag">${escapeHtml(catLabel)}</span>
|
|
${t.nis2_article ? `<span style="color:var(--gray-400);">Art. ${escapeHtml(t.nis2_article)}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function createFromTemplate(category, title, nis2Article) {
|
|
showPolicyFormModal({
|
|
title: title,
|
|
category: category,
|
|
nis2_article: nis2Article,
|
|
version: '1.0',
|
|
content: '',
|
|
});
|
|
}
|
|
|
|
// ── Init ────────────────────────────────────────────────
|
|
loadPolicies();
|
|
</script>
|
|
</body>
|
|
</html>
|