|
|
|
@@ -8,12 +8,12 @@ import { useNavigate } from 'react-router-dom';
|
|
|
|
const roleLabel: Record<ManagedUser['role'], string> = { admin: 'Superadmin', instructor: 'Instrutor', student: 'Aluno' };
|
|
|
|
const roleLabel: Record<ManagedUser['role'], string> = { admin: 'Superadmin', instructor: 'Instrutor', student: 'Aluno' };
|
|
|
|
type CourseOption = { id: string; title: string; status: string };
|
|
|
|
type CourseOption = { id: string; title: string; status: string };
|
|
|
|
type HomeConfiguration = { featuredCourseId: string | null; courseOrder: string[]; defaultCoverImageUrl: string | null; courses: CourseOption[] };
|
|
|
|
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', kind: 'trail', courseIds: [] });
|
|
|
|
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', kind: path.kind, courseIds: path.courses.map((course) => course.id) });
|
|
|
|
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 = () => {
|
|
|
|
export const SuperAdmin: React.FC = () => {
|
|
|
|
const { user } = useAuth(); const { showToast } = useToast(); const navigate = useNavigate();
|
|
|
|
const { user } = useAuth(); const { showToast } = useToast(); const navigate = useNavigate();
|
|
|
|
const [activeTab, setActiveTab] = useState<'courses' | 'trails' | 'formations' | 'home' | 'access'>('courses');
|
|
|
|
const [activeTab, setActiveTab] = useState<'courses' | 'trails' | 'home' | 'access'>('courses');
|
|
|
|
const [users, setUsers] = useState<ManagedUser[]>([]); const [analytics, setAnalytics] = useState<PlatformAnalytics | null>(null);
|
|
|
|
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 [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 [homeConfiguration, setHomeConfiguration] = useState<HomeConfiguration | null>(null); const [paths, setPaths] = useState<LearningPath[]>([]);
|
|
|
|
@@ -36,7 +36,7 @@ export const SuperAdmin: React.FC = () => {
|
|
|
|
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 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 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 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 = (kind: 'trail' | 'formation') => { setEditingPath(null); setPathInput({ ...emptyPathInput(), kind }); setIsPathEditorOpen(true); }; const openEditPath = (path: LearningPath) => { setEditingPath(path); setPathInput(pathToInput(path)); setIsPathEditorOpen(true); }; const closePathEditor = () => { if (!isSavingPath) setIsPathEditorOpen(false); };
|
|
|
|
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 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 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 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); } };
|
|
|
|
@@ -47,10 +47,9 @@ export const SuperAdmin: React.FC = () => {
|
|
|
|
return <main className="min-h-screen pt-28 pb-20 px-6 md:px-12 max-w-[1440px] mx-auto">
|
|
|
|
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="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="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 === 'formations'} onClick={() => setActiveTab('formations')} label="Formações" /><TabButton active={activeTab === 'home'} onClick={() => setActiveTab('home')} label="Página inicial" /><TabButton active={activeTab === 'access'} onClick={() => setActiveTab('access')} label="Acessos" /></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 === 'courses' && <CoursesPanel courses={homeConfiguration?.courses || []} onManage={() => navigate('/gerenciar')} />}
|
|
|
|
{activeTab === 'trails' && <PathManager title="Trilhas" description="Sequências guiadas de cursos existentes." paths={paths.filter((path) => path.kind === 'trail')} onNew={() => openNewPath('trail')} onEdit={openEditPath} onDelete={deletePath} />}
|
|
|
|
{activeTab === 'trails' && <PathManager title="Trilhas" description="Sequências guiadas de cursos existentes." paths={paths} onNew={openNewPath} onEdit={openEditPath} onDelete={deletePath} />}
|
|
|
|
{activeTab === 'formations' && <PathManager title="Formações" description="Currículos organizados a partir de cursos existentes." paths={paths.filter((path) => path.kind === 'formation')} onNew={() => openNewPath('formation')} onEdit={openEditPath} onDelete={deletePath} />}
|
|
|
|
|
|
|
|
{activeTab === 'home' && <><BannerManager courses={homeConfiguration?.courses || []} /><HomePanel homeConfiguration={homeConfiguration} homeCourses={homeCourses} categories={categories} newCategory={newCategory} setNewCategory={setNewCategory} setHomeConfiguration={setHomeConfiguration} onCreateCategory={createCategory} onToggleCategory={toggleCategory} onMoveHomeCourse={moveHomeCourse} onSaveHome={saveHomeConfiguration} /></>}
|
|
|
|
{activeTab === 'home' && <><BannerManager courses={homeConfiguration?.courses || []} /><HomePanel homeConfiguration={homeConfiguration} homeCourses={homeCourses} categories={categories} newCategory={newCategory} setNewCategory={setNewCategory} setHomeConfiguration={setHomeConfiguration} onCreateCategory={createCategory} onToggleCategory={toggleCategory} onMoveHomeCourse={moveHomeCourse} onSaveHome={saveHomeConfiguration} /></>}
|
|
|
|
{activeTab === 'access' && <PeoplePanel users={filteredUsers} isLoading={isLoading} query={query} setQuery={setQuery} accessLink={accessLink} inviteEmail={inviteEmail} setInviteEmail={setInviteEmail} inviteRole={inviteRole} setInviteRole={setInviteRole} selectedUser={selectedUser} setSelectedUser={setSelectedUser} updatingUserId={updatingUserId} loadingDetailId={loadingDetailId} currentUserId={user?.id} onInvite={createInvite} onUpdateUser={updateUser} onShowDetail={showUserDetail} onResetPassword={resetPassword} onExport={exportUsers} />}
|
|
|
|
{activeTab === 'access' && <PeoplePanel users={filteredUsers} isLoading={isLoading} query={query} setQuery={setQuery} accessLink={accessLink} inviteEmail={inviteEmail} setInviteEmail={setInviteEmail} inviteRole={inviteRole} setInviteRole={setInviteRole} selectedUser={selectedUser} setSelectedUser={setSelectedUser} updatingUserId={updatingUserId} loadingDetailId={loadingDetailId} currentUserId={user?.id} onInvite={createInvite} onUpdateUser={updateUser} onShowDetail={showUserDetail} 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} />}
|
|
|
|
{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} />}
|
|
|
|
|