Files
PolyMPR/routes/(apps)/notes/(_islands)/NotesView.tsx
T

231 lines
6.6 KiB
TypeScript

import { useEffect, useState } from "preact/hooks";
import {
applyAjustement,
calculateWeightedAverage,
getEffectiveNote,
} from "$root/logic/grades.ts";
type Note = {
numEtud: number;
idModule: string;
note: number;
noteSession2: number | null;
};
// ... rest of types unchanged ...
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;
malus: 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("/notes/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);
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 etudiant n'est associe a 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>
);
}
const filteredUeModules = activePromo
? ueModules.filter((um) => um.idPromo === activePromo)
: ueModules;
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]),
);
const ajMap = Object.fromEntries(
ajustements.map((a) => [a.idUE, a]),
);
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 periode.</p>
)}
{ueIds.map((ueId) => {
const ue = ues.find((u) => u.id === ueId);
if (!ue) return null;
const ueModsForUE = filteredUeModules.filter((um) => um.idUE === ueId);
const avg = calculateWeightedAverage(ueModsForUE, noteMap);
const ajust = ajMap[ueId] ?? null;
const finalAvg = applyAjustement(avg, ajust);
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
{ajust && ajust.malus > 0 && (
<span>(malus : -{ajust.malus})</span>
)}
</p>
)
: <p class="ue-card-avg avg-warn">Notes non disponibles</p>}
</div>
{ueModsForUE.map((um) => {
const mod = moduleMap[um.idModule];
const noteObj = noteMap[um.idModule] ?? null;
const effective = noteObj ? getEffectiveNote(noteObj) : null;
const hasS2 = noteObj?.noteSession2 != null;
return (
<div key={um.idModule} class="ue-module-row">
<span class="ue-module-name">
{mod ? mod.id : um.idModule} {" "}
{mod ? mod.nom : "ECUE inconnu"} (coef {um.coeff})
</span>
<span class={`score-chip ${scoreClass(effective)}`}>
{effective !== null ? `${effective}/20` : "—"}
{hasS2 && (
<span
style="font-size: 0.7rem; opacity: 0.7; margin-left: 0.35rem"
title={`Session 1 : ${noteObj!.note}/20`}
>
(S1: {noteObj!.note})
</span>
)}
</span>
</div>
);
})}
</div>
);
})}
</div>
);
}