css drag and drop
This commit is contained in:
@@ -110,21 +110,27 @@
|
|||||||
|
|
||||||
/* Petit bouton + pour les colonnes */
|
/* Petit bouton + pour les colonnes */
|
||||||
.btn-icon-small {
|
.btn-icon-small {
|
||||||
background: transparent;
|
background: white !important;
|
||||||
border: 1px dashed #cbd5e1;
|
border: 1px solid #e2e8f0 !important;
|
||||||
border-radius: 4px;
|
border-radius: 6px !important;
|
||||||
padding: 6px 12px;
|
width: 32px !important;
|
||||||
font-size: 0.8rem;
|
height: 32px !important;
|
||||||
color: var(--text-muted);
|
min-width: 32px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 100%;
|
transition: all 0.2s;
|
||||||
margin-top: 10px;
|
box-shadow: var(--shadow-sm);
|
||||||
transition: 0.2s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-icon-small:hover {
|
.btn-icon-small:hover {
|
||||||
background: #f1f5f9;
|
background: #f1f5f9 !important;
|
||||||
color: var(--text-main);
|
border-color: #cbd5e1 !important;
|
||||||
border-color: var(--text-muted);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- 4. CARTES --- */
|
/* --- 4. CARTES --- */
|
||||||
@@ -623,28 +629,48 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
|
|||||||
|
|
||||||
.hol-cp-header {
|
.hol-cp-header {
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
padding: 10px 15px;
|
padding: 12px 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: 1px solid #e2e8f0;
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hol-cp-info-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hol-cp-title {
|
.hol-cp-title {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 1rem;
|
font-size: 0.95rem;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.2s;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hol-cp-title:hover {
|
.hol-cp-title:hover {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hol-cp-actions-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.hol-cp-total {
|
.hol-cp-total {
|
||||||
font-size: 0.8rem;
|
font-size: 0.9rem;
|
||||||
color: var(--text-muted);
|
font-weight: 700;
|
||||||
|
color: var(--primary);
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hol-cp-body {
|
.hol-cp-body {
|
||||||
|
|||||||
@@ -413,3 +413,79 @@ function deleteCheckpoint() {
|
|||||||
form.appendChild(input);
|
form.appendChild(input);
|
||||||
form.submit();
|
form.submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 5. GLISSER-DÉPOSER POUR RÉORDONNER LES ÉTAPES (ROADTRIP)
|
||||||
|
// ============================================================================
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const checkpoints = document.querySelectorAll(".hol-checkpoint-draggable");
|
||||||
|
const container = checkpoints[0]?.parentElement;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
let draggedItem = null;
|
||||||
|
|
||||||
|
checkpoints.forEach((item) => {
|
||||||
|
item.addEventListener("dragstart", function (e) {
|
||||||
|
draggedItem = this;
|
||||||
|
setTimeout(() => (this.style.opacity = "0.4"), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
item.addEventListener("dragend", function () {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.style.opacity = "1";
|
||||||
|
draggedItem = null;
|
||||||
|
saveCheckpointOrder(); // On sauvegarde quand on lâche !
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
item.addEventListener("dragover", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const afterElement = getDragAfterElement(container, e.clientY);
|
||||||
|
if (afterElement == null) {
|
||||||
|
container.appendChild(draggedItem);
|
||||||
|
} else {
|
||||||
|
container.insertBefore(draggedItem, afterElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function getDragAfterElement(container, y) {
|
||||||
|
const draggableElements = [
|
||||||
|
...container.querySelectorAll(
|
||||||
|
'.hol-checkpoint-draggable:not([style*="opacity: 0.4"])',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
return draggableElements.reduce(
|
||||||
|
(closest, child) => {
|
||||||
|
const box = child.getBoundingClientRect();
|
||||||
|
const offset = y - box.top - box.height / 2;
|
||||||
|
if (offset < 0 && offset > closest.offset) {
|
||||||
|
return { offset: offset, element: child };
|
||||||
|
} else {
|
||||||
|
return closest;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ offset: Number.NEGATIVE_INFINITY },
|
||||||
|
).element;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCheckpointOrder() {
|
||||||
|
// On récupère le nom des lieux dans le nouvel ordre de haut en bas
|
||||||
|
const locations = [
|
||||||
|
...document.querySelectorAll(".hol-checkpoint-draggable"),
|
||||||
|
].map((el) => el.getAttribute("data-location"));
|
||||||
|
const holidayId = document.querySelector('input[name="holiday_id"]').value; // Input caché dans la modale
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("holiday_id", holidayId);
|
||||||
|
formData.append("locations", JSON.stringify(locations));
|
||||||
|
|
||||||
|
fetch("/modules/holidays/includes/api/reorder_checkpoints.php", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
}).then(() => {
|
||||||
|
// Recharge la page pour que la carte redessine le trait bleu dans le bon ordre !
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
// modules/holidays/includes/api/reorder_checkpoints.php
|
||||||
|
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||||
|
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
$holiday_id = (int)$_POST['holiday_id'];
|
||||||
|
$locations = json_decode($_POST['locations'] ?? '[]', true);
|
||||||
|
|
||||||
|
if ($holiday_id > 0 && is_array($locations)) {
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_holidays_items SET sort_order = ? WHERE holiday_id = ? AND location_name = ?");
|
||||||
|
|
||||||
|
foreach ($locations as $index => $loc) {
|
||||||
|
// On met à jour toutes les dépenses de ce lieu avec son nouveau rang (0, 1, 2...)
|
||||||
|
$stmt->execute([$index, $holiday_id, $loc]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,8 @@ $stmt = $pdo->prepare("
|
|||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);
|
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$stmtItems = $pdo->prepare("SELECT * FROM pf_holidays_items WHERE holiday_id = ? ORDER BY id ASC");
|
// IMPORTANT : Le tri se fait maintenant sur "sort_order" pour que le glisser-déposer fonctionne
|
||||||
|
$stmtItems = $pdo->prepare("SELECT * FROM pf_holidays_items WHERE holiday_id = ? ORDER BY sort_order ASC, id ASC");
|
||||||
$stmtItems->execute([$id]);
|
$stmtItems->execute([$id]);
|
||||||
$items = $stmtItems->fetchAll(PDO::FETCH_ASSOC);
|
$items = $stmtItems->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
@@ -123,16 +124,24 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
<p style="color:var(--text-muted); font-style:italic; text-align:center; margin-top:40px;">Aucune étape planifiée.</p>
|
<p style="color:var(--text-muted); font-style:italic; text-align:center; margin-top:40px;">Aucune étape planifiée.</p>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($steps as $step): ?>
|
<?php foreach ($steps as $step): ?>
|
||||||
<div class="hol-checkpoint">
|
<div class="hol-checkpoint hol-checkpoint-draggable" draggable="true" data-location="<?= htmlspecialchars($step['location_name']) ?>">
|
||||||
<div class="hol-cp-header">
|
<div class="hol-cp-header">
|
||||||
<div>
|
|
||||||
<div class="hol-cp-title" onclick="panMapTo(<?= $step['lat'] ?>, <?= $step['lng'] ?>)">📍 <?= htmlspecialchars($step['location_name']) ?></div>
|
<div class="hol-cp-info-group">
|
||||||
<div class="hol-cp-total">Total Étape : <?= number_format($step['total_amount'], 2) ?> €</div>
|
<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']) ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button onclick='openCheckpointModal("edit", <?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="Modifier cette étape" style="margin:0;">✏️</button>
|
|
||||||
</div>
|
|
||||||
<div class="hol-cp-body">
|
|
||||||
|
|
||||||
|
<div class="hol-cp-actions-group">
|
||||||
|
<div class="hol-cp-total"><?= number_format($step['total_amount'], 2, ',', ' ') ?> €</div>
|
||||||
|
<button onclick='openCheckpointModal("edit", <?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="Modifier">✏️</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hol-cp-body">
|
||||||
<?php
|
<?php
|
||||||
$visibleItemsCount = 0;
|
$visibleItemsCount = 0;
|
||||||
foreach ($step['items'] as $it):
|
foreach ($step['items'] as $it):
|
||||||
@@ -143,16 +152,17 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
<div class="hol-expense-line">
|
<div class="hol-expense-line">
|
||||||
<span style="color:#475569;"><?= $icon ?> <?= htmlspecialchars($it['name']) ?></span>
|
<span style="color:#475569;"><?= $icon ?> <?= htmlspecialchars($it['name']) ?></span>
|
||||||
<span>
|
<span>
|
||||||
<strong style="color:var(--text-main);"><?= number_format($it['amount'], 2) ?> €</strong>
|
<strong style="color:var(--text-main);"><?= number_format($it['amount'], 2, ',', ' ') ?> €</strong>
|
||||||
<span style="margin-left:5px; color:<?= $it['is_paid'] ? '#10b981' : '#f59e0b' ?>;" title="<?= $it['is_paid'] ? 'Payé' : 'À payer' ?>"><?= $it['is_paid'] ? '✓' : '⏳' ?></span>
|
<span style="margin-left:5px; color:<?= $it['is_paid'] ? '#10b981' : '#f59e0b' ?>;"><?= $it['is_paid'] ? '✓' : '⏳' ?></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
<?php if ($visibleItemsCount === 0): ?>
|
<?php if ($visibleItemsCount === 0): ?>
|
||||||
<div style="font-size:0.8rem; color:var(--text-muted); font-style:italic;">Point de passage (Aucune dépense)</div>
|
<div style="font-size:0.8rem; color:var(--text-muted); font-style:italic; padding: 5px 0;">
|
||||||
|
📍 Point de passage (Aucune dépense)
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user