@@ -1196,7 +1196,7 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
|
|||||||
top: 1px;
|
top: 1px;
|
||||||
left: 45px;
|
left: 45px;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
height: calc(75px * var(--duration) - 2px);
|
height: calc(60px * var(--duration) - 2px);
|
||||||
}
|
}
|
||||||
/* Dans la boîte d'attente : Position normale */
|
/* Dans la boîte d'attente : Position normale */
|
||||||
.hol-unmapped-zone .hol-drag-item {
|
.hol-unmapped-zone .hol-drag-item {
|
||||||
@@ -1253,7 +1253,7 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hol-time-slot {
|
.hol-time-slot {
|
||||||
height: 75px;
|
height: 60px;
|
||||||
border-bottom: 1px solid var(--border-light);
|
border-bottom: 1px solid var(--border-light);
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
|
|||||||
+513
-112
@@ -257,9 +257,9 @@ function initDetailMap() {
|
|||||||
Promise.all(routePromises).then((results) => {
|
Promise.all(routePromises).then((results) => {
|
||||||
results.sort((a, b) => a.index - b.index);
|
results.sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
// 🔥 NOUVEAU : Compteurs et HTML de la modale
|
|
||||||
let totalTripDistance = 0;
|
let totalTripDistance = 0;
|
||||||
let totalTripDuration = 0; // en secondes
|
let totalTripDuration = 0; // en secondes
|
||||||
|
let totalFuelCost = 0; // Coût exact accumulé
|
||||||
let transitDetailsHtml = "";
|
let transitDetailsHtml = "";
|
||||||
|
|
||||||
let returnStartIndex = latlngs.length - 2;
|
let returnStartIndex = latlngs.length - 2;
|
||||||
@@ -310,6 +310,8 @@ function initDetailMap() {
|
|||||||
const fuelPrice = window.FUEL_PRICE || 1.85;
|
const fuelPrice = window.FUEL_PRICE || 1.85;
|
||||||
const cost = (distanceKm / 100) * fuelL100 * fuelPrice;
|
const cost = (distanceKm / 100) * fuelL100 * fuelPrice;
|
||||||
|
|
||||||
|
totalFuelCost += cost;
|
||||||
|
|
||||||
// 1. ON DÉCLARE LES POINTS D'ABORD
|
// 1. ON DÉCLARE LES POINTS D'ABORD
|
||||||
const startPt = MAP_POINTS[res.index];
|
const startPt = MAP_POINTS[res.index];
|
||||||
const endPt = MAP_POINTS[res.index + 1];
|
const endPt = MAP_POINTS[res.index + 1];
|
||||||
@@ -322,14 +324,41 @@ function initDetailMap() {
|
|||||||
cost: cost,
|
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
|
// 🔥 CONSTRUCTION DU CONTENU DE LA MODALE
|
||||||
transitDetailsHtml += `
|
transitDetailsHtml += `
|
||||||
<div style="padding: 12px 0; border-bottom: 1px dashed var(--border-light);">
|
<div style="padding: 12px 0; border-bottom: 1px dashed var(--border-light); display: flex; justify-content: space-between; align-items: flex-end;">
|
||||||
<div style="font-weight: 600; font-size: 0.9rem; color: var(--text-main); margin-bottom: 4px;">
|
<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}
|
📍 ${startPt.location_name} ➔ ${endPt.location_name}
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size: 0.8rem; color: var(--text-muted); display: flex; justify-content: space-between; align-items: center;">
|
<div style="font-size: 0.8rem; color: var(--text-muted);">
|
||||||
<span>🚗 ${Math.round(distanceKm)} km • ⏱️ ${formatDuration(durationSec)}</span>
|
🚗 ${Math.round(distanceKm)} km • ⏱️ ${formatDuration(durationSec)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
<strong style="color: var(--primary);">⛽ ${cost.toFixed(2)} €</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -370,6 +399,7 @@ function initDetailMap() {
|
|||||||
const distEl = document.getElementById("global_total_distance");
|
const distEl = document.getElementById("global_total_distance");
|
||||||
const timeEl = document.getElementById("global_total_duration");
|
const timeEl = document.getElementById("global_total_duration");
|
||||||
const distBlock = document.getElementById("block_total_distance");
|
const distBlock = document.getElementById("block_total_distance");
|
||||||
|
const globalFuelCostEl = document.getElementById("global_fuel_cost");
|
||||||
|
|
||||||
if (distEl && timeEl && distBlock) {
|
if (distEl && timeEl && distBlock) {
|
||||||
distEl.innerText = Math.round(totalTripDistance);
|
distEl.innerText = Math.round(totalTripDistance);
|
||||||
@@ -377,6 +407,10 @@ function initDetailMap() {
|
|||||||
distBlock.style.display = "block";
|
distBlock.style.display = "block";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (globalFuelCostEl) {
|
||||||
|
globalFuelCostEl.innerText = Math.round(totalFuelCost);
|
||||||
|
}
|
||||||
|
|
||||||
// 🔥 INJECTION DU HTML DANS LA MODALE
|
// 🔥 INJECTION DU HTML DANS LA MODALE
|
||||||
const modalContainer = document.getElementById("transitDetailsContainer");
|
const modalContainer = document.getElementById("transitDetailsContainer");
|
||||||
if (modalContainer) {
|
if (modalContainer) {
|
||||||
@@ -573,10 +607,7 @@ function addCpExpenseLine(
|
|||||||
<option value="transport" ${category === "transport" ? "selected" : ""}>🚗</option>
|
<option value="transport" ${category === "transport" ? "selected" : ""}>🚗</option>
|
||||||
<option value="activity" ${category === "activity" ? "selected" : ""}>🎫</option>
|
<option value="activity" ${category === "activity" ? "selected" : ""}>🎫</option>
|
||||||
</select>
|
</select>
|
||||||
<select name="items[context][]" class="pf-input hol-form-select" style="width:auto; margin-left:5px; font-size:0.75rem;">
|
<input type="hidden" name="items[context][]" value="local">
|
||||||
<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="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}">
|
<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() {
|
function closePlanningModal() {
|
||||||
document.getElementById("planningModal").style.display = "none";
|
document.getElementById("planningModal").style.display = "none";
|
||||||
document.body.classList.remove("no-scroll");
|
document.body.classList.remove("no-scroll");
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPlanningModal(step) {
|
function openGlobalPlanningModal() {
|
||||||
|
const holidayDataJsonEl = document.getElementById("holidayDataJson");
|
||||||
|
if (!holidayDataJsonEl) return;
|
||||||
|
|
||||||
|
const holidayData = JSON.parse(holidayDataJsonEl.textContent).main;
|
||||||
|
|
||||||
|
if (!holidayData.start_date || !holidayData.end_date) {
|
||||||
|
alert(
|
||||||
|
"Veuillez d'abord définir les dates globales du voyage dans 'Modifier les bases' ⚙️",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById("planningModalTitle").innerText =
|
document.getElementById("planningModalTitle").innerText =
|
||||||
tr("hdl_planning_title") + " : " + step.location_name;
|
"📅 Planning Global : " + holidayData.title;
|
||||||
const container = document.getElementById("planningContainer");
|
const container = document.getElementById("planningContainer");
|
||||||
|
|
||||||
selectedItemIdForMove = null;
|
selectedItemIdForMove = null;
|
||||||
|
let allPlaced = [];
|
||||||
|
window.PLANNING_ALL_UNPLACED = [];
|
||||||
|
window.PLANNING_ITEM_MAP = {};
|
||||||
|
|
||||||
let validItems = step.items.filter((it) => it.name !== "PF_TECHNICAL_POINT");
|
// 1. Collecte de TOUS les éléments
|
||||||
|
window.MAP_POINTS.forEach((step) => {
|
||||||
window.CURRENT_PLANNING_STEP = step; // Mémorise l'étape en cours
|
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]) {
|
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(
|
||||||
let hasTransit = validItems.some((it) => it.expense_context === "transit");
|
(it) => it.expense_context === "transit",
|
||||||
|
);
|
||||||
if (!hasTransit) {
|
if (!hasTransit) {
|
||||||
const tData = window.TRANSIT_DATA[step.sort_order];
|
const tData = window.TRANSIT_DATA[step.sort_order];
|
||||||
const h = Math.max(1, Math.round(tData.sec / 3600)); // Arrondi en heures
|
const h = Math.max(1, Math.round(tData.sec / 3600));
|
||||||
validItems.push({
|
validItems.push({
|
||||||
id: "virtual-transit",
|
id: "virtual-transit-" + step.sort_order,
|
||||||
name: `Essence depuis ${tData.from}`, // Garde ce nom pour lier avec le budget
|
sort_order: step.sort_order,
|
||||||
|
name: `Essence depuis ${tData.from}`,
|
||||||
category: "transport",
|
category: "transport",
|
||||||
expense_context: "transit",
|
expense_context: "transit",
|
||||||
duration: h,
|
duration: h,
|
||||||
notes: `Trajet GPS (${Math.round(tData.sec / 60)} min). Déplacez pour planifier la route.`,
|
notes: `Trajet GPS (~${Math.round(tData.sec / 60)} min).`,
|
||||||
is_virtual: true,
|
is_virtual: true,
|
||||||
|
amount: tData.cost,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!step.step_start_date || !step.step_end_date) {
|
validItems.forEach((it) => {
|
||||||
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>`;
|
it.step_start_date = step.step_start_date;
|
||||||
document.getElementById("planningModal").style.display = "flex";
|
it.step_end_date = step.step_end_date;
|
||||||
return;
|
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 datesToDisplay = [];
|
||||||
let curr = new Date(step.step_start_date);
|
let curr = new Date(holidayData.start_date);
|
||||||
let end = new Date(step.step_end_date);
|
let endD = new Date(holidayData.end_date);
|
||||||
while (curr <= end) {
|
while (curr <= endD) {
|
||||||
datesToDisplay.push(curr.toISOString().split("T")[0]);
|
datesToDisplay.push(curr.toISOString().split("T")[0]);
|
||||||
curr.setDate(curr.getDate() + 1);
|
curr.setDate(curr.getDate() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Construction de l'interface (Avec règles CSS injectées)
|
||||||
let html = `
|
let html = `
|
||||||
<div class="hol-planning-layout">
|
<style>
|
||||||
<div class="hol-unmapped-zone" id="unmapped-pool"
|
/* 🌟 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, '', '')"
|
ondragover="allowDrop(event)" ondrop="handleDropEvent(event, '', '')"
|
||||||
onclick="handleZoneTap(event, '', '')">
|
onclick="handleZoneTap(event, '', '')">
|
||||||
<div class="hol-unmapped-title" style="width:100%;">📥 ${tr("hdl_to_place")}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="hol-calendar-zone">
|
</div>
|
||||||
|
|
||||||
|
<!-- 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) => {
|
datesToDisplay.forEach((dateStr) => {
|
||||||
@@ -775,36 +872,177 @@ function openPlanningModal(step) {
|
|||||||
month: "short",
|
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 += `
|
html += `
|
||||||
<div class="hol-day-column">
|
<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">
|
<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 class="hol-cal-weekday">${dayName}</div>
|
<div style="position: absolute; top: 10px; right: 10px;">${badgeHtml}</div>
|
||||||
<div class="hol-cal-date">${dayNum}</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 id="plan-weather-${dateStr}" style="margin-top: 5px; display: flex; justify-content: center; min-height: 20px;"></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>
|
||||||
<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++) {
|
for (let h = 6; h <= 23; h++) {
|
||||||
let hourStr = h.toString().padStart(2, "0") + ":00";
|
let hourStr = h.toString().padStart(2, "0") + ":00";
|
||||||
html += `
|
html += `
|
||||||
<div class="hol-time-slot" data-date="${dateStr}" data-time="${hourStr}"
|
<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)"
|
ondragover="allowDrop(event)" ondragenter="dragEnter(event)" ondragleave="dragLeave(event)"
|
||||||
ondrop="handleDropEvent(event, '${dateStr}', '${hourStr}')"
|
ondrop="handleDropEvent(event, '${dateStr}', '${hourStr}')"
|
||||||
onclick="handleZoneTap(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>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
html += `</div></div>`;
|
html += `</div></div>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
html += `</div></div>`;
|
html += `</div></div>`;
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
|
|
||||||
const isMobile = window.innerWidth <= 768;
|
// 4. Placement des éléments
|
||||||
const dragAttr = isMobile ? "" : 'draggable="true"';
|
allPlaced.forEach((it) => {
|
||||||
|
if (datesToDisplay.includes(it.item_date)) {
|
||||||
|
const hourPrefix = it.item_time.substring(0, 2) + ":00";
|
||||||
|
const targetSlot = container.querySelector(
|
||||||
|
`.hol-time-slot[data-date="${it.item_date}"][data-time="${hourPrefix}"]`,
|
||||||
|
);
|
||||||
|
if (targetSlot)
|
||||||
|
targetSlot.insertAdjacentHTML("beforeend", buildDragItemHtml(it));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
validItems.forEach((it) => {
|
// 5. Météo et Focus auto
|
||||||
|
datesToDisplay.forEach((dateStr) => {
|
||||||
|
let activeStep = window.MAP_POINTS.find((step) =>
|
||||||
|
isDateInStep(dateStr, step.step_start_date, step.step_end_date),
|
||||||
|
);
|
||||||
|
if (activeStep && activeStep.lat && activeStep.lng) {
|
||||||
|
loadWeatherForPlanning(activeStep.lat, activeStep.lng, dateStr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("planningModal").style.display = "flex";
|
||||||
|
document.body.classList.add("no-scroll");
|
||||||
|
|
||||||
|
let todayStr = new Date().toISOString().split("T")[0];
|
||||||
|
let defaultFocusDate =
|
||||||
|
todayStr >= holidayData.start_date && todayStr <= holidayData.end_date
|
||||||
|
? todayStr
|
||||||
|
: holidayData.start_date;
|
||||||
|
|
||||||
|
filterPoolByDate(defaultFocusDate);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const activeCol = document.getElementById("col-" + defaultFocusDate);
|
||||||
|
if (activeCol) {
|
||||||
|
document.getElementById("calendarZoneContainer").scrollTo({
|
||||||
|
left: activeCol.offsetLeft - 300,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
// Moteur Scroll Natif (Drag To Scroll)
|
||||||
|
const slider = document.getElementById("calendarZoneContainer");
|
||||||
|
let isDown = false;
|
||||||
|
let startX, startY, scrollLeft, scrollTop;
|
||||||
|
|
||||||
|
slider.addEventListener("mousedown", (e) => {
|
||||||
|
if (e.target.closest(".hol-drag-item") || e.target.closest("button"))
|
||||||
|
return;
|
||||||
|
isDown = true;
|
||||||
|
slider.style.cursor = "grabbing";
|
||||||
|
startX = e.pageX - slider.offsetLeft;
|
||||||
|
startY = e.pageY - slider.offsetTop;
|
||||||
|
scrollLeft = slider.scrollLeft;
|
||||||
|
scrollTop = slider.scrollTop;
|
||||||
|
});
|
||||||
|
slider.addEventListener("mouseleave", () => {
|
||||||
|
isDown = false;
|
||||||
|
slider.style.cursor = "grab";
|
||||||
|
});
|
||||||
|
slider.addEventListener("mouseup", () => {
|
||||||
|
isDown = false;
|
||||||
|
slider.style.cursor = "grab";
|
||||||
|
});
|
||||||
|
slider.addEventListener("mousemove", (e) => {
|
||||||
|
if (!isDown) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const x = e.pageX - slider.offsetLeft;
|
||||||
|
const y = e.pageY - slider.offsetTop;
|
||||||
|
const walkX = (x - startX) * 1.5;
|
||||||
|
const walkY = (y - startY) * 1.5;
|
||||||
|
slider.scrollLeft = scrollLeft - walkX;
|
||||||
|
slider.scrollTop = scrollTop - walkY;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDateInStep(targetDate, stepStart, stepEnd) {
|
||||||
|
if (!stepStart || !stepEnd) return false;
|
||||||
|
return targetDate >= stepStart && targetDate <= stepEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterPoolByDate(dateStr) {
|
||||||
|
window.CURRENT_PLANNING_FILTER_DATE = dateStr;
|
||||||
|
const pool = document.getElementById("unmapped-pool");
|
||||||
|
pool.innerHTML = "";
|
||||||
|
|
||||||
|
let poolItemsHtml = "";
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
// Itération sur la MAP, donc l'ordre natif de tri (sort_order/ID) est préservé !
|
||||||
|
Object.values(window.PLANNING_ITEM_MAP).forEach((it) => {
|
||||||
|
const htmlId = it.is_virtual ? it.id : `drag-item-${it.id}`;
|
||||||
|
const isPlaced = document.querySelector(`.hol-time-slot #${htmlId}`);
|
||||||
|
|
||||||
|
if (!isPlaced) {
|
||||||
|
if (
|
||||||
|
!dateStr ||
|
||||||
|
isDateInStep(dateStr, it.step_start_date, it.step_end_date)
|
||||||
|
) {
|
||||||
|
poolItemsHtml += buildDragItemHtml(it);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (count === 0) {
|
||||||
|
pool.innerHTML = `<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 icon = "🏷️";
|
||||||
let catClass = "cat-activity";
|
let catClass = "cat-activity";
|
||||||
if (it.category === "accommodation") {
|
if (it.category === "accommodation") {
|
||||||
@@ -818,75 +1056,70 @@ function openPlanningModal(step) {
|
|||||||
|
|
||||||
const dur = it.duration || 1;
|
const dur = it.duration || 1;
|
||||||
const noteHtml = it.notes
|
const noteHtml = it.notes
|
||||||
? `<div class="hol-drag-note">${it.notes}</div>`
|
? `<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 isVirtual = it.is_virtual === true;
|
||||||
const isTransit = it.expense_context === "transit";
|
const isTransit = it.expense_context === "transit";
|
||||||
|
|
||||||
const durControls =
|
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
|
isVirtual || isTransit
|
||||||
? `<span class="hol-dur-text" style="background:#e2e8f0; padding:2px 6px; border-radius:4px; font-weight:bold;">${dur}h (Auto)</span>`
|
? "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);"
|
||||||
: `<button class="hol-dur-btn" onclick="changeDuration(event, '${it.id}', -1)">-</button>
|
: "background: var(--bg-panel); border: 1px solid var(--border-strong);";
|
||||||
<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
|
const visualName = isTransit ? `🛣️ Trajet & Essence` : `${icon} ${it.name}`;
|
||||||
? "background: repeating-linear-gradient(45deg, #ffffff, #ffffff 10px, #f8fafc 10px, #f8fafc 20px); border: 2px dashed var(--primary);"
|
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>`
|
||||||
: "";
|
: "";
|
||||||
const visualName = isTransit
|
|
||||||
? `🛣️ Trajet & ` + it.name
|
|
||||||
: `${icon} ${it.name}`;
|
|
||||||
|
|
||||||
const elHtml = `
|
// 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}
|
<div class="hol-drag-item ${catClass}" ${dragAttr}
|
||||||
id="drag-item-${it.id}" data-id="${it.id}"
|
id="${htmlId}" data-id="${it.id}" data-virtual="${isVirtual}" data-sort="${it.sort_order}"
|
||||||
style="--duration: ${dur}; ${bgStyle}"
|
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)" onclick="handleItemTap(event, '${it.id}')">
|
ondragstart="dragStart(event)" ondragend="dragEnd(event)" onclick="handleItemTap(event, '${htmlId}')">
|
||||||
|
${locHint}
|
||||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 5px;">
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 8px;">
|
||||||
<div class="hol-drag-title" style="flex:1;">${visualName}</div>
|
<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">
|
<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}
|
${durControls}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${noteHtml}
|
${noteHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (it.item_date && it.item_time && datesToDisplay.includes(it.item_date)) {
|
|
||||||
const hourPrefix = it.item_time.substring(0, 2) + ":00";
|
|
||||||
const targetSlot = container.querySelector(
|
|
||||||
`.hol-time-slot[data-date="${it.item_date}"][data-time="${hourPrefix}"]`,
|
|
||||||
);
|
|
||||||
if (targetSlot) {
|
|
||||||
targetSlot.insertAdjacentHTML("beforeend", elHtml);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document
|
|
||||||
.getElementById("unmapped-pool")
|
|
||||||
.insertAdjacentHTML("beforeend", elHtml);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("planningModal").style.display = "flex";
|
|
||||||
document.body.classList.add("no-scroll");
|
|
||||||
|
|
||||||
datesToDisplay.forEach((dateStr) => {
|
|
||||||
loadWeatherForPlanning(step.lat, step.lng, dateStr);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeDuration(e, itemId, delta) {
|
function changeDuration(e, itemId, delta) {
|
||||||
e.stopPropagation();
|
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 currentDur = parseInt(itemEl.style.getPropertyValue("--duration")) || 1;
|
||||||
let newDur = currentDur + delta;
|
let newDur = currentDur + delta;
|
||||||
if (newDur < 1) newDur = 1;
|
if (newDur < 1) newDur = 1;
|
||||||
if (newDur > 12) newDur = 12;
|
if (newDur > 12) newDur = 12;
|
||||||
|
|
||||||
itemEl.style.setProperty("--duration", newDur);
|
itemEl.style.setProperty("--duration", newDur);
|
||||||
document.getElementById(`dur-text-${itemId}`).innerText = newDur + "h";
|
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 });
|
updateItemMemory(itemId, { duration: newDur });
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("action", "update_item_duration");
|
formData.append("action", "update_item_duration");
|
||||||
formData.append("item_id", itemId);
|
formData.append("item_id", itemId);
|
||||||
@@ -897,24 +1130,32 @@ function changeDuration(e, itemId, delta) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleItemTap(e, itemId) {
|
function handleItemTap(e, htmlId) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
document
|
document
|
||||||
.querySelectorAll(".hol-drag-item")
|
.querySelectorAll(".hol-drag-item")
|
||||||
.forEach((el) => el.classList.remove("selected-for-move"));
|
.forEach((el) => el.classList.remove("selected-for-move"));
|
||||||
if (selectedItemIdForMove === itemId) {
|
if (selectedItemIdForMove === htmlId) {
|
||||||
selectedItemIdForMove = null;
|
selectedItemIdForMove = null;
|
||||||
} else {
|
} else {
|
||||||
selectedItemIdForMove = itemId;
|
selectedItemIdForMove = htmlId;
|
||||||
document
|
const el = document.getElementById(htmlId);
|
||||||
.getElementById("drag-item-" + itemId)
|
if (el) el.classList.add("selected-for-move");
|
||||||
.classList.add("selected-for-move");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleZoneTap(e, dateStr, timeStr) {
|
function handleZoneTap(e, dateStr, timeStr) {
|
||||||
if (selectedItemIdForMove) {
|
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;
|
selectedItemIdForMove = null;
|
||||||
document
|
document
|
||||||
.querySelectorAll(".hol-drag-item")
|
.querySelectorAll(".hol-drag-item")
|
||||||
@@ -925,63 +1166,179 @@ function handleZoneTap(e, dateStr, timeStr) {
|
|||||||
function dragStart(e) {
|
function dragStart(e) {
|
||||||
e.dataTransfer.setData("text/plain", e.target.id);
|
e.dataTransfer.setData("text/plain", e.target.id);
|
||||||
e.dataTransfer.effectAllowed = "move";
|
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) {
|
function allowDrop(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragEnter(e) {
|
function dragEnter(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let s = e.target.closest(".hol-time-slot");
|
let slot = e.target.closest(".hol-time-slot");
|
||||||
if (s) s.classList.add("drag-over");
|
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) {
|
function dragLeave(e) {
|
||||||
let s = e.target.closest(".hol-time-slot");
|
// La gestion précise des surbrillances se fait via le dragEnter et dragEnd pour éviter le scintillement (flickering).
|
||||||
if (s) s.classList.remove("drag-over");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDropEvent(e, dateStr, timeStr) {
|
function handleDropEvent(e, dateStr, timeStr) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let slot = e.target.closest(".hol-time-slot");
|
document
|
||||||
if (slot) slot.classList.remove("drag-over");
|
.querySelectorAll(".hol-time-slot")
|
||||||
|
.forEach((s) => s.classList.remove("drag-over-duration"));
|
||||||
|
|
||||||
const idStr = e.dataTransfer.getData("text/plain");
|
const idStr = e.dataTransfer.getData("text/plain");
|
||||||
const itemId = idStr.replace("drag-item-", "");
|
const itemEl = document.getElementById(idStr);
|
||||||
const dropZone = slot || document.getElementById("unmapped-pool");
|
const dropZone =
|
||||||
handleDropLogic(itemId, dateStr, timeStr, 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) {
|
function handleDropLogic(htmlId, dateStr, timeStr) {
|
||||||
if (itemId === "virtual-transit") {
|
const itemEl = document.getElementById(htmlId);
|
||||||
const step = window.CURRENT_PLANNING_STEP;
|
if (!itemEl) return;
|
||||||
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));
|
|
||||||
|
|
||||||
|
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();
|
const fd = new FormData();
|
||||||
fd.append("action", "add_single_item");
|
fd.append("action", "add_single_item");
|
||||||
fd.append("holiday_id", holidayId);
|
fd.append("holiday_id", holidayId);
|
||||||
fd.append("sort_order", step.sort_order);
|
fd.append("sort_order", sortOrder);
|
||||||
fd.append("category", "transport");
|
fd.append("category", "transport");
|
||||||
fd.append("context", "transit");
|
fd.append("context", "transit");
|
||||||
|
fd.append("expense_context", "transit");
|
||||||
fd.append("name", `Essence depuis ${tData.from}`);
|
fd.append("name", `Essence depuis ${tData.from}`);
|
||||||
fd.append("amount", tData.cost);
|
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_date", dateStr);
|
||||||
fd.append("item_time", timeStr);
|
fd.append("item_time", timeStr);
|
||||||
|
|
||||||
fetch("/modules/holidays/includes/api/save_checkpoint.php", {
|
fetch("/modules/holidays/includes/api/save_checkpoint.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: fd,
|
body: fd,
|
||||||
}).then(() => window.location.reload());
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recalcAllBadges();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateItemMemory(itemId, changes) {
|
function updateItemMemory(itemId, changes) {
|
||||||
|
if (typeof MAP_POINTS !== "undefined") {
|
||||||
MAP_POINTS.forEach((step) => {
|
MAP_POINTS.forEach((step) => {
|
||||||
let item = step.items.find((i) => i.id == itemId);
|
let item = step.items.find((i) => i.id == itemId);
|
||||||
if (item) Object.assign(item, changes);
|
if (item) Object.assign(item, changes);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// MÉTÉO SPÉCIFIQUE AU HEADER DU PLANNING
|
// MÉTÉO SPÉCIFIQUE AU HEADER DU PLANNING
|
||||||
@@ -1377,3 +1734,47 @@ window.generateTravelBook = function () {
|
|||||||
btn.disabled = false;
|
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)
|
// 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');
|
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 {
|
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') {
|
if ($_POST['action'] === 'add_single_item') {
|
||||||
$holiday_id = (int)$_POST['holiday_id'];
|
$holiday_id = (int)$_POST['holiday_id'];
|
||||||
$sort_order = (int)$_POST['sort_order'];
|
$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;
|
$date = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
|
||||||
$time = !empty($_POST['item_time']) ? $_POST['item_time'] : null;
|
$time = !empty($_POST['item_time']) ? $_POST['item_time'] : null;
|
||||||
|
|
||||||
|
$context = $_POST['expense_context'] ?? ($_POST['context'] ?? 'local');
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
$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 = $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([
|
$ins->execute([
|
||||||
$holiday_id, $_POST['category'], $_POST['name'], (float)$_POST['amount'], 0,
|
$holiday_id, $_POST['category'], $_POST['name'], (float)$_POST['amount'], 0,
|
||||||
$stepInfo['location_name'], $stepInfo['lat'], $stepInfo['lng'],
|
$stepInfo['location_name'], $stepInfo['lat'], $stepInfo['lng'],
|
||||||
$sort_order, $stepInfo['step_start_date'], $stepInfo['step_end_date'],
|
$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();
|
$pdo->commit();
|
||||||
echo json_encode(['success' => true]);
|
|
||||||
|
echo json_encode(['success' => true, 'id' => $newId]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
echo json_encode(['success' => false, 'error' => 'Etape introuvable']);
|
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]);
|
echo json_encode(['success' => true]);
|
||||||
exit;
|
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) {
|
} catch (Exception $e) {
|
||||||
if ($pdo->inTransaction()) { $pdo->rollBack(); }
|
if ($pdo->inTransaction()) { $pdo->rollBack(); }
|
||||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
|||||||
@@ -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,
|
(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_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_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
|
FROM pf_holidays h
|
||||||
LEFT JOIN pf_vehicles v ON h.vehicle_id = v.id
|
LEFT JOIN pf_vehicles v ON h.vehicle_id = v.id
|
||||||
WHERE h.id = ?
|
WHERE h.id = ?
|
||||||
@@ -108,6 +109,10 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
📖 Carnet de Voyage
|
📖 Carnet de Voyage
|
||||||
</button>
|
</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))">
|
<button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="editHoliday(JSON.parse(document.getElementById('holidayDataJson').textContent))">
|
||||||
<?= tr('btn_edit_bases') ?>
|
<?= tr('btn_edit_bases') ?>
|
||||||
</button>
|
</button>
|
||||||
@@ -127,18 +132,30 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
|
|
||||||
<div class="hol-summary-item">
|
<div class="hol-summary-item">
|
||||||
<div class="hol-summary-label">
|
<div class="hol-summary-label">
|
||||||
Frais de route (Essence/Péages)
|
Frais de route
|
||||||
</div>
|
</div>
|
||||||
<div class="hol-summary-value" style="display:flex; align-items:center; gap:6px;">
|
<div class="hol-summary-value" style="display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
|
||||||
<span style="font-size: 1.1rem;">⛽</span>
|
|
||||||
<strong><?= number_format($holiday['total_transit'], 0) ?> €</strong>
|
|
||||||
|
|
||||||
|
<!-- 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 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 id="display_fuel_price">1.85</span> €/L) <span style="font-size:0.7rem; opacity:0.8;">✏️</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<?php if ($holiday['total_transit'] > 0): ?>
|
<!-- Bouton Détails (Oeil) -->
|
||||||
<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">
|
<?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>
|
</span>
|
||||||
<?php endif; ?>
|
<?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>
|
<p style="color:var(--text-muted); font-style:italic; text-align:center; margin-top:40px;"><?= tr('hdl_no_steps') ?></p>
|
||||||
<?php else: ?>
|
<?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): ?>
|
<?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']) ?>">
|
<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>
|
||||||
<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="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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
@@ -349,16 +313,17 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (!empty($holiday['notes'])): ?>
|
<div class="pf-card" style="margin-top: 24px;">
|
||||||
<div class="hol-summary-card" style="margin-top: 24px; padding: 25px; border-left: 5px solid #f59e0b;">
|
<h2 class="pf-card-title">📝 <?= tr('hdl_label_notes') ?></h2>
|
||||||
<h3 style="margin: 0 0 15px 0; font-size: 1.2rem; color: #0f172a; display: flex; align-items: center; gap: 8px;">
|
<div class="pf-card-body">
|
||||||
📝 <?= tr('hdl_label_notes') ?>
|
<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>
|
||||||
</h3>
|
<div style="text-align: right; margin-top: 12px;">
|
||||||
<div style="font-size: 0.95rem; color: #334155; white-space: pre-wrap; line-height: 1.6;">
|
<button id="btnSaveHolidayNote" class="pf-btn" onclick="saveHolidayGlobalNote(<?= (int)$_GET['id'] ?>)">
|
||||||
<?= htmlspecialchars($holiday['notes']) ?>
|
💾 <?= tr('btn_save') ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -454,13 +419,16 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="planningModal" class="pf-modal">
|
<div id="planningModal" class="pf-modal">
|
||||||
<div class="pf-modal-content" style="max-width: 550px;">
|
<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: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);">📅 <?= tr('hdl_planning_title') ?></h3>
|
<h3 id="planningModalTitle" style="margin:0; color:var(--primary);">📅 Planning Global</h3>
|
||||||
<button type="button" onclick="closePlanningModal()" class="pf-modal-close">×</button>
|
<button type="button" onclick="closePlanningModal()" class="pf-modal-close">×</button>
|
||||||
</div>
|
</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>
|
<button type="button" onclick="closePlanningModal()" class="pf-btn btn-secondary"><?= tr('btn_close') ?></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,10 +62,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="modal-footer">
|
||||||
<button type="button" onclick="deleteHoliday()" id="btn_delete" class="pf-btn btn-secondary hol-btn-delete"><?= tr('btn_delete') ?></button>
|
<button type="button" onclick="deleteHoliday()" id="btn_delete" class="pf-btn btn-secondary hol-btn-delete"><?= tr('btn_delete') ?></button>
|
||||||
|
|||||||
Reference in New Issue
Block a user