revamp gemini
This commit is contained in:
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()]);
|
||||
}
|
||||
}
|
||||
+506
-528
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
+476
-510
File diff suppressed because it is too large
Load Diff
+240
-248
@@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// --- 2. GESTION MODALE "AJOUTER" ---
|
||||
const addModalCtrl = setupModal("hol-add-modal");
|
||||
const addBtn = document.getElementById("hol-add-open");
|
||||
if (addBtn && addModalCtrl) {
|
||||
addBtn.addEventListener("click", addModalCtrl.open);
|
||||
}
|
||||
|
||||
// --- 3. GESTION MODALE "ÉDITER" ---
|
||||
const editModalCtrl = setupModal("hol-edit-modal");
|
||||
|
||||
// Fonction pour charger et ouvrir l'édition via AJAX
|
||||
async function openEditForId(id) {
|
||||
if (!editModalCtrl) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/modules/holidays/view.php?id=${encodeURIComponent(id)}`,
|
||||
{
|
||||
headers: { Accept: "application/json" },
|
||||
},
|
||||
{ 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 l’idé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) =>
|
||||
({
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
})[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
@@ -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="Mai–Juin"></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 l’idé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 l’idé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="Mai–Juin"></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 l’idé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
@@ -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
@@ -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']);
|
||||
}
|
||||
Reference in New Issue
Block a user