diff --git a/garage.php b/garage.php index 7a5b75e..3f578b7 100644 --- a/garage.php +++ b/garage.php @@ -217,15 +217,25 @@ require __DIR__ . '/header.php'; +
-
+
+ + +
+
-
+ +
+
+
+
+
diff --git a/modules/garage/api.php b/modules/garage/api.php index 312a07b..d519000 100644 --- a/modules/garage/api.php +++ b/modules/garage/api.php @@ -109,14 +109,31 @@ if ($action === 'vehicles') { } if ($method === 'POST') { $photo = handleUpload('photo', $UPLOAD_DIR); $d = $_POST ?: gBody(); - $stmt = $pdo->prepare("INSERT INTO pf_vehicles (name,brand,model,year,license_plate,vin,fuel_type,color,purchase_date,purchase_price,current_km,photo,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)"); - $stmt->execute([$d['name']??'', $d['brand']??'', $d['model']??'', $d['year']??null, $d['license_plate']??null, $d['vin']??null, $d['fuel_type']??'Essence', $d['color']??null, $d['purchase_date']??null, $d['purchase_price']??null, $d['current_km']??0, $photo, $d['notes']??null]); + // Ajout de consumption dans la liste des champs et d'un '?' supplémentaire + $stmt = $pdo->prepare("INSERT INTO pf_vehicles (name,brand,model,year,license_plate,vin,fuel_type,consumption,color,purchase_date,purchase_price,current_km,photo,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); + $stmt->execute([ + $d['name']??'', + $d['brand']??'', + $d['model']??'', + $d['year']??null, + $d['license_plate']??null, + $d['vin']??null, + $d['fuel_type']??'Essence', + ($d['consumption'] !== '' && $d['consumption'] !== null) ? (float)$d['consumption'] : null, // Cast propre en float + $d['color']??null, + $d['purchase_date']??null, + $d['purchase_price']??null, + $d['current_km']??0, + $photo, + $d['notes']??null + ]); gOk(['id' => $pdo->lastInsertId()]); } if ($method === 'PUT') { $id = $_GET['id'] ?? null; if (!$id) gErr('ID manquant'); $d = gBody(); - $int_fields = ['year','current_km']; $float_fields = ['purchase_price']; - $fields = ['name','brand','model','year','license_plate','vin','fuel_type','color','purchase_date','purchase_price','current_km','notes']; + $int_fields = ['year','current_km']; + $float_fields = ['purchase_price', 'consumption']; + $fields = ['name','brand','model','year','license_plate','vin','fuel_type','consumption','color','purchase_date','purchase_price','current_km','notes']; $sets = array_map(fn($f) => "$f = ?", $fields); $vals = array_map(function($f) use ($d, $int_fields, $float_fields) { $v = $d[$f] ?? null; diff --git a/modules/garage/assets/garage.js b/modules/garage/assets/garage.js index 14a46bc..0b9fd86 100644 --- a/modules/garage/assets/garage.js +++ b/modules/garage/assets/garage.js @@ -1,784 +1,1550 @@ // GarageManager - SPA Frontend -const API = '/modules/garage/api.php'; -const UPLOADS = '/modules/garage/api.php?action=photo&file='; +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 $(s, ctx = document) { + return ctx.querySelector(s); } -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); +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; +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(); +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'); +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)); +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');} + } 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 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
'+ - '
💶 '+fmtPrice(v.total_cost)+'
'+ - '
'+ - '
'+ - '
'; +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':'--')+'
'; +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');} +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) +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');} + 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
'+ - '
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 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 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)+')':'--')+''+ - ' '+ - ''+ - '
'; +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) +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; + currentVehicleId = m.vehicle_id; renderMaintenanceDetailHeader(m); - renderMaintenanceParts(parts,id,m.vehicle_id); + 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');} + 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 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)+'
'; +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');} +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(); +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();} +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.innerHTML = ""; el.appendChild(wrap); - }catch(e){} + } 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';} +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');} +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)+')':'--')+''+ - ' '+ - ''+ - '
'; +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; } - el.innerHTML=html; - }catch(e){toast(e.message,'error');} + 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):'--')+''+ - ''+ - '
'; +async function loadAllMaintenances() { + try { + const vehicles = await api("vehicles"); + const el = $("#all-maintenances-list"); + if (!vehicles.length) { + el.innerHTML = + '
🔧

Aucun véhicule

'; + return; } - $('#all-maint-count').textContent=countAll; - $('#all-maint-total').textContent=fmtPrice(totalAll); - el.innerHTML=html||'
🔧

Aucun entretien

'; - }catch(e){console.error(e);} + 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');} +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';} +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 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','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')); +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é'); +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); + closeModal("modal-vehicle"); + if (currentPage === "vehicles") loadVehicles(); + else if (currentPage === "vehicle") loadVehicleDetail(currentVehicleId); else loadDashboard(); - }catch(e){toast(e.message,'error');} + } 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');} +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 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 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')); +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); +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');} + } 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}); +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');} + } 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 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')); +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)); +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'); + 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); + closeModal("modal-part"); + if (currentPage === "maintenance") + loadMaintenanceDetail(currentMaintenanceId); + else if (currentPage === "vehicle") loadVehicleDetail(currentVehicleId); else loadParts(); - }catch(e){toast(e.message,'error');} + } 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); +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');} + } 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 openUploadPhoto(id) { + $("#upload-vehicle-id").value = id; + openModal("modal-upload-photo"); } - -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

'; +async function doUploadPhoto() { + const id = $("#upload-vehicle-id").value; + const file = $("#upload-photo-file").files[0]; + if (!file) { + toast("Choisissez une photo", "error"); return; } - el.innerHTML=''; + 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"); + } } -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); +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');} + } 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); +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');} + } 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); +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); + } else if (currentPage === "maintenance") { + const m = await api( + "maintenances", + "GET", + null, + "&id=" + currentMaintenanceId, + ); renderMaintenanceDetailHeader(m); await loadMaintenanceDocuments(); } - }catch(e){toast(e.message,'error');} + } 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'&¤tVehicleId)loadVehicleDocuments(); +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';}; +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'); +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"); });