`);
// 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 = `