revamp gemini

This commit is contained in:
2026-01-30 23:32:25 +01:00
parent fbc95815d8
commit e248eee882
15 changed files with 3776 additions and 7931 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -31,3 +31,11 @@
[2026-01-08T09:23:59+01:00] RAW INPUT: [{"date":"2025-12-22","type":"CENTRE","duration":1}]
[2026-01-08T09:24:00+01:00] RAW INPUT: [{"date":"2025-12-23","type":"CENTRE","duration":1}]
[2026-01-08T09:24:02+01:00] RAW INPUT: [{"date":"2025-12-24","type":"CENTRE","duration":1}]
[2026-01-30T22:49:07+01:00] RAW INPUT: [{"date":"2025-10-24","type":"AVIS","duration":1,"person":null}]
[2026-01-30T22:49:50+01:00] RAW INPUT: [{"date":"2025-10-16","type":"OFF_CAROLE","duration":1,"person":null}]
[2026-01-30T22:50:00+01:00] RAW INPUT: [{"date":"2025-09-11","type":"OFF_CAROLE","duration":1,"person":null}]
[2026-01-30T22:51:16+01:00] RAW INPUT: [{"date":"2025-09-05","type":"AVIS","duration":1,"person":null}]
[2026-01-30T22:53:23+01:00] RAW INPUT: [{"date":"2025-09-03","type":"AVIS","duration":1,"person":null}]
[2026-01-30T23:16:56+01:00] RAW INPUT: [{"date":"2025-09-28","type":"OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-09-29","type":"OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-09-30","type":"OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-01","type":"OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-02","type":"OFF_CAROLE","duration":1,"person":"Carole"}]
[2026-01-30T23:17:02+01:00] RAW INPUT: [{"date":"2025-10-05","type":"EXTRA_OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-06","type":"EXTRA_OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-07","type":"EXTRA_OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-08","type":"EXTRA_OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-09","type":"EXTRA_OFF_CAROLE","duration":1,"person":"Carole"}]
[2026-01-30T23:17:09+01:00] RAW INPUT: [{"date":"2025-10-05","type":"AVIS","duration":1,"person":"Carole"},{"date":"2025-10-06","type":"AVIS","duration":1,"person":"Carole"},{"date":"2025-10-07","type":"AVIS","duration":1,"person":"Carole"},{"date":"2025-10-08","type":"AVIS","duration":1,"person":"Carole"}]
@@ -1,103 +1,94 @@
<?php
// modules/family-calendar/includes/api/manage-event.php
header('Content-Type: application/json');
require __DIR__ . '/../../../../includes/db.php';
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? '';
if (!isset($input['action'])) {
if (!$action) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Action manquante.']);
exit;
}
$action = $input['action'];
try {
// --- SUPPRESSION UNITAIRE ---
if ($action === 'delete') {
// Suppression d'un seul événement
if (!isset($input['event_id'])) {
$eventId = (int)($input['event_id'] ?? 0);
if ($eventId <= 0) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'ID manquant pour la suppression.']);
echo json_encode(['status' => 'error', 'message' => 'ID manquant.']);
exit;
}
$eventId = (int)$input['event_id'];
$stmt = $pdo->prepare("DELETE FROM pf_events WHERE id = ?");
$stmt->execute([$eventId]);
echo json_encode(['status' => 'success']);
exit;
}
echo json_encode(['status' => 'success', 'message' => 'Événement supprimé.']);
} elseif ($action === 'update') {
// Mise à jour d'un seul événement
if (!isset($input['event_id'], $input['new_type'])) {
// --- MISE À JOUR UNITAIRE ---
if ($action === 'update') {
$eventId = (int)($input['event_id'] ?? 0);
$newType = $input['new_type'] ?? '';
if ($eventId <= 0 || !$newType) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'ID ou nouveau type manquant pour la mise à jour.']);
echo json_encode(['status' => 'error', 'message' => 'Données manquantes.']);
exit;
}
$eventId = (int)$input['event_id'];
$newType = $input['new_type'];
$stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id = ?");
$stmt->execute([$newType, $eventId]);
echo json_encode(['status' => 'success', 'message' => 'Événement mis à jour.']);
} elseif ($action === 'bulk_delete') {
// Suppression en masse
if (empty($input['event_ids']) || !is_array($input['event_ids'])) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Liste event_ids manquante pour bulk_delete.']);
exit;
}
$eventIds = array_map('intval', $input['event_ids']);
$eventIds = array_filter($eventIds, fn($id) => $id > 0);
if (empty($eventIds)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Aucun ID valide pour bulk_delete.']);
exit;
}
$placeholders = implode(',', array_fill(0, count($eventIds), '?'));
$stmt = $pdo->prepare("DELETE FROM pf_events WHERE id IN ($placeholders)");
$stmt->execute($eventIds);
echo json_encode(['status' => 'success', 'message' => 'Événements supprimés en masse.']);
} elseif ($action === 'bulk_update') {
// Mise à jour en masse
if (empty($input['event_ids']) || !is_array($input['event_ids']) || !isset($input['new_type'])) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'event_ids ou new_type manquant pour bulk_update.']);
exit;
}
$eventIds = array_map('intval', $input['event_ids']);
$eventIds = array_filter($eventIds, fn($id) => $id > 0);
$newType = $input['new_type'];
if (empty($eventIds)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Aucun ID valide pour bulk_update.']);
exit;
}
$placeholders = implode(',', array_fill(0, count($eventIds), '?'));
$params = array_merge([$newType], $eventIds);
$stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id IN ($placeholders)");
$stmt->execute($params);
echo json_encode(['status' => 'success', 'message' => 'Événements mis à jour en masse.']);
} else {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Action non valide.']);
echo json_encode(['status' => 'success']);
exit;
}
// --- SUPPRESSION DE MASSE (Par date et type) ---
// Utilisé quand on ajoute un événement pour nettoyer les doublons potentiels (ex: Off vs Extra)
if ($action === 'bulk_delete_day_types') {
$dates = $input['dates'] ?? [];
$types = $input['types'] ?? [];
if (empty($dates) || empty($types)) {
echo json_encode(['status' => 'success']); // Rien à faire
exit;
}
// Création des placeholders IN (?,?,?)
$datePlaceholders = implode(',', array_fill(0, count($dates), '?'));
$typePlaceholders = implode(',', array_fill(0, count($types), '?'));
$sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders) AND event_type IN ($typePlaceholders)";
$stmt = $pdo->prepare($sql);
// Fusion des tableaux pour l'exécution
$stmt->execute(array_merge($dates, $types));
echo json_encode(['status' => 'success']);
exit;
}
// --- SUPPRESSION TOTALE SUR DES DATES ---
if ($action === 'bulk_delete_all') {
$dates = $input['dates'] ?? [];
if (empty($dates)) {
echo json_encode(['status' => 'success']);
exit;
}
$datePlaceholders = implode(',', array_fill(0, count($dates), '?'));
$sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders)";
$stmt = $pdo->prepare($sql);
$stmt->execute($dates);
echo json_encode(['status' => 'success']);
exit;
}
// Action inconnue
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Action non reconnue : ' . $action]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
}
File diff suppressed because it is too large Load Diff
+76 -51
View File
@@ -1,73 +1,95 @@
<?php
require __DIR__ . '/../../includes/auth.php';
require_login('/gift-list.php');
// modules/gift-list/save-gift.php
require __DIR__ . '/../../includes/auth.php';
require_login('/login.php');
require __DIR__ . '/../../includes/db.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = trim($_POST['action'] ?? 'create');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /gift-list.php');
exit;
}
// Configuration
$tableName = 'pf_gifts'; // Harmonisé avec gift-list.php
// Récupération des données
$action = $_POST['action'] ?? 'create';
$gift_id = (int)($_POST['gift_id'] ?? 0);
// Logique de redirection (pour rester sur la bonne vue)
// Par défaut on renvoie vers le referer ou vers la page principale
$redirectUrl = '/gift-list.php';
$occasionForView = $_POST['occasion'] ?? '';
if (in_array($occasionForView, ['ANNIV', 'SANT'])) {
$redirectUrl .= '?view=anniversary';
} else {
$redirectUrl .= '?view=nadal';
}
try {
// --- SUPPRESSION ---
if ($action === 'delete') {
$gift_id = (int)($_POST['gift_id'] ?? 0);
if ($gift_id > 0) {
$stmt = $pdo->prepare("DELETE FROM pf_gift_gifts WHERE id = :id");
$stmt = $pdo->prepare("DELETE FROM {$tableName} WHERE id = :id");
$stmt->execute(['id' => $gift_id]);
}
header('Location: /gift-list.php');
header("Location: $redirectUrl");
exit;
}
// Champs communs
$year = (int)($_POST['year'] ?? date('Y'));
$adult_name = trim($_POST['adult_name'] ?? '');
$payer_name = trim($_POST['payer_name'] ?? ''); // nouveau champ
$child_name = trim($_POST['child_name'] ?? '');
$occasion = trim($_POST['occasion'] ?? '');
$gift_desc = trim($_POST['gift_description'] ?? '');
$product_link = trim($_POST['product_link'] ?? '');
$amount = $_POST['amount'] !== '' ? (float)$_POST['amount'] : 0.0;
$year = (int)($_POST['year'] ?? date('Y'));
$adult_name = trim($_POST['adult_name'] ?? '');
$payer_name = trim($_POST['payer_name'] ?? '');
$child_name = trim($_POST['child_name'] ?? '');
$occasion = trim($_POST['occasion'] ?? '');
$gift_desc = trim($_POST['gift_description'] ?? '');
$prod_link = trim($_POST['product_link'] ?? '');
$amount = ($_POST['amount'] !== '') ? (float)$_POST['amount'] : 0.0;
// Si payeur vide, c'est l'adulte responsable qui paye
if ($payer_name === '') {
$payer_name = $adult_name;
}
if ($action === 'update') {
$gift_id = (int)($_POST['gift_id'] ?? 0);
if ($gift_id > 0 && $adult_name && $payer_name && $child_name && $occasion && $gift_desc) {
$stmt = $pdo->prepare("
UPDATE pf_gift_gifts
SET year = :year,
adult_name = :adult_name,
payer_name = :payer_name,
child_name = :child_name,
occasion = :occasion,
gift_description = :gift_description,
product_link = :product_link,
amount = :amount
WHERE id = :id
");
$stmt->execute([
'id' => $gift_id,
'year' => $year,
'adult_name' => $adult_name,
'payer_name' => $payer_name,
'child_name' => $child_name,
'occasion' => $occasion,
'gift_description' => $gift_desc,
'product_link' => $product_link ?: null,
'amount' => $amount,
]);
}
header('Location: /gift-list.php');
// Validation minimale
if (!$adult_name || !$child_name || !$occasion || !$gift_desc) {
// En cas d'erreur, on redirige sans rien faire (ou on pourrait gérer une erreur)
header("Location: $redirectUrl");
exit;
}
// create (par défaut)
if ($adult_name && $payer_name && $child_name && $occasion && $gift_desc) {
// --- UPDATE ---
if ($action === 'update' && $gift_id > 0) {
$stmt = $pdo->prepare("
INSERT INTO pf_gift_gifts
UPDATE {$tableName}
SET year = :year,
adult_name = :adult_name,
payer_name = :payer_name,
child_name = :child_name,
occasion = :occasion,
gift_description = :gift_description,
product_link = :product_link,
amount = :amount
WHERE id = :id
");
$stmt->execute([
'id' => $gift_id,
'year' => $year,
'adult_name' => $adult_name,
'payer_name' => $payer_name,
'child_name' => $child_name,
'occasion' => $occasion,
'gift_description' => $gift_desc,
'product_link' => $prod_link ?: null,
'amount' => $amount,
]);
}
// --- CREATE ---
else {
$stmt = $pdo->prepare("
INSERT INTO {$tableName}
(year, adult_name, payer_name, child_name, occasion, gift_description, product_link, amount)
VALUES
(:year, :adult_name, :payer_name, :child_name, :occasion, :gift_description, :product_link, :amount)
@@ -79,11 +101,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'child_name' => $child_name,
'occasion' => $occasion,
'gift_description' => $gift_desc,
'product_link' => $product_link ?: null,
'product_link' => $prod_link ?: null,
'amount' => $amount,
]);
}
header('Location: /gift-list.php');
exit;
} catch (PDOException $e) {
// Log l'erreur si besoin
}
header("Location: $redirectUrl");
exit;
+99 -57
View File
@@ -6,100 +6,142 @@ require __DIR__ . '/../../includes/db.php';
header('Content-Type: application/json; charset=utf-8');
// 1. Validation de l'entrée
$q = trim($_GET['q'] ?? '');
if ($q === '') {
http_response_code(400);
echo json_encode(['error' => 'missing q']);
exit;
http_response_code(400);
echo json_encode(['error' => 'missing_q']);
exit;
}
// borne la limite entre 1 et 5 (usage perso)
// Borner la limite
$limit = (int)($_GET['limit'] ?? 1);
if ($limit < 1) $limit = 1;
if ($limit > 5) $limit = 5;
if ($limit > 10) $limit = 10; // Nominatim bloque souvent au-dessus de 10-50
// Cache local (silencieux si table absente)
// 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é)
try {
$pdo->exec("
CREATE TABLE IF NOT EXISTS pf_geocode_cache (
q_hash CHAR(64) PRIMARY KEY,
q VARCHAR(255),
lat DECIMAL(10, 7),
lng DECIMAL(10, 7),
display_name TEXT,
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)
}
// 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.
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;
echo json_encode([
'lat' => (float)$row['lat'],
'lng' => (float)$row['lng'],
'display_name' => $row['display_name'],
'cached' => true
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
}
} catch (Throwable $e) {
// pas bloquant
// Erreur SQL silencieuse sur le cache
}
// Appel Nominatim (respect des règles d'usage)
// 5. Appel Nominatim (Si pas en cache)
$endpoint = 'https://nominatim.openstreetmap.org/search';
$params = http_build_query([
'format' => 'jsonv2',
'addressdetails' => 1,
'limit' => $limit,
'q' => $q,
'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)'
],
CURLOPT_RETURNTRANSFER => true,
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)'
],
]);
$body = curl_exec($ch);
$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);
echo json_encode(['error' => 'geocode_failed', 'details' => $err ?: ('HTTP '.$http)]);
exit;
http_response_code(502); // Bad Gateway
echo json_encode([
'error' => 'geocode_failed',
'details' => $err ?: ('HTTP ' . $http)
]);
exit;
}
$data = json_decode($body, true);
// Gestion erreur JSON ou vide
if (!is_array($data) || empty($data)) {
http_response_code(404);
echo json_encode(['error' => 'not_found']);
exit;
// 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.
if (isset($data[0])) {
$r = $data[0];
$lat = round((float)$r['lat'], 6);
$lng = round((float)$r['lon'], 6);
$display = $r['display_name'] ?? '';
try {
$st = $pdo->prepare("
INSERT INTO pf_geocode_cache (q_hash, q, lat, lng, display_name, updated_at)
VALUES (?, ?, ?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE lat=VALUES(lat), lng=VALUES(lng), display_name=VALUES(display_name), updated_at=NOW()
");
$st->execute([$qHash, $q, $lat, $lng, $display]);
} 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
if ($limit === 1) {
$r = $data[0];
$lat = round((float)$r['lat'], 6);
$lng = round((float)$r['lon'], 6);
$display = $r['display_name'] ?? null;
$r = $data[0];
echo json_encode([
'lat' => round((float)$r['lat'], 6),
'lng' => round((float)$r['lon'], 6),
'display_name' => $r['display_name'] ?? ''
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} else {
$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);
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);
echo json_encode(['results' => $results], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
File diff suppressed because it is too large Load Diff
+240 -248
View File
@@ -1,107 +1,128 @@
// 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);
// --- 1. FONCTIONS UTILITAIRES ---
/**
* Configure les comportements de fermeture d'une modale (Backdrop, Cancel, Escape)
*/
function setupModal(modalId, openAction = null) {
const modal = document.getElementById(modalId);
if (!modal) return null;
const backdrop = modal.querySelector(".hol-backdrop");
const cancelBtn = modal.querySelector(".hol-cancel");
const close = () => modal.classList.remove("open");
const open = () => {
modal.classList.add("open");
if (openAction) openAction();
};
if (backdrop) backdrop.addEventListener("click", close);
if (cancelBtn) cancelBtn.addEventListener("click", close);
// Fermeture avec ECHAP (uniquement si cette modale est ouverte)
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") close();
if (e.key === "Escape" && modal.classList.contains("open")) {
close();
}
});
return { modal, open, 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");
};
/**
* Échappe les caractères HTML pour éviter les failles XSS simples
*/
function esc(s) {
if (s === null || s === undefined) return "";
return String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// --- 2. GESTION MODALE "AJOUTER" ---
const addModalCtrl = setupModal("hol-add-modal");
const addBtn = document.getElementById("hol-add-open");
if (addBtn && addModalCtrl) {
addBtn.addEventListener("click", addModalCtrl.open);
}
// --- 3. GESTION MODALE "ÉDITER" ---
const editModalCtrl = setupModal("hol-edit-modal");
// Fonction pour charger et ouvrir l'édition via AJAX
async function openEditForId(id) {
if (!editModalCtrl) return;
try {
const res = await fetch(
`/modules/holidays/view.php?id=${encodeURIComponent(id)}`,
{
headers: { Accept: "application/json" },
},
{ headers: { Accept: "application/json" } },
);
if (!res.ok) throw new Error("HTTP " + res.status);
const it = await res.json();
if (!res.ok) throw new Error("Erreur HTTP " + res.status);
if (!editModal) {
alert("Modale édition introuvable");
return;
}
// Scope les sélecteurs à la modale pour éviter les null
const $ = (sel) => editModal.querySelector(sel);
const it = await res.json();
const modal = editModalCtrl.modal;
// Helper pour remplir les champs
const setVal = (sel, val) => {
const el = $(sel);
const el = modal.querySelector(sel);
if (el) el.value = val ?? "";
};
// Remplissage du formulaire
setVal("#edit-id", it.id);
setVal("#edit-title", it.title || "");
setVal("#edit-country", it.country || "");
setVal("#edit-region", it.region || "");
setVal("#edit-city", it.city || "");
setVal("#edit-lat", it.lat ?? "");
setVal("#edit-lng", it.lng ?? "");
setVal("#edit-start", it.desired_start_date ?? "");
setVal("#edit-end", it.desired_end_date ?? "");
setVal("#edit-season", it.season_hint || "");
setVal("#edit-days", it.ideal_days ?? "");
setVal("#edit-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 || "");
setVal("#edit-notes", it.notes);
openEditModal();
} catch {
alert("Impossible de charger lidée.");
editModalCtrl.open();
} catch (err) {
console.error(err);
alert("Impossible de charger les données de 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);
});
// Écouteurs sur les boutons "Éditer" (Liste + Détail)
document.body.addEventListener("click", (e) => {
// Utilisation de la délégation d'événement pour gérer tous les boutons (même dynamiques)
const btn = e.target.closest(".btn-edit");
if (btn && btn.hasAttribute("data-edit-id")) {
const id = btn.getAttribute("data-edit-id");
openEditForId(id);
}
}
// Cas spécifique du bouton dans le header de la vue détail
else if (e.target.id === "hol-edit-open") {
const id = e.target.getAttribute("data-edit-id");
openEditForId(id);
}
});
// --- Suppression (liste + planifiées + page détail)
document.querySelectorAll(".btn-delete[data-del-id]").forEach((btn) => {
btn.addEventListener("click", () => {
// --- 4. GESTION SUPPRESSION ---
document.body.addEventListener("click", (e) => {
const btn = e.target.closest(".btn-delete");
if (btn && btn.hasAttribute("data-del-id")) {
const id = btn.getAttribute("data-del-id");
if (!id) return;
if (confirm("Supprimer cette idée ?")) {
if (
confirm(
"Voulez-vous vraiment supprimer cette idée ?\nCette action est irréversible.",
)
) {
// Création d'un formulaire temporaire pour le POST
const form = document.createElement("form");
form.method = "post";
form.action = "/modules/holidays/save.php";
@@ -112,45 +133,51 @@ document.addEventListener("DOMContentLoaded", () => {
document.body.appendChild(form);
form.submit();
}
});
}
});
// --- Géocodage via Nominatim + UI multi-résultats
// --- 5. GÉOCODAGE (Nominatim) ---
document.querySelectorAll(".hol-geocode-btn").forEach((btn) => {
btn.addEventListener("click", async () => {
const scope = btn.getAttribute("data-scope"); // 'add' ou 'edit'
let form, city, region, country, latInput, lngInput;
let latInput,
lngInput,
qParts = [];
// Récupération des inputs selon le scope
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();
const getVal = (id) =>
(document.getElementById(id)?.value || "").trim();
qParts = [
getVal("edit-city"),
getVal("edit-region"),
getVal("edit-country"),
];
latInput = document.getElementById("edit-lat");
lngInput = document.getElementById("edit-lng");
} else {
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();
// Scope 'add' : on cherche dans le formulaire parent
const form = btn.closest("form");
const getVal = (name) =>
(form?.querySelector(`input[name="${name}"]`)?.value || "").trim();
qParts = [getVal("city"), getVal("region"), getVal("country")];
latInput = form?.querySelector('input[name="lat"]');
lngInput = form?.querySelector('input[name="lng"]');
}
const q = [city, region, country].filter(Boolean).join(", ");
const q = qParts.filter(Boolean).join(", ");
if (!q) {
alert("Renseigne au moins Ville/Pays.");
alert(
"Veuillez renseigner au moins une Ville ou un Pays pour géocoder.",
);
return;
}
// UI : chargement
removeNearbyPicker(btn);
btn.disabled = true;
const original = btn.textContent;
btn.textContent = "Recherche...";
const originalText = btn.textContent;
btn.textContent = "...";
try {
const res = await fetch(
@@ -160,28 +187,30 @@ document.addEventListener("DOMContentLoaded", () => {
},
);
const data = await res.json();
if (!res.ok) throw new Error(data?.error || "Erreur géocodage");
if ("lat" in data && "lng" in data) {
if (!res.ok) throw new Error(data?.error || "Erreur inconnue");
// Cas 1: Résultat direct (lat/lng uniques)
if (data.lat && data.lng) {
if (latInput) latInput.value = data.lat;
if (lngInput) lngInput.value = data.lng;
return;
}
if (Array.isArray(data.results) && data.results.length > 0) {
// Cas 2: Liste de choix
else if (Array.isArray(data.results) && data.results.length > 0) {
renderGeocodePicker(btn, data.results, (choice) => {
if (latInput) latInput.value = choice.lat;
if (lngInput) lngInput.value = choice.lng;
removeNearbyPicker(btn);
});
} else {
alert("Aucun résultat.");
alert("Aucun résultat trouvé pour : " + q);
}
} catch {
alert("Impossible de trouver les coordonnées pour: " + q);
} catch (err) {
console.error(err);
alert("Erreur lors du géocodage.");
} finally {
btn.disabled = false;
btn.textContent = original;
btn.textContent = originalText;
}
});
});
@@ -192,46 +221,36 @@ document.addEventListener("DOMContentLoaded", () => {
const wrapper = document.createElement("div");
wrapper.className = "hol-geocode-picker";
// Header
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);
header.innerHTML = `<span>Choix multiples</span><button type="button" class="hol-close">×</button>`;
header
.querySelector(".hol-close")
.addEventListener("click", () => removeNearbyPicker(anchorBtn));
wrapper.appendChild(header);
// Liste
const list = document.createElement("ul");
list.className = "hol-geocode-picker__list";
results.forEach((r) => {
const li = document.createElement("li");
li.className = "hol-geocode-picker__item";
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);
li.innerHTML = `
<div class="hol-info">
<span class="hol-label">${esc(r.display_name)}</span>
<span class="hol-coords">(${r.lat}, ${r.lng})</span>
</div>
<button type="button">Choisir</button>
`;
li.querySelector("button").addEventListener("click", () => onPick(r));
list.appendChild(li);
});
wrapper.appendChild(header);
wrapper.appendChild(list);
// Insertion après le conteneur du bouton
const container =
anchorBtn.closest(".hol-inline") || anchorBtn.parentElement;
container.insertAdjacentElement("afterend", wrapper);
@@ -246,123 +265,96 @@ document.addEventListener("DOMContentLoaded", () => {
}
}
// --- Carte (Leaflet)
// --- 6. CARTE LEAFLET ---
let mapInitialized = false;
let mapInstance;
// Fonction d'initialisation de la carte (appelée à l'ouverture de la modale)
function initMap() {
if (mapInitialized) {
// Si déjà init, on force juste le redimensionnement pour éviter les bugs d'affichage
setTimeout(() => mapInstance.invalidateSize(), 200);
return;
}
if (typeof L === "undefined") {
console.error("Leaflet n'est pas chargé.");
return;
}
// Récupération sécurisée des données injectées par PHP
const MAP_DATA = Array.isArray(window.HOL_MAP_DATA)
? window.HOL_MAP_DATA
: [];
mapInstance = L.map("hol-map", { scrollWheelZoom: true });
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap",
}).addTo(mapInstance);
const markers = [];
MAP_DATA.forEach((it) => {
const lat = parseFloat(it.lat);
const lng = parseFloat(it.lng);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
// Code couleur selon statut
const colors = {
planned: "#16a34a",
favorite: "#f59e0b",
shortlist: "#3b82f6",
default: "#6b7280",
};
const color = colors[it.status] || colors.default;
const m = L.circleMarker([lat, lng], {
radius: 7,
color: "#ffffff",
weight: 1,
fillColor: color,
fillOpacity: 0.9,
}).addTo(mapInstance);
// Construction du popup
const loc = [it.city, it.region, it.country].filter(Boolean).join(", ");
const dates = it.desired_start_date
? `${it.desired_start_date}${it.desired_end_date ? " → " + it.desired_end_date : ""}`
: null;
m.bindPopup(`
<div class="hol-map-popup">
<strong>${esc(it.title)}</strong>
<div style="font-size:0.9em; color:#666;">${esc(loc)}</div>
${dates ? `<div style="font-size:0.85em; margin-top:4px;">📅 ${esc(dates)}</div>` : ""}
<div style="margin-top:8px;">
<span class="hol-status hol-status--${esc(it.status)}">${esc(it.status)}</span>
<a href="/holidays.php?id=${it.id}" style="margin-left:8px;">Voir</a>
</div>
</div>
`);
markers.push(m);
});
// Centrage de la carte
if (markers.length > 0) {
const group = L.featureGroup(markers);
mapInstance.fitBounds(group.getBounds(), { padding: [50, 50] });
} else {
mapInstance.setView([46.603354, 1.888334], 5); // France par défaut si vide
}
mapInitialized = true;
// Hack indispensable pour que Leaflet calcule la bonne taille dans une modale
setTimeout(() => mapInstance.invalidateSize(), 200);
}
// Connexion de la carte à la modale
const mapBtn = document.getElementById("hol-map-open");
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();
});
if (mapBtn) {
const mapModalCtrl = setupModal("hol-map-modal", initMap); // On passe initMap en callback d'ouverture
mapBtn.addEventListener("click", mapModalCtrl.open);
}
});
+414 -461
View File
@@ -1,506 +1,459 @@
<?php
// modules/holidays/index.php
// ------------------------------------------------------------------
// 1. LOGIQUE PHP : RÉCUPÉRATION DES DONNÉES
// ------------------------------------------------------------------
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);
}
function hol_q(PDO $pdo, string $sql, array $params = []): array {
$st = $pdo->prepare($sql);
$st->execute($params);
return $st->fetchAll(PDO::FETCH_ASSOC);
}
}
$ideaId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
$isDetailView = ($ideaId > 0);
$currentIdea = null; // Stockera les données si on est en vue détail
// Préparation des variables par défaut pour éviter les erreurs "undefined variable"
$transport = []; $lodging = []; $acts = []; $budget = [];
$fixedTotal = 0; $ppTotal = 0;
$planned = []; $ideas = []; $archived = [];
if ($isDetailView) {
// --- MODE DÉTAIL ---
// 1. Récupérer l'idée principale
$rows = hol_q($pdo, "SELECT * FROM pf_holidays_ideas WHERE id = ?", [$ideaId]);
$currentIdea = $rows[0] ?? null;
if ($currentIdea) {
// 2. Récupérer les données pour la CARTE (un seul point)
// Le JS filtrera lat/lng invalides
$mapIdeas = hol_q($pdo, "
SELECT id, title, country, region, city,
CAST(lat AS DECIMAL(9,6)) AS lat,
CAST(lng AS DECIMAL(9,6)) AS lng,
status, desired_start_date, desired_end_date
FROM pf_holidays_ideas
WHERE id = ?
", [$ideaId]);
// 3. Récupérer les sous-éléments (Transport, Logement, etc.)
$transport = hol_q($pdo, "SELECT * FROM pf_holidays_transport WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
$lodging = hol_q($pdo, "SELECT * FROM pf_holidays_lodging WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
$acts = hol_q($pdo, "SELECT * FROM pf_holidays_activities WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
$budget = hol_q($pdo, "SELECT category, label, amount, per_person FROM pf_holidays_budget_items WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
// 4. Calculs totaux budget
$sumRow = hol_q($pdo, "
SELECT
SUM(CASE WHEN per_person=0 THEN amount ELSE 0 END) AS fixed_total,
SUM(CASE WHEN per_person=1 THEN amount ELSE 0 END) AS per_person_total
FROM pf_holidays_budget_items WHERE idea_id = ?
", [$ideaId])[0] ?? ['fixed_total' => 0, 'per_person_total' => 0];
$fixedTotal = (float)($sumRow['fixed_total'] ?? 0);
$ppTotal = (float)($sumRow['per_person_total'] ?? 0);
}
/* 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
");
// --- MODE LISTE ---
// 1. Données pour la CARTE (tous les points valides)
$mapIdeas = hol_q($pdo, "
SELECT id, title, country, region, city,
CAST(lat AS DECIMAL(9,6)) AS lat,
CAST(lng AS DECIMAL(9,6)) AS lng,
status, desired_start_date, desired_end_date
FROM pf_holidays_ideas
WHERE status IN ('draft','shortlist','favorite','planned')
AND lat IS NOT NULL
AND lng IS NOT NULL
");
// 2. Récupérer les listes
$planned = hol_q($pdo, "
SELECT * FROM pf_holidays_ideas
WHERE status = 'planned'
ORDER BY COALESCE(desired_start_date, created_at) DESC
");
$ideas = hol_q($pdo, "
SELECT * FROM pf_holidays_ideas
WHERE status IN ('draft','shortlist','favorite')
ORDER BY FIELD(status,'favorite','shortlist','draft'), created_at DESC
");
$archived = hol_q($pdo, "
SELECT * FROM pf_holidays_ideas
WHERE status = 'archived'
ORDER BY updated_at DESC
");
}
?>
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>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<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>
<?php if ($isDetailView): ?>
<?php if (!$currentIdea): ?>
<div class="pf-holidays__titlebar">
<h1>Idée introuvable</h1>
<div class="hol-title-actions">
<a class="btn" href="/holidays.php">← Retour à la liste</a>
</div>
</div>
<p>Cette idée de vacances n'existe pas ou a été supprimée.</p>
<?php else: ?>
<div class="pf-holidays__titlebar">
<h1><?= htmlspecialchars($currentIdea['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(($currentIdea['city'] ? $currentIdea['city'] . ', ' : '') . ($currentIdea['region'] ? $currentIdea['region'] . ', ' : '') . ($currentIdea['country'] ?? ''))) ?>
<?php if (!empty($currentIdea['desired_start_date'])): ?>
• Dates: <?= htmlspecialchars($currentIdea['desired_start_date']) ?><?= !empty($currentIdea['desired_end_date']) ? ' → ' . htmlspecialchars($currentIdea['desired_end_date']) : '' ?>
<?php elseif (!empty($currentIdea['season_hint'])): ?>
• Saison: <?= htmlspecialchars($currentIdea['season_hint']) ?>
<?php endif; ?>
<?php if (!empty($currentIdea['ideal_days'])): ?>
• Durée idéale: <?= (int)$currentIdea['ideal_days'] ?> j
<?php endif; ?>
• Statut: <strong><?= htmlspecialchars($currentIdea['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>
<?php endif; ?>
<?php else: ?>
<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>
</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>
<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>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>
<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>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>
<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>
<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-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">
<div class="hol-budget-summary">
<strong>Fixe:</strong> <?= number_format($fixedTotal, 0, ',', ' ') ?> €
• <strong>Par personne:</strong> <?= number_format($ppTotal, 0, ',', ' ') ?> €
</div>
<label>Titre <input type="text" name="title" required></label>
<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>
<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>
<!-- Modale édition idée (pré-remplie via JS) -->
<div class="hol-modal" id="hol-edit-modal" aria-hidden="true">
<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>
<?php endif; ?>
<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">
<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>
<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>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>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>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>
<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>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>
<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 class="hol-actions">
<button type="button" class="hol-cancel">Annuler</button>
<button type="submit" class="hol-ok">Enregistrer</button>
</div>
</form>
</div>
</div>
</div>
<!-- Modale carte -->
<div class="hol-modal" id="hol-map-modal" aria-hidden="true">
<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 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>
</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) ?>;
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>
<script src="/modules/holidays/holidays.js"></script>
+207 -167
View File
@@ -1,194 +1,234 @@
<?php
// modules/holidays/save.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;
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;
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
* Normalise une date (YYYY-MM-DD) : '' -> NULL
*/
function hol_norm_date($v): ?string {
if (!isset($v)) return null;
$s = trim((string)$v);
return $s === '' ? null : $s;
if (!isset($v)) return null;
$s = trim((string)$v);
return $s === '' ? null : $s;
}
/**
* Redirection explicite vers la vue détail ou liste
*/
function hol_redirect(int $id = 0): void {
if ($id > 0) {
header("Location: /holidays.php?id=" . $id);
} else {
header("Location: /holidays.php");
}
exit;
}
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'; }
switch ($action) {
// --- CRÉATION ---
case 'create_idea': {
$title = trim($_POST['title'] ?? '');
if ($title === '') {
throw new Exception("Le titre est obligatoire.");
}
$latVal = hol_norm_decimal($_POST['lat'] ?? null);
$lngVal = hol_norm_decimal($_POST['lng'] ?? null);
$status = $_POST['status'] ?? 'draft';
$start = hol_norm_date($_POST['desired_start_date'] ?? null);
$end = hol_norm_date($_POST['desired_end_date'] ?? null);
// Règle métier : si on met une date, on passe probablement en 'planned'
if (!empty($start) && $status === 'draft') { $status = 'planned'; }
$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;
$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' => $title,
':country' => trim($_POST['country'] ?? ''),
':region' => trim($_POST['region'] ?? ''),
':city' => trim($_POST['city'] ?? ''),
':lat' => hol_norm_decimal($_POST['lat'] ?? null),
':lng' => hol_norm_decimal($_POST['lng'] ?? null),
':start' => $start,
':end' => $end,
':season' => trim($_POST['season_hint'] ?? ''),
':days' => ($_POST['ideal_days'] !== '' ? (int)$_POST['ideal_days'] : null),
':status' => $status,
':notes' => trim($_POST['notes'] ?? ''),
]);
// Redirection vers la nouvelle idée
hol_redirect((int)$pdo->lastInsertId());
break;
}
// --- MISE À JOUR ---
case 'update_idea': {
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) throw new Exception("ID invalide.");
$title = trim($_POST['title'] ?? '');
if ($title === '') throw new Exception("Le titre est obligatoire.");
$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'; }
$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' => $id,
':title' => $title,
':country' => trim($_POST['country'] ?? ''),
':region' => trim($_POST['region'] ?? ''),
':city' => trim($_POST['city'] ?? ''),
':lat' => hol_norm_decimal($_POST['lat'] ?? null),
':lng' => hol_norm_decimal($_POST['lng'] ?? null),
':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_redirect($id);
break;
}
// --- SUPPRESSION ---
case 'delete_idea': {
$id = (int)($_POST['id'] ?? 0);
if ($id > 0) {
// On supprime l'idée (les sous-tables devraient être supprimées via ON DELETE CASCADE côté SQL,
// sinon il faudrait les supprimer ici manuellement)
$stmt = $pdo->prepare("DELETE FROM pf_holidays_ideas WHERE id = :id");
$stmt->execute([':id' => $id]);
}
hol_redirect(0); // Retour liste
break;
}
// --- SOUS-ÉLÉMENTS (Transport, Logement, Activités, Budget) ---
case 'add_transport': {
$id = (int)$_POST['idea_id'];
$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' => $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_redirect($id);
break;
}
case 'add_lodging': {
$id = (int)$_POST['idea_id'];
$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' => $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_redirect($id);
break;
}
case 'add_activity': {
$id = (int)$_POST['idea_id'];
$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' => $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_redirect($id);
break;
}
case 'add_budget': {
$id = (int)$_POST['idea_id'];
$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' => $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_redirect($id);
break;
}
default:
http_response_code(400);
echo 'Unknown action';
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());
}
http_response_code(500);
// En production, éviter d'afficher l'erreur brute à l'utilisateur
// Mais utile pour le debug actuel
echo "Erreur lors de l'enregistrement : " . htmlspecialchars($e->getMessage());
echo '<br><a href="/holidays.php">Retour</a>';
}
+32 -12
View File
@@ -1,4 +1,6 @@
<?php
// modules/holidays/view.php
require __DIR__ . '/../../includes/auth.php';
require_login('/login.php');
require __DIR__ . '/../../includes/db.php';
@@ -6,20 +8,38 @@ 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;
http_response_code(400);
echo json_encode(['error' => 'invalid_id']);
exit;
}
$st = $pdo->prepare("SELECT * FROM pf_holidays_ideas WHERE id = ?");
$st->execute([$id]);
$it = $st->fetch(PDO::FETCH_ASSOC);
try {
$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;
}
if (!$it) {
http_response_code(404);
echo json_encode(['error' => 'not_found']);
exit;
}
echo json_encode($it, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// 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) {
http_response_code(500);
echo json_encode(['error' => 'server_error']);
}