document.addEventListener("DOMContentLoaded", () => { // Fermer les modales si on clique en dehors du contenu window.onclick = function (event) { const modal = document.getElementById("holidayModal"); if (event.target == modal) { closeHolidayModal(); } }; }); // --- 1. GESTION DE LA MODALE D'ÉDITION RAPIDE --- function openHolidayModal(mode) { const modal = document.getElementById("holidayModal"); const form = document.getElementById("holidayForm"); const btnDelete = document.getElementById("btn_delete"); // Reset complet pour éviter les résidus d'une carte précédente 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 = "Planifier le voyage"; btnDelete.style.display = "none"; } else { document.getElementById("modalTitle").innerText = "Modification rapide"; btnDelete.style.display = "block"; } modal.style.display = "flex"; // Focus sur le champ titre pour une saisie rapide (petit délai pour l'animation d'ouverture) setTimeout(() => document.getElementById("inp_title").focus(), 100); } function closeHolidayModal() { document.getElementById("holidayModal").style.display = "none"; } function editHoliday(data) { console.log("1. 🛠️ editHoliday déclenchée avec :", data); const h = data.main; const modal = document.getElementById("holidayModal"); if (!modal) { alert( "Erreur : La modale 'holidayModal' est introuvable dans le code HTML.", ); return; } console.log("2. ✅ Modale trouvée dans le DOM."); document.body.appendChild(modal); if (typeof openHolidayModal === "function") { openHolidayModal("edit"); console.log("3. 🔄 openHolidayModal (ancienne fonction) exécutée."); } // On force l'affichage de manière agressive modal.classList.add("open"); modal.style.setProperty("display", "flex", "important"); modal.style.setProperty("z-index", "999999", "important"); document.body.classList.add("no-scroll"); console.log("4. 👁️ Affichage forcé appliqué."); // Remplissage des champs (avec un try/catch pour capter la moindre erreur silencieuse) try { document.getElementById("inp_id").value = h.id; document.getElementById("inp_title").value = h.title; document.getElementById("inp_status").value = h.status; document.getElementById("inp_period").value = h.period_hint || ""; document.getElementById("inp_start").value = h.start_date || ""; document.getElementById("inp_end").value = h.end_date || ""; document.getElementById("inp_food").value = h.budget_food > 0 ? h.budget_food : ""; document.getElementById("inp_extra").value = h.budget_extra > 0 ? h.budget_extra : ""; document.getElementById("inp_notes").value = h.notes || ""; console.log("5. ✍️ Champs textes remplis avec succès."); } catch (err) { console.error("❌ Erreur lors du remplissage des champs textes :", err); } // Remplissage des listes d'items généraux try { document.getElementById("list_transport").innerHTML = ""; document.getElementById("list_accommodation").innerHTML = ""; document.getElementById("list_activity").innerHTML = ""; if (data.items && data.items.length > 0) { data.items.forEach((item) => { // On évite d'afficher le point technique invisible dans la liste des dépenses if ( typeof addItem === "function" && item.name !== "PF_TECHNICAL_POINT" ) { addItem(item.category, item.name, item.amount, item.is_paid); } }); } console.log("6. 📋 Listes dynamiques remplies. Fin de la fonction ! 🎉"); } catch (err) { console.error("❌ Erreur lors du remplissage des listes dynamiques :", err); } } // --- 2. GESTION DES LISTES DYNAMIQUES DANS LA MODALE --- function addItem(category, name = "", amount = "", isPaid = 0) { const container = document.getElementById("list_" + category); const div = document.createElement("div"); // Style en ligne pour s'assurer que ça reste propre sans dépendre de classes externes complexes div.style.display = "flex"; div.style.gap = "8px"; div.style.alignItems = "center"; div.style.marginBottom = "10px"; // Astuce pour lier la checkbox visuelle à l'input caché (valeur 0 ou 1 pour MySQL) const checkedAttr = isPaid == 1 ? "checked" : ""; div.innerHTML = ` `; container.appendChild(div); } function deleteHoliday() { if ( !confirm( "Voulez-vous vraiment supprimer définitivement ce voyage ? Cette action est irréversible.", ) ) return; const form = document.getElementById("holidayForm"); // On injecte un input caché pour signaler au PHP que c'est une demande de suppression const input = document.createElement("input"); input.type = "hidden"; input.name = "action_delete"; input.value = "1"; form.appendChild(input); form.submit(); } // --- 3. GESTION DE LA CARTE (Leaflet - Optionnel selon si tu l'utilises ou non) --- let map = null; function toggleMap() { const modal = document.getElementById("hol-map-modal"); if (modal.style.display === "flex") { modal.style.display = "none"; } else { modal.style.display = "flex"; // Petit délai pour laisser le temps au DOM de s'afficher avant d'init Leaflet setTimeout(initMap, 100); } } function initMap() { if (map) { map.invalidateSize(); // Recalcule la taille si la fenêtre a changé return; } if (typeof L === "undefined") return; // Centré sur l'Europe par défaut 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); // On récupère les points définis en global dans le PHP if (typeof HOL_MAP_POINTS !== "undefined") { HOL_MAP_POINTS.forEach((pt) => { const color = pt.status === "planned" || pt.status === "booked" ? "green" : "blue"; // Cercle simple 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 // ============================================================================ let detailMap = null; document.addEventListener("DOMContentLoaded", () => { // Si on est sur la page détail et que la div "tripMap" existe, on initie la carte if (document.getElementById("tripMap")) { initDetailMap(); } }); function initDetailMap() { if (typeof L === "undefined" || typeof MAP_POINTS === "undefined") return; detailMap = L.map("tripMap"); // Carte standard : Affiche toujours la langue locale exacte (Français en France, Catalan en Catalogne) L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: '© OpenStreetMap contributors', }).addTo(detailMap); if (MAP_POINTS.length === 0) { detailMap.setView([46.6, 2.4], 5); // Centré sur la France par défaut si vide return; } const latlngs = []; const bounds = L.latLngBounds(); MAP_POINTS.forEach((pt, index) => { const pos = [pt.lat, pt.lng]; latlngs.push(pos); bounds.extend(pos); // Couleur de base (Bleu PachaFamily) const color = "#2563eb"; // On place un petit cercle pour chaque point const marker = L.circleMarker(pos, { color: color, radius: 8, fillOpacity: 1, fillColor: "white", weight: 3, }).addTo(detailMap); // Correction : Numérotation et Popup avec les bonnes variables marker.bindPopup(`
Étape ${index + 1}
${pt.location_name}
${parseFloat(pt.total_amount).toFixed(2)} €
`); // NOUVEAU : Focus et scroll sur la liste au clic sur la carte ! marker.on("click", function () { const card = document.getElementById("step-card-" + pt.sort_order); if (card) { // Fait défiler la liste doucement jusqu'à l'élément card.scrollIntoView({ behavior: "smooth", block: "center" }); // Effet visuel "Flash" pour repérer la carte card.style.transition = "box-shadow 0.3s, transform 0.3s"; card.style.boxShadow = "0 0 0 3px #3b82f6"; card.style.transform = "scale(1.02)"; // Retire l'effet après 1.5 seconde setTimeout(() => { card.style.boxShadow = ""; card.style.transform = ""; }, 1500); } }); }); // On dessine le vrai trajet par la route avec gestion des superpositions if (latlngs.length > 1) { const routePromises = []; // 1. On lance toutes les requêtes de calcul de route en même temps for (let i = 0; i < latlngs.length - 1; i++) { const startPt = MAP_POINTS[i]; const endPt = MAP_POINTS[i + 1]; const coordsString = `${startPt.lng},${startPt.lat};${endPt.lng},${endPt.lat}`; const promise = fetch( `https://router.project-osrm.org/route/v1/driving/${coordsString}?overview=full&geometries=geojson`, ) .then((response) => response.json()) .then((data) => ({ index: i, data: data, coords: [latlngs[i], latlngs[i + 1]], })) .catch((err) => ({ index: i, error: true, coords: [latlngs[i], latlngs[i + 1]], })); routePromises.push(promise); } // 2. On attend que TOUTES les routes soient calculées pour les dessiner dans le bon ordre Promise.all(routePromises).then((results) => { // On s'assure de dessiner l'Étape 1 d'abord, puis la 2, etc. results.sort((a, b) => a.index - b.index); results.forEach((res) => { const i = res.index; // Styles par défaut (Aller / Étape 1) let routeColor = "#3b82f6"; // Bleu let routeWeight = 6; // Épais let routeDash = null; // Ligne continue // Modification des styles selon le segment if (i === latlngs.length - 2) { // DERNIER SEGMENT (Retour) routeColor = "#f97316"; // Orange vif (Tranche parfaitement avec le bleu) routeWeight = 3; // Plus fin pour se superposer sans cacher routeDash = "10, 10"; // En pointillés pour laisser voir le bleu en dessous } else if (i > 0) { // SEGMENTS INTERMÉDIAIRES routeColor = "#8b5cf6"; // Violet routeWeight = 5; } // Dessin du trait if (res.data && res.data.code === "Ok" && res.data.routes.length > 0) { const routeCoords = res.data.routes[0].geometry.coordinates.map( (c) => [c[1], c[0]], ); L.polyline(routeCoords, { color: routeColor, weight: routeWeight, dashArray: routeDash, opacity: 0.9, lineCap: "round", lineJoin: "round", }).addTo(detailMap); } else { // Plan B si pas de route carrossable (ex: avion/bateau) drawFallbackLine(res.coords, routeColor, routeWeight); } }); }); } // Plan B : La ligne droite function drawFallbackLine(coords, color, weight) { L.polyline(coords, { color: color, weight: weight || 3, dashArray: "8, 8", // Toujours en pointillés pour le plan B opacity: 0.7, }).addTo(detailMap); } // On zoome automatiquement pour voir tous les points detailMap.fitBounds(bounds, { padding: [50, 50] }); } // Fonction appelée quand on clique sur "👁️ Voir sur la carte" dans la liste function panMapTo(lat, lng) { if (detailMap) { detailMap.setView([lat, lng], 14, { animate: true }); } } // --- 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"); const formBlock = document.getElementById("formCheckpoint"); const container = document.getElementById("cpExpensesContainer"); const btnDel = document.getElementById("btnDeleteCp"); container.innerHTML = ""; if (mode === "add") { document.getElementById("cpModalTitle").innerText = "📍 Placer une nouvelle étape"; searchBlock.style.display = "block"; formBlock.style.display = "none"; btnDel.style.display = "none"; document.getElementById("cp_old_sort_order").value = ""; document.getElementById("cp_name").value = ""; addCpExpenseLine(); } else if (mode === "edit" && data) { document.getElementById("cpModalTitle").innerText = "✏️ Modifier l'étape"; searchBlock.style.display = "none"; formBlock.style.display = "block"; btnDel.style.display = "block"; document.getElementById("cp_lat").value = data.lat; 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 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++; } }); if (visibleCount === 0) addCpExpenseLine(); } else { addCpExpenseLine(); } } document.getElementById("checkpointModal").style.display = "flex"; document.body.classList.add("no-scroll"); } function searchPlace() { const q = document.getElementById("searchPlaceInput").value.trim(); if (q.length < 3) return; const resultsDiv = document.getElementById("searchResults"); resultsDiv.innerHTML = 'Recherche en cours... ⏳'; fetch( "/modules/holidays/includes/api/geocode.php?limit=5&q=" + encodeURIComponent(q), ) .then((res) => res.json()) .then((data) => { resultsDiv.innerHTML = ""; if (data.error || !data.results || data.results.length === 0) { resultsDiv.innerHTML = 'Aucun résultat trouvé.'; return; } data.results.forEach((place) => { const btn = document.createElement("button"); btn.type = "button"; btn.className = "pf-btn btn-secondary"; btn.style.textAlign = "left"; btn.style.padding = "8px"; btn.style.height = "auto"; btn.innerText = "📍 " + place.display_name; btn.onclick = () => selectPlace(place.lat, place.lng, place.display_name); resultsDiv.appendChild(btn); }); }) .catch((err) => { resultsDiv.innerHTML = 'Erreur réseau.'; }); } function selectPlace(lat, lng, fullName) { document.getElementById("cp_lat").value = lat; document.getElementById("cp_lng").value = lng; // On nettoie le nom pour que ce soit joli (ex: "Paris, France" -> "Paris") document.getElementById("cp_name").value = fullName.split(",")[0].trim(); document.getElementById("cpSearchBlock").style.display = "none"; document.getElementById("formCheckpoint").style.display = "block"; } function addCpExpenseLine( category = "accommodation", name = "", amount = "", isPaid = 0, notes = "", itemId = "", itemDate = "", itemTime = "", itemDur = 1, ) { const container = document.getElementById("cpExpensesContainer"); const div = document.createElement("div"); div.className = "hol-form-row"; const isChecked = isPaid == 1 ? "checked" : ""; div.innerHTML = `
`; container.appendChild(div); } function deleteCheckpoint() { if (!confirm("Supprimer cette étape et toutes les dépenses associées ?")) return; const form = document.getElementById("formCheckpoint"); const input = document.createElement("input"); input.type = "hidden"; input.name = "action_delete"; input.value = "1"; form.appendChild(input); form.submit(); } // ============================================================================ // 5. GLISSER-DÉPOSER POUR RÉORDONNER LES ÉTAPES (ROADTRIP) // ============================================================================ document.addEventListener("DOMContentLoaded", () => { const checkpoints = document.querySelectorAll(".hol-checkpoint-draggable"); const container = checkpoints[0]?.parentElement; if (!container) return; let draggedItem = null; checkpoints.forEach((item) => { item.addEventListener("dragstart", function (e) { draggedItem = this; setTimeout(() => (this.style.opacity = "0.4"), 0); }); item.addEventListener("dragend", function () { setTimeout(() => { this.style.opacity = "1"; draggedItem = null; saveCheckpointOrder(); // On sauvegarde quand on lâche ! }, 0); }); item.addEventListener("dragover", function (e) { e.preventDefault(); const afterElement = getDragAfterElement(container, e.clientY); if (afterElement == null) { container.appendChild(draggedItem); } else { container.insertBefore(draggedItem, afterElement); } }); }); function getDragAfterElement(container, y) { const draggableElements = [ ...container.querySelectorAll( '.hol-checkpoint-draggable:not([style*="opacity: 0.4"])', ), ]; return draggableElements.reduce( (closest, child) => { const box = child.getBoundingClientRect(); const offset = y - box.top - box.height / 2; if (offset < 0 && offset > closest.offset) { return { offset: offset, element: child }; } else { return closest; } }, { offset: Number.NEGATIVE_INFINITY }, ).element; } function saveCheckpointOrder() { // On récupère le nom des lieux dans le nouvel ordre de haut en bas const locations = [ ...document.querySelectorAll(".hol-checkpoint-draggable"), ].map((el) => el.getAttribute("data-location")); const holidayId = document.querySelector('input[name="holiday_id"]').value; // Input caché dans la modale const formData = new FormData(); formData.append("holiday_id", holidayId); formData.append("locations", JSON.stringify(locations)); fetch("/modules/holidays/includes/api/reorder_checkpoints.php", { method: "POST", body: formData, }).then(() => { // Recharge la page pour que la carte redessine le trait bleu dans le bon ordre ! window.location.reload(); }); } }); // ============================================================================ // 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"); } });