// Tela de Configurações do sistema — abas laterais (Perfil, Empresa, Portadores, // Centros de Custo, Formas de Pagamento, Aparência) const { useState: useState_S, useMemo: useMemo_S, useEffect: useEffect_S } = React; function readFileAsDataURL(file, cb) { if (!file) return; const r = new FileReader(); r.onload = () => cb(r.result); r.readAsDataURL(file); } const ABAS_CONFIG = [ { id: 'perfil', label: 'Meu Perfil', icon: 'user' }, { id: 'empresa', label: 'Minha Empresa', icon: 'building' }, { id: 'seguranca', label: 'Segurança', icon: 'lock' }, { id: 'usuarios', icon: 'users', label: 'Usuários & Acesso', desc: 'Gerencie permissões da equipe' }, { id: 'portadores', label: 'Portadores', icon: 'bank' }, { id: 'centros', label: 'Centros de Custo', icon: 'target' }, { id: 'formas', label: 'Formas de Pagamento', icon: 'creditCard' }, { id: 'notificacoes', label: 'Notificações', icon: 'bell' }, { id: 'aparencia', label: 'Aparência', icon: 'settings' }, { id: 'backup', label: 'Backup de Dados', icon: 'download' }, ]; function Configuracoes(props) { const { initialTab, session, perfil, onUpdatePerfil, empresaInfo, onUpdateEmpresaInfo, portadores = [], centrosCusto = [], formasPagamento = [], onSavePortador, onDeletePortador, onSaveCentro, onDeleteCentro, onSaveForma, onDeleteForma, tweaks, setTweak, colorOptions, fontOptions, data, } = props; const [aba, setAba] = useState_S(initialTab || 'perfil'); const isMobile = useIsMobile(768); const cargoLower = (perfil?.cargo || '').toLowerCase(); const isVisualizador = cargoLower.includes('visualizador'); const isAnalista = cargoLower.includes('analista'); const isAdmin = cargoLower.includes('admin') || cargoLower.includes('administrador'); const abasFiltradas = useMemo_S(() => { return ABAS_CONFIG.filter(aba => { if (isVisualizador) { return !['usuarios', 'portadores', 'centros', 'formas', 'backup'].includes(aba.id); } if (isAnalista) { return !['usuarios', 'backup'].includes(aba.id); } return true; }); }, [isVisualizador, isAnalista]); // Reage à navegação externa (dropdown de perfil / ícone ⚙️) useEffect_S(() => { if (initialTab) setAba(initialTab); }, [initialTab]); return (
{/* Menu de abas (lateral no desktop, topo rolável no mobile) */} {/* Conteúdo */}
{aba === 'perfil' && } {aba === 'empresa' && } {aba === 'seguranca' && } {aba === 'usuarios' && !isVisualizador && !isAnalista && } {aba === 'portadores' && !isVisualizador && } {aba === 'centros' && !isVisualizador && } {aba === 'formas' && !isVisualizador && } {aba === 'notificacoes' && } {aba === 'aparencia' && } {aba === 'backup' && !isVisualizador && !isAnalista && }
); } // Cabeçalho padrão de cada aba function AbaHeader({ titulo, descricao }) { return (

{titulo}

{descricao}
); } // ----- Aba 1: Meu Perfil ----- function PerfilTab({ perfil, onUpdate }) { const toast = useToast(); const isMobile = useIsMobile(); const [f, setF] = useState_S({ ...perfil }); const [erros, setErros] = useState_S({}); const [salvando, setSalvando] = useState_S(false); const set = (k, v) => setF(p => ({ ...p, [k]: v })); async function salvar() { const e = {}; if (!f.nome?.trim()) e.nome = 'Nome obrigatório'; if (f.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(f.email)) e.email = 'E-mail inválido'; if (Object.keys(e).length > 0) { setErros(e); return; } setErros({}); setSalvando(true); await onUpdate({ ...f, inicial: (f.nome || 'K').charAt(0).toUpperCase() }); setSalvando(false); toast.push('Perfil atualizado com sucesso!', 'success'); } return (
{/* Foto */}
{f.foto ? : (f.nome || 'K').charAt(0).toUpperCase()}
{f.foto && }
set('nome', e.target.value)} style={{ borderColor: erros.nome ? '#dc2626' : undefined }} /> {erros.nome && {erros.nome}} set('email', e.target.value)} style={{ borderColor: erros.email ? '#dc2626' : undefined }} /> {erros.email && {erros.email}} set('telefone', e.target.value)} placeholder="(00) 00000-0000" /> set('cargo', e.target.value)} disabled={perfil?.cargo?.toLowerCase()?.includes('visualizador')} style={perfil?.cargo?.toLowerCase()?.includes('visualizador') ? { background: 'var(--c-surface)', cursor: 'not-allowed', color: 'var(--c-text-muted)' } : undefined} />
{salvando ? <> Salvando... : 'Salvar alterações'}
); } // ----- Aba 2: Minha Empresa (escritório dono do sistema) ----- function EmpresaInfoTab({ info, onUpdate }) { const toast = useToast(); const isMobile = useIsMobile(); const [f, setF] = useState_S({ ...info }); const set = (k, v) => setF(p => ({ ...p, [k]: v })); function salvar() { const errRazao = Validacao.required(f.razaoSocial, 'Razão Social'); if (errRazao) return toast.push(errRazao, 'error'); const errCNPJ = f.cnpj ? Validacao.cnpj(f.cnpj) : null; if (errCNPJ) return toast.push(errCNPJ, 'error'); const errCEP = f.cep ? Validacao.cep(f.cep) : null; if (errCEP) return toast.push(errCEP, 'error'); const errTelefone = f.telefone ? Validacao.telefone(f.telefone) : null; if (errTelefone) return toast.push(errTelefone, 'error'); const errEmail = f.email ? Validacao.email(f.email) : null; if (errEmail) return toast.push(errEmail, 'error'); onUpdate(f); toast.push('Dados da empresa atualizados'); } return (
{/* Logo */}
{f.logo ? : (f.nomeFantasia || f.razaoSocial || 'K').charAt(0).toUpperCase()}
{f.logo && }
set('razaoSocial', e.target.value)} /> set('nomeFantasia', e.target.value)} /> set('cnpj', e.target.value)} placeholder="00.000.000/0000-00" /> set('rua', e.target.value)} /> set('numero', e.target.value)} /> set('bairro', e.target.value)} /> set('cidade', e.target.value)} /> set('estado', e.target.value)} placeholder="UF" /> set('cep', e.target.value)} placeholder="00000-000" /> set('telefone', e.target.value)} /> set('email', e.target.value)} />
Salvar
); } // ----- Aba 3: Portadores ----- const TIPOS_PORTADOR = [ { v: 'banco', label: 'Banco' }, { v: 'caixa', label: 'Caixa' }, { v: 'cofre', label: 'Cofre' }, ]; const CORES_PRESET = ['#CC092F', '#EC7000', '#0066B3', '#16a34a', '#7C3AED', '#0EA5E9', '#F59E0B', '#475569']; function PortadoresConfigTab({ portadores, onSave, onDelete, isAdmin }) { const toast = useToast(); const [modal, setModal] = useState_S(null); async function salvar(p) { await onSave(p); setModal(null); } async function excluir(id) { if (!confirm('Excluir este portador?')) return; await onDelete(id); } return (
{portadores.length} portador{portadores.length !== 1 ? 'es' : ''}
{isAdmin && setModal({ id: uid('port'), nome: '', tipo: 'banco', cor: CORES_PRESET[0] })}>Adicionar portador}
{portadores.map(p => (
{p.nome}
{p.tipo}
{isAdmin && } {isAdmin && }
))} {portadores.length === 0 && }
{modal && setModal(null)} onSave={salvar} />}
); } function PortadorModal({ portador, portadores = [], onClose, onSave }) { const [f, setF] = useState_S({ ...portador }); const set = (k, v) => setF(p => ({ ...p, [k]: v })); const novo = !portador.nome; const toast = useToast(); function salvar() { const errNome = Validacao.required(f.nome, 'Nome'); if (errNome) { toast.push(errNome, 'error'); return; } const existeDup = portadores.some(p => p.id !== f.id && p.nome.trim().toLowerCase() === f.nome.trim().toLowerCase() && p.tipo === f.tipo); if (existeDup) { toast.push(`Já existe um portador cadastrado com o nome "${f.nome.trim()}".`, 'error'); return; } onSave(f); } return ( Cancelar Salvar }>
set('nome', e.target.value)} placeholder="Ex: Bradesco" autoFocus /> set('tipo', e.target.value)} options={[ ...TIPOS_PORTADOR.map(t => ({ value: t.v, label: t.label })) ]} />
set('cor', e.target.value)} style={{ width: 44, height: 36, border: '1px solid var(--c-border)', borderRadius: 8, background: 'transparent', cursor: 'pointer', padding: 2 }} />
{CORES_PRESET.map(c => (
); } // ----- Aba 4: Centros de Custo ----- function CentrosConfigTab({ centros, onSave, onDelete }) { const toast = useToast(); const [modal, setModal] = useState_S(null); async function salvar(c) { await onSave(c); setModal(null); } async function excluir(id) { if (!confirm('Excluir este centro de custo?')) return; await onDelete(id); } return (
{centros.length} centro{centros.length !== 1 ? 's' : ''}
setModal({ id: uid('cc'), nome: '', tipo: 'saida' })}>Adicionar centro
{centros.map(c => (
{c.nome}
{c.tipo === 'entrada' ? 'Entrada' : 'Saída'}
))} {centros.length === 0 && }
{modal && setModal(null)} onSave={salvar} />}
); } function CentroModal({ centro, onClose, onSave }) { const [f, setF] = useState_S({ ...centro }); const set = (k, v) => setF(p => ({ ...p, [k]: v })); const novo = !centro.nome; const toast = useToast(); function salvar() { const errNome = Validacao.required(f.nome, 'Nome'); if (errNome) { toast.push(errNome, 'error'); return; } onSave(f); } return ( Cancelar Salvar }>
set('nome', e.target.value)} placeholder="Ex: Marketing" autoFocus /> set('tipo', e.target.value)} options={[ { value: "entrada", label: "Entrada (receita)" }, { value: "saida", label: "Saída (despesa)" } ]} />
); } // ----- Aba 5: Formas de Pagamento ----- function FormasConfigTab({ formas, onSave, onDelete }) { const toast = useToast(); const [nova, setNova] = useState_S(''); async function adicionar(e) { if (e && e.preventDefault) e.preventDefault(); const n = nova.trim(); const errNova = Validacao.required(n, 'Nome da forma de pagamento'); if (errNova) return toast.push(errNova, 'error'); if (formas.includes(n)) return toast.push('Forma de pagamento já existe', 'error'); await onSave(n); setNova(''); } async function excluir(f) { if (!confirm(`Excluir forma de pagamento "${f}"?`)) return; await onDelete(f); } return (
setNova(e.target.value)} placeholder="Nova forma de pagamento..." onKeyDown={e => { if (e.key === 'Enter') adicionar(); }} /> Adicionar
{formas.map(f => (
{f}
))} {formas.length === 0 &&
Nenhuma forma cadastrada.
}
); } // ----- Aba 6: Aparência ----- function ConfigToggle({ label, value, onChange }) { return ( ); } function ConfigRow({ label, hint, children }) { return (
{label}
{hint &&
{hint}
}
{children}
); } function AparenciaTab({ tweaks, setTweak, colorOptions, fontOptions }) { const densidades = [ { v: 'compact', label: 'Compacto' }, { v: 'comfortable', label: 'Confortável' }, { v: 'spacious', label: 'Espaçoso' }, ]; return (
{(colorOptions || []).map(c => (
setTweak('darkMode', v)} /> setTweak('fontFamily', e.target.value)} style={{ width: 180 }} options={[ ...(fontOptions || []).map(f => ({ value: f, label: f })) ]} />
{densidades.map(d => ( ))}
setTweak('sidebarCollapsed', v)} />
); } const iconBtnCfg = { background: 'transparent', border: '1px solid var(--c-border)', borderRadius: 6, width: 28, height: 28, cursor: 'pointer', color: 'var(--c-text-muted)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }; // ========================================================================= // ABA 3: SEGURANÇA // ========================================================================= function AbaSeguranca({ session, perfil }) { const [senhaAtual, setSenhaAtual] = useState_S(''); const [novaSenha, setNovaSenha] = useState_S(''); const [confirmar, setConfirmar] = useState_S(''); const [loading, setLoading] = useState_S(false); const toast = useToast(); const [erros, setErros] = useState_S({}); const isVisualizador = (perfil?.cargo || '').toLowerCase().includes('visualizador'); async function alterarSenha() { const e = {}; if (!senhaAtual) e.atual = 'Senha atual obrigatória'; const errNova = Validacao.senha(novaSenha); if (errNova) e.nova = errNova; if (novaSenha !== confirmar) e.confirmar = 'As senhas não coincidem'; if (Object.keys(e).length > 0) { setErros(e); return; } setErros({}); setLoading(true); // Reautentica com senha atual primeiro const { error: errReauth } = await window.supabaseClient.auth.signInWithPassword({ email: session.user.email, password: senhaAtual }); if (errReauth) { toast.push('Senha atual incorreta', 'error'); setLoading(false); return; } // Altera senha const { error } = await window.supabaseClient.auth.updateUser({ password: novaSenha }); if (error) { toast.push('Erro ao alterar senha', 'error'); } else { toast.push('Senha alterada com sucesso!', 'success'); setSenhaAtual(''); setNovaSenha(''); setConfirmar(''); } setLoading(false); } async function sairTodosDispositivos() { if (!confirm('Isso vai encerrar sua sessão em todos os dispositivos. Continuar?')) return; await window.supabaseClient.auth.signOut({ scope: 'global' }); } return (
{/* Alterar senha */}
setSenhaAtual(e.target.value)} placeholder="••••••••" style={{ borderColor: erros.atual ? '#dc2626' : undefined }} /> {erros.atual && {erros.atual}} setNovaSenha(e.target.value)} placeholder="Mínimo 6 caracteres" style={{ borderColor: erros.nova ? '#dc2626' : undefined }} /> {erros.nova && {erros.nova}} setConfirmar(e.target.value)} placeholder="Repita a nova senha" style={{ borderColor: erros.confirmar ? '#dc2626' : undefined }} /> {erros.confirmar && {erros.confirmar}}
{loading ? 'Alterando...' : 'Alterar senha'}
{/* Informações da sessão atual */}
{/* Encerrar sessões */} {!isVisualizador && ( Sair de todos os dispositivos )} {/* Excluir conta */} {!isVisualizador && (
Excluir minha conta
Remove permanentemente sua conta e todos os dados associados (empresas, lançamentos, configurações). Esta ação não pode ser desfeita.
{ if (prompt('Digite "EXCLUIR" para confirmar:') === 'EXCLUIR') { window.supabaseClient.auth.signOut(); toast.push('Solicitação enviada. Conta será excluída em até 24h.', 'error'); } }}> Excluir minha conta
)}
); } // Componente indicador de força de senha function IndicadorForcaSenha({ senha }) { if (!senha) return null; const forca = calcularForcaSenha(senha); const cores = { fraca:'#dc2626', media:'#f59e0b', forte:'#16a34a', 'muito forte':'#0ea5e9' }; const larguras = { fraca:'25%', media:'50%', forte:'75%', 'muito forte':'100%' }; return (
Senha {forca}
); } function calcularForcaSenha(senha) { let pontos = 0; if (senha.length >= 8) pontos++; if (senha.length >= 12) pontos++; if (/[A-Z]/.test(senha)) pontos++; if (/[0-9]/.test(senha)) pontos++; if (/[^A-Za-z0-9]/.test(senha)) pontos++; if (pontos <= 1) return 'fraca'; if (pontos <= 2) return 'media'; if (pontos <= 3) return 'forte'; return 'muito forte'; } function SecaoConfig({ titulo, descricao, children }) { return (
{titulo}
{descricao &&
{descricao}
}
{children}
); } function InfoLinha({ label, valor }) { return (
{label} {valor}
); } function AbaUsuarios({ session, data }) { const [usuarios, setUsuarios] = useState_S([]); const [usuariosEmpresas, setUsuariosEmpresas] = useState_S({}); const [loading, setLoading] = useState_S(true); const [actionLoading, setActionLoading] = useState_S(false); const [busca, setBusca] = useState_S(''); const [filtroRole, setFiltroRole] = useState_S('todos'); const [filtroStatus, setFiltroStatus] = useState_S('todos'); const [selecionados, setSelecionados] = useState_S([]); const [editUsuario, setEditUsuario] = useState_S(null); const [viewUsuario, setViewUsuario] = useState_S(null); const [confirmRevogar, setConfirmRevogar] = useState_S(null); const [confirmLote, setConfirmLote] = useState_S(false); const [showCriar, setShowCriar] = useState_S(false); // Criar usuario states const [nomeNovo, setNomeNovo] = useState_S(''); const [emailNovo, setEmailNovo] = useState_S(''); const [senhaNova, setSenhaNova] = useState_S(''); const [papelNovo, setPapelNovo] = useState_S('analista'); const [empresasSelecionadas, setEmpresasSelecionadas] = useState_S([]); const [erros, setErros] = useState_S({}); const toast = useToast(); const isMobile = useIsMobile(768); const PAPEIS = [ { value:'admin', label:'Administrador', desc:'Acesso total a todas as empresas' }, { value:'analista', label:'Analista', desc:'Pode criar e editar lançamentos nas empresas liberadas' }, { value:'visualizador', label:'Visualizador', desc:'Apenas visualiza dados nas empresas liberadas' }, ]; const corPapel = { admin:'#7c3aed', 'Administrador':'#7c3aed', 'Administrador(a)':'#7c3aed', analista:'#3b82f6', 'Analista':'#3b82f6', operador:'#3b82f6', visualizador:'#64748b', 'Visualizador':'#64748b' }; const PAPEL_LABELS = { admin: 'Administrador', 'Administrador':'Administrador', 'Administrador(a)':'Administrador(a)', operador: 'Analista', analista: 'Analista', 'Analista':'Analista', visualizador: 'Visualizador', 'Visualizador':'Visualizador' }; useEffect_S(() => { carregarUsuarios(); }, []); async function carregarUsuarios() { setLoading(true); try { const token = await getToken(); const response = await fetch('https://svgvtmkqjvxsoduohfuy.supabase.co/functions/v1/admin-criar-usuario', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ action: 'list' }) }); let perfis = []; let vinculacoes = []; if (response.ok) { const result = await response.json(); perfis = result.perfis || []; vinculacoes = result.vinculacoes || []; // Filtra fora o próprio usuário atual perfis = perfis.filter(p => p.id !== session.user.id); } else { const errJson = await response.json().catch(() => ({})); console.warn('Erro na listagem:', errJson.error); } const { data: convs } = await window.supabaseClient.from('convites') .select('*').eq('user_id', session.user.id).eq('status', 'pendente').order('created_at', { ascending: false }); const mapVinculacoes = {}; if (vinculacoes) { vinculacoes.forEach(v => { if (!mapVinculacoes[v.user_id]) mapVinculacoes[v.user_id] = []; mapVinculacoes[v.user_id].push(v.empresa_id); }); } const lista = []; if (perfis) lista.push(...perfis.map(p => ({ ...p, isConvite: false }))); if (convs) { convs.forEach(c => lista.push({ id: c.id, nome: c.nome || c.email_convidado, email: c.email_convidado, cargo: c.papel, status: 'pendente', isConvite: true, empresas_ids: c.empresas_ids, created_at: c.created_at, ativo: true })); } setUsuarios(lista); setUsuariosEmpresas(mapVinculacoes); } catch (err) { toast.push('Erro ao carregar usuários: ' + err.message, 'error'); } setLoading(false); } async function getToken() { const { data: { session: s } } = await window.supabaseClient.auth.getSession(); return s?.access_token; } async function executarExclusao(id, isConvite) { setActionLoading(true); try { const token = await getToken(); const response = await fetch('https://svgvtmkqjvxsoduohfuy.supabase.co/functions/v1/admin-criar-usuario', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ action: 'delete', id, isConvite }) }); if (!response.ok) { const errJson = await response.json().catch(() => ({})); throw new Error(errJson.error || 'Erro ao excluir usuário'); } toast.push('Usuário removido com sucesso!', 'success'); setSelecionados(prev => prev.filter(s => s !== id)); carregarUsuarios(); } catch (err) { toast.push(err.message, 'error'); } setActionLoading(false); } async function executarExclusaoLote() { setActionLoading(true); let sucesso = 0, falha = 0; for (const id of selecionados) { const u = usuarios.find(u => u.id === id); if (!u) continue; try { const token = await getToken(); const response = await fetch('https://svgvtmkqjvxsoduohfuy.supabase.co/functions/v1/admin-criar-usuario', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ action: 'delete', id, isConvite: u.isConvite }) }); if (response.ok) sucesso++; else falha++; } catch { falha++; } } if (sucesso > 0) toast.push(`${sucesso} usuário(s) removido(s) com sucesso!`, 'success'); if (falha > 0) toast.push(`Falha ao remover ${falha} usuário(s).`, 'error'); setSelecionados([]); setConfirmLote(false); carregarUsuarios(); setActionLoading(false); } async function salvarEdicao(payload) { setActionLoading(true); try { const token = await getToken(); const response = await fetch('https://svgvtmkqjvxsoduohfuy.supabase.co/functions/v1/admin-criar-usuario', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ action: 'update', ...payload }) }); if (!response.ok) { const errJson = await response.json().catch(() => ({})); throw new Error(errJson.error || 'Erro ao atualizar usuário'); } toast.push('Usuário atualizado com sucesso!', 'success'); setEditUsuario(null); carregarUsuarios(); } catch (err) { toast.push(err.message, 'error'); } setActionLoading(false); } async function criarUsuario() { const e = {}; if (!nomeNovo?.trim()) e.nome = 'Nome obrigatório'; if (!emailNovo?.trim()) e.email = 'E-mail obrigatório'; else { const errEmail = Validacao.email(emailNovo); if (errEmail) e.email = errEmail; } const errSenha = Validacao.senha(senhaNova); if (errSenha) e.senha = errSenha; if (papelNovo !== 'admin' && empresasSelecionadas.length === 0) { toast.push('Selecione pelo menos uma empresa', 'error'); return; } if (Object.keys(e).length > 0) { setErros(e); return; } setErros({}); setActionLoading(true); try { const token = await getToken(); const response = await fetch('https://svgvtmkqjvxsoduohfuy.supabase.co/functions/v1/admin-criar-usuario', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ email: emailNovo.toLowerCase().trim(), password: senhaNova, nome: nomeNovo.trim(), cargo: PAPEIS.find(p=>p.value===papelNovo)?.label || 'Analista', empresasIds: papelNovo === 'admin' ? data.empresas.map(e=>e.id) : empresasSelecionadas }) }); if (!response.ok) { const errJson = await response.json().catch(() => ({})); throw new Error(errJson.error || 'Erro ao criar usuário'); } toast.push(`Usuário ${nomeNovo} criado com sucesso!`, 'success'); setNomeNovo(''); setEmailNovo(''); setSenhaNova(''); setEmpresasSelecionadas([]); setShowCriar(false); carregarUsuarios(); } catch (err) { toast.push(err.message, 'error'); } setActionLoading(false); } // --- Filtragem --- const usuariosFiltrados = useMemo_S(() => { return usuarios.filter(u => { const buscaLower = busca.toLowerCase(); if (buscaLower && !u.nome?.toLowerCase().includes(buscaLower) && !u.email?.toLowerCase().includes(buscaLower)) return false; if (filtroRole !== 'todos') { const cargoNorm = (u.cargo || '').toLowerCase(); if (filtroRole === 'admin' && !cargoNorm.includes('admin')) return false; if (filtroRole === 'analista' && !cargoNorm.includes('analista') && !cargoNorm.includes('operador')) return false; if (filtroRole === 'visualizador' && !cargoNorm.includes('visualizador')) return false; } if (filtroStatus === 'ativo' && u.ativo === false) return false; if (filtroStatus === 'inativo' && u.ativo !== false) return false; if (filtroStatus === 'pendente' && u.status !== 'pendente') return false; return true; }); }, [usuarios, busca, filtroRole, filtroStatus]); const allSelected = usuariosFiltrados.length > 0 && usuariosFiltrados.every(u => selecionados.includes(u.id)); function toggleSelectAll() { if (allSelected) { setSelecionados(prev => prev.filter(id => !usuariosFiltrados.find(u => u.id === id))); } else { const ids = usuariosFiltrados.map(u => u.id); setSelecionados(prev => [...new Set([...prev, ...ids])]); } } function toggleSelect(id) { setSelecionados(prev => prev.includes(id) ? prev.filter(s => s !== id) : [...prev, id]); } function getNomeEmpresas(u) { const vinculadas = u.isConvite ? (u.empresas_ids || []) : (usuariosEmpresas[u.id] || []); return data.empresas.filter(e => vinculadas.includes(e.id)).map(e => e.nome); } function getCargoNormalizado(cargo) { const c = (cargo || '').toLowerCase(); if (c.includes('admin')) return 'admin'; if (c.includes('analista') || c.includes('operador')) return 'analista'; if (c.includes('visualizador')) return 'visualizador'; return c; } // --- Rendering --- const checkboxStyle = (checked) => ({ width: 18, height: 18, borderRadius: 4, flexShrink: 0, cursor: 'pointer', border: `1.5px solid ${checked ? 'var(--c-primary)' : 'var(--c-border)'}`, background: checked ? 'var(--c-primary)' : 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.15s ease' }); return (
{/* --- Toolbar superior --- */}
{/* Search */}
setBusca(e.target.value)} placeholder="Buscar por nome ou e-mail..." style={{ width: '100%', padding: '8px 10px 8px 28px', border: '1.5px solid var(--c-border)', borderRadius: 8, fontSize: 13, background: 'var(--c-bg)', color: 'var(--c-text)', fontFamily: 'inherit', outline: 'none', position: 'relative' }} />
{/* Filter: Cargo */} setFiltroRole(e.target.value)} options={[ { value: 'todos', label: 'Todos os cargos' }, { value: 'admin', label: 'Administrador' }, { value: 'analista', label: 'Analista' }, { value: 'visualizador', label: 'Visualizador' } ]} style={{ minWidth: 150 }} /> {/* Filter: Status */} setFiltroStatus(e.target.value)} options={[ { value: 'todos', label: 'Todos os status' }, { value: 'ativo', label: 'Ativo' }, { value: 'inativo', label: 'Inativo' }, { value: 'pendente', label: 'Pendente' } ]} style={{ minWidth: 140 }} />
{/* Batch delete */} {selecionados.length > 0 && ( setConfirmLote(true)} style={{ background:'#dc2626', color:'#fff', border:'1px solid #dc2626', fontSize: 12, padding: '7px 14px' }}> Excluir ({selecionados.length}) )} {/* Criar button */} setShowCriar(true)} style={{ fontSize: 12, padding: '7px 14px' }}> Novo Usuário
{/* --- Summary cards --- */}
{[ { label: 'Total', value: usuarios.length, color: 'var(--c-primary)', icon: 'users' }, { label: 'Ativos', value: usuarios.filter(u => u.ativo !== false && u.status !== 'pendente').length, color: '#16a34a', icon: 'check' }, { label: 'Inativos', value: usuarios.filter(u => u.ativo === false).length, color: '#dc2626', icon: 'x' }, { label: 'Pendentes', value: usuarios.filter(u => u.status === 'pendente').length, color: '#f59e0b', icon: 'alert' } ].map((card, i) => (
{card.value}
{card.label}
))}
{/* --- Tabela de usuários --- */} {loading ? (
) : usuariosFiltrados.length === 0 ? ( ) : (
{/* Cabeçalho da tabela (só desktop) */} {!isMobile && (
{allSelected && }
Usuário
E-mail
Cargo
Status
Ações
)} {/* Linhas */} {usuariosFiltrados.map((u, idx) => { const isSelected = selecionados.includes(u.id); const cargoNorm = getCargoNormalizado(u.cargo); const cor = corPapel[cargoNorm] || corPapel[u.cargo] || 'var(--c-primary)'; const nomesEmpresas = getNomeEmpresas(u); const isInativo = u.ativo === false; const isPendente = u.status === 'pendente'; if (isMobile) { // --- Mobile card layout --- return (
toggleSelect(u.id)} style={{ ...checkboxStyle(isSelected), marginTop: 2 }}> {isSelected && }
{(u.nome || '?').charAt(0).toUpperCase()}
{u.nome}
{u.email}
{PAPEL_LABELS[u.cargo] || u.cargo} {isPendente && Pendente} {isInativo && Inativo} {!isPendente && !isInativo && Ativo} {nomesEmpresas.length} empresa(s)
{!u.isConvite && ( )}
); } // --- Desktop row --- return (
{/* Checkbox */}
toggleSelect(u.id)} style={checkboxStyle(isSelected)}> {isSelected && }
{/* Nome + Avatar */}
{(u.nome || '?').charAt(0).toUpperCase()}
{u.nome}
{nomesEmpresas.length} empresa(s)
{/* Email */}
{u.email}
{/* Cargo badge */}
{PAPEL_LABELS[u.cargo] || u.cargo}
{/* Status */}
{isPendente && Pendente} {isInativo && !isPendente && Inativo} {!isPendente && !isInativo && Ativo}
{/* Ações */}
{!u.isConvite && ( )}
); })} {/* Rodapé da tabela */}
{selecionados.length > 0 ? `${selecionados.length} de ${usuariosFiltrados.length} selecionado(s)` : `${usuariosFiltrados.length} usuário(s) encontrado(s)`} {selecionados.length > 0 && ( )}
)} {/* ====== MODAIS ====== */} {/* Modal: Visualizar Usuário */} {viewUsuario && (() => { const u = viewUsuario; const cargoNorm = getCargoNormalizado(u.cargo); const cor = corPapel[cargoNorm] || corPapel[u.cargo] || 'var(--c-primary)'; const nomesEmpresas = getNomeEmpresas(u); const isInativo = u.ativo === false; const isPendente = u.status === 'pendente'; return ( setViewUsuario(null)} title="Detalhes do Usuário" width={520} footer={<> {!u.isConvite && ( { setViewUsuario(null); setEditUsuario({ id: u.id, nome: u.nome, email: u.email, cargo: cargoNorm, empresasIds: u.isConvite ? (u.empresas_ids || []) : (usuariosEmpresas[u.id] || []), ativo: u.ativo !== false }); }}> Editar )} setViewUsuario(null)}>Fechar }>
{/* Avatar + nome */}
{(u.nome || '?').charAt(0).toUpperCase()}
{u.nome}
{u.email}
{/* Info rows */}
{[ { label: 'Cargo', value: {PAPEL_LABELS[u.cargo] || u.cargo} }, { label: 'Status', value: isPendente ? Pendente : isInativo ? Inativo : Ativo }, { label: 'Empresas vinculadas', value: nomesEmpresas.length === 0 ? Nenhuma :
{nomesEmpresas.map((n, i) => ( {n} ))}
}, { label: 'Criado em', value: {u.created_at ? new Date(u.created_at).toLocaleDateString('pt-BR') : '—'} } ].map((row, i, arr) => (
{row.label}
{row.value}
))}
); })()} {/* Modal: Criar Usuário */} {showCriar && ( { setShowCriar(false); setErros({}); }} title="Criar Novo Usuário" width={560} footer={<> { setShowCriar(false); setErros({}); }} disabled={actionLoading}>Cancelar {actionLoading ? 'Criando...' : 'Criar Usuário'} }>
setNomeNovo(e.target.value)} placeholder="João Silva" style={{ borderColor: erros.nome ? '#dc2626' : undefined }} /> {erros.nome && {erros.nome}} setEmailNovo(e.target.value)} placeholder="joao@empresa.com" style={{ borderColor: erros.email ? '#dc2626' : undefined }} /> {erros.email && {erros.email}}
setSenhaNova(e.target.value)} placeholder="Defina uma senha" style={{ borderColor: erros.senha ? '#dc2626' : undefined }} /> {erros.senha && {erros.senha}} { setPapelNovo(e.target.value); if(e.target.value==='admin') setEmpresasSelecionadas(data.empresas.map(em=>em.id)); }} options={PAPEIS.map(p=>({value:p.value, label:p.label}))} />
{papelNovo !== 'admin' && (
Empresas com acesso
{data.empresas?.length === 0 ? (
Nenhuma empresa cadastrada
) : (data.empresas || []).map((emp, i) => { const selecionada = empresasSelecionadas.includes(emp.id); return (
{ if (selecionada) setEmpresasSelecionadas(empresasSelecionadas.filter(id => id !== emp.id)); else setEmpresasSelecionadas([...empresasSelecionadas, emp.id]); }} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 14px', cursor: 'pointer', borderBottom: i < data.empresas.length - 1 ? '1px solid var(--c-border)' : 'none', background: selecionada ? 'var(--c-primary-soft)' : 'transparent', transition: 'background 0.1s' }}>
{selecionada && }
{emp.nome}
); })}
Selecione quais empresas este usuário poderá acessar.
)}
)} {/* Modal: Editar Usuário */} {editUsuario && ( setEditUsuario(null)} loading={actionLoading} onSave={salvarEdicao} /> )} {/* Modal: Confirmar exclusão individual */} {confirmRevogar && ( { await executarExclusao(confirmRevogar.id, confirmRevogar.isConvite); setConfirmRevogar(null); }} onCancelar={() => setConfirmRevogar(null)} /> )} {/* Modal: Confirmar exclusão em lote */} {confirmLote && ( setConfirmLote(false)} /> )}
); } function ModalEditarUsuario({ usuario, todasEmpresas, onClose, onSave, loading }) { const [nome, setNome] = useState_S(usuario.nome); const [email, setEmail] = useState_S(usuario.email); const [cargo, setCargo] = useState_S(usuario.cargo); const [ativo, setAtivo] = useState_S(usuario.ativo); const [empresasSelecionadas, setEmpresasSelecionadas] = useState_S(usuario.empresasIds || []); const toast = useToast(); const PAPEIS = [ { value: 'admin', label: 'Administrador' }, { value: 'analista', label: 'Analista' }, { value: 'visualizador', label: 'Visualizador' } ]; function salvar() { if (!nome.trim()) return toast.push('Nome é obrigatório', 'error'); if (!email.trim()) return toast.push('E-mail é obrigatório', 'error'); onSave({ id: usuario.id, nome: nome.trim(), email: email.trim().toLowerCase(), cargo, ativo, empresasIds: cargo === 'admin' ? todasEmpresas.map(e => e.id) : empresasSelecionadas }); } const checkboxStyle = (checked) => ({ width: 18, height: 18, borderRadius: 4, flexShrink: 0, cursor: 'pointer', border: `1.5px solid ${checked ? 'var(--c-primary)' : 'var(--c-border)'}`, background: checked ? 'var(--c-primary)' : 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.15s ease' }); return ( Cancelar {loading ? 'Salvando...' : 'Salvar Alterações'} }>
setNome(e.target.value)} /> setEmail(e.target.value)} />
{ setCargo(e.target.value); if (e.target.value === 'admin') { setEmpresasSelecionadas(todasEmpresas.map(emp => emp.id)); } }} options={PAPEIS} /> setAtivo(e.target.value === 'ativo')} options={[ { value: 'ativo', label: 'Ativo' }, { value: 'inativo', label: 'Inativo' } ]} />
{cargo !== 'admin' && (
Empresas com acesso
{todasEmpresas.length === 0 ? (
Nenhuma empresa cadastrada
) : todasEmpresas.map((emp, i) => { const selecionada = empresasSelecionadas.includes(emp.id); return (
{ if (selecionada) setEmpresasSelecionadas(empresasSelecionadas.filter(id => id !== emp.id)); else setEmpresasSelecionadas([...empresasSelecionadas, emp.id]); }} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 14px', cursor: 'pointer', borderBottom: i < todasEmpresas.length - 1 ? '1px solid var(--c-border)' : 'none', background: selecionada ? 'var(--c-primary-soft)' : 'transparent', transition: 'background 0.1s' }} >
{selecionada && }
{emp.nome}
); })}
)}
); } // ========================================================================= // ABA 8: NOTIFICAÇÕES // ========================================================================= function AbaNotificacoes({ session }) { const [prefs, setPrefs] = useState_S({ email_vencimento: true, email_vencimento_dias: 3, email_resumo_semanal: true, email_resumo_dia_semana: 1, email_inadimplencia: true, email_novo_lancamento: false, email_relatorio_mensal: true, }); const [loading, setLoading] = useState_S(false); const [salvando, setSalvando] = useState_S(false); const toast = useToast(); const DIAS_SEMANA = ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb']; useEffect_S(() => { carregarPrefs(); }, []); async function carregarPrefs() { setLoading(true); const { data } = await window.supabaseClient .from('preferencias_notificacao') .select('*').eq('user_id', session.user.id); if (data && data.length > 0) setPrefs(data[0]); setLoading(false); } async function salvar() { setSalvando(true); const { error } = await window.supabaseClient .from('preferencias_notificacao') .upsert({ user_id: session.user.id, ...prefs }, { onConflict: 'user_id' }); if (error) toast.push('Erro ao salvar preferências', 'error'); else toast.push('Preferências de notificação salvas!'); setSalvando(false); } function set(k, v) { setPrefs(p => ({ ...p, [k]: v })); } if (loading) return
Carregando...
; return (
set('email_vencimento', v)} /> {prefs.email_vencimento && (
Alertar com set('email_vencimento_dias', parseInt(e.target.value))} style={{ width:80 }} options={[ { value: 1, label: "1 dia" }, { value: 2, label: "2 dias" }, { value: 3, label: "3 dias" }, { value: 5, label: "5 dias" }, { value: 7, label: "7 dias" } ]} /> de antecedência
)} set('email_inadimplencia', v)} />
set('email_resumo_semanal', v)} /> {prefs.email_resumo_semanal && (
Enviar toda set('email_resumo_dia_semana', parseInt(e.target.value))} style={{ width:100 }} options={[ ...DIAS_SEMANA.map((d, i) => ({ value: i, label: d })) ]} />
)} set('email_relatorio_mensal', v)} /> set('email_novo_lancamento', v)} />
{salvando ? 'Salvando...' : 'Salvar preferências'} Os e-mails serão enviados para {session?.user?.email}
{/* Nota */}
ℹ️ As notificações por e-mail requerem configuração de um serviço de envio (Resend, SendGrid ou SMTP). As preferências já são salvas e estarão prontas quando o serviço for integrado.
); } function ToggleNotif({ label, desc, checked, onChange }) { return (
{/* Toggle inline — não depende de componente externo */}
{label}
{desc}
); } function AbaBackup({ data }) { const toast = useToast(); function doExport() { exportarBackup(data); toast.push('Backup gerado com sucesso!'); } return (
Este arquivo contém abas separadas para: Empresas, Lançamentos, Portadores, e Centros de Custo. Mantenha este arquivo em local seguro, pois ele contém todas as informações financeiras registradas no sistema.
Baixar Backup (.xlsx)
); } Object.assign(window, { Configuracoes });