diff --git a/backend/server.ts b/backend/server.ts index abc47b1..65f07e6 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -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'; @@ -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/im_export.controller.ts b/backend/src/controllers/im_export.controller.ts index ee1505f..a7586ff 100644 --- a/backend/src/controllers/im_export.controller.ts +++ b/backend/src/controllers/im_export.controller.ts @@ -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/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/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/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..ea0dd0f --- /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'), makerBattleController.distributeGroups); +makerBattleRouter.get('/admin/export/:group', checkRole('Admin'), makerBattleController.exportGroups); + +makerBattleRouter.get('/group/me', makerBattleController.getCurrentUser); +export default makerBattleRouter; 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/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/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/src/App.tsx b/frontend/src/App.tsx index 5b14d8b..6ed228c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,6 +7,7 @@ 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')); @@ -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 + + + +
+ + + + inputId="maker-battle-group-types" + options={groupTypeOptions} + value={selectedGroupType} + onChange={setSelectedGroupType} + placeholder="Sélectionner un type" + isClearable + isDisabled={isDownloading} + /> +
+ +
+ +
+
+
+ ); +}; 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 +
+
+ +
+ + +
+ {/* 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 6c56745..f02a0ec 100644 --- a/frontend/src/components/navbar.tsx +++ b/frontend/src/components/navbar.tsx @@ -91,6 +91,7 @@ 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: 'Export / Import', to: '/admin/export-import', rolesAllowed: ['Admin'] }, { label: 'Factions', to: '/admin/factions', rolesAllowed: ['Admin', 'Respo CE'] }, 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/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/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 index dea7186..32a8bf9 100644 --- a/frontend/src/services/requests/settings.service.ts +++ b/frontend/src/services/requests/settings.service.ts @@ -44,6 +44,8 @@ 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; 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); +};