// ========================================== // API — Recueil de Bonnes Pratiques (backend FastAPI) // ========================================== const API_BASE = "/api/bonnes-pratiques"; function authHeaders() { const token = localStorage.getItem("fablab_token"); return token ? { Authorization: "Bearer " + token } : {}; } async function apiFetchJson(url, options) { const r = await fetch(url, options); if (!r.ok) { let detail = "Erreur serveur."; try { detail = (await r.json()).detail || detail; } catch (e) { /* réponse non-JSON */ } throw new Error(detail); } if (r.status === 204) return null; return await r.json(); } // ========================================== // ONGLETS DU MENU // ========================================== // Repli statique utilisé uniquement si le backend est injoignable (résilience) — // la source de vérité est désormais l'API (table bp_tabs), pas localStorage. const DEFAULT_TABS = [ { id: "elec", label: "Électronique", icon: "⚡", href: "elec.html", category: "elec", pageKey: "elec" }, { id: "code", label: "Code Embarqué", icon: "💻", href: "code.html", category: "code", pageKey: "code" }, { id: "CAO-methodo", label: "Organi & CAO", icon: "🏗️", href: "CAO-methodo.html", category: "cao-methodo", pageKey: "CAO-methodo" }, { id: "CAO-dfm", label: "Design For Mfg", icon: "⚙️", href: "CAO-dfm.html", category: "dfm", pageKey: "CAO-dfm" }, { id: "auto", label: "Automatique", icon: "🤖", href: "auto.html", category: "auto", pageKey: "auto" }, { id: "concep-meca", label: "CAO / Conception", icon: "📐", href: "concep-meca.html", category: "meca", pageKey: "concep-meca" }, { id: "calcul-meca", label: "Calculs Méca", icon: "🧮", href: "calcul-meca.html", category: "rdm", pageKey: "calcul-meca" }, { id: "fab", label: "Fabrication", icon: "🏭", href: "fab.html", category: "fab", pageKey: "fab" }, { id: "doc-projet", label: "Documentation", icon: "📂", href: "doc-projet.html", category: "doc", pageKey: "doc-projet" }, { id: "gest-projet", label: "Gestion Projet", icon: "📊", href: "gest-projet.html", category: "projet", pageKey: "gest-projet" }, { id: "achats", label: "Achats & BOM", icon: "🛒", href: "achats.html", category: "bom", pageKey: "achats" }, { id: "tests", label: "Qualité & Tests", icon: "✅", href: "tests.html", category: "tests", pageKey: "tests" }, ]; async function fetchTabs() { try { const r = await fetch(`${API_BASE}/tabs`); if (r.ok) { const data = await r.json(); if (Array.isArray(data) && data.length) return data; } } catch (e) { /* backend indisponible */ } return DEFAULT_TABS.map(t => ({ ...t, position: 0 })); } async function createTab(label, icon) { return apiFetchJson(`${API_BASE}/tabs`, { method: "POST", headers: { "Content-Type": "application/json", ...authHeaders() }, body: JSON.stringify({ label, icon }), }); } async function updateTabApi(id, data) { return apiFetchJson(`${API_BASE}/tabs/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json", ...authHeaders() }, body: JSON.stringify(data), }); } async function deleteTabApi(id) { return apiFetchJson(`${API_BASE}/tabs/${id}`, { method: "DELETE", headers: authHeaders() }); } async function reorderTabsApi(order) { return apiFetchJson(`${API_BASE}/tabs/reorder`, { method: "POST", headers: { "Content-Type": "application/json", ...authHeaders() }, body: JSON.stringify({ order }), }); } async function resetTabsApi() { return apiFetchJson(`${API_BASE}/tabs/reset`, { method: "POST", headers: authHeaders() }); } function slugify(text) { return (text || "").toString().normalize("NFD").replace(/[̀-ͯ]/g, "") .toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || "onglet"; } // Clé de la page courante : le paramètre ?cat= prime (pages génériques custom.html), // sinon le nom de fichier (pages historiques figées). function getPageKey() { const cat = new URLSearchParams(window.location.search).get("cat"); if (cat) return cat.toLowerCase(); return window.location.pathname.split("/").pop().replace(".html", "").toLowerCase() || "index"; } // ========================================== // INJECTION DU HEADER ET DU MENU // ========================================== document.addEventListener("DOMContentLoaded", async () => { const tabs = await fetchTabs(); // index.html porte son propre header/hero statique (page d'accueil) : ne pas dupliquer le bandeau ici. if (!document.querySelector("header")) { const navLinksHTML = tabs.map(t => `${t.icon} ${t.label}` ).join("\n "); const headerHTML = `

🛠️ Recueil de Bonnes Pratiques

Le guide de survie interactif pour vos projets mécatroniques

`; document.body.insertAdjacentHTML("afterbegin", headerHTML); // Page active (comparaison sur nom de fichier + query string, pour distinguer les pages génériques custom.html?cat=...) const currentFull = (window.location.pathname.split("/").pop() || "index.html") + window.location.search; document.querySelectorAll(".nav-link").forEach(link => { if (link.getAttribute("href").toLowerCase() === currentFull.toLowerCase()) { link.classList.add("bg-blue-100", "text-blue-700"); } }); } isAdmin = await checkAdminStatus(); if (isAdmin) { const btn = document.getElementById('btn-admin-tool'); if (btn) btn.classList.remove('hidden'); } await displayCustomPractices(tabs); }); // ========================================== // OUTIL DE CRÉATION — réservé aux administrateurs // ========================================== let isAdmin = false; async function checkAdminStatus() { const token = localStorage.getItem('fablab_token'); if (!token) return false; try { const r = await fetch('/api/auth/me', { headers: { Authorization: 'Bearer ' + token } }); if (!r.ok) return false; const account = await r.json(); return account.role === 'admin'; } catch (e) { return false; /* portail hors ligne : accès refusé par défaut */ } } // ========================================== // PRATIQUES PERSONNALISÉES // ========================================== // Cache de la dernière liste affichée sur cette page — permet à l'édition // (openEditPracticeModal) de retrouver une règle par id sans refaire un appel réseau. let currentPagePractices = []; async function displayCustomPractices(tabs) { // Pages génériques (custom.html?cat=...) : la catégorie est directement le paramètre ?cat=. // Pages historiques figées (elec.html, ...) : la catégorie est résolue via la liste des onglets. const genericTarget = document.getElementById("custom-tab-content"); const pageKey = getPageKey(); const category = genericTarget ? pageKey : (tabs.find(t => t.pageKey.toLowerCase() === pageKey) || {}).category; if (!category) return; let practices = []; try { const r = await fetch(`${API_BASE}/practices?category=${encodeURIComponent(category)}`); if (r.ok) practices = await r.json(); } catch (e) { /* backend indisponible */ } currentPagePractices = practices; practices.forEach(practice => { const target = genericTarget || document.getElementById(`custom-${practice.category}`); if (!target) return; let mediaHTML = ""; if (practice.image_url) mediaHTML += `
Illustration
`; if (practice.link) { const linkText = (practice.link.includes("youtube.com") || practice.link.includes("youtu.be")) ? "📺 Voir le tutoriel Vidéo" : "🔗 Lien explicatif / Documentation"; mediaHTML += `
${linkText} →
`; } const adminControlsHTML = isAdmin ? `
` : ""; target.insertAdjacentHTML("beforeend", `

${practice.title}

${practice.description}

${mediaHTML} ${adminControlsHTML}
`); }); } // ========================================== // ÉDITION / SUPPRESSION D'UNE RÈGLE — directement depuis un onglet (admin) // ========================================== async function deleteCustomPractice(id) { if (!confirm('Supprimer cette règle ?')) return; try { await apiFetchJson(`${API_BASE}/practices/${id}`, { method: 'DELETE', headers: authHeaders() }); location.reload(); } catch (e) { alert(e.message); } } let epmImageBase64 = ''; let epmImageCleared = false; function ensureEditPracticeModal() { if (document.getElementById('edit-practice-modal')) return; document.body.insertAdjacentHTML('beforeend', ` `); document.getElementById('epm-image-file').addEventListener('change', function () { const file = this.files[0]; if (!file) return; epmImageCleared = false; const reader = new FileReader(); reader.onloadend = () => { epmImageBase64 = reader.result; document.getElementById('epm-preview-img').src = reader.result; document.getElementById('epm-image-preview').classList.remove('hidden'); }; reader.readAsDataURL(file); }); } function clearEpmImage() { epmImageBase64 = ''; epmImageCleared = true; document.getElementById('epm-image-file').value = ''; document.getElementById('epm-image-preview').classList.add('hidden'); } function openEditPracticeModal(id) { const p = currentPagePractices.find(x => x.id === id); if (!p) return; ensureEditPracticeModal(); document.getElementById('epm-id').value = id; document.getElementById('epm-title').value = p.title; document.getElementById('epm-description').value = p.description; document.getElementById('epm-checklist').value = p.checklist; document.getElementById('epm-link').value = p.link || ''; epmImageBase64 = ''; epmImageCleared = false; document.getElementById('epm-image-file').value = ''; const preview = document.getElementById('epm-image-preview'); if (p.image_url) { document.getElementById('epm-preview-img').src = '/api' + p.image_url; preview.classList.remove('hidden'); } else { preview.classList.add('hidden'); } document.getElementById('edit-practice-modal').classList.remove('hidden'); } function closeEditPracticeModal() { const m = document.getElementById('edit-practice-modal'); if (m) m.classList.add('hidden'); } async function saveEditedPractice() { const id = document.getElementById('epm-id').value; const title = document.getElementById('epm-title').value.trim(); const description = document.getElementById('epm-description').value.trim(); const checklist = document.getElementById('epm-checklist').value.trim(); if (!title || !description || !checklist) { alert('Remplissez tous les champs obligatoires.'); return; } const body = { title, description, checklist, link: document.getElementById('epm-link').value.trim() }; if (epmImageCleared) body.clear_image = true; else if (epmImageBase64) body.image = epmImageBase64; try { await apiFetchJson(`${API_BASE}/practices/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', ...authHeaders() }, body: JSON.stringify(body), }); closeEditPracticeModal(); location.reload(); } catch (e) { alert(e.message); } } // ========================================== // TEMPLATES // ========================================== // Templates statiques fournis avec le site (fichiers réels dans templates/, pas en base). const DEFAULT_TEMPLATES = [ { id: "default-fiche-test", name: "Fiche de Test", description: "Gabarit Word pour documenter un protocole de test.", fileName: "Fiche_de_Test_Template.docx", fileUrl: "templates/Fiche_de_Test_Template.docx", pages: ["CAO-methodo", "CAO-dfm"] }, { id: "default-suivi-nc", name: "Suivi Non-Conformités", description: "Gabarit Excel pour le suivi des non-conformités.", fileName: "Suivi_Non_Conformites.xlsx", fileUrl: "templates/Suivi_Non_Conformites.xlsx", pages: ["CAO-methodo", "CAO-dfm"] }, ]; // Icône selon extension function templateIcon(fileName) { const ext = (fileName || '').split('.').pop().toLowerCase(); const icons = { pdf: '📄', docx: '📝', doc: '📝', xlsx: '📊', xls: '📊', csv: '📋', pptx: '📑', ppt: '📑' }; return icons[ext] || '📎'; } // Templates visibles pour une page donnée : gabarits statiques du site + templates ajoutés via l'Outil de Création (API). async function getVisibleTemplates(pageKey) { const defaults = DEFAULT_TEMPLATES.filter(t => t.pages.includes(pageKey)); let custom = []; try { const r = await fetch(`${API_BASE}/templates?page=${encodeURIComponent(pageKey)}`); if (r.ok) { const data = await r.json(); custom = data.map(t => ({ id: t.id, name: t.name, description: t.description, fileName: t.file_name, fileUrl: '/api' + t.download_url, })); } } catch (e) { /* backend indisponible */ } return [...defaults, ...custom]; } function renderTemplateListInto(container, visible) { if (visible.length === 0) { container.innerHTML = '

Aucun template disponible pour cette section.
Ajoutez-en via l\'Outil de Création.

'; return; } container.innerHTML = visible.map(t => `
${templateIcon(t.fileName)}

${t.name}

${t.description ? `

${t.description}

` : ''}
⬇️ DL
`).join(''); } // ========================================== // RECHERCHE IN-PAGE // ========================================== function filterPagePractices() { const input = document.getElementById('search-bar'); if (!input) return; const query = input.value.toLowerCase(); let visible = 0; document.querySelectorAll('.practice-card').forEach(card => { const match = card.innerText.toLowerCase().includes(query); card.style.display = match ? '' : 'none'; if (match) visible++; }); const noResults = document.getElementById('no-results'); if (noResults) noResults.classList.toggle('hidden', visible > 0 || query === ''); } // ========================================== // QUIZ // ========================================== let currentQuizQuestions = []; let currentQuestionIndex = 0; let score = 0; let currentQuizPageKey = null; async function openQuiz(pageKey) { ensureQuizModal(); const key = pageKey || getPageKey(); let questions = []; try { const r = await fetch(`${API_BASE}/quiz?page=${encodeURIComponent(key)}`); if (r.ok) questions = await r.json(); } catch (e) { /* backend indisponible */ } if (!questions.length) { alert(`Aucun quiz configuré pour cette page (clé : "${key}").`); return; } currentQuizQuestions = questions.map(q => ({ question: q.question, answers: q.answers.map(a => ({ text: a.text, isCorrect: a.is_correct, rationale: a.rationale })), })).sort(() => Math.random() - 0.5); currentQuestionIndex = 0; score = 0; currentQuizPageKey = key; document.getElementById("modal-quizz").classList.remove("hidden"); const tabs = await fetchTabs(); const tab = tabs.find(t => t.pageKey === key); document.getElementById("quiz-title").textContent = `Quiz : ${tab ? tab.label : key.toUpperCase()}`; displayQuestion(); } function closeQuiz() { document.getElementById("modal-quizz").classList.add("hidden"); } function displayQuestion() { const q = currentQuizQuestions[currentQuestionIndex]; document.getElementById("quiz-progress").textContent = `Question ${currentQuestionIndex+1}/${currentQuizQuestions.length} — Score : ${score}/${currentQuestionIndex}`; document.getElementById("btn-next-quiz").classList.add("hidden"); const shuffled = [...q.answers].sort(() => Math.random() - 0.5); document.getElementById("quiz-body").innerHTML = `

${q.question}

${shuffled.map(a => ` `).join('')}
`; } function submitAnswer(btn, isCorrect, rationale) { document.querySelectorAll(".answer-btn").forEach(b => { b.disabled = true; b.classList.remove("hover:bg-blue-50","hover:border-blue-400"); }); const fb = document.getElementById("quiz-feedback"); fb.classList.remove("hidden"); if (isCorrect) { btn.classList.add("bg-green-100","border-green-500","text-green-800"); fb.className = "mt-3 p-3 rounded text-sm font-medium bg-green-50 text-green-800 border border-green-200"; fb.innerHTML = `✅ Correct ! ${rationale}`; score++; } else { btn.classList.add("bg-red-100","border-red-500","text-red-800"); fb.className = "mt-3 p-3 rounded text-sm font-medium bg-red-50 text-red-800 border border-red-200"; fb.innerHTML = `❌ Incorrect. ${rationale}`; } document.getElementById("quiz-progress").textContent = `Question ${currentQuestionIndex+1}/${currentQuizQuestions.length} — Score : ${score}/${currentQuestionIndex+1}`; const nb = document.getElementById("btn-next-quiz"); nb.textContent = currentQuestionIndex < currentQuizQuestions.length-1 ? "Question suivante ➡️" : "Voir le résultat 🏁"; nb.classList.remove("hidden"); } function nextQuestion() { currentQuestionIndex++; if (currentQuestionIndex < currentQuizQuestions.length) { displayQuestion(); } else { const pct = Math.round(score / currentQuizQuestions.length * 100); const emoji = pct >= 80 ? '🏆' : pct >= 50 ? '👍' : '📖'; const msg = pct >= 80 ? 'Excellent !' : pct >= 50 ? 'Pas mal !' : 'À retravailler.'; document.getElementById("quiz-body").innerHTML = `

${emoji}

${msg}

Score final : ${score}/${currentQuizQuestions.length} (${pct}%)

`; document.getElementById("btn-next-quiz").classList.add("hidden"); document.getElementById("quiz-progress").textContent = "Quiz terminé"; } } // ========================================== // UTILITAIRES // ========================================== function toggleModal(id) { document.getElementById(id).classList.toggle('hidden'); } // ========================================== // MODALES QUIZ / TEMPLATES — injectées à la demande (accueil uniquement désormais) // ========================================== function ensureQuizModal() { if (document.getElementById('modal-quizz')) return; document.body.insertAdjacentHTML('beforeend', ` `); } function ensureTemplateModal() { if (document.getElementById('modal-template')) return; document.body.insertAdjacentHTML('beforeend', ` `); } async function showTemplatesModalForDomain(pageKey) { ensureTemplateModal(); const tabs = await fetchTabs(); const tab = tabs.find(t => t.pageKey === pageKey); const heading = document.getElementById('template-modal-heading'); if (heading) heading.textContent = `📂 Templates — ${tab ? tab.label : pageKey}`; renderTemplateListInto(document.getElementById('template-modal-list'), await getVisibleTemplates(pageKey)); document.getElementById('modal-template').classList.remove('hidden'); } // ========================================== // SÉLECTEUR DE DOMAINE — page d'accueil // ========================================== function ensureDomainPickerModal() { if (document.getElementById('domain-picker-modal')) return; document.body.insertAdjacentHTML('beforeend', ` `); } async function openDomainPicker(mode) { ensureDomainPickerModal(); const tabs = await fetchTabs(); document.getElementById('domain-picker-title').textContent = mode === 'quiz' ? '🧠 Choisir un domaine pour le quiz' : '📂 Choisir un domaine pour les templates'; document.getElementById('domain-picker-list').innerHTML = tabs.map(t => ` `).join(''); document.getElementById('domain-picker-modal').classList.remove('hidden'); } function closeDomainPicker() { const m = document.getElementById('domain-picker-modal'); if (m) m.classList.add('hidden'); } function selectDomain(pageKey, mode) { closeDomainPicker(); if (mode === 'quiz') openQuiz(pageKey); else showTemplatesModalForDomain(pageKey); }