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