// GarageManager - SPA Frontend const API = "/modules/garage/api.php"; const UPLOADS = "/modules/garage/api.php?action=photo&file="; function $(s, ctx = document) { return ctx.querySelector(s); } function $$(s, ctx = document) { return [...ctx.querySelectorAll(s)]; } function fmt(n) { return n != null ? Number(n).toLocaleString("fr-FR") : "--"; } function fmtPrice(n) { return n != null ? Number(n).toFixed(2) + " €" : "--"; } function fmtDate(d) { if (!d) return "--"; return new Date(d).toLocaleDateString("fr-FR"); } function formatBytes(n) { n = Number(n) || 0; if (n < 1024) return n + " o"; if (n < 1048576) return (n / 1024).toFixed(1) + " Ko"; return (n / 1048576).toFixed(1) + " Mo"; } function ago(d) { if (!d) return ""; const diff = Math.floor((Date.now() - new Date(d)) / 86400000); if (diff === 0) return "aujourd'hui"; if (diff === 1) return "hier"; return "il y a " + diff + "j"; } function escHtml(s) { const d = document.createElement("div"); d.textContent = String(s ?? ""); return d.innerHTML; } function toast(msg, type = "success") { const c = document.getElementById("toasts"); const t = document.createElement("div"); t.className = "toast " + type; t.textContent = msg; c.appendChild(t); setTimeout(() => t.remove(), 3000); } // ─── STATE ──────────────────────────────────────────────────────────────────── let currentPage = "dashboard"; let currentVehicleId = null; let currentMaintenanceId = null; // ─── NAV ────────────────────────────────────────────────────────────────────── function navigate(page, params = {}) { currentPage = page; $$(".page").forEach((p) => p.classList.remove("active")); $$(".nav-link").forEach((n) => n.classList.remove("active")); document.getElementById("page-" + page)?.classList.add("active"); const navTarget = page === "maintenance" ? "maintenances-all" : page === "vehicle" ? "vehicles" : page; document .querySelector('.nav-link[data-page="' + navTarget + '"]') ?.classList.add("active"); if (page === "dashboard") loadDashboard(); else if (page === "vehicles") loadVehicles(); else if (page === "vehicle") loadVehicleDetail(params.id); else if (page === "maintenance") loadMaintenanceDetail(params.id); else if (page === "parts") loadParts(); else if (page === "maintenances-all") loadAllMaintenances(); } // ─── API ────────────────────────────────────────────────────────────────────── async function api(action, method = "GET", data = null, extra = "") { const opts = { method, headers: {} }; if (data && !(data instanceof FormData)) { opts.headers["Content-Type"] = "application/json"; opts.body = JSON.stringify(data); } else if (data instanceof FormData) { opts.body = data; } const r = await fetch(API + "?action=" + action + extra, opts); const j = await r.json(); if (!j.ok) throw new Error(j.error || "Erreur API"); return j.data; } // ─── DASHBOARD ──────────────────────────────────────────────────────────────── async function loadDashboard() { try { const [stats, vehicles, reminders] = await Promise.all([ api("stats"), api("vehicles"), api("maintenances"), ]); $("#stat-vehicles").textContent = stats.vehicles; $("#stat-maintenances").textContent = stats.maintenances; $("#stat-parts").textContent = stats.parts; $("#stat-cost").textContent = fmtPrice( parseFloat(stats.total_cost) + parseFloat(stats.total_parts_cost), ); renderDashboardVehicles(vehicles); renderReminders(reminders); } catch (e) { toast(e.message, "error"); } } function renderDashboardVehicles(list) { const el = $("#dashboard-vehicles"); if (!list.length) { el.innerHTML = '
Aucun véhicule
Aucun rappel configuré
| Véhicule | Type | Prochaine date | Prochain km | Écart km |
|---|---|---|---|---|
| " + escHtml(r.vehicle_name) + "" + (r.license_plate ? ' ' + escHtml(r.license_plate) + "" : "") + " | " + "" + escHtml(r.type) + " | " + "" + (r.next_date ? '' + fmtDate(r.next_date) + "" : "--") + " | " + "" + (r.next_km ? fmt(r.next_km) + " km" : "--") + " | " + "" + (diff != null ? '' + (diff >= 0 ? "+" : "") + fmt(diff) + " km" : "--") + " |
Aucun véhicule. Ajoutez-en un !
Aucun entretien enregistré
| Date | Type | Description | Kilométrage | Coût MO | Pièces | Prochain | |
|---|---|---|---|---|---|---|---|
| " +
fmtDate(m.date) +
' ' + ago(m.date) + "" + (m.documents_count > 0 ? ' 📎 ' + m.documents_count + "" : "") + " | " +
'' + escHtml(m.type) + " | " + '' + (m.description ? escHtml(m.description) : "--") + " | " + "" + (m.km ? fmt(m.km) + " km" : "--") + " | " + "" + fmtPrice(m.cost) + " | " + "" + (m.parts_count > 0 ? '🔩 ' + m.parts_count + " (" + fmtPrice(m.parts_cost) + ")" : "--") + " | " + '' +
(m.next_date ? "📅 " + fmtDate(m.next_date) : "") +
" " +
(m.next_km ? " 🛣️ " + fmt(m.next_km) + " km" : "") + " | " +
'' + ' ' + '' + " | " + "
Aucune pièce enregistrée
| Photo | Nom | Marque | Référence | Catégorie | Prix unit. | Qté | Total | Entretien | |
|---|---|---|---|---|---|---|---|---|---|
| " +
(p.photo
? ' | " +
"" + escHtml(p.name) + " | " + "" + (p.brand ? escHtml(p.brand) : "--") + " | " + '' +
(p.reference ? escHtml(p.reference) : "--") +
" | " +
'' + escHtml(p.category) + " | " + "" + fmtPrice(p.price) + " | " + "" + p.quantity + " " + escHtml(p.unit || "pièce") + " | " + "" + fmtPrice(parseFloat(p.price || 0) * parseInt(p.quantity || 1)) + " | " + '' + (p.maintenance_type ? escHtml(p.maintenance_type) + " (" + fmtDate(p.maintenance_date) + ")" : "--") + " | " + "" + ' ' + '' + " | " + "
Aucune pièce utilisée
| Photo | Nom | Marque | Réf. | Catégorie | Prix unit. | Qté | Total | |
|---|---|---|---|---|---|---|---|---|
| " +
(p.photo
? ' | " +
"" + escHtml(p.name) + " | " + "" + (p.brand ? escHtml(p.brand) : "--") + " | " + '' +
(p.reference ? escHtml(p.reference) : "--") +
" | " +
'' + escHtml(p.category) + " | " + "" + fmtPrice(p.price) + " | " + "" + p.quantity + " " + escHtml(p.unit || "pièce") + " | " + "" + fmtPrice(parseFloat(p.price || 0) * parseInt(p.quantity || 1)) + " | " + "" + ' ' + '' + " | " + "
| Total pièces | ' + fmtPrice(total) + " | |||||||
Aucune pièce connue pour ce véhicule
'; return; } const seen = new Set(); const unique = parts.filter((p) => { if (seen.has(p.name)) return false; seen.add(p.name); return true; }); const wrap = document.createElement("div"); wrap.className = "scroll-list"; unique.forEach((p, i) => { const lbl = document.createElement("label"); lbl.className = "scroll-list-item"; lbl.innerHTML = '' + '' + escHtml(p.name) + "" + (p.reference ? '' + escHtml(p.reference) + "" : "") + (p.brand ? '' + escHtml(p.brand) + "" : "") + '' + fmtPrice(p.price) + ""; lbl.querySelector("input").addEventListener("change", () => { const n = document.getElementById("ipart-name"); const b = document.getElementById("ipart-brand"); const r = document.getElementById("ipart-reference"); const pr = document.getElementById("ipart-price"); if (n) n.value = p.name; if (b) b.value = p.brand || ""; if (r) r.value = p.reference || ""; if (pr) { pr.value = p.price; updateIPriceTTC(); } }); wrap.appendChild(lbl); }); el.innerHTML = ""; el.appendChild(wrap); } catch (e) {} } function updateIPriceTTC() { const ht = document.getElementById("ipart-prix-ht")?.checked; const price = parseFloat(document.getElementById("ipart-price")?.value) || 0; const preview = document.getElementById("ipart-ttc-preview"); if (!preview) return; if (ht && price > 0) { preview.textContent = "→ Prix TTC : " + (price * 1.2).toFixed(2) + " €"; preview.style.display = "block"; } else { preview.style.display = "none"; } } async function saveInlinePart() { const name = document.getElementById("ipart-name")?.value.trim(); if (!name) { toast("Nom requis", "error"); return; } let price = parseFloat(document.getElementById("ipart-price")?.value) || 0; if (document.getElementById("ipart-prix-ht")?.checked) price = parseFloat((price * 1.2).toFixed(2)); const fd = new FormData(); fd.append("name", name); fd.append("vehicle_id", currentVehicleId || ""); fd.append("maintenance_id", currentMaintenanceId || ""); fd.append("price", price); fd.append( "quantity", parseInt(document.getElementById("ipart-quantity")?.value) || 1, ); fd.append("category", "Autre"); fd.append("unit", "pièce"); const brand = document.getElementById("ipart-brand")?.value.trim(); const ref = document.getElementById("ipart-reference")?.value.trim(); if (brand) fd.append("brand", brand); if (ref) fd.append("reference", ref); const photoFile = document.getElementById("ipart-photo")?.files[0]; if (photoFile) fd.append("photo", photoFile); try { await api("parts", "POST", fd); toast("Pièce ajoutée"); ["ipart-name", "ipart-brand", "ipart-reference"].forEach((id) => { const el = document.getElementById(id); if (el) el.value = ""; }); const pr = document.getElementById("ipart-price"); if (pr) pr.value = "0"; const qty = document.getElementById("ipart-quantity"); if (qty) qty.value = "1"; const ht = document.getElementById("ipart-prix-ht"); if (ht) ht.checked = false; const prev = document.getElementById("ipart-ttc-preview"); if (prev) prev.style.display = "none"; const ph = document.getElementById("ipart-photo"); if (ph) ph.value = ""; const parts = await api( "parts", "GET", null, "&maintenance_id=" + currentMaintenanceId, ); renderMaintenanceParts(parts, currentMaintenanceId, currentVehicleId); } catch (e) { toast(e.message, "error"); } } // ─── ALL PARTS ──────────────────────────────────────────────────────────────── async function loadParts() { try { const list = await api("parts"); const el = $("#all-parts-list"); const total = list.reduce( (s, p) => s + parseFloat(p.price || 0) * parseInt(p.quantity || 1), 0, ); $("#all-parts-total").textContent = fmtPrice(total); $("#all-parts-count").textContent = list.length; if (!list.length) { el.innerHTML = 'Aucune pièce
| Photo | Nom | Marque | Référence | Catégorie | Prix unit. | Qté | Total | Entretien | |
|---|---|---|---|---|---|---|---|---|---|
| " +
(p.photo
? ' | " +
"" + escHtml(p.name) + " | " + "" + (p.brand ? escHtml(p.brand) : "--") + " | " + '' +
(p.reference ? escHtml(p.reference) : "--") +
" | " +
'' + escHtml(p.category) + " | " + "" + fmtPrice(p.price) + " | " + "" + p.quantity + " " + escHtml(p.unit || "pièce") + " | " + "" + fmtPrice(parseFloat(p.price || 0) * parseInt(p.quantity || 1)) + " | " + '' + (p.maintenance_type ? escHtml(p.maintenance_type) + " (" + fmtDate(p.maintenance_date) + ")" : "--") + " | " + "" + ' ' + '' + " | " + "
Aucun véhicule
| Date | Type | Description | Km | Coût M.O. | Pièces | Total | Mécanicien | |
|---|---|---|---|---|---|---|---|---|
| " + fmtDate(m.date) + " | " + '' + escHtml(m.type) + " | " + "" + (m.description ? escHtml(m.description) : "--") + " | " + "" + (m.km ? fmt(m.km) + " km" : "--") + " | " + "" + fmtPrice(m.cost) + " | " + "" + (m.parts_count > 0 ? m.parts_count + " (" + fmtPrice(m.parts_cost) + ")" : "--") + " | " + "" + fmtPrice( parseFloat(m.cost || 0) + parseFloat(m.parts_cost || 0), ) + " | " + "" + (m.mechanic ? escHtml(m.mechanic) : "--") + " | " + '' + '' + " | " + "
Aucun entretien
Aucun document pour l’instant