FIX frontend admin

This commit is contained in:
Eddi3_As 2025-03-16 00:52:22 -04:00
parent 985764a064
commit f006dfd195
12 changed files with 407 additions and 26 deletions

View file

@ -26,10 +26,15 @@ import Footer from './components/Footer/Footer';
import ApiService from './services/ApiService';
import OAuthCallback from './pages/AuthManager/callback/AuthCallback';
import Users from './pages/Admin/Users';
import Images from './pages/Admin/Images';
import Stats from './pages/Admin/Stats';
const App: React.FC = () => {
const [isAuthenticated, setIsAuthenticated] = useState(ApiService.isLoggedIn());
const [isTeacherAuthenticated, setIsTeacherAuthenticated] = useState(ApiService.isLoggedInTeacher());
const [isAdmin, setIsAdmin] = useState(false);
//const [isAdmin, setIsAdmin] = useState(false);
const [isRoomRequireAuthentication, setRoomsRequireAuth] = useState(null);
const location = useLocation();
@ -100,6 +105,11 @@ const App: React.FC = () => {
{/* Pages authentification sélection */}
<Route path="/auth/callback" element={<OAuthCallback />} />
<Route path="/admin/stats" element={<Stats />} />
<Route path="/admin/images" element={<Images />} />
<Route path="/admin/users" element={<Users />} />
</Routes>
</main>
</div>

View file

@ -0,0 +1,17 @@
export interface ImageType {
id: string;
file_content: string;
file_name: string;
mime_type: string;
}
export interface ImagesResponse {
images: ImageType[];
total: number;
}
export interface ImagesParams {
page: number;
limit: number;
uid?: string;
}

View file

@ -9,3 +9,11 @@ export interface QuizType {
created_at: Date;
updated_at: Date;
}
export interface QuizTypeShort {
_id: string;
userId: string;
title: string;
created_at: Date;
updated_at: Date;
}

View file

@ -0,0 +1,11 @@
export interface UserType {
id: string;
name: string;
email: string;
created_at: string;
roles: string[];
}
export interface UsersResponse {
users: UserType[];
}

View file

@ -10,6 +10,7 @@ import ListItemText from '@mui/material/ListItemText';
import BarChartIcon from '@mui/icons-material/BarChart';
import ImageIcon from '@mui/icons-material/Image';
import PeopleIcon from '@mui/icons-material/People';
import { useNavigate } from 'react-router-dom';
const styles = {
drawerBg: 'rgba(82, 113, 255, 0.85)',
@ -21,23 +22,29 @@ const styles = {
export default function AdminDrawer() {
const [open, setOpen] = React.useState(false);
const navigate = useNavigate();
const toggleDrawer = (isOpen: boolean) => () => {
setOpen(isOpen);
};
const handleNavigation = (path: string) => {
navigate(path);
setOpen(false);
};
const menuItems = [
{ text: 'Stats', icon: <BarChartIcon /> },
{ text: 'Images', icon: <ImageIcon /> },
{ text: 'Users', icon: <PeopleIcon /> },
{ text: 'Stats', icon: <BarChartIcon />, path: '/admin/stats' },
{ text: 'Images', icon: <ImageIcon />, path: '/admin/images' },
{ text: 'Users', icon: <PeopleIcon />, path: '/admin/users' },
];
const list = (
<Box sx={{ width: 250, backgroundColor: styles.drawerBg, height: styles.height, color: styles.drawerTxtColor }} role="presentation" onClick={toggleDrawer(false)}>
<List>
{menuItems.map(({ text, icon }) => (
{menuItems.map(({ text, icon, path }) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemButton onClick={() => handleNavigation(path)}>
<ListItemIcon sx={{ color: styles.drawerTxtColor }}>{icon}</ListItemIcon>
<ListItemText primary={text} />
</ListItemButton>

View file

@ -0,0 +1,114 @@
import React, { useState, useEffect } from "react";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
TableHead,
IconButton,
Paper,
Box,
CircularProgress,
Button,
Typography
} from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
import { ImageType } from "../../Types/ImageType";
import ApiService from '../../services/ApiService';
const Images: React.FC = () => {
const [images, setImages] = useState<ImageType[]>([]);
const [totalImg, setTotalImg] = useState(0);
const [imgPage, setImgPage] = useState(1);
const [imgLimit] = useState(10);
const [loading, setLoading] = useState(false);
const fetchImages = async (page: number, limit: number) => {
setLoading(true);
const data = await ApiService.getImages(page, limit);
setImages(data.images);
setTotalImg(data.total);
setLoading(false);
};
useEffect(() => {
fetchImages(imgPage, imgLimit);
}, [imgPage]);
const handleDelete = async (id: string) => {
setLoading(true);
const isDeleted = await ApiService.deleteImage(id);
setLoading(false);
if (isDeleted) {
setImages(images.filter(image => image.id !== id));
}
};
const handleNextPage = () => {
if ((imgPage * imgLimit) < totalImg) {
setImgPage(prev => prev + 1);
}
};
const handlePrevPage = () => {
if (imgPage > 1) {
setImgPage(prev => prev - 1);
}
};
return (
<Box p={3}>
{loading ? (
<Box display="flex" justifyContent="center" alignItems="center" height={200}>
<CircularProgress />
</Box>
) : (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell></TableCell>
<TableCell>Nom</TableCell>
<TableCell>Image ID</TableCell>
</TableRow>
</TableHead>
<TableBody>
{images.map((obj: ImageType) => (
<TableRow key={obj.id}>
<TableCell>
<img
src={`data:${obj.mime_type};base64,${obj.file_content}`}
alt={`Image ${obj.file_name}`}
style={{ width: 350, height: "auto", borderRadius: 8 }}
/>
</TableCell>
<TableCell>{obj.file_name}</TableCell>
<TableCell style={{ minWidth: 150 }}>
{obj.id}
</TableCell>
<TableCell>
<IconButton onClick={() => handleDelete(obj.id)} color="error">
<DeleteIcon fontSize="small" />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
<Box display="flex" justifyContent="center" mt={2}>
<Button onClick={handlePrevPage} disabled={imgPage === 1} color="primary">
Précédent
</Button>
<Button onClick={handleNextPage} disabled={(imgPage * imgLimit) >= totalImg} color="primary">
Suivant
</Button>
</Box>
</Box>
);
};
export default Images;

View file

@ -0,0 +1,57 @@
import React, { useState, useEffect } from "react";
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
import ApiService from '../../services/ApiService';
import { QuizTypeShort } from "../../Types/QuizType";
const Users: React.FC = () => {
const [quizzes, setQuizzes] = useState<QuizTypeShort[]>([]);
useEffect(() => {
const fetchUsers = async () => {
try {
const data = await ApiService.getQuizzes();
setQuizzes(data);
} catch (error) {
console.error("Error fetching quizzes:", error);
}
};
fetchUsers();
}, []);
const handleDelete = (id: string) => {
setQuizzes(quizzes.filter(quiz => quiz._id !== id));
};
return (
<TableContainer component={Paper} className="p-4">
<Table>
<TableHead>
<TableRow>
<TableCell>Enseignant</TableCell>
<TableCell>Titre</TableCell>
<TableCell>Crée</TableCell>
<TableCell>Modifié</TableCell>
</TableRow>
</TableHead>
<TableBody>
{quizzes.map((quiz) => (
<TableRow key={quiz._id}>
<TableCell>{quiz.userId}</TableCell>
<TableCell>{quiz.title}</TableCell>
<TableCell>{new Date(quiz.created_at).toLocaleDateString()}</TableCell>
<TableCell>{new Date(quiz.updated_at).toLocaleDateString()}</TableCell>
<TableCell>
<IconButton onClick={() => handleDelete(quiz._id)} color="error">
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
export default Users;

View file

@ -0,0 +1,57 @@
import React, { useState, useEffect } from "react";
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
import ApiService from '../../services/ApiService';
import { UserType } from "../../Types/UserType";
const Users: React.FC = () => {
const [users, setUsers] = useState<UserType[]>([]);
useEffect(() => {
const fetchUsers = async () => {
try {
const data = await ApiService.getUsers();
setUsers(data);
} catch (error) {
console.error("Error fetching users:", error);
}
};
fetchUsers();
}, []);
const handleDelete = (email: string) => {
setUsers(users.filter(user => user.email !== email));
};
return (
<TableContainer component={Paper} className="p-4">
<Table>
<TableHead>
<TableRow>
<TableCell>Nom</TableCell>
<TableCell>Courriel</TableCell>
<TableCell>Crée</TableCell>
<TableCell>Roles</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users.map((user) => (
<TableRow key={user.email}>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{new Date(user.created_at).toLocaleDateString()}</TableCell>
<TableCell>{user.roles?.join(", ")}</TableCell>
<TableCell>
<IconButton onClick={() => handleDelete(user.email)} color="error">
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
export default Users;

View file

@ -3,8 +3,10 @@ import { jwtDecode } from 'jwt-decode';
import { ENV_VARIABLES } from '../constants';
import { FolderType } from 'src/Types/FolderType';
import { QuizType } from 'src/Types/QuizType';
import { QuizType, QuizTypeShort } from 'src/Types/QuizType';
import { RoomType } from 'src/Types/RoomType';
import { UserType } from 'src/Types/UserType';
import { ImagesResponse, ImagesParams } from 'src/Types/ImageType';
type ApiResponse = boolean | string;
@ -1009,6 +1011,7 @@ public async login(email: string, password: string): Promise<any> {
return `Une erreur inattendue s'est produite.`;
}
}
public async getRoomTitle(roomId: string): Promise<string | string> {
try {
if (!roomId) {
@ -1167,7 +1170,119 @@ public async login(email: string, password: string): Promise<any> {
return `ERROR : Une erreur inattendue s'est produite.`
}
}
// NOTE : Get Image pas necessaire
public async getUsers(): Promise<UserType[]> {
try {
const url: string = this.constructRequestUrl(`/admin/getUsers`);
const headers = this.constructRequestHeaders();
const result: AxiosResponse = await axios.get(url, { headers });
if (result.status !== 200) {
throw new Error(`L'obtention des titres des salles a échoué. Status: ${result.status}`);
}
console.log(result.data);
return result.data.users;
} catch (error) {
console.log("Error details: ", error);
if (axios.isAxiosError(error)) {
const err = error as AxiosError;
const data = err.response?.data as { error: string } | undefined;
const msg = data?.error || 'Erreur serveur inconnue lors de la requête.';
throw new Error(`L'enregistrement a échoué. Status: ${msg}`);
}
throw new Error(`ERROR : Une erreur inattendue s'est produite.`);
}
}
public async getImages(page: number, limit: number): Promise<ImagesResponse> {
try {
const url: string = this.constructRequestUrl(`/admin/getImages`);
const headers = this.constructRequestHeaders();
let params : ImagesParams = { page: page, limit: limit };
const result: AxiosResponse = await axios.get(url, { params: params, headers: headers });
if (result.status !== 200) {
throw new Error(`L'affichage des images a échoué. Status: ${result.status}`);
}
console.log(result.data);
const images = result.data.data;
return images;
} catch (error) {
console.log("Error details: ", error);
if (axios.isAxiosError(error)) {
const err = error as AxiosError;
const data = err.response?.data as { error: string } | undefined;
const msg = data?.error || 'Erreur serveur inconnue lors de la requête.';
throw new Error(`L'enregistrement a échoué. Status: ${msg}`);
}
throw new Error(`ERROR : Une erreur inattendue s'est produite.`);
}
}
public async deleteImage(imgId: string): Promise<ApiResponse> {
try {
const url: string = this.constructRequestUrl(`/admin/deleteImage`);
const headers = this.constructRequestHeaders();
let params = { imgId: imgId };
const result: AxiosResponse = await axios.delete(url, { params: params, headers: headers });
if (result.status !== 200) {
throw new Error(`La suppression de l'image a échoué. Status: ${result.status}`);
}
const deleted = result.data.deleted;
return deleted;
} catch (error) {
console.log("Error details: ", error);
if (axios.isAxiosError(error)) {
const err = error as AxiosError;
const data = err.response?.data as { error: string } | undefined;
const msg = data?.error || 'Erreur serveur inconnue lors de la requête.';
throw new Error(`L'enregistrement a échoué. Status: ${msg}`);
}
throw new Error(`ERROR : Une erreur inattendue s'est produite.`);
}
}
public async getQuizzes(): Promise<QuizTypeShort[]> {
try {
const url: string = this.constructRequestUrl(`/admin/getQuizzes`);
const headers = this.constructRequestHeaders();
const result: AxiosResponse = await axios.get(url, { headers });
if (result.status !== 200) {
throw new Error(`L'affichage des images a échoué. Status: ${result.status}`);
}
const quiz = result.data.quizzes;
return quiz;
} catch (error) {
console.log("Error details: ", error);
if (axios.isAxiosError(error)) {
const err = error as AxiosError;
const data = err.response?.data as { error: string } | undefined;
const msg = data?.error || 'Erreur serveur inconnue lors de la requête.';
throw new Error(`L'enregistrement a échoué. Status: ${msg}`);
}
throw new Error(`ERROR : Une erreur inattendue s'est produite.`);
}
}
}

View file

@ -36,7 +36,7 @@ class AdminController {
const imgs = await this.model.getImages(page, limit);
return res.status(200).json({ imgs });
return res.status(200).json({ data: imgs });
} catch (error) {
return next(error);
}

View file

@ -38,28 +38,14 @@ class Admin {
const conn = this.db.getConnection();
const quizColl = conn.collection('files');
const result = await quizColl.find({}).toArray();
const projection = { content: 0, folderName: 0, folderId: 0 };
const result = await quizColl.find({}, projection).toArray();
if (!result) return null;
return result;
}
async deleteQuiz(id) {
let deleted = false;
await this.db.connect()
const conn = this.db.getConnection();
const quizColl = conn.collection('files');
const result = await quizColl.deleteOne({ _id: ObjectId.createFromHexString(id) });
if (result) deleted = true;
return deleted;
}
async getImages(page, limit) {
await this.db.connect()
const conn = this.db.getConnection();

View file

@ -10,7 +10,6 @@ router.get("/getUsers", asyncHandler(admin.getUsers));
router.get("/getQuizzes", asyncHandler(admin.getQuizzes));
router.get("/getImages", asyncHandler(admin.getImages));
router.delete("/deleteUser", asyncHandler(admin.deleteUser));
router.delete("/deleteQuiz", asyncHandler(admin.deleteQuiz));
router.delete("/deleteImage", jwt.authenticate, asyncHandler(admin.deleteImage));
module.exports = router;