Refonte de la slide 7 FDS en étiquette cliquable (hotspots)
Remplace les menus déroulants de la partie Étiquette par une étiquette DÉS'INFECT reconstituée (Annexe I du guide FHER) avec 9 zones cliquables : info-bulle de justification réglementaire au clic, états visuels correct/incorrect, mise en situation professionnelle en introduction, et écran de synthèse sur le modèle de la check-list FHER p.15 (étiquette annotée + tableau). Export PDF recâblé sur les nouvelles zones.
This commit is contained in:
@@ -502,6 +502,139 @@ select:focus { outline: none; border-color: var(--amber); box-shadow: 0 0 0 3px
|
|||||||
.answer-row { flex-wrap: wrap; }
|
.answer-row { flex-wrap: wrap; }
|
||||||
select { width: 100%; min-width: 0; }
|
select { width: 100%; min-width: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============ SLIDE 7 — ÉTIQUETTE CLIQUABLE (HOTSPOTS) ============ */
|
||||||
|
#slide-7 { justify-content: flex-start; }
|
||||||
|
#slide-7 .scroll-area { gap: 0.75rem; }
|
||||||
|
|
||||||
|
/* Mise en situation */
|
||||||
|
.situation-box {
|
||||||
|
display: flex; gap: 12px; align-items: flex-start;
|
||||||
|
max-width: 680px; width: 100%;
|
||||||
|
background: var(--teal-light);
|
||||||
|
border: 1px solid var(--teal-mid);
|
||||||
|
border-left: 4px solid var(--teal-mid);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: var(--teal-dark);
|
||||||
|
font-size: 13.5px; line-height: 1.55;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.situation-box .sit-ico { font-size: 22px; line-height: 1.1; flex-shrink: 0; }
|
||||||
|
.situation-box strong { color: var(--teal-dark); }
|
||||||
|
|
||||||
|
/* Barre de consigne / question courante */
|
||||||
|
.eti-prompt {
|
||||||
|
display: flex; align-items: center; gap: 12px;
|
||||||
|
flex-wrap: wrap; justify-content: center;
|
||||||
|
max-width: 680px; width: 100%;
|
||||||
|
background: #fff; border: 1px solid var(--border); border-radius: 12px;
|
||||||
|
padding: 10px 16px; text-align: center;
|
||||||
|
}
|
||||||
|
.eti-progress {
|
||||||
|
font-family: 'Syne', sans-serif; font-weight: 800; font-size: 11px;
|
||||||
|
letter-spacing: 0.04em; text-transform: uppercase;
|
||||||
|
color: var(--teal-dark); background: var(--teal-light);
|
||||||
|
padding: 4px 10px; border-radius: 20px; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.eti-question { font-size: 14.5px; font-weight: 600; color: var(--text); }
|
||||||
|
|
||||||
|
/* Scène de l'étiquette + popover */
|
||||||
|
.eti-stage { position: relative; width: 100%; max-width: 680px; display: flex; justify-content: center; }
|
||||||
|
|
||||||
|
/* Étiquette reconstituée (DÉS'INFECT — Annexe I FHER) */
|
||||||
|
.label-card {
|
||||||
|
position: relative;
|
||||||
|
width: 100%; max-width: 640px;
|
||||||
|
background: #fff; color: #1a1a18;
|
||||||
|
border: 1px solid #d9d2c4; border-radius: 6px;
|
||||||
|
padding: 16px 18px 0; overflow: hidden;
|
||||||
|
font-family: 'DM Sans', sans-serif; font-size: 10.5px; line-height: 1.42;
|
||||||
|
box-shadow: 0 8px 30px rgba(0,0,0,0.10);
|
||||||
|
}
|
||||||
|
.lbl-top { display: flex; align-items: center; gap: 12px; border-bottom: 2px solid #1a1a18; padding-bottom: 6px; margin-bottom: 10px; }
|
||||||
|
.lbl-flag { font-size: 20px; }
|
||||||
|
.lbl-title { font-family: 'Syne', sans-serif; font-weight: 800; font-size: clamp(24px, 5vw, 40px); letter-spacing: -0.01em; margin: 0; }
|
||||||
|
.lbl-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||||
|
.lbl-h { font-family: 'Syne', sans-serif; font-weight: 700; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.02em; margin-bottom: 2px; }
|
||||||
|
.lbl-block { margin-bottom: 9px; }
|
||||||
|
.lbl-small { color: #555; font-size: 9.5px; line-height: 1.6; margin-top: 4px; }
|
||||||
|
.lbl-bottom { display: grid; grid-template-columns: auto 1fr auto; align-items: end; gap: 14px; margin-top: 4px; padding: 10px 0; }
|
||||||
|
.lbl-net { display: flex; flex-direction: column; gap: 2px; }
|
||||||
|
.lbl-net span { font-size: 10px; }
|
||||||
|
.lbl-net strong { font-family: 'Syne', sans-serif; font-size: 22px; line-height: 1; }
|
||||||
|
.lbl-barcode {
|
||||||
|
height: 42px; align-self: end; min-width: 90px;
|
||||||
|
background: repeating-linear-gradient(90deg, #1a1a18 0 2px, #fff 2px 4px, #1a1a18 4px 5px, #fff 5px 9px);
|
||||||
|
}
|
||||||
|
.lbl-picto-wrap { display: flex; align-items: center; gap: 8px; }
|
||||||
|
.pictos { display: flex; gap: 6px; }
|
||||||
|
.ghs {
|
||||||
|
width: 38px; height: 38px; position: relative;
|
||||||
|
border: 2.5px solid #c62828; background: #fff;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.ghs span { transform: rotate(-45deg); font-size: 17px; line-height: 1; }
|
||||||
|
.ghs.excl span { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 24px; color: #1a1a18; }
|
||||||
|
.lbl-danger { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 15px; color: #1a1a18; letter-spacing: 0.02em; }
|
||||||
|
.lbl-footer { margin: 6px -18px 0; padding: 8px 18px; background: #2b2b28; color: #e8e2d5; text-align: center; font-size: 9px; line-height: 1.5; }
|
||||||
|
|
||||||
|
/* Zones cliquables (hotspots) */
|
||||||
|
.hotspot {
|
||||||
|
position: relative; cursor: pointer; border-radius: 5px;
|
||||||
|
outline: 1.5px dashed transparent; outline-offset: 1px;
|
||||||
|
transition: background 0.15s ease, outline-color 0.15s ease;
|
||||||
|
}
|
||||||
|
span.hotspot { display: inline; padding: 0 2px; -webkit-box-decoration-break: clone; box-decoration-break: clone; }
|
||||||
|
.label-card:not(.recap) .hotspot:hover { background: rgba(43,122,120,0.10); outline-color: var(--teal-mid); }
|
||||||
|
.hotspot.hs-correct { background: rgba(46,125,50,0.14); outline: 1.5px solid #2e7d32; }
|
||||||
|
.hotspot.hs-wrong { background: rgba(198,40,40,0.16); outline: 1.5px solid #c62828; animation: hsShake 0.4s ease; }
|
||||||
|
@keyframes hsShake { 0%,100%{transform:translateX(0);} 25%{transform:translateX(-3px);} 75%{transform:translateX(3px);} }
|
||||||
|
.hs-num {
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
width: 16px; height: 16px; border-radius: 50%;
|
||||||
|
background: #2e7d32; color: #fff;
|
||||||
|
font-family: 'Syne', sans-serif; font-weight: 800; font-size: 9px;
|
||||||
|
margin-right: 3px; vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info-bulle réglementaire */
|
||||||
|
.eti-pop {
|
||||||
|
position: absolute; z-index: 20; max-width: 290px; width: max-content;
|
||||||
|
background: #fff; border: 1px solid var(--border); border-radius: 10px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.18);
|
||||||
|
padding: 12px 30px 12px 14px; font-size: 12.5px; line-height: 1.5;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.eti-pop[hidden] { display: none; }
|
||||||
|
.eti-pop .pop-status { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 4px; display: flex; align-items: center; gap: 6px; }
|
||||||
|
.eti-pop.ok .pop-status { color: #2e7d32; }
|
||||||
|
.eti-pop.ko .pop-status { color: #c62828; }
|
||||||
|
.eti-pop.info .pop-status { color: var(--teal-dark); }
|
||||||
|
.eti-pop .pop-title { font-weight: 700; color: var(--text); margin-bottom: 3px; }
|
||||||
|
.eti-pop .pop-reg { color: var(--text-muted); }
|
||||||
|
.eti-pop .pop-close { position: absolute; top: 5px; right: 8px; cursor: pointer; color: var(--text-muted); font-size: 16px; line-height: 1; border: none; background: none; padding: 2px; }
|
||||||
|
|
||||||
|
/* Check-list de synthèse (modèle FHER p.15) */
|
||||||
|
.checklist-table {
|
||||||
|
width: 100%; max-width: 780px; border-collapse: collapse;
|
||||||
|
background: #fff; border: 1px solid var(--border); border-radius: 12px; overflow: hidden;
|
||||||
|
font-size: 12.5px; margin: 0.25rem 0 1rem;
|
||||||
|
}
|
||||||
|
.checklist-table th { background: var(--teal-light); color: var(--teal-dark); font-family: 'Syne', sans-serif; font-weight: 700; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.03em; padding: 10px 12px; text-align: left; border-bottom: 2px solid var(--teal-mid); }
|
||||||
|
.checklist-table td { padding: 9px 12px; border-bottom: 1px solid var(--border); color: var(--text-muted); vertical-align: top; }
|
||||||
|
.checklist-table tr:last-child td { border-bottom: none; }
|
||||||
|
.checklist-table td.cl-num { text-align: center; }
|
||||||
|
.cl-badge { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 50%; background: #2e7d32; color: #fff; font-family: 'Syne', sans-serif; font-weight: 800; font-size: 10px; }
|
||||||
|
.checklist-table strong { color: var(--text); }
|
||||||
|
.cl-check { color: #2e7d32; font-weight: 700; }
|
||||||
|
.cl-cross { color: #c62828; font-weight: 700; }
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.lbl-cols { grid-template-columns: 1fr; }
|
||||||
|
.lbl-title { font-size: 26px; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -652,22 +785,117 @@ select:focus { outline: none; border-color: var(--amber); box-shadow: 0 0 0 3px
|
|||||||
<p class="slide-intro">L'étiquette est l'interface directe entre le produit et l'utilisateur. Elle communique rapidement les <strong>dangers essentiels</strong>, les <strong>EPI</strong> et les <strong>consignes d'utilisation</strong>.</p>
|
<p class="slide-intro">L'étiquette est l'interface directe entre le produit et l'utilisateur. Elle communique rapidement les <strong>dangers essentiels</strong>, les <strong>EPI</strong> et les <strong>consignes d'utilisation</strong>.</p>
|
||||||
<div class="info-card" style="background:var(--teal-light);color:var(--teal-dark);">
|
<div class="info-card" style="background:var(--teal-light);color:var(--teal-dark);">
|
||||||
<span>🎯</span>
|
<span>🎯</span>
|
||||||
<div>Indiquez la <strong>zone de l'étiquette</strong> où trouver l'information : Identité, Pictogrammes, Mots d'avertissement, Mentions de danger, Conseils de prudence, Contenu net, Numéro d'urgence — ou « Absent ».</div>
|
<div>Sur une <strong>étiquette réelle</strong> (produit professionnel DÉS'INFECT), <strong>cliquez la zone</strong> qui répond à chaque question : nom commercial, pictogrammes, mention d'avertissement, phrases H, phrases P (EPI, premiers secours, élimination), contenu net et centre antipoison. Chaque clic ouvre la <strong>justification réglementaire</strong> de la zone.</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-teal" onclick="goTo(7)" style="margin-top:1.5rem;">Répondre aux questions →</button>
|
<button class="btn btn-teal" onclick="goTo(7)" style="margin-top:1.5rem;">Répondre aux questions →</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ============ SLIDE 7 — ÉTIQUETTE QUIZ ============ -->
|
<!-- ============ SLIDE 7 — ÉTIQUETTE CLIQUABLE (HOTSPOTS) ============ -->
|
||||||
<div class="slide" id="slide-7">
|
<div class="slide" id="slide-7">
|
||||||
<div class="scroll-area">
|
<div class="scroll-area">
|
||||||
<div class="section-tag" style="background:var(--teal-light);color:var(--teal-dark);">Partie 3 / 3 — Étiquette</div>
|
|
||||||
<div class="slide-title">Zones de l'étiquette</div>
|
<!-- ---- PANNEAU A : exercice ---- -->
|
||||||
<div class="quiz-list" id="etiquette-questions"></div>
|
<div id="eti-exercise" style="width:100%;display:flex;flex-direction:column;align-items:center;gap:0.75rem;">
|
||||||
<button class="verify-btn btn-teal" id="eti-verify" onclick="verifyEtiquette()" disabled>✓ Vérifier mes réponses</button>
|
<div class="section-tag" style="background:var(--teal-light);color:var(--teal-dark);">Partie 3 / 3 — Étiquette</div>
|
||||||
<div id="etiquette-result" class="result-message"></div>
|
<div class="slide-title">Zones de l'étiquette</div>
|
||||||
<div class="actions-row">
|
|
||||||
<button class="btn btn-amber" onclick="goTo(8)">Voir la synthèse →</button>
|
<div class="situation-box">
|
||||||
|
<span class="sit-ico">💬</span>
|
||||||
|
<div><strong>Mise en situation —</strong> Vous intervenez chez un client avec un désinfectant de surfaces <strong>à usage exclusivement professionnel</strong>. Avant l'application, il vous interroge sur les dangers et les précautions du produit. Bidon en main, retrouvez rapidement sur l'étiquette chaque information demandée.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="eti-prompt">
|
||||||
|
<span class="eti-progress" id="eti-progress">Question 1 / 9</span>
|
||||||
|
<span class="eti-question" id="eti-question">—</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="eti-stage" id="eti-stage">
|
||||||
|
<div class="label-card" id="labelCard">
|
||||||
|
<!-- Titre / nom commercial -->
|
||||||
|
<div class="lbl-top">
|
||||||
|
<span class="lbl-flag">🇫🇷</span>
|
||||||
|
<span class="hotspot" data-zone="z-nom"><span class="lbl-title">DÉS'INFECT</span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lbl-cols">
|
||||||
|
<!-- Colonne gauche (contexte, non cliquable) -->
|
||||||
|
<div class="lbl-col">
|
||||||
|
<div class="lbl-block"><div class="lbl-h">Propriétés :</div>Désinfecte et désodorise les sanitaires. Produit strictement destiné à un usage professionnel.</div>
|
||||||
|
<div class="lbl-block"><div class="lbl-h">Mode d'emploi :</div>Concentré à diluer : 5 % pour une activité fongicide, 1 % pour une activité bactéricide. Pulvériser sur les parois, laisser agir 5 à 15 min, rincer à l'eau claire. Type de formulation : concentré soluble (SL). Utilisable en TP2.</div>
|
||||||
|
<div class="lbl-small">UFI : 1234-1234-1234-1234<br>Date de péremption : MM/AAAA<br>N° LOT : 123456</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonne droite (précautions, cliquable) -->
|
||||||
|
<div class="lbl-col">
|
||||||
|
<div class="lbl-block"><div class="lbl-h">Substances actives :</div>Chlorure de didécyldiméthylammonium (N°CAS 7173-51-5) à 75 g/kg ; acide glycolique (CAS 79-14-1) à 35 g/kg.</div>
|
||||||
|
<div class="lbl-block">
|
||||||
|
<div class="lbl-h">Précautions d'emploi :</div>
|
||||||
|
<span>Produit strictement professionnel (FR) — Lire le mode d'emploi ci-joint avant l'emploi. </span>
|
||||||
|
<span class="hotspot" data-zone="z-antipoison">Formule déposée au centre anti-poison de Nancy : 03 83 22 50 50.</span>
|
||||||
|
<span> </span>
|
||||||
|
<span class="hotspot" data-zone="z-h">H302 Nocif en cas d'ingestion. H314 Provoque de graves brûlures de la peau et des lésions oculaires. H410 Très toxique pour les organismes aquatiques, entraîne des effets néfastes à long terme.</span>
|
||||||
|
<span> </span>
|
||||||
|
<span class="hotspot" data-zone="z-p-epi">P280 Porter des gants de protection, des vêtements de protection et un équipement de protection du visage.</span>
|
||||||
|
<span> </span>
|
||||||
|
<span class="hotspot" data-zone="z-p-secours">P301+P330+P331 EN CAS D'INGESTION : rincer la bouche, ne pas faire vomir. P305+P351+P338 EN CAS DE CONTACT AVEC LES YEUX : rincer avec précaution à l'eau pendant plusieurs minutes. P310 Appeler immédiatement un CENTRE ANTIPOISON ou un médecin.</span>
|
||||||
|
<span> </span>
|
||||||
|
<span class="hotspot" data-zone="z-p-elim">P501 Éliminer le contenu / le récipient dans un point de collecte des déchets spéciaux ou dangereux.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bas : contenu net, code-barres, pictogrammes, mention -->
|
||||||
|
<div class="lbl-bottom">
|
||||||
|
<span class="hotspot" data-zone="z-net">
|
||||||
|
<span class="lbl-net"><span>Volume net :</span><strong>5 L</strong></span>
|
||||||
|
</span>
|
||||||
|
<div class="lbl-barcode" aria-hidden="true"></div>
|
||||||
|
<div class="lbl-picto-wrap">
|
||||||
|
<span class="hotspot" data-zone="z-picto">
|
||||||
|
<span class="pictos">
|
||||||
|
<span class="ghs"><span>🐟</span></span>
|
||||||
|
<span class="ghs"><span>🧪</span></span>
|
||||||
|
<span class="ghs excl"><span>!</span></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="hotspot" data-zone="z-mention"><span class="lbl-danger">DANGER</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lbl-footer">FHER — 2, rue de Sèze, 75009 Paris — Tél. 01 23 45 67 89</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="eti-pop" id="eti-pop" hidden>
|
||||||
|
<button class="pop-close" onclick="closeEtiPop()" aria-label="Fermer">×</button>
|
||||||
|
<div class="pop-status" id="pop-status"></div>
|
||||||
|
<div class="pop-title" id="pop-title"></div>
|
||||||
|
<div class="pop-reg" id="pop-reg"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="etiquette-result" class="result-message"></div>
|
||||||
|
<div class="actions-row">
|
||||||
|
<button class="btn btn-outline" onclick="resetEtiquette()">↺ Recommencer</button>
|
||||||
|
<button class="btn btn-teal" id="eti-recap-btn" onclick="showEtiRecap()" disabled>Voir le récapitulatif →</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ---- PANNEAU B : synthèse / check-list ---- -->
|
||||||
|
<div id="eti-recap" hidden style="width:100%;display:flex;flex-direction:column;align-items:center;gap:0.75rem;">
|
||||||
|
<div class="section-tag" style="background:var(--teal-light);color:var(--teal-dark);">Check-list d'étiquetage</div>
|
||||||
|
<div class="slide-title">Récapitulatif — l'étiquette annotée</div>
|
||||||
|
<p class="slide-intro">Les 9 zones réglementaires et leurs bonnes réponses, présentées comme la <strong>check-list officielle d'étiquetage des produits biocides</strong> (Guide FHER, p. 15) — un outil réutilisable en conditions réelles.</p>
|
||||||
|
|
||||||
|
<div class="eti-stage" id="recap-stage"></div>
|
||||||
|
|
||||||
|
<table class="checklist-table" id="checklist-table"></table>
|
||||||
|
|
||||||
|
<div class="actions-row">
|
||||||
|
<button class="btn btn-outline" onclick="hideEtiRecap()">← Retour à l'exercice</button>
|
||||||
|
<button class="btn btn-amber" onclick="goTo(8)">Voir la synthèse générale →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -741,19 +969,41 @@ const ftQuestions = [
|
|||||||
];
|
];
|
||||||
const ftOptions = ["Description", "Propriétés biocides", "Conditionnements", "Utilisations"];
|
const ftOptions = ["Description", "Propriétés biocides", "Conditionnements", "Utilisations"];
|
||||||
|
|
||||||
const etiquetteQuestions = [
|
/* 9 zones réglementaires de l'étiquette (ordre canonique = check-list FHER p.15).
|
||||||
{ id: 1, text: "Quel est le nom du produit et le nom de l'entreprise ?", answer: "Identité" },
|
zone : identifiant du hotspot correspondant dans l'étiquette
|
||||||
{ id: 2, text: "Quels pictogrammes de danger sont affichés ?", answer: "Pictogrammes" },
|
text : consigne affichée pendant l'exercice
|
||||||
{ id: 3, text: "Quels sont les mots d'avertissement (« Attention » ou « Danger ») ?", answer: "Mots d'avertissement" },
|
label : intitulé check-list (réutilisé dans la synthèse et le PDF)
|
||||||
{ id: 4, text: "Quelles sont les mentions de danger (H-statements) identifiées ?", answer: "Mentions de danger" },
|
reg : justification réglementaire (info-bulle) — Guide FHER, Partie 3, p.10-12 */
|
||||||
{ id: 5, text: "Quels équipements de protection individuelle (EPI) sont recommandés ?", answer: "Conseils de prudence" },
|
const etiZones = [
|
||||||
{ id: 6, text: "Quels premiers secours sont indiqués ?", answer: "Conseils de prudence" },
|
{ zone: "z-nom", text: "Retrouvez le nom commercial du produit.",
|
||||||
{ id: 7, text: "Comment éliminer l'emballage après utilisation ?", answer: "Conseils de prudence" },
|
label: "Nom commercial",
|
||||||
{ id: 8, text: "Quel est le contenu net du produit ?", answer: "Contenu net" },
|
reg: "Obligation CLP : le nom commercial doit être identique à celui figurant sur la FDS (Guide FHER, Partie 3 — I.2)." },
|
||||||
{ id: 9, text: "Y a-t-il un numéro d'appel d'urgence mentionné ?", answer: "Numéro d'urgence" },
|
{ zone: "z-picto", text: "Cliquez sur les pictogrammes de danger.",
|
||||||
{ id: 10, text: "Quelles consignes de stockage sont affichées ?", answer: "Conseils de prudence" }
|
label: "Pictogrammes de danger",
|
||||||
|
reg: "CLP : pictogrammes aux dimensions requises, regroupés avec la mention d'avertissement et les phrases H/P, se détachant nettement du fond (Partie 3 — I.5)." },
|
||||||
|
{ zone: "z-mention", text: "Où figure la mention d'avertissement ?",
|
||||||
|
label: "Mention d'avertissement",
|
||||||
|
reg: "CLP : « Danger » ou « Attention », disposée dans l'encart d'étiquetage avec pictogrammes et phrases H/P (Partie 3 — I.5)." },
|
||||||
|
{ zone: "z-h", text: "Repérez les mentions de danger (phrases H).",
|
||||||
|
label: "Mentions de danger (phrases H)",
|
||||||
|
reg: "CLP : les phrases H décrivent la nature du danger ; regroupées dans l'encart d'étiquetage (Partie 3 — I.5)." },
|
||||||
|
{ zone: "z-p-epi", text: "Où sont indiqués les EPI à porter (phrases P) ?",
|
||||||
|
label: "Conseils de prudence — EPI",
|
||||||
|
reg: "CLP : conseils de prudence P (ici P280). Six conseils P au maximum, sauf si la nature et la gravité des dangers l'exigent (CLP Art. 28-3) — Partie 3, I.5." },
|
||||||
|
{ zone: "z-p-secours", text: "Où trouver les instructions de premiers secours ?",
|
||||||
|
label: "Premiers secours (phrases P)",
|
||||||
|
reg: "BPR : indications relatives aux premiers soins (Partie 3 — II.8), traduites par les conseils P 3xx (contact, ingestion, inhalation)." },
|
||||||
|
{ zone: "z-p-elim", text: "Où sont les consignes d'élimination de l'emballage ?",
|
||||||
|
label: "Élimination du produit et de l'emballage",
|
||||||
|
reg: "BPR : instructions pour l'élimination en toute sécurité du produit et de son emballage, le cas échéant avec interdiction de le réutiliser (Partie 3 — II.10). Ici P501." },
|
||||||
|
{ zone: "z-net", text: "Retrouvez le contenu net du produit.",
|
||||||
|
label: "Contenu net",
|
||||||
|
reg: "Quantité nette de produit contenue dans l'emballage — mention générale du pavé d'information produit." },
|
||||||
|
{ zone: "z-antipoison", text: "Où figurent les coordonnées du centre antipoison ?",
|
||||||
|
label: "Centre antipoison / n° d'urgence",
|
||||||
|
reg: "Coordonnées d'urgence complétant les instructions de premiers soins (Partie 3 — II.8) ; les mélanges dangereux sont déclarés aux centres antipoison (annexe VIII du CLP)." }
|
||||||
];
|
];
|
||||||
const etiquetteOptions = ["Identité", "Pictogrammes", "Mots d'avertissement", "Mentions de danger", "Conseils de prudence", "Contenu net", "Numéro d'urgence"];
|
function etiZoneById(id) { return etiZones.find(z => z.zone === id); }
|
||||||
|
|
||||||
function shuffleArray(array) {
|
function shuffleArray(array) {
|
||||||
const a = [...array];
|
const a = [...array];
|
||||||
@@ -764,17 +1014,6 @@ function shuffleArray(array) {
|
|||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRow(qNum, selectId, answerAttr, text, options, badgeStyle) {
|
|
||||||
let opts = '<option value="">-- Sélectionner --</option>';
|
|
||||||
options.forEach(o => { opts += `<option value="${o.value}">${o.label}</option>`; });
|
|
||||||
return `
|
|
||||||
<div class="answer-row">
|
|
||||||
<div class="question-num" style="${badgeStyle}">Q${qNum}</div>
|
|
||||||
<div class="question-text">${text}</div>
|
|
||||||
<select id="${selectId}" data-answer="${answerAttr}">${opts}</select>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateQuizzes() {
|
function generateQuizzes() {
|
||||||
// FDS — glisser-déposer : banques d'étiquettes + cases de dépôt
|
// FDS — glisser-déposer : banques d'étiquettes + cases de dépôt
|
||||||
buildFdsDnd();
|
buildFdsDnd();
|
||||||
@@ -782,17 +1021,183 @@ function generateQuizzes() {
|
|||||||
// FT — exercice « relier » (questions à gauche, sections à droite)
|
// FT — exercice « relier » (questions à gauche, sections à droite)
|
||||||
buildFtMatch();
|
buildFtMatch();
|
||||||
|
|
||||||
// Étiquette
|
// Étiquette — exercice à zones cliquables (hotspots)
|
||||||
const etiOpts = etiquetteOptions.map(o => ({ value: o, label: o }));
|
buildEtiquette();
|
||||||
etiOpts.push({ value: 'absent', label: 'Absent' });
|
}
|
||||||
const etiC = document.getElementById('etiquette-questions');
|
|
||||||
etiC.innerHTML = '';
|
|
||||||
etiquetteQuestions.forEach((q, i) => {
|
|
||||||
etiC.innerHTML += buildRow(i + 1, 'eti' + q.id, q.answer, q.text, etiOpts, 'background:var(--teal-light);color:var(--teal-dark);');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bloque la vérification tant que toutes les questions ne sont pas répondues
|
/* ---- Étiquette : zones cliquables ---- */
|
||||||
wireVerifyGate('etiquette-questions', 'eti-verify');
|
let etiOrder = []; // ordre aléatoire des questions
|
||||||
|
let etiPos = 0; // question courante
|
||||||
|
let etiAnswers = {}; // { zoneCible : zoneCliquée en 1re tentative } — pour le PDF
|
||||||
|
let etiFirstTry = {}; // { zoneCible : true si trouvée sans erreur }
|
||||||
|
let etiCurError = false; // une erreur a-t-elle eu lieu sur la question courante
|
||||||
|
let etiInit = false; // écouteur de clic posé une seule fois
|
||||||
|
|
||||||
|
function buildEtiquette() {
|
||||||
|
etiOrder = shuffleArray(etiZones.map((_, i) => i));
|
||||||
|
etiPos = 0;
|
||||||
|
etiAnswers = {};
|
||||||
|
etiFirstTry = {};
|
||||||
|
etiCurError = false;
|
||||||
|
|
||||||
|
const card = document.getElementById('labelCard');
|
||||||
|
card.querySelectorAll('.hotspot').forEach(h => h.classList.remove('hs-correct', 'hs-wrong'));
|
||||||
|
|
||||||
|
closeEtiPop();
|
||||||
|
const r = document.getElementById('etiquette-result');
|
||||||
|
r.className = 'result-message'; r.innerHTML = '';
|
||||||
|
document.getElementById('eti-recap-btn').disabled = true;
|
||||||
|
document.getElementById('eti-exercise').style.display = 'flex';
|
||||||
|
document.getElementById('eti-recap').style.display = 'none';
|
||||||
|
|
||||||
|
if (!etiInit) {
|
||||||
|
card.addEventListener('click', onEtiClick);
|
||||||
|
etiInit = true;
|
||||||
|
}
|
||||||
|
renderEtiQuestion();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEtiQuestion() {
|
||||||
|
const prog = document.getElementById('eti-progress');
|
||||||
|
const ques = document.getElementById('eti-question');
|
||||||
|
if (etiPos >= etiOrder.length) {
|
||||||
|
prog.textContent = 'Terminé';
|
||||||
|
ques.textContent = '✓ Les 9 zones ont été identifiées.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const z = etiZones[etiOrder[etiPos]];
|
||||||
|
prog.textContent = 'Question ' + (etiPos + 1) + ' / ' + etiZones.length;
|
||||||
|
ques.textContent = z.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEtiClick(e) {
|
||||||
|
const hs = e.target.closest('.hotspot');
|
||||||
|
if (!hs) return;
|
||||||
|
const clicked = hs.dataset.zone;
|
||||||
|
|
||||||
|
// Exploration libre une fois l'exercice terminé
|
||||||
|
if (etiPos >= etiOrder.length) {
|
||||||
|
openEtiPop(hs, 'info', clicked);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = etiZones[etiOrder[etiPos]];
|
||||||
|
if (clicked === target.zone) {
|
||||||
|
hs.classList.remove('hs-wrong');
|
||||||
|
hs.classList.add('hs-correct');
|
||||||
|
if (etiAnswers[target.zone] === undefined) etiAnswers[target.zone] = clicked;
|
||||||
|
etiFirstTry[target.zone] = !etiCurError;
|
||||||
|
openEtiPop(hs, 'ok', clicked);
|
||||||
|
etiPos++;
|
||||||
|
etiCurError = false;
|
||||||
|
renderEtiQuestion();
|
||||||
|
if (etiPos >= etiOrder.length) finishEtiquette();
|
||||||
|
} else {
|
||||||
|
if (etiAnswers[target.zone] === undefined) etiAnswers[target.zone] = clicked; // 1re réponse (erronée) mémorisée
|
||||||
|
etiCurError = true;
|
||||||
|
etiFirstTry[target.zone] = false;
|
||||||
|
hs.classList.remove('hs-wrong'); void hs.offsetWidth; // relance l'animation
|
||||||
|
hs.classList.add('hs-wrong');
|
||||||
|
setTimeout(() => hs.classList.remove('hs-wrong'), 700);
|
||||||
|
openEtiPop(hs, 'ko', clicked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishEtiquette() {
|
||||||
|
const correct = etiZones.filter(z => etiFirstTry[z.zone]).length;
|
||||||
|
showScore(document.getElementById('etiquette-result'), correct, etiZones.length);
|
||||||
|
document.getElementById('eti-recap-btn').disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info-bulle réglementaire positionnée près de la zone cliquée */
|
||||||
|
function openEtiPop(hs, kind, clickedZone) {
|
||||||
|
const pop = document.getElementById('eti-pop');
|
||||||
|
const stage = document.getElementById('eti-stage');
|
||||||
|
const z = etiZoneById(clickedZone);
|
||||||
|
|
||||||
|
const statusEl = document.getElementById('pop-status');
|
||||||
|
pop.className = 'eti-pop ' + kind;
|
||||||
|
if (kind === 'ok') statusEl.innerHTML = '✅ Bonne zone';
|
||||||
|
else if (kind === 'ko') statusEl.innerHTML = '❌ Ce n\'est pas la bonne zone';
|
||||||
|
else statusEl.innerHTML = '🔎 ' + z.label;
|
||||||
|
|
||||||
|
document.getElementById('pop-title').textContent = z.label;
|
||||||
|
document.getElementById('pop-reg').textContent = z.reg;
|
||||||
|
|
||||||
|
pop.hidden = false;
|
||||||
|
// Positionnement relatif à la scène
|
||||||
|
const sr = stage.getBoundingClientRect();
|
||||||
|
const hr = hs.getBoundingClientRect();
|
||||||
|
const popW = pop.offsetWidth, popH = pop.offsetHeight;
|
||||||
|
let left = hr.left - sr.left + hr.width / 2 - popW / 2;
|
||||||
|
left = Math.max(4, Math.min(left, sr.width - popW - 4));
|
||||||
|
let top = hr.bottom - sr.top + 8;
|
||||||
|
if (top + popH > sr.height) top = hr.top - sr.top - popH - 8; // bascule au-dessus si déborde
|
||||||
|
if (top < 0) top = 4;
|
||||||
|
pop.style.left = left + 'px';
|
||||||
|
pop.style.top = top + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEtiPop() {
|
||||||
|
const pop = document.getElementById('eti-pop');
|
||||||
|
if (pop) pop.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetEtiquette() { buildEtiquette(); }
|
||||||
|
|
||||||
|
/* ---- Synthèse : étiquette annotée + check-list ---- */
|
||||||
|
function showEtiRecap() {
|
||||||
|
closeEtiPop();
|
||||||
|
const stage = document.getElementById('recap-stage');
|
||||||
|
stage.innerHTML = '';
|
||||||
|
|
||||||
|
// Clone de l'étiquette, toutes zones en « correct » + numéros
|
||||||
|
const clone = document.getElementById('labelCard').cloneNode(true);
|
||||||
|
clone.removeAttribute('id');
|
||||||
|
clone.classList.add('recap');
|
||||||
|
clone.querySelectorAll('.hotspot').forEach(h => {
|
||||||
|
h.classList.remove('hs-wrong');
|
||||||
|
h.classList.add('hs-correct');
|
||||||
|
const idx = etiZones.findIndex(z => z.zone === h.dataset.zone);
|
||||||
|
if (idx >= 0) {
|
||||||
|
const num = document.createElement('b');
|
||||||
|
num.className = 'hs-num';
|
||||||
|
num.textContent = idx + 1;
|
||||||
|
h.insertBefore(num, h.firstChild);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stage.appendChild(clone);
|
||||||
|
|
||||||
|
// Table check-list
|
||||||
|
const tbl = document.getElementById('checklist-table');
|
||||||
|
let rows = '<thead><tr><th style="width:34px;">N°</th><th>Élément d\'étiquetage</th><th>Base réglementaire</th><th style="width:70px;">Statut</th></tr></thead><tbody>';
|
||||||
|
etiZones.forEach((z, i) => {
|
||||||
|
const ok = etiFirstTry[z.zone];
|
||||||
|
const answered = etiAnswers[z.zone] !== undefined;
|
||||||
|
const statut = ok
|
||||||
|
? '<span class="cl-check">✓ trouvé</span>'
|
||||||
|
: (answered ? '<span class="cl-cross">✗ à revoir</span>' : '<span class="cl-cross">—</span>');
|
||||||
|
rows += `<tr>
|
||||||
|
<td class="cl-num"><span class="cl-badge">${i + 1}</span></td>
|
||||||
|
<td><strong>${z.label}</strong></td>
|
||||||
|
<td>${z.reg}</td>
|
||||||
|
<td>${statut}</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
rows += '</tbody>';
|
||||||
|
tbl.innerHTML = rows;
|
||||||
|
|
||||||
|
document.getElementById('eti-exercise').style.display = 'none';
|
||||||
|
document.getElementById('eti-recap').style.display = 'flex';
|
||||||
|
const sa = document.querySelector('#slide-7 .scroll-area');
|
||||||
|
if (sa) sa.scrollTop = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideEtiRecap() {
|
||||||
|
document.getElementById('eti-recap').style.display = 'none';
|
||||||
|
document.getElementById('eti-exercise').style.display = 'flex';
|
||||||
|
const sa = document.querySelector('#slide-7 .scroll-area');
|
||||||
|
if (sa) sa.scrollTop = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- FT : exercice « relier » ---- */
|
/* ---- FT : exercice « relier » ---- */
|
||||||
@@ -1024,22 +1429,6 @@ function wireVerifyGate(containerId, btnId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ======================== VÉRIFICATION ======================== */
|
/* ======================== VÉRIFICATION ======================== */
|
||||||
function showInlineResult(selectEl, isCorrect, isUnanswered, correctAnswerText) {
|
|
||||||
const row = selectEl.closest('.answer-row');
|
|
||||||
row.querySelector('.answer-label')?.remove();
|
|
||||||
row.querySelector('.answer-icon')?.remove();
|
|
||||||
|
|
||||||
const label = document.createElement('div');
|
|
||||||
label.className = 'answer-label';
|
|
||||||
label.textContent = correctAnswerText;
|
|
||||||
selectEl.before(label);
|
|
||||||
|
|
||||||
const icon = document.createElement('div');
|
|
||||||
icon.className = 'answer-icon';
|
|
||||||
icon.textContent = isUnanswered ? '❓' : (isCorrect ? '✅' : '❌');
|
|
||||||
selectEl.before(icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showScore(resultDiv, correct, total) {
|
function showScore(resultDiv, correct, total) {
|
||||||
const p = Math.round((correct / total) * 100);
|
const p = Math.round((correct / total) * 100);
|
||||||
if (p === 100) {
|
if (p === 100) {
|
||||||
@@ -1102,18 +1491,6 @@ function verifyFT() {
|
|||||||
showScore(document.getElementById('ft-result'), correct, ftQuestions.length);
|
showScore(document.getElementById('ft-result'), correct, ftQuestions.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyEtiquette() {
|
|
||||||
let correct = 0;
|
|
||||||
etiquetteQuestions.forEach(q => {
|
|
||||||
const select = document.getElementById('eti' + q.id);
|
|
||||||
const userAnswer = select.value;
|
|
||||||
const isCorrect = userAnswer === q.answer;
|
|
||||||
if (isCorrect) correct++;
|
|
||||||
showInlineResult(select, isCorrect, userAnswer === '', q.answer);
|
|
||||||
});
|
|
||||||
showScore(document.getElementById('etiquette-result'), correct, etiquetteQuestions.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ======================== EXPORT PDF ======================== */
|
/* ======================== EXPORT PDF ======================== */
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
if (!str) return '';
|
if (!str) return '';
|
||||||
@@ -1168,7 +1545,20 @@ function exporterPDF() {
|
|||||||
|
|
||||||
const p1 = partHtml('P1', 'Partie 1 — FDS (Fiche de Données de Sécurité)', fdsQuestions, 'fds', fdsAnswerLabel, fdsExpectedLabel, isFdsCorrect);
|
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, q => ftConnections[q.id] || '');
|
const p2 = partHtml('P2', 'Partie 2 — FT (Fiche Technique)', ftQuestions, 'ft', textAnswerLabel, a => a, isTextCorrect, q => ftConnections[q.id] || '');
|
||||||
const p3 = partHtml('P3', 'Partie 3 — Étiquette', etiquetteQuestions, 'eti', textAnswerLabel, a => a, isTextCorrect);
|
|
||||||
|
// Partie 3 — Étiquette (zones cliquables) : ordre check-list
|
||||||
|
const p3rows = etiZones.map((z, i) => {
|
||||||
|
const given = etiAnswers[z.zone];
|
||||||
|
const ok = given === z.zone;
|
||||||
|
nbTotal++; if (ok) scoreTotal++;
|
||||||
|
const givenLabel = given === undefined ? '(non répondu)' : (etiZoneById(given) ? etiZoneById(given).label : given);
|
||||||
|
const mark = given === undefined ? '❓' : (ok ? '✓' : '✗');
|
||||||
|
return `<div style="margin-bottom:10px;">
|
||||||
|
<div><strong>[P3-Q${i + 1}]</strong> ${escapeHtml(z.text)}</div>
|
||||||
|
<div style="margin-top:2px;">Zone désignée : <strong>${escapeHtml(givenLabel)}</strong> — Attendu : ${escapeHtml(z.label)} — ${mark}</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
const p3 = `<h2>Partie 3 — Étiquette (zones réglementaires)</h2>${p3rows}`;
|
||||||
|
|
||||||
const w = window.open('', '_blank');
|
const w = window.open('', '_blank');
|
||||||
w.document.write(`
|
w.document.write(`
|
||||||
|
|||||||
Reference in New Issue
Block a user