This commit is contained in:
2026-01-26 20:21:05 +01:00
parent c5b89f2bf4
commit fbc95815d8
9 changed files with 1840 additions and 11 deletions
+105
View File
@@ -0,0 +1,105 @@
<?php
// modules/holidays/geocode.php
require __DIR__ . '/../../includes/auth.php';
require_login('/login.php');
require __DIR__ . '/../../includes/db.php';
header('Content-Type: application/json; charset=utf-8');
$q = trim($_GET['q'] ?? '');
if ($q === '') {
http_response_code(400);
echo json_encode(['error' => '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);
+604
View File
@@ -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));
}
}
+368
View File
@@ -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 lidé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 = `
<input type="hidden" name="action" value="delete_idea">
<input type="hidden" name="id" value="${id}">
`;
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(`
<strong>${esc(it.title || "")}</strong><br/>
${esc(loc)}<br/>
${dates ? "Dates: " + esc(dates) + "<br/>" : ""}
Statut: ${esc(it.status || "")}<br/>
<a href="/holidays.php?id=${it.id}">Ouvrir</a>
`);
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) =>
({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
})[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();
});
}
});
+506
View File
@@ -0,0 +1,506 @@
<?php
// modules/holidays/index.php
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;
/* 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 '<p>Idée introuvable.</p>';
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);
?>
<!-- Leaflet (carte) -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const HOL_MAP_DATA = <?= json_encode($mapIdeas, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
</script>
<div class="pf-holidays__titlebar">
<h1><?= htmlspecialchars($idea['title']) ?></h1>
<div class="hol-title-actions">
<a class="btn" href="/holidays.php">← Retour</a>
<button class="btn btn-edit" id="hol-edit-open" data-edit-id="<?= (int)$ideaId ?>">Éditer</button>
<button class="btn btn-delete" id="hol-delete" data-del-id="<?= (int)$ideaId ?>">Supprimer</button>
<button class="hol-map-toggle" id="hol-map-open">🌍 Carte</button>
</div>
</div>
<p class="hol-idea-meta">
<?= htmlspecialchars(trim(($idea['city'] ? $idea['city'] . ', ' : '') . ($idea['region'] ? $idea['region'] . ', ' : '') . ($idea['country'] ?? ''))) ?>
<?php if (!empty($idea['desired_start_date'])): ?>
• Dates: <?= htmlspecialchars($idea['desired_start_date']) ?><?= !empty($idea['desired_end_date']) ? ' → ' . htmlspecialchars($idea['desired_end_date']) : '' ?>
<?php elseif (!empty($idea['season_hint'])): ?>
• Saison: <?= htmlspecialchars($idea['season_hint']) ?>
<?php endif; ?>
<?php if (!empty($idea['ideal_days'])): ?>
• Durée idéale: <?= (int)$idea['ideal_days'] ?> j
<?php endif; ?>
• Statut: <strong><?= htmlspecialchars($idea['status']) ?></strong>
</p>
<div class="hol-grid">
<section class="pf-section pf-section--panel">
<h2>Transport</h2>
<form method="post" action="/modules/holidays/save.php" class="hol-inline-form">
<input type="hidden" name="action" value="add_transport">
<input type="hidden" name="idea_id" value="<?= (int)$ideaId ?>">
<select name="mode" required>
<option value="TRAIN">TRAIN</option><option value="PLANE">PLANE</option>
<option value="CAR">CAR</option><option value="BUS">BUS</option><option value="BOAT">BOAT</option>
</select>
<input type="number" name="duration_min" min="0" placeholder="Durée (min)">
<input type="number" step="0.01" name="cost" placeholder="Coût (€)">
<input type="number" step="0.01" name="co2_kg" placeholder="CO₂ (kg)">
<input type="url" name="link" placeholder="Lien">
<input type="text" name="notes" placeholder="Notes">
<button type="submit" class="btn">Ajouter</button>
</form>
<ul class="hol-list">
<?php foreach ($transport as $t): ?>
<li><?= htmlspecialchars($t['mode']) ?>
<?= $t['duration_min'] !== null ? ' • ' . (int)$t['duration_min'] . ' min' : '' ?>
<?= $t['cost'] !== null ? ' • ' . number_format((float)$t['cost'], 0, ',', ' ') . ' €' : '' ?>
<?php if (!empty($t['link'])): ?> • <a href="<?= htmlspecialchars($t['link']) ?>" target="_blank" rel="noopener">🔗</a><?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</section>
<section class="pf-section pf-section--panel">
<h2>Hébergement</h2>
<form method="post" action="/modules/holidays/save.php" class="hol-inline-form">
<input type="hidden" name="action" value="add_lodging">
<input type="hidden" name="idea_id" value="<?= (int)$ideaId ?>">
<select name="type" required>
<option value="HOTEL">HOTEL</option><option value="APT">APT</option>
<option value="HOUSE">HOUSE</option><option value="CAMPING">CAMPING</option><option value="OTHER">OTHER</option>
</select>
<input type="text" name="location_text" placeholder="Localisation">
<input type="number" step="0.01" name="price_per_n" placeholder="€ / nuit">
<input type="number" name="nights" placeholder="Nuits">
<label><input type="checkbox" name="free_cancel" value="1"> Annulation gratuite</label>
<label><input type="checkbox" name="family_friendly" value="1" checked> Family-friendly</label>
<input type="url" name="link" placeholder="Lien">
<input type="text" name="notes" placeholder="Notes">
<button type="submit" class="btn">Ajouter</button>
</form>
<ul class="hol-list">
<?php foreach ($lodging as $l): ?>
<li><?= 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' : '' ?>
<?php if (!empty($l['link'])): ?> • <a href="<?= htmlspecialchars($l['link']) ?>" target="_blank" rel="noopener">🔗</a><?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</section>
<section class="pf-section pf-section--panel">
<h2>Activités</h2>
<form method="post" action="/modules/holidays/save.php" class="hol-inline-form">
<input type="hidden" name="action" value="add_activity">
<input type="hidden" name="idea_id" value="<?= (int)$ideaId ?>">
<input type="text" name="name" placeholder="Nom" required>
<input type="text" name="kind" placeholder="Type (ex: PARK)">
<input type="number" step="0.01" name="cost_est" placeholder="€ estimé">
<label><input type="checkbox" name="need_booking" value="1"> Réservation</label>
<select name="weather">
<option value="ANY">ANY</option><option value="GOOD">GOOD</option><option value="RAIN">RAIN</option>
</select>
<input type="url" name="link" placeholder="Lien">
<input type="text" name="notes" placeholder="Notes">
<button type="submit" class="btn">Ajouter</button>
</form>
<ul class="hol-list">
<?php foreach ($acts as $a): ?>
<li><?= 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' : '' ?>
<?php if (!empty($a['link'])): ?> • <a href="<?= htmlspecialchars($a['link']) ?>" target="_blank" rel="noopener">🔗</a><?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</section>
<section class="pf-section pf-section--panel">
<h2>Budget</h2>
<form method="post" action="/modules/holidays/save.php" class="hol-inline-form">
<input type="hidden" name="action" value="add_budget">
<input type="hidden" name="idea_id" value="<?= (int)$ideaId ?>">
<select name="category" required>
<option>TRANSPORT</option><option>LODGING</option><option>FOOD</option>
<option>ACTIVITIES</option><option>LOCAL</option><option>INSURANCE</option><option>VISAS</option><option>OTHER</option>
</select>
<input type="text" name="label" placeholder="Label (facultatif)">
<input type="number" step="0.01" name="amount" placeholder="Montant (€)" required>
<label><input type="checkbox" name="per_person" value="1"> Par personne</label>
<button type="submit" class="btn">Ajouter</button>
</form>
<div class="hol-budget-summary">
<strong>Fixe:</strong> <?= number_format($fixedTotal, 0, ',', ' ') ?> €
• <strong>Par personne:</strong> <?= number_format($ppTotal, 0, ',', ' ') ?> €
</div>
<ul class="hol-list">
<?php foreach ($budget as $b): ?>
<li>[<?= htmlspecialchars($b['category']) ?>] <?= htmlspecialchars($b['label'] ?? '') ?> —
<?= number_format((float)$b['amount'], 0, ',', ' ') ?> €<?= $b['per_person'] ? ' /pers.' : '' ?>
</li>
<?php endforeach; ?>
</ul>
</section>
</div>
<!-- Modale édition idée (pré-remplie via JS) -->
<div class="hol-modal" id="hol-edit-modal" aria-hidden="true">
<div class="hol-backdrop"></div>
<div class="hol-dialog" role="dialog" aria-modal="true" aria-labelledby="hol-edit-title">
<form method="post" action="/modules/holidays/save.php" class="hol-form" id="hol-edit-form">
<h3 id="hol-edit-title">Éditer lidée</h3>
<input type="hidden" name="action" value="update_idea">
<input type="hidden" name="id" id="edit-id">
<label>Titre
<input type="text" name="title" id="edit-title" required>
</label>
<div class="hol-inline">
<label>Pays <input type="text" name="country" id="edit-country"></label>
<label>Région <input type="text" name="region" id="edit-region"></label>
<label>Ville <input type="text" name="city" id="edit-city"></label>
</div>
<div class="hol-inline">
<label>Lat <input type="number" step="0.000001" name="lat" id="edit-lat"></label>
<label>Lng <input type="number" step="0.000001" name="lng" id="edit-lng"></label>
<button type="button" class="btn hol-geocode-btn" data-scope="edit">Géocoder</button>
</div>
<div class="hol-inline">
<label>Début <input type="date" name="desired_start_date" id="edit-start"></label>
<label>Fin <input type="date" name="desired_end_date" id="edit-end"></label>
</div>
<div class="hol-inline">
<label>Saison <input type="text" name="season_hint" id="edit-season"></label>
<label>Durée idéale <input type="number" name="ideal_days" id="edit-days" min="1" step="1"></label>
</div>
<label>Statut
<select name="status" id="edit-status">
<option value="draft">draft</option>
<option value="shortlist">shortlist</option>
<option value="favorite">favorite</option>
<option value="planned">planned</option>
<option value="archived">archived</option>
</select>
</label>
<label>Notes <textarea name="notes" rows="4" id="edit-notes"></textarea></label>
<div class="hol-actions">
<button type="button" class="hol-cancel">Annuler</button>
<button type="submit" class="hol-ok">Enregistrer</button>
</div>
</form>
</div>
</div>
<!-- Modale carte -->
<div class="hol-modal" id="hol-map-modal" aria-hidden="true">
<div class="hol-backdrop"></div>
<div class="hol-dialog hol-dialog--map" role="dialog" aria-modal="true" aria-labelledby="hol-map-title">
<div class="hol-map-header">
<h3 id="hol-map-title">Carte des idées</h3>
<button class="hol-cancel">Fermer</button>
</div>
<div id="hol-map" style="width: 100%; height: calc(100vh - 140px);"></div>
</div>
</div>
<script src="/modules/holidays/holidays.js"></script>
<?php
return;
}
/* Vue liste + carte */
$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
");
?>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const HOL_MAP_DATA = <?= json_encode($mapIdeas, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
</script>
<div class="pf-holidays__titlebar">
<h1>Idées de vacances</h1>
<div class="hol-title-actions">
<button class="hol-add-btn" id="hol-add-open">+ Ajouter une idée</button>
<button class="hol-map-toggle" id="hol-map-open">🌍 Carte</button>
</div>
</div>
<section class="pf-section pf-section--panel">
<h2>Vacances planifiées</h2>
<p class="cl-legend">Dates souhaitées, prêtes à être réservées.</p>
<div class="hol-ideas-grid">
<?php foreach ($planned as $it): ?>
<div class="hol-idea-card" data-id="<?= (int)$it['id'] ?>">
<div class="hol-idea-card__head">
<h3><?= htmlspecialchars($it['title']) ?></h3>
<span class="hol-status hol-status--planned">planned</span>
</div>
<p class="hol-idea-meta">
<?= htmlspecialchars(trim(($it['city'] ? $it['city'] . ', ' : '') . ($it['region'] ? $it['region'] . ', ' : '') . ($it['country'] ?? ''))) ?>
<?php if (!empty($it['desired_start_date'])): ?>
• Dates: <?= htmlspecialchars($it['desired_start_date']) ?><?= !empty($it['desired_end_date']) ? ' → ' . htmlspecialchars($it['desired_end_date']) : '' ?>
<?php endif; ?>
<?php if (!empty($it['ideal_days'])): ?>
• Durée idéale: <?= (int)$it['ideal_days'] ?> j
<?php endif; ?>
</p>
<?php if (!empty($it['notes'])): ?>
<p class="hol-notes"><?= nl2br(htmlspecialchars(mb_strimwidth($it['notes'], 0, 160, '…'))) ?></p>
<?php endif; ?>
<div class="hol-card-actions">
<a class="btn" href="/holidays.php?id=<?= (int)$it['id'] ?>">Ouvrir</a>
<button class="btn btn-edit" data-edit-id="<?= (int)$it['id'] ?>">Éditer</button>
<button class="btn btn-delete" data-del-id="<?= (int)$it['id'] ?>">Supprimer</button>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
<section class="pf-section pf-section--panel">
<h2>Idées</h2>
<p class="cl-legend">Brouillons, favoris, shortlist.</p>
<div class="hol-ideas-grid">
<?php foreach ($ideas as $it): ?>
<div class="hol-idea-card" data-id="<?= (int)$it['id'] ?>">
<div class="hol-idea-card__head">
<h3><?= htmlspecialchars($it['title']) ?></h3>
<span class="hol-status hol-status--<?= htmlspecialchars($it['status']) ?>"><?= htmlspecialchars($it['status']) ?></span>
</div>
<p class="hol-idea-meta">
<?= htmlspecialchars(trim(($it['city'] ? $it['city'] . ', ' : '') . ($it['region'] ? $it['region'] . ', ' : '') . ($it['country'] ?? ''))) ?>
<?php if (!empty($it['desired_start_date'])): ?>
• Dates: <?= htmlspecialchars($it['desired_start_date']) ?><?= !empty($it['desired_end_date']) ? ' → ' . htmlspecialchars($it['desired_end_date']) : '' ?>
<?php elseif (!empty($it['season_hint'])): ?>
• Saison: <?= htmlspecialchars($it['season_hint']) ?>
<?php endif; ?>
<?php if (!empty($it['ideal_days'])): ?>
• Durée idéale: <?= (int)$it['ideal_days'] ?> j
<?php endif; ?>
</p>
<?php if (!empty($it['notes'])): ?>
<p class="hol-notes"><?= nl2br(htmlspecialchars(mb_strimwidth($it['notes'], 0, 160, '…'))) ?></p>
<?php endif; ?>
<div class="hol-card-actions">
<a class="btn" href="/holidays.php?id=<?= (int)$it['id'] ?>">Ouvrir</a>
<button class="btn btn-edit" data-edit-id="<?= (int)$it['id'] ?>">Éditer</button>
<button class="btn btn-delete" data-del-id="<?= (int)$it['id'] ?>">Supprimer</button>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
<section class="pf-section pf-section--panel">
<h2>Archivées</h2>
<div class="hol-ideas-grid hol-ideas-grid--archived">
<?php foreach ($archived as $it): ?>
<div class="hol-idea-card hol-idea-card--archived" data-id="<?= (int)$it['id'] ?>">
<h3><?= htmlspecialchars($it['title']) ?></h3>
<div class="hol-card-actions">
<button class="btn btn-edit" data-edit-id="<?= (int)$it['id'] ?>">Éditer</button>
<button class="btn btn-delete" data-del-id="<?= (int)$it['id'] ?>">Supprimer</button>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
<!-- Modale ajout idée -->
<div class="hol-modal" id="hol-add-modal" aria-hidden="true">
<div class="hol-backdrop"></div>
<div class="hol-dialog" role="dialog" aria-modal="true" aria-labelledby="hol-add-title">
<form method="post" action="/modules/holidays/save.php" class="hol-form">
<h3 id="hol-add-title">Ajouter une idée</h3>
<input type="hidden" name="action" value="create_idea">
<label>Titre <input type="text" name="title" required></label>
<div class="hol-inline">
<label>Pays <input type="text" name="country"></label>
<label>Région <input type="text" name="region"></label>
<label>Ville <input type="text" name="city" placeholder="Pour la carte"></label>
</div>
<div class="hol-inline">
<label>Lat <input type="number" step="0.000001" name="lat" placeholder="41.385064"></label>
<label>Lng <input type="number" step="0.000001" name="lng" placeholder="2.173404"></label>
<button type="button" class="btn hol-geocode-btn" data-scope="add">Géocoder</button>
</div>
<div class="hol-inline">
<label>Dates souhaitées (début) <input type="date" name="desired_start_date"></label>
<label>Dates souhaitées (fin) <input type="date" name="desired_end_date"></label>
</div>
<div class="hol-inline">
<label>Saison (facultatif) <input type="text" name="season_hint" placeholder="MaiJuin"></label>
<label>Durée idéale (jours) <input type="number" name="ideal_days" min="1" step="1"></label>
</div>
<label>Statut
<select name="status">
<option value="draft">draft</option>
<option value="shortlist">shortlist</option>
<option value="favorite">favorite</option>
<option value="planned">planned</option>
<option value="archived">archived</option>
</select>
</label>
<label>Notes <textarea name="notes" rows="4" placeholder="Activités phares, contraintes, liens..."></textarea></label>
<div class="hol-actions">
<button type="button" class="hol-cancel">Annuler</button>
<button type="submit" class="hol-ok">Créer</button>
</div>
</form>
</div>
</div>
<!-- Modale édition (commune à la liste) -->
<div class="hol-modal" id="hol-edit-modal" aria-hidden="true">
<div class="hol-backdrop"></div>
<div class="hol-dialog" role="dialog" aria-modal="true" aria-labelledby="hol-edit-title">
<form method="post" action="/modules/holidays/save.php" class="hol-form" id="hol-edit-form">
<h3 id="hol-edit-title">Éditer lidée</h3>
<input type="hidden" name="action" value="update_idea">
<input type="hidden" name="id" id="edit-id">
<label>Titre <input type="text" name="title" id="edit-title" required></label>
<div class="hol-inline">
<label>Pays <input type="text" name="country" id="edit-country"></label>
<label>Région <input type="text" name="region" id="edit-region"></label>
<label>Ville <input type="text" name="city" id="edit-city"></label>
</div>
<div class="hol-inline">
<label>Lat <input type="number" step="0.000001" name="lat" id="edit-lat"></label>
<label>Lng <input type="number" step="0.000001" name="lng" id="edit-lng"></label>
<button type="button" class="btn hol-geocode-btn" data-scope="edit">Géocoder</button>
</div>
<div class="hol-inline">
<label>Début <input type="date" name="desired_start_date" id="edit-start"></label>
<label>Fin <input type="date" name="desired_end_date" id="edit-end"></label>
</div>
<div class="hol-inline">
<label>Saison <input type="text" name="season_hint" id="edit-season"></label>
<label>Durée idéale <input type="number" name="ideal_days" id="edit-days" min="1" step="1"></label>
</div>
<label>Statut
<select name="status" id="edit-status">
<option value="draft">draft</option>
<option value="shortlist">shortlist</option>
<option value="favorite">favorite</option>
<option value="planned">planned</option>
<option value="archived">archived</option>
</select>
</label>
<label>Notes <textarea name="notes" rows="4" id="edit-notes"></textarea></label>
<div class="hol-actions">
<button type="button" class="hol-cancel">Annuler</button>
<button type="submit" class="hol-ok">Enregistrer</button>
</div>
</form>
</div>
</div>
<!-- Modale carte -->
<div class="hol-modal" id="hol-map-modal" aria-hidden="true">
<div class="hol-backdrop"></div>
<div class="hol-dialog hol-dialog--map" role="dialog" aria-modal="true" aria-labelledby="hol-map-title">
<div class="hol-map-header">
<h3 id="hol-map-title">Carte des idées</h3>
<button class="hol-cancel">Fermer</button>
</div>
<div id="hol-map" style="width: 100%; height: calc(100vh - 140px);"></div>
</div>
</div>
<script src="/modules/holidays/holidays.js"></script>
+194
View File
@@ -0,0 +1,194 @@
<?php
require __DIR__ . '/../../includes/auth.php';
require_login('/login.php');
require __DIR__ . '/../../includes/db.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo 'Method Not Allowed';
exit;
}
$action = $_POST['action'] ?? '';
function hol_back(string $fallback = '/holidays.php'): void {
$to = $_SERVER['HTTP_REFERER'] ?? $fallback;
header("Location: $to");
exit;
}
/**
* Normalise un décimal saisi (accepte virgule ou point), retourne float|NULL
*/
function hol_norm_decimal($v): ?float {
if (!isset($v)) return null;
$s = trim((string)$v);
if ($s === '') return null;
$s = str_replace(',', '.', $s);
return is_numeric($s) ? (float)$s : null;
}
/**
* Normalise une date (YYYY-MM-DD) : '' -> 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());
}
+25
View File
@@ -0,0 +1,25 @@
<?php
require __DIR__ . '/../../includes/auth.php';
require_login('/login.php');
require __DIR__ . '/../../includes/db.php';
header('Content-Type: application/json; charset=utf-8');
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) {
http_response_code(400);
echo json_encode(['error' => '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);