From c2aa1e9f7bf842f47828a07c18af7c4e299b4f2d Mon Sep 17 00:00:00 2001 From: "fefe.clochette" Date: Thu, 2 Apr 2026 15:54:40 +0200 Subject: [PATCH] planning view --- modules/holidays/holidays.css | 246 +++++++++++++++ modules/holidays/holidays.js | 295 +++++++++++++++++- .../holidays/includes/api/save_checkpoint.php | 46 ++- modules/holidays/views/detail.php | 38 ++- 4 files changed, 611 insertions(+), 14 deletions(-) diff --git a/modules/holidays/holidays.css b/modules/holidays/holidays.css index 215f034..2183f9d 100644 --- a/modules/holidays/holidays.css +++ b/modules/holidays/holidays.css @@ -1,6 +1,11 @@ /* modules/holidays/holidays.css */ /* --- 1. VARIABLES & BASE --- */ + +/* Bloque le défilement de la page en arrière-plan */ +body.no-scroll { + overflow: hidden !important; +} :root { --primary: #2563eb; --primary-hover: #1d4ed8; @@ -939,3 +944,244 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover { background: #fca5a5; color: #b91c1c; } + +/* ========================================================================== + 16. PLANNING DRAG & DROP (CARNET DE VOYAGE) + ========================================================================== */ +/* Simplification de la sous-ligne du formulaire d'édition */ +.hol-form-subrow { + display: flex; + margin-top: 6px; + padding-left: 58px; +} +.hol-form-notes-full { + margin: 0 !important; + width: 100%; +} + +/* Layout du Planning Interactif */ +.hol-planning-layout { + display: flex; + gap: 20px; + height: 65vh; + align-items: stretch; + overflow: hidden; +} +@media (max-width: 768px) { + .hol-planning-layout { + flex-direction: column; + height: 75vh; + } + .hol-unmapped-zone { + width: 100%; + height: 180px; + min-height: 180px; + flex-direction: row; + overflow-x: auto; + overflow-y: hidden; + align-items: flex-start; + } + .hol-calendar-zone { + flex: 1; + } +} + +/* Zone des activités non planifiées */ +.hol-unmapped-zone { + width: 250px; + background: #f8fafc; + padding: 15px; + border-radius: 12px; + border: 2px dashed #cbd5e1; + display: flex; + flex-direction: column; + gap: 10px; + overflow-y: auto; +} +.hol-unmapped-title { + font-size: 0.85rem; + font-weight: 700; + text-transform: uppercase; + color: #64748b; + margin-bottom: 5px; + text-align: center; +} + +/* Grille du calendrier */ +.hol-calendar-zone { + flex: 1; + display: flex; + gap: 15px; + overflow: auto; /* Gère à la fois gauche/droite ET haut/bas */ + padding-bottom: 10px; + position: relative; +} +.hol-calendar-zone::-webkit-scrollbar, +.hol-unmapped-zone::-webkit-scrollbar { + height: 10px; +} +.hol-calendar-zone::-webkit-scrollbar-track, +.hol-unmapped-zone::-webkit-scrollbar-track { + background: #f1f5f9; + border-radius: 5px; +} +.hol-calendar-zone::-webkit-scrollbar-thumb, +.hol-unmapped-zone::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 5px; +} +.hol-calendar-zone::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + +.hol-calendar-zone .hol-drag-item { + position: absolute; + top: 1px; + left: 45px; + right: 5px; + height: calc(75px * var(--duration) - 2px); +} +/* Dans la boîte d'attente : Position normale */ +.hol-unmapped-zone .hol-drag-item { + position: relative; + min-width: 150px; + height: auto; +} + +/* Mode "Tap-to-Move" pour Mobile */ +.hol-drag-item.selected-for-move { + border-color: #f59e0b; + transform: scale(1.02); + box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.3); +} + +/* Boutons Durée */ +.hol-item-duration-controls { + display: flex; + align-items: center; + gap: 3px; + margin: 0; + padding: 2px; + border: none; + background: rgba(255, 255, 255, 0.4); /* Léger fond pour se détacher */ + border-radius: 4px; +} + +.hol-day-column { + min-width: 220px; + flex: 1; + background: white; + border-radius: 12px; + border: 1px solid #e2e8f0; + display: flex; + flex-direction: column; + height: max-content; /* Laisse la colonne grandir jusqu'à 22h */ + overflow: visible; +} + +.hol-calendar-day-header { + text-align: center; + padding: 12px 10px; + background: #f8fafc; + border-bottom: 2px solid #e2e8f0; + position: sticky; + top: 0; /* Garde la date toujours visible en haut ! */ + z-index: 20; +} + +/* Les créneaux horaires (Drop Zones) */ +.hol-time-slots-container { + display: flex; + flex-direction: column; +} + +.hol-time-slot { + height: 75px; + border-bottom: 1px solid #f1f5f9; + position: relative; + transition: background 0.2s; + box-sizing: border-box; +} + +.hol-time-slot.drag-over { + background: #e0f2fe; /* Surbrillance quand on survole avec une activité */ +} +.hol-slot-label { + position: absolute; + top: 2px; + right: 5px; + font-size: 0.65rem; + font-weight: 700; + color: #94a3b8; + pointer-events: none; +} + +/* L'Activité (Élément draggable) */ +.hol-drag-item { + background: white; + padding: 6px 8px; + border-radius: 6px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); + cursor: grab; + font-size: 0.85rem; + border: 1px solid #e2e8f0; + border-left: 4px solid var(--primary); + display: flex; + flex-direction: column; + z-index: 10; + overflow: hidden; + transition: + transform 0.2s, + border-color 0.2s; +} +.hol-drag-item:active { + cursor: grabbing; +} +.hol-drag-item.cat-accommodation { + border-left-color: #8b5cf6; +} +.hol-drag-item.cat-transport { + border-left-color: #10b981; +} + +.hol-drag-title { + font-weight: 600; + color: #0f172a; + line-height: 1.2; +} +.hol-drag-note { + font-size: 0.75rem; + color: #64748b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Élargir la modale de planning */ +#planningModal .pf-modal-content { + max-width: 1100px !important; + width: 95%; +} + +.hol-dur-btn { + background: #f1f5f9; + border: none; + border-radius: 4px; + width: 18px; + height: 18px; /* Plus petit */ + cursor: pointer; + font-weight: bold; + color: #475569; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8rem; +} +.hol-dur-btn:hover { + background: #e2e8f0; +} +.hol-dur-text { + font-size: 0.7rem; + color: #64748b; + font-weight: 700; +} diff --git a/modules/holidays/holidays.js b/modules/holidays/holidays.js index 6a62c18..40aed04 100644 --- a/modules/holidays/holidays.js +++ b/modules/holidays/holidays.js @@ -359,6 +359,8 @@ function panMapTo(lat, lng) { } // --- LOGIQUE DE LA MODALE CHECKPOINT (Lignes multiples) --- +document.getElementById("cp_start_date").value = ""; +document.getElementById("cp_end_date").value = ""; function openCheckpointModal(mode, data = null) { const searchBlock = document.getElementById("cpSearchBlock"); @@ -387,18 +389,24 @@ function openCheckpointModal(mode, data = null) { document.getElementById("cp_lng").value = data.lng; document.getElementById("cp_old_sort_order").value = data.sort_order; 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 || ""; if (data.items && data.items.length > 0) { let visibleCount = 0; data.items.forEach((it) => { if (it.name !== "PF_TECHNICAL_POINT") { - // On passe it.notes à la fonction ! + // On transmet l'ID, la date, l'heure et la durée ! addCpExpenseLine( it.category, it.name, it.amount, it.is_paid, it.notes || "", + it.id || "", + it.item_date || "", + it.item_time || "", + it.duration || 1, ); visibleCount++; } @@ -409,6 +417,7 @@ function openCheckpointModal(mode, data = null) { } } document.getElementById("checkpointModal").style.display = "flex"; + document.body.classList.add("no-scroll"); } function searchPlace() { @@ -466,10 +475,14 @@ function addCpExpenseLine( amount = "", isPaid = 0, notes = "", + itemId = "", + itemDate = "", + itemTime = "", + itemDur = 1, ) { const container = document.getElementById("cpExpensesContainer"); const div = document.createElement("div"); - div.className = "hol-form-row"; // Classe globale de la ligne + div.className = "hol-form-row"; const isChecked = isPaid == 1 ? "checked" : ""; @@ -480,7 +493,7 @@ function addCpExpenseLine( - + - - +
+ +
+ + + + + `; container.appendChild(div); } @@ -583,3 +602,269 @@ document.addEventListener("DOMContentLoaded", () => { }); } }); + +// ============================================================================ +// MOTEUR DRAG & DROP DU PLANNING +// ============================================================================ +let selectedItemIdForMove = null; // Mémoire pour le mode Mobile + +function openPlanningModal(step) { + document.getElementById("planningModalTitle").innerText = + "📅 Planning : " + step.location_name; + const container = document.getElementById("planningContainer"); + selectedItemIdForMove = null; // Reset à l'ouverture + + let validItems = step.items.filter((it) => it.name !== "PF_TECHNICAL_POINT"); + + if (!step.step_start_date || !step.step_end_date) { + container.innerHTML = `

⚠️ Dates manquantes

Veuillez modifier cette étape (✏️) et renseigner sa Date d'arrivée et de départ.

`; + document.getElementById("planningModal").style.display = "flex"; + return; + } + + let datesToDisplay = []; + let curr = new Date(step.step_start_date); + let end = new Date(step.step_end_date); + while (curr <= end) { + datesToDisplay.push(curr.toISOString().split("T")[0]); + curr.setDate(curr.getDate() + 1); + } + + // Zone À PLACER : Ajout du onclick pour le mobile + let html = ` +
+
+
📥 À Placer
+
+
+ `; + + datesToDisplay.forEach((dateStr) => { + const dObj = new Date(dateStr); + const dayName = dObj.toLocaleDateString("fr-FR", { weekday: "short" }); + const dayNum = dObj.toLocaleDateString("fr-FR", { + day: "numeric", + month: "short", + }); + + html += ` +
+
${dayName}
${dayNum}
+
+ `; + + for (let h = 8; h <= 22; h++) { + let hourStr = h.toString().padStart(2, "0") + ":00"; + // Créneaux : Ajout du onclick pour le mobile + html += ` +
+ ${hourStr} +
+ `; + } + html += `
`; + }); + html += `
`; + container.innerHTML = html; + + // Rendu des activités + 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}
` + : ""; + + // Ajout du style inline --duration, et des événements drag + click(mobile) + const elHtml = ` +
+ +
+
${icon} ${it.name}
+
+ + ${dur}h + +
+
+ + ${noteHtml} +
+ `; + + if (it.item_date && it.item_time && 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; + } + } + document + .getElementById("unmapped-pool") + .insertAdjacentHTML("beforeend", elHtml); + }); + + // Affiche la modale et bloque le scroll de l'arrière-plan + document.getElementById("planningModal").style.display = "flex"; + document.body.classList.add("no-scroll"); +} + +// -- GESTION DES DURÉES -- +function changeDuration(e, itemId, delta) { + e.stopPropagation(); // Empêche de déclencher le Tap-to-Move + const itemEl = document.getElementById("drag-item-" + itemId); + let currentDur = parseInt(itemEl.style.getPropertyValue("--duration")) || 1; + let newDur = currentDur + delta; + + if (newDur < 1) newDur = 1; + if (newDur > 12) newDur = 12; // Max 12h pour pas casser l'affichage + + itemEl.style.setProperty("--duration", newDur); + document.getElementById(`dur-text-${itemId}`).innerText = newDur + "h"; + + // Mise à jour de la mémoire JS (Évite le bug au refresh) + updateItemMemory(itemId, { duration: newDur }); + + // Sauvegarde BDD + const formData = new FormData(); + formData.append("action", "update_item_duration"); + formData.append("item_id", itemId); + formData.append("duration", newDur); + fetch("/modules/holidays/includes/api/save_checkpoint.php", { + method: "POST", + body: formData, + }); +} + +// -- MODE MOBILE (TAP TO MOVE) -- +function handleItemTap(e, itemId) { + e.stopPropagation(); + document + .querySelectorAll(".hol-drag-item") + .forEach((el) => el.classList.remove("selected-for-move")); + + if (selectedItemIdForMove === itemId) { + selectedItemIdForMove = null; // Désélectionner + } else { + selectedItemIdForMove = itemId; + document + .getElementById("drag-item-" + itemId) + .classList.add("selected-for-move"); + } +} + +function handleZoneTap(e, dateStr, timeStr) { + if (selectedItemIdForMove) { + handleDropLogic(selectedItemIdForMove, dateStr, timeStr, e.currentTarget); + selectedItemIdForMove = null; + document + .querySelectorAll(".hol-drag-item") + .forEach((el) => el.classList.remove("selected-for-move")); + } +} + +// -- MODE DESKTOP (DRAG & DROP) -- +function dragStart(e) { + e.dataTransfer.setData("text/plain", e.target.id); + e.dataTransfer.effectAllowed = "move"; +} +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"); +} +function dragLeave(e) { + let s = e.target.closest(".hol-time-slot"); + if (s) s.classList.remove("drag-over"); +} + +function handleDropEvent(e, dateStr, timeStr) { + e.preventDefault(); + let slot = e.target.closest(".hol-time-slot"); + if (slot) slot.classList.remove("drag-over"); + + 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); +} + +// -- LOGIQUE COMMUNE (Déplacement, Mémoire et BDD) -- +function handleDropLogic(itemId, dateStr, timeStr, dropZone) { + const draggedEl = document.getElementById("drag-item-" + itemId); + if (dropZone && draggedEl) { + dropZone.appendChild(draggedEl); // Déplacement visuel + + // 1. Mise à jour de la mémoire JS Globale (Corrige le bug de la zone "À PLACER" au retour) + updateItemMemory(itemId, { item_date: dateStr, item_time: timeStr }); + + // 2. Sauvegarde BDD + 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); + fetch("/modules/holidays/includes/api/save_checkpoint.php", { + method: "POST", + body: formData, + }); + } +} + +// Met à jour l'objet JS global MAP_POINTS pour que la modale s'ouvre avec les bonnes données sans F5 +function updateItemMemory(itemId, changes) { + MAP_POINTS.forEach((step) => { + let item = step.items.find((i) => i.id == itemId); + if (item) { + Object.assign(item, changes); + } + }); +} + +function saveItemDateTime(itemId, dateStr, 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); + + fetch("/modules/holidays/includes/api/save_checkpoint.php", { + method: "POST", + body: formData, + }).catch((err) => + console.error("Erreur lors de la sauvegarde du planning :", err), + ); +} + +// Fermeture des modales en cliquant sur l'arrière-plan flouté +window.addEventListener("click", function (event) { + if (event.target.classList.contains("pf-modal")) { + event.target.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 0e1d3a0..12248d8 100644 --- a/modules/holidays/includes/api/save_checkpoint.php +++ b/modules/holidays/includes/api/save_checkpoint.php @@ -4,6 +4,25 @@ 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']; + + 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 { + $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 ! +} + $holiday_id = (int)$_POST['holiday_id']; $location_name = trim($_POST['location_name']); $lat = (float)$_POST['lat']; @@ -34,8 +53,13 @@ if ($holiday_id > 0 && !empty($location_name)) { $target_order = ($max !== null) ? (int)$max + 1 : 0; } - // 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) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); $validItemsCount = 0; + // Récupération des dates de l'étape globale + $step_start = !empty($_POST['step_start_date']) ? $_POST['step_start_date'] : null; + $step_end = !empty($_POST['step_end_date']) ? $_POST['step_end_date'] : null; + + // 3. INSERTION DES LIGNES (Avec Mémoire de la Durée et du Planning) + $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) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + $validItemsCount = 0; if (isset($_POST['items']['name'])) { foreach ($_POST['items']['name'] as $i => $raw_name) { @@ -44,15 +68,27 @@ if ($holiday_id > 0 && !empty($location_name)) { if ($name !== '' || $amount_raw !== '') { $cat = $_POST['items']['cat'][$i] ?? 'activity'; $amount = (float)$amount_raw; - $paid = (isset($_POST['items']['paid'][$i]) && (int)$_POST['items']['paid'][$i] === 1) ? 1 : 0; + $paid = (isset($_POST['items']['paid'][$i]) && (int)$_POST['items']['paid'][$i] === 1) ? 1 : 0; $note = trim($_POST['items']['notes'][$i] ?? ''); - $stmt->execute([$holiday_id, $cat, $name ?: 'Dépense', $amount, $paid, $location_name, $lat, $lng, $target_order, $note]); $validItemsCount++; + + // Récupération des données invisibles + $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; + + $stmt->execute([$holiday_id, $cat, $name ?: 'Dépense', $amount, $paid, $location_name, $lat, $lng, $target_order, $note, $date, $time, $step_start, $step_end, $dur]); + $validItemsCount++; } } } if ($validItemsCount === 0) { - $stmt->execute([$holiday_id, 'activity', 'PF_TECHNICAL_POINT', 0, 1, $location_name, $lat, $lng, $target_order, '']); + $stmt->execute([$holiday_id, 'activity', 'PF_TECHNICAL_POINT', 0, 1, $location_name, $lat, $lng, $target_order, '', null, null, $step_start, $step_end, 1]); + } + + // Point technique si aucune dépense n'est saisie (pour garder la trace de l'étape et de ses dates) + 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]); } // 4. GESTION DES FAVORIS diff --git a/modules/holidays/views/detail.php b/modules/holidays/views/detail.php index bf5468f..949ab5c 100644 --- a/modules/holidays/views/detail.php +++ b/modules/holidays/views/detail.php @@ -28,6 +28,7 @@ $favorites = json_decode($stmtFav->fetchColumn() ?: '[]', true); $steps = []; $generalItems = []; +// C'est ici qu'il manquait sûrement une accolade ! foreach ($items as $it) { if ($it['location_name'] !== null) { $orderKey = $it['sort_order']; @@ -37,6 +38,8 @@ foreach ($items as $it) { 'lat' => (float)$it['lat'], 'lng' => (float)$it['lng'], 'sort_order' => $it['sort_order'], + 'step_start_date' => $it['step_start_date'], + 'step_end_date' => $it['step_end_date'], 'total_amount' => 0, 'items' => [] ]; @@ -138,10 +141,16 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
📍 + +
+ Du au +
+
+
@@ -199,8 +208,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;

📍 Placer une étape

- -
+
@@ -232,6 +240,16 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
+
+
+ + +
+
+ + +
+
@@ -253,14 +271,26 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
- - +
+
+
+
+

📅 Planning

+
+ +
+ +
+
+
+
+