From 18740c5e1bbb3fad53f5585026a67aebc2a32320 Mon Sep 17 00:00:00 2001 From: "fefe.clochette" Date: Sat, 20 Jun 2026 19:01:18 +0200 Subject: [PATCH] =?UTF-8?q?temps=20/=20modale=20d=C3=A9tail=20/=20drag=20/?= =?UTF-8?q?=20waze?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/holidays/holidays.css | 156 ++++++++++- modules/holidays/holidays.js | 250 ++++++++++++------ .../holidays/includes/api/save_checkpoint.php | 9 +- modules/holidays/views/detail.php | 141 +++++++--- 4 files changed, 426 insertions(+), 130 deletions(-) diff --git a/modules/holidays/holidays.css b/modules/holidays/holidays.css index f4c78f2..a794f3c 100644 --- a/modules/holidays/holidays.css +++ b/modules/holidays/holidays.css @@ -788,7 +788,7 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover { .hol-cp-header { background: var(--bg-page); - padding: 12px 15px; + padding: 6px 8px; display: flex; justify-content: space-between; align-items: center; @@ -1485,3 +1485,157 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover { justify-content: center; } } + +/* --- EN-TÊTE DES ÉTAPES ROADTRIP --- */ +.hol-step-header { + padding: 6px 8px; + border-bottom: 1px solid var(--border-light); + display: flex; + flex-direction: column; + gap: 8px; +} + +/* Ligne 1 : Icône + Titre + Poignée */ +.hol-step-row-top { + display: flex; + align-items: flex-start; + gap: 10px; + width: 100%; +} + +.hol-step-icon { + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + flex-shrink: 0; + margin-top: 2px; +} + +.hol-step-title { + flex: 1; + font-size: 1.05rem; + font-weight: 700; + color: var(--text-main); + line-height: 1.3; + word-break: break-word; + cursor: pointer; + padding-top: 4px; +} + +.hol-step-controls { + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: center; +} + +.hol-drag-handle { + color: #cbd5e1; + font-size: 1.4rem; + cursor: grab; + user-select: none; + line-height: 1; +} + +.hol-step-arrows { + display: flex; + flex-direction: column; + gap: 0; +} + +.hol-step-arrows button { + background: transparent; + border: none; + color: #94a3b8; + font-size: 0.75rem; + padding: 4px 8px; + cursor: pointer; + line-height: 1; +} + +/* Ligne 2 : Tags/Dates + Prix/Actions */ +.hol-step-row-bottom { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 5px; + width: 100%; +} + +.hol-step-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; +} + +.hol-tag-return { + background: #fff7ed; + color: #ea580c; + padding: 2px 6px; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 700; + border: 1px solid #ffedd5; + line-height: 1.2; +} + +.hol-step-date { + font-size: 0.8rem; + color: #64748b; + background: var(--bg-page); + padding: 2px 6px; + border-radius: 4px; + border: 1px solid var(--border-light); + line-height: 1.2; +} + +.hol-step-actions { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; /* Pousse le prix et les boutons tout à droite */ +} + +.hol-step-price { + font-weight: 800; + color: var(--primary); + white-space: nowrap; +} + +/* Optimisation Mobile */ +@media (max-width: 768px) { + .hol-step-row-bottom { + padding-left: 0; /* Sur mobile, on prend toute la largeur pour éviter d'écraser les boutons */ + } +} + +/* --- LIGNES DE DÉPENSES --- */ +.hol-expense-main { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; +} + +.hol-expense-name { + flex: 1; + min-width: 0; + word-break: break-word; +} + +.hol-expense-price-group { + display: inline-flex; + align-items: center; + gap: 6px; + white-space: nowrap; /* Interdit le passage à la ligne */ + flex-shrink: 0; /* Empêche le bloc d'être écrasé */ + margin-top: 2px; +} + +.hol-expense-price-group .status-paid, +.hol-expense-price-group .status-pending { + line-height: 1; +} diff --git a/modules/holidays/holidays.js b/modules/holidays/holidays.js index 35df442..51f2d37 100644 --- a/modules/holidays/holidays.js +++ b/modules/holidays/holidays.js @@ -257,10 +257,11 @@ function initDetailMap() { Promise.all(routePromises).then((results) => { results.sort((a, b) => a.index - b.index); - // Compteur global de distance + // 🔥 NOUVEAU : Compteurs et HTML de la modale let totalTripDistance = 0; + let totalTripDuration = 0; // en secondes + let transitDetailsHtml = ""; - // Détection de l'étape de retour (Ligne Orange) let returnStartIndex = latlngs.length - 2; if ( typeof window.GLOBAL_RETURN_STEP_ID !== "undefined" && @@ -269,22 +270,21 @@ function initDetailMap() { const customReturnStep = MAP_POINTS.findIndex( (p) => p.sort_order == window.GLOBAL_RETURN_STEP_ID, ); - if (customReturnStep > 0) { - returnStartIndex = customReturnStep; - } + if (customReturnStep > 0) returnStartIndex = customReturnStep; } results.forEach((res) => { const i = res.index; - let routeColor = "#3b82f6"; - let routeWeight = window.innerWidth < 768 ? 4 : 6; - let routeDash = null; - - if (i >= returnStartIndex) { - routeColor = "#f97316"; - routeWeight = window.innerWidth < 768 ? 3 : 4; - routeDash = "10, 10"; - } + let routeColor = i >= returnStartIndex ? "#f97316" : "#3b82f6"; + let routeWeight = + window.innerWidth < 768 + ? i >= returnStartIndex + ? 3 + : 4 + : i >= returnStartIndex + ? 4 + : 6; + let routeDash = i >= returnStartIndex ? "10, 10" : null; if (res.data && res.data.code === "Ok" && res.data.routes.length > 0) { const routeCoords = res.data.routes[0].geometry.coordinates.map( @@ -299,30 +299,54 @@ function initDetailMap() { lineJoin: "round", }).addTo(detailMap); - // CALCUL AUTO DU COUT ET KILOMÈTRES + // CALCULS const distanceKm = res.data.routes[0].distance / 1000; + const durationSec = res.data.routes[0].duration; // Durée en secondes + totalTripDistance += distanceKm; + totalTripDuration += durationSec; 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; + // 1. ON DÉCLARE LES POINTS D'ABORD + const startPt = MAP_POINTS[res.index]; + const endPt = MAP_POINTS[res.index + 1]; + + // 2. ON SAUVEGARDE EN MÉMOIRE ENSUITE (Correction du bug !) + window.TRANSIT_DATA = window.TRANSIT_DATA || {}; + window.TRANSIT_DATA[endPt.sort_order] = { + sec: durationSec, + from: startPt.location_name, + cost: cost, + }; + + // 🔥 CONSTRUCTION DU CONTENU DE LA MODALE + transitDetailsHtml += ` +
+
+ 📍 ${startPt.location_name} ➔ ${endPt.location_name} +
+
+ 🚗 ${Math.round(distanceKm)} km  •  ⏱️ ${formatDuration(durationSec)} + ⛽ ${cost.toFixed(2)} € +
+
+ `; + + // INJECTION DANS LA CARTE (Sous l'étape) const targetCard = document.getElementById( - "step-card-" + targetOrder, + "step-card-" + endPt.sort_order, ); - if (targetCard) { - const existingTransit = - targetCard.querySelectorAll(".transit-auto-info"); - existingTransit.forEach((el) => el.remove()); - - const rawLocationName = MAP_POINTS[res.index].location_name; + targetCard + .querySelectorAll(".transit-auto-info") + .forEach((el) => el.remove()); + const rawLocationName = startPt.location_name; const safeLocationName = rawLocationName.replace(/'/g, "\\'"); const expenseDesc = `Essence depuis ${rawLocationName}`; - - const targetStepData = MAP_POINTS[res.index + 1]; - const isAlreadyAdded = targetStepData.items.some( + const isAlreadyAdded = endPt.items.some( (it) => it.name === expenseDesc, ); @@ -331,59 +355,52 @@ function initDetailMap() { 🚗 ${Math.round(distanceKm)} km | ⛽ ~${cost.toFixed(2)} € - ${ - !isAlreadyAdded - ? `` - : `✓ Ajouté` - } + ${!isAlreadyAdded ? `` : `✓ Ajouté`} `; const cpHeader = targetCard.querySelector(".hol-cp-header"); - if (cpHeader) { - cpHeader.insertAdjacentHTML("afterend", summaryHtml); - } + if (cpHeader) cpHeader.insertAdjacentHTML("afterend", summaryHtml); } } else { drawFallbackLine(res.coords, routeColor, routeWeight); } }); - // Affichage du total global en haut de page + // 🔥 AFFICHAGE DES TOTAUX (KM + TEMPS) EN HAUT DE PAGE const distEl = document.getElementById("global_total_distance"); + const timeEl = document.getElementById("global_total_duration"); const distBlock = document.getElementById("block_total_distance"); - if (distEl && distBlock) { + + if (distEl && timeEl && distBlock) { distEl.innerText = Math.round(totalTripDistance); - distBlock.style.display = "Block"; + timeEl.innerText = formatDuration(totalTripDuration); + distBlock.style.display = "block"; + } + + // 🔥 INJECTION DU HTML DANS LA MODALE + const modalContainer = document.getElementById("transitDetailsContainer"); + if (modalContainer) { + modalContainer.innerHTML = + transitDetailsHtml || + '

Aucun trajet calculé.

'; } }); - } - // 4. Chargement des badges Météo - if (typeof MAP_POINTS !== "undefined") { - MAP_POINTS.forEach((pt) => { - if (typeof loadWeatherForStep === "function") { - loadWeatherForStep(pt); + // 5. Fix Leaflet Resize + setTimeout(() => { + if (detailMap) { + detailMap.invalidateSize(); } - }); - } + }, 300); - // 5. Fix Leaflet Resize - setTimeout(() => { - if (detailMap) { - detailMap.invalidateSize(); + function drawFallbackLine(coords, color, weight) { + L.polyline(coords, { + color: color, + weight: weight || 3, + dashArray: "8, 8", + opacity: 0.7, + }).addTo(detailMap); } - }, 300); - - function drawFallbackLine(coords, color, weight) { - L.polyline(coords, { - color: color, - weight: weight || 3, - dashArray: "8, 8", - opacity: 0.7, - }).addTo(detailMap); } } @@ -706,6 +723,26 @@ function openPlanningModal(step) { let validItems = step.items.filter((it) => it.name !== "PF_TECHNICAL_POINT"); + window.CURRENT_PLANNING_STEP = step; // Mémorise l'étape en cours + + if (window.TRANSIT_DATA && window.TRANSIT_DATA[step.sort_order]) { + // Est-ce qu'on a déjà planifié ou ajouté ce trajet ? + let hasTransit = validItems.some((it) => it.expense_context === "transit"); + if (!hasTransit) { + const tData = window.TRANSIT_DATA[step.sort_order]; + const h = Math.max(1, Math.round(tData.sec / 3600)); // Arrondi en heures + validItems.push({ + id: "virtual-transit", + name: `Essence depuis ${tData.from}`, // Garde ce nom pour lier avec le budget + category: "transport", + expense_context: "transit", + duration: h, + notes: `Trajet GPS (${Math.round(tData.sec / 60)} min). Déplacez pour planifier la route.`, + is_virtual: true, + }); + } + } + if (!step.step_start_date || !step.step_end_date) { container.innerHTML = `

${tr("hdl_js_missing_dates_title")}

${tr("hdl_js_missing_dates_msg")}

`; document.getElementById("planningModal").style.display = "flex"; @@ -784,18 +821,33 @@ function openPlanningModal(step) { ? `
${it.notes}
` : ""; + const isVirtual = it.is_virtual === true; + const isTransit = it.expense_context === "transit"; + + const durControls = + isVirtual || isTransit + ? `${dur}h (Auto)` + : ` + ${dur}h + `; + + const bgStyle = isVirtual + ? "background: repeating-linear-gradient(45deg, #ffffff, #ffffff 10px, #f8fafc 10px, #f8fafc 20px); border: 2px dashed var(--primary);" + : ""; + const visualName = isTransit + ? `🛣️ Trajet & ` + it.name + : `${icon} ${it.name}`; + const elHtml = `
+ style="--duration: ${dur}; ${bgStyle}" + ondragstart="dragStart(event)" onclick="handleItemTap(event, '${it.id}')">
-
${icon} ${it.name}
+
${visualName}
- - ${dur}h - + ${durControls}
${noteHtml} @@ -898,19 +950,29 @@ function handleDropEvent(e, dateStr, timeStr) { } function handleDropLogic(itemId, dateStr, timeStr, dropZone) { - const draggedEl = document.getElementById("drag-item-" + itemId); - if (dropZone && draggedEl) { - dropZone.appendChild(draggedEl); - updateItemMemory(itemId, { item_date: dateStr, item_time: timeStr }); - const formData = new FormData(); - formData.append("action", "update_item_datetime"); - formData.append("item_id", itemId); - formData.append("item_date", dateStr); - formData.append("item_time", timeStr); + if (itemId === "virtual-transit") { + const step = window.CURRENT_PLANNING_STEP; + const tData = window.TRANSIT_DATA[step.sort_order]; + const holidayId = document.querySelector('input[name="holiday_id"]').value; + const h = Math.max(1, Math.round(tData.sec / 3600)); + + const fd = new FormData(); + fd.append("action", "add_single_item"); + fd.append("holiday_id", holidayId); + fd.append("sort_order", step.sort_order); + fd.append("category", "transport"); + fd.append("context", "transit"); + fd.append("name", `Essence depuis ${tData.from}`); + fd.append("amount", tData.cost); + fd.append("duration", h); + fd.append("item_date", dateStr); + fd.append("item_time", timeStr); + fetch("/modules/holidays/includes/api/save_checkpoint.php", { method: "POST", - body: formData, - }); + body: fd, + }).then(() => window.location.reload()); + return; } } @@ -974,6 +1036,7 @@ function addQuickTransitExpense( amount, description, btnElement, + durationSec = 3600, ) { if ( !confirm( @@ -1006,16 +1069,17 @@ function addQuickTransitExpense( fd.append("amount", amount); fd.append("context", "transit"); - // 3. Envoi en arrière-plan sans bloquer l'utilisateur + // 🔥 On sécurise la durée en base de données + const h = Math.max(1, Math.round(durationSec / 3600)); + fd.append("duration", h); + fetch("/modules/holidays/includes/api/save_checkpoint.php", { method: "POST", body: fd, }) .then((res) => res.json()) .then((data) => { - if (!data.success) { - alert("Erreur lors de la sauvegarde en arrière-plan : " + data.error); - } + if (!data.success) alert("Erreur : " + data.error); }); } @@ -1037,3 +1101,25 @@ function updateFuelPrice() { } } } + +// Formatte les secondes en "XXhYY" ou "YYmin" +function formatDuration(seconds) { + if (!seconds || isNaN(seconds)) return "0min"; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + if (h > 0) { + return `${h}h${m.toString().padStart(2, "0")}`; + } + return `${m}min`; +} + +// Ouvre et ferme la modale des détails (L'œil) +function openTransitModal() { + document.getElementById("transitModal").style.display = "flex"; + document.body.classList.add("no-scroll"); +} + +function closeTransitModal() { + document.getElementById("transitModal").style.display = "none"; + document.body.classList.remove("no-scroll"); +} diff --git a/modules/holidays/includes/api/save_checkpoint.php b/modules/holidays/includes/api/save_checkpoint.php index e459caa..87954a9 100644 --- a/modules/holidays/includes/api/save_checkpoint.php +++ b/modules/holidays/includes/api/save_checkpoint.php @@ -21,14 +21,17 @@ if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime $stepInfo = $stmt->fetch(); if ($stepInfo) { - // Utilisation de transactions isolées pour l'AJAX + $dur = isset($_POST['duration']) ? (int)$_POST['duration'] : 1; + $date = !empty($_POST['item_date']) ? $_POST['item_date'] : null; + $time = !empty($_POST['item_time']) ? $_POST['item_time'] : null; + $pdo->beginTransaction(); - $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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)"); + $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, item_date, item_time) 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'] + $stepInfo['step_type'], $_POST['context'], $dur, $date, $time ]); $pdo->commit(); echo json_encode(['success' => true]); diff --git a/modules/holidays/views/detail.php b/modules/holidays/views/detail.php index fd048fa..3343214 100644 --- a/modules/holidays/views/detail.php +++ b/modules/holidays/views/detail.php @@ -131,7 +131,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0; 0): ?> - + 👁️ @@ -139,10 +139,13 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
@@ -193,17 +196,18 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0; 0 || $holiday['budget_extra'] > 0): ?>
-
-
- 🌍 -
+
+
+ 🌍 +
+ -
-
+
+
@@ -225,13 +229,20 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0; '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' }; ?> -
- - - - - -
+
+
+ + + + + + + + + + +
+
@@ -239,39 +250,66 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
-
-
- -
- - -
-
- 📍 - - 🏁 - +
+ +
+ - 🛫 DÉPART - - 🛬 ARRIVÉE FINALE - - -
- - - -
-
- + if ($step['step_type'] === 'origin') { + $stepIcon = '🛫'; $iconBg = '#ecfdf5'; $iconColor = '#059669'; + } elseif ($step['step_type'] === 'destination') { + $stepIcon = '🛬'; $iconBg = '#fef2f2'; $iconColor = '#e11d48'; + } + ?> +
+ +
+ +
+ +
+ +
+ +
+ + +
-
-
- - + +
+
+ + 🏁 Retour + + + + + + +
+ +
+ +
+ + + + 🗺️ + + + + +
+
@@ -419,6 +457,21 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
+
+
+
+

🛣️ Détail des trajets

+ +
+
+

Calcul en cours...

+
+ +
+
+