- Nouvelle page corriger-exercice.html : sélection de l'exercice (BioCID, FDS, TP Nuisibles) + zone de dépôt PDF - Le PDF déposé est transmis à la page de correction dédiée qui se charge automatiquement (?auto=1) - Carte 'Corriger un exercice' ajoutée à la section Exercices de l'index - Slide 6 BioCID : suppression du bouton biocid-anses.fr, alignement des boutons Télécharger / Corriger / Recommencer - Le bouton 'Corriger un rendu' redirige vers la nouvelle page
623 lines
23 KiB
HTML
623 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Correction FDS, FT & Étiquettes — Certibiocide</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@600;700;800&family=DM+Sans:ital,wght@0,400;0,500;1,400&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="../header-style.css"/>
|
|
<style>
|
|
body {
|
|
padding: calc(36px + 70px) 20px 80px;
|
|
}
|
|
.wrap { max-width: 820px; margin: 0 auto; }
|
|
.syne { font-family: 'Syne', sans-serif; }
|
|
.eyebrow {
|
|
font-family: 'Syne', sans-serif;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--amber);
|
|
margin-bottom: 6px;
|
|
}
|
|
h1 {
|
|
font-family: 'Syne', sans-serif;
|
|
font-size: 28px;
|
|
font-weight: 800;
|
|
letter-spacing: -0.5px;
|
|
margin-bottom: 28px;
|
|
}
|
|
|
|
/* DROPZONE */
|
|
#dropzone {
|
|
background: #fff;
|
|
border: 2px dashed rgba(186,117,23,0.35);
|
|
border-radius: 16px;
|
|
padding: 56px 24px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: border-color 0.2s, background 0.2s;
|
|
}
|
|
#dropzone.drag { border-color: var(--amber); background: var(--amber-light); }
|
|
#dropzone .icon { font-size: 40px; margin-bottom: 12px; }
|
|
#dropzone p.title { font-family: 'Syne', sans-serif; font-weight: 700; font-size: 16px; margin-bottom: 6px; }
|
|
#dropzone p.sub { color: var(--text-muted); font-size: 13.5px; margin-bottom: 20px; }
|
|
#filePicker { display: none; }
|
|
.btn-pick {
|
|
font-family: 'Syne', sans-serif;
|
|
display: inline-block;
|
|
background: var(--amber);
|
|
color: #fff;
|
|
font-weight: 700;
|
|
font-size: 13px;
|
|
padding: 10px 26px;
|
|
border-radius: 30px;
|
|
cursor: pointer;
|
|
border: none;
|
|
}
|
|
#status { margin-top: 16px; font-size: 13px; }
|
|
#status.loading { color: var(--amber); }
|
|
#status.error { color: var(--coral); }
|
|
|
|
/* RESULT */
|
|
#result { display: none; }
|
|
.toprow {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 18px;
|
|
}
|
|
#fileName { font-size: 12.5px; color: var(--text-muted); }
|
|
.btn-reset {
|
|
font-family: 'Syne', sans-serif;
|
|
background: transparent;
|
|
border: 1px solid var(--border);
|
|
border-radius: 20px;
|
|
padding: 6px 16px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
color: var(--text-muted);
|
|
}
|
|
.btn-reset:hover { border-color: var(--amber); color: var(--amber); }
|
|
|
|
/* BANDEAU PARTICIPANTS */
|
|
#participantsBanner {
|
|
background: linear-gradient(135deg, var(--amber) 0%, #D8923A 100%);
|
|
border-radius: 16px;
|
|
padding: 20px 26px;
|
|
margin-bottom: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
color: #fff;
|
|
flex-wrap: wrap;
|
|
}
|
|
#participantsBanner .pb-icon {
|
|
width: 48px; height: 48px;
|
|
border-radius: 12px;
|
|
background: rgba(255,255,255,0.2);
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 24px;
|
|
flex-shrink: 0;
|
|
}
|
|
#participantsBanner .pb-group {
|
|
font-family: 'Syne', sans-serif;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
opacity: 0.85;
|
|
margin-bottom: 4px;
|
|
}
|
|
#participantsBanner .pb-names {
|
|
font-family: 'Syne', sans-serif;
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
}
|
|
#participantsBanner .pb-meta {
|
|
margin-left: auto;
|
|
font-size: 12.5px;
|
|
text-align: right;
|
|
opacity: 0.92;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* SCORE GLOBAL */
|
|
.score-card {
|
|
background: #fff;
|
|
border: 1px solid var(--border);
|
|
border-radius: 16px;
|
|
padding: 20px 24px;
|
|
margin-bottom: 26px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 22px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.score-ring {
|
|
width: 84px; height: 84px;
|
|
border-radius: 50%;
|
|
display: flex; align-items: center; justify-content: center;
|
|
flex-direction: column;
|
|
flex-shrink: 0;
|
|
color: #fff;
|
|
font-family: 'Syne', sans-serif;
|
|
}
|
|
.score-ring .score-pct { font-size: 22px; font-weight: 800; line-height: 1; }
|
|
.score-ring .score-lbl { font-size: 9px; letter-spacing: 0.06em; opacity: 0.9; }
|
|
.score-detail .score-main { font-family: 'Syne', sans-serif; font-size: 18px; font-weight: 800; }
|
|
.score-detail .score-sub { font-size: 13px; color: var(--text-muted); margin-top: 3px; }
|
|
.score-parts { margin-left: auto; display: flex; gap: 10px; flex-wrap: wrap; }
|
|
.score-pill {
|
|
font-size: 12px;
|
|
padding: 6px 12px;
|
|
border-radius: 20px;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
font-family: 'Syne', sans-serif;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* CARROUSEL DE PARTIES */
|
|
.carousel-wrap { margin-bottom: 36px; }
|
|
.carousel-nav {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 16px;
|
|
}
|
|
.nav-btn {
|
|
width: 38px; height: 38px;
|
|
border-radius: 50%;
|
|
border: 1px solid var(--border);
|
|
background: #fff;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
display: flex; align-items: center; justify-content: center;
|
|
transition: background 0.15s, border-color 0.15s;
|
|
}
|
|
.nav-btn:hover { background: var(--amber-light); border-color: var(--amber); }
|
|
.nav-btn:disabled { opacity: 0.3; cursor: default; }
|
|
.nav-btn:disabled:hover { background: #fff; border-color: var(--border); }
|
|
.dots { display: flex; gap: 7px; }
|
|
.dot { width: 8px; height: 8px; border-radius: 50%; background: rgba(0,0,0,0.15); cursor: pointer; transition: background 0.15s, transform 0.15s; }
|
|
.dot.active { background: var(--amber); transform: scale(1.3); }
|
|
|
|
.theme-panel {
|
|
background: #fff;
|
|
border: 1px solid var(--border);
|
|
border-radius: 18px;
|
|
padding: 26px 28px;
|
|
min-height: 360px;
|
|
}
|
|
.theme-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-bottom: 18px;
|
|
}
|
|
.theme-icon {
|
|
width: 44px; height: 44px;
|
|
border-radius: 12px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 22px;
|
|
flex-shrink: 0;
|
|
}
|
|
.theme-title { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 17px; }
|
|
.theme-count { font-size: 12.5px; color: var(--text-muted); margin-left: auto; }
|
|
|
|
.cards { display: flex; flex-direction: column; gap: 10px; }
|
|
.qcard {
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 14px 18px;
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: flex-start;
|
|
transition: box-shadow 0.2s;
|
|
}
|
|
.qcard:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.06); }
|
|
.qcard .badge {
|
|
width: 22px; height: 22px;
|
|
border-radius: 50%;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 12px;
|
|
flex-shrink: 0;
|
|
margin-top: 1px;
|
|
}
|
|
.qcard .badge.ok { background: var(--teal-light); color: var(--teal); }
|
|
.qcard .badge.ko { background: var(--coral-light); color: var(--coral); }
|
|
.qcard .badge.miss { background: rgba(0,0,0,0.06); color: var(--text-muted); }
|
|
.qcard .qtext { flex: 1; }
|
|
.qcard .qlabel { font-weight: 600; font-size: 13.5px; margin-bottom: 5px; }
|
|
.qcard .qgroup { font-size: 13px; }
|
|
.qcard .qgroup.ok { color: var(--teal); }
|
|
.qcard .qgroup.ko { color: var(--coral); }
|
|
.qcard .qgroup.miss { color: var(--text-muted); font-style: italic; }
|
|
.qcard .qexpected { font-size: 12.5px; color: var(--text-muted); margin-top: 2px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<div class="logo">Gauthier Chombart & Nathan Chauwin</div>
|
|
<nav>
|
|
<a href="../index.html">Accueil</a>
|
|
<a href="../index.html#formations">Exercices</a>
|
|
<a href="../index.html#documentation">Documentation</a>
|
|
<a href="../index.html#rapports">Rapports</a>
|
|
<a href="../index.html#about">À propos</a>
|
|
<a href="../index.html#contact">Contact</a>
|
|
</nav>
|
|
</header>
|
|
|
|
<div class="wrap">
|
|
<div id="participantsBanner" style="display:none;"></div>
|
|
<div class="eyebrow">Certibiocide — Correction de TD</div>
|
|
<h1>FDS, FT & Étiquettes — Correction du rendu</h1>
|
|
|
|
<div id="dropzone">
|
|
<div class="icon">📄</div>
|
|
<p class="title">Déposez le PDF du groupe ici</p>
|
|
<p class="sub">ou cliquez pour sélectionner le fichier exporté depuis le TD FDS, FT & Étiquettes</p>
|
|
<label class="btn-pick">Choisir un fichier
|
|
<input type="file" id="filePicker" accept="application/pdf">
|
|
</label>
|
|
<div id="status"></div>
|
|
</div>
|
|
|
|
<div id="result">
|
|
<div class="toprow">
|
|
<span id="fileName"></span>
|
|
<button class="btn-reset" onclick="resetAll()">↺ Charger un autre PDF</button>
|
|
</div>
|
|
|
|
<div class="score-card" id="scoreCard"></div>
|
|
|
|
<div class="carousel-wrap">
|
|
<div class="carousel-nav">
|
|
<button class="nav-btn" id="prevBtn" onclick="goTo(currentPart - 1)">←</button>
|
|
<div class="dots" id="dots"></div>
|
|
<button class="nav-btn" id="nextBtn" onclick="goTo(currentPart + 1)">→</button>
|
|
</div>
|
|
<div id="themePanel"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<footer>
|
|
<p>© 2026 Gauthier Chombart & Nathan Chauwin<br>Tous droits réservés — Formations Certibiocide & Techniques — Formateurs indépendants</p>
|
|
<p style="margin-top: 10px; opacity: 0.6;">Tous droits réservés — Gauthier Chombart & Nathan Chauwin — Formateurs indépendants</p>
|
|
</footer>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
|
<script>
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
|
|
|
|
/* ==== Clé de correction (identique à l'exercice) ==== */
|
|
const fdsQuestions = [
|
|
{ id: 1, text: "Quel est le nom du produit et qui est le fabricant ?", answer: "1" },
|
|
{ id: 2, text: "Quels sont les dangers du produit ?", answer: "2" },
|
|
{ id: 3, text: "Quelles sont les molécules actives et leurs concentrations ?", answer: "3" },
|
|
{ id: 4, text: "Quels gestes adopter en premiers secours ?", answer: "4" },
|
|
{ id: 5, text: "En cas d'incendie, quels sont les moyens d'extinction conseillés ?", answer: "5" },
|
|
{ id: 6, text: "Que faire en cas de dispersion accidentelle ?", answer: "6" },
|
|
{ id: 7, text: "Dans quelles conditions stocker mon produit ?", answer: "7" },
|
|
{ id: 8, text: "Quels équipements de protection individuelle (EPI) dois-je porter ?", answer: "8" },
|
|
{ id: 9, text: "Quel est le pH de ce produit ?", answer: "9" },
|
|
{ id: 10, text: "Le produit est-il stable dans des conditions normales ?", answer: "10" },
|
|
{ id: 11, text: "Quels sont les effets potentiels sur la santé ?", answer: "11" },
|
|
{ id: 12, text: "Quel est le degré de toxicité pour les poissons ? Potentiel de bioaccumulation ?", answer: "12" },
|
|
{ id: 13, text: "Comment éliminer le produit ?", answer: "13" },
|
|
{ id: 14, text: "Est-ce que mon produit est classé dangereux pour le transport ?", answer: "14" },
|
|
{ id: 15, text: "En cas d'ingestion du produit, que faire ?", answer: "4,11" },
|
|
{ id: 16, text: "Où trouver le mode d'emploi du produit ?", answer: "absent" }
|
|
];
|
|
const ftQuestions = [
|
|
{ id: 1, text: "Quel est le nom du produit et qui est le fabricant ?", answer: "Description" },
|
|
{ id: 2, text: "Sur quelles matières ce produit est-il compatible ?", answer: "Description" },
|
|
{ id: 3, text: "Quel est (sont) le(s) type(s) de ce produit ?", answer: "Propriétés biocides" },
|
|
{ id: 4, text: "Quels sont les types de conditionnements disponibles ?", answer: "Conditionnements" },
|
|
{ id: 5, text: "Où retrouver les différents modes d'utilisation de ce produit ?", answer: "Utilisations" }
|
|
];
|
|
const etiquetteQuestions = [
|
|
{ id: 1, text: "Quel est le nom du produit et le nom de l'entreprise ?", answer: "Identité" },
|
|
{ id: 2, text: "Quels pictogrammes de danger sont affichés ?", answer: "Pictogrammes" },
|
|
{ id: 3, text: "Quels sont les mots d'avertissement (« Attention » ou « Danger ») ?", answer: "Mots d'avertissement" },
|
|
{ id: 4, text: "Quelles sont les mentions de danger (H-statements) identifiées ?", answer: "Mentions de danger" },
|
|
{ id: 5, text: "Quels équipements de protection individuelle (EPI) sont recommandés ?", answer: "Conseils de prudence" },
|
|
{ id: 6, text: "Quels premiers secours sont indiqués ?", answer: "Conseils de prudence" },
|
|
{ id: 7, text: "Comment éliminer l'emballage après utilisation ?", answer: "Conseils de prudence" },
|
|
{ id: 8, text: "Quel est le contenu net du produit ?", answer: "Contenu net" },
|
|
{ id: 9, text: "Y a-t-il un numéro d'appel d'urgence mentionné ?", answer: "Numéro d'urgence" },
|
|
{ id: 10, text: "Quelles consignes de stockage sont affichées ?", answer: "Conseils de prudence" }
|
|
];
|
|
|
|
const PARTS = [
|
|
{ prefix: "P1", title: "Partie 1 — FDS", icon: "📄", color: "amber", questions: fdsQuestions, kind: "fds" },
|
|
{ prefix: "P2", title: "Partie 2 — Fiche Technique", icon: "📋", color: "coral", questions: ftQuestions, kind: "text" },
|
|
{ prefix: "P3", title: "Partie 3 — Étiquette", icon: "🏷️", color: "teal", questions: etiquetteQuestions, kind: "text" },
|
|
];
|
|
const COLOR_MAP = {
|
|
amber: { bg: "var(--amber-light)", fg: "var(--amber-dark)" },
|
|
coral: { bg: "var(--coral-light)", fg: "var(--coral)" },
|
|
teal: { bg: "var(--teal-light)", fg: "var(--teal)" },
|
|
};
|
|
|
|
function escapeRx(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
|
|
function escapeHtml(s) {
|
|
if (!s) return "";
|
|
return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
|
}
|
|
|
|
function captureBetween(text, label, nextLabels) {
|
|
const startRx = new RegExp(escapeRx(label) + "\\s*:?\\s*", "i");
|
|
const m = startRx.exec(text);
|
|
if (!m) return "";
|
|
const from = m.index + m[0].length;
|
|
let to = text.length;
|
|
for (const nl of nextLabels) {
|
|
const endRx = new RegExp(escapeRx(nl), "i");
|
|
const m2 = endRx.exec(text.slice(from));
|
|
if (m2 && from + m2.index < to) to = from + m2.index;
|
|
}
|
|
return text.slice(from, to).trim();
|
|
}
|
|
|
|
// Extrait la réponse du groupe pour un marqueur [Px-Qn]
|
|
function extractSelected(fullText, marker, nextMarker) {
|
|
const seg = captureBetween(fullText, marker, nextMarker ? [nextMarker] : []);
|
|
const m = /Réponse du groupe\s*:?\s*([\s\S]*?)\s*[—–-]*\s*Attendu\s*:/i.exec(seg);
|
|
return m ? m[1].trim() : "";
|
|
}
|
|
|
|
/* ==== Correction ==== */
|
|
function normFdsToken(sel) {
|
|
if (!sel || /non répondu/i.test(sel)) return null;
|
|
if (/^absent$/i.test(sel.trim())) return "absent";
|
|
const m = /(\d+)/.exec(sel);
|
|
return m ? m[1] : "";
|
|
}
|
|
function scoreFds(q, sel) {
|
|
const tok = normFdsToken(sel);
|
|
if (tok === null) return { state: "miss" };
|
|
return { state: q.answer.split(",").includes(tok) ? "ok" : "ko" };
|
|
}
|
|
function scoreText(q, sel) {
|
|
if (!sel || /non répondu/i.test(sel)) return { state: "miss" };
|
|
const norm = /^absent$/i.test(sel.trim()) ? "absent" : sel.trim();
|
|
return { state: norm.toLowerCase() === q.answer.toLowerCase() ? "ok" : "ko" };
|
|
}
|
|
function expectedLabel(part, q) {
|
|
if (part.kind === "fds") {
|
|
const parts = q.answer.split(",");
|
|
if (parts.length > 1) return "Rubrique " + parts.join(" ou ");
|
|
return parts[0] === "absent" ? "Absent" : "Rubrique " + parts[0];
|
|
}
|
|
return q.answer;
|
|
}
|
|
|
|
function parsePdfText(raw) {
|
|
let clean = raw.replace(/\r/g, "").replace(/[ \t]+\n/g, "\n").replace(/\s+/g, " ").trim();
|
|
// Nettoie les en-têtes/pieds d'impression du navigateur sans toucher au corps du document
|
|
clean = clean.replace(/\d{1,2}\/\d{1,2}\/\d{4},?\s+\d{1,2}:\d{2}/g, " ");
|
|
clean = clean.replace(/FDS, FT (&|&) Étiquettes\s*[—–-]\s*Réponses\s*\([^)]*\)/gi, " ");
|
|
clean = clean.replace(/about:blank\s*\d{1,3}\s*\/\s*\d{1,3}/gi, " ");
|
|
clean = clean.replace(/about:blank/gi, " ");
|
|
clean = clean.replace(/\s+/g, " ").trim();
|
|
|
|
const meta = {
|
|
nom: captureBetween(clean, "Groupe / Stagiaire", ["Date de session"]),
|
|
date_session: captureBetween(clean, "Date de session", ["Formateur"]),
|
|
formateur: captureBetween(clean, "Formateur", ["Score global"]),
|
|
};
|
|
const groupMatch = /^Groupe\s*(\d+)\s*[—\-:]*\s*(.*)$/i.exec(meta.nom.trim());
|
|
if (groupMatch) {
|
|
meta.groupe = "Groupe " + groupMatch[1];
|
|
meta.participants = groupMatch[2].trim();
|
|
} else {
|
|
meta.groupe = "";
|
|
meta.participants = meta.nom.trim();
|
|
}
|
|
|
|
// Liste ordonnée de tous les marqueurs pour borner chaque extraction
|
|
const markers = [];
|
|
PARTS.forEach(p => p.questions.forEach((q, i) => markers.push("[" + p.prefix + "-Q" + (i + 1) + "]")));
|
|
|
|
const selected = {};
|
|
let mIdx = 0;
|
|
PARTS.forEach(p => {
|
|
p.questions.forEach((q, i) => {
|
|
const marker = "[" + p.prefix + "-Q" + (i + 1) + "]";
|
|
const next = markers[mIdx + 1];
|
|
selected[marker] = extractSelected(clean, marker, next);
|
|
mIdx++;
|
|
});
|
|
});
|
|
|
|
return { meta, selected };
|
|
}
|
|
|
|
let currentData = null;
|
|
let currentPart = 0;
|
|
|
|
function partResults(part) {
|
|
return part.questions.map((q, i) => {
|
|
const marker = "[" + part.prefix + "-Q" + (i + 1) + "]";
|
|
const sel = (currentData.selected[marker] || "").trim();
|
|
const res = part.kind === "fds" ? scoreFds(q, sel) : scoreText(q, sel);
|
|
return { q, i, sel, state: res.state };
|
|
});
|
|
}
|
|
|
|
function renderPart() {
|
|
const part = PARTS[currentPart];
|
|
const colors = COLOR_MAP[part.color];
|
|
const results = partResults(part);
|
|
const ok = results.filter(r => r.state === "ok").length;
|
|
|
|
document.getElementById("themePanel").innerHTML = `
|
|
<div class="theme-panel">
|
|
<div class="theme-head">
|
|
<div class="theme-icon" style="background:${colors.bg};color:${colors.fg};">${part.icon}</div>
|
|
<div class="theme-title">${part.title}</div>
|
|
<div class="theme-count">${ok}/${results.length} corrects</div>
|
|
</div>
|
|
<div class="cards">
|
|
${results.map(r => {
|
|
const badge = r.state === "ok" ? "ok" : (r.state === "ko" ? "ko" : "miss");
|
|
const badgeIcon = r.state === "ok" ? "✓" : (r.state === "ko" ? "✗" : "?");
|
|
const groupTxt = r.state === "miss" ? "Sans réponse" : escapeHtml(r.sel);
|
|
const showExpected = r.state !== "ok";
|
|
return `
|
|
<div class="qcard">
|
|
<div class="badge ${badge}">${badgeIcon}</div>
|
|
<div class="qtext">
|
|
<div class="qlabel">Q${r.i + 1}. ${escapeHtml(r.q.text)}</div>
|
|
<div class="qgroup ${badge}">Réponse du groupe : ${groupTxt}</div>
|
|
${showExpected ? `<div class="qexpected">Attendu : ${escapeHtml(expectedLabel(part, r.q))}</div>` : ""}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join("")}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById("prevBtn").disabled = currentPart === 0;
|
|
document.getElementById("nextBtn").disabled = currentPart === PARTS.length - 1;
|
|
document.querySelectorAll(".dot").forEach((d, i) => d.classList.toggle("active", i === currentPart));
|
|
}
|
|
|
|
function goTo(idx) {
|
|
if (idx < 0 || idx >= PARTS.length) return;
|
|
currentPart = idx;
|
|
renderPart();
|
|
}
|
|
|
|
function renderResult(data) {
|
|
currentData = data;
|
|
currentPart = 0;
|
|
|
|
const banner = document.getElementById("participantsBanner");
|
|
const noms = data.meta.participants || data.meta.nom;
|
|
if (noms) {
|
|
banner.style.display = "flex";
|
|
banner.innerHTML = `
|
|
<div class="pb-icon">👥</div>
|
|
<div>
|
|
<div class="pb-group">${escapeHtml(data.meta.groupe) || "Participants"}</div>
|
|
<div class="pb-names">${escapeHtml(noms)}</div>
|
|
</div>
|
|
<div class="pb-meta">
|
|
${data.meta.date_session ? "Session : " + escapeHtml(data.meta.date_session) + "<br>" : ""}
|
|
${data.meta.formateur ? "Formateur : " + escapeHtml(data.meta.formateur) : ""}
|
|
</div>
|
|
`;
|
|
} else {
|
|
banner.style.display = "none";
|
|
}
|
|
|
|
// Score global + par partie
|
|
let totalOk = 0, totalNb = 0;
|
|
const pillHtml = PARTS.map(p => {
|
|
const results = partResults(p);
|
|
const ok = results.filter(r => r.state === "ok").length;
|
|
totalOk += ok; totalNb += results.length;
|
|
return `<div class="score-pill">${p.icon} ${ok}/${results.length}</div>`;
|
|
}).join("");
|
|
|
|
const pct = totalNb ? Math.round((totalOk / totalNb) * 100) : 0;
|
|
const ringColor = pct >= 80 ? "var(--teal-mid)" : (pct >= 60 ? "var(--amber)" : "var(--coral-mid)");
|
|
const verdict = pct >= 80 ? "Très bon rendu" : (pct >= 60 ? "Rendu correct" : "À revoir en priorité");
|
|
|
|
document.getElementById("scoreCard").innerHTML = `
|
|
<div class="score-ring" style="background:${ringColor};">
|
|
<div class="score-pct">${pct}%</div>
|
|
<div class="score-lbl">RÉUSSITE</div>
|
|
</div>
|
|
<div class="score-detail">
|
|
<div class="score-main">${totalOk} / ${totalNb} bonnes réponses</div>
|
|
<div class="score-sub">${verdict}</div>
|
|
</div>
|
|
<div class="score-parts">${pillHtml}</div>
|
|
`;
|
|
|
|
document.getElementById("dots").innerHTML = PARTS.map((_, i) =>
|
|
`<div class="dot${i === 0 ? ' active' : ''}" onclick="goTo(${i})"></div>`
|
|
).join("");
|
|
|
|
renderPart();
|
|
|
|
document.getElementById("dropzone").style.display = "none";
|
|
document.getElementById("result").style.display = "block";
|
|
}
|
|
|
|
async function handleFile(file) {
|
|
if (!file) return;
|
|
const statusEl = document.getElementById("status");
|
|
statusEl.className = "loading";
|
|
statusEl.textContent = "Lecture du PDF en cours…";
|
|
document.getElementById("fileName").textContent = file.name;
|
|
|
|
try {
|
|
const arrayBuffer = await file.arrayBuffer();
|
|
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
|
let fullText = "";
|
|
for (let p = 1; p <= pdf.numPages; p++) {
|
|
const page = await pdf.getPage(p);
|
|
const content = await page.getTextContent();
|
|
fullText += content.items.map(it => it.str).join(" ") + "\n";
|
|
}
|
|
const data = parsePdfText(fullText);
|
|
renderResult(data);
|
|
statusEl.textContent = "";
|
|
} catch (e) {
|
|
console.error(e);
|
|
statusEl.className = "error";
|
|
statusEl.textContent = "Impossible de lire ce PDF. Vérifiez qu'il s'agit bien d'un export du TD FDS, FT & Étiquettes.";
|
|
}
|
|
}
|
|
|
|
function resetAll() {
|
|
document.getElementById("result").style.display = "none";
|
|
document.getElementById("dropzone").style.display = "block";
|
|
document.getElementById("participantsBanner").style.display = "none";
|
|
document.getElementById("status").textContent = "";
|
|
document.getElementById("filePicker").value = "";
|
|
}
|
|
|
|
const dropzone = document.getElementById("dropzone");
|
|
dropzone.addEventListener("dragover", e => { e.preventDefault(); dropzone.classList.add("drag"); });
|
|
dropzone.addEventListener("dragleave", () => dropzone.classList.remove("drag"));
|
|
dropzone.addEventListener("drop", e => {
|
|
e.preventDefault();
|
|
dropzone.classList.remove("drag");
|
|
const file = e.dataTransfer.files?.[0];
|
|
if (file) handleFile(file);
|
|
});
|
|
document.getElementById("filePicker").addEventListener("change", e => handleFile(e.target.files?.[0]));
|
|
|
|
// Chargement automatique depuis la page « Corriger un exercice » (?auto=1)
|
|
(function autoLoadFromHub() {
|
|
const params = new URLSearchParams(location.search);
|
|
if (params.get("auto") !== "1") return;
|
|
const dataUrl = localStorage.getItem("correctionFile");
|
|
const name = localStorage.getItem("correctionFileName") || "rendu.pdf";
|
|
localStorage.removeItem("correctionFile");
|
|
localStorage.removeItem("correctionFileName");
|
|
localStorage.removeItem("correctionExo");
|
|
if (!dataUrl) return;
|
|
try {
|
|
const bin = atob(dataUrl.split(",").pop());
|
|
const bytes = new Uint8Array(bin.length);
|
|
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
handleFile(new File([bytes], name, { type: "application/pdf" }));
|
|
} catch (e) { console.error(e); }
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|