5ba8b8cb68
Add interactive island components and server partials for notes, students, and admin modules, following the Figma prototype design. - static/styles/ui.css: shared component library (buttons, tables, chips, cards, filters, tabs, form inputs) - notes: NotesView (student grade view with UE cards, promo tabs, weighted averages), AdminConsultNotes, AdminUEs islands + partials - students: ConsultStudents (list/filter/delete), AdminPromotions (CRUD) islands + partials - admin: AdminModules, AdminUsers, AdminRoles islands + partials - All partials use State type with unknown cast for session access Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
224 lines
6.5 KiB
TypeScript
224 lines
6.5 KiB
TypeScript
import { useEffect, useState } from "preact/hooks";
|
|
|
|
type Note = { numEtud: number; idModule: string; note: number };
|
|
type UE = { id: number; nom: string };
|
|
type UEModule = {
|
|
idModule: string;
|
|
idUE: number;
|
|
idPromo: string;
|
|
coeff: number;
|
|
};
|
|
type Module = { id: string; nom: string };
|
|
type Ajustement = { numEtud: number; idUE: number; valeur: number };
|
|
|
|
type Props = {
|
|
numEtud: number | null;
|
|
prenom: string;
|
|
};
|
|
|
|
function scoreClass(score: number | null): string {
|
|
if (score === null) return "score-none";
|
|
return score >= 10 ? "score-good" : "score-warn";
|
|
}
|
|
|
|
function avgClass(avg: number | null): string {
|
|
if (avg === null) return "";
|
|
return avg >= 10 ? "avg-good" : "avg-warn";
|
|
}
|
|
|
|
export default function NotesView({ numEtud, prenom }: Props) {
|
|
const [notes, setNotes] = useState<Note[]>([]);
|
|
const [ues, setUes] = useState<UE[]>([]);
|
|
const [ueModules, setUeModules] = useState<UEModule[]>([]);
|
|
const [modules, setModules] = useState<Module[]>([]);
|
|
const [ajustements, setAjustements] = useState<Ajustement[]>([]);
|
|
const [promos, setPromos] = useState<string[]>([]);
|
|
const [activePromo, setActivePromo] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (numEtud === null) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
async function load() {
|
|
try {
|
|
const [notesRes, uesRes, ueModRes, modRes, ajRes] = await Promise.all([
|
|
fetch(`/notes/api/notes?numEtud=${numEtud}`),
|
|
fetch("/notes/api/ues"),
|
|
fetch("/notes/api/ue-modules"),
|
|
fetch("/admin/api/modules"),
|
|
fetch(`/notes/api/ajustements?numEtud=${numEtud}`),
|
|
]);
|
|
|
|
if (!notesRes.ok || !uesRes.ok || !ueModRes.ok) {
|
|
throw new Error("Erreur lors du chargement");
|
|
}
|
|
|
|
const [notesData, uesData, ueModData, modData, ajData] = await Promise
|
|
.all([
|
|
notesRes.json(),
|
|
uesRes.json(),
|
|
ueModRes.json(),
|
|
modRes.ok ? modRes.json() : [],
|
|
ajRes.ok ? ajRes.json() : [],
|
|
]);
|
|
|
|
setNotes(notesData);
|
|
setUes(uesData);
|
|
setUeModules(ueModData);
|
|
setModules(modData);
|
|
setAjustements(ajData);
|
|
|
|
// Derive promos from UE-modules for this student's notes
|
|
const noteModuleIds = new Set(notesData.map((n: Note) => n.idModule));
|
|
const relevantPromos = [
|
|
...new Set(
|
|
ueModData
|
|
.filter((um: UEModule) => noteModuleIds.has(um.idModule))
|
|
.map((um: UEModule) => um.idPromo),
|
|
),
|
|
] as string[];
|
|
|
|
setPromos(relevantPromos);
|
|
if (relevantPromos.length > 0) setActivePromo(relevantPromos[0]);
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : "Erreur inconnue");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
load();
|
|
}, [numEtud]);
|
|
|
|
if (numEtud === null) {
|
|
return (
|
|
<div class="page-content">
|
|
<p class="state-empty">
|
|
Bonjour {prenom}{" "}
|
|
— aucun dossier étudiant n'est associé à votre compte.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div class="page-content">
|
|
<p class="state-loading">Chargement…</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div class="page-content">
|
|
<p class="state-error">{error}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Filter UE-modules by active promo
|
|
const filteredUeModules = activePromo
|
|
? ueModules.filter((um) => um.idPromo === activePromo)
|
|
: ueModules;
|
|
|
|
// Group UE-modules by UE
|
|
const ueIds = [...new Set(filteredUeModules.map((um) => um.idUE))];
|
|
|
|
const moduleMap = Object.fromEntries(modules.map((m) => [m.id, m]));
|
|
const noteMap = Object.fromEntries(
|
|
notes.map((n) => [n.idModule, n.note]),
|
|
);
|
|
const ajMap = Object.fromEntries(
|
|
ajustements.map((a) => [a.idUE, a.valeur]),
|
|
);
|
|
|
|
return (
|
|
<div class="page-content">
|
|
{promos.length > 1 && (
|
|
<div class="tabs">
|
|
{promos.map((p) => (
|
|
<button
|
|
type="button"
|
|
key={p}
|
|
class={`tab-btn${activePromo === p ? " active" : ""}`}
|
|
onClick={() => setActivePromo(p)}
|
|
>
|
|
{p}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{ueIds.length === 0 && (
|
|
<p class="state-empty">Aucune note disponible pour cette période.</p>
|
|
)}
|
|
|
|
{ueIds.map((ueId) => {
|
|
const ue = ues.find((u) => u.id === ueId);
|
|
if (!ue) return null;
|
|
|
|
const ueModsForUE = filteredUeModules.filter((um) => um.idUE === ueId);
|
|
let weightedSum = 0;
|
|
let coveredCoeff = 0;
|
|
ueModsForUE.forEach((um) => {
|
|
const note = noteMap[um.idModule];
|
|
if (note !== undefined) {
|
|
weightedSum += note * um.coeff;
|
|
coveredCoeff += um.coeff;
|
|
}
|
|
});
|
|
|
|
const avg = coveredCoeff > 0 ? weightedSum / coveredCoeff : null;
|
|
const ajustement = ajMap[ueId] ?? null;
|
|
const finalAvg = avg !== null && ajustement !== null
|
|
? avg + ajustement
|
|
: avg;
|
|
|
|
return (
|
|
<div key={ueId} class="ue-card">
|
|
<div class="ue-card-header">
|
|
<p class="ue-card-title">UE : {ue.nom}</p>
|
|
{finalAvg !== null && (
|
|
<p class={`ue-card-avg ${avgClass(finalAvg)}`}>
|
|
Moyenne : {finalAvg.toFixed(2)}/20
|
|
{ajustement !== null && ajustement !== 0 && (
|
|
<span>
|
|
{" "}
|
|
(ajustement : {ajustement > 0 ? "+" : ""}
|
|
{ajustement})
|
|
</span>
|
|
)}
|
|
</p>
|
|
)}
|
|
{finalAvg === null && (
|
|
<p class="ue-card-avg avg-warn">Notes non disponibles</p>
|
|
)}
|
|
</div>
|
|
|
|
{ueModsForUE.map((um) => {
|
|
const mod = moduleMap[um.idModule];
|
|
const note = noteMap[um.idModule] ?? null;
|
|
return (
|
|
<div key={um.idModule} class="ue-module-row">
|
|
<span class="ue-module-name">
|
|
{mod ? mod.id : um.idModule} —{" "}
|
|
{mod ? mod.nom : "Module inconnu"} (coef {um.coeff})
|
|
</span>
|
|
<span class={`score-chip ${scoreClass(note)}`}>
|
|
{note !== null ? `${note}/20` : "—"}
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|