@@ -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
@@ -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
@@ -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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
|||||||
@@ -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>'+
|
||||||
'<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="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 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 ────────────────────────────────────────────────────────────────────
|
// ─── 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'&¤tVehicleId)loadVehicleDocuments();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── PHOTO PREVIEW ────────────────────────────────────────────────────────────
|
// ─── PHOTO PREVIEW ────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user