planning mobil
Deploy HouseHub / deploy (push) Successful in 2s

This commit is contained in:
2026-07-28 16:37:39 +02:00
parent 659b286967
commit ebdbee71db
7 changed files with 388 additions and 140 deletions
+12 -2
View File
@@ -34,7 +34,17 @@ if ($tab === 'holiday_detail' && isset($_GET['id'])) {
require __DIR__ . '/modules/holidays/views/list.php'; require __DIR__ . '/modules/holidays/views/list.php';
} }
// 4. Inclusion du JS global du module (Pont i18n déjà géré dans le header) // 4. Inclusion des librairies globales du module (Flatpickr)
echo '<script src="/modules/holidays/holidays.js"></script>'; ?>
<!-- Flatpickr (Vanilla JS Date Range) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/themes/dark.css" id="flatpickr-dark-theme" disabled>
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/fr.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/cat.js"></script>
<?php
// On ajoute un paramètre de version (time) pour forcer le navigateur à recharger tes récentes modifications dans holidays.js
echo '<script src="/modules/holidays/holidays.js?v=' . time() . '"></script>';
require __DIR__ . '/footer.php'; require __DIR__ . '/footer.php';
+262 -45
View File
@@ -1,5 +1,22 @@
window.PLANNING_MODIFIED = false; window.PLANNING_MODIFIED = false;
// Variable globale pour Flatpickr
let cpDateRangePicker = null;
// Écouteur pour activer le thème sombre de Flatpickr selon le thème global HouseHub
document.addEventListener("DOMContentLoaded", () => {
const checkDarkTheme = () => {
const isDark =
document.documentElement.getAttribute("data-theme") === "dark";
const themeLink = document.getElementById("flatpickr-dark-theme");
if (themeLink) themeLink.disabled = !isDark;
};
checkDarkTheme();
document
.getElementById("theme-toggle")
?.addEventListener("click", () => setTimeout(checkDarkTheme, 50));
});
// ============================================================================ // ============================================================================
// FONCTION DE TRADUCTION JS & LANGUE COURANTE // FONCTION DE TRADUCTION JS & LANGUE COURANTE
// ============================================================================ // ============================================================================
@@ -87,6 +104,11 @@ function openHolidayModal(mode) {
if (mode === "add") { if (mode === "add") {
document.getElementById("modalTitle").innerText = tr("hdl_modal_title"); document.getElementById("modalTitle").innerText = tr("hdl_modal_title");
btnDelete.style.display = "none"; btnDelete.style.display = "none";
// Réinitialisation du calendrier global
document.getElementById("inp_start").value = "";
document.getElementById("inp_end").value = "";
initHolFlatpickr("", "");
} else { } else {
document.getElementById("modalTitle").innerText = tr( document.getElementById("modalTitle").innerText = tr(
"hdl_quick_edit_title", "hdl_quick_edit_title",
@@ -114,18 +136,27 @@ function editHoliday(data) {
document.getElementById("inp_title").value = h.title; document.getElementById("inp_title").value = h.title;
document.getElementById("inp_status").value = h.status; document.getElementById("inp_status").value = h.status;
document.getElementById("inp_period").value = h.period_hint || ""; document.getElementById("inp_period").value = h.period_hint || "";
document.getElementById("inp_start").value = h.start_date || "";
document.getElementById("inp_end").value = h.end_date || "";
document.getElementById("inp_food").value = document.getElementById("inp_food").value =
h.budget_food > 0 ? h.budget_food : ""; h.budget_food > 0 ? h.budget_food : "";
document.getElementById("inp_extra").value = document.getElementById("inp_extra").value =
h.budget_extra > 0 ? h.budget_extra : ""; h.budget_extra > 0 ? h.budget_extra : "";
document.getElementById("inp_notes").value = h.notes || "";
if (document.getElementById("inp_notes")) {
document.getElementById("inp_notes").value = h.notes || "";
}
const vehicleInput = document.getElementById("inp_vehicle_id"); const vehicleInput = document.getElementById("inp_vehicle_id");
if (vehicleInput) { if (vehicleInput) {
vehicleInput.value = h.vehicle_id || ""; vehicleInput.value = h.vehicle_id || "";
} }
// Initialisation des dates
const startD = h.start_date || "";
const endD = h.end_date || "";
document.getElementById("inp_start").value = startD;
document.getElementById("inp_end").value = endD;
initHolFlatpickr(startD, endD);
} }
function deleteHoliday() { function deleteHoliday() {
@@ -458,64 +489,100 @@ function panMapTo(lat, lng) {
// ============================================================================ // ============================================================================
function openCheckpointModal(mode, data = null) { function openCheckpointModal(mode, data = null) {
const searchBlock = document.getElementById("cpSearchBlock"); const searchBlock = document.getElementById("cpSearchBlock");
const formBlock = document.getElementById("formCheckpoint");
const container = document.getElementById("cpExpensesContainer"); const container = document.getElementById("cpExpensesContainer");
const btnDel = document.getElementById("btnDeleteCp"); const btnDel = document.getElementById("btnDeleteCp");
const insertGroup = document.getElementById("cp_insert_group");
const insertSelect = document.getElementById("cp_insert_after");
container.innerHTML = ""; container.innerHTML = "";
if (document.getElementById("cp_start_date"))
document.getElementById("cp_start_date").value = "";
if (document.getElementById("cp_end_date"))
document.getElementById("cp_end_date").value = "";
if (document.getElementById("searchPlaceInput")) if (document.getElementById("searchPlaceInput"))
document.getElementById("searchPlaceInput").value = ""; document.getElementById("searchPlaceInput").value = "";
if (document.getElementById("searchResults")) if (document.getElementById("searchResults"))
document.getElementById("searchResults").innerHTML = ""; document.getElementById("searchResults").innerHTML = "";
searchBlock.style.display = "block"; const holidayData = JSON.parse(
document.getElementById("holidayDataJson").textContent,
).main;
let tripStartDate =
holidayData.start_date || new Date().toISOString().split("T")[0];
// ==========================================
// CONFIGURATION DES DATES FLATPICKR
// ==========================================
let defaultStart = "";
let defaultEnd = "";
if (mode === "add") { if (mode === "add") {
document.getElementById("cpModalTitle").innerText = tr("hdl_btn_add_step"); document.getElementById("cpModalTitle").innerText = tr("hdl_btn_add_step");
formBlock.style.display = "none";
btnDel.style.display = "none"; btnDel.style.display = "none";
searchBlock.style.display = "block";
insertGroup.style.display = "block";
document.getElementById("cp_old_sort_order").value = ""; document.getElementById("cp_old_sort_order").value = "";
document.getElementById("cp_name").value = ""; document.getElementById("cp_name").value = "";
switchCpTab("info");
if (document.getElementById("cp_step_type")) { if (document.getElementById("cp_step_type")) {
document.getElementById("cp_step_type").value = "stop"; document.getElementById("cp_step_type").value = "stop";
toggleStepDates("stop"); toggleStepDates("stop");
} }
if (document.getElementById("cp_set_as_return")) { if (document.getElementById("cp_set_as_return"))
document.getElementById("cp_set_as_return").checked = false; document.getElementById("cp_set_as_return").checked = false;
let lastDate = tripStartDate;
if (window.MAP_POINTS && window.MAP_POINTS.length > 0) {
const lastStep = window.MAP_POINTS[window.MAP_POINTS.length - 1];
lastDate =
lastStep.step_end_date || lastStep.step_start_date || tripStartDate;
} }
insertSelect.innerHTML = `<option value="end" data-enddate="${lastDate}">-- À la fin du voyage --</option>`;
if (window.MAP_POINTS && window.MAP_POINTS.length > 0) {
window.MAP_POINTS.forEach((step) => {
let dateStr = "";
if (
step.step_start_date &&
step.step_end_date &&
step.step_start_date !== step.step_end_date
) {
dateStr = ` (${new Date(step.step_start_date).toLocaleDateString(window.appLang, { day: "2-digit", month: "2-digit" })} > ${new Date(step.step_end_date).toLocaleDateString(window.appLang, { day: "2-digit", month: "2-digit" })})`;
} else if (step.step_start_date) {
dateStr = ` (${new Date(step.step_start_date).toLocaleDateString(window.appLang, { day: "2-digit", month: "2-digit" })})`;
}
insertSelect.innerHTML += `<option value="${step.sort_order}" data-enddate="${step.step_end_date || step.step_start_date || tripStartDate}">Après : ${step.location_name}${dateStr}</option>`;
});
}
defaultStart = lastDate;
defaultEnd = lastDate;
addCpExpenseLine(); addCpExpenseLine();
} else if (mode === "edit" && data) { } else if (mode === "edit" && data) {
document.getElementById("cpModalTitle").innerText = tr("hdl_js_edit_step"); document.getElementById("cpModalTitle").innerText = tr("hdl_js_edit_step");
formBlock.style.display = "block";
btnDel.style.display = "block"; btnDel.style.display = "block";
searchBlock.style.display = "none";
insertGroup.style.display = "none";
switchCpTab("prog");
document.getElementById("cp_lat").value = data.lat; document.getElementById("cp_lat").value = data.lat;
document.getElementById("cp_lng").value = data.lng; document.getElementById("cp_lng").value = data.lng;
document.getElementById("cp_old_sort_order").value = data.sort_order; document.getElementById("cp_old_sort_order").value = data.sort_order;
document.getElementById("cp_name").value = data.location_name; 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 || "";
// 🔥 PRE-REMPLISSAGE DU TYPE D'ÉTAPE ET UI DATES
if (document.getElementById("cp_step_type")) { if (document.getElementById("cp_step_type")) {
const type = data.step_type || "stop"; const type = data.step_type || "stop";
document.getElementById("cp_step_type").value = type; document.getElementById("cp_step_type").value = type;
toggleStepDates(type); toggleStepDates(type);
} }
// 🔥 PRE-REMPLISSAGE DE LA CASE RETOUR BASEE SUR LA GLOBALE
if (document.getElementById("cp_set_as_return")) { if (document.getElementById("cp_set_as_return")) {
document.getElementById("cp_set_as_return").checked = document.getElementById("cp_set_as_return").checked =
window.GLOBAL_RETURN_STEP_ID == data.sort_order; window.GLOBAL_RETURN_STEP_ID == data.sort_order;
} }
defaultStart = data.step_start_date || tripStartDate;
defaultEnd = data.step_end_date || tripStartDate;
if (data.items && data.items.length > 0) { if (data.items && data.items.length > 0) {
let visibleCount = 0; let visibleCount = 0;
data.items.forEach((it) => { data.items.forEach((it) => {
@@ -540,10 +607,137 @@ function openCheckpointModal(mode, data = null) {
addCpExpenseLine(); addCpExpenseLine();
} }
} }
// Destruction et ré-instanciation de Flatpickr pour forcer le saut au bon mois
if (cpDateRangePicker) cpDateRangePicker.destroy();
document.getElementById("cp_start_date").value = defaultStart;
document.getElementById("cp_end_date").value = defaultEnd;
cpDateRangePicker = flatpickr("#cp_date_range", {
mode: "range",
altInput: true, // 💡 NOUVEAU : Crée un champ de présentation séparé
altFormat: "d/m", // 💡 NOUVEAU : Format ultra compact (ex: 15/08)
dateFormat: "Y-m-d", // Format technique (MariaDB) conservé en arrière-plan
defaultDate:
defaultStart && defaultEnd && defaultStart !== defaultEnd
? [defaultStart, defaultEnd]
: [defaultStart],
locale: window.appLang === "ca-ES" ? "cat" : "fr",
onChange: function (selectedDates, dateStr, instance) {
if (selectedDates.length === 2) {
document.getElementById("cp_start_date").value = instance.formatDate(
selectedDates[0],
"Y-m-d",
);
document.getElementById("cp_end_date").value = instance.formatDate(
selectedDates[1],
"Y-m-d",
);
} else if (selectedDates.length === 1) {
document.getElementById("cp_start_date").value = instance.formatDate(
selectedDates[0],
"Y-m-d",
);
document.getElementById("cp_end_date").value = instance.formatDate(
selectedDates[0],
"Y-m-d",
);
} else {
document.getElementById("cp_start_date").value = "";
document.getElementById("cp_end_date").value = "";
}
},
});
document.getElementById("checkpointModal").style.display = "flex"; document.getElementById("checkpointModal").style.display = "flex";
document.body.classList.add("no-scroll"); document.body.classList.add("no-scroll");
} }
// ==========================================
// 🎯 DATES GLOBALES DU VOYAGE (MODALE PRINCIPALE)
// ==========================================
let holDateRangePicker = null;
function initHolFlatpickr(defaultStart, defaultEnd) {
if (holDateRangePicker) holDateRangePicker.destroy();
holDateRangePicker = flatpickr("#hol_date_range", {
mode: "range",
altInput: true,
altFormat: "d/m", // 💡 Format ultra compact (ex: 15/08)
dateFormat: "Y-m-d", // 💡 Le vrai format envoyé au serveur
defaultDate:
defaultStart && defaultEnd && defaultStart !== defaultEnd
? [defaultStart, defaultEnd]
: defaultStart
? [defaultStart]
: [],
locale: window.appLang === "ca-ES" ? "cat" : "fr",
onChange: function (selectedDates, dateStr, instance) {
if (selectedDates.length === 2) {
document.getElementById("inp_start").value = instance.formatDate(
selectedDates[0],
"Y-m-d",
);
document.getElementById("inp_end").value = instance.formatDate(
selectedDates[1],
"Y-m-d",
);
} else if (selectedDates.length === 1) {
document.getElementById("inp_start").value = instance.formatDate(
selectedDates[0],
"Y-m-d",
);
document.getElementById("inp_end").value = instance.formatDate(
selectedDates[0],
"Y-m-d",
);
} else {
document.getElementById("inp_start").value = "";
document.getElementById("inp_end").value = "";
}
},
});
}
function switchCpTab(tabId) {
const btnInfo = document.getElementById("tabBtnInfo");
const btnProg = document.getElementById("tabBtnProg");
const tabInfo = document.getElementById("cpTabInfo");
const tabProg = document.getElementById("cpTabProg");
if (tabId === "info") {
btnInfo.style.borderBottomColor = "var(--primary)";
btnInfo.style.color = "var(--primary)";
btnProg.style.borderBottomColor = "transparent";
btnProg.style.color = "var(--text-muted)";
tabInfo.style.display = "block";
tabProg.style.display = "none";
} else {
btnProg.style.borderBottomColor = "var(--primary)";
btnProg.style.color = "var(--primary)";
btnInfo.style.borderBottomColor = "transparent";
btnInfo.style.color = "var(--text-muted)";
tabProg.style.display = "block";
tabInfo.style.display = "none";
}
}
function injectDynamicDates(selectEl) {
const selectedOpt = selectEl.options[selectEl.selectedIndex];
if (selectedOpt && selectedOpt.dataset.enddate) {
const dateToSet = selectedOpt.dataset.enddate;
document.getElementById("cp_start_date").value = dateToSet;
document.getElementById("cp_end_date").value = dateToSet;
// On met à jour l'UI du calendrier
if (cpDateRangePicker) {
cpDateRangePicker.setDate([dateToSet, dateToSet]);
}
}
}
function searchPlace() { function searchPlace() {
const q = document.getElementById("searchPlaceInput").value.trim(); const q = document.getElementById("searchPlaceInput").value.trim();
if (q.length < 3) return; if (q.length < 3) return;
@@ -597,34 +791,41 @@ function addCpExpenseLine(
itemDate = "", itemDate = "",
itemTime = "", itemTime = "",
itemDur = 1, itemDur = 1,
expenseContext = "local", // 🔥 FIX : On accepte le contexte dynamique expenseContext = "local",
) { ) {
const container = document.getElementById("cpExpensesContainer"); const container = document.getElementById("cpExpensesContainer");
const div = document.createElement("div"); const div = document.createElement("div");
div.className = "hol-form-row"; div.className = "hol-form-row";
const isChecked = isPaid == 1 ? "checked" : ""; const isChecked = isPaid == 1 ? "checked" : "";
// 💡 Nouveaux types (Les valeurs à 0 par défaut pour les visites gratuites)
let defaultAmount = amount;
if (category === "visit_free" && amount === "") defaultAmount = "0.00";
div.innerHTML = ` div.innerHTML = `
<div class="hol-form-inner"> <div class="hol-form-inner">
<select name="items[cat][]" class="pf-input hol-form-select"> <select name="items[cat][]" class="pf-input hol-form-select" style="width:55px; padding:6px; font-size:1.1rem;" title="Catégorie">
<option value="accommodation" ${category === "accommodation" ? "selected" : ""}>🏨</option> <option value="accommodation" ${category === "accommodation" ? "selected" : ""}>🏨</option>
<option value="transport" ${category === "transport" ? "selected" : ""}>🚗</option> <option value="transport" ${category === "transport" ? "selected" : ""}>🚗</option>
<option value="food" ${category === "food" ? "selected" : ""}>🍽️</option>
<option value="activity" ${category === "activity" ? "selected" : ""}>🎫</option> <option value="activity" ${category === "activity" ? "selected" : ""}>🎫</option>
<option value="visit_free" ${category === "visit_free" ? "selected" : ""}>🏞️</option>
<option value="other" ${category === "other" ? "selected" : ""}>🛍️</option>
</select> </select>
<!-- 🔥 FIX : Le champ hidden prend la vraie valeur -->
<input type="hidden" name="items[context][]" value="${expenseContext}"> <input type="hidden" name="items[context][]" value="${expenseContext}">
<input type="text" name="items[name][]" class="pf-input hol-form-text" placeholder="${tr("hdl_js_ph_expense_name")}" value="${name}"> <input type="text" name="items[name][]" class="pf-input hol-form-text" placeholder="Description (Ex: Musée, Restaurant...)" value="${name}">
<input type="number" step="0.01" name="items[amount][]" class="pf-input hol-form-number" placeholder="0.00" value="${amount}"> <input type="number" step="0.01" name="items[amount][]" class="pf-input hol-form-number" placeholder="0.00" value="${defaultAmount}">
<label class="hol-form-paid-label" title="${tr("hdl_paid")}"> <label class="hol-form-paid-label" title="${tr("hdl_paid")}">
<input type="checkbox" ${isChecked} onchange="this.nextElementSibling.value = this.checked ? 1 : 0"> <input type="checkbox" ${isChecked} onchange="this.nextElementSibling.value = this.checked ? 1 : 0" style="accent-color:var(--success); width:16px; height:16px;">
<input type="hidden" name="items[paid][]" value="${isPaid}"> <input type="hidden" name="items[paid][]" value="${isPaid}">
<span class="hol-form-paid-text">${tr("hdl_paid")}</span> <span class="hol-form-paid-text" style="display:none;">${tr("hdl_paid")}</span>
</label> </label>
<button type="button" class="btn-icon-action delete btn-remove-expense" onclick="this.parentElement.parentElement.remove()" title="${tr("btn_delete")}">🗑️</button> <button type="button" class="btn-icon-action delete btn-remove-expense" onclick="this.parentElement.parentElement.remove()" title="${tr("btn_delete")}">🗑️</button>
</div> </div>
<div class="hol-form-subrow"> <div class="hol-form-subrow" style="padding-left:0; margin-top:4px;">
<input type="text" name="items[notes][]" class="pf-input hol-form-notes-input hol-form-notes-full" placeholder="${tr("hdl_ph_notes")}" value="${notes}"> <input type="text" name="items[notes][]" class="pf-input hol-form-notes-input hol-form-notes-full" placeholder="🔗 Réservation ou notes..." value="${notes}" style="margin-left:0; width:100%; border-radius:6px;">
</div> </div>
<input type="hidden" name="items[id][]" value="${itemId}"> <input type="hidden" name="items[id][]" value="${itemId}">
<input type="hidden" name="items[date][]" value="${itemDate}"> <input type="hidden" name="items[date][]" value="${itemDate}">
@@ -1065,15 +1266,24 @@ function recalcAllBadges() {
} }
function buildDragItemHtml(it) { function buildDragItemHtml(it) {
// Mapping intelligent des nouvelles icônes
let icon = "🏷️"; let icon = "🏷️";
let catClass = "cat-activity"; let catClass = "cat-activity";
if (it.category === "accommodation") { if (it.category === "accommodation") {
icon = "🏨"; icon = "🏨";
catClass = "cat-accommodation"; catClass = "cat-accommodation";
} } else if (it.category === "transport") {
if (it.category === "transport") {
icon = "🚗"; icon = "🚗";
catClass = "cat-transport"; catClass = "cat-transport";
} else if (it.category === "food") {
icon = "🍽️";
} else if (it.category === "visit_free") {
icon = "🏞️";
} else if (it.category === "activity") {
icon = "🎫";
} else if (it.category === "other") {
icon = "🛍️";
} }
const dur = it.duration || 1; const dur = it.duration || 1;
@@ -1096,28 +1306,38 @@ function buildDragItemHtml(it) {
const isMobile = window.innerWidth <= 768; const isMobile = window.innerWidth <= 768;
const dragAttr = isMobile ? "" : 'draggable="true"'; const dragAttr = isMobile ? "" : 'draggable="true"';
const htmlId = isVirtual ? it.id : `drag-item-${it.id}`; const htmlId = isVirtual ? it.id : `drag-item-${it.id}`;
const locHint =
!it.item_date && it.step_location // 💡 NOUVEAU : Affichage compact des dates associées pour faciliter le placement
? `<div style="font-size:0.65rem; color:var(--primary); font-weight:800; margin-bottom:4px; text-transform:uppercase;">📍 ${it.step_location}</div>` let locHintHtml = "";
if (!it.item_date) {
const datesStr =
it.step_start_date &&
it.step_end_date &&
it.step_start_date !== it.step_end_date
? ` <span style="color:var(--text-muted); text-transform:none;">(${new Date(it.step_start_date).toLocaleDateString(currentLang, { day: "2-digit", month: "2-digit" })} > ${new Date(it.step_end_date).toLocaleDateString(currentLang, { day: "2-digit", month: "2-digit" })})</span>`
: it.step_start_date
? ` <span style="color:var(--text-muted); text-transform:none;">(${new Date(it.step_start_date).toLocaleDateString(currentLang, { day: "2-digit", month: "2-digit" })})</span>`
: "";
locHintHtml = it.step_location
? `<div style="font-size:0.65rem; color:var(--primary); font-weight:800; margin-bottom:4px; text-transform:uppercase;">📍 ${it.step_location}${datesStr}</div>`
: ""; : "";
}
return ` return `
<div class="hol-drag-item ${catClass}" ${dragAttr} <div class="hol-drag-item ${catClass}" ${dragAttr}
id="${htmlId}" data-id="${it.id}" data-virtual="${isVirtual}" data-sort="${it.sort_order}" id="${htmlId}" data-id="${it.id}" data-virtual="${isVirtual}" data-sort="${it.sort_order}"
style="--duration: ${dur}; flex-shrink: 0; ${bgStyle} padding: 8px 10px; border-radius: 6px; cursor: grab; box-shadow: 0 2px 4px rgba(0,0,0,0.05); transition: transform 0.2s; z-index: 10;" style="--duration: ${dur}; flex-shrink: 0; ${bgStyle} padding: 8px 10px; border-radius: 6px; cursor: grab; box-shadow: 0 2px 4px rgba(0,0,0,0.05); transition: transform 0.2s; z-index: 10;"
ondragstart="dragStart(event)" ondragend="dragEnd(event)" onclick="handleItemTap(event, '${htmlId}')"> ondragstart="dragStart(event)" ondragend="dragEnd(event)" onclick="handleItemTap(event, '${htmlId}')">
${locHint} ${locHintHtml}
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 8px;"> <div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 8px;">
<div class="hol-drag-title" style="flex:1; font-size: 0.85rem; font-weight: 700; color: var(--text-main); line-height:1.2;">${visualName}</div> <div class="hol-drag-title" style="flex:1; font-size: 0.85rem; font-weight: 700; color: var(--text-main); line-height:1.2;">${visualName}</div>
<div style="display:flex; align-items:center;"> <div style="display:flex; align-items:center;">
<div class="hol-item-duration-controls" style="display:flex; align-items:center; background:var(--bg-subtle); border-radius:4px; border:1px solid var(--border-light);"> <div class="hol-item-duration-controls" style="display:flex; align-items:center; background:var(--bg-subtle); border-radius:4px; border:1px solid var(--border-light);">
${durControls} ${durControls}
</div> </div>
<!-- 🔄 Le fameux bouton d'annulation (Géré par CSS) -->
<button type="button" class="hol-unplace-btn" onclick="unplaceItem(event, '${htmlId}')" title="Retirer du planning">↩️</button> <button type="button" class="hol-unplace-btn" onclick="unplaceItem(event, '${htmlId}')" title="Retirer du planning">↩️</button>
</div> </div>
</div> </div>
${noteHtml} ${noteHtml}
</div> </div>
@@ -1493,18 +1713,15 @@ async function loadWeatherForPlanning(lat, lng, dateStr) {
// Gère l'affichage des dates dans la modale d'étape // Gère l'affichage des dates dans la modale d'étape
function toggleStepDates(type) { function toggleStepDates(type) {
const grpEnd = document.getElementById("grp_end_date"); const dateLabel = document.getElementById("lbl_date_range");
const lblStart = document.getElementById("lbl_start_date"); if (!dateLabel) return;
if (type === "origin") { if (type === "origin") {
grpEnd.style.display = "none"; dateLabel.innerText = "🛫 Date de départ";
lblStart.innerText = "📅 Date de départ";
} else if (type === "destination") { } else if (type === "destination") {
grpEnd.style.display = "none"; dateLabel.innerText = "🛬 Date d'arrivée finale";
lblStart.innerText = "📅 Date d'arrivée";
} else { } else {
grpEnd.style.display = "block"; dateLabel.innerText = "📅 Période de l'étape (Arrivée ➔ Départ)";
lblStart.innerText = tr("hdl_label_arrival");
} }
} }
@@ -129,10 +129,19 @@ if ($holiday_id > 0 && !empty($location_name)) {
$pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ?")->execute([$holiday_id, $old_sort_order]); $pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ?")->execute([$holiday_id, $old_sort_order]);
$target_order = $old_sort_order; $target_order = $old_sort_order;
} else { } else {
$stmtMax = $pdo->prepare("SELECT MAX(sort_order) FROM pf_holidays_items WHERE holiday_id = ?"); // NOUVEAU : Logique d'intercalage d'étape
$stmtMax->execute([$holiday_id]); $insert_after = $_POST['insert_after'] ?? 'end';
$max = $stmtMax->fetchColumn();
$target_order = ($max !== null) ? (int)$max + 1 : 0; if ($insert_after === 'end') {
$stmtMax = $pdo->prepare("SELECT MAX(sort_order) FROM pf_holidays_items WHERE holiday_id = ?");
$stmtMax->execute([$holiday_id]);
$max = $stmtMax->fetchColumn();
$target_order = ($max !== null) ? (int)$max + 1 : 0;
} else {
$target_order = (int)$insert_after + 1;
// On décale toutes les étapes suivantes vers le bas
$pdo->prepare("UPDATE pf_holidays_items SET sort_order = sort_order + 1 WHERE holiday_id = ? AND sort_order >= ?")->execute([$holiday_id, $target_order]);
}
} }
$step_start = !empty($_POST['step_start_date']) ? $_POST['step_start_date'] : null; $step_start = !empty($_POST['step_start_date']) ? $_POST['step_start_date'] : null;
+81 -70
View File
@@ -66,8 +66,9 @@ $mapPoints = array_values($steps);
// Affichage de la date // Affichage de la date
$dateDisplay = htmlspecialchars($holiday['period_hint'] ?? ''); $dateDisplay = htmlspecialchars($holiday['period_hint'] ?? '');
if (empty($dateDisplay) && $holiday['start_date']) { if (empty($dateDisplay) && $holiday['start_date']) {
$dateDisplay = date('d/m/Y', strtotime($holiday['start_date'])); // 💡 Format Jour/Mois uniquement
if ($holiday['end_date']) $dateDisplay .= ' → ' . date('d/m/Y', strtotime($holiday['end_date'])); $dateDisplay = date('d/m', strtotime($holiday['start_date']));
if ($holiday['end_date']) $dateDisplay .= ' → ' . date('d/m', strtotime($holiday['end_date']));
} }
$cost = (float)$holiday['total_cost']; $cost = (float)$holiday['total_cost'];
@@ -331,89 +332,99 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<div id="checkpointModal" class="pf-modal"> <div id="checkpointModal" class="pf-modal">
<div class="pf-modal-content" style="max-width: 600px;"> <div class="pf-modal-content" style="max-width: 600px; padding: 0; overflow: hidden;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 id="cpModalTitle" style="margin:0;">📍 <?= tr('hdl_btn_add_step') ?></h3> <!-- HEADER MODALE -->
<button type="button" onclick="closeCheckpointModal()" class="pf-modal-close">&times;</button> <div style="display:flex; justify-content:space-between; align-items:center; padding: 20px 20px 0 20px;">
<h3 id="cpModalTitle" style="margin:0; font-size:1.3rem;">📍 Ajouter une étape</h3>
<button type="button" onclick="closeCheckpointModal()" class="pf-modal-close" style="margin-top:-10px;">&times;</button>
</div>
<!-- NOUVEAU : SYSTÈME D'ONGLETS -->
<div style="display:flex; border-bottom: 1px solid var(--border-light); margin-top: 15px; padding: 0 20px;">
<button type="button" id="tabBtnInfo" onclick="switchCpTab('info')" style="flex:1; padding:10px; background:none; border:none; border-bottom:3px solid var(--primary); font-weight:bold; color:var(--primary); cursor:pointer; font-size:0.95rem;">📍 Infos de l'étape</button>
<button type="button" id="tabBtnProg" onclick="switchCpTab('prog')" style="flex:1; padding:10px; background:none; border:none; border-bottom:3px solid transparent; font-weight:bold; color:var(--text-muted); cursor:pointer; font-size:0.95rem;">🎟️ Programme & Frais</button>
</div> </div>
<div id="cpSearchBlock" style="margin-bottom:20px;"> <!-- DÉBUT DU FORMULAIRE UNIQUE -->
<?php if (!empty($favorites)): ?> <form action="/modules/holidays/includes/api/save_checkpoint.php" method="POST" id="formCheckpoint" style="display:flex; flex-direction:column; max-height: calc(90vh - 100px);">
<div style="margin-bottom:15px; display:flex; gap:8px; flex-wrap:wrap;">
<?php foreach($favorites as $fav): ?>
<button type="button" class="pf-btn btn-secondary" style="padding:4px 10px; font-size:0.8rem; border-radius:20px;" onclick="selectPlace(<?= $fav['lat'] ?>, <?= $fav['lng'] ?>, '<?= htmlspecialchars(addslashes($fav['name'])) ?>')">
⭐ <?= htmlspecialchars($fav['name']) ?>
</button>
<?php endforeach; ?>
</div>
<?php endif; ?>
<label class="pf-label"><?= tr('hdl_search_location') ?></label>
<div style="display:flex; gap:10px;">
<input type="text" id="searchPlaceInput" class="pf-input" placeholder="<?= tr('hdl_ph_search') ?>" onkeypress="if(event.key === 'Enter') { searchPlace(); return false; }">
<button type="button" class="pf-btn btn-secondary" onclick="searchPlace()">🔍</button>
</div>
<div id="searchResults" style="margin-top:10px; max-height:200px; overflow-y:auto;"></div>
</div>
<form action="/modules/holidays/includes/api/save_checkpoint.php" method="POST" id="formCheckpoint" style="display:none; border-top:1px solid #e2e8f0; padding-top:20px;">
<input type="hidden" name="holiday_id" value="<?= $id ?>"> <input type="hidden" name="holiday_id" value="<?= $id ?>">
<input type="hidden" name="old_location_name" id="cp_old_name"> <input type="hidden" name="old_location_name" id="cp_old_name">
<input type="hidden" name="lat" id="cp_lat"> <input type="hidden" name="lat" id="cp_lat">
<input type="hidden" name="lng" id="cp_lng"> <input type="hidden" name="lng" id="cp_lng">
<input type="hidden" name="old_sort_order" id="cp_old_sort_order"> <input type="hidden" name="old_sort_order" id="cp_old_sort_order">
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label"><?= tr('hdl_label_step_name') ?></label>
<input type="text" name="location_name" id="cp_name" class="pf-input" style="font-weight:bold; color:var(--primary);" required>
</div>
<div class="form-group" style="margin-bottom:15px; background:#fff7ed; padding:10px; border-radius:8px; border:1px solid #ffedd5;"> <!-- ONGLET 1 : INFOS GÉNÉRIQUES -->
<label class="pf-label" style="color:#ea580c;">📍 Type d'étape</label> <div id="cpTabInfo" style="padding: 20px; overflow-y: auto;">
<select name="step_type" id="cp_step_type" class="pf-input" onchange="toggleStepDates(this.value)">
<option value="origin">DÉPART (Point de départ du voyage)</option> <div id="cpSearchBlock" style="margin-bottom:15px; background:var(--bg-subtle); padding:12px; border-radius:8px; border: 1px dashed var(--border-strong);">
<option value="stop">SÉJOUR (Étape classique avec arrivée et départ)</option> <label class="pf-label" style="margin-bottom:4px; font-size:0.8rem;">🔍 Chercher un lieu (Autocomplétion)</label>
<option value="destination">ARRIVÉE FINALE (Fin du voyage)</option> <div style="display:flex; gap:8px;">
</select> <input type="text" id="searchPlaceInput" class="pf-input" placeholder="Ville, hôtel, adresse..." onkeypress="if(event.key === 'Enter') { searchPlace(); return false; }">
</div> <button type="button" class="pf-btn btn-secondary" onclick="searchPlace()" style="padding: 0 15px;">Go</button>
</div>
<div style="display:flex; gap:15px; margin-bottom:15px; background:#f8fafc; padding:12px; border-radius:8px;"> <div id="searchResults" style="margin-top:8px; max-height:150px; overflow-y:auto; display:flex; flex-direction:column; gap:4px;"></div>
<div class="form-group" id="grp_start_date" style="flex:1;">
<label class="pf-label" id="lbl_start_date"><?= tr('hdl_label_arrival') ?></label>
<input type="date" name="step_start_date" id="cp_start_date" class="pf-input">
</div> </div>
<div class="form-group" id="grp_end_date" style="flex:1;">
<label class="pf-label" id="lbl_end_date"><?= tr('hdl_label_departure') ?></label> <div class="form-group" style="margin-bottom:15px;">
<input type="date" name="step_end_date" id="cp_end_date" class="pf-input"> <label class="pf-label">Nom de l'étape</label>
<input type="text" name="location_name" id="cp_name" class="pf-input" style="font-weight:bold; color:var(--primary); font-size:1.1rem;" required>
</div>
<!-- DATES COMPACTES AVEC FLATPICKR -->
<div class="form-group" style="margin-bottom:15px; background:var(--bg-subtle); padding:12px; border-radius:8px; border: 1px dashed var(--border-strong);">
<label class="pf-label" id="lbl_date_range" style="margin-bottom:8px;">📅 Période de l'étape (Arrivée ➔ Départ)</label>
<input type="text" id="cp_date_range" class="pf-input" placeholder="Sélectionnez les dates..." readonly style="cursor: pointer; background-color: var(--bg-panel); color: var(--primary); font-weight: bold; text-align: center; letter-spacing: 0.5px;">
<!-- Les vraies valeurs envoyées au serveur -->
<input type="hidden" name="step_start_date" id="cp_start_date">
<input type="hidden" name="step_end_date" id="cp_end_date">
</div>
<div class="form-group" id="cp_insert_group" style="margin-bottom:15px;">
<label class="pf-label">🔽 Où placer cette étape ?</label>
<select name="insert_after" id="cp_insert_after" class="pf-input" onchange="injectDynamicDates(this)">
<!-- Rempli dynamiquement en JS avec Noms + Dates -->
</select>
</div>
<div style="display:flex; gap:10px;">
<div class="form-group" style="flex:1; margin:0;">
<label class="pf-label">📍 Type d'étape</label>
<select name="step_type" id="cp_step_type" class="pf-input" onchange="toggleStepDates(this.value)">
<option value="origin">DÉPART</option>
<option value="stop" selected>SÉJOUR</option>
<option value="destination">ARRIVÉE FINALE</option>
</select>
</div>
</div>
<div style="margin-top: 15px; display:flex; flex-direction:column; gap:8px;">
<label style="display:flex; align-items:center; cursor:pointer; font-size:0.85rem; color:var(--text-main);">
<input type="checkbox" name="set_as_return" id="cp_set_as_return" value="1" style="margin-right:8px; width:16px; height:16px; accent-color:#ea580c;">
🏁 Définir comme étape de retour (Tracé Orange)
</label>
<label style="display:flex; align-items:center; cursor:pointer; font-size:0.85rem; color:var(--text-main);">
<input type="checkbox" name="save_favorite" value="1" style="margin-right:8px; width:16px; height:16px;">
⭐ Sauvegarder dans mes favoris
</label>
</div> </div>
</div> </div>
<div style="margin-bottom: 20px; padding-top: 15px; border-top: 1px dashed #e2e8f0;"> <!-- ONGLET 2 : PROGRAMME ET DEPENSES -->
<label style="display:flex; align-items:center; cursor:pointer; color:#ea580c; font-weight:600; font-size:0.9rem;"> <div id="cpTabProg" style="display:none; padding: 20px; overflow-y: auto;">
<input type="checkbox" name="set_as_return" id="cp_set_as_return" value="1" style="margin-right:8px; width:16px; height:16px;"> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; position:sticky; top:0; background:var(--bg-panel); z-index:10; padding-bottom:10px; border-bottom:1px solid var(--border-light);">
🏁 Définir comme retour <label class="pf-label" style="margin:0;">🎟️ Liste des activités & frais</label>
</label> <button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="addCpExpenseLine()">+ Ajouter</button>
<p style="margin: 4px 0 0 24px; font-size: 0.75rem; color: #64748b;">La route sera tracée en orange à partir d'ici.</p> </div>
<div id="cpExpensesContainer" style="display:flex; flex-direction:column; gap:10px;"></div>
</div> </div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;"> <!-- FOOTER FIXE -->
<label class="pf-label" style="margin:0;"><?= tr('hdl_planned_expenses') ?></label> <div class="modal-footer" style="padding: 15px 20px; border-top:1px solid var(--border-light); background:var(--bg-page); margin-top:auto;">
<button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="addCpExpenseLine()"><?= tr('hdl_btn_add_expense') ?></button> <button type="button" onclick="deleteCheckpoint()" id="btnDeleteCp" class="pf-btn btn-secondary" style="color:var(--danger); border-color:var(--border-danger-soft); display:none; margin-right:auto;">Supprimer</button>
</div>
<div id="cpExpensesContainer" style="margin-bottom:15px;"></div>
<div style="margin-bottom: 20px;">
<label style="display:flex; align-items:center; cursor:pointer; font-size:0.85rem; color:#475569;">
<input type="checkbox" name="save_favorite" value="1" style="margin-right:8px;">
⭐ <?= tr('hdl_save_fav') ?>
</label>
</div>
<div class="modal-footer" style="padding-top:15px; border-top:1px solid #e2e8f0;">
<button type="button" onclick="deleteCheckpoint()" id="btnDeleteCp" class="pf-btn btn-secondary" style="color:#ef4444; border-color:#fca5a5; display:none;"><?= tr('btn_delete') ?></button>
<button type="button" onclick="closeCheckpointModal()" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button> <button type="button" onclick="closeCheckpointModal()" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button> <button type="submit" class="pf-btn">💾 <?= tr('btn_save') ?></button>
</div> </div>
</form> </form>
</div> </div>
+6 -3
View File
@@ -122,10 +122,12 @@ window.closeHolidayModal = window.closeHolidayModal || function() {
if(modal) modal.style.display = 'none'; if(modal) modal.style.display = 'none';
document.body.classList.remove('no-scroll'); document.body.classList.remove('no-scroll');
}; };
</script>
</script>
<script src="/modules/holidays/holidays.js"></script> <script src="/modules/holidays/holidays.js"></script>
<?php <?php
function renderHolidayCard($h, $pdo) { function renderHolidayCard($h, $pdo) {
$stmt = $pdo->prepare("SELECT * FROM pf_holidays_items WHERE holiday_id = ?"); $stmt = $pdo->prepare("SELECT * FROM pf_holidays_items WHERE holiday_id = ?");
@@ -136,8 +138,9 @@ function renderHolidayCard($h, $pdo) {
$dateDisplay = htmlspecialchars($h['period_hint'] ?? ''); $dateDisplay = htmlspecialchars($h['period_hint'] ?? '');
if (empty($dateDisplay) && $h['start_date']) { if (empty($dateDisplay) && $h['start_date']) {
$dateDisplay = date('d/m/Y', strtotime($h['start_date'])); // 💡 Format Jour/Mois uniquement
if ($h['end_date']) $dateDisplay .= ' → ' . date('d/m/Y', strtotime($h['end_date'])); $dateDisplay = date('d/m', strtotime($h['start_date']));
if ($h['end_date']) $dateDisplay .= ' → ' . date('d/m', strtotime($h['end_date']));
} }
$statusClass = match($h['status']) { $statusClass = match($h['status']) {
+13 -15
View File
@@ -1,5 +1,6 @@
<div id="holidayModal" class="pf-modal"> <div id="holidayModal" class="pf-modal">
<div class="pf-modal-content hol-modal-content" style="max-width: 500px;"> <h3 id="modalTitle" class="pf-modal-title"><?= tr('hdl_modal_title') ?></h3> <div class="pf-modal-content hol-modal-content" style="max-width: 500px;">
<h3 id="modalTitle" class="pf-modal-title"><?= tr('hdl_modal_title') ?></h3>
<form action="/modules/holidays/includes/api/save_holiday.php" method="POST" id="holidayForm"> <form action="/modules/holidays/includes/api/save_holiday.php" method="POST" id="holidayForm">
<input type="hidden" name="id" id="inp_id"> <input type="hidden" name="id" id="inp_id">
@@ -23,23 +24,21 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<div> <div class="hol-flex-1">
<label class="pf-label"><?= tr('hdl_label_period') ?></label> <label class="pf-label"><?= tr('hdl_label_period') ?></label>
<input type="text" name="period_hint" id="inp_period" class="pf-input" placeholder="<?= tr('hdl_ph_period') ?>"> <input type="text" name="period_hint" id="inp_period" class="pf-input" placeholder="<?= tr('hdl_ph_period') ?>">
</div> </div>
<div class="hol-date-range-group"> <!-- DATES AVEC FLATPICKR -->
<div class="hol-flex-1"> <div class="hol-flex-2">
<label class="pf-label"><?= tr('hdl_label_from') ?></label> <label class="pf-label">📅 Dates du voyage</label>
<input type="date" name="start_date" id="inp_start" class="pf-input"> <input type="text" id="hol_date_range" class="pf-input" placeholder="Sélectionnez les dates..." readonly style="cursor: pointer; background-color: var(--bg-panel); color: var(--primary); font-weight: bold; text-align: center; letter-spacing: 0.5px;">
</div> <!-- Vraies données pour save_holiday.php -->
<div class="hol-flex-1"> <input type="hidden" name="start_date" id="inp_start">
<label class="pf-label"><?= tr('hdl_label_to') ?></label> <input type="hidden" name="end_date" id="inp_end">
<input type="date" name="end_date" id="inp_end" class="pf-input">
</div>
</div> </div>
</div> </div>
<div class="form-group" style="margin-top: 15px; padding: 10px; background: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0;"> <div class="form-group" style="margin-top: 15px; padding: 10px; background: var(--bg-subtle); border-radius: 8px; border: 1px solid var(--border-light);">
<label class="pf-label" style="margin-bottom: 5px;">🚗 Véhicule utilisé (Optionnel)</label> <label class="pf-label" style="margin-bottom: 5px;">🚗 Véhicule utilisé (Optionnel)</label>
<select name="vehicle_id" id="inp_vehicle_id" class="pf-input"> <select name="vehicle_id" id="inp_vehicle_id" class="pf-input">
<option value="">-- Aucun / Autre transport --</option> <option value="">-- Aucun / Autre transport --</option>
@@ -52,17 +51,16 @@
<hr class="hol-divider"> <hr class="hol-divider">
<div class="form-row"> <div class="form-row">
<div> <div class="hol-flex-1">
<label class="pf-label">🍔 <?= tr('hdl_label_budget_food') ?></label> <label class="pf-label">🍔 <?= tr('hdl_label_budget_food') ?></label>
<input type="number" step="0.01" name="budget_food" id="inp_food" class="pf-input" placeholder="0.00"> <input type="number" step="0.01" name="budget_food" id="inp_food" class="pf-input" placeholder="0.00">
</div> </div>
<div> <div class="hol-flex-1">
<label class="pf-label">🎁 <?= tr('hdl_label_budget_extras') ?></label> <label class="pf-label">🎁 <?= tr('hdl_label_budget_extras') ?></label>
<input type="number" step="0.01" name="budget_extra" id="inp_extra" class="pf-input" placeholder="0.00"> <input type="number" step="0.01" name="budget_extra" id="inp_extra" class="pf-input" placeholder="0.00">
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" onclick="deleteHoliday()" id="btn_delete" class="pf-btn btn-secondary hol-btn-delete"><?= tr('btn_delete') ?></button> <button type="button" onclick="deleteHoliday()" id="btn_delete" class="pf-btn btn-secondary hol-btn-delete"><?= tr('btn_delete') ?></button>
<button type="button" onclick="closeHolidayModal()" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button> <button type="button" onclick="closeHolidayModal()" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
+1 -1
View File
@@ -19,7 +19,7 @@ $grandTotal = $totalGeneral + $totalSteps;
<h1 style="font-size: 2.5rem; color: #0f172a; margin-bottom: 10px;"><?= htmlspecialchars($holiday['title'] ?? $holiday['name'] ?? 'Mon Voyage') ?></h1> <h1 style="font-size: 2.5rem; color: #0f172a; margin-bottom: 10px;"><?= htmlspecialchars($holiday['title'] ?? $holiday['name'] ?? 'Mon Voyage') ?></h1>
<h2 style="font-size: 1.5rem; color: #64748b; font-weight: normal; margin-top: 0;"> <h2 style="font-size: 1.5rem; color: #64748b; font-weight: normal; margin-top: 0;">
<?php if (!empty($holiday['start_date']) && !empty($holiday['end_date'])): ?> <?php if (!empty($holiday['start_date']) && !empty($holiday['end_date'])): ?>
Du <?= date('d/m/Y', strtotime($holiday['start_date'])) ?> au <?= date('d/m/Y', strtotime($holiday['end_date'])) ?> Du <?= date('d/m', strtotime($holiday['start_date'])) ?> au <?= date('d/m', strtotime($holiday['end_date'])) ?>
<?php else: ?> <?php else: ?>
Dates à définir Dates à définir
<?php endif; ?> <?php endif; ?>