// ==========================================
// 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
⚙️ Outil de Création
`;
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 += `

`;
if (practice.link) {
const linkText = (practice.link.includes("youtube.com") || practice.link.includes("youtu.be")) ? "📺 Voir le tutoriel Vidéo" : "🔗 Lien explicatif / Documentation";
mediaHTML += ``;
}
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', `
🧠 Validation des connaissances
Question 1 sur ?
`);
}
function ensureTemplateModal() {
if (document.getElementById('modal-template')) return;
document.body.insertAdjacentHTML('beforeend', `
📂 Templates disponibles
`);
}
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);
}