/** * Logique métier pour le calcul des notes et moyennes. */ export interface Note { note: number; noteSession2: number | null; } export interface UEModule { idModule: string; coeff: number; } export interface Ajustement { valeur: number; malus: number; } /** * Retourne la note effective (Session 2 si présente, sinon Session 1). */ export function getEffectiveNote(n: Note): number { return n.noteSession2 ?? n.note; } /** * Calcule la moyenne pondérée d'une liste de modules. * Retourne null si aucun module n'est noté. */ export function calculateWeightedAverage( ueModules: UEModule[], notesMap: Record, ): number | null { let weightedSum = 0; let coveredCoeff = 0; for (const um of ueModules) { const noteObj = notesMap[um.idModule]; if (noteObj) { const val = getEffectiveNote(noteObj); weightedSum += val * um.coeff; coveredCoeff += um.coeff; } } if (coveredCoeff === 0) return null; return weightedSum / coveredCoeff; } /** * Applique l'ajustement et le malus à une moyenne. * L'ajustement REMPLACE la moyenne calculée si présent. */ export function applyAjustement( calculatedAvg: number | null, ajustement: Ajustement | null, ): number | null { let finalAvg = calculatedAvg; if (ajustement) { // L'ajustement remplace la moyenne finalAvg = ajustement.valeur; if (ajustement.malus > 0) { finalAvg = (finalAvg ?? 0) - ajustement.malus; } } return finalAvg; } /** * Arrondit une note à 2 décimales. */ export function roundGrade(grade: number): number { return Math.round(grade * 100) / 100; } /** * Formate une note pour l'affichage (2 décimales). */ export function formatGrade(grade: number | null): string { if (grade === null) return "—"; return grade.toFixed(2); }