// App principal - sidebar colapsável + topbar com botão Empresas e search overlay const { useState: useState_A, useEffect: useEffect_A, useMemo: useMemo_A, useRef: useRef_A } = React; const DEFAULT_TWEAKS = /*EDITMODE-BEGIN*/{ "primaryColor": "#2563EB", "fontFamily": "Inter", "darkMode": false, "density": "comfortable", "sidebarCollapsed": false }/*EDITMODE-END*/; const COLOR_OPTIONS = ['#2F5D8A','#123A63','#0B1D39','#9CC7E6','#2563EB','#6366F1']; const FONT_OPTIONS = ['Inter', 'Manrope', 'IBM Plex Sans', 'DM Sans', 'Sora']; function hexToRgb(hex) { const m = hex.replace('#', ''); return [parseInt(m.slice(0, 2), 16), parseInt(m.slice(2, 4), 16), parseInt(m.slice(4, 6), 16)]; } function App() { const toast = useToast(); const [session, setSession] = useState_A(null); const [loadingAuth, setLoadingAuth] = useState_A(true); const [perfil, setPerfil] = useState_A(null); const [emailConfirmado, setEmailConfirmado] = useState_A(false); useEffect_A(() => { const hash = window.location.hash; if (hash.includes('access_token') || hash.includes('type=recovery')) { const params = new URLSearchParams(hash.replace('#', '?')); const tipo = params.get('type'); if (tipo === 'recovery') { setRoute({ view: 'reset-senha' }); } else if (tipo === 'signup') { window.history.replaceState({}, '', window.location.pathname); setEmailConfirmado(true); } } supabaseClient.auth.getSession().then(({ data: { session } }) => { setSession(session); if (session) carregarPerfil(session.user.id); setLoadingAuth(false); }); const { data: { subscription } } = supabaseClient.auth.onAuthStateChange( (_event, session) => { setSession(session); if (session) carregarPerfil(session.user.id); else { setPerfil(null); } } ); return () => subscription.unsubscribe(); }, []); async function carregarPerfil(userId) { const { data } = await supabaseClient.from('perfis').select('*').eq('id', userId).single(); if (data) { if (data.ativo === false) { toast.push('Sua conta foi desativada pelo administrador.', 'error'); await window.supabaseClient.auth.signOut(); setPerfil(null); return; } setPerfil({ ...data, foto: data.foto_url, inicial: data.nome?.charAt(0) }); } } useEffect_A(() => { function onUnhandled(e) { console.error('Erro não tratado:', e.reason); } window.addEventListener('unhandledrejection', onUnhandled); return () => window.removeEventListener('unhandledrejection', onUnhandled); }, []); const { data, setData, recarregar } = useAppData(session?.user?.id); const [route, setRoute] = useState_A({ view: 'central' }); const [searchOpen, setSearchOpen] = useState_A(false); const [newEmpOpen, setNewEmpOpen] = useState_A(false); const [editEmp, setEditEmp] = useState_A(null); const [notifOpen, setNotifOpen] = useState_A(false); const [empresasOpen, setEmpresasOpen] = useState_A(false); const [perfilOpen, setPerfilOpen] = useState_A(false); const [mobileNavOpen, setMobileNavOpen] = useState_A(false); const isMobile = useIsMobile(768); const [empresaInfo, setEmpresaInfo] = useState_A({ razaoSocial: 'KS Gestão & BPO Financeiro Ltda', nomeFantasia: 'KS Gestão', cnpj: '12.345.678/0001-00', rua: 'Av. Paulista', numero: '1000', bairro: 'Bela Vista', cidade: 'São Paulo', estado: 'SP', cep: '01310-100', telefone: '(11) 3000-0000', email: 'contato@ksgestao.com.br', logo: null, }); const [t, setTweak] = useTweaks(DEFAULT_TWEAKS); const [confirmDeleteEmp, setConfirmDeleteEmp] = useState_A(null); const [sidebarCollapsed, setSidebarCollapsed] = useState_A(isMobile); useEffect_A(() => { const onResize = () => setSidebarCollapsed(window.innerWidth < 768); window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []); useEffect_A(() => { const root = document.documentElement; const [r, g, b] = hexToRgb(t.primaryColor); root.style.setProperty('--c-primary', t.primaryColor); root.style.setProperty('--c-primary-rgb', `${r}, ${g}, ${b}`); root.style.setProperty('--c-primary-soft', `rgba(${r}, ${g}, ${b}, 0.10)`); root.style.setProperty('--font-family', `'${t.fontFamily}', system-ui, sans-serif`); if (t.darkMode) { root.style.setProperty('--c-bg', '#0a0a0f'); root.style.setProperty('--c-bg-sidebar', '#0a0a0f'); root.style.setProperty('--c-surface', '#15151c'); root.style.setProperty('--c-text', '#e7e9ee'); root.style.setProperty('--c-text-muted', '#8b8fa0'); root.style.setProperty('--c-border', '#262633'); root.style.setProperty('--c-card', '#15151c'); document.body.classList.add('dark'); } else { root.style.setProperty('--c-bg', '#F8FAFC'); root.style.setProperty('--c-bg-sidebar', '#0F172A'); root.style.setProperty('--c-surface', '#fff'); root.style.setProperty('--c-text', '#0F172A'); root.style.setProperty('--c-text-muted', '#64748B'); root.style.setProperty('--c-border', '#E2E8F0'); root.style.setProperty('--c-card', '#fff'); document.body.classList.remove('dark'); } }, [t]); useEffect_A(() => { function onKey(e) { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); setSearchOpen(o => !o); } if (e.key === 'Escape') { setSearchOpen(false); setNotifOpen(false); setEmpresasOpen(false); setPerfilOpen(false); setMobileNavOpen(false); } } window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, []); // Click fora fecha os dropdowns useEffect_A(() => { function onClick(e) { if (!e.target.closest('[data-dropdown="empresas"]')) setEmpresasOpen(false); if (!e.target.closest('[data-dropdown="notif"]')) setNotifOpen(false); if (!e.target.closest('[data-dropdown="perfil"]')) setPerfilOpen(false); } document.addEventListener('mousedown', onClick); return () => document.removeEventListener('mousedown', onClick); }, []); async function savePortador(p) { const isNew = String(p.id).startsWith('port-'); if (isNew) { const { data: res, error } = await window.supabaseClient.from('portadores') .insert({ user_id: session.user.id, nome: p.nome, tipo: p.tipo, cor: p.cor }) .select().single(); if (!error) { setData(d => ({ ...d, portadores: [...d.portadores, { id: res.id, nome: res.nome, tipo: res.tipo, cor: res.cor }] })); toast.push('Portador adicionado!'); } else toast.push('Erro ao adicionar portador', 'error'); } else { const result = await window.supabaseClient.from('portadores') .update({ nome: p.nome, tipo: p.tipo, cor: p.cor, updated_at: new Date().toISOString() }) .eq('id', p.id) .eq('user_id', session.user.id); if (!result.error) { setData(d => ({ ...d, portadores: d.portadores.map(x => x.id === p.id ? p : x) })); toast.push('Portador atualizado!'); } else { console.error('Supabase Error Portadores Update:', result.error); toast.push('Erro ao atualizar portador', 'error'); } } } async function deletePortador(id) { const { error } = await window.supabaseClient.from('portadores').update({ ativo: false }).eq('id', id).eq('user_id', session.user.id); if (!error) { setData(d => ({ ...d, portadores: d.portadores.filter(x => x.id !== id) })); toast.push('Portador excluído'); } else toast.push('Erro ao excluir portador', 'error'); } async function saveCentro(c) { const isNew = String(c.id).startsWith('cc-'); if (isNew) { const { data: res, error } = await window.supabaseClient.from('centros_custo') .insert({ user_id: session.user.id, nome: c.nome, tipo: c.tipo }) .select().single(); if (!error) { setData(d => ({ ...d, centrosCusto: [...d.centrosCusto, { id: res.id, nome: res.nome, tipo: res.tipo }] })); toast.push('Centro adicionado!'); } else toast.push('Erro ao adicionar centro', 'error'); } else { const result = await window.supabaseClient.from('centros_custo') .update({ nome: c.nome, tipo: c.tipo, updated_at: new Date().toISOString() }) .eq('id', c.id) .eq('user_id', session.user.id); if (!result.error) { setData(d => ({ ...d, centrosCusto: d.centrosCusto.map(x => x.id === c.id ? c : x) })); toast.push('Centro atualizado!'); } else { console.error('Supabase Error Centros Update:', result.error); toast.push('Erro ao atualizar centro', 'error'); } } } async function deleteCentro(id) { const { error } = await window.supabaseClient.from('centros_custo').update({ ativo: false }).eq('id', id).eq('user_id', session.user.id); if (!error) { setData(d => ({ ...d, centrosCusto: d.centrosCusto.filter(x => x.id !== id) })); toast.push('Centro excluído'); } else toast.push('Erro ao excluir centro', 'error'); } async function saveForma(nome) { const ordem = data.formasPagamento.length; const { error } = await window.supabaseClient.from('formas_pagamento') .insert({ user_id: session.user.id, nome, ordem }); if (!error) { setData(d => ({ ...d, formasPagamento: [...d.formasPagamento, nome] })); toast.push('Forma adicionada!'); } else toast.push('Erro ao adicionar forma', 'error'); } async function deleteForma(nome) { const { error } = await window.supabaseClient.from('formas_pagamento').update({ ativo: false }).eq('nome', nome).eq('user_id', session.user.id); if (!error) { setData(d => ({ ...d, formasPagamento: d.formasPagamento.filter(x => x !== nome) })); toast.push('Forma excluída'); } else toast.push('Erro ao excluir forma', 'error'); } async function createEmpresa(emp) { const docValue = (emp.documento && emp.documento.trim() !== '') ? emp.documento.trim() : null; const { data: resData, error } = await window.supabaseClient.from('empresas').insert({ user_id: session.user.id, tipo_pessoa: emp.tipoPessoa || 'pj', documento: docValue, cnpj: docValue, nome: emp.nome, nome_fantasia: emp.tipoPessoa === 'pf' ? null : emp.nomeFantasia, segmento: emp.segmento, responsavel: emp.responsavel, email: emp.email, telefone: emp.telefone, portadores_ativos: emp.portadoresAtivos || [], centros_ativos: emp.centrosAtivos || [] }).select().single(); if (error) { toast.push('Erro ao criar empresa: ' + error.message, 'error'); return; } setData(d => ({ ...d, empresas: [...d.empresas, { id: resData.id, tipoPessoa: resData.tipo_pessoa, documento: resData.documento, cnpj: resData.cnpj, nome: resData.nome, nomeFantasia: resData.nome_fantasia, segmento: resData.segmento, responsavel: resData.responsavel, email: resData.email, telefone: resData.telefone, portadoresAtivos: resData.portadores_ativos || [], centrosAtivos: resData.centros_ativos || [], criadaEm: resData.created_at }], lancamentos: { ...d.lancamentos, [resData.id]: [] } })); toast.push('Empresa cadastrada com sucesso!'); } async function editEmpresa(emp) { const docValue = (emp.documento && emp.documento.trim() !== '') ? emp.documento.trim() : null; const { error } = await window.supabaseClient.from('empresas') .update({ tipo_pessoa: emp.tipoPessoa || 'pj', documento: docValue, cnpj: docValue, nome: emp.nome, nome_fantasia: emp.tipoPessoa === 'pf' ? null : emp.nomeFantasia, segmento: emp.segmento, responsavel: emp.responsavel, email: emp.email, telefone: emp.telefone, portadores_ativos: emp.portadoresAtivos || [], centros_ativos: emp.centrosAtivos || [], updated_at: new Date().toISOString() }) .eq('id', emp.id); if (error) { toast.push('Erro ao atualizar empresa', 'error'); return; } setData(d => ({ ...d, empresas: d.empresas.map(e => e.id === emp.id ? emp : e) })); toast.push('Empresa atualizada!'); } async function deleteEmpresa(id) { if (!session?.access_token) return; try { const response = await fetch('https://svgvtmkqjvxsoduohfuy.supabase.co/functions/v1/admin-criar-usuario', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${session.access_token}` }, body: JSON.stringify({ action: 'delete_empresa', id }) }); if (!response.ok) { const errJson = await response.json().catch(() => ({})); toast.push('Erro ao excluir empresa: ' + (errJson.error || 'Verifique permissões e pendências.'), 'error'); return; } setData(d => { const novo = { ...d.lancamentos }; delete novo[id]; return { ...d, empresas: d.empresas.filter(e => e.id !== id), lancamentos: novo }; }); if (route.view === 'empresa' && route.id === id) setRoute({ view: 'central' }); toast.push('Empresa excluída com sucesso!'); } catch (err) { toast.push('Erro de conexão ao excluir.', 'error'); } } async function upsertLanc(l) { const isArray = Array.isArray(l); const lancsToProcess = isArray ? l : [l]; const results = []; const createdIds = []; const empresaId = lancsToProcess[0].empresaId; const cur = data.lancamentos[empresaId] || []; for (const lanc of lancsToProcess) { const payload = { user_id: session.user.id, empresa_id: lanc.empresaId, tipo: lanc.tipo, descricao: lanc.descricao, valor: parseFloat(String(lanc.valor).replace(',', '.')) || 0, vencimento: lanc.vencimento, competencia: lanc.competencia || competenciaFromDate(lanc.vencimento), portador_id: lanc.portadorId || null, centro_custo_id: lanc.centroCustoId || null, forma_pagamento: lanc.formaPagamento || null, pago: Boolean(lanc.pago), pagamento_data: lanc.pagamento?.data || null, pagamento_comprovante: lanc.pagamento?.comprovante || null, observacao: lanc.observacao || '', parcela_ref: lanc.parcelaRef || null, parcela_num: lanc.parcelaNum || null, parcela_total: lanc.parcelaTotal || null }; const existe = cur.some(x => x.id === lanc.id); let result; if (existe) { result = await window.supabaseClient.from('lancamentos') .update({ ...payload, updated_at: new Date().toISOString() }).eq('id', lanc.id).select().single(); } else { result = await window.supabaseClient.from('lancamentos').insert(payload).select().single(); } if (result.error) { console.error('Erro Supabase:', result.error); toast.push('Erro ao salvar lançamento: ' + result.error.message, 'error'); continue; } results.push({ id: result.data.id, empresaId: result.data.empresa_id, tipo: result.data.tipo, descricao: result.data.descricao, valor: parseFloat(result.data.valor), vencimento: result.data.vencimento, competencia: result.data.competencia, portadorId: result.data.portador_id, centroCustoId: result.data.centro_custo_id, formaPagamento: result.data.forma_pagamento, pago: result.data.pago, pagamento: result.data.pago ? { data: result.data.pagamento_data, comprovante: result.data.pagamento_comprovante } : null, observacao: result.data.observacao, parcelaRef: result.data.parcela_ref, parcelaNum: result.data.parcela_num, parcelaTotal: result.data.parcela_total }); if (!existe) createdIds.push(result.data.id); } if (results.length > 0) { setData(d => { const current = d.lancamentos[empresaId] || []; const novo = [...current]; results.forEach(res => { const idx = novo.findIndex(x => x.id === res.id); if (idx >= 0) novo[idx] = res; else novo.push(res); }); return { ...d, lancamentos: { ...d.lancamentos, [empresaId]: novo } }; }); if (isArray) toast.push(`${results.length} lançamentos criados!`); else toast.push(createdIds.length > 0 ? 'Lançamento criado!' : 'Lançamento atualizado!'); } createdIds.forEach(id => { window.supabaseClient.functions.invoke('notificacao-lancamento', { body: { lancamento_id: id, user_id: session.user.id } }).catch(() => {}); }); } async function deleteLanc(empId, lancId) { const { error } = await window.supabaseClient.from('lancamentos').delete().eq('id', lancId); if (error) { toast.push('Erro ao excluir', 'error'); return; } setData(d => ({ ...d, lancamentos: { ...d.lancamentos, [empId]: (d.lancamentos[empId] || []).filter(x => x.id !== lancId) } })); } async function payLanc(empId, lancId, payload) { const lanc = (data.lancamentos[empId] || []).find(x => x.id === lancId); if (!lanc) return; const valorPago = parseFloat(payload.valor); const saldoAnterior = lanc.saldoRestante !== undefined ? lanc.saldoRestante : lanc.valor; const quitado = valorPago >= saldoAnterior; // 1. Inserir o registro de pagamento parcial const { data: pgData, error: pgError } = await window.supabaseClient.from('pagamentos_parciais') .insert({ user_id: session.user.id, lancamento_id: lancId, valor: valorPago, data: payload.data, portador_id: payload.portadorId, comprovante: payload.comprovante || `CMP-${Math.floor(Math.random() * 90000 + 10000)}.pdf` }) .select().single(); if (pgError) { toast.push('Erro ao registrar pagamento parcial: ' + pgError.message, 'error'); return; } // 2. Se quitou, atualiza o lançamento principal if (quitado) { const { error: errorLanc } = await window.supabaseClient.from('lancamentos') .update({ pago: true, portador_id: payload.portadorId, pagamento_data: payload.data, pagamento_comprovante: payload.comprovante || pgData.comprovante, updated_at: new Date().toISOString() }) .eq('id', lancId); if (errorLanc) { toast.push('Erro ao quitar lançamento: ' + errorLanc.message, 'error'); await window.supabaseClient.from('pagamentos_parciais').delete().eq('id', pgData.id); return; } } // 3. Recarrega os dados para recalcular tudo com os novos valores await recarregar(); } const currentEmpresa = route.view === 'empresa' ? data.empresas.find(e => e.id === route.id) : null; const collapsed = t.sidebarCollapsed; const notifs = useMemo_A(() => { const hoje = todayISO(); const arr = []; data.empresas.forEach(e => { (data.lancamentos[e.id] || []).forEach(l => { if (l.pago) return; const s = lancStatus(l, hoje); if (s === 'vencido' || s === 'vencendo') arr.push({ ...l, empresaNome: e.nome, empresaId: e.id, statusKey: s }); }); }); return arr.sort((a, b) => a.vencimento.localeCompare(b.vencimento)); }, [data]); const breadcrumb = useMemo_A(() => { if (route.view === 'central') return [{ label: 'Central de Gestão' }]; if (route.view === 'lancamentos') return [{ label: 'Lançamentos' }]; if (route.view === 'relatorios') return [{ label: 'Relatórios' }]; if (route.view === 'configuracoes') return [{ label: 'Configurações' }]; if (route.view === 'empresa' && currentEmpresa) { return [{ label: 'Central de Gestão', onClick: () => setRoute({ view: 'central' }) }, { label: currentEmpresa.nome }]; } return []; }, [route, currentEmpresa]); if (loadingAuth) return (
); if (!session) { if (route.view === 'reset-senha') return ; return ; } if (data.loading) return (
{[1,2,3,4].map(i => (
))}
); return (
setTweak('sidebarCollapsed', v)} route={route} setRoute={setRoute} isMobile={isMobile} mobileOpen={mobileNavOpen} onCloseMobile={() => setMobileNavOpen(false)} perfil={perfil} />
setMobileNavOpen(o => !o)} onSearchClick={() => setSearchOpen(true)} notifCount={notifs.length} notifOpen={notifOpen} onNotifClick={() => { setNotifOpen(o => !o); setEmpresasOpen(false); }} notifs={notifs} onNotifSelect={(empId) => { setRoute({ view: 'empresa', id: empId }); setNotifOpen(false); }} empresasCount={data.empresas.length} empresasOpen={empresasOpen} onEmpresasClick={() => { setEmpresasOpen(o => !o); setNotifOpen(false); }} data={data} onSelectEmpresa={(id) => { setRoute({ view: 'empresa', id }); setEmpresasOpen(false); }} onNewEmpresa={() => { setEmpresasOpen(false); setNewEmpOpen(true); }} perfil={perfil} session={session} perfilOpen={perfilOpen} onPerfilClick={() => { setPerfilOpen(o => !o); setNotifOpen(false); setEmpresasOpen(false); }} onOpenSettings={(tab) => { setRoute({ view: 'configuracoes', tab }); setPerfilOpen(false); }} />
{route.view === 'central' && ( setRoute({ view: 'empresa', id })} onCreateEmpresa={() => setNewEmpOpen(true)} onEditEmpresa={(emp) => setEditEmp(emp)} onDeleteEmpresa={deleteEmpresa} onOpenSearch={() => setSearchOpen(true)} /> )} {route.view === 'empresa' && currentEmpresa && ( setRoute({ view: 'central' })} onUpsertLanc={upsertLanc} onDeleteLanc={(lancId) => deleteLanc(currentEmpresa.id, lancId)} onPayLanc={(lancId, payload) => payLanc(currentEmpresa.id, lancId, payload)} /> )} {route.view === 'lancamentos' && setRoute({ view: 'empresa', id })} onUpsertLanc={upsertLanc} onPayLanc={(empId, lancId, payload) => payLanc(empId, lancId, payload)} onGoCentral={() => setRoute({ view: 'central' })} />} {route.view === 'relatorios' && } {route.view === 'configuracoes' && ( setEmpresaInfo(p => ({ ...p, ...novo }))} portadores={data.portadores || []} centrosCusto={data.centrosCusto || []} formasPagamento={data.formasPagamento || []} onSavePortador={savePortador} onDeletePortador={deletePortador} onSaveCentro={saveCentro} onDeleteCentro={deleteCentro} onSaveForma={saveForma} onDeleteForma={deleteForma} tweaks={t} setTweak={setTweak} colorOptions={COLOR_OPTIONS} fontOptions={FONT_OPTIONS} /> )}
setSearchOpen(false)} data={data} onSelectEmpresa={(id) => { setRoute({ view: 'empresa', id }); setSearchOpen(false); }} onNewEmpresa={() => { setSearchOpen(false); setNewEmpOpen(true); }} perfil={perfil} /> {(newEmpOpen || editEmp) && ( { setNewEmpOpen(false); setEditEmp(null); }} onSave={(emp) => { if (editEmp) editEmpresa(emp); else createEmpresa(emp); setNewEmpOpen(false); setEditEmp(null); }} onDelete={editEmp ? () => setConfirmDeleteEmp(editEmp) : null} /> )} {confirmDeleteEmp && ( { await deleteEmpresa(confirmDeleteEmp.id); setEditEmp(null); setConfirmDeleteEmp(null); }} onCancelar={() => setConfirmDeleteEmp(null)} /> )} setTweak('primaryColor', v)} /> setTweak('darkMode', v)} /> setTweak('fontFamily', v)} /> setTweak('density', v)} /> setTweak('sidebarCollapsed', v)} />
); } // ----- Sidebar — SEM lista de empresas ----- function Sidebar({ collapsed, setCollapsed, route, setRoute, isMobile, mobileOpen, onCloseMobile, perfil }) { const items = [ { id: 'central', label: 'Central de Gestão', icon: 'home', view: 'central' }, { id: 'lancamentos', label: 'Lançamentos', icon: 'list', view: 'lancamentos' }, { id: 'relatorios', label: 'Relatórios', icon: 'chart', view: 'relatorios' }, ]; // No mobile o menu é sempre expandido (drawer); no desktop respeita o colapso collapsed = isMobile ? false : collapsed; const w = collapsed ? 64 : 220; const navigate = (view) => { setRoute({ view }); if (isMobile) onCloseMobile(); }; const asideStyle = isMobile ? { width: 240, background: 'var(--c-bg-sidebar)', color: '#fff', display: 'flex', flexDirection: 'column', position: 'fixed', top: 0, bottom: 0, left: 0, zIndex: 1000, transform: mobileOpen ? 'translateX(0)' : 'translateX(-100%)', transition: 'transform 0.25s ease', overflow: 'hidden', boxShadow: mobileOpen ? '0 0 40px rgba(0,0,0,.4)' : 'none' } : { width: w, background: 'var(--c-bg-sidebar)', color: '#fff', display: 'flex', flexDirection: 'column', flexShrink: 0, position: 'fixed', top: 0, bottom: 0, left: 0, zIndex: 100, transition: 'width 0.22s ease', overflow: 'hidden' }; return ( <> {isMobile && mobileOpen && (
)} ); } // ----- TopBar ----- function TopBar({ breadcrumb, isMobile, onMenuClick, onSearchClick, notifCount, onNotifClick, notifOpen, notifs, onNotifSelect, empresasCount, empresasOpen, onEmpresasClick, data, onSelectEmpresa, onNewEmpresa, perfil, session, perfilOpen, onPerfilClick, onOpenSettings }) { return (
{/* Botão de menu (mobile) */} {isMobile && ( )} {/* Breadcrumb */}
{!isMobile && breadcrumb.map((b, i) => ( {i > 0 && } {b.onClick ? ( ) : ( {b.label} )} ))}
{/* Centro: busca */} {isMobile ? ( ) : ( {}} /> )} {/* Direita */}
{/* Botão Empresas com dropdown */}
{empresasOpen && ( )}
{!isMobile &&
} {/* Notificações */}
{notifOpen && }
{!isMobile && ( )} {!isMobile &&
} {/* Avatar + dropdown de perfil */}
{perfilOpen && }
); } // ----- Dropdown Empresas ----- function EmpresasDropdown({ data, onSelectEmpresa, onNewEmpresa, perfil }) { const cargoLower = (perfil?.cargo || '').toLowerCase(); const isAdmin = cargoLower.includes('admin') || cargoLower.includes('administrador'); const [q, setQ] = useState_A(''); const inputRef = useRef_A(null); const hoje = todayISO(); useEffect_A(() => { setTimeout(() => inputRef.current?.focus(), 60); }, []); const empresasComStats = useMemo_A(() => { return data.empresas.map(e => ({ ...e, stats: empresaStats(e, data.lancamentos[e.id] || [], hoje) })); }, [data]); const filtradas = useMemo_A(() => { if (!q.trim()) return empresasComStats; const ql = q.toLowerCase(); return empresasComStats.filter(e => e.nome.toLowerCase().includes(ql) || e.cnpj.includes(q) || (e.segmento || '').toLowerCase().includes(ql) ); }, [empresasComStats, q]); const grupos = [ { key: 'vencido', label: 'Vencidas', color: '#ef4444', items: filtradas.filter(e => e.stats.statusEmpresa === 'vencido') }, { key: 'vencendo', label: 'Vencendo', color: '#f59e0b', items: filtradas.filter(e => e.stats.statusEmpresa === 'vencendo') }, { key: 'em-dia', label: 'Em dia', color: '#16a34a', items: filtradas.filter(e => e.stats.statusEmpresa === 'em-dia') }, ]; return (
{/* Search */}
setQ(e.target.value)} placeholder="Buscar empresa..." style={{ flex: 1, border: 'none', outline: 'none', fontSize: 13, background: 'transparent', color: 'var(--c-text)', fontFamily: 'inherit' }} /> {q && }
{/* Nova empresa — visível apenas para administradores */} {isAdmin && ( )} {/* Lista agrupada por status */}
{filtradas.length === 0 && (
Nenhuma empresa encontrada
)} {grupos.map(g => { if (!g.items.length) return null; return (
{g.label} · {g.items.length}
{g.items.map(e => { const ic = stringToColor(e.nome); return ( ); })}
); })}
{/* Footer */}
{data.empresas.length} empresa{data.empresas.length !== 1 ? 's' : ''} ESC para fechar
); } // ----- Notif Dropdown ----- function NotifDropdown({ notifs, onSelect }) { return (
Notificações {notifs.length} pendência{notifs.length !== 1 ? 's' : ''}
{notifs.length === 0 ? (
Tudo em dia ✓
) : notifs.slice(0, 15).map(n => ( ))} {notifs.length > 15 && (
+ {notifs.length - 15} outros
)}
); } // ----- Dropdown de Perfil ----- function PerfilDropdown({ perfil, onOpenSettings }) { const itemStyle = { width: '100%', display: 'flex', alignItems: 'center', gap: 10, padding: '9px 14px', background: 'transparent', border: 'none', cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit', fontSize: 13, color: 'var(--c-text)', transition: 'background 0.1s' }; const items = [ { icon: 'user', label: 'Meu Perfil', tab: 'perfil' }, { icon: 'building', label: 'Minha Empresa', tab: 'empresa' }, { icon: 'settings', label: 'Configurações', tab: 'aparencia' }, ]; return (
{/* Cabeçalho */}
{perfil?.foto ? : (perfil?.inicial || (perfil?.nome || 'K').charAt(0))}
{perfil?.nome}
{perfil?.email}
{perfil?.cargo}
{items.map(it => ( ))}
); } const topBarBtn = { background: 'transparent', border: 'none', cursor: 'pointer', width: 34, height: 34, borderRadius: 8, color: 'var(--c-text-muted)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', position: 'relative', transition: 'background 0.15s, color 0.15s' }; // ----- Search Overlay (Cmd+K) ----- function SearchOverlay({ open, onClose, data, onSelectEmpresa, onNewEmpresa, perfil }) { const cargoLower = (perfil?.cargo || '').toLowerCase(); const isAdmin = cargoLower.includes('admin') || cargoLower.includes('administrador'); const [q, setQ] = useState_A(''); const inputRef = useRef_A(null); const hoje = todayISO(); useEffect_A(() => { if (open) { setQ(''); setTimeout(() => inputRef.current?.focus(), 50); } }, [open]); const empresasFiltered = useMemo_A(() => { return data.empresas.map(e => ({ ...e, stats: empresaStats(e, data.lancamentos[e.id] || [], hoje) })).filter(e => !q || e.nome.toLowerCase().includes(q.toLowerCase()) || e.cnpj.includes(q) || (e.segmento || '').toLowerCase().includes(q.toLowerCase())); }, [data, q]); if (!open) return null; return (
e.stopPropagation()} style={{ width: '94%', maxWidth: 640, background: 'var(--c-surface)', borderRadius: 14, boxShadow: '0 24px 64px rgba(15,23,42,.25)', overflow: 'hidden', border: '1px solid var(--c-border)', animation: 'scIn 0.18s cubic-bezier(.16,1,.3,1)' }}>
setQ(e.target.value)} placeholder="Buscar por empresa, CNPJ ou setor..." style={{ flex: 1, border: 'none', outline: 'none', fontSize: 16, background: 'transparent', color: 'var(--c-text)', fontFamily: 'inherit' }} /> ESC
{isAdmin && ( )}
{empresasFiltered.length} empresa{empresasFiltered.length !== 1 ? 's' : ''}
{empresasFiltered.length === 0 ? (
Nenhuma empresa para "{q}"
) : empresasFiltered.map(e => { const ic = stringToColor(e.nome); return ( ); })}
abrir ↑↓ navegar ESC fechar
KS Gestão & BPO
); } const kbdStyle = { fontSize: 10, color: 'var(--c-text-muted)', background: 'var(--c-surface)', padding: '1px 5px', borderRadius: 4, border: '1px solid var(--c-border)', fontFamily: 'ui-monospace, monospace', marginRight: 4 }; window.stringToColor = stringToColor; function stringToColor(s) { const palette = [ { bg: '#dbeafe', fg: '#1e40af' }, { bg: '#fce7f3', fg: '#9d174d' }, { bg: '#dcfce7', fg: '#166534' }, { bg: '#fef3c7', fg: '#92400e' }, { bg: '#f3e8ff', fg: '#6b21a8' }, { bg: '#ccfbf1', fg: '#115e59' }, { bg: '#fee2e2', fg: '#991b1b' }, { bg: '#e0e7ff', fg: '#3730a3' }, ]; let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; return palette[h % palette.length]; } // ----- 2-step Empresa Wizard ----- function EmpresaWizard({ empresa, todasEmpresas, portadores, centrosCusto, onClose, onSave, onDelete }) { const [step, setStep] = useState_A(1); const [f, setF] = useState_A(empresa || { id: uid('emp'), tipoPessoa: 'pj', documento: '', nome: '', nomeFantasia: '', segmento: '', responsavel: '', email: '', telefone: '', portadoresAtivos: [], centrosAtivos: [], criadaEm: todayISO() }); const [erros, setErros] = useState_A({}); const toast = useToast(); const set = (k, v) => setF(p => ({ ...p, [k]: v })); function validarStep1() { const e = {}; const errNome = Validacao.required(f.nome, f.tipoPessoa === 'pj' ? 'Razão Social' : 'Nome Completo'); let errDoc = null; if (f.documento && f.documento.trim() !== '') { errDoc = f.tipoPessoa === 'pj' ? Validacao.cnpj(f.documento) : Validacao.cpf(f.documento); if (!errDoc && todasEmpresas) { const cleanDoc = f.documento.replace(/\D/g, ''); const existe = todasEmpresas.find(emp => (emp.documento?.replace(/\D/g, '') === cleanDoc || emp.cnpj?.replace(/\D/g, '') === cleanDoc) && emp.id !== f.id); if (existe) { errDoc = 'Este documento já está cadastrado em outra empresa.'; } } } const errEmail = f.email ? Validacao.email(f.email) : null; const errTelefone = f.telefone ? Validacao.telefone(f.telefone) : null; if (errNome) e.nome = errNome; if (errDoc) e.documento = errDoc; if (errEmail) e.email = errEmail; if (errTelefone) e.telefone = errTelefone; if (Object.keys(e).length > 0) { setErros(e); Object.values(e).forEach(msg => toast.push(msg, 'error')); return false; } setErros({}); return true; } function proceedToStep2() { if (validarStep1()) setStep(2); } function submit() { if (validarStep1()) onSave(f); else setStep(1); } function togglePort(id) { set('portadoresAtivos', f.portadoresAtivos.includes(id) ? f.portadoresAtivos.filter(x => x !== id) : [...f.portadoresAtivos, id]); } function toggleCC(id) { set('centrosAtivos', f.centrosAtivos.includes(id) ? f.centrosAtivos.filter(x => x !== id) : [...f.centrosAtivos, id]); } return ( {onDelete && Excluir} Cancelar {step === 1 ? Continuar → : <> setStep(1)}>← Voltar{empresa ? 'Salvar' : 'Cadastrar'} } }> {/* Progress bar */}
{[{ n: 1, label: 'Dados básicos' }, { n: 2, label: 'Configurações' }].map((s, i) => (
= s.n ? 'var(--c-primary)' : 'var(--c-border)', color: step >= s.n ? '#fff' : 'var(--c-text-muted)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 700, fontSize: 11, flexShrink: 0, transition: 'all 0.2s' }}> {step > s.n ? : s.n}
{s.label}
{i === 0 &&
1 ? 'var(--c-primary)' : 'var(--c-border)', transition: 'background 0.2s' }} />} ))}
{step === 1 && (
set('nome', e.target.value)} placeholder={f.tipoPessoa === 'pj' ? "Ex: Padaria Bom Pão Ltda" : "Ex: João da Silva"} autoFocus style={{ borderColor: erros.nome ? '#dc2626' : undefined }} /> {erros.nome && {erros.nome}} {f.tipoPessoa === 'pj' && ( set('nomeFantasia', e.target.value)} placeholder="Como a empresa é conhecida" /> )} set('documento', f.tipoPessoa === 'pj' ? maskCNPJ(e.target.value) : maskCPF(e.target.value))} placeholder={f.tipoPessoa === 'pj' ? "00.000.000/0000-00" : "000.000.000-00"} maxLength={18} style={{ borderColor: erros.documento ? '#dc2626' : undefined }} /> {erros.documento && {erros.documento}} set('segmento', e.target.value)} placeholder="Ex: Alimentação" /> set('responsavel', e.target.value)} /> set('telefone', maskTelefone(e.target.value))} placeholder="(00) 00000-0000" maxLength={15} /> set('email', e.target.value)} placeholder="contato@empresa.com.br" style={{ borderColor: erros.email ? '#dc2626' : undefined }} /> {erros.email && {erros.email}}
)} {step === 2 && (
Portadores bancários
Selecione os bancos, caixas e cofres desta empresa
{portadores.map(p => { const checked = f.portadoresAtivos.includes(p.id); return ( ); })}
Centros de custo
Categorias para classificar os lançamentos
{centrosCusto.map(c => { const checked = f.centrosAtivos.includes(c.id); return ( ); })}
Tudo pronto! Após cadastrar, você poderá lançar receitas e despesas no workspace.
)} ); } // ----- BuscaGlobal ----- function BuscaGlobal({ data, onSelectEmpresa, onSelectLanc }) { const [q, setQ] = useState_A(''); const [aberta, setAberta] = useState_A(false); const inputRef = useRef_A(null); useEffect_A(() => { function onClickFora(e) { if (aberta && !e.target.closest('[data-busca]')) setAberta(false); } document.addEventListener('mousedown', onClickFora); return () => document.removeEventListener('mousedown', onClickFora); }, [aberta]); const resultados = useMemo_A(() => { if (q.length < 2) return { empresas: [], lancamentos: [] }; const ql = q.toLowerCase(); const empresas = data.empresas.filter(e => e.nome.toLowerCase().includes(ql) || (e.cnpj || '').includes(q) || (e.segmento || '').toLowerCase().includes(ql) ).slice(0, 5); const lancamentos = []; Object.entries(data.lancamentos).forEach(([empId, lancs]) => { const emp = data.empresas.find(e => e.id === empId); lancs.filter(l => l.descricao.toLowerCase().includes(ql)) .slice(0, 3) .forEach(l => lancamentos.push({ ...l, empresaNome: emp?.nome })); }); return { empresas, lancamentos: lancamentos.slice(0, 5) }; }, [q, data]); return (
{ setQ(e.target.value); setAberta(true); }} onFocus={() => setAberta(true)} placeholder="Pesquisar..." style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', color: 'inherit', fontFamily: 'inherit', fontSize: 'inherit' }} /> {q ? ( ) : ( ⌘K )}
{aberta && (q.length >= 2) && (
{resultados.empresas.length === 0 && resultados.lancamentos.length === 0 ? (
Nenhum resultado encontrado.
) : (
{resultados.empresas.length > 0 && (
Empresas
{resultados.empresas.map(e => ( ))}
)} {resultados.lancamentos.length > 0 && (
0 ? '1px solid var(--c-border)' : 'none', marginTop: resultados.empresas.length > 0 ? 6 : 0 }}>Lançamentos
{resultados.lancamentos.map(l => ( ))}
)}
)}
)}
); } function TelaResetSenha() { const [novaSenha, setNovaSenha] = useState_A(''); const [confirmar, setConfirmar] = useState_A(''); const [loading, setLoading] = useState_A(false); const [erro, setErro] = useState_A(''); const [sucesso, setSucesso] = useState_A(false); const toast = useToast(); async function salvar(e) { e.preventDefault(); const errSenha = Validacao.senha(novaSenha); if (errSenha) { setErro(errSenha); toast.push(errSenha, 'error'); return; } if (novaSenha !== confirmar) { const msg = 'As senhas não coincidem'; setErro(msg); toast.push(msg, 'error'); return; } setLoading(true); setErro(''); const { error } = await window.supabaseClient.auth.updateUser({ password: novaSenha }); if (error) { const msg = 'Erro ao redefinir senha: ' + error.message; setErro(msg); toast.push(msg, 'error'); } else { setSucesso(true); toast.push('Senha redefinida com sucesso!'); setTimeout(() => window.history.replaceState({}, '', window.location.pathname), 2000); } setLoading(false); } const layoutCentrado = { minHeight:'100vh', display:'flex', alignItems:'center', justifyContent:'center', background:'var(--c-bg)', padding:20 }; const cardLogin = { width:'100%', maxWidth:420, background:'var(--c-surface)', borderRadius:16, padding:36, border:'1px solid var(--c-border)', boxShadow:'0 20px 60px rgba(0,0,0,.1)' }; const logoStyle = { width:52, height:52, borderRadius:12, background:'var(--c-primary)', display:'flex', alignItems:'center', justifyContent:'center', margin:'0 auto 12px', fontWeight:800, fontSize:22, color:'#fff' }; if (sucesso) return (

Senha redefinida!

Redirecionando para o login...

); return (
K

Nova senha

Digite sua nova senha de acesso

{erro &&
{erro}
}
setNovaSenha(e.target.value)} placeholder="Mínimo 6 caracteres" autoFocus /> setConfirmar(e.target.value)} placeholder="Repita a senha" /> {loading ? 'Salvando...' : 'Redefinir senha'}
); } // ----- LoginScreen ----- function LoginScreen({ emailConfirmado }) { const [modo, setModo] = useState_A('login'); const [email, setEmail] = useState_A(''); const [senha, setSenha] = useState_A(''); const [lembrar, setLembrar] = useState_A(true); const [loading, setLoading] = useState_A(false); const [erro, setErro] = useState_A(''); const [erroEspecial, setErroEspecial] = useState_A(''); const [sucesso, setSucesso] = useState_A(''); const toast = useToast(); useEffect_A(() => { if (emailConfirmado) setSucesso('E-mail confirmado com sucesso! Você já pode entrar.'); }, [emailConfirmado]); async function handleLogin(e) { e.preventDefault(); setErro(''); setErroEspecial(''); setLoading(true); const errEmail = Validacao.email(email); if (errEmail) { setErro(errEmail); toast.push(errEmail, 'error'); setLoading(false); return; } const { error } = await window.supabaseClient.auth.signInWithPassword({ email, password: senha }); if (error) { if (error.message.includes('Email not confirmed')) { setErroEspecial('email_nao_confirmado'); } else { const tr = traduzirErroAuth(error.message); setErro(tr); toast.push(tr, 'error'); } } else { if (!lembrar) { const { data: { session } } = await window.supabaseClient.auth.getSession(); if (session) { sessionStorage.setItem('sb-session', JSON.stringify(session)); localStorage.removeItem(`sb-${window.SUPABASE_URL?.split('//')[1].split('.')[0] || 'svgvtmkqjvxsoduohfuy'}-auth-token`); } } } setLoading(false); } async function reenviarConfirmacao() { const { error } = await window.supabaseClient.auth.resend({ type: 'signup', email }); if (!error) toast.push('E-mail reenviado! Verifique sua caixa de entrada.'); else toast.push('Erro ao reenviar e-mail.', 'error'); } async function handleReset(e) { e.preventDefault(); setErro(''); setLoading(true); const errEmail = Validacao.email(email); if (errEmail) { setErro(errEmail); toast.push(errEmail, 'error'); setLoading(false); return; } const { error } = await window.supabaseClient.auth.resetPasswordForEmail(email, { redirectTo: window.location.origin }); if (error) { const tr = traduzirErroAuth(error.message); setErro(tr); toast.push(tr, 'error'); } else { toast.push('E-mail de recuperação enviado! Verifique sua caixa de entrada.'); setSucesso('E-mail de recuperação enviado! Verifique sua caixa de entrada.'); } setLoading(false); } function traduzirErroAuth(msg) { if (msg.includes('Invalid login')) return 'E-mail ou senha incorretos.'; if (msg.includes('Email not confirmed')) return 'Confirme seu e-mail antes de entrar.'; if (msg.includes('User already registered')) return 'Este e-mail já está cadastrado.'; if (msg.includes('Password should be')) return 'Senha deve ter pelo menos 6 caracteres.'; return 'Erro: ' + msg; } return (
K
KS Gestão
BPO Financeiro

{modo==='login' ? 'Entrar na conta' : 'Recuperar senha'}

{modo==='login' ? 'Acesse o painel de gestão' : 'Enviaremos um link para seu e-mail'}

{erro &&
{erro}
} {sucesso &&
{sucesso}
} {erroEspecial === 'email_nao_confirmado' && (
📧 Confirme seu e-mail primeiro
Enviamos um link de confirmação para {email}. Verifique sua caixa de entrada (e o spam).
)}
setEmail(e.target.value)} placeholder="seu@email.com" autoFocus /> {modo === 'login' && ( setSenha(e.target.value)} placeholder="••••••••" /> )} {modo === 'login' && (
setLembrar(e.target.checked)} style={{ width:15, height:15, accentColor:'var(--c-primary)', cursor:'pointer' }} />
)} {loading ? : modo==='login' ? 'Entrar' : 'Enviar link'}
{modo === 'recuperar' && (
)}
); } // Render ReactDOM.createRoot(document.getElementById('root')).render( );