Garage module : toutes les features GarageManager
Deploy to Pacha Family / deploy (push) Failing after 3s

- Navigation vers détail entretien (page-maintenance) au clic sur une ligne
- Boutons édition sur entretiens et pièces (openEditMaintenance, openEditPart)
- Formulaire inline ajout pièce sur page entretien avec TVA +20%
- Chips "pièces connues" pour réutiliser rapidement une pièce existante
- Calcul prix TTC dans modal pièce (updatePartPriceTTC) et formulaire inline (updateIPriceTTC)
- API : GET par ID pour maintenances et pièces
- Correction champ garage : name="garage_name" cohérent POST/PUT

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
perco
2026-05-11 16:57:57 +02:00
co-authored by Claude Sonnet 4.6
parent a443ec389b
commit eb34657473
3 changed files with 499 additions and 231 deletions
+136 -88
View File
@@ -9,147 +9,196 @@ require __DIR__ . '/header.php';
?>
<link rel="stylesheet" href="/modules/garage/assets/garage.css">
<div id="toasts" class="toast-container"></div>
<!-- Sous-navigation interne Garage -->
<!-- Sous-navigation -->
<nav class="garage-subnav">
<button class="nav-link active" data-page="dashboard">📊 Dashboard</button>
<button class="nav-link" data-page="vehicles">🚗 <?= tr('garage_vehicles_title') ?></button>
<button class="nav-link" data-page="maintenances-all">🔧 <?= tr('garage_maintenances') ?></button>
<button class="nav-link" data-page="parts">🔩 <?= tr('garage_all_parts') ?></button>
<button class="nav-link" data-page="vehicles">🚗 hicules</button>
<button class="nav-link" data-page="maintenances-all">🔧 Entretiens</button>
<button class="nav-link" data-page="parts">🔩 Pièces</button>
</nav>
<div id="page-maintenances-all" class="page">
<div class="container">
<div class="card-header" style="margin-bottom:1rem">
<div>
<h1 style="font-size:1.2rem;font-weight:700">🔧 <?= tr('garage_maintenances') ?></h1>
<p style="color:var(--muted);font-size:.85rem"><span id="all-maint-count">0</span> <?= tr('garage_maintenances') ?> · Total : <span id="all-maint-total">--</span></p>
</div>
</div>
<div id="all-maintenances-list"></div>
</div>
</div>
<!-- ── DASHBOARD ─────────────────────────────────────────────────────────── -->
<div id="page-dashboard" class="page active">
<div class="container">
<div class="stats-grid">
<div class="stat-pill blue"><div class="label"><?= tr('garage_stat_vehicles') ?></div><div class="value" id="stat-vehicles">--</div></div>
<div class="stat-pill green"><div class="label"><?= tr('garage_stat_maintenances') ?></div><div class="value" id="stat-maintenances">--</div></div>
<div class="stat-pill amber"><div class="label"><?= tr('garage_stat_parts') ?></div><div class="value" id="stat-parts">--</div></div>
<div class="stat-pill red"><div class="label"><?= tr('garage_stat_cost') ?></div><div class="value" id="stat-cost">--</div></div>
<div class="stat-pill blue"><div class="label">hicules</div><div class="value" id="stat-vehicles">--</div></div>
<div class="stat-pill green"><div class="label">Entretiens</div><div class="value" id="stat-maintenances">--</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>
<div style="display:grid;grid-template-columns:1fr 380px;gap:1.5rem;align-items:start">
<div>
<div class="card-header" style="margin-bottom:.75rem">
<h2 style="font-size:1rem;font-weight:600"><?= tr('garage_my_vehicles') ?></h2>
<button class="btn btn-primary btn-sm" onclick="openAddVehicle()">+ <?= tr('garage_add') ?></button>
<h2 style="font-size:1rem;font-weight:600">Mes véhicules</h2>
<button class="btn btn-primary btn-sm" onclick="openAddVehicle()">+ Ajouter</button>
</div>
<div id="dashboard-vehicles" class="vehicles-grid"></div>
</div>
<div class="card">
<div class="card-header"><div class="card-title">🔔 <?= tr('garage_reminders') ?></div></div>
<div class="card-header"><div class="card-title">🔔 Rappels & prochains entretiens</div></div>
<div id="reminders-list"></div>
</div>
</div>
</div>
</div>
<!-- ── VÉHICULES ──────────────────────────────────────────────────────────── -->
<div id="page-vehicles" class="page">
<div class="container">
<div class="card-header" style="margin-bottom:1rem">
<h1 style="font-size:1.2rem;font-weight:700">🚗 <?= tr('garage_vehicles_title') ?></h1>
<button class="btn btn-primary" onclick="openAddVehicle()">+ <?= tr('garage_add_vehicle') ?></button>
<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>
</div>
<div id="vehicles-grid" class="vehicles-grid"></div>
</div>
</div>
<!-- ── DÉTAIL VÉHICULE ─────────────────────────────────────────────────────── -->
<div id="page-vehicle" class="page">
<div class="container">
<div style="margin-bottom:1rem">
<button class="btn btn-secondary btn-sm" onclick="navigate('vehicles')">← <?= tr('btn_back') ?></button>
<button class="btn btn-secondary btn-sm" onclick="navigate('vehicles')">← Retour</button>
</div>
<div id="vehicle-header"></div>
<div class="tabs">
<button class="tab-btn active" data-tab="maintenances" onclick="switchTab('maintenances')">🔧 <?= tr('garage_maintenances') ?> <span id="maintenance-total" style="color:var(--muted);font-size:.78rem"></span></button>
<button class="tab-btn" data-tab="parts" onclick="switchTab('parts')">🔩 <?= tr('garage_parts') ?> <span id="parts-total-vehicle" 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>
</div>
<div id="tab-maintenances" class="tab-pane active">
<div style="margin-bottom:1rem;display:flex;justify-content:flex-end">
<button class="btn btn-primary btn-sm" onclick="openAddMaintenance()">+ <?= tr('garage_add_maintenance') ?></button>
<button class="btn btn-primary btn-sm" onclick="openAddMaintenance()">+ Ajouter un entretien</button>
</div>
<div id="maintenance-list"></div>
</div>
<div id="tab-parts" class="tab-pane">
<div style="margin-bottom:1rem;display:flex;justify-content:flex-end">
<button class="btn btn-primary btn-sm" onclick="openAddPart()">+ <?= tr('garage_add_part') ?></button>
<button class="btn btn-primary btn-sm" onclick="openAddPart()">+ Ajouter une pièce</button>
</div>
<div id="parts-list-vehicle"></div>
</div>
</div>
</div>
<!-- ── DÉTAIL ENTRETIEN ─────────────────────────────────────────────────────── -->
<div id="page-maintenance" class="page">
<div class="container">
<div style="margin-bottom:1rem">
<button class="btn btn-secondary btn-sm" id="maintenance-back-btn" onclick="navigate('vehicle',{id:currentVehicleId})">← Retour au véhicule</button>
</div>
<div id="maintenance-detail-header"></div>
<div class="card" style="padding:0;margin-top:1.5rem">
<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>
<button class="btn btn-primary btn-sm" onclick="toggleInlinePartForm()">+ Ajouter une pièce</button>
</div>
<!-- Formulaire inline ajout pièce -->
<div id="inline-part-form" style="display:none;background:#f8fafc;border-bottom:1px solid #e2e8f0;padding:1.25rem 1.5rem">
<p style="font-size:.8rem;font-weight:600;color:#64748b;margin-bottom:.75rem;text-transform:uppercase;letter-spacing:.05em">🔄 Réutiliser une pièce connue</p>
<div id="known-parts-list" style="display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
<div class="form-group"><label class="form-label">Nom *</label><input class="form-control" id="ipart-name" placeholder="Filtre à huile"></div>
<div class="form-group"><label class="form-label">Référence</label><input class="form-control" id="ipart-reference" placeholder="Ex: 06A115561B" style="font-family:monospace"></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem">
<div class="form-group"><label class="form-label">Marque</label><input class="form-control" id="ipart-brand" placeholder="Bosch, NGK..."></div>
<div class="form-group">
<label class="form-label">Prix unitaire (€)</label>
<input class="form-control" id="ipart-price" type="number" step="0.01" value="0" oninput="updateIPriceTTC()">
<label style="display:flex;align-items:center;gap:.4rem;margin-top:.4rem;font-size:.8rem;color:#64748b;cursor:pointer">
<input type="checkbox" id="ipart-prix-ht" onchange="updateIPriceTTC()" style="width:14px;height:14px"> Prix HT (+20% TVA)
</label>
<p id="ipart-ttc-preview" style="display:none;font-size:.8rem;color:#2563eb;margin-top:.2rem;font-family:monospace"></p>
</div>
<div class="form-group"><label class="form-label">Quantité</label><input class="form-control" id="ipart-quantity" type="number" value="1" min="1"></div>
</div>
<div class="form-group"><label class="form-label">Photo (optionnel)</label><input class="form-control" id="ipart-photo" type="file" accept="image/*"></div>
<div style="display:flex;gap:.5rem;margin-top:.5rem">
<button class="btn btn-primary btn-sm" onclick="saveInlinePart()">+ Ajouter</button>
<button class="btn btn-secondary btn-sm" onclick="toggleInlinePartForm()">Annuler</button>
</div>
</div>
<div id="maintenance-parts-list"></div>
</div>
</div>
</div>
<!-- ── TOUS LES ENTRETIENS ─────────────────────────────────────────────────── -->
<div id="page-maintenances-all" class="page">
<div class="container">
<div class="card-header" style="margin-bottom:1rem">
<div>
<h1 style="font-size:1.2rem;font-weight:700">🔧 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>
</div>
</div>
<div id="all-maintenances-list"></div>
</div>
</div>
<!-- ── TOUTES LES PIÈCES ──────────────────────────────────────────────────── -->
<div id="page-parts" class="page">
<div class="container">
<div class="card-header" style="margin-bottom:1rem">
<div>
<h1 style="font-size:1.2rem;font-weight:700">🔩 <?= tr('garage_all_parts') ?></h1>
<p style="color:var(--muted);font-size:.85rem"><span id="all-parts-count">0</span> <?= tr('garage_parts_count') ?> · Total : <span id="all-parts-total">--</span></p>
<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>
</div>
<button class="btn btn-primary" onclick="openAddPart()">+ <?= tr('garage_add_part') ?></button>
<button class="btn btn-primary" onclick="openAddPart()">+ Ajouter une pièce</button>
</div>
<div id="all-parts-list"></div>
</div>
</div>
<!-- ══ MODALS ════════════════════════════════════════════════════════════════ -->
<!-- MODAL: VEHICLE -->
<div id="modal-vehicle" class="modal-backdrop">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="modal-vehicle-title"><?= tr('garage_add_vehicle') ?></div>
<div class="modal-title" id="modal-vehicle-title">Ajouter un véhicule</div>
<button class="modal-close" onclick="closeModal('modal-vehicle')">✕</button>
</div>
<div class="modal-body">
<form id="form-vehicle" onsubmit="return false">
<input type="hidden" id="form-vehicle-id" name="id">
<div class="form-row">
<div class="form-group"><label class="form-label"><?= tr('garage_vehicle_name') ?> *</label><input class="form-control" id="vehicle-name" name="name" required placeholder="Ma Clio"></div>
<div class="form-group"><label class="form-label"><?= tr('garage_brand') ?> *</label><input class="form-control" id="vehicle-brand" name="brand" required placeholder="Renault"></div>
<div class="form-group"><label class="form-label">Nom *</label><input class="form-control" id="vehicle-name" name="name" required placeholder="Ma Clio"></div>
<div class="form-group"><label class="form-label">Marque *</label><input class="form-control" id="vehicle-brand" name="brand" required placeholder="Renault"></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label"><?= tr('garage_model') ?> *</label><input class="form-control" id="vehicle-model" name="model" required placeholder="Clio 4"></div>
<div class="form-group"><label class="form-label"><?= tr('garage_year') ?></label><input class="form-control" id="vehicle-year" name="year" type="number" placeholder="2019"></div>
<div class="form-group"><label class="form-label">Modèle *</label><input class="form-control" id="vehicle-model" name="model" required placeholder="Clio 4"></div>
<div class="form-group"><label class="form-label">Année</label><input class="form-control" id="vehicle-year" name="year" type="number" placeholder="2019"></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label"><?= tr('garage_plate') ?></label><input class="form-control" id="vehicle-license-plate" name="license_plate" placeholder="AB-123-CD"></div>
<div class="form-group"><label class="form-label"><?= tr('garage_fuel') ?></label>
<div class="form-group"><label class="form-label">Immatriculation</label><input class="form-control" id="vehicle-license-plate" name="license_plate" placeholder="AB-123-CD"></div>
<div class="form-group"><label class="form-label">Carburant</label>
<select class="form-control" id="vehicle-fuel-type" name="fuel_type">
<option>Essence</option><option>Diesel</option><option>Hybride</option><option>Electrique</option><option>GPL</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label"><?= tr('garage_km') ?></label><input class="form-control" id="vehicle-current-km" name="current_km" type="number" placeholder="85000"></div>
<div class="form-group"><label class="form-label"><?= tr('garage_color') ?></label><input class="form-control" id="vehicle-color" name="color" placeholder="Blanc nacré"></div>
<div class="form-group"><label class="form-label">Kilométrage</label><input class="form-control" id="vehicle-current-km" name="current_km" type="number" placeholder="85000"></div>
<div class="form-group"><label class="form-label">Couleur</label><input class="form-control" id="vehicle-color" name="color" placeholder="Blanc nacré"></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label"><?= tr('garage_purchase_date') ?></label><input class="form-control" id="vehicle-purchase-date" name="purchase_date" type="date"></div>
<div class="form-group"><label class="form-label"><?= tr('garage_purchase_price') ?></label><input class="form-control" id="vehicle-purchase-price" name="purchase_price" type="number" step="0.01"></div>
<div class="form-group"><label class="form-label">Date d'achat</label><input class="form-control" id="vehicle-purchase-date" name="purchase_date" type="date"></div>
<div class="form-group"><label class="form-label">Prix d'achat (€)</label><input class="form-control" id="vehicle-purchase-price" name="purchase_price" type="number" step="0.01"></div>
</div>
<div class="form-group"><label class="form-label">VIN</label><input class="form-control" id="vehicle-vin" name="vin" placeholder="VF1..."></div>
<div class="form-group">
<label class="form-label"><?= tr('garage_photo') ?></label>
<label class="form-label">Photo</label>
<input class="form-control" id="vehicle-photo" name="photo" type="file" accept="image/*" onchange="previewPhoto('vehicle-photo','vehicle-photo-preview')">
<img id="vehicle-photo-preview" style="display:none;max-height:120px;margin-top:.5rem;border-radius:6px" alt="">
</div>
<div class="form-group"><label class="form-label"><?= tr('garage_notes') ?></label><textarea class="form-control" id="vehicle-notes" name="notes" rows="2"></textarea></div>
<div class="form-group"><label class="form-label">Notes</label><textarea class="form-control" id="vehicle-notes" name="notes" rows="2"></textarea></div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('modal-vehicle')"><?= tr('btn_cancel') ?></button>
<button class="btn btn-primary" onclick="saveVehicle()"><?= tr('btn_save') ?></button>
<button class="btn btn-secondary" onclick="closeModal('modal-vehicle')">Annuler</button>
<button class="btn btn-primary" onclick="saveVehicle()">Enregistrer</button>
</div>
</div>
</div>
@@ -158,7 +207,7 @@ require __DIR__ . '/header.php';
<div id="modal-maintenance" class="modal-backdrop">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="modal-maintenance-title"><?= tr('garage_add_maintenance') ?></div>
<div class="modal-title" id="modal-maintenance-title">Ajouter un entretien</div>
<button class="modal-close" onclick="closeModal('modal-maintenance')">✕</button>
</div>
<div class="modal-body">
@@ -166,37 +215,37 @@ require __DIR__ . '/header.php';
<input type="hidden" id="form-maintenance-id" name="id">
<input type="hidden" id="maintenance-vehicle-id" name="vehicle_id">
<div class="form-row">
<div class="form-group"><label class="form-label"><?= tr('garage_maint_type') ?> *</label>
<div class="form-group"><label class="form-label">Type *</label>
<select class="form-control" id="maintenance-type" name="type">
<option>Vidange</option><option>Révision</option><option>Freins</option><option>Pneus</option>
<option>Distribution</option><option>Filtres</option><option>Batterie</option><option>Climatisation</option>
<option>Carrosserie</option><option>Diagnostic</option><option>Contrôle technique</option><option>Autre</option>
</select>
</div>
<div class="form-group"><label class="form-label"><?= tr('garage_date') ?> *</label><input class="form-control" id="maintenance-date" name="date" type="date" required></div>
<div class="form-group"><label class="form-label">Date *</label><input class="form-control" id="maintenance-date" name="date" type="date" required></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label"><?= tr('garage_km_at') ?></label><input class="form-control" id="maintenance-km" name="km" type="number" placeholder="85000"></div>
<div class="form-group"><label class="form-label"><?= tr('garage_labor_cost') ?></label><input class="form-control" id="maintenance-cost" name="cost" type="number" step="0.01" placeholder="0"></div>
<div class="form-group"><label class="form-label">Kilométrage</label><input class="form-control" id="maintenance-km" name="km" type="number" placeholder="85000"></div>
<div class="form-group"><label class="form-label">Coût main d'œuvre (€)</label><input class="form-control" id="maintenance-cost" name="cost" type="number" step="0.01" placeholder="0"></div>
</div>
<div class="form-group"><label class="form-label"><?= tr('garage_description') ?></label><textarea class="form-control" id="maintenance-description" name="description" rows="2"></textarea></div>
<div class="form-group"><label class="form-label">Description</label><textarea class="form-control" id="maintenance-description" name="description" rows="2"></textarea></div>
<div class="form-row">
<div class="form-group"><label class="form-label"><?= tr('garage_mechanic') ?></label><input class="form-control" id="maintenance-mechanic" name="mechanic"></div>
<div class="form-group"><label class="form-label"><?= tr('garage_garage') ?></label><input class="form-control" id="maintenance-garage" name="garage"></div>
<div class="form-group"><label class="form-label">Mécanicien</label><input class="form-control" id="maintenance-mechanic" name="mechanic"></div>
<div class="form-group"><label class="form-label">Garage / Atelier</label><input class="form-control" id="maintenance-garage" name="garage_name"></div>
</div>
<div style="background:rgba(37,99,235,.05);border:1px solid rgba(37,99,235,.1);border-radius:8px;padding:1rem;margin-top:.5rem">
<div style="font-size:.82rem;color:var(--muted);margin-bottom:.75rem;font-weight:600">🔔 <?= tr('garage_next_reminder') ?></div>
<div style="font-size:.82rem;color:var(--muted);margin-bottom:.75rem;font-weight:600">🔔 Prochain entretien</div>
<div class="form-row">
<div class="form-group"><label class="form-label"><?= tr('garage_next_date') ?></label><input class="form-control" id="maintenance-next-date" name="next_date" type="date"></div>
<div class="form-group"><label class="form-label"><?= tr('garage_next_km') ?></label><input class="form-control" id="maintenance-next-km" name="next_km" type="number" placeholder="95000"></div>
<div class="form-group"><label class="form-label">Date prévue</label><input class="form-control" id="maintenance-next-date" name="next_date" type="date"></div>
<div class="form-group"><label class="form-label">Kilométrage prévu</label><input class="form-control" id="maintenance-next-km" name="next_km" type="number" placeholder="95000"></div>
</div>
</div>
<div class="form-group" style="margin-top:1rem"><label class="form-label"><?= tr('garage_notes') ?></label><textarea class="form-control" id="maintenance-notes" name="notes" rows="2"></textarea></div>
<div class="form-group" style="margin-top:1rem"><label class="form-label">Notes</label><textarea class="form-control" id="maintenance-notes" name="notes" rows="2"></textarea></div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('modal-maintenance')"><?= tr('btn_cancel') ?></button>
<button class="btn btn-primary" onclick="saveMaintenance()"><?= tr('btn_save') ?></button>
<button class="btn btn-secondary" onclick="closeModal('modal-maintenance')">Annuler</button>
<button class="btn btn-primary" onclick="saveMaintenance()">Enregistrer</button>
</div>
</div>
</div>
@@ -205,16 +254,17 @@ require __DIR__ . '/header.php';
<div id="modal-part" class="modal-backdrop">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="modal-part-title"><?= tr('garage_add_part') ?></div>
<div class="modal-title" id="modal-part-title">Ajouter une pièce</div>
<button class="modal-close" onclick="closeModal('modal-part')">✕</button>
</div>
<div class="modal-body">
<form id="form-part" onsubmit="return false">
<input type="hidden" id="form-part-id" name="id">
<input type="hidden" id="part-vehicle-id" name="vehicle_id">
<input type="hidden" id="part-maintenance-id" name="maintenance_id">
<div class="form-row">
<div class="form-group"><label class="form-label"><?= tr('garage_part_name') ?> *</label><input class="form-control" id="part-name" name="name" required placeholder="Filtre à huile"></div>
<div class="form-group"><label class="form-label"><?= tr('garage_category') ?></label>
<div class="form-group"><label class="form-label">Nom *</label><input class="form-control" id="part-name" name="name" required placeholder="Filtre à huile"></div>
<div class="form-group"><label class="form-label">Catégorie</label>
<select class="form-control" id="part-category" name="category">
<option>Moteur</option><option>Freinage</option><option>Suspension</option><option>Transmission</option>
<option>Carrosserie</option><option>Electricite</option><option>Eclairage</option><option>Filtration</option>
@@ -223,51 +273,49 @@ require __DIR__ . '/header.php';
</div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label"><?= tr('garage_brand') ?></label><input class="form-control" id="part-brand" name="brand" placeholder="Bosch, NGK..."></div>
<div class="form-group"><label class="form-label"><?= tr('garage_reference') ?></label><input class="form-control" id="part-reference" name="reference"></div>
</div>
<div class="form-row-3">
<div class="form-group"><label class="form-label"><?= tr('garage_unit_price') ?></label><input class="form-control" id="part-price" name="price" type="number" step="0.01" placeholder="12.50"></div>
<div class="form-group"><label class="form-label"><?= tr('garage_quantity') ?></label><input class="form-control" id="part-quantity" name="quantity" type="number" value="1" min="1"></div>
<div class="form-group"><label class="form-label"><?= tr('garage_unit') ?></label>
<select class="form-control" id="part-unit" name="unit">
<option value="piece">pièce</option><option value="litre">litre</option><option value="kg">kg</option><option value="m">mètre</option>
</select>
</div>
<div class="form-group"><label class="form-label">Marque</label><input class="form-control" id="part-brand" name="brand" placeholder="Bosch, NGK..."></div>
<div class="form-group"><label class="form-label">Référence</label><input class="form-control" id="part-reference" name="reference"></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label"><?= tr('garage_supplier') ?></label><input class="form-control" id="part-supplier" name="supplier" placeholder="Amazon, Oscaro..."></div>
<div class="form-group"><label class="form-label"><?= tr('garage_purchase_date') ?></label><input class="form-control" id="part-purchase-date" name="purchase_date" type="date"></div>
<div class="form-group">
<label class="form-label">Prix unitaire (€)</label>
<input class="form-control" id="part-price" name="price" type="number" step="0.01" placeholder="12.50" oninput="updatePartPriceTTC()">
<label style="display:flex;align-items:center;gap:.4rem;margin-top:.4rem;font-size:.8rem;color:#64748b;cursor:pointer">
<input type="checkbox" id="part-prix-ht" onchange="updatePartPriceTTC()" style="width:14px;height:14px"> Prix HT (+20% TVA)
</label>
<p id="part-ttc-preview" style="display:none;font-size:.8rem;color:#2563eb;margin-top:.2rem;font-family:monospace"></p>
</div>
<div class="form-group"><label class="form-label">Quantité</label><input class="form-control" id="part-quantity" name="quantity" type="number" value="1" min="1"></div>
</div>
<div class="form-group">
<label class="form-label"><?= tr('garage_photo') ?></label>
<label class="form-label">Photo</label>
<input class="form-control" id="part-photo" name="photo" type="file" accept="image/*" onchange="previewPhoto('part-photo','part-photo-preview')">
<img id="part-photo-preview" style="display:none;max-height:100px;margin-top:.5rem;border-radius:6px" alt="">
</div>
<div class="form-group"><label class="form-label"><?= tr('garage_notes') ?></label><textarea class="form-control" id="part-notes" name="notes" rows="2"></textarea></div>
<div class="form-group"><label class="form-label">Notes</label><textarea class="form-control" id="part-notes" name="notes" rows="2"></textarea></div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('modal-part')"><?= tr('btn_cancel') ?></button>
<button class="btn btn-primary" onclick="savePart()"><?= tr('btn_save') ?></button>
<button class="btn btn-secondary" onclick="closeModal('modal-part')">Annuler</button>
<button class="btn btn-primary" onclick="savePart()">Enregistrer</button>
</div>
</div>
</div>
<!-- MODAL: UPLOAD PHOTO -->
<!-- MODAL: UPLOAD PHOTO VÉHICULE -->
<div id="modal-upload-photo" class="modal-backdrop">
<div class="modal" style="max-width:400px">
<div class="modal-header">
<div class="modal-title">📷 <?= tr('garage_change_photo') ?></div>
<div class="modal-title">📷 Changer la photo</div>
<button class="modal-close" onclick="closeModal('modal-upload-photo')">✕</button>
</div>
<div class="modal-body">
<input type="hidden" id="upload-vehicle-id">
<div class="form-group"><label class="form-label"><?= tr('garage_new_photo') ?></label><input class="form-control" id="upload-photo-file" type="file" accept="image/*"></div>
<div class="form-group"><label class="form-label">Nouvelle photo</label><input class="form-control" id="upload-photo-file" type="file" accept="image/*"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('modal-upload-photo')"><?= tr('btn_cancel') ?></button>
<button class="btn btn-primary" onclick="doUploadPhoto()"><?= tr('garage_update_photo') ?></button>
<button class="btn btn-secondary" onclick="closeModal('modal-upload-photo')">Annuler</button>
<button class="btn btn-primary" onclick="doUploadPhoto()">Mettre à jour</button>
</div>
</div>
</div>
+9 -1
View File
@@ -74,6 +74,12 @@ if ($action === 'vehicles') {
if ($action === 'maintenances') {
$vid = $_GET['vehicle_id'] ?? null;
if ($method === 'GET') {
$id = $_GET['id'] ?? null;
if ($id) {
$stmt = $pdo->prepare("SELECT m.*, v.name as vehicle_name, v.license_plate, v.current_km FROM pf_maintenances m JOIN pf_vehicles v ON v.id = m.vehicle_id WHERE m.id = ?");
$stmt->execute([$id]); $m = $stmt->fetch(); if (!$m) gErr('Entretien introuvable', 404);
gOk($m);
}
if ($vid) {
$stmt = $pdo->prepare("SELECT m.*, GROUP_CONCAT(p.name SEPARATOR ', ') as parts_names, COUNT(p.id) as parts_count, COALESCE(SUM(p.price*p.quantity),0) as parts_cost FROM pf_maintenances m LEFT JOIN pf_parts p ON p.maintenance_id = m.id WHERE m.vehicle_id = ? GROUP BY m.id ORDER BY m.date DESC, m.created_at DESC");
$stmt->execute([$vid]); gOk($stmt->fetchAll());
@@ -85,7 +91,7 @@ if ($action === 'maintenances') {
$photo = handleUpload('invoice_photo', $UPLOAD_DIR); $d = $_POST ?: gBody();
if (!($d['vehicle_id'] ?? null)) gErr('vehicle_id manquant');
$stmt = $pdo->prepare("INSERT INTO pf_maintenances (vehicle_id,type,description,date,km,cost,mechanic,garage_name,next_km,next_date,invoice_photo,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)");
$stmt->execute([$d['vehicle_id'], $d['type']??'', $d['description']??null, $d['date']??date('Y-m-d'), $d['km']??null, $d['cost']??0, $d['mechanic']??null, $d['garage']??null, $d['next_km']??null, $d['next_date']??null, $photo, $d['notes']??null]);
$stmt->execute([$d['vehicle_id'], $d['type']??'', $d['description']??null, $d['date']??date('Y-m-d'), $d['km']??null, $d['cost']??0, $d['mechanic']??null, $d['garage_name']??null, $d['next_km']??null, $d['next_date']??null, $photo, $d['notes']??null]);
$mid = $pdo->lastInsertId();
if (!empty($d['km'])) { $pdo->prepare("UPDATE pf_vehicles SET current_km = GREATEST(current_km, ?), updated_at = NOW() WHERE id = ?")->execute([$d['km'], $d['vehicle_id']]); }
gOk(['id' => $mid]);
@@ -115,6 +121,8 @@ if ($action === 'maintenances') {
if ($action === 'parts') {
if ($method === 'GET') {
$vid = $_GET['vehicle_id'] ?? null; $mid = $_GET['maintenance_id'] ?? null;
$id = $_GET['id'] ?? null;
if ($id) { $stmt = $pdo->prepare("SELECT * FROM pf_parts WHERE id = ?"); $stmt->execute([$id]); $p = $stmt->fetch(); if (!$p) gErr('Pièce introuvable', 404); gOk($p); }
if ($mid) { $stmt = $pdo->prepare("SELECT * FROM pf_parts WHERE maintenance_id = ? ORDER BY created_at DESC"); $stmt->execute([$mid]); gOk($stmt->fetchAll()); }
if ($vid) { $stmt = $pdo->prepare("SELECT p.*, m.type as maintenance_type, m.date as maintenance_date FROM pf_parts p LEFT JOIN pf_maintenances m ON m.id = p.maintenance_id WHERE p.vehicle_id = ? ORDER BY p.created_at DESC"); $stmt->execute([$vid]); gOk($stmt->fetchAll()); }
$stmt = $pdo->query("SELECT p.*, v.name as vehicle_name, m.type as maintenance_type, m.date as maintenance_date FROM pf_parts p LEFT JOIN pf_vehicles v ON v.id = p.vehicle_id LEFT JOIN pf_maintenances m ON m.id = p.maintenance_id ORDER BY p.created_at DESC");
+354 -142
View File
@@ -2,34 +2,40 @@
const API = '/modules/garage/api.php';
const UPLOADS = '/modules/garage/api.php?action=photo&file=';
function $(s, ctx=document){return ctx.querySelector(s);}
function $$(s, ctx=document){return [...ctx.querySelectorAll(s)];}
function $(s,ctx=document){return ctx.querySelector(s);}
function $$(s,ctx=document){return [...ctx.querySelectorAll(s)];}
function fmt(n){return n!=null?Number(n).toLocaleString('fr-FR'):'--';}
function fmtPrice(n){return n!=null?Number(n).toFixed(2)+' \u20ac':'--';}
function fmtPrice(n){return n!=null?Number(n).toFixed(2)+' ':'--';}
function fmtDate(d){if(!d)return '--';return new Date(d).toLocaleDateString('fr-FR');}
function ago(d){if(!d)return '';const diff=Math.floor((Date.now()-new Date(d))/86400000);if(diff===0)return "aujourd'hui";if(diff===1)return 'hier';return 'il y a '+diff+'j';}
function escHtml(s){const d=document.createElement('div');d.textContent=String(s??'');return d.innerHTML;}
function toast(msg,type='success'){
const c=document.getElementById('toasts');const t=document.createElement('div');
t.className='toast '+type;t.textContent=msg;c.appendChild(t);
setTimeout(()=>t.remove(),3000);
t.className='toast '+type;t.textContent=msg;c.appendChild(t);setTimeout(()=>t.remove(),3000);
}
// NAV
// ─── STATE ────────────────────────────────────────────────────────────────────
let currentPage='dashboard';
let currentVehicleId=null;
let currentMaintenanceId=null;
// ─── NAV ──────────────────────────────────────────────────────────────────────
function navigate(page,params={}){
currentPage=page;
$$('.page').forEach(p=>p.classList.remove('active'));
$$('.nav-link').forEach(n=>n.classList.remove('active'));
document.getElementById('page-'+page)?.classList.add('active');
document.querySelector('.nav-link[data-page="'+page+'"]')?.classList.add('active');
const navTarget=page==='maintenance'?'maintenances-all':page;
document.querySelector('.nav-link[data-page="'+navTarget+'"]')?.classList.add('active');
if(page==='dashboard')loadDashboard();
else if(page==='vehicles')loadVehicles();
else if(page==='vehicle')loadVehicleDetail(params.id);
else if(page==='maintenance')loadMaintenanceDetail(params.id);
else if(page==='parts')loadParts();
else if(page==='maintenances-all')loadAllMaintenances();
}
// API
// ─── API ──────────────────────────────────────────────────────────────────────
async function api(action,method='GET',data=null,extra=''){
const opts={method,headers:{}};
if(data&&!(data instanceof FormData)){opts.headers['Content-Type']='application/json';opts.body=JSON.stringify(data);}
@@ -40,7 +46,7 @@ async function api(action,method='GET',data=null,extra=''){
return j.data;
}
// DASHBOARD
// ─── DASHBOARD ────────────────────────────────────────────────────────────────
async function loadDashboard(){
try{
const[stats,vehicles,reminders]=await Promise.all([api('stats'),api('vehicles'),api('maintenances')]);
@@ -55,42 +61,42 @@ async function loadDashboard(){
function renderDashboardVehicles(list){
const el=$('#dashboard-vehicles');
if(!list.length){el.innerHTML='<div class="empty-state"><div class="icon">\ud83d\ude97</div><p>Aucun v\u00e9hicule</p></div>';return;}
if(!list.length){el.innerHTML='<div class="empty-state"><div class="icon">🚗</div><p>Aucun véhicule</p></div>';return;}
el.innerHTML=list.slice(0,6).map(v=>vehicleCardHTML(v)).join('');
$$('.vehicle-card',el).forEach(c=>c.addEventListener('click',()=>navigate('vehicle',{id:c.dataset.id})));
}
function vehicleCardHTML(v){
const photoHTML=v.photo?'<img src="'+UPLOADS+v.photo+'" alt="">':'<span class="no-photo">\ud83d\ude97</span>';
const photoHTML=v.photo?'<img src="'+UPLOADS+v.photo+'" alt="">':'<span class="no-photo">🚗</span>';
const fuel=v.fuel_type||'Essence';
const fuelClass='fuel-'+fuel.replace(/\s/g,'');
return '<div class="vehicle-card" data-id="'+v.id+'">'+
'<div class="vehicle-card-img">'+photoHTML+'<span class="fuel-badge '+fuelClass+'">'+fuel+'</span></div>'+
'<div class="vehicle-card-img">'+photoHTML+'<span class="fuel-badge '+fuelClass+'">'+escHtml(fuel)+'</span></div>'+
'<div class="vehicle-card-body">'+
'<div class="vehicle-card-title">'+v.name+'</div>'+
'<div class="vehicle-card-sub">'+v.brand+' '+v.model+(v.year?' &middot; '+v.year:'')+'</div>'+
'<div class="vehicle-card-title">'+escHtml(v.name)+'</div>'+
'<div class="vehicle-card-sub">'+escHtml(v.brand)+' '+escHtml(v.model)+(v.year?' &middot; '+v.year:'')+'</div>'+
'<div class="vehicle-card-stats">'+
'<div class="vc-stat">\ud83d\udd27 <strong>'+(v.maintenance_count||0)+'</strong> entretiens</div>'+
'<div class="vc-stat">\ud83d\udcb6 <strong>'+fmtPrice(v.total_cost)+'</strong></div>'+
'<div class="vc-stat">🔧 <strong>'+(v.maintenance_count||0)+'</strong> entretiens</div>'+
'<div class="vc-stat">💶 <strong>'+fmtPrice(v.total_cost)+'</strong></div>'+
'</div>'+
'</div>'+
'<div class="vehicle-card-footer">'+
(v.license_plate?'<span class="plate">'+v.license_plate+'</span>':'<span></span>')+
(v.license_plate?'<span class="plate">'+escHtml(v.license_plate)+'</span>':'<span></span>')+
'<span class="vc-stat">'+(v.current_km?fmt(v.current_km)+' km':'--')+'</span>'+
'</div></div>';
}
function renderReminders(list){
const el=$('#reminders-list');const today=new Date();
if(!list.length){el.innerHTML='<div class="empty-state" style="padding:1.5rem"><p style="color:var(--muted)">Aucun rappel configur\u00e9</p></div>';return;}
el.innerHTML='<div class="table-wrap"><table><thead><tr><th>V\u00e9hicule</th><th>Type</th><th>Prochaine date</th><th>Prochain km</th><th>\u00c9cart km</th></tr></thead><tbody>'+
if(!list.length){el.innerHTML='<div class="empty-state" style="padding:1.5rem"><p style="color:var(--muted)">Aucun rappel configuré</p></div>';return;}
el.innerHTML='<div class="table-wrap"><table><thead><tr><th>Véhicule</th><th>Type</th><th>Prochaine date</th><th>Prochain km</th><th>Écart km</th></tr></thead><tbody>'+
list.map(r=>{
const diff=r.next_km&&r.current_km?r.next_km-r.current_km:null;
const dateOk=r.next_date?new Date(r.next_date)>today:true;
const kmOk=diff==null||diff>0;
const cls=(!dateOk||!kmOk)?'badge-red':diff!=null&&diff<2000?'badge-amber':'badge-green';
return '<tr><td><strong>'+r.vehicle_name+'</strong>'+(r.license_plate?' <span class="plate">'+r.license_plate+'</span>':'')+'</td>'+
'<td>'+r.type+'</td>'+
return '<tr><td><strong>'+escHtml(r.vehicle_name)+'</strong>'+(r.license_plate?' <span class="plate">'+escHtml(r.license_plate)+'</span>':'')+'</td>'+
'<td>'+escHtml(r.type)+'</td>'+
'<td>'+(r.next_date?'<span class="badge '+cls+'">'+fmtDate(r.next_date)+'</span>':'--')+'</td>'+
'<td>'+(r.next_km?fmt(r.next_km)+' km':'--')+'</td>'+
'<td>'+(diff!=null?'<span class="badge '+(diff<0?'badge-red':diff<2000?'badge-amber':'badge-green')+'">'+(diff>=0?'+':'')+fmt(diff)+' km</span>':'--')+'</td></tr>';
@@ -98,19 +104,18 @@ function renderReminders(list){
'</tbody></table></div>';
}
// VEHICLES LIST
// ─── VEHICLES LIST ────────────────────────────────────────────────────────────
async function loadVehicles(){
try{
const list=await api('vehicles');
const el=$('#vehicles-grid');
if(!list.length){el.innerHTML='<div class="empty-state"><div class="icon">\ud83d\ude97</div><p>Aucun v\u00e9hicule. Ajoutez-en un !</p></div>';return;}
if(!list.length){el.innerHTML='<div class="empty-state"><div class="icon">🚗</div><p>Aucun véhicule. Ajoutez-en un !</p></div>';return;}
el.innerHTML=list.map(v=>vehicleCardHTML(v)).join('');
$$('.vehicle-card',el).forEach(c=>c.addEventListener('click',()=>navigate('vehicle',{id:c.dataset.id})));
}catch(e){toast(e.message,'error');}
}
// VEHICLE DETAIL
let currentVehicleId=null;
// ─── VEHICLE DETAIL ───────────────────────────────────────────────────────────
async function loadVehicleDetail(id){
currentVehicleId=id;
try{
@@ -122,36 +127,36 @@ async function loadVehicleDetail(id){
renderVehicleHeader(v);
renderMaintenances(maintenances);
renderVehicleParts(parts);
document.title='GarageManager \u00b7 '+v.name;
document.title='GarageManager · '+v.name;
}catch(e){toast(e.message,'error');}
}
function renderVehicleHeader(v){
const photoHTML=v.photo?'<img src="'+UPLOADS+v.photo+'" alt="">':'<div class="no-photo">\ud83d\ude97</div>';
const photoHTML=v.photo?'<img src="'+UPLOADS+v.photo+'" alt="">':'<div class="no-photo">🚗</div>';
const totalCost=(parseFloat(v.stats?.total||0)+parseFloat(v.parts_stats?.total||0)).toFixed(2);
$('#vehicle-header').innerHTML=
'<div class="vehicle-detail-header">'+
'<div class="vehicle-detail-img">'+photoHTML+'</div>'+
'<div class="vehicle-meta">'+
'<h2>'+v.name+'</h2>'+
'<p class="sub">'+v.brand+' '+v.model+(v.year?' \u00b7 '+v.year:'')+'</p>'+
'<h2>'+escHtml(v.name)+'</h2>'+
'<p class="sub">'+escHtml(v.brand)+' '+escHtml(v.model)+(v.year?' · '+v.year:'')+'</p>'+
'<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:.75rem">'+
(v.license_plate?'<span class="plate">'+v.license_plate+'</span>':'')+
(v.license_plate?'<span class="plate">'+escHtml(v.license_plate)+'</span>':'')+
'<span class="badge badge-blue">'+(v.fuel_type||'Essence')+'</span>'+
(v.color?'<span class="badge badge-gray">\ud83c\udfa8 '+v.color+'</span>':'')+
(v.color?'<span class="badge badge-gray">🎨 '+escHtml(v.color)+'</span>':'')+
'</div>'+
'<div class="meta-grid">'+
'<div class="meta-item"><span class="k">Kilom\u00e9trage</span><br><span class="v">'+fmt(v.current_km)+' km</span></div>'+
'<div class="meta-item"><span class="k">Kilométrage</span><br><span class="v">'+fmt(v.current_km)+' km</span></div>'+
'<div class="meta-item"><span class="k">Entretiens</span><br><span class="v">'+(v.stats?.cnt||0)+'</span></div>'+
'<div class="meta-item"><span class="k">Co\u00fbt total</span><br><span class="v">'+fmtPrice(totalCost)+'</span></div>'+
'<div class="meta-item"><span class="k">Coût total</span><br><span class="v">'+fmtPrice(totalCost)+'</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">'+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 style="display:flex;gap:.5rem;flex-wrap:wrap;align-self:flex-start">'+
'<button class="btn btn-secondary btn-sm" onclick="openEditVehicle('+v.id+')">\u270f\ufe0f Modifier</button>'+
'<button class="btn btn-secondary btn-sm" onclick="openUploadPhoto('+v.id+')">\ud83d\udcf7 Photo</button>'+
'<button class="btn btn-danger btn-sm" onclick="deleteVehicle('+v.id+')">\ud83d\uddd1\ufe0f Supprimer</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-danger btn-sm" onclick="deleteVehicle('+v.id+')">🗑️ Supprimer</button>'+
'</div>'+
'</div>';
}
@@ -160,17 +165,20 @@ function renderMaintenances(list){
const el=$('#maintenance-list');
const totalCost=list.reduce((s,m)=>s+parseFloat(m.cost||0)+parseFloat(m.parts_cost||0),0);
$('#maintenance-total').textContent=fmtPrice(totalCost);
if(!list.length){el.innerHTML='<div class="empty-state"><div class="icon">\ud83d\udd27</div><p>Aucun entretien enregistr\u00e9</p></div>';return;}
el.innerHTML='<div class="table-wrap"><table><thead><tr><th>Date</th><th>Type</th><th>Description</th><th>Kilom\u00e9trage</th><th>Co\u00fbt MO</th><th>Pi\u00e8ces</th><th>Prochain</th><th></th></tr></thead><tbody>'+
list.map(m=>'<tr>'+
if(!list.length){el.innerHTML='<div class="empty-state"><div class="icon">🔧</div><p>Aucun entretien enregistré</p></div>';return;}
el.innerHTML='<div class="table-wrap"><table><thead><tr><th>Date</th><th>Type</th><th>Description</th><th>Kilométrage</th><th>Coût MO</th><th>Pièces</th><th>Prochain</th><th></th></tr></thead><tbody>'+
list.map(m=>'<tr style="cursor:pointer" onclick="navigate(\'maintenance\',{id:'+m.id+'})">'+
'<td><strong>'+fmtDate(m.date)+'</strong><br><span style="color:var(--muted);font-size:.75rem">'+ago(m.date)+'</span></td>'+
'<td><span class="badge badge-blue">'+m.type+'</span></td>'+
'<td style="max-width:200px;color:var(--muted)">'+(m.description||'--')+'</td>'+
'<td><span class="badge badge-blue">'+escHtml(m.type)+'</span></td>'+
'<td style="max-width:200px;color:var(--muted)">'+(m.description?escHtml(m.description):'--')+'</td>'+
'<td>'+(m.km?fmt(m.km)+' km':'--')+'</td>'+
'<td>'+fmtPrice(m.cost)+'</td>'+
'<td>'+(m.parts_count>0?'<span class="badge badge-purple">\ud83d\udd29 '+m.parts_count+' ('+fmtPrice(m.parts_cost)+')</span>':'--')+'</td>'+
'<td style="font-size:.78rem">'+(m.next_date?'\ud83d\udcc5 '+fmtDate(m.next_date):'')+' '+(m.next_km?'<br>\ud83d\udee3\ufe0f '+fmt(m.next_km)+' km':'')+'</td>'+
'<td><button class="btn btn-danger btn-sm btn-icon" onclick="deleteMaintenance('+m.id+')" title="Supprimer">\ud83d\uddd1\ufe0f</button></td>'+
'<td>'+(m.parts_count>0?'<span class="badge badge-purple">🔩 '+m.parts_count+' ('+fmtPrice(m.parts_cost)+')</span>':'--')+'</td>'+
'<td style="font-size:.78rem">'+(m.next_date?'📅 '+fmtDate(m.next_date):'')+' '+(m.next_km?'<br>🛣️ '+fmt(m.next_km)+' km':'')+'</td>'+
'<td onclick="event.stopPropagation()">'+
'<button class="btn btn-secondary btn-sm btn-icon" onclick="openEditMaintenance('+m.id+')" title="Modifier">✏️</button> '+
'<button class="btn btn-danger btn-sm btn-icon" onclick="deleteMaintenance('+m.id+')" title="Supprimer">🗑️</button>'+
'</td>'+
'</tr>').join('')+
'</tbody></table></div>';
}
@@ -179,24 +187,180 @@ function renderVehicleParts(list){
const el=$('#parts-list-vehicle');
const total=list.reduce((s,p)=>s+parseFloat(p.price||0)*parseInt(p.quantity||1),0);
$('#parts-total-vehicle').textContent=fmtPrice(total);
if(!list.length){el.innerHTML='<div class="empty-state"><div class="icon">\ud83d\udd29</div><p>Aucune pi\u00e8ce enregistr\u00e9e</p></div>';return;}
el.innerHTML='<div class="table-wrap"><table><thead><tr><th>Photo</th><th>Nom</th><th>Marque</th><th>R\u00e9f\u00e9rence</th><th>Cat\u00e9gorie</th><th>Prix unit.</th><th>Qt\u00e9</th><th>Total</th><th>Fournisseur</th><th></th></tr></thead><tbody>'+
if(!list.length){el.innerHTML='<div class="empty-state"><div class="icon">🔩</div><p>Aucune pièce enregistrée</p></div>';return;}
el.innerHTML='<div class="table-wrap"><table><thead><tr><th>Photo</th><th>Nom</th><th>Marque</th><th>Référence</th><th>Catégorie</th><th>Prix unit.</th><th>Qté</th><th>Total</th><th>Entretien</th><th></th></tr></thead><tbody>'+
list.map(p=>'<tr>'+
'<td>'+(p.photo?'<img class="part-thumb" src="'+UPLOADS+p.photo+'" alt="">':'<span style="color:var(--muted)">--</span>')+'</td>'+
'<td><strong>'+p.name+'</strong></td>'+
'<td>'+(p.brand||'--')+'</td>'+
'<td><code style="font-size:.75rem;color:var(--muted)">'+(p.reference||'--')+'</code></td>'+
'<td><span class="badge badge-gray">'+p.category+'</span></td>'+
'<td><strong>'+escHtml(p.name)+'</strong></td>'+
'<td>'+(p.brand?escHtml(p.brand):'--')+'</td>'+
'<td><code style="font-size:.75rem;color:var(--muted)">'+(p.reference?escHtml(p.reference):'--')+'</code></td>'+
'<td><span class="badge badge-gray">'+escHtml(p.category)+'</span></td>'+
'<td>'+fmtPrice(p.price)+'</td>'+
'<td>'+p.quantity+' '+p.unit+'</td>'+
'<td>'+p.quantity+' '+escHtml(p.unit||'pièce')+'</td>'+
'<td><strong>'+fmtPrice(parseFloat(p.price||0)*parseInt(p.quantity||1))+'</strong></td>'+
'<td>'+(p.supplier||'--')+'</td>'+
'<td><button class="btn btn-danger btn-sm btn-icon" onclick="deletePart('+p.id+')" title="Supprimer">\ud83d\uddd1\ufe0f</button></td>'+
'<td style="font-size:.78rem">'+(p.maintenance_type?escHtml(p.maintenance_type)+' ('+fmtDate(p.maintenance_date)+')':'--')+'</td>'+
'<td>'+
'<button class="btn btn-secondary btn-sm btn-icon" onclick="openEditPart('+p.id+')" title="Modifier">✏️</button> '+
'<button class="btn btn-danger btn-sm btn-icon" onclick="deletePart('+p.id+')" title="Supprimer">🗑️</button>'+
'</td>'+
'</tr>').join('')+
'</tbody></table></div>';
}
// ALL PARTS
// ─── MAINTENANCE DETAIL ───────────────────────────────────────────────────────
async function loadMaintenanceDetail(id){
currentMaintenanceId=id;
try{
const[m,parts]=await Promise.all([
api('maintenances','GET',null,'&id='+id),
api('parts','GET',null,'&maintenance_id='+id)
]);
currentVehicleId=m.vehicle_id;
renderMaintenanceDetailHeader(m);
renderMaintenanceParts(parts,id,m.vehicle_id);
loadKnownParts(m.vehicle_id);
const f=document.getElementById('inline-part-form');
if(f)f.style.display='none';
}catch(e){toast(e.message,'error');}
}
function renderMaintenanceDetailHeader(m){
document.getElementById('maintenance-detail-header').innerHTML=
'<div class="vehicle-detail-header" style="align-items:flex-start">'+
'<div class="vehicle-meta" style="flex:1">'+
'<div style="display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;margin-bottom:.5rem">'+
'<span class="badge badge-blue" style="font-size:.95rem;padding:.35rem .8rem">'+escHtml(m.type)+'</span>'+
'<span style="color:var(--muted)">'+fmtDate(m.date)+' · '+ago(m.date)+'</span>'+
(m.vehicle_name?'<span style="color:var(--muted);font-size:.85rem">🚗 <a href="#" onclick="navigate(\'vehicle\',{id:'+m.vehicle_id+'});return false" style="color:inherit;text-decoration:underline">'+escHtml(m.vehicle_name)+'</a></span>':'')+
'</div>'+
(m.description?'<p style="margin-bottom:.75rem">'+escHtml(m.description)+'</p>':'')+
'<div class="meta-grid">'+
(m.km?'<div class="meta-item"><span class="k">Kilométrage</span><br><span class="v">'+fmt(m.km)+' km</span></div>':'')+
'<div class="meta-item"><span class="k">Coût M.O.</span><br><span class="v">'+fmtPrice(m.cost)+'</span></div>'+
(m.mechanic?'<div class="meta-item"><span class="k">Mécanicien</span><br><span class="v">'+escHtml(m.mechanic)+'</span></div>':'')+
(m.garage_name?'<div class="meta-item"><span class="k">Garage</span><br><span class="v">'+escHtml(m.garage_name)+'</span></div>':'')+
(m.next_date?'<div class="meta-item"><span class="k">Prochain entretien</span><br><span class="v">📅 '+fmtDate(m.next_date)+'</span></div>':'')+
(m.next_km?'<div class="meta-item"><span class="k">Prochain km</span><br><span class="v">🛣️ '+fmt(m.next_km)+' km</span></div>':'')+
'</div>'+
(m.notes?'<p style="margin-top:.75rem;color:var(--muted);font-size:.85rem;font-style:italic">'+escHtml(m.notes)+'</p>':'')+
'</div>'+
'<div style="display:flex;gap:.5rem;flex-wrap:wrap;align-self:flex-start">'+
'<button class="btn btn-secondary btn-sm" onclick="openEditMaintenance('+m.id+')">✏️ Modifier</button>'+
'<button class="btn btn-danger btn-sm" onclick="deleteMaintenance('+m.id+')">🗑️ Supprimer</button>'+
'</div>'+
'</div>';
}
function renderMaintenanceParts(list,mid,vid){
const el=document.getElementById('maintenance-parts-list');
const cnt=document.getElementById('maint-parts-count');
if(cnt)cnt.textContent=list.length?list.length+' pièce'+(list.length>1?'s':''):'';
if(!list.length){el.innerHTML='<div class="empty-state" style="padding:2rem"><div class="icon">🔩</div><p>Aucune pièce utilisée</p></div>';return;}
const total=list.reduce((s,p)=>s+parseFloat(p.price||0)*parseInt(p.quantity||1),0);
el.innerHTML='<div class="table-wrap"><table><thead><tr><th>Photo</th><th>Nom</th><th>Marque</th><th>Réf.</th><th>Catégorie</th><th>Prix unit.</th><th>Qté</th><th>Total</th><th></th></tr></thead><tbody>'+
list.map(p=>'<tr>'+
'<td>'+(p.photo?'<img class="part-thumb" src="'+UPLOADS+p.photo+'" alt="">':'--')+'</td>'+
'<td><strong>'+escHtml(p.name)+'</strong></td>'+
'<td>'+(p.brand?escHtml(p.brand):'--')+'</td>'+
'<td><code style="font-size:.75rem;color:var(--muted)">'+(p.reference?escHtml(p.reference):'--')+'</code></td>'+
'<td><span class="badge badge-gray">'+escHtml(p.category)+'</span></td>'+
'<td>'+fmtPrice(p.price)+'</td>'+
'<td>'+p.quantity+' '+escHtml(p.unit||'pièce')+'</td>'+
'<td><strong>'+fmtPrice(parseFloat(p.price||0)*parseInt(p.quantity||1))+'</strong></td>'+
'<td>'+
'<button class="btn btn-secondary btn-sm btn-icon" onclick="openEditPart('+p.id+')" title="Modifier">✏️</button> '+
'<button class="btn btn-danger btn-sm btn-icon" onclick="deleteMaintPart('+p.id+','+mid+','+vid+')" title="Supprimer">🗑️</button>'+
'</td>'+
'</tr>').join('')+
'<tr style="background:rgba(0,0,0,.02)"><td colspan="7" style="text-align:right;font-weight:600;color:var(--muted)">Total pièces</td><td><strong>'+fmtPrice(total)+'</strong></td><td></td></tr>'+
'</tbody></table></div>';
}
async function deleteMaintPart(id,mid,vid){
if(!confirm('Supprimer cette pièce ?'))return;
try{
await api('parts','DELETE',null,'&id='+id);
toast('Pièce supprimée');
const parts=await api('parts','GET',null,'&maintenance_id='+mid);
renderMaintenanceParts(parts,mid,vid);
}catch(e){toast(e.message,'error');}
}
// ─── INLINE PART FORM ────────────────────────────────────────────────────────
function toggleInlinePartForm(){
const f=document.getElementById('inline-part-form');
if(!f)return;
const visible=f.style.display==='block';
f.style.display=visible?'none':'block';
if(!visible)document.getElementById('ipart-name')?.focus();
}
async function loadKnownParts(vehicleId){
try{
const parts=await api('parts','GET',null,'&vehicle_id='+vehicleId);
const el=document.getElementById('known-parts-list');
if(!el)return;
if(!parts.length){el.innerHTML='<span style="color:var(--muted);font-size:.8rem">Aucune pièce connue</span>';return;}
const seen=new Set();
const unique=parts.filter(p=>{if(seen.has(p.name))return false;seen.add(p.name);return true;}).slice(0,15);
el.innerHTML=unique.map(p=>
'<button class="badge badge-gray" style="cursor:pointer;border:none;padding:.3rem .6rem;font-size:.78rem" '+
'data-name="'+escHtml(p.name)+'" data-brand="'+escHtml(p.brand||'')+'" data-ref="'+escHtml(p.reference||'')+'" data-price="'+p.price+'">'+escHtml(p.name)+'</button>'
).join('');
$$('button[data-name]',el).forEach(btn=>btn.addEventListener('click',()=>{
const n=document.getElementById('ipart-name');const b=document.getElementById('ipart-brand');
const r=document.getElementById('ipart-reference');const pr=document.getElementById('ipart-price');
if(n)n.value=btn.dataset.name;
if(b)b.value=btn.dataset.brand;
if(r)r.value=btn.dataset.ref;
if(pr){pr.value=btn.dataset.price;updateIPriceTTC();}
}));
}catch(e){}
}
function updateIPriceTTC(){
const ht=document.getElementById('ipart-prix-ht')?.checked;
const price=parseFloat(document.getElementById('ipart-price')?.value)||0;
const preview=document.getElementById('ipart-ttc-preview');
if(!preview)return;
if(ht&&price>0){preview.textContent='→ Prix TTC : '+(price*1.2).toFixed(2)+' €';preview.style.display='block';}
else{preview.style.display='none';}
}
async function saveInlinePart(){
const name=document.getElementById('ipart-name')?.value.trim();
if(!name){toast('Nom requis','error');return;}
let price=parseFloat(document.getElementById('ipart-price')?.value)||0;
if(document.getElementById('ipart-prix-ht')?.checked)price=parseFloat((price*1.2).toFixed(2));
const fd=new FormData();
fd.append('name',name);
fd.append('vehicle_id',currentVehicleId||'');
fd.append('maintenance_id',currentMaintenanceId||'');
fd.append('price',price);
fd.append('quantity',parseInt(document.getElementById('ipart-quantity')?.value)||1);
fd.append('category','Autre');
fd.append('unit','pièce');
const brand=document.getElementById('ipart-brand')?.value.trim();
const ref=document.getElementById('ipart-reference')?.value.trim();
if(brand)fd.append('brand',brand);
if(ref)fd.append('reference',ref);
const photoFile=document.getElementById('ipart-photo')?.files[0];
if(photoFile)fd.append('photo',photoFile);
try{
await api('parts','POST',fd);
toast('Pièce ajoutée');
['ipart-name','ipart-brand','ipart-reference'].forEach(id=>{const el=document.getElementById(id);if(el)el.value='';});
const pr=document.getElementById('ipart-price');if(pr)pr.value='0';
const qty=document.getElementById('ipart-quantity');if(qty)qty.value='1';
const ht=document.getElementById('ipart-prix-ht');if(ht)ht.checked=false;
const prev=document.getElementById('ipart-ttc-preview');if(prev)prev.style.display='none';
const ph=document.getElementById('ipart-photo');if(ph)ph.value='';
const parts=await api('parts','GET',null,'&maintenance_id='+currentMaintenanceId);
renderMaintenanceParts(parts,currentMaintenanceId,currentVehicleId);
}catch(e){toast(e.message,'error');}
}
// ─── ALL PARTS ────────────────────────────────────────────────────────────────
async function loadParts(){
try{
const list=await api('parts');
@@ -204,34 +368,32 @@ async function loadParts(){
const total=list.reduce((s,p)=>s+parseFloat(p.price||0)*parseInt(p.quantity||1),0);
$('#all-parts-total').textContent=fmtPrice(total);
$('#all-parts-count').textContent=list.length;
if(!list.length){el.innerHTML='<div class="empty-state"><div class="icon">\ud83d\udd29</div><p>Aucune pi\u00e8ce</p></div>';return;}
// Grouper par v\u00e9hicule
if(!list.length){el.innerHTML='<div class="empty-state"><div class="icon">🔩</div><p>Aucune pièce</p></div>';return;}
const byVehicle={};
list.forEach(p=>{
const key=p.vehicle_name||'Sans v\u00e9hicule';
if(!byVehicle[key])byVehicle[key]=[];
byVehicle[key].push(p);
});
list.forEach(p=>{const key=p.vehicle_name||'Sans véhicule';if(!byVehicle[key])byVehicle[key]=[];byVehicle[key].push(p);});
let html='';
for(const[vname,parts] of Object.entries(byVehicle)){
const vtotal=parts.reduce((s,p)=>s+parseFloat(p.price||0)*parseInt(p.quantity||1),0);
html+='<div style="margin-bottom:2rem">'+
'<div class="card-header" style="margin-bottom:.5rem">'+
'<h3 style="font-size:1rem;font-weight:700">\ud83d\ude97 '+escHtml(vname)+'</h3>'+
'<span style="margin-left:auto;color:var(--muted);font-size:.85rem">'+parts.length+' pi\u00e8ces \u00b7 '+fmtPrice(vtotal)+'</span>'+
'<h3 style="font-size:1rem;font-weight:700">🚗 '+escHtml(vname)+'</h3>'+
'<span style="margin-left:auto;color:var(--muted);font-size:.85rem">'+parts.length+' pièce'+(parts.length>1?'s':'')+' · '+fmtPrice(vtotal)+'</span>'+
'</div>'+
'<div class="table-wrap"><table><thead><tr><th>Photo</th><th>Nom</th><th>Marque</th><th>R\u00e9f\u00e9rence</th><th>Cat\u00e9gorie</th><th>Prix unit.</th><th>Qt\u00e9</th><th>Total</th><th>Entretien</th><th></th></tr></thead><tbody>'+
'<div class="table-wrap"><table><thead><tr><th>Photo</th><th>Nom</th><th>Marque</th><th>Référence</th><th>Catégorie</th><th>Prix unit.</th><th>Qté</th><th>Total</th><th>Entretien</th><th></th></tr></thead><tbody>'+
parts.map(p=>'<tr>'+
'<td>'+(p.photo?'<img class="part-thumb" src="'+UPLOADS+p.photo+'" alt="">':'--')+'</td>'+
'<td><strong>'+escHtml(p.name)+'</strong></td>'+
'<td>'+(p.brand||'--')+'</td>'+
'<td><code style="font-size:.75rem;color:var(--muted)">'+(p.reference||'--')+'</code></td>'+
'<td><span class="badge badge-gray">'+p.category+'</span></td>'+
'<td>'+(p.brand?escHtml(p.brand):'--')+'</td>'+
'<td><code style="font-size:.75rem;color:var(--muted)">'+(p.reference?escHtml(p.reference):'--')+'</code></td>'+
'<td><span class="badge badge-gray">'+escHtml(p.category)+'</span></td>'+
'<td>'+fmtPrice(p.price)+'</td>'+
'<td>'+p.quantity+' '+p.unit+'</td>'+
'<td>'+p.quantity+' '+escHtml(p.unit||'pièce')+'</td>'+
'<td><strong>'+fmtPrice(parseFloat(p.price||0)*parseInt(p.quantity||1))+'</strong></td>'+
'<td>'+(p.maintenance_type?p.maintenance_type+' ('+fmtDate(p.maintenance_date)+')':'--')+'</td>'+
'<td><button class="btn btn-danger btn-sm btn-icon" onclick="deletePart('+p.id+')" title="Supprimer">\ud83d\uddd1\ufe0f</button></td>'+
'<td style="font-size:.78rem">'+(p.maintenance_type?escHtml(p.maintenance_type)+' ('+fmtDate(p.maintenance_date)+')':'--')+'</td>'+
'<td>'+
'<button class="btn btn-secondary btn-sm btn-icon" onclick="openEditPart('+p.id+')" title="Modifier">✏️</button> '+
'<button class="btn btn-danger btn-sm btn-icon" onclick="deletePart('+p.id+')" title="Supprimer">🗑️</button>'+
'</td>'+
'</tr>').join('')+
'</tbody></table></div></div>';
}
@@ -239,28 +401,79 @@ async function loadParts(){
}catch(e){toast(e.message,'error');}
}
// MODALS
// ─── ALL MAINTENANCES ─────────────────────────────────────────────────────────
async function loadAllMaintenances(){
try{
const vehicles=await api('vehicles');
const el=$('#all-maintenances-list');
if(!vehicles.length){el.innerHTML='<div class="empty-state"><div class="icon">🔧</div><p>Aucun véhicule</p></div>';return;}
let totalAll=0,countAll=0,html='';
for(const v of vehicles){
const mlist=await api('maintenances',undefined,null,'&vehicle_id='+v.id);
if(!mlist.length)continue;
const total=mlist.reduce((s,m)=>s+parseFloat(m.cost||0)+parseFloat(m.parts_cost||0),0);
totalAll+=total;countAll+=mlist.length;
html+='<div style="margin-bottom:2rem">'+
'<div class="card-header" style="margin-bottom:.5rem">'+
'<h3 style="font-size:1rem;font-weight:700">🚗 '+escHtml(v.name)+'</h3>'+
(v.license_plate?'<span class="plate">'+escHtml(v.license_plate)+'</span>':'')+
'<span style="margin-left:auto;color:var(--muted);font-size:.85rem">'+mlist.length+' entretiens · '+fmtPrice(total)+'</span>'+
'</div>'+
'<div class="table-wrap"><table><thead><tr><th>Date</th><th>Type</th><th>Description</th><th>Km</th><th>Coût M.O.</th><th>Pièces</th><th>Total</th><th>Mécanicien</th><th></th></tr></thead><tbody>'+
mlist.map(m=>'<tr style="cursor:pointer" onclick="navigate(\'maintenance\',{id:'+m.id+'})">'+
'<td>'+fmtDate(m.date)+'</td>'+
'<td><span class="badge badge-gray">'+escHtml(m.type)+'</span></td>'+
'<td>'+(m.description?escHtml(m.description):'--')+'</td>'+
'<td>'+(m.km?fmt(m.km)+' km':'--')+'</td>'+
'<td>'+fmtPrice(m.cost)+'</td>'+
'<td>'+(m.parts_count>0?m.parts_count+' ('+fmtPrice(m.parts_cost)+')':'--')+'</td>'+
'<td><strong>'+fmtPrice(parseFloat(m.cost||0)+parseFloat(m.parts_cost||0))+'</strong></td>'+
'<td>'+(m.mechanic?escHtml(m.mechanic):'--')+'</td>'+
'<td onclick="event.stopPropagation()">'+
'<button class="btn btn-danger btn-sm btn-icon" onclick="deleteMaintenance('+m.id+')" title="Supprimer">🗑️</button>'+
'</td>'+
'</tr>').join('')+
'</tbody></table></div></div>';
}
$('#all-maint-count').textContent=countAll;
$('#all-maint-total').textContent=fmtPrice(totalAll);
el.innerHTML=html||'<div class="empty-state"><div class="icon">🔧</div><p>Aucun entretien</p></div>';
}catch(e){console.error(e);}
}
// ─── MODALS ───────────────────────────────────────────────────────────────────
function openModal(id){document.getElementById(id).classList.add('show');}
function closeModal(id){document.getElementById(id).classList.remove('show');}
// VEHICLE CRUD
// ─── TVA ──────────────────────────────────────────────────────────────────────
function updatePartPriceTTC(){
const ht=document.getElementById('part-prix-ht')?.checked;
const price=parseFloat(document.getElementById('part-price')?.value)||0;
const preview=document.getElementById('part-ttc-preview');
if(!preview)return;
if(ht&&price>0){preview.textContent='→ Prix TTC : '+(price*1.2).toFixed(2)+' €';preview.style.display='block';}
else{preview.style.display='none';}
}
// ─── VEHICLE CRUD ─────────────────────────────────────────────────────────────
function openAddVehicle(){
$('#form-vehicle').reset();$('#form-vehicle-id').value='';
$('#modal-vehicle-title').textContent='Ajouter un v\u00e9hicule';
$('#modal-vehicle-title').textContent='Ajouter un véhicule';
$('#vehicle-photo-preview').src='';$('#vehicle-photo-preview').style.display='none';
openModal('modal-vehicle');
}
function openEditVehicle(id){
api('vehicles','GET',null,'&id='+id).then(v=>{
$('#form-vehicle-id').value=v.id;
$('#modal-vehicle-title').textContent='Modifier le v\u00e9hicule';
$('#modal-vehicle-title').textContent='Modifier le véhicule';
['name','brand','model','year','license_plate','vin','fuel_type','color','purchase_date','purchase_price','current_km','notes'].forEach(f=>{
const el=$('#vehicle-'+f.replace(/_/g,'-'));
if(el)el.value=v[f]||'';
if(el)el.value=v[f]??'';
});
if(v.photo){$('#vehicle-photo-preview').src=UPLOADS+v.photo;$('#vehicle-photo-preview').style.display='block';}
else{$('#vehicle-photo-preview').style.display='none';}
openModal('modal-vehicle');
});
}).catch(e=>toast(e.message,'error'));
}
async function saveVehicle(){
const id=$('#form-vehicle-id').value;
@@ -271,10 +484,10 @@ async function saveVehicle(){
await api('vehicles','PUT',d,'&id='+id);
const photoFile=$('#vehicle-photo').files[0];
if(photoFile){const pfd=new FormData();pfd.append('photo',photoFile);await fetch(API+'?action=upload_vehicle_photo&id='+id,{method:'POST',body:pfd});}
toast('V\u00e9hicule modifi\u00e9');
toast('Véhicule modifié');
}else{
await api('vehicles','POST',fd);
toast('V\u00e9hicule ajout\u00e9');
toast('Véhicule ajouté');
}
closeModal('modal-vehicle');
if(currentPage==='vehicles')loadVehicles();
@@ -283,11 +496,11 @@ async function saveVehicle(){
}catch(e){toast(e.message,'error');}
}
async function deleteVehicle(id){
if(!confirm('Supprimer ce v\u00e9hicule et tout son historique ?'))return;
try{await api('vehicles','DELETE',null,'&id='+id);toast('V\u00e9hicule supprim\u00e9');navigate('vehicles');}catch(e){toast(e.message,'error');}
if(!confirm('Supprimer ce véhicule et tout son historique ?'))return;
try{await api('vehicles','DELETE',null,'&id='+id);toast('Véhicule supprimé');navigate('vehicles');}catch(e){toast(e.message,'error');}
}
// MAINTENANCE CRUD
// ─── MAINTENANCE CRUD ─────────────────────────────────────────────────────────
function openAddMaintenance(){
$('#form-maintenance').reset();$('#form-maintenance-id').value='';
$('#maintenance-vehicle-id').value=currentVehicleId||'';
@@ -295,60 +508,101 @@ function openAddMaintenance(){
$('#maintenance-date').value=new Date().toISOString().slice(0,10);
openModal('modal-maintenance');
}
function openEditMaintenance(id){
api('maintenances','GET',null,'&id='+id).then(m=>{
$('#form-maintenance-id').value=m.id;
$('#maintenance-vehicle-id').value=m.vehicle_id;
$('#modal-maintenance-title').textContent="Modifier l'entretien";
const map={type:'maintenance-type',description:'maintenance-description',date:'maintenance-date',
km:'maintenance-km',cost:'maintenance-cost',mechanic:'maintenance-mechanic',
garage_name:'maintenance-garage',next_date:'maintenance-next-date',next_km:'maintenance-next-km',notes:'maintenance-notes'};
for(const[k,elId] of Object.entries(map)){const el=document.getElementById(elId);if(el)el.value=m[k]??'';}
openModal('modal-maintenance');
}).catch(e=>toast(e.message,'error'));
}
async function saveMaintenance(){
const id=$('#form-maintenance-id').value;
const d={};new FormData($('#form-maintenance')).forEach((v,k)=>d[k]=v);
try{
if(id){await api('maintenances','PUT',d,'&id='+id);toast('Entretien modifi\u00e9');}
else{await api('maintenances','POST',d);toast('Entretien ajout\u00e9');}
if(id){await api('maintenances','PUT',d,'&id='+id);toast('Entretien modifié');}
else{await api('maintenances','POST',d);toast('Entretien ajouté');}
closeModal('modal-maintenance');
loadVehicleDetail(currentVehicleId);
if(currentPage==='maintenance')loadMaintenanceDetail(currentMaintenanceId);
else loadVehicleDetail(currentVehicleId);
}catch(e){toast(e.message,'error');}
}
async function deleteMaintenance(id){
if(!confirm('Supprimer cet entretien ?'))return;
try{await api('maintenances','DELETE',null,'&id='+id);toast('Entretien supprim\u00e9');loadVehicleDetail(currentVehicleId);}catch(e){toast(e.message,'error');}
try{
await api('maintenances','DELETE',null,'&id='+id);
toast('Entretien supprimé');
if(currentPage==='maintenance')navigate('vehicle',{id:currentVehicleId});
else loadVehicleDetail(currentVehicleId);
}catch(e){toast(e.message,'error');}
}
// PARTS CRUD
// ─── PARTS CRUD ───────────────────────────────────────────────────────────────
function openAddPart(){
$('#form-part').reset();$('#form-part-id').value='';
$('#part-vehicle-id').value=currentVehicleId||'';
$('#modal-part-title').textContent='Ajouter une pi\u00e8ce';
$('#part-purchase-date').value=new Date().toISOString().slice(0,10);
$('#part-maintenance-id').value=currentMaintenanceId||'';
$('#modal-part-title').textContent='Ajouter une pièce';
$('#part-photo-preview').src='';$('#part-photo-preview').style.display='none';
const ht=document.getElementById('part-prix-ht');if(ht)ht.checked=false;
const prev=document.getElementById('part-ttc-preview');if(prev)prev.style.display='none';
openModal('modal-part');
}
function openEditPart(id){
api('parts','GET',null,'&id='+id).then(p=>{
$('#form-part-id').value=p.id;
$('#part-vehicle-id').value=p.vehicle_id||'';
$('#part-maintenance-id').value=p.maintenance_id||'';
$('#modal-part-title').textContent='Modifier la pièce';
const map={name:'part-name',category:'part-category',brand:'part-brand',reference:'part-reference',price:'part-price',quantity:'part-quantity',notes:'part-notes'};
for(const[k,elId] of Object.entries(map)){const el=document.getElementById(elId);if(el)el.value=p[k]??'';}
const ht=document.getElementById('part-prix-ht');if(ht)ht.checked=false;
const prev=document.getElementById('part-ttc-preview');if(prev)prev.style.display='none';
if(p.photo){$('#part-photo-preview').src=UPLOADS+p.photo;$('#part-photo-preview').style.display='block';}
else{$('#part-photo-preview').style.display='none';}
openModal('modal-part');
}).catch(e=>toast(e.message,'error'));
}
async function savePart(){
const id=$('#form-part-id').value;
const fd=new FormData($('#form-part'));
if(document.getElementById('part-prix-ht')?.checked){
const htPrice=parseFloat(fd.get('price'))||0;
fd.set('price',(htPrice*1.2).toFixed(2));
}
try{
if(id){
const d={};for(const[k,v]of fd.entries())d[k]=v;
await api('parts','PUT',d,'&id='+id);
const photoFile=$('#part-photo').files[0];
if(photoFile){const pfd=new FormData();pfd.append('photo',photoFile);await fetch(API+'?action=upload_part_photo&id='+id,{method:'POST',body:pfd});}
toast('Pi\u00e8ce modifi\u00e9e');
toast('Pièce modifiée');
}else{
await api('parts','POST',fd);
toast('Pi\u00e8ce ajout\u00e9e');
toast('Pièce ajoutée');
}
closeModal('modal-part');
if(currentPage==='vehicle')loadVehicleDetail(currentVehicleId);
if(currentPage==='maintenance')loadMaintenanceDetail(currentMaintenanceId);
else if(currentPage==='vehicle')loadVehicleDetail(currentVehicleId);
else loadParts();
}catch(e){toast(e.message,'error');}
}
async function deletePart(id){
if(!confirm('Supprimer cette pi\u00e8ce ?'))return;
if(!confirm('Supprimer cette pièce ?'))return;
try{
await api('parts','DELETE',null,'&id='+id);
toast('Pi\u00e8ce supprim\u00e9e');
if(currentPage==='vehicle')loadVehicleDetail(currentVehicleId);
toast('Pièce supprimée');
if(currentPage==='maintenance')loadMaintenanceDetail(currentMaintenanceId);
else if(currentPage==='vehicle')loadVehicleDetail(currentVehicleId);
else loadParts();
}catch(e){toast(e.message,'error');}
}
// UPLOAD PHOTO
// ─── UPLOAD PHOTO ─────────────────────────────────────────────────────────────
function openUploadPhoto(id){$('#upload-vehicle-id').value=id;openModal('modal-upload-photo');}
async function doUploadPhoto(){
const id=$('#upload-vehicle-id').value;
@@ -357,12 +611,12 @@ async function doUploadPhoto(){
const fd=new FormData();fd.append('photo',file);
try{
await fetch(API+'?action=upload_vehicle_photo&id='+id,{method:'POST',body:fd});
toast('Photo mise \u00e0 jour');closeModal('modal-upload-photo');
toast('Photo mise à jour');closeModal('modal-upload-photo');
loadVehicleDetail(id);
}catch(e){toast(e.message,'error');}
}
// TABS
// ─── TABS ────────────────────────────────────────────────────────────────────
function switchTab(tab){
$$('.tab-btn').forEach(b=>b.classList.remove('active'));
$$('.tab-pane').forEach(p=>p.classList.remove('active'));
@@ -370,7 +624,7 @@ function switchTab(tab){
document.getElementById('tab-'+tab)?.classList.add('active');
}
// PHOTO PREVIEW
// ─── PHOTO PREVIEW ────────────────────────────────────────────────────────────
function previewPhoto(inputId,previewId){
const file=document.getElementById(inputId).files[0];if(!file)return;
const reader=new FileReader();
@@ -378,49 +632,7 @@ function previewPhoto(inputId,previewId){
reader.readAsDataURL(file);
}
// ALL MAINTENANCES
async function loadAllMaintenances(){
try{
// Récupérer tous les véhicules puis leurs maintenances
const vehicles=await api('vehicles');
const el=$('#all-maintenances-list');
if(!vehicles.length){el.innerHTML='<div class="empty-state"><div class="icon">🔧</div><p>Aucun véhicule</p></div>';return;}
let totalAll=0, countAll=0;
let html='';
for(const v of vehicles){
const mlist=await api('maintenances',undefined,null,'&vehicle_id='+v.id);
if(!mlist.length)continue;
const total=mlist.reduce((s,m)=>s+parseFloat(m.cost||0)+parseFloat(m.parts_cost||0),0);
totalAll+=total; countAll+=mlist.length;
html+='<div style="margin-bottom:2rem">'+
'<div class="card-header" style="margin-bottom:.5rem">'+
'<h3 style="font-size:1rem;font-weight:700">🚗 '+v.name+'</h3>'+
(v.license_plate?'<span class="plate">'+v.license_plate+'</span>':'')+
'<span style="margin-left:auto;color:var(--muted);font-size:.85rem">'+mlist.length+' entretiens · '+fmtPrice(total)+'</span>'+
'</div>'+
'<div class="table-wrap"><table><thead><tr><th>Date</th><th>Type</th><th>Description</th><th>Km</th><th>Coût M.O.</th><th>Pièces</th><th>Total</th><th>Mécanicien</th><th></th></tr></thead><tbody>'+
mlist.map(m=>'<tr>'+
'<td>'+fmtDate(m.date)+'</td>'+
'<td><span class="badge badge-gray">'+m.type+'</span></td>'+
'<td>'+escHtml(m.description||'--')+'</td>'+
'<td>'+(m.km?fmt(m.km)+' km':'--')+'</td>'+
'<td>'+fmtPrice(m.cost)+'</td>'+
'<td>'+(m.parts_count>0?m.parts_count+' ('+fmtPrice(m.parts_cost)+')':'--')+'</td>'+
'<td><strong>'+fmtPrice(parseFloat(m.cost||0)+parseFloat(m.parts_cost||0))+'</strong></td>'+
'<td>'+(m.mechanic||'--')+'</td>'+
'<td><button class="btn btn-danger btn-sm btn-icon" onclick="deleteMaintenance('+m.id+')" title="Supprimer">🗑️</button></td>'+
'</tr>').join('')+
'</tbody></table></div></div>';
}
$('#all-maint-count').textContent=countAll;
$('#all-maint-total').textContent=fmtPrice(totalAll);
el.innerHTML=html||'<div class="empty-state"><div class="icon">🔧</div><p>Aucun entretien</p></div>';
}catch(e){console.error(e);}
}
function escHtml(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML;}
// INIT
// ─── INIT ────────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded',()=>{
$$('.nav-link').forEach(n=>n.addEventListener('click',()=>navigate(n.dataset.page)));
$$('.modal-backdrop').forEach(m=>m.addEventListener('click',e=>{if(e.target===m)m.classList.remove('show');}));