[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:
co-authored by
Claude Opus 4.7
parent
e4f9e9179e
commit
a7bd37a797
+187
-2
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user