[FEAT] UI Fasi 4+5: Impersonate banner + Preferenze + Branding white-label (v1.6.0)

Completamento UI per gli endpoint backend già attivi (commit e4f9e91):

- admin/users.html: colonna Azioni con pulsante "Impersonate" per utenti non-super_admin
  attivi → salva token originale in sessionStorage, sostituisce con quello impersonate,
  redirige a dashboard
- js/common.js: banner persistente arancione "Modalità Impersonate" in tutte le
  pagine quando sessionStorage ha impersonate origin → pulsante "Esci impersonate"
  ripristina token originale e torna ad admin/users
- settings.html: nuovo tab "Preferenze" (lingua/tema/timezone/notifiche email+in-app)
  con form salva via PUT /auth/preferences
- settings.html: nuovo tab "Branding" (solo super_admin / consulente) con
  brand_name/logo_url/primary_color/secondary_color, PUT /branding

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-05-29 13:28:57 +02:00
co-authored by Claude Opus 4.7
parent e4f9e9179e
commit a7bd37a797
4 changed files with 251 additions and 3 deletions
+187 -2
View File
@@ -210,10 +210,12 @@
<div class="settings-tabs">
<button class="settings-tab active" onclick="switchTab('org')">Organizzazione</button>
<button class="settings-tab" onclick="switchTab('profile')">Profilo</button>
<button class="settings-tab" onclick="switchTab('preferences')">Preferenze</button>
<button class="settings-tab" onclick="switchTab('members')">Membri</button>
<button class="settings-tab" onclick="switchTab('security')">Sicurezza</button>
<button class="settings-tab" onclick="switchTab('apikeys')">API Keys</button>
<button class="settings-tab" onclick="switchTab('webhooks')">Webhook</button>
<button class="settings-tab" id="tab-btn-branding" onclick="switchTab('branding')" style="display:none;">Branding</button>
</div>
<!-- ══════════════ TAB: Organizzazione ══════════════ -->
@@ -420,6 +422,71 @@
</div>
<!-- ══════════════ TAB: Membri ══════════════ -->
<!-- ══════════════ TAB: Preferenze (Fase 4 / G12) ══════════════ -->
<div class="tab-panel" id="tab-preferences">
<div class="card">
<div class="card-header">
<h3>Preferenze utente</h3>
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">Personalizzazione interfaccia e notifiche.</p>
</div>
<div class="card-body">
<form id="preferences-form" style="max-width:560px;">
<div class="form-group">
<label class="form-label" for="pref-lang">Lingua</label>
<select id="pref-lang" class="form-input">
<option value="it">Italiano</option>
<option value="en">English</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="pref-theme">Tema</label>
<select id="pref-theme" class="form-input">
<option value="auto">Auto (segue sistema)</option>
<option value="light">Chiaro</option>
<option value="dark">Scuro</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="pref-tz">Timezone</label>
<select id="pref-tz" class="form-input">
<option value="Europe/Rome">Europe/Rome (CET/CEST)</option>
<option value="Europe/London">Europe/London (GMT/BST)</option>
<option value="Europe/Paris">Europe/Paris (CET/CEST)</option>
<option value="Europe/Berlin">Europe/Berlin (CET/CEST)</option>
<option value="Europe/Madrid">Europe/Madrid (CET/CEST)</option>
<option value="America/New_York">America/New_York (EST/EDT)</option>
<option value="America/Los_Angeles">America/Los_Angeles (PST/PDT)</option>
<option value="UTC">UTC</option>
</select>
</div>
<div class="form-group">
<label style="display:flex; gap:10px; align-items:center; font-size:.9rem;">
<input type="checkbox" id="pref-notif-email">
<span>Ricevi notifiche via email (incidenti, deadline, training)</span>
</label>
</div>
<div class="form-group">
<label style="display:flex; gap:10px; align-items:center; font-size:.9rem;">
<input type="checkbox" id="pref-notif-inapp">
<span>Mostra notifiche in-app (badge sidebar)</span>
</label>
</div>
<div class="form-group" style="margin-top:24px;">
<button type="submit" class="btn btn-primary" id="pref-save-btn">Salva preferenze</button>
<span id="pref-saved-msg" style="margin-left:12px; color:var(--success); display:none; font-size:.85rem;">✓ Salvato</span>
</div>
</form>
</div>
</div>
</div>
<div class="tab-panel" id="tab-members">
<div class="card">
<div class="card-header">
@@ -546,6 +613,44 @@
</div>
</div>
<!-- ══════════════ TAB: Branding (Fase 5 / G16) ══════════════ -->
<div class="tab-panel" id="tab-branding">
<div class="card">
<div class="card-header">
<h3>Branding white-label</h3>
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">Personalizza colori, logo e nome prodotto per il tuo studio. Visibile a tutti i tuoi clienti loggati.</p>
</div>
<div class="card-body">
<form id="branding-form" style="max-width:560px;">
<div class="form-group">
<label class="form-label" for="brand-name">Nome prodotto custom (opzionale)</label>
<input type="text" id="brand-name" class="form-input" maxlength="120" placeholder="es. Studio Rossi NIS2 Suite">
</div>
<div class="form-group">
<label class="form-label" for="brand-logo">URL Logo (opzionale)</label>
<input type="url" id="brand-logo" class="form-input" maxlength="512" placeholder="https://...">
</div>
<div class="form-group">
<label class="form-label" for="brand-pri">Colore primario</label>
<input type="color" id="brand-pri" class="form-input" style="height:42px; padding:4px;" value="#1e40af">
</div>
<div class="form-group">
<label class="form-label" for="brand-sec">Colore secondario</label>
<input type="color" id="brand-sec" class="form-input" style="height:42px; padding:4px;" value="#06b6d4">
</div>
<div class="form-group" style="margin-top:24px;">
<button type="submit" class="btn btn-primary" id="brand-save-btn">Salva branding</button>
<span id="brand-saved-msg" style="margin-left:12px; color:var(--success); display:none; font-size:.85rem;">✓ Salvato — ricarica per vedere</span>
</div>
</form>
</div>
</div>
</div>
</div>
</main>
</div>
@@ -570,24 +675,104 @@
// Carica dati iniziali
loadOrgData();
loadProfileData();
// Mostra tab Branding solo a super_admin / consulente
(async () => {
try {
const me = await api.getMe();
if (me && me.success && me.data && (me.data.role === 'super_admin' || me.data.role === 'consultant')) {
const btn = document.getElementById('tab-btn-branding');
if (btn) btn.style.display = '';
}
} catch (e) { /* silenzioso */ }
})();
// ── Tab Navigation ───────────────────────────────────────
function switchTab(tab) {
document.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
const tabMap = { org: 0, profile: 1, members: 2, security: 3, apikeys: 4, webhooks: 5 };
const panelMap = { org: 'tab-org', profile: 'tab-profile', members: 'tab-members', security: 'tab-security', apikeys: 'tab-apikeys', webhooks: 'tab-webhooks' };
const tabMap = { org: 0, profile: 1, preferences: 2, members: 3, security: 4, apikeys: 5, webhooks: 6, branding: 7 };
const panelMap = { org: 'tab-org', profile: 'tab-profile', preferences: 'tab-preferences', members: 'tab-members', security: 'tab-security', apikeys: 'tab-apikeys', webhooks: 'tab-webhooks', branding: 'tab-branding' };
document.querySelectorAll('.settings-tab')[tabMap[tab]].classList.add('active');
document.getElementById(panelMap[tab]).classList.add('active');
if (tab === 'preferences') loadPreferences();
if (tab === 'members') loadMembers();
if (tab === 'security') { loadSessions(); loadAuditLog(); }
if (tab === 'apikeys') loadApiKeys();
if (tab === 'webhooks') { loadWebhooks(); loadDeliveries(); }
if (tab === 'branding') loadBranding();
}
// ── Preferenze utente (Fase 4 / G12 — UI) ─────────────────
async function loadPreferences() {
try {
const res = await api.get('/auth/preferences');
if (!res.success) return;
const p = res.data;
document.getElementById('pref-lang').value = p.preferred_language || 'it';
document.getElementById('pref-theme').value = p.theme || 'auto';
document.getElementById('pref-tz').value = p.timezone || 'Europe/Rome';
document.getElementById('pref-notif-email').checked = !!p.notif_email;
document.getElementById('pref-notif-inapp').checked = !!p.notif_inapp;
} catch (e) { /* silenzioso */ }
}
// ── Branding white-label (Fase 5 / G16 — UI) ─────────────
async function loadBranding() {
try {
const res = await api.get('/branding/current');
if (!res.success) return;
const b = res.data;
document.getElementById('brand-name').value = b.custom_brand_name || '';
document.getElementById('brand-logo').value = b.logo_url || '';
document.getElementById('brand-pri').value = b.primary_color || '#1e40af';
document.getElementById('brand-sec').value = b.secondary_color || '#06b6d4';
} catch (e) { /* silenzioso */ }
}
document.getElementById('branding-form').addEventListener('submit', async function(e) {
e.preventDefault();
const btn = document.getElementById('brand-save-btn');
const msg = document.getElementById('brand-saved-msg');
btn.disabled = true; btn.textContent = 'Salvataggio...';
try {
await api.put('/branding', {
custom_brand_name: document.getElementById('brand-name').value.trim() || null,
logo_url: document.getElementById('brand-logo').value.trim() || null,
primary_color: document.getElementById('brand-pri').value,
secondary_color: document.getElementById('brand-sec').value,
});
msg.style.display = 'inline';
} catch (e) {
alert('Errore: ' + (e.message || 'impossibile salvare'));
} finally {
btn.disabled = false; btn.textContent = 'Salva branding';
}
});
document.getElementById('preferences-form').addEventListener('submit', async function(e) {
e.preventDefault();
const btn = document.getElementById('pref-save-btn');
const msg = document.getElementById('pref-saved-msg');
btn.disabled = true; btn.textContent = 'Salvataggio...';
try {
await api.put('/auth/preferences', {
preferred_language: document.getElementById('pref-lang').value,
theme: document.getElementById('pref-theme').value,
timezone: document.getElementById('pref-tz').value,
notif_email: document.getElementById('pref-notif-email').checked,
notif_inapp: document.getElementById('pref-notif-inapp').checked,
});
msg.style.display = 'inline'; setTimeout(function(){ msg.style.display = 'none'; }, 3000);
} catch (e) {
alert('Errore: ' + (e.message || 'impossibile salvare'));
} finally {
btn.disabled = false; btn.textContent = 'Salva preferenze';
}
});
// ── Sessioni Multi-Device (Fase 2 / G07) ─────────────────
async function loadSessions() {
const container = document.getElementById('sessions-container');