fix: garage — photos (limite 20MB), entretiens globaux, pièces groupées par véhicule
Deploy to Pacha Family / deploy (push) Failing after 3s
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:
co-authored by
Claude Sonnet 4.6
parent
52c8e82221
commit
a443ec389b
@@ -11,3 +11,4 @@ COPY . /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/php.ini /usr/local/etc/php/conf.d/uploads.ini
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
upload_max_filesize = 20M
|
||||
post_max_size = 25M
|
||||
memory_limit = 256M
|
||||
+14
-1
@@ -14,11 +14,24 @@ require __DIR__ . '/header.php';
|
||||
|
||||
<!-- Sous-navigation interne Garage -->
|
||||
<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="maintenances-all">🔧 <?= tr('garage_maintenances') ?></button>
|
||||
<button class="nav-link" data-page="parts">🔩 <?= tr('garage_all_parts') ?></button>
|
||||
</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 class="container">
|
||||
<div class="stats-grid">
|
||||
|
||||
@@ -26,6 +26,7 @@ function navigate(page,params={}){
|
||||
else if(page==='vehicles')loadVehicles();
|
||||
else if(page==='vehicle')loadVehicleDetail(params.id);
|
||||
else if(page==='parts')loadParts();
|
||||
else if(page==='maintenances-all')loadAllMaintenances();
|
||||
}
|
||||
|
||||
// API
|
||||
@@ -204,11 +205,25 @@ async function loadParts(){
|
||||
$('#all-parts-total').textContent=fmtPrice(total);
|
||||
$('#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;}
|
||||
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>'+
|
||||
list.map(p=>'<tr>'+
|
||||
// Grouper par v\u00e9hicule
|
||||
const byVehicle={};
|
||||
list.forEach(p=>{
|
||||
const key=p.vehicle_name||'Sans v\u00e9hicule';
|
||||
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+='<div style="margin-bottom:2rem">'+
|
||||
'<div class="card-header" style="margin-bottom:.5rem">'+
|
||||
'<h3 style="font-size:1rem;font-weight:700">\ud83d\ude97 '+escHtml(vname)+'</h3>'+
|
||||
'<span style="margin-left:auto;color:var(--muted);font-size:.85rem">'+parts.length+' pi\u00e8ces \u00b7 '+fmtPrice(vtotal)+'</span>'+
|
||||
'</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>'+p.name+'</strong></td>'+
|
||||
'<td>'+(p.vehicle_name?'<span class="badge badge-blue">'+p.vehicle_name+'</span>':'--')+'</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>'+
|
||||
@@ -218,7 +233,9 @@ async function loadParts(){
|
||||
'<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>';
|
||||
'</tbody></table></div></div>';
|
||||
}
|
||||
el.innerHTML=html;
|
||||
}catch(e){toast(e.message,'error');}
|
||||
}
|
||||
|
||||
@@ -361,6 +378,48 @@ 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='<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
|
||||
document.addEventListener('DOMContentLoaded',()=>{
|
||||
$$('.nav-link').forEach(n=>n.addEventListener('click',()=>navigate(n.dataset.page)));
|
||||
|
||||
Reference in New Issue
Block a user