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