mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
FIX frontend admin
This commit is contained in:
parent
985764a064
commit
f006dfd195
12 changed files with 407 additions and 26 deletions
|
|
@ -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>
|
||||
|
|
|
|||
17
client/src/Types/ImageType.tsx
Normal file
17
client/src/Types/ImageType.tsx
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
11
client/src/Types/UserType.tsx
Normal file
11
client/src/Types/UserType.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export interface UserType {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
created_at: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export interface UsersResponse {
|
||||
users: UserType[];
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
114
client/src/pages/Admin/Images.tsx
Normal file
114
client/src/pages/Admin/Images.tsx
Normal 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;
|
||||
57
client/src/pages/Admin/Stats.tsx
Normal file
57
client/src/pages/Admin/Stats.tsx
Normal 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;
|
||||
57
client/src/pages/Admin/Users.tsx
Normal file
57
client/src/pages/Admin/Users.tsx
Normal 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;
|
||||
|
|
@ -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,8 +1170,120 @@ 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.`);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const apiService = new ApiService();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,27 +38,13 @@ 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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue