diff --git a/modules/holidays/holidays.css b/modules/holidays/holidays.css index a3f4cd2..215f034 100644 --- a/modules/holidays/holidays.css +++ b/modules/holidays/holidays.css @@ -716,3 +716,226 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover { text-align: left !important; /* On annule l'alignement à droite du total */ } } + +/* ========================================================================== + 13. BARRE DE PROGRESSION & FINANCES + ========================================================================== */ +.hol-progress-bar { + width: 100%; + height: 12px; + background: #e2e8f0; + border-radius: 6px; + margin-bottom: 10px; + display: flex; + overflow: hidden; +} +.hol-progress-paid { + background: #10b981; +} +.hol-progress-saved { + background: #3b82f6; +} + +.hol-progress-labels { + display: flex; + justify-content: space-between; + font-size: 0.85rem; +} +.hol-label-paid { + color: #10b981; + font-weight: 600; +} +.hol-label-saved { + color: #3b82f6; + font-weight: 600; +} +.hol-label-left { + color: #ef4444; + font-weight: 700; +} + +/* ========================================================================== + 14. LÉGENDE DE LA CARTE + ========================================================================== */ +.hol-panel-header-group { + display: flex; + align-items: center; + gap: 15px; + flex-wrap: wrap; +} +.hol-map-legend { + display: flex; + gap: 12px; + font-size: 0.75rem; + color: #64748b; + background: white; + padding: 4px 10px; + border-radius: 12px; + border: 1px solid #e2e8f0; +} +.hol-legend-item { + display: flex; + align-items: center; + gap: 4px; +} +.hol-legend-color { + display: inline-block; + width: 12px; + height: 4px; + border-radius: 2px; +} +.hol-legend-color.aller { + background: #3b82f6; +} +.hol-legend-color.interm { + background: #8b5cf6; +} +.hol-legend-color.retour { + background: transparent; + height: 0; + border-bottom: 3px dashed #f97316; + border-radius: 0; +} + +/* ========================================================================== + 15. LIGNES DE DÉPENSES (Timeline & Modale JS) + ========================================================================== */ +.hol-expense-wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + margin-bottom: 8px; + border-bottom: 1px dashed #f1f5f9; + padding-bottom: 8px; +} +.hol-expense-wrapper:last-child { + margin-bottom: 0; + border-bottom: none; + padding-bottom: 0; +} + +.hol-expense-main { + display: flex; + justify-content: space-between; + width: 100%; + font-size: 0.85rem; +} +.hol-expense-name { + color: #475569; + font-weight: 500; +} +.hol-expense-amount { + color: var(--text-main); + font-weight: bold; +} +.status-paid { + color: #10b981; + margin-left: 5px; +} +.status-pending { + color: #f59e0b; + margin-left: 5px; +} + +.hol-expense-note { + font-size: 0.75rem; + color: #94a3b8; + padding-left: 24px; + word-break: break-all; +} +.hol-expense-link { + color: #3b82f6; + text-decoration: none; +} +.hol-expense-link:hover { + text-decoration: underline; +} + +.hol-empty-step { + font-size: 0.8rem; + color: var(--text-muted); + font-style: italic; + padding: 5px 0; +} + +/* Boutons de petite taille */ +.pf-btn-small { + padding: 6px 14px !important; + font-size: 0.85rem !important; + width: auto !important; + height: auto !important; + margin: 0 !important; +} + +/* Modale JS d'ajout de dépense */ +.hol-form-row { + display: flex; + flex-direction: column; + gap: 6px; + padding: 10px; + background: #f8fafc; + border-radius: 8px; + border: 1px solid #e2e8f0; +} +.hol-form-inner { + display: flex; + gap: 8px; + align-items: center; + width: 100%; +} +.hol-form-select { + width: 50px; + padding: 8px 4px; + font-size: 1.2rem; + cursor: pointer; +} +.hol-form-text { + flex: 2; + padding: 8px; + font-size: 0.9rem; +} +.hol-form-number { + width: 80px; + text-align: right; + padding: 8px; + font-size: 0.9rem; +} +.hol-form-paid-label { + display: flex; + align-items: center; + cursor: pointer; + width: 55px; +} +.hol-form-paid-text { + font-size: 0.75rem; + margin-left: 4px; + font-weight: bold; + color: #64748b; +} +.hol-form-notes-input { + font-size: 0.8rem; + padding: 6px 8px; + border-style: dashed; + color: #475569; + width: calc(100% - 58px); + margin-left: 58px; +} +.btn-remove-expense { + width: 28px; + height: 28px; + border: none; + background: #fee2e2; + color: #ef4444; + border-radius: 6px; + cursor: pointer; + font-weight: bold; + display: flex; + justify-content: center; + align-items: center; + transition: 0.2s; +} +.btn-remove-expense:hover { + background: #fca5a5; + color: #b91c1c; +} diff --git a/modules/holidays/holidays.js b/modules/holidays/holidays.js index c75b933..6a62c18 100644 --- a/modules/holidays/holidays.js +++ b/modules/holidays/holidays.js @@ -200,13 +200,12 @@ function initDetailMap() { detailMap = L.map("tripMap"); - // Fond de carte propre (CartoDB Voyager) - L.tileLayer( - "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png", - { - attribution: "© OpenStreetMap", - }, - ).addTo(detailMap); + // Carte standard : Affiche toujours la langue locale exacte (Français en France, Catalan en Catalogne) + L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { + maxZoom: 19, + attribution: + '© OpenStreetMap contributors', + }).addTo(detailMap); if (MAP_POINTS.length === 0) { detailMap.setView([46.6, 2.4], 5); // Centré sur la France par défaut si vide @@ -263,12 +262,87 @@ function initDetailMap() { }); }); - // On dessine le trait en pointillés pour relier le roadtrip ! + // On dessine le vrai trajet par la route avec gestion des superpositions if (latlngs.length > 1) { - L.polyline(latlngs, { - color: "#3b82f6", - weight: 3, - dashArray: "8, 8", + const routePromises = []; + + // 1. On lance toutes les requêtes de calcul de route en même temps + for (let i = 0; i < latlngs.length - 1; i++) { + const startPt = MAP_POINTS[i]; + const endPt = MAP_POINTS[i + 1]; + const coordsString = `${startPt.lng},${startPt.lat};${endPt.lng},${endPt.lat}`; + + const promise = fetch( + `https://router.project-osrm.org/route/v1/driving/${coordsString}?overview=full&geometries=geojson`, + ) + .then((response) => response.json()) + .then((data) => ({ + index: i, + data: data, + coords: [latlngs[i], latlngs[i + 1]], + })) + .catch((err) => ({ + index: i, + error: true, + coords: [latlngs[i], latlngs[i + 1]], + })); + + routePromises.push(promise); + } + + // 2. On attend que TOUTES les routes soient calculées pour les dessiner dans le bon ordre + Promise.all(routePromises).then((results) => { + // On s'assure de dessiner l'Étape 1 d'abord, puis la 2, etc. + results.sort((a, b) => a.index - b.index); + + results.forEach((res) => { + const i = res.index; + + // Styles par défaut (Aller / Étape 1) + let routeColor = "#3b82f6"; // Bleu + let routeWeight = 6; // Épais + let routeDash = null; // Ligne continue + + // Modification des styles selon le segment + if (i === latlngs.length - 2) { + // DERNIER SEGMENT (Retour) + routeColor = "#f97316"; // Orange vif (Tranche parfaitement avec le bleu) + routeWeight = 3; // Plus fin pour se superposer sans cacher + routeDash = "10, 10"; // En pointillés pour laisser voir le bleu en dessous + } else if (i > 0) { + // SEGMENTS INTERMÉDIAIRES + routeColor = "#8b5cf6"; // Violet + routeWeight = 5; + } + + // Dessin du trait + if (res.data && res.data.code === "Ok" && res.data.routes.length > 0) { + const routeCoords = res.data.routes[0].geometry.coordinates.map( + (c) => [c[1], c[0]], + ); + + L.polyline(routeCoords, { + color: routeColor, + weight: routeWeight, + dashArray: routeDash, + opacity: 0.9, + lineCap: "round", + lineJoin: "round", + }).addTo(detailMap); + } else { + // Plan B si pas de route carrossable (ex: avion/bateau) + drawFallbackLine(res.coords, routeColor, routeWeight); + } + }); + }); + } + + // Plan B : La ligne droite + function drawFallbackLine(coords, color, weight) { + L.polyline(coords, { + color: color, + weight: weight || 3, + dashArray: "8, 8", // Toujours en pointillés pour le plan B opacity: 0.7, }).addTo(detailMap); } @@ -395,35 +469,29 @@ function addCpExpenseLine( ) { const container = document.getElementById("cpExpensesContainer"); const div = document.createElement("div"); - div.style.display = "flex"; - div.style.flexDirection = "column"; - div.style.gap = "6px"; - div.style.padding = "10px"; - div.style.background = "#f8fafc"; - div.style.borderRadius = "8px"; - div.style.border = "1px solid #e2e8f0"; + div.className = "hol-form-row"; // Classe globale de la ligne const isChecked = isPaid == 1 ? "checked" : ""; div.innerHTML = ` -
- - - + + -
- + `; container.appendChild(div); } diff --git a/modules/holidays/views/detail.php b/modules/holidays/views/detail.php index acec2d3..bf5468f 100644 --- a/modules/holidays/views/detail.php +++ b/modules/holidays/views/detail.php @@ -18,22 +18,19 @@ $stmt = $pdo->prepare(" $stmt->execute([$id]); $holiday = $stmt->fetch(PDO::FETCH_ASSOC); -// 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]); $items = $stmtItems->fetchAll(PDO::FETCH_ASSOC); -// Récupération des favoris géographiques $stmtFav = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'holiday_favorites'"); $favorites = json_decode($stmtFav->fetchColumn() ?: '[]', true); -// GROUPEMENT PAR INDEX D'ORDRE (Pour autoriser les doublons de lieux comme "Maison") $steps = []; $generalItems = []; foreach ($items as $it) { if ($it['location_name'] !== null) { - $orderKey = $it['sort_order']; // On utilise l'ordre comme clé unique de l'étape + $orderKey = $it['sort_order']; if (!isset($steps[$orderKey])) { $steps[$orderKey] = [ 'location_name' => $it['location_name'], @@ -53,7 +50,6 @@ foreach ($items as $it) { ksort($steps); $mapPoints = array_values($steps); -// Calculs d'affichage (Dates & Finances) $dateDisplay = htmlspecialchars($holiday['period_hint'] ?? ''); if (empty($dateDisplay) && $holiday['start_date']) { $dateDisplay = date('d/m/Y', strtotime($holiday['start_date'])); @@ -75,11 +71,11 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
- ◀ Retour + ◀ Retour

- +
@@ -92,36 +88,43 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
Budget Food & Extras
🍔 € | 🎁
-
+
Coût Total Estimé
-
-
-
+
+
+
-
- ✓ Payé : - 💼 Provisionné : - ⏳ Reste à payer : +
+ ✓ Payé : + 💼 Provisionné : + ⏳ Reste à payer :
-
-

🗺️ Itinéraire & Checkpoints

- +
+
+

🗺️ Itinéraire

+
+ Aller + Interm. + Retour +
+
+
-

📝 Détail des étapes

+

📝 Détail des étapes

@@ -129,7 +132,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;

Aucune étape planifiée.

-
+
@@ -137,12 +140,10 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0; 📍
-
-
@@ -153,18 +154,18 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0; $visibleItemsCount++; $icon = match($it['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' }; ?> -
-
- +
+
+ - - + +
-
+
- 🔗 Voir le lien + 🔗 Voir le lien 📝 @@ -174,7 +175,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0; -
+
📍 Point de passage (Aucune dépense)
@@ -205,7 +206,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
- @@ -228,12 +229,12 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
- +
- +
@@ -242,18 +243,18 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
- +
- +