// modules/holidays/holidays.js document.addEventListener("DOMContentLoaded", () => { // --- 1. FONCTIONS UTILITAIRES --- /** * Configure les comportements de fermeture d'une modale (Backdrop, Cancel, Escape) */ function setupModal(modalId, openAction = null) { const modal = document.getElementById(modalId); if (!modal) return null; const backdrop = modal.querySelector(".hol-backdrop"); const cancelBtn = modal.querySelector(".hol-cancel"); const close = () => modal.classList.remove("open"); const open = () => { modal.classList.add("open"); if (openAction) openAction(); }; if (backdrop) backdrop.addEventListener("click", close); if (cancelBtn) cancelBtn.addEventListener("click", close); // Fermeture avec ECHAP (uniquement si cette modale est ouverte) document.addEventListener("keydown", (e) => { if (e.key === "Escape" && modal.classList.contains("open")) { close(); } }); return { modal, open, close }; } /** * Échappe les caractères HTML pour éviter les failles XSS simples */ function esc(s) { if (s === null || s === undefined) return ""; return String(s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // --- 2. GESTION MODALE "AJOUTER" --- const addModalCtrl = setupModal("hol-add-modal"); const addBtn = document.getElementById("hol-add-open"); if (addBtn && addModalCtrl) { addBtn.addEventListener("click", addModalCtrl.open); } // --- 3. GESTION MODALE "ÉDITER" --- const editModalCtrl = setupModal("hol-edit-modal"); // Fonction pour charger et ouvrir l'édition via AJAX async function openEditForId(id) { if (!editModalCtrl) return; try { const res = await fetch( `/modules/holidays/view.php?id=${encodeURIComponent(id)}`, { headers: { Accept: "application/json" } }, ); if (!res.ok) throw new Error("Erreur HTTP " + res.status); const it = await res.json(); const modal = editModalCtrl.modal; // Helper pour remplir les champs const setVal = (sel, val) => { const el = modal.querySelector(sel); if (el) el.value = val ?? ""; }; // Remplissage du formulaire setVal("#edit-id", it.id); setVal("#edit-title", it.title); setVal("#edit-country", it.country); setVal("#edit-region", it.region); setVal("#edit-city", it.city); setVal("#edit-lat", it.lat); setVal("#edit-lng", it.lng); setVal("#edit-start", it.desired_start_date); setVal("#edit-end", it.desired_end_date); setVal("#edit-season", it.season_hint); setVal("#edit-days", it.ideal_days); setVal("#edit-status", it.status || "draft"); setVal("#edit-notes", it.notes); editModalCtrl.open(); } catch (err) { console.error(err); alert("Impossible de charger les données de l'idée."); } } // Écouteurs sur les boutons "Éditer" (Liste + Détail) document.body.addEventListener("click", (e) => { // Utilisation de la délégation d'événement pour gérer tous les boutons (même dynamiques) const btn = e.target.closest(".btn-edit"); if (btn && btn.hasAttribute("data-edit-id")) { const id = btn.getAttribute("data-edit-id"); openEditForId(id); } // Cas spécifique du bouton dans le header de la vue détail else if (e.target.id === "hol-edit-open") { const id = e.target.getAttribute("data-edit-id"); openEditForId(id); } }); // --- 4. GESTION SUPPRESSION --- document.body.addEventListener("click", (e) => { const btn = e.target.closest(".btn-delete"); if (btn && btn.hasAttribute("data-del-id")) { const id = btn.getAttribute("data-del-id"); if ( confirm( "Voulez-vous vraiment supprimer cette idée ?\nCette action est irréversible.", ) ) { // Création d'un formulaire temporaire pour le POST const form = document.createElement("form"); form.method = "post"; form.action = "/modules/holidays/save.php"; form.innerHTML = ` `; document.body.appendChild(form); form.submit(); } } }); // --- 5. GÉOCODAGE (Nominatim) --- document.querySelectorAll(".hol-geocode-btn").forEach((btn) => { btn.addEventListener("click", async () => { const scope = btn.getAttribute("data-scope"); // 'add' ou 'edit' let latInput, lngInput, qParts = []; // Récupération des inputs selon le scope if (scope === "edit") { const getVal = (id) => (document.getElementById(id)?.value || "").trim(); qParts = [ getVal("edit-city"), getVal("edit-region"), getVal("edit-country"), ]; latInput = document.getElementById("edit-lat"); lngInput = document.getElementById("edit-lng"); } else { // Scope 'add' : on cherche dans le formulaire parent const form = btn.closest("form"); const getVal = (name) => (form?.querySelector(`input[name="${name}"]`)?.value || "").trim(); qParts = [getVal("city"), getVal("region"), getVal("country")]; latInput = form?.querySelector('input[name="lat"]'); lngInput = form?.querySelector('input[name="lng"]'); } const q = qParts.filter(Boolean).join(", "); if (!q) { alert( "Veuillez renseigner au moins une Ville ou un Pays pour géocoder.", ); return; } // UI : chargement removeNearbyPicker(btn); btn.disabled = true; const originalText = btn.textContent; btn.textContent = "⏳..."; try { const res = await fetch( `/modules/holidays/geocode.php?q=${encodeURIComponent(q)}&limit=5`, { headers: { Accept: "application/json" }, }, ); const data = await res.json(); if (!res.ok) throw new Error(data?.error || "Erreur inconnue"); // Cas 1: Résultat direct (lat/lng uniques) if (data.lat && data.lng) { if (latInput) latInput.value = data.lat; if (lngInput) lngInput.value = data.lng; } // Cas 2: Liste de choix else if (Array.isArray(data.results) && data.results.length > 0) { renderGeocodePicker(btn, data.results, (choice) => { if (latInput) latInput.value = choice.lat; if (lngInput) lngInput.value = choice.lng; removeNearbyPicker(btn); }); } else { alert("Aucun résultat trouvé pour : " + q); } } catch (err) { console.error(err); alert("Erreur lors du géocodage."); } finally { btn.disabled = false; btn.textContent = originalText; } }); }); function renderGeocodePicker(anchorBtn, results, onPick) { removeNearbyPicker(anchorBtn); const wrapper = document.createElement("div"); wrapper.className = "hol-geocode-picker"; // Header const header = document.createElement("div"); header.className = "hol-geocode-picker__header"; header.innerHTML = `Choix multiples`; header .querySelector(".hol-close") .addEventListener("click", () => removeNearbyPicker(anchorBtn)); wrapper.appendChild(header); // Liste const list = document.createElement("ul"); list.className = "hol-geocode-picker__list"; results.forEach((r) => { const li = document.createElement("li"); li.className = "hol-geocode-picker__item"; li.innerHTML = `
${esc(r.display_name)} (${r.lat}, ${r.lng})
`; li.querySelector("button").addEventListener("click", () => onPick(r)); list.appendChild(li); }); wrapper.appendChild(list); // Insertion après le conteneur du bouton const container = anchorBtn.closest(".hol-inline") || anchorBtn.parentElement; container.insertAdjacentElement("afterend", wrapper); } function removeNearbyPicker(anchorBtn) { const container = anchorBtn.closest(".hol-inline") || anchorBtn.parentElement; const next = container?.nextElementSibling; if (next && next.classList.contains("hol-geocode-picker")) { next.remove(); } } // --- 6. CARTE LEAFLET --- let mapInitialized = false; let mapInstance; // Fonction d'initialisation de la carte (appelée à l'ouverture de la modale) function initMap() { if (mapInitialized) { // Si déjà init, on force juste le redimensionnement pour éviter les bugs d'affichage setTimeout(() => mapInstance.invalidateSize(), 200); return; } if (typeof L === "undefined") { console.error("Leaflet n'est pas chargé."); return; } // Récupération sécurisée des données injectées par PHP const MAP_DATA = Array.isArray(window.HOL_MAP_DATA) ? window.HOL_MAP_DATA : []; mapInstance = L.map("hol-map", { scrollWheelZoom: true }); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: "© OpenStreetMap", }).addTo(mapInstance); const markers = []; MAP_DATA.forEach((it) => { const lat = parseFloat(it.lat); const lng = parseFloat(it.lng); if (!Number.isFinite(lat) || !Number.isFinite(lng)) return; // Code couleur selon statut const colors = { planned: "#16a34a", favorite: "#f59e0b", shortlist: "#3b82f6", default: "#6b7280", }; const color = colors[it.status] || colors.default; const m = L.circleMarker([lat, lng], { radius: 7, color: "#ffffff", weight: 1, fillColor: color, fillOpacity: 0.9, }).addTo(mapInstance); // Construction du popup const loc = [it.city, it.region, it.country].filter(Boolean).join(", "); const dates = it.desired_start_date ? `${it.desired_start_date}${it.desired_end_date ? " → " + it.desired_end_date : ""}` : null; m.bindPopup(`
${esc(it.title)}
${esc(loc)}
${dates ? `
📅 ${esc(dates)}
` : ""}
${esc(it.status)} Voir
`); markers.push(m); }); // Centrage de la carte if (markers.length > 0) { const group = L.featureGroup(markers); mapInstance.fitBounds(group.getBounds(), { padding: [50, 50] }); } else { mapInstance.setView([46.603354, 1.888334], 5); // France par défaut si vide } mapInitialized = true; // Hack indispensable pour que Leaflet calcule la bonne taille dans une modale setTimeout(() => mapInstance.invalidateSize(), 200); } // Connexion de la carte à la modale const mapBtn = document.getElementById("hol-map-open"); if (mapBtn) { const mapModalCtrl = setupModal("hol-map-modal", initMap); // On passe initMap en callback d'ouverture mapBtn.addEventListener("click", mapModalCtrl.open); } });