feat: cascade deletes, student notes, import popups, module reorganization

- Cascade delete on all entities (student, module, UE, user, role, promotion)
- Fix Response body reuse bug (factory functions instead of constants)
- Student note viewing via CAS uid (strip non-digit prefix)
- Fix middleware page visibility for students in LOCAL mode
- Import result popup component (shared across all import pages)
- Fix student import to use numEtud from Excel
- Bulk student selection with promo change and delete
- Move UE/UE-Module API and pages from notes to admin module
- Move promotions page from students to admin module
- Multi-year maquette import with per-year promo selection
- Inline promo creation in maquette import
- Static Excel templates (students, notes, maquette)
- Fix XLSX export using blob download instead of writeFile
- Allow students to read modules list (GET /modules)
This commit is contained in:
2026-04-30 13:47:16 +02:00
parent 04be659d6b
commit 6c38cd0019
51 changed files with 3022 additions and 437 deletions
@@ -1,18 +0,0 @@
import {
getPartialsConfig,
makePartials,
} from "$root/defaults/makePartials.tsx";
import { FreshContext } from "$fresh/server.ts";
import { State } from "$root/defaults/interfaces.ts";
import AdminUEs from "../../(_islands)/AdminUEs.tsx";
// deno-lint-ignore require-await
async function UEs(
_request: Request,
_context: FreshContext<State>,
) {
return <AdminUEs />;
}
export const config = getPartialsConfig();
export default makePartials(UEs);
+33 -12
View File
@@ -6,31 +6,52 @@ import {
getPartialsConfig,
makePartials,
} from "$root/defaults/makePartials.tsx";
import { State } from "$root/defaults/interfaces.ts";
import { CasContent, State } from "$root/defaults/interfaces.ts";
import NotesView from "../(_islands)/NotesView.tsx";
async function Notes(
_request: Request,
context: FreshContext<State>,
) {
const session =
(context.state as unknown as { session: { sn: string; givenName: string } })
.session;
const { sn, givenName } = session;
const session = (context.state as unknown as { session: CasContent }).session;
let numEtud: number | null = null;
try {
const student = await db
.select()
.from(students)
.where(and(eq(students.nom, sn), eq(students.prenom, givenName)))
.then((rows) => rows[0] ?? null);
numEtud = student?.numEtud ?? null;
if (session.eduPersonPrimaryAffiliation === "student") {
// Students: uid is "<letter>21212006" in AMU CAS — strip non-digit prefix
const etudId = parseInt(session.uid.replace(/^\D+/, ""), 10);
if (!isNaN(etudId)) {
const student = await db
.select()
.from(students)
.where(eq(students.numEtud, etudId))
.then((rows) => rows[0] ?? null);
numEtud = student?.numEtud ?? null;
}
} else {
// Employees: look up by nom/prenom
const student = await db
.select()
.from(students)
.where(
and(
eq(students.nom, session.sn),
eq(students.prenom, session.givenName),
),
)
.then((rows) => rows[0] ?? null);
numEtud = student?.numEtud ?? null;
}
} catch {
// DB lookup failed — island will show fallback message
}
return <NotesView numEtud={numEtud} prenom={session.givenName} />;
return (
<NotesView
numEtud={numEtud}
prenom={session.givenName || session.displayName}
/>
);
}
export const config = getPartialsConfig();