/** * OPERA v2 — Dashboard — app.js (core) v0.58.0 * ============================================= * Bootstrap, autenticazione, UI helpers globali. * * Splitting v0.58.0 (Hardening Milestone 13): * - app.js (this file) — init, auth, UI helpers, bootstrap * - projects-list.js — caricamento, filtri, rendering lista progetti * - projects-actions.js — apriProgetto, openNuovoProgetto, handleNuovoProgetto * - odoo-modals.js — modal Odoo (cliente live + Flusso B collega) * * Stato condiviso esposto come window.DASHBOARD_STATE per i moduli. * * Dipendenze (caricamento ordinato in index.html): * - api-client-*.js (6 moduli) * - project-manager.js * - dashboard-utils.js * - odoo-modals.js * - app.js (this) — espone DASHBOARD_STATE prima di projects-* * - projects-list.js * - projects-actions.js * - preventivo-render.js, preventivo-view.js, config-view.js */ 'use strict'; // ──────────────────────────────────────────────────────────── // 1. STATO GLOBALE — esposto su window per moduli splittati // ──────────────────────────────────────────────────────────── const STATE = { user: null, progetti: [], progettiFiltered: [], filtri: { stato: '', cliente: '', periodo: '' }, // v1.99.190: default Tabella (richiesta Alberto). Ricorda l'ultima scelta. vistaCorrente: (function () { try { return localStorage.getItem('opera_dash_vista') || 'table'; } catch (_e) { return 'table'; } })(), // 'cards' | 'table' loading: false, error: null }; // Esposto immediatamente su window perché i moduli splittati lo leggono // via getState() al momento dell'invocazione delle loro funzioni. window.DASHBOARD_STATE = STATE; // ──────────────────────────────────────────────────────────── // 2. INIZIALIZZAZIONE // ──────────────────────────────────────────────────────────── async function init() { console.log('🚀 Avvio Dashboard OPERA v2...'); // v1.5.0: attiva il modulo errori PRIMA di qualunque altra cosa, // così se qualcosa va in errore nel boot lo cattura. if (window.ERROR_HANDLER) { ERROR_HANDLER.init({ app: 'dashboard' }); } // v1.4.2: reset viste al boot — se l'utente ha fatto F5 mentre era in // vista Preventivo, Configurazione, Qualifica o Lista stime, ricomincia // sempre dalla Dashboard. Lo stato JS è azzerato dal refresh ma il DOM HTML // può aver mantenuto hidden=false su qualcuna delle view (specie se abbiamo // loaded l'HTML con hidden cambiato da una sessione precedente). ['view-preventivo', 'view-config', 'view-qualifica', 'view-stime-list', 'view-acquisti'].forEach(id => { const el = document.getElementById(id); if (el) el.hidden = true; }); const dash = document.getElementById('view-dashboard'); if (dash) dash.hidden = false; // Inizializza Supabase API_CLIENT.initSupabase(); // Controllo supporto browser const supporto = DASHBOARD_UTILS.checkSupporto(); if (!supporto.localStorage || !supporto.fetch) { showError('Browser non supportato. Aggiorna il browser.'); return; } // Setup connection monitoring setupConnectionMonitoring(); // Setup event listeners setupEventListeners(); // Controlla autenticazione await checkAuth(); } async function checkAuth() { try { const user = await API_CLIENT.getUtente(); if (user) { STATE.user = user; await onLoginSuccess(); } else { showView('login'); } } catch (err) { console.error('Errore check auth:', err); showView('login'); } } function setupEventListeners() { // Login document.getElementById('btn-login').addEventListener('click', handleLogin); document.getElementById('login-password').addEventListener('keypress', (e) => { if (e.key === 'Enter') handleLogin(); }); // Header actions — funzioni esposte da projects-actions.js document.getElementById('btn-nuovo-progetto').addEventListener('click', () => window.openNuovoProgetto()); document.getElementById('btn-user-menu').addEventListener('click', toggleUserMenu); document.getElementById('btn-logout').addEventListener('click', handleLogout); // v1.34.19: bottone Impostazioni → apre view Manutenzione const btnImp = document.getElementById('btn-impostazioni'); if (btnImp) { btnImp.addEventListener('click', () => { // Chiudi dropdown const dd = document.getElementById('dropdown-user'); if (dd) dd.hidden = true; if (typeof window.apriManutenzione === 'function') { window.apriManutenzione(); } else { alert('Modulo Manutenzione non disponibile'); } }); } // v0.91.0: bottone "Abilita/Disabilita notifiche push" const btnPush = document.getElementById('btn-notifiche-push'); if (btnPush) { btnPush.addEventListener('click', handleTogglePush); } // Filtri — funzioni esposte da projects-list.js document.getElementById('filter-stato').addEventListener('change', () => window.handleFiltroChange()); document.getElementById('filter-periodo').addEventListener('change', () => window.handleFiltroChange()); document.getElementById('btn-reset-filtri').addEventListener('click', () => window.resetFiltri()); // v1.99.189: il tipo è pilotato dalle tab (attivaTab → setTipoContesto). Niente più segmenti. // v1.35.19: popola dinamicamente le option del filtro Stato dal // catalogo unico STATI_CATALOGO. Se window.statiUI non è disponibile // (es. stati-catalogo.js non caricato), lascia le option statiche HTML. // Preserva il valore selezionato. Include 'archiviato'. _popolaFiltroStato(); // v1.7.1: bottone "Rimuovi filtri" nell'empty state filtrato const btnEmptyReset = document.getElementById('btn-empty-reset-filtri'); if (btnEmptyReset) { btnEmptyReset.addEventListener('click', () => window.resetFiltri()); } // Filtro cliente con debounce const filterCliente = DASHBOARD_UTILS.debounce(() => window.handleFiltroChange(), 300); document.getElementById('filter-cliente').addEventListener('input', filterCliente); // Vista switcher document.getElementById('btn-view-cards').addEventListener('click', () => switchView('cards')); document.getElementById('btn-view-table').addEventListener('click', () => switchView('table')); // v1.99.190: allinea i bottoni alla vista ricordata (il render usa STATE.vistaCorrente). document.getElementById('btn-view-cards').classList.toggle('active', STATE.vistaCorrente === 'cards'); document.getElementById('btn-view-table').classList.toggle('active', STATE.vistaCorrente === 'table'); // Modal nuovo progetto — funzione esposta da projects-actions.js document.getElementById('form-nuovo-progetto').addEventListener('submit', (e) => window.handleNuovoProgetto(e)); // Click fuori per chiudere dropdown document.addEventListener('click', (e) => { if (!e.target.closest('.user-menu')) { document.getElementById('dropdown-user').hidden = true; } }); } function setupConnectionMonitoring() { API_CLIENT.onConnectionChange((isOnline) => { const banner = document.getElementById('banner-offline'); banner.hidden = isOnline; }); } /** * v1.35.19 — Popola dinamicamente le option del filtro Stato. * Usa window.statiUI() dal catalogo unico (stati-catalogo.js). * Preserva il valore selezionato se presente nella nuova lista. * Se statiUI non è disponibile, lascia invariate le option HTML statiche. * * Per ogni stato include icona + label. L'opzione "Tutti" resta come * primo elemento. */ function _popolaFiltroStato() { if (typeof window.statiUI !== 'function') return; // fallback HTML statico const sel = document.getElementById('filter-stato'); if (!sel) return; const valoreCorrente = sel.value; const stati = window.statiUI(true); // include 'archiviato' // Rimuovi tutte le option tranne "Tutti" (la prima, value="") const tutti = sel.querySelector('option[value=""]'); sel.innerHTML = ''; if (tutti) sel.appendChild(tutti); else { const opt = document.createElement('option'); opt.value = ''; opt.textContent = 'Tutti'; sel.appendChild(opt); } // Aggiungi tutti gli stati dal catalogo for (const s of stati) { const opt = document.createElement('option'); opt.value = s.value; opt.textContent = `${s.icona} ${s.label}`; sel.appendChild(opt); } // Ripristina valore selezionato se ancora valido if (valoreCorrente && Array.from(sel.options).some(o => o.value === valoreCorrente)) { sel.value = valoreCorrente; } } // ──────────────────────────────────────────────────────────── // 3. AUTENTICAZIONE // ──────────────────────────────────────────────────────────── async function handleLogin() { const email = document.getElementById('login-email').value.trim(); const password = document.getElementById('login-password').value; const btnLogin = document.getElementById('btn-login'); const errorEl = document.getElementById('login-errore'); if (!email || !password) { showError('Inserisci email e password', errorEl); return; } btnLogin.disabled = true; btnLogin.textContent = 'Accesso...'; try { const result = await API_CLIENT.login(email, password); if (result.ok) { STATE.user = result.data.user; await onLoginSuccess(); } else { showError(result.error?.message || 'Errore di autenticazione', errorEl); } } catch (err) { console.error('Errore login:', err); showError('Errore di connessione', errorEl); } finally { btnLogin.disabled = false; btnLogin.textContent = 'Accedi'; } } async function onLoginSuccess() { // v1.35.9 — Mostra la toolbar globale post-login (visibile in ogni view) const toolbar = document.getElementById('dashboard-toolbar-globale'); if (toolbar) toolbar.hidden = false; // v1.99.73 — Mostra SUBITO i tab "Cose da fare / Progetti", PRIMA dei // caricamenti dati. Prima venivano scoperti solo dopo `await Promise.all` // dei caricamenti: se una chiamata falliva (es. 500 su tablet) l'await // rcompiva e i tab restavano nascosti → sul tablet non si vedevano. const tabsNavEarly = document.getElementById('tabs-dashboard'); if (tabsNavEarly) tabsNavEarly.hidden = false; // v1.35.9 — All'avvio mostriamo la view "Cose da fare" (era 'dashboard'). // Decisione Alberto 27/05/2026: "Cose da fare" è la vista di partenza. // attivaTab gestisce show/hide view + titolo toolbar + apriCoseDaFare(). // Le view sono ancora hidden a questo punto (mai mostrate), quindi attivaTab // farà il primo show. La chiamata vera ad apriCoseDaFare avverrà appena // cose-da-fare-view è registrato globalmente (dopo init). // Per evitare race con loading dati nuovo progetto, attiviamo dopo // il caricamento dei dati iniziali sotto. // Mostra nome utente + RUOLO (v1.99.66: identità chiara. Su Edge la sessione // resta salvata e rientra senza schermata login → è importante vedere CHI è // connesso e con che profilo). const userName = STATE.user.email?.split('@')[0] || ''; const userEl = document.getElementById('user-name'); if (userEl) { userEl.textContent = userName || 'Utente'; userEl.title = STATE.user.email || ''; if (window.OPERA_RUOLI?.getRuoloCorrente) { window.OPERA_RUOLI.getRuoloCorrente(true).then(async (ruolo) => { // v1.99.183 — link "App Posa" nell'header: solo Titolare (amministrazione). try { const ruoli = window.OPERA_RUOLI.getRuoliCorrenti ? await window.OPERA_RUOLI.getRuoliCorrenti() : [ruolo]; // v1.99.205 — App Posa e tab Presenze ora sono .solo-titolare (come // tutto il resto): nascosti per l'ufficio via body.ruolo-ufficio. // Qui togliamo la classe SOLO per il Titolare confermato → vede tutto. if (Array.isArray(ruoli) && ruoli.includes('amministrazione')) { document.body.classList.remove('ruolo-ufficio'); } } catch (_e) { /* in caso di errore resta tutto in modalità ufficio (nasconde) */ } // v1.99.125 — se l'email non era pronta al primo render (sessione // ripristinata su Edge), recuperala dalla sessione live così il badge // mostra l'utente reale (es. "alberto.locatelli · Titolare") e non "Utente". let nome = userName; if (!nome) { try { const sb = API_CLIENT.getSupabase && API_CLIENT.getSupabase(); const { data } = sb ? await sb.auth.getUser() : { data: null }; if (data?.user?.email) { nome = data.user.email.split('@')[0]; userEl.title = data.user.email; } } catch (_e) { /* resta il fallback */ } } const base = nome || 'Utente'; userEl.textContent = ruolo ? (base + ' · ' + window.OPERA_RUOLI.etichetta(ruolo)) : base; }).catch(() => { /* resta solo il nome */ }); } } // Carica dati iniziali — funzioni da projects-list.js + projects-handoff.js (v0.79.0) // v1.99.73 — allSettled (non all): se UN caricamento fallisce (es. 500 // transitorio su tablet), gli altri proseguono e — soprattutto — il codice // sotto (tab + apertura "Cose da fare") viene comunque eseguito. await Promise.allSettled([ window.caricaProgetti(), window.caricaScadenze(), window.caricaStatistiche(), window.caricaERenderRilieviSmistare ? window.caricaERenderRilieviSmistare() : Promise.resolve(), window.caricaUltimeAttivita ? window.caricaUltimeAttivita() : Promise.resolve(), // v0.96.0 window.aggiornaBadgeRichieste ? window.aggiornaBadgeRichieste() : Promise.resolve() // v1.99.208 ]); // v0.83.0: attiva sottoscrizione realtime per nuovi rilievi da smistare if (typeof window.sottoscriviRealtimeRilievi === 'function') { window.sottoscriviRealtimeRilievi(); } // v1.34.22: carica scaglioni durata posa da DB (Migration 034). // Fire-and-forget: se fallisce, restano gli scaglioni hardcoded. if (window.STIMA_DURATA_POSA?.caricaScaglioniDaDb && window.API_CLIENT?.getSupabase) { window.STIMA_DURATA_POSA.caricaScaglioniDaDb(window.API_CLIENT.getSupabase()) .catch(e => console.warn('[app] caricamento scaglioni durata posa fallito:', e)); } // v1.33.0 — Sessione H Step 2: mostra tab nav + carica conteggi // notifiche + sottoscrivi Realtime su notifiche_dashboard const tabsNav = document.getElementById('tabs-dashboard'); if (tabsNav) tabsNav.hidden = false; if (typeof window.aggiornaPalliniTabCose === 'function') { window.aggiornaPalliniTabCose(); } if (typeof window.sottoscriviRealtimeNotifiche === 'function') { window.sottoscriviRealtimeNotifiche(); } // Mostra eventuali popup K persi (notifiche con popup_bloccante mai mostrate) if (window.API_NOTIFICHE?.listaNotifichePopupNuove) { window.API_NOTIFICHE.listaNotifichePopupNuove().then(res => { if (res.ok && res.data && res.data.length > 0 && window.mostraPopupConferma) { for (const n of res.data) { window.mostraPopupConferma(n); } } }); } // v0.91.0: inizializza Web Push e aggiorna UI bottone (no-op se VAPID non configurato) if (typeof window.pushInit === 'function') { await window.pushInit(); await aggiornaUIPush(); } // v1.28.17 — Deep-link: se l'URL contiene ?progetto= (o ?progetto_id=) // apre direttamente la scheda di quel progetto invece della lista. // Usato dal bottone "← Dashboard" di App Rilievo per tornare ESATTAMENTE // alla scheda del progetto su cui si stava lavorando, non alla lista generica. // Se NON c'è deep-link → attivo "Cose da fare" come default (v1.35.9). const haDeepLink = _gestisciDeepLinkProgetto(); if (!haDeepLink) { // Default v1.35.9 — apre Cose da fare invece di Progetti if (typeof window.attivaTab === 'function') { window.attivaTab('cose-da-fare'); } else { // Fallback: se attivaTab non ancora disponibile, mostra view-dashboard showView('dashboard'); } } } /** * v1.28.17 — Legge ?progetto / ?progetto_id dall'URL e apre la scheda. * Pulisce l'URL dopo l'apertura (replaceState) così un F5 successivo * riparte dalla lista normale. */ function _gestisciDeepLinkProgetto() { try { const params = new URLSearchParams(window.location.search); const id = params.get('progetto_id') || params.get('progetto'); if (!id) return false; // Pulisce subito l'URL (l'apertura è "una tantum") window.history.replaceState({}, '', window.location.pathname); if (typeof window.apriPreventivoView === 'function') { console.log('[dashboard] deep-link: apro progetto', id); window.apriPreventivoView(id); return true; } else { console.warn('[dashboard] deep-link: apriPreventivoView non disponibile'); return false; } } catch (err) { console.warn('[dashboard] errore deep-link progetto:', err); return false; } } /** * v0.91.0: aggiorna il bottone "Abilita/Disabilita notifiche push" nel * dropdown user-menu in base allo stato attuale del browser. * - SW non supportato → bottone nascosto * - VAPID non configurato → bottone nascosto * - già sottoscritto → "🔕 Disabilita notifiche push" * - permesso negato → "🚫 Notifiche bloccate (sblocca dal browser)" * - tutto ok ma non sottoscritto → "🔔 Abilita notifiche push" */ async function aggiornaUIPush() { const btn = document.getElementById('btn-notifiche-push'); const label = document.getElementById('btn-notifiche-push-label'); if (!btn || !label || typeof window.pushStato !== 'function') { if (btn) btn.hidden = true; return; } const stato = await window.pushStato(); if (!stato.supportato || !stato.configurato) { btn.hidden = true; return; } btn.hidden = false; if (stato.permesso === 'denied') { label.textContent = '🚫 Notifiche bloccate dal browser'; btn.disabled = true; } else if (stato.sottoscritto) { label.textContent = '🔕 Disabilita notifiche push'; btn.disabled = false; } else { label.textContent = '🔔 Abilita notifiche push'; btn.disabled = false; } } async function handleTogglePush() { const stato = await window.pushStato(); if (stato.sottoscritto) { const res = await window.pushDisabilita(); if (!res.ok) alert('Errore: ' + (res.motivo || 'sconosciuto')); } else { const res = await window.pushAbilita(); if (!res.ok) { if (res.motivo === 'permesso_negato') { alert('Hai negato il permesso. Puoi sbloccarlo dalle impostazioni del browser.'); } else if (res.motivo === 'non_disponibile') { // alert già mostrato dal modulo } else { alert('Errore: ' + (res.errore || res.motivo || 'sconosciuto')); } } } await aggiornaUIPush(); // Chiudi dropdown document.getElementById('dropdown-user').hidden = true; } async function handleLogout() { try { // v0.83.0: pulisci sottoscrizione realtime prima del logout if (typeof window.disattivaRealtime === 'function') { window.disattivaRealtime(); } await API_CLIENT.logout(); STATE.user = null; STATE.progetti = []; STATE.progettiFiltered = []; window.resetFiltri(); // v1.35.9 — nascondi toolbar globale e tab nav al logout const toolbar = document.getElementById('dashboard-toolbar-globale'); if (toolbar) toolbar.hidden = true; const tabsNav = document.getElementById('tabs-dashboard'); if (tabsNav) tabsNav.hidden = true; showView('login'); } catch (err) { console.error('Errore logout:', err); showError('Errore durante disconnessione'); } } // ──────────────────────────────────────────────────────────── // 4. UI HELPERS — esposti su window per moduli splittati // ──────────────────────────────────────────────────────────── function showView(viewName) { document.querySelectorAll('.view').forEach(view => { view.hidden = !view.id.includes(viewName); }); } /** * Mostra stato loading per progetti. * v1.7.1: garantisce mutua esclusione con altri stati (empty, container, tabella). * Quando show=false, NON tocca gli altri stati — quel compito spetta al * chiamante successivo (renderProgetti o showEmptyState). */ function showLoadingProgetti(show) { const loading = document.getElementById('progetti-loading'); if (!loading) return; loading.hidden = !show; if (show) { // Quando appare il loading, nasconde tutto il resto per evitare overlap visivi const container = document.getElementById('progetti-container'); const tabella = document.getElementById('progetti-tabella'); const empty = document.getElementById('progetti-empty'); const emptyFiltrato = document.getElementById('progetti-empty-filtrato'); if (container) container.hidden = false; // serve per vedere il loading dentro if (tabella) tabella.hidden = true; if (empty) empty.hidden = true; if (emptyFiltrato) emptyFiltrato.hidden = true; } } /** * Mostra stato vuoto (nessun progetto OR nessun risultato dai filtri). * v1.7.1: * - Se ci sono 0 progetti totali → onboarding mode (empty caldo + nasconde tutto il resto) * - Se ci sono progetti ma i filtri non matchano → empty filtrato (più discreto) * * Garantisce mutua esclusione con loading e lista progetti. * * @param {Object} opzioni * @param {boolean} opzioni.filtrato - true se l'empty è dovuto a filtri attivi * (non onboarding, ma "i filtri non trovano nulla") */ function showEmptyState(opzioni) { const loading = document.getElementById('progetti-loading'); const container = document.getElementById('progetti-container'); const tabella = document.getElementById('progetti-tabella'); const empty = document.getElementById('progetti-empty'); const emptyFiltrato = document.getElementById('progetti-empty-filtrato'); if (loading) loading.hidden = true; if (container) container.hidden = true; if (tabella) tabella.hidden = true; const filtrato = opzioni && opzioni.filtrato; if (filtrato) { // Filtri attivi che non matchano nessun progetto: empty discreto, sidebar/header restano if (empty) empty.hidden = true; if (emptyFiltrato) emptyFiltrato.hidden = false; } else { // Vero stato vuoto (0 progetti totali): empty grande + onboarding mode if (empty) empty.hidden = false; if (emptyFiltrato) emptyFiltrato.hidden = true; } } /** * v1.7.1: imposta o rimuove la "modalità onboarding" (zero progetti totali). * In onboarding mode il body riceve la classe .onboarding-mode che nasconde * sidebar widget, barra filtri, header con count e tab, banner smistamento. * La schermata mostra solo l'empty state caldo centrato. * * @param {boolean} attiva */ function setOnboardingMode(attiva) { if (attiva) { document.body.classList.add('onboarding-mode'); } else { document.body.classList.remove('onboarding-mode'); } } function switchView(vista) { STATE.vistaCorrente = vista; try { localStorage.setItem('opera_dash_vista', vista); } catch (_e) {} // Aggiorna bottoni document.getElementById('btn-view-cards').classList.toggle('active', vista === 'cards'); document.getElementById('btn-view-table').classList.toggle('active', vista === 'table'); // Re-render — funzione da projects-list.js window.renderProgetti(); } function toggleUserMenu() { const dropdown = document.getElementById('dropdown-user'); dropdown.hidden = !dropdown.hidden; } function chiudiModal() { document.getElementById('modal-nuovo-progetto').style.display = 'none'; document.getElementById('form-nuovo-progetto').reset(); } function showError(message, element = null) { if (element) { element.textContent = message; element.hidden = false; } else { // Banner globale const banner = document.getElementById('banner-info'); banner.textContent = '❌ ' + message; banner.className = 'banner banner-warn'; banner.hidden = false; setTimeout(() => { banner.hidden = true; }, 5000); } } function showInfo(message) { const banner = document.getElementById('banner-info'); banner.textContent = '✅ ' + message; banner.className = 'banner banner-info'; banner.hidden = false; setTimeout(() => { banner.hidden = true; }, 3000); } // Helper UI esposti su window per moduli splittati (projects-list.js, projects-actions.js) window.showError = showError; window.showInfo = showInfo; window.showLoadingProgetti = showLoadingProgetti; window.showEmptyState = showEmptyState; window.setOnboardingMode = setOnboardingMode; // v1.7.1 window.chiudiModal = chiudiModal; // ──────────────────────────────────────────────────────────── // 4b. TAB NAVIGATION — v1.33.0 Sessione H Step 2 // ──────────────────────────────────────────────────────────── // // Tab nav semplice tra "Cose da fare" e "Progetti" (dashboard classica). // Aggiunge attivo/disattivo sulla tab e mostra/nasconde la view corrispondente. // Pulisce eventuali view secondarie (preventivo, config, etc.) per evitare // double-rendering quando si torna alla landing. function attivaTab(nomeTab) { // Mappa nome tab → id view e id tab button // v1.99.189: 'dashboard' (Progetti) e 'assistenza' condividono la STESSA view // (view-dashboard, stessa lista/filtri/render). La tab attiva pilota il filtro // tipo: Progetti → vendite (legacy senza tipo = vendita), Assistenza → assistenze. const map = { 'cose-da-fare': { view: 'view-cose-da-fare', tab: 'tab-cose-da-fare', titolo: 'Cose da fare', tipo: undefined }, 'messaggi': { view: 'view-messaggi', tab: 'tab-messaggi', titolo: 'Messaggi ufficio', tipo: undefined }, 'dashboard': { view: 'view-dashboard', tab: 'tab-progetti', titolo: 'Progetti', tipo: 'vendita' }, 'richieste': { view: 'view-richieste', tab: 'tab-richieste', titolo: 'Richieste', tipo: undefined }, 'fatture-fornitori': { view: 'view-fatture-fornitori', tab: 'tab-fatture-fornitori', titolo: 'Fatture fornitori', tipo: undefined }, 'assistenza': { view: 'view-dashboard', tab: 'tab-assistenza', titolo: 'Assistenza', tipo: 'assistenza' }, 'documenti': { view: 'view-documenti', tab: 'tab-documenti', titolo: 'Documenti', tipo: undefined }, 'presenze': { view: 'view-presenze', tab: 'tab-presenze', titolo: 'Presenze', tipo: undefined }, }; const cfg = map[nomeTab]; if (!cfg) { console.warn('[attivaTab] Tab sconosciuta:', nomeTab); return; } // Nasconde tutte le view (incluse quelle secondarie) document.querySelectorAll('.view').forEach(v => { v.hidden = true; }); // Mostra la view della tab attivata const view = document.getElementById(cfg.view); if (view) view.hidden = false; // Aggiorna stato attivo sui bottoni tab document.querySelectorAll('.tab-dashboard').forEach(b => b.classList.remove('attivo')); const tabBtn = document.getElementById(cfg.tab); if (tabBtn) tabBtn.classList.add('attivo'); // v1.35.9 — aggiorna titolo della toolbar globale in base alla tab attiva const titoloEl = document.getElementById('dashboard-toolbar-titolo'); if (titoloEl) titoloEl.textContent = cfg.titolo; // v1.99.189 — Progetti/Assistenza condividono view-dashboard: imposta il tipo // (e ri-renderizza la lista) in base alla tab. Le altre tab non toccano il tipo. if (cfg.tipo !== undefined && typeof window.setTipoContesto === 'function') { window.setTipoContesto(cfg.tipo); } // Handler dedicato per "Cose da fare" (carica dati + polling) if (nomeTab === 'cose-da-fare' && typeof window.apriCoseDaFare === 'function') { window.apriCoseDaFare(); } else if (typeof window.chiudiCoseDaFare === 'function') { window.chiudiCoseDaFare(); } // Render Documenti alla sua apertura (v1.99.104) if (nomeTab === 'documenti' && typeof window.renderDocumenti === 'function') { window.renderDocumenti(); } // Render Messaggi ufficio alla sua apertura (v1.99.355) if (nomeTab === 'messaggi' && window.MESSAGGI_UFFICIO && typeof window.MESSAGGI_UFFICIO.apri === 'function') { window.MESSAGGI_UFFICIO.apri(); } // Render Presenze alla sua apertura (v1.99.197) — solo Titolare (gating dentro la view) if (nomeTab === 'presenze' && typeof window.renderPresenze === 'function') { window.renderPresenze(); } // Render Richieste (lead da email) alla sua apertura (v1.99.208) if (nomeTab === 'richieste' && typeof window.renderRichieste === 'function') { window.renderRichieste(); } // Render Fatture fornitori alla sua apertura (v1.99.214) if (nomeTab === 'fatture-fornitori' && typeof window.renderFattureFornitori === 'function') { window.renderFattureFornitori(); } } window.attivaTab = attivaTab; // ──────────────────────────────────────────────────────────── // 5. BOOTSTRAP // ──────────────────────────────────────────────────────────── if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } console.log('✅ Dashboard app.js v1.99.611 caricato');