// ============================================================================
// FONCTION DE TRADUCTION JS & LANGUE COURANTE
// ============================================================================
function tr(key) {
return window.I18N && window.I18N[key] ? window.I18N[key] : key;
}
// On détecte la langue de la page (définie dans la balise du header)
const currentLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
// ============================================================================
// FERMETURE UNIVERSELLE DES MODALES
// ============================================================================
window.addEventListener("click", function (event) {
if (event.target.classList.contains("pf-modal")) {
event.target.style.display = "none";
document.body.classList.remove("no-scroll");
}
});
// --- 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");
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 = tr("modal_plan_trip");
btnDelete.style.display = "none";
} else {
document.getElementById("modalTitle").innerText = tr("modal_quick_edit");
btnDelete.style.display = "block";
}
modal.style.display = "flex";
setTimeout(() => document.getElementById("inp_title").focus(), 100);
}
function closeHolidayModal() {
document.getElementById("holidayModal").style.display = "none";
document.body.classList.remove("no-scroll");
}
function editHoliday(data) {
const h = data.main;
const modal = document.getElementById("holidayModal");
if (!modal) {
alert(tr("err_modal_missing"));
return;
}
document.body.appendChild(modal);
if (typeof openHolidayModal === "function") {
openHolidayModal("edit");
}
modal.classList.add("open");
modal.style.setProperty("display", "flex", "important");
modal.style.setProperty("z-index", "999999", "important");
document.body.classList.add("no-scroll");
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 || "";
} catch (err) {
console.error("Erreur champs textes :", err);
}
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) => {
if (
typeof addItem === "function" &&
item.name !== "PF_TECHNICAL_POINT"
) {
addItem(item.category, item.name, item.amount, item.is_paid);
}
});
}
} catch (err) {
console.error("Erreur listes :", 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");
div.style.display = "flex";
div.style.gap = "8px";
div.style.alignItems = "center";
div.style.marginBottom = "10px";
const checkedAttr = isPaid == 1 ? "checked" : "";
div.innerHTML = `
${tr("paid")}
×
`;
container.appendChild(div);
}
function deleteHoliday() {
if (!confirm(tr("confirm_delete_trip"))) return;
const form = document.getElementById("holidayForm");
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 ---
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";
setTimeout(initMap, 100);
}
}
function initMap() {
if (map) {
map.invalidateSize();
return;
}
if (typeof L === "undefined") return;
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);
if (typeof HOL_MAP_POINTS !== "undefined") {
HOL_MAP_POINTS.forEach((pt) => {
const color =
pt.status === "planned" || pt.status === "booked" ? "green" : "blue";
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", () => {
if (document.getElementById("tripMap")) {
initDetailMap();
}
});
function initDetailMap() {
if (typeof L === "undefined" || typeof MAP_POINTS === "undefined") return;
detailMap = L.map("tripMap");
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);
return;
}
const latlngs = [];
const bounds = L.latLngBounds();
MAP_POINTS.forEach((pt, index) => {
const pos = [pt.lat, pt.lng];
latlngs.push(pos);
bounds.extend(pos);
const color = "#2563eb";
const marker = L.circleMarker(pos, {
color: color,
radius: 8,
fillOpacity: 1,
fillColor: "white",
weight: 3,
}).addTo(detailMap);
marker.bindPopup(`
${tr("step_number")} ${index + 1}
${pt.location_name}
${parseFloat(pt.total_amount).toFixed(2)} €
`);
marker.on("click", function () {
const card = document.getElementById("step-card-" + pt.sort_order);
if (card) {
card.scrollIntoView({ behavior: "smooth", block: "center" });
card.style.transition = "box-shadow 0.3s, transform 0.3s";
card.style.boxShadow = "0 0 0 3px #3b82f6";
card.style.transform = "scale(1.02)";
setTimeout(() => {
card.style.boxShadow = "";
card.style.transform = "";
}, 1500);
}
});
});
if (latlngs.length > 1) {
const routePromises = [];
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);
}
Promise.all(routePromises).then((results) => {
results.sort((a, b) => a.index - b.index);
let returnStartIndex = latlngs.length - 2;
const customReturnStep = MAP_POINTS.findIndex((p) => p.is_return == 1);
if (customReturnStep > 0) {
returnStartIndex = customReturnStep;
}
results.forEach((res) => {
const i = res.index;
let routeColor = "#3b82f6";
let routeWeight = 6;
let routeDash = null;
if (i >= returnStartIndex) {
routeColor = "#f97316";
routeWeight = 3;
routeDash = "10, 10";
}
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 {
drawFallbackLine(res.coords, routeColor, routeWeight);
}
});
});
}
function drawFallbackLine(coords, color, weight) {
L.polyline(coords, {
color: color,
weight: weight || 3,
dashArray: "8, 8",
opacity: 0.7,
}).addTo(detailMap);
}
detailMap.fitBounds(bounds, { padding: [50, 50] });
}
function panMapTo(lat, lng) {
if (detailMap) {
detailMap.setView([lat, lng], 14, { animate: true });
}
}
// --- LOGIQUE DE LA MODALE CHECKPOINT ---
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 = "";
// Sécurisation : On vide les dates ici pour ne pas planter sur la page d'accueil !
if (document.getElementById("cp_start_date"))
document.getElementById("cp_start_date").value = "";
if (document.getElementById("cp_end_date"))
document.getElementById("cp_end_date").value = "";
if (mode === "add") {
document.getElementById("cpModalTitle").innerText = tr("place_new_step");
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();
document.getElementById("cp_is_return").checked = false;
} else if (mode === "edit" && data) {
document.getElementById("cpModalTitle").innerText = tr("edit_step");
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 || "";
document.getElementById("cp_is_return").checked = data.is_return == 1;
if (data.items && data.items.length > 0) {
let visibleCount = 0;
data.items.forEach((it) => {
if (it.name !== "PF_TECHNICAL_POINT") {
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 = `${tr("search_in_progress")} `;
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 = `${tr("no_result_found")} `;
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 = `${tr("network_error")} `;
});
}
function selectPlace(lat, lng, fullName) {
document.getElementById("cp_lat").value = lat;
document.getElementById("cp_lng").value = lng;
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 = `
🏨
🚗
🎫
${tr("paid")}
×
`;
container.appendChild(div);
}
function deleteCheckpoint() {
if (!confirm(tr("confirm_delete_step"))) 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
// ============================================================================
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();
}, 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() {
const locations = [
...document.querySelectorAll(".hol-checkpoint-draggable"),
].map((el) => el.getAttribute("data-location"));
const holidayId = document.querySelector('input[name="holiday_id"]').value;
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(() => window.location.reload());
}
});
// ============================================================================
// MOTEUR DRAG & DROP DU PLANNING
// ============================================================================
let selectedItemIdForMove = null;
function openPlanningModal(step) {
document.getElementById("planningModalTitle").innerText =
tr("planning_of") + step.location_name;
const container = document.getElementById("planningContainer");
selectedItemIdForMove = null;
let validItems = step.items.filter((it) => it.name !== "PF_TECHNICAL_POINT");
if (!step.step_start_date || !step.step_end_date) {
container.innerHTML = `${tr("missing_dates_title")} ${tr("missing_dates_msg")}
`;
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);
}
let html = `
`;
datesToDisplay.forEach((dateStr) => {
const dObj = new Date(dateStr);
// NOUVEAU : On utilise la langue actuelle du navigateur pour traduire les jours !
const dayName = dObj.toLocaleDateString(currentLang, { weekday: "short" });
const dayNum = dObj.toLocaleDateString(currentLang, {
day: "numeric",
month: "short",
});
html += `
`;
for (let h = 8; h <= 22; h++) {
let hourStr = h.toString().padStart(2, "0") + ":00";
html += `
${hourStr}
`;
}
html += `
`;
});
html += `
`;
container.innerHTML = html;
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}
`
: "";
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);
});
document.getElementById("planningModal").style.display = "flex";
document.body.classList.add("no-scroll");
}
function changeDuration(e, itemId, delta) {
e.stopPropagation();
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;
itemEl.style.setProperty("--duration", newDur);
document.getElementById(`dur-text-${itemId}`).innerText = newDur + "h";
updateItemMemory(itemId, { duration: newDur });
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,
});
}
function handleItemTap(e, itemId) {
e.stopPropagation();
document
.querySelectorAll(".hol-drag-item")
.forEach((el) => el.classList.remove("selected-for-move"));
if (selectedItemIdForMove === itemId) {
selectedItemIdForMove = null;
} 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"));
}
}
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);
}
function handleDropLogic(itemId, dateStr, timeStr, dropZone) {
const draggedEl = document.getElementById("drag-item-" + itemId);
if (dropZone && draggedEl) {
dropZone.appendChild(draggedEl);
updateItemMemory(itemId, { item_date: dateStr, item_time: 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,
});
}
}
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:", err));
}