This commit is contained in:
@@ -788,7 +788,7 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
|
||||
|
||||
.hol-cp-header {
|
||||
background: var(--bg-page);
|
||||
padding: 12px 15px;
|
||||
padding: 6px 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@@ -1485,3 +1485,157 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- EN-TÊTE DES ÉTAPES ROADTRIP --- */
|
||||
.hol-step-header {
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Ligne 1 : Icône + Titre + Poignée */
|
||||
.hol-step-row-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hol-step-icon {
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.hol-step-title {
|
||||
flex: 1;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
line-height: 1.3;
|
||||
word-break: break-word;
|
||||
cursor: pointer;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.hol-step-controls {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hol-drag-handle {
|
||||
color: #cbd5e1;
|
||||
font-size: 1.4rem;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.hol-step-arrows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.hol-step-arrows button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
font-size: 0.75rem;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Ligne 2 : Tags/Dates + Prix/Actions */
|
||||
.hol-step-row-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hol-step-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.hol-tag-return {
|
||||
background: #fff7ed;
|
||||
color: #ea580c;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
border: 1px solid #ffedd5;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.hol-step-date {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
background: var(--bg-page);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-light);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.hol-step-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto; /* Pousse le prix et les boutons tout à droite */
|
||||
}
|
||||
|
||||
.hol-step-price {
|
||||
font-weight: 800;
|
||||
color: var(--primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Optimisation Mobile */
|
||||
@media (max-width: 768px) {
|
||||
.hol-step-row-bottom {
|
||||
padding-left: 0; /* Sur mobile, on prend toute la largeur pour éviter d'écraser les boutons */
|
||||
}
|
||||
}
|
||||
|
||||
/* --- LIGNES DE DÉPENSES --- */
|
||||
.hol-expense-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.hol-expense-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.hol-expense-price-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap; /* Interdit le passage à la ligne */
|
||||
flex-shrink: 0; /* Empêche le bloc d'être écrasé */
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.hol-expense-price-group .status-paid,
|
||||
.hol-expense-price-group .status-pending {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
+168
-82
@@ -257,10 +257,11 @@ function initDetailMap() {
|
||||
Promise.all(routePromises).then((results) => {
|
||||
results.sort((a, b) => a.index - b.index);
|
||||
|
||||
// Compteur global de distance
|
||||
// 🔥 NOUVEAU : Compteurs et HTML de la modale
|
||||
let totalTripDistance = 0;
|
||||
let totalTripDuration = 0; // en secondes
|
||||
let transitDetailsHtml = "";
|
||||
|
||||
// Détection de l'étape de retour (Ligne Orange)
|
||||
let returnStartIndex = latlngs.length - 2;
|
||||
if (
|
||||
typeof window.GLOBAL_RETURN_STEP_ID !== "undefined" &&
|
||||
@@ -269,22 +270,21 @@ function initDetailMap() {
|
||||
const customReturnStep = MAP_POINTS.findIndex(
|
||||
(p) => p.sort_order == window.GLOBAL_RETURN_STEP_ID,
|
||||
);
|
||||
if (customReturnStep > 0) {
|
||||
returnStartIndex = customReturnStep;
|
||||
}
|
||||
if (customReturnStep > 0) returnStartIndex = customReturnStep;
|
||||
}
|
||||
|
||||
results.forEach((res) => {
|
||||
const i = res.index;
|
||||
let routeColor = "#3b82f6";
|
||||
let routeWeight = window.innerWidth < 768 ? 4 : 6;
|
||||
let routeDash = null;
|
||||
|
||||
if (i >= returnStartIndex) {
|
||||
routeColor = "#f97316";
|
||||
routeWeight = window.innerWidth < 768 ? 3 : 4;
|
||||
routeDash = "10, 10";
|
||||
}
|
||||
let routeColor = i >= returnStartIndex ? "#f97316" : "#3b82f6";
|
||||
let routeWeight =
|
||||
window.innerWidth < 768
|
||||
? i >= returnStartIndex
|
||||
? 3
|
||||
: 4
|
||||
: i >= returnStartIndex
|
||||
? 4
|
||||
: 6;
|
||||
let routeDash = i >= returnStartIndex ? "10, 10" : null;
|
||||
|
||||
if (res.data && res.data.code === "Ok" && res.data.routes.length > 0) {
|
||||
const routeCoords = res.data.routes[0].geometry.coordinates.map(
|
||||
@@ -299,30 +299,54 @@ function initDetailMap() {
|
||||
lineJoin: "round",
|
||||
}).addTo(detailMap);
|
||||
|
||||
// CALCUL AUTO DU COUT ET KILOMÈTRES
|
||||
// CALCULS
|
||||
const distanceKm = res.data.routes[0].distance / 1000;
|
||||
const durationSec = res.data.routes[0].duration; // Durée en secondes
|
||||
|
||||
totalTripDistance += distanceKm;
|
||||
totalTripDuration += durationSec;
|
||||
|
||||
const fuelL100 = window.VEHICLE_CONSUMPTION || 7;
|
||||
const fuelPrice = window.FUEL_PRICE || 1.85;
|
||||
const cost = (distanceKm / 100) * fuelL100 * fuelPrice;
|
||||
|
||||
const targetOrder = MAP_POINTS[res.index + 1].sort_order;
|
||||
// 1. ON DÉCLARE LES POINTS D'ABORD
|
||||
const startPt = MAP_POINTS[res.index];
|
||||
const endPt = MAP_POINTS[res.index + 1];
|
||||
|
||||
// 2. ON SAUVEGARDE EN MÉMOIRE ENSUITE (Correction du bug !)
|
||||
window.TRANSIT_DATA = window.TRANSIT_DATA || {};
|
||||
window.TRANSIT_DATA[endPt.sort_order] = {
|
||||
sec: durationSec,
|
||||
from: startPt.location_name,
|
||||
cost: cost,
|
||||
};
|
||||
|
||||
// 🔥 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>
|
||||
<div style="font-size: 0.8rem; color: var(--text-muted); display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>🚗 ${Math.round(distanceKm)} km • ⏱️ ${formatDuration(durationSec)}</span>
|
||||
<strong style="color: var(--primary);">⛽ ${cost.toFixed(2)} €</strong>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// INJECTION DANS LA CARTE (Sous l'étape)
|
||||
const targetCard = document.getElementById(
|
||||
"step-card-" + targetOrder,
|
||||
"step-card-" + endPt.sort_order,
|
||||
);
|
||||
|
||||
if (targetCard) {
|
||||
const existingTransit =
|
||||
targetCard.querySelectorAll(".transit-auto-info");
|
||||
existingTransit.forEach((el) => el.remove());
|
||||
|
||||
const rawLocationName = MAP_POINTS[res.index].location_name;
|
||||
targetCard
|
||||
.querySelectorAll(".transit-auto-info")
|
||||
.forEach((el) => el.remove());
|
||||
const rawLocationName = startPt.location_name;
|
||||
const safeLocationName = rawLocationName.replace(/'/g, "\\'");
|
||||
const expenseDesc = `Essence depuis ${rawLocationName}`;
|
||||
|
||||
const targetStepData = MAP_POINTS[res.index + 1];
|
||||
const isAlreadyAdded = targetStepData.items.some(
|
||||
const isAlreadyAdded = endPt.items.some(
|
||||
(it) => it.name === expenseDesc,
|
||||
);
|
||||
|
||||
@@ -331,59 +355,52 @@ function initDetailMap() {
|
||||
🚗 ${Math.round(distanceKm)} km
|
||||
<span style="opacity: 0.5;">|</span>
|
||||
<strong>⛽ ~${cost.toFixed(2)} €</strong>
|
||||
${
|
||||
!isAlreadyAdded
|
||||
? `<button type="button" style="background:none; border:none; color:var(--primary); cursor:pointer; font-weight:600; font-size:0.8rem; padding:0; margin-left: 5px;"
|
||||
onclick="addQuickTransitExpense(${document.querySelector("input[name=holiday_id]").value}, ${targetOrder}, ${cost.toFixed(2)}, 'Essence depuis ${safeLocationName}', this)">
|
||||
+ Ajouter
|
||||
</button>`
|
||||
: `<span style="color:var(--success); font-weight:bold; margin-left: 5px;" title="Dépense déjà ajoutée à cette étape">✓ Ajouté</span>`
|
||||
}
|
||||
${!isAlreadyAdded ? `<button type="button" style="background:none; border:none; color:var(--primary); cursor:pointer; font-weight:600; font-size:0.8rem; padding:0; margin-left: 5px;" onclick="addQuickTransitExpense(${document.querySelector('input[name="holiday_id"]').value}, ${endPt.sort_order}, ${cost.toFixed(2)}, 'Essence depuis ${safeLocationName}', this, ${durationSec})">+ Ajouter</button>` : `<span style="color:var(--success); font-weight:bold; margin-left: 5px;" title="Dépense déjà ajoutée à cette étape">✓ Ajouté</span>`}
|
||||
</div>`;
|
||||
|
||||
const cpHeader = targetCard.querySelector(".hol-cp-header");
|
||||
if (cpHeader) {
|
||||
cpHeader.insertAdjacentHTML("afterend", summaryHtml);
|
||||
}
|
||||
if (cpHeader) cpHeader.insertAdjacentHTML("afterend", summaryHtml);
|
||||
}
|
||||
} else {
|
||||
drawFallbackLine(res.coords, routeColor, routeWeight);
|
||||
}
|
||||
});
|
||||
|
||||
// Affichage du total global en haut de page
|
||||
// 🔥 AFFICHAGE DES TOTAUX (KM + TEMPS) EN HAUT DE PAGE
|
||||
const distEl = document.getElementById("global_total_distance");
|
||||
const timeEl = document.getElementById("global_total_duration");
|
||||
const distBlock = document.getElementById("block_total_distance");
|
||||
if (distEl && distBlock) {
|
||||
|
||||
if (distEl && timeEl && distBlock) {
|
||||
distEl.innerText = Math.round(totalTripDistance);
|
||||
distBlock.style.display = "Block";
|
||||
timeEl.innerText = formatDuration(totalTripDuration);
|
||||
distBlock.style.display = "block";
|
||||
}
|
||||
|
||||
// 🔥 INJECTION DU HTML DANS LA MODALE
|
||||
const modalContainer = document.getElementById("transitDetailsContainer");
|
||||
if (modalContainer) {
|
||||
modalContainer.innerHTML =
|
||||
transitDetailsHtml ||
|
||||
'<p style="text-align:center; color:var(--text-muted);">Aucun trajet calculé.</p>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Chargement des badges Météo
|
||||
if (typeof MAP_POINTS !== "undefined") {
|
||||
MAP_POINTS.forEach((pt) => {
|
||||
if (typeof loadWeatherForStep === "function") {
|
||||
loadWeatherForStep(pt);
|
||||
// 5. Fix Leaflet Resize
|
||||
setTimeout(() => {
|
||||
if (detailMap) {
|
||||
detailMap.invalidateSize();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 300);
|
||||
|
||||
// 5. Fix Leaflet Resize
|
||||
setTimeout(() => {
|
||||
if (detailMap) {
|
||||
detailMap.invalidateSize();
|
||||
function drawFallbackLine(coords, color, weight) {
|
||||
L.polyline(coords, {
|
||||
color: color,
|
||||
weight: weight || 3,
|
||||
dashArray: "8, 8",
|
||||
opacity: 0.7,
|
||||
}).addTo(detailMap);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
function drawFallbackLine(coords, color, weight) {
|
||||
L.polyline(coords, {
|
||||
color: color,
|
||||
weight: weight || 3,
|
||||
dashArray: "8, 8",
|
||||
opacity: 0.7,
|
||||
}).addTo(detailMap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,6 +723,26 @@ function openPlanningModal(step) {
|
||||
|
||||
let validItems = step.items.filter((it) => it.name !== "PF_TECHNICAL_POINT");
|
||||
|
||||
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";
|
||||
@@ -784,18 +821,33 @@ function openPlanningModal(step) {
|
||||
? `<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};"
|
||||
ondragstart="dragStart(event)" onclick="handleItemTap(event, ${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;">${icon} ${it.name}</div>
|
||||
<div class="hol-drag-title" style="flex:1;">${visualName}</div>
|
||||
<div class="hol-item-duration-controls">
|
||||
<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>
|
||||
${durControls}
|
||||
</div>
|
||||
</div>
|
||||
${noteHtml}
|
||||
@@ -898,19 +950,29 @@ function handleDropEvent(e, dateStr, timeStr) {
|
||||
}
|
||||
|
||||
function handleDropLogic(itemId, dateStr, timeStr, dropZone) {
|
||||
const draggedEl = document.getElementById("drag-item-" + itemId);
|
||||
if (dropZone && draggedEl) {
|
||||
dropZone.appendChild(draggedEl);
|
||||
updateItemMemory(itemId, { item_date: dateStr, item_time: timeStr });
|
||||
const formData = new FormData();
|
||||
formData.append("action", "update_item_datetime");
|
||||
formData.append("item_id", itemId);
|
||||
formData.append("item_date", dateStr);
|
||||
formData.append("item_time", timeStr);
|
||||
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));
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append("action", "add_single_item");
|
||||
fd.append("holiday_id", holidayId);
|
||||
fd.append("sort_order", step.sort_order);
|
||||
fd.append("category", "transport");
|
||||
fd.append("context", "transit");
|
||||
fd.append("name", `Essence depuis ${tData.from}`);
|
||||
fd.append("amount", tData.cost);
|
||||
fd.append("duration", h);
|
||||
fd.append("item_date", dateStr);
|
||||
fd.append("item_time", timeStr);
|
||||
|
||||
fetch("/modules/holidays/includes/api/save_checkpoint.php", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
body: fd,
|
||||
}).then(() => window.location.reload());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,6 +1036,7 @@ function addQuickTransitExpense(
|
||||
amount,
|
||||
description,
|
||||
btnElement,
|
||||
durationSec = 3600,
|
||||
) {
|
||||
if (
|
||||
!confirm(
|
||||
@@ -1006,16 +1069,17 @@ function addQuickTransitExpense(
|
||||
fd.append("amount", amount);
|
||||
fd.append("context", "transit");
|
||||
|
||||
// 3. Envoi en arrière-plan sans bloquer l'utilisateur
|
||||
// 🔥 On sécurise la durée en base de données
|
||||
const h = Math.max(1, Math.round(durationSec / 3600));
|
||||
fd.append("duration", h);
|
||||
|
||||
fetch("/modules/holidays/includes/api/save_checkpoint.php", {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (!data.success) {
|
||||
alert("Erreur lors de la sauvegarde en arrière-plan : " + data.error);
|
||||
}
|
||||
if (!data.success) alert("Erreur : " + data.error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1037,3 +1101,25 @@ function updateFuelPrice() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Formatte les secondes en "XXhYY" ou "YYmin"
|
||||
function formatDuration(seconds) {
|
||||
if (!seconds || isNaN(seconds)) return "0min";
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
if (h > 0) {
|
||||
return `${h}h${m.toString().padStart(2, "0")}`;
|
||||
}
|
||||
return `${m}min`;
|
||||
}
|
||||
|
||||
// Ouvre et ferme la modale des détails (L'œil)
|
||||
function openTransitModal() {
|
||||
document.getElementById("transitModal").style.display = "flex";
|
||||
document.body.classList.add("no-scroll");
|
||||
}
|
||||
|
||||
function closeTransitModal() {
|
||||
document.getElementById("transitModal").style.display = "none";
|
||||
document.body.classList.remove("no-scroll");
|
||||
}
|
||||
|
||||
@@ -21,14 +21,17 @@ if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime
|
||||
$stepInfo = $stmt->fetch();
|
||||
|
||||
if ($stepInfo) {
|
||||
// Utilisation de transactions isolées pour l'AJAX
|
||||
$dur = isset($_POST['duration']) ? (int)$_POST['duration'] : 1;
|
||||
$date = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
|
||||
$time = !empty($_POST['item_time']) ? $_POST['item_time'] : null;
|
||||
|
||||
$pdo->beginTransaction();
|
||||
$ins = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, step_start_date, step_end_date, step_type, expense_context, duration) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)");
|
||||
$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']
|
||||
$stepInfo['step_type'], $_POST['context'], $dur, $date, $time
|
||||
]);
|
||||
$pdo->commit();
|
||||
echo json_encode(['success' => true]);
|
||||
|
||||
@@ -131,7 +131,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
</span>
|
||||
|
||||
<?php if ($holiday['total_transit'] > 0): ?>
|
||||
<span onclick="alert('Bientôt : Liste détaillée des frais de route !')" style="font-size: 1rem; cursor: pointer; opacity: 0.5; transition: opacity 0.2s; margin-left: 4px; display: inline-flex; align-items: center;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.5'" title="Voir le détail">
|
||||
<span 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">
|
||||
👁️
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
@@ -139,10 +139,13 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
</div>
|
||||
|
||||
<div class="hol-summary-item" id="block_total_distance" style="display:none;">
|
||||
<div class="hol-summary-label">Distance totale</div>
|
||||
<div class="hol-summary-label">Route & Temps de conduite</div>
|
||||
<div class="hol-summary-value" style="display:flex; align-items:center; gap:6px;">
|
||||
<span style="font-size: 1.1rem;">🛣️</span>
|
||||
<strong><span id="global_total_distance">0</span> km</strong>
|
||||
<span style="font-size: 0.85rem; color: var(--text-muted); margin-left: 5px;">
|
||||
(⏱️ <span id="global_total_duration">0h00</span>)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -193,17 +196,18 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
|
||||
<?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" style="background: transparent;">
|
||||
<div class="hol-cp-info-group">
|
||||
<span style="font-size:1.4rem;">🌍</span>
|
||||
<div class="hol-cp-title"><?= tr('hdl_general_costs') ?></div>
|
||||
<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 class="hol-cp-actions-group">
|
||||
<div class="hol-cp-total"><?= number_format($generalTotal, 2, ',', ' ') ?> €</div>
|
||||
<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>
|
||||
@@ -225,13 +229,20 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
<?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>
|
||||
<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>
|
||||
<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>
|
||||
@@ -239,39 +250,66 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
|
||||
<?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 class="hol-cp-header">
|
||||
<div class="hol-cp-info-group">
|
||||
<span class="desktop-only" style="color:#94a3b8; font-size:1.1rem; cursor:grab; user-select:none;">☰</span>
|
||||
<div class="mobile-only" style="flex-direction:column; gap:4px; margin-right:8px;">
|
||||
<button type="button" onclick="moveStepMobile(this, -1)" class="btn-icon-small" style="width:24px!important; height:24px!important; font-size:0.6rem;">▲</button>
|
||||
<button type="button" onclick="moveStepMobile(this, 1)" class="btn-icon-small" style="width:24px!important; height:24px!important; font-size:0.6rem;">▼</button>
|
||||
</div>
|
||||
<div class="hol-cp-title" onclick="panMapTo(<?= $step['lat'] ?>, <?= $step['lng'] ?>)" title="<?= htmlspecialchars($step['location_name']) ?>">
|
||||
📍 <?= htmlspecialchars($step['location_name']) ?>
|
||||
<?php if ($holiday['return_step_id'] !== null && $holiday['return_step_id'] == $step['sort_order']): ?>
|
||||
<span style="background: #fff7ed; color: #ea580c; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: bold; margin-left: 5px; border: 1px solid #ffedd5; vertical-align: middle;">🏁 <?= tr('hdl_return') ?></span>
|
||||
<?php endif; ?>
|
||||
<div class="hol-step-header">
|
||||
|
||||
<?php if ($step['step_type'] === 'origin'): ?>
|
||||
<span style="background: #ecfdf5; color: #059669; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: bold; margin-left: 5px; border: 1px solid #d1fae5; vertical-align: middle;">🛫 DÉPART</span>
|
||||
<?php elseif ($step['step_type'] === 'destination'): ?>
|
||||
<span style="background: #fef2f2; color: #e11d48; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: bold; margin-left: 5px; border: 1px solid #fee2e2; vertical-align: middle;">🛬 ARRIVÉE FINALE</span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($step['step_start_date']) && !empty($step['step_end_date'])): ?>
|
||||
<div class="hol-date-weather-wrapper">
|
||||
<span class="hol-step-dates">
|
||||
<?= tr('hdl_from') ?> <?= date('d/m', strtotime($step['step_start_date'])) ?> <?= tr('hdl_to') ?> <?= date('d/m', strtotime($step['step_end_date'])) ?>
|
||||
</span>
|
||||
<div class="hol-weather-info"></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="hol-step-row-top">
|
||||
<?php
|
||||
$stepIcon = '📍'; $iconBg = 'transparent'; $iconColor = '#64748b';
|
||||
$isReturn = ($holiday['return_step_id'] !== null && $holiday['return_step_id'] == $step['sort_order']);
|
||||
|
||||
if ($step['step_type'] === 'origin') {
|
||||
$stepIcon = '🛫'; $iconBg = '#ecfdf5'; $iconColor = '#059669';
|
||||
} elseif ($step['step_type'] === 'destination') {
|
||||
$stepIcon = '🛬'; $iconBg = '#fef2f2'; $iconColor = '#e11d48';
|
||||
}
|
||||
?>
|
||||
<div class="hol-step-icon" style="background: <?= $iconBg ?>; color: <?= $iconColor ?>;">
|
||||
<?= $stepIcon ?>
|
||||
</div>
|
||||
|
||||
<div class="hol-step-title" onclick="panMapTo(<?= $step['lat'] ?>, <?= $step['lng'] ?>)" title="<?= htmlspecialchars($step['location_name']) ?>">
|
||||
<?= htmlspecialchars($step['location_name']) ?>
|
||||
</div>
|
||||
|
||||
<div class="hol-step-controls">
|
||||
<span class="desktop-only hol-drag-handle">⣿</span>
|
||||
<div class="mobile-only hol-step-arrows">
|
||||
<button type="button" onclick="moveStepMobile(this, -1)">▲</button>
|
||||
<button type="button" onclick="moveStepMobile(this, 1)">▼</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hol-cp-actions-group">
|
||||
<div class="hol-cp-total"><?= number_format($step['total_amount'], 2, ',', ' ') ?> €</div>
|
||||
<button onclick='openPlanningModal(<?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('hdl_view_planning') ?>">📅</button>
|
||||
<button onclick='openCheckpointModal("edit", <?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('btn_edit') ?>">✏️</button>
|
||||
|
||||
<div class="hol-step-row-bottom">
|
||||
<div class="hol-step-meta">
|
||||
<?php if ($isReturn): ?>
|
||||
<span class="hol-tag-return">🏁 Retour</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($step['step_start_date']) && !empty($step['step_end_date'])): ?>
|
||||
<span class="hol-step-date">
|
||||
<?= date('d/m', strtotime($step['step_start_date'])) ?> ➔ <?= date('d/m', strtotime($step['step_end_date'])) ?>
|
||||
</span>
|
||||
<div class="hol-weather-info" style="display:inline-flex;"></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="hol-step-actions">
|
||||
<span class="hol-step-price"><?= number_format($step['total_amount'], 2, ',', ' ') ?> €</span>
|
||||
|
||||
<a href="https://www.google.com/maps/dir/?api=1&destination=<?= $step['lat'] ?>,<?= $step['lng'] ?>"
|
||||
target="_blank"
|
||||
class="btn-icon-small"
|
||||
title="Y aller avec le GPS (Maps/Waze)"
|
||||
style="text-decoration:none; display:inline-flex; align-items:center; justify-content:center; width:26px!important; height:26px!important; font-size:0.8rem; min-width:26px!important; min-height:26px!important;">
|
||||
🗺️
|
||||
</a>
|
||||
|
||||
<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>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="hol-cp-body">
|
||||
@@ -419,6 +457,21 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="transitModal" class="pf-modal">
|
||||
<div class="pf-modal-content" style="max-width: 500px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; border-bottom: 1px solid var(--border-light); padding-bottom: 10px;">
|
||||
<h3 style="margin:0; color:var(--text-main);">🛣️ Détail des trajets</h3>
|
||||
<button type="button" onclick="closeTransitModal()" class="pf-modal-close">×</button>
|
||||
</div>
|
||||
<div id="transitDetailsContainer" style="max-height: 60vh; overflow-y: auto; padding-right: 5px;">
|
||||
<p style="text-align:center; color:var(--text-muted); font-style:italic;">Calcul en cours...</p>
|
||||
</div>
|
||||
<div class="modal-footer" style="padding-top:15px; border-top:1px solid var(--border-light); margin-top:15px;">
|
||||
<button type="button" onclick="closeTransitModal()" class="pf-btn btn-secondary"><?= tr('btn_close') ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include __DIR__ . '/modal.php'; ?>
|
||||
|
||||
<script>
|
||||
|
||||
Reference in New Issue
Block a user