Files
Site_web/exercices/corriger-exercice.html
gauthier.chombart 421ae4c2bf Unifie la correction des exercices en une page unique
La page de correction ne réagissait pas au dépôt d'un PDF : elle encodait
le fichier en base64 dans localStorage puis redirigeait vers une page
dédiée censée le relire. Ce relais échoue en file://, qui est le mode
d'ouverture habituel des pages.

corriger-exercice.html devient autonome : sélection de l'exercice, dépôt
du PDF et affichage des réponses se font sur place, sans localStorage ni
redirection. Les trois parseurs (BioCID, FDS, TP Nuisibles) sont regroupés
derrière un format commun avec carrousel exercice par exercice, bandeau
participants, correction chiffrée pour la partie FDS et annexe TP3.

Ajoute un garde-fou signalant un PDF ne correspondant pas à l'exercice
sélectionné, repointe les boutons « Corriger » de Exo_FDS et TP_Nuisibles
vers la page unifiée, et supprime les trois pages de correction dédiées
devenues sans référence.
2026-07-20 20:30:16 +02:00

1011 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Corriger un exercice — 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: 860px; margin: 0 auto; }
.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: 8px;
}
.lead { color: var(--text-muted); font-size: 14.5px; line-height: 1.6; margin-bottom: 28px; max-width: 620px; }
/* STEP LABEL */
.step-label {
font-family: 'Syne', sans-serif;
font-size: 13px;
font-weight: 700;
color: var(--text);
margin: 0 0 14px;
display: flex;
align-items: center;
gap: 10px;
}
.step-num {
width: 24px; height: 24px;
border-radius: 50%;
background: var(--amber);
color: #fff;
font-size: 12px;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
/* EXERCISE SELECTOR */
.exo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 12px;
margin-bottom: 36px;
}
.exo-option { position: relative; }
.exo-option input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.exo-option label {
display: flex;
align-items: center;
gap: 14px;
background: #fff;
border: 1.5px solid var(--border);
border-radius: 14px;
padding: 16px 18px;
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
height: 100%;
}
.exo-option label:hover { border-color: var(--amber); }
.exo-option input:checked + label {
border-color: var(--amber);
background: var(--amber-light);
box-shadow: 0 4px 16px rgba(186,117,23,0.12);
}
.exo-option input:focus-visible + label { outline: 2px solid var(--amber); outline-offset: 2px; }
.exo-icon {
width: 44px; height: 44px;
border-radius: 12px;
background: var(--amber-light);
display: flex; align-items: center; justify-content: center;
font-size: 22px;
flex-shrink: 0;
}
.exo-option input:checked + label .exo-icon { background: #fff; }
.exo-text { flex: 1; min-width: 0; }
.exo-name { display: block; font-family: 'Syne', sans-serif; font-weight: 700; font-size: 14.5px; }
.exo-sub { display: block; font-size: 12.5px; color: var(--text-muted); margin-top: 2px; }
.exo-check {
width: 22px; height: 22px;
border-radius: 50%;
border: 1.5px solid var(--border);
flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
color: #fff;
font-size: 12px;
transition: background 0.15s, border-color 0.15s;
}
.exo-option input:checked + label .exo-check {
background: var(--amber);
border-color: var(--amber);
}
.exo-option input:checked + label .exo-check::after { content: '✓'; }
/* 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, opacity 0.2s;
}
#dropzone.drag { border-color: var(--amber); background: var(--amber-light); }
#dropzone.disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; }
#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; min-height: 18px; }
#status.hint { color: var(--text-muted); }
#status.loading { color: var(--amber); }
#status.error { color: var(--coral); }
/* ======================= RÉSULTAT ======================= */
#result { display: none; }
#setup.hidden { display: none; }
.toprow {
display: flex;
justify-content: space-between;
align-items: center;
gap: 14px;
margin-bottom: 18px;
flex-wrap: wrap;
}
#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: 24px;
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;
}
.progress-row { display: flex; align-items: center; gap: 10px; margin-bottom: 30px; font-size: 13px; color: var(--text-muted); }
.progress-track { flex: 1; height: 7px; background: rgba(0,0,0,0.08); border-radius: 6px; overflow: hidden; }
.progress-fill { height: 100%; background: var(--amber); transition: width 0.4s ease; }
.progress-row strong { font-family: 'Syne', sans-serif; color: var(--text); }
/* CARROUSEL */
.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; white-space: nowrap; }
.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; min-width: 0; }
.qcard .qlabel { font-weight: 600; font-size: 13.5px; margin-bottom: 4px; }
.qcard .qanswer { font-size: 13.5px; white-space: pre-wrap; overflow-wrap: anywhere; }
.qcard .qanswer.empty { color: var(--text-muted); font-style: italic; }
.qcard .qexpected { font-size: 12.5px; color: var(--text-muted); margin-top: 4px; }
.qcard .qexpected strong { color: var(--teal); }
/* ANNEXES (TP3 …) */
.annex {
background: #fff;
border: 1px solid var(--border);
border-radius: 18px;
padding: 24px 28px;
margin-bottom: 24px;
}
.annex h2 {
font-family: 'Syne', sans-serif;
font-size: 15px;
font-weight: 800;
margin-bottom: 14px;
}
.annex .kv { font-size: 13.5px; margin-bottom: 6px; }
.annex .kv span { color: var(--text-muted); }
.table-scroll { overflow-x: auto; margin-top: 14px; }
.annex table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 520px; }
.annex th, .annex td {
border: 1px solid var(--border);
padding: 8px 10px;
text-align: left;
vertical-align: top;
}
.annex th { background: var(--amber-light); font-family: 'Syne', sans-serif; font-size: 12px; }
</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="setup">
<div class="eyebrow">Certibiocide — Correction des rendus</div>
<h1>Corriger un exercice</h1>
<p class="lead">Sélectionnez l'exercice concerné, puis déposez le fichier PDF exporté par le groupe. Les réponses s'affichent directement ici, exercice par exercice.</p>
<p class="step-label"><span class="step-num">1</span> Choisissez l'exercice à corriger</p>
<div class="exo-grid" id="exoGrid">
<div class="exo-option">
<input type="radio" name="exo" id="exo-biocid" value="biocid">
<label for="exo-biocid">
<span class="exo-icon">🧪</span>
<span class="exo-text">
<span class="exo-name">Plateforme BioCID</span>
<span class="exo-sub">Recherche de produits biocides</span>
</span>
<span class="exo-check"></span>
</label>
</div>
<div class="exo-option">
<input type="radio" name="exo" id="exo-fds" value="fds">
<label for="exo-fds">
<span class="exo-icon">📋</span>
<span class="exo-text">
<span class="exo-name">FDS, FT &amp; Étiquettes</span>
<span class="exo-sub">Lecture documentaire</span>
</span>
<span class="exo-check"></span>
</label>
</div>
<div class="exo-option">
<input type="radio" name="exo" id="exo-tp" value="tp">
<label for="exo-tp">
<span class="exo-icon">📚</span>
<span class="exo-text">
<span class="exo-name">TP Nuisibles final</span>
<span class="exo-sub">Étude de cas de synthèse</span>
</span>
<span class="exo-check"></span>
</label>
</div>
</div>
<p class="step-label"><span class="step-num">2</span> Déposez le rendu PDF</p>
<div id="dropzone" class="disabled">
<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 l'exercice</p>
<label class="btn-pick">Choisir un fichier
<input type="file" id="filePicker" accept="application/pdf">
</label>
<div id="status" class="hint">Sélectionnez d'abord un exercice ci-dessus.</div>
</div>
</div>
<div id="result">
<div id="participantsBanner" style="display:none;"></div>
<div class="eyebrow">Certibiocide — Correction d'exercice</div>
<h1 id="resultTitle"></h1>
<div class="toprow">
<span id="fileName"></span>
<button class="btn-reset" onclick="resetAll()">↺ Charger un autre PDF</button>
</div>
<div class="progress-row">
<div class="progress-track"><div class="progress-fill" id="progressFill"></div></div>
<span><strong id="progressCount"></strong> <span id="progressLabel"></span></span>
</div>
<div class="carousel-wrap">
<div class="carousel-nav">
<button class="nav-btn" id="prevBtn" onclick="goTo(currentGroup - 1)"></button>
<div class="dots" id="dots"></div>
<button class="nav-btn" id="nextBtn" onclick="goTo(currentGroup + 1)"></button>
</div>
<div id="groupPanel"></div>
</div>
<div id="annexes"></div>
</div>
</div>
<footer>
<p>&copy; 2026 Gauthier Chombart &amp; Nathan Chauwin<br>Tous droits réservés — Formations Certibiocide &amp; Techniques — Formateurs indépendants</p>
<p style="margin-top: 10px; opacity: 0.6;">Tous droits réservés — Gauthier Chombart &amp; 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";
/* ======================= OUTILS COMMUNS ======================= */
const COLOR_MAP = {
amber: { bg: "var(--amber-light)", fg: "var(--amber-dark)" },
blue: { bg: "var(--blue-light)", fg: "var(--blue)" },
teal: { bg: "var(--teal-light)", fg: "var(--teal)" },
coral: { bg: "var(--coral-light)", fg: "var(--coral)" },
purple: { bg: "var(--purple-light)", fg: "var(--purple)" },
};
function escapeRx(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
function escapeHtml(s) {
if (!s) return "";
return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
}
function isEmpty(v) {
if (!v) return true;
const t = v.trim();
return t === "" || /^\(?sans réponse\)?$/i.test(t) || t === "—";
}
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();
}
// Aplatit le texte et retire les en-têtes/pieds d'impression du navigateur.
function flatten(raw) {
let clean = raw.replace(/\r/g, "").replace(/\s+/g, " ").trim();
clean = clean.replace(/\d{1,2}\/\d{1,2}\/\d{4},?\s+\d{1,2}:\d{2}/g, " ");
clean = clean.replace(/Page\s*\d{1,3}\s*sur\s*\d{1,3}/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(/file:\/\/\S+/gi, " ");
return clean.replace(/\s+/g, " ").trim();
}
// Sépare "Groupe 3 — Alice Bob" en { groupe, participants }
function splitGroupe(nom) {
const t = (nom || "").trim();
const m = /^Groupe\s*(\d+)\s*[—\-:]*\s*(.*)$/i.exec(t);
if (m) return { groupe: "Groupe " + m[1], participants: m[2].trim() };
return { groupe: "", participants: t };
}
function readMeta(clean, lastKey) {
const meta = {
nom: captureBetween(clean, "Groupe / Stagiaire", ["Date de session"]),
date_session: captureBetween(clean, "Date de session", ["Formateur"]),
formateur: captureBetween(clean, "Formateur", [lastKey]),
};
Object.assign(meta, splitGroupe(meta.nom));
return meta;
}
/* ======================= EXERCICE 1 — BIOCID ======================= */
const BIOCID_GROUPS = [
{ title: "Exercice 1 — Recherche par nom", icon: "🔎", color: "amber", items: [
"Ex1 — Produit : type, substance active, fabricant",
"Ex1 — Date d'autorisation",
"Ex1 — Conditions d'utilisation",
]},
{ title: "Exercice 2 — Recherche par type", icon: "🗂️", color: "blue", items: [
"Ex2 — Nombre de produits dans la catégorie",
"Ex2 — Comparaison entre catégories",
]},
{ title: "Exercice 3 — Recherche par substance", icon: "🧪", color: "teal", items: [
"Ex3 — 3 produits contenant la substance",
"Ex3 — Formulations et concentrations comparées",
]},
{ title: "Exercice 4 — Cas de synthèse", icon: "📞", color: "coral", items: [
"Ex4 — 3 produits applicables",
"Ex4 — Fiche de chaque produit",
"Ex4 — Argumentaire (+ bonus FDS)",
]},
];
function parseBiocid(raw) {
let clean = flatten(raw);
clean = clean.replace(/Exercices BioCID\s*[—–-]\s*Réponses\s*\([^)]*\)/gi, " ").replace(/\s+/g, " ").trim();
const meta = readMeta(clean, "Réponses aux exercices");
const startIdx = clean.search(/Réponses aux exercices/i);
const block = startIdx >= 0 ? clean.slice(startIdx) : clean;
const all = BIOCID_GROUPS.flatMap(g => g.items);
const answers = {};
all.forEach((label, i) => {
answers[label] = captureBetween(block, label, all[i + 1] ? [all[i + 1]] : []);
});
const groups = BIOCID_GROUPS.map(g => ({
title: g.title, icon: g.icon, color: g.color,
items: g.items.map(label => {
const value = answers[label] || "";
return {
label: label.replace(/^Ex\d+\s*[—-]\s*/, ""),
answer: value,
state: isEmpty(value) ? "miss" : "ok",
};
}),
}));
return { title: "Plateforme BioCID — Lecture du rendu", meta, groups, scored: false };
}
/* ======================= EXERCICE 2 — FDS / FT / ÉTIQUETTE ======================= */
const fdsQuestions = [
{ text: "Quel est le nom du produit et qui est le fabricant ?", answer: "1" },
{ text: "Quels sont les dangers du produit ?", answer: "2" },
{ text: "Quelles sont les molécules actives et leurs concentrations ?", answer: "3" },
{ text: "Quels gestes adopter en premiers secours ?", answer: "4" },
{ text: "En cas d'incendie, quels sont les moyens d'extinction conseillés ?", answer: "5" },
{ text: "Que faire en cas de dispersion accidentelle ?", answer: "6" },
{ text: "Dans quelles conditions stocker mon produit ?", answer: "7" },
{ text: "Quels équipements de protection individuelle (EPI) dois-je porter ?", answer: "8" },
{ text: "Quel est le pH de ce produit ?", answer: "9" },
{ text: "Le produit est-il stable dans des conditions normales ?", answer: "10" },
{ text: "Quels sont les effets potentiels sur la santé ?", answer: "11" },
{ text: "Quel est le degré de toxicité pour les poissons ? Potentiel de bioaccumulation ?", answer: "12" },
{ text: "Comment éliminer le produit ?", answer: "13" },
{ text: "Est-ce que mon produit est classé dangereux pour le transport ?", answer: "14" },
{ text: "En cas d'ingestion du produit, que faire ?", answer: "4,11" },
{ text: "Où trouver le mode d'emploi du produit ?", answer: "absent" },
];
const ftQuestions = [
{ text: "Quel est le nom du produit et qui est le fabricant ?", answer: "Description" },
{ text: "Sur quelles matières ce produit est-il compatible ?", answer: "Description" },
{ text: "Quel est (sont) le(s) type(s) de ce produit ?", answer: "Propriétés biocides" },
{ text: "Quels sont les types de conditionnements disponibles ?", answer: "Conditionnements" },
{ text: "Où retrouver les différents modes d'utilisation de ce produit ?", answer: "Utilisations" },
];
const etiquetteQuestions = [
{ text: "Quel est le nom du produit et le nom de l'entreprise ?", answer: "Identité" },
{ text: "Quels pictogrammes de danger sont affichés ?", answer: "Pictogrammes" },
{ text: "Quels sont les mots d'avertissement (« Attention » ou « Danger ») ?", answer: "Mots d'avertissement" },
{ text: "Quelles sont les mentions de danger (H-statements) identifiées ?", answer: "Mentions de danger" },
{ text: "Quels équipements de protection individuelle (EPI) sont recommandés ?", answer: "Conseils de prudence" },
{ text: "Quels premiers secours sont indiqués ?", answer: "Conseils de prudence" },
{ text: "Comment éliminer l'emballage après utilisation ?", answer: "Conseils de prudence" },
{ text: "Quel est le contenu net du produit ?", answer: "Contenu net" },
{ text: "Y a-t-il un numéro d'appel d'urgence mentionné ?", answer: "Numéro d'urgence" },
{ text: "Quelles consignes de stockage sont affichées ?", answer: "Conseils de prudence" },
];
const FDS_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" },
];
// Récupère « Réponse du groupe : X — Attendu : … » 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() : "";
}
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 "miss";
return q.answer.split(",").includes(tok) ? "ok" : "ko";
}
function scoreText(q, sel) {
if (!sel || /non répondu/i.test(sel)) return "miss";
const norm = /^absent$/i.test(sel.trim()) ? "absent" : sel.trim();
return norm.toLowerCase() === q.answer.toLowerCase() ? "ok" : "ko";
}
function fdsExpectedLabel(part, q) {
if (part.kind !== "fds") return q.answer;
const parts = q.answer.split(",");
if (parts.length > 1) return "Rubrique " + parts.join(" ou ");
return parts[0] === "absent" ? "Absent de la FDS" : "Rubrique " + parts[0];
}
function parseFds(raw) {
let clean = flatten(raw);
clean = clean.replace(/FDS, FT (&|&amp;) Étiquettes\s*[—–-]\s*Réponses\s*\([^)]*\)/gi, " ").replace(/\s+/g, " ").trim();
const meta = readMeta(clean, "Score global");
const markers = [];
FDS_PARTS.forEach(p => p.questions.forEach((q, i) => markers.push("[" + p.prefix + "-Q" + (i + 1) + "]")));
let mIdx = 0;
const groups = FDS_PARTS.map(part => ({
title: part.title, icon: part.icon, color: part.color,
items: part.questions.map((q, i) => {
const marker = "[" + part.prefix + "-Q" + (i + 1) + "]";
const sel = extractSelected(clean, marker, markers[mIdx + 1]);
mIdx++;
const state = part.kind === "fds" ? scoreFds(q, sel) : scoreText(q, sel);
return {
label: q.text,
answer: sel,
state,
expected: fdsExpectedLabel(part, q),
};
}),
}));
return { title: "FDS, FT & Étiquettes — Correction du rendu", meta, groups, scored: true };
}
/* ======================= EXERCICE 3 — TP NUISIBLES ======================= */
const TP_GROUPS = [
{ title: "Premier contact & diagnostic", icon: "🗣️", color: "purple", items: [
"Questions posées au client lors du premier contact",
"Conseils donnés en premier lieu",
"Actions de prévention menées sur le site",
"Éléments observés / preuve de présence du nuisible",
]},
{ title: "Solutions envisagées", icon: "🧰", color: "blue", items: [
"Solutions alternatives non-biocides",
"Solutions biocides mises en place",
"Type de produit (TP) et mode de traitement",
"Suivi mis en place et durée",
"Gestion des déchets",
]},
{ title: "Produit & traçabilité", icon: "🧪", color: "teal", items: [
"FDS trouvée (éléments principaux)",
"Nom du produit et fabricant/fournisseur",
"Numéro AMM",
]},
{ title: "Sécurité & santé", icon: "🛡️", color: "coral", items: [
"Risques pour la santé",
"EPI à porter",
"Risques environnement / dispersion accidentelle",
"DL50 substance(s) active(s)",
"Gestes de premiers secours",
]},
];
const TP_KNOWN_STEPS = [
"remplir la poudreuse",
"sécurisation du site",
"application traitements",
"rangement du matériel. purger la perche",
"retrait du nid",
"gestion des déchets. élimination par des filiaires spé.",
];
function parseTpRiskTable(tp3Zone) {
const rows = [];
const tableMatch = /Étapes\s*Risques\s*Mesures de prévention([\s\S]*?)(Réalisée par|$)/i.exec(tp3Zone);
if (!tableMatch) return rows;
const raw = tableMatch[1].trim();
if (!raw) return rows;
const positions = [];
TP_KNOWN_STEPS.forEach(step => {
const idx = raw.toLowerCase().indexOf(step.toLowerCase());
if (idx >= 0) positions.push({ step, idx });
});
positions.sort((a, b) => a.idx - b.idx);
if (positions.length === 0) {
rows.push({ etape: raw, risque: "—", mesure: "—" });
return rows;
}
for (let i = 0; i < positions.length; i++) {
const from = positions[i].idx + positions[i].step.length;
const to = i + 1 < positions.length ? positions[i + 1].idx : raw.length;
const rest = raw.slice(from, to).trim();
const measureRx = /(gants\s*\/\s*masques\s*\/\s*lunettes[^.]*|barrieres\.?\s*epi)/i;
const mm = measureRx.exec(rest);
let risque = rest, mesure = "";
if (mm) {
risque = rest.slice(0, mm.index).trim();
mesure = rest.slice(mm.index).trim();
}
risque = risque.replace(/^[\s\-.]+/, "").trim();
rows.push({ etape: positions[i].step, risque: risque || "—", mesure: mesure || "—" });
}
return rows;
}
function parseTp(raw) {
let clean = flatten(raw);
clean = clean.replace(/Résultats TP\s*[—–-]\s*[^[]{0,80}?(?=Groupe \/ Stagiaire)/gi, " ").replace(/\s+/g, " ").trim();
const meta = readMeta(clean, "Cas pratique");
meta.cas = captureBetween(clean, "Cas pratique", ["Réponses TP1"]);
const startIdx = clean.search(/Réponses TP1\s*&?\s*TP2/i);
const endIdx = clean.search(/TP3\s*[—–-]\s*Analyse de risques/i);
const block = startIdx >= 0 ? clean.slice(startIdx, endIdx >= 0 ? endIdx : clean.length) : clean;
const all = TP_GROUPS.flatMap(g => g.items);
const answers = {};
all.forEach((label, i) => {
answers[label] = captureBetween(block, label, all[i + 1] ? [all[i + 1]] : []);
});
const groups = TP_GROUPS.map(g => ({
title: g.title, icon: g.icon, color: g.color,
items: g.items.map(label => ({
label,
answer: answers[label] || "",
state: isEmpty(answers[label]) ? "miss" : "ok",
})),
}));
const tp3Zone = endIdx >= 0 ? clean.slice(endIdx) : "";
const tp3 = {
intitule: captureBetween(tp3Zone, "Intitulé", ["Description"]),
description: captureBetween(tp3Zone, "Description", ["Lieu"]),
lieu: captureBetween(tp3Zone, "Lieu", ["Début prévu le"]),
debut: captureBetween(tp3Zone, "Début prévu le", ["Étapes"]),
};
const riskRows = parseTpRiskTable(tp3Zone);
const mReal = clean.match(/Réalisée par\s*:\s*([^—\n]*)/i);
const mVerif = clean.match(/Vérifiée par\s*:\s*([^—\n]*)/i);
const mMaj = clean.match(/Mise à jour par\s*:\s*([^—\n]*)/i);
const annexes = [];
if (tp3.intitule || riskRows.length) {
annexes.push({
title: "TP3 — Analyse de risques",
html: `
<div class="kv"><span>Intitulé :</span> ${escapeHtml(tp3.intitule) || "—"}</div>
<div class="kv"><span>Description :</span> ${escapeHtml(tp3.description) || "—"}</div>
<div class="kv"><span>Lieu :</span> ${escapeHtml(tp3.lieu) || "—"}</div>
<div class="kv"><span>Début prévu le :</span> ${escapeHtml(tp3.debut) || "—"}</div>
${riskRows.length ? `
<div class="table-scroll">
<table>
<tr><th>Étapes</th><th>Risques</th><th>Mesures de prévention</th></tr>
${riskRows.map(r => `<tr><td>${escapeHtml(r.etape)}</td><td>${escapeHtml(r.risque)}</td><td>${escapeHtml(r.mesure)}</td></tr>`).join("")}
</table>
</div>` : ""}
<div class="kv" style="margin-top:14px;">
<span>Réalisée par :</span> ${escapeHtml(mReal ? mReal[1].trim() : "") || "—"} &nbsp;·&nbsp;
<span>Vérifiée par :</span> ${escapeHtml(mVerif ? mVerif[1].trim() : "") || "—"} &nbsp;·&nbsp;
<span>Mise à jour par :</span> ${escapeHtml(mMaj ? mMaj[1].trim() : "") || "—"}
</div>
`,
});
}
return { title: "TP Nuisibles — Lecture du rendu", meta, groups, scored: false, annexes };
}
/* ======================= REGISTRE DES EXERCICES ======================= */
const EXERCISES = {
biocid: { parse: parseBiocid, sentinel: /Réponses aux exercices|Ex1\s*[—-]/i, name: "BioCID" },
fds: { parse: parseFds, sentinel: /\[P1-Q1\]|Partie 1\s*[—-]\s*FDS/i, name: "FDS, FT & Étiquettes" },
tp: { parse: parseTp, sentinel: /Réponses TP1|TP Nuisibles/i, name: "TP Nuisibles" },
};
/* ======================= RENDU ======================= */
let currentData = null;
let currentGroup = 0;
function renderGroupPanel() {
const group = currentData.groups[currentGroup];
const colors = COLOR_MAP[group.color] || COLOR_MAP.amber;
const good = group.items.filter(it => it.state === "ok").length;
const countLabel = currentData.scored
? `${good}/${group.items.length} corrects`
: `${good}/${group.items.length} renseignées`;
document.getElementById("groupPanel").innerHTML = `
<div class="theme-panel">
<div class="theme-head">
<div class="theme-icon" style="background:${colors.bg};color:${colors.fg};">${group.icon}</div>
<div class="theme-title">${escapeHtml(group.title)}</div>
<div class="theme-count">${countLabel}</div>
</div>
<div class="cards">
${group.items.map(it => {
const mark = it.state === "ok" ? "✓" : (it.state === "ko" ? "✗" : "!");
const empty = it.state === "miss";
return `
<div class="qcard">
<div class="badge ${it.state}">${mark}</div>
<div class="qtext">
<div class="qlabel">${escapeHtml(it.label)}</div>
<div class="qanswer ${empty ? "empty" : ""}">${empty ? "Sans réponse" : escapeHtml(it.answer)}</div>
${it.expected && it.state !== "ok"
? `<div class="qexpected">Attendu : <strong>${escapeHtml(it.expected)}</strong></div>`
: ""}
</div>
</div>`;
}).join("")}
</div>
</div>`;
document.getElementById("prevBtn").disabled = currentGroup === 0;
document.getElementById("nextBtn").disabled = currentGroup === currentData.groups.length - 1;
document.querySelectorAll(".dot").forEach((d, i) => d.classList.toggle("active", i === currentGroup));
}
function goTo(idx) {
if (!currentData || idx < 0 || idx >= currentData.groups.length) return;
currentGroup = idx;
renderGroupPanel();
}
function renderResult(data) {
currentData = data;
currentGroup = 0;
document.getElementById("resultTitle").textContent = data.title;
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) + "<br>" : ""}
${data.meta.cas ? "Cas : " + escapeHtml(data.meta.cas) : ""}
</div>`;
} else {
banner.style.display = "none";
}
const allItems = data.groups.flatMap(g => g.items);
const good = allItems.filter(it => it.state === "ok").length;
document.getElementById("progressFill").style.width = (good / allItems.length * 100) + "%";
document.getElementById("progressCount").textContent = `${good} / ${allItems.length}`;
document.getElementById("progressLabel").textContent = data.scored ? "réponses correctes" : "questions renseignées";
document.getElementById("dots").innerHTML = data.groups.map((_, i) =>
`<div class="dot${i === 0 ? " active" : ""}" onclick="goTo(${i})"></div>`
).join("");
document.getElementById("annexes").innerHTML = (data.annexes || []).map(a =>
`<div class="annex"><h2>${escapeHtml(a.title)}</h2>${a.html}</div>`
).join("");
renderGroupPanel();
document.getElementById("setup").classList.add("hidden");
document.getElementById("result").style.display = "block";
window.scrollTo({ top: 0, behavior: "smooth" });
}
/* ======================= CHARGEMENT DU PDF ======================= */
const dropzone = document.getElementById("dropzone");
const statusEl = document.getElementById("status");
const filePicker = document.getElementById("filePicker");
function selectedExo() {
const el = document.querySelector('input[name="exo"]:checked');
return el ? el.value : null;
}
function refreshDropzone() {
if (selectedExo()) {
dropzone.classList.remove("disabled");
statusEl.className = "hint";
statusEl.textContent = "Prêt à recevoir le PDF.";
} else {
dropzone.classList.add("disabled");
statusEl.className = "hint";
statusEl.textContent = "Sélectionnez d'abord un exercice ci-dessus.";
}
}
async function readPdfText(file) {
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";
}
return fullText;
}
async function handleFile(file) {
const exo = selectedExo();
if (!exo) {
statusEl.className = "error";
statusEl.textContent = "Choisissez un exercice avant de déposer un fichier.";
return;
}
if (!file) return;
if (file.type !== "application/pdf" && !/\.pdf$/i.test(file.name)) {
statusEl.className = "error";
statusEl.textContent = "Le fichier doit être un PDF.";
return;
}
statusEl.className = "loading";
statusEl.textContent = "Lecture du PDF en cours…";
try {
const text = await readPdfText(file);
const config = EXERCISES[exo];
if (!config.sentinel.test(text)) {
statusEl.className = "error";
statusEl.textContent = `Ce PDF ne ressemble pas à un rendu « ${config.name} ». Vérifiez l'exercice sélectionné.`;
return;
}
const data = config.parse(text);
document.getElementById("fileName").textContent = file.name;
renderResult(data);
statusEl.className = "hint";
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 d'exercice.";
}
}
function resetAll() {
currentData = null;
document.getElementById("result").style.display = "none";
document.getElementById("setup").classList.remove("hidden");
document.getElementById("participantsBanner").style.display = "none";
document.getElementById("annexes").innerHTML = "";
filePicker.value = "";
refreshDropzone();
window.scrollTo({ top: 0, behavior: "smooth" });
}
// Préselection via ?exo=biocid|fds|tp
(function preselect() {
const exo = new URLSearchParams(location.search).get("exo");
if (exo && EXERCISES[exo]) {
const input = document.getElementById("exo-" + exo);
if (input) input.checked = true;
}
})();
document.querySelectorAll('input[name="exo"]').forEach(r =>
r.addEventListener("change", refreshDropzone)
);
refreshDropzone();
dropzone.addEventListener("dragover", e => {
if (dropzone.classList.contains("disabled")) return;
e.preventDefault();
dropzone.classList.add("drag");
});
dropzone.addEventListener("dragleave", () => dropzone.classList.remove("drag"));
dropzone.addEventListener("drop", e => {
if (dropzone.classList.contains("disabled")) return;
e.preventDefault();
dropzone.classList.remove("drag");
handleFile(e.dataTransfer.files && e.dataTransfer.files[0]);
});
filePicker.addEventListener("change", e => handleFile(e.target.files && e.target.files[0]));
</script>
</body>
</html>