diff --git a/backend/server.ts b/backend/server.ts index 5f5b6dc..abc47b1 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -18,7 +18,7 @@ import challengeRoutes from './src/routes/challenge.routes'; import defaultRoute from './src/routes/default.routes'; import discordRoutes from './src/routes/discord.routes'; import emailRoutes from './src/routes/email.routes'; -import eventRoutes from './src/routes/event.routes'; +import settingsRoutes from './src/routes/settings.routes'; import factionRoutes from './src/routes/faction.routes'; import imexportRouter from './src/routes/im_export.routes'; import newsRoutes from './src/routes/news.routes'; @@ -59,7 +59,7 @@ async function startServer() { app.use('/api/role', authenticateUser, roleRoutes); app.use('/api/user', authenticateUser, userRoutes); app.use('/api/team', authenticateUser, teamRoutes); - app.use('/api/event', authenticateUser, eventRoutes); + app.use('/api/settings', authenticateUser, settingsRoutes); app.use('/api/faction', authenticateUser, factionRoutes); app.use('/api/imexport', authenticateUser, imexportRouter); app.use('/api/permanence', authenticateUser, permanenceRoutes); diff --git a/backend/src/controllers/event.controller.ts b/backend/src/controllers/event.controller.ts deleted file mode 100644 index b32b11c..0000000 --- a/backend/src/controllers/event.controller.ts +++ /dev/null @@ -1,199 +0,0 @@ -import * as event_service from '../services/event.service'; -import * as team_service from '../services/team.service'; -import { Conflict, Error, Ok, Teapot, Unauthorized } from '../shared/http/responses'; -import { shotgun_password } from '../shared/secrets/secrets'; -import type { AppRequestHandler } from '../types/http'; -import type { ShotgunBody, ToggleStatusBody } from '../dto/event.dto'; - -export const checkShotgunStatus: AppRequestHandler = async (_req, res) => { - try { - const status = await event_service.getEventsStatus(); - Ok(res, { - data: { status: Boolean(status?.shotgun_open), password: status?.shotgun_open ? shotgun_password : '' }, - }); - } catch (error) { - Error(res, { msg: 'Error while catching shotgun status :' + error }); - } -}; - -export const checkPreRegisterStatus: AppRequestHandler = async (_req, res) => { - try { - const status = await event_service.getEventsStatus(); - Ok(res, { data: status?.pre_registration_open }); - } catch (error) { - Error(res, { msg: 'Error while catching pre-registration status :' + error }); - } -}; - -export const checkSDIStatus: AppRequestHandler = async (_req, res) => { - try { - const status = await event_service.getEventsStatus(); - Ok(res, { data: status?.sdi_open }); - } catch (error) { - Error(res, { msg: 'Error while catching SDI status :' + error }); - } -}; - -export const checkWEIStatus: AppRequestHandler = async (_req, res) => { - try { - const status = await event_service.getEventsStatus(); - Ok(res, { data: status?.wei_open }); - } catch (error) { - Error(res, { msg: 'Error while catching WEI status :' + error }); - } -}; - -export const checkFoodStatus: AppRequestHandler = async (_req, res) => { - try { - const status = await event_service.getEventsStatus(); - Ok(res, { data: status?.food_open }); - } catch (error) { - Error(res, { msg: 'Error while catching Food status :' + error }); - } -}; - -export const checkChallStatus: AppRequestHandler = async (_req, res) => { - try { - const status = await event_service.getEventsStatus(); - Ok(res, { data: status?.chall_open }); - } catch (error) { - Error(res, { msg: 'Error while catching Challenge status :' + error }); - } -}; - -export const getShotgunAttempts: AppRequestHandler = async (_req, res) => { - try { - const shotgunAttempts = await event_service.getAllTeamShotguns(); - const shotgunAttemptsWithLeaders = await Promise.all( - shotgunAttempts.map(async (attempt) => { - if (!attempt.teamId) { - return { ...attempt, leaderCount: 0 }; - } - - const teamUsers = await team_service.getTeamUsers(attempt.teamId); - const leaderCount = teamUsers.filter((user) => user.permission !== 'Nouveau').length; - - return { ...attempt, leaderCount }; - }), - ); - - Ok(res, { data: shotgunAttemptsWithLeaders }); - } catch (error) { - Error(res, { msg: 'Erreur lors de la récupération des tentatives shotgun : ' + error }); - } -}; - -export const shotgunAttempt: AppRequestHandler = async (req, res) => { - const { password } = req.body; - - const userId = req.user?.userId; - - if (!userId) { - Unauthorized(res, { msg: 'Utilisateur non authentifié.' }); - return; - } - - if (!shotgun_password) { - Error(res, { msg: 'Mot de passe shotgun non configuré côté serveur.' }); - return; - } - - if (password !== shotgun_password) { - Teapot(res, { msg: 'Le mot de passe shotgun est incorrect.' }); - return; - } - - const status = await event_service.getEventsStatus(); - if (!status?.shotgun_open) { - Unauthorized(res, { msg: 'Le shotgun est fermé.' }); - return; - } - try { - const userTeam = await team_service.getUserTeam(userId); - - if (!userTeam) { - Error(res, { msg: "Erreur : Tu n'as pas d'équipe !" }); - return; - } - - const alreadyShotgun = await event_service.alreadyShotgun(userTeam); - - if (alreadyShotgun) { - Conflict(res, { msg: 'Votre équipe est déjà dans le shotgun.' }); - return; - } - - await event_service.validateShotgun(userTeam); - Ok(res, { msg: 'Shotgun validé !' }); - return; - } catch (error) { - Error(res, { msg: 'Erreur pendant le shotguns : ' + error }); - return; - } -}; - -export const togglePreRegistration: AppRequestHandler = async (req, res) => { - const { preRegistrationOpen } = req.body; - - try { - const result = await event_service.updatepreRegistrationStatus(preRegistrationOpen); - Ok(res, { msg: 'Paramètres mis à jour.', data: result }); - } catch { - Error(res, { msg: 'Erreur lors de la mise à jour.' }); - } -}; - -export const toggleShotgun: AppRequestHandler = async (req, res) => { - const { shotgunOpen } = req.body; - - try { - const result = await event_service.updateShotgunStatus(shotgunOpen); - Ok(res, { msg: 'Paramètres mis à jour.', data: result }); - } catch { - Error(res, { msg: 'Erreur lors de la mise à jour.' }); - } -}; - -export const toggleSDI: AppRequestHandler = async (req, res) => { - const { sdiOpen } = req.body; - - try { - const result = await event_service.updateSDIStatus(sdiOpen); - Ok(res, { msg: 'Paramètres mis à jour.', data: result }); - } catch { - Error(res, { msg: 'Erreur lors de la mise à jour.' }); - } -}; - -export const toggleWEI: AppRequestHandler = async (req, res) => { - const { weiOpen } = req.body; - - try { - const result = await event_service.updateWEIStatus(weiOpen); - Ok(res, { msg: 'Paramètres mis à jour.', data: result }); - } catch { - Error(res, { msg: 'Erreur lors de la mise à jour.' }); - } -}; - -export const toggleFood: AppRequestHandler = async (req, res) => { - const { foodOpen } = req.body; - - try { - const result = await event_service.updateFoodStatus(foodOpen); - Ok(res, { msg: 'Paramètres mis à jour.', data: result }); - } catch { - Error(res, { msg: 'Erreur lors de la mise à jour.' }); - } -}; - -export const toggleChall: AppRequestHandler = async (req, res) => { - const { challOpen } = req.body; - - try { - const result = await event_service.updateChallStatus(challOpen); - Ok(res, { msg: 'Paramètres mis à jour.', data: result }); - } catch { - Error(res, { msg: 'Erreur lors de la mise à jour.' }); - } -}; diff --git a/backend/src/controllers/im_export.controller.ts b/backend/src/controllers/im_export.controller.ts index 668c40f..ee1505f 100644 --- a/backend/src/controllers/im_export.controller.ts +++ b/backend/src/controllers/im_export.controller.ts @@ -1,6 +1,6 @@ import fs from 'fs'; import path from 'path'; -import * as event_service from '../services/event.service'; +import * as settings_service from '../services/settings.service'; import * as export_service from '../services/im_export.service'; import * as permanence_service from '../services/permanence.service'; import * as team_service from '../services/team.service'; @@ -22,7 +22,7 @@ export const exportAllDataToSheets: AppRequestHandler = async (_req, res) => { const userList = await user_service.getUsersAll(); const teamList = await team_service.getTeamsAll(); const permanenceList = await permanence_service.getAllPermanencesWithUsers(); - const shotgunList = await event_service.getAllTeamShotguns(); + const shotgunList = await settings_service.getAllTeamShotguns(); // 2. Mapping -> format pour Google Sheets (array de array) const usersValues = [ diff --git a/backend/src/controllers/settings.controller.ts b/backend/src/controllers/settings.controller.ts new file mode 100644 index 0000000..004e768 --- /dev/null +++ b/backend/src/controllers/settings.controller.ts @@ -0,0 +1,139 @@ +import * as settings_service from '../services/settings.service'; +import * as team_service from '../services/team.service'; +import { Conflict, Error, Ok, Teapot, Unauthorized } from '../shared/http/responses'; +import { shotgun_password } from '../shared/secrets/secrets'; +import type { AppRequestHandler } from '../types/http'; +import type { ShotgunBody, ToggleStatusBody } from '../dto/event.dto'; + +export const getSettingStatus: AppRequestHandler = async (req, res) => { + const { setting } = req.params; + + if (!setting || !settings_service.isSetting(setting)) { + Error(res, { msg: 'Setting événement inconnu.' }); + return; + } + + try { + const status = await settings_service.getSettingStatus(setting); + if (setting === 'shotgun') { + Ok(res, { data: { status, password: status ? shotgun_password : '' } }); + } else { + Ok(res, { data: status }); + } + } catch (error) { + Error(res, { msg: 'Erreur lors de la récupération du statut :' + error }); + } +}; + +export const getAvailableSettings: AppRequestHandler = async (req, res) => { + try { + const userPermission = req.user?.userPermission ?? ''; + const userRoles = req.user?.userRoles?.map((role) => role.roleName) ?? []; + const settings = await settings_service.getAvailableSettings(userPermission, userRoles); + Ok(res, { data: settings }); + } catch (error) { + Error(res, { msg: 'Erreur lors de la récupération des settings :' + error }); + } +}; + +export const getAdminSettings: AppRequestHandler = async (_req, res) => { + try { + const settings = await settings_service.getAllSettings(); + Ok(res, { data: settings }); + } catch (error) { + Error(res, { msg: 'Erreur lors de la récupération des settings :' + error }); + } +}; + +export const getShotgunAttempts: AppRequestHandler = async (_req, res) => { + try { + const shotgunAttempts = await settings_service.getAllTeamShotguns(); + const shotgunAttemptsWithLeaders = await Promise.all( + shotgunAttempts.map(async (attempt) => { + if (!attempt.teamId) { + return { ...attempt, leaderCount: 0 }; + } + + const teamUsers = await team_service.getTeamUsers(attempt.teamId); + const leaderCount = teamUsers.filter((user) => user.permission !== 'Nouveau').length; + + return { ...attempt, leaderCount }; + }), + ); + + Ok(res, { data: shotgunAttemptsWithLeaders }); + } catch (error) { + Error(res, { msg: 'Erreur lors de la récupération des tentatives shotgun : ' + error }); + } +}; + +export const shotgunAttempt: AppRequestHandler = async (req, res) => { + const { password } = req.body; + + const userId = req.user?.userId; + + if (!userId) { + Unauthorized(res, { msg: 'Utilisateur non authentifié.' }); + return; + } + + if (!shotgun_password) { + Error(res, { msg: 'Mot de passe shotgun non configuré côté serveur.' }); + return; + } + + if (password !== shotgun_password) { + Teapot(res, { msg: 'Le mot de passe shotgun est incorrect.' }); + return; + } + + const status = await settings_service.getSettingsStatus(); + if (!status?.shotgun_open) { + Unauthorized(res, { msg: 'Le shotgun est fermé.' }); + return; + } + try { + const userTeam = await team_service.getUserTeam(userId); + + if (!userTeam) { + Error(res, { msg: "Erreur : Tu n'as pas d'équipe !" }); + return; + } + + const alreadyShotgun = await settings_service.alreadyShotgun(userTeam); + + if (alreadyShotgun) { + Conflict(res, { msg: 'Votre équipe est déjà dans le shotgun.' }); + return; + } + + await settings_service.validateShotgun(userTeam); + Ok(res, { msg: 'Shotgun validé !' }); + return; + } catch (error) { + Error(res, { msg: 'Erreur pendant le shotguns : ' + error }); + return; + } +}; + +export const updateSettingStatus: AppRequestHandler = async (req, res) => { + const { setting } = req.params; + const { open } = req.body; + + if (!setting || !settings_service.isSetting(setting)) { + Error(res, { msg: 'Setting événement inconnu.' }); + return; + } + + if (typeof open !== 'boolean') { + Error(res, { msg: "Le champ 'open' doit être un booléen." }); + return; + } + + try { + const result = await settings_service.updateSettingStatus(setting, open); + Ok(res, { msg: 'Paramètres mis à jour.', data: result }); + } catch (error) { + Error(res, { msg: 'Erreur lors de la mise à jour : ' + error }); + } +}; diff --git a/backend/src/controllers/team.controller.ts b/backend/src/controllers/team.controller.ts index 5377d9a..78a6461 100644 --- a/backend/src/controllers/team.controller.ts +++ b/backend/src/controllers/team.controller.ts @@ -1,5 +1,5 @@ import { type Event } from '../schemas/Basic/event.schema'; -import * as event_service from '../services/event.service'; +import * as settings_service from '../services/settings.service'; import * as faction_service from '../services/faction.service'; import * as team_service from '../services/team.service'; import { Error, Ok } from '../shared/http/responses'; @@ -14,7 +14,7 @@ export const createNewTeam: AppRequestHandler = async (req, res) Error(res, { msg: "Il n'y a pas de nom d'équipe" }); return; } - const status: Event = await event_service.getEventsStatus(); + const status: Event = await settings_service.getSettingsStatus(); if (!status?.pre_registration_open) { Error(res, { msg: "L'enregistrement d'équipe est fermé." }); return; diff --git a/backend/src/dto/event.dto.ts b/backend/src/dto/event.dto.ts index d70cf2f..b126d58 100644 --- a/backend/src/dto/event.dto.ts +++ b/backend/src/dto/event.dto.ts @@ -3,10 +3,5 @@ export type ShotgunBody = { }; export type ToggleStatusBody = { - preRegistrationOpen?: boolean; - shotgunOpen?: boolean; - sdiOpen?: boolean; - weiOpen?: boolean; - foodOpen?: boolean; - challOpen?: boolean; + open?: boolean; }; diff --git a/backend/src/routes/event.routes.ts b/backend/src/routes/event.routes.ts deleted file mode 100644 index 206174d..0000000 --- a/backend/src/routes/event.routes.ts +++ /dev/null @@ -1,25 +0,0 @@ -import express from 'express'; -import * as eventController from '../controllers/event.controller'; -import { checkRole } from '../middlewares/user.middleware'; - -const eventRouter = express.Router(); - -// User routes -eventRouter.get("/user/shotgunstatus", checkRole("Student", []), eventController.checkShotgunStatus); -eventRouter.get("/user/preregisterstatus", checkRole("Student", []), eventController.checkPreRegisterStatus); -eventRouter.get("/user/sdistatus", eventController.checkSDIStatus); -eventRouter.get("/user/weistatus", eventController.checkWEIStatus); -eventRouter.get("/user/foodstatus", eventController.checkFoodStatus); -eventRouter.get("/user/challstatus", eventController.checkChallStatus); -eventRouter.post("/user/shotgunattempt", checkRole("Student", []), eventController.shotgunAttempt); - -// Admin routes -eventRouter.post("/admin/shotguntoggle", checkRole("Admin", []), eventController.toggleShotgun); -eventRouter.get("/admin/shotgunattempts", checkRole("Admin", ["Respo CE"]), eventController.getShotgunAttempts); -eventRouter.post("/admin/preregistrationtoggle", checkRole("Admin", []), eventController.togglePreRegistration); -eventRouter.post("/admin/sditoggle", checkRole("Admin", []), eventController.toggleSDI); -eventRouter.post("/admin/weitoggle", checkRole("Admin", []), eventController.toggleWEI); -eventRouter.post("/admin/foodtoggle", checkRole("Admin", []), eventController.toggleFood); -eventRouter.post("/admin/challtoggle", checkRole("Admin", []), eventController.toggleChall); - -export default eventRouter; diff --git a/backend/src/routes/settings.routes.ts b/backend/src/routes/settings.routes.ts new file mode 100644 index 0000000..4e8ddc8 --- /dev/null +++ b/backend/src/routes/settings.routes.ts @@ -0,0 +1,17 @@ +import express from 'express'; +import * as settingsController from '../controllers/settings.controller'; +import { checkRole } from '../middlewares/user.middleware'; + +const settingsRouter = express.Router(); + +// User routes +settingsRouter.get('/user/status', settingsController.getAvailableSettings); +settingsRouter.get('/user/status/:setting', settingsController.getSettingStatus); +settingsRouter.post('/user/shotgunattempt', checkRole('Student', []), settingsController.shotgunAttempt); + +// Admin routes +settingsRouter.get('/admin/shotgunattempts', checkRole('Admin', ['Respo CE']), settingsController.getShotgunAttempts); +settingsRouter.get('/admin/settings', checkRole('Admin', []), settingsController.getAdminSettings); +settingsRouter.patch('/admin/status/:setting', checkRole('Admin', []), settingsController.updateSettingStatus); + +export default settingsRouter; diff --git a/backend/src/schemas/Basic/event.schema.ts b/backend/src/schemas/Basic/event.schema.ts index 0ca2969..466fd24 100644 --- a/backend/src/schemas/Basic/event.schema.ts +++ b/backend/src/schemas/Basic/event.schema.ts @@ -1,13 +1,14 @@ -import { boolean, pgTable, serial } from "drizzle-orm/pg-core"; +import { boolean, pgTable, serial } from 'drizzle-orm/pg-core'; -export const eventSchema = pgTable("events", { - id: serial("id").primaryKey(), - pre_registration_open: boolean("pre_registration_open").default(false), - shotgun_open: boolean("shotgun_open").default(false), - sdi_open: boolean("sdi_open").default(false), - wei_open: boolean("wei_open").default(false), - food_open: boolean("food_open").default(false), - chall_open: boolean("chall_open").default(false), +export const eventSchema = pgTable('events', { + id: serial('id').primaryKey(), + pre_registration_open: boolean('pre_registration_open').default(false), + shotgun_open: boolean('shotgun_open').default(false), + sdi_open: boolean('sdi_open').default(false), + wei_open: boolean('wei_open').default(false), + food_open: boolean('food_open').default(false), + chall_open: boolean('chall_open').default(false), + maker_battle_group_open: boolean('chall_open').default(false), }); export type Event = typeof eventSchema.$inferSelect; diff --git a/backend/src/services/event.service.ts b/backend/src/services/event.service.ts deleted file mode 100644 index 1457dec..0000000 --- a/backend/src/services/event.service.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { asc, eq } from "drizzle-orm"; -import { db } from "../database/db"; -import { eventSchema } from "../schemas/Basic/event.schema"; -import { teamSchema } from "../schemas/Basic/team.schema"; -import { teamShotgunSchema } from "../schemas/Relational/teamshotgun.schema"; - -export const getEventsStatus = async () => { - const events = await db.select().from(eventSchema); - if (events.length > 0) { - return events[0]; // Renvoie le premier événement s'il existe - } else { - return null; // ou une valeur par défaut - } -}; - -export const validateShotgun = async (teamId: number) => { - await db.transaction(async (tx) => { - await tx.insert(teamShotgunSchema).values({ team_id: teamId }); - }) -}; - -export const alreadyShotgun = async (teamId: number) => { - const shotgunTeam = await db.select({ shotgunId: teamShotgunSchema.id }) - .from(teamShotgunSchema) - .where(eq(teamShotgunSchema.team_id, teamId)); - - if (shotgunTeam[0]) { - return true - } - else { - return false - } -}; - -export const updatepreRegistrationStatus = async (preRegistrationOpen: boolean) => { - return await db.update(eventSchema) - .set({ pre_registration_open: preRegistrationOpen }) - .returning(); -}; - -export const updateShotgunStatus = async (shotgunOpen: boolean) => { - return await db.update(eventSchema) - .set({ shotgun_open: shotgunOpen }) - .returning(); -}; - -export const getAllTeamShotguns = async () => { - return await db - .select({ - id: teamShotgunSchema.id, - teamId: teamShotgunSchema.team_id, - timestamp: teamShotgunSchema.timestamp, - teamName: teamSchema.name, - teamType: teamSchema.type, - }) - .from(teamShotgunSchema) - .leftJoin(teamSchema, eq(teamShotgunSchema.team_id, teamSchema.id)) - .orderBy(asc(teamShotgunSchema.timestamp), asc(teamShotgunSchema.id)); -}; - -export const updateSDIStatus = async (sdiOpen: boolean) => { - return await db.update(eventSchema) - .set({ sdi_open: sdiOpen }) - .returning(); -}; - -export const updateWEIStatus = async (weiOpen: boolean) => { - return await db.update(eventSchema) - .set({ wei_open: weiOpen }) - .returning(); -}; - -export const updateFoodStatus = async (foodOpen: boolean) => { - return await db.update(eventSchema) - .set({ food_open: foodOpen }) - .returning(); -}; - -export const updateChallStatus = async (challOpen: boolean) => { - return await db.update(eventSchema) - .set({ chall_open: challOpen }) - .returning(); -}; diff --git a/backend/src/services/settings.service.ts b/backend/src/services/settings.service.ts new file mode 100644 index 0000000..43055a0 --- /dev/null +++ b/backend/src/services/settings.service.ts @@ -0,0 +1,116 @@ +import { asc, eq } from 'drizzle-orm'; +import { db } from '../database/db'; +import { eventSchema } from '../schemas/Basic/event.schema'; +import { teamSchema } from '../schemas/Basic/team.schema'; +import { teamShotgunSchema } from '../schemas/Relational/teamshotgun.schema'; + +export const settingColumns = { + preRegistration: 'pre_registration_open', + shotgun: 'shotgun_open', + sdi: 'sdi_open', + wei: 'wei_open', + food: 'food_open', + challenge: 'chall_open', + makerBattleGroup: 'maker_battle_group_open', +} as const; + +export type Setting = keyof typeof settingColumns; + +type SettingDefinition = { + key: Setting; + label: string; + column: (typeof settingColumns)[Setting]; + roles: string[]; +}; + +export const settingDefinitions: SettingDefinition[] = [ + { key: 'preRegistration', label: 'Pré-inscription', column: 'pre_registration_open', roles: [] }, + { key: 'shotgun', label: 'Shotgun', column: 'shotgun_open', roles: [] }, + { key: 'sdi', label: 'SDI (Billetterie)', column: 'sdi_open', roles: [] }, + { key: 'wei', label: 'WEI (Billetterie + Tentes)', column: 'wei_open', roles: [] }, + { key: 'food', label: 'Nourriture (Billetterie)', column: 'food_open', roles: [] }, + { key: 'challenge', label: 'Challenges (Affichage des challenges)', column: 'chall_open', roles: [] }, + { + key: 'makerBattleGroup', + label: 'Groupes de défis TC & Branche (Affichage des groupes)', + column: 'maker_battle_group_open', + roles: [], + }, +]; + +export const isSetting = (setting: string): setting is Setting => setting in settingColumns; + +export const getSettingsStatus = async () => { + const events = await db.select().from(eventSchema); + if (events.length > 0) { + return events[0]; // Renvoie le premier événement s'il existe + } else { + return null; // ou une valeur par défaut + } +}; + +export const getSettingStatus = async (setting: Setting) => { + const settings = await getSettingsStatus(); + return settings ? Boolean(settings[settingColumns[setting] as keyof typeof settings]) : false; +}; + +const canAccessSetting = (definition: SettingDefinition, userPermission: string, userRoles: string[]) => + userPermission === 'Admin' || + definition.roles.length === 0 || + definition.roles.includes(userPermission) || + definition.roles.some((role) => userRoles.includes(role)); + +export const getAvailableSettings = async (userPermission: string, userRoles: string[]) => { + const settings = await getSettingsStatus(); + + return settingDefinitions + .filter((definition) => canAccessSetting(definition, userPermission, userRoles)) + .map(({ key, label, roles, column }) => ({ + key, + label, + roles, + open: settings ? Boolean(settings[column as keyof typeof settings]) : false, + })); +}; + +export const getAllSettings = async () => getAvailableSettings('Admin', []); + +export const validateShotgun = async (teamId: number) => { + await db.transaction(async (tx) => { + await tx.insert(teamShotgunSchema).values({ team_id: teamId }); + }); +}; + +export const alreadyShotgun = async (teamId: number) => { + const shotgunTeam = await db + .select({ shotgunId: teamShotgunSchema.id }) + .from(teamShotgunSchema) + .where(eq(teamShotgunSchema.team_id, teamId)); + + if (shotgunTeam[0]) { + return true; + } else { + return false; + } +}; + +export const updateSettingStatus = async (setting: Setting, open: boolean) => { + return await db + .update(eventSchema) + .set({ [settingColumns[setting]]: open }) + .returning(); +}; + +export const getAllTeamShotguns = async () => { + return await db + .select({ + id: teamShotgunSchema.id, + teamId: teamShotgunSchema.team_id, + timestamp: teamShotgunSchema.timestamp, + teamName: teamSchema.name, + teamType: teamSchema.type, + }) + .from(teamShotgunSchema) + .leftJoin(teamSchema, eq(teamShotgunSchema.team_id, teamSchema.id)) + .orderBy(asc(teamShotgunSchema.timestamp), asc(teamShotgunSchema.id)); +}; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 46b0562..5b14d8b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,7 +12,7 @@ const AdminPageBanned = lazy(() => import('./pages/admin/adminBanned')); const AdminPageBus = lazy(() => import('./pages/admin/adminBus')); const AdminPageChallenges = lazy(() => import('./pages/admin/adminChallenges')); const AdminPageEmail = lazy(() => import('./pages/admin/adminEmail')); -const AdminPageEvents = lazy(() => import('./pages/admin/adminEvents')); +const AdminPageSettings = lazy(() => import('./pages/admin/adminSettings')); const AdminPageExport = lazy(() => import('./pages/admin/adminExport')); const AdminPageFaction = lazy(() => import('./pages/admin/adminFaction')); const AdminPageGames = lazy(() => import('./pages/admin/adminGames')); @@ -264,10 +264,10 @@ const App: React.FC = () => { } /> - + } /> diff --git a/frontend/src/components/Admin/adminEvent.tsx b/frontend/src/components/Admin/adminSettings.tsx similarity index 57% rename from frontend/src/components/Admin/adminEvent.tsx rename to frontend/src/components/Admin/adminSettings.tsx index 2e85f79..126ca88 100644 --- a/frontend/src/components/Admin/adminEvent.tsx +++ b/frontend/src/components/Admin/adminSettings.tsx @@ -2,57 +2,22 @@ import { CheckCircle, Loader2, XCircle } from 'lucide-react'; import { useEffect, useState } from 'react'; import Swal from 'sweetalert2'; -import { - checkChallengeStatus, - checkFoodStatus, - checkPreRegisterStatus, - checkSDIStatus, - checkShotgunStatus, - checkWEIStatus, - toggleChallenge, - toggleFood, - togglePreRegistration, - toggleSDI, - toggleShotgun, - toggleWEI, -} from '../../services/requests/event.service'; +import type { Setting } from '../../interfaces/settings.interface'; +import { getAdminSettings, updateSetting } from '../../services/requests/settings.service'; import { Button } from '../ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; -export const AdminEvents = () => { +export const AdminSettings = () => { const [loading, setLoading] = useState(false); const [loadingStatuses, setLoadingStatuses] = useState(true); - const [statuses, setStatuses] = useState({ - preRegistration: false, - shotgun: false, - sdi: false, - wei: false, - food: false, - chall: false, - }); + const [settings, setSettings] = useState([]); // Charger les statuts au montage useEffect(() => { const fetchStatuses = async () => { try { - const [preReg, shot, sdi, wei, food, chall] = await Promise.all([ - checkPreRegisterStatus(), - checkShotgunStatus(), - checkSDIStatus(), - checkWEIStatus(), - checkFoodStatus(), - checkChallengeStatus(), - ]); - - setStatuses({ - preRegistration: preReg, - shotgun: shot.status, - sdi, - wei, - food, - chall, - }); + setSettings(await getAdminSettings()); } catch { Swal.fire({ icon: 'error', @@ -67,19 +32,18 @@ export const AdminEvents = () => { }, []); // Fonction générique pour toggle un événement - const handleToggle = async ( - key: keyof typeof statuses, - toggleFn: (value: boolean) => Promise, - successMsg: string, - ) => { + const handleToggle = async (setting: Setting) => { setLoading(true); try { - await toggleFn(!statuses[key]); - setStatuses((prev) => ({ ...prev, [key]: !prev[key] })); + const open = !setting.open; + await updateSetting(setting.key, open); + setSettings((previous) => + previous.map((current) => (current.key === setting.key ? { ...current, open } : current)), + ); Swal.fire({ icon: 'success', title: 'Succès', - text: successMsg, + text: `${setting.label} mis à jour !`, timer: 1500, showConfirmButton: false, }); @@ -94,40 +58,6 @@ export const AdminEvents = () => { } }; - // Configuration des événements - const events = [ - { - key: 'preRegistration' as const, - label: 'Pré-inscription', - toggleFn: togglePreRegistration, - }, - { - key: 'shotgun' as const, - label: 'Shotgun', - toggleFn: toggleShotgun, - }, - { - key: 'sdi' as const, - label: 'SDI (Billetterie)', - toggleFn: toggleSDI, - }, - { - key: 'wei' as const, - label: 'WEI (Billetterie + Tentes)', - toggleFn: toggleWEI, - }, - { - key: 'food' as const, - label: 'Nourriture (Billetterie)', - toggleFn: toggleFood, - }, - { - key: 'chall' as const, - label: 'Challenges (Affichage des challenges)', - toggleFn: toggleChallenge, - }, - ]; - if (loadingStatuses) { return (
@@ -141,15 +71,15 @@ export const AdminEvents = () => { - ⚙️ Gestion des Événements + ⚙️ Gestion des settings - {events.map(({ key, label, toggleFn }) => { - const isActive = statuses[key]; + {settings.map((setting) => { + const isActive = setting.open; return (
{isActive ? ( @@ -157,11 +87,11 @@ export const AdminEvents = () => { ) : ( )} - {label} + {setting.label}
diff --git a/frontend/src/components/shotgun/preregisterTeamSection.tsx b/frontend/src/components/shotgun/preregisterTeamSection.tsx index 6e3fa46..cd8eae0 100644 --- a/frontend/src/components/shotgun/preregisterTeamSection.tsx +++ b/frontend/src/components/shotgun/preregisterTeamSection.tsx @@ -1,15 +1,15 @@ -import { useEffect, useState } from "react"; -import Select from "react-select"; +import { useEffect, useState } from 'react'; +import Select from 'react-select'; -import { checkPreRegisterStatus } from "../../services/requests/event.service"; -import { createTeam } from "../../services/requests/team.service"; -import { getUsers } from "../../services/requests/user.service"; -import { Button } from "../ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; -import { Input } from "../ui/input"; +import { checkPreRegisterStatus } from '../../services/requests/settings.service'; +import { createTeam } from '../../services/requests/team.service'; +import { getUsers } from '../../services/requests/user.service'; +import { Button } from '../ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; +import { Input } from '../ui/input'; export const PreregisterTeamSection = () => { - const [teamName, setTeamName] = useState(""); + const [teamName, setTeamName] = useState(''); const [members, setMembers] = useState([]); const [isPreRegistrationOpen, setIsPreRegistrationOpen] = useState(false); const [users, setUsers] = useState<{ userId: number; firstName: string; lastName: string }[]>([]); @@ -20,7 +20,7 @@ export const PreregisterTeamSection = () => { const status = await checkPreRegisterStatus(); setIsPreRegistrationOpen(status); } catch { - alert("Erreur lors de la récupération du statut de pré-inscription."); + alert('Erreur lors de la récupération du statut de pré-inscription.'); } }; fetchStatus(); @@ -32,7 +32,7 @@ export const PreregisterTeamSection = () => { const userList = await getUsers(); setUsers(userList); } catch { - alert("Erreur lors de la récupération des utilisateurs."); + alert('Erreur lors de la récupération des utilisateurs.'); } }; fetchUsers(); @@ -72,20 +72,23 @@ export const PreregisterTeamSection = () => { {isPreRegistrationOpen ? ( <> -

Etape 1: le GForm de motivation !

+

+ Etape 1: le GForm de motivation ! +

-

Etape 2: Sélection des membres

+

+ Etape 2: Sélection des membres +

@@ -141,15 +144,15 @@ export const PreregisterTeamSection = () => {

- Si tu ne trouves pas un coéquipier, c'est qu'il ne s'est jamais connecté sur ce site ! + Si tu ne trouves pas un coéquipier, c'est qu'il ne s'est jamais connecté sur ce + site !
Il lui suffit de se connecter une fois pour apparaitre dans cette liste.

@@ -162,6 +165,6 @@ export const PreregisterTeamSection = () => {

)} - + ); }; diff --git a/frontend/src/components/shotgun/shotgunSection.tsx b/frontend/src/components/shotgun/shotgunSection.tsx index ba41295..8c62380 100644 --- a/frontend/src/components/shotgun/shotgunSection.tsx +++ b/frontend/src/components/shotgun/shotgunSection.tsx @@ -1,17 +1,17 @@ -import { type AxiosError } from "axios"; -import { useEffect, useState } from "react"; +import { type AxiosError } from 'axios'; +import { useEffect, useState } from 'react'; -import { type ApiErrorResponse } from "../../interfaces/event.interface"; -import { attemptShotgun, checkShotgunStatus } from "../../services/requests/event.service"; -import { Button } from "../ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; -import { Input } from "../ui/input"; +import { type ApiErrorResponse } from '../../interfaces/settings.interface'; +import { attemptShotgun, checkShotgunStatus } from '../../services/requests/settings.service'; +import { Button } from '../ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; +import { Input } from '../ui/input'; export const Shotgun = () => { const [status, setStatus] = useState(false); - const [message, setMessage] = useState(""); - const [inputValue, setInputValue] = useState(""); - const [shotgunPassword, setShotgunPassword] = useState(""); + const [message, setMessage] = useState(''); + const [inputValue, setInputValue] = useState(''); + const [shotgunPassword, setShotgunPassword] = useState(''); useEffect(() => { const fetchStatus = async () => { @@ -26,12 +26,12 @@ export const Shotgun = () => { e.preventDefault(); if (!shotgunPassword) { - setMessage("❌ Erreur : mot de passe shotgun indisponible."); + setMessage('❌ Erreur : mot de passe shotgun indisponible.'); return; } if (inputValue !== shotgunPassword) { - setMessage("❌ Erreur : Mot de passe de Shotgun incorrect."); + setMessage('❌ Erreur : Mot de passe de Shotgun incorrect.'); return; } @@ -40,16 +40,14 @@ export const Shotgun = () => { setMessage(response.message); } catch (error) { const axiosError = error as AxiosError; - setMessage(axiosError.response?.data?.message || "Une erreur est survenue."); + setMessage(axiosError.response?.data?.message || 'Une erreur est survenue.'); } }; return ( - - Shotgun 🎯 - + Shotgun 🎯

Tape exactement la bonne phrase pour valider ton shotgun (majuscules incluses).

@@ -57,9 +55,10 @@ export const Shotgun = () => {

- Mot à entrer :{" "} - - {shotgunPassword || "patience..."} + Mot à entrer :{' '} + + {shotgunPassword || 'patience...'}

{!status && ( @@ -80,17 +79,18 @@ export const Shotgun = () => { /> {message && (

+ className={`text-center text-lg mt-4 ${ + message.includes('Erreur') || + message.toLowerCase().includes('déjà') || + message.toLowerCase().includes('incorrect') + ? 'text-red-500' + : 'text-green-600' + }`}> {message}

)} diff --git a/frontend/src/components/tent/tentSection.tsx b/frontend/src/components/tent/tentSection.tsx index a641ab9..cd6ca7a 100644 --- a/frontend/src/components/tent/tentSection.tsx +++ b/frontend/src/components/tent/tentSection.tsx @@ -6,7 +6,7 @@ import { useOnboarding } from '../../contexts/onboarding'; import { type Tent } from '../../interfaces/tent.interface'; import { type User } from '../../interfaces/user.interface'; import { decodeToken, getToken } from '../../services/requests/auth.service'; -import { checkWEIStatus } from '../../services/requests/event.service'; +import { checkWEIStatus } from '../../services/requests/settings.service'; import { cancelTent, createTent, getUserTent } from '../../services/requests/tent.service'; import { getUsers } from '../../services/requests/user.service'; import { Button } from '../ui/button'; diff --git a/frontend/src/interfaces/event.interface.ts b/frontend/src/interfaces/settings.interface.ts similarity index 84% rename from frontend/src/interfaces/event.interface.ts rename to frontend/src/interfaces/settings.interface.ts index 5954848..983c50f 100644 --- a/frontend/src/interfaces/event.interface.ts +++ b/frontend/src/interfaces/settings.interface.ts @@ -3,6 +3,12 @@ export interface ShotgunStatusData { password: string; } +export interface Setting { + key: string; + label: string; + open: boolean; +} + export interface ShotgunAttemptPayload { password: string; } diff --git a/frontend/src/pages/admin/adminEvents.tsx b/frontend/src/pages/admin/adminSettings.tsx similarity index 63% rename from frontend/src/pages/admin/adminEvents.tsx rename to frontend/src/pages/admin/adminSettings.tsx index 27b200f..b53e27c 100644 --- a/frontend/src/pages/admin/adminEvents.tsx +++ b/frontend/src/pages/admin/adminSettings.tsx @@ -1,15 +1,15 @@ -import { AdminEvents } from '../../components/Admin/adminEvent'; import { AdminLayout } from '../../components/Admin/adminLayout'; +import { AdminSettings } from '../../components/Admin/adminSettings'; import { RevealSection } from '../../components/ui/revealSection'; -const AdminPageEvents: React.FC = () => ( +const AdminPageSettings: React.FC = () => (
- +
); -export default AdminPageEvents; +export default AdminPageSettings; diff --git a/frontend/src/services/requests/event.service.ts b/frontend/src/services/requests/event.service.ts deleted file mode 100644 index bc46a59..0000000 --- a/frontend/src/services/requests/event.service.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { type ApiMessageResponse, type ShotgunAttemptPayload, type ShotgunAttemptRow, type ShotgunStatusData } from '../../interfaces/event.interface'; -import api from '../api'; - -export const checkShotgunStatus = async (): Promise => { - const response = await api.get<{ data: ShotgunStatusData }>("/event/user/shotgunstatus"); - return response.data.data; -}; - -export const checkPreRegisterStatus = async () => { - const response = await api.get("/event/user/preregisterstatus"); - return response.data.data; -}; - -export const checkSDIStatus = async () => { - const response = await api.get("/event/user/sdistatus"); - return response.data.data; -}; - -export const checkWEIStatus = async () => { - const response = await api.get("/event/user/weistatus"); - return response.data.data; -}; - -export const checkFoodStatus = async () => { - const response = await api.get("/event/user/foodstatus"); - return response.data.data; -}; - -export const checkChallengeStatus = async () => { - const response = await api.get("/event/user/challstatus"); - return response.data.data; -}; - -export const attemptShotgun = async (payload: ShotgunAttemptPayload): Promise => { - const response = await api.post("event/user/shotgunattempt", payload); - return response.data; -}; - -export const getShotgunAttemptsAdmin = async (): Promise => { - const response = await api.get<{ data: ShotgunAttemptRow[] }>("/event/admin/shotgunattempts"); - return response.data.data; -}; - -export const toggleShotgun = async (shotgunOpen: boolean) => { - const response = await api.post(`event/admin/shotguntoggle`, { shotgunOpen }); - return response.data; -}; - -export const togglePreRegistration = async (preRegistrationOpen: boolean) => { - const response = await api.post(`event/admin/preregistrationtoggle`, { preRegistrationOpen }); - return response.data; -}; - -export const toggleSDI = async (sdiOpen: boolean) => { - const response = await api.post(`event/admin/sditoggle`, { sdiOpen }); - return response.data; -}; - -export const toggleWEI = async (weiOpen: boolean) => { - const response = await api.post(`event/admin/weitoggle`, { weiOpen }); - return response.data; -}; - -export const toggleFood = async (foodOpen: boolean) => { - const response = await api.post(`event/admin/foodtoggle`, { foodOpen }); - return response.data; -}; - -export const toggleChallenge = async (challOpen: boolean) => { - const response = await api.post(`event/admin/challtoggle`, { challOpen }); - return response.data; -}; diff --git a/frontend/src/services/requests/settings.service.ts b/frontend/src/services/requests/settings.service.ts new file mode 100644 index 0000000..dea7186 --- /dev/null +++ b/frontend/src/services/requests/settings.service.ts @@ -0,0 +1,55 @@ +import { + type ApiMessageResponse, + type Setting, + type ShotgunAttemptPayload, + type ShotgunAttemptRow, + type ShotgunStatusData, +} from '../../interfaces/settings.interface'; +import api from '../api'; + +export const checkShotgunStatus = async (): Promise => { + const response = await api.get<{ data: ShotgunStatusData }>('/settings/user/status/shotgun'); + return response.data.data; +}; + +const getSetting = async (key: string): Promise => { + const response = await api.get<{ data: Array }>('/settings/user/status'); + const setting = response.data.data.find((item) => item.key === key); + if (!setting) throw new Error(`Le setting ${key} n'est pas disponible.`); + return setting; +}; + +export const getSettings = async (): Promise => { + const response = await api.get<{ data: Setting[] }>('/settings/user/status'); + return response.data.data; +}; + +export const getAdminSettings = async (): Promise => { + const response = await api.get<{ data: Setting[] }>('/settings/admin/settings'); + return response.data.data; +}; + +export const updateSetting = async (key: string, open: boolean) => { + const response = await api.patch(`/settings/admin/status/${key}`, { open }); + return response.data; +}; + +export const checkPreRegisterStatus = async () => (await getSetting('preRegistration')).open; + +export const checkSDIStatus = async () => (await getSetting('sdi')).open; + +export const checkWEIStatus = async () => (await getSetting('wei')).open; + +export const checkFoodStatus = async () => (await getSetting('food')).open; + +export const checkChallengeStatus = async () => (await getSetting('challenge')).open; + +export const attemptShotgun = async (payload: ShotgunAttemptPayload): Promise => { + const response = await api.post('settings/user/shotgunattempt', payload); + return response.data; +}; + +export const getShotgunAttemptsAdmin = async (): Promise => { + const response = await api.get<{ data: ShotgunAttemptRow[] }>('/settings/admin/shotgunattempts'); + return response.data.data; +};