// ============================================================ // App.jsx — Lógica principal: auth + sheets + navegação // ============================================================ const App = () => { // ── Estado ───────────────────────────────────────────────── const [authState, setAuthState] = React.useState('idle'); // idle | loading | ready | error const [authError, setAuthError] = React.useState(''); const [user, setUser] = React.useState(() => { try { return JSON.parse(localStorage.getItem('crm_user') || 'null'); } catch { return null; } }); const [spreadsheetId, setSpreadsheetId] = React.useState( () => localStorage.getItem('crm_spreadsheet_id') || null ); const [leads, setLeads] = React.useState([]); const [view, setView] = React.useState('dashboard'); const [modal, setModal] = React.useState(null); const [syncStatus, setSyncStatus] = React.useState('idle'); // idle | saving | saved | error const [gsiReady, setGsiReady] = React.useState(false); const saveTimer = React.useRef(null); // ── Aguardar Google Identity Services carregar ────────────── React.useEffect(() => { const check = setInterval(() => { if (window.google && window.google.accounts) { setGsiReady(true); clearInterval(check); } }, 100); return () => clearInterval(check); }, []); // ── Tentar login silencioso ao carregar ───────────────────── React.useEffect(() => { if (!gsiReady) return; if (user && spreadsheetId) { // Usuário já logou antes — tentar renovar token silenciosamente setAuthState('loading'); Auth.silentSignIn( (token) => handleAuthSuccess(token, true), () => setAuthState('idle') // falhou silenciosamente → mostra tela de login ); } }, [gsiReady]); // ── Fluxo pós-autenticação ────────────────────────────────── async function handleAuthSuccess(token, silent = false) { setAuthState('loading'); setAuthError(''); try { // 1. Obter dados do usuário const userInfo = await Auth.getUserInfo(token); setUser(userInfo); localStorage.setItem('crm_user', JSON.stringify(userInfo)); // 2. Verificar / criar planilha let sheetId = localStorage.getItem('crm_spreadsheet_id'); if (sheetId) { const exists = await Sheets.spreadsheetExists(token, sheetId); if (!exists) sheetId = null; // planilha foi deletada → criar nova } if (!sheetId) { sheetId = await Sheets.createSpreadsheet(token, userInfo.name); localStorage.setItem('crm_spreadsheet_id', sheetId); } setSpreadsheetId(sheetId); // 3. Carregar leads const loadedLeads = await Sheets.loadLeads(token, sheetId); setLeads(loadedLeads); setAuthState('ready'); } catch (err) { console.error('Erro na inicialização:', err); setAuthError('Erro ao conectar com o Google. Tente novamente.'); setAuthState('error'); } } // ── Login manual ──────────────────────────────────────────── function handleSignIn() { if (!gsiReady) return; setAuthState('loading'); Auth.signIn( (token) => handleAuthSuccess(token, false), (err) => { setAuthError('Login cancelado ou recusado. Tente novamente.'); setAuthState('idle'); } ); } // ── Logout ────────────────────────────────────────────────── function handleSignOut() { Auth.signOut(); setUser(null); setSpreadsheetId(null); setLeads([]); setAuthState('idle'); } // ── Salvar no Sheets (debounced 800ms) ────────────────────── function debouncedSave(leadsToSave) { clearTimeout(saveTimer.current); setSyncStatus('saving'); saveTimer.current = setTimeout(async () => { const token = Auth.getToken(); const sheetId = localStorage.getItem('crm_spreadsheet_id'); if (!token || !sheetId) { setSyncStatus('error'); return; } try { await Sheets.saveAllLeads(token, sheetId, leadsToSave); setSyncStatus('saved'); setTimeout(() => setSyncStatus('idle'), 2000); } catch (err) { console.error('Erro ao salvar:', err); setSyncStatus('error'); } }, 800); } // ── Atualizar leads + triggerar save ───────────────────────── const updateLeads = React.useCallback((updater) => { setLeads(prev => { const next = typeof updater === 'function' ? updater(prev) : updater; debouncedSave(next); return next; }); }, []); // ── CRUD de leads ─────────────────────────────────────────── const openLead = (lead) => setModal(lead); const newLead = () => setModal({}); const closeModal = () => setModal(null); const saveLead = (lead) => { updateLeads(prev => { const exists = prev.find(l => l.id === lead.id); if (exists) return prev.map(l => l.id === lead.id ? lead : l); return [...prev, lead]; }); }; // ── Indicador de sincronização ────────────────────────────── const syncLabel = { idle: '', saving: '↻ Salvando...', saved: '✓ Salvo', error: '⚠ Erro ao salvar' }; const syncColor = { idle: 'transparent', saving: '#4a4f6a', saved: '#4ade80', error: '#fb923c' }; // ── Render ────────────────────────────────────────────────── if (authState === 'idle' || authState === 'error') { return ( ); } if (authState === 'loading') { return (
Conectando ao Google Sheets...
Preparando sua planilha de leads
); } return (
{/* Barra de sync no topo */} {syncStatus !== 'idle' && (
{syncLabel[syncStatus]}
)}
{view === 'dashboard' && } {view === 'pipeline' && } {view === 'planilha' && }
{modal !== null && ( )}
); }; const appStyles = { loadScreen: { minHeight: '100vh', background: '#0d1020', display: 'flex', alignItems: 'center', justifyContent: 'center', }, loadCard: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16, background: '#151828', border: '1px solid #1e2235', borderRadius: 16, padding: '48px 56px', }, loadSpinner: { width: 40, height: 40, borderRadius: '50%', border: '3px solid #1e2235', borderTopColor: '#4f7cff', animation: 'spin 0.8s linear infinite', }, loadText: { fontSize: 16, fontWeight: 600, color: '#c7d2ff' }, loadSub: { fontSize: 12, color: '#4a4f6a' }, syncBar: { textAlign: 'center', fontSize: 11, fontWeight: 600, padding: '4px 0', background: '#0d0f18', borderBottom: '1px solid #1e2235', letterSpacing: 0.3, flexShrink: 0, }, }; // Animação do spinner const styleTag = document.createElement('style'); styleTag.textContent = `@keyframes spin { to { transform: rotate(360deg); } }`; document.head.appendChild(styleTag); ReactDOM.createRoot(document.getElementById('root')).render();