temps / modale détail / drag / waze
Deploy HouseHub / deploy (push) Successful in 2s

This commit is contained in:
2026-06-20 19:01:18 +02:00
parent 854c6388ba
commit 18740c5e1b
4 changed files with 426 additions and 130 deletions
+155 -1
View File
@@ -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
View File
@@ -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 &nbsp;•&nbsp; ⏱️ ${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]);
+97 -44
View File
@@ -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">&times;</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>