From d557636eeec2d6ccdd4a6095d31ce80416ba2e90 Mon Sep 17 00:00:00 2001 From: "fefe.clochette" Date: Mon, 9 Feb 2026 15:29:12 +0100 Subject: [PATCH] refacto holidays --- modules/holidays/holidays.css | 678 +++++++++++++++------------------- modules/holidays/holidays.js | 530 +++++++++----------------- modules/holidays/index.php | 591 +++++++++-------------------- modules/holidays/save.php | 234 ------------ modules/holidays/save_new.php | 66 ++++ 5 files changed, 704 insertions(+), 1395 deletions(-) delete mode 100644 modules/holidays/save.php create mode 100644 modules/holidays/save_new.php diff --git a/modules/holidays/holidays.css b/modules/holidays/holidays.css index 705bc6f..6adac49 100644 --- a/modules/holidays/holidays.css +++ b/modules/holidays/holidays.css @@ -2,15 +2,17 @@ /* --- 1. VARIABLES & BASE --- */ :root { - --primary: #2563eb; /* Bleu vibrant */ + --primary: #2563eb; --primary-hover: #1d4ed8; - --bg-page: #f1f5f9; /* Gris-bleu très pâle */ + --bg-page: #f1f5f9; --bg-card: #ffffff; - --text-main: #1e293b; /* Gris très foncé (pas noir) */ + --text-main: #1e293b; --text-muted: #64748b; + --radius-l: 16px; --radius-m: 10px; --radius-s: 6px; + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); --shadow-lg: @@ -27,24 +29,10 @@ -apple-system, sans-serif; color: var(--text-main); + padding-bottom: 50px; } -/* Titres */ -.pf-holidays h1 { - font-size: 1.8rem; - font-weight: 800; - letter-spacing: -0.025em; - color: #0f172a; - margin-bottom: 0.5rem; -} - -.pf-holidays p { - font-size: 0.95rem; - color: var(--text-muted); - line-height: 1.5; -} - -/* --- 2. EN-TÊTE (TITLEBAR) --- */ +/* --- 2. EN-TÊTE --- */ .pf-holidays__titlebar { background: transparent; padding-bottom: 1rem; @@ -57,17 +45,24 @@ gap: 1rem; } +.pf-holidays__titlebar h1 { + font-size: 1.8rem; + font-weight: 800; + color: #0f172a; + margin: 0; +} + .hol-title-actions { display: flex; gap: 12px; } -/* --- 3. BOUTONS MODERNES --- */ -.btn, +/* --- 3. BOUTONS --- */ .hol-add-btn, -.hol-map-toggle { +.hol-map-toggle, +.pf-btn { border: none; - border-radius: 50px; /* Pill shape */ + border-radius: 50px; padding: 10px 20px; font-size: 0.9rem; font-weight: 600; @@ -76,21 +71,21 @@ display: inline-flex; align-items: center; justify-content: center; - text-decoration: none; box-shadow: var(--shadow-sm); + text-decoration: none; } -/* Bouton principal (Ajouter) */ -.hol-add-btn { +.hol-add-btn, +.pf-btn { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); color: white; } -.hol-add-btn:hover { +.hol-add-btn:hover, +.pf-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); } -/* Bouton Carte */ .hol-map-toggle { background: white; color: #0f766e; @@ -98,53 +93,44 @@ } .hol-map-toggle:hover { background: #f0fdfa; - border-color: #99f6e4; color: #0d9488; + border-color: #99f6e4; } -/* Boutons classiques */ -.btn { +.pf-btn.btn-secondary { background: white; - color: var(--text-main); + color: var(--text-muted); border: 1px solid #cbd5e1; -} -.btn:hover { - background: #f8fafc; - border-color: #94a3b8; -} - -/* Actions contextuelles (edit/delete) dans les cards */ -.btn-edit, -.btn-delete { - border-radius: var(--radius-s); - padding: 6px 12px; - font-size: 0.8rem; box-shadow: none; } -.btn-edit { +.pf-btn.btn-secondary:hover { + background: #f8fafc; + color: var(--text-main); +} + +/* Petit bouton + pour les colonnes */ +.btn-icon-small { + background: transparent; + border: 1px dashed #cbd5e1; + border-radius: 4px; + padding: 6px 12px; + font-size: 0.8rem; + color: var(--text-muted); + cursor: pointer; + width: 100%; + margin-top: 10px; + transition: 0.2s; +} +.btn-icon-small:hover { background: #f1f5f9; - color: #475569; - border: none; -} -.btn-edit:hover { - background: #e2e8f0; - color: #1e293b; + color: var(--text-main); + border-color: var(--text-muted); } -.btn-delete { - background: #fef2f2; - color: #dc2626; - border: none; -} -.btn-delete:hover { - background: #fee2e2; - color: #b91c1c; -} - -/* --- 4. GRILLE & CARTES (Le cœur du design) --- */ +/* --- 4. CARTES --- */ .hol-ideas-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 24px; margin-bottom: 40px; } @@ -159,52 +145,31 @@ transition: transform 0.25s ease, box-shadow 0.25s ease; - border: 1px solid transparent; /* Pour éviter le saut au hover */ + border: 1px solid transparent; + cursor: pointer; position: relative; overflow: hidden; } -/* Effet au survol : la carte "flotte" */ .hol-idea-card:hover { transform: translateY(-5px); box-shadow: var(--shadow-hover); border-color: #e2e8f0; } -/* Bande de couleur décorative en haut de carte selon statut (Optionnel mais joli) */ -.hol-idea-card::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: #e2e8f0; /* Default */ -} -.hol-idea-card:has(.hol-status--planned)::before { - background: #22c55e; -} -.hol-idea-card:has(.hol-status--favorite)::before { - background: #f59e0b; -} -.hol-idea-card:has(.hol-status--shortlist)::before { - background: #3b82f6; -} - .hol-idea-card__head { display: flex; justify-content: space-between; align-items: flex-start; - margin-bottom: 12px; + margin-bottom: 8px; } .hol-idea-card h3 { margin: 0; font-size: 1.15rem; font-weight: 700; - line-height: 1.3; color: var(--text-main); - margin-right: 8px; + line-height: 1.3; } .hol-idea-meta { @@ -216,355 +181,310 @@ gap: 4px; } -/* Notes stylisées */ .hol-notes { - background: #fffbeb; /* Jaune très pâle, style post-it */ - color: #78350f; - padding: 10px 12px; - border-radius: var(--radius-m); font-size: 0.85rem; - font-style: italic; - margin-bottom: 16px; - flex-grow: 1; - border-left: 3px solid #fcd34d; -} - -.hol-card-actions { - margin-top: auto; - display: flex; - gap: 8px; - padding-top: 16px; - border-top: 1px solid #f1f5f9; -} - -/* Statuts (Badges Modernes) */ -.hol-status { - padding: 4px 10px; - border-radius: 20px; - font-size: 0.7rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.05em; -} -.hol-status--planned { - background: #dcfce7; - color: #15803d; -} /* Vert */ -.hol-status--favorite { - background: #fff7ed; - color: #c2410c; - border: 1px solid #ffedd5; -} /* Orange */ -.hol-status--shortlist { - background: #eff6ff; - color: #1d4ed8; -} /* Bleu */ -.hol-status--draft { - background: #f1f5f9; color: #64748b; -} /* Gris */ -.hol-status--archived { - background: #f8fafc; - color: #94a3b8; - text-decoration: line-through; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + margin: 0; + font-style: italic; } -/* Style spécifique pour les archivés */ -.hol-idea-card--archived { - background: #f8fafc; - box-shadow: none; - border: 1px solid #e2e8f0; - opacity: 0.8; +/* --- 5. UTILITAIRES STATUTS --- */ +.bg-green-100 { + background-color: #dcfce7; } - -/* --- 5. VUE DÉTAIL (Panels) --- */ -.pf-section--panel { - background: white; - border-radius: var(--radius-l); - box-shadow: var(--shadow-sm); - padding: 24px; - margin-bottom: 24px; - border: 1px solid #f1f5f9; +.text-green-800 { + color: #166534; } - -.pf-section--panel h2 { - font-size: 1.25rem; - color: var(--text-main); - margin-bottom: 16px; - display: flex; - align-items: center; - gap: 8px; +.bg-blue-100 { + background-color: #dbeafe; } -/* Petite ligne décorative sous le titre h2 */ -.pf-section--panel h2::after { - content: ""; - flex: 1; - height: 1px; - background: #e2e8f0; - margin-left: 12px; -} - -.hol-list li { - padding: 12px 0; - border-bottom: 1px solid #f1f5f9; - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 8px; -} -.hol-list li:last-child { - border-bottom: none; -} - -.hol-budget-summary { - background: linear-gradient(to right, #eff6ff, #ffffff); - border: 1px solid #dbeafe; +.text-blue-800 { color: #1e40af; - padding: 16px; - border-radius: var(--radius-m); - font-weight: 600; - display: flex; - justify-content: space-between; - margin-bottom: 16px; +} +.bg-yellow-50 { + background-color: #fefce8; +} +.text-yellow-800 { + color: #854d0e; +} +.bg-gray-100 { + background-color: #f3f4f6; +} +.text-gray-600 { + color: #4b5563; } -/* Formulaires Inline (Détail) */ -.hol-inline-form { - background: #f8fafc; - padding: 12px; - border-radius: var(--radius-m); - border: 1px dashed #cbd5e1; - display: flex; - flex-wrap: wrap; - gap: 10px; - align-items: center; - margin-bottom: 20px; -} -.hol-inline-form input, -.hol-inline-form select { - border: 1px solid #cbd5e1; - border-radius: var(--radius-s); - padding: 8px 12px; - font-size: 0.9rem; - transition: border-color 0.2s; -} -.hol-inline-form input:focus, -.hol-inline-form select:focus { - border-color: var(--primary); - outline: 2px solid rgba(37, 99, 235, 0.1); -} -.hol-inline-form button { - background: #334155; - color: white; - border: none; - padding: 8px 16px; - border-radius: var(--radius-s); - cursor: pointer; -} -.hol-inline-form button:hover { - background: #1e293b; -} - -/* --- 6. MODALES MODERNES (Glassmorphism léger) --- */ -.hol-modal { +/* --- 6. MODALE & FORMULAIRE (Refonte Centrage & Largeur) --- */ +.pf-modal { display: none; -} -.hol-modal.open { - display: flex; - align-items: center; - justify-content: center; position: fixed; inset: 0; z-index: 9999; + align-items: center; /* Centrage Vertical */ + justify-content: center; /* Centrage Horizontal */ + background: rgba(15, 23, 42, 0.6); + backdrop-filter: blur(4px); +} +.pf-modal.open { + display: flex; } -.hol-backdrop { - position: absolute; - inset: 0; - background: rgba(15, 23, 42, 0.6); /* Plus sombre */ - backdrop-filter: blur(4px); /* Flou d'arrière plan */ -} - -.hol-dialog { +.pf-modal-content { position: relative; background: white; - width: min(600px, 90vw); + width: 95%; /* Responsive */ + max-width: 1100px; /* Largeur augmentée pour 3 colonnes */ max-height: 90vh; overflow-y: auto; - border-radius: 20px; /* Très arrondi */ + border-radius: 20px; box-shadow: var(--shadow-lg); - z-index: 10; - animation: modalPop 0.3s cubic-bezier(0.16, 1, 0.3, 1); + padding: 32px; + display: flex; + flex-direction: column; + margin: auto; /* Sécurité centrage */ } -@keyframes modalPop { - from { - transform: scale(0.95) translateY(10px); - opacity: 0; - } - to { - transform: scale(1) translateY(0); - opacity: 1; - } -} - -/* Modale Carte */ +/* Cas spécifique modale carte */ .hol-dialog--map { width: 95vw; height: 90vh; max-width: 1400px; padding: 0; + background: white; + border-radius: 12px; display: flex; flex-direction: column; } -/* Formulaires Modale */ -.hol-form { - padding: 32px; - display: flex; - flex-direction: column; - gap: 16px; -} -.hol-form h3 { - font-size: 1.5rem; - margin-bottom: 12px; - color: var(--text-main); -} -.hol-form label { +/* Inputs généraux */ +.pf-label { + display: block; font-weight: 600; font-size: 0.85rem; - color: #475569; - margin-bottom: 4px; + color: var(--text-muted); + margin-bottom: 6px; } -.hol-form input, -.hol-form select, -.hol-form textarea { + +.pf-input { width: 100%; padding: 10px 12px; border: 1px solid #cbd5e1; border-radius: var(--radius-m); font-size: 1rem; - background: #f8fafc; - box-sizing: border-box; /* Important */ + background: #ffffff; /* Fond blanc forcé */ + color: var(--text-main); + box-sizing: border-box; + transition: all 0.2s; } -.hol-form input:focus, -.hol-form select:focus, -.hol-form textarea:focus { + +.pf-input:focus { background: white; border-color: var(--primary); - outline: 4px solid rgba(37, 99, 235, 0.1); + outline: none; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); } -.hol-inline { +.form-row { display: flex; gap: 16px; -} -.hol-inline > label { - flex: 1; + flex-wrap: wrap; + margin-bottom: 16px; } -.hol-actions { - margin-top: 16px; +/* --- 7. SECTIONS DE DÉTAILS (Transport/Hotel/Activité) --- */ +.hol-columns-wrapper { + display: flex; /* Flexbox */ + flex-direction: column; /* Alignement vertical (les uns sous les autres) */ + gap: 24px; /* Espace entre les blocs */ + width: 100%; +} + +.hol-col { + background: #f8fafc; + padding: 20px; + border-radius: 12px; + border: 1px solid #e2e8f0; + width: 100%; /* Prend toute la largeur disponible */ + box-sizing: border-box; /* Empêche le padding de casser la largeur */ +} + +.hol-col-header { + display: flex; + justify-content: space-between; /* Titre à gauche, bouton à droite */ + align-items: center; + border-bottom: 1px solid #e2e8f0; + padding-bottom: 8px; + margin-bottom: 12px; +} + +.hol-col-header h4 { + margin: 0; + font-size: 1rem; + text-transform: uppercase; + font-weight: 700; + /* On retire les bordures/marges de l'ancien h4 car c'est le header qui gère */ + border: none; + padding: 0; +} + +/* Le bouton "+" style Gift List */ +.btn-add-item { + /* Style de base */ + background: rgba(255, 255, 255, 0.6); + color: var(--text-muted); + border: 1px solid rgba(0, 0, 0, 0.1); + + /* Forme et Taille */ + width: 28px; + height: 28px; + border-radius: 50%; + + /* Texte */ + font-size: 18px; + line-height: 1; + + /* Flex pour centrer le + */ + display: flex; + align-items: center; + justify-content: center; + + cursor: pointer; + + /* Animation fluide "Cubic Bezier" copiée de gift-list */ + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +.btn-add-item:hover { + background: white; + color: var(--primary); /* On ajoute la couleur primaire au survol */ + + /* L'effet de rotation et d'agrandissement */ + transform: rotate(90deg) scale(1.1); + + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); + border-color: currentColor; +} + +/* --- 8. ITEMS DYNAMIQUES (Inputs optimisés) --- */ +.savings-line-item { + display: flex; + align-items: center; + gap: 8px; /* Espace réduit */ + margin-bottom: 10px; + background: white; + padding: 8px; + border-radius: 8px; + border: 1px solid #f1f5f9; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); +} + +/* Champ NOM : prend toute la place */ +.savings-line-item input[type="text"] { + flex-grow: 1; + min-width: 120px; + padding: 6px 10px; + font-size: 0.9rem; +} + +/* Champ PRIX : compact */ +.savings-line-item input[type="number"] { + width: 120px !important; + padding: 6px 8px !important; + text-align: right; + font-weight: 600; + color: #059669; +} + +/* Checkbox Payé */ +.savings-line-item label { + display: flex; + align-items: center; + font-size: 0.75rem; + color: #64748b; + gap: 4px; + cursor: pointer; + white-space: nowrap; +} + +/* Bouton Supprimer (Croix) */ +.btn-remove { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + color: #ef4444; + border: none; + border-radius: 4px; + font-size: 1.2rem; + cursor: pointer; + line-height: 1; + transition: background 0.2s; +} +.btn-remove:hover { + background: #fef2f2; + color: #b91c1c; +} + +/* --- 9. FOOTER MODALE --- */ +.modal-footer { display: flex; justify-content: flex-end; - gap: 12px; -} -.hol-cancel { - background: transparent; - color: var(--text-muted); - border: none; - font-weight: 600; -} -.hol-cancel:hover { - color: var(--text-main); - background: #f1f5f9; + gap: 10px; + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid #e2e8f0; } -.hol-ok { - background: var(--primary); - color: white; - border: none; - padding: 10px 24px; - border-radius: 50px; - font-weight: 600; - box-shadow: 0 4px 6px rgba(37, 99, 235, 0.2); -} -.hol-ok:hover { - background: var(--primary-hover); - transform: translateY(-1px); -} - -/* --- 7. PICKER GEOCODE --- */ -.hol-geocode-picker { - background: white; - border: 1px solid #e2e8f0; - border-radius: var(--radius-m); - box-shadow: var(--shadow-lg); - margin-top: 8px; - overflow: hidden; -} -.hol-geocode-picker__header { - background: #f8fafc; - padding: 8px 12px; - font-weight: 600; - font-size: 0.85rem; - display: flex; - justify-content: space-between; -} -.hol-geocode-picker__item { - padding: 10px 12px; - border-bottom: 1px solid #f1f5f9; - cursor: pointer; - display: flex; - justify-content: space-between; - align-items: center; -} -.hol-geocode-picker__item:hover { - background: #f0f9ff; -} -.hol-geocode-picker__item button { - background: white; - border: 1px solid #cbd5e1; - border-radius: 4px; - font-size: 0.75rem; - padding: 4px 8px; -} - -/* --- 8. MOBILE RESPONSIVE --- */ +/* --- 10. MOBILE --- */ @media (max-width: 768px) { - .hol-inline { + .pf-holidays__titlebar { flex-direction: column; - gap: 12px; - } - .hol-inline-form { - flex-direction: column; - align-items: stretch; - } - .hol-inline-form button { - margin-top: 8px; - width: 100%; - } - - .pf-holidays h1 { - font-size: 1.5rem; - } - - .hol-idea-card { - padding: 16px; - } - .hol-ideas-grid { - gap: 16px; - } - - .btn, - .hol-add-btn, - .hol-map-toggle { - width: 100%; /* Boutons pleine largeur sur mobile */ - margin-bottom: 8px; + align-items: flex-start; } .hol-title-actions { - flex-direction: column; width: 100%; - gap: 4px; + } + .hol-add-btn, + .hol-map-toggle { + width: 100%; + } + .form-row { + flex-direction: column; + gap: 10px; + } + /* Sur mobile, les colonnes s'empilent */ + .hol-columns-wrapper { + grid-template-columns: 1fr; + } + + .pf-modal-content { + padding: 16px; + width: 100%; + height: 100%; + max-height: 100vh; + border-radius: 0; } } + +/* --- 11. CALENDRIER & DATES (Force Light Theme) --- */ +input[type="date"] { + color-scheme: light; /* Force le popup blanc */ + background-color: #ffffff; + color: #1e293b; +} + +/* Cible spécifique pour l'icône calendrier (Chrome/Edge/Safari) */ +input[type="date"]::-webkit-calendar-picker-indicator { + cursor: pointer; + opacity: 0.6; + filter: invert(0); +} +input[type="date"]::-webkit-calendar-picker-indicator:hover { + opacity: 1; +} diff --git a/modules/holidays/holidays.js b/modules/holidays/holidays.js index 85651de..160729c 100644 --- a/modules/holidays/holidays.js +++ b/modules/holidays/holidays.js @@ -1,360 +1,174 @@ -// modules/holidays/holidays.js - document.addEventListener("DOMContentLoaded", () => { - // --- 1. FONCTIONS UTILITAIRES --- - - /** - * Configure les comportements de fermeture d'une modale (Backdrop, Cancel, Escape) - */ - function setupModal(modalId, openAction = null) { - const modal = document.getElementById(modalId); - if (!modal) return null; - - const backdrop = modal.querySelector(".hol-backdrop"); - const cancelBtn = modal.querySelector(".hol-cancel"); - - const close = () => modal.classList.remove("open"); - const open = () => { - modal.classList.add("open"); - if (openAction) openAction(); - }; - - if (backdrop) backdrop.addEventListener("click", close); - if (cancelBtn) cancelBtn.addEventListener("click", close); - - // Fermeture avec ECHAP (uniquement si cette modale est ouverte) - document.addEventListener("keydown", (e) => { - if (e.key === "Escape" && modal.classList.contains("open")) { - close(); - } - }); - - return { modal, open, close }; - } - - /** - * Échappe les caractères HTML pour éviter les failles XSS simples - */ - function esc(s) { - if (s === null || s === undefined) return ""; - return String(s) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - } - - // --- 2. GESTION MODALE "AJOUTER" --- - const addModalCtrl = setupModal("hol-add-modal"); - const addBtn = document.getElementById("hol-add-open"); - if (addBtn && addModalCtrl) { - addBtn.addEventListener("click", addModalCtrl.open); - } - - // --- 3. GESTION MODALE "ÉDITER" --- - const editModalCtrl = setupModal("hol-edit-modal"); - - // Fonction pour charger et ouvrir l'édition via AJAX - async function openEditForId(id) { - if (!editModalCtrl) return; - - try { - const res = await fetch( - `/modules/holidays/view.php?id=${encodeURIComponent(id)}`, - { headers: { Accept: "application/json" } }, - ); - if (!res.ok) throw new Error("Erreur HTTP " + res.status); - - const it = await res.json(); - const modal = editModalCtrl.modal; - - // Helper pour remplir les champs - const setVal = (sel, val) => { - const el = modal.querySelector(sel); - if (el) el.value = val ?? ""; - }; - - // Remplissage du formulaire - setVal("#edit-id", it.id); - setVal("#edit-title", it.title); - setVal("#edit-country", it.country); - setVal("#edit-region", it.region); - setVal("#edit-city", it.city); - setVal("#edit-lat", it.lat); - setVal("#edit-lng", it.lng); - setVal("#edit-start", it.desired_start_date); - setVal("#edit-end", it.desired_end_date); - setVal("#edit-season", it.season_hint); - setVal("#edit-days", it.ideal_days); - setVal("#edit-status", it.status || "draft"); - setVal("#edit-notes", it.notes); - - editModalCtrl.open(); - } catch (err) { - console.error(err); - alert("Impossible de charger les données de l'idée."); - } - } - - // Écouteurs sur les boutons "Éditer" (Liste + Détail) - document.body.addEventListener("click", (e) => { - // Utilisation de la délégation d'événement pour gérer tous les boutons (même dynamiques) - const btn = e.target.closest(".btn-edit"); - if (btn && btn.hasAttribute("data-edit-id")) { - const id = btn.getAttribute("data-edit-id"); - openEditForId(id); - } - // Cas spécifique du bouton dans le header de la vue détail - else if (e.target.id === "hol-edit-open") { - const id = e.target.getAttribute("data-edit-id"); - openEditForId(id); - } - }); - - // --- 4. GESTION SUPPRESSION --- - document.body.addEventListener("click", (e) => { - const btn = e.target.closest(".btn-delete"); - if (btn && btn.hasAttribute("data-del-id")) { - const id = btn.getAttribute("data-del-id"); - if ( - confirm( - "Voulez-vous vraiment supprimer cette idée ?\nCette action est irréversible.", - ) - ) { - // Création d'un formulaire temporaire pour le POST - const form = document.createElement("form"); - form.method = "post"; - form.action = "/modules/holidays/save.php"; - form.innerHTML = ` - - - `; - document.body.appendChild(form); - form.submit(); - } - } - }); - - // --- 5. GÉOCODAGE (Nominatim) --- - document.querySelectorAll(".hol-geocode-btn").forEach((btn) => { - btn.addEventListener("click", async () => { - const scope = btn.getAttribute("data-scope"); // 'add' ou 'edit' - let latInput, - lngInput, - qParts = []; - - // Récupération des inputs selon le scope - if (scope === "edit") { - const getVal = (id) => - (document.getElementById(id)?.value || "").trim(); - qParts = [ - getVal("edit-city"), - getVal("edit-region"), - getVal("edit-country"), - ]; - latInput = document.getElementById("edit-lat"); - lngInput = document.getElementById("edit-lng"); - } else { - // Scope 'add' : on cherche dans le formulaire parent - const form = btn.closest("form"); - const getVal = (name) => - (form?.querySelector(`input[name="${name}"]`)?.value || "").trim(); - qParts = [getVal("city"), getVal("region"), getVal("country")]; - latInput = form?.querySelector('input[name="lat"]'); - lngInput = form?.querySelector('input[name="lng"]'); - } - - const q = qParts.filter(Boolean).join(", "); - if (!q) { - alert( - "Veuillez renseigner au moins une Ville ou un Pays pour géocoder.", - ); - return; - } - - // UI : chargement - removeNearbyPicker(btn); - btn.disabled = true; - const originalText = btn.textContent; - btn.textContent = "⏳..."; - - try { - const res = await fetch( - `/modules/holidays/geocode.php?q=${encodeURIComponent(q)}&limit=5`, - { - headers: { Accept: "application/json" }, - }, - ); - const data = await res.json(); - - if (!res.ok) throw new Error(data?.error || "Erreur inconnue"); - - // Cas 1: Résultat direct (lat/lng uniques) - if (data.lat && data.lng) { - if (latInput) latInput.value = data.lat; - if (lngInput) lngInput.value = data.lng; - } - // Cas 2: Liste de choix - else if (Array.isArray(data.results) && data.results.length > 0) { - renderGeocodePicker(btn, data.results, (choice) => { - if (latInput) latInput.value = choice.lat; - if (lngInput) lngInput.value = choice.lng; - removeNearbyPicker(btn); - }); - } else { - alert("Aucun résultat trouvé pour : " + q); - } - } catch (err) { - console.error(err); - alert("Erreur lors du géocodage."); - } finally { - btn.disabled = false; - btn.textContent = originalText; - } - }); - }); - - function renderGeocodePicker(anchorBtn, results, onPick) { - removeNearbyPicker(anchorBtn); - - const wrapper = document.createElement("div"); - wrapper.className = "hol-geocode-picker"; - - // Header - const header = document.createElement("div"); - header.className = "hol-geocode-picker__header"; - header.innerHTML = `Choix multiples`; - header - .querySelector(".hol-close") - .addEventListener("click", () => removeNearbyPicker(anchorBtn)); - wrapper.appendChild(header); - - // Liste - const list = document.createElement("ul"); - list.className = "hol-geocode-picker__list"; - - results.forEach((r) => { - const li = document.createElement("li"); - li.className = "hol-geocode-picker__item"; - li.innerHTML = ` -
- ${esc(r.display_name)} - (${r.lat}, ${r.lng}) -
- - `; - li.querySelector("button").addEventListener("click", () => onPick(r)); - list.appendChild(li); - }); - - wrapper.appendChild(list); - - // Insertion après le conteneur du bouton - const container = - anchorBtn.closest(".hol-inline") || anchorBtn.parentElement; - container.insertAdjacentElement("afterend", wrapper); - } - - function removeNearbyPicker(anchorBtn) { - const container = - anchorBtn.closest(".hol-inline") || anchorBtn.parentElement; - const next = container?.nextElementSibling; - if (next && next.classList.contains("hol-geocode-picker")) { - next.remove(); - } - } - - // --- 6. CARTE LEAFLET --- - let mapInitialized = false; - let mapInstance; - - // Fonction d'initialisation de la carte (appelée à l'ouverture de la modale) - function initMap() { - if (mapInitialized) { - // Si déjà init, on force juste le redimensionnement pour éviter les bugs d'affichage - setTimeout(() => mapInstance.invalidateSize(), 200); - return; - } - - if (typeof L === "undefined") { - console.error("Leaflet n'est pas chargé."); - return; - } - - // Récupération sécurisée des données injectées par PHP - const MAP_DATA = Array.isArray(window.HOL_MAP_DATA) - ? window.HOL_MAP_DATA - : []; - - mapInstance = L.map("hol-map", { scrollWheelZoom: true }); - L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { - attribution: "© OpenStreetMap", - }).addTo(mapInstance); - - const markers = []; - - MAP_DATA.forEach((it) => { - const lat = parseFloat(it.lat); - const lng = parseFloat(it.lng); - if (!Number.isFinite(lat) || !Number.isFinite(lng)) return; - - // Code couleur selon statut - const colors = { - planned: "#16a34a", - favorite: "#f59e0b", - shortlist: "#3b82f6", - default: "#6b7280", - }; - const color = colors[it.status] || colors.default; - - const m = L.circleMarker([lat, lng], { - radius: 7, - color: "#ffffff", - weight: 1, - fillColor: color, - fillOpacity: 0.9, - }).addTo(mapInstance); - - // Construction du popup - const loc = [it.city, it.region, it.country].filter(Boolean).join(", "); - const dates = it.desired_start_date - ? `${it.desired_start_date}${it.desired_end_date ? " → " + it.desired_end_date : ""}` - : null; - - m.bindPopup(` -
- ${esc(it.title)} -
${esc(loc)}
- ${dates ? `
📅 ${esc(dates)}
` : ""} -
- ${esc(it.status)} - Voir -
-
- `); - - markers.push(m); - }); - - // Centrage de la carte - if (markers.length > 0) { - const group = L.featureGroup(markers); - mapInstance.fitBounds(group.getBounds(), { padding: [50, 50] }); - } else { - mapInstance.setView([46.603354, 1.888334], 5); // France par défaut si vide - } - - mapInitialized = true; - - // Hack indispensable pour que Leaflet calcule la bonne taille dans une modale - setTimeout(() => mapInstance.invalidateSize(), 200); - } - - // Connexion de la carte à la modale - const mapBtn = document.getElementById("hol-map-open"); - if (mapBtn) { - const mapModalCtrl = setupModal("hol-map-modal", initMap); // On passe initMap en callback d'ouverture - mapBtn.addEventListener("click", mapModalCtrl.open); - } + // Initialisation des écouteurs globaux si besoin }); + +// --- 1. GESTION DE LA MODALE D'ÉDITION --- + +function openHolidayModal(mode) { + const modal = document.getElementById("holidayModal"); + const form = document.getElementById("holidayForm"); + const btnDelete = document.getElementById("btn_delete"); + + // Reset complet + form.reset(); + document.getElementById("inp_id").value = ""; + document.getElementById("list_transport").innerHTML = ""; + document.getElementById("list_accommodation").innerHTML = ""; + document.getElementById("list_activity").innerHTML = ""; + + if (mode === "add") { + document.getElementById("modalTitle").innerText = "Nouveau Voyage"; + btnDelete.style.display = "none"; + } else { + document.getElementById("modalTitle").innerText = "Modifier le voyage"; + btnDelete.style.display = "block"; + } + + modal.classList.add("open"); + modal.style.display = "flex"; + + // --- AJOUT : FORCER LE SCROLL EN HAUT --- + const content = modal.querySelector(".pf-modal-content"); + if (content) { + content.scrollTop = 0; + } + // Optionnel : Mettre le focus sur le premier champ (Titre) + // setTimeout(() => document.getElementById('inp_title').focus(), 50); +} + +function closeHolidayModal() { + const modal = document.getElementById("holidayModal"); + modal.classList.remove("open"); + modal.style.display = "none"; +} + +// Fonction appelée au clic sur une carte (injectée par PHP) +function editHoliday(data) { + openHolidayModal("edit"); + + const h = data.main; + + // Remplissage des champs principaux + document.getElementById("inp_id").value = h.id; + document.getElementById("inp_title").value = h.title; + document.getElementById("inp_status").value = h.status; + 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 = + h.budget_food > 0 ? h.budget_food : ""; + document.getElementById("inp_extra").value = + h.budget_extra > 0 ? h.budget_extra : ""; + document.getElementById("inp_notes").value = h.notes; + + // Remplissage des listes dynamiques + if (data.items && data.items.length > 0) { + data.items.forEach((item) => { + addItem(item.category, item.name, item.amount, item.is_paid); + }); + } +} + +// --- 2. GESTION DES LISTES DYNAMIQUES (Transport, etc.) --- + +function addItem(category, name = "", amount = "", isPaid = 0) { + const container = document.getElementById("list_" + category); + const div = document.createElement("div"); + + // Utilisation des classes CSS définies précédemment + div.className = "savings-line-item"; + + // Checkbox logique pour "Payé" + const checkedAttr = isPaid == 1 ? "checked" : ""; + + div.innerHTML = ` + + + + + + + + + + `; + + container.appendChild(div); +} + +function deleteHoliday() { + if (!confirm("Supprimer définitivement ce voyage ?")) return; + + const form = document.getElementById("holidayForm"); + + // On ajoute un input caché pour signaler la suppression au PHP + const input = document.createElement("input"); + input.type = "hidden"; + input.name = "action_delete"; + input.value = "1"; + + form.appendChild(input); + form.submit(); +} + +// --- 3. GESTION DE LA CARTE (Leaflet) --- + +let map = null; + +function toggleMap() { + const modal = document.getElementById("hol-map-modal"); + + if (modal.style.display === "flex") { + modal.style.display = "none"; + } else { + modal.style.display = "flex"; + // Petit délai pour laisser le temps au DOM de s'afficher avant d'init Leaflet + setTimeout(initMap, 100); + } +} + +function initMap() { + if (map) { + map.invalidateSize(); // Recalcule la taille si la fenêtre a changé + return; + } + + if (typeof L === "undefined") return; + + // Centré sur l'Europe par défaut + map = L.map("hol-map").setView([46.6, 2.4], 4); + + L.tileLayer( + "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png", + { + attribution: "© OpenStreetMap", + }, + ).addTo(map); + + // On récupère les points définis en global dans le PHP + if (typeof HOL_MAP_POINTS !== "undefined") { + HOL_MAP_POINTS.forEach((pt) => { + const color = + pt.status === "planned" || pt.status === "booked" ? "green" : "blue"; + + // Cercle simple + L.circleMarker([pt.lat, pt.lng], { + color: color, + radius: 8, + fillOpacity: 0.8, + }) + .addTo(map) + .bindPopup(`${pt.title}
${pt.status}`); + }); + } +} diff --git a/modules/holidays/index.php b/modules/holidays/index.php index 713e87b..2568b5c 100644 --- a/modules/holidays/index.php +++ b/modules/holidays/index.php @@ -1,459 +1,202 @@ query($sql)->fetchAll(PDO::FETCH_ASSOC); -if (!function_exists('hol_q')) { - function hol_q(PDO $pdo, string $sql, array $params = []): array { - $st = $pdo->prepare($sql); - $st->execute($params); - return $st->fetchAll(PDO::FETCH_ASSOC); - } -} - -$ideaId = isset($_GET['id']) ? (int)$_GET['id'] : 0; -$isDetailView = ($ideaId > 0); -$currentIdea = null; // Stockera les données si on est en vue détail - -// Préparation des variables par défaut pour éviter les erreurs "undefined variable" -$transport = []; $lodging = []; $acts = []; $budget = []; -$fixedTotal = 0; $ppTotal = 0; -$planned = []; $ideas = []; $archived = []; - -if ($isDetailView) { - // --- MODE DÉTAIL --- - - // 1. Récupérer l'idée principale - $rows = hol_q($pdo, "SELECT * FROM pf_holidays_ideas WHERE id = ?", [$ideaId]); - $currentIdea = $rows[0] ?? null; - - if ($currentIdea) { - // 2. Récupérer les données pour la CARTE (un seul point) - // Le JS filtrera lat/lng invalides - $mapIdeas = hol_q($pdo, " - SELECT id, title, country, region, city, - CAST(lat AS DECIMAL(9,6)) AS lat, - CAST(lng AS DECIMAL(9,6)) AS lng, - status, desired_start_date, desired_end_date - FROM pf_holidays_ideas - WHERE id = ? - ", [$ideaId]); - - // 3. Récupérer les sous-éléments (Transport, Logement, etc.) - $transport = hol_q($pdo, "SELECT * FROM pf_holidays_transport WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]); - $lodging = hol_q($pdo, "SELECT * FROM pf_holidays_lodging WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]); - $acts = hol_q($pdo, "SELECT * FROM pf_holidays_activities WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]); - $budget = hol_q($pdo, "SELECT category, label, amount, per_person FROM pf_holidays_budget_items WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]); - - // 4. Calculs totaux budget - $sumRow = hol_q($pdo, " - SELECT - SUM(CASE WHEN per_person=0 THEN amount ELSE 0 END) AS fixed_total, - SUM(CASE WHEN per_person=1 THEN amount ELSE 0 END) AS per_person_total - FROM pf_holidays_budget_items WHERE idea_id = ? - ", [$ideaId])[0] ?? ['fixed_total' => 0, 'per_person_total' => 0]; - - $fixedTotal = (float)($sumRow['fixed_total'] ?? 0); - $ppTotal = (float)($sumRow['per_person_total'] ?? 0); - } - -} else { - // --- MODE LISTE --- - - // 1. Données pour la CARTE (tous les points valides) - $mapIdeas = hol_q($pdo, " - SELECT id, title, country, region, city, - CAST(lat AS DECIMAL(9,6)) AS lat, - CAST(lng AS DECIMAL(9,6)) AS lng, - status, desired_start_date, desired_end_date - FROM pf_holidays_ideas - WHERE status IN ('draft','shortlist','favorite','planned') - AND lat IS NOT NULL - AND lng IS NOT NULL - "); - - // 2. Récupérer les listes - $planned = hol_q($pdo, " - SELECT * FROM pf_holidays_ideas - WHERE status = 'planned' - ORDER BY COALESCE(desired_start_date, created_at) DESC - "); - $ideas = hol_q($pdo, " - SELECT * FROM pf_holidays_ideas - WHERE status IN ('draft','shortlist','favorite') - ORDER BY FIELD(status,'favorite','shortlist','draft'), created_at DESC - "); - $archived = hol_q($pdo, " - SELECT * FROM pf_holidays_ideas - WHERE status = 'archived' - ORDER BY updated_at DESC - "); -} +// Tri par statut +$active = array_filter($holidays, fn($h) => in_array($h['status'], ['draft', 'planned', 'booked'])); +$history = array_filter($holidays, fn($h) => in_array($h['status'], ['passed', 'archived'])); ?> - - - - - -
-

Idée introuvable

- -
-

Cette idée de vacances n'existe pas ou a été supprimée.

- -
-

-
- ← Retour - - - -
+
+ +
+

Mes Vacances ✈️

+
+
+
-

- - - • Dates: - - • Saison: +

+
+ +

Aucun voyage en cours. Planifions quelque chose !

- - • Durée idéale: j - - • Statut: -

-
-
-

Transport

-
- - - - - - - - - -
-
    - -
  • - - - 🔗 -
  • - -
-
- -
-

Hébergement

-
- - - - - - - - - - - -
-
    - -
  • - - - - 🔗 -
  • - -
-
- -
-

Activités

-
- - - - - - - - - - -
-
    - -
  • - - - 🔗 -
  • - -
-
- -
-

Budget

-
- - - - - - - -
- -
- Fixe: € - • Par personne: € -
- -
    - -
  • [] — - -
  • - -
-
+ + +
+
+ + +
+

Historique

+
+ + + +
+
- -
-

Idées de vacances

-
- - -
-
+
-
-

Vacances planifiées

-

Dates souhaitées, prêtes à être réservées.

-
- -
-
-

- planned -
-

- - - • Dates: - - - • Durée idéale: j - -

- -

- -
- Ouvrir - - -
+
+
+

Planifier le voyage

+ +
+ + + +
+
+ +
- -
-
- -
-

Idées

-

Brouillons, favoris, shortlist.

-
- -
-
-

- -
-

- - - • Dates: - - • Saison: - - - • Durée idéale: j - -

- -

- -
- Ouvrir - - -
-
- -
-
- -
-

Archivées

-
- -
-

-
- - -
-
- -
-
- - - - - - -