diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 6ce061b..698b929 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -3,12 +3,13 @@ name: CI
on:
push:
branches:
- - prod
- dev
+ tags:
+ - 'v*'
pull_request:
branches:
- - prod
- dev
+ - prod
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -68,7 +69,7 @@ jobs:
build-api:
runs-on: self-hosted
- if: github.event_name == 'pull_request'
+ if: github.event_name == 'pull_request' || github.event_name == 'push'
needs:
- lint-api
steps:
@@ -97,7 +98,7 @@ jobs:
build-front:
runs-on: self-hosted
- if: github.event_name == 'pull_request'
+ if: github.event_name == 'pull_request' || github.event_name == 'push'
needs:
- lint-front
steps:
@@ -126,10 +127,11 @@ jobs:
deploy-api:
runs-on: self-hosted
- if : github.event_name == 'push'
- environment: ${{ github.ref == 'refs/heads/dev' && 'development' || 'production' }}
+ if: github.event_name == 'push' && (github.ref == 'refs/heads/dev' || startswith(github.ref, 'refs/tags/v'))
+ environment: ${{ startswith(github.ref, 'refs/tags/') && 'production' || 'development' }}
needs:
- lint-api
+ - build-api
steps:
- name: Checkout repository
uses: actions/checkout@v5
@@ -150,14 +152,16 @@ jobs:
context: ./backend
push: true
tags: |
- ${{ secrets.REGISTRY_URL }}/integration/api:${{ github.ref == 'refs/heads/prod' && 'prod' || 'dev' }}
+ ${{ secrets.REGISTRY_URL }}/integration/api:${{ startswith(github.ref, 'refs/tags/') && github.ref_name || 'dev' }}
+ ${{ startswith(github.ref, 'refs/tags/') && format('{0}/integration/api:prod', secrets.REGISTRY_URL) || '' }}
deploy-front:
runs-on: self-hosted
- if : github.event_name == 'push'
- environment: ${{ github.ref == 'refs/heads/dev' && 'development' || 'production' }}
+ if: github.event_name == 'push' && (github.ref == 'refs/heads/dev' || startswith(github.ref, 'refs/tags/v'))
+ environment: ${{ startswith(github.ref, 'refs/tags/') && 'production' || 'development' }}
needs:
- lint-front
+ - build-front
steps:
- name: Checkout repository
uses: actions/checkout@v5
@@ -172,7 +176,7 @@ jobs:
- name: Install docker
uses: docker/setup-buildx-action@v3
- - name: Front- Build and push
+ - name: Front - Build and push
uses: docker/build-push-action@v6
with:
context: ./frontend
@@ -199,5 +203,8 @@ jobs:
VITE_BDE_SIREN=${{ vars.BDE_SIREN }}
VITE_DPO_EMAIL=${{ vars.DPO_EMAIL }}
VITE_DPO_NAME=${{ vars.DPO_NAME }}
+ VITE_APP_VERSION=${{ startswith(github.ref, 'refs/tags/') && github.ref_name || 'dev' }}
+ VITE_RELEASE_URL=${{ startswith(github.ref, 'refs/tags/') && format('https://github.com/{0}/releases/tag/{1}', github.repository, github.ref_name) || '' }}
tags: |
- ${{ secrets.REGISTRY_URL }}/integration/front:${{ github.ref == 'refs/heads/prod' && 'prod' || 'dev' }}
+ ${{ secrets.REGISTRY_URL }}/integration/front:${{ startswith(github.ref, 'refs/tags/') && github.ref_name || 'dev' }}
+ ${{ startswith(github.ref, 'refs/tags/') && format('{0}/integration/front:prod', secrets.REGISTRY_URL) || '' }}
diff --git a/backend/server.ts b/backend/server.ts
index 5f5b6dc..65f07e6 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';
@@ -27,6 +27,7 @@ import roleRoutes from './src/routes/role.routes';
import teamRoutes from './src/routes/team.routes';
import tentRoutes from './src/routes/tent.routes';
import userRoutes from './src/routes/user.routes';
+import makerBattleRoutes from './src/routes/maker_battle.routes';
import bannedRoutes from './src/routes/banned.routes';
import { server_port } from './src/shared/secrets/secrets';
import { initQcmvss } from './src/database/initdb/initQcmvss';
@@ -59,7 +60,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);
@@ -69,6 +70,7 @@ async function startServer() {
app.use('/api/discord', authenticateUser, discordRoutes);
app.use('/api/tent', authenticateUser, tentRoutes);
app.use('/api/bus', authenticateUser, busRoutes);
+ app.use('/api/maker-battle', authenticateUser, makerBattleRoutes);
app.use('/api/uploads/news', express.static(path.join(__dirname, '/uploads/news')));
app.use('/api/uploads/notebooks', express.static(path.join(__dirname, '/uploads/notebooks')));
app.use('/api/uploads/foodmenu', express.static(path.join(__dirname, '/uploads/foodmenu')));
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..a7586ff 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 = [
@@ -162,23 +162,29 @@ export const updatePlannings: AppRequestHandler = async (_req, res) => {
}
};
-export const exportUsersCSV: AppRequestHandler = async (_req, res) => {
+export const exportBus: AppRequestHandler = async (_req, res) => {
try {
- await export_service.exportUsersToCSV();
- Ok(res, { msg: 'CSV des bus généré' });
+ const exportData = await export_service.exportBus();
+ res.setHeader('Content-Type', 'application/json');
+ res.setHeader('Content-Disposition', `attachment; filename="bus_${Date.now()}.json"`);
+
+ return res.status(200).json(exportData);
} catch (error) {
console.error(error);
- Error(res, { msg: "Erreur lors de l'export CSV" });
+ return Error(res, { msg: "Erreur lors de l'exportation des memsbres des équipes : " + error });
}
};
-export const exportTeamMembersCSV: AppRequestHandler = async (_req, res) => {
+export const exportTeamMembers: AppRequestHandler = async (_req, res) => {
try {
- await export_service.exportTeamMembersToCSV();
- Ok(res, { msg: "CSV des membres de l'équipe généré" });
+ const exportData = await export_service.exportTeamMembers();
+ res.setHeader('Content-Type', 'application/json');
+ res.setHeader('Content-Disposition', `attachment; filename="team_members_${Date.now()}.json"`);
+
+ return res.status(200).json(exportData);
} catch (error) {
console.error(error);
- Error(res, { msg: "Erreur lors de l'export CSV" });
+ return Error(res, { msg: "Erreur lors de l'exportation des memsbres des équipes : " + error });
}
};
export const getUploadedDocumentStatus: AppRequestHandler = async (
diff --git a/backend/src/controllers/maker_battle.controller.ts b/backend/src/controllers/maker_battle.controller.ts
new file mode 100644
index 0000000..cdf6afe
--- /dev/null
+++ b/backend/src/controllers/maker_battle.controller.ts
@@ -0,0 +1,61 @@
+import * as maker_battle_service from '../services/maker_battle.service';
+import type { AppRequestHandler } from '../types/http';
+import { Error, Ok } from '../shared/http/responses';
+import type { Group, GroupsList } from '../dto/maker_battle.dto';
+
+export const distributeGroups: AppRequestHandler = async (req, res) => {
+ const groups = req.body.groups;
+ if (!groups || !Array.isArray(groups)) {
+ return Error(res, { msg: 'Invalid request: groups must be an array' });
+ }
+
+ try {
+ for (const group of groups) {
+ await maker_battle_service.distributeGroups(group);
+ }
+ await maker_battle_service.placeTeamsOnTables(groups);
+
+ Ok(res, { data: { success: true, message: 'Groups allocated successfully' } });
+ return;
+ } catch (err) {
+ return Error(res, { msg: 'Erreur lors de la distribution des groupes : ' + err });
+ }
+};
+
+export const exportGroups: AppRequestHandler = async (req, res) => {
+ const { group } = req.params;
+ if (!group || typeof group !== 'string') {
+ return Error(res, { msg: 'Invalid request: group must be a string' });
+ }
+
+ try {
+ const exportData = await maker_battle_service.exportGroups(group);
+
+ res.setHeader('Content-Type', 'application/json');
+ res.setHeader('Content-Disposition', `attachment; filename="maker_battle_${group}_${Date.now()}.json"`);
+
+ Ok(res, { data: exportData });
+ return;
+ } catch (err) {
+ return Error(res, { msg: "Erreur lors de l'exportation des groupes : " + err });
+ }
+};
+
+export const getCurrentUser: AppRequestHandler = async (req, res) => {
+ try {
+ const userId = req.user.userId;
+
+ if (!userId) {
+ return Error(res, { msg: 'User not authenticated' });
+ }
+
+ const userGroup = await maker_battle_service.getUserTeam(userId);
+
+ Ok(res, { data: { group: userGroup ?? null } });
+ return;
+ } catch (err) {
+ return Error(res, {
+ msg: "Erreur lors de la récupération du groupe de l'utilisateur : " + err,
+ });
+ }
+};
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/database/migrations/0028_tan_slapstick.sql b/backend/src/database/migrations/0028_tan_slapstick.sql
new file mode 100644
index 0000000..e907d3f
--- /dev/null
+++ b/backend/src/database/migrations/0028_tan_slapstick.sql
@@ -0,0 +1,9 @@
+CREATE TABLE "maker_battle_attribution" (
+ "user_id" integer PRIMARY KEY NOT NULL,
+ "maker_team_id" integer NOT NULL,
+ "faction_id" integer NOT NULL,
+ "table" integer,
+ "group" text NOT NULL
+);
+--> statement-breakpoint
+ALTER TABLE "maker_battle_attribution" ADD CONSTRAINT "maker_battle_attribution_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
\ No newline at end of file
diff --git a/backend/src/database/migrations/meta/0028_snapshot.json b/backend/src/database/migrations/meta/0028_snapshot.json
new file mode 100644
index 0000000..8d77e1b
--- /dev/null
+++ b/backend/src/database/migrations/meta/0028_snapshot.json
@@ -0,0 +1,1566 @@
+{
+ "id": "affda32f-de8a-46ec-ab02-7425eb922c0b",
+ "prevId": "6179fb88-23bd-4656-9980-faa52a9d9b24",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.banned_addresses": {
+ "name": "banned_addresses",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "banned_addresses_email_unique": {
+ "name": "banned_addresses_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "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
+ },
+ "socialLink": {
+ "name": "socialLink",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "riCompatible": {
+ "name": "riCompatible",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 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
+ },
+ "male": {
+ "name": "male",
+ "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()"
+ },
+ "vss_form": {
+ "name": "vss_form",
+ "type": "vss_form",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'pending'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vssqcmquestion": {
+ "name": "vssqcmquestion",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "question": {
+ "name": "question",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "question_en": {
+ "name": "question_en",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "points": {
+ "name": "points",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "question_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "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.maker_battle_attribution": {
+ "name": "maker_battle_attribution",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "maker_team_id": {
+ "name": "maker_team_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "faction_id": {
+ "name": "faction_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "table": {
+ "name": "table",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "group": {
+ "name": "group",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "maker_battle_attribution_user_id_users_id_fk": {
+ "name": "maker_battle_attribution_user_id_users_id_fk",
+ "tableFrom": "maker_battle_attribution",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_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.user_informations": {
+ "name": "user_informations",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "emergency_contact_name": {
+ "name": "emergency_contact_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "emergency_contact_phone": {
+ "name": "emergency_contact_phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_informations_user_id_users_id_fk": {
+ "name": "user_informations_user_id_users_id_fk",
+ "tableFrom": "user_informations",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "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
+ },
+ "public.vssqcmanswer": {
+ "name": "vssqcmanswer",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "questionid": {
+ "name": "questionid",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "answer": {
+ "name": "answer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "answer_en": {
+ "name": "answer_en",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_correct": {
+ "name": "is_correct",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "vssqcmanswer_questionid_vssqcmquestion_id_fk": {
+ "name": "vssqcmanswer_questionid_vssqcmquestion_id_fk",
+ "tableFrom": "vssqcmanswer",
+ "tableTo": "vssqcmquestion",
+ "columnsFrom": [
+ "questionid"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.vss_form": {
+ "name": "vss_form",
+ "schema": "public",
+ "values": [
+ "pending",
+ "toretry",
+ "validated",
+ "rejected"
+ ]
+ },
+ "public.question_type": {
+ "name": "question_type",
+ "schema": "public",
+ "values": [
+ "single_choice",
+ "multiple_choice"
+ ]
+ }
+ },
+ "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 3424d95..62fbd6f 100644
--- a/backend/src/database/migrations/meta/_journal.json
+++ b/backend/src/database/migrations/meta/_journal.json
@@ -1,202 +1,209 @@
{
- "version": "7",
- "dialect": "postgresql",
- "entries": [
- {
- "idx": 0,
- "version": "7",
- "when": 1743857362115,
- "tag": "0000_workable_rafael_vega",
- "breakpoints": true
- },
- {
- "idx": 1,
- "version": "7",
- "when": 1743868246465,
- "tag": "0001_stormy_quicksilver",
- "breakpoints": true
- },
- {
- "idx": 2,
- "version": "7",
- "when": 1743897931065,
- "tag": "0002_luxuriant_lockjaw",
- "breakpoints": true
- },
- {
- "idx": 3,
- "version": "7",
- "when": 1743979532382,
- "tag": "0003_material_lorna_dane",
- "breakpoints": true
- },
- {
- "idx": 4,
- "version": "7",
- "when": 1744064732443,
- "tag": "0004_careless_misty_knight",
- "breakpoints": true
- },
- {
- "idx": 5,
- "version": "7",
- "when": 1744118020611,
- "tag": "0005_needy_darwin",
- "breakpoints": true
- },
- {
- "idx": 6,
- "version": "7",
- "when": 1744126517418,
- "tag": "0006_bright_boomerang",
- "breakpoints": true
- },
- {
- "idx": 7,
- "version": "7",
- "when": 1744212319949,
- "tag": "0007_striped_mulholland_black",
- "breakpoints": true
- },
- {
- "idx": 8,
- "version": "7",
- "when": 1744215076184,
- "tag": "0008_striped_bushwacker",
- "breakpoints": true
- },
- {
- "idx": 9,
- "version": "7",
- "when": 1744241202940,
- "tag": "0009_previous_mystique",
- "breakpoints": true
- },
- {
- "idx": 10,
- "version": "7",
- "when": 1744241242502,
- "tag": "0010_fair_mulholland_black",
- "breakpoints": true
- },
- {
- "idx": 11,
- "version": "7",
- "when": 1744241259194,
- "tag": "0011_fearless_nextwave",
- "breakpoints": true
- },
- {
- "idx": 12,
- "version": "7",
- "when": 1746040673007,
- "tag": "0012_productive_giant_man",
- "breakpoints": true
- },
- {
- "idx": 13,
- "version": "7",
- "when": 1752757642698,
- "tag": "0013_curly_angel",
- "breakpoints": true
- },
- {
- "idx": 14,
- "version": "7",
- "when": 1753638076374,
- "tag": "0014_special_reaper",
- "breakpoints": true
- },
- {
- "idx": 15,
- "version": "7",
- "when": 1753744481956,
- "tag": "0015_greedy_genesis",
- "breakpoints": true
- },
- {
- "idx": 16,
- "version": "7",
- "when": 1754903172897,
- "tag": "0016_sudden_ultimatum",
- "breakpoints": true
- },
- {
- "idx": 17,
- "version": "7",
- "when": 1755637642205,
- "tag": "0017_melted_meggan",
- "breakpoints": true
- },
- {
- "idx": 18,
- "version": "7",
- "when": 1755858317109,
- "tag": "0018_puzzling_arachne",
- "breakpoints": true
- },
- {
- "idx": 19,
- "version": "7",
- "when": 1755906116757,
- "tag": "0019_complete_moondragon",
- "breakpoints": true
- },
- {
- "idx": 20,
- "version": "7",
- "when": 1755907709198,
- "tag": "0020_strange_colonel_america",
- "breakpoints": true
- },
- {
- "idx": 21,
- "version": "7",
- "when": 1756063134903,
- "tag": "0021_colossal_madame_web",
- "breakpoints": true
- },
- {
- "idx": 22,
- "version": "7",
- "when": 1757002717384,
- "tag": "0022_light_omega_red",
- "breakpoints": true
- },
- {
- "idx": 23,
- "version": "7",
- "when": 1782943789540,
- "tag": "0023_zippy_colonel_america",
- "breakpoints": true
- },
- {
- "idx": 24,
- "version": "7",
- "when": 1783776285772,
- "tag": "0024_optimal_garia",
- "breakpoints": true
- },
- {
- "idx": 25,
- "version": "7",
- "when": 1784742510283,
- "tag": "0025_perfect_ulik",
- "breakpoints": true
- },
- {
- "idx": 26,
- "version": "7",
- "when": 1784824629918,
- "tag": "0026_swift_excalibur",
- "breakpoints": true
- },
- {
- "idx": 27,
- "version": "7",
- "when": 1785000000000,
- "tag": "0027_faulty_hellion",
- "breakpoints": true
- }
- ]
-}
+ "version": "7",
+ "dialect": "postgresql",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "7",
+ "when": 1743857362115,
+ "tag": "0000_workable_rafael_vega",
+ "breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "7",
+ "when": 1743868246465,
+ "tag": "0001_stormy_quicksilver",
+ "breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "7",
+ "when": 1743897931065,
+ "tag": "0002_luxuriant_lockjaw",
+ "breakpoints": true
+ },
+ {
+ "idx": 3,
+ "version": "7",
+ "when": 1743979532382,
+ "tag": "0003_material_lorna_dane",
+ "breakpoints": true
+ },
+ {
+ "idx": 4,
+ "version": "7",
+ "when": 1744064732443,
+ "tag": "0004_careless_misty_knight",
+ "breakpoints": true
+ },
+ {
+ "idx": 5,
+ "version": "7",
+ "when": 1744118020611,
+ "tag": "0005_needy_darwin",
+ "breakpoints": true
+ },
+ {
+ "idx": 6,
+ "version": "7",
+ "when": 1744126517418,
+ "tag": "0006_bright_boomerang",
+ "breakpoints": true
+ },
+ {
+ "idx": 7,
+ "version": "7",
+ "when": 1744212319949,
+ "tag": "0007_striped_mulholland_black",
+ "breakpoints": true
+ },
+ {
+ "idx": 8,
+ "version": "7",
+ "when": 1744215076184,
+ "tag": "0008_striped_bushwacker",
+ "breakpoints": true
+ },
+ {
+ "idx": 9,
+ "version": "7",
+ "when": 1744241202940,
+ "tag": "0009_previous_mystique",
+ "breakpoints": true
+ },
+ {
+ "idx": 10,
+ "version": "7",
+ "when": 1744241242502,
+ "tag": "0010_fair_mulholland_black",
+ "breakpoints": true
+ },
+ {
+ "idx": 11,
+ "version": "7",
+ "when": 1744241259194,
+ "tag": "0011_fearless_nextwave",
+ "breakpoints": true
+ },
+ {
+ "idx": 12,
+ "version": "7",
+ "when": 1746040673007,
+ "tag": "0012_productive_giant_man",
+ "breakpoints": true
+ },
+ {
+ "idx": 13,
+ "version": "7",
+ "when": 1752757642698,
+ "tag": "0013_curly_angel",
+ "breakpoints": true
+ },
+ {
+ "idx": 14,
+ "version": "7",
+ "when": 1753638076374,
+ "tag": "0014_special_reaper",
+ "breakpoints": true
+ },
+ {
+ "idx": 15,
+ "version": "7",
+ "when": 1753744481956,
+ "tag": "0015_greedy_genesis",
+ "breakpoints": true
+ },
+ {
+ "idx": 16,
+ "version": "7",
+ "when": 1754903172897,
+ "tag": "0016_sudden_ultimatum",
+ "breakpoints": true
+ },
+ {
+ "idx": 17,
+ "version": "7",
+ "when": 1755637642205,
+ "tag": "0017_melted_meggan",
+ "breakpoints": true
+ },
+ {
+ "idx": 18,
+ "version": "7",
+ "when": 1755858317109,
+ "tag": "0018_puzzling_arachne",
+ "breakpoints": true
+ },
+ {
+ "idx": 19,
+ "version": "7",
+ "when": 1755906116757,
+ "tag": "0019_complete_moondragon",
+ "breakpoints": true
+ },
+ {
+ "idx": 20,
+ "version": "7",
+ "when": 1755907709198,
+ "tag": "0020_strange_colonel_america",
+ "breakpoints": true
+ },
+ {
+ "idx": 21,
+ "version": "7",
+ "when": 1756063134903,
+ "tag": "0021_colossal_madame_web",
+ "breakpoints": true
+ },
+ {
+ "idx": 22,
+ "version": "7",
+ "when": 1757002717384,
+ "tag": "0022_light_omega_red",
+ "breakpoints": true
+ },
+ {
+ "idx": 23,
+ "version": "7",
+ "when": 1782943789540,
+ "tag": "0023_zippy_colonel_america",
+ "breakpoints": true
+ },
+ {
+ "idx": 24,
+ "version": "7",
+ "when": 1783776285772,
+ "tag": "0024_optimal_garia",
+ "breakpoints": true
+ },
+ {
+ "idx": 25,
+ "version": "7",
+ "when": 1784742510283,
+ "tag": "0025_perfect_ulik",
+ "breakpoints": true
+ },
+ {
+ "idx": 26,
+ "version": "7",
+ "when": 1784824629918,
+ "tag": "0026_swift_excalibur",
+ "breakpoints": true
+ },
+ {
+ "idx": 27,
+ "version": "7",
+ "when": 1785000000000,
+ "tag": "0027_faulty_hellion",
+ "breakpoints": true
+ },
+ {
+ "idx": 28,
+ "version": "7",
+ "when": 1788026112577,
+ "tag": "0028_tan_slapstick",
+ "breakpoints": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/backend/src/dto/battle_maker.dto.ts b/backend/src/dto/battle_maker.dto.ts
new file mode 100644
index 0000000..a66483f
--- /dev/null
+++ b/backend/src/dto/battle_maker.dto.ts
@@ -0,0 +1,3 @@
+export interface BattleMakerDTO {
+ groups: string[];
+}
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/dto/maker_battle.dto.ts b/backend/src/dto/maker_battle.dto.ts
new file mode 100644
index 0000000..e8c7b0c
--- /dev/null
+++ b/backend/src/dto/maker_battle.dto.ts
@@ -0,0 +1,6 @@
+export interface GroupsList {
+ groups: string[];
+}
+export interface Group {
+ group: string;
+}
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/im_export.routes.ts b/backend/src/routes/im_export.routes.ts
index 7dbb04a..71cfeec 100644
--- a/backend/src/routes/im_export.routes.ts
+++ b/backend/src/routes/im_export.routes.ts
@@ -15,8 +15,8 @@ imexportRouter.post(
);
imexportRouter.post('/admin/exportgsheet', checkRole('Admin', []), imexportController.exportAllDataToSheets);
-imexportRouter.get('/admin/exportbus', checkRole('Admin', []), imexportController.exportUsersCSV);
-imexportRouter.get('/admin/exportteammembers', checkRole('Admin', []), imexportController.exportTeamMembersCSV);
+imexportRouter.get('/admin/exportbus', checkRole('Admin', []), imexportController.exportBus);
+imexportRouter.get('/admin/exportteammembers', checkRole('Admin', []), imexportController.exportTeamMembers);
imexportRouter.get(
'/admin/document/:category/:item',
checkRole('Admin', []),
diff --git a/backend/src/routes/maker_battle.routes.ts b/backend/src/routes/maker_battle.routes.ts
new file mode 100644
index 0000000..d4f3f8f
--- /dev/null
+++ b/backend/src/routes/maker_battle.routes.ts
@@ -0,0 +1,10 @@
+import { Router } from 'express';
+import * as makerBattleController from '../controllers/maker_battle.controller';
+import { checkRole } from '../middlewares/user.middleware';
+
+const makerBattleRouter = Router();
+makerBattleRouter.post('/admin/allocate', checkRole('Admin', ['Défis TC']), makerBattleController.distributeGroups);
+makerBattleRouter.get('/admin/export/:group', checkRole('Admin', ['Défis TC']), makerBattleController.exportGroups);
+
+makerBattleRouter.get('/group/me', makerBattleController.getCurrentUser);
+export default makerBattleRouter;
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/schemas/Relational/makerbattletribution.schema.ts b/backend/src/schemas/Relational/makerbattletribution.schema.ts
new file mode 100644
index 0000000..b898937
--- /dev/null
+++ b/backend/src/schemas/Relational/makerbattletribution.schema.ts
@@ -0,0 +1,15 @@
+import { integer, pgTable, text } from 'drizzle-orm/pg-core';
+import { userSchema } from '../Basic/user.schema';
+
+export const MakerBattleAttributionSchema = pgTable('maker_battle_attribution', {
+ user_id: integer('user_id')
+ .primaryKey()
+ .notNull()
+ .references(() => userSchema.id),
+ maker_team_id: integer('maker_team_id').notNull(),
+ faction_id: integer('faction_id').notNull(),
+ table: integer('table'),
+ group: text('group').notNull(),
+});
+
+export type MakerBattleAttributionSchema = typeof MakerBattleAttributionSchema.$inferInsert;
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/im_export.service.ts b/backend/src/services/im_export.service.ts
index 8f46705..5a99f42 100644
--- a/backend/src/services/im_export.service.ts
+++ b/backend/src/services/im_export.service.ts
@@ -1,10 +1,29 @@
-import { existsSync, mkdirSync, writeFileSync } from 'fs';
import { google } from 'googleapis';
-import { parse } from 'json2csv';
import * as user_service from './user.service';
import path from 'path';
+export interface teamMemberUser {
+ prenom: string;
+ nom: string;
+ ce: boolean;
+ equipe: string | null;
+}
+
+export interface busUser {
+ id: number;
+ prenom: string;
+ nom: string;
+ mail: string;
+ telephone: string;
+ nouveau: boolean;
+ ce: boolean;
+ num_equipe: number | null;
+ benevole: boolean;
+ orga: boolean;
+ majeur: boolean;
+}
+
const keyFilePath = path.resolve(__dirname, '../utils/google_credentials.json');
// Crée une instance JWT en utilisant la clé du service account
@@ -36,7 +55,7 @@ export const writeToGoogleSheet = async (spreadsheetId: string, range: string, v
}
};
-export const exportUsersToCSV = async (): Promise => {
+export const exportBus = async (): Promise => {
const users = await user_service.getUsersAll();
const formattedUsers = users.map((u) => {
@@ -59,39 +78,13 @@ export const exportUsersToCSV = async (): Promise => {
};
});
- const csv = parse(formattedUsers, {
- fields: [
- 'id',
- 'prenom',
- 'nom',
- 'mail',
- 'telephone',
- 'nouveau',
- 'ce',
- 'num_equipe',
- 'benevole',
- 'orga',
- 'majeur',
- 'bus_manual',
- ],
- });
-
- const exportDir = path.join(__dirname, '../../exports/bus');
- const filePath = path.join(exportDir, 'bus.csv');
-
- if (!existsSync(exportDir)) {
- mkdirSync(exportDir, { recursive: true });
- }
-
- writeFileSync(filePath, csv);
-
- return filePath;
+ return formattedUsers;
};
-export const exportTeamMembersToCSV = async (): Promise => {
- const users = await user_service.getUsersAll();
+export const exportTeamMembers = async (): Promise => {
+ const usersInTeams = (await user_service.getUsersAll()).filter((u) => u.teamId !== null);
- const formattedUsers = users.map((u) => {
+ const formattedUsers = usersInTeams.map((u) => {
const isCE = u.permission === 'Student' && u.teamId !== null;
return {
prenom: u.first_name ?? '',
@@ -101,19 +94,5 @@ export const exportTeamMembersToCSV = async (): Promise => {
};
});
- const csv = parse(formattedUsers, {
- fields: ['prenom', 'nom', 'ce', 'equipe'],
- });
-
- const exportDir = path.join(__dirname, '../../exports/teammembers');
- const filePath = path.join(exportDir, 'teammembers.csv');
- console.log('Exporting team members to:', filePath);
-
- if (!existsSync(exportDir)) {
- mkdirSync(exportDir, { recursive: true });
- }
-
- writeFileSync(filePath, csv);
-
- return filePath;
+ return formattedUsers;
};
diff --git a/backend/src/services/maker_battle.service.ts b/backend/src/services/maker_battle.service.ts
new file mode 100644
index 0000000..83cd5d9
--- /dev/null
+++ b/backend/src/services/maker_battle.service.ts
@@ -0,0 +1,365 @@
+import { and, eq, inArray, not, or, sql } from 'drizzle-orm';
+import { db } from '../database/db';
+import { userSchema } from '../schemas/Basic/user.schema';
+import { userTeamsSchema } from '../schemas/Relational/userteams.schema';
+import { teamSchema } from '../schemas/Basic/team.schema';
+import { teamFactionSchema } from '../schemas/Relational/teamfaction.schema';
+import { MakerBattleAttributionSchema } from '../schemas/Relational/makerbattletribution.schema';
+
+export interface UserWithTeamFaction {
+ user_id: number;
+ group: string;
+ team_id: number;
+ faction_id: number;
+}
+
+export interface TeamsWithGroup {
+ maker_team_id: number;
+ group: string;
+}
+
+export interface MakerBattleExport {
+ maker_team_id: number;
+ table: number;
+}
+
+const TEAM_SIZE = 6;
+const MAX_PLACEMENT_ATTEMPTS = 20;
+
+export const distributeGroups = async (group: string): Promise => {
+ let usersWithTeamsFactions = [];
+
+ if (group === 'tc') {
+ usersWithTeamsFactions = await db
+ .select({
+ user_id: userSchema.id,
+ team_id: userTeamsSchema.team_id,
+ faction_id: teamFactionSchema.faction_id,
+ })
+ .from(userSchema)
+ .innerJoin(userTeamsSchema, eq(userSchema.id, userTeamsSchema.user_id))
+ .innerJoin(teamSchema, eq(userTeamsSchema.team_id, teamSchema.id))
+ .innerJoin(teamFactionSchema, eq(teamSchema.id, teamFactionSchema.team_id))
+ .where(
+ and(
+ eq(userSchema.permission, 'Nouveau'),
+ or(eq(userSchema.branch, 'TC'), eq(userSchema.branch, 'IA_BACH')),
+ ),
+ );
+ usersWithTeamsFactions = usersWithTeamsFactions.map((user) => ({ ...user, group: 'tc' }));
+ } else if (group === 'ri') {
+ usersWithTeamsFactions = await db
+ .select({
+ user_id: userSchema.id,
+ team_id: userTeamsSchema.team_id,
+ faction_id: teamFactionSchema.faction_id,
+ })
+ .from(userSchema)
+ .innerJoin(userTeamsSchema, eq(userSchema.id, userTeamsSchema.user_id))
+ .innerJoin(teamSchema, eq(userTeamsSchema.team_id, teamSchema.id))
+ .innerJoin(teamFactionSchema, eq(teamSchema.id, teamFactionSchema.team_id))
+ .where(and(eq(userSchema.permission, 'Nouveau'), eq(userSchema.branch, 'RI')));
+ usersWithTeamsFactions = usersWithTeamsFactions.map((user) => ({ ...user, group: 'ri' }));
+ } else if (group === 'branch') {
+ usersWithTeamsFactions = await db
+ .select({
+ user_id: userSchema.id,
+ team_id: userTeamsSchema.team_id,
+ faction_id: teamFactionSchema.faction_id,
+ })
+ .from(userSchema)
+ .innerJoin(userTeamsSchema, eq(userSchema.id, userTeamsSchema.user_id))
+ .innerJoin(teamSchema, eq(userTeamsSchema.team_id, teamSchema.id))
+ .innerJoin(teamFactionSchema, eq(teamSchema.id, teamFactionSchema.team_id))
+ .where(
+ and(
+ eq(userSchema.permission, 'Nouveau'),
+ not(
+ or(
+ eq(userSchema.branch, 'RI'),
+ eq(userSchema.branch, 'TC'),
+ eq(userSchema.branch, 'IA_BACH'),
+ eq(userSchema.branch, 'MM'),
+ ),
+ ),
+ ),
+ );
+ usersWithTeamsFactions = usersWithTeamsFactions.map((user) => ({ ...user, group: 'branch' }));
+ } else {
+ throw new Error(`Unknown group: ${group}`);
+ }
+
+ if (usersWithTeamsFactions.length === 0) {
+ return;
+ }
+
+ const usersByFaction = new Map();
+ for (const user of usersWithTeamsFactions) {
+ if (!usersByFaction.has(user.faction_id)) {
+ usersByFaction.set(user.faction_id, []);
+ }
+ usersByFaction.get(user.faction_id)!.push(user);
+ }
+
+ let numberOfTeams = 0;
+ for (const factionUsers of usersByFaction.values()) {
+ const neededForThisFaction = Math.ceil(factionUsers.length / TEAM_SIZE);
+ if (neededForThisFaction > numberOfTeams) {
+ numberOfTeams = neededForThisFaction;
+ }
+ }
+
+ if (numberOfTeams === 0) {
+ return;
+ }
+
+ const allTeams: UserWithTeamFaction[][] = [];
+ for (const [, factionUsers] of usersByFaction) {
+ const teamsForFaction = generateTeamsWithRetry(factionUsers, numberOfTeams, MAX_PLACEMENT_ATTEMPTS);
+ allTeams.push(...teamsForFaction);
+ }
+
+ const rows: MakerBattleAttributionSchema[] = [];
+ let globalTeamId = 1;
+
+ for (const team of allTeams) {
+ for (const user of team) {
+ rows.push({
+ user_id: user.user_id,
+ maker_team_id: globalTeamId,
+ table: null,
+ faction_id: user.faction_id,
+ group: user.group,
+ });
+ }
+ globalTeamId++;
+ }
+
+ if (rows.length > 0) {
+ await db
+ .insert(MakerBattleAttributionSchema)
+ .values(rows)
+ .onConflictDoUpdate({
+ target: MakerBattleAttributionSchema.user_id,
+ set: {
+ maker_team_id: sql`excluded.maker_team_id`,
+ table: sql`excluded.table`,
+ faction_id: sql`excluded.faction_id`,
+ group: sql`excluded.group`,
+ },
+ });
+ }
+
+ return;
+};
+
+const generateTeamsWithRetry = (
+ users: UserWithTeamFaction[],
+ numberOfTeams: number,
+ maxAttempts = MAX_PLACEMENT_ATTEMPTS,
+): UserWithTeamFaction[][] => {
+ let lastError: unknown;
+
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
+ try {
+ const result = generateTeams(users, numberOfTeams);
+ return result;
+ } catch (err) {
+ lastError = err;
+ }
+ }
+
+ throw new Error(
+ `Impossible de générer les équipes après ${maxAttempts} tentatives. ` +
+ `Dernière erreur : ${lastError instanceof Error ? lastError.message : String(lastError)}`,
+ );
+};
+
+export const generateTeams = (users: UserWithTeamFaction[], numberOfTeams: number): UserWithTeamFaction[][] => {
+ const numberOfSixTeams = users.length - numberOfTeams * 5;
+ const capacities = [...Array(numberOfSixTeams).fill(6), ...Array(numberOfTeams - numberOfSixTeams).fill(5)];
+
+ const shuffle = (array: T[]): T[] => {
+ const result = [...array];
+ for (let i = result.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [result[i], result[j]] = [result[j], result[i]];
+ }
+ return result;
+ };
+
+ const usersByTeamId = new Map();
+ for (const user of users) {
+ if (!usersByTeamId.has(user.team_id)) {
+ usersByTeamId.set(user.team_id, []);
+ }
+ usersByTeamId.get(user.team_id)!.push(user);
+ }
+
+ for (const [teamId, members] of usersByTeamId) {
+ usersByTeamId.set(teamId, shuffle(members));
+ }
+
+ const teams: UserWithTeamFaction[][] = capacities.map(() => []);
+
+ let placedAllUsers = false;
+ let attemptCount = 0;
+ const maxAttempts = 100;
+
+ while (!placedAllUsers && attemptCount < maxAttempts) {
+ attemptCount++;
+
+ for (let i = 0; i < teams.length; i++) {
+ teams[i] = [];
+ }
+
+ const allUsers = shuffle([...users]);
+
+ let allPlaced = true;
+ for (const user of allUsers) {
+ let placed = false;
+
+ const shuffledTeamIndices = shuffle([...Array(numberOfTeams).keys()]);
+
+ for (const teamIndex of shuffledTeamIndices) {
+ if (teams[teamIndex].length < capacities[teamIndex]) {
+ const hasSameTeam = teams[teamIndex].some((u) => u.team_id === user.team_id);
+ if (!hasSameTeam) {
+ teams[teamIndex].push(user);
+ placed = true;
+ break;
+ }
+ }
+ }
+
+ if (!placed) {
+ for (const teamIndex of shuffledTeamIndices) {
+ if (teams[teamIndex].length < capacities[teamIndex]) {
+ teams[teamIndex].push(user);
+ placed = true;
+ break;
+ }
+ }
+ }
+
+ if (!placed) {
+ allPlaced = false;
+ break;
+ }
+ }
+
+ if (allPlaced) {
+ placedAllUsers = true;
+ }
+ }
+
+ if (!placedAllUsers) {
+ throw new Error(`Could not place all users after ${maxAttempts} attempts`);
+ }
+
+ return teams;
+};
+
+export const placeTeamsOnTables = async (groups: string[]): Promise => {
+ await db
+ .update(MakerBattleAttributionSchema)
+ .set({ table: null })
+ .where(inArray(MakerBattleAttributionSchema.group, groups));
+
+ const teams = await db
+ .selectDistinct({
+ maker_team_id: MakerBattleAttributionSchema.maker_team_id,
+ group: MakerBattleAttributionSchema.group,
+ faction_id: MakerBattleAttributionSchema.faction_id,
+ })
+ .from(MakerBattleAttributionSchema)
+ .where(inArray(MakerBattleAttributionSchema.group, groups));
+ if (teams.length === 0) return;
+
+ const groupsMap = new Map();
+ for (const t of teams) {
+ if (!groupsMap.has(t.group)) groupsMap.set(t.group, []);
+ groupsMap.get(t.group).push(t);
+ }
+
+ const groupOrder = ['ri', 'branch', 'tc'];
+ const sortedGroups = groups.sort((a, b) => {
+ return groupOrder.indexOf(a) - groupOrder.indexOf(b);
+ });
+
+ const assignments = [];
+ let tableNum = 0;
+
+ for (const group of sortedGroups) {
+ const groupTeams = groupsMap.get(group) || [];
+ if (groupTeams.length === 0) continue;
+
+ const factions = new Map();
+ for (const t of groupTeams) {
+ if (!factions.has(t.faction_id)) factions.set(t.faction_id, []);
+ factions.get(t.faction_id).push(t);
+ }
+
+ const factionIds = Array.from(factions.keys());
+ const maxLen = Math.max(...Array.from(factions.values()).map((arr) => arr.length));
+
+ for (let i = 0; i < maxLen; i++) {
+ for (const fid of factionIds) {
+ const fTeams = factions.get(fid);
+ if (i < fTeams.length) {
+ tableNum++;
+ assignments.push({
+ maker_team_id: fTeams[i].maker_team_id,
+ faction_id: fTeams[i].faction_id,
+ group,
+ table: tableNum,
+ });
+ }
+ }
+ }
+ }
+
+ for (const assignment of assignments) {
+ await db
+ .update(MakerBattleAttributionSchema)
+ .set({
+ table: assignment.table,
+ })
+ .where(
+ and(
+ eq(MakerBattleAttributionSchema.maker_team_id, assignment.maker_team_id),
+ eq(MakerBattleAttributionSchema.group, assignment.group),
+ ),
+ );
+ }
+};
+
+export const getUserTeam = async (userId: number): Promise<{ team_id: number; table: number } | undefined> => {
+ const result = await db
+ .select({
+ team_id: MakerBattleAttributionSchema.maker_team_id,
+ table: MakerBattleAttributionSchema.table,
+ })
+ .from(MakerBattleAttributionSchema)
+ .where(eq(MakerBattleAttributionSchema.user_id, userId));
+
+ return result[0];
+};
+
+export const exportGroups = async (group: string): Promise => {
+ const teams = await db
+ .selectDistinct({
+ first_name: userSchema.first_name,
+ last_name: userSchema.last_name,
+ user_id: MakerBattleAttributionSchema.user_id,
+ group: MakerBattleAttributionSchema.group,
+ faction_id: MakerBattleAttributionSchema.faction_id,
+ maker_team_id: MakerBattleAttributionSchema.maker_team_id,
+ table: MakerBattleAttributionSchema.table,
+ })
+ .from(MakerBattleAttributionSchema)
+ .innerJoin(userSchema, eq(MakerBattleAttributionSchema.user_id, userSchema.id))
+ .where(eq(MakerBattleAttributionSchema.group, group));
+
+ return teams;
+};
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/backend/src/services/user.service.ts b/backend/src/services/user.service.ts
index 6737f05..d1c7c90 100644
--- a/backend/src/services/user.service.ts
+++ b/backend/src/services/user.service.ts
@@ -18,6 +18,7 @@ import { userInformationSchema } from '../schemas/Relational/userinformation.sch
import { addUserToRespondentStudentsList } from '../shared/integrations/billetweb';
import { generateEmailHtml, sendEmail } from './email.service';
import { email_from } from '../shared/secrets/secrets';
+import { MakerBattleAttributionSchema } from '../schemas/Relational/makerbattletribution.schema';
// Fonction pour récupérer un utilisateur par email
export const getUserByEmail = async (email: string) => {
@@ -206,8 +207,11 @@ export const getUsersAdmin = async () => {
contact: userSchema.contact,
permission: userSchema.permission,
discord_id: userSchema.discord_id,
+ maker_battle_table: MakerBattleAttributionSchema.table,
+ maker_battle_team: MakerBattleAttributionSchema.maker_team_id,
})
- .from(userSchema);
+ .from(userSchema)
+ .leftJoin(MakerBattleAttributionSchema, eq(userSchema.id, MakerBattleAttributionSchema.user_id));
return users;
} catch (err) {
console.error('Erreur lors de la récupération des utilisateurs ', err);
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index f5b8a1e..ab6a1e8 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -22,6 +22,8 @@ ARG VITE_BDE_SIRET=""
ARG VITE_BDE_SIREN=""
ARG VITE_DPO_EMAIL=""
ARG VITE_DPO_NAME=""
+ARG VITE_APP_VERSION=""
+ARG VITE_RELEASE_URL=""
ENV VITE_CAS_LOGIN_URL=${VITE_CAS_LOGIN_URL}
ENV VITE_SERVICE_URL=${VITE_SERVICE_URL}
@@ -44,6 +46,8 @@ ENV VITE_BDE_SIRET=${VITE_BDE_SIRET}
ENV VITE_BDE_SIREN=${VITE_BDE_SIREN}
ENV VITE_DPO_EMAIL=${VITE_DPO_EMAIL}
ENV VITE_DPO_NAME=${VITE_DPO_NAME}
+ENV VITE_APP_VERSION=${VITE_APP_VERSION}
+ENV VITE_RELEASE_URL=${VITE_RELEASE_URL}
# COPY package.json package-lock.json ./
# RUN npm install -g npm@latest
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 46b0562..8559d9f 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -7,12 +7,13 @@ import ProtectedRoute from './components/utils/protectedroute';
import { OnboardingProvider } from './contexts/onboarding';
import { PermanencesProvider } from './contexts/permanences';
import { UserProvider } from './contexts/user';
+import AdminPageMakerBattle from './pages/admin/adminMakerBattle';
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 +265,10 @@ const App: React.FC = () => {
}
/>
-
+
}
/>
@@ -327,6 +328,14 @@ const App: React.FC = () => {
}
/>
+
+
+
+ }
+ />
{/* Fallback */}
} />
diff --git a/frontend/src/components/Admin/AdminMakerBattle/adminMakerBattleDownload.tsx b/frontend/src/components/Admin/AdminMakerBattle/adminMakerBattleDownload.tsx
new file mode 100644
index 0000000..94204fa
--- /dev/null
+++ b/frontend/src/components/Admin/AdminMakerBattle/adminMakerBattleDownload.tsx
@@ -0,0 +1,84 @@
+import { useState } from 'react';
+import Select from 'react-select';
+import Swal from 'sweetalert2';
+
+import { type MakerBattleGroupTypeOption } from '../../../interfaces/maker_battle.interface';
+import { fetchExportData } from '../../../services/requests/maker_battle.service';
+import { downloadJsonAsCsv } from '../../../utils/utils';
+import { Button } from '../../ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card';
+
+type Props = {
+ groupTypeOptions: MakerBattleGroupTypeOption[];
+};
+
+export const AdminMakerBattleTeamDownload = ({ groupTypeOptions }: Props) => {
+ const [selectedGroupType, setSelectedGroupType] = useState(null);
+
+ const [isDownloading, setIsDownloading] = useState(false);
+
+ const handleDownloadGroup = async () => {
+ if (!selectedGroupType) {
+ Swal.fire({
+ icon: 'error',
+ title: 'Erreur',
+ text: 'Veuillez sélectionner un groupe.',
+ });
+ return;
+ }
+
+ setIsDownloading(true);
+
+ try {
+ const exportData = await fetchExportData(selectedGroupType);
+
+ downloadJsonAsCsv(exportData, `maker_battle_${selectedGroupType.value}_${Date.now()}.csv`);
+ } catch (error) {
+ console.error('Erreur lors du téléchargement des groupes:', error);
+
+ Swal.fire({
+ icon: 'error',
+ title: 'Erreur',
+ text: 'Une erreur est survenue lors du téléchargement des groupes.',
+ });
+ } finally {
+ setIsDownloading(false);
+ }
+ };
+
+ return (
+
+
+ Téléchargement
+
+
+
+
+
+ Groupes à télécharger
+
+
+
+ inputId="maker-battle-group-types"
+ options={groupTypeOptions}
+ value={selectedGroupType}
+ onChange={setSelectedGroupType}
+ placeholder="Sélectionner un type"
+ isClearable
+ isDisabled={isDownloading}
+ />
+
+
+
+
+ {isDownloading ? 'Téléchargement...' : 'Télécharger'}
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/Admin/AdminMakerBattle/adminMakerBattleTeamGeneration.tsx b/frontend/src/components/Admin/AdminMakerBattle/adminMakerBattleTeamGeneration.tsx
new file mode 100644
index 0000000..f9060fe
--- /dev/null
+++ b/frontend/src/components/Admin/AdminMakerBattle/adminMakerBattleTeamGeneration.tsx
@@ -0,0 +1,95 @@
+import { useState } from 'react';
+import Select, { type MultiValue } from 'react-select';
+import Swal from 'sweetalert2';
+
+import type { MakerBattleGroupTypeOption } from '../../../interfaces/maker_battle.interface';
+import { allocateGroups } from '../../../services/requests/maker_battle.service';
+import { Button } from '../../ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card';
+
+type Props = {
+ groupTypeOptions: MakerBattleGroupTypeOption[];
+};
+
+export const AdminMakerBattleTeamGeneration = ({ groupTypeOptions }: Props) => {
+ const [selectedGroupTypes, setSelectedGroupTypes] = useState([]);
+
+ const handleGenerateGroups = async () => {
+ if (selectedGroupTypes.length === 0) {
+ Swal.fire({
+ icon: 'error',
+ title: 'Erreur',
+ text: 'Veuillez sélectionner au moins un type de groupe.',
+ });
+ return;
+ }
+
+ try {
+ await allocateGroups(selectedGroupTypes);
+ Swal.fire({
+ icon: 'success',
+ title: 'Répartition réussie',
+ text: 'Les nouveaux ont été répartis par table avec succès.',
+ });
+ setSelectedGroupTypes([]); // Réinitialiser la sélection après la répartition
+ } catch (error) {
+ console.error('Erreur lors de la répartition des groupes:', error);
+ Swal.fire({
+ icon: 'error',
+ title: 'Erreur',
+ text: 'Une erreur est survenue lors de la répartition des groupes.',
+ });
+ }
+ };
+
+ return (
+
+
+
+ Répartition des nouveaux
+
+ Batailles de conception
+
+
+
+
+
+ Types de nouveaux à répartir:
+
+
selectedGroupTypes.includes(option))}
+ onChange={(options: MultiValue) =>
+ setSelectedGroupTypes(Array.from(options))
+ }
+ placeholder="Sélectionner un ou plusieurs types"
+ closeMenuOnSelect={false}
+ isClearable
+ />
+
+ ATTENTION : Si vous ne souhaitez pas que les
+ numéros de table entrent en collision entre deux groupes, veillez à les générer ensemble.
+
+
+
+ Exemple : si vous souhaitez que les
+ nouveaux Branche et RI effectuent la bataille en même temps, il faut sélectionner
+ les deux groupes afin de les générer en même temps. Dans le cas contraire, une table{' '}
+ Branche pourrait avoir le même numéro qu'une table RI .
+
+
+
+
+ Répartir les nouveaux par table
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/Admin/adminExportImport.tsx b/frontend/src/components/Admin/adminExportImport.tsx
index e59f204..370105e 100644
--- a/frontend/src/components/Admin/adminExportImport.tsx
+++ b/frontend/src/components/Admin/adminExportImport.tsx
@@ -1,39 +1,57 @@
import { useState } from 'react';
import { exportBus, exportDb, exportTeamMembers } from '../../services/requests/im_export.service';
+import { downloadJsonAsCsv } from '../../utils/utils';
import { Button } from '../ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { AdminFileImport } from './adminFileImport';
+type ExportType = 'db' | 'bus' | 'teamMembers';
+
export const AdminExportConnect = () => {
- const [loading, setLoading] = useState<{ db: boolean; bus: boolean; teamMembers: boolean }>({
+ const [loading, setLoading] = useState>({
db: false,
bus: false,
teamMembers: false,
});
+
const [error, setError] = useState(null);
const [message, setMessage] = useState('');
- const [showBusExport, setShowBusExport] = useState(false);
- const [showTeamMembersExport, setShowTeamMembersExport] = useState(false);
- const busUrl = `${import.meta.env.VITE_API_URL}/exports/bus/bus.csv`;
+ const handleExportDb = async () => {
+ setLoading((prev) => ({ ...prev, db: true }));
+ setError(null);
+ setMessage('');
- const teamMembersUrl = `${import.meta.env.VITE_API_URL}/exports/teammembers/teammembers.csv`;
+ try {
+ const response = await exportDb();
+ setMessage(response.message);
+ } catch (err) {
+ console.error('Erreur export db', err);
+ setError("Erreur lors de l'export vers Google Sheets.");
+ } finally {
+ setLoading((prev) => ({ ...prev, db: false }));
+ }
+ };
- const handleExport = async (type: 'db' | 'bus' | 'teamMembers', exportFn: () => Promise<{ message: string }>) => {
+ const handleExportCsv = async (
+ type: 'bus' | 'teamMembers',
+ exportFn: () => Promise[]>,
+ filename: string,
+ ) => {
setLoading((prev) => ({ ...prev, [type]: true }));
setError(null);
setMessage('');
+
try {
- const response = await exportFn();
- setMessage(response.message);
- if (type === 'bus') setShowBusExport(true);
- if (type === 'teamMembers') setShowTeamMembersExport(true);
+ const exportData = await exportFn();
+
+ downloadJsonAsCsv(exportData, `${filename}_${Date.now()}.csv`);
+
+ setMessage('Export terminé avec succès.');
} catch (err) {
console.error(`Erreur export ${type}`, err);
- setError(
- type === 'db' ? "Erreur lors de l'export vers Google Sheets." : "Erreur lors de l'export des bus.",
- );
+ setError("Erreur lors de l'export.");
} finally {
setLoading((prev) => ({ ...prev, [type]: false }));
}
@@ -46,60 +64,39 @@ export const AdminExportConnect = () => {
⚡ Exporter les données
+
+ {/* Export DB → Google Sheets */}
handleExport('db', exportDb)}
+ onClick={handleExportDb}
disabled={loading.db}
className="w-full sm:w-auto bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white py-2 px-6 rounded-xl shadow-md transition-all duration-200">
{loading.db ? '⏳ Export en cours...' : 'Exporter vers Google Sheets'}
+ {/* Export Bus → CSV */}
handleExport('bus', exportBus)}
+ onClick={() => handleExportCsv('bus', exportBus, 'bus_export')}
disabled={loading.bus}
className="w-full sm:w-auto bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 text-white py-2 px-6 rounded-xl shadow-md transition-all duration-200">
{loading.bus ? '⏳ Export en cours...' : 'Exporter les bus'}
+
+ {/* Export Team Members → CSV */}
handleExport('teamMembers', exportTeamMembers)}
+ onClick={() => handleExportCsv('teamMembers', exportTeamMembers, 'team_members_export')}
+ disabled={loading.teamMembers}
className="w-full sm:w-auto bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 text-white py-2 px-6 rounded-xl shadow-md transition-all duration-200">
{loading.teamMembers ? '⏳ Export en cours...' : 'Exporter les équipes'}
{error && {error}
}
+
{message && !error && (
✅ {message}
)}
-
- {showBusExport && (
-
-
- 📄 Télécharger le csv des bus
-
- ⬇️ Télécharger le csv
-
-
-
- )}
-
- {showTeamMembersExport && (
-
-
- 📄 Télécharger le csv des équipes
-
- ⬇️ Télécharger le csv
-
-
-
- )}
);
@@ -111,6 +108,7 @@ export const AdminImportFoodMenu = () => {
Importer le menu
+
@@ -127,13 +125,14 @@ export const AdminImportPlannings = () => {
Importer les plannings
+
-
+
@@ -148,6 +147,7 @@ export const AdminImportNotebooks = () => {
Importer les cahiers de vacances
+
@@ -162,6 +162,7 @@ export const AdminImportOther = () => {
Autres imports
+
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}
handleToggle(key, toggleFn, `${label} mis à jour !`)}
+ onClick={() => handleToggle(setting)}
disabled={loading}
className={`transition-colors duration-300 ${
isActive
diff --git a/frontend/src/components/Admin/adminShotgun.tsx b/frontend/src/components/Admin/adminShotgun.tsx
index f2a9f29..218c65d 100644
--- a/frontend/src/components/Admin/adminShotgun.tsx
+++ b/frontend/src/components/Admin/adminShotgun.tsx
@@ -1,8 +1,8 @@
import { useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
-import { type ShotgunAttemptRow } from '../../interfaces/event.interface';
-import { getShotgunAttemptsAdmin } from '../../services/requests/event.service';
+import { type ShotgunAttemptRow } from '../../interfaces/settings.interface';
+import { getShotgunAttemptsAdmin } from '../../services/requests/settings.service';
import { Button } from '../ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
diff --git a/frontend/src/components/Admin/adminUser.tsx b/frontend/src/components/Admin/adminUser.tsx
index 1bbf1fd..d273c65 100644
--- a/frontend/src/components/Admin/adminUser.tsx
+++ b/frontend/src/components/Admin/adminUser.tsx
@@ -3,7 +3,7 @@ import Select from 'react-select';
import { type SingleValue } from 'react-select';
import Swal from 'sweetalert2';
-import type { User, UserContactInformation } from '../../interfaces/user.interface';
+import type { User, UserContactInformation, UserWithMakerBattle } from '../../interfaces/user.interface';
import { renewTokenUser, requestPasswordUser } from '../../services/requests/auth.service';
import {
createUserByAdmin,
@@ -53,7 +53,7 @@ type MajorOption = (typeof majeurOptions)[number];
export const AdminUser = () => {
const [users, setUsers] = useState([]);
const [selectedUser, setSelectedUser] = useState(null);
- const [formData, setFormData] = useState>({});
+ const [formData, setFormData] = useState>({});
const [contactInformation, setContactInformation] = useState>({});
useEffect(() => {
@@ -243,6 +243,18 @@ export const AdminUser = () => {
onChange={handleSelectChange('permission')}
isClearable
/>
+
+
{
diff --git a/frontend/src/components/WEI_SDI_Food/weiSection.tsx b/frontend/src/components/WEI_SDI_Food/weiSection.tsx
index c728b01..ca2e0c9 100644
--- a/frontend/src/components/WEI_SDI_Food/weiSection.tsx
+++ b/frontend/src/components/WEI_SDI_Food/weiSection.tsx
@@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
import { useOnboarding } from '../../contexts/onboarding';
import { useUser } from '../../contexts/user';
import { decodeToken, getToken } from '../../services/requests/auth.service';
-import { checkWEIStatus } from '../../services/requests/event.service';
+import { checkWEIStatus } from '../../services/requests/settings.service';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
export const WeiSection = () => {
diff --git a/frontend/src/components/challenge/challengeList.tsx b/frontend/src/components/challenge/challengeList.tsx
index 9deba70..1f141f5 100644
--- a/frontend/src/components/challenge/challengeList.tsx
+++ b/frontend/src/components/challenge/challengeList.tsx
@@ -4,8 +4,8 @@ import Swal from 'sweetalert2';
import { type Challenge } from '../../interfaces/challenge.interface';
import { type Faction } from '../../interfaces/faction.interface';
import { getAllChallenges, getFactionsPoints } from '../../services/requests/challenge.service';
-import { checkChallengeStatus } from '../../services/requests/event.service';
import { getAllFactionsUser } from '../../services/requests/faction.service';
+import { checkChallengeStatus } from '../../services/requests/settings.service';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
export const UserChallengeList = () => {
diff --git a/frontend/src/components/footer.tsx b/frontend/src/components/footer.tsx
index 0ff7ceb..085e45d 100644
--- a/frontend/src/components/footer.tsx
+++ b/frontend/src/components/footer.tsx
@@ -1,19 +1,70 @@
export const Footer = () => {
const currentYear = new Date().getFullYear();
+ const APP_VERSION = import.meta.env.VITE_APP_VERSION;
+ const RELEASE_URL = import.meta.env.VITE_RELEASE_URL;
+ const SERVICE_URL = import.meta.env.VITE_SERVICE_URL;
+ const envType =
+ !RELEASE_URL || !APP_VERSION
+ ? SERVICE_URL.includes('localhost') || SERVICE_URL.includes('127.0.0.1')
+ ? 'local'
+ : 'dev'
+ : 'production';
return (
);
-}
+};
diff --git a/frontend/src/components/home/infosSection.tsx b/frontend/src/components/home/infosSection.tsx
index 7cce01d..4429a6d 100644
--- a/frontend/src/components/home/infosSection.tsx
+++ b/frontend/src/components/home/infosSection.tsx
@@ -6,6 +6,7 @@ import { Swiper, SwiperSlide } from 'swiper/react';
import { Button } from '../ui/button';
import { RevealSection } from '../ui/revealSection';
+import { MakerBattle } from './makerBattleSection';
import { Team } from './teamSection';
export const Infos = () => (
@@ -49,6 +50,11 @@ export const Infos = () => (
+ {/* Team */}
+
+
+
+
{/* Team */}
diff --git a/frontend/src/components/home/makerBattleSection.tsx b/frontend/src/components/home/makerBattleSection.tsx
new file mode 100644
index 0000000..a721f51
--- /dev/null
+++ b/frontend/src/components/home/makerBattleSection.tsx
@@ -0,0 +1,87 @@
+import { useEffect, useState } from 'react';
+
+import { useUser } from '../../contexts/user';
+import type { MakerBattleGroupResponseData } from '../../interfaces/maker_battle.interface';
+import { getUserGroup } from '../../services/requests/maker_battle.service';
+import { checkMakerBattleGroupStatus } from '../../services/requests/settings.service';
+import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
+
+export const MakerBattle = () => {
+ const [groupInfos, setGroupInfos] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [makerBattleType, setMakerBattleType] = useState('');
+ const { user, loading: userLoading } = useUser();
+
+ useEffect(() => {
+ const fetchGroup = async () => {
+ const isEnabled = await checkMakerBattleGroupStatus();
+
+ if (!isEnabled) {
+ setLoading(false);
+ return;
+ }
+
+ const { group } = await getUserGroup();
+ setGroupInfos(group);
+ setLoading(false);
+ };
+
+ if (user?.permission === 'Nouveau') {
+ void fetchGroup();
+ setMakerBattleType(['TC', 'IA_BACH'].includes(user.branch) ? 'TC' : 'Branche');
+ } else {
+ // if not a new user, we don't need to load team
+ setLoading(false);
+ }
+ }, [user]);
+
+ const loadingDiv = (
+
+
+
+ Chargement de ton groupe de défis...
+
+
+
+ );
+
+ if (userLoading) {
+ return loadingDiv;
+ }
+
+ if (user?.permission !== 'Nouveau') {
+ return;
+ }
+
+ if (loading) {
+ return loadingDiv;
+ }
+
+ if (!groupInfos) {
+ return;
+ }
+
+ return (
+
+
+
+
+ Trouve ta table au défi {makerBattleType} !
+
+
+
+ Tu as été placé(e) à la table
+
+
+ {groupInfos.table}
+
+
+
+ Rejoins ta table dès que possible pour rencontrer tes cohéquipiers et cohéquipières pour relever
+ le défi !
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/home/teamSection.tsx b/frontend/src/components/home/teamSection.tsx
index d825d35..7e0a51a 100644
--- a/frontend/src/components/home/teamSection.tsx
+++ b/frontend/src/components/home/teamSection.tsx
@@ -27,16 +27,18 @@ export const Team = () => {
}
}, [user]);
+ const loadingDiv = (
+
+
+
+ Chargement de ton équipe...
+
+
+
+ );
+
if (userLoading) {
- return (
-
-
-
- Chargement de ton équipe...
-
-
-
- );
+ return loadingDiv;
}
if (user?.permission !== 'Nouveau') {
@@ -44,15 +46,7 @@ export const Team = () => {
}
if (loading) {
- return (
-
-
-
- Chargement de ton équipe...
-
-
-
- );
+ return loadingDiv;
}
if (!teamInfos) {
diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx
index 79bd146..f02a0ec 100644
--- a/frontend/src/components/navbar.tsx
+++ b/frontend/src/components/navbar.tsx
@@ -91,14 +91,15 @@ export const Navbar = () => {
{ label: 'Bannis', to: '/admin/banned', rolesAllowed: ['Admin'] },
{ label: 'Bus', to: '/admin/bus', rolesAllowed: ['Admin'] },
{ label: 'Challenge', to: '/admin/challenge', rolesAllowed: ['Admin', 'Arbitre'] },
+ { label: 'Défis TC', to: '/admin/maker-battle', rolesAllowed: ['Admin', 'Défis TC'] },
{ label: 'Email', to: '/admin/email', rolesAllowed: ['Admin'] },
- { label: 'Events', to: '/admin/events', rolesAllowed: ['Admin'] },
{ label: 'Export / Import', to: '/admin/export-import', rolesAllowed: ['Admin'] },
{ label: 'Factions', to: '/admin/factions', rolesAllowed: ['Admin', 'Respo CE'] },
{ label: 'Games', to: '/admin/games', rolesAllowed: ['Admin'] },
{ label: 'News', to: '/admin/news', rolesAllowed: ['Admin', 'Communication'] },
{ label: 'Permanences', to: '/admin/permanences', rolesAllowed: ['Admin', 'Respo CE'] },
{ label: 'Roles', to: '/admin/roles', rolesAllowed: ['Admin'] },
+ { label: 'Settings', to: '/admin/settings', rolesAllowed: ['Admin'] },
{ label: 'Shotgun', to: '/admin/shotgun', rolesAllowed: ['Admin', 'Respo CE'] },
{ label: 'Teams', to: '/admin/teams', rolesAllowed: ['Admin', 'Respo CE'] },
{ label: 'Tentes', to: '/admin/tent', rolesAllowed: ['Admin'] },
diff --git a/frontend/src/components/shotgun/preregisterCESection.tsx b/frontend/src/components/shotgun/preregisterCESection.tsx
index 8a8e907..3c03751 100644
--- a/frontend/src/components/shotgun/preregisterCESection.tsx
+++ b/frontend/src/components/shotgun/preregisterCESection.tsx
@@ -1,7 +1,7 @@
-import { useEffect, useState } from "react";
+import { useEffect, useState } from 'react';
-import { checkPreRegisterStatus } from "../../services/requests/event.service";
-import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
+import { checkPreRegisterStatus } from '../../services/requests/settings.service';
+import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
export const PreregisterCESection = () => {
const [isPreRegistrationOpen, setIsPreRegistrationOpen] = useState(false);
@@ -12,7 +12,7 @@ export const PreregisterCESection = () => {
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();
@@ -35,8 +35,7 @@ export const PreregisterCESection = () => {
src="https://forms.gle/32yHKGSTzfFvp7NP9"
className="absolute inset-0 w-full h-full border-none"
title="Formulaire de pré-inscription CE"
- loading="lazy"
- >
+ loading="lazy">
Chargement…
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.
+ className="w-full py-3 text-lg bg-blue-600 text-white rounded-xl shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-300">
Enregistrer l'équipe
@@ -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 = () => {
/>
+ className="w-full py-3 text-lg bg-purple-600 text-white rounded-xl shadow-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 transition duration-300">
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/maker_battle.interface.ts b/frontend/src/interfaces/maker_battle.interface.ts
new file mode 100644
index 0000000..3b4410e
--- /dev/null
+++ b/frontend/src/interfaces/maker_battle.interface.ts
@@ -0,0 +1,13 @@
+export interface MakerBattleGroupTypeOption {
+ value: string;
+ label: string;
+}
+
+export interface MakerBattleGroupDownloadResponseData {
+ filename: string;
+}
+
+export interface MakerBattleGroupResponseData {
+ table: number;
+ team_id: number;
+}
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/interfaces/user.interface.ts b/frontend/src/interfaces/user.interface.ts
index 5234bc2..b14c0a9 100644
--- a/frontend/src/interfaces/user.interface.ts
+++ b/frontend/src/interfaces/user.interface.ts
@@ -11,6 +11,11 @@ export interface User {
vss_form?: 'pending' | 'toretry' | 'validated' | 'rejected';
}
+export interface UserWithMakerBattle extends User {
+ maker_battle_table: number | null;
+ maker_battle_team: number | null;
+}
+
export interface UserContactInformation {
userId: number;
emergency_contact_name: string;
diff --git a/frontend/src/pages/admin/adminMakerBattle.tsx b/frontend/src/pages/admin/adminMakerBattle.tsx
new file mode 100644
index 0000000..4fd47ca
--- /dev/null
+++ b/frontend/src/pages/admin/adminMakerBattle.tsx
@@ -0,0 +1,26 @@
+import { AdminLayout } from '../../components/Admin/adminLayout';
+import { AdminMakerBattleTeamDownload } from '../../components/Admin/AdminMakerBattle/adminMakerBattleDownload';
+import { AdminMakerBattleTeamGeneration } from '../../components/Admin/AdminMakerBattle/adminMakerBattleTeamGeneration';
+import { RevealSection } from '../../components/ui/revealSection';
+import type { MakerBattleGroupTypeOption } from '../../interfaces/maker_battle.interface';
+
+const groupTypeOptions: MakerBattleGroupTypeOption[] = [
+ { value: 'tc', label: 'TC' },
+ { value: 'ri', label: 'RI' },
+ { value: 'branch', label: 'Branche' },
+];
+
+const AdminPageMakerBattle: React.FC = () => (
+
+
+
+);
+
+export default AdminPageMakerBattle;
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/maker_battle.service.ts b/frontend/src/services/requests/maker_battle.service.ts
new file mode 100644
index 0000000..f58f8b4
--- /dev/null
+++ b/frontend/src/services/requests/maker_battle.service.ts
@@ -0,0 +1,17 @@
+import type { MakerBattleGroupTypeOption } from '../../interfaces/maker_battle.interface';
+import api from '../api';
+
+export const allocateGroups = async (groups: MakerBattleGroupTypeOption[]) => {
+ const response = await api.post('/maker-battle/admin/allocate', { groups: groups.map((group) => group.value) });
+ return response.data.data;
+};
+
+export const fetchExportData = async (group: MakerBattleGroupTypeOption) => {
+ const response = await api.get(`/maker-battle/admin/export/${group.value}`);
+ return response.data.data;
+};
+
+export const getUserGroup = async () => {
+ const response = await api.get(`/maker-battle/group/me`);
+ return response.data.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..32a8bf9
--- /dev/null
+++ b/frontend/src/services/requests/settings.service.ts
@@ -0,0 +1,57 @@
+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 checkMakerBattleGroupStatus = async () => (await getSetting('makerBattleGroup')).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;
+};
diff --git a/frontend/src/utils/utils.ts b/frontend/src/utils/utils.ts
index 26e8e4d..3a61909 100644
--- a/frontend/src/utils/utils.ts
+++ b/frontend/src/utils/utils.ts
@@ -38,3 +38,53 @@ export const checkUploadAvailability = async (url: string, callback?: () => void
return false;
}
};
+
+type CsvRow = Record;
+
+const escapeCsvValue = (value: unknown): string => {
+ if (value === null || value === undefined) {
+ return '';
+ }
+
+ const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
+
+ // CSV : on entoure de guillemets si nécessaire
+ if (/[",\n\r]/.test(stringValue)) {
+ return `"${stringValue.replace(/"/g, '""')}"`;
+ }
+
+ return stringValue;
+};
+
+export const downloadJsonAsCsv = (data: CsvRow[], filename = 'export.csv'): void => {
+ if (!data.length) {
+ console.warn('Aucune donnée à exporter.');
+ return;
+ }
+
+ const headers = Array.from(new Set(data.flatMap((row) => Object.keys(row))));
+
+ const csvRows = [
+ headers.map(escapeCsvValue).join(','),
+ ...data.map((row) => headers.map((header) => escapeCsvValue(row[header])).join(',')),
+ ];
+
+ // BOM UTF-8 pour une bonne gestion des accents avec Excel (c'est copilot qui veut)
+ const csv = `\uFEFF${csvRows.join('\r\n')}`;
+
+ const blob = new Blob([csv], {
+ type: 'text/csv;charset=utf-8;',
+ });
+
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+
+ link.href = url;
+ link.download = filename;
+
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ URL.revokeObjectURL(url);
+};