From 557a1451de6647798ca96a85a5284f93f3482e16 Mon Sep 17 00:00:00 2001 From: "fefe.clochette" Date: Sat, 20 Jun 2026 12:50:40 +0200 Subject: [PATCH] voyage garage --- docker/schema_family.sql | 27 +- holidays.php | 3 + migrate-voyages.php | 38 ++ migrate.php | 191 ++-------- modules/holidays/holidays.css | 90 ++++- modules/holidays/holidays.js | 346 +++++++++--------- .../holidays/includes/api/save_checkpoint.php | 69 ++-- .../holidays/includes/api/save_holiday.php | 57 +-- modules/holidays/views/detail.php | 242 ++++++------ modules/holidays/views/modal.php | 39 +- 10 files changed, 537 insertions(+), 565 deletions(-) create mode 100644 migrate-voyages.php 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

🎉 Migration terminée !

"; +} catch (Exception $e) { + die("Erreur fatale : " . $e->getMessage()); +} +?> \ No newline at end of file diff --git a/migrate.php b/migrate.php index 3fd3d13..fcdd0e2 100644 --- a/migrate.php +++ b/migrate.php @@ -1,70 +1,11 @@ 🚀 Début de la migration Multi-Tenant (Auto-Sync)"; - -// --------------------------------------------------------- -// 1. LECTURE ET PARSING DU FICHIER SCHEMA_FAMILY.SQL -// --------------------------------------------------------- -$schemaPath = __DIR__ . '/schema_family.sql'; // Modifie si rangé dans /docker/ -if (!file_exists($schemaPath)) { - $schemaPath = __DIR__ . '/docker/schema_family.sql'; -} -if (!file_exists($schemaPath)) { - die("❌ Impossible de trouver le fichier schema_family.sql"); -} - -$sqlContent = file_get_contents($schemaPath); - -/** - * Analyse le code SQL pour en extraire la structure [Table => [Colonnes]] - */ -function parseExpectedSchema($sql) { - $schema = []; - // Récupère tout ce qui se trouve entre CREATE TABLE (...) ENGINE - preg_match_all('/CREATE TABLE (?:IF NOT EXISTS )?`?([a-zA-Z0-9_]+)`?\s*\((.*?)\)\s*ENGINE/si', $sql, $tableMatches, PREG_SET_ORDER); - - foreach($tableMatches as $match) { - $tableName = $match[1]; - $body = $match[2]; - - // Sépare les lignes par virgule, en ignorant les virgules entre parenthèses (ex: DECIMAL(10,2) ou ENUM('a','b')) - $lines = preg_split('/,(?![^\(]*\))/', $body); - - $columns = []; - foreach($lines as $line) { - $line = trim($line); - if (empty($line)) continue; - - // On ignore la déclaration des clés, contraintes et index - if (preg_match('/^(PRIMARY KEY|UNIQUE KEY|FOREIGN KEY|KEY|INDEX|FULLTEXT KEY|CONSTRAINT)\b/i', $line)) { - continue; - } - - // Extrait le nom de la colonne et sa définition SQL - if (preg_match('/^`?([a-zA-Z0-9_]+)`?\s+(.*)$/i', $line, $colMatch)) { - $colName = $colMatch[1]; - $colDef = rtrim($colMatch[2], ','); - $columns[$colName] = $colDef; - } - } - $schema[$tableName] = $columns; - } - return $schema; -} - -$expectedSchema = parseExpectedSchema($sqlContent); -echo "ℹ️ Modèle SQL chargé et analysé : " . count($expectedSchema) . " tables détectées.
"; +echo "

🗺️ Migration : Refonte Modèle Voyages

🎉 Migration terminée !

"; } catch (Exception $e) { - die("❌ Erreur fatale Meta DB : " . $e->getMessage()); + die("Erreur fatale : " . $e->getMessage()); } ?> \ No newline at end of file diff --git a/modules/holidays/holidays.css b/modules/holidays/holidays.css index 63a49f5..f4c78f2 100644 --- a/modules/holidays/holidays.css +++ b/modules/holidays/holidays.css @@ -1400,8 +1400,88 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover { } /* --- Toll cost estimator panel --- */ -.hol-cost-stat { background: var(--bg-page, #f8fafc); border: 1px solid var(--border-light, #e2e8f0); border-radius: 10px; padding: .75rem 1rem; text-align: center; } -.hol-cost-stat-val { font-size: 1.2rem; font-weight: 700; color: var(--text-main, #0f172a); } -.hol-cost-stat-label { font-size: .72rem; color: var(--text-muted, #64748b); margin-top: 2px; text-transform: uppercase; letter-spacing: .04em; } -.hol-cost-total { border-color: var(--primary, #4361ee); } -.hol-cost-total .hol-cost-stat-val { color: var(--primary, #4361ee); } +.hol-cost-stat { + background: var(--bg-page, #f8fafc); + border: 1px solid var(--border-light, #e2e8f0); + border-radius: 10px; + padding: 0.75rem 1rem; + text-align: center; +} +.hol-cost-stat-val { + font-size: 1.2rem; + font-weight: 700; + color: var(--text-main, #0f172a); +} +.hol-cost-stat-label { + font-size: 0.72rem; + color: var(--text-muted, #64748b); + margin-top: 2px; + text-transform: uppercase; + letter-spacing: 0.04em; +} +.hol-cost-total { + border-color: var(--primary, #4361ee); +} +.hol-cost-total .hol-cost-stat-val { + color: var(--primary, #4361ee); +} + +/* ========================================================================== + 17. ENCART D'ESTIMATION DE TRAJET (OSRM) + ========================================================================== */ +.hol-transit-info { + margin: 0 15px 15px 15px; + padding: 12px 16px; + background: var(--hol-info-bg); + border-radius: var(--radius-m); + border: 1px dashed #7dd3fc; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 12px; + transition: all 0.2s ease; +} + +.hol-transit-details { + font-size: 0.85rem; + color: var(--hol-info-text); + display: flex; + align-items: center; + gap: 12px; +} + +.hol-transit-icon { + font-size: 1.6rem; + line-height: 1; +} + +.hol-transit-text strong { + font-size: 0.95rem; +} + +.hol-transit-text span { + opacity: 0.85; + font-size: 0.8rem; + display: block; + margin-top: 2px; +} + +.hol-transit-btn { + padding: 6px 14px; + font-size: 0.8rem; + white-space: nowrap; +} + +@media (max-width: 768px) { + .hol-transit-info { + margin: 0 10px 15px 10px; + padding: 10px; + flex-direction: column; + align-items: flex-start; + } + .hol-transit-btn { + width: 100%; + justify-content: center; + } +} diff --git a/modules/holidays/holidays.js b/modules/holidays/holidays.js index 749f5d4..b692c72 100644 --- a/modules/holidays/holidays.js +++ b/modules/holidays/holidays.js @@ -5,26 +5,21 @@ function tr(key) { return window.I18N && window.I18N[key] ? window.I18N[key] : key; } -// On utilise 'var' au lieu de 'const/let' pour éviter les crashs si le fichier est lu 2 fois var currentLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR"; -var selectedItemIdForMove = null; // Déplacé ici pour plus de clarté +var selectedItemIdForMove = null; // ============================================================================ // UTILITAIRES MÉTÉO // ============================================================================ function getWeatherInfo(code) { - // Transformation en conditions pour regrouper les codes WMO if (code === 0) return { icon: "☀️", label: tr("weather_sunny") }; if ([1, 2].includes(code)) return { icon: "🌤️", label: tr("weather_sunny") }; if ([3, 45, 48].includes(code)) return { icon: "☁️", label: tr("weather_cloudy") }; - // Les codes 51 à 67 et 80 à 82 couvrent toutes les formes de pluie et bruine if ([51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82].includes(code)) return { icon: "🌧️", label: tr("weather_rainy") }; - // Les codes neigeux if ([71, 73, 75, 77, 85, 86].includes(code)) return { icon: "❄️", label: tr("weather_snowy") }; - // Orages if ([95, 96, 99].includes(code)) return { icon: "⛈️", label: tr("weather_rainy") }; @@ -45,12 +40,8 @@ async function loadWeatherForStep(pt) { ); const res = await resp.json(); - console.log(`Météo pour ${pt.location_name} :`, res); - if (res.success) { const info = getWeatherInfo(res.data.code); - - // Si c'est une estimation basée sur le passé, on adapte l'affichage const approxSymbol = res.data.is_historical ? "~" : ""; const badgeTitle = res.data.is_historical ? `${info.label} (${tr("weather_historical")})` @@ -80,8 +71,9 @@ window.addEventListener("click", function (event) { } }); -// --- 1. GESTION DE LA MODALE D'ÉDITION RAPIDE --- - +// ============================================================================ +// 1. GESTION DE LA MODALE D'ÉDITION RAPIDE (BASES VOYAGE) +// ============================================================================ function openHolidayModal(mode) { const modal = document.getElementById("holidayModal"); const form = document.getElementById("holidayForm"); @@ -89,9 +81,6 @@ function openHolidayModal(mode) { form.reset(); document.getElementById("inp_id").value = ""; - document.getElementById("list_transport").innerHTML = ""; - document.getElementById("list_accommodation").innerHTML = ""; - document.getElementById("list_activity").innerHTML = ""; if (mode === "add") { document.getElementById("modalTitle").innerText = tr("hdl_modal_title"); @@ -131,67 +120,12 @@ function editHoliday(data) { h.budget_extra > 0 ? h.budget_extra : ""; document.getElementById("inp_notes").value = h.notes || ""; - document.getElementById("list_transport").innerHTML = ""; - document.getElementById("list_accommodation").innerHTML = ""; - document.getElementById("list_activity").innerHTML = ""; - - if (data.items) { - data.items.forEach((item) => { - if (item.name !== "PF_TECHNICAL_POINT") { - // On passe maintenant l'ID et le lieu à addItem - addItem( - item.category, - item.name, - item.amount, - item.is_paid, - item.id, - item.location_name || "", - ); - } - }); + const vehicleInput = document.getElementById("inp_vehicle_id"); + if (vehicleInput) { + vehicleInput.value = h.vehicle_id || ""; } } -// --- 2. GESTION DES LISTES DYNAMIQUES DANS LA MODALE --- - -function addItem( - category, - name = "", - amount = "", - isPaid = 0, - id = "", - location = "", -) { - const container = document.getElementById("list_" + category); - const div = document.createElement("div"); - div.className = "savings-line-item"; // Utilisation de ta classe existante - div.style.marginBottom = "10px"; - - const checkedAttr = isPaid == 1 ? "checked" : ""; - // Optimisation : Affichage du badge d'étape si existant - const locationBadge = location - ? `📍 ${location}` - : ""; - - div.innerHTML = ` - - - -
- ${locationBadge} - -
- - - - `; - container.appendChild(div); -} - function deleteHoliday() { if (!confirm(tr("hdl_js_confirm_del_trip"))) return; const form = document.getElementById("holidayForm"); @@ -203,55 +137,9 @@ function deleteHoliday() { form.submit(); } -// --- 3. GESTION DE LA CARTE --- - -var map = null; - -function toggleMap() { - const modal = document.getElementById("hol-map-modal"); - if (!modal) return; - if (modal.style.display === "flex") { - modal.style.display = "none"; - } else { - modal.style.display = "flex"; - setTimeout(initMap, 100); - } -} - -function initMap() { - if (map) { - map.invalidateSize(); - return; - } - if (typeof L === "undefined") return; - - map = L.map("hol-map").setView([46.6, 2.4], 4); - L.tileLayer( - "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png", - { - attribution: "© OpenStreetMap", - }, - ).addTo(map); - - if (typeof HOL_MAP_POINTS !== "undefined") { - HOL_MAP_POINTS.forEach((pt) => { - const color = - pt.status === "planned" || pt.status === "booked" ? "green" : "blue"; - L.circleMarker([pt.lat, pt.lng], { - color: color, - radius: 8, - fillOpacity: 0.8, - }) - .addTo(map) - .bindPopup(`${pt.title}
${pt.status}`); - }); - } -} - // ============================================================================ -// 4. GESTION DE LA CARTE DÉTAILLÉE (ROADTRIP) ET GÉOCODAGE +// 4. GESTION DE LA CARTE DÉTAILLÉE (ROADTRIP) ET TRACÉS OSRM // ============================================================================ - var detailMap = null; document.addEventListener("DOMContentLoaded", () => { @@ -263,7 +151,6 @@ document.addEventListener("DOMContentLoaded", () => { function initDetailMap() { if (typeof L === "undefined" || typeof MAP_POINTS === "undefined") return; - // 1. 🧹 NETTOYAGE PROPRE : On détruit l'ancienne instance si elle existe (Évite le bug de la souris bloquée) if (detailMap !== null) { detailMap.remove(); detailMap = null; @@ -272,17 +159,12 @@ function initDetailMap() { const mapContainer = document.getElementById("tripMap"); if (!mapContainer) return; - // 2. 🛡️ BOUCLIER FIREFOX DESKTOP : Empêche le drag natif HTML5 de voler le clic mapContainer.style.touchAction = "none"; mapContainer.ondragstart = function (e) { e.preventDefault(); }; - // 3. 🛠️ INITIALISATION DE LA CARTE - detailMap = L.map("tripMap", { - tap: false, // Désactive le tap simulé (anti-warning mobile/Firefox) - dragging: true, // Force l'autorisation du déplacement à la souris - }); + detailMap = L.map("tripMap", { tap: false, dragging: true }); L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, @@ -290,25 +172,21 @@ function initDetailMap() { '© OpenStreetMap contributors', }).addTo(detailMap); - // Cas : Aucun point if (MAP_POINTS.length === 0) { - detailMap.setView([46.6, 2.4], 5); // France par défaut + detailMap.setView([46.6, 2.4], 5); return; } const latlngs = []; const bounds = L.latLngBounds(); - // 4. PLACEMENT DES MARQUEURS MAP_POINTS.forEach((pt, index) => { const pos = [pt.lat, pt.lng]; latlngs.push(pos); bounds.extend(pos); - const color = "#2563eb"; - const marker = L.circleMarker(pos, { - color: color, + color: "#2563eb", radius: window.innerWidth < 768 ? 6 : 8, fillOpacity: 1, fillColor: "white", @@ -323,11 +201,10 @@ function initDetailMap() {
${stepLabel} ${index + 1}
${pt.location_name}
- ${parseFloat(pt.total_amount).toFixed(2)} € + ${parseFloat(pt.total_amount).toFixed(2)} €
`); - // Animation au clic sur le marqueur marker.on("click", function () { const card = document.getElementById("step-card-" + pt.sort_order); if (card) { @@ -345,7 +222,6 @@ function initDetailMap() { const mapPadding = window.innerWidth < 768 ? [20, 20] : [50, 50]; - // 5. CENTRAGE ET TRACÉS (OSRM) if (latlngs.length === 1) { detailMap.setView(latlngs[0], 12); } else if (latlngs.length > 1) { @@ -379,9 +255,16 @@ function initDetailMap() { results.sort((a, b) => a.index - b.index); let returnStartIndex = latlngs.length - 2; - const customReturnStep = MAP_POINTS.findIndex((p) => p.is_return == 1); - if (customReturnStep > 0) { - returnStartIndex = customReturnStep; + if ( + typeof window.GLOBAL_RETURN_STEP_ID !== "undefined" && + window.GLOBAL_RETURN_STEP_ID !== null + ) { + const customReturnStep = MAP_POINTS.findIndex( + (p) => p.sort_order == window.GLOBAL_RETURN_STEP_ID, + ); + if (customReturnStep > 0) { + returnStartIndex = customReturnStep; + } } results.forEach((res) => { @@ -408,6 +291,52 @@ function initDetailMap() { lineCap: "round", lineJoin: "round", }).addTo(detailMap); + + // 🔥 CALCUL AUTO DU COUT ET KILOMÈTRES + const distanceKm = res.data.routes[0].distance / 1000; + const fuelL100 = window.VEHICLE_CONSUMPTION || 7; + const fuelPrice = window.FUEL_PRICE || 1.85; + const cost = (distanceKm / 100) * fuelL100 * fuelPrice; + + const targetOrder = MAP_POINTS[res.index + 1].sort_order; + const targetCard = document.getElementById( + "step-card-" + targetOrder, + ); + + if (targetCard) { + const existingTransit = + targetCard.querySelectorAll(".transit-auto-info"); + existingTransit.forEach((el) => el.remove()); + + const rawLocationName = MAP_POINTS[res.index].location_name; + const safeLocationName = rawLocationName.replace(/'/g, "\\'"); + const expenseDesc = `Essence depuis ${rawLocationName}`; + + const targetStepData = MAP_POINTS[res.index + 1]; + const isAlreadyAdded = targetStepData.items.some( + (it) => it.name === expenseDesc, + ); + + const summaryHtml = ` +
+ 🚗 ${Math.round(distanceKm)} km + | + **⛽ ~${cost.toFixed(2)} €** + ${ + !isAlreadyAdded + ? `` + : `✓ Ajouté` + } +
`; + + const cpHeader = targetCard.querySelector(".hol-cp-header"); + if (cpHeader) { + cpHeader.insertAdjacentHTML("afterend", summaryHtml); + } + } } else { drawFallbackLine(res.coords, routeColor, routeWeight); } @@ -415,7 +344,6 @@ function initDetailMap() { }); } - // 6. LANCEMENT DE LA MÉTÉO if (typeof MAP_POINTS !== "undefined") { MAP_POINTS.forEach((pt) => { if (typeof loadWeatherForStep === "function") { @@ -424,14 +352,12 @@ function initDetailMap() { }); } - // 7. FIX FINAL : Force Leaflet à recalculer sa taille une fois le DOM stabilisé setTimeout(() => { if (detailMap) { detailMap.invalidateSize(); } }, 300); - // Fonction utilitaire locale function drawFallbackLine(coords, color, weight) { L.polyline(coords, { color: color, @@ -446,7 +372,6 @@ function panMapTo(lat, lng) { if (detailMap) { detailMap.setView([lat, lng], 14, { animate: true }); - // 🛠️ ERGONOMIE MOBILE : Auto-scroll vers la carte si on est sur petit écran if (window.innerWidth < 768) { const mapDiv = document.getElementById("tripMap"); if (mapDiv) { @@ -456,8 +381,9 @@ function panMapTo(lat, lng) { } } -// --- LOGIQUE DE LA MODALE CHECKPOINT --- - +// ============================================================================ +// 5. LOGIQUE DE LA MODALE CHECKPOINT (ÉTAPES) +// ============================================================================ function openCheckpointModal(mode, data = null) { const searchBlock = document.getElementById("cpSearchBlock"); const formBlock = document.getElementById("formCheckpoint"); @@ -470,8 +396,10 @@ function openCheckpointModal(mode, data = null) { document.getElementById("cp_start_date").value = ""; if (document.getElementById("cp_end_date")) document.getElementById("cp_end_date").value = ""; - document.getElementById("searchPlaceInput").value = ""; - document.getElementById("searchResults").innerHTML = ""; + if (document.getElementById("searchPlaceInput")) + document.getElementById("searchPlaceInput").value = ""; + if (document.getElementById("searchResults")) + document.getElementById("searchResults").innerHTML = ""; searchBlock.style.display = "block"; @@ -481,8 +409,16 @@ function openCheckpointModal(mode, data = null) { btnDel.style.display = "none"; document.getElementById("cp_old_sort_order").value = ""; document.getElementById("cp_name").value = ""; + + if (document.getElementById("cp_step_type")) { + document.getElementById("cp_step_type").value = "stop"; + toggleStepDates("stop"); + } + if (document.getElementById("cp_set_as_return")) { + document.getElementById("cp_set_as_return").checked = false; + } + addCpExpenseLine(); - document.getElementById("cp_is_return").checked = false; } else if (mode === "edit" && data) { document.getElementById("cpModalTitle").innerText = tr("hdl_js_edit_step"); formBlock.style.display = "block"; @@ -494,7 +430,19 @@ function openCheckpointModal(mode, data = null) { document.getElementById("cp_name").value = data.location_name; document.getElementById("cp_start_date").value = data.step_start_date || ""; document.getElementById("cp_end_date").value = data.step_end_date || ""; - document.getElementById("cp_is_return").checked = data.is_return == 1; + + // 🔥 PRE-REMPLISSAGE DU TYPE D'ÉTAPE ET UI DATES + if (document.getElementById("cp_step_type")) { + const type = data.step_type || "stop"; + document.getElementById("cp_step_type").value = type; + toggleStepDates(type); + } + + // 🔥 PRE-REMPLISSAGE DE LA CASE RETOUR BASEE SUR LA GLOBALE + if (document.getElementById("cp_set_as_return")) { + document.getElementById("cp_set_as_return").checked = + window.GLOBAL_RETURN_STEP_ID == data.sort_order; + } if (data.items && data.items.length > 0) { let visibleCount = 0; @@ -589,6 +537,10 @@ function addCpExpenseLine( + @@ -621,10 +573,8 @@ function deleteCheckpoint() { } // ============================================================================ -// 5. RÉORDONNANCEMENT DES ÉTAPES (DRAG & DROP PC + FLÈCHES MOBILE) +// 6. REORDONNANCEMENT DES ÉTAPES (DRAG & DROP PC + MOBILE) // ============================================================================ - -// On sort cette fonction pour pouvoir l'appeler depuis les boutons fléchés sur mobile function saveCheckpointOrder() { const locations = [ ...document.querySelectorAll(".hol-checkpoint-draggable"), @@ -641,7 +591,6 @@ function saveCheckpointOrder() { }).then(() => window.location.reload()); } -// Fonction appelée par les flèches Haut/Bas sur mobile function moveStepMobile(btn, direction) { const item = btn.closest(".hol-checkpoint-draggable"); const container = item.parentElement; @@ -672,7 +621,6 @@ document.addEventListener("DOMContentLoaded", () => { let draggedItem = null; checkpoints.forEach((item) => { - // Si on est sur mobile, on supprime l'attribut draggable pour éviter les conflits de scroll if (isMobile) { item.removeAttribute("draggable"); return; @@ -724,9 +672,8 @@ document.addEventListener("DOMContentLoaded", () => { }); // ============================================================================ -// MOTEUR DRAG & DROP DU PLANNING +// 7. MOTEUR DRAG & DROP DU PLANNING CARNET DE BORD // ============================================================================ - function closePlanningModal() { document.getElementById("planningModal").style.display = "none"; document.body.classList.remove("no-scroll"); @@ -798,7 +745,6 @@ function openPlanningModal(step) { html += ``; container.innerHTML = html; - // 1. On détecte si on est sur mobile juste avant la boucle const isMobile = window.innerWidth <= 768; const dragAttr = isMobile ? "" : 'draggable="true"'; @@ -819,7 +765,6 @@ function openPlanningModal(step) { ? `
${it.notes}
` : ""; - // 2. MODIFICATION ICI : On remplace le texte en dur draggable="true" par la variable ${dragAttr} const elHtml = `
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;
+ +
-
-
+
Transport
+
🚗
+ + +
+
+ Frais de route (Essence/Péages) +
+
+ + + + + (1.85 €/L) ✏️ + + + 0): ?> + + 👁️ + + +
+
+
🍔 € | 🎁
@@ -211,9 +240,15 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
📍 - + 🏁 + + + 🛫 DÉPART + + 🛬 ARRIVÉE FINALE +
@@ -261,22 +296,6 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
- = 2): ?> -
-
-

🚗 Coût du trajet

-
- - - - - -
-
-
Cliquez sur Calculer pour estimer le coût du trajet.
-
- -

@@ -328,22 +347,32 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;

+
+ + +
+
-
- +
+
-
- +
+
-
-