From b5f2eac84235e3dff2e7249f6d1beb9b3979d447 Mon Sep 17 00:00:00 2001 From: "gauthier.chombart" Date: Mon, 20 Jul 2026 20:44:47 +0200 Subject: [PATCH] =?UTF-8?q?Sauvegarde=20les=20r=C3=A9ponses=20de=20BioCID?= =?UTF-8?q?=20et=20du=20TP=20Nuisibles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les réponses saisies sont désormais enregistrées dans le navigateur au fil de la frappe, sous une clé propre à chaque exercice. Un rechargement ne les perd plus. Au chargement, rien n'est restauré d'office : un bandeau signale les réponses retrouvées avec leur date et propose « Restaurer » ou « Repartir de zéro ». Sur un poste de formation partagé, un nouveau groupe ne démarre donc jamais sur le travail du précédent. Sont restaurés les champs d'identification, les réponses, la position dans le diaporama et, pour le TP, le tableau de risques (lignes ajoutées recréées avant remplissage), le cas pratique choisi et le formateur. L'avertissement avant rechargement est conservé comme second filet. Les accès à localStorage sont protégés : en navigation privée ou en file:// sous Safari, la page fonctionne normalement, sans sauvegarde. FDS reste à traiter : ses réponses ne vivent pas dans des champs de formulaire mais dans l'état des zones de dépôt, des liaisons et de l'étiquette, ce qui demande une logique de réaffichage dédiée. --- exercices/Exo_BioCID.html | 103 +++++++++++++++++++++++++- exercices/TP_Nuisibles.html | 143 +++++++++++++++++++++++++++++++++++- 2 files changed, 238 insertions(+), 8 deletions(-) diff --git a/exercices/Exo_BioCID.html b/exercices/Exo_BioCID.html index b6da2a2..0c930f5 100644 --- a/exercices/Exo_BioCID.html +++ b/exercices/Exo_BioCID.html @@ -859,21 +859,116 @@ function exporterPDF() { setTimeout(() => w.print(), 300); } -/* ======================== PROTECTION CONTRE LA PERTE DES RÉPONSES ======================== */ -/* Les réponses ne sont pas enregistrées : on avertit avant tout rechargement ou départ. - Le texte de la boîte est imposé par le navigateur, il n'est pas personnalisable. */ +/* ======================== SAUVEGARDE AUTOMATIQUE ======================== */ +/* Les réponses sont enregistrées dans le navigateur au fil de la saisie, puis + proposées à la restauration au rechargement. En file://, localStorage peut être + indisponible (Safari) : dans ce cas la page fonctionne normalement, sans filet. */ +const CLE_SAUVEGARDE = 'certibiocide:biocid:v1'; + let reponsesModifiees = false; -function marquerModifie() { reponsesModifiees = true; } +function marquerModifie() { reponsesModifiees = true; sauvegarder(); } document.addEventListener('input', marquerModifie, true); document.addEventListener('change', marquerModifie, true); +// Avertit avant tout rechargement ou départ. Le texte de la boîte est imposé +// par le navigateur, il n'est pas personnalisable. window.addEventListener('beforeunload', e => { if (!reponsesModifiees) return; e.preventDefault(); e.returnValue = ''; }); +function etatCourant() { + const champs = {}; + document.querySelectorAll('input[id], textarea[id]').forEach(el => { + if (el.value) champs[el.id] = el.value; + }); + return { savedAt: new Date().toISOString(), slide: currentSlide, champs }; +} + +let minuteurSauvegarde = null; +function sauvegarder() { + clearTimeout(minuteurSauvegarde); + minuteurSauvegarde = setTimeout(() => { + try { + localStorage.setItem(CLE_SAUVEGARDE, JSON.stringify(etatCourant())); + } catch (e) { /* navigation privée ou quota : on continue sans sauvegarde */ } + }, 400); +} + +function lireSauvegarde() { + try { return JSON.parse(localStorage.getItem(CLE_SAUVEGARDE)); } + catch (e) { return null; } +} +function effacerSauvegarde() { + try { localStorage.removeItem(CLE_SAUVEGARDE); } catch (e) {} +} + +function appliquerSauvegarde(data) { + Object.entries(data.champs || {}).forEach(([id, v]) => { + const el = document.getElementById(id); + if (el) el.value = v; + }); + reponsesModifiees = true; + if (typeof data.slide === 'number' && data.slide > 0) goTo(data.slide); + updateUI(); +} + +/* Bandeau de restauration : on ne restaure jamais sans demander, pour qu'un + nouveau groupe n'hérite pas des réponses du précédent sur un poste partagé. */ +function injecterStylesBandeau() { + const style = document.createElement('style'); + style.textContent = ` + .restore-bar { + position: fixed; top: 84px; left: 50%; transform: translateX(-50%); + z-index: 900; max-width: min(680px, calc(100vw - 32px)); + display: flex; align-items: center; gap: 16px; flex-wrap: wrap; + background: var(--white); border: 1.5px solid var(--amber); + border-radius: 14px; padding: 14px 18px; + box-shadow: 0 8px 28px rgba(0,0,0,0.12); font-size: 13.5px; + } + .restore-bar .restore-text { flex: 1; min-width: 220px; line-height: 1.5; } + .restore-bar .restore-actions { display: flex; gap: 8px; flex-shrink: 0; } + .restore-bar button { + font-family: 'Syne', sans-serif; font-weight: 700; font-size: 12.5px; + padding: 8px 16px; border-radius: 20px; cursor: pointer; + border: 1px solid var(--border); background: transparent; color: var(--text-muted); + } + .restore-bar button:hover { border-color: var(--amber); color: var(--amber); } + .restore-bar button.primary { + background: var(--amber); border-color: var(--amber); color: #fff; + } + .restore-bar button.primary:hover { background: var(--amber-dark); border-color: var(--amber-dark); color: #fff; } + `; + document.head.appendChild(style); +} + +function proposerRestauration() { + const data = lireSauvegarde(); + if (!data || !data.champs || Object.keys(data.champs).length === 0) return; + + const quand = new Date(data.savedAt); + const libelle = isNaN(quand.getTime()) ? '' : + ` du ${quand.toLocaleDateString('fr-FR')} à ${quand.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`; + + injecterStylesBandeau(); + const bar = document.createElement('div'); + bar.className = 'restore-bar'; + bar.innerHTML = ` + 💾 Des réponses enregistrées${libelle} ont été retrouvées sur ce poste. + + + + `; + document.body.appendChild(bar); + + document.getElementById('btnRestaurer').onclick = () => { appliquerSauvegarde(data); bar.remove(); }; + document.getElementById('btnRepartir').onclick = () => { effacerSauvegarde(); bar.remove(); }; +} + +proposerRestauration(); + updateUI(); diff --git a/exercices/TP_Nuisibles.html b/exercices/TP_Nuisibles.html index dbf1964..9d82c77 100644 --- a/exercices/TP_Nuisibles.html +++ b/exercices/TP_Nuisibles.html @@ -1621,21 +1621,156 @@ function escapeHtml(str) { return str.replace(/&/g, '&').replace(//g, '>'); } -/* ======================== PROTECTION CONTRE LA PERTE DES RÉPONSES ======================== */ -/* Les réponses ne sont pas enregistrées : on avertit avant tout rechargement ou départ. - Le texte de la boîte est imposé par le navigateur, il n'est pas personnalisable. */ +/* ======================== SAUVEGARDE AUTOMATIQUE ======================== */ +/* Les réponses sont enregistrées dans le navigateur au fil de la saisie, puis + proposées à la restauration au rechargement. En file://, localStorage peut être + indisponible (Safari) : dans ce cas la page fonctionne normalement, sans filet. */ +const CLE_SAUVEGARDE = 'certibiocide:tp-nuisibles:v1'; + let reponsesModifiees = false; -function marquerModifie() { reponsesModifiees = true; } +function marquerModifie() { reponsesModifiees = true; sauvegarder(); } document.addEventListener('input', marquerModifie, true); document.addEventListener('change', marquerModifie, true); +// Avertit avant tout rechargement ou départ. Le texte de la boîte est imposé +// par le navigateur, il n'est pas personnalisable. window.addEventListener('beforeunload', e => { if (!reponsesModifiees) return; e.preventDefault(); e.returnValue = ''; }); +function etatCourant() { + const champs = {}; + document.querySelectorAll('input[id], textarea[id]').forEach(el => { + if (el.value) champs[el.id] = el.value; + }); + // Les cellules du tableau TP3 n'ont pas d'id : on les repère par leur position. + const risques = Array.from(document.querySelectorAll('#riskTableBody .risk-row')) + .map(row => Array.from(row.querySelectorAll('.risk-cell')).map(c => c.value)); + + return { + savedAt: new Date().toISOString(), + slide: currentSlide, + champs, + risques, + cas: selectedCase.key || '', + formateur: stagiaire.formateur || '', + nom: stagiaire.nom || '', + date: stagiaire.date || '', + }; +} + +let minuteurSauvegarde = null; +function sauvegarder() { + clearTimeout(minuteurSauvegarde); + minuteurSauvegarde = setTimeout(() => { + try { + localStorage.setItem(CLE_SAUVEGARDE, JSON.stringify(etatCourant())); + } catch (e) { /* navigation privée ou quota : on continue sans sauvegarde */ } + }, 400); +} + +function lireSauvegarde() { + try { return JSON.parse(localStorage.getItem(CLE_SAUVEGARDE)); } + catch (e) { return null; } +} +function effacerSauvegarde() { + try { localStorage.removeItem(CLE_SAUVEGARDE); } catch (e) {} +} + +function appliquerSauvegarde(data) { + Object.entries(data.champs || {}).forEach(([id, v]) => { + const el = document.getElementById(id); + if (el) el.value = v; + }); + + // Recrée autant de lignes de risques que nécessaire avant de les remplir + const risques = data.risques || []; + const tbody = document.getElementById('riskTableBody'); + while (tbody.querySelectorAll('.risk-row').length < risques.length) addRiskRow(); + tbody.querySelectorAll('.risk-row').forEach((row, i) => { + const valeurs = risques[i] || []; + row.querySelectorAll('.risk-cell').forEach((cell, j) => { cell.value = valeurs[j] || ''; }); + }); + + // Le cas pratique et le formateur passent par leurs boutons, qui remettent + // aussi l'interface (carte sélectionnée, bouton de confirmation) dans le bon état. + if (data.cas) { + const carte = Array.from(document.querySelectorAll('.case-card')) + .find(c => (c.getAttribute('onclick') || '').includes("'" + data.cas + "'")); + if (carte) carte.click(); + } + if (data.formateur) { + const btn = document.getElementById('btn' + data.formateur); + if (btn) btn.click(); + } + stagiaire.nom = data.nom || stagiaire.nom; + stagiaire.date = data.date || stagiaire.date; + + reponsesModifiees = true; + if (typeof data.slide === 'number' && data.slide > 0) goTo(data.slide); + updateUI(); +} + +/* Bandeau de restauration : on ne restaure jamais sans demander, pour qu'un + nouveau groupe n'hérite pas des réponses du précédent sur un poste partagé. */ +function injecterStylesBandeau() { + const style = document.createElement('style'); + style.textContent = ` + .restore-bar { + position: fixed; top: 84px; left: 50%; transform: translateX(-50%); + z-index: 900; max-width: min(680px, calc(100vw - 32px)); + display: flex; align-items: center; gap: 16px; flex-wrap: wrap; + background: var(--white); border: 1.5px solid var(--amber); + border-radius: 14px; padding: 14px 18px; + box-shadow: 0 8px 28px rgba(0,0,0,0.12); font-size: 13.5px; + } + .restore-bar .restore-text { flex: 1; min-width: 220px; line-height: 1.5; } + .restore-bar .restore-actions { display: flex; gap: 8px; flex-shrink: 0; } + .restore-bar button { + font-family: 'Syne', sans-serif; font-weight: 700; font-size: 12.5px; + padding: 8px 16px; border-radius: 20px; cursor: pointer; + border: 1px solid var(--border); background: transparent; color: var(--text-muted); + } + .restore-bar button:hover { border-color: var(--amber); color: var(--amber); } + .restore-bar button.primary { + background: var(--amber); border-color: var(--amber); color: #fff; + } + .restore-bar button.primary:hover { background: var(--amber-dark); border-color: var(--amber-dark); color: #fff; } + `; + document.head.appendChild(style); +} + +function proposerRestauration() { + const data = lireSauvegarde(); + if (!data) return; + const aDesReponses = (data.champs && Object.keys(data.champs).length > 0) + || (data.risques || []).some(r => r.some(v => v)); + if (!aDesReponses) return; + + const quand = new Date(data.savedAt); + const libelle = isNaN(quand.getTime()) ? '' : + ` du ${quand.toLocaleDateString('fr-FR')} à ${quand.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`; + + injecterStylesBandeau(); + const bar = document.createElement('div'); + bar.className = 'restore-bar'; + bar.innerHTML = ` + 💾 Des réponses enregistrées${libelle} ont été retrouvées sur ce poste. + + + + `; + document.body.appendChild(bar); + + document.getElementById('btnRestaurer').onclick = () => { appliquerSauvegarde(data); bar.remove(); }; + document.getElementById('btnRepartir').onclick = () => { effacerSauvegarde(); bar.remove(); }; +} + +proposerRestauration(); + updateUI();