// 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) */}
{!isMobile && Configurações
}
{abasFiltradas.map(a => {
const active = aba === a.id;
return (
setAba(a.id)} style={{
display: 'flex', alignItems: 'center', gap: 10, padding: '9px 12px',
borderRadius: 8, border: 'none', cursor: 'pointer', fontFamily: 'inherit',
fontSize: 13, fontWeight: active ? 600 : 500, textAlign: 'left',
width: isMobile ? 'auto' : '100%', whiteSpace: 'nowrap', flexShrink: 0,
background: active ? 'var(--c-primary-soft)' : 'transparent',
color: active ? 'var(--c-primary)' : 'var(--c-text)',
transition: 'background 0.12s, color 0.12s'
}}
onMouseEnter={e => { if (!active) e.currentTarget.style.background = 'var(--c-surface)'; }}
onMouseLeave={e => { if (!active) e.currentTarget.style.background = 'transparent'; }}
>
{a.label}
);
})}
{/* 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 (
);
}
// ----- 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()}
Enviar foto
readFileAsDataURL(e.target.files[0], v => set('foto', v))} />
{f.foto && set('foto', null)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--c-text-muted)', fontSize: 12, textAlign: 'left', padding: 0, fontFamily: 'inherit' }}>Remover 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()}
Enviar logo
readFileAsDataURL(e.target.files[0], v => set('logo', v))} />
{f.logo && set('logo', null)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--c-text-muted)', fontSize: 12, textAlign: 'left', padding: 0, fontFamily: 'inherit' }}>Remover 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 => (
{isAdmin &&
setModal({ ...p })} style={iconBtnCfg} title="Editar"> }
{isAdmin &&
excluir(p.id)} style={iconBtnCfg} title="Excluir"> }
))}
{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 => (
set('cor', c)} style={{ width: 24, height: 24, borderRadius: 6, background: c, cursor: 'pointer', border: f.cor.toLowerCase() === c.toLowerCase() ? '2px solid var(--c-text)' : '1px solid var(--c-border)' }} />
))}
);
}
// ----- 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'}
setModal({ ...c })} style={iconBtnCfg} title="Editar">
excluir(c.id)} style={iconBtnCfg} title="Excluir">
))}
{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}
excluir(f)} style={iconBtnCfg} title="Remover">
))}
{formas.length === 0 &&
Nenhuma forma cadastrada.
}
);
}
// ----- Aba 6: Aparência -----
function ConfigToggle({ label, value, onChange }) {
return (
onChange(!value)} role="switch" aria-checked={!!value} style={{
position: 'relative', width: 40, height: 22, borderRadius: 999, border: 'none', cursor: 'pointer',
background: value ? 'var(--c-primary)' : 'var(--c-border)', transition: 'background 0.15s', flexShrink: 0, padding: 0
}}>
);
}
function ConfigRow({ label, hint, children }) {
return (
);
}
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('primaryColor', c)} style={{
width: 28, height: 28, borderRadius: 8, background: c, cursor: 'pointer',
border: (tweaks.primaryColor || '').toLowerCase() === c.toLowerCase() ? '2px solid var(--c-text)' : '1px solid var(--c-border)',
boxShadow: (tweaks.primaryColor || '').toLowerCase() === c.toLowerCase() ? '0 0 0 2px var(--c-surface), 0 0 0 4px ' + c : 'none'
}} />
))}
setTweak('darkMode', v)} />
setTweak('fontFamily', e.target.value)} style={{ width: 180 }} options={[
...(fontOptions || []).map(f => ({ value: f, label: f }))
]} />
{densidades.map(d => (
setTweak('density', d.v)} style={{
padding: '6px 12px', border: 'none', borderRadius: 6, cursor: 'pointer', fontFamily: 'inherit',
fontSize: 12, fontWeight: 600,
background: tweaks.density === d.v ? 'var(--c-surface)' : 'transparent',
color: tweaks.density === d.v ? 'var(--c-primary)' : 'var(--c-text-muted)',
boxShadow: tweaks.density === d.v ? '0 1px 2px rgba(0,0,0,.06)' : 'none'
}}>{d.label}
))}
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 */}
{/* 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 (
);
}
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 */}
{/* 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 && (
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()}
{PAPEL_LABELS[u.cargo] || u.cargo}
{isPendente && Pendente }
{isInativo && Inativo }
{!isPendente && !isInativo && Ativo }
{nomesEmpresas.length} empresa(s)
setViewUsuario(u)} style={{ background: 'none', border: '1px solid var(--c-border)', borderRadius: 6, padding: '4px 10px', cursor: 'pointer', fontSize: 11, color: 'var(--c-text)', fontFamily: 'inherit' }}>
Visualizar
{!u.isConvite && (
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
})} style={{ background: 'none', border: '1px solid var(--c-border)', borderRadius: 6, padding: '4px 10px', cursor: 'pointer', fontSize: 11, color: 'var(--c-primary)', fontFamily: 'inherit' }}>
Editar
)}
setConfirmRevogar({ id: u.id, isConvite: u.isConvite })} style={{ background: 'none', border: '1px solid var(--c-border)', borderRadius: 6, padding: '4px 10px', cursor: 'pointer', fontSize: 11, color: '#dc2626', fontFamily: 'inherit' }}>
Excluir
);
}
// --- 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 */}
setViewUsuario(u)} title="Visualizar" style={{
background: 'none', border: '1px solid var(--c-border)', borderRadius: 6,
width: 32, height: 32, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center'
}}>
{!u.isConvite && (
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
})} title="Editar" style={{
background: 'none', border: '1px solid var(--c-border)', borderRadius: 6,
width: 32, height: 32, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center'
}}>
)}
setConfirmRevogar({ id: u.id, isConvite: u.isConvite })} title="Excluir" style={{
background: 'none', border: '1px solid var(--c-border)', borderRadius: 6,
width: 32, height: 32, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center'
}}>
);
})}
{/* Rodapé da tabela */}
{selecionados.length > 0
? `${selecionados.length} de ${usuariosFiltrados.length} selecionado(s)`
: `${usuariosFiltrados.length} usuário(s) encontrado(s)`}
{selecionados.length > 0 && (
setSelecionados([])} style={{
background: 'none', border: 'none', cursor: 'pointer', fontSize: 12,
color: 'var(--c-primary)', fontFamily: 'inherit', textDecoration: 'underline'
}}>
Limpar seleção
)}
)}
{/* ====== 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()}
{/* 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) => (
))}
);
})()}
{/* 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 */}
onChange(!checked)}
style={{
width: 44, height: 24, borderRadius: 99,
background: checked ? 'var(--c-primary)' : 'var(--c-border)',
border: 'none', cursor: 'pointer', position: 'relative',
transition: 'background 0.2s', flexShrink: 0
}}
>
);
}
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 });