import { useEffect, useState } from "preact/hooks"; type Student = { numEtud: number; nom: string; prenom: string; idPromo: string }; type UE = { id: number; nom: string }; type UEModule = { idModule: string; idUE: number; idPromo: string; coeff: number }; type Module = { id: string; nom: string }; type Note = { numEtud: number; idModule: string; note: number }; type Ajustement = { numEtud: number; idUE: number; valeur: number }; type Props = { numEtud: number }; function fmt(n: number): string { return `${Math.round(n * 10) / 10}/20`; } function noteClass(n: number): string { return n >= 10 ? "note-chip note-chip--ok" : "note-chip note-chip--fail"; } export default function NoteRecap({ numEtud }: Props) { const [student, setStudent] = useState(null); const [ueList, setUeList] = useState([]); const [ueModules, setUeModules] = useState([]); const [moduleMap, setModuleMap] = useState>(new Map()); const [noteMap, setNoteMap] = useState>(new Map()); const [ajustements, setAjustements] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [editingNote, setEditingNote] = useState< { idModule: string; value: string } | null >(null); const [ajustInputs, setAjustInputs] = useState>({}); async function load() { try { const sRes = await fetch(`/students/api/students/${numEtud}`); if (!sRes.ok) throw new Error("Élève introuvable"); const s: Student = await sRes.json(); setStudent(s); const [uesRes, umRes, mRes, notesRes, ajustRes] = await Promise.all([ fetch("/notes/api/ues"), fetch( `/notes/api/ue-modules?idPromo=${encodeURIComponent(s.idPromo)}`, ), fetch("/admin/api/modules"), fetch(`/notes/api/notes?numEtud=${numEtud}`), fetch(`/notes/api/ajustements?numEtud=${numEtud}`), ]); if (uesRes.ok) setUeList(await uesRes.json()); if (umRes.ok) setUeModules(await umRes.json()); if (mRes.ok) { const mods: Module[] = await mRes.json(); setModuleMap(new Map(mods.map((m) => [m.id, m.nom]))); } if (notesRes.ok) { const ns: Note[] = await notesRes.json(); setNoteMap(new Map(ns.map((n) => [n.idModule, n.note]))); } if (ajustRes.ok) { const aj: Ajustement[] = await ajustRes.json(); setAjustements(aj); const inputs: Record = {}; for (const a of aj) inputs[a.idUE] = String(a.valeur); setAjustInputs(inputs); } } catch (e) { setError(e instanceof Error ? e.message : "Erreur"); } finally { setLoading(false); } } useEffect(() => { load(); }, [numEtud]); function calcAvg(ueMods: UEModule[]): number | null { let total = 0, coeff = 0; for (const um of ueMods) { const n = noteMap.get(um.idModule); if (n === undefined) return null; total += n * um.coeff; coeff += um.coeff; } return coeff > 0 ? total / coeff : null; } async function saveNote(idModule: string, value: string) { const note = parseFloat(value.replace(",", ".")); if (isNaN(note) || note < 0 || note > 20) { setEditingNote(null); return; } const res = await fetch( `/notes/api/notes/${numEtud}/${encodeURIComponent(idModule)}`, { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ note }), }, ); if (res.ok) { const updated: Note = await res.json(); setNoteMap((prev) => new Map(prev).set(idModule, updated.note)); } setEditingNote(null); } async function applyAjust(idUE: number) { const val = parseFloat((ajustInputs[idUE] ?? "").replace(",", ".")); if (isNaN(val) || val < 0 || val > 20) return; const existing = ajustements.find((a) => a.idUE === idUE); const res = existing ? await fetch(`/notes/api/ajustements/${numEtud}/${idUE}`, { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ valeur: val }), }) : await fetch("/notes/api/ajustements", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ numEtud, idUE, valeur: val }), }); if (res.ok) { const updated: Ajustement = await res.json(); setAjustements((prev) => existing ? prev.map((a) => a.idUE === idUE ? updated : a) : [...prev, updated] ); } } async function resetAjust(idUE: number) { const res = await fetch(`/notes/api/ajustements/${numEtud}/${idUE}`, { method: "DELETE", }); if (res.ok) { setAjustements((prev) => prev.filter((a) => a.idUE !== idUE)); setAjustInputs((prev) => { const c = { ...prev }; delete c[idUE]; return c; }); } } if (loading) { return (

Chargement…

); } if (error && !student) { return (

{error}

); } if (!student) return null; return (
← Retour à la liste

Récap notes – {student.prenom} {student.nom}

{student.numEtud} {student.prenom} {student.nom} {student.idPromo}
{error &&

{error}

} {ueList.length === 0 ? (

Aucune UE configurée pour cette promotion.

) : ueList.map((ue) => { const ueMods = ueModules.filter((um) => um.idUE === ue.id); const avg = calcAvg(ueMods); const ajust = ajustements.find((a) => a.idUE === ue.id); return (
{/* UE header */}

{ue.nom}

{avg !== null && ( Moy. calculée : {fmt(avg)} )} {ajust && ( ⚡ Ajust. actif : {fmt(ajust.valeur)} )}
{/* Module rows */} {ueMods.length === 0 ? (

Aucun module associé à cette UE pour cette promotion.

) : (
{ueMods.map((um) => { const noteVal = noteMap.get(um.idModule); const nomMod = moduleMap.get(um.idModule) ?? um.idModule; const isEditing = editingNote?.idModule === um.idModule; return (
{um.idModule} {nomMod} coef {um.coeff} {isEditing ? (
setEditingNote({ idModule: um.idModule, value: (e.target as HTMLInputElement).value, })} onKeyDown={(e) => { if (e.key === "Enter") { saveNote( um.idModule, editingNote!.value, ); } if (e.key === "Escape") { setEditingNote(null); } }} onBlur={() => saveNote(um.idModule, editingNote!.value)} /> /20
) : ( setEditingNote({ idModule: um.idModule, value: noteVal !== undefined ? String(noteVal) : "", })} > {noteVal !== undefined ? fmt(noteVal) : "—/20"} )}
); })}
)} {/* Ajustement */}

Ajustement de la moyenne UE

Override ponctuel – laisser vide pour utiliser la moy. calculée

setAjustInputs((prev) => ({ ...prev, [ue.id]: (e.target as HTMLInputElement).value, }))} /> /20
{ajust && ( <> Affiché à l'élève : {fmt(ajust.valeur)} {avg !== null ? ` (calculée : ${fmt(avg)})` : ""} )}
); })}
); }