test : changed test format + added playwright support
This commit is contained in:
@@ -2,98 +2,19 @@
|
||||
import * as XLSX from "https://cdn.sheetjs.com/xlsx-0.20.3/package/xlsx.mjs";
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
import { useSignal } from "@preact/signals";
|
||||
import {
|
||||
parseMaquette,
|
||||
type ParsedModule,
|
||||
type ParsedUE,
|
||||
type ParsedYear,
|
||||
} from "$root/logic/maquette.ts";
|
||||
import ImportResultPopup, {
|
||||
type ImportDetail,
|
||||
type ImportResult,
|
||||
} from "$root/defaults/ImportResultPopup.tsx";
|
||||
|
||||
type ParsedUE = {
|
||||
code: string | null;
|
||||
name: string;
|
||||
ects: number | null;
|
||||
modules: ParsedModule[];
|
||||
};
|
||||
|
||||
type ParsedModule = {
|
||||
code: string;
|
||||
name: string;
|
||||
coeff: number;
|
||||
};
|
||||
|
||||
type ParsedYear = {
|
||||
label: string;
|
||||
ues: ParsedUE[];
|
||||
};
|
||||
|
||||
type Promo = { id: string; annee: string | null };
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export default function ImportMaquette() {
|
||||
const file = useSignal<File | null>(null);
|
||||
const dragging = useSignal(false);
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
import * as XLSX from "https://cdn.sheetjs.com/xlsx-0.20.3/package/xlsx.mjs";
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
import { useSignal } from "@preact/signals";
|
||||
import {
|
||||
calculateWeightedAverage,
|
||||
getEffectiveNote,
|
||||
roundGrade,
|
||||
} from "$root/logic/grades.ts";
|
||||
import ImportResultPopup, {
|
||||
type ImportDetail,
|
||||
type ImportResult,
|
||||
@@ -393,19 +398,19 @@ export default function ImportNotes() {
|
||||
const ueMods = orderedCols.filter(
|
||||
(c) => c.type === "module" && c.ueId === col.ueId,
|
||||
);
|
||||
let total = 0, coeffSum = 0;
|
||||
for (const um of ueMods) {
|
||||
|
||||
const notesRecord: Record<string, { note: number; noteSession2: number | null }> = {};
|
||||
ueMods.forEach(um => {
|
||||
const n = sNotes.get(um.id);
|
||||
if (n && um.coeff) {
|
||||
total += n.note * um.coeff;
|
||||
coeffSum += um.coeff;
|
||||
}
|
||||
}
|
||||
row.push(
|
||||
coeffSum > 0
|
||||
? Math.round((total / coeffSum) * 100) / 100
|
||||
: null,
|
||||
if (n) notesRecord[um.id] = n;
|
||||
});
|
||||
|
||||
const avg = calculateWeightedAverage(
|
||||
ueMods.map(m => ({ idModule: m.id, coeff: m.coeff ?? 0 })),
|
||||
notesRecord
|
||||
);
|
||||
|
||||
row.push(avg !== null ? roundGrade(avg) : null);
|
||||
}
|
||||
}
|
||||
s1Rows.push(row);
|
||||
@@ -425,20 +430,19 @@ export default function ImportNotes() {
|
||||
const ueMods = orderedCols.filter(
|
||||
(c) => c.type === "module" && c.ueId === col.ueId,
|
||||
);
|
||||
let total = 0, coeffSum = 0;
|
||||
for (const um of ueMods) {
|
||||
|
||||
const notesRecord: Record<string, { note: number; noteSession2: number | null }> = {};
|
||||
ueMods.forEach(um => {
|
||||
const n = sNotes.get(um.id);
|
||||
if (n && um.coeff) {
|
||||
const noteVal = n.noteSession2 ?? n.note;
|
||||
total += noteVal * um.coeff;
|
||||
coeffSum += um.coeff;
|
||||
}
|
||||
}
|
||||
row.push(
|
||||
coeffSum > 0
|
||||
? Math.round((total / coeffSum) * 100) / 100
|
||||
: null,
|
||||
if (n) notesRecord[um.id] = n;
|
||||
});
|
||||
|
||||
const avg = calculateWeightedAverage(
|
||||
ueMods.map(m => ({ idModule: m.id, coeff: m.coeff ?? 0 })),
|
||||
notesRecord
|
||||
);
|
||||
|
||||
row.push(avg !== null ? roundGrade(avg) : null);
|
||||
}
|
||||
}
|
||||
s2Rows.push(row);
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import {
|
||||
applyAjustement,
|
||||
calculateWeightedAverage,
|
||||
getEffectiveNote,
|
||||
} from "$root/logic/grades.ts";
|
||||
|
||||
type Student = {
|
||||
// ...
|
||||
numEtud: number;
|
||||
nom: string;
|
||||
prenom: string;
|
||||
@@ -37,11 +43,6 @@ function noteClass(n: number): string {
|
||||
return n >= 10 ? "note-chip note-chip--ok" : "note-chip note-chip--fail";
|
||||
}
|
||||
|
||||
/** Returns the effective note (session 2 if exists, otherwise session 1). */
|
||||
function effectiveNote(n: Note): number {
|
||||
return n.noteSession2 ?? n.note;
|
||||
}
|
||||
|
||||
export default function NoteRecap({ numEtud }: Props) {
|
||||
const [student, setStudent] = useState<Student | null>(null);
|
||||
const [ueList, setUeList] = useState<UE[]>([]);
|
||||
@@ -108,19 +109,6 @@ export default function NoteRecap({ numEtud }: Props) {
|
||||
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;
|
||||
const val = effectiveNote(n);
|
||||
total += val * um.coeff;
|
||||
coeff += um.coeff;
|
||||
}
|
||||
return coeff > 0 ? total / coeff : null;
|
||||
}
|
||||
|
||||
async function saveNote(
|
||||
idModule: string,
|
||||
field: "note" | "noteSession2",
|
||||
@@ -280,18 +268,11 @@ export default function NoteRecap({ numEtud }: Props) {
|
||||
</p>
|
||||
)
|
||||
: 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);
|
||||
|
||||
// Final displayed average: if ajust.valeur exists it replaces avg, then subtract malus
|
||||
let finalAvg = avg;
|
||||
if (ajust) {
|
||||
finalAvg = ajust.valeur;
|
||||
if (ajust.malus > 0) {
|
||||
finalAvg = (finalAvg ?? 0) - ajust.malus;
|
||||
}
|
||||
}
|
||||
const ueMods = ueList.length > 0 ? ueModules.filter((um) => um.idUE === ue.id) : [];
|
||||
const notesRecord = Object.fromEntries(noteMap);
|
||||
const avg = calculateWeightedAverage(ueMods, notesRecord);
|
||||
const ajust = ajustements.find((a) => a.idUE === ue.id) ?? null;
|
||||
const finalAvg = applyAjustement(avg, ajust);
|
||||
|
||||
return (
|
||||
<div key={ue.id} class="edit-section">
|
||||
@@ -341,7 +322,7 @@ export default function NoteRecap({ numEtud }: Props) {
|
||||
const noteVal = noteObj?.note;
|
||||
const noteS2 = noteObj?.noteSession2;
|
||||
const effective = noteObj
|
||||
? effectiveNote(noteObj)
|
||||
? getEffectiveNote(noteObj)
|
||||
: undefined;
|
||||
const nomMod = moduleMap.get(um.idModule) ?? um.idModule;
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import {
|
||||
applyAjustement,
|
||||
calculateWeightedAverage,
|
||||
getEffectiveNote,
|
||||
} from "$root/logic/grades.ts";
|
||||
|
||||
type Note = {
|
||||
numEtud: number;
|
||||
@@ -6,6 +11,7 @@ type Note = {
|
||||
note: number;
|
||||
noteSession2: number | null;
|
||||
};
|
||||
// ... rest of types unchanged ...
|
||||
type UE = { id: number; nom: string };
|
||||
type UEModule = {
|
||||
idModule: string;
|
||||
@@ -36,11 +42,6 @@ function avgClass(avg: number | null): string {
|
||||
return avg >= 10 ? "avg-good" : "avg-warn";
|
||||
}
|
||||
|
||||
/** Returns the effective note (session 2 if exists, otherwise session 1). */
|
||||
function effectiveNote(n: Note): number {
|
||||
return n.noteSession2 ?? n.note;
|
||||
}
|
||||
|
||||
export default function NotesView({ numEtud, prenom }: Props) {
|
||||
const [notes, setNotes] = useState<Note[]>([]);
|
||||
const [ues, setUes] = useState<UE[]>([]);
|
||||
@@ -175,29 +176,9 @@ export default function NotesView({ numEtud, prenom }: Props) {
|
||||
if (!ue) return null;
|
||||
|
||||
const ueModsForUE = filteredUeModules.filter((um) => um.idUE === ueId);
|
||||
let weightedSum = 0;
|
||||
let coveredCoeff = 0;
|
||||
ueModsForUE.forEach((um) => {
|
||||
const noteObj = noteMap[um.idModule];
|
||||
if (noteObj) {
|
||||
const val = effectiveNote(noteObj);
|
||||
weightedSum += val * um.coeff;
|
||||
coveredCoeff += um.coeff;
|
||||
}
|
||||
});
|
||||
|
||||
const avg = coveredCoeff > 0 ? weightedSum / coveredCoeff : null;
|
||||
const avg = calculateWeightedAverage(ueModsForUE, noteMap);
|
||||
const ajust = ajMap[ueId] ?? null;
|
||||
|
||||
// If ajust.valeur exists, it replaces the calculated average
|
||||
// Then malus is subtracted
|
||||
let finalAvg: number | null = avg;
|
||||
if (ajust) {
|
||||
finalAvg = ajust.valeur;
|
||||
if (ajust.malus > 0) {
|
||||
finalAvg = (finalAvg ?? 0) - ajust.malus;
|
||||
}
|
||||
}
|
||||
const finalAvg = applyAjustement(avg, ajust);
|
||||
|
||||
return (
|
||||
<div key={ueId} class="ue-card">
|
||||
@@ -218,7 +199,7 @@ export default function NotesView({ numEtud, prenom }: Props) {
|
||||
{ueModsForUE.map((um) => {
|
||||
const mod = moduleMap[um.idModule];
|
||||
const noteObj = noteMap[um.idModule] ?? null;
|
||||
const effective = noteObj ? effectiveNote(noteObj) : null;
|
||||
const effective = noteObj ? getEffectiveNote(noteObj) : null;
|
||||
const hasS2 = noteObj?.noteSession2 != null;
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user