update road trip
Deploy HouseHub / deploy (push) Successful in 1s

This commit is contained in:
2026-07-24 17:55:56 +02:00
parent 49d4ec7c35
commit ac8ebf7328
5 changed files with 634 additions and 236 deletions
+2 -2
View File
@@ -1196,7 +1196,7 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
top: 1px;
left: 45px;
right: 5px;
height: calc(75px * var(--duration) - 2px);
height: calc(60px * var(--duration) - 2px);
}
/* Dans la boîte d'attente : Position normale */
.hol-unmapped-zone .hol-drag-item {
@@ -1253,7 +1253,7 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
}
.hol-time-slot {
height: 75px;
height: 60px;
border-bottom: 1px solid var(--border-light);
position: relative;
transition: background 0.2s;
+551 -150
View File
@@ -257,9 +257,9 @@ function initDetailMap() {
Promise.all(routePromises).then((results) => {
results.sort((a, b) => a.index - b.index);
// 🔥 NOUVEAU : Compteurs et HTML de la modale
let totalTripDistance = 0;
let totalTripDuration = 0; // en secondes
let totalFuelCost = 0; // Coût exact accumulé
let transitDetailsHtml = "";
let returnStartIndex = latlngs.length - 2;
@@ -310,6 +310,8 @@ function initDetailMap() {
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];
@@ -322,14 +324,41 @@ function initDetailMap() {
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
? `<strong style="color: var(--text-main);">💳 ${stepTollCost.toFixed(2)} €</strong>`
: "";
// 🔥 CONSTRUCTION DU CONTENU DE LA MODALE
transitDetailsHtml += `
<div style="padding: 12px 0; border-bottom: 1px dashed var(--border-light);">
<div style="font-weight: 600; font-size: 0.9rem; color: var(--text-main); margin-bottom: 4px;">
📍 ${startPt.location_name}${endPt.location_name}
<div style="padding: 12px 0; border-bottom: 1px dashed var(--border-light); display: flex; justify-content: space-between; align-items: flex-end;">
<div style="display: flex; flex-direction: column; gap: 4px;">
<div style="font-weight: 600; font-size: 0.9rem; color: var(--text-main);">
📍 ${startPt.location_name}${endPt.location_name}
</div>
<div style="font-size: 0.8rem; color: var(--text-muted);">
🚗 ${Math.round(distanceKm)} km &nbsp;•&nbsp; ⏱️ ${formatDuration(durationSec)}
</div>
</div>
<div style="font-size: 0.8rem; color: var(--text-muted); display: flex; justify-content: space-between; align-items: center;">
<span>🚗 ${Math.round(distanceKm)} km &nbsp;•&nbsp; ⏱️ ${formatDuration(durationSec)}</span>
<div style="display: flex; flex-direction: column; align-items: flex-end; gap: 4px; font-size: 0.8rem;">
${tollHtml}
<strong style="color: var(--primary);">⛽ ${cost.toFixed(2)} €</strong>
</div>
</div>
@@ -370,6 +399,7 @@ function initDetailMap() {
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);
@@ -377,6 +407,10 @@ function initDetailMap() {
distBlock.style.display = "block";
}
if (globalFuelCostEl) {
globalFuelCostEl.innerText = Math.round(totalFuelCost);
}
// 🔥 INJECTION DU HTML DANS LA MODALE
const modalContainer = document.getElementById("transitDetailsContainer");
if (modalContainer) {
@@ -573,10 +607,7 @@ 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="hidden" name="items[context][]" value="local">
<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}">
@@ -708,63 +739,129 @@ document.addEventListener("DOMContentLoaded", () => {
});
// ============================================================================
// 7. MOTEUR DRAG & DROP DU PLANNING CARNET DE BORD
// 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 openPlanningModal(step) {
document.getElementById("planningModalTitle").innerText =
tr("hdl_planning_title") + " : " + step.location_name;
const container = document.getElementById("planningContainer");
selectedItemIdForMove = null;
function openGlobalPlanningModal() {
const holidayDataJsonEl = document.getElementById("holidayDataJson");
if (!holidayDataJsonEl) return;
let validItems = step.items.filter((it) => it.name !== "PF_TECHNICAL_POINT");
const holidayData = JSON.parse(holidayDataJsonEl.textContent).main;
window.CURRENT_PLANNING_STEP = step; // Mémorise l'étape en cours
if (window.TRANSIT_DATA && window.TRANSIT_DATA[step.sort_order]) {
// Est-ce qu'on a déjà planifié ou ajouté ce trajet ?
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)); // Arrondi en heures
validItems.push({
id: "virtual-transit",
name: `Essence depuis ${tData.from}`, // Garde ce nom pour lier avec le budget
category: "transport",
expense_context: "transit",
duration: h,
notes: `Trajet GPS (${Math.round(tData.sec / 60)} min). Déplacez pour planifier la route.`,
is_virtual: true,
});
}
}
if (!step.step_start_date || !step.step_end_date) {
container.innerHTML = `<div style="text-align:center; padding:40px;"><h3>${tr("hdl_js_missing_dates_title")}</h3><p style="color:#64748b;">${tr("hdl_js_missing_dates_msg")}</p></div>`;
document.getElementById("planningModal").style.display = "flex";
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(step.step_start_date);
let end = new Date(step.step_end_date);
while (curr <= end) {
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 = `
<div class="hol-planning-layout">
<div class="hol-unmapped-zone" id="unmapped-pool"
ondragover="allowDrop(event)" ondrop="handleDropEvent(event, '', '')"
onclick="handleZoneTap(event, '', '')">
<div class="hol-unmapped-title" style="width:100%;">📥 ${tr("hdl_to_place")}</div>
<style>
/* 🌟 MAGIE CSS : Ajustement adaptatif des tailles selon la zone */
#unmapped-pool .hol-drag-item {
/* Base 40px + 10px par heure supp, capé à +30px max (soit environ 4h visuelles max) */
min-height: calc(40px + (min(var(--duration) - 1, 3) * 10px)) !important;
}
.hol-time-slots-container .hol-drag-item {
/* Dans le calendrier : taille 100% fidèle (40px par heure) */
min-height: calc(var(--duration) * 40px - 8px) !important;
}
/* 🌟 Surbrillance bleue pour chaque case survolée correspondante à la durée */
.hol-time-slot.drag-over-duration {
background: rgba(59, 130, 246, 0.15) !important;
border-left: 3px solid var(--primary) !important;
}
</style>
<div style="display: flex; width: 100%; height: 100%; gap: 15px;">
<!-- Panneau Gauche : À Placer -->
<div class="hol-unmapped-zone" style="width: 280px; display: flex; flex-direction: column; background: var(--bg-subtle); border-radius: 8px; border: 1px solid var(--border-light); padding: 12px; flex-shrink: 0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; flex-shrink: 0;">
<div class="hol-unmapped-title" style="margin:0; font-weight:700; color:var(--text-main);">📥 ${tr("hdl_to_place")}</div>
<button onclick="filterPoolByDate(null)" class="pf-btn btn-secondary pf-btn-small" style="padding: 2px 8px; font-size: 0.75rem;">🔄 Tous</button>
</div>
<div id="unmapped-pool" style="flex: 1; overflow-y: auto; padding-right: 5px; display: flex; flex-direction: column; gap: 8px;"
ondragover="allowDrop(event)" ondrop="handleDropEvent(event, '', '')"
onclick="handleZoneTap(event, '', '')">
</div>
</div>
<div class="hol-calendar-zone">
<!-- Grille Droite : Jours -->
<div class="hol-calendar-zone" id="calendarZoneContainer" style="cursor: grab; flex: 1; display: flex; overflow: auto; gap: 12px; padding: 4px 4px 10px 4px; margin-top: -4px; align-items: flex-start;">
`;
datesToDisplay.forEach((dateStr) => {
@@ -775,118 +872,254 @@ function openPlanningModal(step) {
month: "short",
});
let unplacedForDay = window.PLANNING_ALL_UNPLACED.filter((it) =>
isDateInStep(dateStr, it.step_start_date, it.step_end_date),
);
let badgeHtml = `<span class="pf-badge" id="badge-${dateStr}" style="display: ${unplacedForDay.length > 0 ? "inline-block" : "none"}; background: var(--danger); color: white; border-radius: 12px; padding: 3px 8px; font-size: 0.75rem; font-weight: bold; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.15); transition: transform 0.2s;" onclick="filterPoolByDate('${dateStr}')">${unplacedForDay.length}</span>`;
html += `
<div class="hol-day-column">
<div class="hol-calendar-day-header">
<div class="hol-cal-weekday">${dayName}</div>
<div class="hol-cal-date">${dayNum}</div>
<div id="plan-weather-${dateStr}" style="margin-top: 5px; display: flex; justify-content: center; min-height: 20px;"></div>
<div class="hol-day-column" id="col-${dateStr}" style="width: 240px; flex-shrink: 0; display: flex; flex-direction: column; background: var(--bg-panel); border: 1px solid var(--border-light); border-radius: 8px;">
<div class="hol-calendar-day-header" style="position: sticky; top: 0; z-index: 20; text-align: center; padding: 12px 12px 8px 12px; background: var(--bg-page); border-bottom: 1px solid var(--border-light); border-radius: 8px 8px 0 0;">
<div style="position: absolute; top: 10px; right: 10px;">${badgeHtml}</div>
<div class="hol-cal-weekday" style="text-transform: uppercase; font-size: 0.75rem; color: var(--text-muted); font-weight: 700; letter-spacing: 0.05em; line-height: 1.4; margin-bottom: 2px;">${dayName}</div>
<div class="hol-cal-date" style="font-size: 1.2rem; font-weight: 800; color: var(--text-main); line-height: 1.2;">${dayNum}</div>
<div id="plan-weather-${dateStr}" style="margin-top: 5px; display: flex; justify-content: center; min-height: 22px;"></div>
</div>
<div class="hol-time-slots-container">
<div class="hol-time-slots-container" style="flex: 1; overflow: visible; padding-top: 12px;">
`;
for (let h = 6; h <= 23; h++) {
let hourStr = h.toString().padStart(2, "0") + ":00";
html += `
<div class="hol-time-slot" data-date="${dateStr}" data-time="${hourStr}"
style="min-height: 40px; border-bottom: 1px dashed var(--border-light); position: relative; padding: 6px; display: flex; flex-direction: column; gap: 4px;"
ondragover="allowDrop(event)" ondragenter="dragEnter(event)" ondragleave="dragLeave(event)"
ondrop="handleDropEvent(event, '${dateStr}', '${hourStr}')"
onclick="handleZoneTap(event, '${dateStr}', '${hourStr}')">
<span class="hol-slot-label">${hourStr}</span>
<span class="hol-slot-label" style="position: absolute; top: -8px; left: 4px; font-size: 0.65rem; color: var(--text-muted); background: var(--bg-panel); padding: 0 4px;">${hourStr}</span>
</div>
`;
}
html += `</div></div>`;
});
html += `</div></div>`;
container.innerHTML = html;
const isMobile = window.innerWidth <= 768;
const dragAttr = isMobile ? "" : 'draggable="true"';
validItems.forEach((it) => {
let icon = "🏷️";
let catClass = "cat-activity";
if (it.category === "accommodation") {
icon = "🏨";
catClass = "cat-accommodation";
}
if (it.category === "transport") {
icon = "🚗";
catClass = "cat-transport";
}
const dur = it.duration || 1;
const noteHtml = it.notes
? `<div class="hol-drag-note">${it.notes}</div>`
: "";
const isVirtual = it.is_virtual === true;
const isTransit = it.expense_context === "transit";
const durControls =
isVirtual || isTransit
? `<span class="hol-dur-text" style="background:#e2e8f0; padding:2px 6px; border-radius:4px; font-weight:bold;">${dur}h (Auto)</span>`
: `<button class="hol-dur-btn" onclick="changeDuration(event, '${it.id}', -1)">-</button>
<span class="hol-dur-text" id="dur-text-${it.id}">${dur}h</span>
<button class="hol-dur-btn" onclick="changeDuration(event, '${it.id}', 1)">+</button>`;
const bgStyle = isVirtual
? "background: repeating-linear-gradient(45deg, #ffffff, #ffffff 10px, #f8fafc 10px, #f8fafc 20px); border: 2px dashed var(--primary);"
: "";
const visualName = isTransit
? `🛣️ Trajet & ` + it.name
: `${icon} ${it.name}`;
const elHtml = `
<div class="hol-drag-item ${catClass}" ${dragAttr}
id="drag-item-${it.id}" data-id="${it.id}"
style="--duration: ${dur}; ${bgStyle}"
ondragstart="dragStart(event)" onclick="handleItemTap(event, '${it.id}')">
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 5px;">
<div class="hol-drag-title" style="flex:1;">${visualName}</div>
<div class="hol-item-duration-controls">
${durControls}
</div>
</div>
${noteHtml}
</div>
`;
if (it.item_date && it.item_time && datesToDisplay.includes(it.item_date)) {
// 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", elHtml);
return;
}
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("unmapped-pool")
.insertAdjacentHTML("beforeend", elHtml);
});
document.getElementById("planningModal").style.display = "flex";
document.body.classList.add("no-scroll");
datesToDisplay.forEach((dateStr) => {
loadWeatherForPlanning(step.lat, step.lng, dateStr);
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 = `<div style="text-align:center; margin-top:30px; color:var(--text-muted);"><span style="font-size:2rem;">🎉</span><br><br>Rien à placer${dateStr ? " pour cette journée" : ""}.</div>`;
} 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
? `<div class="hol-drag-note" style="font-size:0.7rem; color:var(--text-muted); line-height:1.2; margin-top:4px;">${it.notes}</div>`
: "";
const isVirtual = it.is_virtual === true;
const isTransit = it.expense_context === "transit";
const durControls = `<button class="hol-dur-btn" style="border:none;background:transparent;cursor:pointer;font-weight:bold;padding:0 4px;" onclick="changeDuration(event, '${it.id}', -1)">-</button>
<span class="hol-dur-text" id="dur-text-${it.id}" style="font-size:0.75rem;font-weight:bold;">${dur}h</span>
<button class="hol-dur-btn" style="border:none;background:transparent;cursor:pointer;font-weight:bold;padding:0 4px;" onclick="changeDuration(event, '${it.id}', 1)">+</button>`;
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
? `<div style="font-size:0.65rem; color:var(--primary); font-weight:800; margin-bottom:4px; text-transform:uppercase;">📍 ${it.step_location}</div>`
: "";
// Suppression du calcul de hauteur en ligne, c'est désormais géré via CSS et la variable "--duration"
return `
<div class="hol-drag-item ${catClass}" ${dragAttr}
id="${htmlId}" data-id="${it.id}" data-virtual="${isVirtual}" data-sort="${it.sort_order}"
style="--duration: ${dur}; flex-shrink: 0; ${bgStyle} padding: 8px 10px; border-radius: 6px; cursor: grab; box-shadow: 0 2px 4px rgba(0,0,0,0.05); transition: transform 0.2s; z-index: 10;"
ondragstart="dragStart(event)" ondragend="dragEnd(event)" onclick="handleItemTap(event, '${htmlId}')">
${locHint}
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 8px;">
<div class="hol-drag-title" style="flex:1; font-size: 0.85rem; font-weight: 700; color: var(--text-main); line-height:1.2;">${visualName}</div>
<div class="hol-item-duration-controls" style="display:flex; align-items:center; background:var(--bg-subtle); border-radius:4px; border:1px solid var(--border-light);">
${durControls}
</div>
</div>
${noteHtml}
</div>
`;
}
function changeDuration(e, itemId, delta) {
e.stopPropagation();
const itemEl = document.getElementById("drag-item-" + itemId);
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);
@@ -897,24 +1130,32 @@ function changeDuration(e, itemId, delta) {
});
}
function handleItemTap(e, itemId) {
function handleItemTap(e, htmlId) {
e.stopPropagation();
document
.querySelectorAll(".hol-drag-item")
.forEach((el) => el.classList.remove("selected-for-move"));
if (selectedItemIdForMove === itemId) {
if (selectedItemIdForMove === htmlId) {
selectedItemIdForMove = null;
} else {
selectedItemIdForMove = itemId;
document
.getElementById("drag-item-" + itemId)
.classList.add("selected-for-move");
selectedItemIdForMove = htmlId;
const el = document.getElementById(htmlId);
if (el) el.classList.add("selected-for-move");
}
}
function handleZoneTap(e, dateStr, timeStr) {
if (selectedItemIdForMove) {
handleDropLogic(selectedItemIdForMove, dateStr, timeStr, e.currentTarget);
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")
@@ -925,62 +1166,178 @@ function handleZoneTap(e, dateStr, timeStr) {
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 s = e.target.closest(".hol-time-slot");
if (s) s.classList.add("drag-over");
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) {
let s = e.target.closest(".hol-time-slot");
if (s) s.classList.remove("drag-over");
// 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();
let slot = e.target.closest(".hol-time-slot");
if (slot) slot.classList.remove("drag-over");
document
.querySelectorAll(".hol-time-slot")
.forEach((s) => s.classList.remove("drag-over-duration"));
const idStr = e.dataTransfer.getData("text/plain");
const itemId = idStr.replace("drag-item-", "");
const dropZone = slot || document.getElementById("unmapped-pool");
handleDropLogic(itemId, dateStr, timeStr, dropZone);
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(itemId, dateStr, timeStr, dropZone) {
if (itemId === "virtual-transit") {
const step = window.CURRENT_PLANNING_STEP;
const tData = window.TRANSIT_DATA[step.sort_order];
const holidayId = document.querySelector('input[name="holiday_id"]').value;
const h = Math.max(1, Math.round(tData.sec / 3600));
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", step.sort_order);
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", h);
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,
}).then(() => window.location.reload());
return;
});
}
recalcAllBadges();
}
function updateItemMemory(itemId, changes) {
MAP_POINTS.forEach((step) => {
let item = step.items.find((i) => i.id == itemId);
if (item) Object.assign(item, 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);
});
}
}
// ============================================================================
@@ -1377,3 +1734,47 @@ window.generateTravelBook = function () {
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;
}
}
@@ -7,11 +7,20 @@ require_login();
// ---------------------------------------------------------------------------
// INTERCEPTIONS AJAX (Exécution ultra rapide sans rechargement lourd)
// ---------------------------------------------------------------------------
if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime', 'update_item_duration', 'add_single_item'])) {
if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime', 'update_item_duration', 'add_single_item', 'update_holiday_note'])) {
header('Content-Type: application/json');
session_write_close(); // 🚀 LIBÈRE LA SESSION : Permet d'autres requêtes simultanées sans bloquer le navigateur
session_write_close();
try {
if ($_POST['action'] === 'update_holiday_note') {
$hId = (int)$_POST['holiday_id'];
$notes = $_POST['notes'] ?? '';
$stmt = $pdo->prepare("UPDATE pf_holidays SET notes = ? WHERE id = ?");
$stmt->execute([$notes, $hId]);
echo json_encode(['success' => true]);
exit;
}
if ($_POST['action'] === 'add_single_item') {
$holiday_id = (int)$_POST['holiday_id'];
$sort_order = (int)$_POST['sort_order'];
@@ -25,16 +34,28 @@ if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime
$date = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
$time = !empty($_POST['item_time']) ? $_POST['item_time'] : null;
$context = $_POST['expense_context'] ?? ($_POST['context'] ?? 'local');
$pdo->beginTransaction();
if ($context === 'transit') {
$stmtDel = $pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ? AND expense_context = 'transit'");
$stmtDel->execute([$holiday_id, $sort_order]);
}
$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, item_date, item_time) 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'], $dur, $date, $time
$stepInfo['step_type'], $context, $dur, $date, $time
]);
$newId = $pdo->lastInsertId();
$pdo->commit();
echo json_encode(['success' => true]);
echo json_encode(['success' => true, 'id' => $newId]);
exit;
}
echo json_encode(['success' => false, 'error' => 'Etape introuvable']);
@@ -61,6 +82,18 @@ if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime
echo json_encode(['success' => true]);
exit;
}
// Mise à jour de la note globale du voyage
if ($_POST['action'] === 'update_holiday_note') {
$hId = (int)$_POST['holiday_id'];
$notes = $_POST['notes'] ?? '';
$stmt = $pdo->prepare("UPDATE pf_holidays SET notes = ? WHERE id = ?");
$stmt->execute([$notes, $hId]);
echo json_encode(['success' => true]);
exit;
}
} catch (Exception $e) {
if ($pdo->inTransaction()) { $pdo->rollBack(); }
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
+41 -73
View File
@@ -16,7 +16,8 @@ $stmt = $pdo->prepare("
(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,
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND expense_context = 'transit') as total_transit
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND (expense_context = 'transit' OR (category = 'transport' AND (name LIKE '%Essence%' OR name LIKE '%Carburant%')))) as total_fuel,
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND category = 'transport' AND (name LIKE '%Péage%' OR name LIKE '%Peage%')) as total_tolls
FROM pf_holidays h
LEFT JOIN pf_vehicles v ON h.vehicle_id = v.id
WHERE h.id = ?
@@ -108,6 +109,10 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
📖 Carnet de Voyage
</button>
<button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="openGlobalPlanningModal()" style="display: flex; align-items: center; gap: 6px; margin-right: 10px;">
📅 Planning Global
</button>
<button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="editHoliday(JSON.parse(document.getElementById('holidayDataJson').textContent))">
<?= tr('btn_edit_bases') ?>
</button>
@@ -127,18 +132,30 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<div class="hol-summary-item">
<div class="hol-summary-label">
Frais de route (Essence/Péages)
Frais de route
</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>
<div class="hol-summary-value" style="display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
<!-- Bloc Péages -->
<span style="display:flex; align-items:center; gap:4px;" title="Péages">
<span style="font-size: 1.1rem;">💳</span>
<strong><?= number_format($holiday['total_tolls'], 0) ?> €</strong>
</span>
<!-- Bloc Essence -->
<span style="display:flex; align-items:center; gap:4px;" title="Carburant estimé et manuel">
<span style="font-size: 1.1rem;">⛽</span>
<strong><span id="global_fuel_cost"><?= number_format($holiday['total_fuel'], 0) ?></span> €</strong>
</span>
<!-- Paramètres prix essence -->
<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="openTransitModal()" 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 des trajets">
<!-- Bouton Détails (Oeil) -->
<?php if ($holiday['total_fuel'] > 0 || $holiday['total_tolls'] > 0): ?>
<span onclick="openTransitModal()" style="font-size: 1rem; cursor: pointer; opacity: 0.5; transition: opacity 0.2s; display: inline-flex; align-items: center;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.5'" title="Voir le détail des trajets">
👁️
</span>
<?php endif; ?>
@@ -201,59 +218,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<p style="color:var(--text-muted); font-style:italic; text-align:center; margin-top:40px;"><?= tr('hdl_no_steps') ?></p>
<?php else: ?>
<?php if (!empty($generalItems) || $holiday['budget_food'] > 0 || $holiday['budget_extra'] > 0): ?>
<div class="hol-checkpoint" style="border-left-color: #64748b; background: #f8fafc; margin-bottom: 20px;">
<div class="hol-cp-header">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 1.2rem;">🌍</span>
<strong style="color: #0f172a;"><?= tr('hdl_general_costs') ?></strong>
</div>
<?php
$generalTotal = $holiday['budget_food'] + $holiday['budget_extra'];
foreach ($generalItems as $gi) { $generalTotal += $gi['amount']; }
?>
<div style="display: flex; align-items: center; gap: 12px;">
<div style="font-size: 1.1rem; font-weight: 800; color: var(--primary); white-space: nowrap;"><?= number_format($generalTotal, 2, ',', ' ') ?> €</div>
<button onclick='editHoliday(JSON.parse(document.getElementById("holidayDataJson").textContent))' class="btn-icon-small" title="<?= tr('btn_edit') ?>">⚙️</button>
</div>
</div>
<div class="hol-cp-body">
<?php if ($holiday['budget_food'] > 0): ?>
<div class="hol-expense-wrapper"><div class="hol-expense-main">
<span class="hol-expense-name" style="color:#64748b;">🍔 <?= tr('hdl_food_bev') ?></span>
<span><strong class="hol-expense-amount"><?= number_format($holiday['budget_food'], 2, ',', ' ') ?> €</strong><span class="status-pending">⏳</span></span>
</div></div>
<?php endif; ?>
<?php if ($holiday['budget_extra'] > 0): ?>
<div class="hol-expense-wrapper"><div class="hol-expense-main">
<span class="hol-expense-name" style="color:#64748b;">🎁 <?= tr('hdl_extras') ?></span>
<span><strong class="hol-expense-amount"><?= number_format($holiday['budget_extra'], 2, ',', ' ') ?> €</strong><span class="status-pending">⏳</span></span>
</div></div>
<?php endif; ?>
<?php foreach ($generalItems as $it):
$icon = match($it['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
?>
<div class="hol-expense-wrapper">
<div class="hol-expense-main">
<span class="hol-expense-name">
<?= $icon ?> <?= htmlspecialchars($it['name']) ?>
</span>
<span class="hol-expense-price-group">
<strong class="hol-expense-amount"><?= number_format($it['amount'], 2, ',', ' ') ?> €</strong>
<span class="<?= $it['is_paid'] ? 'status-paid' : 'status-pending' ?>">
<?= $it['is_paid'] ? '✓' : '⏳' ?>
</span>
</span>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php foreach ($steps as $step): ?>
<div id="step-card-<?= $step['sort_order'] ?>" class="hol-checkpoint hol-checkpoint-draggable" draggable="true" data-location="<?= htmlspecialchars($step['location_name']) ?>">
@@ -312,7 +277,6 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
</button>
<button onclick="openDocsModal(<?= $step['sort_order'] ?>)" class="btn-icon-small" title="Documents & Billets" style="width:26px!important; height:26px!important; font-size:0.8rem; min-width:26px!important; min-height:26px!important;">📎</button>
<button onclick='openPlanningModal(<?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('hdl_view_planning') ?>" style="width:26px!important; height:26px!important; font-size:0.8rem; min-width:26px!important; min-height:26px!important;">📅</button>
<button onclick='openCheckpointModal("edit", <?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('btn_edit') ?>" style="width:26px!important; height:26px!important; font-size:0.8rem; min-width:26px!important; min-height:26px!important;">✏️</button>
</div>
</div>
@@ -349,16 +313,17 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
</div>
</div>
<?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;">
📝 <?= tr('hdl_label_notes') ?>
</h3>
<div style="font-size: 0.95rem; color: #334155; white-space: pre-wrap; line-height: 1.6;">
<?= htmlspecialchars($holiday['notes']) ?>
<div class="pf-card" style="margin-top: 24px;">
<h2 class="pf-card-title">📝 <?= tr('hdl_label_notes') ?></h2>
<div class="pf-card-body">
<textarea id="holidayGlobalNotes" class="pf-input" rows="6" placeholder="<?= tr('hdl_ph_notes') ?>" style="resize: vertical; width: 100%; line-height: 1.5; padding: 12px;"><?= htmlspecialchars($holiday['notes'] ?? '') ?></textarea>
<div style="text-align: right; margin-top: 12px;">
<button id="btnSaveHolidayNote" class="pf-btn" onclick="saveHolidayGlobalNote(<?= (int)$_GET['id'] ?>)">
💾 <?= tr('btn_save') ?>
</button>
</div>
</div>
<?php endif; ?>
</div>
</div>
@@ -454,13 +419,16 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
</div>
<div id="planningModal" class="pf-modal">
<div class="pf-modal-content" style="max-width: 550px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 id="planningModalTitle" style="margin:0; color:var(--primary);">📅 <?= tr('hdl_planning_title') ?></h3>
<div class="pf-modal-content" style="max-width: 95vw; width: 1400px; height: 90vh; display: flex; flex-direction: column; padding: 20px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; flex-shrink: 0; padding-bottom: 10px; border-bottom: 1px solid var(--border-light);">
<h3 id="planningModalTitle" style="margin:0; color:var(--primary);">📅 Planning Global</h3>
<button type="button" onclick="closePlanningModal()" class="pf-modal-close">&times;</button>
</div>
<div id="planningContainer" style="width: 100%;"></div>
<div class="modal-footer">
<!-- Conteneur Injecté en JS -->
<div id="planningContainer" style="flex: 1; overflow: hidden; display: flex;"></div>
<div class="modal-footer" style="flex-shrink: 0; padding-top: 15px; margin-top: 15px; border-top: 1px solid var(--border-light);">
<button type="button" onclick="closePlanningModal()" class="pf-btn btn-secondary"><?= tr('btn_close') ?></button>
</div>
</div>
-4
View File
@@ -62,10 +62,6 @@
</div>
</div>
<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>
<div class="modal-footer">
<button type="button" onclick="deleteHoliday()" id="btn_delete" class="pf-btn btn-secondary hol-btn-delete"><?= tr('btn_delete') ?></button>