fix: garage — photos (limite 20MB), entretiens globaux, pièces groupées par véhicule
Deploy to Pacha Family / deploy (push) Failing after 3s

- Dockerfile + docker/php.ini : upload_max_filesize 2M → 20M
- garage.php : onglet Entretiens dans sous-nav + page #page-maintenances-all
- garage.js : loadAllMaintenances() (entretiens groupés par véhicule)
- garage.js : loadParts() redesigné — pièces groupées par véhicule
- garage.js : navigate() gère 'maintenances-all'
- DB : vehicle_id des pièces mis à jour via maintenance

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
perco
2026-05-11 16:33:49 +02:00
co-authored by Claude Sonnet 4.6
parent 52c8e82221
commit a443ec389b
4 changed files with 92 additions and 16 deletions
+1
View File
@@ -11,3 +11,4 @@ COPY . /var/www/html/
RUN chown -R www-data:www-data /var/www/html RUN chown -R www-data:www-data /var/www/html
COPY docker/apache.conf /etc/apache2/sites-available/000-default.conf COPY docker/apache.conf /etc/apache2/sites-available/000-default.conf
COPY docker/php.ini /usr/local/etc/php/conf.d/uploads.ini
+3
View File
@@ -0,0 +1,3 @@
upload_max_filesize = 20M
post_max_size = 25M
memory_limit = 256M
+14 -1
View File
@@ -14,11 +14,24 @@ require __DIR__ . '/header.php';
<!-- Sous-navigation interne Garage --> <!-- Sous-navigation interne Garage -->
<nav class="garage-subnav"> <nav class="garage-subnav">
<button class="nav-link active" data-page="dashboard">📊 <?= tr('garage_stat_vehicles') !== 'Véhicules' ? 'Dashboard' : 'Dashboard' ?></button> <button class="nav-link active" data-page="dashboard">📊 Dashboard</button>
<button class="nav-link" data-page="vehicles">🚗 <?= tr('garage_vehicles_title') ?></button> <button class="nav-link" data-page="vehicles">🚗 <?= tr('garage_vehicles_title') ?></button>
<button class="nav-link" data-page="maintenances-all">🔧 <?= tr('garage_maintenances') ?></button>
<button class="nav-link" data-page="parts">🔩 <?= tr('garage_all_parts') ?></button> <button class="nav-link" data-page="parts">🔩 <?= tr('garage_all_parts') ?></button>
</nav> </nav>
<div id="page-maintenances-all" class="page">
<div class="container">
<div class="card-header" style="margin-bottom:1rem">
<div>
<h1 style="font-size:1.2rem;font-weight:700">🔧 <?= tr('garage_maintenances') ?></h1>
<p style="color:var(--muted);font-size:.85rem"><span id="all-maint-count">0</span> <?= tr('garage_maintenances') ?> · Total : <span id="all-maint-total">--</span></p>
</div>
</div>
<div id="all-maintenances-list"></div>
</div>
</div>
<div id="page-dashboard" class="page active"> <div id="page-dashboard" class="page active">
<div class="container"> <div class="container">
<div class="stats-grid"> <div class="stats-grid">
+74 -15
View File
@@ -26,6 +26,7 @@ function navigate(page,params={}){
else if(page==='vehicles')loadVehicles(); else if(page==='vehicles')loadVehicles();
else if(page==='vehicle')loadVehicleDetail(params.id); else if(page==='vehicle')loadVehicleDetail(params.id);
else if(page==='parts')loadParts(); else if(page==='parts')loadParts();
else if(page==='maintenances-all')loadAllMaintenances();
} }
// API // API
@@ -204,21 +205,37 @@ async function loadParts(){
$('#all-parts-total').textContent=fmtPrice(total); $('#all-parts-total').textContent=fmtPrice(total);
$('#all-parts-count').textContent=list.length; $('#all-parts-count').textContent=list.length;
if(!list.length){el.innerHTML='<div class="empty-state"><div class="icon">\ud83d\udd29</div><p>Aucune pi\u00e8ce</p></div>';return;} if(!list.length){el.innerHTML='<div class="empty-state"><div class="icon">\ud83d\udd29</div><p>Aucune pi\u00e8ce</p></div>';return;}
el.innerHTML='<div class="table-wrap"><table><thead><tr><th>Photo</th><th>Nom</th><th>V\u00e9hicule</th><th>Marque</th><th>R\u00e9f\u00e9rence</th><th>Cat\u00e9gorie</th><th>Prix unit.</th><th>Qt\u00e9</th><th>Total</th><th>Entretien</th><th></th></tr></thead><tbody>'+ // Grouper par v\u00e9hicule
list.map(p=>'<tr>'+ const byVehicle={};
'<td>'+(p.photo?'<img class="part-thumb" src="'+UPLOADS+p.photo+'" alt="">':'--')+'</td>'+ list.forEach(p=>{
'<td><strong>'+p.name+'</strong></td>'+ const key=p.vehicle_name||'Sans v\u00e9hicule';
'<td>'+(p.vehicle_name?'<span class="badge badge-blue">'+p.vehicle_name+'</span>':'--')+'</td>'+ if(!byVehicle[key])byVehicle[key]=[];
'<td>'+(p.brand||'--')+'</td>'+ byVehicle[key].push(p);
'<td><code style="font-size:.75rem;color:var(--muted)">'+(p.reference||'--')+'</code></td>'+ });
'<td><span class="badge badge-gray">'+p.category+'</span></td>'+ let html='';
'<td>'+fmtPrice(p.price)+'</td>'+ for(const[vname,parts] of Object.entries(byVehicle)){
'<td>'+p.quantity+' '+p.unit+'</td>'+ const vtotal=parts.reduce((s,p)=>s+parseFloat(p.price||0)*parseInt(p.quantity||1),0);
'<td><strong>'+fmtPrice(parseFloat(p.price||0)*parseInt(p.quantity||1))+'</strong></td>'+ html+='<div style="margin-bottom:2rem">'+
'<td>'+(p.maintenance_type?p.maintenance_type+' ('+fmtDate(p.maintenance_date)+')':'--')+'</td>'+ '<div class="card-header" style="margin-bottom:.5rem">'+
'<td><button class="btn btn-danger btn-sm btn-icon" onclick="deletePart('+p.id+')" title="Supprimer">\ud83d\uddd1\ufe0f</button></td>'+ '<h3 style="font-size:1rem;font-weight:700">\ud83d\ude97 '+escHtml(vname)+'</h3>'+
'</tr>').join('')+ '<span style="margin-left:auto;color:var(--muted);font-size:.85rem">'+parts.length+' pi\u00e8ces \u00b7 '+fmtPrice(vtotal)+'</span>'+
'</tbody></table></div>'; '</div>'+
'<div class="table-wrap"><table><thead><tr><th>Photo</th><th>Nom</th><th>Marque</th><th>R\u00e9f\u00e9rence</th><th>Cat\u00e9gorie</th><th>Prix unit.</th><th>Qt\u00e9</th><th>Total</th><th>Entretien</th><th></th></tr></thead><tbody>'+
parts.map(p=>'<tr>'+
'<td>'+(p.photo?'<img class="part-thumb" src="'+UPLOADS+p.photo+'" alt="">':'--')+'</td>'+
'<td><strong>'+escHtml(p.name)+'</strong></td>'+
'<td>'+(p.brand||'--')+'</td>'+
'<td><code style="font-size:.75rem;color:var(--muted)">'+(p.reference||'--')+'</code></td>'+
'<td><span class="badge badge-gray">'+p.category+'</span></td>'+
'<td>'+fmtPrice(p.price)+'</td>'+
'<td>'+p.quantity+' '+p.unit+'</td>'+
'<td><strong>'+fmtPrice(parseFloat(p.price||0)*parseInt(p.quantity||1))+'</strong></td>'+
'<td>'+(p.maintenance_type?p.maintenance_type+' ('+fmtDate(p.maintenance_date)+')':'--')+'</td>'+
'<td><button class="btn btn-danger btn-sm btn-icon" onclick="deletePart('+p.id+')" title="Supprimer">\ud83d\uddd1\ufe0f</button></td>'+
'</tr>').join('')+
'</tbody></table></div></div>';
}
el.innerHTML=html;
}catch(e){toast(e.message,'error');} }catch(e){toast(e.message,'error');}
} }
@@ -361,6 +378,48 @@ function previewPhoto(inputId,previewId){
reader.readAsDataURL(file); 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='<div class="empty-state"><div class="icon">🔧</div><p>Aucun véhicule</p></div>';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+='<div style="margin-bottom:2rem">'+
'<div class="card-header" style="margin-bottom:.5rem">'+
'<h3 style="font-size:1rem;font-weight:700">🚗 '+v.name+'</h3>'+
(v.license_plate?'<span class="plate">'+v.license_plate+'</span>':'')+
'<span style="margin-left:auto;color:var(--muted);font-size:.85rem">'+mlist.length+' entretiens · '+fmtPrice(total)+'</span>'+
'</div>'+
'<div class="table-wrap"><table><thead><tr><th>Date</th><th>Type</th><th>Description</th><th>Km</th><th>Coût M.O.</th><th>Pièces</th><th>Total</th><th>Mécanicien</th><th></th></tr></thead><tbody>'+
mlist.map(m=>'<tr>'+
'<td>'+fmtDate(m.date)+'</td>'+
'<td><span class="badge badge-gray">'+m.type+'</span></td>'+
'<td>'+escHtml(m.description||'--')+'</td>'+
'<td>'+(m.km?fmt(m.km)+' km':'--')+'</td>'+
'<td>'+fmtPrice(m.cost)+'</td>'+
'<td>'+(m.parts_count>0?m.parts_count+' ('+fmtPrice(m.parts_cost)+')':'--')+'</td>'+
'<td><strong>'+fmtPrice(parseFloat(m.cost||0)+parseFloat(m.parts_cost||0))+'</strong></td>'+
'<td>'+(m.mechanic||'--')+'</td>'+
'<td><button class="btn btn-danger btn-sm btn-icon" onclick="deleteMaintenance('+m.id+')" title="Supprimer">🗑️</button></td>'+
'</tr>').join('')+
'</tbody></table></div></div>';
}
$('#all-maint-count').textContent=countAll;
$('#all-maint-total').textContent=fmtPrice(totalAll);
el.innerHTML=html||'<div class="empty-state"><div class="icon">🔧</div><p>Aucun entretien</p></div>';
}catch(e){console.error(e);}
}
function escHtml(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML;}
// INIT // INIT
document.addEventListener('DOMContentLoaded',()=>{ document.addEventListener('DOMContentLoaded',()=>{
$$('.nav-link').forEach(n=>n.addEventListener('click',()=>navigate(n.dataset.page))); $$('.nav-link').forEach(n=>n.addEventListener('click',()=>navigate(n.dataset.page)));