- register.html: step 0 scelta profilo (Azienda / Consulente) - onboarding.html: wizard 4-step con P.IVA obbligatoria (auto-fetch CertiSource) - companies.html: nuova dashboard consulente con cards aziende e compliance score - common.js: org-switcher sidebar + role labels corretti per consulente - login.html: routing post-login (consulente → companies.html) - api.js: isConsultant(), setUserRole(), register con user_type - AuthController: user_type=consultant → role=consultant in users table - OnboardingController: multi-org per consulente, duplicate VAT check - 005_consultant_support.sql: aggiunge 'consultant' a user_organizations.role ENUM Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
549 lines
22 KiB
HTML
549 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Le Mie Aziende - NIS2 Agile</title>
|
|
<link rel="stylesheet" href="css/style.css">
|
|
<style>
|
|
.companies-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
margin-bottom: 28px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.companies-header h1 {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: var(--gray-900);
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.companies-header-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.search-box {
|
|
position: relative;
|
|
min-width: 240px;
|
|
}
|
|
|
|
.search-box svg {
|
|
position: absolute;
|
|
left: 12px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: var(--gray-400);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.search-box input {
|
|
padding-left: 38px;
|
|
}
|
|
|
|
/* ── Company Cards Grid ─────────────────────────────────────── */
|
|
.companies-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
.company-card {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--gray-200);
|
|
border-radius: var(--border-radius-lg);
|
|
padding: 24px;
|
|
transition: all var(--transition);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.company-card:hover {
|
|
box-shadow: var(--card-shadow-hover);
|
|
border-color: var(--primary-light);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.company-card-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 14px;
|
|
}
|
|
|
|
.company-card-avatar {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: var(--border-radius);
|
|
background: var(--primary-bg);
|
|
color: var(--primary);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 700;
|
|
font-size: 1.1rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.company-card-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.company-card-name {
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
color: var(--gray-900);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.company-card-vat {
|
|
font-size: 0.8125rem;
|
|
color: var(--gray-500);
|
|
font-family: monospace;
|
|
}
|
|
|
|
.entity-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 3px 10px;
|
|
border-radius: 99px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.entity-badge.essential {
|
|
background: rgba(234, 67, 53, 0.1);
|
|
color: #c5221f;
|
|
}
|
|
|
|
.entity-badge.important {
|
|
background: rgba(251, 188, 4, 0.15);
|
|
color: #b06000;
|
|
}
|
|
|
|
.entity-badge.not_applicable,
|
|
.entity-badge.voluntary {
|
|
background: var(--gray-100);
|
|
color: var(--gray-500);
|
|
}
|
|
|
|
/* ── Score Bar ───────────────────────────────────────────────── */
|
|
.company-score-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.company-score-label {
|
|
font-size: 0.8125rem;
|
|
color: var(--gray-500);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.company-score-bar {
|
|
flex: 1;
|
|
height: 6px;
|
|
background: var(--gray-100);
|
|
border-radius: 99px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.company-score-bar-fill {
|
|
height: 100%;
|
|
border-radius: 99px;
|
|
transition: width 0.6s ease;
|
|
}
|
|
|
|
.company-score-value {
|
|
font-size: 0.875rem;
|
|
font-weight: 700;
|
|
min-width: 36px;
|
|
text-align: right;
|
|
}
|
|
|
|
/* ── Stats Row ───────────────────────────────────────────────── */
|
|
.company-stats {
|
|
display: flex;
|
|
gap: 16px;
|
|
}
|
|
|
|
.company-stat {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.company-stat-value {
|
|
font-size: 1.125rem;
|
|
font-weight: 700;
|
|
color: var(--gray-900);
|
|
}
|
|
|
|
.company-stat-label {
|
|
font-size: 0.75rem;
|
|
color: var(--gray-500);
|
|
}
|
|
|
|
/* ── Card Footer ─────────────────────────────────────────────── */
|
|
.company-card-footer {
|
|
border-top: 1px solid var(--gray-100);
|
|
padding-top: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
}
|
|
|
|
.company-card-sector {
|
|
font-size: 0.8125rem;
|
|
color: var(--gray-500);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
/* ── Empty State ─────────────────────────────────────────────── */
|
|
.companies-empty {
|
|
text-align: center;
|
|
padding: 80px 24px;
|
|
color: var(--gray-500);
|
|
}
|
|
|
|
.companies-empty svg {
|
|
width: 64px;
|
|
height: 64px;
|
|
color: var(--gray-300);
|
|
margin: 0 auto 20px;
|
|
display: block;
|
|
}
|
|
|
|
.companies-empty h3 {
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
color: var(--gray-700);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.companies-empty p {
|
|
font-size: 0.9375rem;
|
|
margin-bottom: 24px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* ── Loading skeleton ────────────────────────────────────────── */
|
|
.skeleton {
|
|
background: linear-gradient(90deg, var(--gray-100) 25%, var(--gray-50) 50%, var(--gray-100) 75%);
|
|
background-size: 200% 100%;
|
|
animation: shimmer 1.5s infinite;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
@keyframes shimmer {
|
|
0% { background-position: 200% 0; }
|
|
100% { background-position: -200% 0; }
|
|
}
|
|
|
|
.skeleton-card {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--gray-200);
|
|
border-radius: var(--border-radius-lg);
|
|
padding: 24px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app-layout">
|
|
<!-- Sidebar -->
|
|
<aside id="sidebar" class="sidebar"></aside>
|
|
|
|
<!-- Main Content -->
|
|
<main class="main-content">
|
|
<div class="page-container">
|
|
|
|
<!-- Page Header -->
|
|
<div class="companies-header">
|
|
<div>
|
|
<h1>Le Mie Aziende</h1>
|
|
<p style="color:var(--gray-500);font-size:.9rem;margin-top:2px;">Gestisci il portafoglio clienti e monitora la compliance NIS2</p>
|
|
</div>
|
|
<div class="companies-header-actions">
|
|
<div class="search-box">
|
|
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"/></svg>
|
|
<input type="text" class="form-input" id="search-input" placeholder="Cerca azienda..." oninput="filterCompanies(this.value)">
|
|
</div>
|
|
<a href="onboarding.html" class="btn btn-primary">
|
|
<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/></svg>
|
|
Aggiungi Cliente
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Companies Grid -->
|
|
<div class="companies-grid" id="companies-grid">
|
|
<!-- Skeleton loaders -->
|
|
<div class="skeleton-card">
|
|
<div style="display:flex;gap:12px;align-items:center">
|
|
<div class="skeleton" style="width:48px;height:48px;border-radius:8px;flex-shrink:0"></div>
|
|
<div style="flex:1"><div class="skeleton" style="height:16px;margin-bottom:8px"></div><div class="skeleton" style="height:12px;width:60%"></div></div>
|
|
</div>
|
|
<div class="skeleton" style="height:6px"></div>
|
|
<div style="display:flex;gap:16px"><div class="skeleton" style="height:40px;flex:1"></div><div class="skeleton" style="height:40px;flex:1"></div></div>
|
|
<div class="skeleton" style="height:36px"></div>
|
|
</div>
|
|
<div class="skeleton-card">
|
|
<div style="display:flex;gap:12px;align-items:center">
|
|
<div class="skeleton" style="width:48px;height:48px;border-radius:8px;flex-shrink:0"></div>
|
|
<div style="flex:1"><div class="skeleton" style="height:16px;margin-bottom:8px"></div><div class="skeleton" style="height:12px;width:60%"></div></div>
|
|
</div>
|
|
<div class="skeleton" style="height:6px"></div>
|
|
<div style="display:flex;gap:16px"><div class="skeleton" style="height:40px;flex:1"></div><div class="skeleton" style="height:40px;flex:1"></div></div>
|
|
<div class="skeleton" style="height:36px"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty state (hidden initially) -->
|
|
<div class="companies-empty" id="companies-empty" style="display:none">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008z"/>
|
|
</svg>
|
|
<h3>Nessuna azienda ancora</h3>
|
|
<p>Aggiungi il tuo primo cliente per iniziare a monitorare la compliance NIS2.</p>
|
|
<a href="onboarding.html" class="btn btn-primary">
|
|
<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/></svg>
|
|
Aggiungi Primo Cliente
|
|
</a>
|
|
</div>
|
|
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script src="js/api.js"></script>
|
|
<script src="js/common.js"></script>
|
|
<script>
|
|
if (!checkAuth()) throw new Error('Not authenticated');
|
|
|
|
// Check that user is consultant, else redirect
|
|
if (!api.isConsultant()) {
|
|
window.location.href = 'dashboard.html';
|
|
throw new Error('Not consultant');
|
|
}
|
|
|
|
loadSidebar();
|
|
|
|
// ── Data ────────────────────────────────────────────────────────
|
|
let allCompanies = [];
|
|
|
|
// ── Load companies ───────────────────────────────────────────────
|
|
async function loadCompanies() {
|
|
const result = await api.listOrganizations();
|
|
const grid = document.getElementById('companies-grid');
|
|
const emptyState = document.getElementById('companies-empty');
|
|
|
|
if (!result.success) {
|
|
showNotification('Errore nel caricamento delle aziende.', 'error');
|
|
grid.innerHTML = '';
|
|
emptyState.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
const orgs = result.data || [];
|
|
allCompanies = orgs;
|
|
|
|
if (orgs.length === 0) {
|
|
grid.innerHTML = '';
|
|
emptyState.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
emptyState.style.display = 'none';
|
|
renderCompanies(orgs);
|
|
|
|
// Load compliance scores in background
|
|
orgs.forEach(org => loadOrgScore(org.id || org.organization_id));
|
|
}
|
|
|
|
// ── Render companies ─────────────────────────────────────────────
|
|
function renderCompanies(orgs) {
|
|
const grid = document.getElementById('companies-grid');
|
|
grid.innerHTML = orgs.map(org => renderCompanyCard(org)).join('');
|
|
}
|
|
|
|
function renderCompanyCard(org) {
|
|
const orgId = org.id || org.organization_id;
|
|
const name = org.name || 'Azienda';
|
|
const vat = org.vat_number ? 'IT' + org.vat_number : '-';
|
|
const sector = org.sector || '';
|
|
const entityType = org.entity_type || 'not_applicable';
|
|
const voluntary = org.voluntary_compliance ? true : false;
|
|
|
|
const initials = name.split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase();
|
|
|
|
const entityBadge = renderEntityBadge(entityType, voluntary);
|
|
const sectorLabel = getSectorLabel(sector);
|
|
|
|
return `
|
|
<div class="company-card" data-name="${escapeHtml(name.toLowerCase())}" data-org-id="${orgId}">
|
|
<div class="company-card-header">
|
|
<div class="company-card-avatar">${escapeHtml(initials)}</div>
|
|
<div class="company-card-info">
|
|
<div class="company-card-name" title="${escapeHtml(name)}">${escapeHtml(name)}</div>
|
|
<div class="company-card-vat">${escapeHtml(vat)}</div>
|
|
</div>
|
|
${entityBadge}
|
|
</div>
|
|
|
|
<!-- Score bar (loaded async) -->
|
|
<div class="company-score-row" id="score-row-${orgId}" style="display:none">
|
|
<span class="company-score-label">Compliance</span>
|
|
<div class="company-score-bar">
|
|
<div class="company-score-bar-fill" id="score-fill-${orgId}" style="width:0%;background:#ccc"></div>
|
|
</div>
|
|
<span class="company-score-value" id="score-val-${orgId}">-</span>
|
|
</div>
|
|
|
|
<!-- Stats row (loaded async) -->
|
|
<div class="company-stats" id="stats-row-${orgId}" style="display:none">
|
|
<div class="company-stat">
|
|
<span class="company-stat-value" id="stat-risks-${orgId}">-</span>
|
|
<span class="company-stat-label">Rischi aperti</span>
|
|
</div>
|
|
<div class="company-stat">
|
|
<span class="company-stat-value" id="stat-incidents-${orgId}">-</span>
|
|
<span class="company-stat-label">Incidenti attivi</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="company-card-footer">
|
|
<span class="company-card-sector">${escapeHtml(sectorLabel)}</span>
|
|
<button class="btn btn-primary btn-sm" onclick="manageCompany(${orgId})">
|
|
Gestisci
|
|
<svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
function renderEntityBadge(type, voluntary) {
|
|
if (voluntary) {
|
|
return `<span class="entity-badge voluntary">Volontaria</span>`;
|
|
}
|
|
if (type === 'essential') {
|
|
return `<span class="entity-badge essential">Essenziale</span>`;
|
|
}
|
|
if (type === 'important') {
|
|
return `<span class="entity-badge important">Importante</span>`;
|
|
}
|
|
return `<span class="entity-badge not_applicable">Non NIS2</span>`;
|
|
}
|
|
|
|
// ── Load compliance score per org ────────────────────────────────
|
|
async function loadOrgScore(orgId) {
|
|
try {
|
|
// Temporarily switch to this org for the API call
|
|
const savedOrgId = api.orgId;
|
|
api.orgId = orgId;
|
|
const result = await api.getDashboardOverview();
|
|
api.orgId = savedOrgId;
|
|
|
|
if (result.success && result.data) {
|
|
const score = result.data.compliance_score || 0;
|
|
const risks = result.data.open_risks || 0;
|
|
const incidents = result.data.active_incidents || 0;
|
|
|
|
const scoreRow = document.getElementById('score-row-' + orgId);
|
|
const scoreVal = document.getElementById('score-val-' + orgId);
|
|
const scoreFill = document.getElementById('score-fill-' + orgId);
|
|
const statsRow = document.getElementById('stats-row-' + orgId);
|
|
const statRisks = document.getElementById('stat-risks-' + orgId);
|
|
const statIncidents = document.getElementById('stat-incidents-' + orgId);
|
|
|
|
if (scoreRow) scoreRow.style.display = 'flex';
|
|
if (scoreVal) {
|
|
scoreVal.textContent = score + '%';
|
|
scoreVal.style.color = getScoreColor(score);
|
|
}
|
|
if (scoreFill) {
|
|
scoreFill.style.width = score + '%';
|
|
scoreFill.style.background = getScoreColor(score);
|
|
}
|
|
if (statsRow) statsRow.style.display = 'flex';
|
|
if (statRisks) statRisks.textContent = risks;
|
|
if (statIncidents) statIncidents.textContent = incidents;
|
|
}
|
|
} catch (e) {
|
|
// Silently ignore per-org errors
|
|
}
|
|
}
|
|
|
|
// ── Manage company (switch org + go to dashboard) ─────────────────
|
|
function manageCompany(orgId) {
|
|
api.setOrganization(orgId);
|
|
window.location.href = 'dashboard.html';
|
|
}
|
|
|
|
// ── Search / Filter ──────────────────────────────────────────────
|
|
function filterCompanies(query) {
|
|
const q = query.toLowerCase().trim();
|
|
const cards = document.querySelectorAll('.company-card');
|
|
let anyVisible = false;
|
|
|
|
cards.forEach(card => {
|
|
const name = card.dataset.name || '';
|
|
const visible = !q || name.includes(q);
|
|
card.style.display = visible ? '' : 'none';
|
|
if (visible) anyVisible = true;
|
|
});
|
|
|
|
const emptyState = document.getElementById('companies-empty');
|
|
if (emptyState) {
|
|
emptyState.style.display = (!anyVisible && allCompanies.length > 0) ? 'block' : 'none';
|
|
}
|
|
}
|
|
|
|
// ── Sector labels ────────────────────────────────────────────────
|
|
function getSectorLabel(sector) {
|
|
const labels = {
|
|
energy_electricity: 'Energia - Elettricità',
|
|
energy_oil: 'Energia - Petrolio', energy_gas: 'Energia - Gas',
|
|
energy_hydrogen: 'Energia - Idrogeno',
|
|
transport_air: 'Trasporti - Aereo', transport_rail: 'Trasporti - Ferroviario',
|
|
transport_water: 'Trasporti - Marittimo', transport_road: 'Trasporti - Stradale',
|
|
banking: 'Banche', financial_markets: 'Mercati Finanziari',
|
|
health: 'Sanità', drinking_water: 'Acqua Potabile',
|
|
waste_water: 'Acque Reflue', digital_infrastructure: 'Infrastruttura Digitale',
|
|
ict_service_management: 'Gestione ICT B2B', public_administration: 'PA',
|
|
space: 'Spazio', postal_courier: 'Postale/Corrieri',
|
|
waste_management: 'Gestione Rifiuti', chemicals: 'Chimica',
|
|
food: 'Alimentare', manufacturing_medical: 'Dispositivi Medici',
|
|
manufacturing_computers: 'Computer/Elettronica',
|
|
manufacturing_electrical: 'App. Elettriche',
|
|
manufacturing_machinery: 'Macchinari', manufacturing_vehicles: 'Autoveicoli',
|
|
manufacturing_transport: 'Mezzi di Trasporto',
|
|
digital_providers: 'Fornitori Digitali', research: 'Ricerca', other: 'Altro'
|
|
};
|
|
return labels[sector] || sector || 'Settore non specificato';
|
|
}
|
|
|
|
// ── Init ─────────────────────────────────────────────────────────
|
|
loadCompanies();
|
|
</script>
|
|
</body>
|
|
</html>
|