diff --git a/modules/holidays/holidays.css b/modules/holidays/holidays.css
index 215f034..2183f9d 100644
--- a/modules/holidays/holidays.css
+++ b/modules/holidays/holidays.css
@@ -1,6 +1,11 @@
/* modules/holidays/holidays.css */
/* --- 1. VARIABLES & BASE --- */
+
+/* Bloque le défilement de la page en arrière-plan */
+body.no-scroll {
+ overflow: hidden !important;
+}
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
@@ -939,3 +944,244 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
background: #fca5a5;
color: #b91c1c;
}
+
+/* ==========================================================================
+ 16. PLANNING DRAG & DROP (CARNET DE VOYAGE)
+ ========================================================================== */
+/* Simplification de la sous-ligne du formulaire d'édition */
+.hol-form-subrow {
+ display: flex;
+ margin-top: 6px;
+ padding-left: 58px;
+}
+.hol-form-notes-full {
+ margin: 0 !important;
+ width: 100%;
+}
+
+/* Layout du Planning Interactif */
+.hol-planning-layout {
+ display: flex;
+ gap: 20px;
+ height: 65vh;
+ align-items: stretch;
+ overflow: hidden;
+}
+@media (max-width: 768px) {
+ .hol-planning-layout {
+ flex-direction: column;
+ height: 75vh;
+ }
+ .hol-unmapped-zone {
+ width: 100%;
+ height: 180px;
+ min-height: 180px;
+ flex-direction: row;
+ overflow-x: auto;
+ overflow-y: hidden;
+ align-items: flex-start;
+ }
+ .hol-calendar-zone {
+ flex: 1;
+ }
+}
+
+/* Zone des activités non planifiées */
+.hol-unmapped-zone {
+ width: 250px;
+ background: #f8fafc;
+ padding: 15px;
+ border-radius: 12px;
+ border: 2px dashed #cbd5e1;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ overflow-y: auto;
+}
+.hol-unmapped-title {
+ font-size: 0.85rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ color: #64748b;
+ margin-bottom: 5px;
+ text-align: center;
+}
+
+/* Grille du calendrier */
+.hol-calendar-zone {
+ flex: 1;
+ display: flex;
+ gap: 15px;
+ overflow: auto; /* Gère à la fois gauche/droite ET haut/bas */
+ padding-bottom: 10px;
+ position: relative;
+}
+.hol-calendar-zone::-webkit-scrollbar,
+.hol-unmapped-zone::-webkit-scrollbar {
+ height: 10px;
+}
+.hol-calendar-zone::-webkit-scrollbar-track,
+.hol-unmapped-zone::-webkit-scrollbar-track {
+ background: #f1f5f9;
+ border-radius: 5px;
+}
+.hol-calendar-zone::-webkit-scrollbar-thumb,
+.hol-unmapped-zone::-webkit-scrollbar-thumb {
+ background: #cbd5e1;
+ border-radius: 5px;
+}
+.hol-calendar-zone::-webkit-scrollbar-thumb:hover {
+ background: #94a3b8;
+}
+
+.hol-calendar-zone .hol-drag-item {
+ position: absolute;
+ top: 1px;
+ left: 45px;
+ right: 5px;
+ height: calc(75px * var(--duration) - 2px);
+}
+/* Dans la boîte d'attente : Position normale */
+.hol-unmapped-zone .hol-drag-item {
+ position: relative;
+ min-width: 150px;
+ height: auto;
+}
+
+/* Mode "Tap-to-Move" pour Mobile */
+.hol-drag-item.selected-for-move {
+ border-color: #f59e0b;
+ transform: scale(1.02);
+ box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.3);
+}
+
+/* Boutons Durée */
+.hol-item-duration-controls {
+ display: flex;
+ align-items: center;
+ gap: 3px;
+ margin: 0;
+ padding: 2px;
+ border: none;
+ background: rgba(255, 255, 255, 0.4); /* Léger fond pour se détacher */
+ border-radius: 4px;
+}
+
+.hol-day-column {
+ min-width: 220px;
+ flex: 1;
+ background: white;
+ border-radius: 12px;
+ border: 1px solid #e2e8f0;
+ display: flex;
+ flex-direction: column;
+ height: max-content; /* Laisse la colonne grandir jusqu'à 22h */
+ overflow: visible;
+}
+
+.hol-calendar-day-header {
+ text-align: center;
+ padding: 12px 10px;
+ background: #f8fafc;
+ border-bottom: 2px solid #e2e8f0;
+ position: sticky;
+ top: 0; /* Garde la date toujours visible en haut ! */
+ z-index: 20;
+}
+
+/* Les créneaux horaires (Drop Zones) */
+.hol-time-slots-container {
+ display: flex;
+ flex-direction: column;
+}
+
+.hol-time-slot {
+ height: 75px;
+ border-bottom: 1px solid #f1f5f9;
+ position: relative;
+ transition: background 0.2s;
+ box-sizing: border-box;
+}
+
+.hol-time-slot.drag-over {
+ background: #e0f2fe; /* Surbrillance quand on survole avec une activité */
+}
+.hol-slot-label {
+ position: absolute;
+ top: 2px;
+ right: 5px;
+ font-size: 0.65rem;
+ font-weight: 700;
+ color: #94a3b8;
+ pointer-events: none;
+}
+
+/* L'Activité (Élément draggable) */
+.hol-drag-item {
+ background: white;
+ padding: 6px 8px;
+ border-radius: 6px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
+ cursor: grab;
+ font-size: 0.85rem;
+ border: 1px solid #e2e8f0;
+ border-left: 4px solid var(--primary);
+ display: flex;
+ flex-direction: column;
+ z-index: 10;
+ overflow: hidden;
+ transition:
+ transform 0.2s,
+ border-color 0.2s;
+}
+.hol-drag-item:active {
+ cursor: grabbing;
+}
+.hol-drag-item.cat-accommodation {
+ border-left-color: #8b5cf6;
+}
+.hol-drag-item.cat-transport {
+ border-left-color: #10b981;
+}
+
+.hol-drag-title {
+ font-weight: 600;
+ color: #0f172a;
+ line-height: 1.2;
+}
+.hol-drag-note {
+ font-size: 0.75rem;
+ color: #64748b;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* Élargir la modale de planning */
+#planningModal .pf-modal-content {
+ max-width: 1100px !important;
+ width: 95%;
+}
+
+.hol-dur-btn {
+ background: #f1f5f9;
+ border: none;
+ border-radius: 4px;
+ width: 18px;
+ height: 18px; /* Plus petit */
+ cursor: pointer;
+ font-weight: bold;
+ color: #475569;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.8rem;
+}
+.hol-dur-btn:hover {
+ background: #e2e8f0;
+}
+.hol-dur-text {
+ font-size: 0.7rem;
+ color: #64748b;
+ font-weight: 700;
+}
diff --git a/modules/holidays/holidays.js b/modules/holidays/holidays.js
index 6a62c18..40aed04 100644
--- a/modules/holidays/holidays.js
+++ b/modules/holidays/holidays.js
@@ -359,6 +359,8 @@ function panMapTo(lat, lng) {
}
// --- LOGIQUE DE LA MODALE CHECKPOINT (Lignes multiples) ---
+document.getElementById("cp_start_date").value = "";
+document.getElementById("cp_end_date").value = "";
function openCheckpointModal(mode, data = null) {
const searchBlock = document.getElementById("cpSearchBlock");
@@ -387,18 +389,24 @@ function openCheckpointModal(mode, data = null) {
document.getElementById("cp_lng").value = data.lng;
document.getElementById("cp_old_sort_order").value = data.sort_order;
document.getElementById("cp_name").value = data.location_name;
+ document.getElementById("cp_start_date").value = data.step_start_date || "";
+ document.getElementById("cp_end_date").value = data.step_end_date || "";
if (data.items && data.items.length > 0) {
let visibleCount = 0;
data.items.forEach((it) => {
if (it.name !== "PF_TECHNICAL_POINT") {
- // On passe it.notes à la fonction !
+ // On transmet l'ID, la date, l'heure et la durée !
addCpExpenseLine(
it.category,
it.name,
it.amount,
it.is_paid,
it.notes || "",
+ it.id || "",
+ it.item_date || "",
+ it.item_time || "",
+ it.duration || 1,
);
visibleCount++;
}
@@ -409,6 +417,7 @@ function openCheckpointModal(mode, data = null) {
}
}
document.getElementById("checkpointModal").style.display = "flex";
+ document.body.classList.add("no-scroll");
}
function searchPlace() {
@@ -466,10 +475,14 @@ function addCpExpenseLine(
amount = "",
isPaid = 0,
notes = "",
+ itemId = "",
+ itemDate = "",
+ itemTime = "",
+ itemDur = 1,
) {
const container = document.getElementById("cpExpensesContainer");
const div = document.createElement("div");
- div.className = "hol-form-row"; // Classe globale de la ligne
+ div.className = "hol-form-row";
const isChecked = isPaid == 1 ? "checked" : "";
@@ -480,7 +493,7 @@ function addCpExpenseLine(
🚗
🎫
-
+
@@ -488,10 +501,16 @@ function addCpExpenseLine(
Payé
-
×
-
+
+
+
+
+
+
+
+
`;
container.appendChild(div);
}
@@ -583,3 +602,269 @@ document.addEventListener("DOMContentLoaded", () => {
});
}
});
+
+// ============================================================================
+// MOTEUR DRAG & DROP DU PLANNING
+// ============================================================================
+let selectedItemIdForMove = null; // Mémoire pour le mode Mobile
+
+function openPlanningModal(step) {
+ document.getElementById("planningModalTitle").innerText =
+ "📅 Planning : " + step.location_name;
+ const container = document.getElementById("planningContainer");
+ selectedItemIdForMove = null; // Reset à l'ouverture
+
+ let validItems = step.items.filter((it) => it.name !== "PF_TECHNICAL_POINT");
+
+ if (!step.step_start_date || !step.step_end_date) {
+ container.innerHTML = `⚠️ Dates manquantes Veuillez modifier cette étape (✏️) et renseigner sa Date d'arrivée et de départ .
`;
+ document.getElementById("planningModal").style.display = "flex";
+ return;
+ }
+
+ let datesToDisplay = [];
+ let curr = new Date(step.step_start_date);
+ let end = new Date(step.step_end_date);
+ while (curr <= end) {
+ datesToDisplay.push(curr.toISOString().split("T")[0]);
+ curr.setDate(curr.getDate() + 1);
+ }
+
+ // Zone À PLACER : Ajout du onclick pour le mobile
+ let html = `
+
+
+
+ `;
+
+ datesToDisplay.forEach((dateStr) => {
+ const dObj = new Date(dateStr);
+ const dayName = dObj.toLocaleDateString("fr-FR", { weekday: "short" });
+ const dayNum = dObj.toLocaleDateString("fr-FR", {
+ day: "numeric",
+ month: "short",
+ });
+
+ html += `
+
+
+
+ `;
+
+ for (let h = 8; h <= 22; h++) {
+ let hourStr = h.toString().padStart(2, "0") + ":00";
+ // Créneaux : Ajout du onclick pour le mobile
+ html += `
+
+ ${hourStr}
+
+ `;
+ }
+ html += `
`;
+ });
+ html += `
`;
+ container.innerHTML = html;
+
+ // Rendu des activités
+ validItems.forEach((it) => {
+ let icon = "🏷️";
+ let catClass = "cat-activity";
+ if (it.category === "accommodation") {
+ icon = "🏨";
+ catClass = "cat-accommodation";
+ }
+ if (it.category === "transport") {
+ icon = "🚗";
+ catClass = "cat-transport";
+ }
+
+ const dur = it.duration || 1;
+ const noteHtml = it.notes
+ ? `${it.notes}
`
+ : "";
+
+ // Ajout du style inline --duration, et des événements drag + click(mobile)
+ const elHtml = `
+
+
+
+
${icon} ${it.name}
+
+ -
+ ${dur}h
+ +
+
+
+
+ ${noteHtml}
+
+ `;
+
+ 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);
+ });
+
+ // Affiche la modale et bloque le scroll de l'arrière-plan
+ document.getElementById("planningModal").style.display = "flex";
+ document.body.classList.add("no-scroll");
+}
+
+// -- GESTION DES DURÉES --
+function changeDuration(e, itemId, delta) {
+ e.stopPropagation(); // Empêche de déclencher le Tap-to-Move
+ const itemEl = document.getElementById("drag-item-" + itemId);
+ let currentDur = parseInt(itemEl.style.getPropertyValue("--duration")) || 1;
+ let newDur = currentDur + delta;
+
+ if (newDur < 1) newDur = 1;
+ if (newDur > 12) newDur = 12; // Max 12h pour pas casser l'affichage
+
+ itemEl.style.setProperty("--duration", newDur);
+ document.getElementById(`dur-text-${itemId}`).innerText = newDur + "h";
+
+ // Mise à jour de la mémoire JS (Évite le bug au refresh)
+ updateItemMemory(itemId, { duration: newDur });
+
+ // Sauvegarde BDD
+ const formData = new FormData();
+ formData.append("action", "update_item_duration");
+ formData.append("item_id", itemId);
+ formData.append("duration", newDur);
+ fetch("/modules/holidays/includes/api/save_checkpoint.php", {
+ method: "POST",
+ body: formData,
+ });
+}
+
+// -- MODE MOBILE (TAP TO MOVE) --
+function handleItemTap(e, itemId) {
+ e.stopPropagation();
+ document
+ .querySelectorAll(".hol-drag-item")
+ .forEach((el) => el.classList.remove("selected-for-move"));
+
+ if (selectedItemIdForMove === itemId) {
+ selectedItemIdForMove = null; // Désélectionner
+ } else {
+ selectedItemIdForMove = itemId;
+ document
+ .getElementById("drag-item-" + itemId)
+ .classList.add("selected-for-move");
+ }
+}
+
+function handleZoneTap(e, dateStr, timeStr) {
+ if (selectedItemIdForMove) {
+ handleDropLogic(selectedItemIdForMove, dateStr, timeStr, e.currentTarget);
+ selectedItemIdForMove = null;
+ document
+ .querySelectorAll(".hol-drag-item")
+ .forEach((el) => el.classList.remove("selected-for-move"));
+ }
+}
+
+// -- MODE DESKTOP (DRAG & DROP) --
+function dragStart(e) {
+ e.dataTransfer.setData("text/plain", e.target.id);
+ e.dataTransfer.effectAllowed = "move";
+}
+function allowDrop(e) {
+ e.preventDefault();
+}
+function dragEnter(e) {
+ e.preventDefault();
+ let s = e.target.closest(".hol-time-slot");
+ if (s) s.classList.add("drag-over");
+}
+function dragLeave(e) {
+ let s = e.target.closest(".hol-time-slot");
+ if (s) s.classList.remove("drag-over");
+}
+
+function handleDropEvent(e, dateStr, timeStr) {
+ e.preventDefault();
+ let slot = e.target.closest(".hol-time-slot");
+ if (slot) slot.classList.remove("drag-over");
+
+ const idStr = e.dataTransfer.getData("text/plain");
+ const itemId = idStr.replace("drag-item-", "");
+ const dropZone = slot || document.getElementById("unmapped-pool");
+
+ handleDropLogic(itemId, dateStr, timeStr, dropZone);
+}
+
+// -- LOGIQUE COMMUNE (Déplacement, Mémoire et BDD) --
+function handleDropLogic(itemId, dateStr, timeStr, dropZone) {
+ const draggedEl = document.getElementById("drag-item-" + itemId);
+ if (dropZone && draggedEl) {
+ dropZone.appendChild(draggedEl); // Déplacement visuel
+
+ // 1. Mise à jour de la mémoire JS Globale (Corrige le bug de la zone "À PLACER" au retour)
+ updateItemMemory(itemId, { item_date: dateStr, item_time: timeStr });
+
+ // 2. Sauvegarde BDD
+ const formData = new FormData();
+ formData.append("action", "update_item_datetime");
+ formData.append("item_id", itemId);
+ formData.append("item_date", dateStr);
+ formData.append("item_time", timeStr);
+ fetch("/modules/holidays/includes/api/save_checkpoint.php", {
+ method: "POST",
+ body: formData,
+ });
+ }
+}
+
+// Met à jour l'objet JS global MAP_POINTS pour que la modale s'ouvre avec les bonnes données sans F5
+function updateItemMemory(itemId, changes) {
+ MAP_POINTS.forEach((step) => {
+ let item = step.items.find((i) => i.id == itemId);
+ if (item) {
+ Object.assign(item, changes);
+ }
+ });
+}
+
+function saveItemDateTime(itemId, dateStr, timeStr) {
+ const formData = new FormData();
+ formData.append("action", "update_item_datetime");
+ formData.append("item_id", itemId);
+ formData.append("item_date", dateStr);
+ formData.append("item_time", timeStr);
+
+ fetch("/modules/holidays/includes/api/save_checkpoint.php", {
+ method: "POST",
+ body: formData,
+ }).catch((err) =>
+ console.error("Erreur lors de la sauvegarde du planning :", err),
+ );
+}
+
+// Fermeture des modales en cliquant sur l'arrière-plan flouté
+window.addEventListener("click", function (event) {
+ if (event.target.classList.contains("pf-modal")) {
+ event.target.style.display = "none";
+ document.body.classList.remove("no-scroll");
+ }
+});
diff --git a/modules/holidays/includes/api/save_checkpoint.php b/modules/holidays/includes/api/save_checkpoint.php
index 0e1d3a0..12248d8 100644
--- a/modules/holidays/includes/api/save_checkpoint.php
+++ b/modules/holidays/includes/api/save_checkpoint.php
@@ -4,6 +4,25 @@ require dirname(__DIR__, 4) . '/includes/auth.php';
require dirname(__DIR__, 4) . '/includes/db.php';
require_login();
+// INTERCEPTION AJAX : Sauvegarde du planning (Drag & Drop / Durée)
+if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime', 'update_item_duration'])) {
+ $itemId = (int)$_POST['item_id'];
+
+ if ($_POST['action'] === 'update_item_datetime') {
+ $itemDate = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
+ $itemTime = !empty($_POST['item_time']) ? $_POST['item_time'] : null;
+ $stmt = $pdo->prepare("UPDATE pf_holidays_items SET item_date = ?, item_time = ? WHERE id = ?");
+ $stmt->execute([$itemDate, $itemTime, $itemId]);
+ } else {
+ $duration = (int)$_POST['duration'];
+ $stmt = $pdo->prepare("UPDATE pf_holidays_items SET duration = ? WHERE id = ?");
+ $stmt->execute([$duration, $itemId]);
+ }
+
+ echo json_encode(['success' => true]);
+ exit; // Crucial : on arrête le script ici !
+}
+
$holiday_id = (int)$_POST['holiday_id'];
$location_name = trim($_POST['location_name']);
$lat = (float)$_POST['lat'];
@@ -34,8 +53,13 @@ if ($holiday_id > 0 && !empty($location_name)) {
$target_order = ($max !== null) ? (int)$max + 1 : 0;
}
- // 3. INSERTION DES LIGNES
- $stmt = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); $validItemsCount = 0;
+ // Récupération des dates de l'étape globale
+ $step_start = !empty($_POST['step_start_date']) ? $_POST['step_start_date'] : null;
+ $step_end = !empty($_POST['step_end_date']) ? $_POST['step_end_date'] : null;
+
+ // 3. INSERTION DES LIGNES (Avec Mémoire de la Durée et du Planning)
+ $stmt = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, notes, item_date, item_time, step_start_date, step_end_date, duration) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
+ $validItemsCount = 0;
if (isset($_POST['items']['name'])) {
foreach ($_POST['items']['name'] as $i => $raw_name) {
@@ -44,15 +68,27 @@ if ($holiday_id > 0 && !empty($location_name)) {
if ($name !== '' || $amount_raw !== '') {
$cat = $_POST['items']['cat'][$i] ?? 'activity';
$amount = (float)$amount_raw;
- $paid = (isset($_POST['items']['paid'][$i]) && (int)$_POST['items']['paid'][$i] === 1) ? 1 : 0;
+ $paid = (isset($_POST['items']['paid'][$i]) && (int)$_POST['items']['paid'][$i] === 1) ? 1 : 0;
$note = trim($_POST['items']['notes'][$i] ?? '');
- $stmt->execute([$holiday_id, $cat, $name ?: 'Dépense', $amount, $paid, $location_name, $lat, $lng, $target_order, $note]); $validItemsCount++;
+
+ // Récupération des données invisibles
+ $date = !empty($_POST['items']['date'][$i]) ? $_POST['items']['date'][$i] : null;
+ $time = !empty($_POST['items']['time'][$i]) ? $_POST['items']['time'][$i] : null;
+ $dur = !empty($_POST['items']['duration'][$i]) ? (int)$_POST['items']['duration'][$i] : 1;
+
+ $stmt->execute([$holiday_id, $cat, $name ?: 'Dépense', $amount, $paid, $location_name, $lat, $lng, $target_order, $note, $date, $time, $step_start, $step_end, $dur]);
+ $validItemsCount++;
}
}
}
if ($validItemsCount === 0) {
- $stmt->execute([$holiday_id, 'activity', 'PF_TECHNICAL_POINT', 0, 1, $location_name, $lat, $lng, $target_order, '']);
+ $stmt->execute([$holiday_id, 'activity', 'PF_TECHNICAL_POINT', 0, 1, $location_name, $lat, $lng, $target_order, '', null, null, $step_start, $step_end, 1]);
+ }
+
+ // Point technique si aucune dépense n'est saisie (pour garder la trace de l'étape et de ses dates)
+ if ($validItemsCount === 0) {
+ $stmt->execute([$holiday_id, 'activity', 'PF_TECHNICAL_POINT', 0, 1, $location_name, $lat, $lng, $target_order, '', null, null, $step_start, $step_end]);
}
// 4. GESTION DES FAVORIS
diff --git a/modules/holidays/views/detail.php b/modules/holidays/views/detail.php
index bf5468f..949ab5c 100644
--- a/modules/holidays/views/detail.php
+++ b/modules/holidays/views/detail.php
@@ -28,6 +28,7 @@ $favorites = json_decode($stmtFav->fetchColumn() ?: '[]', true);
$steps = [];
$generalItems = [];
+// C'est ici qu'il manquait sûrement une accolade !
foreach ($items as $it) {
if ($it['location_name'] !== null) {
$orderKey = $it['sort_order'];
@@ -37,6 +38,8 @@ foreach ($items as $it) {
'lat' => (float)$it['lat'],
'lng' => (float)$it['lng'],
'sort_order' => $it['sort_order'],
+ 'step_start_date' => $it['step_start_date'],
+ 'step_end_date' => $it['step_end_date'],
'total_amount' => 0,
'items' => []
];
@@ -138,10 +141,16 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
☰
📍 = htmlspecialchars($step['location_name']) ?>
+
+
+ Du = date('d/m', strtotime($step['step_start_date'])) ?> au = date('d/m', strtotime($step['step_end_date'])) ?>
+
+
= number_format($step['total_amount'], 2, ',', ' ') ?> €
+
📅
✏️
@@ -199,8 +208,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
📍 Placer une étape
- ×
-
+
×
@@ -232,6 +240,16 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
Nom de l'étape
+
Dépenses prévues
@@ -253,14 +271,26 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
🗑️ Supprimer
- Annuler
- Enregistrer
+ Annuler Enregistrer
+