// 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
'+ '
💶 '+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
'+ '
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','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'&¤tVehicleId)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'); });