@@ -205,16 +254,17 @@ require __DIR__ . '/header.php';
-
+
diff --git a/modules/garage/api.php b/modules/garage/api.php
index 380a35f..97971fe 100644
--- a/modules/garage/api.php
+++ b/modules/garage/api.php
@@ -74,6 +74,12 @@ if ($action === 'vehicles') {
if ($action === 'maintenances') {
$vid = $_GET['vehicle_id'] ?? null;
if ($method === 'GET') {
+ $id = $_GET['id'] ?? null;
+ if ($id) {
+ $stmt = $pdo->prepare("SELECT m.*, v.name as vehicle_name, v.license_plate, v.current_km FROM pf_maintenances m JOIN pf_vehicles v ON v.id = m.vehicle_id WHERE m.id = ?");
+ $stmt->execute([$id]); $m = $stmt->fetch(); if (!$m) gErr('Entretien introuvable', 404);
+ gOk($m);
+ }
if ($vid) {
$stmt = $pdo->prepare("SELECT m.*, GROUP_CONCAT(p.name SEPARATOR ', ') as parts_names, COUNT(p.id) as parts_count, COALESCE(SUM(p.price*p.quantity),0) as parts_cost FROM pf_maintenances m LEFT JOIN pf_parts p ON p.maintenance_id = m.id WHERE m.vehicle_id = ? GROUP BY m.id ORDER BY m.date DESC, m.created_at DESC");
$stmt->execute([$vid]); gOk($stmt->fetchAll());
@@ -85,7 +91,7 @@ if ($action === 'maintenances') {
$photo = handleUpload('invoice_photo', $UPLOAD_DIR); $d = $_POST ?: gBody();
if (!($d['vehicle_id'] ?? null)) gErr('vehicle_id manquant');
$stmt = $pdo->prepare("INSERT INTO pf_maintenances (vehicle_id,type,description,date,km,cost,mechanic,garage_name,next_km,next_date,invoice_photo,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)");
- $stmt->execute([$d['vehicle_id'], $d['type']??'', $d['description']??null, $d['date']??date('Y-m-d'), $d['km']??null, $d['cost']??0, $d['mechanic']??null, $d['garage']??null, $d['next_km']??null, $d['next_date']??null, $photo, $d['notes']??null]);
+ $stmt->execute([$d['vehicle_id'], $d['type']??'', $d['description']??null, $d['date']??date('Y-m-d'), $d['km']??null, $d['cost']??0, $d['mechanic']??null, $d['garage_name']??null, $d['next_km']??null, $d['next_date']??null, $photo, $d['notes']??null]);
$mid = $pdo->lastInsertId();
if (!empty($d['km'])) { $pdo->prepare("UPDATE pf_vehicles SET current_km = GREATEST(current_km, ?), updated_at = NOW() WHERE id = ?")->execute([$d['km'], $d['vehicle_id']]); }
gOk(['id' => $mid]);
@@ -115,6 +121,8 @@ if ($action === 'maintenances') {
if ($action === 'parts') {
if ($method === 'GET') {
$vid = $_GET['vehicle_id'] ?? null; $mid = $_GET['maintenance_id'] ?? null;
+ $id = $_GET['id'] ?? null;
+ if ($id) { $stmt = $pdo->prepare("SELECT * FROM pf_parts WHERE id = ?"); $stmt->execute([$id]); $p = $stmt->fetch(); if (!$p) gErr('Pièce introuvable', 404); gOk($p); }
if ($mid) { $stmt = $pdo->prepare("SELECT * FROM pf_parts WHERE maintenance_id = ? ORDER BY created_at DESC"); $stmt->execute([$mid]); gOk($stmt->fetchAll()); }
if ($vid) { $stmt = $pdo->prepare("SELECT p.*, m.type as maintenance_type, m.date as maintenance_date FROM pf_parts p LEFT JOIN pf_maintenances m ON m.id = p.maintenance_id WHERE p.vehicle_id = ? ORDER BY p.created_at DESC"); $stmt->execute([$vid]); gOk($stmt->fetchAll()); }
$stmt = $pdo->query("SELECT p.*, v.name as vehicle_name, m.type as maintenance_type, m.date as maintenance_date FROM pf_parts p LEFT JOIN pf_vehicles v ON v.id = p.vehicle_id LEFT JOIN pf_maintenances m ON m.id = p.maintenance_id ORDER BY p.created_at DESC");
diff --git a/modules/garage/assets/garage.js b/modules/garage/assets/garage.js
index 12a5494..2539636 100644
--- a/modules/garage/assets/garage.js
+++ b/modules/garage/assets/garage.js
@@ -2,34 +2,40 @@
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 $(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)+' \u20ac':'--';}
+function fmtPrice(n){return n!=null?Number(n).toFixed(2)+' β¬':'--';}
function fmtDate(d){if(!d)return '--';return new Date(d).toLocaleDateString('fr-FR');}
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);
+ t.className='toast '+type;t.textContent=msg;c.appendChild(t);setTimeout(()=>t.remove(),3000);
}
-// NAV
+// βββ 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');
- document.querySelector('.nav-link[data-page="'+page+'"]')?.classList.add('active');
+ const navTarget=page==='maintenance'?'maintenances-all':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
+// βββ 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);}
@@ -40,7 +46,7 @@ async function api(action,method='GET',data=null,extra=''){
return j.data;
}
-// DASHBOARD
+// βββ DASHBOARD ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async function loadDashboard(){
try{
const[stats,vehicles,reminders]=await Promise.all([api('stats'),api('vehicles'),api('maintenances')]);
@@ -55,42 +61,42 @@ async function loadDashboard(){
function renderDashboardVehicles(list){
const el=$('#dashboard-vehicles');
- if(!list.length){el.innerHTML='
\ud83d\ude97
Aucun v\u00e9hicule
';return;}
+ if(!list.length){el.innerHTML='
';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?'

':'
\ud83d\ude97';
+ const photoHTML=v.photo?'

':'
π';
const fuel=v.fuel_type||'Essence';
const fuelClass='fuel-'+fuel.replace(/\s/g,'');
return '
'+
- '
'+photoHTML+''+fuel+'
'+
+ '
'+photoHTML+''+escHtml(fuel)+'
'+
'
'+
- '
'+v.name+'
'+
- '
'+v.brand+' '+v.model+(v.year?' · '+v.year:'')+'
'+
+ '
'+escHtml(v.name)+'
'+
+ '
'+escHtml(v.brand)+' '+escHtml(v.model)+(v.year?' · '+v.year:'')+'
'+
'
'+
- '
\ud83d\udd27 '+(v.maintenance_count||0)+' entretiens
'+
- '
\ud83d\udcb6 '+fmtPrice(v.total_cost)+'
'+
+ '
π§ '+(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\u00e9
';return;}
- el.innerHTML='
| V\u00e9hicule | Type | Prochaine date | Prochain km | \u00c9cart km |
'+
+ if(!list.length){el.innerHTML='';return;}
+ el.innerHTML='| VΓ©hicule | Type | Prochaine date | Prochain km | Γcart km |
'+
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 '| '+r.vehicle_name+''+(r.license_plate?' '+r.license_plate+'':'')+' | '+
- ''+r.type+' | '+
+ return '
| '+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':'--')+' |
';
@@ -98,19 +104,18 @@ function renderReminders(list){
'
';
}
-// VEHICLES LIST
+// βββ VEHICLES LIST ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async function loadVehicles(){
try{
const list=await api('vehicles');
const el=$('#vehicles-grid');
- if(!list.length){el.innerHTML='\ud83d\ude97
Aucun v\u00e9hicule. Ajoutez-en un !
';return;}
+ 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
-let currentVehicleId=null;
+// βββ VEHICLE DETAIL βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async function loadVehicleDetail(id){
currentVehicleId=id;
try{
@@ -122,36 +127,36 @@ async function loadVehicleDetail(id){
renderVehicleHeader(v);
renderMaintenances(maintenances);
renderVehicleParts(parts);
- document.title='GarageManager \u00b7 '+v.name;
+ document.title='GarageManager Β· '+v.name;
}catch(e){toast(e.message,'error');}
}
function renderVehicleHeader(v){
- const photoHTML=v.photo?'
':'\ud83d\ude97
';
+ const photoHTML=v.photo?'
':'π
';
const totalCost=(parseFloat(v.stats?.total||0)+parseFloat(v.parts_stats?.total||0)).toFixed(2);
$('#vehicle-header').innerHTML=
'';
}
@@ -160,17 +165,20 @@ 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='\ud83d\udd27
Aucun entretien enregistr\u00e9
';return;}
- el.innerHTML='| Date | Type | Description | Kilom\u00e9trage | Co\u00fbt MO | Pi\u00e8ces | Prochain | |
'+
- list.map(m=>''+
+ if(!list.length){el.innerHTML='π§
Aucun entretien enregistrΓ©
';return;}
+ el.innerHTML='| Date | Type | Description | Kilométrage | Coût MO | Pièces | Prochain | |
'+
+ list.map(m=>''+
''+fmtDate(m.date)+' '+ago(m.date)+' | '+
- ''+m.type+' | '+
- ''+(m.description||'--')+' | '+
+ ''+escHtml(m.type)+' | '+
+ ''+(m.description?escHtml(m.description):'--')+' | '+
''+(m.km?fmt(m.km)+' km':'--')+' | '+
''+fmtPrice(m.cost)+' | '+
- ''+(m.parts_count>0?'\ud83d\udd29 '+m.parts_count+' ('+fmtPrice(m.parts_cost)+')':'--')+' | '+
- ''+(m.next_date?'\ud83d\udcc5 '+fmtDate(m.next_date):'')+' '+(m.next_km?' \ud83d\udee3\ufe0f '+fmt(m.next_km)+' km':'')+' | '+
- ' | '+
+ ''+(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':'')+' | '+
+ ''+
+ ' '+
+ ''+
+ ' | '+
'
').join('')+
'
';
}
@@ -179,24 +187,180 @@ 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='\ud83d\udd29
Aucune pi\u00e8ce enregistr\u00e9e
';return;}
- el.innerHTML='| Photo | Nom | Marque | R\u00e9f\u00e9rence | Cat\u00e9gorie | Prix unit. | Qt\u00e9 | Total | Fournisseur | |
'+
+ if(!list.length){el.innerHTML='π©
Aucune pièce enregistrée
';return;}
+ el.innerHTML='| Photo | Nom | Marque | RΓ©fΓ©rence | CatΓ©gorie | Prix unit. | QtΓ© | Total | Entretien | |
'+
list.map(p=>''+
''+(p.photo?' ':'--')+' | '+
- ''+p.name+' | '+
- ''+(p.brand||'--')+' | '+
- ''+(p.reference||'--')+' | '+
- ''+p.category+' | '+
+ ''+escHtml(p.name)+' | '+
+ ''+(p.brand?escHtml(p.brand):'--')+' | '+
+ ''+(p.reference?escHtml(p.reference):'--')+' | '+
+ ''+escHtml(p.category)+' | '+
''+fmtPrice(p.price)+' | '+
- ''+p.quantity+' '+p.unit+' | '+
+ ''+p.quantity+' '+escHtml(p.unit||'pièce')+' | '+
''+fmtPrice(parseFloat(p.price||0)*parseInt(p.quantity||1))+' | '+
- ''+(p.supplier||'--')+' | '+
- ' | '+
+ ''+(p.maintenance_type?escHtml(p.maintenance_type)+' ('+fmtDate(p.maintenance_date)+')':'--')+' | '+
+ ''+
+ ' '+
+ ''+
+ ' | '+
'
').join('')+
'
';
}
-// ALL PARTS
+// βββ 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);
+ 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=
+ '';
+}
+
+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='| Photo | Nom | Marque | RΓ©f. | CatΓ©gorie | Prix unit. | QtΓ© | Total | |
'+
+ list.map(p=>''+
+ ''+(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))+' | '+
+ ''+
+ ' '+
+ ''+
+ ' | '+
+ '
').join('')+
+ '| 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';return;}
+ const seen=new Set();
+ const unique=parts.filter(p=>{if(seen.has(p.name))return false;seen.add(p.name);return true;}).slice(0,15);
+ el.innerHTML=unique.map(p=>
+ ''
+ ).join('');
+ $$('button[data-name]',el).forEach(btn=>btn.addEventListener('click',()=>{
+ 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=btn.dataset.name;
+ if(b)b.value=btn.dataset.brand;
+ if(r)r.value=btn.dataset.ref;
+ if(pr){pr.value=btn.dataset.price;updateIPriceTTC();}
+ }));
+ }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');
@@ -204,34 +368,32 @@ async function loadParts(){
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='\ud83d\udd29
Aucune pi\u00e8ce
';return;}
- // Grouper par v\u00e9hicule
+ if(!list.length){el.innerHTML='';return;}
const byVehicle={};
- list.forEach(p=>{
- const key=p.vehicle_name||'Sans v\u00e9hicule';
- if(!byVehicle[key])byVehicle[key]=[];
- byVehicle[key].push(p);
- });
+ 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+=''+
''+
- '
| Photo | Nom | Marque | R\u00e9f\u00e9rence | Cat\u00e9gorie | Prix unit. | Qt\u00e9 | Total | Entretien | |
'+
+ '| Photo | Nom | Marque | RΓ©fΓ©rence | CatΓ©gorie | Prix unit. | QtΓ© | Total | Entretien | |
'+
parts.map(p=>''+
''+(p.photo?' ':'--')+' | '+
''+escHtml(p.name)+' | '+
- ''+(p.brand||'--')+' | '+
- ''+(p.reference||'--')+' | '+
- ''+p.category+' | '+
+ ''+(p.brand?escHtml(p.brand):'--')+' | '+
+ ''+(p.reference?escHtml(p.reference):'--')+' | '+
+ ''+escHtml(p.category)+' | '+
''+fmtPrice(p.price)+' | '+
- ''+p.quantity+' '+p.unit+' | '+
+ ''+p.quantity+' '+escHtml(p.unit||'pièce')+' | '+
''+fmtPrice(parseFloat(p.price||0)*parseInt(p.quantity||1))+' | '+
- ''+(p.maintenance_type?p.maintenance_type+' ('+fmtDate(p.maintenance_date)+')':'--')+' | '+
- ' | '+
+ ''+(p.maintenance_type?escHtml(p.maintenance_type)+' ('+fmtDate(p.maintenance_date)+')':'--')+' | '+
+ ''+
+ ' '+
+ ''+
+ ' | '+
'
').join('')+
'
';
}
@@ -239,28 +401,79 @@ async function loadParts(){
}catch(e){toast(e.message,'error');}
}
-// MODALS
+// βββ ALL MAINTENANCES βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+async function loadAllMaintenances(){
+ try{
+ const vehicles=await api('vehicles');
+ const el=$('#all-maintenances-list');
+ if(!vehicles.length){el.innerHTML='';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+=''+
+ ''+
+ '
| Date | Type | Description | Km | Coût M.O. | Pièces | Total | Mécanicien | |
'+
+ mlist.map(m=>''+
+ '| '+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):'--')+' | '+
+ ''+
+ ''+
+ ' | '+
+ '
').join('')+
+ '
';
+ }
+ $('#all-maint-count').textContent=countAll;
+ $('#all-maint-total').textContent=fmtPrice(totalAll);
+ el.innerHTML=html||'';
+ }catch(e){console.error(e);}
+}
+
+// βββ MODALS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function openModal(id){document.getElementById(id).classList.add('show');}
function closeModal(id){document.getElementById(id).classList.remove('show');}
-// VEHICLE CRUD
+// βββ 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\u00e9hicule';
+ $('#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\u00e9hicule';
+ $('#modal-vehicle-title').textContent='Modifier le vΓ©hicule';
['name','brand','model','year','license_plate','vin','fuel_type','color','purchase_date','purchase_price','current_km','notes'].forEach(f=>{
const el=$('#vehicle-'+f.replace(/_/g,'-'));
- if(el)el.value=v[f]||'';
+ if(el)el.value=v[f]??'';
});
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;
@@ -271,10 +484,10 @@ async function saveVehicle(){
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\u00e9hicule modifi\u00e9');
+ toast('VΓ©hicule modifiΓ©');
}else{
await api('vehicles','POST',fd);
- toast('V\u00e9hicule ajout\u00e9');
+ toast('VΓ©hicule ajoutΓ©');
}
closeModal('modal-vehicle');
if(currentPage==='vehicles')loadVehicles();
@@ -283,11 +496,11 @@ async function saveVehicle(){
}catch(e){toast(e.message,'error');}
}
async function deleteVehicle(id){
- if(!confirm('Supprimer ce v\u00e9hicule et tout son historique ?'))return;
- try{await api('vehicles','DELETE',null,'&id='+id);toast('V\u00e9hicule supprim\u00e9');navigate('vehicles');}catch(e){toast(e.message,'error');}
+ 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
+// βββ MAINTENANCE CRUD βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function openAddMaintenance(){
$('#form-maintenance').reset();$('#form-maintenance-id').value='';
$('#maintenance-vehicle-id').value=currentVehicleId||'';
@@ -295,60 +508,101 @@ function openAddMaintenance(){
$('#maintenance-date').value=new Date().toISOString().slice(0,10);
openModal('modal-maintenance');
}
+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";
+ const map={type:'maintenance-type',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\u00e9');}
- else{await api('maintenances','POST',d);toast('Entretien ajout\u00e9');}
+ if(id){await api('maintenances','PUT',d,'&id='+id);toast('Entretien modifiΓ©');}
+ else{await api('maintenances','POST',d);toast('Entretien ajoutΓ©');}
closeModal('modal-maintenance');
- loadVehicleDetail(currentVehicleId);
+ 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\u00e9');loadVehicleDetail(currentVehicleId);}catch(e){toast(e.message,'error');}
+ 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
+// βββ PARTS CRUD βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function openAddPart(){
$('#form-part').reset();$('#form-part-id').value='';
$('#part-vehicle-id').value=currentVehicleId||'';
- $('#modal-part-title').textContent='Ajouter une pi\u00e8ce';
- $('#part-purchase-date').value=new Date().toISOString().slice(0,10);
+ $('#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',category:'part-category',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]??'';}
+ 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\u00e8ce modifi\u00e9e');
+ toast('Pièce modifiée');
}else{
await api('parts','POST',fd);
- toast('Pi\u00e8ce ajout\u00e9e');
+ toast('Pièce ajoutée');
}
closeModal('modal-part');
- if(currentPage==='vehicle')loadVehicleDetail(currentVehicleId);
+ 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\u00e8ce ?'))return;
+ if(!confirm('Supprimer cette pièce ?'))return;
try{
await api('parts','DELETE',null,'&id='+id);
- toast('Pi\u00e8ce supprim\u00e9e');
- if(currentPage==='vehicle')loadVehicleDetail(currentVehicleId);
+ 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
+// βββ UPLOAD PHOTO βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function openUploadPhoto(id){$('#upload-vehicle-id').value=id;openModal('modal-upload-photo');}
async function doUploadPhoto(){
const id=$('#upload-vehicle-id').value;
@@ -357,12 +611,12 @@ async function doUploadPhoto(){
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 \u00e0 jour');closeModal('modal-upload-photo');
+ toast('Photo mise Γ jour');closeModal('modal-upload-photo');
loadVehicleDetail(id);
}catch(e){toast(e.message,'error');}
}
-// TABS
+// βββ TABS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function switchTab(tab){
$$('.tab-btn').forEach(b=>b.classList.remove('active'));
$$('.tab-pane').forEach(p=>p.classList.remove('active'));
@@ -370,7 +624,7 @@ function switchTab(tab){
document.getElementById('tab-'+tab)?.classList.add('active');
}
-// PHOTO PREVIEW
+// βββ PHOTO PREVIEW ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function previewPhoto(inputId,previewId){
const file=document.getElementById(inputId).files[0];if(!file)return;
const reader=new FileReader();
@@ -378,49 +632,7 @@ function previewPhoto(inputId,previewId){
reader.readAsDataURL(file);
}
-// ALL MAINTENANCES
-async function loadAllMaintenances(){
- try{
- // RΓ©cupΓ©rer tous les vΓ©hicules puis leurs maintenances
- const vehicles=await api('vehicles');
- const el=$('#all-maintenances-list');
- if(!vehicles.length){el.innerHTML='';return;}
- let totalAll=0, countAll=0;
- let 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+=''+
- ''+
- '
| Date | Type | Description | Km | Coût M.O. | Pièces | Total | Mécanicien | |
'+
- mlist.map(m=>''+
- '| '+fmtDate(m.date)+' | '+
- ''+m.type+' | '+
- ''+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||'--')+' | '+
- ' | '+
- '
').join('')+
- '
';
- }
- $('#all-maint-count').textContent=countAll;
- $('#all-maint-total').textContent=fmtPrice(totalAll);
- el.innerHTML=html||'';
- }catch(e){console.error(e);}
-}
-
-function escHtml(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML;}
-
-// INIT
+// βββ 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');}));