diff --git a/backend/server.ts b/backend/server.ts index db858b0..1ac0f09 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -18,6 +18,7 @@ import emailRoutes from './src/routes/email.routes'; import newsRoutes from './src/routes/news.routes'; import discordRoutes from './src/routes/discord.routes'; import tentRoutes from './src/routes/tent.routes'; +import busRoutes from './src/routes/bus.routes'; import { server_port } from './src/utils/secret'; import { initUser } from './src/database/initdb/initUser' import { initRoles } from './src/database/initdb/initrole' @@ -59,6 +60,7 @@ async function startServer() { app.use('/api/news',authenticateUser, newsRoutes); app.use('/api/discord',authenticateUser, discordRoutes); app.use('/api/tent',authenticateUser, tentRoutes); + app.use('/api/bus',authenticateUser, busRoutes); app.use("/api/uploads/imgnews", express.static(path.join(__dirname, "/uploads/imgnews"))); app.use("/api/uploads/foodmenu", express.static(path.join(__dirname, "/uploads/foodmenu"))); app.use("/api/uploads/plannings", express.static(path.join(__dirname, "/uploads/plannings"))); diff --git a/backend/src/controllers/bus.controller.ts b/backend/src/controllers/bus.controller.ts new file mode 100644 index 0000000..0c530a0 --- /dev/null +++ b/backend/src/controllers/bus.controller.ts @@ -0,0 +1,59 @@ +// src/controllers/bus.controller.ts +import { Request, Response } from "express"; +import * as bus_service from "../services/bus.service"; +import { sendEmail } from "../services/email.service"; +import { Error, Ok } from "../utils/responses"; +import { generateEmailHtml } from "./email.controller"; + +interface MulterRequest extends Request { + file?: Express.Multer.File; +} + +export const sendBusAttributionEmails = async (req: Request, res: Response) => { + try { + const attributions = await bus_service.getAllBusAttributions(); + + if (!attributions.length) { + Error(res, { msg: "Aucune attribution de bus trouvée." }); + return; + } + + for (const attr of attributions) { + const htmlEmail = generateEmailHtml("templateAttributionBus", { + bus: attr.bus, time: attr.departure_time + }); + + const emailOptions = { + from: "integration@utt.fr", + to: [attr.email], + cc: [], + bcc: [], + subject: `Attribution Bus - ${attr.firstName ?? ""} ${attr.lastName ?? ""}`, + text: `Votre bus attribué est le numéro ${attr.bus}`, + html: htmlEmail || "", + }; + + await sendEmail(emailOptions); + } + + Ok(res, { msg: "Emails d'attribution bus envoyés avec succès !" }); + } catch (err) { + console.error(err); + Error(res, { msg: "Erreur lors de l'envoi des emails d'attribution bus." }); + } +}; + +export const uploadbusCSV = async (req: MulterRequest, res: Response) => { + try { + const file = req.file; + if (!file) { + Error(res, { msg: "Fichier CSV manquant." }); + } + + await bus_service.importBusFromCSV(file.path); + Ok(res,{ msg: "Importation réalisée avec succès." }); + } catch (error) { + console.error("Erreur import CSV :", error); + Error(res, { msg: "Échec de l'importation." }); + } +}; diff --git a/backend/src/controllers/news.controller.ts b/backend/src/controllers/news.controller.ts index 19ed9ea..92994c0 100644 --- a/backend/src/controllers/news.controller.ts +++ b/backend/src/controllers/news.controller.ts @@ -24,7 +24,7 @@ export const createNews = async (req: Request, res: Response) => { target, image_url); Ok(res, { msg: "Actu créée avec succès", data: news }); - return; + ; } catch (err) { console.error(err); Error(res, { msg: "Erreur lors de la création de l'actu" }); @@ -81,7 +81,7 @@ export const publishNews = async (req: Request, res: Response) => { if(recipients.length === 0){ Error(res, {msg : "No recipients"}); - return + } const email = { @@ -118,7 +118,7 @@ export const deleteNews = async (req: Request, res: Response) => { } await news_service.deleteNews(Number(newsId)); Ok(res, { msg: "Actus supprimée avec succès !" }); - return; + ; } catch (error) { Error(res, { msg: "Erreur lors de la suppression de l'actus" }); @@ -134,7 +134,7 @@ export const updateNews = async (req: Request, res: Response) => { try { const existing = await news_service.getNewsById(Number(id)); if (!existing) { - return Error(res, { msg: "Actu introuvable" }); + Error(res, { msg: "Actu introuvable" }); } // Supprimer l'ancienne image si une nouvelle est uploadée @@ -151,7 +151,7 @@ export const updateNews = async (req: Request, res: Response) => { const updated = await news_service.updateNews(Number(id), updates); Ok(res, { msg: "Actu mise à jour avec succès", data: updated }); - return; + ; } catch (err) { console.error(err); Error(res, { msg: "Erreur lors de la mise à jour de l'actu" }); diff --git a/backend/src/controllers/role.controller.ts b/backend/src/controllers/role.controller.ts index 2c7a687..e53382d 100644 --- a/backend/src/controllers/role.controller.ts +++ b/backend/src/controllers/role.controller.ts @@ -9,7 +9,7 @@ export const updateUserPreferences = async (req: Request, res: Response) => { const { roleIds } = req.body; if (!userId || !Array.isArray(roleIds)) { - return Error(res, { msg: "Données invalides" }); + Error(res, { msg: "Données invalides" }); } await role_service.updateUserPreferences(userId, roleIds); @@ -24,7 +24,7 @@ export const getUserPreferences = async (req: Request, res: Response) => { try { const userId = req.user?.userId; - if (!userId) return Error(res, { msg: "Utilisateur non authentifié" }); + if (!userId) Error(res, { msg: "Utilisateur non authentifié" }); const preferences = await role_service.getUserPreferences(userId); const roleIds = preferences.map((pref) => pref.roleId); @@ -39,7 +39,7 @@ export const getUserPreferences = async (req: Request, res: Response) => { export const getUsersByRoleHandler = async (req: Request, res: Response) => { try { const { roleName } = req.params; - if (!roleName) return Error(res, { msg: "Nom du rôle requis" }); + if (!roleName) Error(res, { msg: "Nom du rôle requis" }); const users = await role_service.getUsersByRoleName(roleName); Ok(res, { msg: "Utilisateurs récupérés", data: users }); @@ -55,7 +55,7 @@ export const addRoleToUser = async (req: Request, res: Response) => { const { userId, roleIds } = req.body; if (!userId || !Array.isArray(roleIds)) { - return Error(res, { msg: "userId et roleIds requis" }); + Error(res, { msg: "userId et roleIds requis" }); } for (const roleId of roleIds) { @@ -78,7 +78,7 @@ export const deleteRoleToUser = async (req: Request, res: Response) => { const { userId, roleId } = req.body; if (!userId || !roleId) { - return Error(res, { msg: "userId et roleId requis" }); + Error(res, { msg: "userId et roleId requis" }); } await role_service.removeRoleFromUser(userId, roleId); @@ -115,7 +115,7 @@ export const getRoles = async (req: Request, res: Response) => { export const getUserRoles = async (req: Request, res: Response) => { try { const { userId } = req.query; - if (!userId) return Error(res, { msg: "userId requis" }); + if (!userId) Error(res, { msg: "userId requis" }); const roles = await role_service.getUserRoles(Number(userId)); Ok(res, { data: roles }); @@ -135,7 +135,7 @@ export const addPointsToRole = async (req: Request, res: Response) => { const { roleId, points } = req.body; if (!roleId || typeof points !== "number") { - return Error(res, { msg: "roleId et points requis" }); + Error(res, { msg: "roleId et points requis" }); } await role_service.addPointsToRole(roleId, points); @@ -152,7 +152,7 @@ export const removePointsFromRole = async (req: Request, res: Response) => { const { roleId, points } = req.body; if (!roleId || typeof points !== "number") { - return Error(res, { msg: "roleId et points requis" }); + Error(res, { msg: "roleId et points requis" }); } await role_service.removePointsFromRole(roleId, points); @@ -178,10 +178,10 @@ export const getAllRolePoints = async (_req: Request, res: Response) => { export const getRolePoints = async (req: Request, res: Response) => { try { const { roleId } = req.params; - if (!roleId) return Error(res, { msg: "roleId requis" }); + if (!roleId) Error(res, { msg: "roleId requis" }); const role = await role_service.getRolePoints(Number(roleId)); - if (!role) return Error(res, { msg: "Rôle introuvable" }); + if (!role) Error(res, { msg: "Rôle introuvable" }); Ok(res, { data: role }); } catch (error) { diff --git a/backend/src/controllers/tent.controller.ts b/backend/src/controllers/tent.controller.ts index b9dfb39..9b9a55a 100644 --- a/backend/src/controllers/tent.controller.ts +++ b/backend/src/controllers/tent.controller.ts @@ -10,7 +10,7 @@ export const createTent = async (req: Request, res: Response) => { const userId1 = req.user?.userId; // Créateur = utilisateur connecté if (!userId1 || !userId2) { - return Error(res, { msg: "Identifiants utilisateurs manquants." }); + Error(res, { msg: "Identifiants utilisateurs manquants." }); } try { @@ -26,7 +26,7 @@ export const cancelTent = async (req: Request, res: Response) => { const userId1 = req.user?.userId; if (!userId1) { - return Error(res, { msg: "Identifiants utilisateurs manquants." }); + Error(res, { msg: "Identifiants utilisateurs manquants." }); } try { @@ -39,7 +39,7 @@ export const cancelTent = async (req: Request, res: Response) => { export const getUserTent = async (req: Request, res: Response) => { const userId = req.user?.userId; - if (!userId) return Error(res, { msg: "Utilisateur non authentifié." }); + if (!userId) Error(res, { msg: "Utilisateur non authentifié." }); try { const tent = await tent_service.getTentByUser(userId); @@ -63,7 +63,7 @@ export const toggleTentConfirmation = async (req: Request, res: Response) => { const { userId1, userId2, confirmed } = req.body; if (!userId1 || !userId2 || typeof confirmed !== "boolean") { - return Error(res, { msg: "Paramètres manquants ou invalides." }); + Error(res, { msg: "Paramètres manquants ou invalides." }); } try { @@ -75,7 +75,7 @@ export const toggleTentConfirmation = async (req: Request, res: Response) => { const user2 = await getUserById(userId2); if (!user1 || !user2) { - return Error(res, { msg: "Impossible de récupérer les utilisateurs." }); + Error(res, { msg: "Impossible de récupérer les utilisateurs." }); } // Génération du contenu HTML diff --git a/backend/src/database/migrations/0022_light_omega_red.sql b/backend/src/database/migrations/0022_light_omega_red.sql new file mode 100644 index 0000000..3d82d12 --- /dev/null +++ b/backend/src/database/migrations/0022_light_omega_red.sql @@ -0,0 +1 @@ +ALTER TABLE "bus_attribution" ADD COLUMN "departure_time" text NOT NULL; \ No newline at end of file diff --git a/backend/src/database/migrations/meta/0022_snapshot.json b/backend/src/database/migrations/meta/0022_snapshot.json new file mode 100644 index 0000000..0ba039e --- /dev/null +++ b/backend/src/database/migrations/meta/0022_snapshot.json @@ -0,0 +1,1284 @@ +{ + "id": "93925edf-9622-4ce2-80bc-26050abbce0b", + "prevId": "9a19155e-ddd0-492f-a2df-0bb9ef0d9879", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.challenges": { + "name": "challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "challenges_created_by_users_id_fk": { + "name": "challenges_created_by_users_id_fk", + "tableFrom": "challenges", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pre_registration_open": { + "name": "pre_registration_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "shotgun_open": { + "name": "shotgun_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sdi_open": { + "name": "sdi_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "wei_open": { + "name": "wei_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "food_open": { + "name": "food_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "chall_open": { + "name": "chall_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.factions": { + "name": "factions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "factions_name_unique": { + "name": "factions_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.news": { + "name": "news", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permanences": { + "name": "permanences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_at": { + "name": "start_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "end_at": { + "name": "end_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_open": { + "name": "is_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_name_unique": { + "name": "teams_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "majeur": { + "name": "majeur", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact": { + "name": "contact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Nouveau'" + }, + "discord_id": { + "name": "discord_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bus_attribution": { + "name": "bus_attribution", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "bus": { + "name": "bus", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "departure_time": { + "name": "departure_time", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bus_attribution_user_id_users_id_fk": { + "name": "bus_attribution_user_id_users_id_fk", + "tableFrom": "bus_attribution", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.challenge_validation": { + "name": "challenge_validation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "challenge_id": { + "name": "challenge_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "validated_by_admin_id": { + "name": "validated_by_admin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "validated_at": { + "name": "validated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "target_user_id": { + "name": "target_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "target_team_id": { + "name": "target_team_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "target_faction_id": { + "name": "target_faction_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "added_by_admin_id": { + "name": "added_by_admin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "challenge_validation_challenge_id_challenges_id_fk": { + "name": "challenge_validation_challenge_id_challenges_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "challenges", + "columnsFrom": [ + "challenge_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "challenge_validation_validated_by_admin_id_users_id_fk": { + "name": "challenge_validation_validated_by_admin_id_users_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "users", + "columnsFrom": [ + "validated_by_admin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "challenge_validation_target_user_id_users_id_fk": { + "name": "challenge_validation_target_user_id_users_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "challenge_validation_target_team_id_teams_id_fk": { + "name": "challenge_validation_target_team_id_teams_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "teams", + "columnsFrom": [ + "target_team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "challenge_validation_target_faction_id_factions_id_fk": { + "name": "challenge_validation_target_faction_id_factions_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "factions", + "columnsFrom": [ + "target_faction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "challenge_validation_added_by_admin_id_users_id_fk": { + "name": "challenge_validation_added_by_admin_id_users_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "users", + "columnsFrom": [ + "added_by_admin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registration_tokens": { + "name": "registration_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_tokens_user_id_users_id_fk": { + "name": "registration_tokens_user_id_users_id_fk", + "tableFrom": "registration_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "registration_tokens_token_unique": { + "name": "registration_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_points": { + "name": "role_points", + "schema": "", + "columns": { + "role_points": { + "name": "role_points", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "role_points_role_points_roles_id_fk": { + "name": "role_points_role_points_roles_id_fk", + "tableFrom": "role_points", + "tableTo": "roles", + "columnsFrom": [ + "role_points" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "role_points_role_points_pk": { + "name": "role_points_role_points_pk", + "columns": [ + "role_points" + ] + } + }, + "uniqueConstraints": { + "role_points_role_points_unique": { + "name": "role_points_role_points_unique", + "nullsNotDistinct": false, + "columns": [ + "role_points" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_faction": { + "name": "team_faction", + "schema": "", + "columns": { + "faction_id": { + "name": "faction_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "team_faction_faction_id_factions_id_fk": { + "name": "team_faction_faction_id_factions_id_fk", + "tableFrom": "team_faction", + "tableTo": "factions", + "columnsFrom": [ + "faction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_faction_team_id_teams_id_fk": { + "name": "team_faction_team_id_teams_id_fk", + "tableFrom": "team_faction", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_faction_faction_id_team_id_pk": { + "name": "team_faction_faction_id_team_id_pk", + "columns": [ + "faction_id", + "team_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_shotgun": { + "name": "team_shotgun", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_shotgun_team_id_teams_id_fk": { + "name": "team_shotgun_team_id_teams_id_fk", + "tableFrom": "team_shotgun", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.respo_permanences": { + "name": "respo_permanences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "permanence_id": { + "name": "permanence_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "respo_permanences_user_id_users_id_fk": { + "name": "respo_permanences_user_id_users_id_fk", + "tableFrom": "respo_permanences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "respo_permanences_permanence_id_permanences_id_fk": { + "name": "respo_permanences_permanence_id_permanences_id_fk", + "tableFrom": "respo_permanences", + "tableTo": "permanences", + "columnsFrom": [ + "permanence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "respo_permanences_user_id_permanence_id_pk": { + "name": "respo_permanences_user_id_permanence_id_pk", + "columns": [ + "user_id", + "permanence_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_permanences": { + "name": "user_permanences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "permanence_id": { + "name": "permanence_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "claimed": { + "name": "claimed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_permanences_user_id_users_id_fk": { + "name": "user_permanences_user_id_users_id_fk", + "tableFrom": "user_permanences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_permanences_permanence_id_permanences_id_fk": { + "name": "user_permanences_permanence_id_permanences_id_fk", + "tableFrom": "user_permanences", + "tableTo": "permanences", + "columnsFrom": [ + "permanence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_permanences_user_id_permanence_id_pk": { + "name": "user_permanences_user_id_permanence_id_pk", + "columns": [ + "user_id", + "permanence_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_role_id_roles_id_fk": { + "name": "user_preferences_role_id_roles_id_fk", + "tableFrom": "user_preferences", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id_role_id_pk": { + "name": "user_preferences_user_id_role_id_pk", + "columns": [ + "user_id", + "role_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_teams": { + "name": "user_teams", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_teams_user_id_team_id_pk": { + "name": "user_teams_user_id_team_id_pk", + "columns": [ + "user_id", + "team_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_tent": { + "name": "user_tent", + "schema": "", + "columns": { + "user_id_1": { + "name": "user_id_1", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id_2": { + "name": "user_id_2", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "confirmed": { + "name": "confirmed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_tent_user_id_1_users_id_fk": { + "name": "user_tent_user_id_1_users_id_fk", + "tableFrom": "user_tent", + "tableTo": "users", + "columnsFrom": [ + "user_id_1" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_tent_user_id_2_users_id_fk": { + "name": "user_tent_user_id_2_users_id_fk", + "tableFrom": "user_tent", + "tableTo": "users", + "columnsFrom": [ + "user_id_2" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_tent_user_id_1_user_id_2_pk": { + "name": "user_tent_user_id_1_user_id_2_pk", + "columns": [ + "user_id_1", + "user_id_2" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/backend/src/database/migrations/meta/_journal.json b/backend/src/database/migrations/meta/_journal.json index 71ffc61..6924d8e 100644 --- a/backend/src/database/migrations/meta/_journal.json +++ b/backend/src/database/migrations/meta/_journal.json @@ -155,6 +155,13 @@ "when": 1756063134903, "tag": "0021_colossal_madame_web", "breakpoints": true + }, + { + "idx": 22, + "version": "7", + "when": 1757002717384, + "tag": "0022_light_omega_red", + "breakpoints": true } ] } \ No newline at end of file diff --git a/backend/src/middlewares/multer.middleware.ts b/backend/src/middlewares/multer.middleware.ts index 184b4d4..f39eab1 100644 --- a/backend/src/middlewares/multer.middleware.ts +++ b/backend/src/middlewares/multer.middleware.ts @@ -28,7 +28,7 @@ export const createUploadMiddleware = ( next: NextFunction ) => { try { - if (!req.file) return Error(res, {msg: "Aucun fichier reçu"}); + if (!req.file) Error(res, {msg: "Aucun fichier reçu"}); const user = (req as Request).user?.userId || "anonymous"; const { originalname, mimetype, buffer } = req.file; @@ -47,7 +47,7 @@ export const createUploadMiddleware = ( const isPDF = detected?.mime === "application/pdf"; if (!isImage && !isPDF) { - return Error(res,{ msg:"Seules les images et les PDF sont autorisés"}); + Error(res,{ msg:"Seules les images et les PDF sont autorisés"}); } // Création dossier si nécessaire diff --git a/backend/src/routes/bus.routes.ts b/backend/src/routes/bus.routes.ts new file mode 100644 index 0000000..916ed55 --- /dev/null +++ b/backend/src/routes/bus.routes.ts @@ -0,0 +1,13 @@ +// src/routes/bus.routes.ts +import { Router } from "express"; +import * as busController from "../controllers/bus.controller"; +import { checkRole } from "../middlewares/user.middleware"; +import multer from "multer"; + +const busRouter = Router(); +const upload = multer({ dest: "uploads/buscsv/" }); + +busRouter.post("/admin/attributionemail", checkRole("Admin", []), busController.sendBusAttributionEmails); +busRouter.post("/admin/importbus",checkRole("Admin",["Respo CE"]), upload.single("file"), busController.uploadbusCSV ); + +export default busRouter; diff --git a/backend/src/schemas/Relational/busattribution.schema.ts b/backend/src/schemas/Relational/busattribution.schema.ts index 6813dca..c323e9f 100644 --- a/backend/src/schemas/Relational/busattribution.schema.ts +++ b/backend/src/schemas/Relational/busattribution.schema.ts @@ -1,10 +1,10 @@ -import { pgTable, serial, text, integer, pgEnum, date } from "drizzle-orm/pg-core"; -import {primaryKey} from "drizzle-orm/pg-core/index"; +import { pgTable, integer, text } from "drizzle-orm/pg-core"; import { userSchema } from "../Basic/user.schema"; export const busAttributionSchema = pgTable("bus_attribution", { user_id: integer('user_id').primaryKey().notNull().references(() => userSchema.id), - bus: integer("bus").notNull() + bus: integer("bus").notNull(), + departure_time : text("departure_time").notNull(), }); export type BusAttributionSchema = typeof busAttributionSchema.$inferInsert; diff --git a/backend/src/services/bus.service.ts b/backend/src/services/bus.service.ts new file mode 100644 index 0000000..684e73e --- /dev/null +++ b/backend/src/services/bus.service.ts @@ -0,0 +1,83 @@ +// src/services/bus.service.ts +import fs from "fs"; +import Papa from "papaparse"; +import { db } from "../database/db"; +import { busAttributionSchema } from "../schemas/Relational/busattribution.schema"; +import { userSchema } from "../schemas/Basic/user.schema"; +import { eq } from "drizzle-orm"; + +export interface BusAttribution { + userId: number; + firstName: string | null; + lastName: string | null; + email: string; + bus: number; + departure_time: string; +} + +type CsvBus = { + user_id: number; + bus: number; + departure_time : string; +}; + +// Récupérer toutes les attributions bus + user +export const getAllBusAttributions = async (): Promise => { + const results = await db + .select({ + userId: userSchema.id, + firstName: userSchema.first_name, + lastName: userSchema.last_name, + email: userSchema.email, + bus: busAttributionSchema.bus, + departure_time: busAttributionSchema.departure_time + }) + .from(busAttributionSchema) + .innerJoin(userSchema, eq(userSchema.id, busAttributionSchema.user_id)); + + return results; +}; + +// Récupérer une attribution précise par userId +export const getBusAttributionByUserId = async (userId: number): Promise => { + const result = await db + .select({ + userId: userSchema.id, + firstName: userSchema.first_name, + lastName: userSchema.last_name, + email: userSchema.email, + bus: busAttributionSchema.bus, + departure_time: busAttributionSchema.departure_time, + }) + .from(busAttributionSchema) + .innerJoin(userSchema, eq(userSchema.id, busAttributionSchema.user_id)) + .where(eq(busAttributionSchema.user_id, userId)); + + return result.length > 0 ? result[0] : null; +}; + + +export const importBusFromCSV = async ( + filePath: string +): Promise => { + const fileContent = fs.readFileSync(filePath, "utf8"); + + const { data, errors } = Papa.parse(fileContent, { + header: true, + skipEmptyLines: true, + }); + + if (errors.length > 0) { + console.error("CSV parsing errors:", errors); + throw new Error("Erreur lors du parsing du CSV."); + } + + const parsedData = data.map((r) => ({ + user_id: r.user_id, + bus: r.bus, + departure_time: r.departure_time, + + })); + + await db.insert(busAttributionSchema).values(parsedData); +}; \ No newline at end of file diff --git a/backend/src/services/im_export.service.ts b/backend/src/services/im_export.service.ts index 44c9a62..2985268 100644 --- a/backend/src/services/im_export.service.ts +++ b/backend/src/services/im_export.service.ts @@ -45,14 +45,14 @@ export const writeToGoogleSheet = async ( export const exportUsersToCSV = async (): Promise => { const users = await user_service.getUsersAll(); - + console.log(users); const formattedUsers = users.map(u => { const isOrga = u.roles && u.roles.length > 0; const isCE = u.permission === "Student" && u.teamId !== null; const isBenevole = u.roles.some(role => role.roleName === "Bénévole"); return { - userId: u.id, + id: u.id, prenom: u.first_name ?? "", nom: u.last_name ?? "", mail: u.email ?? "", diff --git a/backend/src/services/tent.service.ts b/backend/src/services/tent.service.ts index bab65c0..917fc24 100644 --- a/backend/src/services/tent.service.ts +++ b/backend/src/services/tent.service.ts @@ -71,9 +71,11 @@ export const getAllTents = async () => { user1_first_name: userSchema.first_name, user1_last_name: userSchema.last_name, user1_email: userSchema.email, + user1_majeur: userSchema.majeur, user2_first_name: user2.first_name, user2_last_name: user2.last_name, user2_email: user2.email, + user2_majeur: user2.majeur, confirmed: userTentSchema.confirmed }) .from(userTentSchema) diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 31b35d0..f24ca15 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -4,7 +4,7 @@ "strict": false, "target": "ES2020", "module": "CommonJS", // Utilisation de CommonJS car nodemon et ts-node ne supportent pas encore bien ESNext - "moduleResolution": "node", + "moduleResolution": "node16", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2099a37..a99f152 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -31,7 +31,7 @@ "react-router-dom": "^7.5.0", "react-select": "^5.10.1", "shadcn": "^2.10.0", - "sweetalert2": "^11.6.13", + "sweetalert2": "^11.23.0", "swiper": "^11.2.6", "tailwind-merge": "^3.2.0", "tailwindcss": "^4.1.3", @@ -6765,21 +6765,12 @@ } }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, "node_modules/react-dom": { @@ -7667,9 +7658,9 @@ } }, "node_modules/sweetalert2": { - "version": "11.22.3", - "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.22.3.tgz", - "integrity": "sha512-//WtZEvs9XRmyQVs77tz02ERu2JUTATQvrec+MjJYtKtnYSYDDgAmDdWzRWMkcxClj828EQgfHrpM+tI6qcMOg==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.23.0.tgz", + "integrity": "sha512-cKzzbC3C1sIs7o9XAMw4E8F9kBtGXsBDUsd2JZ8JM/dqa+nzWwSGM+9LLYILZWzWHzX9W+HJNHyBlbHPVS/krw==", "license": "MIT", "funding": { "type": "individual", @@ -8434,4 +8425,4 @@ } } } -} \ No newline at end of file +} diff --git a/frontend/package.json b/frontend/package.json index 9a690a9..d1931bb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,7 +33,7 @@ "react-router-dom": "^7.5.0", "react-select": "^5.10.1", "shadcn": "^2.10.0", - "sweetalert2": "^11.6.13", + "sweetalert2": "^11.23.0", "swiper": "^11.2.6", "tailwind-merge": "^3.2.0", "tailwindcss": "^4.1.3", @@ -54,4 +54,4 @@ "typescript-eslint": "^8.24.1", "vite": "^6.2.0" } -} \ No newline at end of file +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index aeaa043..07d0cab 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -18,7 +18,8 @@ import { AdminPageUser, AdminPageNews, AdminPageGames, - AdminPageTent + AdminPageTent, + AdminPageBus } from './pages/admin'; import ProtectedRoute from './components/utils/protectedroute'; @@ -101,6 +102,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> diff --git a/frontend/src/components/Admin/adminBus.tsx b/frontend/src/components/Admin/adminBus.tsx new file mode 100644 index 0000000..04a853a --- /dev/null +++ b/frontend/src/components/Admin/adminBus.tsx @@ -0,0 +1,159 @@ +import { useState } from "react"; +import Swal from "sweetalert2"; +import { busAttribution, importBusCSV } from "../../services/requests/bus.service"; +import { Button } from "../ui/button"; + +export const AdminBusTools = () => { + const [file, setFile] = useState(null); + const [message, setMessage] = useState(""); + + // 📤 Envoi des emails d’attribution bus + const handleSendBusAttribution = async () => { + const confirm = await Swal.fire({ + title: "Envoyer les attributions de bus ?", + text: "Tous les utilisateurs recevront leur email d'attribution.", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#2563eb", + cancelButtonColor: "#d33", + confirmButtonText: "🚍 Oui, envoyer", + cancelButtonText: "Annuler", + }); + + if (confirm.isConfirmed) { + try { + const res = await busAttribution(); + Swal.fire({ + title: "Succès ✅", + text: res?.msg || "Emails envoyés avec succès !", + icon: "success", + }); + } catch (error: any) { + Swal.fire({ + title: "Erreur ❌", + text: error?.response?.data?.msg || "Une erreur est survenue.", + icon: "error", + }); + } + } + }; + + // 📥 Import CSV bus + const handleFileChange = (e: React.ChangeEvent) => { + const selectedFile = e.target.files?.[0]; + if (selectedFile) { + setFile(selectedFile); + } + }; + + const handleFileUpload = async () => { + if (!file) { + setMessage("Veuillez sélectionner un fichier CSV."); + return; + } + + const formData = new FormData(); + formData.append("file", file); + + try { + const response = await importBusCSV(formData); + setMessage(response.message); + } catch (error) { + console.error(error); + setMessage("Erreur lors de l'import du fichier CSV."); + } + }; + + return ( +
+ {/* Titre principal */} +

+ 🚍 Outils d’administration des Bus +

+ + {/* Section attribution */} +
+

+ ✉️ Envoyer les attributions +

+

+ Envoie les emails d’attribution de bus à tous les utilisateurs. +

+
+ +
+
+ +
+ + {/* Section import CSV */} +
+

+ 📥 Importer un fichier CSV +

+

+ Chargez un fichier CSV contenant les attributions de bus. +

+ +
+ + + +
+ + {message && ( +

+ {message} +

+ )} +
+ +
+ + {/* Exemple CSV */} +
+

+ 📄 Exemple de fichier CSV : +

+
+{`user_id,bus,departure_time
+1,2,11h
+2,3,13h
+3,1,11h`}
+        
+

+ Le fichier doit être encodé en UTF-8 et utiliser une virgule comme séparateur. +

+
+
+ ); +}; diff --git a/frontend/src/components/Admin/adminEmail.tsx b/frontend/src/components/Admin/adminEmail.tsx index 59dce3b..3b2bab3 100644 --- a/frontend/src/components/Admin/adminEmail.tsx +++ b/frontend/src/components/Admin/adminEmail.tsx @@ -28,7 +28,6 @@ export const AdminEmail = () => { const templateOptions = [ { value: 'templateWelcome', label: 'Template Welcome' }, - { value: 'templateAttributionBus', label: 'Template Bus' }, { value: 'templateNotebook', label: 'Template Cahier de Vacances' }, ]; diff --git a/frontend/src/components/Admin/adminTent.tsx b/frontend/src/components/Admin/adminTent.tsx index c8c35a7..07bd7be 100644 --- a/frontend/src/components/Admin/adminTent.tsx +++ b/frontend/src/components/Admin/adminTent.tsx @@ -9,10 +9,12 @@ interface TentPair { user1_first_name: string; user1_last_name: string; user1_email: string; + user1_majeur: boolean; user2_id: number; user2_first_name: string; user2_last_name: string; user2_email: string; + user2_majeur: boolean; confirmed: boolean; } @@ -151,8 +153,10 @@ export const TentAdmin = () => { 👤 Nom 1 📧 Email 1 + 🔞 Majeur 1 👤 Nom 2 📧 Email 2 + 🔞 Majeur 2 ✅ État ⚡ Action @@ -162,8 +166,10 @@ export const TentAdmin = () => { {pair.user1_first_name} {pair.user1_last_name} {pair.user1_email} + {pair.user1_majeur ? "Oui" : "Non"} {pair.user2_first_name} {pair.user2_last_name} {pair.user2_email} + {pair.user2_majeur ? "Oui" : "Non"} {pair.confirmed ? ( diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx index 4fe3dd3..0b4d9c3 100644 --- a/frontend/src/components/navbar.tsx +++ b/frontend/src/components/navbar.tsx @@ -71,6 +71,7 @@ export const Navbar = () => { { label: "Email", to: "/admin/email", rolesAllowed: ["Admin"] }, { label: "News", to: "/admin/news", rolesAllowed: ["Admin", "Communication"] }, { label: "Tentes", to: "/admin/tent", rolesAllowed: ["Admin"] }, + { label: "Bus", to: "/admin/bus", rolesAllowed: ["Admin"] }, { label: "Games", to: "/admin/games", rolesAllowed: ["Admin"] }, ], }, diff --git a/frontend/src/pages/admin.tsx b/frontend/src/pages/admin.tsx index d472fc1..db08861 100644 --- a/frontend/src/pages/admin.tsx +++ b/frontend/src/pages/admin.tsx @@ -10,6 +10,7 @@ import { AdminEmail } from "../components/Admin/adminEmail"; import { AdminSyncNewStudent, AdminUser } from "../components/Admin/adminUser"; import { AdminNews } from "../components/Admin/adminNews"; import { AdminRolePointsManager } from "../components/Admin/adminGames"; +import { AdminBusTools } from "../components/Admin/adminBus"; //--------------Challenge Import--------------// import ChallengeEditor from "../components/Admin/AdminChallenge/adminChallengeEditor"; @@ -394,6 +395,18 @@ export const AdminPageTent: React.FC = () => { ); }; +export const AdminPageBus: React.FC = () => { + return ( + +
+
+ < AdminBusTools/> +
+
+
+ ); +}; + export const AdminPageGames: React.FC = () => { return ( diff --git a/frontend/src/services/requests/auth.service.ts b/frontend/src/services/requests/auth.service.ts index adf8f16..80111f6 100644 --- a/frontend/src/services/requests/auth.service.ts +++ b/frontend/src/services/requests/auth.service.ts @@ -70,7 +70,7 @@ export const requestPasswordUser = async(user_email : string)=>{ } export const renewTokenUser = async(userId : number)=>{ - console.log(userId) + const response = await api.post('authadmin/admin/renewtoken', {userId}); return response?.data diff --git a/frontend/src/services/requests/bus.service.ts b/frontend/src/services/requests/bus.service.ts new file mode 100644 index 0000000..40f1f34 --- /dev/null +++ b/frontend/src/services/requests/bus.service.ts @@ -0,0 +1,19 @@ +import api from "../api"; + + +export const busAttribution = async()=>{ + + const response = await api.post('bus/admin/attributionemail'); + + return response?.data +} + +export const importBusCSV = async(formData : FormData) => { + + const response = await api.post(`/bus/admin/importbus`, formData, { + headers: { + "Content-Type": "multipart/form-data", + },}); + return response.data; + +} \ No newline at end of file