diff --git a/modules/holidays/holidays.css b/modules/holidays/holidays.css index 1728b41..a5e7d88 100644 --- a/modules/holidays/holidays.css +++ b/modules/holidays/holidays.css @@ -1124,24 +1124,6 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover { 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 { @@ -1343,6 +1325,34 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover { font-weight: 700; } +/* Bouton pour retirer une carte du planning (Undo) */ +.hol-unplace-btn { + display: none; /* Caché par défaut dans la zone "À placer" */ + background: #fee2e2; + color: #ef4444; + border: 1px solid #fca5a5; + border-radius: 4px; + width: 24px; + height: 24px; + cursor: pointer; + font-size: 0.85rem; + align-items: center; + justify-content: center; + padding: 0; + transition: all 0.2s; + margin-left: 4px; +} + +.hol-unplace-btn:hover { + background: #ef4444; + color: white; +} + +/* Le bouton n'apparaît QUE quand la carte est physiquement dans le calendrier */ +.hol-time-slot .hol-drag-item .hol-unplace-btn { + display: flex; +} + .highlight-step { transition: box-shadow 0.3s, @@ -1639,3 +1649,127 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover { .hol-expense-price-group .status-pending { line-height: 1; } + +/* ============================================================================ + 📱 UX MOBILE : MODALE DE PLANNING (DRAG & DROP) + ============================================================================ */ +@media (max-width: 768px) { + /* --- MODALE GLOBALE --- */ + #planningModal .pf-modal-content { + padding: 8px !important; + height: 98vh !important; + max-height: 98vh !important; + display: flex !important; + flex-direction: column !important; + } + + #planningModal .pf-modal-content > div:first-child { + margin-bottom: 6px !important; + padding-bottom: 6px !important; + } + #planningModalTitle { + font-size: 1rem !important; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + #planningContainer > div { + flex-direction: column !important; + padding-bottom: 0 !important; + gap: 8px !important; + height: 100% !important; + } + + /* --- 1. LE CALENDRIER (EN HAUT) --- */ + .hol-calendar-zone { + width: 100% !important; + flex: 1 1 auto !important; /* Le calendrier prend tout l'espace restant */ + order: 1 !important; + padding-bottom: 10px !important; + margin-top: 0 !important; + gap: 6px !important; + } + + /* 🎯 Header des Jours ultra-compressé */ + .hol-calendar-day-header { + padding: 4px !important; + } + .hol-cal-weekday { + font-size: 0.6rem !important; + margin-bottom: 0 !important; + } + .hol-cal-date { + font-size: 0.9rem !important; + line-height: 1 !important; + margin-bottom: 2px !important; + } + + div[id^="plan-weather-"] { + min-height: 14px !important; + margin-top: 0 !important; + } + div[id^="plan-weather-"] .pf-weather-badge { + padding: 1px 4px !important; + font-size: 0.55rem !important; + } + + .hol-time-slot { + min-height: 40px !important; + padding: 4px !important; + } + + /* --- 2. LA ZONE "À PLACER" (BOTTOM SHEET - 30%) --- */ + .hol-unmapped-zone { + width: 100% !important; + height: 30vh !important; /* 🔼 Prend exactement 30% de l'écran */ + min-height: 180px !important; + flex: 0 0 auto !important; /* 🛡️ Protection anti-écrasement */ + order: 2 !important; + border-radius: 12px 12px 0 0 !important; + border-top: 2px solid var(--primary) !important; + border-bottom: none !important; + border-left: none !important; + border-right: none !important; + box-shadow: 0 -4px 15px rgba(0, 0, 0, 0.15) !important; + margin: 0 -8px -8px -8px !important; + padding: 12px 10px 15px 10px !important; + z-index: 100; + background: var(--bg-panel) !important; + display: flex !important; + flex-direction: column !important; /* ↕️ On repasse en mode vertical */ + } + + .hol-unmapped-title { + font-size: 0.8rem !important; + margin-bottom: 8px !important; + flex-shrink: 0 !important; + } + + /* --- 3. LISTE VERTICALE (Scroll Down) --- */ + #unmapped-pool { + flex-direction: column !important; /* ↕️ Les cartes s'empilent */ + overflow-y: auto !important; /* ↕️ Apparition du scroll vertical */ + overflow-x: hidden !important; + padding-bottom: 10px !important; + padding-right: 5px !important; /* Espace pour la barre de scroll */ + gap: 8px !important; + -webkit-overflow-scrolling: touch; + flex: 1 !important; /* Prend tout l'espace des 30vh */ + align-items: stretch !important; + } + + /* 🎯 Les cartes s'adaptent à 100% de la largeur du téléphone */ + #unmapped-pool .hol-drag-item { + width: 100% !important; + height: auto !important; + min-height: 50px !important; + padding: 8px 10px !important; + white-space: normal !important; + } + + /* 🗑️ On cache le footer de la modale pour gagner de l'espace */ + #planningModal .modal-footer { + display: none !important; + } +} diff --git a/modules/holidays/holidays.js b/modules/holidays/holidays.js index 8c4b099..5e92d44 100644 --- a/modules/holidays/holidays.js +++ b/modules/holidays/holidays.js @@ -1,3 +1,5 @@ +window.PLANNING_MODIFIED = false; + // ============================================================================ // FONCTION DE TRADUCTION JS & LANGUE COURANTE // ============================================================================ @@ -528,6 +530,7 @@ function openCheckpointModal(mode, data = null) { it.item_date || "", it.item_time || "", it.duration || 1, + it.expense_context || "local", ); visibleCount++; } @@ -594,6 +597,7 @@ function addCpExpenseLine( itemDate = "", itemTime = "", itemDur = 1, + expenseContext = "local", // 🔥 FIX : On accepte le contexte dynamique ) { const container = document.getElementById("cpExpensesContainer"); const div = document.createElement("div"); @@ -607,7 +611,8 @@ function addCpExpenseLine( - + + @@ -616,7 +621,8 @@ function addCpExpenseLine( ${tr("hdl_paid")} - + +
@@ -679,7 +685,8 @@ function moveStepMobile(btn, direction) { } } -document.addEventListener("DOMContentLoaded", () => { +// 🔥 NOUVEAU : Fonction réutilisable pour ré-attacher les événements +function initStepDragAndDrop() { const checkpoints = document.querySelectorAll(".hol-checkpoint-draggable"); const container = checkpoints[0]?.parentElement; if (!container) return; @@ -687,7 +694,17 @@ document.addEventListener("DOMContentLoaded", () => { const isMobile = window.innerWidth <= 768; let draggedItem = null; + // Nettoyage des anciens écouteurs en clonant les nœuds (Indispensable après un refresh silencieux) checkpoints.forEach((item) => { + const clone = item.cloneNode(true); + if (item.parentNode) item.parentNode.replaceChild(clone, item); + }); + + const freshCheckpoints = document.querySelectorAll( + ".hol-checkpoint-draggable", + ); + + freshCheckpoints.forEach((item) => { if (isMobile) { item.removeAttribute("draggable"); return; @@ -727,16 +744,17 @@ document.addEventListener("DOMContentLoaded", () => { (closest, child) => { const box = child.getBoundingClientRect(); const offset = y - box.top - box.height / 2; - if (offset < 0 && offset > closest.offset) { + if (offset < 0 && offset > closest.offset) return { offset: offset, element: child }; - } else { - return closest; - } + else return closest; }, { offset: Number.NEGATIVE_INFINITY }, ).element; } -}); +} + +// On lance l'initialisation au démarrage de la page +document.addEventListener("DOMContentLoaded", initStepDragAndDrop); // ============================================================================ // 7. MOTEUR DRAG & DROP DU PLANNING GLOBAL @@ -986,8 +1004,12 @@ function openGlobalPlanningModal() { } function isDateInStep(targetDate, stepStart, stepEnd) { - if (!stepStart || !stepEnd) return false; - return targetDate >= stepStart && targetDate <= stepEnd; + if (!stepStart && !stepEnd) return false; + + const start = stepStart || stepEnd; + const end = stepEnd || stepStart; + + return targetDate >= start && targetDate <= end; } function filterPoolByDate(dateStr) { @@ -1079,7 +1101,6 @@ function buildDragItemHtml(it) { ? `
📍 ${it.step_location}
` : ""; - // Suppression du calcul de hauteur en ligne, c'est désormais géré via CSS et la variable "--duration" return `
${visualName}
-
- ${durControls} + +
+
+ ${durControls} +
+ +
+
${noteHtml}
`; } +function unplaceItem(e, htmlId) { + e.stopPropagation(); + handleDropLogic(htmlId, "", ""); +} + function changeDuration(e, itemId, delta) { e.stopPropagation(); const itemEl = @@ -1240,7 +1272,7 @@ function handleDropLogic(htmlId, dateStr, timeStr) { 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 + // DÉSASSIGNER (Annulation) : Si on replace la carte dans la zone "À placer" if (dateStr === "" || timeStr === "") { if (!isVirtual) { updateItemMemory(realId, { item_date: null, item_time: null }); @@ -1249,14 +1281,23 @@ function handleDropLogic(htmlId, dateStr, timeStr) { fd.append("item_id", realId); fd.append("item_date", ""); fd.append("item_time", ""); + + // Enregistrement en base + Toast fetch("/modules/holidays/includes/api/save_checkpoint.php", { method: "POST", body: fd, + }).then(() => { + if (typeof showToast === "function") + showToast("📍 Élément remis en attente", "success"); }); } - // 🔥 FIX #3 : Re-filtrer reconstruit la colonne de gauche dans SON ORDRE D'ORIGINE ! + + // 🔥 C'EST LA CLÉ : On enlève physiquement la carte du calendrier ! + itemEl.remove(); + filterPoolByDate(window.CURRENT_PLANNING_FILTER_DATE); recalcAllBadges(); + window.PLANNING_MODIFIED = true; return; } @@ -1307,13 +1348,35 @@ function handleDropLogic(htmlId, dateStr, timeStr) { category: "transport", is_virtual: false, }; + + if (typeof window.MAP_POINTS !== "undefined") { + const stepObj = window.MAP_POINTS.find( + (s) => s.sort_order == sortOrder, + ); + if (stepObj) { + stepObj.items.push({ + id: data.id, + category: "transport", + name: `Essence depuis ${tData.from}`, + amount: tData.cost, + expense_context: "transit", + duration: dur, + item_date: dateStr, + item_time: timeStr, + }); + } + } + + if (typeof showToast === "function") + showToast("🚗 Trajet généré et sauvegardé !", "success"); + itemEl.style.transition = "box-shadow 0.3s ease"; + itemEl.style.boxShadow = "0 0 0 3px var(--success)"; + setTimeout(() => (itemEl.style.boxShadow = ""), 1500); } else { window.location.reload(); } }) - .catch(() => { - window.location.reload(); - }); + .catch(() => window.location.reload()); } else { updateItemMemory(realId, { item_date: dateStr, item_time: timeStr }); const fd = new FormData(); @@ -1325,10 +1388,22 @@ function handleDropLogic(htmlId, dateStr, timeStr) { fetch("/modules/holidays/includes/api/save_checkpoint.php", { method: "POST", body: fd, - }); + }) + .then(() => { + if (typeof showToast === "function") + showToast("📅 Activité planifiée !", "success"); + itemEl.style.transition = "box-shadow 0.3s ease"; + itemEl.style.boxShadow = "0 0 0 3px var(--success)"; + setTimeout(() => (itemEl.style.boxShadow = ""), 1500); + }) + .catch(() => { + if (typeof showToast === "function") + showToast("❌ Erreur de sauvegarde", "error"); + }); } recalcAllBadges(); + silentlyRefreshSteps(); } function updateItemMemory(itemId, changes) { @@ -1340,6 +1415,53 @@ function updateItemMemory(itemId, changes) { } } +/** + * Rafraîchit uniquement le conteneur des étapes sans recharger toute la page + */ +async function silentlyRefreshSteps() { + try { + const res = await fetch(window.location.href); + const html = await res.text(); + const parser = new DOMParser(); + const doc = parser.parseFromString(html, "text/html"); + + // 🕵️ CIBLAGE INTELLIGENT : On trouve le panel-body qui contient les étapes (ou le texte "Aucune étape") + const getTargetContainer = (documentObj) => + Array.from(documentObj.querySelectorAll(".hol-panel-body")).find( + (el) => + el.querySelector(".hol-checkpoint") || + el.innerHTML.toLowerCase().includes("aucune étape") || + el.innerHTML.toLowerCase().includes("cap etapa"), + ); + + const currentContainer = getTargetContainer(document); + const newContainer = getTargetContainer(doc); + + if (currentContainer && newContainer) { + // Un léger effet de flash pour indiquer à l'utilisateur que ça s'est mis à jour + currentContainer.style.opacity = "0.5"; + + setTimeout(() => { + currentContainer.innerHTML = newContainer.innerHTML; + currentContainer.style.opacity = "1"; + + // Mise à jour du prix global de l'essence en haut + const globalFuel = document.getElementById("global_fuel_cost"); + const newGlobalFuel = doc.getElementById("global_fuel_cost"); + if (globalFuel && newGlobalFuel) + globalFuel.innerHTML = newGlobalFuel.innerHTML; + + // 🚀 On relance le Drag & Drop des étapes ! + initStepDragAndDrop(); + }, 150); + } else { + window.location.reload(); // Fallback de sécurité ultime + } + } catch (e) { + console.error("Erreur lors du rafraîchissement silencieux", e); + } +} + // ============================================================================ // MÉTÉO SPÉCIFIQUE AU HEADER DU PLANNING // ============================================================================ @@ -1426,7 +1548,8 @@ function addQuickTransitExpense( fd.append("amount", amount); fd.append("context", "transit"); - // 🔥 On sécurise la durée en base de données + fd.append("expense_context", "transit"); + const h = Math.max(1, Math.round(durationSec / 3600)); fd.append("duration", h); @@ -1778,3 +1901,46 @@ async function saveHolidayGlobalNote(holidayId) { btn.disabled = false; } } + +// ============================================================================ +// SOUMISSION AJAX DU FORMULAIRE "MODIFIER LES BASES" +// ============================================================================ +document.addEventListener("DOMContentLoaded", () => { + const holidayForm = document.getElementById("holidayForm"); + if (holidayForm) { + holidayForm.addEventListener("submit", async function (e) { + // Si on demande la suppression, on laisse le comportement natif faire le travail + if (this.querySelector('input[name="action_delete"]')) return; + + e.preventDefault(); + const submitBtn = this.querySelector('button[type="submit"]'); + const originalText = submitBtn.innerHTML; + submitBtn.innerHTML = "⏳..."; + submitBtn.disabled = true; + + const fd = new FormData(this); + // On s'assure d'appeler l'API de sauvegarde + const actionUrl = + this.getAttribute("action") || + "/modules/holidays/includes/api/save_holiday.php"; + + try { + // On utilise pachaFetch en mode raw/text car save_holiday.php fait sûrement un header('Location: ...') au lieu d'un JSON + const response = await fetch(actionUrl, { + method: "POST", + body: fd, + headers: { "X-Requested-With": "XMLHttpRequest" }, + }); + + // Quoi qu'il arrive, on recharge la page COURANTE (detail.php) pour afficher les modifications + window.location.reload(); + } catch (err) { + console.error(err); + if (typeof showToast === "function") + showToast("Erreur lors de la sauvegarde", "error"); + submitBtn.innerHTML = originalText; + submitBtn.disabled = false; + } + }); + } +}); diff --git a/modules/holidays/includes/api/save_checkpoint.php b/modules/holidays/includes/api/save_checkpoint.php index 85fe61e..e27e1e5 100644 --- a/modules/holidays/includes/api/save_checkpoint.php +++ b/modules/holidays/includes/api/save_checkpoint.php @@ -34,25 +34,40 @@ 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'); + // Récupération sécurisée du contexte + $context = !empty($_POST['expense_context']) ? $_POST['expense_context'] : (!empty($_POST['context']) ? $_POST['context'] : 'local'); + $amount = (float)$_POST['amount']; + $name = $_POST['name'] ?? 'Trajet'; + $category = $_POST['category'] ?? 'transport'; $pdo->beginTransaction(); + // 🛡️ ANTI-DOUBLON ABSOLU 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]); + $stmtCheck = $pdo->prepare("SELECT id FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ? AND expense_context = 'transit'"); + $stmtCheck->execute([$holiday_id, $sort_order]); + $existingId = $stmtCheck->fetchColumn(); + + if ($existingId) { + // Mise à jour de la ligne existante + $stmtUp = $pdo->prepare("UPDATE pf_holidays_items SET item_date = ?, item_time = ?, amount = ?, duration = ?, name = ? WHERE id = ?"); + $stmtUp->execute([$date, $time, $amount, $dur, $name, $existingId]); + $pdo->commit(); + echo json_encode(['success' => true, 'id' => $existingId]); + exit; + } } + // Insertion si aucun doublon n'existe $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, + $holiday_id, $category, $name, $amount, 0, $stepInfo['location_name'], $stepInfo['lat'], $stepInfo['lng'], $sort_order, $stepInfo['step_start_date'], $stepInfo['step_end_date'], $stepInfo['step_type'], $context, $dur, $date, $time ]); $newId = $pdo->lastInsertId(); - $pdo->commit(); echo json_encode(['success' => true, 'id' => $newId]); @@ -83,17 +98,6 @@ if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime 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 8515312..b623fe3 100644 --- a/modules/holidays/views/detail.php +++ b/modules/holidays/views/detail.php @@ -22,6 +22,7 @@ $stmt = $pdo->prepare(" LEFT JOIN pf_vehicles v ON h.vehicle_id = v.id WHERE h.id = ? "); + $stmt->execute([$id]); $holiday = $stmt->fetch(PDO::FETCH_ASSOC);