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