diff --git a/holidays.php b/holidays.php index 5be4281..e90bfa3 100644 --- a/holidays.php +++ b/holidays.php @@ -7,15 +7,27 @@ require __DIR__ . '/includes/db.php'; if (session_status() === PHP_SESSION_NONE) { session_start(); } -$pageTitle = "PachaFamily - Idées de vacances"; +// 1. On récupère la vue demandée (par défaut 'list') +$tab = $_GET['tab'] ?? 'list'; + +// 2. Configuration des variables pour le header +$pageTitle = ($tab === 'holiday_detail') ? "PachaFamily - Détail du voyage" : "PachaFamily - Mes Vacances"; $activePage = "holidays"; $bodyClass = "pf-holidays"; -// On charge le CSS spécifique au module $pageCss = "/modules/holidays/holidays.css"; require __DIR__ . '/header.php'; -// Inclusion de la logique et de la vue unifiées -require __DIR__ . '/modules/holidays/index.php'; +// 3. ROUTEUR DU MODULE VACANCES +if ($tab === 'holiday_detail' && isset($_GET['id'])) { + // Si on demande le détail ET qu'un ID est fourni + require __DIR__ . '/modules/holidays/views/detail.php'; +} else { + // Vue par défaut : la liste des cartes + require __DIR__ . '/modules/holidays/views/list.php'; +} + +// 4. Inclusion du JS global du module (s'applique aux deux vues) +echo ''; require __DIR__ . '/footer.php'; \ No newline at end of file diff --git a/modules/holidays/holidays.js b/modules/holidays/holidays.js index 160729c..d4007e2 100644 --- a/modules/holidays/holidays.js +++ b/modules/holidays/holidays.js @@ -1,15 +1,21 @@ document.addEventListener("DOMContentLoaded", () => { - // Initialisation des écouteurs globaux si besoin + // Fermer les modales si on clique en dehors du contenu + window.onclick = function (event) { + const modal = document.getElementById("holidayModal"); + if (event.target == modal) { + closeHolidayModal(); + } + }; }); -// --- 1. GESTION DE LA MODALE D'ÉDITION --- +// --- 1. GESTION DE LA MODALE D'ÉDITION RAPIDE --- function openHolidayModal(mode) { const modal = document.getElementById("holidayModal"); const form = document.getElementById("holidayForm"); const btnDelete = document.getElementById("btn_delete"); - // Reset complet + // Reset complet pour éviter les résidus d'une carte précédente form.reset(); document.getElementById("inp_id").value = ""; document.getElementById("list_transport").innerHTML = ""; @@ -17,51 +23,46 @@ function openHolidayModal(mode) { document.getElementById("list_activity").innerHTML = ""; if (mode === "add") { - document.getElementById("modalTitle").innerText = "Nouveau Voyage"; + document.getElementById("modalTitle").innerText = "Planifier le voyage"; btnDelete.style.display = "none"; } else { - document.getElementById("modalTitle").innerText = "Modifier le voyage"; + document.getElementById("modalTitle").innerText = "Modification rapide"; 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); + // Focus sur le champ titre pour une saisie rapide (petit délai pour l'animation d'ouverture) + setTimeout(() => document.getElementById("inp_title").focus(), 100); } function closeHolidayModal() { - const modal = document.getElementById("holidayModal"); - modal.classList.remove("open"); - modal.style.display = "none"; + document.getElementById("holidayModal").style.display = "none"; } -// Fonction appelée au clic sur une carte (injectée par PHP) +// Fonction appelée au clic sur le bouton ✏️ de la carte (injectée par PHP) function editHoliday(data) { - openHolidayModal("edit"); - const h = data.main; + // On ouvre la modale en mode édition (ça reset les listes) + openHolidayModal("edit"); + // 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_period").value = h.period_hint || ""; + document.getElementById("inp_start").value = h.start_date || ""; + document.getElementById("inp_end").value = h.end_date || ""; + + // Pour éviter d'afficher "0" dans un champ vide, on vérifie si la valeur est > 0 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; + document.getElementById("inp_notes").value = h.notes || ""; - // Remplissage des listes dynamiques + // Remplissage des listes dynamiques (Transport, Hébergement, Activité) if (data.items && data.items.length > 0) { data.items.forEach((item) => { addItem(item.category, item.name, item.amount, item.is_paid); @@ -69,36 +70,40 @@ function editHoliday(data) { } } -// --- 2. GESTION DES LISTES DYNAMIQUES (Transport, etc.) --- +// --- 2. GESTION DES LISTES DYNAMIQUES DANS LA MODALE --- 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"; + // Style en ligne pour s'assurer que ça reste propre sans dépendre de classes externes complexes + div.style.display = "flex"; + div.style.gap = "8px"; + div.style.alignItems = "center"; + div.style.marginBottom = "10px"; - // Checkbox logique pour "Payé" + // Astuce pour lier la checkbox visuelle à l'input caché (valeur 0 ou 1 pour MySQL) const checkedAttr = isPaid == 1 ? "checked" : ""; div.innerHTML = ` + placeholder="Intitulé" value="${name}" + style="flex: 2; padding: 8px; font-size:0.9rem;" required> + placeholder="Prix (€)" value="${amount}" + style="width: 80px; text-align: right; padding: 8px; font-size:0.9rem;"> - + - Payé + Payé - + × `; @@ -107,11 +112,16 @@ function addItem(category, name = "", amount = "", isPaid = 0) { } function deleteHoliday() { - if (!confirm("Supprimer définitivement ce voyage ?")) return; + if ( + !confirm( + "Voulez-vous vraiment supprimer définitivement ce voyage ? Cette action est irréversible.", + ) + ) + return; const form = document.getElementById("holidayForm"); - // On ajoute un input caché pour signaler la suppression au PHP + // On injecte un input caché pour signaler au PHP que c'est une demande de suppression const input = document.createElement("input"); input.type = "hidden"; input.name = "action_delete"; @@ -121,7 +131,7 @@ function deleteHoliday() { form.submit(); } -// --- 3. GESTION DE LA CARTE (Leaflet) --- +// --- 3. GESTION DE LA CARTE (Leaflet - Optionnel selon si tu l'utilises ou non) --- let map = null; @@ -172,3 +182,234 @@ function initMap() { }); } } +// ============================================================================ +// 4. GESTION DE LA CARTE DÉTAILLÉE (ROADTRIP) ET GÉOCODAGE +// ============================================================================ + +let detailMap = null; + +document.addEventListener("DOMContentLoaded", () => { + // Si on est sur la page détail et que la div "tripMap" existe, on initie la carte + if (document.getElementById("tripMap")) { + initDetailMap(); + } +}); + +function initDetailMap() { + if (typeof L === "undefined" || typeof MAP_POINTS === "undefined") return; + + 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); + + if (MAP_POINTS.length === 0) { + detailMap.setView([46.6, 2.4], 5); // Centré sur la France par défaut si vide + return; + } + + const latlngs = []; + const bounds = L.latLngBounds(); + + MAP_POINTS.forEach((pt, index) => { + const pos = [pt.lat, pt.lng]; + latlngs.push(pos); + bounds.extend(pos); + + // Couleur selon le paiement + const color = pt.paid == 1 ? "#10b981" : "#f59e0b"; + + // On place un petit cercle pour chaque point + const marker = L.circleMarker(pos, { + color: color, + radius: 7, + fillOpacity: 1, + fillColor: "white", + weight: 3, + }).addTo(detailMap); + + // Numérotation et Popup + marker.bindPopup(` + + Étape ${index + 1} + ${pt.title} + ${parseFloat(pt.amount).toFixed(2)} € + + `); + }); + + // On dessine le trait en pointillés pour relier le roadtrip ! + if (latlngs.length > 1) { + L.polyline(latlngs, { + color: "#3b82f6", + weight: 3, + dashArray: "8, 8", + opacity: 0.7, + }).addTo(detailMap); + } + + // On zoome automatiquement pour voir tous les points + detailMap.fitBounds(bounds, { padding: [50, 50] }); +} + +// Fonction appelée quand on clique sur "👁️ Voir sur la carte" dans la liste +function panMapTo(lat, lng) { + if (detailMap) { + detailMap.setView([lat, lng], 14, { animate: true }); + } +} + +// --- LOGIQUE DE LA MODALE CHECKPOINT (Lignes multiples) --- + +function openCheckpointModal(mode, data = null) { + const searchBlock = document.getElementById("cpSearchBlock"); + const formBlock = document.getElementById("formCheckpoint"); + const container = document.getElementById("cpExpensesContainer"); + const btnDel = document.getElementById("btnDeleteCp"); + + container.innerHTML = ""; // Reset des lignes + + if (mode === "add") { + document.getElementById("cpModalTitle").innerText = + "📍 Placer une nouvelle étape"; + searchBlock.style.display = "block"; + formBlock.style.display = "none"; + btnDel.style.display = "none"; + + document.getElementById("searchPlaceInput").value = ""; + document.getElementById("searchResults").innerHTML = ""; + + document.getElementById("cp_old_name").value = ""; + document.getElementById("cp_name").value = ""; + + // Ajout d'une ligne vide par défaut + addCpExpenseLine(); + } else if (mode === "edit" && data) { + document.getElementById("cpModalTitle").innerText = "✏️ Modifier l'étape"; + searchBlock.style.display = "none"; // On cache la recherche en mode édition + formBlock.style.display = "block"; + btnDel.style.display = "block"; + + document.getElementById("cp_lat").value = data.lat; + document.getElementById("cp_lng").value = data.lng; + document.getElementById("cp_old_name").value = data.location_name; + document.getElementById("cp_name").value = data.location_name; + + // Remplissage des lignes existantes (en filtrant le point technique) + if (data.items && data.items.length > 0) { + let visibleCount = 0; + data.items.forEach((it) => { + if (it.name !== "PF_TECHNICAL_POINT") { + addCpExpenseLine(it.category, it.name, it.amount, it.is_paid); + visibleCount++; + } + }); + // Si l'étape ne contenait QUE le point technique, on affiche une ligne vide pour inviter à la saisie + if (visibleCount === 0) { + addCpExpenseLine(); + } + } else { + addCpExpenseLine(); // Sécurité + } + } + + document.getElementById("checkpointModal").style.display = "flex"; +} + +function searchPlace() { + const q = document.getElementById("searchPlaceInput").value.trim(); + if (q.length < 3) return; + + const resultsDiv = document.getElementById("searchResults"); + resultsDiv.innerHTML = + 'Recherche en cours... ⏳'; + + fetch( + "/modules/holidays/includes/api/geocode.php?limit=5&q=" + + encodeURIComponent(q), + ) + .then((res) => res.json()) + .then((data) => { + resultsDiv.innerHTML = ""; + if (data.error || !data.results || data.results.length === 0) { + resultsDiv.innerHTML = + 'Aucun résultat trouvé.'; + return; + } + + data.results.forEach((place) => { + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "pf-btn btn-secondary"; + btn.style.textAlign = "left"; + btn.style.padding = "8px"; + btn.style.height = "auto"; + btn.innerText = "📍 " + place.display_name; + btn.onclick = () => + selectPlace(place.lat, place.lng, place.display_name); + resultsDiv.appendChild(btn); + }); + }) + .catch((err) => { + resultsDiv.innerHTML = + 'Erreur réseau.'; + }); +} + +function selectPlace(lat, lng, fullName) { + document.getElementById("cp_lat").value = lat; + document.getElementById("cp_lng").value = lng; + // On nettoie le nom pour que ce soit joli (ex: "Paris, France" -> "Paris") + document.getElementById("cp_name").value = fullName.split(",")[0].trim(); + document.getElementById("cpSearchBlock").style.display = "none"; + document.getElementById("formCheckpoint").style.display = "block"; +} + +function addCpExpenseLine( + category = "accommodation", + name = "", + amount = "", + isPaid = 0, +) { + const container = document.getElementById("cpExpensesContainer"); + const div = document.createElement("div"); + div.style.display = "flex"; + div.style.gap = "8px"; + div.style.alignItems = "center"; + + const isChecked = isPaid == 1 ? "checked" : ""; + + div.innerHTML = ` + + 🏨 + 🚗 + 🎫 + + + + + + + Payé + + × + `; + container.appendChild(div); +} + +function deleteCheckpoint() { + if (!confirm("Supprimer cette étape et toutes les dépenses associées ?")) + return; + const form = document.getElementById("formCheckpoint"); + const input = document.createElement("input"); + input.type = "hidden"; + input.name = "action_delete"; + input.value = "1"; + form.appendChild(input); + form.submit(); +} diff --git a/modules/holidays/geocode.php b/modules/holidays/includes/api/geocode.php similarity index 71% rename from modules/holidays/geocode.php rename to modules/holidays/includes/api/geocode.php index 493d7b4..c0adf8b 100644 --- a/modules/holidays/geocode.php +++ b/modules/holidays/includes/api/geocode.php @@ -1,8 +1,9 @@ 10) $limit = 10; // Nominatim bloque souvent au-dessus de 10-50 +if ($limit > 10) $limit = 10; // 2. Normalisation pour le cache $qNorm = mb_strtolower($q); $qHash = hash('sha256', $qNorm); -// 3. Création automatique de la table cache si elle n'existe pas (Sécurité) +// 3. Création automatique de la table cache try { $pdo->exec(" CREATE TABLE IF NOT EXISTS pf_geocode_cache ( @@ -35,13 +36,9 @@ try { updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; "); -} catch (Throwable $e) { - // On continue même si ça échoue (l'admin devra créer la table manuellement) -} +} catch (Throwable $e) {} -// 4. Vérification du cache (Même si limit > 1) -// Stratégie : Si on a déjà cherché exactement "Paris", on renvoie le résultat stocké -// pour économiser l'API et aller plus vite. Le JS gérera le résultat unique. +// 4. Vérification du cache try { $st = $pdo->prepare("SELECT lat, lng, display_name FROM pf_geocode_cache WHERE q_hash = ?"); $st->execute([$qHash]); @@ -54,11 +51,9 @@ try { ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } -} catch (Throwable $e) { - // Erreur SQL silencieuse sur le cache -} +} catch (Throwable $e) {} -// 5. Appel Nominatim (Si pas en cache) +// 5. Appel Nominatim $endpoint = 'https://nominatim.openstreetmap.org/search'; $params = http_build_query([ 'format' => 'jsonv2', @@ -75,8 +70,6 @@ curl_setopt_array($ch, [ CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => [ - // Ton User-Agent est correct. - // Important : Nominatim demande une identification claire. 'User-Agent: PachaFamily-Holidays/1.0 (+contact: ferlan.alexandre@gmail.com)' ], ]); @@ -85,9 +78,8 @@ $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch); -// Gestion erreur cURL / HTTP if ($body === false || $http !== 200) { - http_response_code(502); // Bad Gateway + http_response_code(502); echo json_encode([ 'error' => 'geocode_failed', 'details' => $err ?: ('HTTP ' . $http) @@ -97,17 +89,13 @@ if ($body === false || $http !== 200) { $data = json_decode($body, true); -// Gestion erreur JSON ou vide if (!is_array($data) || empty($data)) { - // On renvoie 200 avec une liste vide ou 404, le JS gère les deux. - // 404 est plus sémantique "Not Found". http_response_code(404); echo json_encode(['error' => 'not_found']); exit; } -// 6. Mise en cache du PREMIER résultat (Le "meilleur") -// On ne cache que le top result pour simplifier la structure de la DB. +// 6. Mise en cache if (isset($data[0])) { $r = $data[0]; $lat = round((float)$r['lat'], 6); @@ -124,9 +112,7 @@ if (isset($data[0])) { } catch (Throwable $e) {} } -// 7. Retour des résultats -// Si limit=1, on renvoie format plat (pour compatibilité stricte) -// Si limit>1, on renvoie format liste +// 7. Retour if ($limit === 1) { $r = $data[0]; echo json_encode([ diff --git a/modules/holidays/view.php b/modules/holidays/includes/api/get_holiday_data.php similarity index 69% rename from modules/holidays/view.php rename to modules/holidays/includes/api/get_holiday_data.php index 024aacc..3a554da 100644 --- a/modules/holidays/view.php +++ b/modules/holidays/includes/api/get_holiday_data.php @@ -1,9 +1,9 @@ prepare("SELECT * FROM pf_holidays_ideas WHERE id = ?"); $st->execute([$id]); $it = $st->fetch(PDO::FETCH_ASSOC); @@ -26,17 +27,12 @@ try { exit; } - // Amélioration : Typage explicite pour le JSON - // Cela évite que JS reçoive "4" (string) au lieu de 4 (int) pour les calculs $it['id'] = (int)$it['id']; if (isset($it['lat'])) $it['lat'] = (float)$it['lat']; if (isset($it['lng'])) $it['lng'] = (float)$it['lng']; if (isset($it['ideal_days'])) $it['ideal_days'] = (int)$it['ideal_days']; - // On s'assure que les null restent null et pas des chaines vides si la DB est stricte - // (Optionnel mais propre) - echo json_encode($it, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } catch (Throwable $e) { diff --git a/modules/holidays/includes/api/save_checkpoint.php b/modules/holidays/includes/api/save_checkpoint.php new file mode 100644 index 0000000..c9fa577 --- /dev/null +++ b/modules/holidays/includes/api/save_checkpoint.php @@ -0,0 +1,81 @@ +prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND location_name = ?")->execute([$holiday_id, $loc]); + header("Location: /holidays.php?tab=holiday_detail&id=" . $holiday_id); + exit; +} + +// Ajout / Modification d'une étape +$location_name = trim($_POST['location_name']); +$old_location = trim($_POST['old_location_name'] ?? ''); +$lat = (float)$_POST['lat']; +$lng = (float)$_POST['lng']; + +if ($holiday_id > 0 && !empty($location_name)) { + try { + $pdo->beginTransaction(); + + // 1. GESTION DES FAVORIS + if (isset($_POST['save_favorite']) && $_POST['save_favorite'] == '1') { + $stmtFav = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'holiday_favorites'"); + $favs = json_decode($stmtFav->fetchColumn() ?: '[]', true); + $exists = false; + foreach ($favs as $f) { if ($f['name'] === $location_name) $exists = true; } + if (!$exists) { + $favs[] = ['name' => $location_name, 'lat' => $lat, 'lng' => $lng]; + $pdo->prepare("INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('holiday_favorites', 'GLOBAL', ?) ON DUPLICATE KEY UPDATE content = VALUES(content)")->execute([json_encode($favs)]); + } + } + + // 2. GESTION DES DÉPENSES + if (!empty($old_location)) { + $pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND location_name = ?")->execute([$holiday_id, $old_location]); + } + + $stmt = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); + $validItemsCount = 0; + + if (isset($_POST['items']['name'])) { + $count = count($_POST['items']['name']); + for ($i = 0; $i < $count; $i++) { + $name = trim($_POST['items']['name'][$i]); + $amount_raw = $_POST['items']['amount'][$i]; + + // Si la ligne n'est pas totalement vide + if ($name !== '' || $amount_raw !== '') { + $cat = $_POST['items']['cat'][$i] ?? 'activity'; + $amount = (float)$amount_raw; + if ($name === '') $name = 'Dépense liée'; + $paid = isset($_POST['items']['paid'][$i]) ? 1 : 0; + + $stmt->execute([$holiday_id, $cat, $name, $amount, $paid, $location_name, $lat, $lng]); + $validItemsCount++; + } + } + } + + // 3. ÉTAPE SANS DÉPENSE (Point de passage) + // Si l'utilisateur n'a saisi aucune dépense, on crée une ligne technique invisible pour forcer l'affichage du point GPS + if ($validItemsCount === 0) { + $stmt->execute([$holiday_id, 'activity', 'PF_TECHNICAL_POINT', 0, 1, $location_name, $lat, $lng]); + } + + $pdo->commit(); + } catch (Exception $e) { + $pdo->rollBack(); + die("Erreur de sauvegarde : " . $e->getMessage()); + } +} + +header("Location: /holidays.php?tab=holiday_detail&id=" . $holiday_id); +exit; \ No newline at end of file diff --git a/modules/holidays/save_new.php b/modules/holidays/includes/api/save_holiday.php similarity index 67% rename from modules/holidays/save_new.php rename to modules/holidays/includes/api/save_holiday.php index 6a5ae40..64f619f 100644 --- a/modules/holidays/save_new.php +++ b/modules/holidays/includes/api/save_holiday.php @@ -1,7 +1,10 @@ includes -> holidays -> modules -> racine) +require dirname(__DIR__, 4) . '/includes/auth.php'; +require dirname(__DIR__, 4) . '/includes/db.php'; +require_login(); // Si cette fonction nécessite une redirection, gère-la dans auth.php if (isset($_POST['action_delete']) && $_POST['action_delete'] == '1') { $stmt = $pdo->prepare("DELETE FROM pf_holidays WHERE id = ?"); @@ -36,18 +39,19 @@ try { $id = $pdo->lastInsertId(); } - // GESTION DES ITEMS (On supprime tout et on recrée) - $pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ?")->execute([$id]); + // GESTION DES ITEMS GLOBAUX (On ne supprime QUE les items qui n'ont pas de lieu défini !) + $pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND location_name IS NULL")->execute([$id]); if (!empty($_POST['items']['name'])) { $stmtItem = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid) VALUES (?, ?, ?, ?, ?)"); $count = count($_POST['items']['name']); for ($i = 0; $i < $count; $i++) { - $cat = $_POST['items']['cat'][$i]; - $name = trim($_POST['items']['name'][$i]); - $amount = floatval($_POST['items']['amount'][$i]); - $paid = $_POST['items']['paid'][$i]; + // Utilisation de "?? ''" pour sécuriser si la donnée n'est pas envoyée + $cat = $_POST['items']['cat'][$i] ?? ''; + $name = trim($_POST['items']['name'][$i] ?? ''); + $amount = floatval($_POST['items']['amount'][$i] ?? 0); + $paid = isset($_POST['items']['paid'][$i]) ? $_POST['items']['paid'][$i] : 0; if (!empty($name)) { $stmtItem->execute([$id, $cat, $name, $amount, $paid]); @@ -59,8 +63,9 @@ try { } catch (Exception $e) { $pdo->rollBack(); - die("Erreur : " . $e->getMessage()); + die("Erreur base de données : " . $e->getMessage()); } +// Redirection vers la page principale header("Location: /holidays.php"); exit; \ No newline at end of file diff --git a/modules/holidays/index.php b/modules/holidays/index.php deleted file mode 100644 index 884eb2f..0000000 --- a/modules/holidays/index.php +++ /dev/null @@ -1,229 +0,0 @@ -query($sql)->fetchAll(PDO::FETCH_ASSOC); - -// 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'])); -?> - - - - - Mes Vacances ✈️ - - + Créer un voyage - - - - - - - Aucun voyage en cours. Planifions quelque chose ! - - - - - - - - - - - Historique - - - - - - - - - - - - - Planifier le voyage - - - - - - - - Nom du voyage - - - - Statut - - Brouillon ✏️ - Planifié 📅 - Réservé ✅ - Passé 👋 - Archivé 🗄️ - - - - - - - Période (Texte libre) - - - - - Du - - - - Au - - - - - - - - - - - - 🚗 Transport - + - - - - - - - 🏨 Hébergement - + - - - - - - - 🎫 Activité - + - - - - - - - - - - 🍔 Budget Food & Bev (€) - - - - 🎁 Budget Extras (€) - - - - - - Notes - - - - - - - - -prepare("SELECT * FROM pf_holidays_items WHERE holiday_id = ?"); - $stmt->execute([$h['id']]); - $items = $stmt->fetchAll(PDO::FETCH_ASSOC); - - $json = htmlspecialchars(json_encode(['main' => $h, 'items' => $items]), ENT_QUOTES, 'UTF-8'); - $dateDisplay = htmlspecialchars($h['period_hint'] ?? ''); - - if (empty($dateDisplay) && $h['start_date']) { - $dateDisplay = date('d/m/Y', strtotime($h['start_date'])); - if ($h['end_date']) $dateDisplay .= ' → ' . date('d/m/Y', strtotime($h['end_date'])); - } - - $statusClass = match($h['status']) { - 'booked' => 'bg-green-100 text-green-800', - 'planned' => 'bg-blue-100 text-blue-800', - 'passed' => 'bg-gray-100 text-gray-600', - default => 'bg-yellow-50 text-yellow-800' - }; - - // --- NOUVEAU : Calcul de la progression --- - $cost = (float)$h['total_cost']; - $funded = (float)$h['total_funded']; - $leftToPay = max(0, $cost - $funded); - - // Pourcentage pour la barre de progression (max 100%) - $percent = $cost > 0 ? min(100, round(($funded / $cost) * 100)) : 0; - - // Couleur de la barre : Rouge si < 50%, Jaune si < 100%, Vert si tout est payé - $barColor = $percent === 100 ? '#10b981' : ($percent > 50 ? '#f59e0b' : '#ef4444'); - - echo " - - - ".htmlspecialchars($h['title'])." - - ".strtoupper($h['status'])." - - - - 🗓️ ".($dateDisplay ?: 'Dates à définir')." - - - - - - Budget Total - ".number_format($cost, 0, ',', ' ')." € - - - - - - - - ✓ Financé : ".number_format($funded, 0, ',', ' ')." € - Reste : ".number_format($leftToPay, 0, ',', ' ')." € - - - - - "; -} -?> - - \ No newline at end of file diff --git a/modules/holidays/views/detail.php b/modules/holidays/views/detail.php new file mode 100644 index 0000000..6dcc46f --- /dev/null +++ b/modules/holidays/views/detail.php @@ -0,0 +1,242 @@ +Erreur."; exit; } + +$stmt = $pdo->prepare(" + SELECT h.*, + (COALESCE(h.budget_food, 0) + COALESCE(h.budget_extra, 0) + COALESCE((SELECT SUM(amount) FROM pf_holidays_items WHERE holiday_id = h.id), 0)) as total_cost, + (SELECT COALESCE(SUM(ABS(amount)), 0) FROM pf_expenses WHERE holiday_id = h.id) as total_paid, + (SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved + FROM pf_holidays h WHERE h.id = ? +"); +$stmt->execute([$id]); +$holiday = $stmt->fetch(PDO::FETCH_ASSOC); + +$stmtItems = $pdo->prepare("SELECT * FROM pf_holidays_items WHERE holiday_id = ? ORDER BY 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 LIEU (Pour la carte et l'affichage) +$steps = []; +$generalItems = []; + +foreach ($items as $it) { + if (!empty($it['location_name'])) { + $loc = $it['location_name']; + if (!isset($steps[$loc])) { + $steps[$loc] = [ + 'location_name' => $loc, + 'lat' => (float)$it['lat'], + 'lng' => (float)$it['lng'], + 'total_amount' => 0, + 'items' => [] + ]; + } + $steps[$loc]['items'][] = $it; + $steps[$loc]['total_amount'] += (float)$it['amount']; + } else { + $generalItems[] = $it; + } +} +$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'])); + if ($holiday['end_date']) $dateDisplay .= ' → ' . date('d/m/Y', strtotime($holiday['end_date'])); +} + +$cost = (float)$holiday['total_cost']; +$paid = (float)$holiday['total_paid']; +$saved = (float)$holiday['total_saved']; +$leftToPay = max(0, $cost - $paid); +$pctPaid = $cost > 0 ? min(100, ($paid / $cost) * 100) : 0; +$pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0; +?> + + + + + + + + ◀ Retour + = htmlspecialchars($holiday['title']) ?> + + = strtoupper($holiday['status']) ?> + + + $holiday, 'items' => $generalItems]), ENT_QUOTES, 'UTF-8') ?>)' class="pf-btn btn-secondary" style="width:auto;">⚙️ Modifier les bases + + + + + + Période + = $dateDisplay ?: 'À définir' ?> + + + Budget Food & Extras + 🍔 = number_format($holiday['budget_food'], 0) ?> € | 🎁 = number_format($holiday['budget_extra'], 0) ?> € + + + Coût Total Estimé + = number_format($cost, 0, ',', ' ') ?> € + + + + + + + + + ✓ Payé : = number_format($paid, 0, ',', ' ') ?> € + 💼 Provisionné : = number_format($saved, 0, ',', ' ') ?> € + ⏳ Reste à payer : = number_format($leftToPay, 0, ',', ' ') ?> € + + + + + + + 🗺️ Itinéraire & Checkpoints + 📍 Placer une étape + + + + + + + 📝 Détail des étapes + + + + + Aucune étape planifiée. + + + + + + 📍 = htmlspecialchars($step['location_name']) ?> + Total Étape : = number_format($step['total_amount'], 2) ?> € + + ✏️ + + + + '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' }; + ?> + + = $icon ?> = htmlspecialchars($it['name']) ?> + + = number_format($it['amount'], 2) ?> € + = $it['is_paid'] ? '✓' : '⏳' ?> + + + + + + Point de passage (Aucune dépense) + + + + + + + + + + + Notes du voyage : + = htmlspecialchars($holiday['notes']) ?> + + + + + + + + + + 📍 Placer une étape + × + + + + + + + + ⭐ = htmlspecialchars($fav['name']) ?> + + + + + + Rechercher un lieu géographique + + + 🔍 + + + + + + + + + + + + Nom de l'étape (Ce qui s'affichera sur la carte) + + + + + Dépenses prévues à cette étape + + Ajouter une dépense + + + + + + + + + ⭐ Sauvegarder cette adresse dans mes favoris rapides + + + + + + 🗑️ Supprimer l'étape + + + + Annuler + Enregistrer l'étape + + + + + + + + + \ No newline at end of file diff --git a/modules/holidays/views/list.php b/modules/holidays/views/list.php new file mode 100644 index 0000000..3071f4e --- /dev/null +++ b/modules/holidays/views/list.php @@ -0,0 +1,146 @@ +query($sql)->fetchAll(PDO::FETCH_ASSOC); + +// 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'])); +?> + + + + + Mes Vacances ✈️ + + + Créer un voyage + + + + + + + Aucun voyage en cours. Planifions quelque chose ! + + + + + + + + + + + Historique + + + + + + + + + + + + +prepare("SELECT * FROM pf_holidays_items WHERE holiday_id = ?"); + $stmt->execute([$h['id']]); + $items = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $json = htmlspecialchars(json_encode(['main' => $h, 'items' => $items]), ENT_QUOTES, 'UTF-8'); + $dateDisplay = htmlspecialchars($h['period_hint'] ?? ''); + + if (empty($dateDisplay) && $h['start_date']) { + $dateDisplay = date('d/m/Y', strtotime($h['start_date'])); + if ($h['end_date']) $dateDisplay .= ' → ' . date('d/m/Y', strtotime($h['end_date'])); + } + + $statusClass = match($h['status']) { + 'booked' => 'bg-green-100 text-green-800', + 'planned' => 'bg-blue-100 text-blue-800', + 'passed' => 'bg-gray-100 text-gray-600', + default => 'bg-yellow-50 text-yellow-800' + }; + + // --- Calcul des métriques financières --- + $cost = (float)$h['total_cost']; + $paid = (float)$h['total_paid']; + $saved = (float)$h['total_saved']; + + $leftToPay = max(0, $cost - $paid); + + // Calculs pour la barre de progression bicolore + $pctPaid = $cost > 0 ? min(100, ($paid / $cost) * 100) : 0; + $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0; + + echo " + + + + + + + ".htmlspecialchars($h['title'])." + + + + ".strtoupper($h['status'])." + + + + + ✏️ + 👁️ + + + + + + 🗓️ ".($dateDisplay ?: 'Dates à définir')." + + + + + + Budget Total + ".number_format($cost, 0, ',', ' ')." € + + + + + + + + + ✓ Payé : ".number_format($paid, 0, ',', ' ')." € + 💼 Financé : ".number_format($saved, 0, ',', ' ')." € + + ⏳ Reste à payer : ".number_format($leftToPay, 0, ',', ' ')." € + + + + + + "; +} +?> \ No newline at end of file diff --git a/modules/holidays/views/modal.php b/modules/holidays/views/modal.php new file mode 100644 index 0000000..660e987 --- /dev/null +++ b/modules/holidays/views/modal.php @@ -0,0 +1,96 @@ + + + Planifier le voyage + + + + + + + + Nom du voyage + + + + Statut + + Brouillon ✏️ + Planifié 📅 + Réservé ✅ + Passé 👋 + Archivé 🗄️ + + + + + + + Période (Texte libre) + + + + + Du + + + + Au + + + + + + + + + + + 🚗 Transport + + + + + + + + + 🏨 Hébergement + + + + + + + + + 🎫 Activité + + + + + + + + + + + + 🍔 Budget Food & Bev (€) + + + + 🎁 Budget Extras (€) + + + + + + Notes + + + + + + + \ No newline at end of file
Aucun voyage en cours. Planifions quelque chose !
Erreur.
Aucune étape planifiée.
= htmlspecialchars($holiday['notes']) ?>