feat: improve learning and platform operations
All checks were successful
CI / Validate frontend and API (push) Successful in 45s
CI / Build and publish Docker images (push) Successful in 24s

This commit is contained in:
Cauê Faleiros
2026-09-01 15:38:14 -03:00
parent 4736fb5208
commit 3d4c72e85c
20 changed files with 292 additions and 45 deletions

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Loader2, RefreshCw, ShieldCheck, UserCheck, UserX, Users } from 'lucide-react';
import { adminApi, ManagedUser, ManagedUserDetail, PlatformAnalytics } from '../services/api';
import { Download, Loader2, RefreshCw, ScrollText, ShieldCheck, UserCheck, UserX, Users } from 'lucide-react';
import { adminApi, AuditEntry, ManagedUser, ManagedUserDetail, PlatformAnalytics } from '../services/api';
import { useAuth } from '../context/AuthContext';
import { useToast } from '../context/ToastContext';
import { useNavigate } from 'react-router-dom';
@@ -25,14 +25,15 @@ export const SuperAdmin: React.FC = () => {
const [accessLink, setAccessLink] = useState('');
const [selectedUser, setSelectedUser] = useState<ManagedUserDetail | null>(null);
const [loadingDetailId, setLoadingDetailId] = useState<string | null>(null);
const [auditEntries, setAuditEntries] = useState<AuditEntry[]>([]);
const loadUsers = async () => {
setIsLoading(true);
try {
const response = await adminApi.listUsers();
const [response, dashboard, auditLog] = await Promise.all([adminApi.listUsers(), adminApi.dashboard(), adminApi.auditLog()]);
setUsers(response.data);
const dashboard = await adminApi.dashboard();
setAnalytics(dashboard.data);
setAuditEntries(auditLog.data);
} catch {
showToast('Não foi possível carregar os usuários.', 'error');
} finally {
@@ -89,6 +90,12 @@ export const SuperAdmin: React.FC = () => {
setLoadingDetailId(null);
}
};
const exportUsers = () => {
const csv = ['Nome,E-mail,Perfil,Status,Cadastro', ...filteredUsers.map((account) => [account.name, account.email, roleLabel[account.role], account.isActive ? 'Ativo' : 'Desativado', new Intl.DateTimeFormat('pt-BR').format(new Date(account.createdAt))].map((value) => `"${value.replaceAll('"', '""')}"`).join(','))].join('\n');
const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
const link = document.createElement('a'); link.href = url; link.download = 'usuarios-compor-hub.csv'; link.click(); URL.revokeObjectURL(url);
};
const auditLabel: Record<string, string> = { 'invitation.created': 'Convite criado', 'password_reset.created': 'Redefinição de senha criada', 'user.updated': 'Usuário atualizado', 'course.published': 'Curso publicado', 'course.drafted': 'Rascunho salvo', 'course.archived': 'Curso arquivado', 'comment.replied': 'Comentário respondido', 'comment.deleted': 'Comentário removido' };
return (
<main className="min-h-screen pt-28 pb-20 px-6 md:px-12 max-w-[1440px] mx-auto">
@@ -113,6 +120,7 @@ export const SuperAdmin: React.FC = () => {
>
<RefreshCw className="w-4 h-4" /> Atualizar
</button>
<button onClick={exportUsers} className="inline-flex items-center justify-center gap-2 rounded-full px-5 py-3 text-sm font-semibold bg-white/10 hover:bg-white/15 text-white transition-colors"><Download className="w-4 h-4" /> Exportar CSV</button>
</div>
</div>
@@ -226,6 +234,11 @@ export const SuperAdmin: React.FC = () => {
</div>
)}
</section>
<section className="rounded-2xl border border-white/10 bg-zinc-950/80 overflow-hidden mt-8">
<div className="p-5 border-b border-white/10 flex items-center gap-2"><ScrollText className="w-5 h-5 text-orange-400" /><h2 className="text-lg font-bold text-white">Registro de atividades</h2></div>
{auditEntries.length === 0 ? <p className="px-5 py-10 text-sm text-gray-500">Nenhuma atividade administrativa registrada ainda.</p> : <div className="divide-y divide-white/5">{auditEntries.map((entry) => <div key={entry.id} className="p-4 flex flex-col sm:flex-row sm:items-center gap-1 sm:justify-between text-sm"><div><span className="font-semibold text-white">{auditLabel[entry.action] || entry.action}</span><span className="text-gray-500"> · {entry.actorName}</span></div><time className="text-xs text-gray-500">{new Intl.DateTimeFormat('pt-BR', { dateStyle: 'short', timeStyle: 'short' }).format(new Date(entry.createdAt))}</time></div>)}</div>}
</section>
</main>
);
};