/* ============================================================ app.jsx — Shell, navegação e persistência (LocalStorage) ============================================================ */ function App() { const store = window.crmStore; const clientParams = (() => { const q = new URLSearchParams(location.search); const c = q.get('c'); if (c) return { c, t: c }; // link curto: código serve como token const p = q.get('p'); return p ? { p, t: q.get('t') || '', d: q.get('d') || '' } : null; // links antigos })(); if (clientParams) return ; const [view, setView] = React.useState('pipeline'); // 'pipeline' | 'wizard' const [deals, setDeals] = React.useState([]); const [navOpen, setNavOpen] = React.useState(false); const [resume, setResume] = React.useState(null); // negócio a retomar (ou null = novo) const [user, setUser] = React.useState(null); const [ready, setReady] = React.useState(false); // sessão verificada const [loadingDeals, setLoadingDeals] = React.useState(false); const startNew = () => { setResume(null); setView('wizard'); setNavOpen(false); }; const resumeDeal = (deal) => { setResume(deal); setView('wizard'); setNavOpen(false); }; const refresh = React.useCallback(async () => { try { setDeals(await store.list()); } catch (e) { console.warn('list falhou', e); } }, [store]); /* sessão + carga inicial + realtime */ React.useEffect(() => { let unsubAuth = () => {}, unsubRt = () => {}; (async () => { const u = await store.init(); setUser(u); setReady(true); unsubAuth = store.onAuth((nu) => { setUser(nu); if (nu) refresh(); else setDeals([]); }); })(); return () => { unsubAuth(); unsubRt(); }; }, [store, refresh]); React.useEffect(() => { if (!user) return; setLoadingDeals(true); refresh().then(() => setLoadingDeals(false)); const unsubRt = store.subscribe(() => refresh()); // rede de segurança: recarrega periodicamente e ao focar a aba // (garante ver o aceite/pagamento do cliente mesmo se o realtime não disparar) let poll = null; if (store.hasCloud) poll = setInterval(() => { if (!document.hidden) refresh(); }, 12000); const onFocus = () => { if (!document.hidden) refresh(); }; document.addEventListener('visibilitychange', onFocus); window.addEventListener('focus', onFocus); return () => { unsubRt(); if (poll) clearInterval(poll); document.removeEventListener('visibilitychange', onFocus); window.removeEventListener('focus', onFocus); }; }, [user, store, refresh]); const commitDeal = (deal) => { setDeals(prev => { const exists = prev.find(d => d.id === deal.id); return exists ? prev.map(d => d.id === deal.id ? { ...deal } : d) : [{ ...deal }, ...prev]; }); return store.upsert(deal).catch(e => { console.warn('upsert falhou', e); throw e; }); }; const moveDeal = (id, stage) => { let updated = null; setDeals(prev => prev.map(d => { if (d.id === id) { updated = { ...d, stage, updatedAt: Date.now() }; return updated; } return d; })); if (updated) store.upsert(updated).catch(e => console.warn('upsert falhou', e)); }; const paidDeal = (id) => moveDeal(id, 'pagamento'); const deleteDeal = (id) => { setDeals(prev => prev.filter(d => d.id !== id)); store.remove(id).catch(e => console.warn('remove falhou', e)); }; const logout = async () => { await store.signOut(); setView('pipeline'); }; const nav = [ { id: 'pipeline', label: 'Pipeline', icon: 'layers' }, { id: 'wizard', label: 'Nova proposta', icon: 'spark' }, ]; /* gating: aguardando sessão */ if (!ready) { return
; } /* gating: modo nuvem sem login */ if (store.mode === 'supabase' && !user) { return setUser(store.user)} />; } return (
{/* Sidebar */} {navOpen &&
setNavOpen(false)} />} {/* Main */}
{view === 'pipeline' && ( )} {view === 'wizard' && ( { setResume(null); setView(v || 'pipeline'); }} /> )}
); } window.__ddRoot = window.__ddRoot || ReactDOM.createRoot(document.getElementById('root')); window.__ddRoot.render();