import React, { useEffect, useMemo, useRef, useState } from 'react'; import { ArrowDown, ArrowUp, Download, FolderKanban, Loader2, Pencil, Plus, RefreshCw, ShieldCheck, Trash2, Upload, UserCheck, UserX, Users, X } from 'lucide-react'; import { adminApi, instructorApi, LearningPath, LearningPathInput, ManagedUser, ManagedUserDetail, PlatformAnalytics } from '../services/api'; import { useAuth } from '../context/AuthContext'; import { useToast } from '../context/ToastContext'; import { useNavigate } from 'react-router-dom'; const roleLabel: Record = { admin: 'Superadmin', instructor: 'Instrutor', student: 'Aluno' }; type CourseOption = { id: string; title: string; status: string }; type HomeConfiguration = { featuredCourseId: string | null; courseOrder: string[]; defaultCoverImageUrl: string | null; courses: CourseOption[] }; const emptyPathInput = (): LearningPathInput => ({ title: '', description: '', coverImageUrl: null, coverImageZoom: 1, coverImagePositionX: 50, coverImagePositionY: 50, status: 'draft', courseIds: [] }); const pathToInput = (path: LearningPath): LearningPathInput => ({ title: path.title, description: path.description, coverImageUrl: path.coverImageUrl, coverImageZoom: path.coverImageZoom, coverImagePositionX: path.coverImagePositionX, coverImagePositionY: path.coverImagePositionY, status: path.status === 'published' ? 'published' : 'draft', courseIds: path.courses.map((course) => course.id) }); export const SuperAdmin: React.FC = () => { const { user } = useAuth(); const { showToast } = useToast(); const navigate = useNavigate(); const [activeTab, setActiveTab] = useState<'content' | 'people'>('content'); const [users, setUsers] = useState([]); const [analytics, setAnalytics] = useState(null); const [categories, setCategories] = useState>([]); const [homeConfiguration, setHomeConfiguration] = useState(null); const [paths, setPaths] = useState([]); const [isLoading, setIsLoading] = useState(true); const [updatingUserId, setUpdatingUserId] = useState(null); const [query, setQuery] = useState(''); const [newCategory, setNewCategory] = useState(''); const [inviteEmail, setInviteEmail] = useState(''); const [inviteRole, setInviteRole] = useState<'student' | 'instructor'>('student'); const [accessLink, setAccessLink] = useState(''); const [selectedUser, setSelectedUser] = useState(null); const [loadingDetailId, setLoadingDetailId] = useState(null); const [editingPath, setEditingPath] = useState(null); const [pathInput, setPathInput] = useState(emptyPathInput); const [isPathEditorOpen, setIsPathEditorOpen] = useState(false); const [isSavingPath, setIsSavingPath] = useState(false); const [isUploadingCover, setIsUploadingCover] = useState(false); const coverInputRef = useRef(null); const loadData = async () => { setIsLoading(true); try { const [usersResponse, dashboard, categoryResponse, homeResponse, pathResponse] = await Promise.all([adminApi.listUsers(), adminApi.dashboard(), adminApi.categories(), adminApi.homeConfiguration(), adminApi.listPaths()]); setUsers(usersResponse.data); setAnalytics(dashboard.data); setCategories(categoryResponse.data); setHomeConfiguration(homeResponse.data); setPaths(pathResponse.data); } catch { showToast('Não foi possível carregar o painel administrativo.', 'error'); } finally { setIsLoading(false); } }; useEffect(() => { void loadData(); }, []); const filteredUsers = useMemo(() => { const normalized = query.trim().toLowerCase(); return normalized ? users.filter((account) => account.name.toLowerCase().includes(normalized) || account.email.toLowerCase().includes(normalized)) : users; }, [query, users]); const homeCourses = useMemo(() => { if (!homeConfiguration) return []; const order = new Map(homeConfiguration.courseOrder.map((id, index): [string, number] => [id, index])); return homeConfiguration.courses.filter((course) => course.status === 'published').sort((a, b) => (order.get(a.id) ?? 999_999) - (order.get(b.id) ?? 999_999) || a.title.localeCompare(b.title)); }, [homeConfiguration]); const activeUsers = users.filter((account) => account.isActive).length; const instructorUsers = users.filter((account) => account.role === 'instructor' && account.isActive).length; const updateUser = async (account: ManagedUser, update: Partial>) => { setUpdatingUserId(account.id); try { const response = await adminApi.updateUser(account.id, update); setUsers((current) => current.map((item) => item.id === account.id ? response.data : item)); showToast('Usuário atualizado.', 'success'); } catch { showToast('Não foi possível atualizar este usuário.', 'error'); } finally { setUpdatingUserId(null); } }; const createInvite = async (event: React.FormEvent) => { event.preventDefault(); try { const response = await adminApi.invite(inviteEmail, inviteRole); setAccessLink(response.data.inviteUrl); setInviteEmail(''); showToast('Link de convite criado.', 'success'); } catch { showToast('Não foi possível criar o convite.', 'error'); } }; const resetPassword = async (account: ManagedUser) => { try { const response = await adminApi.passwordReset(account.id); setAccessLink(response.data.resetUrl); showToast('Link de redefinição criado.', 'success'); } catch { showToast('Não foi possível criar o link.', 'error'); } }; const createCategory = async (event: React.FormEvent) => { event.preventDefault(); if (!newCategory.trim()) return; try { const response = await adminApi.createCategory(newCategory.trim()); setCategories((current) => [...current, response.data].sort((a, b) => a.position - b.position || a.name.localeCompare(b.name))); setNewCategory(''); showToast('Categoria criada.', 'success'); } catch { showToast('Não foi possível criar esta categoria. Ela pode já existir.', 'error'); } }; const toggleCategory = async (category: { id: string; name: string; position: number; isActive: boolean }) => { try { const response = await adminApi.updateCategory(category.id, { name: category.name, isActive: !category.isActive }); setCategories((current) => current.map((item) => item.id === category.id ? response.data : item)); } catch { showToast('Não foi possível atualizar a categoria.', 'error'); } }; const saveHomeConfiguration = async () => { if (!homeConfiguration) return; try { await adminApi.updateHomeConfiguration({ featuredCourseId: homeConfiguration.featuredCourseId, courseOrder: homeConfiguration.courseOrder, defaultCoverImageUrl: homeConfiguration.defaultCoverImageUrl || null }); showToast('Página inicial atualizada.', 'success'); } catch { showToast('Não foi possível salvar a configuração inicial.', 'error'); } }; const moveHomeCourse = (courseId: string, direction: -1 | 1) => setHomeConfiguration((current) => { if (!current) return current; const published = current.courses.filter((course) => course.status === 'published'); const order = new Map(current.courseOrder.map((id, index): [string, number] => [id, index])); const arranged = [...published].sort((a, b) => (order.get(a.id) ?? 999_999) - (order.get(b.id) ?? 999_999) || a.title.localeCompare(b.title)); const index = arranged.findIndex((course) => course.id === courseId); const target = index + direction; if (index < 0 || target < 0 || target >= arranged.length) return current; [arranged[index], arranged[target]] = [arranged[target], arranged[index]]; return { ...current, courseOrder: arranged.map((course) => course.id) }; }); const showUserDetail = async (account: ManagedUser) => { setLoadingDetailId(account.id); try { const response = await adminApi.userDetail(account.id); setSelectedUser(response.data); } catch { showToast('Não foi possível carregar os detalhes do usuário.', 'error'); } finally { 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 openNewPath = () => { setEditingPath(null); setPathInput(emptyPathInput()); setIsPathEditorOpen(true); }; const openEditPath = (path: LearningPath) => { setEditingPath(path); setPathInput(pathToInput(path)); setIsPathEditorOpen(true); }; const closePathEditor = () => { if (!isSavingPath) setIsPathEditorOpen(false); }; const togglePathCourse = (courseId: string) => setPathInput((current) => ({ ...current, courseIds: current.courseIds.includes(courseId) ? current.courseIds.filter((id) => id !== courseId) : [...current.courseIds, courseId] })); const movePathCourse = (courseId: string, direction: -1 | 1) => setPathInput((current) => { const index = current.courseIds.indexOf(courseId); const target = index + direction; if (index < 0 || target < 0 || target >= current.courseIds.length) return current; const courseIds = [...current.courseIds]; [courseIds[index], courseIds[target]] = [courseIds[target], courseIds[index]]; return { ...current, courseIds }; }); const uploadPathCover = async (file: File) => { if (!file.type.startsWith('image/')) { showToast('Selecione uma imagem JPG, PNG ou WebP.', 'error'); return; } setIsUploadingCover(true); try { const response = await instructorApi.uploadCover(file); setPathInput((current) => ({ ...current, coverImageUrl: response.data.coverImageUrl })); showToast('Capa da formação enviada.', 'success'); } catch { showToast('Não foi possível enviar a capa.', 'error'); } finally { setIsUploadingCover(false); } }; const savePath = async (event: React.FormEvent) => { event.preventDefault(); if (!pathInput.courseIds.length) { showToast('Adicione pelo menos um curso à formação.', 'error'); return; } setIsSavingPath(true); try { const response = editingPath ? await adminApi.updatePath(editingPath.id, pathInput) : await adminApi.createPath(pathInput); setPaths((current) => editingPath ? current.map((path) => path.id === response.data.id ? response.data : path) : [response.data, ...current]); setIsPathEditorOpen(false); showToast(editingPath ? 'Formação atualizada.' : 'Formação criada.', 'success'); } catch (error) { showToast(error instanceof Error ? error.message : 'Não foi possível salvar a formação.', 'error'); } finally { setIsSavingPath(false); } }; const deletePath = async (path: LearningPath) => { if (!window.confirm(`Excluir a formação “${path.title}”? Os cursos não serão removidos.`)) return; try { await adminApi.deletePath(path.id); setPaths((current) => current.filter((item) => item.id !== path.id)); showToast('Formação excluída. Os cursos foram preservados.', 'success'); } catch { showToast('Não foi possível excluir a formação.', 'error'); } }; const selectedPathCourses = pathInput.courseIds.map((id) => homeConfiguration?.courses.find((course) => course.id === id)).filter((course): course is CourseOption => Boolean(course)); return
Superadmin

Painel Administrativo

Organize conteúdo, formações e acessos da plataforma.

} label="Usuários" value={analytics?.totalUsers ?? users.length} />} label="Contas ativas" value={analytics?.activeUsers ?? activeUsers} />} label="Instrutores" value={instructorUsers} />} label="Cursos publicados" value={analytics?.publishedCourses ?? 0} />} label="Aulas concluídas" value={analytics?.completedLessons ?? 0} />
setActiveTab('content')} label="Conteúdo" /> setActiveTab('people')} label="Pessoas e acessos" />
{activeTab === 'content' ? : } {isPathEditorOpen && }
; }; const ContentPanel: React.FC<{ paths: LearningPath[]; homeConfiguration: HomeConfiguration | null; categories: Array<{ id: string; name: string; position: number; isActive: boolean }>; homeCourses: CourseOption[]; newCategory: string; setNewCategory: (value: string) => void; setHomeConfiguration: React.Dispatch>; onCreateCategory: (event: React.FormEvent) => void; onToggleCategory: (category: { id: string; name: string; position: number; isActive: boolean }) => void; onMoveHomeCourse: (courseId: string, direction: -1 | 1) => void; onSaveHome: () => void; onNewPath: () => void; onEditPath: (path: LearningPath) => void; onDeletePath: (path: LearningPath) => void }> = ({ paths, homeConfiguration, categories, homeCourses, newCategory, setNewCategory, setHomeConfiguration, onCreateCategory, onToggleCategory, onMoveHomeCourse, onSaveHome, onNewPath, onEditPath, onDeletePath }) => <>

Formações

Uma sequência ordenada de cursos existentes.

{paths.length === 0 ?
Nenhuma formação criada ainda.
: paths.map((path) =>
{path.coverImageUrl ? : }

{path.title}

{path.status === 'published' ? 'Publicada' : 'Rascunho'}

{path.courses.length} {path.courses.length === 1 ? 'curso' : 'cursos'}

)}

Página inicial

Escolha o curso do banner e a ordem da vitrine de cursos.

{homeConfiguration &&

Ordem da vitrine

{homeCourses.map((course, index) =>
{index + 1}{course.title}
)}
}

Categorias de cursos

Aparecem no editor de cursos e na biblioteca.

setNewCategory(event.target.value)} placeholder="Nome da nova categoria" className="min-w-0 flex-1 rounded-xl border border-white/10 bg-zinc-900 px-3 py-2.5 text-sm text-white" />
{categories.map((category) => )}
; const PeoplePanel: React.FC<{ users: ManagedUser[]; isLoading: boolean; query: string; setQuery: (value: string) => void; accessLink: string; inviteEmail: string; setInviteEmail: (value: string) => void; inviteRole: 'student' | 'instructor'; setInviteRole: (value: 'student' | 'instructor') => void; selectedUser: ManagedUserDetail | null; setSelectedUser: (user: ManagedUserDetail | null) => void; updatingUserId: string | null; loadingDetailId: string | null; currentUserId?: string; onInvite: (event: React.FormEvent) => void; onUpdateUser: (account: ManagedUser, update: Partial>) => void; onShowDetail: (account: ManagedUser) => void; onResetPassword: (account: ManagedUser) => void; onExport: () => void }> = ({ users, isLoading, query, setQuery, accessLink, inviteEmail, setInviteEmail, inviteRole, setInviteRole, selectedUser, setSelectedUser, updatingUserId, loadingDetailId, currentUserId, onInvite, onUpdateUser, onShowDetail, onResetPassword, onExport }) => <>

Convidar usuário

setInviteEmail(event.target.value)} placeholder="E-mail do convidado" className="flex-1 rounded-xl bg-zinc-900 border border-white/10 px-4 py-2.5 text-sm text-white" />
{accessLink &&
}
{selectedUser &&

Detalhes do usuário

{selectedUser.name}

{selectedUser.email} · {roleLabel[selectedUser.role]}

}

Usuários

setQuery(event.target.value)} placeholder="Buscar por nome ou e-mail" className="w-full sm:w-80 rounded-xl bg-zinc-900 border border-white/10 px-4 py-2.5 text-sm text-white" />
{isLoading ?
:
{users.map((account) => { const isCurrentUser = account.id === currentUserId; const isUpdating = updatingUserId === account.id; return ; })}
UsuárioPerfilStatusCadastroAções
{account.name}
{account.email}
{new Intl.DateTimeFormat('pt-BR').format(new Date(account.createdAt))}
}
; const PathEditor: React.FC<{ input: LearningPathInput; setInput: React.Dispatch>; courses: CourseOption[]; selectedCourses: CourseOption[]; editing: boolean; isSaving: boolean; isUploadingCover: boolean; coverInputRef: React.RefObject; onClose: () => void; onSave: (event: React.FormEvent) => void; onUpload: (file: File) => void; onToggleCourse: (id: string) => void; onMoveCourse: (id: string, direction: -1 | 1) => void }> = ({ input, setInput, courses, selectedCourses, editing, isSaving, isUploadingCover, coverInputRef, onClose, onSave, onUpload, onToggleCourse, onMoveCourse }) =>

Formação

{editing ? 'Editar formação' : 'Nova formação'}

Organize cursos existentes na ordem em que os alunos devem assisti-los.

{ const file = event.target.files?.[0]; if (file) onUpload(file); event.currentTarget.value = ''; }} /> setInput((current) => ({ ...current, title: event.target.value }))} placeholder="Ex.: Formação em Tráfego Pago" className="input" />