From b6586f7715bca67c5a36e7d4e5d979d349039e95 Mon Sep 17 00:00:00 2001 From: Djalim Simaila Date: Fri, 1 May 2026 14:14:33 +0200 Subject: [PATCH] feat: made stuff --- .gitea/workflows/test.yml | 4 + defaults/makeSlug.ts | 26 + openapi.yml | 1536 +++++++++++++++++ .../admin/(_islands)/AdminEnseignements.tsx | 12 +- .../(apps)/admin/(_islands)/AdminModules.tsx | 18 +- routes/(apps)/admin/(_islands)/AdminUEs.tsx | 18 +- routes/(apps)/admin/(_islands)/EditModule.tsx | 12 +- .../admin/(_islands)/ImportMaquette.tsx | 2 +- .../mobility/(_islands)/MobilityOverview.tsx | 162 +- .../mobility/{[slug].tsx => [...slug].tsx} | 0 .../mobility/partials/overview/[numEtud].tsx | 20 + .../(apps)/notes/(_islands)/ImportNotes.tsx | 2 +- .../stages/(_islands)/StagesOverview.tsx | 30 +- .../stages/{[slug].tsx => [...slug].tsx} | 0 .../stages/partials/overview/[numEtud].tsx | 20 + .../students/(_islands)/EditStudents.tsx | 92 +- static/styles/ui.css | 12 +- static/theme.js | 16 +- tests/e2e/modules_test.ts | 4 +- 19 files changed, 1870 insertions(+), 116 deletions(-) create mode 100644 openapi.yml rename routes/(apps)/mobility/{[slug].tsx => [...slug].tsx} (100%) create mode 100644 routes/(apps)/mobility/partials/overview/[numEtud].tsx rename routes/(apps)/stages/{[slug].tsx => [...slug].tsx} (100%) create mode 100644 routes/(apps)/stages/partials/overview/[numEtud].tsx diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index d2a8d16..259baf7 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -56,6 +56,10 @@ jobs: run: | sed 's/--> statement-breakpoint/;/g' databases/migrations/0000_square_jetstream.sql | \ PGPASSWORD=test psql -h 127.0.0.1 -U test -d polympr_test + sed 's/--> statement-breakpoint/;/g' databases/migrations/0003_add_session2_and_malus.sql | \ + PGPASSWORD=test psql -h 127.0.0.1 -U test -d polympr_test + sed 's/--> statement-breakpoint/;/g' databases/migrations/0004_add_stages_and_mobilites.sql | \ + PGPASSWORD=test psql -h 127.0.0.1 -U test -d polympr_test - name: Install dependencies run: npm install --ignore-scripts && deno install diff --git a/defaults/makeSlug.ts b/defaults/makeSlug.ts index ee12fa4..d1110fe 100644 --- a/defaults/makeSlug.ts +++ b/defaults/makeSlug.ts @@ -27,6 +27,32 @@ export default function makeSlug(basePath: string): Route { } } + // For multi-segment slugs (e.g. "overview/12345"), try + // partials//[param].tsx and inject the param into context.params + if (!page && slug.includes("/")) { + const idx = slug.indexOf("/"); + const dir = slug.slice(0, idx); + const param = slug.slice(idx + 1); + + // Discover the dynamic segment name from the file system + try { + const entries: string[] = []; + for await (const entry of Deno.readDir(`${basePath}/partials/${dir}`)) { + if (entry.isFile) entries.push(entry.name); + } + const dynFile = entries.find((n) => + n.startsWith("[") && n.endsWith("].tsx") + ); + if (dynFile) { + const paramName = dynFile.slice(1, -5); // "[numEtud].tsx" → "numEtud" + context.params[paramName] = param; + page = (await import(`${basePath}/partials/${dir}/${dynFile}`)).Page; + } + } catch { + // directory doesn't exist or no dynamic file + } + } + if (!page) { return context.renderNotFound(); } diff --git a/openapi.yml b/openapi.yml new file mode 100644 index 0000000..29b5205 --- /dev/null +++ b/openapi.yml @@ -0,0 +1,1536 @@ +openapi: 3.1.0 +info: + title: PolyMPR API + version: 2.0.0 + description: API de gestion des étudiants, notes, mobilités, stages et administration. + +servers: + - url: / + +tags: + - name: Students + - name: Promotions + - name: Users + - name: Roles + - name: Permissions + - name: Modules + - name: Enseignements + - name: UEs + - name: UE_Modules + - name: Notes + - name: Ajustements + - name: Mobilités + - name: Stages + +paths: + # ── Students ────────────────────────────────────────────── + /students/api/students: + get: + tags: [Students] + summary: Liste des étudiants + parameters: + - $ref: "#/components/parameters/idPromoQuery" + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Student" + post: + tags: [Students] + summary: Créer un étudiant + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/StudentCreate" + responses: + "201": + description: Créé + content: + application/json: + schema: + $ref: "#/components/schemas/Student" + + /students/api/students/import-csv: + post: + tags: [Students] + summary: Importer des étudiants par CSV + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [file, idPromo] + properties: + file: + type: string + format: binary + idPromo: + type: string + responses: + "200": + description: Résultat de l'import + content: + application/json: + schema: + type: object + properties: + imported: + type: integer + errors: + type: array + items: + type: object + properties: + line: + type: integer + message: + type: string + + /students/api/students/{numEtud}: + parameters: + - $ref: "#/components/parameters/numEtud" + get: + tags: [Students] + summary: Détail d'un étudiant + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Student" + "404": + $ref: "#/components/responses/NotFound" + put: + tags: [Students] + summary: Modifier un étudiant + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/StudentCreate" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Student" + "404": + $ref: "#/components/responses/NotFound" + delete: + tags: [Students] + summary: Supprimer un étudiant (cascade mobilités et stages) + responses: + "204": + description: Supprimé + "404": + $ref: "#/components/responses/NotFound" + + # ── Promotions ──────────────────────────────────────────── + /students/api/promotions: + get: + tags: [Promotions] + summary: Liste des promotions + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Promotion" + post: + tags: [Promotions] + summary: Créer une promotion + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PromotionCreate" + responses: + "201": + description: Créé + content: + application/json: + schema: + $ref: "#/components/schemas/Promotion" + + /students/api/promotions/{idPromo}: + parameters: + - $ref: "#/components/parameters/idPromo" + get: + tags: [Promotions] + summary: Détail d'une promotion + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Promotion" + "404": + $ref: "#/components/responses/NotFound" + put: + tags: [Promotions] + summary: Modifier une promotion + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PromotionCreate" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Promotion" + "404": + $ref: "#/components/responses/NotFound" + delete: + tags: [Promotions] + summary: Supprimer une promotion + responses: + "204": + description: Supprimé + "404": + $ref: "#/components/responses/NotFound" + + # ── Users ──────────────────────────────────────────────── + /admin/api/users: + get: + tags: [Users] + summary: Liste des utilisateurs + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/User" + post: + tags: [Users] + summary: Créer un utilisateur (employee only) + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UserCreate" + responses: + "201": + description: Créé + content: + application/json: + schema: + $ref: "#/components/schemas/User" + "403": + description: Accès refusé + + /admin/api/users/{id}: + parameters: + - name: id + in: path + required: true + schema: + type: string + get: + tags: [Users] + summary: Détail d'un utilisateur + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/User" + "404": + $ref: "#/components/responses/NotFound" + put: + tags: [Users] + summary: Modifier un utilisateur + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UserCreate" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/User" + "404": + $ref: "#/components/responses/NotFound" + delete: + tags: [Users] + summary: Supprimer un utilisateur + responses: + "204": + description: Supprimé + "404": + $ref: "#/components/responses/NotFound" + + # ── Roles ──────────────────────────────────────────────── + /admin/api/roles: + get: + tags: [Roles] + summary: Liste des rôles + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Role" + post: + tags: [Roles] + summary: Créer un rôle + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/RoleCreate" + responses: + "201": + description: Créé + content: + application/json: + schema: + $ref: "#/components/schemas/Role" + + /admin/api/roles/{idRole}: + parameters: + - name: idRole + in: path + required: true + schema: + type: integer + get: + tags: [Roles] + summary: Détail d'un rôle + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Role" + "404": + $ref: "#/components/responses/NotFound" + put: + tags: [Roles] + summary: Modifier un rôle + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/RoleCreate" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Role" + "404": + $ref: "#/components/responses/NotFound" + delete: + tags: [Roles] + summary: Supprimer un rôle + responses: + "204": + description: Supprimé + "404": + $ref: "#/components/responses/NotFound" + + # ── Permissions ────────────────────────────────────────── + /admin/api/permissions: + get: + tags: [Permissions] + summary: Liste des permissions + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Permission" + + # ── Modules ─────────────────────────────────────────────── + /admin/api/modules: + get: + tags: [Modules] + summary: Liste des modules + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Module" + post: + tags: [Modules] + summary: Créer un module (employee only) + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ModuleCreate" + responses: + "201": + description: Créé + content: + application/json: + schema: + $ref: "#/components/schemas/Module" + "409": + description: Un module avec cet identifiant existe déjà + + /admin/api/modules/{idModule}: + parameters: + - $ref: "#/components/parameters/idModule" + get: + tags: [Modules] + summary: Détail d'un module + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Module" + "404": + $ref: "#/components/responses/NotFound" + put: + tags: [Modules] + summary: Modifier un module + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [nom] + properties: + nom: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Module" + "404": + $ref: "#/components/responses/NotFound" + delete: + tags: [Modules] + summary: Supprimer un module (cascade notes, ue_modules, enseignements) + responses: + "204": + description: Supprimé + "404": + $ref: "#/components/responses/NotFound" + + # ── Enseignements ─────────────────────────────────────── + /admin/api/enseignements: + get: + tags: [Enseignements] + summary: Liste des enseignements + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Enseignement" + post: + tags: [Enseignements] + summary: Créer un enseignement + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EnseignementCreate" + responses: + "201": + description: Créé + content: + application/json: + schema: + $ref: "#/components/schemas/Enseignement" + "409": + description: Cet enseignement existe déjà + + /admin/api/enseignements/{idProf}/{idModule}/{idPromo}: + parameters: + - name: idProf + in: path + required: true + schema: + type: string + - name: idModule + in: path + required: true + schema: + type: string + - name: idPromo + in: path + required: true + schema: + type: string + get: + tags: [Enseignements] + summary: Détail d'un enseignement + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Enseignement" + "404": + $ref: "#/components/responses/NotFound" + delete: + tags: [Enseignements] + summary: Supprimer un enseignement + responses: + "204": + description: Supprimé + "404": + $ref: "#/components/responses/NotFound" + + # ── UEs ─────────────────────────────────────────────────── + /admin/api/ues: + get: + tags: [UEs] + summary: Liste des UEs + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/UE" + post: + tags: [UEs] + summary: Créer une UE + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UECreate" + responses: + "201": + description: Créé + content: + application/json: + schema: + $ref: "#/components/schemas/UE" + + /admin/api/ues/{idUE}: + parameters: + - $ref: "#/components/parameters/idUE" + get: + tags: [UEs] + summary: Détail d'une UE + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/UE" + "404": + $ref: "#/components/responses/NotFound" + put: + tags: [UEs] + summary: Modifier une UE + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UECreate" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/UE" + "404": + $ref: "#/components/responses/NotFound" + delete: + tags: [UEs] + summary: Supprimer une UE + responses: + "204": + description: Supprimé + "404": + $ref: "#/components/responses/NotFound" + + # ── UE_Modules ──────────────────────────────────────────── + /admin/api/ue-modules: + get: + tags: [UE_Modules] + summary: Liste des associations UE-Module + parameters: + - $ref: "#/components/parameters/idPromoQuery" + - name: idUE + in: query + schema: + type: integer + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/UEModule" + post: + tags: [UE_Modules] + summary: Associer un module à une UE + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UEModuleCreate" + responses: + "201": + description: Créé + content: + application/json: + schema: + $ref: "#/components/schemas/UEModule" + + /admin/api/ue-modules/{idModule}/{idUE}/{idPromo}: + parameters: + - name: idModule + in: path + required: true + schema: + type: string + - name: idUE + in: path + required: true + schema: + type: integer + - name: idPromo + in: path + required: true + schema: + type: string + get: + tags: [UE_Modules] + summary: Détail d'une association UE-Module + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/UEModule" + "404": + $ref: "#/components/responses/NotFound" + put: + tags: [UE_Modules] + summary: Modifier le coefficient + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [coeff] + properties: + coeff: + type: number + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/UEModule" + "404": + $ref: "#/components/responses/NotFound" + delete: + tags: [UE_Modules] + summary: Supprimer l'association + responses: + "204": + description: Supprimé + "404": + $ref: "#/components/responses/NotFound" + + # ── Notes ───────────────────────────────────────────────── + /notes/api/notes: + get: + tags: [Notes] + summary: Liste des notes + parameters: + - name: numEtud + in: query + schema: + type: integer + - name: idModule + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Note" + post: + tags: [Notes] + summary: Créer une note + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NoteCreate" + responses: + "201": + description: Créé + content: + application/json: + schema: + $ref: "#/components/schemas/Note" + "409": + description: Note déjà existante pour cet étudiant/module + + /notes/api/notes/import-xlsx: + post: + tags: [Notes] + summary: Importer des notes par fichier XLSX + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [file, idModule] + properties: + file: + type: string + format: binary + idModule: + type: string + responses: + "200": + description: Import réussi + content: + application/json: + schema: + type: object + properties: + imported: + type: integer + errors: + type: array + items: + type: object + properties: + line: + type: integer + student: + type: string + message: + type: string + "400": + description: Fichier invalide ou données corrompues + + /notes/api/notes/{numEtud}/{idModule}: + parameters: + - name: numEtud + in: path + required: true + schema: + type: integer + - name: idModule + in: path + required: true + schema: + type: string + get: + tags: [Notes] + summary: Détail d'une note + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Note" + "404": + $ref: "#/components/responses/NotFound" + put: + tags: [Notes] + summary: Modifier une note + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [note] + properties: + note: + type: number + minimum: 0 + maximum: 20 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Note" + "404": + $ref: "#/components/responses/NotFound" + delete: + tags: [Notes] + summary: Supprimer une note + responses: + "204": + description: Supprimé + "404": + $ref: "#/components/responses/NotFound" + + # ── Ajustements ─────────────────────────────────────────── + /notes/api/ajustements: + get: + tags: [Ajustements] + summary: Liste des ajustements + parameters: + - name: numEtud + in: query + schema: + type: integer + - name: idUE + in: query + schema: + type: integer + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Ajustement" + post: + tags: [Ajustements] + summary: Créer un ajustement + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AjustementCreate" + responses: + "201": + description: Créé + content: + application/json: + schema: + $ref: "#/components/schemas/Ajustement" + + /notes/api/ajustements/{numEtud}/{idUE}: + parameters: + - name: numEtud + in: path + required: true + schema: + type: integer + - name: idUE + in: path + required: true + schema: + type: integer + get: + tags: [Ajustements] + summary: Détail d'un ajustement + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Ajustement" + "404": + $ref: "#/components/responses/NotFound" + put: + tags: [Ajustements] + summary: Modifier un ajustement + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [valeur] + properties: + valeur: + type: number + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Ajustement" + "404": + $ref: "#/components/responses/NotFound" + delete: + tags: [Ajustements] + summary: Supprimer un ajustement + responses: + "204": + description: Supprimé + "404": + $ref: "#/components/responses/NotFound" + + # ── Mobilités ───────────────────────────────────────────── + /mobility/api/mobilites: + get: + tags: [Mobilités] + summary: Liste des mobilités + parameters: + - name: numEtud + in: query + schema: + type: integer + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Mobilite" + post: + tags: [Mobilités] + summary: Créer une mobilité + description: > + Les étudiants ne peuvent pas définir idStage ni changer le status + (reste contracts_received). Les mobilités liées à un stage sont + automatiquement validées. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/MobiliteCreate" + responses: + "201": + description: Créé + content: + application/json: + schema: + $ref: "#/components/schemas/Mobilite" + "400": + description: Champs requis manquants ou invalides + + /mobility/api/mobilites/{idMob}: + parameters: + - name: idMob + in: path + required: true + schema: + type: integer + get: + tags: [Mobilités] + summary: Détail d'une mobilité + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Mobilite" + "404": + $ref: "#/components/responses/NotFound" + put: + tags: [Mobilités] + summary: Modifier une mobilité (employee only) + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + duree: + type: integer + minimum: 1 + ecole: + type: string + nullable: true + pays: + type: string + nullable: true + status: + $ref: "#/components/schemas/MobilityStatus" + idStage: + type: integer + nullable: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Mobilite" + "403": + description: Accès refusé + "404": + $ref: "#/components/responses/NotFound" + delete: + tags: [Mobilités] + summary: Supprimer une mobilité (employee only, supprime aussi le contrat) + responses: + "204": + description: Supprimé + "403": + description: Accès refusé + "404": + $ref: "#/components/responses/NotFound" + + /mobility/api/mobilites/{idMob}/contrat: + parameters: + - name: idMob + in: path + required: true + schema: + type: integer + get: + tags: [Mobilités] + summary: Télécharger le contrat PDF + responses: + "200": + description: Fichier PDF + content: + application/pdf: + schema: + type: string + format: binary + "404": + $ref: "#/components/responses/NotFound" + post: + tags: [Mobilités] + summary: Uploader un contrat PDF + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [contrat] + properties: + contrat: + type: string + format: binary + description: Fichier PDF du contrat + responses: + "200": + description: Mobilité mise à jour avec le nom du fichier + content: + application/json: + schema: + $ref: "#/components/schemas/Mobilite" + "400": + description: Fichier manquant ou pas un PDF + "404": + $ref: "#/components/responses/NotFound" + delete: + tags: [Mobilités] + summary: Supprimer le contrat (employee only) + responses: + "204": + description: Contrat supprimé + "403": + description: Accès refusé + "404": + $ref: "#/components/responses/NotFound" + + # ── Stages ──────────────────────────────────────────────── + /stages/api/stages: + get: + tags: [Stages] + summary: Liste des stages + parameters: + - name: numEtud + in: query + schema: + type: integer + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Stage" + post: + tags: [Stages] + summary: Créer un stage (employee only) + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/StageCreate" + responses: + "201": + description: Créé + content: + application/json: + schema: + $ref: "#/components/schemas/Stage" + "400": + description: Champs requis manquants + "403": + description: Accès refusé + + /stages/api/stages/{idStage}: + parameters: + - name: idStage + in: path + required: true + schema: + type: integer + get: + tags: [Stages] + summary: Détail d'un stage + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Stage" + "404": + $ref: "#/components/responses/NotFound" + put: + tags: [Stages] + summary: Modifier un stage (employee only, synchronise la durée sur la mobilité liée) + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + duree: + type: integer + minimum: 1 + nomEntreprise: + type: string + mission: + type: string + nullable: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Stage" + "403": + description: Accès refusé + "404": + $ref: "#/components/responses/NotFound" + delete: + tags: [Stages] + summary: Supprimer un stage (employee only, cascade mobilités liées) + responses: + "204": + description: Supprimé + "403": + description: Accès refusé + "404": + $ref: "#/components/responses/NotFound" + +# ── Components ──────────────────────────────────────────────── +components: + parameters: + numEtud: + name: numEtud + in: path + required: true + schema: + type: integer + example: 21212006 + idPromo: + name: idPromo + in: path + required: true + schema: + type: string + example: 4AFISE25/26 + idPromoQuery: + name: idPromo + in: query + schema: + type: string + example: 4AFISE25/26 + idModule: + name: idModule + in: path + required: true + schema: + type: string + idUE: + name: idUE + in: path + required: true + schema: + type: integer + + responses: + NotFound: + description: Ressource introuvable + content: + application/json: + schema: + type: object + properties: + error: + type: string + + schemas: + # ── Student ── + Student: + type: object + properties: + numEtud: + type: integer + nom: + type: string + prenom: + type: string + idPromo: + type: string + StudentCreate: + type: object + required: [numEtud, nom, prenom, idPromo] + properties: + numEtud: + type: integer + nom: + type: string + prenom: + type: string + idPromo: + type: string + + # ── Promotion ── + Promotion: + type: object + properties: + id: + type: string + annee: + type: string + PromotionCreate: + type: object + required: [id, annee] + properties: + id: + type: string + annee: + type: string + + # ── User ── + User: + type: object + properties: + id: + type: string + nom: + type: string + prenom: + type: string + idRole: + type: integer + nullable: true + UserCreate: + type: object + required: [id, nom, prenom] + properties: + id: + type: string + nom: + type: string + prenom: + type: string + idRole: + type: integer + + # ── Role ── + Role: + type: object + properties: + id: + type: integer + nom: + type: string + RoleCreate: + type: object + required: [nom] + properties: + nom: + type: string + + # ── Permission ── + Permission: + type: object + properties: + id: + type: string + nom: + type: string + + # ── Module ── + Module: + type: object + properties: + id: + type: string + nom: + type: string + ModuleCreate: + type: object + required: [id, nom] + properties: + id: + type: string + nom: + type: string + + # ── Enseignement ── + Enseignement: + type: object + properties: + idProf: + type: string + idModule: + type: string + idPromo: + type: string + EnseignementCreate: + type: object + required: [idProf, idModule, idPromo] + properties: + idProf: + type: string + idModule: + type: string + idPromo: + type: string + + # ── UE ── + UE: + type: object + properties: + id: + type: integer + nom: + type: string + UECreate: + type: object + required: [nom] + properties: + nom: + type: string + + # ── UE_Module ── + UEModule: + type: object + properties: + idModule: + type: string + idUE: + type: integer + idPromo: + type: string + coeff: + type: number + UEModuleCreate: + type: object + required: [idModule, idUE, idPromo, coeff] + properties: + idModule: + type: string + idUE: + type: integer + idPromo: + type: string + coeff: + type: number + + # ── Note ── + Note: + type: object + properties: + numEtud: + type: integer + idModule: + type: string + note: + type: number + minimum: 0 + maximum: 20 + NoteCreate: + type: object + required: [numEtud, idModule, note] + properties: + numEtud: + type: integer + idModule: + type: string + note: + type: number + minimum: 0 + maximum: 20 + + # ── Ajustement ── + Ajustement: + type: object + properties: + numEtud: + type: integer + idUE: + type: integer + valeur: + type: number + AjustementCreate: + type: object + required: [numEtud, idUE, valeur] + properties: + numEtud: + type: integer + idUE: + type: integer + valeur: + type: number + + # ── Mobilité ── + MobilityStatus: + type: string + enum: [contracts_received, under_revision, done, validated, canceled] + Mobilite: + type: object + properties: + id: + type: integer + numEtud: + type: integer + duree: + type: integer + contratMob: + type: string + nullable: true + ecole: + type: string + nullable: true + pays: + type: string + nullable: true + status: + $ref: "#/components/schemas/MobilityStatus" + idStage: + type: integer + nullable: true + MobiliteCreate: + type: object + required: [numEtud, duree] + properties: + numEtud: + type: integer + duree: + type: integer + minimum: 1 + ecole: + type: string + pays: + type: string + status: + $ref: "#/components/schemas/MobilityStatus" + idStage: + type: integer + + # ── Stage ── + Stage: + type: object + properties: + id: + type: integer + numEtud: + type: integer + duree: + type: integer + nomEntreprise: + type: string + mission: + type: string + nullable: true + StageCreate: + type: object + required: [numEtud, duree, nomEntreprise] + properties: + numEtud: + type: integer + duree: + type: integer + minimum: 1 + nomEntreprise: + type: string + mission: + type: string diff --git a/routes/(apps)/admin/(_islands)/AdminEnseignements.tsx b/routes/(apps)/admin/(_islands)/AdminEnseignements.tsx index 2a0c2af..3b76991 100644 --- a/routes/(apps)/admin/(_islands)/AdminEnseignements.tsx +++ b/routes/(apps)/admin/(_islands)/AdminEnseignements.tsx @@ -115,7 +115,7 @@ export default function AdminEnseignements() { return (
-

Assignations Enseignant → Module / Promo

+

Assignations Enseignant → ECUE / Promo

{error &&

{error}

} @@ -135,7 +135,7 @@ export default function AdminEnseignements() { onChange={(e) => setFilterModule((e.target as HTMLSelectElement).value)} > - + {modules.map((m) => ( ))} @@ -194,7 +194,7 @@ export default function AdminEnseignements() {
- + setNewNom((e.target as HTMLInputElement).value)} /> diff --git a/routes/(apps)/admin/(_islands)/AdminUEs.tsx b/routes/(apps)/admin/(_islands)/AdminUEs.tsx index c8612c2..5bb7a57 100644 --- a/routes/(apps)/admin/(_islands)/AdminUEs.tsx +++ b/routes/(apps)/admin/(_islands)/AdminUEs.tsx @@ -104,7 +104,7 @@ export default function AdminUEs() { idUE: number, idPromo: string, ) { - if (!confirm("Supprimer ce module de la UE ?")) return; + if (!confirm("Supprimer cet ECUE de la UE ?")) return; try { const res = await fetch( `/admin/api/ue-modules/${encodeURIComponent(idModule)}/${idUE}/${ @@ -121,7 +121,7 @@ export default function AdminUEs() { async function addUeModule() { if (!selectedUe || !addModuleId || !addPromoId) { - setAddError("Module et Promo sont requis"); + setAddError("ECUE et Promo sont requis"); return; } const coeff = parseFloat(addCoeff); @@ -203,7 +203,7 @@ export default function AdminUEs() { class="col-dim" style="font-size: 0.78rem; margin: -0.5rem 0 1rem" > - UE = Unité d'Enseignement regroupant plusieurs modules + UE = Unité d'Enseignement regroupant plusieurs ECUEs

{error &&

{error}

} @@ -314,13 +314,13 @@ export default function AdminUEs() {

{selectedUe.nom}

- Modules assignés (UE_Module) + ECUEs assignés (UE_Module)

- + @@ -331,7 +331,7 @@ export default function AdminUEs() { ? ( ) @@ -441,7 +441,7 @@ export default function AdminUEs() {

- Ajouter un module à cette UE + Ajouter un ECUE à cette UE

{addError && (

@@ -458,7 +458,7 @@ export default function AdminUEs() { )} style="min-width: 12rem" > - + {modules.map((m) => (

- Sélectionnez une UE pour voir ses modules + Sélectionnez une UE pour voir ses ECUEs

)} diff --git a/routes/(apps)/admin/(_islands)/EditModule.tsx b/routes/(apps)/admin/(_islands)/EditModule.tsx index a9770ba..1bd554e 100644 --- a/routes/(apps)/admin/(_islands)/EditModule.tsx +++ b/routes/(apps)/admin/(_islands)/EditModule.tsx @@ -33,7 +33,7 @@ export default function EditModule({ moduleId }: Props) { fetch("/admin/api/users"), fetch("/students/api/promotions"), ]); - if (!mRes.ok) throw new Error("Module introuvable"); + if (!mRes.ok) throw new Error("ECUE introuvable"); const m: Module = await mRes.json(); setMod(m); setNom(m.nom); @@ -70,7 +70,7 @@ export default function EditModule({ moduleId }: Props) { if (!res.ok) throw new Error("Modification échouée"); const updated: Module = await res.json(); setMod(updated); - setSaveMsg("Module enregistré."); + setSaveMsg("ECUE enregistré."); } catch (e) { setError(e instanceof Error ? e.message : "Erreur"); } finally { @@ -79,7 +79,7 @@ export default function EditModule({ moduleId }: Props) { } async function deleteModule() { - if (!confirm(`Supprimer définitivement le module ${moduleId} ?`)) return; + if (!confirm(`Supprimer définitivement l'ECUE ${moduleId} ?`)) return; try { const res = await fetch( `/admin/api/modules/${encodeURIComponent(moduleId)}`, @@ -173,7 +173,7 @@ export default function EditModule({ moduleId }: Props) { class="page-title" style="border-bottom: none; margin-bottom: 0.5rem" > - Module -- {mod.id} + ECUE -- {mod.id}
@@ -202,7 +202,7 @@ export default function EditModule({ moduleId }: Props) { />
- + - Supprimer le module + Supprimer l'ECUE
diff --git a/routes/(apps)/admin/(_islands)/ImportMaquette.tsx b/routes/(apps)/admin/(_islands)/ImportMaquette.tsx index 278081c..9e9dc33 100644 --- a/routes/(apps)/admin/(_islands)/ImportMaquette.tsx +++ b/routes/(apps)/admin/(_islands)/ImportMaquette.tsx @@ -281,7 +281,7 @@ export default function ImportMaquette() { globalThis.open("/templates/modele_maquette.xlsx", "_blank"); } - function downloadExport() { + function _downloadExport() { Promise.all([ fetch("/admin/api/ues").then((r) => r.json()), fetch("/admin/api/ue-modules").then((r) => r.json()), diff --git a/routes/(apps)/mobility/(_islands)/MobilityOverview.tsx b/routes/(apps)/mobility/(_islands)/MobilityOverview.tsx index f429469..a167414 100644 --- a/routes/(apps)/mobility/(_islands)/MobilityOverview.tsx +++ b/routes/(apps)/mobility/(_islands)/MobilityOverview.tsx @@ -67,7 +67,9 @@ function validatedWeeks(mobs: Mobilite[]): number { .reduce((sum, m) => sum + m.duree, 0); } -export default function MobilityOverview() { +export default function MobilityOverview( + { initialNumEtud }: { initialNumEtud?: number } = {}, +) { const [students, setStudents] = useState([]); const [promos, setPromos] = useState([]); const [mobilites, setMobilites] = useState([]); @@ -105,6 +107,12 @@ export default function MobilityOverview() { setStagesMap( Object.fromEntries((stData as Stage[]).map((s) => [s.id, s])), ); + if (initialNumEtud) { + const s = (sData as Student[]).find((s) => + s.numEtud === initialNumEtud + ); + if (s) setDetailStudent(s); + } } catch (e) { setError(e instanceof Error ? e.message : "Erreur"); } finally { @@ -116,6 +124,18 @@ export default function MobilityOverview() { load(); }, []); + function openStudent(s: Student) { + setDetailStudent(s); + history.pushState(null, "", `/mobility/overview/${s.numEtud}`); + } + + function closeStudent() { + setDetailStudent(null); + setEditingMob(null); + setShowAddForm(false); + history.pushState(null, "", "/mobility/overview"); + } + // If in detail view, render that if (detailStudent) { return ( @@ -128,11 +148,7 @@ export default function MobilityOverview() { setEditingMob={setEditingMob} showAddForm={showAddForm} setShowAddForm={setShowAddForm} - onBack={() => { - setDetailStudent(null); - setEditingMob(null); - setShowAddForm(false); - }} + onBack={closeStudent} onReload={load} /> ); @@ -207,14 +223,14 @@ export default function MobilityOverview() { setDetailStudent(s)} + onConsult={(s) => openStudent(s)} /> ) : ( setDetailStudent(s)} + onConsult={(s) => openStudent(s)} /> )} @@ -637,6 +653,9 @@ function DetailView( numEtud={student.numEtud} ecoles={ecoles} paysList={paysList} + availableStages={Object.values(stagesMap) + .filter((s) => s.numEtud === student.numEtud) + .filter((s) => !mobilites.some((m) => m.idStage === s.id))} onCancel={() => setShowAddForm(false)} onSave={async () => { setShowAddForm(false); @@ -774,10 +793,11 @@ function MobEditForm( } function MobAddForm( - { numEtud, ecoles, paysList, onCancel, onSave }: { + { numEtud, ecoles, paysList, availableStages, onCancel, onSave }: { numEtud: number; ecoles: string[]; paysList: string[]; + availableStages: Stage[]; onCancel: () => void; onSave: () => Promise; }, @@ -786,8 +806,19 @@ function MobAddForm( const [ecole, setEcole] = useState(""); const [pays, setPays] = useState(""); const [status, setStatus] = useState("contracts_received"); + const [selectedStageId, setSelectedStageId] = useState(""); const [busy, setBusy] = useState(false); + const isStageLinked = selectedStageId !== ""; + + function onStageChange(value: string) { + setSelectedStageId(value); + if (value) { + const stage = availableStages.find((s) => s.id === Number(value)); + if (stage) setDuree(String(stage.duree)); + } + } + async function submit() { setBusy(true); try { @@ -797,9 +828,10 @@ function MobAddForm( body: JSON.stringify({ numEtud, duree: parseInt(duree), - ecole: ecole || null, - pays: pays || null, - status, + ecole: isStageLinked ? null : (ecole || null), + pays: isStageLinked ? null : (pays || null), + status: isStageLinked ? "validated" : status, + idStage: isStageLinked ? Number(selectedStageId) : null, }), }); if (!res.ok) throw new Error("Erreur"); @@ -815,6 +847,24 @@ function MobAddForm(

Nouvelle mobilité

+ {availableStages.length > 0 && ( +
+ + +
+ )}
setDuree((e.target as HTMLInputElement).value)} />
-
- - setEcole((e.target as HTMLInputElement).value)} - /> - - {ecoles.map((e) => -
-
- - setPays((e.target as HTMLInputElement).value)} - /> - - {paysList.map((p) => -
-
- - -
+ {!isStageLinked && ( + <> +
+ + setEcole((e.target as HTMLInputElement).value)} + /> + + {ecoles.map((e) => +
+
+ + setPays((e.target as HTMLInputElement).value)} + /> + + {paysList.map((p) => +
+
+ + +
+ + )}
+ {isStageLinked && ( +

+ Mobilité liée à un stage — status automatiquement « Validé » +

+ )}
); diff --git a/routes/(apps)/stages/[slug].tsx b/routes/(apps)/stages/[...slug].tsx similarity index 100% rename from routes/(apps)/stages/[slug].tsx rename to routes/(apps)/stages/[...slug].tsx diff --git a/routes/(apps)/stages/partials/overview/[numEtud].tsx b/routes/(apps)/stages/partials/overview/[numEtud].tsx new file mode 100644 index 0000000..3a06562 --- /dev/null +++ b/routes/(apps)/stages/partials/overview/[numEtud].tsx @@ -0,0 +1,20 @@ +import { + getPartialsConfig, + makePartials, +} from "$root/defaults/makePartials.tsx"; +import { FreshContext } from "$fresh/server.ts"; +import { State } from "$root/defaults/interfaces.ts"; +import StagesOverview from "../../(_islands)/StagesOverview.tsx"; + +// deno-lint-ignore require-await +async function Overview( + _request: Request, + context: FreshContext, +) { + const numEtud = Number(context.params.numEtud); + return ; +} + +export { Overview as Page }; +export const config = getPartialsConfig(); +export default makePartials(Overview); diff --git a/routes/(apps)/students/(_islands)/EditStudents.tsx b/routes/(apps)/students/(_islands)/EditStudents.tsx index a7fc770..b72abf4 100644 --- a/routes/(apps)/students/(_islands)/EditStudents.tsx +++ b/routes/(apps)/students/(_islands)/EditStudents.tsx @@ -8,6 +8,8 @@ type Student = { }; type Promo = { id: string; annee: string }; type Module = { id: string; nom: string }; +type Mobilite = { id: number; duree: number; status: string }; +type Stage = { id: number; duree: number }; type Props = { numEtud: number }; @@ -25,6 +27,8 @@ export default function EditStudents({ numEtud }: Props) { const [student, setStudent] = useState(null); const [promos, setPromos] = useState([]); const [_modules, setModules] = useState([]); + const [mobWeeks, setMobWeeks] = useState(0); + const [stageWeeks, setStageWeeks] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [saveMsg, setSaveMsg] = useState(null); @@ -38,10 +42,12 @@ export default function EditStudents({ numEtud }: Props) { useEffect(() => { async function load() { try { - const [sRes, pRes, mRes] = await Promise.all([ + const [sRes, pRes, mRes, mobRes, stRes] = await Promise.all([ fetch(`/students/api/students/${numEtud}`), fetch("/students/api/promotions"), - fetch("/admin/api/modules"), + fetch("/notes/api/modules"), + fetch(`/mobility/api/mobilites?numEtud=${numEtud}`), + fetch(`/stages/api/stages?numEtud=${numEtud}`), ]); if (!sRes.ok) throw new Error("Élève introuvable"); const s: Student = await sRes.json(); @@ -51,6 +57,19 @@ export default function EditStudents({ numEtud }: Props) { setIdPromo(s.idPromo); if (pRes.ok) setPromos(await pRes.json()); if (mRes.ok) setModules(await mRes.json()); + if (mobRes.ok) { + const mobs: Mobilite[] = await mobRes.json(); + setMobWeeks( + mobs.filter((m) => m.status === "validated").reduce( + (s, m) => s + m.duree, + 0, + ), + ); + } + if (stRes.ok) { + const stages: Stage[] = await stRes.json(); + setStageWeeks(stages.reduce((s, st) => s + st.duree, 0)); + } } catch (e) { setError(e instanceof Error ? e.message : "Erreur"); } finally { @@ -207,30 +226,69 @@ export default function EditStudents({ numEtud }: Props) {
- {/* Section 2: Spécialisations */} + {/* Section 2: Notes */}
-

Spécialisations

-

- Fonctionnalité non disponible (endpoint non implémenté). -

-
- - {/* Section 3: Notes lecture seule */} -
-

Notes (lecture seule)

+

Notes

- Voir le récap complet des notes et moyennes de cet étudiant → + Récap complet des notes et moyennes - Récap notes + Voir les notes + +
+
+ + {/* Section 3: Mobilités */} +
+

Mobilités

+
+ + = 12 ? "#22c55e" : "#dc2626", + }} + > + {mobWeeks}/12 semaines validées + + + + Consulter + +
+
+ + {/* Section 4: Stages */} +
+

Stages

+
+ + = 40 ? "#22c55e" : "#dc2626", + }} + > + {stageWeeks}/40 semaines + + + + Consulter
diff --git a/static/styles/ui.css b/static/styles/ui.css index 9d2218e..6583f3c 100644 --- a/static/styles/ui.css +++ b/static/styles/ui.css @@ -40,6 +40,12 @@ font-size: 0.8rem; font-family: inherit; min-width: 8rem; + box-sizing: border-box; +} + +.form-field .filter-select { + width: 100%; + min-width: 0; } .filter-input:focus, @@ -368,7 +374,9 @@ color: light-dark(var(--light-foreground), var(--dark-foreground)); font-size: 0.82rem; font-family: inherit; - min-width: 12rem; + min-width: 0; + width: 100%; + box-sizing: border-box; } .form-input:focus { @@ -799,7 +807,7 @@ .form-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: 0.75rem 1rem; margin-bottom: 0.75rem; } diff --git a/static/theme.js b/static/theme.js index af947af..041e9a8 100644 --- a/static/theme.js +++ b/static/theme.js @@ -1,15 +1,15 @@ (function () { - var t = localStorage.getItem("theme"); + const t = localStorage.getItem("theme"); if (t) document.documentElement.style.colorScheme = t; document.addEventListener("click", function (e) { - var btn = e.target.closest("#theme-toggle"); + const btn = e.target.closest("#theme-toggle"); if (!btn) return; - var cs = getComputedStyle(document.documentElement).colorScheme; - var isDark = cs === "dark" || + const cs = getComputedStyle(document.documentElement).colorScheme; + const isDark = cs === "dark" || (!cs || cs === "light dark") && matchMedia("(prefers-color-scheme:dark)").matches; - var next = isDark ? "light" : "dark"; + const next = isDark ? "light" : "dark"; document.documentElement.style.colorScheme = next; localStorage.setItem("theme", next); btn.querySelector("span").textContent = next === "dark" @@ -18,10 +18,10 @@ }); document.addEventListener("DOMContentLoaded", function () { - var btn = document.getElementById("theme-toggle"); + const btn = document.getElementById("theme-toggle"); if (!btn) return; - var cs = getComputedStyle(document.documentElement).colorScheme; - var isDark = cs === "dark" || + const cs = getComputedStyle(document.documentElement).colorScheme; + const isDark = cs === "dark" || (!cs || cs === "light dark") && matchMedia("(prefers-color-scheme:dark)").matches; btn.querySelector("span").textContent = isDark ? "light_mode" : "dark_mode"; diff --git a/tests/e2e/modules_test.ts b/tests/e2e/modules_test.ts index 7b33ca0..3077062 100644 --- a/tests/e2e/modules_test.ts +++ b/tests/e2e/modules_test.ts @@ -34,7 +34,7 @@ Deno.test({ }); Deno.test({ - name: "e2e modules: GET /modules returns empty for non-employee", + name: "e2e modules: GET /modules returns all for non-employee", async fn() { await truncateAll(); await seedModules([{ id: "MATH101", nom: "Mathématiques" }]); @@ -44,7 +44,7 @@ Deno.test({ ); assertEquals(res.status, 200); const body = await res.json(); - assertEquals(body.length, 0); + assertEquals(body.length, 1); }, sanitizeResources: false, sanitizeOps: false,
ModuleECUE Promo Coeff Actions
- Aucun module assigné + Aucun ECUE assigné