+152
-18
@@ -1124,24 +1124,6 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
overflow: hidden;
|
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 */
|
/* Zone des activités non planifiées */
|
||||||
.hol-unmapped-zone {
|
.hol-unmapped-zone {
|
||||||
@@ -1343,6 +1325,34 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
|
|||||||
font-weight: 700;
|
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 {
|
.highlight-step {
|
||||||
transition:
|
transition:
|
||||||
box-shadow 0.3s,
|
box-shadow 0.3s,
|
||||||
@@ -1639,3 +1649,127 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
|
|||||||
.hol-expense-price-group .status-pending {
|
.hol-expense-price-group .status-pending {
|
||||||
line-height: 1;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+183
-17
@@ -1,3 +1,5 @@
|
|||||||
|
window.PLANNING_MODIFIED = false;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// FONCTION DE TRADUCTION JS & LANGUE COURANTE
|
// FONCTION DE TRADUCTION JS & LANGUE COURANTE
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -528,6 +530,7 @@ function openCheckpointModal(mode, data = null) {
|
|||||||
it.item_date || "",
|
it.item_date || "",
|
||||||
it.item_time || "",
|
it.item_time || "",
|
||||||
it.duration || 1,
|
it.duration || 1,
|
||||||
|
it.expense_context || "local",
|
||||||
);
|
);
|
||||||
visibleCount++;
|
visibleCount++;
|
||||||
}
|
}
|
||||||
@@ -594,6 +597,7 @@ function addCpExpenseLine(
|
|||||||
itemDate = "",
|
itemDate = "",
|
||||||
itemTime = "",
|
itemTime = "",
|
||||||
itemDur = 1,
|
itemDur = 1,
|
||||||
|
expenseContext = "local", // 🔥 FIX : On accepte le contexte dynamique
|
||||||
) {
|
) {
|
||||||
const container = document.getElementById("cpExpensesContainer");
|
const container = document.getElementById("cpExpensesContainer");
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
@@ -607,7 +611,8 @@ function addCpExpenseLine(
|
|||||||
<option value="transport" ${category === "transport" ? "selected" : ""}>🚗</option>
|
<option value="transport" ${category === "transport" ? "selected" : ""}>🚗</option>
|
||||||
<option value="activity" ${category === "activity" ? "selected" : ""}>🎫</option>
|
<option value="activity" ${category === "activity" ? "selected" : ""}>🎫</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="hidden" name="items[context][]" value="local">
|
<!-- 🔥 FIX : Le champ hidden prend la vraie valeur -->
|
||||||
|
<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="${tr("hdl_js_ph_expense_name")}" 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="${amount}">
|
||||||
|
|
||||||
@@ -616,7 +621,8 @@ function addCpExpenseLine(
|
|||||||
<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">${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> </div>
|
<button type="button" class="btn-icon-action delete btn-remove-expense" onclick="this.parentElement.parentElement.remove()" title="${tr("btn_delete")}">🗑️</button>
|
||||||
|
</div>
|
||||||
<div class="hol-form-subrow">
|
<div class="hol-form-subrow">
|
||||||
<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="${tr("hdl_ph_notes")}" value="${notes}">
|
||||||
</div>
|
</div>
|
||||||
@@ -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 checkpoints = document.querySelectorAll(".hol-checkpoint-draggable");
|
||||||
const container = checkpoints[0]?.parentElement;
|
const container = checkpoints[0]?.parentElement;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -687,7 +694,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const isMobile = window.innerWidth <= 768;
|
const isMobile = window.innerWidth <= 768;
|
||||||
let draggedItem = null;
|
let draggedItem = null;
|
||||||
|
|
||||||
|
// Nettoyage des anciens écouteurs en clonant les nœuds (Indispensable après un refresh silencieux)
|
||||||
checkpoints.forEach((item) => {
|
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) {
|
if (isMobile) {
|
||||||
item.removeAttribute("draggable");
|
item.removeAttribute("draggable");
|
||||||
return;
|
return;
|
||||||
@@ -727,16 +744,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
(closest, child) => {
|
(closest, child) => {
|
||||||
const box = child.getBoundingClientRect();
|
const box = child.getBoundingClientRect();
|
||||||
const offset = y - box.top - box.height / 2;
|
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 };
|
return { offset: offset, element: child };
|
||||||
} else {
|
else return closest;
|
||||||
return closest;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ offset: Number.NEGATIVE_INFINITY },
|
{ offset: Number.NEGATIVE_INFINITY },
|
||||||
).element;
|
).element;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// On lance l'initialisation au démarrage de la page
|
||||||
|
document.addEventListener("DOMContentLoaded", initStepDragAndDrop);
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 7. MOTEUR DRAG & DROP DU PLANNING GLOBAL
|
// 7. MOTEUR DRAG & DROP DU PLANNING GLOBAL
|
||||||
@@ -986,8 +1004,12 @@ function openGlobalPlanningModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isDateInStep(targetDate, stepStart, stepEnd) {
|
function isDateInStep(targetDate, stepStart, stepEnd) {
|
||||||
if (!stepStart || !stepEnd) return false;
|
if (!stepStart && !stepEnd) return false;
|
||||||
return targetDate >= stepStart && targetDate <= stepEnd;
|
|
||||||
|
const start = stepStart || stepEnd;
|
||||||
|
const end = stepEnd || stepStart;
|
||||||
|
|
||||||
|
return targetDate >= start && targetDate <= end;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterPoolByDate(dateStr) {
|
function filterPoolByDate(dateStr) {
|
||||||
@@ -1079,7 +1101,6 @@ function buildDragItemHtml(it) {
|
|||||||
? `<div style="font-size:0.65rem; color:var(--primary); font-weight:800; margin-bottom:4px; text-transform:uppercase;">📍 ${it.step_location}</div>`
|
? `<div style="font-size:0.65rem; color:var(--primary); font-weight:800; margin-bottom:4px; text-transform:uppercase;">📍 ${it.step_location}</div>`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
// Suppression du calcul de hauteur en ligne, c'est désormais géré via CSS et la variable "--duration"
|
|
||||||
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}"
|
||||||
@@ -1088,15 +1109,26 @@ function buildDragItemHtml(it) {
|
|||||||
${locHint}
|
${locHint}
|
||||||
<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 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
${noteHtml}
|
${noteHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unplaceItem(e, htmlId) {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDropLogic(htmlId, "", "");
|
||||||
|
}
|
||||||
|
|
||||||
function changeDuration(e, itemId, delta) {
|
function changeDuration(e, itemId, delta) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const itemEl =
|
const itemEl =
|
||||||
@@ -1240,7 +1272,7 @@ function handleDropLogic(htmlId, dateStr, timeStr) {
|
|||||||
const dur = parseInt(itemEl.style.getPropertyValue("--duration")) || 1;
|
const dur = parseInt(itemEl.style.getPropertyValue("--duration")) || 1;
|
||||||
const realId = itemEl.getAttribute("data-id");
|
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 (dateStr === "" || timeStr === "") {
|
||||||
if (!isVirtual) {
|
if (!isVirtual) {
|
||||||
updateItemMemory(realId, { item_date: null, item_time: null });
|
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_id", realId);
|
||||||
fd.append("item_date", "");
|
fd.append("item_date", "");
|
||||||
fd.append("item_time", "");
|
fd.append("item_time", "");
|
||||||
|
|
||||||
|
// Enregistrement en base + Toast
|
||||||
fetch("/modules/holidays/includes/api/save_checkpoint.php", {
|
fetch("/modules/holidays/includes/api/save_checkpoint.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: fd,
|
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);
|
filterPoolByDate(window.CURRENT_PLANNING_FILTER_DATE);
|
||||||
recalcAllBadges();
|
recalcAllBadges();
|
||||||
|
window.PLANNING_MODIFIED = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1307,13 +1348,35 @@ function handleDropLogic(htmlId, dateStr, timeStr) {
|
|||||||
category: "transport",
|
category: "transport",
|
||||||
is_virtual: false,
|
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 {
|
} else {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => window.location.reload());
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
updateItemMemory(realId, { item_date: dateStr, item_time: timeStr });
|
updateItemMemory(realId, { item_date: dateStr, item_time: timeStr });
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
@@ -1325,10 +1388,22 @@ function handleDropLogic(htmlId, dateStr, timeStr) {
|
|||||||
fetch("/modules/holidays/includes/api/save_checkpoint.php", {
|
fetch("/modules/holidays/includes/api/save_checkpoint.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: fd,
|
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();
|
recalcAllBadges();
|
||||||
|
silentlyRefreshSteps();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateItemMemory(itemId, changes) {
|
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
|
// MÉTÉO SPÉCIFIQUE AU HEADER DU PLANNING
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -1426,7 +1548,8 @@ function addQuickTransitExpense(
|
|||||||
fd.append("amount", amount);
|
fd.append("amount", amount);
|
||||||
fd.append("context", "transit");
|
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));
|
const h = Math.max(1, Math.round(durationSec / 3600));
|
||||||
fd.append("duration", h);
|
fd.append("duration", h);
|
||||||
|
|
||||||
@@ -1778,3 +1901,46 @@ async function saveHolidayGlobalNote(holidayId) {
|
|||||||
btn.disabled = false;
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -34,25 +34,40 @@ if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime
|
|||||||
$date = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
|
$date = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
|
||||||
$time = !empty($_POST['item_time']) ? $_POST['item_time'] : 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();
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// 🛡️ ANTI-DOUBLON ABSOLU
|
||||||
if ($context === 'transit') {
|
if ($context === 'transit') {
|
||||||
$stmtDel = $pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ? AND expense_context = 'transit'");
|
$stmtCheck = $pdo->prepare("SELECT id FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ? AND expense_context = 'transit'");
|
||||||
$stmtDel->execute([$holiday_id, $sort_order]);
|
$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 = $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([
|
$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'],
|
$stepInfo['location_name'], $stepInfo['lat'], $stepInfo['lng'],
|
||||||
$sort_order, $stepInfo['step_start_date'], $stepInfo['step_end_date'],
|
$sort_order, $stepInfo['step_start_date'], $stepInfo['step_end_date'],
|
||||||
$stepInfo['step_type'], $context, $dur, $date, $time
|
$stepInfo['step_type'], $context, $dur, $date, $time
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$newId = $pdo->lastInsertId();
|
$newId = $pdo->lastInsertId();
|
||||||
|
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
|
|
||||||
echo json_encode(['success' => true, 'id' => $newId]);
|
echo json_encode(['success' => true, 'id' => $newId]);
|
||||||
@@ -83,17 +98,6 @@ if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime
|
|||||||
exit;
|
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) {
|
} catch (Exception $e) {
|
||||||
if ($pdo->inTransaction()) { $pdo->rollBack(); }
|
if ($pdo->inTransaction()) { $pdo->rollBack(); }
|
||||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ $stmt = $pdo->prepare("
|
|||||||
LEFT JOIN pf_vehicles v ON h.vehicle_id = v.id
|
LEFT JOIN pf_vehicles v ON h.vehicle_id = v.id
|
||||||
WHERE h.id = ?
|
WHERE h.id = ?
|
||||||
");
|
");
|
||||||
|
|
||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);
|
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user