@@ -268,7 +268,8 @@ textarea.form-control { resize: vertical; min-height: 80px; }
|
||||
/* ─── TOASTS ─────────────────────────────────────────────────────────────────── */
|
||||
.toast-container {
|
||||
position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 9999;
|
||||
display: flex; flex-direction: column; gap: .5rem; pointer-events: none;
|
||||
display: flex; flex-direction: column; align-items: flex-end;
|
||||
gap: .5rem; pointer-events: none;
|
||||
}
|
||||
.toast {
|
||||
background: var(--bg-panel); border: 1px solid var(--border-light);
|
||||
@@ -303,18 +304,169 @@ textarea.form-control { resize: vertical; min-height: 80px; }
|
||||
.item-brand { font-size: .75rem; color: var(--text-muted); }
|
||||
.item-price { font-size: .875rem; font-weight: 600; color: var(--primary); white-space: nowrap; }
|
||||
|
||||
/* ─── DASHBOARD LAYOUT ─────────────────────────────────────────────────────── */
|
||||
.garage-dash-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr minmax(260px, 380px);
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
.garage-dash-main { min-width: 0; }
|
||||
.garage-dash-aside { padding: 1rem 1.25rem; }
|
||||
.garage-card-header { flex-wrap: wrap; align-items: center; gap: 0.75rem; }
|
||||
|
||||
.garage-tab-actions {
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* ─── TABS (scroll mobile) ─────────────────────────────────────────────────── */
|
||||
.garage-tabs {
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: thin;
|
||||
gap: 0.15rem;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.garage-tabs .tab-btn {
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
padding: 0.55rem 0.75rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
/* ─── VEHICLE HEADER ACTIONS ───────────────────────────────────────────────── */
|
||||
.vehicle-detail-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
align-self: flex-start;
|
||||
}
|
||||
.garage-docs-recap {
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.65rem 0.85rem;
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 8px;
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.garage-docs-recap strong { color: var(--text-main); }
|
||||
.btn-link-sm {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--primary);
|
||||
font-size: inherit;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.btn-link-sm:hover { color: var(--primary-dark); }
|
||||
|
||||
.garage-maint-doc-badge { font-size: 0.65rem; vertical-align: middle; margin-left: 0.15rem; }
|
||||
|
||||
/* ─── DOCUMENTS ───────────────────────────────────────────────────────────── */
|
||||
.garage-docs-card .card-header { padding: 1rem 1.25rem; }
|
||||
.garage-doc-upload-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 0.75rem 1rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
.garage-doc-upload-btn { align-self: flex-end; flex-shrink: 0; }
|
||||
.garage-doc-hint {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
margin: 0.35rem 0 0.75rem;
|
||||
}
|
||||
.garage-doc-empty { padding: 1.25rem !important; }
|
||||
.garage-doc-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.garage-doc-item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem 0.75rem;
|
||||
padding: 0.65rem 0.85rem;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
background: #fff;
|
||||
}
|
||||
.garage-doc-item:last-child { border-bottom: none; }
|
||||
.garage-doc-link {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-weight: 500;
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
word-break: break-word;
|
||||
}
|
||||
.garage-doc-link:hover { text-decoration: underline; }
|
||||
.garage-doc-meta {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.garage-doc-item .btn-icon { flex-shrink: 0; margin-left: auto; }
|
||||
|
||||
/* ─── RESPONSIVE ─────────────────────────────────────────────────────────────── */
|
||||
@media (max-width: 768px) {
|
||||
.container { padding: 1rem; }
|
||||
.vehicle-detail-header { flex-direction: column; }
|
||||
.vehicle-detail-img { width: 100%; height: 200px; }
|
||||
.vehicle-detail-actions {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
}
|
||||
.vehicle-detail-actions .btn { width: 100%; justify-content: center; }
|
||||
.form-row, .form-row-3 { grid-template-columns: 1fr; }
|
||||
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.modal { max-height: 95vh; }
|
||||
.vehicles-grid { grid-template-columns: 1fr; }
|
||||
.meta-grid { gap: .75rem; }
|
||||
.meta-grid { gap: 0.75rem; }
|
||||
.garage-subnav { padding: 8px 12px; overflow-x: auto; flex-wrap: nowrap; }
|
||||
.garage-subnav .nav-link { white-space: nowrap; }
|
||||
.garage-dash-layout { grid-template-columns: 1fr; }
|
||||
.garage-dash-aside { order: -1; }
|
||||
.garage-doc-upload-bar { flex-direction: column; align-items: stretch; }
|
||||
.garage-doc-upload-btn { width: 100%; align-self: stretch; }
|
||||
.garage-doc-item { flex-direction: column; align-items: stretch; }
|
||||
.garage-doc-item .btn-icon { margin-left: 0; align-self: flex-end; }
|
||||
.garage-doc-meta { white-space: normal; }
|
||||
.toast-container {
|
||||
left: 0.75rem;
|
||||
right: 0.75rem;
|
||||
bottom: max(1rem, env(safe-area-inset-bottom, 0px));
|
||||
align-items: stretch;
|
||||
}
|
||||
.toast { max-width: none; min-width: 0; width: 100%; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.table-wrap {
|
||||
margin-left: -0.25rem;
|
||||
margin-right: -0.25rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.table-wrap table { min-width: 640px; }
|
||||
#inline-part-form .form-row,
|
||||
#inline-part-form .form-row-3 { grid-template-columns: 1fr !important; }
|
||||
}
|
||||
|
||||
/* ══ DARK MODE — Garage module ══════════════════════════════════ */
|
||||
@@ -364,3 +516,10 @@ textarea.form-control { resize: vertical; min-height: 80px; }
|
||||
[data-theme="dark"] .scroll-list-item:hover { background: rgba(255,255,255,.04); }
|
||||
[data-theme="dark"] .item-name { color: var(--text-main); }
|
||||
[data-theme="dark"] #inline-part-form { background: #1c2128 !important; border-color: var(--border-light) !important; }
|
||||
[data-theme="dark"] .garage-docs-recap { background: rgba(255,255,255,.04); border-color: var(--border-light); color: var(--text-muted); }
|
||||
[data-theme="dark"] .garage-docs-recap strong { color: var(--text-main); }
|
||||
[data-theme="dark"] .btn-link-sm { color: var(--primary); }
|
||||
[data-theme="dark"] .garage-doc-list { border-color: var(--border-light); }
|
||||
[data-theme="dark"] .garage-doc-item { background: #1c2128; border-color: rgba(48,54,61,.5); }
|
||||
[data-theme="dark"] .garage-doc-link { color: #93c5fd; }
|
||||
[data-theme="dark"] .garage-doc-meta { color: var(--text-muted); }
|
||||
|
||||
@@ -7,6 +7,12 @@ 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'){
|
||||
@@ -25,7 +31,7 @@ function navigate(page,params={}){
|
||||
$$('.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;
|
||||
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();
|
||||
@@ -127,6 +133,8 @@ async function loadVehicleDetail(id){
|
||||
renderVehicleHeader(v);
|
||||
renderMaintenances(maintenances);
|
||||
renderVehicleParts(parts);
|
||||
setVehicleDocTabBadge(v);
|
||||
await loadVehicleDocuments();
|
||||
document.title='GarageManager · '+v.name;
|
||||
}catch(e){toast(e.message,'error');}
|
||||
}
|
||||
@@ -152,8 +160,13 @@ function renderVehicleHeader(v){
|
||||
(v.purchase_date?'<div class="meta-item"><span class="k">Achat</span><br><span class="v">'+fmtDate(v.purchase_date)+'</span></div>':'')+
|
||||
(v.vin?'<div class="meta-item"><span class="k">VIN</span><br><span class="v" style="font-size:.75rem;font-family:monospace">'+escHtml(v.vin)+'</span></div>':'')+
|
||||
'</div>'+
|
||||
'<div class="garage-docs-recap">'+
|
||||
'📎 <strong>'+(Number(v.vehicle_docs_count)||0)+'</strong> '+(Number(v.vehicle_docs_count)===1?'document':'documents')+' véhicule · '+
|
||||
'<strong>'+(Number(v.maint_docs_count)||0)+'</strong> sur entretiens (factures, etc.) · '+
|
||||
'<button type="button" class="btn-link-sm" onclick="switchTab(\'documents\')">Voir les documents véhicule</button>'+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
'<div style="display:flex;gap:.5rem;flex-wrap:wrap;align-self:flex-start">'+
|
||||
'<div class="vehicle-detail-actions">'+
|
||||
'<button class="btn btn-secondary btn-sm" onclick="openEditVehicle('+v.id+')">✏️ Modifier</button>'+
|
||||
'<button class="btn btn-secondary btn-sm" onclick="openUploadPhoto('+v.id+')">📷 Photo</button>'+
|
||||
'<button class="btn btn-danger btn-sm" onclick="deleteVehicle('+v.id+')">🗑️ Supprimer</button>'+
|
||||
@@ -168,7 +181,9 @@ function renderMaintenances(list){
|
||||
if(!list.length){el.innerHTML='<div class="empty-state"><div class="icon">🔧</div><p>Aucun entretien enregistré</p></div>';return;}
|
||||
el.innerHTML='<div class="table-wrap"><table><thead><tr><th>Date</th><th>Type</th><th>Description</th><th>Kilométrage</th><th>Coût MO</th><th>Pièces</th><th>Prochain</th><th></th></tr></thead><tbody>'+
|
||||
list.map(m=>'<tr style="cursor:pointer" onclick="navigate(\'maintenance\',{id:'+m.id+'})">'+
|
||||
'<td><strong>'+fmtDate(m.date)+'</strong><br><span style="color:var(--muted);font-size:.75rem">'+ago(m.date)+'</span></td>'+
|
||||
'<td><strong>'+fmtDate(m.date)+'</strong><br><span style="color:var(--muted);font-size:.75rem">'+ago(m.date)+'</span>'+
|
||||
(m.documents_count>0?' <span class="badge badge-gray garage-maint-doc-badge" title="Documents / factures">📎 '+m.documents_count+'</span>':'')+
|
||||
'</td>'+
|
||||
'<td><span class="badge badge-blue">'+escHtml(m.type)+'</span></td>'+
|
||||
'<td style="max-width:200px;color:var(--muted)">'+(m.description?escHtml(m.description):'--')+'</td>'+
|
||||
'<td>'+(m.km?fmt(m.km)+' km':'--')+'</td>'+
|
||||
@@ -218,6 +233,7 @@ async function loadMaintenanceDetail(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';
|
||||
@@ -231,6 +247,7 @@ function renderMaintenanceDetailHeader(m){
|
||||
'<div style="display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;margin-bottom:.5rem">'+
|
||||
'<span class="badge badge-blue" style="font-size:.95rem;padding:.35rem .8rem">'+escHtml(m.type)+'</span>'+
|
||||
'<span style="color:var(--muted)">'+fmtDate(m.date)+' · '+ago(m.date)+'</span>'+
|
||||
(m.documents_count>0?'<span class="badge badge-gray" title="Documents attachés à cette révision">📎 '+m.documents_count+' fichier'+(m.documents_count>1?'s':'')+'</span>':'')+
|
||||
(m.vehicle_name?'<span style="color:var(--muted);font-size:.85rem">🚗 <a href="#" onclick="navigate(\'vehicle\',{id:'+m.vehicle_id+'});return false" style="color:inherit;text-decoration:underline">'+escHtml(m.vehicle_name)+'</a></span>':'')+
|
||||
'</div>'+
|
||||
(m.description?'<p style="margin-bottom:.75rem">'+escHtml(m.description)+'</p>':'')+
|
||||
@@ -244,7 +261,7 @@ function renderMaintenanceDetailHeader(m){
|
||||
'</div>'+
|
||||
(m.notes?'<p style="margin-top:.75rem;color:var(--muted);font-size:.85rem;font-style:italic">'+escHtml(m.notes)+'</p>':'')+
|
||||
'</div>'+
|
||||
'<div style="display:flex;gap:.5rem;flex-wrap:wrap;align-self:flex-start">'+
|
||||
'<div class="vehicle-detail-actions">'+
|
||||
'<button class="btn btn-secondary btn-sm" onclick="openEditMaintenance('+m.id+')">✏️ Modifier</button>'+
|
||||
'<button class="btn btn-danger btn-sm" onclick="deleteMaintenance('+m.id+')">🗑️ Supprimer</button>'+
|
||||
'</div>'+
|
||||
@@ -635,12 +652,120 @@ async function doUploadPhoto(){
|
||||
}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='<div class="empty-state garage-doc-empty"><p>Aucun document pour l’instant</p></div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML='<ul class="garage-doc-list">'+
|
||||
list.map(d=>{
|
||||
const href=API+'?action=garage_document&id='+encodeURIComponent(d.id);
|
||||
const title=escHtml(d.label||d.original_name||d.filename||'Document');
|
||||
const meta=fmtDate(d.created_at)+' · '+formatBytes(d.size||0);
|
||||
return '<li class="garage-doc-item">'+
|
||||
'<a class="garage-doc-link" href="'+href+'" target="_blank" rel="noopener noreferrer">📄 '+title+'</a>'+
|
||||
'<span class="garage-doc-meta">'+meta+'</span>'+
|
||||
'<button type="button" class="btn btn-danger btn-sm btn-icon" onclick="deleteGarageDocument('+parseInt(d.id,10)+')" title="Supprimer">🗑️</button>'+
|
||||
'</li>';
|
||||
}).join('')+
|
||||
'</ul>';
|
||||
}
|
||||
|
||||
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 ────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user