Files
compor-academy/pages/SuperAdmin.tsx
Cauê Faleiros f5aa826b32
All checks were successful
CI / Validate frontend and API (push) Successful in 36s
CI / Build and publish Docker images (push) Successful in 20s
feat: add editable ordered trail pages
2026-09-08 15:53:20 -03:00

108 lines
54 KiB
TypeScript

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, HomeBanner, HomeBannerInput, LearningPath, LearningPathInput, ManagedUser, ManagedUserDetail, PlatformAnalytics } from '../services/api';
import { useAuth } from '../context/AuthContext';
import { useToast } from '../context/ToastContext';
import { useNavigate } from 'react-router-dom';
import { CourseCoverImage } from '../components/CourseCoverImage';
import { UserManagementPanel } from '../components/UserManagementPanel';
const roleLabel: Record<ManagedUser['role'], string> = { admin: 'Superadmin', instructor: 'Instrutor', student: 'Aluno' };
type CourseOption = { id: string; title: string; status: string; category: string; coverImageUrl: string | null };
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<'courses' | 'trails' | 'home' | 'access'>('courses');
const [users, setUsers] = useState<ManagedUser[]>([]); const [analytics, setAnalytics] = useState<PlatformAnalytics | null>(null);
const [categories, setCategories] = useState<Array<{ id: string; name: string; position: number; isActive: boolean }>>([]);
const [homeConfiguration, setHomeConfiguration] = useState<HomeConfiguration | null>(null); const [paths, setPaths] = useState<LearningPath[]>([]);
const [isLoading, setIsLoading] = useState(true); const [updatingUserId, setUpdatingUserId] = useState<string | null>(null); const [query, setQuery] = useState('');
const [newCategory, setNewCategory] = useState(''); const [inviteEmail, setInviteEmail] = useState(''); const [accessLink, setAccessLink] = useState('');
const [selectedUser, setSelectedUser] = useState<ManagedUserDetail | null>(null); const [loadingDetailId, setLoadingDetailId] = useState<string | null>(null);
const [editingPath, setEditingPath] = useState<LearningPath | null>(null); const [pathInput, setPathInput] = useState<LearningPathInput>(emptyPathInput); const [isPathEditorOpen, setIsPathEditorOpen] = useState(false); const [isSavingPath, setIsSavingPath] = useState(false); const [isUploadingCover, setIsUploadingCover] = useState(false); const coverInputRef = useRef<HTMLInputElement>(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<string, number>(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<Pick<ManagedUser, 'name' | 'email' | 'role' | 'isActive'>>) => { 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); setAccessLink(response.data.inviteUrl); setInviteEmail(''); showToast('Convite de instrutor 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 deleteUser = async (account: ManagedUser) => { if (!window.confirm(`Excluir “${account.name}”? O histórico de aprendizado e comentários serão removidos. Cursos de instrutor serão preservados e transferidos para você.`)) return; setUpdatingUserId(account.id); try { await adminApi.deleteUser(account.id); setUsers((current) => current.filter((item) => item.id !== account.id)); setSelectedUser(null); showToast('Usuário excluído.', 'success'); } catch { showToast('Não foi possível excluir este usuário.', 'error'); } finally { setUpdatingUserId(null); } };
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<string, number>(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 <main className="min-h-screen pt-28 pb-20 px-6 md:px-12 max-w-[1440px] mx-auto">
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-6 mb-8"><div><div className="flex items-center gap-2 text-orange-400 text-xs font-bold uppercase tracking-wider mb-3"><ShieldCheck className="w-4 h-4" /> Superadmin</div><h1 className="text-3xl md:text-4xl font-bold text-white">Painel Administrativo</h1><p className="text-gray-400 mt-2">Organize conteúdo, formações e acessos da plataforma.</p></div><div className="flex flex-wrap gap-3"><button onClick={() => navigate('/gerenciar')} className="inline-flex items-center justify-center gap-2 rounded-full px-5 py-3 text-sm font-semibold bg-orange-500 hover:bg-orange-600 text-white transition-colors">Gerenciar cursos</button><button onClick={() => void loadData()} 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"><RefreshCw className="w-4 h-4" /> Atualizar</button></div></div>
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-8"><StatCard icon={<Users className="w-5 h-5" />} label="Usuários" value={analytics?.totalUsers ?? users.length} /><StatCard icon={<UserCheck className="w-5 h-5" />} label="Contas ativas" value={analytics?.activeUsers ?? activeUsers} /><StatCard icon={<ShieldCheck className="w-5 h-5" />} label="Instrutores" value={instructorUsers} /><StatCard icon={<FolderKanban className="w-5 h-5" />} label="Cursos publicados" value={analytics?.publishedCourses ?? 0} /><StatCard icon={<UserCheck className="w-5 h-5" />} label="Aulas concluídas" value={analytics?.completedLessons ?? 0} /></div>
<div className="mb-8 flex gap-1 overflow-x-auto border-b border-white/10"><TabButton active={activeTab === 'courses'} onClick={() => setActiveTab('courses')} label="Cursos" /><TabButton active={activeTab === 'trails'} onClick={() => setActiveTab('trails')} label="Trilhas" /><TabButton active={activeTab === 'home'} onClick={() => setActiveTab('home')} label="Página inicial" /><TabButton active={activeTab === 'access'} onClick={() => setActiveTab('access')} label="Acessos" /></div>
{activeTab === 'courses' && <CoursesPanel courses={homeConfiguration?.courses || []} onManage={() => navigate('/gerenciar')} />}
{activeTab === 'trails' && <TrailManager trails={paths} onNew={() => navigate('/admin/trilhas/nova')} onOpen={(trail) => navigate(`/admin/trilhas/${trail.id}`)} />}
{activeTab === 'home' && <><BannerManager courses={homeConfiguration?.courses || []} /><HomepageConfigurationPanel homeConfiguration={homeConfiguration} homeCourses={homeCourses} setHomeConfiguration={setHomeConfiguration} onMoveHomeCourse={moveHomeCourse} onSaveHome={saveHomeConfiguration} /></>}
{activeTab === 'access' && <UserManagementPanel users={filteredUsers} isLoading={isLoading} query={query} setQuery={setQuery} accessLink={accessLink} inviteEmail={inviteEmail} setInviteEmail={setInviteEmail} updatingUserId={updatingUserId} currentUserId={user?.id} onInvite={createInvite} onUpdateUser={updateUser} onDeleteUser={deleteUser} onResetPassword={resetPassword} onExport={exportUsers} />}
{isPathEditorOpen && <PathEditor input={pathInput} setInput={setPathInput} courses={homeConfiguration?.courses || []} selectedCourses={selectedPathCourses} editing={Boolean(editingPath)} isSaving={isSavingPath} isUploadingCover={isUploadingCover} coverInputRef={coverInputRef} onClose={closePathEditor} onSave={savePath} onUpload={uploadPathCover} onToggleCourse={togglePathCourse} onMoveCourse={movePathCourse} />}
</main>;
};
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<React.SetStateAction<HomeConfiguration | null>>; 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 }) => <><div className="grid grid-cols-1 xl:grid-cols-[1.1fr_.9fr] gap-6 mb-6"><section className="rounded-2xl border border-white/10 bg-zinc-950/80 p-5"><div className="flex items-start justify-between gap-4"><div><h2 className="text-lg font-bold text-white">Formações</h2><p className="text-sm text-gray-500 mt-1">Uma sequência ordenada de cursos existentes.</p></div><button onClick={onNewPath} className="inline-flex shrink-0 items-center gap-2 rounded-xl bg-orange-500 px-4 py-2.5 text-sm font-semibold text-white hover:bg-orange-600"><Plus className="h-4 w-4" /> Nova formação</button></div><div className="mt-5 space-y-3">{paths.length === 0 ? <div className="rounded-xl border border-dashed border-white/15 px-4 py-8 text-center text-sm text-gray-500">Nenhuma formação criada ainda.</div> : paths.map((path) => <div key={path.id} className="flex items-center gap-3 rounded-xl bg-white/[0.03] p-3"><div className="h-12 w-20 shrink-0 overflow-hidden rounded-lg bg-zinc-900">{path.coverImageUrl ? <img src={path.coverImageUrl} alt="" className="h-full w-full object-cover" /> : <FolderKanban className="m-3 h-6 w-6 text-orange-400" />}</div><div className="min-w-0 flex-1"><div className="flex items-center gap-2"><p className="truncate font-semibold text-white">{path.title}</p><span className={`rounded-full px-2 py-0.5 text-[10px] font-bold uppercase ${path.status === 'published' ? 'bg-emerald-500/15 text-emerald-400' : 'bg-amber-500/15 text-amber-300'}`}>{path.status === 'published' ? 'Publicada' : 'Rascunho'}</span></div><p className="mt-1 text-xs text-gray-500">{path.courses.length} {path.courses.length === 1 ? 'curso' : 'cursos'}</p></div><button onClick={() => onEditPath(path)} className="rounded-lg p-2 text-gray-400 hover:bg-white/10 hover:text-white" title="Editar formação"><Pencil className="h-4 w-4" /></button><button onClick={() => void onDeletePath(path)} className="rounded-lg p-2 text-gray-500 hover:bg-red-500/15 hover:text-red-400" title="Excluir formação"><Trash2 className="h-4 w-4" /></button></div>)}</div></section><section className="rounded-2xl border border-white/10 bg-zinc-950/80 p-5"><h2 className="text-lg font-bold text-white">Página inicial</h2><p className="text-sm text-gray-500 mt-1 mb-4">Escolha o curso do banner e a ordem da vitrine de cursos.</p>{homeConfiguration && <div className="space-y-3"><label className="block text-xs font-semibold uppercase tracking-wider text-gray-500">Banner principal</label><select value={homeConfiguration.featuredCourseId || ''} onChange={(event) => setHomeConfiguration((current) => current ? { ...current, featuredCourseId: event.target.value || null } : current)} className="w-full rounded-xl border border-white/10 bg-zinc-900 px-3 py-2.5 text-sm text-white"><option value="">Sem curso em destaque</option>{homeConfiguration.courses.filter((course) => course.status === 'published').map((course) => <option key={course.id} value={course.id}>{course.title}</option>)}</select><div className="rounded-xl bg-black/20 p-2"><p className="mb-2 px-1 text-xs text-gray-500">Ordem da vitrine</p>{homeCourses.map((course, index) => <div key={course.id} className="flex items-center gap-2 px-1 py-1.5 text-sm text-gray-300"><span className="w-5 text-xs text-gray-600">{index + 1}</span><span className="flex-1 truncate">{course.title}</span><button onClick={() => onMoveHomeCourse(course.id, -1)} disabled={index === 0} className="p-1 text-gray-400 disabled:opacity-20"><ArrowUp className="h-3.5 w-3.5" /></button><button onClick={() => onMoveHomeCourse(course.id, 1)} disabled={index === homeCourses.length - 1} className="p-1 text-gray-400 disabled:opacity-20"><ArrowDown className="h-3.5 w-3.5" /></button></div>)}</div><button onClick={onSaveHome} className="rounded-xl bg-white/10 px-4 py-2.5 text-sm font-semibold text-white hover:bg-white/15">Salvar página inicial</button></div>}</section></div><section className="rounded-2xl border border-white/10 bg-zinc-950/80 p-5"><h2 className="text-lg font-bold text-white">Categorias de cursos</h2><p className="mb-4 text-sm text-gray-500">Aparecem no editor de cursos e na biblioteca.</p><form onSubmit={onCreateCategory} className="flex max-w-xl gap-2 mb-4"><input value={newCategory} onChange={(event) => 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" /><button className="rounded-xl bg-orange-500 px-4 text-sm font-semibold text-white hover:bg-orange-600">Adicionar</button></form><div className="flex flex-wrap gap-2">{categories.map((category) => <button key={category.id} onClick={() => onToggleCategory(category)} className={`rounded-full px-3 py-1.5 text-xs font-semibold ${category.isActive ? 'bg-emerald-500/15 text-emerald-400' : 'bg-white/10 text-gray-500 line-through'}`}>{category.name}</button>)}</div></section></>;
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<Pick<ManagedUser, 'role' | 'isActive'>>) => 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 }) => <><section className="rounded-2xl border border-white/10 bg-zinc-950/80 p-5 mb-6"><h2 className="text-lg font-bold text-white mb-4">Convidar usuário</h2><form onSubmit={onInvite} className="flex flex-col md:flex-row gap-3"><input required type="email" value={inviteEmail} onChange={(event) => 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" /><select value={inviteRole} onChange={(event) => setInviteRole(event.target.value as 'student' | 'instructor')} className="rounded-xl bg-zinc-900 border border-white/10 px-4 py-2.5 text-sm text-white"><option value="student">Aluno</option><option value="instructor">Instrutor</option></select><button className="rounded-xl bg-orange-500 hover:bg-orange-600 px-5 py-2.5 text-sm font-semibold text-white">Gerar convite</button></form>{accessLink && <div className="mt-4 flex gap-2"><input readOnly value={accessLink} className="flex-1 rounded-xl bg-black border border-white/10 px-3 py-2 text-xs text-gray-300" /><button onClick={() => navigator.clipboard.writeText(accessLink)} className="rounded-xl bg-white/10 px-4 text-xs font-semibold">Copiar</button></div>}</section>{selectedUser && <section className="rounded-2xl border border-orange-500/30 bg-zinc-950/80 p-5 mb-6"><div className="flex items-start justify-between gap-4"><div><p className="text-xs uppercase tracking-wider text-orange-400 font-semibold">Detalhes do usuário</p><h2 className="text-lg font-bold text-white mt-1">{selectedUser.name}</h2><p className="text-sm text-gray-400">{selectedUser.email} · {roleLabel[selectedUser.role]}</p></div><button onClick={() => setSelectedUser(null)} className="text-xs text-gray-400 hover:text-white">Fechar</button></div><div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-5 text-sm"><DetailMetric label="Aulas concluídas" value={selectedUser.completedLessons.toString()} /><DetailMetric label="Última atividade" value={selectedUser.lastLearningAt ? new Intl.DateTimeFormat('pt-BR', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(selectedUser.lastLearningAt)) : 'Sem atividade'} /><DetailMetric label="Status" value={selectedUser.isActive ? 'Ativo' : 'Desativado'} /></div></section>}<section className="rounded-2xl border border-white/10 bg-zinc-950/80 overflow-hidden"><div className="p-5 border-b border-white/10 flex flex-col sm:flex-row gap-4 sm:items-center sm:justify-between"><h2 className="text-lg font-bold text-white">Usuários</h2><div className="flex gap-2"><input value={query} onChange={(event) => 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" /><button onClick={onExport} className="rounded-xl bg-white/10 px-3 text-sm text-white hover:bg-white/15" title="Exportar CSV"><Download className="h-4 w-4" /></button></div></div>{isLoading ? <div className="h-64 flex items-center justify-center"><Loader2 className="w-8 h-8 animate-spin text-orange-400" /></div> : <div className="overflow-x-auto"><table className="w-full min-w-[760px] text-left text-sm"><thead className="bg-white/[0.03] text-xs uppercase tracking-wider text-gray-500"><tr><th className="px-5 py-4">Usuário</th><th className="px-5 py-4">Perfil</th><th className="px-5 py-4">Status</th><th className="px-5 py-4">Cadastro</th><th className="px-5 py-4">Ações</th></tr></thead><tbody className="divide-y divide-white/5">{users.map((account) => { const isCurrentUser = account.id === currentUserId; const isUpdating = updatingUserId === account.id; return <tr key={account.id} className="text-gray-300"><td className="px-5 py-4"><div className="font-semibold text-white">{account.name}</div><div className="text-xs text-gray-500 mt-1">{account.email}</div></td><td className="px-5 py-4"><select value={account.role} disabled={isCurrentUser || isUpdating} onChange={(event) => onUpdateUser(account, { role: event.target.value as ManagedUser['role'] })} className="rounded-lg bg-zinc-900 border border-white/10 px-3 py-2 text-xs text-white disabled:opacity-50">{Object.entries(roleLabel).map(([role, label]) => <option key={role} value={role}>{label}</option>)}</select></td><td className="px-5 py-4"><button disabled={isCurrentUser || isUpdating} onClick={() => onUpdateUser(account, { isActive: !account.isActive })} className={`inline-flex items-center gap-2 rounded-full px-3 py-2 text-xs font-semibold disabled:opacity-50 ${account.isActive ? 'bg-emerald-500/15 text-emerald-400' : 'bg-red-500/15 text-red-400'}`}>{account.isActive ? <UserCheck className="w-3.5 h-3.5" /> : <UserX className="w-3.5 h-3.5" />}{account.isActive ? 'Ativo' : 'Desativado'}</button></td><td className="px-5 py-4 text-xs text-gray-500">{new Intl.DateTimeFormat('pt-BR').format(new Date(account.createdAt))}</td><td className="px-5 py-4"><div className="flex items-center gap-3"><button disabled={loadingDetailId === account.id} onClick={() => onShowDetail(account)} className="text-xs font-semibold text-gray-300 hover:text-white">{loadingDetailId === account.id ? 'Carregando...' : 'Detalhes'}</button><button disabled={isUpdating} onClick={() => onResetPassword(account)} className="text-xs font-semibold text-orange-400 hover:text-orange-300">Redefinir senha</button></div></td></tr>; })}</tbody></table></div>}</section></>;
const PathEditor: React.FC<{ input: LearningPathInput; setInput: React.Dispatch<React.SetStateAction<LearningPathInput>>; courses: CourseOption[]; selectedCourses: CourseOption[]; editing: boolean; isSaving: boolean; isUploadingCover: boolean; coverInputRef: React.RefObject<HTMLInputElement | null>; 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 }) => <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 p-4 backdrop-blur-sm"><form onSubmit={onSave} className="max-h-[92vh] w-full max-w-5xl overflow-y-auto rounded-2xl border border-white/10 bg-zinc-950 shadow-2xl"><div className="sticky top-0 z-10 flex items-start justify-between border-b border-white/10 bg-zinc-950 px-6 py-5"><div><p className="text-xs font-bold uppercase tracking-wider text-orange-400">Formação</p><h2 className="mt-1 text-xl font-bold text-white">{editing ? 'Editar formação' : 'Nova formação'}</h2><p className="mt-1 text-sm text-gray-500">Organize cursos existentes na ordem em que os alunos devem assisti-los.</p></div><button type="button" onClick={onClose} className="rounded-full p-2 text-gray-400 hover:bg-white/10 hover:text-white"><X className="h-5 w-5" /></button></div><div className="grid gap-6 p-6 lg:grid-cols-[.85fr_1.15fr]"><div className="space-y-4"><input ref={coverInputRef} type="file" accept="image/jpeg,image/png,image/webp" className="hidden" onChange={(event) => { const file = event.target.files?.[0]; if (file) onUpload(file); event.currentTarget.value = ''; }} /><button type="button" onClick={() => coverInputRef.current?.click()} className="relative flex aspect-video w-full items-center justify-center overflow-hidden rounded-xl border border-dashed border-white/20 bg-zinc-900 text-sm text-gray-400">{input.coverImageUrl ? <img src={input.coverImageUrl} alt="Prévia da capa" className="h-full w-full object-cover" /> : <span className="flex flex-col items-center gap-2"><Upload className="h-5 w-5 text-orange-400" />Enviar capa da formação</span>}{isUploadingCover && <span className="absolute inset-0 flex items-center justify-center bg-black/70"><Loader2 className="h-6 w-6 animate-spin text-orange-400" /></span>}</button><Field label="Título"><input required value={input.title} onChange={(event) => setInput((current) => ({ ...current, title: event.target.value }))} placeholder="Ex.: Formação em Tráfego Pago" className="input" /></Field><Field label="Descrição"><textarea value={input.description} onChange={(event) => setInput((current) => ({ ...current, description: event.target.value }))} placeholder="O que o aluno vai construir nesta formação?" rows={4} className="input resize-none" /></Field><label className="flex items-center justify-between rounded-xl bg-white/[0.03] p-3 text-sm"><span className="text-gray-300">Publicação</span><select value={input.status} onChange={(event) => setInput((current) => ({ ...current, status: event.target.value as 'draft' | 'published' }))} className="rounded-lg border border-white/10 bg-zinc-900 px-2 py-1.5 text-xs text-white"><option value="draft">Salvar como rascunho</option><option value="published">Publicar formação</option></select></label></div><div><div className="mb-3"><h3 className="font-semibold text-white">Cursos da formação</h3><p className="mt-1 text-sm text-gray-500">Selecione e ordene os cursos. Uma formação publicada só pode conter cursos publicados.</p></div><div className="space-y-2 rounded-xl border border-white/10 bg-black/20 p-3">{selectedCourses.length === 0 ? <p className="px-2 py-4 text-center text-sm text-gray-500">Selecione os cursos abaixo para montar a formação.</p> : selectedCourses.map((course, index) => <div key={course.id} className="flex items-center gap-3 rounded-lg bg-white/[0.04] px-3 py-2.5"><span className="w-5 text-xs font-bold text-orange-400">{index + 1}</span><span className="flex-1 truncate text-sm font-medium text-white">{course.title}</span><button type="button" onClick={() => onMoveCourse(course.id, -1)} disabled={index === 0} className="p-1 text-gray-400 disabled:opacity-20"><ArrowUp className="h-4 w-4" /></button><button type="button" onClick={() => onMoveCourse(course.id, 1)} disabled={index === selectedCourses.length - 1} className="p-1 text-gray-400 disabled:opacity-20"><ArrowDown className="h-4 w-4" /></button><button type="button" onClick={() => onToggleCourse(course.id)} className="p-1 text-gray-500 hover:text-red-400"><X className="h-4 w-4" /></button></div>)}</div><div className="mt-4 max-h-56 space-y-1 overflow-y-auto rounded-xl border border-white/10 p-2">{courses.filter((course) => course.status !== 'archived').sort((a, b) => a.title.localeCompare(b.title)).map((course) => { const selected = input.courseIds.includes(course.id); return <button type="button" key={course.id} onClick={() => onToggleCourse(course.id)} className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm ${selected ? 'bg-orange-500/15 text-orange-100' : 'hover:bg-white/[0.04] text-gray-300'}`}><span className={`flex h-4 w-4 items-center justify-center rounded border ${selected ? 'border-orange-500 bg-orange-500 text-white' : 'border-white/25'}`}>{selected ? '✓' : ''}</span><span className="flex-1 truncate">{course.title}</span><span className={`text-[10px] font-bold uppercase ${course.status === 'published' ? 'text-emerald-400' : 'text-amber-300'}`}>{course.status === 'published' ? 'Publicado' : 'Rascunho'}</span></button>; })}</div></div></div><div className="flex items-center justify-end gap-3 border-t border-white/10 px-6 py-4"><button type="button" onClick={onClose} className="rounded-xl px-4 py-2.5 text-sm font-semibold text-gray-300 hover:bg-white/10">Cancelar</button><button disabled={isSaving || isUploadingCover} className="rounded-xl bg-orange-500 px-5 py-2.5 text-sm font-semibold text-white hover:bg-orange-600 disabled:opacity-50">{isSaving ? 'Salvando...' : editing ? 'Salvar formação' : 'Criar formação'}</button></div></form></div>;
const Field: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => <div><label className="mb-1.5 block text-xs font-semibold uppercase tracking-wider text-gray-500">{label}</label>{children}</div>;
const TabButton: React.FC<{ active: boolean; onClick: () => void; label: string }> = ({ active, onClick, label }) => <button onClick={onClick} className={`border-b-2 px-4 py-3 text-sm font-semibold transition-colors ${active ? 'border-orange-500 text-white' : 'border-transparent text-gray-500 hover:text-gray-300'}`}>{label}</button>;
const StatCard: React.FC<{ icon: React.ReactNode; label: string; value: number }> = ({ icon, label, value }) => <div className="rounded-2xl border border-white/10 bg-zinc-950/80 p-5"><div className="flex items-center gap-2 text-orange-400 mb-3">{icon}<span className="text-xs font-semibold uppercase tracking-wider">{label}</span></div><div className="text-3xl font-bold text-white">{value}</div></div>;
const DetailMetric: React.FC<{ label: string; value: string }> = ({ label, value }) => <div className="rounded-xl bg-white/[0.03] px-4 py-3"><div className="text-xs text-gray-500">{label}</div><div className="text-sm font-semibold text-white mt-1">{value}</div></div>;
const TrailManager: React.FC<{ trails: LearningPath[]; onNew: () => void; onOpen: (trail: LearningPath) => void }> = ({ trails, onNew, onOpen }) => <section className="overflow-hidden rounded-2xl border border-white/10 bg-zinc-950/80"><div className="flex items-center justify-between gap-4 border-b border-white/10 p-5"><div><h2 className="text-lg font-bold text-white">Trilhas</h2><p className="mt-1 text-sm text-gray-500">Sequências ordenadas de cursos. Abra uma trilha para editar sua capa, descrição e conteúdo.</p></div><button onClick={onNew} className="inline-flex shrink-0 items-center gap-2 rounded-xl bg-orange-500 px-4 py-2.5 text-sm font-semibold text-white hover:bg-orange-600"><Plus className="h-4 w-4" /> Nova trilha</button></div><div className="divide-y divide-white/5">{trails.length === 0 ? <p className="p-10 text-center text-sm text-gray-500">Nenhuma trilha criada ainda.</p> : trails.map((trail) => <button key={trail.id} onClick={() => onOpen(trail)} className="flex w-full items-center gap-4 px-5 py-4 text-left transition hover:bg-white/[0.03]"><div className="h-14 w-24 shrink-0 overflow-hidden rounded-xl bg-zinc-900">{trail.coverImageUrl ? <CourseCoverImage source={trail.coverImageUrl} alt="" className="h-full w-full object-cover" /> : <div className="flex h-full items-center justify-center"><FolderKanban className="h-6 w-6 text-orange-400" /></div>}</div><div className="min-w-0 flex-1"><div className="flex items-center gap-2"><p className="truncate font-semibold text-white">{trail.title}</p><span className={`rounded-full px-2 py-0.5 text-[10px] font-bold uppercase ${trail.status === 'published' ? 'bg-emerald-500/15 text-emerald-400' : 'bg-amber-500/15 text-amber-300'}`}>{trail.status === 'published' ? 'Publicada' : 'Rascunho'}</span></div><p className="mt-1 truncate text-xs text-gray-500">{trail.courses.length} {trail.courses.length === 1 ? 'curso' : 'cursos'} · {trail.description || 'Sem descrição'}</p></div><span className="inline-flex items-center gap-1.5 text-xs font-semibold text-orange-300">Gerenciar <Pencil className="h-3.5 w-3.5" /></span></button>)}</div></section>;
const CategoryManager: React.FC<{ categories: Array<{ id: string; name: string; position: number; isActive: boolean }>; newCategory: string; setNewCategory: (value: string) => void; onCreate: (event: React.FormEvent) => void; onToggle: (category: { id: string; name: string; position: number; isActive: boolean }) => void }> = ({ categories, newCategory, setNewCategory, onCreate, onToggle }) => <section className="mt-6 rounded-2xl border border-white/10 bg-zinc-950/80 p-5"><h2 className="text-lg font-bold text-white">Categorias das trilhas</h2><p className="mt-1 text-sm text-gray-500">Organize os temas disponíveis para cursos e trilhas.</p><form onSubmit={onCreate} className="mt-4 flex max-w-xl gap-2"><input value={newCategory} onChange={(event) => setNewCategory(event.target.value)} placeholder="Nome da 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" /><button className="rounded-xl bg-white/10 px-4 text-sm font-semibold text-white hover:bg-white/15">Adicionar</button></form><div className="mt-4 flex flex-wrap gap-2">{categories.map((category) => <button key={category.id} onClick={() => onToggle(category)} className={`rounded-full px-3 py-1.5 text-xs font-semibold ${category.isActive ? 'bg-emerald-500/15 text-emerald-400' : 'bg-white/10 text-gray-500 line-through'}`}>{category.name}</button>)}</div></section>;
const HomepageConfigurationPanel: React.FC<{ homeConfiguration: HomeConfiguration | null; homeCourses: CourseOption[]; setHomeConfiguration: React.Dispatch<React.SetStateAction<HomeConfiguration | null>>; onMoveHomeCourse: (courseId: string, direction: -1 | 1) => void; onSaveHome: () => void }> = ({ homeConfiguration, homeCourses, setHomeConfiguration, onMoveHomeCourse, onSaveHome }) => <section className="rounded-2xl border border-white/10 bg-zinc-950/80 p-5"><h2 className="text-lg font-bold text-white">Vitrine de cursos</h2><p className="mb-5 mt-1 text-sm text-gray-500">Defina a ordem dos cursos abaixo dos banners.</p>{homeConfiguration && <div className="space-y-4"><select value={homeConfiguration.featuredCourseId || ''} onChange={(event) => setHomeConfiguration((current) => current ? { ...current, featuredCourseId: event.target.value || null } : current)} className="w-full rounded-xl border border-white/10 bg-zinc-900 px-3 py-2.5 text-sm text-white"><option value="">Sem curso em destaque adicional</option>{homeConfiguration.courses.filter((course) => course.status === 'published').map((course) => <option key={course.id} value={course.id}>{course.title}</option>)}</select><div className="rounded-xl bg-black/20 p-3">{homeCourses.map((course, index) => <div key={course.id} className="flex items-center gap-2 py-2 text-sm text-gray-300"><span className="w-5 text-xs text-gray-600">{index + 1}</span><span className="flex-1 truncate">{course.title}</span><button onClick={() => onMoveHomeCourse(course.id, -1)} disabled={index === 0} className="p-1 disabled:opacity-20"><ArrowUp className="h-4 w-4" /></button><button onClick={() => onMoveHomeCourse(course.id, 1)} disabled={index === homeCourses.length - 1} className="p-1 disabled:opacity-20"><ArrowDown className="h-4 w-4" /></button></div>)}</div><button onClick={onSaveHome} className="rounded-xl bg-orange-500 px-4 py-2.5 text-sm font-semibold text-white">Salvar vitrine</button></div>}</section>;
const BannerManager: React.FC<{ courses: CourseOption[] }> = ({ courses }) => {
const { showToast } = useToast(); const [banners, setBanners] = useState<HomeBanner[]>([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const fileInput = useRef<HTMLInputElement>(null);
useEffect(() => { adminApi.homeBanners().then((response) => setBanners(response.data)).catch(() => showToast('Não foi possível carregar os banners.', 'error')).finally(() => setLoading(false)); }, []);
const move = (index: number, direction: -1 | 1) => setBanners((current) => { const target = index + direction; if (target < 0 || target >= current.length) return current; const next = [...current]; [next[index], next[target]] = [next[target], next[index]]; return next; });
const remove = (id: string) => setBanners((current) => current.filter((banner) => banner.id !== id));
const addCourse = (courseId: string) => { if (!courseId || banners.some((banner) => banner.kind === 'course' && banner.courseId === courseId)) return; const course = courses.find((item) => item.id === courseId); if (!course) return; setBanners((current) => [...current, { id: crypto.randomUUID(), kind: 'course', courseId, imageUrl: null, title: '', description: '', position: current.length + 1, courseTitle: course.title, courseDescription: null, courseImageUrl: null }]); };
const upload = async (file: File) => { if (!file.type.startsWith('image/')) { showToast('Selecione uma imagem válida.', 'error'); return; } try { const response = await instructorApi.uploadCover(file); setBanners((current) => [...current, { id: crypto.randomUUID(), kind: 'custom', courseId: null, imageUrl: response.data.coverImageUrl, title: '', description: '', position: current.length + 1, courseTitle: null, courseDescription: null, courseImageUrl: null }]); } catch { showToast('Não foi possível enviar o banner.', 'error'); } };
const save = async () => { setSaving(true); try { const input: HomeBannerInput[] = banners.map(({ kind, courseId, imageUrl, title, description }) => ({ kind, courseId, imageUrl, title, description })); const response = await adminApi.updateHomeBanners(input); setBanners(response.data); showToast('Banners da página inicial salvos.', 'success'); } catch { showToast('Não foi possível salvar os banners.', 'error'); } finally { setSaving(false); } };
return <section className="mb-6 rounded-2xl border border-white/10 bg-zinc-950/80 p-5"><div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"><div><h2 className="text-lg font-bold text-white">Banners da página inicial</h2><p className="mt-1 text-sm text-gray-500">Envie imagens próprias ou use a capa de cursos publicados. Mais de um banner vira um carrossel.</p></div><button onClick={save} disabled={saving || loading} className="rounded-xl bg-orange-500 px-4 py-2.5 text-sm font-semibold text-white disabled:opacity-50">{saving ? 'Salvando...' : 'Salvar banners'}</button></div><div className="mt-5 grid gap-3 md:grid-cols-2">{banners.map((banner, index) => <div key={banner.id} className="flex gap-3 rounded-xl border border-white/10 bg-black/20 p-3"><div className="h-16 w-28 shrink-0 overflow-hidden rounded-lg bg-zinc-900">{(banner.imageUrl || banner.courseImageUrl) && <img src={banner.imageUrl || banner.courseImageUrl || ''} alt="" className="h-full w-full object-cover" />}</div><div className="min-w-0 flex-1"><p className="truncate text-sm font-semibold text-white">{banner.kind === 'course' ? banner.courseTitle : banner.title || 'Banner personalizado'}</p><p className="mt-1 text-xs text-gray-500">{banner.kind === 'course' ? 'Capa do curso' : 'Imagem enviada'}</p><div className="mt-2 flex gap-1"><button onClick={() => move(index, -1)} disabled={index === 0} className="rounded bg-white/10 p-1 disabled:opacity-20"><ArrowUp className="h-3.5 w-3.5" /></button><button onClick={() => move(index, 1)} disabled={index === banners.length - 1} className="rounded bg-white/10 p-1 disabled:opacity-20"><ArrowDown className="h-3.5 w-3.5" /></button><button onClick={() => remove(banner.id)} className="rounded bg-red-500/15 p-1 text-red-400"><Trash2 className="h-3.5 w-3.5" /></button></div></div></div>)}</div><div className="mt-4 flex flex-col gap-2 sm:flex-row"><input ref={fileInput} type="file" accept="image/jpeg,image/png,image/webp" className="hidden" onChange={(event) => { const file = event.target.files?.[0]; if (file) void upload(file); event.currentTarget.value = ''; }} /><button onClick={() => fileInput.current?.click()} className="rounded-xl border border-dashed border-orange-500/50 px-4 py-2.5 text-sm font-semibold text-orange-300">Enviar imagem de banner</button><select defaultValue="" onChange={(event) => { addCourse(event.target.value); event.currentTarget.value = ''; }} className="min-w-0 flex-1 rounded-xl border border-white/10 bg-zinc-900 px-3 py-2.5 text-sm text-white"><option value="">Adicionar capa de um curso publicado</option>{courses.filter((course) => course.status === 'published').map((course) => <option key={course.id} value={course.id}>{course.title}</option>)}</select></div></section>;
};
const CoursesPanel: React.FC<{ courses: CourseOption[]; onManage: () => void }> = ({ courses, onManage }) => {
const [query, setQuery] = useState('');
const [selectedTrail, setSelectedTrail] = useState('all');
const trails: string[] = [...new Set<string>(courses.map((course) => course.category))].sort((left, right) => left.localeCompare(right, 'pt-BR'));
const visibleCourses = courses.filter((course) => {
const matchesTrail = selectedTrail === 'all' || course.category === selectedTrail;
const normalized = query.trim().toLocaleLowerCase('pt-BR');
return matchesTrail && (!normalized || `${course.title} ${course.category}`.toLocaleLowerCase('pt-BR').includes(normalized));
});
const byTrail: Record<string, CourseOption[]> = {};
visibleCourses.forEach((course) => {
(byTrail[course.category] ||= []).push(course);
});
return <section className="rounded-2xl border border-white/10 bg-zinc-950/80 overflow-hidden"><div className="flex items-center justify-between gap-4 border-b border-white/10 p-5"><div><h2 className="text-lg font-bold text-white">Cursos</h2><p className="mt-1 text-sm text-gray-500">Visualize cada curso pela sua capa e Trilha.</p></div><button onClick={onManage} className="rounded-xl bg-orange-500 px-4 py-2.5 text-sm font-semibold text-white">Gerenciar cursos</button></div>{courses.length === 0 ? <p className="p-8 text-sm text-gray-500">Nenhum curso cadastrado.</p> : <><div className="flex flex-col gap-2 border-b border-white/10 p-4 sm:flex-row"><input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Buscar curso" className="min-w-0 flex-1 rounded-xl border border-white/10 bg-zinc-900 px-3 py-2.5 text-sm text-white" /><select value={selectedTrail} onChange={(event) => setSelectedTrail(event.target.value)} className="rounded-xl border border-white/10 bg-zinc-900 px-3 py-2.5 text-sm text-white"><option value="all">Todas as Trilhas</option>{trails.map((trail) => <option key={trail} value={trail}>{trail}</option>)}</select></div>{visibleCourses.length === 0 ? <p className="p-10 text-center text-sm text-gray-500">Nenhum curso encontrado.</p> : <div className="divide-y divide-white/10">{Object.entries(byTrail).sort(([a], [b]) => a.localeCompare(b, 'pt-BR')).map(([trail, trailCourses]) => <section key={trail} className="p-5"><div className="mb-3 flex items-center gap-2"><h3 className="font-semibold text-orange-300">{trail}</h3><span className="text-xs text-gray-600">{trailCourses.length} {trailCourses.length === 1 ? 'curso' : 'cursos'}</span></div><div className="grid gap-3 xl:grid-cols-2">{trailCourses.map((course) => <div key={course.id} className="flex items-center gap-3 rounded-xl border border-white/10 bg-black/20 p-3"><div className="h-14 w-24 shrink-0 overflow-hidden rounded-lg bg-zinc-900">{course.coverImageUrl ? <CourseCoverImage source={course.coverImageUrl} alt="" className="h-full w-full object-cover" /> : <div className="h-full w-full bg-gradient-to-br from-zinc-800 to-zinc-950" />}</div><div className="min-w-0 flex-1"><p className="truncate font-semibold text-white">{course.title}</p><span className={`mt-1 inline-block rounded-full px-2 py-0.5 text-[10px] font-bold uppercase ${course.status === 'published' ? 'bg-emerald-500/15 text-emerald-400' : 'bg-amber-500/15 text-amber-300'}`}>{course.status === 'published' ? 'Publicado' : 'Rascunho'}</span></div><button onClick={onManage} className="rounded-lg border border-white/10 px-3 py-1.5 text-xs font-semibold text-gray-300 hover:bg-white/10">Editar</button></div>)}</div></section>)}</div>}</>}</section>;
};
const PathManager: React.FC<{ title: string; description: string; paths: LearningPath[]; onNew: () => void; onEdit: (path: LearningPath) => void; onDelete: (path: LearningPath) => void }> = ({ title, description, paths, onNew, onEdit, onDelete }) => <section className="rounded-2xl border border-white/10 bg-zinc-950/80 overflow-hidden"><div className="flex items-center justify-between gap-4 border-b border-white/10 p-5"><div><h2 className="text-lg font-bold text-white">{title}</h2><p className="mt-1 text-sm text-gray-500">{description}</p></div><button onClick={onNew} className="inline-flex items-center gap-2 rounded-xl bg-orange-500 px-4 py-2.5 text-sm font-semibold text-white"><Plus className="h-4 w-4" /> Nova</button></div><div className="divide-y divide-white/5">{paths.length === 0 ? <div className="p-10 text-center text-sm text-gray-500">Nenhuma {title.toLowerCase()} criada ainda.</div> : paths.map((path) => <div key={path.id} className="flex items-center gap-4 p-4"><div className="h-12 w-20 shrink-0 overflow-hidden rounded-lg bg-zinc-900">{path.coverImageUrl ? <img src={path.coverImageUrl} alt="" className="h-full w-full object-cover" /> : <FolderKanban className="m-3 h-6 w-6 text-orange-400" />}</div><div className="min-w-0 flex-1"><p className="truncate font-semibold text-white">{path.title}</p><p className="mt-1 text-xs text-gray-500">{path.courses.length} {path.courses.length === 1 ? 'curso' : 'cursos'} · {path.status === 'published' ? 'Publicada' : 'Rascunho'}</p></div><button onClick={() => onEdit(path)} className="rounded-lg border border-white/10 p-2 text-gray-300 hover:bg-white/10" title="Editar"><Pencil className="h-4 w-4" /></button><button onClick={() => void onDelete(path)} className="rounded-lg border border-red-500/20 p-2 text-red-400 hover:bg-red-500/10" title="Excluir"><Trash2 className="h-4 w-4" /></button></div>)}</div></section>;
const HomePanel: React.FC<{ homeConfiguration: HomeConfiguration | null; homeCourses: CourseOption[]; categories: Array<{ id: string; name: string; position: number; isActive: boolean }>; newCategory: string; setNewCategory: (value: string) => void; setHomeConfiguration: React.Dispatch<React.SetStateAction<HomeConfiguration | null>>; 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 }> = ({ homeConfiguration, homeCourses, categories, newCategory, setNewCategory, setHomeConfiguration, onCreateCategory, onToggleCategory, onMoveHomeCourse, onSaveHome }) => <div className="grid gap-6 xl:grid-cols-[1fr_.8fr]"><section className="rounded-2xl border border-white/10 bg-zinc-950/80 p-5"><h2 className="text-lg font-bold text-white">Página inicial</h2><p className="mb-5 mt-1 text-sm text-gray-500">Defina o conteúdo principal e a ordem dos cursos.</p>{homeConfiguration && <div className="space-y-4"><select value={homeConfiguration.featuredCourseId || ''} onChange={(event) => setHomeConfiguration((current) => current ? { ...current, featuredCourseId: event.target.value || null } : current)} className="w-full rounded-xl border border-white/10 bg-zinc-900 px-3 py-2.5 text-sm text-white"><option value="">Sem conteúdo em destaque</option>{homeConfiguration.courses.filter((course) => course.status === 'published').map((course) => <option key={course.id} value={course.id}>{course.title}</option>)}</select><div className="rounded-xl bg-black/20 p-3">{homeCourses.map((course, index) => <div key={course.id} className="flex items-center gap-2 py-2 text-sm text-gray-300"><span className="w-5 text-xs text-gray-600">{index + 1}</span><span className="flex-1 truncate">{course.title}</span><button onClick={() => onMoveHomeCourse(course.id, -1)} disabled={index === 0} className="p-1 disabled:opacity-20"><ArrowUp className="h-4 w-4" /></button><button onClick={() => onMoveHomeCourse(course.id, 1)} disabled={index === homeCourses.length - 1} className="p-1 disabled:opacity-20"><ArrowDown className="h-4 w-4" /></button></div>)}</div><button onClick={onSaveHome} className="rounded-xl bg-orange-500 px-4 py-2.5 text-sm font-semibold text-white">Salvar página inicial</button></div>}</section><section className="rounded-2xl border border-white/10 bg-zinc-950/80 p-5"><h2 className="text-lg font-bold text-white">Categorias</h2><form onSubmit={onCreateCategory} className="mt-4 flex gap-2"><input value={newCategory} onChange={(event) => setNewCategory(event.target.value)} placeholder="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" /><button className="rounded-xl bg-white/10 px-4 text-sm font-semibold text-white">Adicionar</button></form><div className="mt-4 flex flex-wrap gap-2">{categories.map((category) => <button key={category.id} onClick={() => onToggleCategory(category)} className={`rounded-full px-3 py-1.5 text-xs font-semibold ${category.isActive ? 'bg-emerald-500/15 text-emerald-400' : 'bg-white/10 text-gray-500 line-through'}`}>{category.name}</button>)}</div></section></div>;