);
}
// ----- 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 (
);
}
// ----- 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 (
{/* 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
);
}
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;
}
const CARGO_EXPLAINS = {
admin: 'Acesso total. Vê e gerencia todas as empresas, usuários, portadores e configurações.',
administrador: 'Acesso total. Vê e gerencia todas as empresas, usuários, portadores e configurações.',
analista: 'Acessa apenas as empresas às quais está vinculado. Pode lançar e dar baixa nessas empresas. Não vê dados de outras empresas.',
visualizador: 'Acessa apenas as empresas às quais está vinculado, somente para consulta. Não altera nada.'
};
function CargoHelpTooltip({ cargo }) {
const [show, setShow] = useState_S(false);
const key = (cargo || '').toLowerCase();
const text = CARGO_EXPLAINS[key] || CARGO_EXPLAINS['analista'];
return (
{show && (
{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.
{/* 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.