// Painel administrativo — visível apenas para usuários com role='admin'. const PLAN_COLORS = { trial: '#5EE3A8', mensal: '#6FB8FF', semestral: '#FFB547', anual: '#B197FC', essencial: '#6FB8FF', premium: '#FFB547', }; function formatDate(iso) { if (!iso) return '—'; try { const d = new Date(iso); return d.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' }); } catch (e) { return '—'; } } function daysAgo(iso) { if (!iso) return null; const ms = new Date() - new Date(iso); return Math.max(0, Math.floor(ms / (1000 * 60 * 60 * 24))); } const AdminScreen = ({ navigate, showToast }) => { const [profiles, setProfiles] = React.useState([]); const [metrics, setMetrics] = React.useState(null); const [revenue, setRevenue] = React.useState(null); const [loading, setLoading] = React.useState(true); const [filter, setFilter] = React.useState('todos'); const [sortBy, setSortBy] = React.useState('recent'); // 'recent' | 'oldest' | 'name' const [search, setSearch] = React.useState(''); const [selected, setSelected] = React.useState(null); const [aiUsage, setAiUsage] = React.useState(null); const refresh = async () => { if (!window.AdminAPI) return; setLoading(true); const [list, mtr, ai] = await Promise.all([ window.AdminAPI.listAllProfiles(), window.AdminAPI.getUsageMetrics(), window.SofiaAPI ? window.SofiaAPI.getCurrentMonthCost() : Promise.resolve(null), ]); setProfiles(list); setMetrics(mtr); setRevenue(window.AdminAPI.getRevenueMetrics(list)); setAiUsage(ai); setLoading(false); }; React.useEffect(() => { refresh(); }, []); const handleExport = () => { if (!window.AdminAPI) return; const ok = window.AdminAPI.exportProfilesCSV(filtered); showToast(ok ? `${filtered.length} assinante(s) exportados` : 'Erro ao exportar'); }; // MRR por plano (em R$/mês) const PLAN_MRR = { mensal: 29.90, semestral: 149.40 / 6, // 24.90 anual: 238.80 / 12, // 19.90 // legado essencial: 29.90, premium: 59.90, trial: 0, }; const PAYING_PLANS = ['mensal', 'semestral', 'anual', 'essencial', 'premium']; const filtered = profiles .filter(p => { if (filter === 'pagantes' && !PAYING_PLANS.includes(p.plan)) return false; if (filter === 'trial' && p.plan !== 'trial') return false; if (filter === 'inativos' && p.is_active) return false; if (filter === 'mensal' && p.plan !== 'mensal') return false; if (filter === 'semestral' && p.plan !== 'semestral') return false; if (filter === 'anual' && p.plan !== 'anual') return false; if (search && !(p.email || '').toLowerCase().includes(search.toLowerCase()) && !(p.name || '').toLowerCase().includes(search.toLowerCase())) return false; return true; }) .sort((a, b) => { if (sortBy === 'recent') return new Date(b.created_at || 0) - new Date(a.created_at || 0); if (sortBy === 'oldest') return new Date(a.created_at || 0) - new Date(b.created_at || 0); if (sortBy === 'name') return (a.name || a.email || '').localeCompare(b.name || b.email || ''); return 0; }); const recent = (window.AdminAPI && window.AdminAPI.getRecentSignups) ? window.AdminAPI.getRecentSignups(profiles, 5) : []; const totalUsers = profiles.length; const totalAdmins = profiles.filter(p => p.role === 'admin').length; const totalInactive = profiles.filter(p => !p.is_active).length; const totalTrial = profiles.filter(p => p.plan === 'trial').length; const totalPayers = profiles.filter(p => PAYING_PLANS.includes(p.plan)).length; const totalMensal = profiles.filter(p => p.plan === 'mensal').length; const totalSemestral = profiles.filter(p => p.plan === 'semestral').length; const totalAnual = profiles.filter(p => p.plan === 'anual').length; const mrr = profiles .filter(p => p.is_active) .reduce((sum, p) => sum + (PLAN_MRR[p.plan] || 0), 0); const updatePlan = async (userId, plan) => { const res = await window.AdminAPI.setUserPlan(userId, plan); if (res.ok) { showToast('Plano atualizado'); refresh(); setSelected(null); } else showToast('Erro ao atualizar plano'); }; const toggleActive = async (userId, isActive) => { const res = await window.AdminAPI.setUserActive(userId, !isActive); if (res.ok) { showToast(isActive ? 'Conta suspensa' : 'Conta reativada'); refresh(); setSelected(null); } else showToast('Erro ao alterar status'); }; const toggleRole = async (userId, role) => { const next = role === 'admin' ? 'user' : 'admin'; if (window.confirm && !window.confirm(next === 'admin' ? 'Promover este usuário a ADMIN?' : 'Remover privilégios de admin deste usuário?')) return; const res = await window.AdminAPI.setUserRole(userId, next); if (res.ok) { showToast(next === 'admin' ? 'Promovido a admin' : 'Privilégios removidos'); refresh(); setSelected(null); } else showToast('Erro ao alterar role'); }; return (
navigate('more')} right={
} /> {/* Cards de métricas principais */}
{revenue && ( <> )}
{/* Sofia IA — gasto do mês */}
Sofia IA · Mês
{aiUsage ? `R$ ${(aiUsage.cost * 5.5).toFixed(2)}` : '—'}
{aiUsage ? `${aiUsage.calls} chamadas · ${aiUsage.users} usuário${aiUsage.users === 1 ? '' : 's'}` : 'Sem chave OpenAI configurada'}
{/* Atividade recente */} {recent.length > 0 && (
Atividade recente
{recent.map(p => (
setSelected(p)} style={{ display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer', padding: '6px 0', borderBottom: `1px solid ${CDV.divider}`, }}>
{((p.name || p.email || '?').charAt(0).toUpperCase())}
{p.email}
{p.plan} · entrou em {formatDate(p.created_at)}
{p.plan}
))}
)} {/* Métricas de uso */} {metrics && (
Engajamento agregado
{totalInactive > 0 && (
⚠ {totalInactive} conta{totalInactive === 1 ? '' : 's'} suspensa{totalInactive === 1 ? '' : 's'}
)}
)} {/* Busca */}
setSearch(e.target.value)} placeholder="Buscar por email ou nome..." style={{ width: '100%', height: 44, borderRadius: 12, border: `1px solid ${CDV.stroke}`, background: CDV.surface, color: CDV.text, padding: '0 14px', fontSize: 13, outline: 'none', fontFamily: 'inherit', }} />
{/* Ordenação */}
Ordenar por
{[ { id: 'recent', label: 'Recentes' }, { id: 'oldest', label: 'Antigos' }, { id: 'name', label: 'Nome' }, ].map(s => ( ))}
{/* Filtros */}
{[ { id: 'todos', label: `Todos · ${totalUsers}` }, { id: 'pagantes', label: `Pagantes · ${totalPayers}` }, { id: 'mensal', label: `Mensal · ${totalMensal}` }, { id: 'semestral', label: `Semestral · ${totalSemestral}` }, { id: 'anual', label: `Anual · ${totalAnual}` }, { id: 'trial', label: `Trial · ${totalTrial}` }, { id: 'inativos', label: `Suspensos · ${totalInactive}` }, ].map(f => ( ))}
{/* Lista */} {loading ? (
Carregando...
) : filtered.length === 0 ? (
Nenhum usuário encontrado com esse filtro.
) : (
{filtered.map(p => setSelected(p)} />)}
)} setSelected(null)} title="Detalhes do usuário"> {selected && ( )}
); }; const MetricCard = ({ label, big, extra, icon, color }) => (
{label}
{big}
{extra}
); const MiniMetric = ({ value, label }) => (
{value}
{label}
); const ProfileRow = ({ p, onClick }) => { const planColor = PLAN_COLORS[p.plan] || CDV.textDim; const isAdmin = p.role === 'admin'; return ( ); }; const ProfileActions = ({ p, onSetPlan, onToggleActive, onToggleRole }) => { const days = daysAgo(p.created_at); const PLAN_MRR_LOCAL = { mensal: 29.90, semestral: 149.40 / 6, anual: 238.80 / 12, essencial: 29.90, premium: 59.90 }; const mrr = PLAN_MRR_LOCAL[p.plan] || 0; const revenueEstimated = mrr * Math.max(1, Math.floor((days || 0) / 30)); return (
{p.name || p.email}
{p.email}
ID: {p.user_id}
{p.role} {p.plan} {p.is_active ? 'ativo' : 'suspenso'}
{/* Stats do user */}
0 ? `R$ ${mrr.toFixed(2)}` : '—'} color={CDV.mint} /> 0 ? `R$ ${revenueEstimated.toFixed(0)}` : '—'} color={CDV.amber} />
{/* Timeline */}
Linha do tempo
{p.trial_ends_at && ( )} {p.plan !== 'trial' && ( )}
Plano
{[ { id: 'trial', label: 'Trial' }, { id: 'mensal', label: 'Mensal' }, { id: 'semestral', label: 'Semestral' }, { id: 'anual', label: 'Anual' }, ].map(plan => { const active = p.plan === plan.id; const color = PLAN_COLORS[plan.id] || CDV.textDim; return ( ); })}
); }; const UserStatBox = ({ label, value, color }) => (
{label}
{value}
); const TimelineItem = ({ icon, color, label, detail, first, last }) => (
{!last && (
)}
{label}
{detail}
); Object.assign(window, { AdminScreen });