// 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 && (
)}
{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}
}
);
}
// ----- 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).
)}
{modo === 'recuperar' && (
)}
);
}
// Render
ReactDOM.createRoot(document.getElementById('root')).render(
);