correction garage
Deploy HouseHub / deploy (push) Successful in 1s

This commit is contained in:
2026-05-14 22:15:55 +02:00
parent f62f724046
commit b2a3e34bbe
5 changed files with 519 additions and 17 deletions
+16
View File
@@ -294,6 +294,22 @@ CREATE TABLE IF NOT EXISTS pf_parts (
FOREIGN KEY (maintenance_id) REFERENCES pf_maintenances(id) ON DELETE SET NULL FOREIGN KEY (maintenance_id) REFERENCES pf_maintenances(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) 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 ───────────────────────────────────────────────────────────── -- ─── Notes / Memo ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_memo_notes ( CREATE TABLE IF NOT EXISTS pf_memo_notes (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
+48 -10
View File
@@ -28,15 +28,15 @@ require __DIR__ . '/header.php';
<div class="stat-pill amber"><div class="label">Pièces</div><div class="value" id="stat-parts">--</div></div> <div class="stat-pill amber"><div class="label">Pièces</div><div class="value" id="stat-parts">--</div></div>
<div class="stat-pill red"><div class="label">Coût total</div><div class="value" id="stat-cost">--</div></div> <div class="stat-pill red"><div class="label">Coût total</div><div class="value" id="stat-cost">--</div></div>
</div> </div>
<div style="display:grid;grid-template-columns:1fr 380px;gap:1.5rem;align-items:start"> <div class="garage-dash-layout">
<div> <div class="garage-dash-main">
<div class="card-header" style="margin-bottom:.75rem"> <div class="card-header garage-card-header" style="margin-bottom:.75rem">
<h2 style="font-size:1rem;font-weight:600">Mes véhicules</h2> <h2 style="font-size:1rem;font-weight:600">Mes véhicules</h2>
<button class="btn btn-primary btn-sm" onclick="openAddVehicle()">+ Ajouter</button> <button class="btn btn-primary btn-sm" onclick="openAddVehicle()">+ Ajouter</button>
</div> </div>
<div id="dashboard-vehicles" class="vehicles-grid"></div> <div id="dashboard-vehicles" class="vehicles-grid"></div>
</div> </div>
<div class="card"> <div class="card garage-dash-aside">
<div class="card-header"><div class="card-title">🔔 Rappels & prochains entretiens</div></div> <div class="card-header"><div class="card-title">🔔 Rappels & prochains entretiens</div></div>
<div id="reminders-list"></div> <div id="reminders-list"></div>
</div> </div>
@@ -47,7 +47,7 @@ require __DIR__ . '/header.php';
<!-- ── VÉHICULES ──────────────────────────────────────────────────────────── --> <!-- ── VÉHICULES ──────────────────────────────────────────────────────────── -->
<div id="page-vehicles" class="page"> <div id="page-vehicles" class="page">
<div class="container"> <div class="container">
<div class="card-header" style="margin-bottom:1rem"> <div class="card-header garage-card-header" style="margin-bottom:1rem">
<h1 style="font-size:1.2rem;font-weight:700">🚗 Mes véhicules</h1> <h1 style="font-size:1.2rem;font-weight:700">🚗 Mes véhicules</h1>
<button class="btn btn-primary" onclick="openAddVehicle()">+ Ajouter un véhicule</button> <button class="btn btn-primary" onclick="openAddVehicle()">+ Ajouter un véhicule</button>
</div> </div>
@@ -62,22 +62,38 @@ require __DIR__ . '/header.php';
<button class="btn btn-secondary btn-sm" onclick="navigate('vehicles')">← Retour</button> <button class="btn btn-secondary btn-sm" onclick="navigate('vehicles')">← Retour</button>
</div> </div>
<div id="vehicle-header"></div> <div id="vehicle-header"></div>
<div class="tabs"> <div class="tabs garage-tabs">
<button class="tab-btn active" data-tab="maintenances" onclick="switchTab('maintenances')">🔧 Entretiens <span id="maintenance-total" style="color:var(--muted);font-size:.78rem"></span></button> <button class="tab-btn active" data-tab="maintenances" onclick="switchTab('maintenances')">🔧 Entretiens <span id="maintenance-total" style="color:var(--muted);font-size:.78rem"></span></button>
<button class="tab-btn" data-tab="parts" onclick="switchTab('parts')">🔩 Pièces <span id="parts-total-vehicle" style="color:var(--muted);font-size:.78rem"></span></button> <button class="tab-btn" data-tab="parts" onclick="switchTab('parts')">🔩 Pièces <span id="parts-total-vehicle" style="color:var(--muted);font-size:.78rem"></span></button>
<button class="tab-btn" data-tab="documents" onclick="switchTab('documents')">📎 Documents <span id="vehicle-docs-tab-count" style="color:var(--muted);font-size:.78rem"></span></button>
</div> </div>
<div id="tab-maintenances" class="tab-pane active"> <div id="tab-maintenances" class="tab-pane active">
<div style="margin-bottom:1rem;display:flex;justify-content:flex-end"> <div class="garage-tab-actions">
<button class="btn btn-primary btn-sm" onclick="openAddMaintenance()">+ Ajouter un entretien</button> <button class="btn btn-primary btn-sm" onclick="openAddMaintenance()">+ Ajouter un entretien</button>
</div> </div>
<div id="maintenance-list"></div> <div id="maintenance-list"></div>
</div> </div>
<div id="tab-parts" class="tab-pane"> <div id="tab-parts" class="tab-pane">
<div style="margin-bottom:1rem;display:flex;justify-content:flex-end"> <div class="garage-tab-actions">
<button class="btn btn-primary btn-sm" onclick="openAddPart()">+ Ajouter une pièce</button> <button class="btn btn-primary btn-sm" onclick="openAddPart()">+ Ajouter une pièce</button>
</div> </div>
<div id="parts-list-vehicle"></div> <div id="parts-list-vehicle"></div>
</div> </div>
<div id="tab-documents" class="tab-pane">
<div class="garage-doc-upload-bar">
<div class="form-group" style="flex:1;min-width:0;margin:0">
<label class="form-label" for="vehicle-doc-label">Libellé (optionnel)</label>
<input type="text" class="form-control" id="vehicle-doc-label" placeholder="Carte grise, assurance…">
</div>
<div class="form-group" style="margin:0;flex:1;min-width:140px">
<label class="form-label" for="vehicle-doc-file">Fichier</label>
<input type="file" class="form-control" id="vehicle-doc-file" accept=".pdf,.png,.jpg,.jpeg,.webp,.doc,.docx,.xls,.xlsx,.txt">
</div>
<button type="button" class="btn btn-primary btn-sm garage-doc-upload-btn" onclick="uploadVehicleDocument()">Envoyer</button>
</div>
<p class="garage-doc-hint">PDF, images, Office… · max 12 Mo</p>
<div id="vehicle-documents-list"></div>
</div>
</div> </div>
</div> </div>
@@ -88,6 +104,28 @@ require __DIR__ . '/header.php';
<button class="btn btn-secondary btn-sm" id="maintenance-back-btn" onclick="navigate('vehicle',{id:currentVehicleId})">← Retour au véhicule</button> <button class="btn btn-secondary btn-sm" id="maintenance-back-btn" onclick="navigate('vehicle',{id:currentVehicleId})">← Retour au véhicule</button>
</div> </div>
<div id="maintenance-detail-header"></div> <div id="maintenance-detail-header"></div>
<div class="card garage-docs-card" style="margin-top:1rem">
<div class="card-header garage-card-header">
<span class="card-title">📎 Documents & factures (cette révision)</span>
</div>
<div style="padding:0 1.25rem 1rem">
<div class="garage-doc-upload-bar">
<div class="form-group" style="flex:1;min-width:0;margin:0">
<label class="form-label" for="maint-doc-label">Libellé (ex. facture garage)</label>
<input type="text" class="form-control" id="maint-doc-label" placeholder="Facture, devis…">
</div>
<div class="form-group" style="margin:0;flex:1;min-width:140px">
<label class="form-label" for="maint-doc-file">Fichier</label>
<input type="file" class="form-control" id="maint-doc-file" accept=".pdf,.png,.jpg,.jpeg,.webp,.doc,.docx,.xls,.xlsx,.txt">
</div>
<button type="button" class="btn btn-primary btn-sm garage-doc-upload-btn" onclick="uploadMaintenanceDocument()">Envoyer</button>
</div>
<p class="garage-doc-hint">PDF, images… · max 12 Mo</p>
<div id="maintenance-documents-list"></div>
</div>
</div>
<div class="card" style="padding:0;margin-top:1.5rem"> <div class="card" style="padding:0;margin-top:1.5rem">
<div class="card-header"> <div class="card-header">
<span class="card-title">🔩 Pièces utilisées <span id="maint-parts-count" class="badge badge-gray" style="margin-left:.5rem"></span></span> <span class="card-title">🔩 Pièces utilisées <span id="maint-parts-count" class="badge badge-gray" style="margin-left:.5rem"></span></span>
@@ -127,7 +165,7 @@ require __DIR__ . '/header.php';
<!-- ── TOUS LES ENTRETIENS ─────────────────────────────────────────────────── --> <!-- ── TOUS LES ENTRETIENS ─────────────────────────────────────────────────── -->
<div id="page-maintenances-all" class="page"> <div id="page-maintenances-all" class="page">
<div class="container"> <div class="container">
<div class="card-header" style="margin-bottom:1rem"> <div class="card-header garage-card-header" style="margin-bottom:1rem">
<div> <div>
<h1 style="font-size:1.2rem;font-weight:700">🔧 Tous les entretiens</h1> <h1 style="font-size:1.2rem;font-weight:700">🔧 Tous les entretiens</h1>
<p style="color:var(--muted);font-size:.85rem"><span id="all-maint-count">0</span> entretiens · Total : <span id="all-maint-total">--</span></p> <p style="color:var(--muted);font-size:.85rem"><span id="all-maint-count">0</span> entretiens · Total : <span id="all-maint-total">--</span></p>
@@ -140,7 +178,7 @@ require __DIR__ . '/header.php';
<!-- ── TOUTES LES PIÈCES ──────────────────────────────────────────────────── --> <!-- ── TOUTES LES PIÈCES ──────────────────────────────────────────────────── -->
<div id="page-parts" class="page"> <div id="page-parts" class="page">
<div class="container"> <div class="container">
<div class="card-header" style="margin-bottom:1rem"> <div class="card-header garage-card-header" style="margin-bottom:1rem">
<div> <div>
<h1 style="font-size:1.2rem;font-weight:700">🔩 Toutes les pièces</h1> <h1 style="font-size:1.2rem;font-weight:700">🔩 Toutes les pièces</h1>
<p style="color:var(--muted);font-size:.85rem"><span id="all-parts-count">0</span> pièces · Total : <span id="all-parts-total">--</span></p> <p style="color:var(--muted);font-size:.85rem"><span id="all-parts-count">0</span> pièces · Total : <span id="all-parts-total">--</span></p>
+165 -1
View File
@@ -17,6 +17,24 @@ require_once dirname(__DIR__, 2) . '/includes/db.php';
$UPLOAD_DIR = '/uploads/garage/'; $UPLOAD_DIR = '/uploads/garage/';
if (!is_dir($UPLOAD_DIR)) { @mkdir($UPLOAD_DIR, 0755, true); } 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']; $method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? ''; $action = $_GET['action'] ?? '';
@@ -38,6 +56,37 @@ function handleUpload(string $field, string $dir): ?string {
return null; 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 ───────────────────────────────────────────────────────────────── // ─── Vehicles ─────────────────────────────────────────────────────────────────
if ($action === 'vehicles') { if ($action === 'vehicles') {
if ($method === 'GET') { if ($method === 'GET') {
@@ -47,6 +96,12 @@ if ($action === 'vehicles') {
$vehicle = $v->fetch(); if (!$vehicle) gErr('Véhicule introuvable', 404); $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(); $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(); $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); 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"); $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') { if ($method === 'DELETE') {
$id = $_GET['id'] ?? null; if (!$id) gErr('ID manquant'); $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']); $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]); $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 ───────────────────────────────────────────────────────────── // ─── Maintenances ─────────────────────────────────────────────────────────────
if ($action === 'maintenances') { if ($action === 'maintenances') {
$vid = $_GET['vehicle_id'] ?? null; $vid = $_GET['vehicle_id'] ?? null;
@@ -89,10 +243,13 @@ if ($action === 'maintenances') {
if ($id) { 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 = $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); $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); gOk($m);
} }
if ($vid) { 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->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"); $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') { if ($method === 'DELETE') {
$id = $_GET['id'] ?? null; if (!$id) gErr('ID manquant'); $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]); $pdo->prepare("DELETE FROM pf_maintenances WHERE id = ?")->execute([$id]); gOk(['deleted' => true]);
} }
} }
+161 -2
View File
@@ -268,7 +268,8 @@ textarea.form-control { resize: vertical; min-height: 80px; }
/* ─── TOASTS ─────────────────────────────────────────────────────────────────── */ /* ─── TOASTS ─────────────────────────────────────────────────────────────────── */
.toast-container { .toast-container {
position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 9999; 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 { .toast {
background: var(--bg-panel); border: 1px solid var(--border-light); 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-brand { font-size: .75rem; color: var(--text-muted); }
.item-price { font-size: .875rem; font-weight: 600; color: var(--primary); white-space: nowrap; } .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 ─────────────────────────────────────────────────────────────── */ /* ─── RESPONSIVE ─────────────────────────────────────────────────────────────── */
@media (max-width: 768px) { @media (max-width: 768px) {
.container { padding: 1rem; } .container { padding: 1rem; }
.vehicle-detail-header { flex-direction: column; } .vehicle-detail-header { flex-direction: column; }
.vehicle-detail-img { width: 100%; height: 200px; } .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; } .form-row, .form-row-3 { grid-template-columns: 1fr; }
.stats-grid { grid-template-columns: repeat(2, 1fr); } .stats-grid { grid-template-columns: repeat(2, 1fr); }
.modal { max-height: 95vh; } .modal { max-height: 95vh; }
.vehicles-grid { grid-template-columns: 1fr; } .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 { padding: 8px 12px; overflow-x: auto; flex-wrap: nowrap; }
.garage-subnav .nav-link { white-space: 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 ══════════════════════════════════ */ /* ══ 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"] .scroll-list-item:hover { background: rgba(255,255,255,.04); }
[data-theme="dark"] .item-name { color: var(--text-main); } [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"] #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); }
+129 -4
View File
@@ -7,6 +7,12 @@ function $$(s,ctx=document){return [...ctx.querySelectorAll(s)];}
function fmt(n){return n!=null?Number(n).toLocaleString('fr-FR'):'--';} function fmt(n){return n!=null?Number(n).toLocaleString('fr-FR'):'--';}
function fmtPrice(n){return n!=null?Number(n).toFixed(2)+' €':'--';} function fmtPrice(n){return n!=null?Number(n).toFixed(2)+' €':'--';}
function fmtDate(d){if(!d)return '--';return new Date(d).toLocaleDateString('fr-FR');} 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 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 escHtml(s){const d=document.createElement('div');d.textContent=String(s??'');return d.innerHTML;}
function toast(msg,type='success'){ function toast(msg,type='success'){
@@ -25,7 +31,7 @@ function navigate(page,params={}){
$$('.page').forEach(p=>p.classList.remove('active')); $$('.page').forEach(p=>p.classList.remove('active'));
$$('.nav-link').forEach(n=>n.classList.remove('active')); $$('.nav-link').forEach(n=>n.classList.remove('active'));
document.getElementById('page-'+page)?.classList.add('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'); document.querySelector('.nav-link[data-page="'+navTarget+'"]')?.classList.add('active');
if(page==='dashboard')loadDashboard(); if(page==='dashboard')loadDashboard();
else if(page==='vehicles')loadVehicles(); else if(page==='vehicles')loadVehicles();
@@ -127,6 +133,8 @@ async function loadVehicleDetail(id){
renderVehicleHeader(v); renderVehicleHeader(v);
renderMaintenances(maintenances); renderMaintenances(maintenances);
renderVehicleParts(parts); renderVehicleParts(parts);
setVehicleDocTabBadge(v);
await loadVehicleDocuments();
document.title='GarageManager · '+v.name; document.title='GarageManager · '+v.name;
}catch(e){toast(e.message,'error');} }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.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>':'')+ (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>'+
'<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>'+
'<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="openEditVehicle('+v.id+')">✏️ Modifier</button>'+
'<button class="btn btn-secondary btn-sm" onclick="openUploadPhoto('+v.id+')">📷 Photo</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>'+ '<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;} 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>'+ 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+'})">'+ 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><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 style="max-width:200px;color:var(--muted)">'+(m.description?escHtml(m.description):'--')+'</td>'+
'<td>'+(m.km?fmt(m.km)+' km':'--')+'</td>'+ '<td>'+(m.km?fmt(m.km)+' km':'--')+'</td>'+
@@ -218,6 +233,7 @@ async function loadMaintenanceDetail(id){
currentVehicleId=m.vehicle_id; currentVehicleId=m.vehicle_id;
renderMaintenanceDetailHeader(m); renderMaintenanceDetailHeader(m);
renderMaintenanceParts(parts,id,m.vehicle_id); renderMaintenanceParts(parts,id,m.vehicle_id);
await loadMaintenanceDocuments();
loadKnownParts(m.vehicle_id); loadKnownParts(m.vehicle_id);
const f=document.getElementById('inline-part-form'); const f=document.getElementById('inline-part-form');
if(f)f.style.display='none'; 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">'+ '<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 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>'+ '<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>':'')+ (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>'+ '</div>'+
(m.description?'<p style="margin-bottom:.75rem">'+escHtml(m.description)+'</p>':'')+ (m.description?'<p style="margin-bottom:.75rem">'+escHtml(m.description)+'</p>':'')+
@@ -244,7 +261,7 @@ function renderMaintenanceDetailHeader(m){
'</div>'+ '</div>'+
(m.notes?'<p style="margin-top:.75rem;color:var(--muted);font-size:.85rem;font-style:italic">'+escHtml(m.notes)+'</p>':'')+ (m.notes?'<p style="margin-top:.75rem;color:var(--muted);font-size:.85rem;font-style:italic">'+escHtml(m.notes)+'</p>':'')+
'</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="openEditMaintenance('+m.id+')">✏️ Modifier</button>'+ '<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>'+ '<button class="btn btn-danger btn-sm" onclick="deleteMaintenance('+m.id+')">🗑️ Supprimer</button>'+
'</div>'+ '</div>'+
@@ -635,12 +652,120 @@ async function doUploadPhoto(){
}catch(e){toast(e.message,'error');} }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 linstant</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 ──────────────────────────────────────────────────────────────────── // ─── TABS ────────────────────────────────────────────────────────────────────
function switchTab(tab){ function switchTab(tab){
$$('.tab-btn').forEach(b=>b.classList.remove('active')); $$('.tab-btn').forEach(b=>b.classList.remove('active'));
$$('.tab-pane').forEach(p=>p.classList.remove('active')); $$('.tab-pane').forEach(p=>p.classList.remove('active'));
document.querySelector('.tab-btn[data-tab="'+tab+'"]')?.classList.add('active'); document.querySelector('.tab-btn[data-tab="'+tab+'"]')?.classList.add('active');
document.getElementById('tab-'+tab)?.classList.add('active'); document.getElementById('tab-'+tab)?.classList.add('active');
if(tab==='documents'&&currentVehicleId)loadVehicleDocuments();
} }
// ─── PHOTO PREVIEW ──────────────────────────────────────────────────────────── // ─── PHOTO PREVIEW ────────────────────────────────────────────────────────────