feat : fix a lot of stuff
This commit is contained in:
@@ -1,15 +1,61 @@
|
||||
// @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";
|
||||
import { useRef } from "preact/hooks";
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
import { useSignal } from "@preact/signals";
|
||||
import ImportResultPopup, {
|
||||
type ImportDetail,
|
||||
type ImportResult,
|
||||
} from "$root/defaults/ImportResultPopup.tsx";
|
||||
|
||||
type Student = { numEtud: number; nom: string; prenom: string };
|
||||
type ColumnInfo = {
|
||||
index: number;
|
||||
code: string;
|
||||
name: string;
|
||||
coeff: number | null;
|
||||
type: "module" | "malus" | "ue" | "semester" | "unknown";
|
||||
};
|
||||
|
||||
function parseHeader(header: string): { code: string; name: string } {
|
||||
const parts = header.split(" - ");
|
||||
if (parts.length >= 2) {
|
||||
return { code: parts[0].trim(), name: parts.slice(1).join(" - ").trim() };
|
||||
}
|
||||
return { code: header.trim(), name: header.trim() };
|
||||
}
|
||||
|
||||
function detectColumnType(
|
||||
header: string,
|
||||
_coeff: number | null,
|
||||
): ColumnInfo["type"] {
|
||||
const h = header.trim();
|
||||
if (/^MALUS/i.test(h)) return "malus";
|
||||
if (/^S\d+$/i.test(h)) return "semester";
|
||||
// UE codes typically contain "U" followed by digits (e.g., JIN5U05, JTR5U01)
|
||||
const { code } = parseHeader(h);
|
||||
if (/U\d/i.test(code) && !/^MALUS/i.test(code)) return "ue";
|
||||
return "module";
|
||||
}
|
||||
|
||||
export default function ImportNotes() {
|
||||
const file = useSignal<File | null>(null);
|
||||
const dragging = useSignal(false);
|
||||
const uploading = useSignal(false);
|
||||
const error = useSignal<string | null>(null);
|
||||
const success = useSignal<string | null>(null);
|
||||
const importResult = useSignal<ImportResult | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const students = useSignal<Student[]>([]);
|
||||
const columns = useSignal<ColumnInfo[]>([]);
|
||||
const sheetNames = useSignal<string[]>([]);
|
||||
const selectedSheet = useSignal("");
|
||||
const session = useSignal<"1" | "2">("1");
|
||||
const workbookRef = useRef<XLSX.WorkBook | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/students/api/students")
|
||||
.then((r) => (r.ok ? r.json() : []))
|
||||
.then((data) => (students.value = data));
|
||||
}, []);
|
||||
|
||||
function pickFile(f: File) {
|
||||
if (!f.name.match(/\.xlsx?$/i)) {
|
||||
@@ -18,76 +64,404 @@ export default function ImportNotes() {
|
||||
}
|
||||
file.value = f;
|
||||
error.value = null;
|
||||
success.value = null;
|
||||
importResult.value = null;
|
||||
columns.value = [];
|
||||
|
||||
f.arrayBuffer().then((buf) => {
|
||||
try {
|
||||
const wb = XLSX.read(buf, { type: "array" });
|
||||
workbookRef.current = wb;
|
||||
sheetNames.value = wb.SheetNames;
|
||||
if (wb.SheetNames.length > 0) {
|
||||
selectedSheet.value = wb.SheetNames[0];
|
||||
parseSheet(wb, wb.SheetNames[0]);
|
||||
}
|
||||
} catch {
|
||||
error.value = "Impossible de lire le fichier.";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onDragOver(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
dragging.value = true;
|
||||
function parseSheet(wb: XLSX.WorkBook, sheetName: string) {
|
||||
const sheet = wb.Sheets[sheetName];
|
||||
const rows = XLSX.utils.sheet_to_json<(string | number | null)[]>(sheet, {
|
||||
header: 1,
|
||||
});
|
||||
if (rows.length < 2) {
|
||||
columns.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const headerRow = rows[0];
|
||||
const coeffRow = rows[1];
|
||||
|
||||
const cols: ColumnInfo[] = [];
|
||||
// First 2 columns are nom/prenom, skip them
|
||||
for (let i = 2; i < headerRow.length; i++) {
|
||||
const h = headerRow[i];
|
||||
if (h == null || String(h).trim() === "") continue;
|
||||
const header = String(h).trim();
|
||||
const coeff = typeof coeffRow[i] === "number" ? coeffRow[i] : null;
|
||||
const { code, name } = parseHeader(header);
|
||||
const type = detectColumnType(header, coeff as number | null);
|
||||
cols.push({ index: i, code, name, coeff: coeff as number | null, type });
|
||||
}
|
||||
columns.value = cols;
|
||||
}
|
||||
|
||||
function onDragLeave() {
|
||||
dragging.value = false;
|
||||
function onSheetChange(name: string) {
|
||||
selectedSheet.value = name;
|
||||
if (workbookRef.current) {
|
||||
parseSheet(workbookRef.current, name);
|
||||
}
|
||||
}
|
||||
|
||||
function onDrop(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
dragging.value = false;
|
||||
const f = e.dataTransfer?.files?.[0];
|
||||
if (f) pickFile(f);
|
||||
}
|
||||
|
||||
function onInputChange(e: Event) {
|
||||
const f = (e.target as HTMLInputElement).files?.[0];
|
||||
if (f) pickFile(f);
|
||||
function findStudent(
|
||||
nom: string,
|
||||
prenom: string,
|
||||
): Student | undefined {
|
||||
const normNom = nom.toUpperCase().trim();
|
||||
const normPrenom = prenom.toUpperCase().trim();
|
||||
return students.value.find(
|
||||
(s) =>
|
||||
s.nom.toUpperCase().trim() === normNom &&
|
||||
s.prenom.toUpperCase().trim() === normPrenom,
|
||||
);
|
||||
}
|
||||
|
||||
async function doImport() {
|
||||
if (!file.value) return;
|
||||
if (!workbookRef.current || !selectedSheet.value) return;
|
||||
uploading.value = true;
|
||||
error.value = null;
|
||||
success.value = null;
|
||||
importResult.value = null;
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.value.arrayBuffer();
|
||||
const workbook = XLSX.read(arrayBuffer, { type: "array" });
|
||||
let imported = 0;
|
||||
let failed = 0;
|
||||
const sheet = workbookRef.current.Sheets[selectedSheet.value];
|
||||
const rows = XLSX.utils.sheet_to_json<(string | number | null)[]>(sheet, {
|
||||
header: 1,
|
||||
});
|
||||
|
||||
for (const sheetName of workbook.SheetNames) {
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
const rows = XLSX.utils.sheet_to_json<{
|
||||
numEtud: number;
|
||||
idModule: string;
|
||||
note: number;
|
||||
}>(sheet, { header: ["numEtud", "idModule", "note"], range: 1 });
|
||||
const moduleCols = columns.value.filter((c) => c.type === "module");
|
||||
|
||||
for (const row of rows) {
|
||||
const res = await fetch("/notes/api/notes", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(row),
|
||||
let added = 0;
|
||||
let modified = 0;
|
||||
let ignored = 0;
|
||||
let errors = 0;
|
||||
const details: ImportDetail[] = [];
|
||||
|
||||
// Process data rows (skip header + coeff rows)
|
||||
for (let r = 2; r < rows.length; r++) {
|
||||
const row = rows[r];
|
||||
if (!row || row.length < 3) continue;
|
||||
|
||||
const nom = row[0] != null ? String(row[0]).trim() : "";
|
||||
const prenom = row[1] != null ? String(row[1]).trim() : "";
|
||||
if (!nom || !prenom) continue;
|
||||
|
||||
const student = findStudent(nom, prenom);
|
||||
if (!student) {
|
||||
ignored++;
|
||||
details.push({
|
||||
type: "error",
|
||||
message: `${nom} ${prenom} : Etudiant non trouve`,
|
||||
});
|
||||
if (res.ok) imported++;
|
||||
else failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Import module notes
|
||||
for (const col of moduleCols) {
|
||||
const val = row[col.index];
|
||||
if (val == null || typeof val !== "number") {
|
||||
if (val != null && typeof val !== "number") {
|
||||
errors++;
|
||||
details.push({
|
||||
type: "error",
|
||||
message:
|
||||
`${student.numEtud} : ${col.code} : Note "${val}" invalide`,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (val < 0 || val > 20) {
|
||||
errors++;
|
||||
details.push({
|
||||
type: "error",
|
||||
message:
|
||||
`${student.numEtud} : ${col.code} : Note ${val} hors limites`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const noteField = session.value === "2" ? "noteSession2" : "note";
|
||||
|
||||
// Try PUT first (update), then POST (create)
|
||||
const putRes = await fetch(
|
||||
`/notes/api/notes/${student.numEtud}/${
|
||||
encodeURIComponent(col.code)
|
||||
}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ [noteField]: val }),
|
||||
},
|
||||
);
|
||||
|
||||
if (putRes.ok) {
|
||||
const prev = await putRes.json();
|
||||
const oldVal = session.value === "2"
|
||||
? prev.noteSession2
|
||||
: prev.note;
|
||||
modified++;
|
||||
details.push({
|
||||
type: "change",
|
||||
message: `${student.numEtud} : ${col.code} : ${
|
||||
oldVal ?? "null"
|
||||
} -> ${val}`,
|
||||
});
|
||||
} else if (putRes.status === 404) {
|
||||
// Note doesn't exist yet, create it
|
||||
const body: Record<string, unknown> = {
|
||||
numEtud: student.numEtud,
|
||||
idModule: col.code,
|
||||
note: session.value === "1" ? val : 0,
|
||||
};
|
||||
if (session.value === "2") body.noteSession2 = val;
|
||||
|
||||
const postRes = await fetch("/notes/api/notes", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (postRes.ok) {
|
||||
added++;
|
||||
details.push({
|
||||
type: "change",
|
||||
message: `${student.numEtud} : ${col.code} : null -> ${val}`,
|
||||
});
|
||||
} else {
|
||||
errors++;
|
||||
details.push({
|
||||
type: "error",
|
||||
message:
|
||||
`${student.numEtud} : ${col.code} : Matiere non trouvee`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
errors++;
|
||||
details.push({
|
||||
type: "error",
|
||||
message: `${student.numEtud} : ${col.code} : Erreur serveur`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
success.value = `Import terminé — ${imported} ajouté${
|
||||
imported !== 1 ? "s" : ""
|
||||
}${failed > 0 ? `, ${failed} erreur${failed !== 1 ? "s" : ""}` : ""}`;
|
||||
importResult.value = { added, modified, ignored, errors, details };
|
||||
} catch {
|
||||
error.value = "Erreur lors de la lecture du fichier.";
|
||||
error.value = "Erreur lors de l'import.";
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function downloadTemplate() {
|
||||
const wb = XLSX.utils.book_new();
|
||||
const ws = XLSX.utils.aoa_to_sheet([["numEtud", "idModule", "note"]]);
|
||||
XLSX.utils.book_append_sheet(wb, ws, "Notes");
|
||||
XLSX.writeFile(wb, "modele_notes.xlsx");
|
||||
globalThis.open("/templates/modele_notes.xlsx", "_blank");
|
||||
}
|
||||
|
||||
function downloadExport() {
|
||||
// Export notes from the API in the same format
|
||||
Promise.all([
|
||||
fetch("/students/api/students").then((r) => r.json()),
|
||||
fetch("/notes/api/notes").then((r) => r.json()),
|
||||
fetch("/admin/api/modules").then((r) => r.json()),
|
||||
fetch("/admin/api/ue-modules").then((r) => r.json()),
|
||||
fetch("/admin/api/ues").then((r) => r.json()),
|
||||
]).then(
|
||||
([
|
||||
studentsData,
|
||||
notesData,
|
||||
modulesData,
|
||||
ueModulesData,
|
||||
uesData,
|
||||
]) => {
|
||||
// Build module map
|
||||
const modMap = new Map<string, string>(
|
||||
modulesData.map((m: { id: string; nom: string }) => [m.id, m.nom]),
|
||||
);
|
||||
|
||||
// Get unique module IDs from notes
|
||||
const moduleIds = [
|
||||
...new Set(
|
||||
notesData.map((n: { idModule: string }) => n.idModule),
|
||||
),
|
||||
] as string[];
|
||||
|
||||
// Group ue-modules by UE
|
||||
const ueMap = new Map<number, string>(
|
||||
uesData.map((u: { id: number; nom: string }) => [u.id, u.nom]),
|
||||
);
|
||||
const umByUE = new Map<number, typeof ueModulesData>();
|
||||
for (const um of ueModulesData) {
|
||||
if (!umByUE.has(um.idUE)) umByUE.set(um.idUE, []);
|
||||
umByUE.get(um.idUE)!.push(um);
|
||||
}
|
||||
|
||||
// Build column order: group modules by UE, add UE avg columns
|
||||
const orderedCols: {
|
||||
id: string;
|
||||
header: string;
|
||||
coeff: number | null;
|
||||
type: "module" | "ue";
|
||||
ueId?: number;
|
||||
}[] = [];
|
||||
|
||||
const usedModules = new Set<string>();
|
||||
for (const [ueId, ums] of umByUE) {
|
||||
for (const um of ums) {
|
||||
if (!moduleIds.includes(um.idModule)) continue;
|
||||
orderedCols.push({
|
||||
id: um.idModule,
|
||||
header: `${um.idModule} - ${
|
||||
modMap.get(um.idModule) || um.idModule
|
||||
}`,
|
||||
coeff: um.coeff,
|
||||
type: "module",
|
||||
ueId,
|
||||
});
|
||||
usedModules.add(um.idModule);
|
||||
}
|
||||
const ueName = ueMap.get(ueId) || `UE ${ueId}`;
|
||||
orderedCols.push({
|
||||
id: `ue_${ueId}`,
|
||||
header: ueName,
|
||||
coeff: ums.reduce(
|
||||
(s: number, um: { coeff: number }) => s + um.coeff,
|
||||
0,
|
||||
),
|
||||
type: "ue",
|
||||
ueId,
|
||||
});
|
||||
}
|
||||
// Add modules not linked to any UE
|
||||
for (const mId of moduleIds) {
|
||||
if (usedModules.has(mId)) continue;
|
||||
orderedCols.push({
|
||||
id: mId,
|
||||
header: `${mId} - ${modMap.get(mId) || mId}`,
|
||||
coeff: null,
|
||||
type: "module",
|
||||
});
|
||||
}
|
||||
|
||||
// Build note lookup: numEtud -> idModule -> note
|
||||
const noteLookup = new Map<
|
||||
number,
|
||||
Map<string, { note: number; noteSession2: number | null }>
|
||||
>();
|
||||
for (const n of notesData) {
|
||||
if (!noteLookup.has(n.numEtud)) noteLookup.set(n.numEtud, new Map());
|
||||
noteLookup.get(n.numEtud)!.set(n.idModule, {
|
||||
note: n.note,
|
||||
noteSession2: n.noteSession2,
|
||||
});
|
||||
}
|
||||
|
||||
// Get students who have notes
|
||||
const studentsWithNotes = studentsData.filter(
|
||||
(s: Student) => noteLookup.has(s.numEtud),
|
||||
);
|
||||
|
||||
// Build header rows
|
||||
const headerRow: (string | null)[] = [null, null];
|
||||
const coeffRow: (number | null)[] = [null, null];
|
||||
for (const col of orderedCols) {
|
||||
headerRow.push(col.header);
|
||||
coeffRow.push(col.coeff);
|
||||
}
|
||||
|
||||
// Build session 1 data rows
|
||||
const s1Rows: (string | number | null)[][] = [];
|
||||
for (const s of studentsWithNotes) {
|
||||
const row: (string | number | null)[] = [s.nom, s.prenom];
|
||||
const sNotes = noteLookup.get(s.numEtud) || new Map();
|
||||
for (const col of orderedCols) {
|
||||
if (col.type === "module") {
|
||||
const n = sNotes.get(col.id);
|
||||
row.push(n ? n.note : null);
|
||||
} else {
|
||||
// UE average - calculate
|
||||
const ueMods = orderedCols.filter(
|
||||
(c) => c.type === "module" && c.ueId === col.ueId,
|
||||
);
|
||||
let total = 0, coeffSum = 0;
|
||||
for (const um of ueMods) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
s1Rows.push(row);
|
||||
}
|
||||
|
||||
// Build session 2 data rows
|
||||
const s2Rows: (string | number | null)[][] = [];
|
||||
for (const s of studentsWithNotes) {
|
||||
const row: (string | number | null)[] = [s.nom, s.prenom];
|
||||
const sNotes = noteLookup.get(s.numEtud) || new Map();
|
||||
for (const col of orderedCols) {
|
||||
if (col.type === "module") {
|
||||
const n = sNotes.get(col.id);
|
||||
// Use session 2 note if available, else session 1
|
||||
row.push(n ? (n.noteSession2 ?? n.note) : null);
|
||||
} else {
|
||||
const ueMods = orderedCols.filter(
|
||||
(c) => c.type === "module" && c.ueId === col.ueId,
|
||||
);
|
||||
let total = 0, coeffSum = 0;
|
||||
for (const um of ueMods) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
s2Rows.push(row);
|
||||
}
|
||||
|
||||
const wb = XLSX.utils.book_new();
|
||||
const ws1 = XLSX.utils.aoa_to_sheet([headerRow, coeffRow, ...s1Rows]);
|
||||
XLSX.utils.book_append_sheet(wb, ws1, "Session 1");
|
||||
const ws2 = XLSX.utils.aoa_to_sheet([headerRow, coeffRow, ...s2Rows]);
|
||||
XLSX.utils.book_append_sheet(wb, ws2, "Session 2");
|
||||
const buf = XLSX.write(wb, { bookType: "xlsx", type: "array" });
|
||||
const blob = new Blob([buf], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "export_notes.xlsx";
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -97,14 +471,25 @@ export default function ImportNotes() {
|
||||
type="file"
|
||||
accept=".xlsx,.xls"
|
||||
style="display:none"
|
||||
onChange={onInputChange}
|
||||
onChange={(e) => {
|
||||
const f = (e.target as HTMLInputElement).files?.[0];
|
||||
if (f) pickFile(f);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
class={`drop-zone${dragging.value ? " dragging" : ""}`}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
dragging.value = true;
|
||||
}}
|
||||
onDragLeave={() => (dragging.value = false)}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
dragging.value = false;
|
||||
const f = e.dataTransfer?.files?.[0];
|
||||
if (f) pickFile(f);
|
||||
}}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>
|
||||
<span class="drop-zone-icon">⬇</span>
|
||||
@@ -117,10 +502,85 @@ export default function ImportNotes() {
|
||||
</div>
|
||||
|
||||
{error.value && <p class="state-error">{error.value}</p>}
|
||||
{success.value && (
|
||||
<p style="font-size:0.82rem; color: light-dark(var(--light-accent-color), var(--dark-accent-color)); margin-bottom: 0.75rem">
|
||||
{success.value}
|
||||
</p>
|
||||
|
||||
{importResult.value && (
|
||||
<ImportResultPopup
|
||||
result={importResult.value}
|
||||
onClose={() => (importResult.value = null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sheet + session selector */}
|
||||
{sheetNames.value.length > 0 && (
|
||||
<div style="display: flex; gap: 1rem; margin-bottom: 0.75rem; flex-wrap: wrap">
|
||||
<div>
|
||||
<label style="font-size: 0.82rem; font-weight: 600; display: block; margin-bottom: 0.25rem">
|
||||
Feuille
|
||||
</label>
|
||||
<select
|
||||
class="filter-select"
|
||||
value={selectedSheet.value}
|
||||
onChange={(e) =>
|
||||
onSheetChange((e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
{sheetNames.value.map((name) => (
|
||||
<option key={name} value={name}>{name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size: 0.82rem; font-weight: 600; display: block; margin-bottom: 0.25rem">
|
||||
Importer en tant que
|
||||
</label>
|
||||
<select
|
||||
class="filter-select"
|
||||
value={session.value}
|
||||
onChange={(e) => (session.value = (e.target as HTMLSelectElement)
|
||||
.value as "1" | "2")}
|
||||
>
|
||||
<option value="1">Session 1 (note)</option>
|
||||
<option value="2">Session 2 (noteSession2)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Column preview */}
|
||||
{columns.value.length > 0 && (
|
||||
<div style="margin-bottom: 1rem">
|
||||
<p style="font-size: 0.82rem; font-weight: 600; margin-bottom: 0.5rem">
|
||||
Colonnes detectees :
|
||||
</p>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.35rem">
|
||||
{columns.value.map((col) => (
|
||||
<span
|
||||
key={col.index}
|
||||
class={`numEtud-chip${
|
||||
col.type === "module"
|
||||
? ""
|
||||
: col.type === "malus"
|
||||
? " note-chip--fail"
|
||||
: " note-chip--promo"
|
||||
}`}
|
||||
style="font-size: 0.72rem"
|
||||
title={`${col.type} — ${col.name}${
|
||||
col.coeff != null ? ` (coef ${col.coeff})` : ""
|
||||
}`}
|
||||
>
|
||||
{col.type === "module"
|
||||
? "M"
|
||||
: col.type === "ue"
|
||||
? "UE"
|
||||
: col.type === "malus"
|
||||
? "X"
|
||||
: "?"} {col.code}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p class="col-dim" style="font-size: 0.72rem; margin-top: 0.35rem">
|
||||
M = module (importe) | UE = moyenne UE (ignore) | X = malus
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="upload-actions">
|
||||
@@ -128,22 +588,31 @@ export default function ImportNotes() {
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
onClick={doImport}
|
||||
disabled={!file.value || uploading.value}
|
||||
disabled={!file.value || uploading.value ||
|
||||
columns.value.filter((c) => c.type === "module").length === 0}
|
||||
>
|
||||
{uploading.value ? "…" : "⊕ Importer"}
|
||||
{uploading.value ? "..." : "+ Importer"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
onClick={downloadTemplate}
|
||||
>
|
||||
⊕ Télécharger Modèle
|
||||
Telecharger Modele
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
onClick={downloadExport}
|
||||
>
|
||||
Exporter Notes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="upload-format">
|
||||
Format : <strong>numEtud</strong> | <strong>idModule</strong> |{" "}
|
||||
<strong>note</strong>
|
||||
Format : <strong>Nom</strong> | <strong>Prenom</strong> |{" "}
|
||||
<strong>CODE - Module</strong> (colonnes notes){" "}
|
||||
— les colonnes UE et MALUS sont auto-detectees
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user