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/compose.prod.yml b/compose.prod.yml
index 6d7f11a..a20b1e8 100644
--- a/compose.prod.yml
+++ b/compose.prod.yml
@@ -30,9 +30,12 @@ services:
ports:
- "4430:443"
env_file: .env
+ volumes:
+ - contracts:/app/uploads/contracts
depends_on:
migrate:
condition: service_completed_successfully
volumes:
db_data:
+ contracts:
diff --git a/databases/migrations/0003_add_session2_and_malus.sql b/databases/migrations/0003_add_session2_and_malus.sql
new file mode 100644
index 0000000..d3a950b
--- /dev/null
+++ b/databases/migrations/0003_add_session2_and_malus.sql
@@ -0,0 +1,3 @@
+ALTER TABLE "notes" ADD COLUMN "noteSession2" double precision;
+--> statement-breakpoint
+ALTER TABLE "ajustements" ADD COLUMN "malus" integer NOT NULL DEFAULT 0;
diff --git a/databases/migrations/0004_add_stages_and_mobilites.sql b/databases/migrations/0004_add_stages_and_mobilites.sql
new file mode 100644
index 0000000..a1f8a5d
--- /dev/null
+++ b/databases/migrations/0004_add_stages_and_mobilites.sql
@@ -0,0 +1,28 @@
+DROP TABLE IF EXISTS "mobility";
+--> statement-breakpoint
+CREATE TYPE "mobility_status" AS ENUM ('contracts_received', 'under_revision', 'done', 'validated', 'canceled');
+--> statement-breakpoint
+CREATE TABLE "stages" (
+ "idStage" serial PRIMARY KEY NOT NULL,
+ "numEtud" integer NOT NULL,
+ "duree" integer NOT NULL,
+ "nomEntreprise" text NOT NULL,
+ "mission" text
+);
+--> statement-breakpoint
+CREATE TABLE "mobilites" (
+ "idMob" serial PRIMARY KEY NOT NULL,
+ "numEtud" integer NOT NULL,
+ "duree" integer NOT NULL,
+ "contratMob" text,
+ "ecole" text,
+ "pays" text,
+ "status" "mobility_status" NOT NULL DEFAULT 'contracts_received',
+ "idStage" integer
+);
+--> statement-breakpoint
+ALTER TABLE "stages" ADD CONSTRAINT "stages_numEtud_students_numEtud_fk" FOREIGN KEY ("numEtud") REFERENCES "public"."students"("numEtud") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "mobilites" ADD CONSTRAINT "mobilites_numEtud_students_numEtud_fk" FOREIGN KEY ("numEtud") REFERENCES "public"."students"("numEtud") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "mobilites" ADD CONSTRAINT "mobilites_idStage_stages_idStage_fk" FOREIGN KEY ("idStage") REFERENCES "public"."stages"("idStage") ON DELETE no action ON UPDATE no action;
diff --git a/databases/migrations/meta/_journal.json b/databases/migrations/meta/_journal.json
index e4f070f..3cb93bd 100644
--- a/databases/migrations/meta/_journal.json
+++ b/databases/migrations/meta/_journal.json
@@ -22,6 +22,20 @@
"when": 1777155028710,
"tag": "0002_update_permission_names",
"breakpoints": true
+ },
+ {
+ "idx": 3,
+ "version": "7",
+ "when": 1777155028711,
+ "tag": "0003_add_session2_and_malus",
+ "breakpoints": true
+ },
+ {
+ "idx": 4,
+ "version": "7",
+ "when": 1777155028712,
+ "tag": "0004_add_stages_and_mobilites",
+ "breakpoints": true
}
]
}
diff --git a/databases/schema.ts b/databases/schema.ts
index 823c7a2..eadbb3a 100644
--- a/databases/schema.ts
+++ b/databases/schema.ts
@@ -1,7 +1,7 @@
import {
- date,
doublePrecision,
integer,
+ pgEnum,
pgTable,
primaryKey,
serial,
@@ -75,6 +75,7 @@ export const notes = pgTable("notes", {
numEtud: integer("numEtud").notNull().references(() => students.numEtud),
idModule: text("idModule").notNull().references(() => modules.id),
note: doublePrecision("note").notNull(),
+ noteSession2: doublePrecision("noteSession2"),
}, (t) => ({
pk: primaryKey({ columns: [t.numEtud, t.idModule] }),
}));
@@ -83,17 +84,34 @@ export const ajustements = pgTable("ajustements", {
numEtud: integer("numEtud").notNull().references(() => students.numEtud),
idUE: integer("idUE").notNull().references(() => ues.id),
valeur: doublePrecision("valeur").notNull(),
+ malus: integer("malus").notNull().default(0),
}, (t) => ({
pk: primaryKey({ columns: [t.numEtud, t.idUE] }),
}));
-export const mobility = pgTable("mobility", {
- id: serial("id").primaryKey(),
- studentId: integer("studentId").references(() => students.numEtud),
- startDate: date("startDate"),
- endDate: date("endDate"),
- weeksCount: integer("weeksCount"),
- destinationCountry: text("destinationCountry"),
- destinationName: text("destinationName"),
- mobilityStatus: text("mobilityStatus").default("N/A"),
+export const stages = pgTable("stages", {
+ id: serial("idStage").primaryKey(),
+ numEtud: integer("numEtud").notNull().references(() => students.numEtud),
+ duree: integer("duree").notNull(),
+ nomEntreprise: text("nomEntreprise").notNull(),
+ mission: text("mission"),
+});
+
+export const mobilityStatusEnum = pgEnum("mobility_status", [
+ "contracts_received",
+ "under_revision",
+ "done",
+ "validated",
+ "canceled",
+]);
+
+export const mobilites = pgTable("mobilites", {
+ id: serial("idMob").primaryKey(),
+ numEtud: integer("numEtud").notNull().references(() => students.numEtud),
+ duree: integer("duree").notNull(),
+ contratMob: text("contratMob"),
+ ecole: text("ecole"),
+ pays: text("pays"),
+ status: mobilityStatusEnum("status").notNull().default("contracts_received"),
+ idStage: integer("idStage").references(() => stages.id),
});
diff --git a/defaults/ImportResultPopup.tsx b/defaults/ImportResultPopup.tsx
new file mode 100644
index 0000000..075db00
--- /dev/null
+++ b/defaults/ImportResultPopup.tsx
@@ -0,0 +1,102 @@
+import { useState } from "preact/hooks";
+
+export type ImportResult = {
+ added: number;
+ modified: number;
+ ignored: number;
+ errors: number;
+ details: ImportDetail[];
+};
+
+export type ImportDetail = {
+ type: "change" | "error";
+ message: string;
+};
+
+type Props = {
+ result: ImportResult;
+ onClose: () => void;
+};
+
+export default function ImportResultPopup({ result, onClose }: Props) {
+ const [showDetails, setShowDetails] = useState(false);
+ const hasErrors = result.errors > 0;
+ const changes = result.details.filter((d) => d.type === "change");
+ const errors = result.details.filter((d) => d.type === "error");
+
+ return (
+
+ );
+}
diff --git a/defaults/interfaces.ts b/defaults/interfaces.ts
index f385846..951201a 100644
--- a/defaults/interfaces.ts
+++ b/defaults/interfaces.ts
@@ -19,6 +19,8 @@ export interface AppProperties {
icon: string;
pages: Record;
adminOnly: string[];
+ studentOnly?: string[];
+ employeeOnly?: boolean;
hint: string;
}
diff --git a/defaults/makeSlug.ts b/defaults/makeSlug.ts
new file mode 100644
index 0000000..d1110fe
--- /dev/null
+++ b/defaults/makeSlug.ts
@@ -0,0 +1,62 @@
+import { FreshContext } from "$fresh/server.ts";
+import { Route, State } from "$root/defaults/interfaces.ts";
+import { ComponentChildren } from "preact";
+
+/**
+ * Generates a catch-all [slug] route that dynamically loads partials.
+ * This enables direct URL navigation to sub-pages (e.g. /admin/modules).
+ * @param basePath The base path of the module, should be `import.meta.dirname!`.
+ * @returns A route handler that loads the partial matching the slug.
+ */
+export default function makeSlug(basePath: string): Route {
+ return async function SlugRoute(
+ request: Request,
+ context: FreshContext,
+ ): Promise {
+ const slug = context.params.slug;
+
+ // Try partials/.tsx, then partials/(admin)/.tsx
+ let page: Route | undefined;
+ try {
+ page = (await import(`${basePath}/partials/${slug}.tsx`)).Page;
+ } catch {
+ try {
+ page = (await import(`${basePath}/partials/(admin)/${slug}.tsx`)).Page;
+ } catch {
+ // No partial found for this slug
+ }
+ }
+
+ // 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();
+ }
+
+ return page(request, context);
+ };
+}
diff --git a/fresh.gen.ts b/fresh.gen.ts
index 22cab59..d119210 100644
--- a/fresh.gen.ts
+++ b/fresh.gen.ts
@@ -4,6 +4,7 @@
import * as $_apps_layout from "./routes/(apps)/_layout.tsx";
import * as $_apps_middleware from "./routes/(apps)/_middleware.ts";
+import * as $_apps_admin_slug_ from "./routes/(apps)/admin/[slug].tsx";
import * as $_apps_admin_api_enseignements from "./routes/(apps)/admin/api/enseignements.ts";
import * as $_apps_admin_api_enseignements_idProf_idModule_idPromo_ from "./routes/(apps)/admin/api/enseignements/[idProf]/[idModule]/[idPromo].ts";
import * as $_apps_admin_api_example from "./routes/(apps)/admin/api/example.ts";
@@ -12,36 +13,56 @@ import * as $_apps_admin_api_modules_idModule_ from "./routes/(apps)/admin/api/m
import * as $_apps_admin_api_permissions from "./routes/(apps)/admin/api/permissions.ts";
import * as $_apps_admin_api_roles from "./routes/(apps)/admin/api/roles.ts";
import * as $_apps_admin_api_roles_idRole_ from "./routes/(apps)/admin/api/roles/[idRole].ts";
+import * as $_apps_admin_api_ue_modules from "./routes/(apps)/admin/api/ue-modules.ts";
+import * as $_apps_admin_api_ue_modules_idModule_idUE_idPromo_ from "./routes/(apps)/admin/api/ue-modules/[idModule]/[idUE]/[idPromo].ts";
+import * as $_apps_admin_api_ues from "./routes/(apps)/admin/api/ues.ts";
+import * as $_apps_admin_api_ues_idUE_ from "./routes/(apps)/admin/api/ues/[idUE].ts";
import * as $_apps_admin_api_users from "./routes/(apps)/admin/api/users.ts";
import * as $_apps_admin_api_users_id_ from "./routes/(apps)/admin/api/users/[id].ts";
import * as $_apps_admin_index from "./routes/(apps)/admin/index.tsx";
+import * as $_apps_admin_modules_idModule_ from "./routes/(apps)/admin/modules/[idModule].tsx";
import * as $_apps_admin_partials_enseignements from "./routes/(apps)/admin/partials/enseignements.tsx";
+import * as $_apps_admin_partials_import_maquette from "./routes/(apps)/admin/partials/import-maquette.tsx";
import * as $_apps_admin_partials_index from "./routes/(apps)/admin/partials/index.tsx";
import * as $_apps_admin_partials_modules from "./routes/(apps)/admin/partials/modules.tsx";
import * as $_apps_admin_partials_permissions from "./routes/(apps)/admin/partials/permissions.tsx";
+import * as $_apps_admin_partials_promotions from "./routes/(apps)/admin/partials/promotions.tsx";
import * as $_apps_admin_partials_roles from "./routes/(apps)/admin/partials/roles.tsx";
+import * as $_apps_admin_partials_ues from "./routes/(apps)/admin/partials/ues.tsx";
import * as $_apps_admin_partials_users from "./routes/(apps)/admin/partials/users.tsx";
-import * as $_apps_mobility_api_insert_mobility from "./routes/(apps)/mobility/api/insert_mobility.ts";
+import * as $_apps_admin_users_id_ from "./routes/(apps)/admin/users/[id].tsx";
+import * as $_apps_mobility_slug_ from "./routes/(apps)/mobility/[...slug].tsx";
+import * as $_apps_mobility_api_mobilites from "./routes/(apps)/mobility/api/mobilites.ts";
+import * as $_apps_mobility_api_mobilites_idMob_ from "./routes/(apps)/mobility/api/mobilites/[idMob].ts";
+import * as $_apps_mobility_api_mobilites_idMob_contrat from "./routes/(apps)/mobility/api/mobilites/[idMob]/contrat.ts";
import * as $_apps_mobility_index from "./routes/(apps)/mobility/index.tsx";
-import * as $_apps_mobility_partials_admin_edit_mobility from "./routes/(apps)/mobility/partials/(admin)/edit_mobility.tsx";
import * as $_apps_mobility_partials_index from "./routes/(apps)/mobility/partials/index.tsx";
import * as $_apps_mobility_partials_overview from "./routes/(apps)/mobility/partials/overview.tsx";
+import * as $_apps_mobility_partials_overview_numEtud_ from "./routes/(apps)/mobility/partials/overview/[numEtud].tsx";
+import * as $_apps_notes_slug_ from "./routes/(apps)/notes/[slug].tsx";
import * as $_apps_notes_api_ajustements from "./routes/(apps)/notes/api/ajustements.ts";
import * as $_apps_notes_api_ajustements_numEtud_idUE_ from "./routes/(apps)/notes/api/ajustements/[numEtud]/[idUE].ts";
+import * as $_apps_notes_api_modules from "./routes/(apps)/notes/api/modules.ts";
import * as $_apps_notes_api_notes from "./routes/(apps)/notes/api/notes.ts";
import * as $_apps_notes_api_notes_numEtud_idModule_ from "./routes/(apps)/notes/api/notes/[numEtud]/[idModule].ts";
+import * as $_apps_notes_api_notes_import_xlsx from "./routes/(apps)/notes/api/notes/import-xlsx.ts";
import * as $_apps_notes_api_ue_modules from "./routes/(apps)/notes/api/ue-modules.ts";
-import * as $_apps_notes_api_ue_modules_idModule_idUE_idPromo_ from "./routes/(apps)/notes/api/ue-modules/[idModule]/[idUE]/[idPromo].ts";
import * as $_apps_notes_api_ues from "./routes/(apps)/notes/api/ues.ts";
-import * as $_apps_notes_api_ues_idUE_ from "./routes/(apps)/notes/api/ues/[idUE].ts";
import * as $_apps_notes_edition_numEtud_ from "./routes/(apps)/notes/edition/[numEtud].tsx";
import * as $_apps_notes_index from "./routes/(apps)/notes/index.tsx";
-import * as $_apps_notes_recap_numEtud_ from "./routes/(apps)/notes/recap/[numEtud].tsx";
import * as $_apps_notes_partials_admin_courses from "./routes/(apps)/notes/partials/(admin)/courses.tsx";
import * as $_apps_notes_partials_admin_import from "./routes/(apps)/notes/partials/(admin)/import.tsx";
-import * as $_apps_notes_partials_admin_ues from "./routes/(apps)/notes/partials/(admin)/ues.tsx";
import * as $_apps_notes_partials_index from "./routes/(apps)/notes/partials/index.tsx";
import * as $_apps_notes_partials_notes from "./routes/(apps)/notes/partials/notes.tsx";
+import * as $_apps_notes_recap_numEtud_ from "./routes/(apps)/notes/recap/[numEtud].tsx";
+import * as $_apps_stages_slug_ from "./routes/(apps)/stages/[...slug].tsx";
+import * as $_apps_stages_api_stages from "./routes/(apps)/stages/api/stages.ts";
+import * as $_apps_stages_api_stages_idStage_ from "./routes/(apps)/stages/api/stages/[idStage].ts";
+import * as $_apps_stages_index from "./routes/(apps)/stages/index.tsx";
+import * as $_apps_stages_partials_index from "./routes/(apps)/stages/partials/index.tsx";
+import * as $_apps_stages_partials_overview from "./routes/(apps)/stages/partials/overview.tsx";
+import * as $_apps_stages_partials_overview_numEtud_ from "./routes/(apps)/stages/partials/overview/[numEtud].tsx";
+import * as $_apps_students_slug_ from "./routes/(apps)/students/[slug].tsx";
import * as $_apps_students_api_promotions from "./routes/(apps)/students/api/promotions.ts";
import * as $_apps_students_api_promotions_idPromo_ from "./routes/(apps)/students/api/promotions/[idPromo].ts";
import * as $_apps_students_api_students from "./routes/(apps)/students/api/students.ts";
@@ -50,7 +71,6 @@ import * as $_apps_students_api_students_import_csv from "./routes/(apps)/studen
import * as $_apps_students_edit_numEtud_ from "./routes/(apps)/students/edit/[numEtud].tsx";
import * as $_apps_students_index from "./routes/(apps)/students/index.tsx";
import * as $_apps_students_partials_admin_consult from "./routes/(apps)/students/partials/(admin)/consult.tsx";
-import * as $_apps_students_partials_admin_promotions from "./routes/(apps)/students/partials/(admin)/promotions.tsx";
import * as $_apps_students_partials_admin_upload from "./routes/(apps)/students/partials/(admin)/upload.tsx";
import * as $_apps_students_partials_index from "./routes/(apps)/students/partials/index.tsx";
import * as $_apps_students_types_d from "./routes/(apps)/students/types.d.ts";
@@ -68,17 +88,19 @@ import * as $_islands_Navbar from "./routes/(_islands)/Navbar.tsx";
import * as $_apps_admin_islands_AdminEnseignements from "./routes/(apps)/admin/(_islands)/AdminEnseignements.tsx";
import * as $_apps_admin_islands_AdminModules from "./routes/(apps)/admin/(_islands)/AdminModules.tsx";
import * as $_apps_admin_islands_AdminPermissions from "./routes/(apps)/admin/(_islands)/AdminPermissions.tsx";
+import * as $_apps_admin_islands_AdminPromotions from "./routes/(apps)/admin/(_islands)/AdminPromotions.tsx";
import * as $_apps_admin_islands_AdminRoles from "./routes/(apps)/admin/(_islands)/AdminRoles.tsx";
+import * as $_apps_admin_islands_AdminUEs from "./routes/(apps)/admin/(_islands)/AdminUEs.tsx";
import * as $_apps_admin_islands_AdminUsers from "./routes/(apps)/admin/(_islands)/AdminUsers.tsx";
-import * as $_apps_mobility_islands_ConsultMobility from "./routes/(apps)/mobility/(_islands)/ConsultMobility.tsx";
-import * as $_apps_mobility_islands_EditMobility from "./routes/(apps)/mobility/(_islands)/EditMobility.tsx";
-import * as $_apps_mobility_islands_ImportFile from "./routes/(apps)/mobility/(_islands)/ImportFile.tsx";
+import * as $_apps_admin_islands_EditModule from "./routes/(apps)/admin/(_islands)/EditModule.tsx";
+import * as $_apps_admin_islands_EditUser from "./routes/(apps)/admin/(_islands)/EditUser.tsx";
+import * as $_apps_admin_islands_ImportMaquette from "./routes/(apps)/admin/(_islands)/ImportMaquette.tsx";
+import * as $_apps_mobility_islands_MobilityOverview from "./routes/(apps)/mobility/(_islands)/MobilityOverview.tsx";
import * as $_apps_notes_islands_AdminConsultNotes from "./routes/(apps)/notes/(_islands)/AdminConsultNotes.tsx";
-import * as $_apps_notes_islands_AdminUEs from "./routes/(apps)/notes/(_islands)/AdminUEs.tsx";
import * as $_apps_notes_islands_ImportNotes from "./routes/(apps)/notes/(_islands)/ImportNotes.tsx";
import * as $_apps_notes_islands_NoteRecap from "./routes/(apps)/notes/(_islands)/NoteRecap.tsx";
import * as $_apps_notes_islands_NotesView from "./routes/(apps)/notes/(_islands)/NotesView.tsx";
-import * as $_apps_students_islands_AdminPromotions from "./routes/(apps)/students/(_islands)/AdminPromotions.tsx";
+import * as $_apps_stages_islands_StagesOverview from "./routes/(apps)/stages/(_islands)/StagesOverview.tsx";
import * as $_apps_students_islands_ConsultStudents from "./routes/(apps)/students/(_islands)/ConsultStudents.tsx";
import * as $_apps_students_islands_EditStudents from "./routes/(apps)/students/(_islands)/EditStudents.tsx";
import * as $_apps_students_islands_UploadStudents from "./routes/(apps)/students/(_islands)/UploadStudents.tsx";
@@ -88,6 +110,7 @@ const manifest = {
routes: {
"./routes/(apps)/_layout.tsx": $_apps_layout,
"./routes/(apps)/_middleware.ts": $_apps_middleware,
+ "./routes/(apps)/admin/[slug].tsx": $_apps_admin_slug_,
"./routes/(apps)/admin/api/enseignements.ts":
$_apps_admin_api_enseignements,
"./routes/(apps)/admin/api/enseignements/[idProf]/[idModule]/[idPromo].ts":
@@ -100,49 +123,76 @@ const manifest = {
"./routes/(apps)/admin/api/roles.ts": $_apps_admin_api_roles,
"./routes/(apps)/admin/api/roles/[idRole].ts":
$_apps_admin_api_roles_idRole_,
+ "./routes/(apps)/admin/api/ue-modules.ts": $_apps_admin_api_ue_modules,
+ "./routes/(apps)/admin/api/ue-modules/[idModule]/[idUE]/[idPromo].ts":
+ $_apps_admin_api_ue_modules_idModule_idUE_idPromo_,
+ "./routes/(apps)/admin/api/ues.ts": $_apps_admin_api_ues,
+ "./routes/(apps)/admin/api/ues/[idUE].ts": $_apps_admin_api_ues_idUE_,
"./routes/(apps)/admin/api/users.ts": $_apps_admin_api_users,
"./routes/(apps)/admin/api/users/[id].ts": $_apps_admin_api_users_id_,
"./routes/(apps)/admin/index.tsx": $_apps_admin_index,
+ "./routes/(apps)/admin/modules/[idModule].tsx":
+ $_apps_admin_modules_idModule_,
"./routes/(apps)/admin/partials/enseignements.tsx":
$_apps_admin_partials_enseignements,
+ "./routes/(apps)/admin/partials/import-maquette.tsx":
+ $_apps_admin_partials_import_maquette,
"./routes/(apps)/admin/partials/index.tsx": $_apps_admin_partials_index,
"./routes/(apps)/admin/partials/modules.tsx": $_apps_admin_partials_modules,
"./routes/(apps)/admin/partials/permissions.tsx":
$_apps_admin_partials_permissions,
+ "./routes/(apps)/admin/partials/promotions.tsx":
+ $_apps_admin_partials_promotions,
"./routes/(apps)/admin/partials/roles.tsx": $_apps_admin_partials_roles,
+ "./routes/(apps)/admin/partials/ues.tsx": $_apps_admin_partials_ues,
"./routes/(apps)/admin/partials/users.tsx": $_apps_admin_partials_users,
- "./routes/(apps)/mobility/api/insert_mobility.ts":
- $_apps_mobility_api_insert_mobility,
+ "./routes/(apps)/admin/users/[id].tsx": $_apps_admin_users_id_,
+ "./routes/(apps)/mobility/[...slug].tsx": $_apps_mobility_slug_,
+ "./routes/(apps)/mobility/api/mobilites.ts": $_apps_mobility_api_mobilites,
+ "./routes/(apps)/mobility/api/mobilites/[idMob].ts":
+ $_apps_mobility_api_mobilites_idMob_,
+ "./routes/(apps)/mobility/api/mobilites/[idMob]/contrat.ts":
+ $_apps_mobility_api_mobilites_idMob_contrat,
"./routes/(apps)/mobility/index.tsx": $_apps_mobility_index,
- "./routes/(apps)/mobility/partials/(admin)/edit_mobility.tsx":
- $_apps_mobility_partials_admin_edit_mobility,
"./routes/(apps)/mobility/partials/index.tsx":
$_apps_mobility_partials_index,
"./routes/(apps)/mobility/partials/overview.tsx":
$_apps_mobility_partials_overview,
+ "./routes/(apps)/mobility/partials/overview/[numEtud].tsx":
+ $_apps_mobility_partials_overview_numEtud_,
+ "./routes/(apps)/notes/[slug].tsx": $_apps_notes_slug_,
"./routes/(apps)/notes/api/ajustements.ts": $_apps_notes_api_ajustements,
"./routes/(apps)/notes/api/ajustements/[numEtud]/[idUE].ts":
$_apps_notes_api_ajustements_numEtud_idUE_,
+ "./routes/(apps)/notes/api/modules.ts": $_apps_notes_api_modules,
"./routes/(apps)/notes/api/notes.ts": $_apps_notes_api_notes,
"./routes/(apps)/notes/api/notes/[numEtud]/[idModule].ts":
$_apps_notes_api_notes_numEtud_idModule_,
+ "./routes/(apps)/notes/api/notes/import-xlsx.ts":
+ $_apps_notes_api_notes_import_xlsx,
"./routes/(apps)/notes/api/ue-modules.ts": $_apps_notes_api_ue_modules,
- "./routes/(apps)/notes/api/ue-modules/[idModule]/[idUE]/[idPromo].ts":
- $_apps_notes_api_ue_modules_idModule_idUE_idPromo_,
"./routes/(apps)/notes/api/ues.ts": $_apps_notes_api_ues,
- "./routes/(apps)/notes/api/ues/[idUE].ts": $_apps_notes_api_ues_idUE_,
"./routes/(apps)/notes/edition/[numEtud].tsx":
$_apps_notes_edition_numEtud_,
"./routes/(apps)/notes/index.tsx": $_apps_notes_index,
- "./routes/(apps)/notes/recap/[numEtud].tsx": $_apps_notes_recap_numEtud_,
"./routes/(apps)/notes/partials/(admin)/courses.tsx":
$_apps_notes_partials_admin_courses,
"./routes/(apps)/notes/partials/(admin)/import.tsx":
$_apps_notes_partials_admin_import,
- "./routes/(apps)/notes/partials/(admin)/ues.tsx":
- $_apps_notes_partials_admin_ues,
"./routes/(apps)/notes/partials/index.tsx": $_apps_notes_partials_index,
"./routes/(apps)/notes/partials/notes.tsx": $_apps_notes_partials_notes,
+ "./routes/(apps)/notes/recap/[numEtud].tsx": $_apps_notes_recap_numEtud_,
+ "./routes/(apps)/stages/[...slug].tsx": $_apps_stages_slug_,
+ "./routes/(apps)/stages/api/stages.ts": $_apps_stages_api_stages,
+ "./routes/(apps)/stages/api/stages/[idStage].ts":
+ $_apps_stages_api_stages_idStage_,
+ "./routes/(apps)/stages/index.tsx": $_apps_stages_index,
+ "./routes/(apps)/stages/partials/index.tsx": $_apps_stages_partials_index,
+ "./routes/(apps)/stages/partials/overview.tsx":
+ $_apps_stages_partials_overview,
+ "./routes/(apps)/stages/partials/overview/[numEtud].tsx":
+ $_apps_stages_partials_overview_numEtud_,
+ "./routes/(apps)/students/[slug].tsx": $_apps_students_slug_,
"./routes/(apps)/students/api/promotions.ts":
$_apps_students_api_promotions,
"./routes/(apps)/students/api/promotions/[idPromo].ts":
@@ -157,8 +207,6 @@ const manifest = {
"./routes/(apps)/students/index.tsx": $_apps_students_index,
"./routes/(apps)/students/partials/(admin)/consult.tsx":
$_apps_students_partials_admin_consult,
- "./routes/(apps)/students/partials/(admin)/promotions.tsx":
- $_apps_students_partials_admin_promotions,
"./routes/(apps)/students/partials/(admin)/upload.tsx":
$_apps_students_partials_admin_upload,
"./routes/(apps)/students/partials/index.tsx":
@@ -183,28 +231,32 @@ const manifest = {
$_apps_admin_islands_AdminModules,
"./routes/(apps)/admin/(_islands)/AdminPermissions.tsx":
$_apps_admin_islands_AdminPermissions,
+ "./routes/(apps)/admin/(_islands)/AdminPromotions.tsx":
+ $_apps_admin_islands_AdminPromotions,
"./routes/(apps)/admin/(_islands)/AdminRoles.tsx":
$_apps_admin_islands_AdminRoles,
+ "./routes/(apps)/admin/(_islands)/AdminUEs.tsx":
+ $_apps_admin_islands_AdminUEs,
"./routes/(apps)/admin/(_islands)/AdminUsers.tsx":
$_apps_admin_islands_AdminUsers,
- "./routes/(apps)/mobility/(_islands)/ConsultMobility.tsx":
- $_apps_mobility_islands_ConsultMobility,
- "./routes/(apps)/mobility/(_islands)/EditMobility.tsx":
- $_apps_mobility_islands_EditMobility,
- "./routes/(apps)/mobility/(_islands)/ImportFile.tsx":
- $_apps_mobility_islands_ImportFile,
+ "./routes/(apps)/admin/(_islands)/EditModule.tsx":
+ $_apps_admin_islands_EditModule,
+ "./routes/(apps)/admin/(_islands)/EditUser.tsx":
+ $_apps_admin_islands_EditUser,
+ "./routes/(apps)/admin/(_islands)/ImportMaquette.tsx":
+ $_apps_admin_islands_ImportMaquette,
+ "./routes/(apps)/mobility/(_islands)/MobilityOverview.tsx":
+ $_apps_mobility_islands_MobilityOverview,
"./routes/(apps)/notes/(_islands)/AdminConsultNotes.tsx":
$_apps_notes_islands_AdminConsultNotes,
- "./routes/(apps)/notes/(_islands)/AdminUEs.tsx":
- $_apps_notes_islands_AdminUEs,
"./routes/(apps)/notes/(_islands)/ImportNotes.tsx":
$_apps_notes_islands_ImportNotes,
"./routes/(apps)/notes/(_islands)/NoteRecap.tsx":
$_apps_notes_islands_NoteRecap,
"./routes/(apps)/notes/(_islands)/NotesView.tsx":
$_apps_notes_islands_NotesView,
- "./routes/(apps)/students/(_islands)/AdminPromotions.tsx":
- $_apps_students_islands_AdminPromotions,
+ "./routes/(apps)/stages/(_islands)/StagesOverview.tsx":
+ $_apps_stages_islands_StagesOverview,
"./routes/(apps)/students/(_islands)/ConsultStudents.tsx":
$_apps_students_islands_ConsultStudents,
"./routes/(apps)/students/(_islands)/EditStudents.tsx":
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/(_components)/Header.tsx b/routes/(_components)/Header.tsx
index 34853ad..ec5573b 100644
--- a/routes/(_components)/Header.tsx
+++ b/routes/(_components)/Header.tsx
@@ -11,6 +11,14 @@ export default function Header(props: HeaderProps) {
Catalog
Log {props.link}
+
+ dark_mode
+
);
diff --git a/routes/(apps)/_middleware.ts b/routes/(apps)/_middleware.ts
index f30f19f..a671134 100644
--- a/routes/(apps)/_middleware.ts
+++ b/routes/(apps)/_middleware.ts
@@ -21,14 +21,29 @@ export const handler: MiddlewareHandler[] = [
`./${currentApp}/(_props)/props.ts`
)).default;
- context.state.availablePages = properties.pages;
- if (
- context.state.session.eduPersonPrimaryAffiliation == "student" &&
- Deno.env.get("LOCAL") != "true"
- ) {
+ const isStudent =
+ context.state.session.eduPersonPrimaryAffiliation === "student";
+ const isLocal = Deno.env.get("LOCAL") === "true";
+
+ // Block students from accessing employeeOnly modules entirely
+ if (isStudent && properties.employeeOnly) {
+ return new Response(null, { status: 403 });
+ }
+
+ context.state.availablePages = { ...properties.pages };
+
+ if (isStudent) {
+ // Students only see studentOnly pages (+ non-restricted pages)
properties.adminOnly.forEach((page) =>
delete context.state.availablePages[page]
);
+ } else if (isLocal) {
+ // In local mode, employees see all pages (admin + student)
+ } else {
+ // In prod, employees don't see studentOnly pages
+ properties.studentOnly?.forEach((page) =>
+ delete context.state.availablePages[page]
+ );
}
return await context.next();
diff --git a/routes/(apps)/admin/(_islands)/AdminEnseignements.tsx b/routes/(apps)/admin/(_islands)/AdminEnseignements.tsx
index 7b158d2..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)}
>
-
Module ▾
+
ECUE ▾
{modules.map((m) => (
{m.id} – {m.nom}
))}
@@ -169,55 +169,77 @@ export default function AdminEnseignements() {
{showAdd && (
-