diff --git a/docker/schema_family.sql b/docker/schema_family.sql
index 5e1e622..ef57872 100644
--- a/docker/schema_family.sql
+++ b/docker/schema_family.sql
@@ -295,7 +295,32 @@ CREATE TABLE IF NOT EXISTS pf_holidays (
status VARCHAR(50) DEFAULT 'draft',
budget_food DECIMAL(10,2) DEFAULT 0,
budget_extra DECIMAL(10,2) DEFAULT 0,
- notes TEXT DEFAULT NULL
+ notes TEXT DEFAULT NULL,
+ vehicle_id INT DEFAULT NULL,
+ return_step_id INT DEFAULT NULL, -- 🔥 NOUVEAU : Point de bascule du retour
+ FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS pf_holidays_items (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ holiday_id INT NOT NULL,
+ category VARCHAR(100),
+ name VARCHAR(255),
+ amount DECIMAL(10,2) DEFAULT 0,
+ is_paid TINYINT(1) DEFAULT 0,
+ location_name VARCHAR(255) DEFAULT NULL,
+ lat DECIMAL(10,7) DEFAULT NULL,
+ lng DECIMAL(10,7) DEFAULT NULL,
+ sort_order INT DEFAULT 0,
+ notes TEXT DEFAULT NULL,
+ item_date DATE DEFAULT NULL,
+ item_time TIME DEFAULT NULL,
+ step_type VARCHAR(20) DEFAULT 'stop',
+ step_start_date DATE DEFAULT NULL,
+ step_end_date DATE DEFAULT NULL,
+ duration INT DEFAULT NULL,
+ expense_context VARCHAR(20) DEFAULT NULL,
+ FOREIGN KEY (holiday_id) REFERENCES pf_holidays(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_holidays_items (
diff --git a/holidays.php b/holidays.php
index 779573d..83139ea 100644
--- a/holidays.php
+++ b/holidays.php
@@ -22,6 +22,9 @@ $pageCss = "/modules/holidays/holidays.css";
require __DIR__ . '/header.php';
+$stmtVehicles = $pdo->query("SELECT id, name FROM pf_vehicles ORDER BY name ASC");
+$garageVehicles = $stmtVehicles->fetchAll(PDO::FETCH_ASSOC);
+
// 3. ROUTEUR DU MODULE VACANCES
if ($tab === 'holiday_detail' && isset($_GET['id'])) {
// Si on demande le détail ET qu'un ID est fourni
diff --git a/migrate-voyages.php b/migrate-voyages.php
new file mode 100644
index 0000000..90b7fd6
--- /dev/null
+++ b/migrate-voyages.php
@@ -0,0 +1,38 @@
+🗺️ Migration : Ajout du Véhicule aux Voyages
console.error("Erreur:", err));
-}
-
// ============================================================================
// MÉTÉO SPÉCIFIQUE AU HEADER DU PLANNING
// ============================================================================
@@ -997,3 +930,76 @@ async function loadWeatherForPlanning(lat, lng, dateStr) {
console.error("Erreur météo planning", e);
}
}
+
+// Gère l'affichage des dates dans la modale d'étape
+function toggleStepDates(type) {
+ const grpEnd = document.getElementById("grp_end_date");
+ const lblStart = document.getElementById("lbl_start_date");
+
+ if (type === "origin") {
+ grpEnd.style.display = "none";
+ lblStart.innerText = "📅 Date de départ";
+ } else if (type === "destination") {
+ grpEnd.style.display = "none";
+ lblStart.innerText = "📅 Date d'arrivée";
+ } else {
+ grpEnd.style.display = "block";
+ lblStart.innerText = tr("hdl_label_arrival");
+ }
+}
+
+// Ajout magique d'une dépense d'essence SÉCURISÉE
+function addQuickTransitExpense(
+ holidayId,
+ sortOrder,
+ amount,
+ description,
+ btnElement,
+) {
+ if (
+ !confirm(
+ `Ajouter une dépense de carburant de ${amount}€ pour cette étape ?`,
+ )
+ )
+ return;
+
+ if (btnElement) {
+ btnElement.disabled = true;
+ btnElement.innerText = "⏳...";
+ btnElement.style.cursor = "not-allowed";
+ btnElement.style.opacity = "0.7";
+ }
+
+ const fd = new FormData();
+ fd.append("action", "add_single_item");
+ fd.append("holiday_id", holidayId);
+ fd.append("sort_order", sortOrder);
+ fd.append("category", "transport");
+ fd.append("name", description);
+ fd.append("amount", amount);
+ fd.append("context", "transit");
+
+ fetch("/modules/holidays/includes/api/save_checkpoint.php", {
+ method: "POST",
+ body: fd,
+ }).then(() => window.location.reload());
+}
+
+// Permet de modifier le prix du carburant à la volée
+function updateFuelPrice() {
+ const currentPrice = window.FUEL_PRICE || 1.85;
+ let newPrice = prompt(
+ "Définit le prix du carburant estimé (€/L) pour tes trajets :",
+ currentPrice,
+ );
+
+ if (newPrice !== null) {
+ newPrice = parseFloat(newPrice.replace(",", "."));
+ if (!isNaN(newPrice) && newPrice > 0) {
+ localStorage.setItem("holidays_fuel_price", newPrice);
+ window.location.reload();
+ } else {
+ alert("Prix invalide.");
+ }
+ }
+}
diff --git a/modules/holidays/includes/api/save_checkpoint.php b/modules/holidays/includes/api/save_checkpoint.php
index 57bcb57..313e8ec 100644
--- a/modules/holidays/includes/api/save_checkpoint.php
+++ b/modules/holidays/includes/api/save_checkpoint.php
@@ -4,26 +4,51 @@ require dirname(__DIR__, 4) . '/includes/auth.php';
require dirname(__DIR__, 4) . '/includes/db.php';
require_login();
-// INTERCEPTION AJAX : Sauvegarde du planning (Drag & Drop / Durée)
-if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime', 'update_item_duration'])) {
- $itemId = (int)$_POST['item_id'];
+// INTERCEPTION AJAX : Sauvegarde du planning (Drag & Drop / Durée) ET Dépense Rapide
+if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime', 'update_item_duration', 'add_single_item'])) {
+ // 🔥 NOUVEAU : Ajout sécurisé d'une dépense unique (Essence OSRM)
+ if ($_POST['action'] === 'add_single_item') {
+ $holiday_id = (int)$_POST['holiday_id'];
+ $sort_order = (int)$_POST['sort_order'];
+
+ // On récupère les infos de l'étape existante pour ne rien casser (lat, lng, dates...)
+ $stmt = $pdo->prepare("SELECT location_name, lat, lng, step_start_date, step_end_date, step_type FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ? LIMIT 1");
+ $stmt->execute([$holiday_id, $sort_order]);
+ $stepInfo = $stmt->fetch();
+
+ if ($stepInfo) {
+ $ins = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, step_start_date, step_end_date, step_type, expense_context, duration) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
+ $ins->execute([
+ $holiday_id, $_POST['category'], $_POST['name'], (float)$_POST['amount'], 0,
+ $stepInfo['location_name'], $stepInfo['lat'], $stepInfo['lng'],
+ $sort_order, $stepInfo['step_start_date'], $stepInfo['step_end_date'],
+ $stepInfo['step_type'], $_POST['context'], 1
+ ]);
+ }
+ echo json_encode(['success' => true]);
+ exit;
+ }
+
+ // --- Ancien code Drag&Drop conservé ---
+ $itemId = (int)$_POST['item_id'];
if ($_POST['action'] === 'update_item_datetime') {
$itemDate = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
$itemTime = !empty($_POST['item_time']) ? $_POST['item_time'] : null;
$stmt = $pdo->prepare("UPDATE pf_holidays_items SET item_date = ?, item_time = ? WHERE id = ?");
$stmt->execute([$itemDate, $itemTime, $itemId]);
- } else {
+ } else if ($_POST['action'] === 'update_item_duration') {
$duration = (int)$_POST['duration'];
$stmt = $pdo->prepare("UPDATE pf_holidays_items SET duration = ? WHERE id = ?");
$stmt->execute([$duration, $itemId]);
}
echo json_encode(['success' => true]);
- exit; // Crucial : on arrête le script ici !
+ exit;
}
$holiday_id = (int)$_POST['holiday_id'];
+// ... (LE RESTE DE TON FICHIER NE CHANGE PAS)
$location_name = trim($_POST['location_name']);
$lat = (float)$_POST['lat'];
$lng = (float)$_POST['lng'];
@@ -53,13 +78,17 @@ if ($holiday_id > 0 && !empty($location_name)) {
$target_order = ($max !== null) ? (int)$max + 1 : 0;
}
- // Récupération des dates de l'étape globale
+ // Récupération des dates de l'étape globale et du type
$step_start = !empty($_POST['step_start_date']) ? $_POST['step_start_date'] : null;
$step_end = !empty($_POST['step_end_date']) ? $_POST['step_end_date'] : null;
- $is_return = isset($_POST['is_return']) ? 1 : 0; // NOUVEAU
+ $step_type = $_POST['step_type'] ?? 'stop';
- // 3. INSERTION DES LIGNES (16 Colonnes)
- $stmt = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, notes, item_date, item_time, step_start_date, step_end_date, duration, is_return) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
+ // Nettoyage des dates selon le type d'étape
+ if ($step_type === 'origin') $step_end = null; // Un départ n'a pas de date de fin
+ if ($step_type === 'destination') $step_end = null; // Une arrivée finale n'a pas de date de départ
+
+ // 3. INSERTION DES LIGNES
+ $stmt = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, notes, item_date, item_time, step_start_date, step_end_date, duration, step_type, expense_context) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$validItemsCount = 0;
if (isset($_POST['items']['name'])) {
@@ -75,30 +104,26 @@ if ($holiday_id > 0 && !empty($location_name)) {
$date = !empty($_POST['items']['date'][$i]) ? $_POST['items']['date'][$i] : null;
$time = !empty($_POST['items']['time'][$i]) ? $_POST['items']['time'][$i] : null;
$dur = !empty($_POST['items']['duration'][$i]) ? (int)$_POST['items']['duration'][$i] : 1;
+ $context = !empty($_POST['items']['context'][$i]) ? $_POST['items']['context'][$i] : 'local';
- // Ajout de $is_return à la fin
- $stmt->execute([$holiday_id, $cat, $name ?: tr('hdl_default_exp_name'), $amount, $paid, $location_name, $lat, $lng, $target_order, $note, $date, $time, $step_start, $step_end, $dur, $is_return]);
+ $stmt->execute([$holiday_id, $cat, $name ?: tr('hdl_default_exp_name'), $amount, $paid, $location_name, $lat, $lng, $target_order, $note, $date, $time, $step_start, $step_end, $dur, $step_type, $context]);
$validItemsCount++;
}
}
}
if ($validItemsCount === 0) {
- $stmt->execute([$holiday_id, 'activity', 'PF_TECHNICAL_POINT', 0, 1, $location_name, $lat, $lng, $target_order, '', null, null, $step_start, $step_end, 1, $is_return]);
+ $stmt->execute([$holiday_id, 'activity', 'PF_TECHNICAL_POINT', 0, 1, $location_name, $lat, $lng, $target_order, '', null, null, $step_start, $step_end, 1, $step_type, 'local']);
}
- // 4. GESTION DES FAVORIS
- if (isset($_POST['save_favorite']) && $_POST['save_favorite'] == '1') {
- $stmtFav = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'holiday_favorites'");
- $favs = json_decode($stmtFav->fetchColumn() ?: '[]', true);
- $exists = false;
- foreach ($favs as $f) { if ($f['name'] === $location_name) $exists = true; }
- if (!$exists) {
- $favs[] = ['name' => $location_name, 'lat' => $lat, 'lng' => $lng];
- $pdo->prepare("INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('holiday_favorites', 'GLOBAL', ?) ON DUPLICATE KEY UPDATE content = VALUES(content)")->execute([json_encode($favs)]);
- }
+ // 4. GESTION DU RETOUR (Si l'utilisateur définit cette étape comme point de retour)
+ if (isset($_POST['set_as_return']) && $_POST['set_as_return'] == '1') {
+ // On enregistre l'ID de cette étape technique comme point de retour global du voyage
+ $pdo->prepare("UPDATE pf_holidays SET return_step_id = ? WHERE id = ?")->execute([$target_order, $holiday_id]);
}
+ // 5. GESTION DES FAVORIS ... (Garde ton code existant ici)
+
$pdo->commit();
} catch (Exception $e) { $pdo->rollBack(); die($e->getMessage()); }
}
diff --git a/modules/holidays/includes/api/save_holiday.php b/modules/holidays/includes/api/save_holiday.php
index 5e247f5..6cc663f 100644
--- a/modules/holidays/includes/api/save_holiday.php
+++ b/modules/holidays/includes/api/save_holiday.php
@@ -1,10 +1,10 @@
includes -> holidays -> modules -> racine)
+// On remonte de 4 niveaux pour atteindre la racine
require dirname(__DIR__, 4) . '/includes/auth.php';
require dirname(__DIR__, 4) . '/includes/db.php';
-require_login(); // Si cette fonction nécessite une redirection, gère-la dans auth.php
+require_login();
if (isset($_POST['action_delete']) && $_POST['action_delete'] == '1') {
$stmt = $pdo->prepare("DELETE FROM pf_holidays WHERE id = ?");
@@ -22,61 +22,25 @@ $status = $_POST['status'];
$food = !empty($_POST['budget_food']) ? $_POST['budget_food'] : 0;
$extra = !empty($_POST['budget_extra']) ? $_POST['budget_extra'] : 0;
$notes = $_POST['notes'];
+// 🔥 NOUVEAU : On récupère le véhicule optionnel
+$vehicle_id = !empty($_POST['vehicle_id']) ? (int)$_POST['vehicle_id'] : null;
try {
$pdo->beginTransaction();
if ($id) {
- // UPDATE
- $sql = "UPDATE pf_holidays SET title=?, period_hint=?, start_date=?, end_date=?, status=?, budget_food=?, budget_extra=?, notes=? WHERE id=?";
+ // UPDATE (avec vehicle_id)
+ $sql = "UPDATE pf_holidays SET title=?, period_hint=?, start_date=?, end_date=?, status=?, budget_food=?, budget_extra=?, notes=?, vehicle_id=? WHERE id=?";
$stmt = $pdo->prepare($sql);
- $stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $id]);
+ $stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $vehicle_id, $id]);
} else {
- // INSERT
- $sql = "INSERT INTO pf_holidays (title, period_hint, start_date, end_date, status, budget_food, budget_extra, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
+ // INSERT (avec vehicle_id)
+ $sql = "INSERT INTO pf_holidays (title, period_hint, start_date, end_date, status, budget_food, budget_extra, notes, vehicle_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $pdo->prepare($sql);
- $stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes]);
+ $stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $vehicle_id]);
$id = $pdo->lastInsertId();
}
- // GESTION INTELLIGENTE DES ITEMS
- if (!empty($_POST['items']['name'])) {
- $count = count($_POST['items']['name']);
- // On prépare une requête qui met à jour si l'ID existe, sinon insère
- $stmtItem = $pdo->prepare("
- INSERT INTO pf_holidays_items (id, holiday_id, category, name, amount, is_paid, location_name)
- VALUES (?, ?, ?, ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE
- category = VALUES(category),
- name = VALUES(name),
- amount = VALUES(amount),
- is_paid = VALUES(is_paid)
- ");
-
- $keepIds = [];
- for ($i = 0; $i < $count; $i++) {
- $itemId = !empty($_POST['items']['id'][$i]) ? (int)$_POST['items']['id'][$i] : null;
- $cat = $_POST['items']['cat'][$i] ?? 'activity';
- $name = trim($_POST['items']['name'][$i] ?? '');
- $amount = floatval($_POST['items']['amount'][$i] ?? 0);
- $paid = (int)($_POST['items']['paid'][$i] ?? 0);
- $loc = !empty($_POST['items']['location'][$i]) ? $_POST['items']['location'][$i] : null;
-
- if (!empty($name)) {
- $stmtItem->execute([$itemId, $id, $cat, $name, $amount, $paid, $loc]);
- $keepIds[] = $itemId ?: $pdo->lastInsertId();
- }
- }
-
- // Nettoyage : On supprime les items qui ont été retirés de la modale
- // (Attention : uniquement ceux du voyage actuel qui ne sont plus dans la liste envoyée)
- if (!empty($keepIds)) {
- $placeholders = implode(',', array_fill(0, count($keepIds), '?'));
- $sqlDel = "DELETE FROM pf_holidays_items WHERE holiday_id = ? AND id NOT IN ($placeholders)";
- $pdo->prepare($sqlDel)->execute(array_merge([$id], $keepIds));
- }
- }
-
$pdo->commit();
} catch (Exception $e) {
@@ -84,6 +48,5 @@ try {
die("Erreur base de données : " . $e->getMessage());
}
-// Redirection vers la page principale
header("Location: /holidays.php");
exit;
\ No newline at end of file
diff --git a/modules/holidays/views/detail.php b/modules/holidays/views/detail.php
index d0540be..ae82f15 100644
--- a/modules/holidays/views/detail.php
+++ b/modules/holidays/views/detail.php
@@ -11,10 +11,15 @@ if ($id === 0) {
// Récupération des données du voyage
$stmt = $pdo->prepare("
SELECT h.*,
+ v.name as vehicle_name,
+ v.consumption as vehicle_consumption,
(COALESCE(h.budget_food, 0) + COALESCE(h.budget_extra, 0) + COALESCE((SELECT SUM(amount) FROM pf_holidays_items WHERE holiday_id = h.id), 0)) as total_cost,
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND is_paid = 1) as total_paid,
- (SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved
- FROM pf_holidays h WHERE h.id = ?
+ (SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved,
+ (SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND expense_context = 'transit') as total_transit
+ FROM pf_holidays h
+ LEFT JOIN pf_vehicles v ON h.vehicle_id = v.id
+ WHERE h.id = ?
");
$stmt->execute([$id]);
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);
@@ -42,7 +47,7 @@ foreach ($items as $it) {
'sort_order' => $it['sort_order'],
'step_start_date' => $it['step_start_date'],
'step_end_date' => $it['step_end_date'],
- 'is_return' => (int)$it['is_return'],
+ 'step_type' => $it['step_type'] ?? 'stop',
'total_amount' => 0,
'items' => []
];
@@ -105,10 +110,34 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
+
+
-
= tr('hdl_label_period') ?>
-
= $dateDisplay ?: tr('hdl_dates_to_define') ?>
+
Transport
+
🚗 = htmlspecialchars($holiday['vehicle_name']) ?>
+
+
+
+
+ Frais de route (Essence/Péages)
+
+
+ ⛽
+ = number_format($holiday['total_transit'], 0) ?> €
+
+
+ (1.85 €/L) ✏️
+
+
+ 0): ?>
+
+ 👁️
+
+
+
+
+
= tr('hdl_label_budget_food_extras') ?>
🍔 = number_format($holiday['budget_food'], 0) ?> € | 🎁 = number_format($holiday['budget_extra'], 0) ?> €
@@ -211,9 +240,15 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
📍 = htmlspecialchars($step['location_name']) ?>
-
+
🏁 = tr('hdl_return') ?>
+
+
+
🛫 DÉPART
+
+
🛬 ARRIVÉE FINALE
+
@@ -261,22 +296,6 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
- = 2): ?>
-
-
-
Cliquez sur Calculer pour estimer le coût du trajet.
-
-
-
@@ -328,22 +347,32 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
+
+
+
+
+