Ajoute la correction sur fichier aux exercices BioCID et FDS
Reprend le fonctionnement du TP Nuisibles (« Corriger » à partir du PDF généré avec les réponses du groupe) pour les deux exercices. - BioCID et FDS : carte « Identification du groupe » (groupe, date, formateur) sur l'écran d'intro, reprise dans l'en-tête du PDF exporté. - FDS : ajout d'un export PDF (inexistant) récapitulant réponse du groupe et réponse attendue, partie par partie, avec le score global. - Bouton « Corriger un rendu » sur l'écran final des deux exercices. - Nouvelle page correction-biocid.html : lecture du PDF, réponses regroupées par exercice avec badge renseigné / sans réponse. - Nouvelle page correction-fds.html : notation automatique du QCM (anneau de réussite, score par partie, ✓/✗/? par question).
This commit is contained in:
@@ -350,6 +350,40 @@
|
|||||||
.scroll-area::-webkit-scrollbar { width: 4px; }
|
.scroll-area::-webkit-scrollbar { width: 4px; }
|
||||||
.scroll-area::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
.scroll-area::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||||
|
|
||||||
|
/* IDENTITY CARD */
|
||||||
|
.identity-card {
|
||||||
|
background: white;
|
||||||
|
border: 0.5px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
max-width: 560px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
.identity-card .identity-title {
|
||||||
|
font-family: 'Syne', sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--amber-dark);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
}
|
||||||
|
.identity-fields { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.identity-fields input {
|
||||||
|
font-family: 'DM Sans', sans-serif;
|
||||||
|
font-size: 13.5px;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 9px 13px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.identity-fields input:focus { outline: none; border-color: var(--amber); background: white; }
|
||||||
|
|
||||||
/* DECO */
|
/* DECO */
|
||||||
.deco-circle { position: fixed; border-radius: 50%; pointer-events: none; z-index: 0; }
|
.deco-circle { position: fixed; border-radius: 50%; pointer-events: none; z-index: 0; }
|
||||||
.slide > * { position: relative; z-index: 1; }
|
.slide > * { position: relative; z-index: 1; }
|
||||||
@@ -426,6 +460,14 @@
|
|||||||
<span>🎯</span>
|
<span>🎯</span>
|
||||||
<div>4 exercices vous attendent : recherche par <strong>nom</strong>, par <strong>type</strong>, par <strong>substance active</strong>, puis un <strong>cas de synthèse</strong>. Durée totale ≈ 30 min.</div>
|
<div>4 exercices vous attendent : recherche par <strong>nom</strong>, par <strong>type</strong>, par <strong>substance active</strong>, puis un <strong>cas de synthèse</strong>. Durée totale ≈ 30 min.</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="identity-card">
|
||||||
|
<div class="identity-title">👥 Identification du groupe</div>
|
||||||
|
<div class="identity-fields">
|
||||||
|
<input type="text" id="idGroupe" placeholder="Nom du groupe / stagiaires (ex : Groupe 3 — Dupont / Martin)" autocomplete="off">
|
||||||
|
<input type="date" id="idDate">
|
||||||
|
<input type="text" id="idFormateur" placeholder="Formateur (facultatif)" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button class="btn btn-amber" onclick="goTo(2)" style="margin-top:1.5rem;">Commencer l'exercice 1 →</button>
|
<button class="btn btn-amber" onclick="goTo(2)" style="margin-top:1.5rem;">Commencer l'exercice 1 →</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -550,9 +592,10 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<div class="actions-row">
|
<div class="actions-row">
|
||||||
<button class="btn btn-amber" onclick="exporterPDF()">⬇ Télécharger mes réponses (PDF)</button>
|
<button class="btn btn-amber" onclick="exporterPDF()">⬇ Télécharger mes réponses (PDF)</button>
|
||||||
<a href="https://biocid-anses.fr/biocid" target="_blank" class="btn btn-outline">biocid-anses.fr ↗</a>
|
<a href="correction-biocid.html" class="btn btn-outline">✔ Corriger un rendu</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions-row" style="margin-top:0.5rem;">
|
<div class="actions-row" style="margin-top:0.5rem;">
|
||||||
|
<a href="https://biocid-anses.fr/biocid" target="_blank" class="btn btn-outline">biocid-anses.fr ↗</a>
|
||||||
<button class="btn btn-outline" onclick="restartAll()">↺ Recommencer</button>
|
<button class="btn btn-outline" onclick="restartAll()">↺ Recommencer</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -664,7 +707,16 @@ function escapeHtml(str) {
|
|||||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function idVal(id) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
return el ? el.value.trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
function exporterPDF() {
|
function exporterPDF() {
|
||||||
|
const groupe = idVal('idGroupe');
|
||||||
|
const date = idVal('idDate') || new Date().toISOString().slice(0, 10);
|
||||||
|
const formateur = idVal('idFormateur');
|
||||||
|
|
||||||
const reponses = [];
|
const reponses = [];
|
||||||
document.querySelectorAll('.answer-field').forEach(el => {
|
document.querySelectorAll('.answer-field').forEach(el => {
|
||||||
reponses.push({ question: el.dataset.question || '', reponse: el.value.trim() });
|
reponses.push({ question: el.dataset.question || '', reponse: el.value.trim() });
|
||||||
@@ -674,12 +726,21 @@ function exporterPDF() {
|
|||||||
`<div style="margin-bottom:14px;"><strong>${escapeHtml(r.question)}</strong><p style="margin:4px 0 0;white-space:pre-wrap;">${escapeHtml(r.reponse) || '<em>(sans réponse)</em>'}</p></div>`
|
`<div style="margin-bottom:14px;"><strong>${escapeHtml(r.question)}</strong><p style="margin:4px 0 0;white-space:pre-wrap;">${escapeHtml(r.reponse) || '<em>(sans réponse)</em>'}</p></div>`
|
||||||
).join('');
|
).join('');
|
||||||
w.document.write(`
|
w.document.write(`
|
||||||
<html><head><meta charset="utf-8"><title>Exercices BioCID — Réponses</title>
|
<html><head><meta charset="utf-8"><title>Exercices BioCID — Réponses (${escapeHtml(groupe) || 'sans nom'})</title>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: Arial, sans-serif; padding: 30px; color: #1a1a18; }
|
body { font-family: Arial, sans-serif; padding: 30px; color: #1a1a18; }
|
||||||
h1 { font-size: 20px; border-bottom: 2px solid #BA7517; padding-bottom: 8px; }
|
h1 { font-size: 20px; border-bottom: 2px solid #BA7517; padding-bottom: 8px; }
|
||||||
|
.meta { font-size: 13px; color: #555; margin: 2px 0; }
|
||||||
|
h2 { font-size: 15px; margin-top: 24px; color: #633806; }
|
||||||
</style></head>
|
</style></head>
|
||||||
<body><h1>Exercices Pratiques BioCID</h1>${lignes}</body></html>`);
|
<body>
|
||||||
|
<h1>Exercices Pratiques BioCID</h1>
|
||||||
|
<p class="meta"><strong>Groupe / Stagiaire :</strong> ${escapeHtml(groupe) || '—'}</p>
|
||||||
|
<p class="meta"><strong>Date de session :</strong> ${escapeHtml(date)}</p>
|
||||||
|
<p class="meta"><strong>Formateur :</strong> ${escapeHtml(formateur) || '—'}</p>
|
||||||
|
<h2>Réponses aux exercices</h2>
|
||||||
|
${lignes}
|
||||||
|
</body></html>`);
|
||||||
w.document.close();
|
w.document.close();
|
||||||
setTimeout(() => w.print(), 300);
|
setTimeout(() => w.print(), 300);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -336,6 +336,40 @@ select:focus { outline: none; border-color: var(--amber); box-shadow: 0 0 0 3px
|
|||||||
.scroll-area::-webkit-scrollbar { width: 4px; }
|
.scroll-area::-webkit-scrollbar { width: 4px; }
|
||||||
.scroll-area::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
.scroll-area::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||||
|
|
||||||
|
/* IDENTITY CARD */
|
||||||
|
.identity-card {
|
||||||
|
background: white;
|
||||||
|
border: 0.5px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
max-width: 560px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
.identity-card .identity-title {
|
||||||
|
font-family: 'Syne', sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--amber-dark);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
}
|
||||||
|
.identity-fields { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.identity-fields input {
|
||||||
|
font-family: 'DM Sans', sans-serif;
|
||||||
|
font-size: 13.5px;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 9px 13px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.identity-fields input:focus { outline: none; border-color: var(--amber); background: white; }
|
||||||
|
|
||||||
/* DECO */
|
/* DECO */
|
||||||
.deco-circle { position: fixed; border-radius: 50%; pointer-events: none; z-index: 0; }
|
.deco-circle { position: fixed; border-radius: 50%; pointer-events: none; z-index: 0; }
|
||||||
.slide > * { position: relative; z-index: 1; }
|
.slide > * { position: relative; z-index: 1; }
|
||||||
@@ -413,6 +447,14 @@ select:focus { outline: none; border-color: var(--amber); box-shadow: 0 0 0 3px
|
|||||||
<span>⏱️</span>
|
<span>⏱️</span>
|
||||||
<div>Durée ≈ 30 min. Pour chaque question, sélectionnez la <strong>zone du document</strong> où trouver l'information, puis vérifiez votre score.</div>
|
<div>Durée ≈ 30 min. Pour chaque question, sélectionnez la <strong>zone du document</strong> où trouver l'information, puis vérifiez votre score.</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="identity-card">
|
||||||
|
<div class="identity-title">👥 Identification du groupe</div>
|
||||||
|
<div class="identity-fields">
|
||||||
|
<input type="text" id="idGroupe" placeholder="Nom du groupe / stagiaires (ex : Groupe 3 — Dupont / Martin)" autocomplete="off">
|
||||||
|
<input type="date" id="idDate">
|
||||||
|
<input type="text" id="idFormateur" placeholder="Formateur (facultatif)" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button class="btn btn-amber" onclick="goTo(2)" style="margin-top:1.5rem;">Commencer la partie 1 →</button>
|
<button class="btn btn-amber" onclick="goTo(2)" style="margin-top:1.5rem;">Commencer la partie 1 →</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -523,7 +565,11 @@ select:focus { outline: none; border-color: var(--amber); box-shadow: 0 0 0 3px
|
|||||||
<li>Les trois documents sont <strong>complémentaires</strong>, jamais redondants — chacun cible un besoin et un public différents.</li>
|
<li>Les trois documents sont <strong>complémentaires</strong>, jamais redondants — chacun cible un besoin et un public différents.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="actions-row">
|
<div class="actions-row">
|
||||||
<button class="btn btn-amber" onclick="restartAll()">↺ Recommencer</button>
|
<button class="btn btn-amber" onclick="exporterPDF()">⬇ Télécharger mes réponses (PDF)</button>
|
||||||
|
<a href="correction-fds.html" class="btn btn-outline">✔ Corriger un rendu</a>
|
||||||
|
</div>
|
||||||
|
<div class="actions-row" style="margin-top:0.5rem;">
|
||||||
|
<button class="btn btn-outline" onclick="restartAll()">↺ Recommencer</button>
|
||||||
<a href="../index.html#formations" class="btn btn-outline">Retour aux exercices</a>
|
<a href="../index.html#formations" class="btn btn-outline">Retour aux exercices</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -700,6 +746,82 @@ function verifyEtiquette() {
|
|||||||
showScore(document.getElementById('etiquette-result'), correct, etiquetteQuestions.length);
|
showScore(document.getElementById('etiquette-result'), correct, etiquetteQuestions.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ======================== EXPORT PDF ======================== */
|
||||||
|
function escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
function idVal(id) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
return el ? el.value.trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Libellé lisible d'une réponse FDS (numéro de rubrique) et de l'attendu
|
||||||
|
function fdsAnswerLabel(v) {
|
||||||
|
if (!v) return '(non répondu)';
|
||||||
|
if (v === 'absent') return 'Absent';
|
||||||
|
return 'Rubrique ' + v;
|
||||||
|
}
|
||||||
|
function fdsExpectedLabel(answer) {
|
||||||
|
const parts = answer.split(',');
|
||||||
|
if (parts.length > 1) return 'Rubrique ' + parts.join(' ou ');
|
||||||
|
return parts[0] === 'absent' ? 'Absent' : 'Rubrique ' + parts[0];
|
||||||
|
}
|
||||||
|
function textAnswerLabel(v) {
|
||||||
|
if (!v) return '(non répondu)';
|
||||||
|
return v === 'absent' ? 'Absent' : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFdsCorrect(q, v) { return v !== '' && q.answer.split(',').includes(v); }
|
||||||
|
function isTextCorrect(q, v) { return v !== '' && v === q.answer; }
|
||||||
|
|
||||||
|
function exporterPDF() {
|
||||||
|
const groupe = idVal('idGroupe');
|
||||||
|
const date = idVal('idDate') || new Date().toISOString().slice(0, 10);
|
||||||
|
const formateur = idVal('idFormateur');
|
||||||
|
|
||||||
|
let scoreTotal = 0, nbTotal = 0;
|
||||||
|
|
||||||
|
function partHtml(prefix, title, questions, selPrefix, answerLabelFn, expectedLabelFn, correctFn) {
|
||||||
|
const rows = questions.map((q, i) => {
|
||||||
|
const sel = document.getElementById(selPrefix + q.id);
|
||||||
|
const v = sel ? sel.value : '';
|
||||||
|
const ok = correctFn(q, v);
|
||||||
|
nbTotal++; if (ok) scoreTotal++;
|
||||||
|
const mark = v === '' ? '❓' : (ok ? '✓' : '✗');
|
||||||
|
return `<div style="margin-bottom:10px;">
|
||||||
|
<div><strong>[${prefix}-Q${i + 1}]</strong> ${escapeHtml(q.text)}</div>
|
||||||
|
<div style="margin-top:2px;">Réponse du groupe : <strong>${escapeHtml(answerLabelFn(v))}</strong> — Attendu : ${escapeHtml(expectedLabelFn(q.answer))} — ${mark}</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
return `<h2>${title}</h2>${rows}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const p1 = partHtml('P1', 'Partie 1 — FDS (Fiche de Données de Sécurité)', fdsQuestions, 'fds', fdsAnswerLabel, fdsExpectedLabel, isFdsCorrect);
|
||||||
|
const p2 = partHtml('P2', 'Partie 2 — FT (Fiche Technique)', ftQuestions, 'ft', textAnswerLabel, a => a, isTextCorrect);
|
||||||
|
const p3 = partHtml('P3', 'Partie 3 — Étiquette', etiquetteQuestions, 'eti', textAnswerLabel, a => a, isTextCorrect);
|
||||||
|
|
||||||
|
const w = window.open('', '_blank');
|
||||||
|
w.document.write(`
|
||||||
|
<html><head><meta charset="utf-8"><title>FDS, FT & Étiquettes — Réponses (${escapeHtml(groupe) || 'sans nom'})</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; padding: 30px; color: #1a1a18; }
|
||||||
|
h1 { font-size: 20px; border-bottom: 2px solid #BA7517; padding-bottom: 8px; }
|
||||||
|
h2 { font-size: 15px; margin-top: 26px; color: #633806; }
|
||||||
|
.meta { font-size: 13px; color: #555; margin: 2px 0; }
|
||||||
|
</style></head>
|
||||||
|
<body>
|
||||||
|
<h1>TD — FDS, FT & Étiquettes</h1>
|
||||||
|
<p class="meta"><strong>Groupe / Stagiaire :</strong> ${escapeHtml(groupe) || '—'}</p>
|
||||||
|
<p class="meta"><strong>Date de session :</strong> ${escapeHtml(date)}</p>
|
||||||
|
<p class="meta"><strong>Formateur :</strong> ${escapeHtml(formateur) || '—'}</p>
|
||||||
|
<p class="meta"><strong>Score global :</strong> ${scoreTotal} / ${nbTotal}</p>
|
||||||
|
${p1}${p2}${p3}
|
||||||
|
</body></html>`);
|
||||||
|
w.document.close();
|
||||||
|
setTimeout(() => w.print(), 300);
|
||||||
|
}
|
||||||
|
|
||||||
/* ======================== NAVIGATION ======================== */
|
/* ======================== NAVIGATION ======================== */
|
||||||
function goTo(n) {
|
function goTo(n) {
|
||||||
const cur = document.getElementById('slide-' + currentSlide);
|
const cur = document.getElementById('slide-' + currentSlide);
|
||||||
|
|||||||
507
exercices/correction-biocid.html
Normal file
507
exercices/correction-biocid.html
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Correction BioCID — 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: 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 DE THEMES */
|
||||||
|
.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.miss { background: var(--coral-light); color: var(--coral); }
|
||||||
|
.qcard .qtext { flex: 1; }
|
||||||
|
.qcard .qlabel { font-weight: 600; font-size: 13.5px; margin-bottom: 4px; }
|
||||||
|
.qcard .qanswer { font-size: 13.5px; white-space: pre-wrap; }
|
||||||
|
.qcard .qanswer.empty { color: var(--coral); font-style: italic; }
|
||||||
|
</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 d'exercice</div>
|
||||||
|
<h1>Plateforme BioCID — Lecture 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 l'exercice BioCID</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="progress-row">
|
||||||
|
<div class="progress-track"><div class="progress-fill" id="progressFill"></div></div>
|
||||||
|
<span><strong id="progressCount"></strong> questions renseignées</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="carousel-wrap">
|
||||||
|
<div class="carousel-nav">
|
||||||
|
<button class="nav-btn" id="prevBtn" onclick="goTo(currentTheme - 1)">←</button>
|
||||||
|
<div class="dots" id="dots"></div>
|
||||||
|
<button class="nav-btn" id="nextBtn" onclick="goTo(currentTheme + 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";
|
||||||
|
|
||||||
|
// Les 4 exercices, avec les intitulés exacts exportés (data-question) comme clés.
|
||||||
|
const THEMES = [
|
||||||
|
{
|
||||||
|
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)",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ALL_SECTIONS = THEMES.flatMap(t => t.items);
|
||||||
|
|
||||||
|
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)" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function escapeRx(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
|
||||||
|
function escapeHtml(s) {
|
||||||
|
if (!s) return "";
|
||||||
|
return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
||||||
|
}
|
||||||
|
function isEmpty(v) {
|
||||||
|
if (!v) return true;
|
||||||
|
const t = v.trim();
|
||||||
|
return t === "" || /^\(sans réponse\)/i.test(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();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePdfText(raw) {
|
||||||
|
let clean = raw.replace(/\r/g, "").replace(/[ \t]+\n/g, "\n").replace(/\s+/g, " ").trim();
|
||||||
|
// Retire les en-têtes/pieds d'impression du navigateur (date, titre, about:blank) sans toucher au corps
|
||||||
|
clean = clean.replace(/\d{1,2}\/\d{1,2}\/\d{4},?\s+\d{1,2}:\d{2}/g, " ");
|
||||||
|
clean = clean.replace(/Exercices BioCID\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+\d{1,3}\/\d{1,3}\s*$/, " ");
|
||||||
|
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", ["Réponses aux exercices"]),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sépare "Groupe N" du nom des participants
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
const startIdx = clean.search(/Réponses aux exercices/i);
|
||||||
|
const block = startIdx >= 0 ? clean.slice(startIdx) : clean;
|
||||||
|
|
||||||
|
const answers = {};
|
||||||
|
for (let i = 0; i < ALL_SECTIONS.length; i++) {
|
||||||
|
const label = ALL_SECTIONS[i];
|
||||||
|
const nextLabel = ALL_SECTIONS[i + 1];
|
||||||
|
answers[label] = captureBetween(block, label, nextLabel ? [nextLabel] : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { meta, answers };
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentThemeData = null;
|
||||||
|
let currentTheme = 0;
|
||||||
|
|
||||||
|
function renderThemePanel() {
|
||||||
|
const theme = THEMES[currentTheme];
|
||||||
|
const data = currentThemeData;
|
||||||
|
const colors = COLOR_MAP[theme.color];
|
||||||
|
const themeAnswered = theme.items.filter(s => !isEmpty(data.answers[s])).length;
|
||||||
|
|
||||||
|
document.getElementById("themePanel").innerHTML = `
|
||||||
|
<div class="theme-panel">
|
||||||
|
<div class="theme-head">
|
||||||
|
<div class="theme-icon" style="background:${colors.bg};color:${colors.fg};">${theme.icon}</div>
|
||||||
|
<div class="theme-title">${theme.title}</div>
|
||||||
|
<div class="theme-count">${themeAnswered}/${theme.items.length}</div>
|
||||||
|
</div>
|
||||||
|
<div class="cards">
|
||||||
|
${theme.items.map(label => {
|
||||||
|
const value = data.answers[label] || "";
|
||||||
|
const empty = isEmpty(value);
|
||||||
|
const displayLabel = label.replace(/^Ex\d+\s*[—-]\s*/, "");
|
||||||
|
return `
|
||||||
|
<div class="qcard">
|
||||||
|
<div class="badge ${empty ? 'miss' : 'ok'}">${empty ? '!' : '✓'}</div>
|
||||||
|
<div class="qtext">
|
||||||
|
<div class="qlabel">${escapeHtml(displayLabel)}</div>
|
||||||
|
<div class="qanswer ${empty ? 'empty' : ''}">${empty ? 'Sans réponse' : escapeHtml(value)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join("")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById("prevBtn").disabled = currentTheme === 0;
|
||||||
|
document.getElementById("nextBtn").disabled = currentTheme === THEMES.length - 1;
|
||||||
|
document.querySelectorAll(".dot").forEach((d, i) => d.classList.toggle("active", i === currentTheme));
|
||||||
|
}
|
||||||
|
|
||||||
|
function goTo(idx) {
|
||||||
|
if (idx < 0 || idx >= THEMES.length) return;
|
||||||
|
currentTheme = idx;
|
||||||
|
renderThemePanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResult(data) {
|
||||||
|
currentThemeData = data;
|
||||||
|
currentTheme = 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";
|
||||||
|
}
|
||||||
|
|
||||||
|
const answeredCount = ALL_SECTIONS.filter(s => !isEmpty(data.answers[s])).length;
|
||||||
|
document.getElementById("progressFill").style.width = (answeredCount / ALL_SECTIONS.length * 100) + "%";
|
||||||
|
document.getElementById("progressCount").textContent = `${answeredCount} / ${ALL_SECTIONS.length}`;
|
||||||
|
|
||||||
|
document.getElementById("dots").innerHTML = THEMES.map((_, i) =>
|
||||||
|
`<div class="dot${i === 0 ? ' active' : ''}" onclick="goTo(${i})"></div>`
|
||||||
|
).join("");
|
||||||
|
|
||||||
|
renderThemePanel();
|
||||||
|
|
||||||
|
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 de l'exercice BioCID.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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]));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
604
exercices/correction-fds.html
Normal file
604
exercices/correction-fds.html
Normal file
@@ -0,0 +1,604 @@
|
|||||||
|
<!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]));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user