diff --git a/header.php b/header.php
index f9fdb7c..5d0a34b 100644
--- a/header.php
+++ b/header.php
@@ -36,7 +36,8 @@ if (!isset($activePage)) {
class="pf-nav-link = $activePage === 'home' ? 'pf-nav-link--active' : ''; ?>">Accueil
Family calendar
-
+ Holidays
Gift list
diff --git a/holidays.php b/holidays.php
new file mode 100644
index 0000000..dba16ac
--- /dev/null
+++ b/holidays.php
@@ -0,0 +1,16 @@
+Accéder au module
+
+
+ 🎁
+ Holidays
+
+ Plannification des vacances.
+
+ Accéder au module
+
+
+
+
+ 🎁
+ Gift list
+
+ Planifica els regals de cada nen, per al Tió, el Nadal i els Reis.
+
+ Accedir al mòdul
+
+
💰
@@ -49,16 +69,6 @@ require __DIR__ . '/header.php';
À venir
-
-
- 🎄
- gift list
-
- Planifica els regals de cada nen, per al Tió, el Nadal i els Reis.
-
- Accedir al mòdul
-
-
diff --git a/modules/holidays/geocode.php b/modules/holidays/geocode.php
new file mode 100644
index 0000000..c2843fb
--- /dev/null
+++ b/modules/holidays/geocode.php
@@ -0,0 +1,105 @@
+ 'missing q']);
+ exit;
+}
+
+// borne la limite entre 1 et 5 (usage perso)
+$limit = (int)($_GET['limit'] ?? 1);
+if ($limit < 1) $limit = 1;
+if ($limit > 5) $limit = 5;
+
+// Cache local (silencieux si table absente)
+$qNorm = mb_strtolower($q);
+$qHash = hash('sha256', $qNorm);
+try {
+ if ($limit === 1) {
+ $st = $pdo->prepare("SELECT lat, lng, display_name FROM pf_geocode_cache WHERE q_hash = ?");
+ $st->execute([$qHash]);
+ if ($row = $st->fetch(PDO::FETCH_ASSOC)) {
+ echo json_encode([
+ 'lat' => (float)$row['lat'],
+ 'lng' => (float)$row['lng'],
+ 'display_name' => $row['display_name'],
+ 'cached' => true
+ ]);
+ exit;
+ }
+ }
+} catch (Throwable $e) {
+ // pas bloquant
+}
+
+// Appel Nominatim (respect des règles d'usage)
+$endpoint = 'https://nominatim.openstreetmap.org/search';
+$params = http_build_query([
+ 'format' => 'jsonv2',
+ 'addressdetails' => 1,
+ 'limit' => $limit,
+ 'q' => $q,
+], '', '&', PHP_QUERY_RFC3986);
+
+$url = $endpoint . '?' . $params;
+
+$ch = curl_init($url);
+curl_setopt_array($ch, [
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_CONNECTTIMEOUT => 5,
+ CURLOPT_TIMEOUT => 10,
+ CURLOPT_HTTPHEADER => [
+ // Contact fourni: ferlan.alexandre@gmail.com
+ 'User-Agent: PachaFamily-Holidays/1.0 (+contact: ferlan.alexandre@gmail.com)'
+ ],
+]);
+$body = curl_exec($ch);
+$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+$err = curl_error($ch);
+curl_close($ch);
+
+if ($body === false || $http !== 200) {
+ http_response_code(502);
+ echo json_encode(['error' => 'geocode_failed', 'details' => $err ?: ('HTTP '.$http)]);
+ exit;
+}
+
+$data = json_decode($body, true);
+if (!is_array($data) || empty($data)) {
+ http_response_code(404);
+ echo json_encode(['error' => 'not_found']);
+ exit;
+}
+
+if ($limit === 1) {
+ $r = $data[0];
+ $lat = round((float)$r['lat'], 6);
+ $lng = round((float)$r['lon'], 6);
+ $display = $r['display_name'] ?? null;
+
+ try {
+ $st = $pdo->prepare("REPLACE INTO pf_geocode_cache (q_hash, q, lat, lng, display_name) VALUES (?, ?, ?, ?, ?)");
+ $st->execute([$qHash, $q, $lat, $lng, $display]);
+ } catch (Throwable $e) {}
+
+ echo json_encode(['lat' => $lat, 'lng' => $lng, 'display_name' => $display]);
+ exit;
+}
+
+// Multi-résultats
+$results = array_map(function ($r) {
+ return [
+ 'lat' => round((float)$r['lat'], 6),
+ 'lng' => round((float)$r['lon'], 6),
+ 'display_name' => (string)($r['display_name'] ?? ''),
+ ];
+}, $data);
+
+echo json_encode(['results' => $results], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
diff --git a/modules/holidays/holidays.css b/modules/holidays/holidays.css
new file mode 100644
index 0000000..fb533f9
--- /dev/null
+++ b/modules/holidays/holidays.css
@@ -0,0 +1,604 @@
+/* === Holidays === */
+
+/* Titres et paragraphes (alignés sur pf-gift-list) */
+.pf-holidays h1 {
+ margin-bottom: 4px;
+}
+.pf-holidays p {
+ margin-top: 0;
+ margin-bottom: 12px;
+ font-size: 13px;
+ color: #4b5563;
+}
+/* Petit texte explicatif (même classe que gift-list pour cohérence) */
+.cl-legend {
+ font-size: 12px;
+ color: #6b7280;
+ margin-bottom: 8px;
+}
+
+/* Titlebar (cohérent avec gift-list .cl-titlebar) */
+.pf-holidays__titlebar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+.hol-title-actions {
+ display: flex;
+ gap: 8px;
+}
+
+/* Boutons (proches de .cl-view-btn) */
+.btn,
+.hol-add-btn,
+.hol-map-toggle {
+ padding: 8px 12px;
+ border: 1px solid #d9e2ec;
+ border-radius: 8px;
+ background: #f0f4f8;
+ color: #243b53;
+ text-decoration: none;
+ cursor: pointer;
+ font-size: 13px;
+}
+.btn:hover,
+.hol-add-btn:hover,
+.hol-map-toggle:hover {
+ background: #e5eef5;
+}
+.btn-edit {
+ background: #e0f2fe;
+ border-color: #93c5fd;
+ color: #0f4c81;
+}
+.btn-delete {
+ background: #fee2e2;
+ border-color: #fca5a5;
+ color: #7c2d12;
+}
+
+/* Cards */
+.hol-idea-card {
+ background: #fff;
+ border: 1px solid #d9e2ec;
+ border-radius: 8px;
+ padding: 12px;
+}
+.hol-idea-card__head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+}
+.hol-card-actions {
+ display: flex;
+ gap: 6px;
+ margin-top: 8px;
+}
+
+/* Statuts (match gift-list badges ton léger) */
+.hol-status {
+ font-size: 11px;
+ padding: 3px 6px;
+ border-radius: 12px;
+ background: #e5e7eb;
+}
+.hol-status--favorite {
+ background: #fde68a;
+}
+.hol-status--shortlist {
+ background: #bfdbfe;
+}
+.hol-status--planned {
+ background: #bbf7d0;
+}
+
+/* === Modales (alignement visuel avec cl-modal de gift-list) === */
+.hol-modal {
+ display: none;
+}
+.hol-modal.open {
+ display: block;
+}
+
+.hol-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(17, 24, 39, 0.35); /* même teinte que gift-list */
+ z-index: 999;
+}
+
+.hol-dialog {
+ position: fixed;
+ z-index: 1000;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ width: min(
+ 560px,
+ calc(100% - 24px)
+ ); /* légèrement plus large que gift-list */
+ background: #fff;
+ border: 1px solid #d9e2ec;
+ border-radius: 8px;
+ box-shadow: 0 10px 30px rgba(17, 24, 39, 0.25); /* même shadow */
+}
+
+/* Modale carte (grande largeur) */
+.hol-dialog--map {
+ width: min(1200px, calc(100% - 24px));
+}
+
+.hol-map-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 12px;
+ border-bottom: 1px solid #e5e7eb;
+}
+
+/* Formulaire modale (alignement gift-list) */
+.hol-form {
+ display: grid;
+ gap: 10px;
+ padding: 14px;
+}
+.hol-form h3 {
+ margin: 0 0 4px;
+ font-size: 16px;
+ color: #243b53;
+}
+
+.hol-inline {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.hol-form label {
+ display: grid;
+ gap: 6px;
+ font-size: 12px;
+ color: #374151;
+}
+
+.hol-form input,
+.hol-form select,
+.hol-form textarea {
+ font-size: 12px;
+ padding: 6px 8px;
+ border: 1px solid #d9e2ec;
+ border-radius: 6px;
+ background: #ffffff;
+ color: #111827;
+}
+
+/* Actions modale */
+.hol-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ margin-top: 4px;
+}
+.hol-cancel,
+.hol-ok {
+ font-size: 12px;
+ padding: 6px 12px;
+ border: 1px solid #d9e2ec;
+ border-radius: 6px;
+ background: #f0f4f8;
+ cursor: pointer;
+}
+.hol-ok {
+ font-weight: 600;
+ color: #1f2933;
+}
+.hol-cancel {
+ color: #d03050;
+}
+.hol-cancel:hover,
+.hol-ok:hover {
+ background: #d9e2ec;
+}
+
+/* Focus accessible */
+.hol-form input:focus-visible,
+.hol-form select:focus-visible,
+.hol-form textarea:focus-visible,
+.hol-cancel:focus-visible,
+.hol-ok:focus-visible {
+ outline: 2px solid #2563eb;
+ outline-offset: 2px;
+}
+
+/* === Picker multi-résultats Géocode (harmonisé) === */
+.hol-geocode-picker {
+ margin-top: 8px;
+ background: #fff;
+ border: 1px solid #d9e2ec;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08);
+ overflow: hidden;
+}
+.hol-geocode-picker__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 10px;
+ background: #f8fafc;
+ border-bottom: 1px solid #e5e7eb;
+ font-size: 13px;
+ font-weight: 600;
+ color: #243b53;
+}
+.hol-geocode-picker__close {
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ font-size: 18px;
+ line-height: 1;
+ padding: 2px 6px;
+}
+.hol-geocode-picker__list {
+ list-style: none;
+ margin: 0;
+ padding: 6px;
+ max-height: 260px;
+ overflow: auto;
+}
+.hol-geocode-picker__item {
+ display: grid;
+ grid-template-columns: 1fr auto auto;
+ align-items: center;
+ gap: 8px;
+ padding: 6px;
+ border-radius: 6px;
+}
+.hol-geocode-picker__item + .hol-geocode-picker__item {
+ margin-top: 4px;
+}
+.hol-geocode-picker__label {
+ font-size: 12px;
+ color: #111827;
+}
+.hol-geocode-picker__coords {
+ font-size: 11px;
+ color: #6b7280;
+}
+.hol-geocode-picker__pick {
+ padding: 6px 10px;
+ border: 1px solid #d9e2ec;
+ border-radius: 6px;
+ background: #f0f4f8;
+ cursor: pointer;
+ font-size: 12px;
+}
+
+/* === Mobile Comfort (aligné gift-list) === */
+@media (pointer: coarse), (max-width: 780px) {
+ .pf-holidays h1 {
+ font-size: clamp(24px, 7vw, 30px);
+ margin: 10px 0 6px;
+ line-height: 1.2;
+ }
+ .pf-holidays h2 {
+ font-size: clamp(20px, 5.6vw, 24px);
+ line-height: 1.25;
+ margin: 10px 0 6px;
+ }
+ .pf-holidays h3 {
+ font-size: clamp(17px, 4.8vw, 20px);
+ line-height: 1.3;
+ margin: 8px 0 4px;
+ }
+
+ .btn,
+ .hol-add-btn,
+ .hol-map-toggle {
+ font-size: 18px;
+ padding: 12px 14px;
+ min-height: 48px;
+ border-radius: 10px;
+ }
+
+ .hol-form input,
+ .hol-form select,
+ .hol-form textarea,
+ .hol-cancel,
+ .hol-ok {
+ font-size: 16px; /* évite zoom iOS */
+ min-height: 44px;
+ }
+
+ .hol-dialog {
+ width: min(640px, calc(100% - 24px));
+ }
+}
+
+/* Cards: style proche gift-list */
+.hol-ideas-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(260px, 1fr));
+ gap: 10px;
+}
+@media (max-width: 1100px) {
+ .hol-ideas-grid {
+ grid-template-columns: repeat(2, minmax(240px, 1fr));
+ }
+}
+@media (max-width: 700px) {
+ .hol-ideas-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.hol-idea-card {
+ background: #fff;
+ border: 1px solid #d9e2ec;
+ border-radius: 8px;
+ padding: 12px;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
+ transition:
+ box-shadow 120ms ease,
+ transform 120ms ease;
+}
+.hol-idea-card:hover {
+ box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08);
+ transform: translateY(-1px);
+}
+.hol-idea-card__head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+}
+.hol-idea-meta,
+.hol-tags,
+.hol-notes {
+ font-size: 12px;
+ color: #374151;
+ margin: 6px 0;
+}
+.hol-card-actions {
+ display: flex;
+ gap: 6px;
+ margin-top: 8px;
+}
+.hol-idea-card--archived {
+ opacity: 0.85;
+}
+
+/* Boutons cohérents */
+.btn,
+.hol-add-btn,
+.hol-map-toggle {
+ padding: 8px 12px;
+ border: 1px solid #d9e2ec;
+ border-radius: 8px;
+ background: #f0f4f8;
+ color: #243b53;
+ text-decoration: none;
+ cursor: pointer;
+ font-size: 13px;
+}
+.btn:hover,
+.hol-add-btn:hover,
+.hol-map-toggle:hover {
+ background: #e5eef5;
+}
+.btn-edit {
+ background: #e0f2fe;
+ border-color: #93c5fd;
+ color: #0f4c81;
+}
+.btn-delete {
+ background: #fee2e2;
+ border-color: #fca5a5;
+ color: #7c2d12;
+}
+
+/* Modale: harmonisée avec gift-list */
+.hol-modal {
+ display: none;
+}
+.hol-modal.open {
+ display: block;
+}
+.hol-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(17, 24, 39, 0.35);
+ z-index: 999;
+}
+.hol-dialog {
+ position: fixed;
+ z-index: 1000;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ width: min(560px, calc(100% - 24px));
+ background: #fff;
+ border: 1px solid #d9e2ec;
+ border-radius: 8px;
+ box-shadow: 0 10px 30px rgba(17, 24, 39, 0.25);
+}
+.hol-dialog--map {
+ width: min(1200px, calc(100% - 24px));
+}
+.hol-map-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 12px;
+ border-bottom: 1px solid #e5e7eb;
+}
+
+.hol-form {
+ display: grid;
+ gap: 10px;
+ padding: 14px;
+}
+.hol-form h3 {
+ margin: 0 0 4px;
+ font-size: 16px;
+ color: #243b53;
+}
+.hol-inline {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+.hol-form label {
+ display: grid;
+ gap: 6px;
+ font-size: 12px;
+ color: #374151;
+}
+.hol-form input,
+.hol-form select,
+.hol-form textarea {
+ font-size: 12px;
+ padding: 6px 8px;
+ border: 1px solid #d9e2ec;
+ border-radius: 6px;
+ background: #fff;
+ color: #111827;
+}
+.hol-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ margin-top: 4px;
+}
+.hol-cancel,
+.hol-ok {
+ font-size: 12px;
+ padding: 6px 12px;
+ border: 1px solid #d9e2ec;
+ border-radius: 6px;
+ background: #f0f4f8;
+ cursor: pointer;
+}
+.hol-ok {
+ font-weight: 600;
+ color: #1f2933;
+}
+.hol-cancel {
+ color: #d03050;
+}
+.hol-cancel:hover,
+.hol-ok:hover {
+ background: #d9e2ec;
+}
+.hol-form input:focus-visible,
+.hol-form select:focus-visible,
+.hol-form textarea:focus-visible,
+.hol-cancel:focus-visible,
+.hol-ok:focus-visible {
+ outline: 2px solid #2563eb;
+ outline-offset: 2px;
+}
+
+/* Geocode picker harmonisé */
+.hol-geocode-picker {
+ margin-top: 8px;
+ background: #fff;
+ border: 1px solid #d9e2ec;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08);
+ overflow: hidden;
+}
+.hol-geocode-picker__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 10px;
+ background: #f8fafc;
+ border-bottom: 1px solid #e5e7eb;
+ font-size: 13px;
+ font-weight: 600;
+ color: #243b53;
+}
+.hol-geocode-picker__close {
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ font-size: 18px;
+ line-height: 1;
+ padding: 2px 6px;
+}
+.hol-geocode-picker__list {
+ list-style: none;
+ margin: 0;
+ padding: 6px;
+ max-height: 260px;
+ overflow: auto;
+}
+.hol-geocode-picker__item {
+ display: grid;
+ grid-template-columns: 1fr auto auto;
+ align-items: center;
+ gap: 8px;
+ padding: 6px;
+ border-radius: 6px;
+}
+.hol-geocode-picker__item + .hol-geocode-picker__item {
+ margin-top: 4px;
+}
+.hol-geocode-picker__label {
+ font-size: 12px;
+ color: #111827;
+}
+.hol-geocode-picker__coords {
+ font-size: 11px;
+ color: #6b7280;
+}
+.hol-geocode-picker__pick {
+ padding: 6px 10px;
+ border: 1px solid #d9e2ec;
+ border-radius: 6px;
+ background: #f0f4f8;
+ cursor: pointer;
+ font-size: 12px;
+}
+
+/* Mobile comfort (match gift-list) */
+@media (pointer: coarse), (max-width: 780px) {
+ .pf-holidays h1 {
+ font-size: clamp(24px, 7vw, 30px);
+ margin: 10px 0 6px;
+ line-height: 1.2;
+ }
+ .pf-holidays h2 {
+ font-size: clamp(20px, 5.6vw, 24px);
+ line-height: 1.25;
+ margin: 10px 0 6px;
+ }
+ .pf-holidays h3 {
+ font-size: clamp(17px, 4.8vw, 20px);
+ line-height: 1.3;
+ margin: 8px 0 4px;
+ }
+
+ .btn,
+ .hol-add-btn,
+ .hol-map-toggle {
+ font-size: 18px;
+ padding: 12px 14px;
+ min-height: 48px;
+ border-radius: 10px;
+ }
+ .hol-form input,
+ .hol-form select,
+ .hol-form textarea,
+ .hol-cancel,
+ .hol-ok {
+ font-size: 16px;
+ min-height: 44px;
+ }
+ .hol-dialog {
+ width: min(640px, calc(100% - 24px));
+ }
+}
diff --git a/modules/holidays/holidays.js b/modules/holidays/holidays.js
new file mode 100644
index 0000000..8147a2b
--- /dev/null
+++ b/modules/holidays/holidays.js
@@ -0,0 +1,368 @@
+// modules/holidays/holidays.js
+
+document.addEventListener("DOMContentLoaded", () => {
+ // --- Modale "Ajouter une idée"
+ const addBtn = document.getElementById("hol-add-open");
+ const addModal = document.getElementById("hol-add-modal");
+ if (addBtn && addModal) {
+ const backdrop = addModal.querySelector(".hol-backdrop");
+ const cancel = addModal.querySelector(".hol-cancel");
+ const open = () => addModal.classList.add("open");
+ const close = () => addModal.classList.remove("open");
+ addBtn.addEventListener("click", open);
+ backdrop.addEventListener("click", close);
+ cancel.addEventListener("click", close);
+ document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") close();
+ });
+ }
+
+ // --- Helpers modale "Éditer"
+ const editModal = document.getElementById("hol-edit-modal");
+ const openEditModal = () => {
+ if (editModal) editModal.classList.add("open");
+ };
+ const closeEditModal = () => {
+ if (editModal) editModal.classList.remove("open");
+ };
+
+ async function openEditForId(id) {
+ try {
+ const res = await fetch(
+ `/modules/holidays/view.php?id=${encodeURIComponent(id)}`,
+ {
+ headers: { Accept: "application/json" },
+ },
+ );
+ if (!res.ok) throw new Error("HTTP " + res.status);
+ const it = await res.json();
+
+ if (!editModal) {
+ alert("Modale édition introuvable");
+ return;
+ }
+ // Scope les sélecteurs à la modale pour éviter les null
+ const $ = (sel) => editModal.querySelector(sel);
+ const setVal = (sel, val) => {
+ const el = $(sel);
+ if (el) el.value = val ?? "";
+ };
+
+ 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 || "");
+
+ openEditModal();
+ } catch {
+ alert("Impossible de charger l’idée.");
+ }
+ }
+
+ // --- Modale "Éditer" (liste + page détail)
+ if (editModal) {
+ const backdrop = editModal.querySelector(".hol-backdrop");
+ const cancel = editModal.querySelector(".hol-cancel");
+ if (backdrop) backdrop.addEventListener("click", closeEditModal);
+ if (cancel) cancel.addEventListener("click", closeEditModal);
+ document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") closeEditModal();
+ });
+
+ // Boutons "Éditer" des cards (liste/planifiées/archivées)
+ document.querySelectorAll(".btn-edit[data-edit-id]").forEach((btn) => {
+ btn.addEventListener("click", () => {
+ const id = btn.getAttribute("data-edit-id");
+ if (id) openEditForId(id);
+ });
+ });
+
+ // Bouton "Éditer" sur la page détail (en-tête)
+ const headerEditBtn = document.getElementById("hol-edit-open");
+ if (headerEditBtn) {
+ headerEditBtn.addEventListener("click", () => {
+ const id = headerEditBtn.getAttribute("data-edit-id");
+ if (id) openEditForId(id);
+ });
+ }
+ }
+
+ // --- Suppression (liste + planifiées + page détail)
+ document.querySelectorAll(".btn-delete[data-del-id]").forEach((btn) => {
+ btn.addEventListener("click", () => {
+ const id = btn.getAttribute("data-del-id");
+ if (!id) return;
+ if (confirm("Supprimer cette idée ?")) {
+ const form = document.createElement("form");
+ form.method = "post";
+ form.action = "/modules/holidays/save.php";
+ form.innerHTML = `
+
+
+ `;
+ document.body.appendChild(form);
+ form.submit();
+ }
+ });
+ });
+
+ // --- Géocodage via Nominatim + UI multi-résultats
+ document.querySelectorAll(".hol-geocode-btn").forEach((btn) => {
+ btn.addEventListener("click", async () => {
+ const scope = btn.getAttribute("data-scope"); // 'add' ou 'edit'
+ let form, city, region, country, latInput, lngInput;
+
+ if (scope === "edit") {
+ form = btn.closest("form") || document;
+ city = (document.getElementById("edit-city")?.value || "").trim();
+ region = (document.getElementById("edit-region")?.value || "").trim();
+ country = (document.getElementById("edit-country")?.value || "").trim();
+ latInput = document.getElementById("edit-lat");
+ lngInput = document.getElementById("edit-lng");
+ } else {
+ form = btn.closest("form");
+ city = (form?.querySelector('input[name="city"]')?.value || "").trim();
+ region = (
+ form?.querySelector('input[name="region"]')?.value || ""
+ ).trim();
+ country = (
+ form?.querySelector('input[name="country"]')?.value || ""
+ ).trim();
+ latInput = form?.querySelector('input[name="lat"]');
+ lngInput = form?.querySelector('input[name="lng"]');
+ }
+
+ const q = [city, region, country].filter(Boolean).join(", ");
+ if (!q) {
+ alert("Renseigne au moins Ville/Pays.");
+ return;
+ }
+
+ removeNearbyPicker(btn);
+ btn.disabled = true;
+ const original = btn.textContent;
+ btn.textContent = "Recherche...";
+
+ 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 géocodage");
+
+ if ("lat" in data && "lng" in data) {
+ if (latInput) latInput.value = data.lat;
+ if (lngInput) lngInput.value = data.lng;
+ return;
+ }
+
+ 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.");
+ }
+ } catch {
+ alert("Impossible de trouver les coordonnées pour: " + q);
+ } finally {
+ btn.disabled = false;
+ btn.textContent = original;
+ }
+ });
+ });
+
+ function renderGeocodePicker(anchorBtn, results, onPick) {
+ removeNearbyPicker(anchorBtn);
+
+ const wrapper = document.createElement("div");
+ wrapper.className = "hol-geocode-picker";
+
+ const header = document.createElement("div");
+ header.className = "hol-geocode-picker__header";
+ header.textContent = "Plusieurs résultats trouvés";
+ const closeBtn = document.createElement("button");
+ closeBtn.type = "button";
+ closeBtn.className = "hol-geocode-picker__close";
+ closeBtn.textContent = "×";
+ closeBtn.addEventListener("click", () => removeNearbyPicker(anchorBtn));
+ header.appendChild(closeBtn);
+
+ 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";
+
+ const label = document.createElement("div");
+ label.className = "hol-geocode-picker__label";
+ label.textContent = r.display_name || `${r.lat}, ${r.lng}`;
+
+ const coords = document.createElement("div");
+ coords.className = "hol-geocode-picker__coords";
+ coords.textContent = `(${r.lat}, ${r.lng})`;
+
+ const pickBtn = document.createElement("button");
+ pickBtn.type = "button";
+ pickBtn.className = "hol-geocode-picker__pick";
+ pickBtn.textContent = "Choisir";
+ pickBtn.addEventListener("click", () => onPick(r));
+
+ li.appendChild(label);
+ li.appendChild(coords);
+ li.appendChild(pickBtn);
+ list.appendChild(li);
+ });
+
+ wrapper.appendChild(header);
+ wrapper.appendChild(list);
+
+ 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();
+ }
+ }
+
+ // --- Carte (Leaflet)
+ const mapBtn = document.getElementById("hol-map-open");
+ const mapModal = document.getElementById("hol-map-modal");
+ if (mapBtn && mapModal) {
+ const backdrop = mapModal.querySelector(".hol-backdrop");
+ const cancel = mapModal.querySelector(".hol-cancel");
+ const open = () => mapModal.classList.add("open");
+ const close = () => mapModal.classList.remove("open");
+
+ let mapInitialized = false;
+ let map;
+
+ function initMap() {
+ // Évite double initialisation
+ if (mapInitialized) return;
+ mapInitialized = true;
+
+ // Données carte (safe fallback)
+ const MAP_DATA = Array.isArray(window.HOL_MAP_DATA)
+ ? window.HOL_MAP_DATA
+ : [];
+ console.log("HOL_MAP_DATA (safe):", MAP_DATA);
+
+ // Leaflet dispo ?
+ if (typeof L === "undefined") {
+ console.error("Leaflet non chargé");
+ return;
+ }
+
+ // Init carte une seule fois (ne pas faire ça dans la boucle)
+ map = L.map("hol-map", { scrollWheelZoom: true });
+ L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
+ attribution: "© OpenStreetMap",
+ }).addTo(map);
+
+ // Corrige la taille après affichage de la modale
+ setTimeout(() => map.invalidateSize(), 0);
+
+ // Ajout des marqueurs
+ const markers = [];
+ MAP_DATA.forEach((it) => {
+ const lat = parseFloat(it.lat);
+ const lng = parseFloat(it.lng);
+ if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
+
+ const color =
+ it.status === "planned"
+ ? "#16a34a"
+ : it.status === "favorite"
+ ? "#f59e0b"
+ : it.status === "shortlist"
+ ? "#3b82f6"
+ : "#6b7280";
+
+ const m = L.circleMarker([lat, lng], {
+ radius: 6,
+ color,
+ fillColor: color,
+ fillOpacity: 0.85,
+ }).addTo(map);
+
+ 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 : ""}`
+ : "";
+
+ m.bindPopup(`
+ ${esc(it.title || "")}
+ ${esc(loc)}
+ ${dates ? "Dates: " + esc(dates) + "
" : ""}
+ Statut: ${esc(it.status || "")}
+ Ouvrir
+ `);
+
+ markers.push(m);
+ });
+
+ // Vue par défaut selon nombre de points
+ if (markers.length === 1) {
+ map.setView(markers[0].getLatLng(), 7);
+ } else if (markers.length > 1) {
+ const group = L.featureGroup(markers);
+ map.fitBounds(group.getBounds(), { padding: [20, 20] });
+ } else {
+ console.warn(
+ "HOL_MAP_DATA vide ou coordonnées non valides.",
+ window.HOL_MAP_DATA,
+ );
+ map.setView([20, 0], 2);
+ }
+
+ // Re-valider la taille après rendu complet
+ setTimeout(() => map.invalidateSize(), 100);
+ }
+
+ function esc(s) {
+ return String(s).replace(
+ /[&<>"']/g,
+ (c) =>
+ ({
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ '"': """,
+ "'": "'",
+ })[c],
+ );
+ }
+
+ mapBtn.addEventListener("click", () => {
+ open();
+ setTimeout(initMap, 0);
+ });
+ if (backdrop) backdrop.addEventListener("click", close);
+ if (cancel) cancel.addEventListener("click", close);
+ document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") close();
+ });
+ }
+});
diff --git a/modules/holidays/index.php b/modules/holidays/index.php
new file mode 100644
index 0000000..582ad38
--- /dev/null
+++ b/modules/holidays/index.php
@@ -0,0 +1,506 @@
+prepare($sql);
+ $st->execute($params);
+ return $st->fetchAll(PDO::FETCH_ASSOC);
+ }
+}
+
+$ideaId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
+
+/* Préparer les données pour la carte AVANT le rendu (liste ou détail) */
+if ($ideaId > 0) {
+ // Vue détail: renvoyer l'idée courante (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]);
+} else {
+ // Vue liste: idées non archivées avec coordonnées
+ $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
+ ");
+}
+
+if ($ideaId > 0) {
+ // Vue détail d'une idée
+ $rows = hol_q($pdo, "SELECT * FROM pf_holidays_ideas WHERE id = ?", [$ideaId]);
+ $idea = $rows[0] ?? null;
+ if (!$idea) {
+ echo 'Idée introuvable.
';
+ return;
+ }
+ $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]);
+ $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);
+ ?>
+
+
+
+
+
+
+
= htmlspecialchars($idea['title']) ?>
+
+
← Retour
+
+
+
+
+
+
+
+ = htmlspecialchars(trim(($idea['city'] ? $idea['city'] . ', ' : '') . ($idea['region'] ? $idea['region'] . ', ' : '') . ($idea['country'] ?? ''))) ?>
+
+ • Dates: = htmlspecialchars($idea['desired_start_date']) ?>= !empty($idea['desired_end_date']) ? ' → ' . htmlspecialchars($idea['desired_end_date']) : '' ?>
+
+ • Saison: = htmlspecialchars($idea['season_hint']) ?>
+
+
+ • Durée idéale: = (int)$idea['ideal_days'] ?> j
+
+ • Statut: = htmlspecialchars($idea['status']) ?>
+
+
+
+
+ Transport
+
+
+
+ - = htmlspecialchars($t['mode']) ?>
+ = $t['duration_min'] !== null ? ' • ' . (int)$t['duration_min'] . ' min' : '' ?>
+ = $t['cost'] !== null ? ' • ' . number_format((float)$t['cost'], 0, ',', ' ') . ' €' : '' ?>
+ • 🔗
+
+
+
+
+
+
+ Hébergement
+
+
+
+ - = htmlspecialchars($l['type']) ?>
+ = !empty($l['location_text']) ? ' • ' . htmlspecialchars($l['location_text']) : '' ?>
+ = $l['price_per_n'] !== null ? ' • ' . number_format((float)$l['price_per_n'], 0, ',', ' ') . ' €/nuit' : '' ?>
+ = $l['nights'] !== null ? ' × ' . (int)$l['nights'] . 'n' : '' ?>
+ • 🔗
+
+
+
+
+
+
+ Activités
+
+
+
+ - = htmlspecialchars($a['name']) ?>= !empty($a['kind']) ? ' (' . htmlspecialchars($a['kind']) . ')' : '' ?>
+ = $a['cost_est'] !== null ? ' • ' . number_format((float)$a['cost_est'], 0, ',', ' ') . ' €' : '' ?>
+ = !empty($a['need_booking']) ? ' • Réservation requise' : '' ?>
+ • 🔗
+
+
+
+
+
+
+ Budget
+
+
+
+ Fixe: = number_format($fixedTotal, 0, ',', ' ') ?> €
+ • Par personne: = number_format($ppTotal, 0, ',', ' ') ?> €
+
+
+
+
+ - [= htmlspecialchars($b['category']) ?>] = htmlspecialchars($b['label'] ?? '') ?> —
+ = number_format((float)$b['amount'], 0, ',', ' ') ?> €= $b['per_person'] ? ' /pers.' : '' ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Idées de vacances
+
+
+
+
+
+
+
+ Vacances planifiées
+ Dates souhaitées, prêtes à être réservées.
+
+
+
+
+
= htmlspecialchars($it['title']) ?>
+ planned
+
+
+ = htmlspecialchars(trim(($it['city'] ? $it['city'] . ', ' : '') . ($it['region'] ? $it['region'] . ', ' : '') . ($it['country'] ?? ''))) ?>
+
+ • Dates: = htmlspecialchars($it['desired_start_date']) ?>= !empty($it['desired_end_date']) ? ' → ' . htmlspecialchars($it['desired_end_date']) : '' ?>
+
+
+ • Durée idéale: = (int)$it['ideal_days'] ?> j
+
+
+
+
= nl2br(htmlspecialchars(mb_strimwidth($it['notes'], 0, 160, '…'))) ?>
+
+
+
Ouvrir
+
+
+
+
+
+
+
+
+
+ Idées
+ Brouillons, favoris, shortlist.
+
+
+
+
+
= htmlspecialchars($it['title']) ?>
+ = htmlspecialchars($it['status']) ?>
+
+
+ = htmlspecialchars(trim(($it['city'] ? $it['city'] . ', ' : '') . ($it['region'] ? $it['region'] . ', ' : '') . ($it['country'] ?? ''))) ?>
+
+ • Dates: = htmlspecialchars($it['desired_start_date']) ?>= !empty($it['desired_end_date']) ? ' → ' . htmlspecialchars($it['desired_end_date']) : '' ?>
+
+ • Saison: = htmlspecialchars($it['season_hint']) ?>
+
+
+ • Durée idéale: = (int)$it['ideal_days'] ?> j
+
+
+
+
= nl2br(htmlspecialchars(mb_strimwidth($it['notes'], 0, 160, '…'))) ?>
+
+
+
Ouvrir
+
+
+
+
+
+
+
+
+
+ Archivées
+
+
+
+
= htmlspecialchars($it['title']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/holidays/save.php b/modules/holidays/save.php
new file mode 100644
index 0000000..e79461f
--- /dev/null
+++ b/modules/holidays/save.php
@@ -0,0 +1,194 @@
+ NULL, sinon retourne la chaîne telle quelle
+ */
+function hol_norm_date($v): ?string {
+ if (!isset($v)) return null;
+ $s = trim((string)$v);
+ return $s === '' ? null : $s;
+}
+
+try {
+ switch ($action) {
+ case 'create_idea': {
+ $status = $_POST['status'] ?? 'draft';
+ $start = hol_norm_date($_POST['desired_start_date'] ?? null);
+ $end = hol_norm_date($_POST['desired_end_date'] ?? null);
+ if (!empty($start) && $status === 'draft') { $status = 'planned'; }
+
+ $latVal = hol_norm_decimal($_POST['lat'] ?? null);
+ $lngVal = hol_norm_decimal($_POST['lng'] ?? null);
+
+ $stmt = $pdo->prepare("
+ INSERT INTO pf_holidays_ideas
+ (title, country, region, city, lat, lng, desired_start_date, desired_end_date, season_hint, ideal_days, status, notes)
+ VALUES
+ (:title,:country,:region,:city,:lat,:lng,:start,:end,:season,:days,:status,:notes)
+ ");
+ $stmt->execute([
+ ':title' => trim($_POST['title'] ?? ''),
+ ':country'=> trim($_POST['country'] ?? ''),
+ ':region' => trim($_POST['region'] ?? ''),
+ ':city' => trim($_POST['city'] ?? ''),
+ ':lat' => $latVal,
+ ':lng' => $lngVal,
+ ':start' => $start,
+ ':end' => $end,
+ ':season' => trim($_POST['season_hint'] ?? ''),
+ ':days' => ($_POST['ideal_days'] !== '' ? (int)$_POST['ideal_days'] : null),
+ ':status' => $status,
+ ':notes' => trim($_POST['notes'] ?? ''),
+ ]);
+ $newId = (int)$pdo->lastInsertId();
+ header("Location: /holidays.php?id={$newId}");
+ exit;
+ }
+
+ case 'update_idea': {
+ $status = $_POST['status'] ?? 'draft';
+ $start = hol_norm_date($_POST['desired_start_date'] ?? null);
+ $end = hol_norm_date($_POST['desired_end_date'] ?? null);
+ if (!empty($start) && $status === 'draft') { $status = 'planned'; }
+
+ $latVal = hol_norm_decimal($_POST['lat'] ?? null);
+ $lngVal = hol_norm_decimal($_POST['lng'] ?? null);
+
+ $stmt = $pdo->prepare("
+ UPDATE pf_holidays_ideas
+ SET title=:title, country=:country, region=:region, city=:city, lat=:lat, lng=:lng,
+ desired_start_date=:start, desired_end_date=:end,
+ season_hint=:season, ideal_days=:days, status=:status, notes=:notes
+ WHERE id=:id
+ ");
+ $stmt->execute([
+ ':id' => (int)$_POST['id'],
+ ':title' => trim($_POST['title'] ?? ''),
+ ':country'=> trim($_POST['country'] ?? ''),
+ ':region' => trim($_POST['region'] ?? ''),
+ ':city' => trim($_POST['city'] ?? ''),
+ ':lat' => $latVal,
+ ':lng' => $lngVal,
+ ':start' => $start,
+ ':end' => $end,
+ ':season' => trim($_POST['season_hint'] ?? ''),
+ ':days' => ($_POST['ideal_days'] !== '' ? (int)$_POST['ideal_days'] : null),
+ ':status' => $status,
+ ':notes' => trim($_POST['notes'] ?? ''),
+ ]);
+ hol_back();
+ }
+
+ case 'delete_idea': {
+ $stmt = $pdo->prepare("DELETE FROM pf_holidays_ideas WHERE id = :id");
+ $stmt->execute([':id' => (int)$_POST['id']]);
+ header("Location: /holidays.php");
+ exit;
+ }
+
+ case 'add_transport': {
+ $stmt = $pdo->prepare("
+ INSERT INTO pf_holidays_transport (idea_id, mode, duration_min, cost, co2_kg, link, notes)
+ VALUES (:id,:mode,:dur,:cost,:co2,:link,:notes)
+ ");
+ $stmt->execute([
+ ':id' => (int)$_POST['idea_id'],
+ ':mode' => strtoupper($_POST['mode'] ?? 'OTHER'),
+ ':dur' => ($_POST['duration_min'] !== '' ? (int)$_POST['duration_min'] : null),
+ ':cost' => hol_norm_decimal($_POST['cost'] ?? null),
+ ':co2' => hol_norm_decimal($_POST['co2_kg'] ?? null),
+ ':link' => trim($_POST['link'] ?? ''),
+ ':notes'=> trim($_POST['notes'] ?? ''),
+ ]);
+ hol_back();
+ }
+
+ case 'add_lodging': {
+ $stmt = $pdo->prepare("
+ INSERT INTO pf_holidays_lodging (idea_id, type, location_text, price_per_n, nights, free_cancel, family_friendly, link, notes)
+ VALUES (:id,:type,:loc,:ppn,:n,:fc,:ff,:link,:notes)
+ ");
+ $stmt->execute([
+ ':id' => (int)$_POST['idea_id'],
+ ':type' => strtoupper($_POST['type'] ?? 'OTHER'),
+ ':loc' => trim($_POST['location_text'] ?? ''),
+ ':ppn' => hol_norm_decimal($_POST['price_per_n'] ?? null),
+ ':n' => ($_POST['nights'] !== '' ? (int)$_POST['nights'] : null),
+ ':fc' => isset($_POST['free_cancel']) ? 1 : 0,
+ ':ff' => isset($_POST['family_friendly']) ? 1 : 0,
+ ':link' => trim($_POST['link'] ?? ''),
+ ':notes'=> trim($_POST['notes'] ?? ''),
+ ]);
+ hol_back();
+ }
+
+ case 'add_activity': {
+ $stmt = $pdo->prepare("
+ INSERT INTO pf_holidays_activities (idea_id, name, kind, cost_est, need_booking, weather, link, notes)
+ VALUES (:id,:name,:kind,:cost,:need,:weather,:link,:notes)
+ ");
+ $stmt->execute([
+ ':id' => (int)$_POST['idea_id'],
+ ':name' => trim($_POST['name'] ?? ''),
+ ':kind' => trim($_POST['kind'] ?? ''),
+ ':cost' => hol_norm_decimal($_POST['cost_est'] ?? null),
+ ':need' => isset($_POST['need_booking']) ? 1 : 0,
+ ':weather'=> strtoupper($_POST['weather'] ?? 'ANY'),
+ ':link' => trim($_POST['link'] ?? ''),
+ ':notes' => trim($_POST['notes'] ?? ''),
+ ]);
+ hol_back();
+ }
+
+ case 'add_budget': {
+ $stmt = $pdo->prepare("
+ INSERT INTO pf_holidays_budget_items (idea_id, category, label, amount, per_person)
+ VALUES (:id,:cat,:label,:amt,:pp)
+ ");
+ $stmt->execute([
+ ':id' => (int)$_POST['idea_id'],
+ ':cat' => strtoupper($_POST['category'] ?? 'OTHER'),
+ ':label' => trim($_POST['label'] ?? ''),
+ ':amt' => hol_norm_decimal($_POST['amount'] ?? null) ?? 0.0,
+ ':pp' => isset($_POST['per_person']) ? 1 : 0,
+ ]);
+ hol_back();
+ }
+
+ default:
+ http_response_code(400);
+ echo 'Unknown action';
+ exit;
+ }
+} catch (Throwable $e) {
+ http_response_code(500);
+ echo "Error: " . htmlspecialchars($e->getMessage());
+}
diff --git a/modules/holidays/view.php b/modules/holidays/view.php
new file mode 100644
index 0000000..5b38031
--- /dev/null
+++ b/modules/holidays/view.php
@@ -0,0 +1,25 @@
+ 'bad id']);
+ exit;
+}
+
+$st = $pdo->prepare("SELECT * FROM pf_holidays_ideas WHERE id = ?");
+$st->execute([$id]);
+$it = $st->fetch(PDO::FETCH_ASSOC);
+
+if (!$it) {
+ http_response_code(404);
+ echo json_encode(['error' => 'not found']);
+ exit;
+}
+
+echo json_encode($it, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);