// ============================================================================
// FONCTION DE TRADUCTION JS & LANGUE COURANTE
// ============================================================================
function tr(key) {
return window.I18N && window.I18N[key] ? window.I18N[key] : key;
}
var currentLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
var selectedItemIdForMove = null;
// ============================================================================
// UTILITAIRES MÉTÉO
// ============================================================================
function getWeatherInfo(code) {
if (code === 0) return { icon: "☀️", label: tr("weather_sunny") };
if ([1, 2].includes(code)) return { icon: "🌤️", label: tr("weather_sunny") };
if ([3, 45, 48].includes(code))
return { icon: "☁️", label: tr("weather_cloudy") };
if ([51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82].includes(code))
return { icon: "🌧️", label: tr("weather_rainy") };
if ([71, 73, 75, 77, 85, 86].includes(code))
return { icon: "❄️", label: tr("weather_snowy") };
if ([95, 96, 99].includes(code))
return { icon: "⛈️", label: tr("weather_rainy") };
return { icon: "🌡️", label: tr("weather_forecast") };
}
async function loadWeatherForStep(pt) {
if (!pt.step_start_date || !pt.lat || !pt.lng) return;
const container = document.querySelector(
`#step-card-${pt.sort_order} .hol-weather-info`,
);
if (!container) return;
try {
const resp = await fetch(
`/modules/holidays/includes/api/get_weather.php?lat=${pt.lat}&lng=${pt.lng}&date=${pt.step_start_date}`,
);
const res = await resp.json();
if (res.success) {
const info = getWeatherInfo(res.data.code);
const approxSymbol = res.data.is_historical ? "~" : "";
const badgeTitle = res.data.is_historical
? `${info.label} (${tr("weather_historical")})`
: info.label;
const opacityStyle = res.data.is_historical
? "opacity: 0.85; font-style: italic;"
: "";
container.innerHTML = `
${info.icon}
${approxSymbol}${Math.round(res.data.temp_min)}° / ${Math.round(res.data.temp_max)}°C
`;
}
} catch (e) {
console.error("Weather error", e);
}
}
// ============================================================================
// 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 (BASES VOYAGE)
// ============================================================================
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 = "";
if (mode === "add") {
document.getElementById("modalTitle").innerText = tr("hdl_modal_title");
btnDelete.style.display = "none";
} else {
document.getElementById("modalTitle").innerText = tr(
"hdl_quick_edit_title",
);
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) return;
openHolidayModal("edit");
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 || "";
const vehicleInput = document.getElementById("inp_vehicle_id");
if (vehicleInput) {
vehicleInput.value = h.vehicle_id || "";
}
}
function deleteHoliday() {
if (!confirm(tr("hdl_js_confirm_del_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();
}
// ============================================================================
// 4. GESTION DE LA CARTE DÉTAILLÉE (ROADTRIP) ET TRACÉS OSRM
// ============================================================================
var detailMap = null;
document.addEventListener("DOMContentLoaded", () => {
if (document.getElementById("tripMap")) {
initDetailMap();
}
});
function initDetailMap() {
if (typeof L === "undefined" || typeof MAP_POINTS === "undefined") return;
// 1. Nettoyage de l'ancienne carte
if (detailMap !== null) {
detailMap.remove();
detailMap = null;
}
const mapContainer = document.getElementById("tripMap");
if (!mapContainer) return;
mapContainer.style.touchAction = "none";
mapContainer.ondragstart = function (e) {
e.preventDefault();
};
detailMap = L.map("tripMap", { tap: false, dragging: true });
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();
// 2. Placement des marqueurs d'étapes
MAP_POINTS.forEach((pt, index) => {
const pos = [pt.lat, pt.lng];
latlngs.push(pos);
bounds.extend(pos);
const marker = L.circleMarker(pos, {
color: "#2563eb",
radius: window.innerWidth < 768 ? 6 : 8,
fillOpacity: 1,
fillColor: "white",
weight: 3,
}).addTo(detailMap);
const stepLabel = window.I18N
? window.I18N["hdl_js_step_label"]
: tr("hdl_js_step_label");
marker.bindPopup(`
${stepLabel} ${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);
}
});
});
const mapPadding = window.innerWidth < 768 ? [20, 20] : [50, 50];
// 3. Tracés OSRM et calculs
if (latlngs.length === 1) {
detailMap.setView(latlngs[0], 12);
} else if (latlngs.length > 1) {
detailMap.fitBounds(bounds, { padding: mapPadding });
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 totalTripDistance = 0;
let totalTripDuration = 0; // en secondes
let totalFuelCost = 0; // Coût exact accumulé
let transitDetailsHtml = "";
let returnStartIndex = latlngs.length - 2;
if (
typeof window.GLOBAL_RETURN_STEP_ID !== "undefined" &&
window.GLOBAL_RETURN_STEP_ID !== null
) {
const customReturnStep = MAP_POINTS.findIndex(
(p) => p.sort_order == window.GLOBAL_RETURN_STEP_ID,
);
if (customReturnStep > 0) returnStartIndex = customReturnStep;
}
results.forEach((res) => {
const i = res.index;
let routeColor = i >= returnStartIndex ? "#f97316" : "#3b82f6";
let routeWeight =
window.innerWidth < 768
? i >= returnStartIndex
? 3
: 4
: i >= returnStartIndex
? 4
: 6;
let routeDash = i >= returnStartIndex ? "10, 10" : null;
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);
// CALCULS
const distanceKm = res.data.routes[0].distance / 1000;
const durationSec = res.data.routes[0].duration; // Durée en secondes
totalTripDistance += distanceKm;
totalTripDuration += durationSec;
const fuelL100 = window.VEHICLE_CONSUMPTION || 7;
const fuelPrice = window.FUEL_PRICE || 1.85;
const cost = (distanceKm / 100) * fuelL100 * fuelPrice;
totalFuelCost += cost;
// 1. ON DÉCLARE LES POINTS D'ABORD
const startPt = MAP_POINTS[res.index];
const endPt = MAP_POINTS[res.index + 1];
// 2. ON SAUVEGARDE EN MÉMOIRE ENSUITE (Correction du bug !)
window.TRANSIT_DATA = window.TRANSIT_DATA || {};
window.TRANSIT_DATA[endPt.sort_order] = {
sec: durationSec,
from: startPt.location_name,
cost: cost,
};
// 🔥 DÉTECTION DES PÉAGES MANUELS DE L'ÉTAPE
let stepTollCost = 0;
if (endPt.items && endPt.items.length > 0) {
endPt.items.forEach((it) => {
const normalizedName = (it.name || "")
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "");
if (
it.category === "transport" &&
normalizedName.includes("peage")
) {
stepTollCost += parseFloat(it.amount);
}
});
}
let tollHtml =
stepTollCost > 0
? `💳 ${stepTollCost.toFixed(2)} € `
: "";
// 🔥 CONSTRUCTION DU CONTENU DE LA MODALE
transitDetailsHtml += `
📍 ${startPt.location_name} ➔ ${endPt.location_name}
🚗 ${Math.round(distanceKm)} km • ⏱️ ${formatDuration(durationSec)}
${tollHtml}
⛽ ${cost.toFixed(2)} €
`;
// INJECTION DANS LA CARTE (Sous l'étape)
const targetCard = document.getElementById(
"step-card-" + endPt.sort_order,
);
if (targetCard) {
targetCard
.querySelectorAll(".transit-auto-info")
.forEach((el) => el.remove());
const rawLocationName = startPt.location_name;
const safeLocationName = rawLocationName.replace(/'/g, "\\'");
const expenseDesc = `Essence depuis ${rawLocationName}`;
const isAlreadyAdded = endPt.items.some(
(it) => it.name === expenseDesc,
);
const summaryHtml = `
🚗 ${Math.round(distanceKm)} km
|
⛽ ~${cost.toFixed(2)} €
${!isAlreadyAdded ? `+ Ajouter ` : `✓ Ajouté `}
`;
const cpHeader = targetCard.querySelector(".hol-cp-header");
if (cpHeader) cpHeader.insertAdjacentHTML("afterend", summaryHtml);
}
} else {
drawFallbackLine(res.coords, routeColor, routeWeight);
}
});
// 🔥 AFFICHAGE DES TOTAUX (KM + TEMPS) EN HAUT DE PAGE
const distEl = document.getElementById("global_total_distance");
const timeEl = document.getElementById("global_total_duration");
const distBlock = document.getElementById("block_total_distance");
const globalFuelCostEl = document.getElementById("global_fuel_cost");
if (distEl && timeEl && distBlock) {
distEl.innerText = Math.round(totalTripDistance);
timeEl.innerText = formatDuration(totalTripDuration);
distBlock.style.display = "block";
}
if (globalFuelCostEl) {
globalFuelCostEl.innerText = Math.round(totalFuelCost);
}
// 🔥 INJECTION DU HTML DANS LA MODALE
const modalContainer = document.getElementById("transitDetailsContainer");
if (modalContainer) {
modalContainer.innerHTML =
transitDetailsHtml ||
'Aucun trajet calculé.
';
}
});
// 5. Fix Leaflet Resize
setTimeout(() => {
if (detailMap) {
detailMap.invalidateSize();
}
}, 300);
function drawFallbackLine(coords, color, weight) {
L.polyline(coords, {
color: color,
weight: weight || 3,
dashArray: "8, 8",
opacity: 0.7,
}).addTo(detailMap);
}
}
}
function panMapTo(lat, lng) {
if (detailMap) {
detailMap.setView([lat, lng], 14, { animate: true });
if (window.innerWidth < 768) {
const mapDiv = document.getElementById("tripMap");
if (mapDiv) {
mapDiv.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
}
}
// ============================================================================
// 5. LOGIQUE DE LA MODALE CHECKPOINT (ÉTAPES)
// ============================================================================
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 (document.getElementById("cp_start_date"))
document.getElementById("cp_start_date").value = "";
if (document.getElementById("cp_end_date"))
document.getElementById("cp_end_date").value = "";
if (document.getElementById("searchPlaceInput"))
document.getElementById("searchPlaceInput").value = "";
if (document.getElementById("searchResults"))
document.getElementById("searchResults").innerHTML = "";
searchBlock.style.display = "block";
if (mode === "add") {
document.getElementById("cpModalTitle").innerText = tr("hdl_btn_add_step");
formBlock.style.display = "none";
btnDel.style.display = "none";
document.getElementById("cp_old_sort_order").value = "";
document.getElementById("cp_name").value = "";
if (document.getElementById("cp_step_type")) {
document.getElementById("cp_step_type").value = "stop";
toggleStepDates("stop");
}
if (document.getElementById("cp_set_as_return")) {
document.getElementById("cp_set_as_return").checked = false;
}
addCpExpenseLine();
} else if (mode === "edit" && data) {
document.getElementById("cpModalTitle").innerText = tr("hdl_js_edit_step");
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 || "";
// 🔥 PRE-REMPLISSAGE DU TYPE D'ÉTAPE ET UI DATES
if (document.getElementById("cp_step_type")) {
const type = data.step_type || "stop";
document.getElementById("cp_step_type").value = type;
toggleStepDates(type);
}
// 🔥 PRE-REMPLISSAGE DE LA CASE RETOUR BASEE SUR LA GLOBALE
if (document.getElementById("cp_set_as_return")) {
document.getElementById("cp_set_as_return").checked =
window.GLOBAL_RETURN_STEP_ID == data.sort_order;
}
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("hdl_js_search_loading")} `;
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("hdl_js_no_result")} `;
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("hdl_js_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("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("hdl_paid")}
🗑️
`;
container.appendChild(div);
}
function deleteCheckpoint() {
if (!confirm(tr("hdl_js_confirm_del_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();
}
// ============================================================================
// 6. REORDONNANCEMENT DES ÉTAPES (DRAG & DROP PC + MOBILE)
// ============================================================================
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());
}
function moveStepMobile(btn, direction) {
const item = btn.closest(".hol-checkpoint-draggable");
const container = item.parentElement;
if (
direction === -1 &&
item.previousElementSibling &&
item.previousElementSibling.classList.contains("hol-checkpoint-draggable")
) {
container.insertBefore(item, item.previousElementSibling);
saveCheckpointOrder();
} else if (
direction === 1 &&
item.nextElementSibling &&
item.nextElementSibling.classList.contains("hol-checkpoint-draggable")
) {
container.insertBefore(item, item.nextElementSibling.nextElementSibling);
saveCheckpointOrder();
}
}
document.addEventListener("DOMContentLoaded", () => {
const checkpoints = document.querySelectorAll(".hol-checkpoint-draggable");
const container = checkpoints[0]?.parentElement;
if (!container) return;
const isMobile = window.innerWidth <= 768;
let draggedItem = null;
checkpoints.forEach((item) => {
if (isMobile) {
item.removeAttribute("draggable");
return;
}
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;
}
});
// ============================================================================
// 7. MOTEUR DRAG & DROP DU PLANNING GLOBAL
// ============================================================================
window.PLANNING_ALL_UNPLACED = [];
window.CURRENT_PLANNING_FILTER_DATE = null;
window.PLANNING_ITEM_MAP = {};
window.CURRENT_DRAG_DURATION = 1; // Variable pour la surbrillance multi-cases
function closePlanningModal() {
document.getElementById("planningModal").style.display = "none";
document.body.classList.remove("no-scroll");
}
function openGlobalPlanningModal() {
const holidayDataJsonEl = document.getElementById("holidayDataJson");
if (!holidayDataJsonEl) return;
const holidayData = JSON.parse(holidayDataJsonEl.textContent).main;
if (!holidayData.start_date || !holidayData.end_date) {
alert(
"Veuillez d'abord définir les dates globales du voyage dans 'Modifier les bases' ⚙️",
);
return;
}
document.getElementById("planningModalTitle").innerText =
"📅 Planning Global : " + holidayData.title;
const container = document.getElementById("planningContainer");
selectedItemIdForMove = null;
let allPlaced = [];
window.PLANNING_ALL_UNPLACED = [];
window.PLANNING_ITEM_MAP = {};
// 1. Collecte de TOUS les éléments
window.MAP_POINTS.forEach((step) => {
let validItems = step.items.filter((it) => {
if (it.name === "PF_TECHNICAL_POINT") return false;
if (it.category === "transport" && it.expense_context !== "transit")
return false;
return true;
});
if (window.TRANSIT_DATA && window.TRANSIT_DATA[step.sort_order]) {
let hasTransit = validItems.some(
(it) => it.expense_context === "transit",
);
if (!hasTransit) {
const tData = window.TRANSIT_DATA[step.sort_order];
const h = Math.max(1, Math.round(tData.sec / 3600));
validItems.push({
id: "virtual-transit-" + step.sort_order,
sort_order: step.sort_order,
name: `Essence depuis ${tData.from}`,
category: "transport",
expense_context: "transit",
duration: h,
notes: `Trajet GPS (~${Math.round(tData.sec / 60)} min).`,
is_virtual: true,
amount: tData.cost,
});
}
}
validItems.forEach((it) => {
it.step_start_date = step.step_start_date;
it.step_end_date = step.step_end_date;
it.step_location = step.location_name;
it.sort_order = step.sort_order;
const htmlId = it.is_virtual ? it.id : `drag-item-${it.id}`;
window.PLANNING_ITEM_MAP[htmlId] = it;
if (it.item_date && it.item_time) {
allPlaced.push(it);
} else {
window.PLANNING_ALL_UNPLACED.push(it);
}
});
});
// 2. Génération des dates globales
let datesToDisplay = [];
let curr = new Date(holidayData.start_date);
let endD = new Date(holidayData.end_date);
while (curr <= endD) {
datesToDisplay.push(curr.toISOString().split("T")[0]);
curr.setDate(curr.getDate() + 1);
}
// 3. Construction de l'interface (Avec règles CSS injectées)
let html = `
📥 ${tr("hdl_to_place")}
🔄 Tous
`;
datesToDisplay.forEach((dateStr) => {
const dObj = new Date(dateStr);
const dayName = dObj.toLocaleDateString(currentLang, { weekday: "short" });
const dayNum = dObj.toLocaleDateString(currentLang, {
day: "numeric",
month: "short",
});
let unplacedForDay = window.PLANNING_ALL_UNPLACED.filter((it) =>
isDateInStep(dateStr, it.step_start_date, it.step_end_date),
);
let badgeHtml = `
${unplacedForDay.length} `;
html += `
`;
for (let h = 6; h <= 23; h++) {
let hourStr = h.toString().padStart(2, "0") + ":00";
html += `
${hourStr}
`;
}
html += `
`;
});
html += `
`;
container.innerHTML = html;
// 4. Placement des éléments
allPlaced.forEach((it) => {
if (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", buildDragItemHtml(it));
}
});
// 5. Météo et Focus auto
datesToDisplay.forEach((dateStr) => {
let activeStep = window.MAP_POINTS.find((step) =>
isDateInStep(dateStr, step.step_start_date, step.step_end_date),
);
if (activeStep && activeStep.lat && activeStep.lng) {
loadWeatherForPlanning(activeStep.lat, activeStep.lng, dateStr);
}
});
document.getElementById("planningModal").style.display = "flex";
document.body.classList.add("no-scroll");
let todayStr = new Date().toISOString().split("T")[0];
let defaultFocusDate =
todayStr >= holidayData.start_date && todayStr <= holidayData.end_date
? todayStr
: holidayData.start_date;
filterPoolByDate(defaultFocusDate);
setTimeout(() => {
const activeCol = document.getElementById("col-" + defaultFocusDate);
if (activeCol) {
document.getElementById("calendarZoneContainer").scrollTo({
left: activeCol.offsetLeft - 300,
behavior: "smooth",
});
}
}, 200);
// Moteur Scroll Natif (Drag To Scroll)
const slider = document.getElementById("calendarZoneContainer");
let isDown = false;
let startX, startY, scrollLeft, scrollTop;
slider.addEventListener("mousedown", (e) => {
if (e.target.closest(".hol-drag-item") || e.target.closest("button"))
return;
isDown = true;
slider.style.cursor = "grabbing";
startX = e.pageX - slider.offsetLeft;
startY = e.pageY - slider.offsetTop;
scrollLeft = slider.scrollLeft;
scrollTop = slider.scrollTop;
});
slider.addEventListener("mouseleave", () => {
isDown = false;
slider.style.cursor = "grab";
});
slider.addEventListener("mouseup", () => {
isDown = false;
slider.style.cursor = "grab";
});
slider.addEventListener("mousemove", (e) => {
if (!isDown) return;
e.preventDefault();
const x = e.pageX - slider.offsetLeft;
const y = e.pageY - slider.offsetTop;
const walkX = (x - startX) * 1.5;
const walkY = (y - startY) * 1.5;
slider.scrollLeft = scrollLeft - walkX;
slider.scrollTop = scrollTop - walkY;
});
}
function isDateInStep(targetDate, stepStart, stepEnd) {
if (!stepStart || !stepEnd) return false;
return targetDate >= stepStart && targetDate <= stepEnd;
}
function filterPoolByDate(dateStr) {
window.CURRENT_PLANNING_FILTER_DATE = dateStr;
const pool = document.getElementById("unmapped-pool");
pool.innerHTML = "";
let poolItemsHtml = "";
let count = 0;
// Itération sur la MAP, donc l'ordre natif de tri (sort_order/ID) est préservé !
Object.values(window.PLANNING_ITEM_MAP).forEach((it) => {
const htmlId = it.is_virtual ? it.id : `drag-item-${it.id}`;
const isPlaced = document.querySelector(`.hol-time-slot #${htmlId}`);
if (!isPlaced) {
if (
!dateStr ||
isDateInStep(dateStr, it.step_start_date, it.step_end_date)
) {
poolItemsHtml += buildDragItemHtml(it);
count++;
}
}
});
if (count === 0) {
pool.innerHTML = `🎉 Rien à placer${dateStr ? " pour cette journée" : ""}.
`;
} else {
pool.innerHTML = poolItemsHtml;
}
}
function recalcAllBadges() {
const pool = document.getElementById("unmapped-pool");
const unmappedIds = Array.from(pool.children).map((el) => el.id);
document.querySelectorAll(".hol-day-column").forEach((col) => {
const dateStr = col.id.replace("col-", "");
const badge = document.getElementById("badge-" + dateStr);
if (!badge) return;
let dayCount = 0;
unmappedIds.forEach((htmlId) => {
const it = window.PLANNING_ITEM_MAP[htmlId];
if (it && isDateInStep(dateStr, it.step_start_date, it.step_end_date))
dayCount++;
});
badge.innerText = dayCount;
badge.style.display = dayCount > 0 ? "inline-block" : "none";
});
}
function buildDragItemHtml(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 isVirtual = it.is_virtual === true;
const isTransit = it.expense_context === "transit";
const durControls = `-
${dur}h
+ `;
const bgStyle =
isVirtual || isTransit
? "background: repeating-linear-gradient(45deg, var(--bg-page), var(--bg-page) 10px, rgba(59, 130, 246, 0.05) 10px, rgba(59, 130, 246, 0.05) 20px); border: 2px dashed var(--primary);"
: "background: var(--bg-panel); border: 1px solid var(--border-strong);";
const visualName = isTransit ? `🛣️ Trajet & Essence` : `${icon} ${it.name}`;
const isMobile = window.innerWidth <= 768;
const dragAttr = isMobile ? "" : 'draggable="true"';
const htmlId = isVirtual ? it.id : `drag-item-${it.id}`;
const locHint =
!it.item_date && it.step_location
? `📍 ${it.step_location}
`
: "";
// Suppression du calcul de hauteur en ligne, c'est désormais géré via CSS et la variable "--duration"
return `
${locHint}
${visualName}
${durControls}
${noteHtml}
`;
}
function changeDuration(e, itemId, delta) {
e.stopPropagation();
const itemEl =
document.getElementById("drag-item-" + itemId) ||
document.getElementById(itemId);
if (!itemEl) return;
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";
if (itemEl.getAttribute("data-virtual") === "true") {
const it = window.PLANNING_ITEM_MAP[itemEl.id];
if (it) it.duration = newDur;
return;
}
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, htmlId) {
e.stopPropagation();
document
.querySelectorAll(".hol-drag-item")
.forEach((el) => el.classList.remove("selected-for-move"));
if (selectedItemIdForMove === htmlId) {
selectedItemIdForMove = null;
} else {
selectedItemIdForMove = htmlId;
const el = document.getElementById(htmlId);
if (el) el.classList.add("selected-for-move");
}
}
function handleZoneTap(e, dateStr, timeStr) {
if (selectedItemIdForMove) {
const itemEl = document.getElementById(selectedItemIdForMove);
if (itemEl) {
const targetZone = e.currentTarget;
if (targetZone.id === "unmapped-pool") {
handleDropLogic(selectedItemIdForMove, "", "");
} else {
targetZone.appendChild(itemEl);
handleDropLogic(selectedItemIdForMove, dateStr, timeStr);
}
}
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";
// Mémorisation de la durée pour le survol dynamique (Fix #2)
window.CURRENT_DRAG_DURATION =
parseInt(e.target.style.getPropertyValue("--duration")) || 1;
}
function dragEnd(e) {
window.CURRENT_DRAG_DURATION = 1;
document
.querySelectorAll(".hol-time-slot")
.forEach((s) => s.classList.remove("drag-over-duration"));
}
function allowDrop(e) {
e.preventDefault();
}
function dragEnter(e) {
e.preventDefault();
let slot = e.target.closest(".hol-time-slot");
if (slot) {
// Retirer toutes les anciennes surbrillances
document
.querySelectorAll(".hol-time-slot")
.forEach((s) => s.classList.remove("drag-over-duration"));
// Appliquer la surbrillance sur les N cases consécutives
let dur = window.CURRENT_DRAG_DURATION || 1;
let currentSlot = slot;
for (let i = 0; i < dur; i++) {
if (currentSlot) {
currentSlot.classList.add("drag-over-duration");
currentSlot = currentSlot.nextElementSibling;
}
}
}
}
function dragLeave(e) {
// La gestion précise des surbrillances se fait via le dragEnter et dragEnd pour éviter le scintillement (flickering).
}
function handleDropEvent(e, dateStr, timeStr) {
e.preventDefault();
document
.querySelectorAll(".hol-time-slot")
.forEach((s) => s.classList.remove("drag-over-duration"));
const idStr = e.dataTransfer.getData("text/plain");
const itemEl = document.getElementById(idStr);
const dropZone =
e.target.closest(".hol-time-slot") ||
document.getElementById("unmapped-pool");
if (itemEl && dropZone) {
if (dropZone.id === "unmapped-pool") {
// FIX #3 : Contourner l'appendChild (qui met tout en bas) et déléguer à la fonction pour re-trier la colonne !
handleDropLogic(idStr, "", "");
} else {
dropZone.appendChild(itemEl);
handleDropLogic(idStr, dateStr, timeStr);
}
}
}
function handleDropLogic(htmlId, dateStr, timeStr) {
const itemEl = document.getElementById(htmlId);
if (!itemEl) return;
const isVirtual = itemEl.getAttribute("data-virtual") === "true";
const holidayId = document.querySelector('input[name="holiday_id"]').value;
const sortOrder = itemEl.getAttribute("data-sort");
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
if (dateStr === "" || timeStr === "") {
if (!isVirtual) {
updateItemMemory(realId, { item_date: null, item_time: null });
const fd = new FormData();
fd.append("action", "update_item_datetime");
fd.append("item_id", realId);
fd.append("item_date", "");
fd.append("item_time", "");
fetch("/modules/holidays/includes/api/save_checkpoint.php", {
method: "POST",
body: fd,
});
}
// 🔥 FIX #3 : Re-filtrer reconstruit la colonne de gauche dans SON ORDRE D'ORIGINE !
filterPoolByDate(window.CURRENT_PLANNING_FILTER_DATE);
recalcAllBadges();
return;
}
// AFFECTATION CALENDRIER
if (isVirtual) {
const tData = window.TRANSIT_DATA[sortOrder];
const fd = new FormData();
fd.append("action", "add_single_item");
fd.append("holiday_id", holidayId);
fd.append("sort_order", sortOrder);
fd.append("category", "transport");
fd.append("context", "transit");
fd.append("expense_context", "transit");
fd.append("name", `Essence depuis ${tData.from}`);
fd.append("amount", tData.cost);
fd.append("duration", dur);
fd.append("item_date", dateStr);
fd.append("item_time", timeStr);
fd.append("ajax", "1");
itemEl.setAttribute("data-virtual", "false");
fetch("/modules/holidays/includes/api/save_checkpoint.php", {
method: "POST",
body: fd,
})
.then((res) => res.json())
.then(async (data) => {
if (data && data.id) {
itemEl.id = `drag-item-${data.id}`;
itemEl.setAttribute("data-id", data.id);
const fdDate = new FormData();
fdDate.append("action", "update_item_datetime");
fdDate.append("item_id", data.id);
fdDate.append("item_date", dateStr);
fdDate.append("item_time", timeStr);
await fetch("/modules/holidays/includes/api/save_checkpoint.php", {
method: "POST",
body: fdDate,
});
window.PLANNING_ITEM_MAP[itemEl.id] = {
id: data.id,
step_start_date: dateStr,
step_end_date: dateStr,
expense_context: "transit",
category: "transport",
is_virtual: false,
};
} else {
window.location.reload();
}
})
.catch(() => {
window.location.reload();
});
} else {
updateItemMemory(realId, { item_date: dateStr, item_time: timeStr });
const fd = new FormData();
fd.append("action", "update_item_datetime");
fd.append("item_id", realId);
fd.append("item_date", dateStr);
fd.append("item_time", timeStr);
fetch("/modules/holidays/includes/api/save_checkpoint.php", {
method: "POST",
body: fd,
});
}
recalcAllBadges();
}
function updateItemMemory(itemId, changes) {
if (typeof MAP_POINTS !== "undefined") {
MAP_POINTS.forEach((step) => {
let item = step.items.find((i) => i.id == itemId);
if (item) Object.assign(item, changes);
});
}
}
// ============================================================================
// MÉTÉO SPÉCIFIQUE AU HEADER DU PLANNING
// ============================================================================
async function loadWeatherForPlanning(lat, lng, dateStr) {
const container = document.getElementById(`plan-weather-${dateStr}`);
if (!container || !lat || !lng) return;
try {
const resp = await fetch(
`/modules/holidays/includes/api/get_weather.php?lat=${lat}&lng=${lng}&date=${dateStr}`,
);
const res = await resp.json();
if (res.success) {
const info = getWeatherInfo(res.data.code);
const approxSymbol = res.data.is_historical ? "~" : "";
container.innerHTML = `
${info.icon}
${approxSymbol}${Math.round(res.data.temp_min)}° / ${Math.round(res.data.temp_max)}°C
`;
}
} catch (e) {
console.error("Erreur météo planning", e);
}
}
// Gère l'affichage des dates dans la modale d'étape
function toggleStepDates(type) {
const grpEnd = document.getElementById("grp_end_date");
const lblStart = document.getElementById("lbl_start_date");
if (type === "origin") {
grpEnd.style.display = "none";
lblStart.innerText = "📅 Date de départ";
} else if (type === "destination") {
grpEnd.style.display = "none";
lblStart.innerText = "📅 Date d'arrivée";
} else {
grpEnd.style.display = "block";
lblStart.innerText = tr("hdl_label_arrival");
}
}
// Ajout magique d'une dépense d'essence SÉCURISÉE et INSTANTANÉE
function addQuickTransitExpense(
holidayId,
sortOrder,
amount,
description,
btnElement,
durationSec = 3600,
) {
if (
!confirm(
`Ajouter une dépense de carburant de ${amount}€ pour cette étape ?`,
)
)
return;
// 1. UI OPTIMISTE : On change visuellement le bouton tout de suite sans attendre le serveur
const parentContainer = btnElement.parentElement;
if (parentContainer) {
parentContainer.innerHTML = `✓ Ajouté `;
}
// 2. On met à jour discrètement le compteur global en haut de page (+ montant)
const totalTransitEl = document.querySelector(".hol-summary-value strong");
if (totalTransitEl) {
const currentTotal =
parseFloat(totalTransitEl.innerText.replace(" €", "").replace(" ", "")) ||
0;
totalTransitEl.innerText = Math.round(currentTotal + amount) + " €";
}
const fd = new FormData();
fd.append("action", "add_single_item");
fd.append("holiday_id", holidayId);
fd.append("sort_order", sortOrder);
fd.append("category", "transport");
fd.append("name", description);
fd.append("amount", amount);
fd.append("context", "transit");
// 🔥 On sécurise la durée en base de données
const h = Math.max(1, Math.round(durationSec / 3600));
fd.append("duration", h);
fetch("/modules/holidays/includes/api/save_checkpoint.php", {
method: "POST",
body: fd,
})
.then((res) => res.json())
.then((data) => {
if (!data.success) alert("Erreur : " + data.error);
});
}
// Permet de modifier le prix du carburant à la volée
function updateFuelPrice() {
const currentPrice = window.FUEL_PRICE || 1.85;
let newPrice = prompt(
"Définit le prix du carburant estimé (€/L) pour tes trajets :",
currentPrice,
);
if (newPrice !== null) {
newPrice = parseFloat(newPrice.replace(",", "."));
if (!isNaN(newPrice) && newPrice > 0) {
localStorage.setItem("holidays_fuel_price", newPrice);
window.location.reload();
} else {
alert("Prix invalide.");
}
}
}
// Formatte les secondes en "XXhYY" ou "YYmin"
function formatDuration(seconds) {
if (!seconds || isNaN(seconds)) return "0min";
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
if (h > 0) {
return `${h}h${m.toString().padStart(2, "0")}`;
}
return `${m}min`;
}
// Ouvre et ferme la modale des détails (L'œil)
function openTransitModal() {
document.getElementById("transitModal").style.display = "flex";
document.body.classList.add("no-scroll");
}
function closeTransitModal() {
document.getElementById("transitModal").style.display = "none";
document.body.classList.remove("no-scroll");
}
// ============================================================================
// CHOIX DE L'APPLICATION GPS (Google Maps, Waze, Apple Maps)
// ============================================================================
window.currentGpsTarget = { lat: 0, lng: 0 };
function openGpsModal(lat, lng) {
window.currentGpsTarget = { lat: lat, lng: lng };
document.getElementById("gpsModal").style.display = "flex";
document.body.classList.add("no-scroll");
}
function closeGpsModal() {
document.getElementById("gpsModal").style.display = "none";
document.body.classList.remove("no-scroll");
}
function launchGpsApp(app) {
const lat = window.currentGpsTarget.lat;
const lng = window.currentGpsTarget.lng;
let url = "";
if (app === "waze") {
// Force l'ouverture de Waze en mode navigation
url = `https://waze.com/ul?ll=${lat},${lng}&navigate=yes`;
} else if (app === "gmaps") {
// Force l'ouverture de Google Maps en mode itinéraire
url = `https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`;
} else if (app === "amaps") {
// Force l'ouverture d'Apple Maps
url = `http://maps.apple.com/?daddr=${lat},${lng}`;
}
if (url !== "") {
window.open(url, "_blank");
closeGpsModal();
}
}
// ============================================================================
// GESTION DU PORTE-DOCUMENTS (UPLOAD)
// ============================================================================
window.currentDocsStepId = null;
// 🔥 On attache le verrou à window pour éviter les erreurs de redéclaration
window.isUploadingDocs = window.isUploadingDocs || false;
function openDocsModal(sortOrder) {
window.currentDocsStepId = sortOrder;
document.getElementById("docsModal").style.display = "flex";
document.body.classList.add("no-scroll");
document.getElementById("uploadStatus").innerHTML = "";
const listContainer = document.getElementById("docsListContainer");
listContainer.innerHTML =
'⏳ Chargement des documents...
';
const holidayId = document.querySelector('input[name="holiday_id"]').value;
// 🔥 On va chercher les documents existants !
fetch(
`/modules/holidays/includes/api/get_attachments.php?holiday_id=${holidayId}&item_id=${sortOrder}`,
)
.then((response) => response.json())
.then((data) => {
listContainer.innerHTML = ""; // On vide le message de chargement
if (data.success && data.files.length > 0) {
data.files.forEach((f) => {
// On rend le nom du fichier cliquable pour ouvrir le document dans un nouvel onglet
const docHtml = `
`;
listContainer.insertAdjacentHTML("beforeend", docHtml);
});
} else {
listContainer.innerHTML =
'Aucun document pour cette étape.
';
}
})
.catch(() => {
listContainer.innerHTML =
'Erreur lors du chargement.
';
});
}
function closeDocsModal() {
document.getElementById("docsModal").style.display = "none";
document.body.classList.remove("no-scroll");
}
function handleFileUpload(input) {
// 1. LE VERROU : Si un envoi est déjà en cours, on bloque tout !
if (window.isUploadingDocs) return;
if (!input.files || input.files.length === 0) return;
// On ferme le verrou
window.isUploadingDocs = true;
const file = input.files[0];
const holidayId = document.querySelector('input[name="holiday_id"]').value;
const statusDiv = document.getElementById("uploadStatus");
const listContainer = document.getElementById("docsListContainer");
if (file.size > 5 * 1024 * 1024) {
statusDiv.innerHTML =
"Fichier trop lourd (Max 5Mo). ";
input.value = "";
window.isUploadingDocs = false; // On rouvre le verrou
return;
}
statusDiv.innerHTML =
"⏳ Envoi en cours... ";
const fd = new FormData();
fd.append("holiday_id", holidayId);
fd.append("item_id", window.currentDocsStepId);
fd.append("file", file);
fetch("/modules/holidays/includes/api/upload_attachment.php", {
method: "POST",
body: fd,
})
.then((response) => response.json())
.then((data) => {
if (data.success) {
statusDiv.innerHTML = `✅ Sauvegardé ! `;
const emptyMsg = listContainer.querySelector("p");
if (emptyMsg) emptyMsg.remove();
const docHtml = `
`;
listContainer.insertAdjacentHTML("beforeend", docHtml);
} else {
statusDiv.innerHTML = `❌ Erreur: ${data.error} `;
}
})
.catch((err) => {
statusDiv.innerHTML =
"❌ Erreur réseau. ";
})
.finally(() => {
input.value = "";
// 🔥 2. On rouvre le verrou SEULEMENT quand tout est terminé
setTimeout(() => {
window.isUploadingDocs = false;
}, 500);
});
}
// Fonction pour supprimer un document
function deleteAttachment(fileId, btnElement) {
if (!confirm("Voulez-vous vraiment supprimer ce document définitivement ?"))
return;
const holidayId = document.querySelector('input[name="holiday_id"]').value;
const row = btnElement.closest('div[style*="border: 1px solid"]'); // Cible la ligne d'affichage
row.style.opacity = "0.4"; // Effet visuel d'attente
const fd = new FormData();
fd.append("file_id", fileId);
fd.append("holiday_id", holidayId);
fetch("/modules/holidays/includes/api/delete_attachment.php", {
method: "POST",
body: fd,
})
.then((response) => response.json())
.then((data) => {
if (data.success) {
row.remove(); // On efface la ligne
// Si c'était le dernier fichier, on remet le texte "Aucun document"
const listContainer = document.getElementById("docsListContainer");
if (listContainer.children.length === 0) {
listContainer.innerHTML =
'Aucun document pour cette étape.
';
}
} else {
alert("Erreur : " + data.error);
row.style.opacity = "1";
}
})
.catch(() => {
alert("Erreur réseau lors de la suppression.");
row.style.opacity = "1";
});
}
// ============================================================================
// GÉNÉRATION DU CARNET DE VOYAGE (PDF Côté Client) - VERSION TEXTE BRUT
// ============================================================================
window.generateTravelBook = function () {
const element = document.getElementById("travelBookTemplate");
const btn = document.querySelector('button[onclick="generateTravelBook()"]');
if (!element) {
alert("Erreur : Le modèle de carnet de voyage est introuvable.");
return;
}
const originalText = btn.innerHTML;
btn.innerHTML = "⏳ Génération...";
btn.disabled = true;
// Options ajustées avec un fond blanc forcé
const opt = {
margin: 10, // 10mm de marge
filename: "Carnet_de_Route.pdf",
image: { type: "jpeg", quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, backgroundColor: "#ffffff" },
jsPDF: { unit: "mm", format: "a4", orientation: "portrait" },
};
// 🔥 L'ASTUCE MAGIQUE :
// On extrait le HTML en texte brut et on l'encapsule dans un bloc 100% blanc.
// Plus aucun conflit possible avec l'affichage de ta page web !
const htmlString = `
${element.outerHTML}
`;
// Génération directe depuis la chaîne de texte
html2pdf()
.set(opt)
.from(htmlString)
.save()
.then(() => {
btn.innerHTML = originalText;
btn.disabled = false;
})
.catch((err) => {
console.error("Erreur html2pdf:", err);
btn.innerHTML = originalText;
btn.disabled = false;
});
};
// ============================================================================
// SAUVEGARDE DES NOTES GLOBALES DU VOYAGE (AJAX)
// ============================================================================
async function saveHolidayGlobalNote(holidayId) {
const btn = document.getElementById("btnSaveHolidayNote");
const textarea = document.getElementById("holidayGlobalNotes");
if (!textarea || !btn) return;
const originalText = btn.innerHTML;
btn.innerHTML = "⏳...";
btn.disabled = true;
const fd = new FormData();
fd.append("action", "update_holiday_note");
fd.append("holiday_id", holidayId);
fd.append("notes", textarea.value);
try {
const res = await pachaFetch(
"/modules/holidays/includes/api/save_checkpoint.php",
{
method: "POST",
body: fd,
},
);
if (res.success) {
// L'appel utilise bien ta fonction showToast() globale pour le design des notifications !
showToast(
window.I18N["bud_prev_saved"] || "Notes sauvegardées !",
"success",
);
} else {
showToast(res.error || "Erreur lors de la sauvegarde", "error");
}
} catch (e) {
console.error("Erreur saveHolidayGlobalNote:", e);
showToast("Erreur réseau.", "error");
} finally {
btn.innerHTML = originalText;
btn.disabled = false;
}
}