feat: improve account management and welcome emails

This commit is contained in:
Cauê Faleiros
2026-09-08 11:47:33 -03:00
parent 7b2d25aabb
commit 03a6491989
8 changed files with 148 additions and 11 deletions

View File

@@ -5,6 +5,7 @@ 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 };
@@ -19,7 +20,7 @@ export const SuperAdmin: React.FC = () => {
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 [inviteRole, setInviteRole] = useState<'student' | 'instructor'>('student'); const [accessLink, setAccessLink] = 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);
@@ -28,9 +29,10 @@ export const SuperAdmin: React.FC = () => {
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, '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, inviteRole); setAccessLink(response.data.inviteUrl); setInviteEmail(''); showToast('Link de convite criado.', 'success'); } catch { showToast('Não foi possível criar o convite.', 'error'); } };
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'); } };
@@ -52,7 +54,7 @@ export const SuperAdmin: React.FC = () => {
{activeTab === 'courses' && <CoursesPanel courses={homeConfiguration?.courses || []} onManage={() => navigate('/gerenciar')} />}
{activeTab === 'trails' && <TrailManager trails={categories} newTrail={newCategory} setNewTrail={setNewCategory} onCreate={createCategory} onToggle={toggleCategory} />}
{activeTab === 'home' && <><BannerManager courses={homeConfiguration?.courses || []} /><HomepageConfigurationPanel homeConfiguration={homeConfiguration} homeCourses={homeCourses} setHomeConfiguration={setHomeConfiguration} 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' && <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>;
};