diff --git a/modules/holidays/holidays.css b/modules/holidays/holidays.css
index 1728b41..a5e7d88 100644
--- a/modules/holidays/holidays.css
+++ b/modules/holidays/holidays.css
@@ -1124,24 +1124,6 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
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 {
@@ -1343,6 +1325,34 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
font-weight: 700;
}
+/* Bouton pour retirer une carte du planning (Undo) */
+.hol-unplace-btn {
+ display: none; /* Caché par défaut dans la zone "À placer" */
+ background: #fee2e2;
+ color: #ef4444;
+ border: 1px solid #fca5a5;
+ border-radius: 4px;
+ width: 24px;
+ height: 24px;
+ cursor: pointer;
+ font-size: 0.85rem;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ transition: all 0.2s;
+ margin-left: 4px;
+}
+
+.hol-unplace-btn:hover {
+ background: #ef4444;
+ color: white;
+}
+
+/* Le bouton n'apparaît QUE quand la carte est physiquement dans le calendrier */
+.hol-time-slot .hol-drag-item .hol-unplace-btn {
+ display: flex;
+}
+
.highlight-step {
transition:
box-shadow 0.3s,
@@ -1639,3 +1649,127 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
.hol-expense-price-group .status-pending {
line-height: 1;
}
+
+/* ============================================================================
+ 📱 UX MOBILE : MODALE DE PLANNING (DRAG & DROP)
+ ============================================================================ */
+@media (max-width: 768px) {
+ /* --- MODALE GLOBALE --- */
+ #planningModal .pf-modal-content {
+ padding: 8px !important;
+ height: 98vh !important;
+ max-height: 98vh !important;
+ display: flex !important;
+ flex-direction: column !important;
+ }
+
+ #planningModal .pf-modal-content > div:first-child {
+ margin-bottom: 6px !important;
+ padding-bottom: 6px !important;
+ }
+ #planningModalTitle {
+ font-size: 1rem !important;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ #planningContainer > div {
+ flex-direction: column !important;
+ padding-bottom: 0 !important;
+ gap: 8px !important;
+ height: 100% !important;
+ }
+
+ /* --- 1. LE CALENDRIER (EN HAUT) --- */
+ .hol-calendar-zone {
+ width: 100% !important;
+ flex: 1 1 auto !important; /* Le calendrier prend tout l'espace restant */
+ order: 1 !important;
+ padding-bottom: 10px !important;
+ margin-top: 0 !important;
+ gap: 6px !important;
+ }
+
+ /* 🎯 Header des Jours ultra-compressé */
+ .hol-calendar-day-header {
+ padding: 4px !important;
+ }
+ .hol-cal-weekday {
+ font-size: 0.6rem !important;
+ margin-bottom: 0 !important;
+ }
+ .hol-cal-date {
+ font-size: 0.9rem !important;
+ line-height: 1 !important;
+ margin-bottom: 2px !important;
+ }
+
+ div[id^="plan-weather-"] {
+ min-height: 14px !important;
+ margin-top: 0 !important;
+ }
+ div[id^="plan-weather-"] .pf-weather-badge {
+ padding: 1px 4px !important;
+ font-size: 0.55rem !important;
+ }
+
+ .hol-time-slot {
+ min-height: 40px !important;
+ padding: 4px !important;
+ }
+
+ /* --- 2. LA ZONE "À PLACER" (BOTTOM SHEET - 30%) --- */
+ .hol-unmapped-zone {
+ width: 100% !important;
+ height: 30vh !important; /* 🔼 Prend exactement 30% de l'écran */
+ min-height: 180px !important;
+ flex: 0 0 auto !important; /* 🛡️ Protection anti-écrasement */
+ order: 2 !important;
+ border-radius: 12px 12px 0 0 !important;
+ border-top: 2px solid var(--primary) !important;
+ border-bottom: none !important;
+ border-left: none !important;
+ border-right: none !important;
+ box-shadow: 0 -4px 15px rgba(0, 0, 0, 0.15) !important;
+ margin: 0 -8px -8px -8px !important;
+ padding: 12px 10px 15px 10px !important;
+ z-index: 100;
+ background: var(--bg-panel) !important;
+ display: flex !important;
+ flex-direction: column !important; /* ↕️ On repasse en mode vertical */
+ }
+
+ .hol-unmapped-title {
+ font-size: 0.8rem !important;
+ margin-bottom: 8px !important;
+ flex-shrink: 0 !important;
+ }
+
+ /* --- 3. LISTE VERTICALE (Scroll Down) --- */
+ #unmapped-pool {
+ flex-direction: column !important; /* ↕️ Les cartes s'empilent */
+ overflow-y: auto !important; /* ↕️ Apparition du scroll vertical */
+ overflow-x: hidden !important;
+ padding-bottom: 10px !important;
+ padding-right: 5px !important; /* Espace pour la barre de scroll */
+ gap: 8px !important;
+ -webkit-overflow-scrolling: touch;
+ flex: 1 !important; /* Prend tout l'espace des 30vh */
+ align-items: stretch !important;
+ }
+
+ /* 🎯 Les cartes s'adaptent à 100% de la largeur du téléphone */
+ #unmapped-pool .hol-drag-item {
+ width: 100% !important;
+ height: auto !important;
+ min-height: 50px !important;
+ padding: 8px 10px !important;
+ white-space: normal !important;
+ }
+
+ /* 🗑️ On cache le footer de la modale pour gagner de l'espace */
+ #planningModal .modal-footer {
+ display: none !important;
+ }
+}
diff --git a/modules/holidays/holidays.js b/modules/holidays/holidays.js
index 8c4b099..5e92d44 100644
--- a/modules/holidays/holidays.js
+++ b/modules/holidays/holidays.js
@@ -1,3 +1,5 @@
+window.PLANNING_MODIFIED = false;
+
// ============================================================================
// FONCTION DE TRADUCTION JS & LANGUE COURANTE
// ============================================================================
@@ -528,6 +530,7 @@ function openCheckpointModal(mode, data = null) {
it.item_date || "",
it.item_time || "",
it.duration || 1,
+ it.expense_context || "local",
);
visibleCount++;
}
@@ -594,6 +597,7 @@ function addCpExpenseLine(
itemDate = "",
itemTime = "",
itemDur = 1,
+ expenseContext = "local", // 🔥 FIX : On accepte le contexte dynamique
) {
const container = document.getElementById("cpExpensesContainer");
const div = document.createElement("div");
@@ -607,7 +611,8 @@ function addCpExpenseLine(
-
+
+
@@ -616,7 +621,8 @@ function addCpExpenseLine(
${tr("hdl_paid")}
-
+
+
@@ -679,7 +685,8 @@ function moveStepMobile(btn, direction) {
}
}
-document.addEventListener("DOMContentLoaded", () => {
+// 🔥 NOUVEAU : Fonction réutilisable pour ré-attacher les événements
+function initStepDragAndDrop() {
const checkpoints = document.querySelectorAll(".hol-checkpoint-draggable");
const container = checkpoints[0]?.parentElement;
if (!container) return;
@@ -687,7 +694,17 @@ document.addEventListener("DOMContentLoaded", () => {
const isMobile = window.innerWidth <= 768;
let draggedItem = null;
+ // Nettoyage des anciens écouteurs en clonant les nœuds (Indispensable après un refresh silencieux)
checkpoints.forEach((item) => {
+ const clone = item.cloneNode(true);
+ if (item.parentNode) item.parentNode.replaceChild(clone, item);
+ });
+
+ const freshCheckpoints = document.querySelectorAll(
+ ".hol-checkpoint-draggable",
+ );
+
+ freshCheckpoints.forEach((item) => {
if (isMobile) {
item.removeAttribute("draggable");
return;
@@ -727,16 +744,17 @@ document.addEventListener("DOMContentLoaded", () => {
(closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
- if (offset < 0 && offset > closest.offset) {
+ if (offset < 0 && offset > closest.offset)
return { offset: offset, element: child };
- } else {
- return closest;
- }
+ else return closest;
},
{ offset: Number.NEGATIVE_INFINITY },
).element;
}
-});
+}
+
+// On lance l'initialisation au démarrage de la page
+document.addEventListener("DOMContentLoaded", initStepDragAndDrop);
// ============================================================================
// 7. MOTEUR DRAG & DROP DU PLANNING GLOBAL
@@ -986,8 +1004,12 @@ function openGlobalPlanningModal() {
}
function isDateInStep(targetDate, stepStart, stepEnd) {
- if (!stepStart || !stepEnd) return false;
- return targetDate >= stepStart && targetDate <= stepEnd;
+ if (!stepStart && !stepEnd) return false;
+
+ const start = stepStart || stepEnd;
+ const end = stepEnd || stepStart;
+
+ return targetDate >= start && targetDate <= end;
}
function filterPoolByDate(dateStr) {
@@ -1079,7 +1101,6 @@ function buildDragItemHtml(it) {
? `📍 ${it.step_location}
`
: "";
- // Suppression du calcul de hauteur en ligne, c'est désormais géré via CSS et la variable "--duration"
return `
${visualName}
-
- ${durControls}
+
+
+
+ ${durControls}
+
+
+
+
${noteHtml}
`;
}
+function unplaceItem(e, htmlId) {
+ e.stopPropagation();
+ handleDropLogic(htmlId, "", "");
+}
+
function changeDuration(e, itemId, delta) {
e.stopPropagation();
const itemEl =
@@ -1240,7 +1272,7 @@ function handleDropLogic(htmlId, dateStr, timeStr) {
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
+ // DÉSASSIGNER (Annulation) : Si on replace la carte dans la zone "À placer"
if (dateStr === "" || timeStr === "") {
if (!isVirtual) {
updateItemMemory(realId, { item_date: null, item_time: null });
@@ -1249,14 +1281,23 @@ function handleDropLogic(htmlId, dateStr, timeStr) {
fd.append("item_id", realId);
fd.append("item_date", "");
fd.append("item_time", "");
+
+ // Enregistrement en base + Toast
fetch("/modules/holidays/includes/api/save_checkpoint.php", {
method: "POST",
body: fd,
+ }).then(() => {
+ if (typeof showToast === "function")
+ showToast("📍 Élément remis en attente", "success");
});
}
- // 🔥 FIX #3 : Re-filtrer reconstruit la colonne de gauche dans SON ORDRE D'ORIGINE !
+
+ // 🔥 C'EST LA CLÉ : On enlève physiquement la carte du calendrier !
+ itemEl.remove();
+
filterPoolByDate(window.CURRENT_PLANNING_FILTER_DATE);
recalcAllBadges();
+ window.PLANNING_MODIFIED = true;
return;
}
@@ -1307,13 +1348,35 @@ function handleDropLogic(htmlId, dateStr, timeStr) {
category: "transport",
is_virtual: false,
};
+
+ if (typeof window.MAP_POINTS !== "undefined") {
+ const stepObj = window.MAP_POINTS.find(
+ (s) => s.sort_order == sortOrder,
+ );
+ if (stepObj) {
+ stepObj.items.push({
+ id: data.id,
+ category: "transport",
+ name: `Essence depuis ${tData.from}`,
+ amount: tData.cost,
+ expense_context: "transit",
+ duration: dur,
+ item_date: dateStr,
+ item_time: timeStr,
+ });
+ }
+ }
+
+ if (typeof showToast === "function")
+ showToast("🚗 Trajet généré et sauvegardé !", "success");
+ itemEl.style.transition = "box-shadow 0.3s ease";
+ itemEl.style.boxShadow = "0 0 0 3px var(--success)";
+ setTimeout(() => (itemEl.style.boxShadow = ""), 1500);
} else {
window.location.reload();
}
})
- .catch(() => {
- window.location.reload();
- });
+ .catch(() => window.location.reload());
} else {
updateItemMemory(realId, { item_date: dateStr, item_time: timeStr });
const fd = new FormData();
@@ -1325,10 +1388,22 @@ function handleDropLogic(htmlId, dateStr, timeStr) {
fetch("/modules/holidays/includes/api/save_checkpoint.php", {
method: "POST",
body: fd,
- });
+ })
+ .then(() => {
+ if (typeof showToast === "function")
+ showToast("📅 Activité planifiée !", "success");
+ itemEl.style.transition = "box-shadow 0.3s ease";
+ itemEl.style.boxShadow = "0 0 0 3px var(--success)";
+ setTimeout(() => (itemEl.style.boxShadow = ""), 1500);
+ })
+ .catch(() => {
+ if (typeof showToast === "function")
+ showToast("❌ Erreur de sauvegarde", "error");
+ });
}
recalcAllBadges();
+ silentlyRefreshSteps();
}
function updateItemMemory(itemId, changes) {
@@ -1340,6 +1415,53 @@ function updateItemMemory(itemId, changes) {
}
}
+/**
+ * Rafraîchit uniquement le conteneur des étapes sans recharger toute la page
+ */
+async function silentlyRefreshSteps() {
+ try {
+ const res = await fetch(window.location.href);
+ const html = await res.text();
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, "text/html");
+
+ // 🕵️ CIBLAGE INTELLIGENT : On trouve le panel-body qui contient les étapes (ou le texte "Aucune étape")
+ const getTargetContainer = (documentObj) =>
+ Array.from(documentObj.querySelectorAll(".hol-panel-body")).find(
+ (el) =>
+ el.querySelector(".hol-checkpoint") ||
+ el.innerHTML.toLowerCase().includes("aucune étape") ||
+ el.innerHTML.toLowerCase().includes("cap etapa"),
+ );
+
+ const currentContainer = getTargetContainer(document);
+ const newContainer = getTargetContainer(doc);
+
+ if (currentContainer && newContainer) {
+ // Un léger effet de flash pour indiquer à l'utilisateur que ça s'est mis à jour
+ currentContainer.style.opacity = "0.5";
+
+ setTimeout(() => {
+ currentContainer.innerHTML = newContainer.innerHTML;
+ currentContainer.style.opacity = "1";
+
+ // Mise à jour du prix global de l'essence en haut
+ const globalFuel = document.getElementById("global_fuel_cost");
+ const newGlobalFuel = doc.getElementById("global_fuel_cost");
+ if (globalFuel && newGlobalFuel)
+ globalFuel.innerHTML = newGlobalFuel.innerHTML;
+
+ // 🚀 On relance le Drag & Drop des étapes !
+ initStepDragAndDrop();
+ }, 150);
+ } else {
+ window.location.reload(); // Fallback de sécurité ultime
+ }
+ } catch (e) {
+ console.error("Erreur lors du rafraîchissement silencieux", e);
+ }
+}
+
// ============================================================================
// MÉTÉO SPÉCIFIQUE AU HEADER DU PLANNING
// ============================================================================
@@ -1426,7 +1548,8 @@ function addQuickTransitExpense(
fd.append("amount", amount);
fd.append("context", "transit");
- // 🔥 On sécurise la durée en base de données
+ fd.append("expense_context", "transit");
+
const h = Math.max(1, Math.round(durationSec / 3600));
fd.append("duration", h);
@@ -1778,3 +1901,46 @@ async function saveHolidayGlobalNote(holidayId) {
btn.disabled = false;
}
}
+
+// ============================================================================
+// SOUMISSION AJAX DU FORMULAIRE "MODIFIER LES BASES"
+// ============================================================================
+document.addEventListener("DOMContentLoaded", () => {
+ const holidayForm = document.getElementById("holidayForm");
+ if (holidayForm) {
+ holidayForm.addEventListener("submit", async function (e) {
+ // Si on demande la suppression, on laisse le comportement natif faire le travail
+ if (this.querySelector('input[name="action_delete"]')) return;
+
+ e.preventDefault();
+ const submitBtn = this.querySelector('button[type="submit"]');
+ const originalText = submitBtn.innerHTML;
+ submitBtn.innerHTML = "⏳...";
+ submitBtn.disabled = true;
+
+ const fd = new FormData(this);
+ // On s'assure d'appeler l'API de sauvegarde
+ const actionUrl =
+ this.getAttribute("action") ||
+ "/modules/holidays/includes/api/save_holiday.php";
+
+ try {
+ // On utilise pachaFetch en mode raw/text car save_holiday.php fait sûrement un header('Location: ...') au lieu d'un JSON
+ const response = await fetch(actionUrl, {
+ method: "POST",
+ body: fd,
+ headers: { "X-Requested-With": "XMLHttpRequest" },
+ });
+
+ // Quoi qu'il arrive, on recharge la page COURANTE (detail.php) pour afficher les modifications
+ window.location.reload();
+ } catch (err) {
+ console.error(err);
+ if (typeof showToast === "function")
+ showToast("Erreur lors de la sauvegarde", "error");
+ submitBtn.innerHTML = originalText;
+ submitBtn.disabled = false;
+ }
+ });
+ }
+});
diff --git a/modules/holidays/includes/api/save_checkpoint.php b/modules/holidays/includes/api/save_checkpoint.php
index 85fe61e..e27e1e5 100644
--- a/modules/holidays/includes/api/save_checkpoint.php
+++ b/modules/holidays/includes/api/save_checkpoint.php
@@ -34,25 +34,40 @@ if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime
$date = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
$time = !empty($_POST['item_time']) ? $_POST['item_time'] : null;
- $context = $_POST['expense_context'] ?? ($_POST['context'] ?? 'local');
+ // Récupération sécurisée du contexte
+ $context = !empty($_POST['expense_context']) ? $_POST['expense_context'] : (!empty($_POST['context']) ? $_POST['context'] : 'local');
+ $amount = (float)$_POST['amount'];
+ $name = $_POST['name'] ?? 'Trajet';
+ $category = $_POST['category'] ?? 'transport';
$pdo->beginTransaction();
+ // 🛡️ ANTI-DOUBLON ABSOLU
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]);
+ $stmtCheck = $pdo->prepare("SELECT id FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ? AND expense_context = 'transit'");
+ $stmtCheck->execute([$holiday_id, $sort_order]);
+ $existingId = $stmtCheck->fetchColumn();
+
+ if ($existingId) {
+ // Mise à jour de la ligne existante
+ $stmtUp = $pdo->prepare("UPDATE pf_holidays_items SET item_date = ?, item_time = ?, amount = ?, duration = ?, name = ? WHERE id = ?");
+ $stmtUp->execute([$date, $time, $amount, $dur, $name, $existingId]);
+ $pdo->commit();
+ echo json_encode(['success' => true, 'id' => $existingId]);
+ exit;
+ }
}
+ // Insertion si aucun doublon n'existe
$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,
+ $holiday_id, $category, $name, $amount, 0,
$stepInfo['location_name'], $stepInfo['lat'], $stepInfo['lng'],
$sort_order, $stepInfo['step_start_date'], $stepInfo['step_end_date'],
$stepInfo['step_type'], $context, $dur, $date, $time
]);
$newId = $pdo->lastInsertId();
-
$pdo->commit();
echo json_encode(['success' => true, 'id' => $newId]);
@@ -83,17 +98,6 @@ if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime
exit;
}
- // Mise à jour de la note globale du voyage
- if ($_POST['action'] === 'update_holiday_note') {
- $hId = (int)$_POST['holiday_id'];
- $notes = $_POST['notes'] ?? '';
- $stmt = $pdo->prepare("UPDATE pf_holidays SET notes = ? WHERE id = ?");
- $stmt->execute([$notes, $hId]);
- echo json_encode(['success' => true]);
- exit;
- }
-
-
} catch (Exception $e) {
if ($pdo->inTransaction()) { $pdo->rollBack(); }
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
diff --git a/modules/holidays/views/detail.php b/modules/holidays/views/detail.php
index 8515312..b623fe3 100644
--- a/modules/holidays/views/detail.php
+++ b/modules/holidays/views/detail.php
@@ -22,6 +22,7 @@ $stmt = $pdo->prepare("
LEFT JOIN pf_vehicles v ON h.vehicle_id = v.id
WHERE h.id = ?
");
+
$stmt->execute([$id]);
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);