nis2-agile/public/supply-chain.html
DevEnv nis2-agile 0e78ec24c1 [FIX] i18n funzionante + bug audit.html + help system
- 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>
2026-02-20 11:17:04 +01:00

1138 lines
52 KiB
HTML

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Supply Chain - NIS2 Agile</title>
<link rel="stylesheet" href="/css/style.css">
<style>
/* ── Supply Chain 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;
}
.supplier-actions {
display: flex;
gap: 6px;
}
.supplier-actions .btn {
padding: 5px 10px;
font-size: 0.75rem;
}
/* Risk overview cards */
.risk-overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.risk-card {
background: var(--card-bg);
border: 1px solid var(--gray-200);
border-radius: var(--border-radius);
padding: 20px;
text-align: center;
}
.risk-card-value {
font-size: 2rem;
font-weight: 700;
color: var(--gray-900);
line-height: 1;
}
.risk-card-label {
font-size: 0.78rem;
color: var(--gray-500);
margin-top: 6px;
font-weight: 500;
}
.risk-card-value.danger { color: var(--danger); }
.risk-card-value.warning { color: var(--warning); }
.risk-card-value.success { color: var(--secondary); }
/* Criticality bar chart */
.criticality-chart {
display: flex;
gap: 16px;
align-items: flex-end;
height: 140px;
padding: 16px 0;
}
.chart-bar-group {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.chart-bar {
width: 48px;
border-radius: 4px 4px 0 0;
transition: height 0.5s ease;
min-height: 4px;
position: relative;
}
.chart-bar-value {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
font-size: 0.75rem;
font-weight: 700;
color: var(--gray-700);
}
.chart-bar-label {
font-size: 0.72rem;
font-weight: 600;
color: var(--gray-500);
text-transform: uppercase;
}
/* Risk score progress */
.risk-score-bar {
width: 100%;
height: 8px;
background: var(--gray-200);
border-radius: 4px;
overflow: hidden;
display: inline-flex;
min-width: 80px;
}
.risk-score-fill {
height: 100%;
border-radius: 4px;
transition: width 0.4s ease;
}
.risk-score-cell {
display: flex;
align-items: center;
gap: 8px;
}
.risk-score-value {
font-weight: 600;
font-size: 0.82rem;
min-width: 32px;
text-align: right;
}
/* Detail view */
.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 320px;
gap: 24px;
}
@media (max-width: 900px) {
.detail-grid {
grid-template-columns: 1fr;
}
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
}
.info-item {
padding: 12px 16px;
border-bottom: 1px solid var(--gray-100);
}
.info-item:nth-child(odd) {
border-right: 1px solid var(--gray-100);
}
.info-item-label {
font-size: 0.72rem;
font-weight: 600;
color: var(--gray-400);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 4px;
}
.info-item-value {
font-size: 0.875rem;
color: var(--gray-800);
font-weight: 500;
}
.meta-card {
background: var(--card-bg);
border: 1px solid var(--gray-200);
border-radius: var(--border-radius);
padding: 20px;
margin-bottom: 16px;
}
.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; }
/* Score gauge (ring) */
.score-ring {
width: 120px;
height: 120px;
margin: 0 auto 12px;
position: relative;
}
.score-ring svg {
transform: rotate(-90deg);
}
.score-ring-bg {
fill: none;
stroke: var(--gray-200);
stroke-width: 8;
}
.score-ring-fill {
fill: none;
stroke-width: 8;
stroke-linecap: round;
transition: stroke-dashoffset 0.6s ease;
}
.score-ring-value {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.score-ring-number {
font-size: 1.75rem;
font-weight: 700;
line-height: 1;
}
.score-ring-label {
font-size: 0.65rem;
color: var(--gray-500);
text-transform: uppercase;
font-weight: 600;
}
/* Assessment questions */
.assessment-question {
padding: 14px 0;
border-bottom: 1px solid var(--gray-100);
}
.assessment-question:last-child { border-bottom: none; }
.assessment-question-text {
font-size: 0.875rem;
color: var(--gray-800);
margin-bottom: 10px;
font-weight: 500;
}
.assessment-options {
display: flex;
gap: 8px;
}
.assessment-option {
padding: 6px 16px;
border: 1px solid var(--gray-200);
border-radius: 100px;
font-size: 0.78rem;
font-weight: 600;
cursor: pointer;
transition: all var(--transition-fast);
background: var(--card-bg);
color: var(--gray-600);
}
.assessment-option:hover {
border-color: var(--primary);
color: var(--primary);
}
.assessment-option.selected {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.assessment-option.selected-partial {
background: var(--warning-bg);
border-color: var(--warning);
color: #a16207;
}
.assessment-option.selected-no {
background: var(--danger-bg);
border-color: var(--danger);
color: var(--danger);
}
.assessment-score-preview {
margin-top: 16px;
padding: 16px;
background: var(--gray-50);
border-radius: var(--border-radius);
text-align: center;
}
.assessment-score-preview .score-val {
font-size: 2rem;
font-weight: 700;
}
.assessment-score-preview .score-label {
font-size: 0.78rem;
color: var(--gray-500);
}
/* Assessment history */
.assessment-history-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 0;
border-bottom: 1px solid var(--gray-100);
font-size: 0.82rem;
}
.assessment-history-item:last-child { border-bottom: none; }
.assessment-history-date {
color: var(--gray-500);
min-width: 80px;
}
.assessment-history-score {
font-weight: 700;
}
.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="supply_chain.title">Sicurezza Supply Chain</h2>
<div class="content-header-actions">
<button class="btn btn-primary" onclick="openCreateModal()">+ Nuovo Fornitore</button>
</div>
</header>
<div class="content-body" id="page-content">
<!-- Risk Overview -->
<div class="risk-overview" id="risk-overview">
<div class="risk-card">
<div class="risk-card-value" id="stat-total">--</div>
<div class="risk-card-label">Totale Fornitori</div>
</div>
<div class="risk-card">
<div class="risk-card-value danger" id="stat-critical">--</div>
<div class="risk-card-label">Critici Non Conformi</div>
</div>
<div class="risk-card">
<div class="risk-card-value warning" id="stat-expiring">--</div>
<div class="risk-card-label">Contratti in Scadenza</div>
</div>
<div class="risk-card">
<div class="risk-card-value success" id="stat-avg-score">--</div>
<div class="risk-card-label">Score Rischio Medio</div>
</div>
</div>
<!-- Criticality Chart -->
<div class="card mb-24">
<div class="card-header">
<h3>Distribuzione Criticita'</h3>
</div>
<div class="card-body">
<div class="criticality-chart" id="criticality-chart">
<div class="chart-bar-group">
<div class="chart-bar" id="chart-bar-critical" style="height:0;background:var(--danger);"><span class="chart-bar-value" id="chart-val-critical">0</span></div>
<span class="chart-bar-label">Critico</span>
</div>
<div class="chart-bar-group">
<div class="chart-bar" id="chart-bar-high" style="height:0;background:#f97316;"><span class="chart-bar-value" id="chart-val-high">0</span></div>
<span class="chart-bar-label">Alto</span>
</div>
<div class="chart-bar-group">
<div class="chart-bar" id="chart-bar-medium" style="height:0;background:var(--warning);"><span class="chart-bar-value" id="chart-val-medium">0</span></div>
<span class="chart-bar-label">Medio</span>
</div>
<div class="chart-bar-group">
<div class="chart-bar" id="chart-bar-low" style="height:0;background:var(--secondary);"><span class="chart-bar-value" id="chart-val-low">0</span></div>
<span class="chart-bar-label">Basso</span>
</div>
</div>
</div>
</div>
<!-- Filters + Table -->
<div id="suppliers-list-section">
<div class="filters-bar">
<select class="form-select" id="filter-criticality" onchange="applyFilters()">
<option value="">Tutte le Criticita'</option>
<option value="critical">Critico</option>
<option value="high">Alto</option>
<option value="medium">Medio</option>
<option value="low">Basso</option>
</select>
<select class="form-select" id="filter-status" onchange="applyFilters()">
<option value="">Tutti gli Stati</option>
<option value="active">Attivo</option>
<option value="under_review">In Revisione</option>
<option value="suspended">Sospeso</option>
<option value="terminated">Terminato</option>
</select>
</div>
<div class="card">
<div class="table-container" id="suppliers-table">
<div class="loading-overlay">
<div class="spinner"></div>
<span>Caricamento fornitori...</span>
</div>
</div>
</div>
</div>
<!-- Detail View (hidden by default) -->
<div id="supplier-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 CRITICALITY_LABELS = {
low: 'Basso',
medium: 'Medio',
high: 'Alto',
critical: 'Critico'
};
const CRITICALITY_BADGE_CLASS = {
low: 'badge-success',
medium: 'badge-warning',
high: 'badge-danger',
critical: 'badge-danger'
};
const STATUS_LABELS = {
active: 'Attivo',
under_review: 'In Revisione',
suspended: 'Sospeso',
terminated: 'Terminato'
};
const STATUS_BADGE_CLASS = {
active: 'badge-success',
under_review: 'badge-info',
suspended: 'badge-warning',
terminated: 'badge-neutral'
};
// ── Assessment Questions ────────────────────────────────
const ASSESSMENT_QUESTIONS = [
{ id: 'security_certs', text: 'Il fornitore ha certificazioni di sicurezza (ISO 27001, SOC2)?', weight: 2 },
{ id: 'security_sla', text: 'Esistono SLA definiti per la sicurezza?', weight: 1.5 },
{ id: 'incident_plan', text: 'Il fornitore ha un piano di incident response?', weight: 2 },
{ id: 'data_encryption', text: 'I dati sono crittografati in transito e a riposo?', weight: 2 },
{ id: 'periodic_audits', text: 'Il fornitore effettua audit periodici?', weight: 1.5 },
{ id: 'access_control', text: 'Il fornitore implementa controlli di accesso robusti?', weight: 1 },
{ id: 'data_backup', text: 'Il fornitore dispone di procedure di backup e disaster recovery?', weight: 1.5 },
{ id: 'compliance_gdpr', text: 'Il fornitore e\' conforme al GDPR?', weight: 1 },
{ id: 'security_training', text: 'Il personale del fornitore riceve formazione sulla sicurezza?', weight: 1 },
{ id: 'vulnerability_mgmt', text: 'Il fornitore gestisce attivamente le vulnerabilita\'?', weight: 1.5 }
];
// ── State ───────────────────────────────────────────────
let allSuppliers = [];
let currentView = 'list';
// ── Load Suppliers ──────────────────────────────────────
async function loadSuppliers() {
const container = document.getElementById('suppliers-table');
container.innerHTML = '<div class="loading-overlay"><div class="spinner"></div><span>Caricamento fornitori...</span></div>';
try {
const result = await api.listSuppliers();
if (result.success) {
allSuppliers = result.data || [];
updateRiskOverview(allSuppliers);
updateCriticalityChart(allSuppliers);
applyFilters();
} else {
container.innerHTML = '<div class="empty-state"><h4>Errore nel caricamento</h4><p>' + escapeHtml(result.message || '') + '</p></div>';
}
} catch (e) {
container.innerHTML = '<div class="empty-state"><h4>Errore di connessione</h4><p>Impossibile caricare i fornitori.</p></div>';
}
}
function applyFilters() {
const criticality = document.getElementById('filter-criticality').value;
const status = document.getElementById('filter-status').value;
let filtered = allSuppliers;
if (criticality) {
filtered = filtered.filter(s => s.criticality === criticality);
}
if (status) {
filtered = filtered.filter(s => s.status === status);
}
renderSuppliersTable(filtered);
}
function updateRiskOverview(suppliers) {
const total = suppliers.length;
const criticalNonCompliant = suppliers.filter(s =>
(s.criticality === 'critical' || s.criticality === 'high') && !s.security_requirements_met
).length;
const today = new Date();
const in90Days = new Date(today.getTime() + 90 * 86400000);
const expiring = suppliers.filter(s => {
if (!s.contract_expiry_date || s.status !== 'active') return false;
const exp = new Date(s.contract_expiry_date);
return exp <= in90Days && exp >= today;
}).length;
const scored = suppliers.filter(s => s.risk_score != null && s.risk_score !== '');
const avgScore = scored.length > 0
? Math.round(scored.reduce((sum, s) => sum + Number(s.risk_score), 0) / scored.length)
: 0;
document.getElementById('stat-total').textContent = total;
document.getElementById('stat-critical').textContent = criticalNonCompliant;
document.getElementById('stat-expiring').textContent = expiring;
document.getElementById('stat-avg-score').textContent = avgScore > 0 ? avgScore + '/100' : '-';
}
function updateCriticalityChart(suppliers) {
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
suppliers.forEach(s => {
if (counts.hasOwnProperty(s.criticality)) {
counts[s.criticality]++;
}
});
const maxCount = Math.max(...Object.values(counts), 1);
Object.entries(counts).forEach(([level, count]) => {
const bar = document.getElementById('chart-bar-' + level);
const val = document.getElementById('chart-val-' + level);
if (bar) {
const height = maxCount > 0 ? Math.max((count / maxCount) * 100, count > 0 ? 10 : 4) : 4;
bar.style.height = height + 'px';
}
if (val) val.textContent = count;
});
}
function renderSuppliersTable(suppliers) {
const container = document.getElementById('suppliers-table');
if (!suppliers || suppliers.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="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd"/>
</svg>
<h4>Nessun fornitore trovato</h4>
<p>Aggiungi il tuo primo fornitore per iniziare il monitoraggio della supply chain.</p>
</div>`;
return;
}
let html = `
<table>
<thead>
<tr>
<th>Nome</th>
<th>Tipo Servizio</th>
<th>Criticita'</th>
<th>Score Rischio</th>
<th>Stato</th>
<th>Ultima Valutazione</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>`;
suppliers.forEach(s => {
const critLabel = CRITICALITY_LABELS[s.criticality] || s.criticality || '-';
const critBadge = CRITICALITY_BADGE_CLASS[s.criticality] || 'badge-neutral';
const statusLabel = STATUS_LABELS[s.status] || s.status || 'Attivo';
const statusBadge = STATUS_BADGE_CLASS[s.status] || 'badge-neutral';
const riskScore = s.risk_score != null && s.risk_score !== '' ? Number(s.risk_score) : null;
const scoreColor = riskScore !== null ? getScoreColor(riskScore) : 'var(--gray-300)';
html += `
<tr>
<td>
<span class="text-link" onclick="viewSupplier(${s.id})">${escapeHtml(s.name)}</span>
</td>
<td>${escapeHtml(s.service_type || '-')}</td>
<td>
<span class="badge ${critBadge}">${escapeHtml(critLabel)}</span>
</td>
<td>
<div class="risk-score-cell">
<div class="risk-score-bar">
<div class="risk-score-fill" style="width:${riskScore !== null ? riskScore : 0}%;background:${scoreColor};"></div>
</div>
<span class="risk-score-value" style="color:${scoreColor};">${riskScore !== null ? riskScore : '-'}</span>
</div>
</td>
<td><span class="badge ${statusBadge}">${escapeHtml(statusLabel)}</span></td>
<td>${formatDate(s.last_assessment_date)}</td>
<td>
<div class="supplier-actions">
<button class="btn btn-sm btn-ghost" onclick="viewSupplier(${s.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(${s.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>
<button class="btn btn-sm btn-primary" onclick="openAssessmentModal(${s.id})" title="Valuta">
<svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14"><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-2V5zm9.707 5.707a1 1 0 00-1.414-1.414L9 12.586l-1.293-1.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>
</button>
</div>
</td>
</tr>`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
// ── View Supplier Detail ────────────────────────────────
async function viewSupplier(id) {
// Hide overview and list
document.getElementById('risk-overview').style.display = 'none';
document.querySelector('.card.mb-24').style.display = 'none';
document.getElementById('suppliers-list-section').style.display = 'none';
const detailView = document.getElementById('supplier-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.getSupplier(id);
if (result.success && result.data) {
renderSupplierDetail(result.data);
} else {
detailView.innerHTML = '<div class="empty-state"><h4>Fornitore non trovato</h4></div>';
}
} catch (e) {
detailView.innerHTML = '<div class="empty-state"><h4>Errore di connessione</h4></div>';
}
}
function renderSupplierDetail(supplier) {
const detailView = document.getElementById('supplier-detail-view');
const critLabel = CRITICALITY_LABELS[supplier.criticality] || supplier.criticality || '-';
const critBadge = CRITICALITY_BADGE_CLASS[supplier.criticality] || 'badge-neutral';
const statusLabel = STATUS_LABELS[supplier.status] || supplier.status || 'Attivo';
const statusBadge = STATUS_BADGE_CLASS[supplier.status] || 'badge-neutral';
const riskScore = supplier.risk_score != null && supplier.risk_score !== '' ? Number(supplier.risk_score) : null;
const scoreColor = riskScore !== null ? getScoreColor(riskScore) : 'var(--gray-300)';
// Score ring SVG
const ringSize = 120;
const ringRadius = (ringSize / 2) - 10;
const ringCircumference = 2 * Math.PI * ringRadius;
const ringOffset = riskScore !== null
? ringCircumference - (riskScore / 100) * ringCircumference
: ringCircumference;
detailView.innerHTML = `
<div class="detail-header">
<div>
<div style="margin-bottom:8px;">
<span class="text-link" onclick="backToList()" style="font-size:0.82rem;">
&larr; Torna all'elenco
</span>
</div>
<h3>${escapeHtml(supplier.name)}</h3>
<div style="margin-top:8px;display:flex;gap:8px;">
<span class="badge ${critBadge}">${escapeHtml(critLabel)}</span>
<span class="badge ${statusBadge}">${escapeHtml(statusLabel)}</span>
${supplier.security_requirements_met ? '<span class="badge badge-success">Conforme</span>' : '<span class="badge badge-danger">Non Conforme</span>'}
</div>
</div>
<div class="detail-header-actions">
<button class="btn btn-primary" onclick="openAssessmentModal(${supplier.id})">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><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-2V5zm9.707 5.707a1 1 0 00-1.414-1.414L9 12.586l-1.293-1.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>
Valuta Fornitore
</button>
<button class="btn btn-secondary" onclick="openEditModal(${supplier.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" style="margin-bottom:16px;">
<div class="card-header">
<h3>Informazioni Fornitore</h3>
</div>
<div class="card-body" style="padding:0;">
<div class="info-grid">
<div class="info-item">
<div class="info-item-label">Nome</div>
<div class="info-item-value">${escapeHtml(supplier.name)}</div>
</div>
<div class="info-item">
<div class="info-item-label">Partita IVA</div>
<div class="info-item-value">${escapeHtml(supplier.vat_number || '-')}</div>
</div>
<div class="info-item">
<div class="info-item-label">Referente</div>
<div class="info-item-value">${escapeHtml(supplier.contact_name || '-')}</div>
</div>
<div class="info-item">
<div class="info-item-label">Email</div>
<div class="info-item-value">${supplier.contact_email ? '<a href="mailto:' + escapeHtml(supplier.contact_email) + '">' + escapeHtml(supplier.contact_email) + '</a>' : '-'}</div>
</div>
<div class="info-item">
<div class="info-item-label">Tipo Servizio</div>
<div class="info-item-value">${escapeHtml(supplier.service_type || '-')}</div>
</div>
<div class="info-item">
<div class="info-item-label">Criticita'</div>
<div class="info-item-value"><span class="badge ${critBadge}">${escapeHtml(critLabel)}</span></div>
</div>
<div class="info-item">
<div class="info-item-label">Inizio Contratto</div>
<div class="info-item-value">${formatDate(supplier.contract_start_date)}</div>
</div>
<div class="info-item">
<div class="info-item-label">Scadenza Contratto</div>
<div class="info-item-value">${formatDate(supplier.contract_expiry_date)}</div>
</div>
</div>
</div>
</div>
${supplier.service_description ? `
<div class="card" style="margin-bottom:16px;">
<div class="card-header"><h3>Descrizione Servizio</h3></div>
<div class="card-body">
<p style="font-size:0.875rem;color:var(--gray-700);white-space:pre-wrap;">${escapeHtml(supplier.service_description)}</p>
</div>
</div>` : ''}
${supplier.notes ? `
<div class="card">
<div class="card-header"><h3>Note</h3></div>
<div class="card-body">
<p style="font-size:0.875rem;color:var(--gray-700);white-space:pre-wrap;">${escapeHtml(supplier.notes)}</p>
</div>
</div>` : ''}
</div>
<div>
<!-- Risk Score Gauge -->
<div class="meta-card" style="text-align:center;">
<h4 style="text-align:left;">Score Rischio</h4>
<div class="score-ring">
<svg width="${ringSize}" height="${ringSize}" viewBox="0 0 ${ringSize} ${ringSize}">
<circle class="score-ring-bg" cx="${ringSize/2}" cy="${ringSize/2}" r="${ringRadius}"/>
<circle class="score-ring-fill" cx="${ringSize/2}" cy="${ringSize/2}" r="${ringRadius}"
stroke="${scoreColor}"
stroke-dasharray="${ringCircumference}"
stroke-dashoffset="${ringOffset}"/>
</svg>
<div class="score-ring-value">
<div class="score-ring-number" style="color:${scoreColor};">${riskScore !== null ? riskScore : '-'}</div>
<div class="score-ring-label">su 100</div>
</div>
</div>
<p style="font-size:0.78rem;color:var(--gray-500);margin-top:8px;">
${riskScore !== null
? (riskScore >= 70 ? 'Requisiti di sicurezza soddisfatti' : 'Requisiti di sicurezza non soddisfatti')
: 'Nessuna valutazione effettuata'}
</p>
</div>
<!-- Assessment Info -->
<div class="meta-card">
<h4>Valutazione</h4>
<div class="meta-row">
<span class="meta-label">Ultima Valutazione</span>
<span class="meta-value">${formatDate(supplier.last_assessment_date)}</span>
</div>
<div class="meta-row">
<span class="meta-label">Prossima Valutazione</span>
<span class="meta-value">${formatDate(supplier.next_assessment_date)}</span>
</div>
<div class="meta-row">
<span class="meta-label">Requisiti Sicurezza</span>
<span class="meta-value">
${supplier.security_requirements_met
? '<span class="badge badge-success">Soddisfatti</span>'
: '<span class="badge badge-danger">Non Soddisfatti</span>'}
</span>
</div>
<div class="meta-row">
<span class="meta-label">Creato il</span>
<span class="meta-value">${formatDate(supplier.created_at)}</span>
</div>
</div>
</div>
</div>
`;
}
function backToList() {
document.getElementById('supplier-detail-view').style.display = 'none';
document.getElementById('risk-overview').style.display = '';
document.querySelector('.card.mb-24').style.display = '';
document.getElementById('suppliers-list-section').style.display = '';
currentView = 'list';
}
// ── Create / Edit Modal ─────────────────────────────────
function openCreateModal() {
showSupplierFormModal(null);
}
async function openEditModal(id) {
try {
const result = await api.getSupplier(id);
if (result.success && result.data) {
showSupplierFormModal(result.data);
} else {
showNotification('Impossibile caricare il fornitore.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
}
function showSupplierFormModal(supplier) {
const isEdit = !!supplier;
const title = isEdit ? 'Modifica Fornitore' : 'Nuovo Fornitore';
const criticalityOptions = Object.entries(CRITICALITY_LABELS).map(([val, label]) =>
`<option value="${val}" ${supplier && supplier.criticality === val ? 'selected' : ''}>${label}</option>`
).join('');
const content = `
<form id="supplier-form" onsubmit="return false;">
<div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="form-group">
<label class="form-label">Nome Fornitore <span class="required">*</span></label>
<input type="text" class="form-input" id="form-name" value="${isEdit ? escapeHtml(supplier.name) : ''}" required placeholder="Nome dell'azienda">
</div>
<div class="form-group">
<label class="form-label">Partita IVA</label>
<input type="text" class="form-input" id="form-vat" value="${isEdit && supplier.vat_number ? escapeHtml(supplier.vat_number) : ''}" placeholder="IT01234567890">
</div>
</div>
<div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="form-group">
<label class="form-label">Referente</label>
<input type="text" class="form-input" id="form-contact-name" value="${isEdit && supplier.contact_name ? escapeHtml(supplier.contact_name) : ''}" placeholder="Nome e cognome">
</div>
<div class="form-group">
<label class="form-label">Email Contatto</label>
<input type="email" class="form-input" id="form-contact-email" value="${isEdit && supplier.contact_email ? escapeHtml(supplier.contact_email) : ''}" placeholder="email@azienda.it">
</div>
</div>
<div class="form-row" style="display:grid;grid-template-columns:2fr 1fr;gap:16px;">
<div class="form-group">
<label class="form-label">Tipo Servizio <span class="required">*</span></label>
<input type="text" class="form-input" id="form-service-type" value="${isEdit && supplier.service_type ? escapeHtml(supplier.service_type) : ''}" required placeholder="es. Cloud Hosting, Sviluppo SW, Consulenza">
</div>
<div class="form-group">
<label class="form-label">Criticita'</label>
<select class="form-select" id="form-criticality">
${criticalityOptions}
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">Descrizione Servizio</label>
<textarea class="form-textarea" id="form-service-desc" rows="3" placeholder="Descrivi il servizio fornito...">${isEdit && supplier.service_description ? escapeHtml(supplier.service_description) : ''}</textarea>
</div>
<div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="form-group">
<label class="form-label">Inizio Contratto</label>
<input type="date" class="form-input" id="form-contract-start" value="${isEdit && supplier.contract_start_date ? supplier.contract_start_date.substring(0, 10) : ''}">
</div>
<div class="form-group">
<label class="form-label">Scadenza Contratto</label>
<input type="date" class="form-input" id="form-contract-expiry" value="${isEdit && supplier.contract_expiry_date ? supplier.contract_expiry_date.substring(0, 10) : ''}">
</div>
</div>
<div class="form-group">
<label class="form-label">Note</label>
<textarea class="form-textarea" id="form-notes" rows="3" placeholder="Note aggiuntive...">${isEdit && supplier.notes ? escapeHtml(supplier.notes) : ''}</textarea>
</div>
</form>
`;
showModal(title, content, {
size: 'lg',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="saveSupplier(${isEdit ? supplier.id : 'null'})">${isEdit ? 'Salva Modifiche' : 'Aggiungi Fornitore'}</button>
`
});
}
async function saveSupplier(id) {
const name = document.getElementById('form-name').value.trim();
const serviceType = document.getElementById('form-service-type').value.trim();
if (!name) {
showNotification('Il nome del fornitore e\' obbligatorio.', 'warning');
return;
}
if (!serviceType) {
showNotification('Il tipo di servizio e\' obbligatorio.', 'warning');
return;
}
const data = {
name: name,
vat_number: document.getElementById('form-vat').value.trim() || null,
contact_email: document.getElementById('form-contact-email').value.trim() || null,
contact_name: document.getElementById('form-contact-name').value.trim() || null,
service_type: serviceType,
service_description: document.getElementById('form-service-desc').value.trim() || null,
criticality: document.getElementById('form-criticality').value,
contract_start_date: document.getElementById('form-contract-start').value || null,
contract_expiry_date: document.getElementById('form-contract-expiry').value || null,
notes: document.getElementById('form-notes').value.trim() || null,
};
try {
let result;
if (id) {
result = await api.updateSupplier(id, data);
} else {
result = await api.createSupplier(data);
}
if (result.success) {
closeModal();
showNotification(id ? 'Fornitore aggiornato con successo.' : 'Fornitore aggiunto con successo.', 'success');
loadSuppliers();
if (currentView === 'detail' && id) {
viewSupplier(id);
}
} else {
showNotification(result.message || 'Errore nel salvataggio.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
}
// ── Assessment Modal ────────────────────────────────────
let assessmentAnswers = {};
function openAssessmentModal(supplierId) {
assessmentAnswers = {};
let questionsHtml = '';
ASSESSMENT_QUESTIONS.forEach((q, idx) => {
questionsHtml += `
<div class="assessment-question">
<div class="assessment-question-text">${idx + 1}. ${escapeHtml(q.text)}</div>
<div class="assessment-options">
<span class="assessment-option" data-q="${q.id}" data-val="yes" onclick="selectAnswer('${q.id}', 'yes', this)">Si'</span>
<span class="assessment-option" data-q="${q.id}" data-val="partial" onclick="selectAnswer('${q.id}', 'partial', this)">Parziale</span>
<span class="assessment-option" data-q="${q.id}" data-val="no" onclick="selectAnswer('${q.id}', 'no', this)">No</span>
</div>
</div>
`;
});
const content = `
<p style="margin-bottom:16px;font-size:0.9rem;color:var(--gray-600);">
Rispondi alle domande per valutare il livello di sicurezza del fornitore.
Il punteggio di rischio verra' calcolato automaticamente.
</p>
<div id="assessment-questions">
${questionsHtml}
</div>
<div class="assessment-score-preview" id="assessment-score-preview">
<div class="score-label">Score Rischio Calcolato</div>
<div class="score-val" id="assessment-live-score" style="color:var(--gray-300);">--</div>
</div>
`;
showModal('Valutazione Fornitore', content, {
size: 'lg',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="submitAssessment(${supplierId})">Salva Valutazione</button>
`
});
}
function selectAnswer(questionId, value, el) {
// Remove selected from siblings
const parent = el.parentElement;
parent.querySelectorAll('.assessment-option').forEach(opt => {
opt.classList.remove('selected', 'selected-partial', 'selected-no');
});
// Add selected class
if (value === 'yes') {
el.classList.add('selected');
} else if (value === 'partial') {
el.classList.add('selected-partial');
} else {
el.classList.add('selected-no');
}
// Store answer
const question = ASSESSMENT_QUESTIONS.find(q => q.id === questionId);
assessmentAnswers[questionId] = {
id: questionId,
value: value,
weight: question ? question.weight : 1
};
// Update live score
updateLiveScore();
}
function updateLiveScore() {
const answers = Object.values(assessmentAnswers);
if (answers.length === 0) {
document.getElementById('assessment-live-score').textContent = '--';
document.getElementById('assessment-live-score').style.color = 'var(--gray-300)';
return;
}
let totalScore = 0;
let totalWeight = 0;
answers.forEach(a => {
const weight = a.weight || 1;
let val = 0;
if (a.value === 'yes') val = 100;
else if (a.value === 'partial') val = 50;
totalScore += val * weight;
totalWeight += 100 * weight;
});
const score = totalWeight > 0 ? Math.round(totalScore / totalWeight * 100) : 0;
const scoreEl = document.getElementById('assessment-live-score');
scoreEl.textContent = score + '/100';
scoreEl.style.color = getScoreColor(score);
}
async function submitAssessment(supplierId) {
const answers = Object.values(assessmentAnswers);
if (answers.length === 0) {
showNotification('Rispondi ad almeno una domanda prima di salvare.', 'warning');
return;
}
try {
const result = await api.assessSupplier(supplierId, {
assessment_responses: answers
});
if (result.success) {
closeModal();
const score = result.data?.risk_score;
showNotification(
'Valutazione completata. Score: ' + (score != null ? score + '/100' : 'N/D'),
'success'
);
loadSuppliers();
if (currentView === 'detail') {
viewSupplier(supplierId);
}
} else {
showNotification(result.message || 'Errore nel salvataggio della valutazione.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
}
// ── Init ────────────────────────────────────────────────
loadSuppliers();
</script>
</body>
</html>