revamp gemini
This commit is contained in:
+51
-137
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
// Active l'affichage des erreurs pour le développement
|
// modules/family-calendar/family-calendar.php
|
||||||
|
|
||||||
|
// Active l'affichage des erreurs pour le développement (à retirer en prod si nécessaire)
|
||||||
ini_set('display_errors', 1);
|
ini_set('display_errors', 1);
|
||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
@@ -9,8 +11,9 @@ require_login();
|
|||||||
require __DIR__ . '/includes/db.php';
|
require __DIR__ . '/includes/db.php';
|
||||||
|
|
||||||
// --- On récupère TOUS les événements sauvegardés en base ---
|
// --- On récupère TOUS les événements sauvegardés en base ---
|
||||||
|
// Note: Si la base grossit trop, il faudra filtrer par année ici.
|
||||||
$stmt_events = $pdo->query("SELECT * FROM pf_events");
|
$stmt_events = $pdo->query("SELECT * FROM pf_events");
|
||||||
$dbEvents = $stmt_events->fetchAll();
|
$dbEvents = $stmt_events->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$pageTitle = "PachaFamily - Family Calendar";
|
$pageTitle = "PachaFamily - Family Calendar";
|
||||||
$activePage = "family-calendar";
|
$activePage = "family-calendar";
|
||||||
@@ -18,75 +21,47 @@ $bodyClass = "pf-family-calendar";
|
|||||||
$pageCss = "/modules/family-calendar/family-calendar.css";
|
$pageCss = "/modules/family-calendar/family-calendar.css";
|
||||||
|
|
||||||
require __DIR__ . '/header.php';
|
require __DIR__ . '/header.php';
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- ===================================================================== -->
|
|
||||||
<!-- INJECTION DES DONNÉES DU SERVEUR VERS JAVASCRIPT -->
|
|
||||||
<!-- Cette variable `serverData` sera lue par le script JS au démarrage. -->
|
|
||||||
<!-- ===================================================================== -->
|
|
||||||
<script>
|
<script>
|
||||||
|
/* JSON_NUMERIC_CHECK convertit les strings "1" en entiers 1, utile pour les calculs JS */
|
||||||
const serverData = <?php echo json_encode($dbEvents, JSON_NUMERIC_CHECK); ?>;
|
const serverData = <?php echo json_encode($dbEvents, JSON_NUMERIC_CHECK); ?>;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1>Family Calendar</h1>
|
<div class="pf-d-flex pf-align-center pf-justify-between pf-mb-4">
|
||||||
|
<h1>Family Calendar</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ===================================================================== -->
|
|
||||||
<!-- PANNEAU DE CONTRÔLE : Légende, Récapitulatif et Vacances -->
|
|
||||||
<!-- ===================================================================== -->
|
|
||||||
<section class="pf-section pf-section--panel">
|
<section class="pf-section pf-section--panel">
|
||||||
<div class="pf-flex pf-flex--wrap pf-gap-lg">
|
<div class="pf-flex pf-flex--wrap pf-gap-lg">
|
||||||
|
|
||||||
<!-- LÉGENDE -->
|
|
||||||
<div class="pf-card pf-card--small">
|
<div class="pf-card pf-card--small">
|
||||||
<h2 class="pf-card-title">Légende</h2>
|
<h2 class="pf-card-title">Légende</h2>
|
||||||
<div class="pf-card-body">
|
<div class="pf-card-body">
|
||||||
<div class="pf-legend-item">
|
<div class="pf-legend-grid">
|
||||||
<div class="pf-legend-color fc-legend-school-holiday"></div>
|
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-school-holiday"></div><span>Vacances</span></div>
|
||||||
<span>Vacances scolaires</span>
|
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-public-holiday"></div><span>Férié</span></div>
|
||||||
</div>
|
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-off-carole"></div><span>Off Carole</span></div>
|
||||||
<div class="pf-legend-item">
|
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-extra-off-carole"></div><span>Extra Off</span></div>
|
||||||
<div class="pf-legend-color fc-legend-public-holiday"></div>
|
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-centre"></div><span>Centre</span></div>
|
||||||
<span>Jour férié</span>
|
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-avis"></div><span>Avis</span></div>
|
||||||
</div>
|
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-pep-sick"></div><span>Pep Malade</span></div>
|
||||||
<div class="pf-legend-item">
|
|
||||||
<div class="pf-legend-color fc-legend-off-carole"></div>
|
|
||||||
<span>Off Carole</span>
|
|
||||||
</div>
|
|
||||||
<div class="pf-legend-item">
|
|
||||||
<div class="pf-legend-color fc-legend-extra-off-carole"></div>
|
|
||||||
<span>Extra Off Carole</span>
|
|
||||||
</div>
|
|
||||||
<div class="pf-legend-item">
|
|
||||||
<div class="pf-legend-color fc-legend-centre"></div>
|
|
||||||
<span>Centre</span>
|
|
||||||
</div>
|
|
||||||
<div class="pf-legend-item">
|
|
||||||
<div class="pf-legend-color fc-legend-avis"></div>
|
|
||||||
<span>Avis</span>
|
|
||||||
</div>
|
|
||||||
<div class="pf-legend-item">
|
|
||||||
<div class="pf-legend-color fc-legend-pep-sick"></div>
|
|
||||||
<span>Pep malade</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- RÉCAPITULATIF ANNUEL -->
|
|
||||||
<div class="pf-card pf-card--small">
|
<div class="pf-card pf-card--small">
|
||||||
<h2 class="pf-card-title">Récapitulatif annuel</h2>
|
<h2 class="pf-card-title">Récapitulatif annuel</h2>
|
||||||
<div class="pf-card-body" id="globalSummary">
|
<div class="pf-card-body" id="globalSummary">
|
||||||
<!-- Rempli par le JavaScript -->
|
<span class="pf-loading-text">Chargement...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- VACANCES SCOLAIRES -->
|
|
||||||
<div class="pf-card pf-card--small pf-card--wide">
|
<div class="pf-card pf-card--small pf-card--wide">
|
||||||
<h2 class="pf-card-title">Vacances scolaires - Zone C (2025-2026)</h2>
|
<h2 class="pf-card-title">Vacances scolaires - Zone C</h2>
|
||||||
<div class="pf-card-body">
|
<div class="pf-card-body">
|
||||||
<div class="pf-table-wrapper">
|
<div class="pf-table-wrapper pf-table-wrapper--max-height">
|
||||||
<table id="schoolHolidaysTable" class="fc-holidays-table">
|
<table id="schoolHolidaysTable" class="fc-holidays-table pf-table pf-table--compact">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Période</th>
|
<th>Période</th>
|
||||||
@@ -96,8 +71,7 @@ require __DIR__ . '/header.php';
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<!-- Rempli par JS -->
|
</tbody>
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,12 +80,10 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ===================================================================== -->
|
|
||||||
<!-- CALENDRIER MENSUEL -->
|
|
||||||
<!-- ===================================================================== -->
|
|
||||||
<section class="pf-section">
|
<section class="pf-section">
|
||||||
<h2>Calendrier mensuel</h2>
|
<h2>Calendrier mensuel</h2>
|
||||||
<div class="fc-month-calendar-wrapper">
|
<div class="fc-month-calendar-wrapper">
|
||||||
|
|
||||||
<div class="fc-month-header">
|
<div class="fc-month-header">
|
||||||
<div class="fc-view-controls">
|
<div class="fc-view-controls">
|
||||||
<button id="fc-view-1month" class="fc-view-button fc-view-button--active" data-view="1month">1 mois</button>
|
<button id="fc-view-1month" class="fc-view-button fc-view-button--active" data-view="1month">1 mois</button>
|
||||||
@@ -124,18 +96,16 @@ require __DIR__ . '/header.php';
|
|||||||
<button id="fc-next-month" class="fc-nav-button">›</button>
|
<button id="fc-next-month" class="fc-nav-button">›</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fc-calendar-and-summary">
|
|
||||||
|
<div class="fc-calendar-container">
|
||||||
<div id="fc-month-calendar" class="fc-month-calendar">
|
<div id="fc-month-calendar" class="fc-month-calendar">
|
||||||
|
</div>
|
||||||
|
<div id="fc-month-selectionMenu" class="fc-selection-menu" hidden></div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Le menu contextuel pour le calendrier mensuel -->
|
|
||||||
<div id="fc-month-selectionMenu" class="fc-selection-menu"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ===================================================================== -->
|
|
||||||
<!-- PLANNING PRINCIPAL -->
|
|
||||||
<!-- ===================================================================== -->
|
|
||||||
<section class="pf-section">
|
<section class="pf-section">
|
||||||
<div class="fc-week-header">
|
<div class="fc-week-header">
|
||||||
<h2>Planning hebdo</h2>
|
<h2>Planning hebdo</h2>
|
||||||
@@ -147,29 +117,15 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pf-table-wrapper" id="planningTable-wrapper">
|
<div class="pf-table-wrapper" id="planningTable-wrapper">
|
||||||
<table id="planningHeaderTable" class="pf-table pf-table--compact">
|
<table id="planningTable" class="pf-table pf-table--compact pf-table--sticky-head pf-table--bordered">
|
||||||
|
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col class="col-month"> <!-- Mois -->
|
<col class="col-month"> <col class="col-month"> <col class="col-day"> <col class="col-day"> <col class="col-day"> <col class="col-day"> <col class="col-day"> <col class="col-total"> <col class="col-total"> <col class="col-total"> <col class="col-total"> <col class="col-total"> <col class="col-total"> <col class="col-alex-sub">
|
||||||
<col class="col-month"> <!-- Semaine -->
|
|
||||||
<col class="col-day"> <!-- Lundi -->
|
|
||||||
<col class="col-day"> <!-- Mardi -->
|
|
||||||
<col class="col-day"> <!-- Mercredi -->
|
|
||||||
<col class="col-day"> <!-- Jeudi -->
|
|
||||||
<col class="col-day"> <!-- Vendredi -->
|
|
||||||
<col class="col-total"> <!-- # Off -->
|
|
||||||
<col class="col-total"> <!-- # Extra -->
|
|
||||||
<col class="col-total"> <!-- # Centre -->
|
|
||||||
<col class="col-total"> <!-- # Avis -->
|
|
||||||
<col class="col-total"> <!-- # Pep malade -->
|
|
||||||
<col class="col-total"> <!-- # Pep Présence -->
|
|
||||||
<!-- ALEX (6 colonnes) -->
|
|
||||||
<col class="col-alex-sub">
|
<col class="col-alex-sub">
|
||||||
<col class="col-alex-sub">
|
<col class="col-alex-sub">
|
||||||
<col class="col-alex-sub">
|
<col class="col-alex-sub">
|
||||||
<col class="col-alex-sub">
|
<col class="col-alex-sub">
|
||||||
<col class="col-alex-sub">
|
<col class="col-alex-sub">
|
||||||
<col class="col-alex-sub">
|
|
||||||
<!-- LAIA (6 colonnes) -->
|
|
||||||
<col class="col-laia-sub">
|
<col class="col-laia-sub">
|
||||||
<col class="col-laia-sub">
|
<col class="col-laia-sub">
|
||||||
<col class="col-laia-sub">
|
<col class="col-laia-sub">
|
||||||
@@ -177,28 +133,27 @@ require __DIR__ . '/header.php';
|
|||||||
<col class="col-laia-sub">
|
<col class="col-laia-sub">
|
||||||
<col class="col-laia-sub">
|
<col class="col-laia-sub">
|
||||||
</colgroup>
|
</colgroup>
|
||||||
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowspan="3" class="col-month">Mois</th>
|
<th rowspan="3" class="col-month">Mois</th>
|
||||||
<th rowspan="3" class="col-month">Semaine</th>
|
<th rowspan="3" class="col-month">Sem.</th>
|
||||||
<th rowspan="3" class="col-day">Lundi</th>
|
<th rowspan="3" class="col-day">Lun</th>
|
||||||
<th rowspan="3" class="col-day">Mardi</th>
|
<th rowspan="3" class="col-day">Mar</th>
|
||||||
<th rowspan="3" class="col-day">Mercredi</th>
|
<th rowspan="3" class="col-day">Mer</th>
|
||||||
<th rowspan="3" class="col-day">Jeudi</th>
|
<th rowspan="3" class="col-day">Jeu</th>
|
||||||
<th rowspan="3" class="col-day">Vendredi</th>
|
<th rowspan="3" class="col-day">Ven</th>
|
||||||
<th rowspan="3" class="col-total"># Off Carole</th>
|
<th rowspan="3" class="col-total rotated-text"><span>Off Carole</span></th>
|
||||||
<th rowspan="3" class="col-total"># Extra off Carole</th>
|
<th rowspan="3" class="col-total rotated-text"><span>Extra Off</span></th>
|
||||||
<th rowspan="3" class="col-total"># Centre</th>
|
<th rowspan="3" class="col-total rotated-text"><span>Centre</span></th>
|
||||||
<th rowspan="3" class="col-total"># Avis</th>
|
<th rowspan="3" class="col-total rotated-text"><span>Avis</span></th>
|
||||||
<th rowspan="3" class="col-total"># Pep malade</th>
|
<th rowspan="3" class="col-total rotated-text"><span>Pep Malade</span></th>
|
||||||
<th rowspan="3" class="col-total"># Pep Présence</th>
|
<th rowspan="3" class="col-total rotated-text"><span>Présence</span></th>
|
||||||
|
|
||||||
<!-- Ligne 1 : ALEX / LAIA -->
|
<th colspan="6" class="col-alex header-group">ALEX</th>
|
||||||
<th colspan="6" class="col-alex">ALEX</th>
|
<th colspan="6" class="col-laia header-group">LAIA</th>
|
||||||
<th colspan="6" class="col-laia">LAIA</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<!-- Ligne 2 : CP / JRA / JA (regroupement) -->
|
|
||||||
<th colspan="2" class="col-alex-sub">CP</th>
|
<th colspan="2" class="col-alex-sub">CP</th>
|
||||||
<th colspan="2" class="col-alex-sub">JRA</th>
|
<th colspan="2" class="col-alex-sub">JRA</th>
|
||||||
<th colspan="2" class="col-alex-sub">JA</th>
|
<th colspan="2" class="col-alex-sub">JA</th>
|
||||||
@@ -208,15 +163,12 @@ require __DIR__ . '/header.php';
|
|||||||
<th colspan="2" class="col-laia-sub">JA</th>
|
<th colspan="2" class="col-laia-sub">JA</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<!-- Ligne 3 : Available / Use pour chaque type -->
|
<th class="col-alex-sub col-alex-av">Av.</th>
|
||||||
<!-- ALEX -->
|
|
||||||
<th class="col-alex-sub col-alex-av">Av.</th>
|
|
||||||
<th class="col-alex-sub col-alex-use">Use</th>
|
<th class="col-alex-sub col-alex-use">Use</th>
|
||||||
<th class="col-alex-sub col-alex-av">Av.</th>
|
<th class="col-alex-sub col-alex-av">Av.</th>
|
||||||
<th class="col-alex-sub col-alex-use">Use</th>
|
<th class="col-alex-sub col-alex-use">Use</th>
|
||||||
<th class="col-alex-sub col-alex-av">Av.</th>
|
<th class="col-alex-sub col-alex-av">Av.</th>
|
||||||
<th class="col-alex-sub col-alex-use">Use</th>
|
<th class="col-alex-sub col-alex-use">Use</th>
|
||||||
<!-- LAIA -->
|
|
||||||
<th class="col-laia-sub col-laia-av">Av.</th>
|
<th class="col-laia-sub col-laia-av">Av.</th>
|
||||||
<th class="col-laia-sub col-laia-use">Use</th>
|
<th class="col-laia-sub col-laia-use">Use</th>
|
||||||
<th class="col-laia-sub col-laia-av">Av.</th>
|
<th class="col-laia-sub col-laia-av">Av.</th>
|
||||||
@@ -225,54 +177,16 @@ require __DIR__ . '/header.php';
|
|||||||
<th class="col-laia-sub col-laia-use">Use</th>
|
<th class="col-laia-sub col-laia-use">Use</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
|
||||||
<table id="planningTable" class="pf-table pf-table--compact">
|
|
||||||
<tbody id="planningBody">
|
<tbody id="planningBody">
|
||||||
<colgroup>
|
</tbody>
|
||||||
<col class="col-month"> <!-- Mois -->
|
|
||||||
<col class="col-month"> <!-- Semaine -->
|
|
||||||
<col class="col-day"> <!-- Lundi -->
|
|
||||||
<col class="col-day"> <!-- Mardi -->
|
|
||||||
<col class="col-day"> <!-- Mercredi -->
|
|
||||||
<col class="col-day"> <!-- Jeudi -->
|
|
||||||
<col class="col-day"> <!-- Vendredi -->
|
|
||||||
<col class="col-total"> <!-- # Off -->
|
|
||||||
<col class="col-total"> <!-- # Extra -->
|
|
||||||
<col class="col-total"> <!-- # Centre -->
|
|
||||||
<col class="col-total"> <!-- # Avis -->
|
|
||||||
<col class="col-total"> <!-- # Pep malade -->
|
|
||||||
<col class="col-total"> <!-- # Pep Présence -->
|
|
||||||
<!-- ALEX (6 colonnes) -->
|
|
||||||
<col class="col-alex-sub">
|
|
||||||
<col class="col-alex-sub">
|
|
||||||
<col class="col-alex-sub">
|
|
||||||
<col class="col-alex-sub">
|
|
||||||
<col class="col-alex-sub">
|
|
||||||
<col class="col-alex-sub">
|
|
||||||
<!-- LAIA (6 colonnes) -->
|
|
||||||
<col class="col-laia-sub">
|
|
||||||
<col class="col-laia-sub">
|
|
||||||
<col class="col-laia-sub">
|
|
||||||
<col class="col-laia-sub">
|
|
||||||
<col class="col-laia-sub">
|
|
||||||
<col class="col-laia-sub">
|
|
||||||
</colgroup>
|
|
||||||
<!-- lignes générées par JS -->
|
|
||||||
</tbody>
|
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- Le menu contextuel est caché par défaut et son contenu est généré par JS -->
|
<div id="selectionMenu" class="fc-selection-menu" hidden></div>
|
||||||
<div id="selectionMenu" class="fc-selection-menu"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ===================================================================== -->
|
|
||||||
<!-- CHARGEMENT DU SCRIPT JAVASCRIPT PRINCIPAL -->
|
|
||||||
<!-- ===================================================================== -->
|
|
||||||
<script src="/modules/family-calendar/family-calendar.js"></script>
|
<script src="/modules/family-calendar/family-calendar.js"></script>
|
||||||
|
|
||||||
<?php
|
<?php require __DIR__ . '/footer.php'; ?>
|
||||||
// Inclusion du pied de page
|
|
||||||
require __DIR__ . '/footer.php';
|
|
||||||
?>
|
|
||||||
+455
-547
File diff suppressed because it is too large
Load Diff
+6
-1
@@ -1,16 +1,21 @@
|
|||||||
<?php
|
<?php
|
||||||
|
// holidays.php
|
||||||
|
|
||||||
require __DIR__ . '/includes/auth.php';
|
require __DIR__ . '/includes/auth.php';
|
||||||
require_login('/login.php');
|
require_login('/login.php');
|
||||||
require __DIR__ . '/includes/db.php';
|
require __DIR__ . '/includes/db.php';
|
||||||
|
|
||||||
if (session_status() === PHP_SESSION_NONE) { session_start(); }
|
if (session_status() === PHP_SESSION_NONE) { session_start(); }
|
||||||
|
|
||||||
$pageTitle = "PachaFamily - Idées de vacances";
|
$pageTitle = "PachaFamily - Idées de vacances";
|
||||||
$activePage = "holidays";
|
$activePage = "holidays";
|
||||||
$bodyClass = "pf-holidays";
|
$bodyClass = "pf-holidays";
|
||||||
|
// On charge le CSS spécifique au module
|
||||||
$pageCss = "/modules/holidays/holidays.css";
|
$pageCss = "/modules/holidays/holidays.css";
|
||||||
|
|
||||||
require __DIR__ . '/header.php';
|
require __DIR__ . '/header.php';
|
||||||
|
|
||||||
|
// Inclusion de la logique et de la vue unifiées
|
||||||
require __DIR__ . '/modules/holidays/index.php';
|
require __DIR__ . '/modules/holidays/index.php';
|
||||||
|
|
||||||
require __DIR__ . '/footer.php';
|
require __DIR__ . '/footer.php';
|
||||||
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: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: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-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
|
<?php
|
||||||
|
// modules/family-calendar/includes/api/manage-event.php
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
require __DIR__ . '/../../../../includes/db.php';
|
require __DIR__ . '/../../../../includes/db.php';
|
||||||
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$action = $input['action'] ?? '';
|
||||||
|
|
||||||
if (!isset($input['action'])) {
|
if (!$action) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Action manquante.']);
|
echo json_encode(['status' => 'error', 'message' => 'Action manquante.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$action = $input['action'];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// --- SUPPRESSION UNITAIRE ---
|
||||||
if ($action === 'delete') {
|
if ($action === 'delete') {
|
||||||
// Suppression d'un seul événement
|
$eventId = (int)($input['event_id'] ?? 0);
|
||||||
if (!isset($input['event_id'])) {
|
if ($eventId <= 0) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'ID manquant pour la suppression.']);
|
echo json_encode(['status' => 'error', 'message' => 'ID manquant.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$eventId = (int)$input['event_id'];
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM pf_events WHERE id = ?");
|
$stmt = $pdo->prepare("DELETE FROM pf_events WHERE id = ?");
|
||||||
$stmt->execute([$eventId]);
|
$stmt->execute([$eventId]);
|
||||||
|
echo json_encode(['status' => 'success']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
echo json_encode(['status' => 'success', 'message' => 'Événement supprimé.']);
|
// --- MISE À JOUR UNITAIRE ---
|
||||||
|
if ($action === 'update') {
|
||||||
} elseif ($action === 'update') {
|
$eventId = (int)($input['event_id'] ?? 0);
|
||||||
// Mise à jour d'un seul événement
|
$newType = $input['new_type'] ?? '';
|
||||||
if (!isset($input['event_id'], $input['new_type'])) {
|
if ($eventId <= 0 || !$newType) {
|
||||||
http_response_code(400);
|
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;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$eventId = (int)$input['event_id'];
|
|
||||||
$newType = $input['new_type'];
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id = ?");
|
$stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id = ?");
|
||||||
$stmt->execute([$newType, $eventId]);
|
$stmt->execute([$newType, $eventId]);
|
||||||
|
echo json_encode(['status' => 'success']);
|
||||||
echo json_encode(['status' => 'success', 'message' => 'Événement mis à jour.']);
|
exit;
|
||||||
|
|
||||||
} 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.']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 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) {
|
} catch (PDOException $e) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
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
|
<?php
|
||||||
require __DIR__ . '/../../includes/auth.php';
|
// modules/gift-list/save-gift.php
|
||||||
require_login('/gift-list.php');
|
|
||||||
|
|
||||||
|
require __DIR__ . '/../../includes/auth.php';
|
||||||
|
require_login('/login.php');
|
||||||
require __DIR__ . '/../../includes/db.php';
|
require __DIR__ . '/../../includes/db.php';
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
$action = trim($_POST['action'] ?? 'create');
|
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') {
|
if ($action === 'delete') {
|
||||||
$gift_id = (int)($_POST['gift_id'] ?? 0);
|
|
||||||
if ($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]);
|
$stmt->execute(['id' => $gift_id]);
|
||||||
}
|
}
|
||||||
|
header("Location: $redirectUrl");
|
||||||
header('Location: /gift-list.php');
|
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Champs communs
|
// Champs communs
|
||||||
$year = (int)($_POST['year'] ?? date('Y'));
|
$year = (int)($_POST['year'] ?? date('Y'));
|
||||||
$adult_name = trim($_POST['adult_name'] ?? '');
|
$adult_name = trim($_POST['adult_name'] ?? '');
|
||||||
$payer_name = trim($_POST['payer_name'] ?? ''); // nouveau champ
|
$payer_name = trim($_POST['payer_name'] ?? '');
|
||||||
$child_name = trim($_POST['child_name'] ?? '');
|
$child_name = trim($_POST['child_name'] ?? '');
|
||||||
$occasion = trim($_POST['occasion'] ?? '');
|
$occasion = trim($_POST['occasion'] ?? '');
|
||||||
$gift_desc = trim($_POST['gift_description'] ?? '');
|
$gift_desc = trim($_POST['gift_description'] ?? '');
|
||||||
$product_link = trim($_POST['product_link'] ?? '');
|
$prod_link = trim($_POST['product_link'] ?? '');
|
||||||
$amount = $_POST['amount'] !== '' ? (float)$_POST['amount'] : 0.0;
|
$amount = ($_POST['amount'] !== '') ? (float)$_POST['amount'] : 0.0;
|
||||||
|
|
||||||
|
// Si payeur vide, c'est l'adulte responsable qui paye
|
||||||
if ($payer_name === '') {
|
if ($payer_name === '') {
|
||||||
$payer_name = $adult_name;
|
$payer_name = $adult_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($action === 'update') {
|
// Validation minimale
|
||||||
$gift_id = (int)($_POST['gift_id'] ?? 0);
|
if (!$adult_name || !$child_name || !$occasion || !$gift_desc) {
|
||||||
if ($gift_id > 0 && $adult_name && $payer_name && $child_name && $occasion && $gift_desc) {
|
// En cas d'erreur, on redirige sans rien faire (ou on pourrait gérer une erreur)
|
||||||
$stmt = $pdo->prepare("
|
header("Location: $redirectUrl");
|
||||||
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');
|
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// create (par défaut)
|
// --- UPDATE ---
|
||||||
if ($adult_name && $payer_name && $child_name && $occasion && $gift_desc) {
|
if ($action === 'update' && $gift_id > 0) {
|
||||||
$stmt = $pdo->prepare("
|
$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)
|
(year, adult_name, payer_name, child_name, occasion, gift_description, product_link, amount)
|
||||||
VALUES
|
VALUES
|
||||||
(:year, :adult_name, :payer_name, :child_name, :occasion, :gift_description, :product_link, :amount)
|
(: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,
|
'child_name' => $child_name,
|
||||||
'occasion' => $occasion,
|
'occasion' => $occasion,
|
||||||
'gift_description' => $gift_desc,
|
'gift_description' => $gift_desc,
|
||||||
'product_link' => $product_link ?: null,
|
'product_link' => $prod_link ?: null,
|
||||||
'amount' => $amount,
|
'amount' => $amount,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
header('Location: /gift-list.php');
|
} catch (PDOException $e) {
|
||||||
exit;
|
// 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');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
// 1. Validation de l'entrée
|
||||||
$q = trim($_GET['q'] ?? '');
|
$q = trim($_GET['q'] ?? '');
|
||||||
if ($q === '') {
|
if ($q === '') {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['error' => 'missing q']);
|
echo json_encode(['error' => 'missing_q']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// borne la limite entre 1 et 5 (usage perso)
|
// Borner la limite
|
||||||
$limit = (int)($_GET['limit'] ?? 1);
|
$limit = (int)($_GET['limit'] ?? 1);
|
||||||
if ($limit < 1) $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);
|
$qNorm = mb_strtolower($q);
|
||||||
$qHash = hash('sha256', $qNorm);
|
$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 {
|
try {
|
||||||
if ($limit === 1) {
|
|
||||||
$st = $pdo->prepare("SELECT lat, lng, display_name FROM pf_geocode_cache WHERE q_hash = ?");
|
$st = $pdo->prepare("SELECT lat, lng, display_name FROM pf_geocode_cache WHERE q_hash = ?");
|
||||||
$st->execute([$qHash]);
|
$st->execute([$qHash]);
|
||||||
if ($row = $st->fetch(PDO::FETCH_ASSOC)) {
|
if ($row = $st->fetch(PDO::FETCH_ASSOC)) {
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'lat' => (float)$row['lat'],
|
'lat' => (float)$row['lat'],
|
||||||
'lng' => (float)$row['lng'],
|
'lng' => (float)$row['lng'],
|
||||||
'display_name' => $row['display_name'],
|
'display_name' => $row['display_name'],
|
||||||
'cached' => true
|
'cached' => true
|
||||||
]);
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (Throwable $e) {
|
} 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';
|
$endpoint = 'https://nominatim.openstreetmap.org/search';
|
||||||
$params = http_build_query([
|
$params = http_build_query([
|
||||||
'format' => 'jsonv2',
|
'format' => 'jsonv2',
|
||||||
'addressdetails' => 1,
|
'addressdetails' => 1,
|
||||||
'limit' => $limit,
|
'limit' => $limit,
|
||||||
'q' => $q,
|
'q' => $q,
|
||||||
], '', '&', PHP_QUERY_RFC3986);
|
], '', '&', PHP_QUERY_RFC3986);
|
||||||
|
|
||||||
$url = $endpoint . '?' . $params;
|
$url = $endpoint . '?' . $params;
|
||||||
|
|
||||||
$ch = curl_init($url);
|
$ch = curl_init($url);
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
CURLOPT_CONNECTTIMEOUT => 5,
|
CURLOPT_CONNECTTIMEOUT => 5,
|
||||||
CURLOPT_TIMEOUT => 10,
|
CURLOPT_TIMEOUT => 10,
|
||||||
CURLOPT_HTTPHEADER => [
|
CURLOPT_HTTPHEADER => [
|
||||||
// Contact fourni: ferlan.alexandre@gmail.com
|
// Ton User-Agent est correct.
|
||||||
'User-Agent: PachaFamily-Holidays/1.0 (+contact: ferlan.alexandre@gmail.com)'
|
// Important : Nominatim demande une identification claire.
|
||||||
],
|
'User-Agent: PachaFamily-Holidays/1.0 (+contact: ferlan.alexandre@gmail.com)'
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
$body = curl_exec($ch);
|
$body = curl_exec($ch);
|
||||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
$err = curl_error($ch);
|
$err = curl_error($ch);
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
|
// Gestion erreur cURL / HTTP
|
||||||
if ($body === false || $http !== 200) {
|
if ($body === false || $http !== 200) {
|
||||||
http_response_code(502);
|
http_response_code(502); // Bad Gateway
|
||||||
echo json_encode(['error' => 'geocode_failed', 'details' => $err ?: ('HTTP '.$http)]);
|
echo json_encode([
|
||||||
exit;
|
'error' => 'geocode_failed',
|
||||||
|
'details' => $err ?: ('HTTP ' . $http)
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = json_decode($body, true);
|
$data = json_decode($body, true);
|
||||||
|
|
||||||
|
// Gestion erreur JSON ou vide
|
||||||
if (!is_array($data) || empty($data)) {
|
if (!is_array($data) || empty($data)) {
|
||||||
http_response_code(404);
|
// On renvoie 200 avec une liste vide ou 404, le JS gère les deux.
|
||||||
echo json_encode(['error' => 'not_found']);
|
// 404 est plus sémantique "Not Found".
|
||||||
exit;
|
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) {
|
if ($limit === 1) {
|
||||||
$r = $data[0];
|
$r = $data[0];
|
||||||
$lat = round((float)$r['lat'], 6);
|
echo json_encode([
|
||||||
$lng = round((float)$r['lon'], 6);
|
'lat' => round((float)$r['lat'], 6),
|
||||||
$display = $r['display_name'] ?? null;
|
'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 {
|
echo json_encode(['results' => $results], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
$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);
|
|
||||||
+476
-510
File diff suppressed because it is too large
Load Diff
+240
-248
@@ -1,107 +1,128 @@
|
|||||||
// modules/holidays/holidays.js
|
// modules/holidays/holidays.js
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
// --- Modale "Ajouter une idée"
|
// --- 1. FONCTIONS UTILITAIRES ---
|
||||||
const addBtn = document.getElementById("hol-add-open");
|
|
||||||
const addModal = document.getElementById("hol-add-modal");
|
/**
|
||||||
if (addBtn && addModal) {
|
* Configure les comportements de fermeture d'une modale (Backdrop, Cancel, Escape)
|
||||||
const backdrop = addModal.querySelector(".hol-backdrop");
|
*/
|
||||||
const cancel = addModal.querySelector(".hol-cancel");
|
function setupModal(modalId, openAction = null) {
|
||||||
const open = () => addModal.classList.add("open");
|
const modal = document.getElementById(modalId);
|
||||||
const close = () => addModal.classList.remove("open");
|
if (!modal) return null;
|
||||||
addBtn.addEventListener("click", open);
|
|
||||||
backdrop.addEventListener("click", close);
|
const backdrop = modal.querySelector(".hol-backdrop");
|
||||||
cancel.addEventListener("click", close);
|
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) => {
|
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");
|
* Échappe les caractères HTML pour éviter les failles XSS simples
|
||||||
const openEditModal = () => {
|
*/
|
||||||
if (editModal) editModal.classList.add("open");
|
function esc(s) {
|
||||||
};
|
if (s === null || s === undefined) return "";
|
||||||
const closeEditModal = () => {
|
return String(s)
|
||||||
if (editModal) editModal.classList.remove("open");
|
.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) {
|
async function openEditForId(id) {
|
||||||
|
if (!editModalCtrl) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`/modules/holidays/view.php?id=${encodeURIComponent(id)}`,
|
`/modules/holidays/view.php?id=${encodeURIComponent(id)}`,
|
||||||
{
|
{ headers: { Accept: "application/json" } },
|
||||||
headers: { Accept: "application/json" },
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
if (!res.ok) throw new Error("Erreur HTTP " + res.status);
|
||||||
const it = await res.json();
|
|
||||||
|
|
||||||
if (!editModal) {
|
const it = await res.json();
|
||||||
alert("Modale édition introuvable");
|
const modal = editModalCtrl.modal;
|
||||||
return;
|
|
||||||
}
|
// Helper pour remplir les champs
|
||||||
// Scope les sélecteurs à la modale pour éviter les null
|
|
||||||
const $ = (sel) => editModal.querySelector(sel);
|
|
||||||
const setVal = (sel, val) => {
|
const setVal = (sel, val) => {
|
||||||
const el = $(sel);
|
const el = modal.querySelector(sel);
|
||||||
if (el) el.value = val ?? "";
|
if (el) el.value = val ?? "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Remplissage du formulaire
|
||||||
setVal("#edit-id", it.id);
|
setVal("#edit-id", it.id);
|
||||||
setVal("#edit-title", it.title || "");
|
setVal("#edit-title", it.title);
|
||||||
setVal("#edit-country", it.country || "");
|
setVal("#edit-country", it.country);
|
||||||
setVal("#edit-region", it.region || "");
|
setVal("#edit-region", it.region);
|
||||||
setVal("#edit-city", it.city || "");
|
setVal("#edit-city", it.city);
|
||||||
setVal("#edit-lat", it.lat ?? "");
|
setVal("#edit-lat", it.lat);
|
||||||
setVal("#edit-lng", it.lng ?? "");
|
setVal("#edit-lng", it.lng);
|
||||||
setVal("#edit-start", it.desired_start_date ?? "");
|
setVal("#edit-start", it.desired_start_date);
|
||||||
setVal("#edit-end", it.desired_end_date ?? "");
|
setVal("#edit-end", it.desired_end_date);
|
||||||
setVal("#edit-season", it.season_hint || "");
|
setVal("#edit-season", it.season_hint);
|
||||||
setVal("#edit-days", it.ideal_days ?? "");
|
setVal("#edit-days", it.ideal_days);
|
||||||
setVal("#edit-status", it.status || "draft");
|
setVal("#edit-status", it.status || "draft");
|
||||||
setVal("#edit-notes", it.notes || "");
|
setVal("#edit-notes", it.notes);
|
||||||
|
|
||||||
openEditModal();
|
editModalCtrl.open();
|
||||||
} catch {
|
} catch (err) {
|
||||||
alert("Impossible de charger l’idée.");
|
console.error(err);
|
||||||
|
alert("Impossible de charger les données de l'idée.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Modale "Éditer" (liste + page détail)
|
// Écouteurs sur les boutons "Éditer" (Liste + Détail)
|
||||||
if (editModal) {
|
document.body.addEventListener("click", (e) => {
|
||||||
const backdrop = editModal.querySelector(".hol-backdrop");
|
// Utilisation de la délégation d'événement pour gérer tous les boutons (même dynamiques)
|
||||||
const cancel = editModal.querySelector(".hol-cancel");
|
const btn = e.target.closest(".btn-edit");
|
||||||
if (backdrop) backdrop.addEventListener("click", closeEditModal);
|
if (btn && btn.hasAttribute("data-edit-id")) {
|
||||||
if (cancel) cancel.addEventListener("click", closeEditModal);
|
const id = btn.getAttribute("data-edit-id");
|
||||||
document.addEventListener("keydown", (e) => {
|
openEditForId(id);
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
// 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)
|
// --- 4. GESTION SUPPRESSION ---
|
||||||
document.querySelectorAll(".btn-delete[data-del-id]").forEach((btn) => {
|
document.body.addEventListener("click", (e) => {
|
||||||
btn.addEventListener("click", () => {
|
const btn = e.target.closest(".btn-delete");
|
||||||
|
if (btn && btn.hasAttribute("data-del-id")) {
|
||||||
const id = btn.getAttribute("data-del-id");
|
const id = btn.getAttribute("data-del-id");
|
||||||
if (!id) return;
|
if (
|
||||||
if (confirm("Supprimer cette idée ?")) {
|
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");
|
const form = document.createElement("form");
|
||||||
form.method = "post";
|
form.method = "post";
|
||||||
form.action = "/modules/holidays/save.php";
|
form.action = "/modules/holidays/save.php";
|
||||||
@@ -112,45 +133,51 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
document.body.appendChild(form);
|
document.body.appendChild(form);
|
||||||
form.submit();
|
form.submit();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Géocodage via Nominatim + UI multi-résultats
|
// --- 5. GÉOCODAGE (Nominatim) ---
|
||||||
document.querySelectorAll(".hol-geocode-btn").forEach((btn) => {
|
document.querySelectorAll(".hol-geocode-btn").forEach((btn) => {
|
||||||
btn.addEventListener("click", async () => {
|
btn.addEventListener("click", async () => {
|
||||||
const scope = btn.getAttribute("data-scope"); // 'add' ou 'edit'
|
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") {
|
if (scope === "edit") {
|
||||||
form = btn.closest("form") || document;
|
const getVal = (id) =>
|
||||||
city = (document.getElementById("edit-city")?.value || "").trim();
|
(document.getElementById(id)?.value || "").trim();
|
||||||
region = (document.getElementById("edit-region")?.value || "").trim();
|
qParts = [
|
||||||
country = (document.getElementById("edit-country")?.value || "").trim();
|
getVal("edit-city"),
|
||||||
|
getVal("edit-region"),
|
||||||
|
getVal("edit-country"),
|
||||||
|
];
|
||||||
latInput = document.getElementById("edit-lat");
|
latInput = document.getElementById("edit-lat");
|
||||||
lngInput = document.getElementById("edit-lng");
|
lngInput = document.getElementById("edit-lng");
|
||||||
} else {
|
} else {
|
||||||
form = btn.closest("form");
|
// Scope 'add' : on cherche dans le formulaire parent
|
||||||
city = (form?.querySelector('input[name="city"]')?.value || "").trim();
|
const form = btn.closest("form");
|
||||||
region = (
|
const getVal = (name) =>
|
||||||
form?.querySelector('input[name="region"]')?.value || ""
|
(form?.querySelector(`input[name="${name}"]`)?.value || "").trim();
|
||||||
).trim();
|
qParts = [getVal("city"), getVal("region"), getVal("country")];
|
||||||
country = (
|
|
||||||
form?.querySelector('input[name="country"]')?.value || ""
|
|
||||||
).trim();
|
|
||||||
latInput = form?.querySelector('input[name="lat"]');
|
latInput = form?.querySelector('input[name="lat"]');
|
||||||
lngInput = form?.querySelector('input[name="lng"]');
|
lngInput = form?.querySelector('input[name="lng"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
const q = [city, region, country].filter(Boolean).join(", ");
|
const q = qParts.filter(Boolean).join(", ");
|
||||||
if (!q) {
|
if (!q) {
|
||||||
alert("Renseigne au moins Ville/Pays.");
|
alert(
|
||||||
|
"Veuillez renseigner au moins une Ville ou un Pays pour géocoder.",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UI : chargement
|
||||||
removeNearbyPicker(btn);
|
removeNearbyPicker(btn);
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
const original = btn.textContent;
|
const originalText = btn.textContent;
|
||||||
btn.textContent = "Recherche...";
|
btn.textContent = "⏳...";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
@@ -160,28 +187,30 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const data = await res.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 (!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 (latInput) latInput.value = data.lat;
|
||||||
if (lngInput) lngInput.value = data.lng;
|
if (lngInput) lngInput.value = data.lng;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
// Cas 2: Liste de choix
|
||||||
if (Array.isArray(data.results) && data.results.length > 0) {
|
else if (Array.isArray(data.results) && data.results.length > 0) {
|
||||||
renderGeocodePicker(btn, data.results, (choice) => {
|
renderGeocodePicker(btn, data.results, (choice) => {
|
||||||
if (latInput) latInput.value = choice.lat;
|
if (latInput) latInput.value = choice.lat;
|
||||||
if (lngInput) lngInput.value = choice.lng;
|
if (lngInput) lngInput.value = choice.lng;
|
||||||
removeNearbyPicker(btn);
|
removeNearbyPicker(btn);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
alert("Aucun résultat.");
|
alert("Aucun résultat trouvé pour : " + q);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
alert("Impossible de trouver les coordonnées pour: " + q);
|
console.error(err);
|
||||||
|
alert("Erreur lors du géocodage.");
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = original;
|
btn.textContent = originalText;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -192,46 +221,36 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
wrapper.className = "hol-geocode-picker";
|
wrapper.className = "hol-geocode-picker";
|
||||||
|
|
||||||
|
// Header
|
||||||
const header = document.createElement("div");
|
const header = document.createElement("div");
|
||||||
header.className = "hol-geocode-picker__header";
|
header.className = "hol-geocode-picker__header";
|
||||||
header.textContent = "Plusieurs résultats trouvés";
|
header.innerHTML = `<span>Choix multiples</span><button type="button" class="hol-close">×</button>`;
|
||||||
const closeBtn = document.createElement("button");
|
header
|
||||||
closeBtn.type = "button";
|
.querySelector(".hol-close")
|
||||||
closeBtn.className = "hol-geocode-picker__close";
|
.addEventListener("click", () => removeNearbyPicker(anchorBtn));
|
||||||
closeBtn.textContent = "×";
|
wrapper.appendChild(header);
|
||||||
closeBtn.addEventListener("click", () => removeNearbyPicker(anchorBtn));
|
|
||||||
header.appendChild(closeBtn);
|
|
||||||
|
|
||||||
|
// Liste
|
||||||
const list = document.createElement("ul");
|
const list = document.createElement("ul");
|
||||||
list.className = "hol-geocode-picker__list";
|
list.className = "hol-geocode-picker__list";
|
||||||
|
|
||||||
results.forEach((r) => {
|
results.forEach((r) => {
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
li.className = "hol-geocode-picker__item";
|
li.className = "hol-geocode-picker__item";
|
||||||
|
li.innerHTML = `
|
||||||
const label = document.createElement("div");
|
<div class="hol-info">
|
||||||
label.className = "hol-geocode-picker__label";
|
<span class="hol-label">${esc(r.display_name)}</span>
|
||||||
label.textContent = r.display_name || `${r.lat}, ${r.lng}`;
|
<span class="hol-coords">(${r.lat}, ${r.lng})</span>
|
||||||
|
</div>
|
||||||
const coords = document.createElement("div");
|
<button type="button">Choisir</button>
|
||||||
coords.className = "hol-geocode-picker__coords";
|
`;
|
||||||
coords.textContent = `(${r.lat}, ${r.lng})`;
|
li.querySelector("button").addEventListener("click", () => onPick(r));
|
||||||
|
|
||||||
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);
|
list.appendChild(li);
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper.appendChild(header);
|
|
||||||
wrapper.appendChild(list);
|
wrapper.appendChild(list);
|
||||||
|
|
||||||
|
// Insertion après le conteneur du bouton
|
||||||
const container =
|
const container =
|
||||||
anchorBtn.closest(".hol-inline") || anchorBtn.parentElement;
|
anchorBtn.closest(".hol-inline") || anchorBtn.parentElement;
|
||||||
container.insertAdjacentElement("afterend", wrapper);
|
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 mapBtn = document.getElementById("hol-map-open");
|
||||||
const mapModal = document.getElementById("hol-map-modal");
|
if (mapBtn) {
|
||||||
if (mapBtn && mapModal) {
|
const mapModalCtrl = setupModal("hol-map-modal", initMap); // On passe initMap en callback d'ouverture
|
||||||
const backdrop = mapModal.querySelector(".hol-backdrop");
|
mapBtn.addEventListener("click", mapModalCtrl.open);
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+414
-461
@@ -1,506 +1,459 @@
|
|||||||
<?php
|
<?php
|
||||||
// modules/holidays/index.php
|
// modules/holidays/index.php
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// 1. LOGIQUE PHP : RÉCUPÉRATION DES DONNÉES
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
if (!function_exists('hol_q')) {
|
if (!function_exists('hol_q')) {
|
||||||
function hol_q(PDO $pdo, string $sql, array $params = []): array {
|
function hol_q(PDO $pdo, string $sql, array $params = []): array {
|
||||||
$st = $pdo->prepare($sql);
|
$st = $pdo->prepare($sql);
|
||||||
$st->execute($params);
|
$st->execute($params);
|
||||||
return $st->fetchAll(PDO::FETCH_ASSOC);
|
return $st->fetchAll(PDO::FETCH_ASSOC);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$ideaId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
$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 {
|
} else {
|
||||||
// Vue liste: idées non archivées avec coordonnées
|
// --- MODE LISTE ---
|
||||||
$mapIdeas = hol_q($pdo, "
|
|
||||||
SELECT id, title, country, region, city,
|
// 1. Données pour la CARTE (tous les points valides)
|
||||||
CAST(lat AS DECIMAL(9,6)) AS lat,
|
$mapIdeas = hol_q($pdo, "
|
||||||
CAST(lng AS DECIMAL(9,6)) AS lng,
|
SELECT id, title, country, region, city,
|
||||||
status, desired_start_date, desired_end_date
|
CAST(lat AS DECIMAL(9,6)) AS lat,
|
||||||
FROM pf_holidays_ideas
|
CAST(lng AS DECIMAL(9,6)) AS lng,
|
||||||
WHERE status IN ('draft','shortlist','favorite','planned')
|
status, desired_start_date, desired_end_date
|
||||||
AND lat IS NOT NULL
|
FROM pf_holidays_ideas
|
||||||
AND lng IS NOT NULL
|
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) {
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||||
// 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">
|
<?php if ($isDetailView): ?>
|
||||||
<h1><?= htmlspecialchars($idea['title']) ?></h1>
|
|
||||||
<div class="hol-title-actions">
|
<?php if (!$currentIdea): ?>
|
||||||
<a class="btn" href="/holidays.php">← Retour</a>
|
<div class="pf-holidays__titlebar">
|
||||||
<button class="btn btn-edit" id="hol-edit-open" data-edit-id="<?= (int)$ideaId ?>">Éditer</button>
|
<h1>Idée introuvable</h1>
|
||||||
<button class="btn btn-delete" id="hol-delete" data-del-id="<?= (int)$ideaId ?>">Supprimer</button>
|
<div class="hol-title-actions">
|
||||||
<button class="hol-map-toggle" id="hol-map-open">🌍 Carte</button>
|
<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>
|
||||||
</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">
|
<section class="pf-section pf-section--panel">
|
||||||
<h2>Transport</h2>
|
<h2>Vacances planifiées</h2>
|
||||||
<form method="post" action="/modules/holidays/save.php" class="hol-inline-form">
|
<p class="cl-legend">Dates souhaitées, prêtes à être réservées.</p>
|
||||||
<input type="hidden" name="action" value="add_transport">
|
<div class="hol-ideas-grid">
|
||||||
<input type="hidden" name="idea_id" value="<?= (int)$ideaId ?>">
|
<?php foreach ($planned as $it): ?>
|
||||||
<select name="mode" required>
|
<div class="hol-idea-card" data-id="<?= (int)$it['id'] ?>">
|
||||||
<option value="TRAIN">TRAIN</option><option value="PLANE">PLANE</option>
|
<div class="hol-idea-card__head">
|
||||||
<option value="CAR">CAR</option><option value="BUS">BUS</option><option value="BOAT">BOAT</option>
|
<h3><?= htmlspecialchars($it['title']) ?></h3>
|
||||||
</select>
|
<span class="hol-status hol-status--planned">planned</span>
|
||||||
<input type="number" name="duration_min" min="0" placeholder="Durée (min)">
|
</div>
|
||||||
<input type="number" step="0.01" name="cost" placeholder="Coût (€)">
|
<p class="hol-idea-meta">
|
||||||
<input type="number" step="0.01" name="co2_kg" placeholder="CO₂ (kg)">
|
<?= htmlspecialchars(trim(($it['city'] ? $it['city'] . ', ' : '') . ($it['region'] ? $it['region'] . ', ' : '') . ($it['country'] ?? ''))) ?>
|
||||||
<input type="url" name="link" placeholder="Lien">
|
<?php if (!empty($it['desired_start_date'])): ?>
|
||||||
<input type="text" name="notes" placeholder="Notes">
|
• Dates: <?= htmlspecialchars($it['desired_start_date']) ?><?= !empty($it['desired_end_date']) ? ' → ' . htmlspecialchars($it['desired_end_date']) : '' ?>
|
||||||
<button type="submit" class="btn">Ajouter</button>
|
<?php endif; ?>
|
||||||
</form>
|
<?php if (!empty($it['ideal_days'])): ?>
|
||||||
<ul class="hol-list">
|
• Durée idéale: <?= (int)$it['ideal_days'] ?> j
|
||||||
<?php foreach ($transport as $t): ?>
|
<?php endif; ?>
|
||||||
<li><?= htmlspecialchars($t['mode']) ?>
|
</p>
|
||||||
<?= $t['duration_min'] !== null ? ' • ' . (int)$t['duration_min'] . ' min' : '' ?>
|
<?php if (!empty($it['notes'])): ?>
|
||||||
<?= $t['cost'] !== null ? ' • ' . number_format((float)$t['cost'], 0, ',', ' ') . ' €' : '' ?>
|
<p class="hol-notes"><?= nl2br(htmlspecialchars(mb_strimwidth($it['notes'], 0, 160, '…'))) ?></p>
|
||||||
<?php if (!empty($t['link'])): ?> • <a href="<?= htmlspecialchars($t['link']) ?>" target="_blank" rel="noopener">🔗</a><?php endif; ?>
|
<?php endif; ?>
|
||||||
</li>
|
<div class="hol-card-actions">
|
||||||
<?php endforeach; ?>
|
<a class="btn" href="/holidays.php?id=<?= (int)$it['id'] ?>">Ouvrir</a>
|
||||||
</ul>
|
<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>
|
||||||
|
|
||||||
<section class="pf-section pf-section--panel">
|
<section class="pf-section pf-section--panel">
|
||||||
<h2>Hébergement</h2>
|
<h2>Idées</h2>
|
||||||
<form method="post" action="/modules/holidays/save.php" class="hol-inline-form">
|
<p class="cl-legend">Brouillons, favoris, shortlist.</p>
|
||||||
<input type="hidden" name="action" value="add_lodging">
|
<div class="hol-ideas-grid">
|
||||||
<input type="hidden" name="idea_id" value="<?= (int)$ideaId ?>">
|
<?php foreach ($ideas as $it): ?>
|
||||||
<select name="type" required>
|
<div class="hol-idea-card" data-id="<?= (int)$it['id'] ?>">
|
||||||
<option value="HOTEL">HOTEL</option><option value="APT">APT</option>
|
<div class="hol-idea-card__head">
|
||||||
<option value="HOUSE">HOUSE</option><option value="CAMPING">CAMPING</option><option value="OTHER">OTHER</option>
|
<h3><?= htmlspecialchars($it['title']) ?></h3>
|
||||||
</select>
|
<span class="hol-status hol-status--<?= htmlspecialchars($it['status']) ?>"><?= htmlspecialchars($it['status']) ?></span>
|
||||||
<input type="text" name="location_text" placeholder="Localisation">
|
</div>
|
||||||
<input type="number" step="0.01" name="price_per_n" placeholder="€ / nuit">
|
<p class="hol-idea-meta">
|
||||||
<input type="number" name="nights" placeholder="Nuits">
|
<?= htmlspecialchars(trim(($it['city'] ? $it['city'] . ', ' : '') . ($it['region'] ? $it['region'] . ', ' : '') . ($it['country'] ?? ''))) ?>
|
||||||
<label><input type="checkbox" name="free_cancel" value="1"> Annulation gratuite</label>
|
<?php if (!empty($it['desired_start_date'])): ?>
|
||||||
<label><input type="checkbox" name="family_friendly" value="1" checked> Family-friendly</label>
|
• Dates: <?= htmlspecialchars($it['desired_start_date']) ?><?= !empty($it['desired_end_date']) ? ' → ' . htmlspecialchars($it['desired_end_date']) : '' ?>
|
||||||
<input type="url" name="link" placeholder="Lien">
|
<?php elseif (!empty($it['season_hint'])): ?>
|
||||||
<input type="text" name="notes" placeholder="Notes">
|
• Saison: <?= htmlspecialchars($it['season_hint']) ?>
|
||||||
<button type="submit" class="btn">Ajouter</button>
|
<?php endif; ?>
|
||||||
</form>
|
<?php if (!empty($it['ideal_days'])): ?>
|
||||||
<ul class="hol-list">
|
• Durée idéale: <?= (int)$it['ideal_days'] ?> j
|
||||||
<?php foreach ($lodging as $l): ?>
|
<?php endif; ?>
|
||||||
<li><?= htmlspecialchars($l['type']) ?>
|
</p>
|
||||||
<?= !empty($l['location_text']) ? ' • ' . htmlspecialchars($l['location_text']) : '' ?>
|
<?php if (!empty($it['notes'])): ?>
|
||||||
<?= $l['price_per_n'] !== null ? ' • ' . number_format((float)$l['price_per_n'], 0, ',', ' ') . ' €/nuit' : '' ?>
|
<p class="hol-notes"><?= nl2br(htmlspecialchars(mb_strimwidth($it['notes'], 0, 160, '…'))) ?></p>
|
||||||
<?= $l['nights'] !== null ? ' × ' . (int)$l['nights'] . 'n' : '' ?>
|
<?php endif; ?>
|
||||||
<?php if (!empty($l['link'])): ?> • <a href="<?= htmlspecialchars($l['link']) ?>" target="_blank" rel="noopener">🔗</a><?php endif; ?>
|
<div class="hol-card-actions">
|
||||||
</li>
|
<a class="btn" href="/holidays.php?id=<?= (int)$it['id'] ?>">Ouvrir</a>
|
||||||
<?php endforeach; ?>
|
<button class="btn btn-edit" data-edit-id="<?= (int)$it['id'] ?>">Éditer</button>
|
||||||
</ul>
|
<button class="btn btn-delete" data-del-id="<?= (int)$it['id'] ?>">Supprimer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="pf-section pf-section--panel">
|
<section class="pf-section pf-section--panel">
|
||||||
<h2>Activités</h2>
|
<h2>Archivées</h2>
|
||||||
<form method="post" action="/modules/holidays/save.php" class="hol-inline-form">
|
<div class="hol-ideas-grid hol-ideas-grid--archived">
|
||||||
<input type="hidden" name="action" value="add_activity">
|
<?php foreach ($archived as $it): ?>
|
||||||
<input type="hidden" name="idea_id" value="<?= (int)$ideaId ?>">
|
<div class="hol-idea-card hol-idea-card--archived" data-id="<?= (int)$it['id'] ?>">
|
||||||
<input type="text" name="name" placeholder="Nom" required>
|
<h3><?= htmlspecialchars($it['title']) ?></h3>
|
||||||
<input type="text" name="kind" placeholder="Type (ex: PARK)">
|
<div class="hol-card-actions">
|
||||||
<input type="number" step="0.01" name="cost_est" placeholder="€ estimé">
|
<button class="btn btn-edit" data-edit-id="<?= (int)$it['id'] ?>">Éditer</button>
|
||||||
<label><input type="checkbox" name="need_booking" value="1"> Réservation</label>
|
<button class="btn btn-delete" data-del-id="<?= (int)$it['id'] ?>">Supprimer</button>
|
||||||
<select name="weather">
|
</div>
|
||||||
<option value="ANY">ANY</option><option value="GOOD">GOOD</option><option value="RAIN">RAIN</option>
|
</div>
|
||||||
</select>
|
<?php endforeach; ?>
|
||||||
<input type="url" name="link" placeholder="Lien">
|
</div>
|
||||||
<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>
|
||||||
|
|
||||||
<section class="pf-section pf-section--panel">
|
<div class="hol-modal" id="hol-add-modal" aria-hidden="true">
|
||||||
<h2>Budget</h2>
|
<div class="hol-backdrop"></div>
|
||||||
<form method="post" action="/modules/holidays/save.php" class="hol-inline-form">
|
<div class="hol-dialog" role="dialog" aria-modal="true" aria-labelledby="hol-add-title">
|
||||||
<input type="hidden" name="action" value="add_budget">
|
<form method="post" action="/modules/holidays/save.php" class="hol-form">
|
||||||
<input type="hidden" name="idea_id" value="<?= (int)$ideaId ?>">
|
<h3 id="hol-add-title">Ajouter une idée</h3>
|
||||||
<select name="category" required>
|
<input type="hidden" name="action" value="create_idea">
|
||||||
<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">
|
<label>Titre <input type="text" name="title" required></label>
|
||||||
<strong>Fixe:</strong> <?= number_format($fixedTotal, 0, ',', ' ') ?> €
|
|
||||||
• <strong>Par personne:</strong> <?= number_format($ppTotal, 0, ',', ' ') ?> €
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="hol-list">
|
<div class="hol-inline">
|
||||||
<?php foreach ($budget as $b): ?>
|
<label>Pays <input type="text" name="country"></label>
|
||||||
<li>[<?= htmlspecialchars($b['category']) ?>] <?= htmlspecialchars($b['label'] ?? '') ?> —
|
<label>Région <input type="text" name="region"></label>
|
||||||
<?= number_format((float)$b['amount'], 0, ',', ' ') ?> €<?= $b['per_person'] ? ' /pers.' : '' ?>
|
<label>Ville <input type="text" name="city" placeholder="Pour la carte"></label>
|
||||||
</li>
|
</div>
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modale édition idée (pré-remplie via JS) -->
|
<div class="hol-inline">
|
||||||
<div class="hol-modal" id="hol-edit-modal" aria-hidden="true">
|
<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-backdrop"></div>
|
||||||
<div class="hol-dialog" role="dialog" aria-modal="true" aria-labelledby="hol-edit-title">
|
<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">
|
<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>
|
<h3 id="hol-edit-title">Éditer l’idée</h3>
|
||||||
<input type="hidden" name="action" value="update_idea">
|
<input type="hidden" name="action" value="update_idea">
|
||||||
<input type="hidden" name="id" id="edit-id">
|
<input type="hidden" name="id" id="edit-id">
|
||||||
|
|
||||||
<label>Titre
|
<label>Titre <input type="text" name="title" id="edit-title" required></label>
|
||||||
<input type="text" name="title" id="edit-title" required>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="hol-inline">
|
<div class="hol-inline">
|
||||||
<label>Pays <input type="text" name="country" id="edit-country"></label>
|
<label>Pays <input type="text" name="country" id="edit-country"></label>
|
||||||
<label>Région <input type="text" name="region" id="edit-region"></label>
|
<label>Région <input type="text" name="region" id="edit-region"></label>
|
||||||
<label>Ville <input type="text" name="city" id="edit-city"></label>
|
<label>Ville <input type="text" name="city" id="edit-city"></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hol-inline">
|
<div class="hol-inline">
|
||||||
<label>Lat <input type="number" step="0.000001" name="lat" id="edit-lat"></label>
|
<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>
|
<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>
|
<button type="button" class="btn hol-geocode-btn" data-scope="edit">Géocoder</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hol-inline">
|
<div class="hol-inline">
|
||||||
<label>Début <input type="date" name="desired_start_date" id="edit-start"></label>
|
<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>
|
<label>Fin <input type="date" name="desired_end_date" id="edit-end"></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hol-inline">
|
<div class="hol-inline">
|
||||||
<label>Saison <input type="text" name="season_hint" id="edit-season"></label>
|
<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>
|
<label>Durée idéale <input type="number" name="ideal_days" id="edit-days" min="1" step="1"></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label>Statut
|
<label>Statut
|
||||||
<select name="status" id="edit-status">
|
<select name="status" id="edit-status">
|
||||||
<option value="draft">draft</option>
|
<option value="draft">draft</option>
|
||||||
<option value="shortlist">shortlist</option>
|
<option value="shortlist">shortlist</option>
|
||||||
<option value="favorite">favorite</option>
|
<option value="favorite">favorite</option>
|
||||||
<option value="planned">planned</option>
|
<option value="planned">planned</option>
|
||||||
<option value="archived">archived</option>
|
<option value="archived">archived</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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">
|
<div class="hol-actions">
|
||||||
<button type="button" class="hol-cancel">Annuler</button>
|
<button type="button" class="hol-cancel">Annuler</button>
|
||||||
<button type="submit" class="hol-ok">Enregistrer</button>
|
<button type="submit" class="hol-ok">Enregistrer</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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-backdrop"></div>
|
||||||
<div class="hol-dialog hol-dialog--map" role="dialog" aria-modal="true" aria-labelledby="hol-map-title">
|
<div class="hol-dialog hol-dialog--map" role="dialog" aria-modal="true" aria-labelledby="hol-map-title">
|
||||||
<div class="hol-map-header">
|
<div class="hol-map-header">
|
||||||
<h3 id="hol-map-title">Carte des idées</h3>
|
<h3 id="hol-map-title">Carte des idées</h3>
|
||||||
<button class="hol-cancel">Fermer</button>
|
<button class="hol-cancel">Fermer</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="hol-map" style="width: 100%; height: calc(100vh - 140px);"></div>
|
<div id="hol-map" style="width: 100%; height: calc(100vh - 140px);"></div>
|
||||||
</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 src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
<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>
|
</script>
|
||||||
|
<script src="/modules/holidays/holidays.js"></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>
|
|
||||||
+207
-167
@@ -1,194 +1,234 @@
|
|||||||
<?php
|
<?php
|
||||||
|
// modules/holidays/save.php
|
||||||
|
|
||||||
require __DIR__ . '/../../includes/auth.php';
|
require __DIR__ . '/../../includes/auth.php';
|
||||||
require_login('/login.php');
|
require_login('/login.php');
|
||||||
require __DIR__ . '/../../includes/db.php';
|
require __DIR__ . '/../../includes/db.php';
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
http_response_code(405);
|
http_response_code(405);
|
||||||
echo 'Method Not Allowed';
|
echo 'Method Not Allowed';
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$action = $_POST['action'] ?? '';
|
$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
|
* Normalise un décimal saisi (accepte virgule ou point), retourne float|NULL
|
||||||
*/
|
*/
|
||||||
function hol_norm_decimal($v): ?float {
|
function hol_norm_decimal($v): ?float {
|
||||||
if (!isset($v)) return null;
|
if (!isset($v)) return null;
|
||||||
$s = trim((string)$v);
|
$s = trim((string)$v);
|
||||||
if ($s === '') return null;
|
if ($s === '') return null;
|
||||||
$s = str_replace(',', '.', $s);
|
$s = str_replace(',', '.', $s);
|
||||||
return is_numeric($s) ? (float)$s : null;
|
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 {
|
function hol_norm_date($v): ?string {
|
||||||
if (!isset($v)) return null;
|
if (!isset($v)) return null;
|
||||||
$s = trim((string)$v);
|
$s = trim((string)$v);
|
||||||
return $s === '' ? null : $s;
|
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 {
|
try {
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
case 'create_idea': {
|
// --- CRÉATION ---
|
||||||
$status = $_POST['status'] ?? 'draft';
|
case 'create_idea': {
|
||||||
$start = hol_norm_date($_POST['desired_start_date'] ?? null);
|
$title = trim($_POST['title'] ?? '');
|
||||||
$end = hol_norm_date($_POST['desired_end_date'] ?? null);
|
if ($title === '') {
|
||||||
if (!empty($start) && $status === 'draft') { $status = 'planned'; }
|
throw new Exception("Le titre est obligatoire.");
|
||||||
|
}
|
||||||
|
|
||||||
$latVal = hol_norm_decimal($_POST['lat'] ?? null);
|
$status = $_POST['status'] ?? 'draft';
|
||||||
$lngVal = hol_norm_decimal($_POST['lng'] ?? null);
|
$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("
|
$stmt = $pdo->prepare("
|
||||||
INSERT INTO pf_holidays_ideas
|
INSERT INTO pf_holidays_ideas
|
||||||
(title, country, region, city, lat, lng, desired_start_date, desired_end_date, season_hint, ideal_days, status, notes)
|
(title, country, region, city, lat, lng, desired_start_date, desired_end_date, season_hint, ideal_days, status, notes)
|
||||||
VALUES
|
VALUES
|
||||||
(:title,:country,:region,:city,:lat,:lng,:start,:end,:season,:days,:status,:notes)
|
(:title,:country,:region,:city,:lat,:lng,:start,:end,:season,:days,:status,:notes)
|
||||||
");
|
");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
':title' => trim($_POST['title'] ?? ''),
|
':title' => $title,
|
||||||
':country'=> trim($_POST['country'] ?? ''),
|
':country' => trim($_POST['country'] ?? ''),
|
||||||
':region' => trim($_POST['region'] ?? ''),
|
':region' => trim($_POST['region'] ?? ''),
|
||||||
':city' => trim($_POST['city'] ?? ''),
|
':city' => trim($_POST['city'] ?? ''),
|
||||||
':lat' => $latVal,
|
':lat' => hol_norm_decimal($_POST['lat'] ?? null),
|
||||||
':lng' => $lngVal,
|
':lng' => hol_norm_decimal($_POST['lng'] ?? null),
|
||||||
':start' => $start,
|
':start' => $start,
|
||||||
':end' => $end,
|
':end' => $end,
|
||||||
':season' => trim($_POST['season_hint'] ?? ''),
|
':season' => trim($_POST['season_hint'] ?? ''),
|
||||||
':days' => ($_POST['ideal_days'] !== '' ? (int)$_POST['ideal_days'] : null),
|
':days' => ($_POST['ideal_days'] !== '' ? (int)$_POST['ideal_days'] : null),
|
||||||
':status' => $status,
|
':status' => $status,
|
||||||
':notes' => trim($_POST['notes'] ?? ''),
|
':notes' => trim($_POST['notes'] ?? ''),
|
||||||
]);
|
]);
|
||||||
$newId = (int)$pdo->lastInsertId();
|
|
||||||
header("Location: /holidays.php?id={$newId}");
|
// Redirection vers la nouvelle idée
|
||||||
exit;
|
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) {
|
} catch (Throwable $e) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo "Error: " . htmlspecialchars($e->getMessage());
|
// 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
|
<?php
|
||||||
|
// modules/holidays/view.php
|
||||||
|
|
||||||
require __DIR__ . '/../../includes/auth.php';
|
require __DIR__ . '/../../includes/auth.php';
|
||||||
require_login('/login.php');
|
require_login('/login.php');
|
||||||
require __DIR__ . '/../../includes/db.php';
|
require __DIR__ . '/../../includes/db.php';
|
||||||
@@ -6,20 +8,38 @@ require __DIR__ . '/../../includes/db.php';
|
|||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||||
|
|
||||||
if ($id <= 0) {
|
if ($id <= 0) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['error' => 'bad id']);
|
echo json_encode(['error' => 'invalid_id']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$st = $pdo->prepare("SELECT * FROM pf_holidays_ideas WHERE id = ?");
|
try {
|
||||||
$st->execute([$id]);
|
$st = $pdo->prepare("SELECT * FROM pf_holidays_ideas WHERE id = ?");
|
||||||
$it = $st->fetch(PDO::FETCH_ASSOC);
|
$st->execute([$id]);
|
||||||
|
$it = $st->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if (!$it) {
|
if (!$it) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
echo json_encode(['error' => 'not found']);
|
echo json_encode(['error' => 'not_found']);
|
||||||
exit;
|
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