// 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 && (
{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 }) => (
);
const MiniMetric = ({ 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 }) => (
);
const TimelineItem = ({ icon, color, label, detail, first, last }) => (
);
Object.assign(window, { AdminScreen });