// 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

'; return; } el.innerHTML = list .slice(0, 6) .map((v) => vehicleCardHTML(v)) .join(""); $$(".vehicle-card", el).forEach((c) => c.addEventListener("click", () => navigate("vehicle", { id: c.dataset.id }), ), ); } function vehicleCardHTML(v) { const photoHTML = v.photo ? '' : '🚗'; const fuel = v.fuel_type || "Essence"; const fuelClass = "fuel-" + fuel.replace(/\s/g, ""); return ( '
' + '
' + photoHTML + '' + escHtml(fuel) + "" + "
" + '
' + '
' + escHtml(v.name) + "
" + '
' + escHtml(v.brand) + " " + escHtml(v.model) + (v.year ? " · " + v.year : "") + "
" + '
' + '
🔧 ' + (v.maintenance_count || 0) + " entretiens
" + // Affichage conditionnel de la conso (v.consumption ? '
' + v.consumption + " L/100
" : "") + '
💶 ' + fmtPrice(v.total_cost) + "
" + "
" + "
" + '" + "
" ); } function renderReminders(list) { const el = $("#reminders-list"); const today = new Date(); if (!list.length) { el.innerHTML = '

Aucun rappel configuré

'; return; } el.innerHTML = '
' + list .map((r) => { const diff = r.next_km && r.current_km ? r.next_km - r.current_km : null; const dateOk = r.next_date ? new Date(r.next_date) > today : true; const kmOk = diff == null || diff > 0; const cls = !dateOk || !kmOk ? "badge-red" : diff != null && diff < 2000 ? "badge-amber" : "badge-green"; return ( "" + "" + "" + "" + "" ); }) .join("") + "
VéhiculeTypeProchaine dateProchain 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" : "--") + "
"; } // ─── VEHICLES LIST ──────────────────────────────────────────────────────────── async function loadVehicles() { try { const list = await api("vehicles"); const el = $("#vehicles-grid"); if (!list.length) { el.innerHTML = '
🚗

Aucun véhicule. Ajoutez-en un !

'; return; } el.innerHTML = list.map((v) => vehicleCardHTML(v)).join(""); $$(".vehicle-card", el).forEach((c) => c.addEventListener("click", () => navigate("vehicle", { id: c.dataset.id }), ), ); } catch (e) { toast(e.message, "error"); } } // ─── VEHICLE DETAIL ─────────────────────────────────────────────────────────── async function loadVehicleDetail(id) { currentVehicleId = id; try { const [v, maintenances, parts] = await Promise.all([ api("vehicles", "GET", null, "&id=" + id), api("maintenances", "GET", null, "&vehicle_id=" + id), api("parts", "GET", null, "&vehicle_id=" + id), ]); renderVehicleHeader(v); renderMaintenances(maintenances); renderVehicleParts(parts); setVehicleDocTabBadge(v); await loadVehicleDocuments(); document.title = "GarageManager · " + v.name; } catch (e) { toast(e.message, "error"); } } function renderVehicleHeader(v) { const photoHTML = v.photo ? '' : '
🚗
'; const totalCost = ( parseFloat(v.stats?.total || 0) + parseFloat(v.parts_stats?.total || 0) ).toFixed(2); $("#vehicle-header").innerHTML = '
' + '
' + photoHTML + "
" + '
' + "

" + escHtml(v.name) + "

" + '

' + escHtml(v.brand) + " " + escHtml(v.model) + (v.year ? " · " + v.year : "") + "

" + '
' + (v.license_plate ? '' + escHtml(v.license_plate) + "" : "") + '' + (v.fuel_type || "Essence") + "" + (v.color ? '🎨 ' + escHtml(v.color) + "" : "") + "
" + '
' + '
Kilométrage
' + fmt(v.current_km) + " km
" + // 🔥 L'AJOUT EST ICI : Affichage de la consommation moyenne si elle existe (v.consumption ? '
Conso. moy.
⛽ ' + v.consumption + " L/100
" : "") + '
Entretiens
' + (v.stats?.cnt || 0) + "
" + '
Coût total
' + fmtPrice(totalCost) + "
" + (v.purchase_date ? '
Achat
' + fmtDate(v.purchase_date) + "
" : "") + (v.vin ? '
VIN
' + escHtml(v.vin) + "
" : "") + "
" + '
' + "📎 " + (Number(v.vehicle_docs_count) || 0) + " " + (Number(v.vehicle_docs_count) === 1 ? "document" : "documents") + " véhicule · " + "" + (Number(v.maint_docs_count) || 0) + " sur entretiens (factures, etc.) · " + '' + "
" + "
" + '
' + '' + '' + '' + "
" + "
"; } function renderMaintenances(list) { const el = $("#maintenance-list"); const totalCost = list.reduce( (s, m) => s + parseFloat(m.cost || 0) + parseFloat(m.parts_cost || 0), 0, ); $("#maintenance-total").textContent = fmtPrice(totalCost); if (!list.length) { el.innerHTML = '
🔧

Aucun entretien enregistré

'; return; } el.innerHTML = '
' + list .map( (m) => '' + "" + '" + '" + "" + "" + "" + '" + '" + "", ) .join("") + "
DateTypeDescriptionKilométrageCoût MOPiècesProchain
" + 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" : "") + "
' + ' ' + '' + "
"; } function renderVehicleParts(list) { const el = $("#parts-list-vehicle"); const total = list.reduce( (s, p) => s + parseFloat(p.price || 0) * parseInt(p.quantity || 1), 0, ); $("#parts-total-vehicle").textContent = fmtPrice(total); if (!list.length) { el.innerHTML = '
🔩

Aucune pièce enregistrée

'; return; } el.innerHTML = '
' + list .map( (p) => "" + "" + "" + "" + '" + '" + "" + "" + "" + '" + "" + "", ) .join("") + "
PhotoNomMarqueRéférenceCatégoriePrix unit.QtéTotalEntretien
" + (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) + ")" : "--") + "" + ' ' + '' + "
"; } // ─── MAINTENANCE DETAIL ─────────────────────────────────────────────────────── async function loadMaintenanceDetail(id) { currentMaintenanceId = id; try { const [m, parts] = await Promise.all([ api("maintenances", "GET", null, "&id=" + id), api("parts", "GET", null, "&maintenance_id=" + id), ]); currentVehicleId = m.vehicle_id; renderMaintenanceDetailHeader(m); renderMaintenanceParts(parts, id, m.vehicle_id); await loadMaintenanceDocuments(); loadKnownParts(m.vehicle_id); const f = document.getElementById("inline-part-form"); if (f) f.style.display = "none"; } catch (e) { toast(e.message, "error"); } } function renderMaintenanceDetailHeader(m) { document.getElementById("maintenance-detail-header").innerHTML = '
' + '
' + '
' + '' + escHtml(m.type) + "" + '' + fmtDate(m.date) + " · " + ago(m.date) + "" + (m.documents_count > 0 ? '📎 ' + m.documents_count + " fichier" + (m.documents_count > 1 ? "s" : "") + "" : "") + (m.vehicle_name ? '🚗 ' + escHtml(m.vehicle_name) + "" : "") + "
" + (m.description ? '

' + escHtml(m.description) + "

" : "") + '
' + (m.km ? '
Kilométrage
' + fmt(m.km) + " km
" : "") + '
Coût M.O.
' + fmtPrice(m.cost) + "
" + (m.mechanic ? '
Mécanicien
' + escHtml(m.mechanic) + "
" : "") + (m.garage_name ? '
Garage
' + escHtml(m.garage_name) + "
" : "") + (m.next_date ? '
Prochain entretien
📅 ' + fmtDate(m.next_date) + "
" : "") + (m.next_km ? '
Prochain km
🛣️ ' + fmt(m.next_km) + " km
" : "") + "
" + (m.notes ? '

' + escHtml(m.notes) + "

" : "") + "
" + '
' + '' + '' + "
" + "
"; } function renderMaintenanceParts(list, mid, vid) { const el = document.getElementById("maintenance-parts-list"); const cnt = document.getElementById("maint-parts-count"); if (cnt) cnt.textContent = list.length ? list.length + " pièce" + (list.length > 1 ? "s" : "") : ""; if (!list.length) { el.innerHTML = '
🔩

Aucune pièce utilisée

'; return; } const total = list.reduce( (s, p) => s + parseFloat(p.price || 0) * parseInt(p.quantity || 1), 0, ); el.innerHTML = '
' + list .map( (p) => "" + "" + "" + "" + '" + '" + "" + "" + "" + "" + "", ) .join("") + '" + "
PhotoNomMarqueRéf.CatégoriePrix 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) + "
"; } async function deleteMaintPart(id, mid, vid) { if (!confirm("Supprimer cette pièce ?")) return; try { await api("parts", "DELETE", null, "&id=" + id); toast("Pièce supprimée"); const parts = await api("parts", "GET", null, "&maintenance_id=" + mid); renderMaintenanceParts(parts, mid, vid); } catch (e) { toast(e.message, "error"); } } // ─── INLINE PART FORM ──────────────────────────────────────────────────────── function toggleInlinePartForm() { const f = document.getElementById("inline-part-form"); if (!f) return; const visible = f.style.display === "block"; f.style.display = visible ? "none" : "block"; if (!visible) document.getElementById("ipart-name")?.focus(); } async function loadKnownParts(vehicleId) { try { const parts = await api("parts", "GET", null, "&vehicle_id=" + vehicleId); const el = document.getElementById("known-parts-list"); if (!el) return; if (!parts.length) { el.innerHTML = '

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

'; return; } const byVehicle = {}; list.forEach((p) => { const key = p.vehicle_name || "Sans véhicule"; if (!byVehicle[key]) byVehicle[key] = []; byVehicle[key].push(p); }); let html = ""; for (const [vname, parts] of Object.entries(byVehicle)) { const vtotal = parts.reduce( (s, p) => s + parseFloat(p.price || 0) * parseInt(p.quantity || 1), 0, ); html += '
' + '
' + '

🚗 ' + escHtml(vname) + "

" + '' + parts.length + " pièce" + (parts.length > 1 ? "s" : "") + " · " + fmtPrice(vtotal) + "" + "
" + '
' + parts .map( (p) => "" + "" + "" + "" + '" + '" + "" + "" + "" + '" + "" + "", ) .join("") + "
PhotoNomMarqueRéférenceCatégoriePrix unit.QtéTotalEntretien
" + (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) + ")" : "--") + "" + ' ' + '' + "
"; } el.innerHTML = html; } catch (e) { toast(e.message, "error"); } } // ─── ALL MAINTENANCES ───────────────────────────────────────────────────────── async function loadAllMaintenances() { try { const vehicles = await api("vehicles"); const el = $("#all-maintenances-list"); if (!vehicles.length) { el.innerHTML = '
🔧

Aucun véhicule

'; return; } let totalAll = 0, countAll = 0, html = ""; for (const v of vehicles) { const mlist = await api( "maintenances", undefined, null, "&vehicle_id=" + v.id, ); if (!mlist.length) continue; const total = mlist.reduce( (s, m) => s + parseFloat(m.cost || 0) + parseFloat(m.parts_cost || 0), 0, ); totalAll += total; countAll += mlist.length; html += '
' + '
' + '

🚗 ' + escHtml(v.name) + "

" + (v.license_plate ? '' + escHtml(v.license_plate) + "" : "") + '' + mlist.length + " entretiens · " + fmtPrice(total) + "" + "
" + '
' + mlist .map( (m) => '' + "" + '" + "" + "" + "" + "" + "" + "" + '" + "", ) .join("") + "
DateTypeDescriptionKmCoût M.O.PiècesTotalMé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) : "--") + "' + '' + "
"; } $("#all-maint-count").textContent = countAll; $("#all-maint-total").textContent = fmtPrice(totalAll); el.innerHTML = html || '
🔧

Aucun entretien

'; } catch (e) { console.error(e); } } // ─── MODALS ─────────────────────────────────────────────────────────────────── function openModal(id) { document.getElementById(id).classList.add("show"); } function closeModal(id) { document.getElementById(id).classList.remove("show"); } // ─── TVA ────────────────────────────────────────────────────────────────────── function updatePartPriceTTC() { const ht = document.getElementById("part-prix-ht")?.checked; const price = parseFloat(document.getElementById("part-price")?.value) || 0; const preview = document.getElementById("part-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"; } } // ─── VEHICLE CRUD ───────────────────────────────────────────────────────────── function openAddVehicle() { $("#form-vehicle").reset(); $("#form-vehicle-id").value = ""; $("#modal-vehicle-title").textContent = "Ajouter un véhicule"; $("#vehicle-photo-preview").src = ""; $("#vehicle-photo-preview").style.display = "none"; openModal("modal-vehicle"); } function openEditVehicle(id) { api("vehicles", "GET", null, "&id=" + id) .then((v) => { $("#form-vehicle-id").value = v.id; $("#modal-vehicle-title").textContent = "Modifier le véhicule"; [ "name", "brand", "model", "year", "license_plate", "vin", "color", "purchase_date", "purchase_price", "current_km", "consumption", "notes", ].forEach((f) => { const el = $("#vehicle-" + f.replace(/_/g, "-")); if (el) el.value = v[f] ?? ""; }); setSelectVal("vehicle-fuel-type", v.fuel_type); if (v.photo) { $("#vehicle-photo-preview").src = UPLOADS + v.photo; $("#vehicle-photo-preview").style.display = "block"; } else { $("#vehicle-photo-preview").style.display = "none"; } openModal("modal-vehicle"); }) .catch((e) => toast(e.message, "error")); } async function saveVehicle() { const id = $("#form-vehicle-id").value; const fd = new FormData($("#form-vehicle")); try { if (id) { const d = {}; for (const [k, v] of fd.entries()) d[k] = v; await api("vehicles", "PUT", d, "&id=" + id); const photoFile = $("#vehicle-photo").files[0]; if (photoFile) { const pfd = new FormData(); pfd.append("photo", photoFile); await fetch(API + "?action=upload_vehicle_photo&id=" + id, { method: "POST", body: pfd, }); } toast("Véhicule modifié"); } else { await api("vehicles", "POST", fd); toast("Véhicule ajouté"); } closeModal("modal-vehicle"); if (currentPage === "vehicles") loadVehicles(); else if (currentPage === "vehicle") loadVehicleDetail(currentVehicleId); else loadDashboard(); } catch (e) { toast(e.message, "error"); } } async function deleteVehicle(id) { if (!confirm("Supprimer ce véhicule et tout son historique ?")) return; try { await api("vehicles", "DELETE", null, "&id=" + id); toast("Véhicule supprimé"); navigate("vehicles"); } catch (e) { toast(e.message, "error"); } } // ─── MAINTENANCE CRUD ───────────────────────────────────────────────────────── function openAddMaintenance() { $("#form-maintenance").reset(); $("#form-maintenance-id").value = ""; $("#maintenance-vehicle-id").value = currentVehicleId || ""; $("#modal-maintenance-title").textContent = "Ajouter un entretien"; $("#maintenance-date").value = new Date().toISOString().slice(0, 10); openModal("modal-maintenance"); } function setSelectVal(elId, val) { const el = document.getElementById(elId); if (!el || val == null) return; el.value = val; if (el.value !== String(val) && val !== "") { const o = new Option(val, val); el.add(o); el.value = val; } } function openEditMaintenance(id) { api("maintenances", "GET", null, "&id=" + id) .then((m) => { $("#form-maintenance-id").value = m.id; $("#maintenance-vehicle-id").value = m.vehicle_id; $("#modal-maintenance-title").textContent = "Modifier l'entretien"; setSelectVal("maintenance-type", m.type); const map = { description: "maintenance-description", date: "maintenance-date", km: "maintenance-km", cost: "maintenance-cost", mechanic: "maintenance-mechanic", garage_name: "maintenance-garage", next_date: "maintenance-next-date", next_km: "maintenance-next-km", notes: "maintenance-notes", }; for (const [k, elId] of Object.entries(map)) { const el = document.getElementById(elId); if (el) el.value = m[k] ?? ""; } openModal("modal-maintenance"); }) .catch((e) => toast(e.message, "error")); } async function saveMaintenance() { const id = $("#form-maintenance-id").value; const d = {}; new FormData($("#form-maintenance")).forEach((v, k) => (d[k] = v)); try { if (id) { await api("maintenances", "PUT", d, "&id=" + id); toast("Entretien modifié"); } else { await api("maintenances", "POST", d); toast("Entretien ajouté"); } closeModal("modal-maintenance"); if (currentPage === "maintenance") loadMaintenanceDetail(currentMaintenanceId); else loadVehicleDetail(currentVehicleId); } catch (e) { toast(e.message, "error"); } } async function deleteMaintenance(id) { if (!confirm("Supprimer cet entretien ?")) return; try { await api("maintenances", "DELETE", null, "&id=" + id); toast("Entretien supprimé"); if (currentPage === "maintenance") navigate("vehicle", { id: currentVehicleId }); else loadVehicleDetail(currentVehicleId); } catch (e) { toast(e.message, "error"); } } // ─── PARTS CRUD ─────────────────────────────────────────────────────────────── function openAddPart() { $("#form-part").reset(); $("#form-part-id").value = ""; $("#part-vehicle-id").value = currentVehicleId || ""; $("#part-maintenance-id").value = currentMaintenanceId || ""; $("#modal-part-title").textContent = "Ajouter une pièce"; $("#part-photo-preview").src = ""; $("#part-photo-preview").style.display = "none"; const ht = document.getElementById("part-prix-ht"); if (ht) ht.checked = false; const prev = document.getElementById("part-ttc-preview"); if (prev) prev.style.display = "none"; openModal("modal-part"); } function openEditPart(id) { api("parts", "GET", null, "&id=" + id) .then((p) => { $("#form-part-id").value = p.id; $("#part-vehicle-id").value = p.vehicle_id || ""; $("#part-maintenance-id").value = p.maintenance_id || ""; $("#modal-part-title").textContent = "Modifier la pièce"; const map = { name: "part-name", brand: "part-brand", reference: "part-reference", price: "part-price", quantity: "part-quantity", notes: "part-notes", }; for (const [k, elId] of Object.entries(map)) { const el = document.getElementById(elId); if (el) el.value = p[k] ?? ""; } setSelectVal("part-category", p.category); const ht = document.getElementById("part-prix-ht"); if (ht) ht.checked = false; const prev = document.getElementById("part-ttc-preview"); if (prev) prev.style.display = "none"; if (p.photo) { $("#part-photo-preview").src = UPLOADS + p.photo; $("#part-photo-preview").style.display = "block"; } else { $("#part-photo-preview").style.display = "none"; } openModal("modal-part"); }) .catch((e) => toast(e.message, "error")); } async function savePart() { const id = $("#form-part-id").value; const fd = new FormData($("#form-part")); if (document.getElementById("part-prix-ht")?.checked) { const htPrice = parseFloat(fd.get("price")) || 0; fd.set("price", (htPrice * 1.2).toFixed(2)); } try { if (id) { const d = {}; for (const [k, v] of fd.entries()) d[k] = v; await api("parts", "PUT", d, "&id=" + id); const photoFile = $("#part-photo").files[0]; if (photoFile) { const pfd = new FormData(); pfd.append("photo", photoFile); await fetch(API + "?action=upload_part_photo&id=" + id, { method: "POST", body: pfd, }); } toast("Pièce modifiée"); } else { await api("parts", "POST", fd); toast("Pièce ajoutée"); } closeModal("modal-part"); if (currentPage === "maintenance") loadMaintenanceDetail(currentMaintenanceId); else if (currentPage === "vehicle") loadVehicleDetail(currentVehicleId); else loadParts(); } catch (e) { toast(e.message, "error"); } } async function deletePart(id) { if (!confirm("Supprimer cette pièce ?")) return; try { await api("parts", "DELETE", null, "&id=" + id); toast("Pièce supprimée"); if (currentPage === "maintenance") loadMaintenanceDetail(currentMaintenanceId); else if (currentPage === "vehicle") loadVehicleDetail(currentVehicleId); else loadParts(); } catch (e) { toast(e.message, "error"); } } // ─── UPLOAD PHOTO ───────────────────────────────────────────────────────────── function openUploadPhoto(id) { $("#upload-vehicle-id").value = id; openModal("modal-upload-photo"); } async function doUploadPhoto() { const id = $("#upload-vehicle-id").value; const file = $("#upload-photo-file").files[0]; if (!file) { toast("Choisissez une photo", "error"); return; } const fd = new FormData(); fd.append("photo", file); try { await fetch(API + "?action=upload_vehicle_photo&id=" + id, { method: "POST", body: fd, }); toast("Photo mise à jour"); closeModal("modal-upload-photo"); loadVehicleDetail(id); } catch (e) { toast(e.message, "error"); } } function setVehicleDocTabBadge(v) { const el = document.getElementById("vehicle-docs-tab-count"); if (!el) return; const n = Number(v.vehicle_docs_count) || 0; el.textContent = n > 0 ? "(" + n + ")" : ""; } async function loadVehicleDocuments() { if (!currentVehicleId) return; try { const list = await api( "garage_documents", "GET", null, "&vehicle_id=" + currentVehicleId + "&vehicle_only=1", ); renderGarageDocList("vehicle-documents-list", list); } catch (e) { toast(e.message, "error"); } } async function loadMaintenanceDocuments() { if (!currentMaintenanceId) return; try { const list = await api( "garage_documents", "GET", null, "&maintenance_id=" + currentMaintenanceId, ); renderGarageDocList("maintenance-documents-list", list); } catch (e) { toast(e.message, "error"); } } function renderGarageDocList(elId, list) { const el = document.getElementById(elId); if (!el) return; if (!list || !list.length) { el.innerHTML = '

Aucun document pour l’instant

'; return; } el.innerHTML = '"; } async function uploadVehicleDocument() { const inp = document.getElementById("vehicle-doc-file"); if (!inp || !inp.files[0]) { toast("Choisissez un fichier", "error"); return; } const fd = new FormData(); fd.append("vehicle_id", String(currentVehicleId)); fd.append("file", inp.files[0]); const lab = document.getElementById("vehicle-doc-label")?.value.trim(); if (lab) fd.append("label", lab); try { const r = await fetch(API + "?action=garage_documents", { method: "POST", body: fd, }); const j = await r.json(); if (!j.ok) throw new Error(j.error || "Erreur"); toast("Document ajouté"); inp.value = ""; const ll = document.getElementById("vehicle-doc-label"); if (ll) ll.value = ""; const v = await api("vehicles", "GET", null, "&id=" + currentVehicleId); renderVehicleHeader(v); setVehicleDocTabBadge(v); await loadVehicleDocuments(); } catch (e) { toast(e.message, "error"); } } async function uploadMaintenanceDocument() { const inp = document.getElementById("maint-doc-file"); if (!inp || !inp.files[0]) { toast("Choisissez un fichier", "error"); return; } if (!currentVehicleId || !currentMaintenanceId) { toast("Contexte manquant", "error"); return; } const fd = new FormData(); fd.append("vehicle_id", String(currentVehicleId)); fd.append("maintenance_id", String(currentMaintenanceId)); fd.append("file", inp.files[0]); const lab = document.getElementById("maint-doc-label")?.value.trim(); if (lab) fd.append("label", lab); try { const r = await fetch(API + "?action=garage_documents", { method: "POST", body: fd, }); const j = await r.json(); if (!j.ok) throw new Error(j.error || "Erreur"); toast("Document ajouté"); inp.value = ""; const ll = document.getElementById("maint-doc-label"); if (ll) ll.value = ""; const m = await api( "maintenances", "GET", null, "&id=" + currentMaintenanceId, ); renderMaintenanceDetailHeader(m); await loadMaintenanceDocuments(); } catch (e) { toast(e.message, "error"); } } async function deleteGarageDocument(id) { if (!confirm("Supprimer ce document ?")) return; try { await api("garage_documents", "DELETE", null, "&id=" + id); toast("Document supprimé"); if (currentPage === "vehicle") { const v = await api("vehicles", "GET", null, "&id=" + currentVehicleId); renderVehicleHeader(v); setVehicleDocTabBadge(v); await loadVehicleDocuments(); } else if (currentPage === "maintenance") { const m = await api( "maintenances", "GET", null, "&id=" + currentMaintenanceId, ); renderMaintenanceDetailHeader(m); await loadMaintenanceDocuments(); } } catch (e) { toast(e.message, "error"); } } // ─── TABS ──────────────────────────────────────────────────────────────────── function switchTab(tab) { $$(".tab-btn").forEach((b) => b.classList.remove("active")); $$(".tab-pane").forEach((p) => p.classList.remove("active")); document .querySelector('.tab-btn[data-tab="' + tab + '"]') ?.classList.add("active"); document.getElementById("tab-" + tab)?.classList.add("active"); if (tab === "documents" && currentVehicleId) loadVehicleDocuments(); } // ─── PHOTO PREVIEW ──────────────────────────────────────────────────────────── function previewPhoto(inputId, previewId) { const file = document.getElementById(inputId).files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { const img = document.getElementById(previewId); img.src = e.target.result; img.style.display = "block"; }; reader.readAsDataURL(file); } // ─── INIT ──────────────────────────────────────────────────────────────────── document.addEventListener("DOMContentLoaded", () => { $$(".nav-link").forEach((n) => n.addEventListener("click", () => navigate(n.dataset.page)), ); $$(".modal-backdrop").forEach((m) => m.addEventListener("click", (e) => { if (e.target === m) m.classList.remove("show"); }), ); navigate("dashboard"); });