voyage garage
Deploy HouseHub / deploy (push) Successful in 2s

This commit is contained in:
2026-06-20 12:50:40 +02:00
parent 03a97df8f1
commit 557a1451de
10 changed files with 537 additions and 565 deletions
+85 -5
View File
@@ -1400,8 +1400,88 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
}
/* --- Toll cost estimator panel --- */
.hol-cost-stat { background: var(--bg-page, #f8fafc); border: 1px solid var(--border-light, #e2e8f0); border-radius: 10px; padding: .75rem 1rem; text-align: center; }
.hol-cost-stat-val { font-size: 1.2rem; font-weight: 700; color: var(--text-main, #0f172a); }
.hol-cost-stat-label { font-size: .72rem; color: var(--text-muted, #64748b); margin-top: 2px; text-transform: uppercase; letter-spacing: .04em; }
.hol-cost-total { border-color: var(--primary, #4361ee); }
.hol-cost-total .hol-cost-stat-val { color: var(--primary, #4361ee); }
.hol-cost-stat {
background: var(--bg-page, #f8fafc);
border: 1px solid var(--border-light, #e2e8f0);
border-radius: 10px;
padding: 0.75rem 1rem;
text-align: center;
}
.hol-cost-stat-val {
font-size: 1.2rem;
font-weight: 700;
color: var(--text-main, #0f172a);
}
.hol-cost-stat-label {
font-size: 0.72rem;
color: var(--text-muted, #64748b);
margin-top: 2px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.hol-cost-total {
border-color: var(--primary, #4361ee);
}
.hol-cost-total .hol-cost-stat-val {
color: var(--primary, #4361ee);
}
/* ==========================================================================
17. ENCART D'ESTIMATION DE TRAJET (OSRM)
========================================================================== */
.hol-transit-info {
margin: 0 15px 15px 15px;
padding: 12px 16px;
background: var(--hol-info-bg);
border-radius: var(--radius-m);
border: 1px dashed #7dd3fc;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
transition: all 0.2s ease;
}
.hol-transit-details {
font-size: 0.85rem;
color: var(--hol-info-text);
display: flex;
align-items: center;
gap: 12px;
}
.hol-transit-icon {
font-size: 1.6rem;
line-height: 1;
}
.hol-transit-text strong {
font-size: 0.95rem;
}
.hol-transit-text span {
opacity: 0.85;
font-size: 0.8rem;
display: block;
margin-top: 2px;
}
.hol-transit-btn {
padding: 6px 14px;
font-size: 0.8rem;
white-space: nowrap;
}
@media (max-width: 768px) {
.hol-transit-info {
margin: 0 10px 15px 10px;
padding: 10px;
flex-direction: column;
align-items: flex-start;
}
.hol-transit-btn {
width: 100%;
justify-content: center;
}
}
+176 -170
View File
@@ -5,26 +5,21 @@ function tr(key) {
return window.I18N && window.I18N[key] ? window.I18N[key] : key;
}
// On utilise 'var' au lieu de 'const/let' pour éviter les crashs si le fichier est lu 2 fois
var currentLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
var selectedItemIdForMove = null; // Déplacé ici pour plus de clarté
var selectedItemIdForMove = null;
// ============================================================================
// UTILITAIRES MÉTÉO
// ============================================================================
function getWeatherInfo(code) {
// Transformation en conditions pour regrouper les codes WMO
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") };
// Les codes 51 à 67 et 80 à 82 couvrent toutes les formes de pluie et bruine
if ([51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82].includes(code))
return { icon: "🌧️", label: tr("weather_rainy") };
// Les codes neigeux
if ([71, 73, 75, 77, 85, 86].includes(code))
return { icon: "❄️", label: tr("weather_snowy") };
// Orages
if ([95, 96, 99].includes(code))
return { icon: "⛈️", label: tr("weather_rainy") };
@@ -45,12 +40,8 @@ async function loadWeatherForStep(pt) {
);
const res = await resp.json();
console.log(`Météo pour ${pt.location_name} :`, res);
if (res.success) {
const info = getWeatherInfo(res.data.code);
// Si c'est une estimation basée sur le passé, on adapte l'affichage
const approxSymbol = res.data.is_historical ? "~" : "";
const badgeTitle = res.data.is_historical
? `${info.label} (${tr("weather_historical")})`
@@ -80,8 +71,9 @@ window.addEventListener("click", function (event) {
}
});
// --- 1. GESTION DE LA MODALE D'ÉDITION RAPIDE ---
// ============================================================================
// 1. GESTION DE LA MODALE D'ÉDITION RAPIDE (BASES VOYAGE)
// ============================================================================
function openHolidayModal(mode) {
const modal = document.getElementById("holidayModal");
const form = document.getElementById("holidayForm");
@@ -89,9 +81,6 @@ function openHolidayModal(mode) {
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("hdl_modal_title");
@@ -131,67 +120,12 @@ function editHoliday(data) {
h.budget_extra > 0 ? h.budget_extra : "";
document.getElementById("inp_notes").value = h.notes || "";
document.getElementById("list_transport").innerHTML = "";
document.getElementById("list_accommodation").innerHTML = "";
document.getElementById("list_activity").innerHTML = "";
if (data.items) {
data.items.forEach((item) => {
if (item.name !== "PF_TECHNICAL_POINT") {
// On passe maintenant l'ID et le lieu à addItem
addItem(
item.category,
item.name,
item.amount,
item.is_paid,
item.id,
item.location_name || "",
);
}
});
const vehicleInput = document.getElementById("inp_vehicle_id");
if (vehicleInput) {
vehicleInput.value = h.vehicle_id || "";
}
}
// --- 2. GESTION DES LISTES DYNAMIQUES DANS LA MODALE ---
function addItem(
category,
name = "",
amount = "",
isPaid = 0,
id = "",
location = "",
) {
const container = document.getElementById("list_" + category);
const div = document.createElement("div");
div.className = "savings-line-item"; // Utilisation de ta classe existante
div.style.marginBottom = "10px";
const checkedAttr = isPaid == 1 ? "checked" : "";
// Optimisation : Affichage du badge d'étape si existant
const locationBadge = location
? `<span style="font-size:0.65rem; background:#e2e8f0; padding:2px 6px; border-radius:4px; margin-right:5px; color:#64748b; font-weight:bold;">📍 ${location}</span>`
: "";
div.innerHTML = `
<input type="hidden" name="items[id][]" value="${id}">
<input type="hidden" name="items[cat][]" value="${category}">
<input type="hidden" name="items[location][]" value="${location}">
<div style="flex: 2; display:flex; flex-direction:column; gap:4px;">
${locationBadge}
<input type="text" name="items[name][]" class="pf-input" placeholder="${tr("hdl_js_ph_expense_name")}" value="${name}" style="padding: 8px; font-size:0.9rem;" required>
</div>
<input type="number" step="0.01" name="items[amount][]" class="pf-input" placeholder="0.00" value="${amount}" style="width: 90px; text-align: right; padding: 8px; font-size:0.9rem;">
<label title="${tr("hdl_paid")}" style="display: flex; align-items: center; cursor: pointer; padding: 0 5px;">
<input type="checkbox" ${checkedAttr} onchange="this.nextElementSibling.value = this.checked ? 1 : 0" style="margin:0;">
<input type="hidden" name="items[paid][]" value="${isPaid}">
<span style="font-size:0.75rem; margin-left:4px; font-weight:bold; color:#64748b;">${tr("hdl_paid")}</span>
</label>
<button type="button" onclick="this.parentElement.remove()" class="btn-icon-action delete" title="${tr("btn_delete")}">🗑️</button>
`;
container.appendChild(div);
}
function deleteHoliday() {
if (!confirm(tr("hdl_js_confirm_del_trip"))) return;
const form = document.getElementById("holidayForm");
@@ -203,55 +137,9 @@ function deleteHoliday() {
form.submit();
}
// --- 3. GESTION DE LA CARTE ---
var map = null;
function toggleMap() {
const modal = document.getElementById("hol-map-modal");
if (!modal) return;
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(`<b>${pt.title}</b><br>${pt.status}`);
});
}
}
// ============================================================================
// 4. GESTION DE LA CARTE DÉTAILLÉE (ROADTRIP) ET GÉOCODAGE
// 4. GESTION DE LA CARTE DÉTAILLÉE (ROADTRIP) ET TRACÉS OSRM
// ============================================================================
var detailMap = null;
document.addEventListener("DOMContentLoaded", () => {
@@ -263,7 +151,6 @@ document.addEventListener("DOMContentLoaded", () => {
function initDetailMap() {
if (typeof L === "undefined" || typeof MAP_POINTS === "undefined") return;
// 1. 🧹 NETTOYAGE PROPRE : On détruit l'ancienne instance si elle existe (Évite le bug de la souris bloquée)
if (detailMap !== null) {
detailMap.remove();
detailMap = null;
@@ -272,17 +159,12 @@ function initDetailMap() {
const mapContainer = document.getElementById("tripMap");
if (!mapContainer) return;
// 2. 🛡️ BOUCLIER FIREFOX DESKTOP : Empêche le drag natif HTML5 de voler le clic
mapContainer.style.touchAction = "none";
mapContainer.ondragstart = function (e) {
e.preventDefault();
};
// 3. 🛠️ INITIALISATION DE LA CARTE
detailMap = L.map("tripMap", {
tap: false, // Désactive le tap simulé (anti-warning mobile/Firefox)
dragging: true, // Force l'autorisation du déplacement à la souris
});
detailMap = L.map("tripMap", { tap: false, dragging: true });
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
@@ -290,25 +172,21 @@ function initDetailMap() {
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(detailMap);
// Cas : Aucun point
if (MAP_POINTS.length === 0) {
detailMap.setView([46.6, 2.4], 5); // France par défaut
detailMap.setView([46.6, 2.4], 5);
return;
}
const latlngs = [];
const bounds = L.latLngBounds();
// 4. PLACEMENT DES MARQUEURS
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,
color: "#2563eb",
radius: window.innerWidth < 768 ? 6 : 8,
fillOpacity: 1,
fillColor: "white",
@@ -323,11 +201,10 @@ function initDetailMap() {
<div style="text-align:center;">
<div style="font-size:0.75rem; color:#64748b; margin-bottom:2px; font-weight:bold;">${stepLabel} ${index + 1}</div>
<strong style="font-size:1rem; color:#0f172a;">${pt.location_name}</strong><br>
<span style="font-weight:bold; color:${color};">${parseFloat(pt.total_amount).toFixed(2)} €</span>
<span style="font-weight:bold; color:#2563eb;">${parseFloat(pt.total_amount).toFixed(2)} €</span>
</div>
`);
// Animation au clic sur le marqueur
marker.on("click", function () {
const card = document.getElementById("step-card-" + pt.sort_order);
if (card) {
@@ -345,7 +222,6 @@ function initDetailMap() {
const mapPadding = window.innerWidth < 768 ? [20, 20] : [50, 50];
// 5. CENTRAGE ET TRACÉS (OSRM)
if (latlngs.length === 1) {
detailMap.setView(latlngs[0], 12);
} else if (latlngs.length > 1) {
@@ -379,9 +255,16 @@ function initDetailMap() {
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;
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) => {
@@ -408,6 +291,52 @@ function initDetailMap() {
lineCap: "round",
lineJoin: "round",
}).addTo(detailMap);
// 🔥 CALCUL AUTO DU COUT ET KILOMÈTRES
const distanceKm = res.data.routes[0].distance / 1000;
const fuelL100 = window.VEHICLE_CONSUMPTION || 7;
const fuelPrice = window.FUEL_PRICE || 1.85;
const cost = (distanceKm / 100) * fuelL100 * fuelPrice;
const targetOrder = MAP_POINTS[res.index + 1].sort_order;
const targetCard = document.getElementById(
"step-card-" + targetOrder,
);
if (targetCard) {
const existingTransit =
targetCard.querySelectorAll(".transit-auto-info");
existingTransit.forEach((el) => el.remove());
const rawLocationName = MAP_POINTS[res.index].location_name;
const safeLocationName = rawLocationName.replace(/'/g, "\\'");
const expenseDesc = `Essence depuis ${rawLocationName}`;
const targetStepData = MAP_POINTS[res.index + 1];
const isAlreadyAdded = targetStepData.items.some(
(it) => it.name === expenseDesc,
);
const summaryHtml = `
<div class="transit-auto-info" style="font-size: 0.8rem; color: var(--text-muted); padding: 4px 0 10px 42px; display: flex; align-items: center; gap: 8px;">
🚗 ${Math.round(distanceKm)} km
<span style="opacity: 0.5;">|</span>
**⛽ ~${cost.toFixed(2)} €**
${
!isAlreadyAdded
? `<button type="button" style="background:none; border:none; color:var(--primary); cursor:pointer; font-weight:600; font-size:0.8rem; padding:0; margin-left: 5px;"
onclick="addQuickTransitExpense(${document.querySelector("input[name=holiday_id]").value}, ${targetOrder}, ${cost.toFixed(2)}, 'Essence depuis ${safeLocationName}', this)">
+ Ajouter
</button>`
: `<span style="color:var(--success); font-weight:bold; margin-left: 5px;" title="Dépense déjà ajoutée à cette étape">✓ Ajouté</span>`
}
</div>`;
const cpHeader = targetCard.querySelector(".hol-cp-header");
if (cpHeader) {
cpHeader.insertAdjacentHTML("afterend", summaryHtml);
}
}
} else {
drawFallbackLine(res.coords, routeColor, routeWeight);
}
@@ -415,7 +344,6 @@ function initDetailMap() {
});
}
// 6. LANCEMENT DE LA MÉTÉO
if (typeof MAP_POINTS !== "undefined") {
MAP_POINTS.forEach((pt) => {
if (typeof loadWeatherForStep === "function") {
@@ -424,14 +352,12 @@ function initDetailMap() {
});
}
// 7. FIX FINAL : Force Leaflet à recalculer sa taille une fois le DOM stabilisé
setTimeout(() => {
if (detailMap) {
detailMap.invalidateSize();
}
}, 300);
// Fonction utilitaire locale
function drawFallbackLine(coords, color, weight) {
L.polyline(coords, {
color: color,
@@ -446,7 +372,6 @@ function panMapTo(lat, lng) {
if (detailMap) {
detailMap.setView([lat, lng], 14, { animate: true });
// 🛠️ ERGONOMIE MOBILE : Auto-scroll vers la carte si on est sur petit écran
if (window.innerWidth < 768) {
const mapDiv = document.getElementById("tripMap");
if (mapDiv) {
@@ -456,8 +381,9 @@ function panMapTo(lat, lng) {
}
}
// --- LOGIQUE DE LA MODALE CHECKPOINT ---
// ============================================================================
// 5. LOGIQUE DE LA MODALE CHECKPOINT (ÉTAPES)
// ============================================================================
function openCheckpointModal(mode, data = null) {
const searchBlock = document.getElementById("cpSearchBlock");
const formBlock = document.getElementById("formCheckpoint");
@@ -470,8 +396,10 @@ function openCheckpointModal(mode, data = null) {
document.getElementById("cp_start_date").value = "";
if (document.getElementById("cp_end_date"))
document.getElementById("cp_end_date").value = "";
document.getElementById("searchPlaceInput").value = "";
document.getElementById("searchResults").innerHTML = "";
if (document.getElementById("searchPlaceInput"))
document.getElementById("searchPlaceInput").value = "";
if (document.getElementById("searchResults"))
document.getElementById("searchResults").innerHTML = "";
searchBlock.style.display = "block";
@@ -481,8 +409,16 @@ function openCheckpointModal(mode, data = null) {
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();
document.getElementById("cp_is_return").checked = false;
} else if (mode === "edit" && data) {
document.getElementById("cpModalTitle").innerText = tr("hdl_js_edit_step");
formBlock.style.display = "block";
@@ -494,7 +430,19 @@ function openCheckpointModal(mode, data = null) {
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;
// 🔥 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;
@@ -589,6 +537,10 @@ function addCpExpenseLine(
<option value="transport" ${category === "transport" ? "selected" : ""}>🚗</option>
<option value="activity" ${category === "activity" ? "selected" : ""}>🎫</option>
</select>
<select name="items[context][]" class="pf-input hol-form-select" style="width:auto; margin-left:5px; font-size:0.75rem;">
<option value="local">📍 Sur place</option>
<option value="transit">🛣️ Transit</option>
</select>
<input type="text" name="items[name][]" class="pf-input hol-form-text" placeholder="${tr("hdl_js_ph_expense_name")}" value="${name}">
<input type="number" step="0.01" name="items[amount][]" class="pf-input hol-form-number" placeholder="0.00" value="${amount}">
@@ -621,10 +573,8 @@ function deleteCheckpoint() {
}
// ============================================================================
// 5. RÉORDONNANCEMENT DES ÉTAPES (DRAG & DROP PC + FLÈCHES MOBILE)
// 6. REORDONNANCEMENT DES ÉTAPES (DRAG & DROP PC + MOBILE)
// ============================================================================
// On sort cette fonction pour pouvoir l'appeler depuis les boutons fléchés sur mobile
function saveCheckpointOrder() {
const locations = [
...document.querySelectorAll(".hol-checkpoint-draggable"),
@@ -641,7 +591,6 @@ function saveCheckpointOrder() {
}).then(() => window.location.reload());
}
// Fonction appelée par les flèches Haut/Bas sur mobile
function moveStepMobile(btn, direction) {
const item = btn.closest(".hol-checkpoint-draggable");
const container = item.parentElement;
@@ -672,7 +621,6 @@ document.addEventListener("DOMContentLoaded", () => {
let draggedItem = null;
checkpoints.forEach((item) => {
// Si on est sur mobile, on supprime l'attribut draggable pour éviter les conflits de scroll
if (isMobile) {
item.removeAttribute("draggable");
return;
@@ -724,9 +672,8 @@ document.addEventListener("DOMContentLoaded", () => {
});
// ============================================================================
// MOTEUR DRAG & DROP DU PLANNING
// 7. MOTEUR DRAG & DROP DU PLANNING CARNET DE BORD
// ============================================================================
function closePlanningModal() {
document.getElementById("planningModal").style.display = "none";
document.body.classList.remove("no-scroll");
@@ -798,7 +745,6 @@ function openPlanningModal(step) {
html += `</div></div>`;
container.innerHTML = html;
// 1. On détecte si on est sur mobile juste avant la boucle
const isMobile = window.innerWidth <= 768;
const dragAttr = isMobile ? "" : 'draggable="true"';
@@ -819,7 +765,6 @@ function openPlanningModal(step) {
? `<div class="hol-drag-note">${it.notes}</div>`
: "";
// 2. MODIFICATION ICI : On remplace le texte en dur draggable="true" par la variable ${dragAttr}
const elHtml = `
<div class="hol-drag-item ${catClass}" ${dragAttr}
id="drag-item-${it.id}" data-id="${it.id}"
@@ -957,18 +902,6 @@ function updateItemMemory(itemId, 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));
}
// ============================================================================
// MÉTÉO SPÉCIFIQUE AU HEADER DU PLANNING
// ============================================================================
@@ -997,3 +930,76 @@ async function loadWeatherForPlanning(lat, lng, dateStr) {
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
function addQuickTransitExpense(
holidayId,
sortOrder,
amount,
description,
btnElement,
) {
if (
!confirm(
`Ajouter une dépense de carburant de ${amount}€ pour cette étape ?`,
)
)
return;
if (btnElement) {
btnElement.disabled = true;
btnElement.innerText = "⏳...";
btnElement.style.cursor = "not-allowed";
btnElement.style.opacity = "0.7";
}
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");
fetch("/modules/holidays/includes/api/save_checkpoint.php", {
method: "POST",
body: fd,
}).then(() => window.location.reload());
}
// 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.");
}
}
}
@@ -4,26 +4,51 @@ require dirname(__DIR__, 4) . '/includes/auth.php';
require dirname(__DIR__, 4) . '/includes/db.php';
require_login();
// INTERCEPTION AJAX : Sauvegarde du planning (Drag & Drop / Durée)
if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime', 'update_item_duration'])) {
$itemId = (int)$_POST['item_id'];
// INTERCEPTION AJAX : Sauvegarde du planning (Drag & Drop / Durée) ET Dépense Rapide
if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime', 'update_item_duration', 'add_single_item'])) {
// 🔥 NOUVEAU : Ajout sécurisé d'une dépense unique (Essence OSRM)
if ($_POST['action'] === 'add_single_item') {
$holiday_id = (int)$_POST['holiday_id'];
$sort_order = (int)$_POST['sort_order'];
// On récupère les infos de l'étape existante pour ne rien casser (lat, lng, dates...)
$stmt = $pdo->prepare("SELECT location_name, lat, lng, step_start_date, step_end_date, step_type FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ? LIMIT 1");
$stmt->execute([$holiday_id, $sort_order]);
$stepInfo = $stmt->fetch();
if ($stepInfo) {
$ins = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, step_start_date, step_end_date, step_type, expense_context, duration) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$ins->execute([
$holiday_id, $_POST['category'], $_POST['name'], (float)$_POST['amount'], 0,
$stepInfo['location_name'], $stepInfo['lat'], $stepInfo['lng'],
$sort_order, $stepInfo['step_start_date'], $stepInfo['step_end_date'],
$stepInfo['step_type'], $_POST['context'], 1
]);
}
echo json_encode(['success' => true]);
exit;
}
// --- Ancien code Drag&Drop conservé ---
$itemId = (int)$_POST['item_id'];
if ($_POST['action'] === 'update_item_datetime') {
$itemDate = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
$itemTime = !empty($_POST['item_time']) ? $_POST['item_time'] : null;
$stmt = $pdo->prepare("UPDATE pf_holidays_items SET item_date = ?, item_time = ? WHERE id = ?");
$stmt->execute([$itemDate, $itemTime, $itemId]);
} else {
} else if ($_POST['action'] === 'update_item_duration') {
$duration = (int)$_POST['duration'];
$stmt = $pdo->prepare("UPDATE pf_holidays_items SET duration = ? WHERE id = ?");
$stmt->execute([$duration, $itemId]);
}
echo json_encode(['success' => true]);
exit; // Crucial : on arrête le script ici !
exit;
}
$holiday_id = (int)$_POST['holiday_id'];
// ... (LE RESTE DE TON FICHIER NE CHANGE PAS)
$location_name = trim($_POST['location_name']);
$lat = (float)$_POST['lat'];
$lng = (float)$_POST['lng'];
@@ -53,13 +78,17 @@ if ($holiday_id > 0 && !empty($location_name)) {
$target_order = ($max !== null) ? (int)$max + 1 : 0;
}
// Récupération des dates de l'étape globale
// Récupération des dates de l'étape globale et du type
$step_start = !empty($_POST['step_start_date']) ? $_POST['step_start_date'] : null;
$step_end = !empty($_POST['step_end_date']) ? $_POST['step_end_date'] : null;
$is_return = isset($_POST['is_return']) ? 1 : 0; // NOUVEAU
$step_type = $_POST['step_type'] ?? 'stop';
// 3. INSERTION DES LIGNES (16 Colonnes)
$stmt = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, notes, item_date, item_time, step_start_date, step_end_date, duration, is_return) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
// Nettoyage des dates selon le type d'étape
if ($step_type === 'origin') $step_end = null; // Un départ n'a pas de date de fin
if ($step_type === 'destination') $step_end = null; // Une arrivée finale n'a pas de date de départ
// 3. INSERTION DES LIGNES
$stmt = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, notes, item_date, item_time, step_start_date, step_end_date, duration, step_type, expense_context) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$validItemsCount = 0;
if (isset($_POST['items']['name'])) {
@@ -75,30 +104,26 @@ if ($holiday_id > 0 && !empty($location_name)) {
$date = !empty($_POST['items']['date'][$i]) ? $_POST['items']['date'][$i] : null;
$time = !empty($_POST['items']['time'][$i]) ? $_POST['items']['time'][$i] : null;
$dur = !empty($_POST['items']['duration'][$i]) ? (int)$_POST['items']['duration'][$i] : 1;
$context = !empty($_POST['items']['context'][$i]) ? $_POST['items']['context'][$i] : 'local';
// Ajout de $is_return à la fin
$stmt->execute([$holiday_id, $cat, $name ?: tr('hdl_default_exp_name'), $amount, $paid, $location_name, $lat, $lng, $target_order, $note, $date, $time, $step_start, $step_end, $dur, $is_return]);
$stmt->execute([$holiday_id, $cat, $name ?: tr('hdl_default_exp_name'), $amount, $paid, $location_name, $lat, $lng, $target_order, $note, $date, $time, $step_start, $step_end, $dur, $step_type, $context]);
$validItemsCount++;
}
}
}
if ($validItemsCount === 0) {
$stmt->execute([$holiday_id, 'activity', 'PF_TECHNICAL_POINT', 0, 1, $location_name, $lat, $lng, $target_order, '', null, null, $step_start, $step_end, 1, $is_return]);
$stmt->execute([$holiday_id, 'activity', 'PF_TECHNICAL_POINT', 0, 1, $location_name, $lat, $lng, $target_order, '', null, null, $step_start, $step_end, 1, $step_type, 'local']);
}
// 4. GESTION DES FAVORIS
if (isset($_POST['save_favorite']) && $_POST['save_favorite'] == '1') {
$stmtFav = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'holiday_favorites'");
$favs = json_decode($stmtFav->fetchColumn() ?: '[]', true);
$exists = false;
foreach ($favs as $f) { if ($f['name'] === $location_name) $exists = true; }
if (!$exists) {
$favs[] = ['name' => $location_name, 'lat' => $lat, 'lng' => $lng];
$pdo->prepare("INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('holiday_favorites', 'GLOBAL', ?) ON DUPLICATE KEY UPDATE content = VALUES(content)")->execute([json_encode($favs)]);
}
// 4. GESTION DU RETOUR (Si l'utilisateur définit cette étape comme point de retour)
if (isset($_POST['set_as_return']) && $_POST['set_as_return'] == '1') {
// On enregistre l'ID de cette étape technique comme point de retour global du voyage
$pdo->prepare("UPDATE pf_holidays SET return_step_id = ? WHERE id = ?")->execute([$target_order, $holiday_id]);
}
// 5. GESTION DES FAVORIS ... (Garde ton code existant ici)
$pdo->commit();
} catch (Exception $e) { $pdo->rollBack(); die($e->getMessage()); }
}
+10 -47
View File
@@ -1,10 +1,10 @@
<?php
// modules/holidays/includes/api/save_holiday.php
// On remonte de 4 niveaux pour atteindre la racine (api -> includes -> holidays -> modules -> racine)
// On remonte de 4 niveaux pour atteindre la racine
require dirname(__DIR__, 4) . '/includes/auth.php';
require dirname(__DIR__, 4) . '/includes/db.php';
require_login(); // Si cette fonction nécessite une redirection, gère-la dans auth.php
require_login();
if (isset($_POST['action_delete']) && $_POST['action_delete'] == '1') {
$stmt = $pdo->prepare("DELETE FROM pf_holidays WHERE id = ?");
@@ -22,61 +22,25 @@ $status = $_POST['status'];
$food = !empty($_POST['budget_food']) ? $_POST['budget_food'] : 0;
$extra = !empty($_POST['budget_extra']) ? $_POST['budget_extra'] : 0;
$notes = $_POST['notes'];
// 🔥 NOUVEAU : On récupère le véhicule optionnel
$vehicle_id = !empty($_POST['vehicle_id']) ? (int)$_POST['vehicle_id'] : null;
try {
$pdo->beginTransaction();
if ($id) {
// UPDATE
$sql = "UPDATE pf_holidays SET title=?, period_hint=?, start_date=?, end_date=?, status=?, budget_food=?, budget_extra=?, notes=? WHERE id=?";
// UPDATE (avec vehicle_id)
$sql = "UPDATE pf_holidays SET title=?, period_hint=?, start_date=?, end_date=?, status=?, budget_food=?, budget_extra=?, notes=?, vehicle_id=? WHERE id=?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $id]);
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $vehicle_id, $id]);
} else {
// INSERT
$sql = "INSERT INTO pf_holidays (title, period_hint, start_date, end_date, status, budget_food, budget_extra, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
// INSERT (avec vehicle_id)
$sql = "INSERT INTO pf_holidays (title, period_hint, start_date, end_date, status, budget_food, budget_extra, notes, vehicle_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $pdo->prepare($sql);
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes]);
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $vehicle_id]);
$id = $pdo->lastInsertId();
}
// GESTION INTELLIGENTE DES ITEMS
if (!empty($_POST['items']['name'])) {
$count = count($_POST['items']['name']);
// On prépare une requête qui met à jour si l'ID existe, sinon insère
$stmtItem = $pdo->prepare("
INSERT INTO pf_holidays_items (id, holiday_id, category, name, amount, is_paid, location_name)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
category = VALUES(category),
name = VALUES(name),
amount = VALUES(amount),
is_paid = VALUES(is_paid)
");
$keepIds = [];
for ($i = 0; $i < $count; $i++) {
$itemId = !empty($_POST['items']['id'][$i]) ? (int)$_POST['items']['id'][$i] : null;
$cat = $_POST['items']['cat'][$i] ?? 'activity';
$name = trim($_POST['items']['name'][$i] ?? '');
$amount = floatval($_POST['items']['amount'][$i] ?? 0);
$paid = (int)($_POST['items']['paid'][$i] ?? 0);
$loc = !empty($_POST['items']['location'][$i]) ? $_POST['items']['location'][$i] : null;
if (!empty($name)) {
$stmtItem->execute([$itemId, $id, $cat, $name, $amount, $paid, $loc]);
$keepIds[] = $itemId ?: $pdo->lastInsertId();
}
}
// Nettoyage : On supprime les items qui ont été retirés de la modale
// (Attention : uniquement ceux du voyage actuel qui ne sont plus dans la liste envoyée)
if (!empty($keepIds)) {
$placeholders = implode(',', array_fill(0, count($keepIds), '?'));
$sqlDel = "DELETE FROM pf_holidays_items WHERE holiday_id = ? AND id NOT IN ($placeholders)";
$pdo->prepare($sqlDel)->execute(array_merge([$id], $keepIds));
}
}
$pdo->commit();
} catch (Exception $e) {
@@ -84,6 +48,5 @@ try {
die("Erreur base de données : " . $e->getMessage());
}
// Redirection vers la page principale
header("Location: /holidays.php");
exit;
+113 -129
View File
@@ -11,10 +11,15 @@ if ($id === 0) {
// Récupération des données du voyage
$stmt = $pdo->prepare("
SELECT h.*,
v.name as vehicle_name,
v.consumption as vehicle_consumption,
(COALESCE(h.budget_food, 0) + COALESCE(h.budget_extra, 0) + COALESCE((SELECT SUM(amount) FROM pf_holidays_items WHERE holiday_id = h.id), 0)) as total_cost,
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND is_paid = 1) as total_paid,
(SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved
FROM pf_holidays h WHERE h.id = ?
(SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved,
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND expense_context = 'transit') as total_transit
FROM pf_holidays h
LEFT JOIN pf_vehicles v ON h.vehicle_id = v.id
WHERE h.id = ?
");
$stmt->execute([$id]);
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);
@@ -42,7 +47,7 @@ foreach ($items as $it) {
'sort_order' => $it['sort_order'],
'step_start_date' => $it['step_start_date'],
'step_end_date' => $it['step_end_date'],
'is_return' => (int)$it['is_return'],
'step_type' => $it['step_type'] ?? 'stop',
'total_amount' => 0,
'items' => []
];
@@ -105,10 +110,34 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<div class="hol-summary-card">
<div class="hol-summary-grid">
<?php if (!empty($holiday['vehicle_name'])): ?>
<div class="hol-summary-item">
<div class="hol-summary-label"><?= tr('hdl_label_period') ?></div>
<div class="hol-summary-value"><?= $dateDisplay ?: tr('hdl_dates_to_define') ?></div>
<div class="hol-summary-label">Transport</div>
<div class="hol-summary-value">🚗 <?= htmlspecialchars($holiday['vehicle_name']) ?></div>
</div>
<?php endif; ?>
<div class="hol-summary-item">
<div class="hol-summary-label">
Frais de route (Essence/Péages)
</div>
<div class="hol-summary-value" style="display:flex; align-items:center; gap:6px;">
<span style="font-size: 1.1rem;">⛽</span>
<strong><?= number_format($holiday['total_transit'], 0) ?> €</strong>
<span onclick="updateFuelPrice()" style="font-size: 0.75rem; color: var(--text-muted); cursor: pointer; transition: color 0.2s; display: inline-flex; align-items: center; gap: 3px;" onmouseover="this.style.color='var(--primary)';" onmouseout="this.style.color='var(--text-muted)';" title="Modifier le prix estimé du carburant">
(<span id="display_fuel_price">1.85</span> €/L) <span style="font-size:0.7rem; opacity:0.8;">✏️</span>
</span>
<?php if ($holiday['total_transit'] > 0): ?>
<span onclick="alert('Bientôt : Liste détaillée des frais de route !')" style="font-size: 1rem; cursor: pointer; opacity: 0.5; transition: opacity 0.2s; margin-left: 4px; display: inline-flex; align-items: center;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.5'" title="Voir le détail">
👁️
</span>
<?php endif; ?>
</div>
</div>
<div class="hol-summary-item">
<div class="hol-summary-label"><?= tr('hdl_label_budget_food_extras') ?></div>
<div class="hol-summary-value">🍔 <?= number_format($holiday['budget_food'], 0) ?> € | 🎁 <?= number_format($holiday['budget_extra'], 0) ?> €</div>
@@ -211,9 +240,15 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
</div>
<div class="hol-cp-title" onclick="panMapTo(<?= $step['lat'] ?>, <?= $step['lng'] ?>)" title="<?= htmlspecialchars($step['location_name']) ?>">
📍 <?= htmlspecialchars($step['location_name']) ?>
<?php if (!empty($step['is_return'])): ?>
<?php if ($holiday['return_step_id'] !== null && $holiday['return_step_id'] == $step['sort_order']): ?>
<span style="background: #fff7ed; color: #ea580c; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: bold; margin-left: 5px; border: 1px solid #ffedd5; vertical-align: middle;">🏁 <?= tr('hdl_return') ?></span>
<?php endif; ?>
<?php if ($step['step_type'] === 'origin'): ?>
<span style="background: #ecfdf5; color: #059669; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: bold; margin-left: 5px; border: 1px solid #d1fae5; vertical-align: middle;">🛫 DÉPART</span>
<?php elseif ($step['step_type'] === 'destination'): ?>
<span style="background: #fef2f2; color: #e11d48; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: bold; margin-left: 5px; border: 1px solid #fee2e2; vertical-align: middle;">🛬 ARRIVÉE FINALE</span>
<?php endif; ?>
<?php if (!empty($step['step_start_date']) && !empty($step['step_end_date'])): ?>
<div class="hol-date-weather-wrapper">
<span class="hol-step-dates">
@@ -261,22 +296,6 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
</div>
</div>
<?php if (count($mapPoints) >= 2): ?>
<div class="hol-summary-card" id="hol-cost-panel" style="margin-top:24px;">
<div style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px; margin-bottom:16px;">
<h3 style="margin:0; display:flex; align-items:center; gap:8px;">🚗 Coût du trajet</h3>
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
<label style="font-size:.82rem; color:var(--text-muted);">Conso (L/100km)</label>
<input type="number" id="fuel-l100" value="7" min="3" max="30" step="0.5" style="width:65px; padding:.3rem .5rem; border:1px solid var(--border-light); border-radius:8px; font-size:.85rem;">
<label style="font-size:.82rem; color:var(--text-muted);">Prix carburant (€/L)</label>
<input type="number" id="fuel-price" value="1.85" min="1" max="4" step="0.01" style="width:65px; padding:.3rem .5rem; border:1px solid var(--border-light); border-radius:8px; font-size:.85rem;">
<button onclick="estimateTripCost()" class="pf-btn pf-btn-small">Calculer</button>
</div>
</div>
<div id="hol-cost-result" style="color:var(--text-muted); font-size:.9rem;">Cliquez sur Calculer pour estimer le coût du trajet.</div>
</div>
<?php endif; ?>
<?php if (!empty($holiday['notes'])): ?>
<div class="hol-summary-card" style="margin-top: 24px; padding: 25px; border-left: 5px solid #f59e0b;">
<h3 style="margin: 0 0 15px 0; font-size: 1.2rem; color: #0f172a; display: flex; align-items: center; gap: 8px;">
@@ -328,22 +347,32 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<input type="text" name="location_name" id="cp_name" class="pf-input" style="font-weight:bold; color:var(--primary);" required>
</div>
<div class="form-group" style="margin-bottom:15px; background:#fff7ed; padding:10px; border-radius:8px; border:1px solid #ffedd5;">
<label class="pf-label" style="color:#ea580c;">📍 Type d'étape</label>
<select name="step_type" id="cp_step_type" class="pf-input" onchange="toggleStepDates(this.value)">
<option value="origin">DÉPART (Point de départ du voyage)</option>
<option value="stop">SÉJOUR (Étape classique avec arrivée et départ)</option>
<option value="destination">ARRIVÉE FINALE (Fin du voyage)</option>
</select>
</div>
<div style="display:flex; gap:15px; margin-bottom:15px; background:#f8fafc; padding:12px; border-radius:8px;">
<div class="form-group" style="flex:1;">
<label class="pf-label"><?= tr('hdl_label_arrival') ?></label>
<div class="form-group" id="grp_start_date" style="flex:1;">
<label class="pf-label" id="lbl_start_date"><?= tr('hdl_label_arrival') ?></label>
<input type="date" name="step_start_date" id="cp_start_date" class="pf-input">
</div>
<div class="form-group" style="flex:1;">
<label class="pf-label"><?= tr('hdl_label_departure') ?></label>
<div class="form-group" id="grp_end_date" style="flex:1;">
<label class="pf-label" id="lbl_end_date"><?= tr('hdl_label_departure') ?></label>
<input type="date" name="step_end_date" id="cp_end_date" class="pf-input">
</div>
</div>
<div style="margin-bottom: 15px;">
<label style="display:flex; align-items:center; cursor:pointer; color:#ea580c; font-weight:600;">
<input type="checkbox" name="is_return" id="cp_is_return" value="1" style="margin-right:8px;">
🏁 <?= tr('hdl_return') ?>
<div style="margin-bottom: 20px; padding-top: 15px; border-top: 1px dashed #e2e8f0;">
<label style="display:flex; align-items:center; cursor:pointer; color:#ea580c; font-weight:600; font-size:0.9rem;">
<input type="checkbox" name="set_as_return" id="cp_set_as_return" value="1" style="margin-right:8px; width:16px; height:16px;">
🏁 Définir comme retour
</label>
<p style="margin: 4px 0 0 24px; font-size: 0.75rem; color: #64748b;">La route sera tracée en orange à partir d'ici.</p>
</div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
@@ -385,109 +414,64 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<?php include __DIR__ . '/modal.php'; ?>
<script>
// --- 1. SÉCURISATION TRADUCTIONS ET VARIABLES ---
window.MAP_POINTS = <?= json_encode($mapPoints ?? []) ?>;
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
// --- 1. SÉCURISATION TRADUCTIONS ET VARIABLES ---
window.MAP_POINTS = <?= json_encode($mapPoints ?? []) ?>;
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
window.I18N = {
...(window.I18N || {}),
'hdl_js_search_loading': "<?= tr('hdl_js_search_loading') ?>",
'hdl_js_no_result': "<?= tr('hdl_js_no_result') ?>",
'hdl_js_confirm_del_trip': "<?= tr('hdl_js_confirm_del_trip') ?>",
'hdl_js_confirm_del_step': "<?= tr('hdl_js_confirm_del_step') ?>",
'hdl_js_step_label': "<?= tr('hdl_js_step_label') ?>",
'hdl_js_ph_expense_name': "<?= tr('hdl_js_ph_expense_name') ?>",
'hdl_js_delete_line': "<?= tr('btn_delete') ?>",
'hdl_planning_title': "<?= tr('hdl_planning_title') ?>",
'hdl_to_place': "<?= tr('hdl_to_place') ?>",
'hdl_js_missing_dates_title': "<?= tr('hdl_js_missing_dates_title') ?>",
'hdl_js_missing_dates_msg': "<?= tr('hdl_js_missing_dates_msg') ?>",
'hdl_modal_title': "<?= tr('hdl_modal_title') ?>",
'hdl_js_edit_step': "<?= tr('hdl_js_edit_step') ?>",
'hdl_ph_notes': "<?= tr('hdl_ph_notes') ?>",
'hdl_btn_add_step': "<?= tr('hdl_btn_add_step') ?>",
'hdl_quick_edit_title': "<?= tr('hdl_quick_edit_title') ?>",
'hdl_paid': "<?= tr('hdl_paid') ?>",
window.I18N = {
...(window.I18N || {}),
'hdl_js_search_loading': "<?= tr('hdl_js_search_loading') ?>",
'hdl_js_no_result': "<?= tr('hdl_js_no_result') ?>",
'hdl_js_confirm_del_trip': "<?= tr('hdl_js_confirm_del_trip') ?>",
'hdl_js_confirm_del_step': "<?= tr('hdl_js_confirm_del_step') ?>",
'hdl_js_step_label': "<?= tr('hdl_js_step_label') ?>",
'hdl_js_ph_expense_name': "<?= tr('hdl_js_ph_expense_name') ?>",
'hdl_js_delete_line': "<?= tr('btn_delete') ?>",
'hdl_planning_title': "<?= tr('hdl_planning_title') ?>",
'hdl_to_place': "<?= tr('hdl_to_place') ?>",
'hdl_js_missing_dates_title': "<?= tr('hdl_js_missing_dates_title') ?>",
'hdl_js_missing_dates_msg': "<?= tr('hdl_js_missing_dates_msg') ?>",
'hdl_modal_title': "<?= tr('hdl_modal_title') ?>",
'hdl_js_edit_step': "<?= tr('hdl_js_edit_step') ?>",
'hdl_ph_notes': "<?= tr('hdl_ph_notes') ?>",
'hdl_btn_add_step': "<?= tr('hdl_btn_add_step') ?>",
'hdl_quick_edit_title': "<?= tr('hdl_quick_edit_title') ?>",
'hdl_paid': "<?= tr('hdl_paid') ?>",
// --- NOUVELLES CLÉS MÉTÉO ICI ---
'weather_sunny': "<?= tr('weather_sunny') ?>",
'weather_cloudy': "<?= tr('weather_cloudy') ?>",
'weather_rainy': "<?= tr('weather_rainy') ?>",
'weather_snowy': "<?= tr('weather_snowy') ?>",
'weather_forecast': "<?= tr('weather_forecast') ?>",
'weather_historical': "<?= tr('weather_historical') ?>"
};
// Fallback de sécurité pour s'assurer que les modales peuvent toujours se fermer
window.closeCheckpointModal = window.closeCheckpointModal || function() {
const modal = document.getElementById('checkpointModal');
if(modal) modal.style.display = 'none';
document.body.classList.remove('no-scroll');
};
window.closePlanningModal = window.closePlanningModal || function() {
const modal = document.getElementById('planningModal');
if(modal) modal.style.display = 'none';
document.body.classList.remove('no-scroll');
};
// 1. On charge le prix depuis le navigateur (ou 1.85 par défaut)
const savedFuelPrice = localStorage.getItem('holidays_fuel_price') || 1.85;
window.FUEL_PRICE = parseFloat(savedFuelPrice);
// --- NOUVELLES CLÉS MÉTÉO ICI ---
'weather_sunny': "<?= tr('weather_sunny') ?>",
'weather_cloudy': "<?= tr('weather_cloudy') ?>",
'weather_rainy': "<?= tr('weather_rainy') ?>",
'weather_snowy': "<?= tr('weather_snowy') ?>",
'weather_forecast': "<?= tr('weather_forecast') ?>",
'weather_historical': "<?= tr('weather_historical') ?>"
};
// 2. On met à jour le texte du petit bouton "✏️" en haut de la page
const displayFuelEl = document.getElementById('display_fuel_price');
if (displayFuelEl) displayFuelEl.innerText = window.FUEL_PRICE.toFixed(2);
// Fallback de sécurité pour s'assurer que les modales peuvent toujours se fermer
window.closeCheckpointModal = window.closeCheckpointModal || function() {
const modal = document.getElementById('checkpointModal');
if(modal) modal.style.display = 'none';
document.body.classList.remove('no-scroll');
};
// 3. Variables voiture et retour
window.VEHICLE_CONSUMPTION = <?= !empty($holiday['vehicle_consumption']) ? (float)$holiday['vehicle_consumption'] : 7 ?>;
window.GLOBAL_RETURN_STEP_ID = <?= $holiday['return_step_id'] !== null ? $holiday['return_step_id'] : 'null' ?>;
window.closePlanningModal = window.closePlanningModal || function() {
const modal = document.getElementById('planningModal');
if(modal) modal.style.display = 'none';
document.body.classList.remove('no-scroll');
};
async function estimateTripCost() {
const l100 = parseFloat(document.getElementById('fuel-l100').value) || 7;
const price = parseFloat(document.getElementById('fuel-price').value) || 1.85;
const result = document.getElementById('hol-cost-result');
result.innerHTML = '⏳ Calcul en cours…';
const stops = (window.MAP_POINTS || []).map(p => ({lat: p.lat, lng: p.lng, name: p.location_name}));
if (stops.length < 2) { result.innerHTML = 'Pas assez d\'étapes pour calculer.'; return; }
try {
const resp = await fetch('/modules/voyage/api.php?action=estimate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({stops, fuel_l100: l100, fuel_price: price})
});
const data = await resp.json();
if (!data.ok) { result.innerHTML = 'Erreur : ' + (data.error || 'inconnue'); return; }
let html = '<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:12px; margin-bottom:16px;">';
html += `<div class="hol-cost-stat"><div class="hol-cost-stat-val">${Math.round(data.total_km)} km</div><div class="hol-cost-stat-label">Distance totale</div></div>`;
html += `<div class="hol-cost-stat"><div class="hol-cost-stat-val">${data.total_toll.toFixed(2)} €</div><div class="hol-cost-stat-label">Péages estimés</div></div>`;
html += `<div class="hol-cost-stat"><div class="hol-cost-stat-val">${data.fuel_cost.toFixed(2)} €</div><div class="hol-cost-stat-label">Carburant</div></div>`;
html += `<div class="hol-cost-stat hol-cost-total"><div class="hol-cost-stat-val">${data.grand_total.toFixed(2)} €</div><div class="hol-cost-stat-label">Total aller</div></div>`;
html += '</div>';
if (data.segments && data.segments.length) {
html += '<div style="border-top:1px solid var(--border-light); padding-top:12px; margin-top:4px;">';
html += '<div style="font-size:.75rem; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--text-muted); margin-bottom:10px;">Détail du trajet</div>';
data.segments.forEach(s => {
html += `<div style="padding:8px 0; border-bottom:1px solid var(--border-light);">`;
html += `<div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:4px;">
<span style="font-weight:600; font-size:.88rem;">📍 ${esc(s.from)} → ${esc(s.to)}</span>
<span style="font-size:.88rem; color:var(--primary);">péage : <strong>${s.toll.toFixed(2)} €</strong></span>
</div>`;
html += `<div style="font-size:.78rem; color:var(--text-muted);">🛣️ ${Math.round(s.distance_km)} km`;
if (s.entry_plaza && s.exit_plaza) {
html += ` · ${esc(s.entry_plaza)} → ${esc(s.exit_plaza)}`;
if (s.op) html += ` <span style="background:var(--bg-page); border:1px solid var(--border-light); border-radius:4px; padding:0 4px; font-size:.7rem;">${esc(s.op)}</span>`;
} else if (s.note) {
html += ` · <em>${esc(s.note)}</em>`;
}
html += '</div></div>';
});
html += '</div>';
}
function esc(s) {
return String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
result.innerHTML = html;
} catch(e) {
result.innerHTML = 'Erreur de connexion.';
console.error(e);
}
}
</script>
<script src="/modules/holidays/holidays.js"></script>
<script src="/modules/holidays/holidays.js?v=<?= time() ?>"></script>
+10 -29
View File
@@ -1,6 +1,5 @@
<div id="holidayModal" class="pf-modal">
<div class="pf-modal-content hol-modal-content">
<h3 id="modalTitle" class="pf-modal-title"><?= tr('hdl_modal_title') ?></h3>
<div class="pf-modal-content hol-modal-content" style="max-width: 500px;"> <h3 id="modalTitle" class="pf-modal-title"><?= tr('hdl_modal_title') ?></h3>
<form action="/modules/holidays/includes/api/save_holiday.php" method="POST" id="holidayForm">
<input type="hidden" name="id" id="inp_id">
@@ -40,32 +39,14 @@
</div>
</div>
<hr class="hol-divider">
<div class="hol-columns-wrapper">
<div class="hol-col">
<div class="hol-col-header">
<h4 class="hol-cat-transport">🚗 <?= tr('hdl_cat_transport') ?></h4>
<button type="button" class="btn-add-item" onclick="addItem('transport')" title="<?= tr('hdl_add_transport') ?>"></button>
</div>
<div id="list_transport" class="dynamic-list"></div>
</div>
<div class="hol-col">
<div class="hol-col-header">
<h4 class="hol-cat-accommodation">🏨 <?= tr('hdl_cat_accommodation') ?></h4>
<button type="button" class="btn-add-item" onclick="addItem('accommodation')" title="<?= tr('hdl_add_accommodation') ?>"></button>
</div>
<div id="list_accommodation" class="dynamic-list"></div>
</div>
<div class="hol-col">
<div class="hol-col-header">
<h4 class="hol-cat-activity">🎫 <?= tr('hdl_cat_activity') ?></h4>
<button type="button" class="btn-add-item" onclick="addItem('activity')" title="<?= tr('hdl_add_activity') ?>"></button>
</div>
<div id="list_activity" class="dynamic-list"></div>
</div>
<div class="form-group" style="margin-top: 15px; padding: 10px; background: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0;">
<label class="pf-label" style="margin-bottom: 5px;">🚗 Véhicule utilisé (Optionnel)</label>
<select name="vehicle_id" id="inp_vehicle_id" class="pf-input">
<option value="">-- Aucun / Autre transport --</option>
<?php foreach($garageVehicles as $v): ?>
<option value="<?= $v['id'] ?>"><?= htmlspecialchars($v['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<hr class="hol-divider">
@@ -81,7 +62,7 @@
</div>
</div>
<div class="form-group">
<div class="form-group" style="margin-top: 10px;">
<label class="pf-label"><?= tr('hdl_label_notes') ?></label>
<textarea name="notes" id="inp_notes" class="pf-input" rows="2" placeholder="<?= tr('hdl_ph_notes') ?>"></textarea>
</div>