test : changed test format + added playwright support
Check Deno code / Check Deno code (pull_request) Has been cancelled
Tests / Unit tests (pull_request) Has been cancelled
Tests / Integration tests (pull_request) Has been cancelled
Check Deno code / Check Deno code (push) Has been cancelled
Tests / Unit tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled

This commit was merged in pull request #153.
This commit is contained in:
2026-05-03 21:52:02 +02:00
parent ed2fe69f54
commit 951c9c1fea
52 changed files with 3576 additions and 5212 deletions
+85
View File
@@ -0,0 +1,85 @@
/**
* 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<string, Note>,
): 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);
}
+90
View File
@@ -0,0 +1,90 @@
// @deno-types="https://cdn.sheetjs.com/xlsx-0.20.3/package/types/index.d.ts"
import * as XLSX from "https://cdn.sheetjs.com/xlsx-0.20.3/package/xlsx.mjs";
export type ParsedUE = {
code: string | null;
name: string;
ects: number | null;
modules: ParsedModule[];
};
export type ParsedModule = {
code: string;
name: string;
coeff: number;
};
export type ParsedYear = {
label: string;
ues: ParsedUE[];
};
/**
* Analyse un classeur Excel pour en extraire la maquette pédagogique.
*/
export function parseMaquette(workbook: XLSX.WorkBook): ParsedYear[] {
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json<(string | number | null)[]>(sheet, {
header: 1,
});
const years: ParsedYear[] = [];
let currentYear: ParsedYear | null = null;
let currentUE: ParsedUE | null = null;
let moduleIndex = 0;
for (const row of rows) {
if (!row || row.length === 0) continue;
const col0 = row[0] != null ? String(row[0]).trim() : "";
// Detect year row: INFO3A, INFO4A, INFO5A, INFOAPP3A, INFOAPP4A, etc.
if (/^(INFO|INFOAPP)\s*\d+A$/i.test(col0)) {
currentYear = { label: col0, ues: [] };
years.push(currentYear);
currentUE = null;
continue;
}
// Detect UE row: col[0] === "UE" or starts with "UE" (e.g., "UE51")
if (col0 === "UE" || (col0.startsWith("UE") && /^UE\d/.test(col0))) {
const ueCode = row[1] != null ? String(row[1]).trim() : null;
const ueName = row[2] != null ? String(row[2]).trim() : "UE sans nom";
const ects = typeof row[4] === "number" ? row[4] : null;
currentUE = { code: ueCode, name: ueName, ects, modules: [] };
if (currentYear) {
currentYear.ues.push(currentUE);
} else {
// No year detected yet — create a default one
currentYear = { label: "Maquette", ues: [currentUE] };
years.push(currentYear);
}
moduleIndex = 0;
continue;
}
// Detect semester header rows — just skip, don't reset UE
if (/^SEM\s*\d/i.test(col0)) {
currentUE = null;
continue;
}
// Detect module row: inside a UE, col[3] has a name, col[5] is a number (coeff)
if (currentUE && row[3] != null && typeof row[5] === "number") {
const modName = String(row[3]).trim();
if (!modName) continue;
let modCode = row[1] != null ? String(row[1]).trim() : "";
if (!modCode) {
const uePrefix = (currentUE.code || "MOD").replace(/[^A-Z0-9]/gi, "");
modCode = `${uePrefix}_${String(moduleIndex).padStart(2, "0")}`;
}
currentUE.modules.push({ code: modCode, name: modName, coeff: row[5] });
moduleIndex++;
}
}
return years;
}