/* ============================================================
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(
);