diff --git a/docker/schema_family.sql b/docker/schema_family.sql index 7fe0d17..713d1a4 100644 --- a/docker/schema_family.sql +++ b/docker/schema_family.sql @@ -294,6 +294,22 @@ CREATE TABLE IF NOT EXISTS pf_parts ( FOREIGN KEY (maintenance_id) REFERENCES pf_maintenances(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE IF NOT EXISTS pf_garage_documents ( + id INT AUTO_INCREMENT PRIMARY KEY, + vehicle_id INT NOT NULL, + maintenance_id INT DEFAULT NULL, + label VARCHAR(255) DEFAULT NULL, + filename VARCHAR(255) NOT NULL, + original_name VARCHAR(255) DEFAULT NULL, + mime VARCHAR(120) DEFAULT NULL, + size INT DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + KEY idx_gd_vehicle (vehicle_id), + KEY idx_gd_maint (maintenance_id), + FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE CASCADE, + FOREIGN KEY (maintenance_id) REFERENCES pf_maintenances(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + -- ─── Notes / Memo ───────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS pf_memo_notes ( id INT AUTO_INCREMENT PRIMARY KEY, diff --git a/garage.php b/garage.php index c2e3ca3..7a5b75e 100644 --- a/garage.php +++ b/garage.php @@ -28,15 +28,15 @@ require __DIR__ . '/header.php';
PDF, images, Office… · max 12 Mo
+ +PDF, images… · max 12 Mo
+ +0 entretiens · Total : --
@@ -140,7 +178,7 @@ require __DIR__ . '/header.php';0 pièces · Total : --
diff --git a/modules/garage/api.php b/modules/garage/api.php index 5c789e4..3c8fdaf 100644 --- a/modules/garage/api.php +++ b/modules/garage/api.php @@ -17,6 +17,24 @@ require_once dirname(__DIR__, 2) . '/includes/db.php'; $UPLOAD_DIR = '/uploads/garage/'; if (!is_dir($UPLOAD_DIR)) { @mkdir($UPLOAD_DIR, 0755, true); } +$pdo->exec(" +CREATE TABLE IF NOT EXISTS pf_garage_documents ( + id INT AUTO_INCREMENT PRIMARY KEY, + vehicle_id INT NOT NULL, + maintenance_id INT DEFAULT NULL, + label VARCHAR(255) DEFAULT NULL, + filename VARCHAR(255) NOT NULL, + original_name VARCHAR(255) DEFAULT NULL, + mime VARCHAR(120) DEFAULT NULL, + size INT DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + KEY idx_gd_vehicle (vehicle_id), + KEY idx_gd_maint (maintenance_id), + FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE CASCADE, + FOREIGN KEY (maintenance_id) REFERENCES pf_maintenances(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 +"); + $method = $_SERVER['REQUEST_METHOD']; $action = $_GET['action'] ?? ''; @@ -38,6 +56,37 @@ function handleUpload(string $field, string $dir): ?string { return null; } +function handleGarageDocumentUpload(string $field, string $dir): ?array { + if (!isset($_FILES[$field]) || $_FILES[$field]['error'] !== UPLOAD_ERR_OK) { + return null; + } + if ($_FILES[$field]['size'] > 12 * 1024 * 1024) { + gErr('Fichier trop volumineux (max 12 Mo).', 400); + } + $ext = strtolower(pathinfo($_FILES[$field]['name'], PATHINFO_EXTENSION)); + $allowed = ['pdf','jpg','jpeg','png','gif','webp','doc','docx','xls','xlsx','odt','txt']; + if (!in_array($ext, $allowed, true)) { + gErr('Format non autorisé.', 400); + } + $fname = 'd_' . uniqid('', true) . '.' . $ext; + if (!move_uploaded_file($_FILES[$field]['tmp_name'], $dir . $fname)) { + return null; + } + $mimeMap = [ + 'pdf' => 'application/pdf', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', + 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp', + 'doc' => 'application/msword', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'xls' => 'application/vnd.ms-excel', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'odt' => 'application/vnd.oasis.opendocument.text', 'txt' => 'text/plain', + ]; + return [ + 'filename' => $fname, + 'original_name' => $_FILES[$field]['name'], + 'mime' => $mimeMap[$ext] ?? 'application/octet-stream', + 'size' => (int) $_FILES[$field]['size'], + ]; +} + // ─── Vehicles ───────────────────────────────────────────────────────────────── if ($action === 'vehicles') { if ($method === 'GET') { @@ -47,6 +96,12 @@ if ($action === 'vehicles') { $vehicle = $v->fetch(); if (!$vehicle) gErr('Véhicule introuvable', 404); $s = $pdo->prepare("SELECT COUNT(*) as cnt, COALESCE(SUM(cost),0) as total FROM pf_maintenances WHERE vehicle_id = ?"); $s->execute([$id]); $vehicle['stats'] = $s->fetch(); $p = $pdo->prepare("SELECT COUNT(*) as cnt, COALESCE(SUM(price*quantity),0) as total FROM pf_parts WHERE vehicle_id = ?"); $p->execute([$id]); $vehicle['parts_stats'] = $p->fetch(); + $dc = $pdo->prepare("SELECT COUNT(*) FROM pf_garage_documents WHERE vehicle_id = ? AND maintenance_id IS NULL"); + $dc->execute([$id]); + $vehicle['vehicle_docs_count'] = (int) $dc->fetchColumn(); + $dm = $pdo->prepare("SELECT COUNT(*) FROM pf_garage_documents WHERE vehicle_id = ? AND maintenance_id IS NOT NULL"); + $dm->execute([$id]); + $vehicle['maint_docs_count'] = (int) $dm->fetchColumn(); gOk($vehicle); } $stmt = $pdo->query("SELECT v.*, (SELECT COUNT(*) FROM pf_maintenances m WHERE m.vehicle_id = v.id) as maintenance_count, (SELECT COALESCE(SUM(cost),0) FROM pf_maintenances m WHERE m.vehicle_id = v.id) as total_cost, (SELECT date FROM pf_maintenances m WHERE m.vehicle_id = v.id ORDER BY date DESC LIMIT 1) as last_maintenance FROM pf_vehicles v ORDER BY v.created_at DESC"); @@ -76,11 +131,110 @@ if ($action === 'vehicles') { } if ($method === 'DELETE') { $id = $_GET['id'] ?? null; if (!$id) gErr('ID manquant'); + $docs = $pdo->prepare('SELECT filename FROM pf_garage_documents WHERE vehicle_id = ?'); + $docs->execute([$id]); + foreach ($docs->fetchAll(PDO::FETCH_COLUMN) as $fn) { + if ($fn) { + @unlink($UPLOAD_DIR . basename($fn)); + } + } $v = $pdo->prepare("SELECT photo FROM pf_vehicles WHERE id=?"); $v->execute([$id]); $row = $v->fetch(); if ($row['photo']) @unlink($UPLOAD_DIR . $row['photo']); $pdo->prepare("DELETE FROM pf_vehicles WHERE id = ?")->execute([$id]); gOk(['deleted' => true]); } } +// ─── Documents (véhicule / entretien — factures, PDF, etc.) ─────────────────── +if ($action === 'garage_documents') { + if ($method === 'GET') { + $vid = $_GET['vehicle_id'] ?? null; + $mid = $_GET['maintenance_id'] ?? null; + $vehicleOnly = ($_GET['vehicle_only'] ?? '') === '1'; + if ($mid) { + $stmt = $pdo->prepare("SELECT d.*, v.name as vehicle_name FROM pf_garage_documents d JOIN pf_vehicles v ON v.id = d.vehicle_id WHERE d.maintenance_id = ? ORDER BY d.created_at DESC"); + $stmt->execute([$mid]); + gOk($stmt->fetchAll()); + } + if (!$vid) { + gErr('vehicle_id ou maintenance_id requis'); + } + if ($vehicleOnly) { + $stmt = $pdo->prepare("SELECT * FROM pf_garage_documents WHERE vehicle_id = ? AND maintenance_id IS NULL ORDER BY created_at DESC"); + $stmt->execute([$vid]); + } else { + $stmt = $pdo->prepare("SELECT * FROM pf_garage_documents WHERE vehicle_id = ? ORDER BY created_at DESC"); + $stmt->execute([$vid]); + } + gOk($stmt->fetchAll()); + } + if ($method === 'POST') { + $d = $_POST; + $vid = (int) ($d['vehicle_id'] ?? 0); + if ($vid < 1) { + gErr('vehicle_id manquant'); + } + $mid = isset($d['maintenance_id']) && $d['maintenance_id'] !== '' ? (int) $d['maintenance_id'] : null; + if ($mid) { + $chk = $pdo->prepare("SELECT id FROM pf_maintenances WHERE id = ? AND vehicle_id = ?"); + $chk->execute([$mid, $vid]); + if (!$chk->fetch()) { + gErr('Entretien invalide pour ce véhicule'); + } + } + $up = handleGarageDocumentUpload('file', $UPLOAD_DIR); + if (!$up) { + gErr('Upload échoué ou fichier manquant'); + } + $label = trim($d['label'] ?? ''); + $pdo->prepare("INSERT INTO pf_garage_documents (vehicle_id, maintenance_id, label, filename, original_name, mime, size) VALUES (?,?,?,?,?,?,?)") + ->execute([$vid, $mid, $label ?: null, $up['filename'], $up['original_name'], $up['mime'], $up['size']]); + gOk(['id' => (int) $pdo->lastInsertId()]); + } + if ($method === 'DELETE') { + $id = (int) ($_GET['id'] ?? 0); + if ($id < 1) { + gErr('ID manquant'); + } + $r = $pdo->prepare("SELECT filename FROM pf_garage_documents WHERE id = ?"); + $r->execute([$id]); + $row = $r->fetch(); + if (!$row) { + gErr('Document introuvable', 404); + } + if (!empty($row['filename'])) { + @unlink($UPLOAD_DIR . $row['filename']); + } + $pdo->prepare("DELETE FROM pf_garage_documents WHERE id = ?")->execute([$id]); + gOk(['deleted' => true]); + } +} + +if ($action === 'garage_document') { + $id = (int) ($_GET['id'] ?? 0); + if ($id < 1) { + gErr('ID manquant'); + } + $stmt = $pdo->prepare('SELECT * FROM pf_garage_documents WHERE id = ?'); + $stmt->execute([$id]); + $doc = $stmt->fetch(); + if (!$doc || empty($doc['filename'])) { + http_response_code(404); + exit; + } + $f = basename($doc['filename']); + $path = $UPLOAD_DIR . $f; + if (!preg_match('/^d_[a-zA-Z0-9._-]+$/', $f) || !is_file($path)) { + http_response_code(404); + exit; + } + $mime = $doc['mime'] ?: 'application/octet-stream'; + header('Content-Type: ' . $mime); + $disp = $doc['original_name'] ?: $f; + header('Content-Disposition: inline; filename="' . rawurlencode($disp) . '"'); + header('Cache-Control: private, max-age=3600'); + readfile($path); + exit; +} + // ─── Maintenances ───────────────────────────────────────────────────────────── if ($action === 'maintenances') { $vid = $_GET['vehicle_id'] ?? null; @@ -89,10 +243,13 @@ if ($action === 'maintenances') { 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); + $dc = $pdo->prepare("SELECT COUNT(*) FROM pf_garage_documents WHERE maintenance_id = ?"); + $dc->execute([$id]); + $m['documents_count'] = (int) $dc->fetchColumn(); 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 = $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, (SELECT COUNT(*) FROM pf_garage_documents d WHERE d.maintenance_id = m.id) as documents_count 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()); } $stmt = $pdo->query("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.next_date IS NOT NULL OR m.next_km IS NOT NULL ORDER BY m.next_date ASC"); @@ -126,6 +283,13 @@ if ($action === 'maintenances') { } if ($method === 'DELETE') { $id = $_GET['id'] ?? null; if (!$id) gErr('ID manquant'); + $docs = $pdo->prepare('SELECT filename FROM pf_garage_documents WHERE maintenance_id = ?'); + $docs->execute([$id]); + foreach ($docs->fetchAll(PDO::FETCH_COLUMN) as $fn) { + if ($fn) { + @unlink($UPLOAD_DIR . basename($fn)); + } + } $pdo->prepare("DELETE FROM pf_maintenances WHERE id = ?")->execute([$id]); gOk(['deleted' => true]); } } diff --git a/modules/garage/assets/garage.css b/modules/garage/assets/garage.css index b5492f0..1794abe 100644 --- a/modules/garage/assets/garage.css +++ b/modules/garage/assets/garage.css @@ -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); } diff --git a/modules/garage/assets/garage.js b/modules/garage/assets/garage.js index 6eba0c9..14a46bc 100644 --- a/modules/garage/assets/garage.js +++ b/modules/garage/assets/garage.js @@ -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?'':'')+ (v.vin?'':'')+ 'Aucun entretien enregistré
| Date | Type | Description | Kilométrage | Coût MO | Pièces | Prochain | |
|---|---|---|---|---|---|---|---|
| '+fmtDate(m.date)+' '+ago(m.date)+' | '+
+ ''+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':'--')+' | '+ @@ -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){ '