From ac8ebf7328d824521d2481036a2e273c10733f7f Mon Sep 17 00:00:00 2001 From: "fefe.clochette" Date: Fri, 24 Jul 2026 17:55:56 +0200 Subject: [PATCH] update road trip --- modules/holidays/holidays.css | 4 +- modules/holidays/holidays.js | 705 ++++++++++++++---- .../holidays/includes/api/save_checkpoint.php | 41 +- modules/holidays/views/detail.php | 116 ++- modules/holidays/views/modal.php | 4 - 5 files changed, 634 insertions(+), 236 deletions(-) diff --git a/modules/holidays/holidays.css b/modules/holidays/holidays.css index a794f3c..1728b41 100644 --- a/modules/holidays/holidays.css +++ b/modules/holidays/holidays.css @@ -1196,7 +1196,7 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover { top: 1px; left: 45px; right: 5px; - height: calc(75px * var(--duration) - 2px); + height: calc(60px * var(--duration) - 2px); } /* Dans la boîte d'attente : Position normale */ .hol-unmapped-zone .hol-drag-item { @@ -1253,7 +1253,7 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover { } .hol-time-slot { - height: 75px; + height: 60px; border-bottom: 1px solid var(--border-light); position: relative; transition: background 0.2s; diff --git a/modules/holidays/holidays.js b/modules/holidays/holidays.js index b784a0a..8c4b099 100644 --- a/modules/holidays/holidays.js +++ b/modules/holidays/holidays.js @@ -257,9 +257,9 @@ function initDetailMap() { Promise.all(routePromises).then((results) => { results.sort((a, b) => a.index - b.index); - // 🔥 NOUVEAU : Compteurs et HTML de la modale let totalTripDistance = 0; let totalTripDuration = 0; // en secondes + let totalFuelCost = 0; // Coût exact accumulé let transitDetailsHtml = ""; let returnStartIndex = latlngs.length - 2; @@ -310,6 +310,8 @@ function initDetailMap() { const fuelPrice = window.FUEL_PRICE || 1.85; const cost = (distanceKm / 100) * fuelL100 * fuelPrice; + totalFuelCost += cost; + // 1. ON DÉCLARE LES POINTS D'ABORD const startPt = MAP_POINTS[res.index]; const endPt = MAP_POINTS[res.index + 1]; @@ -322,14 +324,41 @@ function initDetailMap() { cost: cost, }; + // 🔥 DÉTECTION DES PÉAGES MANUELS DE L'ÉTAPE + let stepTollCost = 0; + if (endPt.items && endPt.items.length > 0) { + endPt.items.forEach((it) => { + const normalizedName = (it.name || "") + .toLowerCase() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, ""); + if ( + it.category === "transport" && + normalizedName.includes("peage") + ) { + stepTollCost += parseFloat(it.amount); + } + }); + } + + let tollHtml = + stepTollCost > 0 + ? `💳 ${stepTollCost.toFixed(2)} €` + : ""; + // 🔥 CONSTRUCTION DU CONTENU DE LA MODALE transitDetailsHtml += ` -
-
- 📍 ${startPt.location_name} ➔ ${endPt.location_name} +
+
+
+ 📍 ${startPt.location_name} ➔ ${endPt.location_name} +
+
+ 🚗 ${Math.round(distanceKm)} km  •  ⏱️ ${formatDuration(durationSec)} +
-
- 🚗 ${Math.round(distanceKm)} km  •  ⏱️ ${formatDuration(durationSec)} +
+ ${tollHtml} ⛽ ${cost.toFixed(2)} €
@@ -370,6 +399,7 @@ function initDetailMap() { const distEl = document.getElementById("global_total_distance"); const timeEl = document.getElementById("global_total_duration"); const distBlock = document.getElementById("block_total_distance"); + const globalFuelCostEl = document.getElementById("global_fuel_cost"); if (distEl && timeEl && distBlock) { distEl.innerText = Math.round(totalTripDistance); @@ -377,6 +407,10 @@ function initDetailMap() { distBlock.style.display = "block"; } + if (globalFuelCostEl) { + globalFuelCostEl.innerText = Math.round(totalFuelCost); + } + // 🔥 INJECTION DU HTML DANS LA MODALE const modalContainer = document.getElementById("transitDetailsContainer"); if (modalContainer) { @@ -573,10 +607,7 @@ function addCpExpenseLine( - + @@ -708,63 +739,129 @@ document.addEventListener("DOMContentLoaded", () => { }); // ============================================================================ -// 7. MOTEUR DRAG & DROP DU PLANNING CARNET DE BORD +// 7. MOTEUR DRAG & DROP DU PLANNING GLOBAL // ============================================================================ +window.PLANNING_ALL_UNPLACED = []; +window.CURRENT_PLANNING_FILTER_DATE = null; +window.PLANNING_ITEM_MAP = {}; +window.CURRENT_DRAG_DURATION = 1; // Variable pour la surbrillance multi-cases + function closePlanningModal() { document.getElementById("planningModal").style.display = "none"; document.body.classList.remove("no-scroll"); } -function openPlanningModal(step) { - document.getElementById("planningModalTitle").innerText = - tr("hdl_planning_title") + " : " + step.location_name; - const container = document.getElementById("planningContainer"); - selectedItemIdForMove = null; +function openGlobalPlanningModal() { + const holidayDataJsonEl = document.getElementById("holidayDataJson"); + if (!holidayDataJsonEl) return; - let validItems = step.items.filter((it) => it.name !== "PF_TECHNICAL_POINT"); + const holidayData = JSON.parse(holidayDataJsonEl.textContent).main; - 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"; + if (!holidayData.start_date || !holidayData.end_date) { + alert( + "Veuillez d'abord définir les dates globales du voyage dans 'Modifier les bases' ⚙️", + ); return; } + document.getElementById("planningModalTitle").innerText = + "📅 Planning Global : " + holidayData.title; + const container = document.getElementById("planningContainer"); + + selectedItemIdForMove = null; + let allPlaced = []; + window.PLANNING_ALL_UNPLACED = []; + window.PLANNING_ITEM_MAP = {}; + + // 1. Collecte de TOUS les éléments + window.MAP_POINTS.forEach((step) => { + let validItems = step.items.filter((it) => { + if (it.name === "PF_TECHNICAL_POINT") return false; + if (it.category === "transport" && it.expense_context !== "transit") + return false; + return true; + }); + + if (window.TRANSIT_DATA && window.TRANSIT_DATA[step.sort_order]) { + 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)); + validItems.push({ + id: "virtual-transit-" + step.sort_order, + sort_order: step.sort_order, + name: `Essence depuis ${tData.from}`, + category: "transport", + expense_context: "transit", + duration: h, + notes: `Trajet GPS (~${Math.round(tData.sec / 60)} min).`, + is_virtual: true, + amount: tData.cost, + }); + } + } + + validItems.forEach((it) => { + it.step_start_date = step.step_start_date; + it.step_end_date = step.step_end_date; + it.step_location = step.location_name; + it.sort_order = step.sort_order; + + const htmlId = it.is_virtual ? it.id : `drag-item-${it.id}`; + window.PLANNING_ITEM_MAP[htmlId] = it; + + if (it.item_date && it.item_time) { + allPlaced.push(it); + } else { + window.PLANNING_ALL_UNPLACED.push(it); + } + }); + }); + + // 2. Génération des dates globales let datesToDisplay = []; - let curr = new Date(step.step_start_date); - let end = new Date(step.step_end_date); - while (curr <= end) { + let curr = new Date(holidayData.start_date); + let endD = new Date(holidayData.end_date); + while (curr <= endD) { datesToDisplay.push(curr.toISOString().split("T")[0]); curr.setDate(curr.getDate() + 1); } + // 3. Construction de l'interface (Avec règles CSS injectées) let html = ` -
-
-
📥 ${tr("hdl_to_place")}
+ +
+ +
+
+
📥 ${tr("hdl_to_place")}
+ +
+
+
-
+ + +
`; datesToDisplay.forEach((dateStr) => { @@ -775,118 +872,254 @@ function openPlanningModal(step) { month: "short", }); + let unplacedForDay = window.PLANNING_ALL_UNPLACED.filter((it) => + isDateInStep(dateStr, it.step_start_date, it.step_end_date), + ); + + let badgeHtml = `${unplacedForDay.length}`; + html += ` -
-
-
${dayName}
-
${dayNum}
-
+
+
+
${badgeHtml}
+
${dayName}
+
${dayNum}
+
-
+
`; for (let h = 6; h <= 23; h++) { let hourStr = h.toString().padStart(2, "0") + ":00"; html += ` -
- ${hourStr} + ${hourStr}
`; } html += `
`; }); + html += `
`; container.innerHTML = html; - const isMobile = window.innerWidth <= 768; - const dragAttr = isMobile ? "" : 'draggable="true"'; - - validItems.forEach((it) => { - let icon = "🏷️"; - let catClass = "cat-activity"; - if (it.category === "accommodation") { - icon = "🏨"; - catClass = "cat-accommodation"; - } - if (it.category === "transport") { - icon = "🚗"; - catClass = "cat-transport"; - } - - const dur = it.duration || 1; - const noteHtml = it.notes - ? `
${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 = ` -
- -
-
${visualName}
-
- ${durControls} -
-
- ${noteHtml} -
- `; - - if (it.item_date && it.item_time && datesToDisplay.includes(it.item_date)) { + // 4. Placement des éléments + allPlaced.forEach((it) => { + if (datesToDisplay.includes(it.item_date)) { const hourPrefix = it.item_time.substring(0, 2) + ":00"; const targetSlot = container.querySelector( `.hol-time-slot[data-date="${it.item_date}"][data-time="${hourPrefix}"]`, ); - if (targetSlot) { - targetSlot.insertAdjacentHTML("beforeend", elHtml); - return; - } + if (targetSlot) + targetSlot.insertAdjacentHTML("beforeend", buildDragItemHtml(it)); + } + }); + + // 5. Météo et Focus auto + datesToDisplay.forEach((dateStr) => { + let activeStep = window.MAP_POINTS.find((step) => + isDateInStep(dateStr, step.step_start_date, step.step_end_date), + ); + if (activeStep && activeStep.lat && activeStep.lng) { + loadWeatherForPlanning(activeStep.lat, activeStep.lng, dateStr); } - document - .getElementById("unmapped-pool") - .insertAdjacentHTML("beforeend", elHtml); }); document.getElementById("planningModal").style.display = "flex"; document.body.classList.add("no-scroll"); - datesToDisplay.forEach((dateStr) => { - loadWeatherForPlanning(step.lat, step.lng, dateStr); + let todayStr = new Date().toISOString().split("T")[0]; + let defaultFocusDate = + todayStr >= holidayData.start_date && todayStr <= holidayData.end_date + ? todayStr + : holidayData.start_date; + + filterPoolByDate(defaultFocusDate); + + setTimeout(() => { + const activeCol = document.getElementById("col-" + defaultFocusDate); + if (activeCol) { + document.getElementById("calendarZoneContainer").scrollTo({ + left: activeCol.offsetLeft - 300, + behavior: "smooth", + }); + } + }, 200); + + // Moteur Scroll Natif (Drag To Scroll) + const slider = document.getElementById("calendarZoneContainer"); + let isDown = false; + let startX, startY, scrollLeft, scrollTop; + + slider.addEventListener("mousedown", (e) => { + if (e.target.closest(".hol-drag-item") || e.target.closest("button")) + return; + isDown = true; + slider.style.cursor = "grabbing"; + startX = e.pageX - slider.offsetLeft; + startY = e.pageY - slider.offsetTop; + scrollLeft = slider.scrollLeft; + scrollTop = slider.scrollTop; }); + slider.addEventListener("mouseleave", () => { + isDown = false; + slider.style.cursor = "grab"; + }); + slider.addEventListener("mouseup", () => { + isDown = false; + slider.style.cursor = "grab"; + }); + slider.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - slider.offsetLeft; + const y = e.pageY - slider.offsetTop; + const walkX = (x - startX) * 1.5; + const walkY = (y - startY) * 1.5; + slider.scrollLeft = scrollLeft - walkX; + slider.scrollTop = scrollTop - walkY; + }); +} + +function isDateInStep(targetDate, stepStart, stepEnd) { + if (!stepStart || !stepEnd) return false; + return targetDate >= stepStart && targetDate <= stepEnd; +} + +function filterPoolByDate(dateStr) { + window.CURRENT_PLANNING_FILTER_DATE = dateStr; + const pool = document.getElementById("unmapped-pool"); + pool.innerHTML = ""; + + let poolItemsHtml = ""; + let count = 0; + + // Itération sur la MAP, donc l'ordre natif de tri (sort_order/ID) est préservé ! + Object.values(window.PLANNING_ITEM_MAP).forEach((it) => { + const htmlId = it.is_virtual ? it.id : `drag-item-${it.id}`; + const isPlaced = document.querySelector(`.hol-time-slot #${htmlId}`); + + if (!isPlaced) { + if ( + !dateStr || + isDateInStep(dateStr, it.step_start_date, it.step_end_date) + ) { + poolItemsHtml += buildDragItemHtml(it); + count++; + } + } + }); + + if (count === 0) { + pool.innerHTML = `
🎉

Rien à placer${dateStr ? " pour cette journée" : ""}.
`; + } else { + pool.innerHTML = poolItemsHtml; + } +} + +function recalcAllBadges() { + const pool = document.getElementById("unmapped-pool"); + const unmappedIds = Array.from(pool.children).map((el) => el.id); + + document.querySelectorAll(".hol-day-column").forEach((col) => { + const dateStr = col.id.replace("col-", ""); + const badge = document.getElementById("badge-" + dateStr); + if (!badge) return; + + let dayCount = 0; + unmappedIds.forEach((htmlId) => { + const it = window.PLANNING_ITEM_MAP[htmlId]; + if (it && isDateInStep(dateStr, it.step_start_date, it.step_end_date)) + dayCount++; + }); + + badge.innerText = dayCount; + badge.style.display = dayCount > 0 ? "inline-block" : "none"; + }); +} + +function buildDragItemHtml(it) { + let icon = "🏷️"; + let catClass = "cat-activity"; + if (it.category === "accommodation") { + icon = "🏨"; + catClass = "cat-accommodation"; + } + if (it.category === "transport") { + icon = "🚗"; + catClass = "cat-transport"; + } + + const dur = it.duration || 1; + const noteHtml = it.notes + ? `
${it.notes}
` + : ""; + const isVirtual = it.is_virtual === true; + const isTransit = it.expense_context === "transit"; + + const durControls = ` + ${dur}h + `; + + const bgStyle = + isVirtual || isTransit + ? "background: repeating-linear-gradient(45deg, var(--bg-page), var(--bg-page) 10px, rgba(59, 130, 246, 0.05) 10px, rgba(59, 130, 246, 0.05) 20px); border: 2px dashed var(--primary);" + : "background: var(--bg-panel); border: 1px solid var(--border-strong);"; + + const visualName = isTransit ? `🛣️ Trajet & Essence` : `${icon} ${it.name}`; + const isMobile = window.innerWidth <= 768; + const dragAttr = isMobile ? "" : 'draggable="true"'; + const htmlId = isVirtual ? it.id : `drag-item-${it.id}`; + const locHint = + !it.item_date && it.step_location + ? `
📍 ${it.step_location}
` + : ""; + + // Suppression du calcul de hauteur en ligne, c'est désormais géré via CSS et la variable "--duration" + return ` +
+ ${locHint} +
+
${visualName}
+
+ ${durControls} +
+
+ ${noteHtml} +
+ `; } function changeDuration(e, itemId, delta) { e.stopPropagation(); - const itemEl = document.getElementById("drag-item-" + itemId); + const itemEl = + document.getElementById("drag-item-" + itemId) || + document.getElementById(itemId); + if (!itemEl) return; + let currentDur = parseInt(itemEl.style.getPropertyValue("--duration")) || 1; let newDur = currentDur + delta; if (newDur < 1) newDur = 1; if (newDur > 12) newDur = 12; + itemEl.style.setProperty("--duration", newDur); document.getElementById(`dur-text-${itemId}`).innerText = newDur + "h"; + + if (itemEl.getAttribute("data-virtual") === "true") { + const it = window.PLANNING_ITEM_MAP[itemEl.id]; + if (it) it.duration = newDur; + return; + } + updateItemMemory(itemId, { duration: newDur }); + const formData = new FormData(); formData.append("action", "update_item_duration"); formData.append("item_id", itemId); @@ -897,24 +1130,32 @@ function changeDuration(e, itemId, delta) { }); } -function handleItemTap(e, itemId) { +function handleItemTap(e, htmlId) { e.stopPropagation(); document .querySelectorAll(".hol-drag-item") .forEach((el) => el.classList.remove("selected-for-move")); - if (selectedItemIdForMove === itemId) { + if (selectedItemIdForMove === htmlId) { selectedItemIdForMove = null; } else { - selectedItemIdForMove = itemId; - document - .getElementById("drag-item-" + itemId) - .classList.add("selected-for-move"); + selectedItemIdForMove = htmlId; + const el = document.getElementById(htmlId); + if (el) el.classList.add("selected-for-move"); } } function handleZoneTap(e, dateStr, timeStr) { if (selectedItemIdForMove) { - handleDropLogic(selectedItemIdForMove, dateStr, timeStr, e.currentTarget); + const itemEl = document.getElementById(selectedItemIdForMove); + if (itemEl) { + const targetZone = e.currentTarget; + if (targetZone.id === "unmapped-pool") { + handleDropLogic(selectedItemIdForMove, "", ""); + } else { + targetZone.appendChild(itemEl); + handleDropLogic(selectedItemIdForMove, dateStr, timeStr); + } + } selectedItemIdForMove = null; document .querySelectorAll(".hol-drag-item") @@ -925,62 +1166,178 @@ function handleZoneTap(e, dateStr, timeStr) { function dragStart(e) { e.dataTransfer.setData("text/plain", e.target.id); e.dataTransfer.effectAllowed = "move"; + // Mémorisation de la durée pour le survol dynamique (Fix #2) + window.CURRENT_DRAG_DURATION = + parseInt(e.target.style.getPropertyValue("--duration")) || 1; } + +function dragEnd(e) { + window.CURRENT_DRAG_DURATION = 1; + document + .querySelectorAll(".hol-time-slot") + .forEach((s) => s.classList.remove("drag-over-duration")); +} + function allowDrop(e) { e.preventDefault(); } + function dragEnter(e) { e.preventDefault(); - let s = e.target.closest(".hol-time-slot"); - if (s) s.classList.add("drag-over"); + let slot = e.target.closest(".hol-time-slot"); + if (slot) { + // Retirer toutes les anciennes surbrillances + document + .querySelectorAll(".hol-time-slot") + .forEach((s) => s.classList.remove("drag-over-duration")); + + // Appliquer la surbrillance sur les N cases consécutives + let dur = window.CURRENT_DRAG_DURATION || 1; + let currentSlot = slot; + for (let i = 0; i < dur; i++) { + if (currentSlot) { + currentSlot.classList.add("drag-over-duration"); + currentSlot = currentSlot.nextElementSibling; + } + } + } } + function dragLeave(e) { - let s = e.target.closest(".hol-time-slot"); - if (s) s.classList.remove("drag-over"); + // La gestion précise des surbrillances se fait via le dragEnter et dragEnd pour éviter le scintillement (flickering). } function handleDropEvent(e, dateStr, timeStr) { e.preventDefault(); - let slot = e.target.closest(".hol-time-slot"); - if (slot) slot.classList.remove("drag-over"); + document + .querySelectorAll(".hol-time-slot") + .forEach((s) => s.classList.remove("drag-over-duration")); + const idStr = e.dataTransfer.getData("text/plain"); - const itemId = idStr.replace("drag-item-", ""); - const dropZone = slot || document.getElementById("unmapped-pool"); - handleDropLogic(itemId, dateStr, timeStr, dropZone); + const itemEl = document.getElementById(idStr); + const dropZone = + e.target.closest(".hol-time-slot") || + document.getElementById("unmapped-pool"); + + if (itemEl && dropZone) { + if (dropZone.id === "unmapped-pool") { + // FIX #3 : Contourner l'appendChild (qui met tout en bas) et déléguer à la fonction pour re-trier la colonne ! + handleDropLogic(idStr, "", ""); + } else { + dropZone.appendChild(itemEl); + handleDropLogic(idStr, dateStr, timeStr); + } + } } -function handleDropLogic(itemId, dateStr, timeStr, dropZone) { - 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)); +function handleDropLogic(htmlId, dateStr, timeStr) { + const itemEl = document.getElementById(htmlId); + if (!itemEl) return; + const isVirtual = itemEl.getAttribute("data-virtual") === "true"; + const holidayId = document.querySelector('input[name="holiday_id"]').value; + const sortOrder = itemEl.getAttribute("data-sort"); + const dur = parseInt(itemEl.style.getPropertyValue("--duration")) || 1; + const realId = itemEl.getAttribute("data-id"); + + // DÉSASSIGNER : Si on replace la carte dans la zone de gauche + if (dateStr === "" || timeStr === "") { + if (!isVirtual) { + updateItemMemory(realId, { item_date: null, item_time: null }); + const fd = new FormData(); + fd.append("action", "update_item_datetime"); + fd.append("item_id", realId); + fd.append("item_date", ""); + fd.append("item_time", ""); + fetch("/modules/holidays/includes/api/save_checkpoint.php", { + method: "POST", + body: fd, + }); + } + // 🔥 FIX #3 : Re-filtrer reconstruit la colonne de gauche dans SON ORDRE D'ORIGINE ! + filterPoolByDate(window.CURRENT_PLANNING_FILTER_DATE); + recalcAllBadges(); + return; + } + + // AFFECTATION CALENDRIER + if (isVirtual) { + const tData = window.TRANSIT_DATA[sortOrder]; const fd = new FormData(); fd.append("action", "add_single_item"); fd.append("holiday_id", holidayId); - fd.append("sort_order", step.sort_order); + fd.append("sort_order", sortOrder); fd.append("category", "transport"); fd.append("context", "transit"); + fd.append("expense_context", "transit"); fd.append("name", `Essence depuis ${tData.from}`); fd.append("amount", tData.cost); - fd.append("duration", h); + fd.append("duration", dur); + fd.append("item_date", dateStr); + fd.append("item_time", timeStr); + fd.append("ajax", "1"); + + itemEl.setAttribute("data-virtual", "false"); + + fetch("/modules/holidays/includes/api/save_checkpoint.php", { + method: "POST", + body: fd, + }) + .then((res) => res.json()) + .then(async (data) => { + if (data && data.id) { + itemEl.id = `drag-item-${data.id}`; + itemEl.setAttribute("data-id", data.id); + + const fdDate = new FormData(); + fdDate.append("action", "update_item_datetime"); + fdDate.append("item_id", data.id); + fdDate.append("item_date", dateStr); + fdDate.append("item_time", timeStr); + await fetch("/modules/holidays/includes/api/save_checkpoint.php", { + method: "POST", + body: fdDate, + }); + + window.PLANNING_ITEM_MAP[itemEl.id] = { + id: data.id, + step_start_date: dateStr, + step_end_date: dateStr, + expense_context: "transit", + category: "transport", + is_virtual: false, + }; + } else { + window.location.reload(); + } + }) + .catch(() => { + window.location.reload(); + }); + } else { + updateItemMemory(realId, { item_date: dateStr, item_time: timeStr }); + const fd = new FormData(); + fd.append("action", "update_item_datetime"); + fd.append("item_id", realId); fd.append("item_date", dateStr); fd.append("item_time", timeStr); fetch("/modules/holidays/includes/api/save_checkpoint.php", { method: "POST", body: fd, - }).then(() => window.location.reload()); - return; + }); } + + recalcAllBadges(); } function updateItemMemory(itemId, changes) { - MAP_POINTS.forEach((step) => { - let item = step.items.find((i) => i.id == itemId); - if (item) Object.assign(item, changes); - }); + if (typeof MAP_POINTS !== "undefined") { + MAP_POINTS.forEach((step) => { + let item = step.items.find((i) => i.id == itemId); + if (item) Object.assign(item, changes); + }); + } } // ============================================================================ @@ -1377,3 +1734,47 @@ window.generateTravelBook = function () { btn.disabled = false; }); }; + +// ============================================================================ +// SAUVEGARDE DES NOTES GLOBALES DU VOYAGE (AJAX) +// ============================================================================ +async function saveHolidayGlobalNote(holidayId) { + const btn = document.getElementById("btnSaveHolidayNote"); + const textarea = document.getElementById("holidayGlobalNotes"); + if (!textarea || !btn) return; + + const originalText = btn.innerHTML; + btn.innerHTML = "⏳..."; + btn.disabled = true; + + const fd = new FormData(); + fd.append("action", "update_holiday_note"); + fd.append("holiday_id", holidayId); + fd.append("notes", textarea.value); + + try { + const res = await pachaFetch( + "/modules/holidays/includes/api/save_checkpoint.php", + { + method: "POST", + body: fd, + }, + ); + + if (res.success) { + // L'appel utilise bien ta fonction showToast() globale pour le design des notifications ! + showToast( + window.I18N["bud_prev_saved"] || "Notes sauvegardées !", + "success", + ); + } else { + showToast(res.error || "Erreur lors de la sauvegarde", "error"); + } + } catch (e) { + console.error("Erreur saveHolidayGlobalNote:", e); + showToast("Erreur réseau.", "error"); + } finally { + btn.innerHTML = originalText; + btn.disabled = false; + } +} diff --git a/modules/holidays/includes/api/save_checkpoint.php b/modules/holidays/includes/api/save_checkpoint.php index 87954a9..85fe61e 100644 --- a/modules/holidays/includes/api/save_checkpoint.php +++ b/modules/holidays/includes/api/save_checkpoint.php @@ -7,11 +7,20 @@ require_login(); // --------------------------------------------------------------------------- // INTERCEPTIONS AJAX (Exécution ultra rapide sans rechargement lourd) // --------------------------------------------------------------------------- -if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime', 'update_item_duration', 'add_single_item'])) { +if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime', 'update_item_duration', 'add_single_item', 'update_holiday_note'])) { header('Content-Type: application/json'); - session_write_close(); // 🚀 LIBÈRE LA SESSION : Permet d'autres requêtes simultanées sans bloquer le navigateur + session_write_close(); try { + if ($_POST['action'] === 'update_holiday_note') { + $hId = (int)$_POST['holiday_id']; + $notes = $_POST['notes'] ?? ''; + $stmt = $pdo->prepare("UPDATE pf_holidays SET notes = ? WHERE id = ?"); + $stmt->execute([$notes, $hId]); + echo json_encode(['success' => true]); + exit; + } + if ($_POST['action'] === 'add_single_item') { $holiday_id = (int)$_POST['holiday_id']; $sort_order = (int)$_POST['sort_order']; @@ -25,16 +34,28 @@ if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime $date = !empty($_POST['item_date']) ? $_POST['item_date'] : null; $time = !empty($_POST['item_time']) ? $_POST['item_time'] : null; + $context = $_POST['expense_context'] ?? ($_POST['context'] ?? 'local'); + $pdo->beginTransaction(); + + if ($context === 'transit') { + $stmtDel = $pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ? AND expense_context = 'transit'"); + $stmtDel->execute([$holiday_id, $sort_order]); + } + $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'], $dur, $date, $time + $stepInfo['step_type'], $context, $dur, $date, $time ]); + + $newId = $pdo->lastInsertId(); + $pdo->commit(); - echo json_encode(['success' => true]); + + echo json_encode(['success' => true, 'id' => $newId]); exit; } echo json_encode(['success' => false, 'error' => 'Etape introuvable']); @@ -61,6 +82,18 @@ if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime echo json_encode(['success' => true]); exit; } + + // Mise à jour de la note globale du voyage + if ($_POST['action'] === 'update_holiday_note') { + $hId = (int)$_POST['holiday_id']; + $notes = $_POST['notes'] ?? ''; + $stmt = $pdo->prepare("UPDATE pf_holidays SET notes = ? WHERE id = ?"); + $stmt->execute([$notes, $hId]); + echo json_encode(['success' => true]); + exit; + } + + } catch (Exception $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } echo json_encode(['success' => false, 'error' => $e->getMessage()]); diff --git a/modules/holidays/views/detail.php b/modules/holidays/views/detail.php index ac35986..8515312 100644 --- a/modules/holidays/views/detail.php +++ b/modules/holidays/views/detail.php @@ -16,7 +16,8 @@ $stmt = $pdo->prepare(" (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, - (SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND expense_context = 'transit') as total_transit + (SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND (expense_context = 'transit' OR (category = 'transport' AND (name LIKE '%Essence%' OR name LIKE '%Carburant%')))) as total_fuel, + (SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND category = 'transport' AND (name LIKE '%Péage%' OR name LIKE '%Peage%')) as total_tolls FROM pf_holidays h LEFT JOIN pf_vehicles v ON h.vehicle_id = v.id WHERE h.id = ? @@ -108,6 +109,10 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0; 📖 Carnet de Voyage + + @@ -127,18 +132,30 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
- Frais de route (Essence/Péages) + Frais de route
-
- - +
+ + + 💳 + + + + + + + + + + (1.85 €/L) ✏️ - 0): ?> - + + 0 || $holiday['total_tolls'] > 0): ?> + 👁️ @@ -201,59 +218,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;

- 0 || $holiday['budget_extra'] > 0): ?> -
-
-
- 🌍 - -
- - -
-
- -
-
- -
- 0): ?> -
- 🍔 - -
- - 0): ?> -
- 🎁 - -
- - - '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' }; - ?> -
-
- - - - - - - - - - -
-
- -
-
- +
@@ -312,7 +277,6 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0; -
@@ -349,16 +313,17 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
- -
-

- 📝 -

-
- +
+

📝

+
+ +
+
- +
@@ -454,13 +419,16 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
-
-
-

📅

+
+
+

📅 Planning Global

-
- diff --git a/modules/holidays/views/modal.php b/modules/holidays/views/modal.php index 1b33646..3c8a390 100644 --- a/modules/holidays/views/modal.php +++ b/modules/holidays/views/modal.php @@ -62,10 +62,6 @@
-
- - -