revamp gemini

This commit is contained in:
2026-01-30 23:32:25 +01:00
parent fbc95815d8
commit e248eee882
15 changed files with 3776 additions and 7931 deletions
+46 -132
View File
@@ -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>
<div class="pf-d-flex pf-align-center pf-justify-between pf-mb-4">
<h1>Family Calendar</h1> <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,7 +71,6 @@ require __DIR__ . '/header.php';
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<!-- Rempli par JS -->
</tbody> </tbody>
</table> </table>
</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>
<!-- Le menu contextuel pour le calendrier mensuel --> <div id="fc-month-selectionMenu" class="fc-selection-menu" hidden></div>
<div id="fc-month-selectionMenu" class="fc-selection-menu"></div>
</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 -->
<!-- ALEX -->
<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>
<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>
<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> </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';
?>
+180 -272
View File
@@ -1,42 +1,38 @@
<?php <?php
// modules/gift-list/gift-list.php
// Protection : nécessite d'être connecté
require __DIR__ . '/includes/auth.php'; require __DIR__ . '/includes/auth.php';
require_login('/login.php'); require_login('/login.php');
// Connexion DB
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(); }
// Config page // --- 1. CONFIGURATION & DONNÉES ---
$year = (int)date('Y'); $year = (int)date('Y');
$pageTitle = "PachaFamily - Llista de regals"; $pageTitle = "PachaFamily - Llista de regals";
$activePage = "gift-list"; $activePage = "gift-list";
$bodyClass = "pf-gift-list"; $bodyClass = "pf-gift-list";
$pageCss = "/modules/gift-list/gift-list.css"; $pageCss = "/modules/gift-list/gift-list.css";
require __DIR__ . '/header.php'; // Personnes
// Personne
$baseAdults = ['Laia', 'Laura', 'Avi Iaia']; $baseAdults = ['Laia', 'Laura', 'Avi Iaia'];
$adults = $baseAdults;
$children = ['Pol', 'Pep', 'Elna', 'Bru', 'Guim']; $children = ['Pol', 'Pep', 'Elna', 'Bru', 'Guim'];
// Vues et mapping doccasions // Configuration des Vues
$VIEWS = [ $VIEWS = [
'nadal' => ['TIO', 'NOEL', 'ROIS'], 'nadal' => ['TIO', 'NOEL', 'ROIS'],
'anniversary' => ['ANNIV', 'SANT'], 'anniversary' => ['ANNIV', 'SANT'],
]; ];
// Vue courante (URL > Session > défaut) // Vue courante
$currentView = strtolower($_GET['view'] ?? ($_SESSION['gift_view'] ?? 'nadal')); $currentView = strtolower($_GET['view'] ?? ($_SESSION['gift_view'] ?? 'nadal'));
if (!isset($VIEWS[$currentView])) $currentView = 'nadal'; if (!isset($VIEWS[$currentView])) $currentView = 'nadal';
$_SESSION['gift_view'] = $currentView; $_SESSION['gift_view'] = $currentView;
$allowedOccasions = $VIEWS[$currentView]; $allowedOccasions = $VIEWS[$currentView];
// En vue anniversary, on ajoute 3 adultes pour Pol et Pep uniquement // Logique spécifique : Adultes supplémentaires pour Anniversaires
$extraAdults = ['Pauline', 'Papy JC', 'Mamy Caro']; $extraAdults = ['Pauline', 'Papy JC', 'Mamy Caro'];
$adultsByChildForAnniv = [ $adultsByChildForAnniv = [
'Pol' => array_merge($baseAdults, $extraAdults), 'Pol' => array_merge($baseAdults, $extraAdults),
@@ -46,9 +42,7 @@ $adultsByChildForAnniv = [
'Guim' => $baseAdults, 'Guim' => $baseAdults,
]; ];
// Labels & Icônes
// Labels doccasion (affichage)
$allOccasionLabels = [ $allOccasionLabels = [
'TIO' => 'Tió', 'TIO' => 'Tió',
'NOEL' => 'Nadal', 'NOEL' => 'Nadal',
@@ -56,8 +50,6 @@ $allOccasionLabels = [
'ANNIV' => 'Anniversary', 'ANNIV' => 'Anniversary',
'SANT' => 'Sant', 'SANT' => 'Sant',
]; ];
// Icônes (pense à ajouter les 2 nouvelles images)
$occasionIcons = [ $occasionIcons = [
'TIO' => '/modules/gift-list/assets/img/tio.png', 'TIO' => '/modules/gift-list/assets/img/tio.png',
'NOEL' => '/modules/gift-list/assets/img/santa.png', 'NOEL' => '/modules/gift-list/assets/img/santa.png',
@@ -66,61 +58,99 @@ $occasionIcons = [
'SANT' => '/modules/gift-list/assets/img/sant.png', 'SANT' => '/modules/gift-list/assets/img/sant.png',
]; ];
// Nom de table (renommée -> pf_gifts), fallback si pas encore migrée
$tableGifts = 'pf_gifts'; $tableGifts = 'pf_gifts';
// Récup données filtrées par vue // --- 2. RÉCUPÉRATION DES DONNÉES ---
// Préparation requête
$inMarks = implode(',', array_fill(0, count($allowedOccasions), '?')); $inMarks = implode(',', array_fill(0, count($allowedOccasions), '?'));
$sql = " $sql = "SELECT * FROM {$tableGifts} WHERE year = ? AND occasion IN ($inMarks) ORDER BY adult_name, child_name, occasion, created_at";
SELECT *
FROM {$tableGifts}
WHERE year = ? AND occasion IN ($inMarks)
ORDER BY adult_name, child_name, occasion, created_at
";
$stmt = $pdo->prepare($sql); $stmt = $pdo->prepare($sql);
$params = array_merge([$year], $allowedOccasions); $params = array_merge([$year], $allowedOccasions);
$stmt->execute($params); $stmt->execute($params);
$gifts = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: []; $gifts = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
// Index [occasion][child][adult] // Indexation [occasion][child][adult] pour la matrice
$byOccasion = []; $byOccasion = [];
foreach ($gifts as $gift) { foreach ($gifts as $gift) {
$o = $gift['occasion']; $byOccasion[$gift['occasion']][$gift['child_name']][$gift['adult_name']][] = $gift;
$c = $gift['child_name'];
$a = $gift['adult_name'];
$byOccasion[$o][$c][$a][] = $gift;
} }
// Occasions à afficher pour la vue // Occasions à afficher
$occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels), $allowedOccasions)); $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels), $allowedOccasions));
// --- 3. CALCUL TRICOUNT (Backend) ---
// On pré-calcule ici pour alléger la vue HTML
$people = $baseAdults;
$adultsInDb = array_column($gifts, 'adult_name');
$payersInDb = array_column($gifts, 'payer_name'); // Peut contenir des NULL si colonne vide, mais array_unique gère
// Nettoyage et fusion des participants
$people = array_values(array_unique(array_merge($people, $adultsInDb, $payersInDb)));
$people = array_filter($people); // Enlève les vides éventuels
// Matrice de dettes
$matrix = [];
foreach ($people as $p1) {
foreach ($people as $p2) $matrix[$p1][$p2] = 0.0;
}
foreach ($gifts as $g) {
$adult = $g['adult_name'];
$payer = $g['payer_name'] ?? $g['adult_name'];
$amt = (float)$g['amount'];
// Si payé par quelqu'un d'autre que le bénéficiaire (l'adulte responsable)
if ($amt > 0 && $adult && $payer && $adult !== $payer) {
if (isset($matrix[$adult][$payer])) {
$matrix[$adult][$payer] += $amt;
}
}
}
// Résolution des dettes (Liquidations)
$settlements = [];
$countPeople = count($people);
for ($i = 0; $i < $countPeople; $i++) {
for ($j = $i + 1; $j < $countPeople; $j++) {
$a = $people[$i];
$b = $people[$j];
$net = $matrix[$a][$b] - $matrix[$b][$a];
if ($net > 0.01) {
$settlements[] = ['from' => $a, 'to' => $b, 'amount' => $net];
} elseif ($net < -0.01) {
$settlements[] = ['from' => $b, 'to' => $a, 'amount' => -$net];
}
}
}
// --- 4. DÉBUT DU RENDU HTML ---
require __DIR__ . '/header.php';
?> ?>
<div class="pf-container cl-view-<?= htmlspecialchars($currentView) ?>"> <div class="pf-container cl-view-<?= htmlspecialchars($currentView) ?>">
<div class="cl-titlebar"> <div class="cl-titlebar">
<h1>Llista de regals <?= htmlspecialchars($year) ?></h1> <h1>Llista de regals <?= htmlspecialchars($year) ?></h1>
<div class="cl-view-switch" aria-label="Canvia la vista"> <div class="cl-view-switch" aria-label="Canvia la vista">
<a href="?view=nadal" <a href="?view=nadal" class="cl-view-btn <?= $currentView === 'nadal' ? 'is-active' : '' ?>">Nadal</a>
class="cl-view-btn <?= $currentView === 'nadal' ? 'is-active' : '' ?>">Nadal</a> <a href="?view=anniversary" class="cl-view-btn <?= $currentView === 'anniversary' ? 'is-active' : '' ?>">Anniversary</a>
<a href="?view=anniversary"
class="cl-view-btn <?= $currentView === 'anniversary' ? 'is-active' : '' ?>">Anniversary</a>
</div> </div>
</div> </div>
<!-- VUE TABLEAU PAR FÊTE -->
<section class="pf-section pf-section--panel"> <section class="pf-section pf-section--panel">
<h2>Vista per festa</h2> <h2>Vista per festa</h2>
<?php if (empty($gifts)): ?> <?php if (empty($gifts)): ?>
<p>No hi ha cap regal registrat per a <?= htmlspecialchars($year) ?> en aquesta vista.</p> <p class="cl-legend">No hi ha cap regal registrat per a <?= htmlspecialchars($year) ?> en aquesta vista.</p>
<?php endif; ?> <?php endif; ?>
<?php foreach ($occasionsToShow as $occCode): ?> <?php foreach ($occasionsToShow as $occCode): ?>
<div class="cl-occasion-block"> <div class="cl-occasion-block">
<h3 class="cl-occasion-title"> <h3 class="cl-occasion-title">
<?php if (!empty($occasionIcons[$occCode])): ?> <?php if (!empty($occasionIcons[$occCode])): ?>
<img class="cl-occasion-icon" <img class="cl-occasion-icon" src="<?= htmlspecialchars($occasionIcons[$occCode]) ?>" alt="" aria-hidden="true">
src="<?= htmlspecialchars($occasionIcons[$occCode]) ?>"
alt=""
aria-hidden="true">
<?php endif; ?> <?php endif; ?>
<?= htmlspecialchars($allOccasionLabels[$occCode] ?? $occCode) ?> <?= htmlspecialchars($allOccasionLabels[$occCode] ?? $occCode) ?>
</h3> </h3>
@@ -128,6 +158,7 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
<div class="cl-occasion-children-tables"> <div class="cl-occasion-children-tables">
<?php foreach ($children as $childName): ?> <?php foreach ($children as $childName): ?>
<?php <?php
// Détermine quels adultes participent pour cet enfant
$adultsForChild = ($currentView === 'anniversary') $adultsForChild = ($currentView === 'anniversary')
? ($adultsByChildForAnniv[$childName] ?? $baseAdults) ? ($adultsByChildForAnniv[$childName] ?? $baseAdults)
: $baseAdults; : $baseAdults;
@@ -137,12 +168,14 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
$totals = []; $totals = [];
foreach ($adultsForChild as $adultName) { foreach ($adultsForChild as $adultName) {
$lists[$adultName] = $byOccasion[$occCode][$childName][$adultName] ?? []; $lists[$adultName] = $byOccasion[$occCode][$childName][$adultName] ?? [];
$totals[$adultName] = array_sum(array_map(fn($g) => (float)$g['amount'], $lists[$adultName])); $totals[$adultName] = array_sum(array_column($lists[$adultName], 'amount'));
} }
// max() compatible PHP 8
// Calcul hauteur tableau
$counts = array_map('count', $lists); $counts = array_map('count', $lists);
$maxRowsChild = !empty($counts) ? max($counts) : 0; $maxRowsChild = !empty($counts) ? max($counts) : 0;
?> ?>
<table class="cl-child-table child-<?= strtolower($childName) ?>"> <table class="cl-child-table child-<?= strtolower($childName) ?>">
<colgroup> <colgroup>
<?php foreach ($adultsForChild as $_): ?> <?php foreach ($adultsForChild as $_): ?>
@@ -152,15 +185,13 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
<caption> <caption>
<?= htmlspecialchars($childName) ?> <?= htmlspecialchars($childName) ?>
<button <button type="button" class="cl-child-add-btn" title="Afegeix un regal"
type="button"
class="cl-child-add-btn"
title="Afegeix un regal"
data-year="<?= $year ?>" data-year="<?= $year ?>"
data-child="<?= htmlspecialchars($childName) ?>" data-child="<?= htmlspecialchars($childName) ?>"
data-occasion="<?= htmlspecialchars($occCode) ?>" data-occasion="<?= htmlspecialchars($occCode) ?>"
data-adults="<?= htmlspecialchars(json_encode($adultsForChild), ENT_QUOTES) ?>" data-adults="<?= htmlspecialchars(json_encode(array_values($adultsForChild)), ENT_QUOTES) ?>">
>+</button> +
</button>
</caption> </caption>
<thead> <thead>
@@ -169,9 +200,7 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
<th> <th>
<div class="cl-th-inner"> <div class="cl-th-inner">
<span class="cl-th-label"><?= htmlspecialchars($adultName) ?></span> <span class="cl-th-label"><?= htmlspecialchars($adultName) ?></span>
<span class="cl-summary-adult-total"> <span class="cl-summary-adult-total"><?= number_format($totals[$adultName], 0, ',', ' ') ?> €</span>
<?= number_format($totals[$adultName], 0, ',', ' ') ?> €
</span>
</div> </div>
</th> </th>
<?php endforeach; ?> <?php endforeach; ?>
@@ -206,14 +235,11 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
<?php else: ?> <?php else: ?>
<span class="cl-gift-desc"><?= $desc ?></span> <span class="cl-gift-desc"><?= $desc ?></span>
<?php endif; ?> <?php endif; ?>
<div class="cl-gift-right"> <div class="cl-gift-right">
<span class="cl-gift-amount">(<?= number_format($amt, 0, ',', ' ') ?> €)</span> <span class="cl-gift-amount">(<?= number_format($amt, 0, ',', ' ') ?> €)</span>
<span class="cl-gift-actions"> <span class="cl-gift-actions">
<button <button type="button" class="cl-gift-action-btn cl-gift-edit" aria-label="Edita"
type="button"
class="cl-gift-action-btn cl-gift-edit"
title="Edita"
aria-label="Edita"
data-id="<?= $giftId ?>" data-id="<?= $giftId ?>"
data-year="<?= $year ?>" data-year="<?= $year ?>"
data-child="<?= htmlspecialchars($childName) ?>" data-child="<?= htmlspecialchars($childName) ?>"
@@ -222,31 +248,18 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
data-payer="<?= htmlspecialchars($payer) ?>" data-payer="<?= htmlspecialchars($payer) ?>"
data-desc="<?= htmlspecialchars($gift['gift_description']) ?>" data-desc="<?= htmlspecialchars($gift['gift_description']) ?>"
data-amount="<?= htmlspecialchars($gift['amount']) ?>" data-amount="<?= htmlspecialchars($gift['amount']) ?>"
data-link="<?= htmlspecialchars($gift['product_link'] ?? '') ?>" data-link="<?= htmlspecialchars($gift['product_link'] ?? '') ?>">
>
<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor" aria-hidden="true">
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1.003 1.003 0 0 0 0-1.42l-2.34-2.34a1.003 1.003 0 0 0-1.42 0l-1.83 1.83 3.75 3.75 1.84-1.82z"/>
</svg>
</button> </button>
<!-- Delete --> <button type="button" class="cl-gift-action-btn cl-gift-delete" aria-label="Eliminar" data-id="<?= $giftId ?>">
<button ×
type="button"
class="cl-gift-action-btn cl-gift-delete"
title="Eliminar"
aria-label="Eliminar"
data-id="<?= $giftId ?>"
>
<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor" aria-hidden="true">
<path d="M9 3h6a1 1 0 0 1 1 1v2h3a1 1 0 1 1 0 2h-1l-1 12a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 8H4a1 1 0 1 1 0-2h3V4a1 1 0 0 1 1-1zm-1 5h2v10H8V8zm4 0h2v10h-2V8z"/>
</svg>
</button> </button>
</span> </span>
</div> </div>
</div> </div>
<?php if (!empty($payer) && $payer !== $gift['adult_name']): ?> <?php if (!empty($payer) && $payer !== $gift['adult_name']): ?>
<small style="color:#b91c1c; font-style:italic;"> <small style="color:#b91c1c; font-style:italic; display:block; font-size:0.75em;">(pagat per <?= htmlspecialchars($payer) ?>)</small>
(pagat per <?= htmlspecialchars($payer) ?>)
</small>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php else: ?> <?php else: ?>
@@ -260,24 +273,16 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
</tbody> </tbody>
</table> </table>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</section> </section>
<!-- RÉSUMÉ DU BUDGET (AGGRÉGÉ) -->
<section class="pf-section pf-section--panel"> <section class="pf-section pf-section--panel">
<h2>Resum del pressupost</h2> <h2>Resum del pressupost</h2>
<?php <?php
$sqlSum = " // Calcul agrégé SQL pour vérification
SELECT adult_name, child_name, occasion, SUM(amount) AS total $stmtSum = $pdo->prepare("SELECT adult_name, child_name, occasion, SUM(amount) AS total FROM {$tableGifts} WHERE year = ? AND occasion IN ($inMarks) GROUP BY adult_name, child_name, occasion ORDER BY adult_name, child_name, occasion");
FROM {$tableGifts}
WHERE year = ? AND occasion IN ($inMarks)
GROUP BY adult_name, child_name, occasion
ORDER BY adult_name, child_name, occasion
";
$stmtSum = $pdo->prepare($sqlSum);
$stmtSum->execute($params); $stmtSum->execute($params);
$sums = $stmtSum->fetchAll(PDO::FETCH_ASSOC); $sums = $stmtSum->fetchAll(PDO::FETCH_ASSOC);
?> ?>
@@ -300,47 +305,13 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
</div> </div>
</section> </section>
<!-- TRICOUNT (dépend uniquement de $gifts déjà filtré) -->
<?php
$people = $baseAdults;
$adultsInDb = array_values(array_unique(array_column($gifts, 'adult_name')));
$payersInDb = array_values(array_unique(array_column($gifts, 'payer_name')));
$people = array_values(array_unique(array_merge($people, $adultsInDb, $payersInDb)));
$matrix = [];
foreach ($people as $p1) {
foreach ($people as $p2) {
if (!isset($matrix[$p1])) $matrix[$p1] = [];
$matrix[$p1][$p2] = 0.0;
}
}
foreach ($gifts as $g) {
$adult = $g['adult_name'];
$payer = $g['payer_name'] ?? $g['adult_name'];
$amt = (float)$g['amount'];
if ($amt > 0 && $adult !== $payer) {
$matrix[$adult][$payer] += $amt;
}
}
$settlements = [];
for ($i = 0; $i < count($people); $i++) {
for ($j = $i + 1; $j < count($people); $j++) {
$a = $people[$i]; $b = $people[$j];
$net = $matrix[$a][$b] - $matrix[$b][$a];
if ($net > 0.009) $settlements[] = [$a, $b, $net];
elseif ($net < -0.009) $settlements[] = [$b, $a, -$net];
}
}
?>
<section class="pf-section pf-section--panel"> <section class="pf-section pf-section--panel">
<h2>Tricount</h2> <h2>Tricount</h2>
<div class="cl-budget-wrapper"> <div class="cl-budget-wrapper">
<table class="pf-table pf-table--compact cl-debt-matrix"> <table class="pf-table pf-table--compact cl-debt-matrix">
<thead> <thead>
<tr> <tr>
<th class="cl-matrix-corner"> <th class="cl-matrix-corner"><span>Deutor ↓</span><span>Creditor →</span></th>
<span>Deutor ↓</span><span>Creditor →</span>
</th>
<?php foreach ($people as $p): ?><th><?= htmlspecialchars($p) ?></th><?php endforeach; ?> <?php foreach ($people as $p): ?><th><?= htmlspecialchars($p) ?></th><?php endforeach; ?>
</tr> </tr>
</thead> </thead>
@@ -352,8 +323,8 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
<?php <?php
$val = $matrix[$debtor][$creditor] ?? 0; $val = $matrix[$debtor][$creditor] ?? 0;
$isDiag = ($debtor === $creditor); $isDiag = ($debtor === $creditor);
$display = $isDiag || $val == 0 ? '—' : number_format($val, 0, ',', ' ') . ' €';
$cls = $isDiag ? 'cl-mtx-diag' : ($val > 0 ? 'cl-mtx-owe' : 'cl-mtx-empty'); $cls = $isDiag ? 'cl-mtx-diag' : ($val > 0 ? 'cl-mtx-owe' : 'cl-mtx-empty');
$display = $isDiag || $val == 0 ? '—' : number_format($val, 0, ',', ' ') . ' €';
?> ?>
<td class="<?= $cls ?>"><?= $display ?></td> <td class="<?= $cls ?>"><?= $display ?></td>
<?php endforeach; ?> <?php endforeach; ?>
@@ -362,19 +333,19 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
</tbody> </tbody>
</table> </table>
</div> </div>
<h3 style="margin-top:10px;">Liquidacions</h3>
<h3 style="margin-top:16px; font-size:1.1rem; color:#374151;">Liquidacions</h3>
<?php if (empty($settlements)): ?> <?php if (empty($settlements)): ?>
<p class="cl-legend">Cap deute pendent.</p> <p class="cl-legend">Cap deute pendent.</p>
<?php else: ?> <?php else: ?>
<ul> <ul class="hol-list">
<?php foreach ($settlements as [$from, $to, $amt]): ?> <?php foreach ($settlements as $s): ?>
<li><?= htmlspecialchars($from) ?> ha de pagar <?= number_format($amt, 0, ',', ' ') ?> € a <?= htmlspecialchars($to) ?></li> <li><strong><?= htmlspecialchars($s['from']) ?></strong> ha de pagar <strong><?= number_format($s['amount'], 2, ',', ' ') ?> €</strong> a <?= htmlspecialchars($s['to']) ?></li>
<?php endforeach; ?> <?php endforeach; ?>
</ul> </ul>
<?php endif; ?> <?php endif; ?>
</section> </section>
<!-- LISTE DÉTAILLÉE -->
<section class="pf-section pf-section--panel"> <section class="pf-section pf-section--panel">
<h2>Llista detallada de regals</h2> <h2>Llista detallada de regals</h2>
<div class="cl-detail-wrapper"> <div class="cl-detail-wrapper">
@@ -403,196 +374,133 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
</section> </section>
</div> </div>
<!-- JS inline : modale d'ajout/édition -->
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
// Références modale
const modal = document.getElementById('cl-gift-modal'); const modal = document.getElementById('cl-gift-modal');
const backdrop = modal ? modal.querySelector('.cl-modal-backdrop') : null; const backdrop = modal ? modal.querySelector('.cl-modal-backdrop') : null;
const cancelBtn = modal ? modal.querySelector('.clm-cancel') : null; const cancelBtn = modal ? modal.querySelector('.clm-cancel') : null;
function openModal() { if (modal) modal.classList.add('cl-open'); } function toggleModal(show) {
function closeModal() { if (modal) modal.classList.remove('cl-open'); } if (!modal) return;
modal.classList.toggle('cl-open', show);
}
// Helpers // Gestion des options de select (Adultes dynamiques selon l'enfant)
function setOptions(select, values) { function populateSelects(adults) {
if (!select) return; const selects = [document.getElementById('clm-adult'), document.getElementById('clm-payer')];
select.innerHTML = ''; selects.forEach(sel => {
values.forEach(v => { if (!sel) return;
sel.innerHTML = '';
adults.forEach(name => {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = v; opt.value = name;
opt.textContent = v; opt.textContent = name;
select.appendChild(opt); sel.appendChild(opt);
});
}); });
} }
function ensureOption(select, value) { // --- BOUTON AJOUT (+) ---
if (!select || !value) return; document.querySelectorAll('.cl-child-add-btn').forEach(btn => {
const exists = Array.from(select.options).some(o => o.value === value); btn.addEventListener('click', () => {
if (!exists) { const d = btn.dataset;
const opt = document.createElement('option'); let adults = [];
opt.value = value; try { adults = JSON.parse(d.adults || '[]'); } catch(e) {}
opt.textContent = value;
select.appendChild(opt);
}
}
// Ouverture modale en mode création (bouton "+") populateSelects(adults);
document.querySelectorAll('.cl-child-add-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
const year = btn.getAttribute('data-year');
const child = btn.getAttribute('data-child');
const occasion = btn.getAttribute('data-occasion');
// Liste d'adultes spécifique à l'enfant (si fournie) // Valeurs par défaut
let allowedAdults = [];
const adultsAttr = btn.getAttribute('data-adults');
if (adultsAttr) {
try { allowedAdults = JSON.parse(adultsAttr); } catch (e) { allowedAdults = []; }
}
openModal();
// Mode création
document.getElementById('clm-action').value = 'create'; document.getElementById('clm-action').value = 'create';
document.getElementById('clm-id').value = ''; document.getElementById('clm-id').value = '';
document.getElementById('clm-year').value = d.year;
document.getElementById('clm-child').value = d.child;
document.getElementById('clm-occasion').value = d.occasion;
// Contexte
document.getElementById('clm-year').value = year || '';
document.getElementById('clm-child').value = child || '';
document.getElementById('clm-occasion').value = occasion || '';
// Sélecteurs Adult/Payer
const adultSelect = document.getElementById('clm-adult');
const payerSelect = document.getElementById('clm-payer');
if (allowedAdults.length > 0) {
setOptions(adultSelect, allowedAdults);
setOptions(payerSelect, allowedAdults);
}
if (adultSelect && adultSelect.options.length > 0) {
adultSelect.selectedIndex = 0;
if (payerSelect) payerSelect.value = adultSelect.value;
}
// Reset des champs
document.getElementById('clm-gift').value = ''; document.getElementById('clm-gift').value = '';
document.getElementById('clm-amount').value = ''; document.getElementById('clm-amount').value = '';
document.getElementById('clm-link').value = ''; document.getElementById('clm-link').value = '';
// Titre document.getElementById('cl-modal-title').textContent = `Afegeix un regal per ${d.child}`;
document.getElementById('cl-modal-title').textContent = `Afegeix un regal per ${child}`; toggleModal(true);
}); });
}); });
// Ouverture modale en mode édition (icône crayon) // --- BOUTON ÉDITION (Crayon) ---
document.querySelectorAll('.cl-gift-edit').forEach(function (btn) { document.body.addEventListener('click', (e) => {
btn.addEventListener('click', function () { const btn = e.target.closest('.cl-gift-edit');
if (!btn) return;
const d = btn.dataset; const d = btn.dataset;
// Pour l'édition, on s'assure que l'adulte actuel est dans la liste (même si pas standard)
openModal();
// Mode édition
document.getElementById('clm-action').value = 'update';
document.getElementById('clm-id').value = d.id || '';
// Contexte
document.getElementById('clm-year').value = d.year || '';
document.getElementById('clm-child').value = d.child || '';
document.getElementById('clm-occasion').value = d.occasion || '';
// Sélecteurs Adult/Payer garantir l'option si absente
const adultSelect = document.getElementById('clm-adult'); const adultSelect = document.getElementById('clm-adult');
const payerSelect = document.getElementById('clm-payer'); const payerSelect = document.getElementById('clm-payer');
ensureOption(adultSelect, d.adult);
ensureOption(payerSelect, d.payer || d.adult);
if (adultSelect) adultSelect.value = d.adult || ''; // On ne recharge pas toute la liste adults ici car complexe à récupérer du DOM,
if (payerSelect) payerSelect.value = d.payer || d.adult || ''; // on ajoute juste l'option si manquante.
[adultSelect, payerSelect].forEach(sel => {
// Champs const val = (sel === adultSelect) ? d.adult : (d.payer || d.adult);
document.getElementById('clm-gift').value = d.desc || ''; if (!Array.from(sel.options).some(o => o.value === val)) {
document.getElementById('clm-amount').value = d.amount || ''; const opt = document.createElement('option');
document.getElementById('clm-link').value = d.link || ''; opt.value = val;
opt.textContent = val;
// Titre sel.appendChild(opt);
document.getElementById('cl-modal-title').textContent = `Edita un regal per ${d.child || ''}`; }
}); sel.value = val;
}); });
// Suppression (icône poubelle) document.getElementById('clm-action').value = 'update';
const deleteForm = document.getElementById('cl-delete-form'); document.getElementById('clm-id').value = d.id;
const deleteIdInput = document.getElementById('cld-id'); document.getElementById('clm-year').value = d.year;
document.querySelectorAll('.cl-gift-delete').forEach(function (btn) { document.getElementById('clm-child').value = d.child;
btn.addEventListener('click', function () { document.getElementById('clm-occasion').value = d.occasion;
const giftId = btn.getAttribute('data-id');
if (!giftId) return; document.getElementById('clm-gift').value = d.desc;
document.getElementById('clm-amount').value = d.amount;
document.getElementById('clm-link').value = d.link;
document.getElementById('cl-modal-title').textContent = `Edita el regal`;
toggleModal(true);
});
// --- SUPPRESSION ---
document.body.addEventListener('click', (e) => {
const btn = e.target.closest('.cl-gift-delete');
if (!btn) return;
if (confirm('Vols eliminar aquest regal?')) { if (confirm('Vols eliminar aquest regal?')) {
if (deleteIdInput) deleteIdInput.value = giftId; const form = document.getElementById('cl-delete-form');
if (deleteForm) deleteForm.submit(); document.getElementById('cld-id').value = btn.dataset.id;
form.submit();
} }
}); });
});
// Fermeture modale // Fermeture
if (cancelBtn) cancelBtn.addEventListener('click', closeModal); if(cancelBtn) cancelBtn.addEventListener('click', () => toggleModal(false));
if (backdrop) backdrop.addEventListener('click', closeModal); if(backdrop) backdrop.addEventListener('click', () => toggleModal(false));
document.addEventListener('keydown', function (e) { document.addEventListener('keydown', (e) => { if(e.key === 'Escape') toggleModal(false); });
if (e.key === 'Escape') closeModal();
});
// Toggle mobile/hover des actions sur la ligne cadeau
document.querySelectorAll('.cl-gift-right').forEach(function (zone) {
zone.addEventListener('click', function (e) {
if (e.target.closest('.cl-gift-action-btn')) return;
document.querySelectorAll('.cl-gift-right.is-active').forEach(function (z) {
if (z !== zone) z.classList.remove('is-active');
});
zone.classList.toggle('is-active');
});
});
// Fermer les menus d'actions si clic en dehors
document.addEventListener('click', function (e) {
if (!e.target.closest('.cl-gift-right')) {
document.querySelectorAll('.cl-gift-right.is-active').forEach(function (z) {
z.classList.remove('is-active');
});
}
}, true);
}); });
</script> </script>
<!-- Modal d'ajout de cadeau -->
<div id="cl-gift-modal" class="cl-modal" aria-hidden="true"> <div id="cl-gift-modal" class="cl-modal" aria-hidden="true">
<div class="cl-modal-backdrop"></div> <div class="cl-modal-backdrop"></div>
<div class="cl-modal-dialog" role="dialog" aria-modal="true" aria-labelledby="cl-modal-title"> <div class="cl-modal-dialog" role="dialog" aria-modal="true" aria-labelledby="cl-modal-title">
<form method="post" action="/modules/gift-list/save-gift.php" class="cl-modal-form"> <form method="post" action="/modules/gift-list/save-gift.php" class="cl-modal-form">
<h3 id="cl-modal-title">Afegeix un regal</h3> <h3 id="cl-modal-title">Regal</h3>
<input type="hidden" name="year" id="clm-year" value="<?= $year ?>"> <input type="hidden" name="year" id="clm-year">
<input type="hidden" name="child_name" id="clm-child" value=""> <input type="hidden" name="child_name" id="clm-child">
<input type="hidden" name="occasion" id="clm-occasion" value=""> <input type="hidden" name="occasion" id="clm-occasion">
<input type="hidden" name="action" id="clm-action" value="create"> <input type="hidden" name="action" id="clm-action">
<input type="hidden" name="gift_id" id="clm-id" value=""> <input type="hidden" name="gift_id" id="clm-id">
<label class="clm-label"> <label class="clm-label">
Adult Adult
<select name="adult_name" id="clm-adult" required> <select name="adult_name" id="clm-adult" required></select>
<?php foreach ($adults as $adultName): ?>
<option value="<?= htmlspecialchars($adultName) ?>"><?= htmlspecialchars($adultName) ?></option>
<?php endforeach; ?>
</select>
</label> </label>
<label class="clm-label"> <label class="clm-label">
Pagat per Pagat per
<select name="payer_name" id="clm-payer" required> <select name="payer_name" id="clm-payer" required></select>
<?php foreach ($adults as $adultName): ?>
<option value="<?= htmlspecialchars($adultName) ?>"><?= htmlspecialchars($adultName) ?></option>
<?php endforeach; ?>
</select>
</label> </label>
<label class="clm-label"> <label class="clm-label">
@@ -602,17 +510,17 @@ document.addEventListener('DOMContentLoaded', function () {
<label class="clm-label"> <label class="clm-label">
Preu (€) Preu (€)
<input type="number" name="amount" id="clm-amount" placeholder="p. ex., 49,99" step="0.01" min="0"> <input type="number" name="amount" id="clm-amount" placeholder="49.99" step="0.01" min="0">
</label> </label>
<label class="clm-label"> <label class="clm-label">
Enllaç (opcional) Enllaç (opcional)
<input type="url" name="product_link" id="clm-link" placeholder="https://exemple.com/producte"> <input type="url" name="product_link" id="clm-link" placeholder="https://...">
</label> </label>
<div class="cl-modal-actions"> <div class="cl-modal-actions">
<button type="button" class="clm-cancel">Cancel·la</button> <button type="button" class="clm-cancel">Cancel·la</button>
<button type="submit" class="clm-ok">OK</button> <button type="submit" class="clm-ok">Guardar</button>
</div> </div>
</form> </form>
</div> </div>
@@ -624,4 +532,4 @@ document.addEventListener('DOMContentLoaded', function () {
<input type="hidden" name="gift_id" id="cld-id" value=""> <input type="hidden" name="gift_id" id="cld-id" value="">
</form> </form>
<?php require __DIR__ . '/footer.php'; <?php require __DIR__ . '/footer.php'; ?>
+5
View File
@@ -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,102 +1,93 @@
<?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']);
echo json_encode(['status' => 'success', 'message' => 'Événement supprimé.']);
} elseif ($action === 'update') {
// Mise à jour d'un seul événement
if (!isset($input['event_id'], $input['new_type'])) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'ID ou nouveau type manquant pour la mise à jour.']);
exit; exit;
} }
$eventId = (int)$input['event_id']; // --- MISE À JOUR UNITAIRE ---
$newType = $input['new_type']; if ($action === 'update') {
$eventId = (int)($input['event_id'] ?? 0);
$newType = $input['new_type'] ?? '';
if ($eventId <= 0 || !$newType) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Données manquantes.']);
exit;
}
$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.']);
} 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; exit;
} }
$eventIds = array_map('intval', $input['event_ids']); // --- SUPPRESSION DE MASSE (Par date et type) ---
$eventIds = array_filter($eventIds, fn($id) => $id > 0); // 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($eventIds)) { if (empty($dates) || empty($types)) {
http_response_code(400); echo json_encode(['status' => 'success']); // Rien à faire
echo json_encode(['status' => 'error', 'message' => 'Aucun ID valide pour bulk_delete.']);
exit; exit;
} }
$placeholders = implode(',', array_fill(0, count($eventIds), '?')); // Création des placeholders IN (?,?,?)
$stmt = $pdo->prepare("DELETE FROM pf_events WHERE id IN ($placeholders)"); $datePlaceholders = implode(',', array_fill(0, count($dates), '?'));
$stmt->execute($eventIds); $typePlaceholders = implode(',', array_fill(0, count($types), '?'));
echo json_encode(['status' => 'success', 'message' => 'Événements supprimés en masse.']); $sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders) AND event_type IN ($typePlaceholders)";
$stmt = $pdo->prepare($sql);
} elseif ($action === 'bulk_update') { // Fusion des tableaux pour l'exécution
// Mise à jour en masse $stmt->execute(array_merge($dates, $types));
if (empty($input['event_ids']) || !is_array($input['event_ids']) || !isset($input['new_type'])) {
http_response_code(400); echo json_encode(['status' => 'success']);
echo json_encode(['status' => 'error', 'message' => 'event_ids ou new_type manquant pour bulk_update.']);
exit; exit;
} }
$eventIds = array_map('intval', $input['event_ids']); // --- SUPPRESSION TOTALE SUR DES DATES ---
$eventIds = array_filter($eventIds, fn($id) => $id > 0); if ($action === 'bulk_delete_all') {
$newType = $input['new_type']; $dates = $input['dates'] ?? [];
if (empty($dates)) {
if (empty($eventIds)) { echo json_encode(['status' => 'success']);
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Aucun ID valide pour bulk_update.']);
exit; exit;
} }
$placeholders = implode(',', array_fill(0, count($eventIds), '?')); $datePlaceholders = implode(',', array_fill(0, count($dates), '?'));
$params = array_merge([$newType], $eventIds); $sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders)";
$stmt = $pdo->prepare($sql);
$stmt->execute($dates);
$stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id IN ($placeholders)"); echo json_encode(['status' => 'success']);
$stmt->execute($params); exit;
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.']);
} }
// 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()]);
File diff suppressed because it is too large Load Diff
+55 -30
View File
@@ -1,42 +1,69 @@
<?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;
if ($action === 'delete') {
$gift_id = (int)($_POST['gift_id'] ?? 0);
if ($gift_id > 0) {
$stmt = $pdo->prepare("DELETE FROM pf_gift_gifts WHERE id = :id");
$stmt->execute(['id' => $gift_id]);
} }
header('Location: /gift-list.php'); // 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 ($gift_id > 0) {
$stmt = $pdo->prepare("DELETE FROM {$tableName} WHERE id = :id");
$stmt->execute(['id' => $gift_id]);
}
header("Location: $redirectUrl");
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)
header("Location: $redirectUrl");
exit;
}
// --- UPDATE ---
if ($action === 'update' && $gift_id > 0) {
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
UPDATE pf_gift_gifts UPDATE {$tableName}
SET year = :year, SET year = :year,
adult_name = :adult_name, adult_name = :adult_name,
payer_name = :payer_name, payer_name = :payer_name,
@@ -55,19 +82,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,
]); ]);
} }
// --- CREATE ---
header('Location: /gift-list.php'); else {
exit;
}
// create (par défaut)
if ($adult_name && $payer_name && $child_name && $occasion && $gift_desc) {
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO pf_gift_gifts 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;
+61 -19
View File
@@ -6,23 +6,43 @@ 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)) {
@@ -31,15 +51,14 @@ try {
'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',
@@ -56,7 +75,8 @@ curl_setopt_array($ch, [
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.
// Important : Nominatim demande une identification claire.
'User-Agent: PachaFamily-Holidays/1.0 (+contact: ferlan.alexandre@gmail.com)' 'User-Agent: PachaFamily-Holidays/1.0 (+contact: ferlan.alexandre@gmail.com)'
], ],
]); ]);
@@ -65,35 +85,56 @@ $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([
'error' => 'geocode_failed',
'details' => $err ?: ('HTTP ' . $http)
]);
exit; 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)) {
// On renvoie 200 avec une liste vide ou 404, le JS gère les deux.
// 404 est plus sémantique "Not Found".
http_response_code(404); http_response_code(404);
echo json_encode(['error' => 'not_found']); echo json_encode(['error' => 'not_found']);
exit; exit;
} }
if ($limit === 1) { // 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]; $r = $data[0];
$lat = round((float)$r['lat'], 6); $lat = round((float)$r['lat'], 6);
$lng = round((float)$r['lon'], 6); $lng = round((float)$r['lon'], 6);
$display = $r['display_name'] ?? null; $display = $r['display_name'] ?? '';
try { try {
$st = $pdo->prepare("REPLACE INTO pf_geocode_cache (q_hash, q, lat, lng, display_name) VALUES (?, ?, ?, ?, ?)"); $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]); $st->execute([$qHash, $q, $lat, $lng, $display]);
} catch (Throwable $e) {} } catch (Throwable $e) {}
echo json_encode(['lat' => $lat, 'lng' => $lng, 'display_name' => $display]);
exit;
} }
// Multi-résultats // 7. Retour des résultats
// Si limit=1, on renvoie format plat (pour compatibilité stricte)
// Si limit>1, on renvoie format liste
if ($limit === 1) {
$r = $data[0];
echo json_encode([
'lat' => round((float)$r['lat'], 6),
'lng' => round((float)$r['lon'], 6),
'display_name' => $r['display_name'] ?? ''
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} else {
$results = array_map(function ($r) { $results = array_map(function ($r) {
return [ return [
'lat' => round((float)$r['lat'], 6), 'lat' => round((float)$r['lat'], 6),
@@ -103,3 +144,4 @@ $results = array_map(function ($r) {
}, $data); }, $data);
echo json_encode(['results' => $results], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); echo json_encode(['results' => $results], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
File diff suppressed because it is too large Load Diff
+205 -213
View File
@@ -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, "&amp;")
}; .replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// --- 2. GESTION MODALE "AJOUTER" ---
const addModalCtrl = setupModal("hol-add-modal");
const addBtn = document.getElementById("hol-add-open");
if (addBtn && addModalCtrl) {
addBtn.addEventListener("click", addModalCtrl.open);
}
// --- 3. GESTION MODALE "ÉDITER" ---
const editModalCtrl = setupModal("hol-edit-modal");
// Fonction pour charger et ouvrir l'édition via AJAX
async function openEditForId(id) { 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 lidé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);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeEditModal();
});
// Boutons "Éditer" des cards (liste/planifiées/archivées)
document.querySelectorAll(".btn-edit[data-edit-id]").forEach((btn) => {
btn.addEventListener("click", () => {
const id = btn.getAttribute("data-edit-id"); const id = btn.getAttribute("data-edit-id");
if (id) openEditForId(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);
}
}); });
// Bouton "Éditer" sur la page détail (en-tête) // --- 4. GESTION SUPPRESSION ---
const headerEditBtn = document.getElementById("hol-edit-open"); document.body.addEventListener("click", (e) => {
if (headerEditBtn) { const btn = e.target.closest(".btn-delete");
headerEditBtn.addEventListener("click", () => { if (btn && btn.hasAttribute("data-del-id")) {
const id = headerEditBtn.getAttribute("data-edit-id");
if (id) openEditForId(id);
});
}
}
// --- Suppression (liste + planifiées + page détail)
document.querySelectorAll(".btn-delete[data-del-id]").forEach((btn) => {
btn.addEventListener("click", () => {
const id = btn.getAttribute("data-del-id"); 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 ---
const mapBtn = document.getElementById("hol-map-open");
const mapModal = document.getElementById("hol-map-modal");
if (mapBtn && mapModal) {
const backdrop = mapModal.querySelector(".hol-backdrop");
const cancel = mapModal.querySelector(".hol-cancel");
const open = () => mapModal.classList.add("open");
const close = () => mapModal.classList.remove("open");
let mapInitialized = false; let mapInitialized = false;
let map; let mapInstance;
// Fonction d'initialisation de la carte (appelée à l'ouverture de la modale)
function initMap() { function initMap() {
// Évite double initialisation if (mapInitialized) {
if (mapInitialized) return; // Si déjà init, on force juste le redimensionnement pour éviter les bugs d'affichage
mapInitialized = true; setTimeout(() => mapInstance.invalidateSize(), 200);
// 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; return;
} }
// Init carte une seule fois (ne pas faire ça dans la boucle) if (typeof L === "undefined") {
map = L.map("hol-map", { scrollWheelZoom: true }); 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", { L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap", attribution: "© OpenStreetMap",
}).addTo(map); }).addTo(mapInstance);
// Corrige la taille après affichage de la modale
setTimeout(() => map.invalidateSize(), 0);
// Ajout des marqueurs
const markers = []; const markers = [];
MAP_DATA.forEach((it) => { MAP_DATA.forEach((it) => {
const lat = parseFloat(it.lat); const lat = parseFloat(it.lat);
const lng = parseFloat(it.lng); const lng = parseFloat(it.lng);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return; if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
const color = // Code couleur selon statut
it.status === "planned" const colors = {
? "#16a34a" planned: "#16a34a",
: it.status === "favorite" favorite: "#f59e0b",
? "#f59e0b" shortlist: "#3b82f6",
: it.status === "shortlist" default: "#6b7280",
? "#3b82f6" };
: "#6b7280"; const color = colors[it.status] || colors.default;
const m = L.circleMarker([lat, lng], { const m = L.circleMarker([lat, lng], {
radius: 6, radius: 7,
color, color: "#ffffff",
weight: 1,
fillColor: color, fillColor: color,
fillOpacity: 0.85, fillOpacity: 0.9,
}).addTo(map); }).addTo(mapInstance);
// Construction du popup
const loc = [it.city, it.region, it.country].filter(Boolean).join(", "); const loc = [it.city, it.region, it.country].filter(Boolean).join(", ");
const dates = it.desired_start_date const dates = it.desired_start_date
? `${it.desired_start_date}${it.desired_end_date ? " → " + it.desired_end_date : ""}` ? `${it.desired_start_date}${it.desired_end_date ? " → " + it.desired_end_date : ""}`
: ""; : null;
m.bindPopup(` m.bindPopup(`
<strong>${esc(it.title || "")}</strong><br/> <div class="hol-map-popup">
${esc(loc)}<br/> <strong>${esc(it.title)}</strong>
${dates ? "Dates: " + esc(dates) + "<br/>" : ""} <div style="font-size:0.9em; color:#666;">${esc(loc)}</div>
Statut: ${esc(it.status || "")}<br/> ${dates ? `<div style="font-size:0.85em; margin-top:4px;">📅 ${esc(dates)}</div>` : ""}
<a href="/holidays.php?id=${it.id}">Ouvrir</a> <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); markers.push(m);
}); });
// Vue par défaut selon nombre de points // Centrage de la carte
if (markers.length === 1) { if (markers.length > 0) {
map.setView(markers[0].getLatLng(), 7);
} else if (markers.length > 1) {
const group = L.featureGroup(markers); const group = L.featureGroup(markers);
map.fitBounds(group.getBounds(), { padding: [20, 20] }); mapInstance.fitBounds(group.getBounds(), { padding: [50, 50] });
} else { } else {
console.warn( mapInstance.setView([46.603354, 1.888334], 5); // France par défaut si vide
"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 mapInitialized = true;
setTimeout(() => map.invalidateSize(), 100);
// Hack indispensable pour que Leaflet calcule la bonne taille dans une modale
setTimeout(() => mapInstance.invalidateSize(), 200);
} }
function esc(s) { // Connexion de la carte à la modale
return String(s).replace( const mapBtn = document.getElementById("hol-map-open");
/[&<>"']/g, if (mapBtn) {
(c) => const mapModalCtrl = setupModal("hol-map-modal", initMap); // On passe initMap en callback d'ouverture
({ mapBtn.addEventListener("click", mapModalCtrl.open);
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
})[c],
);
}
mapBtn.addEventListener("click", () => {
open();
setTimeout(initMap, 0);
});
if (backdrop) backdrop.addEventListener("click", close);
if (cancel) cancel.addEventListener("click", close);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") close();
});
} }
}); });
+90 -137
View File
@@ -1,6 +1,10 @@
<?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);
@@ -10,10 +14,24 @@ if (!function_exists('hol_q')) {
} }
$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éparer les données pour la carte AVANT le rendu (liste ou détail) */ // Préparation des variables par défaut pour éviter les erreurs "undefined variable"
if ($ideaId > 0) { $transport = []; $lodging = []; $acts = []; $budget = [];
// Vue détail: renvoyer l'idée courante (le JS filtrera lat/lng invalides) $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, " $mapIdeas = hol_q($pdo, "
SELECT id, title, country, region, city, SELECT id, title, country, region, city,
CAST(lat AS DECIMAL(9,6)) AS lat, CAST(lat AS DECIMAL(9,6)) AS lat,
@@ -22,8 +40,29 @@ if ($ideaId > 0) {
FROM pf_holidays_ideas FROM pf_holidays_ideas
WHERE id = ? WHERE id = ?
", [$ideaId]); ", [$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);
}
} else { } else {
// Vue liste: idées non archivées avec coordonnées // --- MODE LISTE ---
// 1. Données pour la CARTE (tous les points valides)
$mapIdeas = hol_q($pdo, " $mapIdeas = hol_q($pdo, "
SELECT id, title, country, region, city, SELECT id, title, country, region, city,
CAST(lat AS DECIMAL(9,6)) AS lat, CAST(lat AS DECIMAL(9,6)) AS lat,
@@ -34,38 +73,41 @@ if ($ideaId > 0) {
AND lat IS NOT NULL AND lat IS NOT NULL
AND lng IS NOT NULL AND lng IS NOT NULL
"); ");
}
if ($ideaId > 0) { // 2. Récupérer les listes
// Vue détail d'une idée $planned = hol_q($pdo, "
$rows = hol_q($pdo, "SELECT * FROM pf_holidays_ideas WHERE id = ?", [$ideaId]); SELECT * FROM pf_holidays_ideas
$idea = $rows[0] ?? null; WHERE status = 'planned'
if (!$idea) { ORDER BY COALESCE(desired_start_date, created_at) DESC
echo '<p>Idée introuvable.</p>'; ");
return; $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
");
} }
$transport = hol_q($pdo, "SELECT * FROM pf_holidays_transport WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
$lodging = hol_q($pdo, "SELECT * FROM pf_holidays_lodging WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
$acts = hol_q($pdo, "SELECT * FROM pf_holidays_activities WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
$budget = hol_q($pdo, "SELECT category, label, amount, per_person FROM pf_holidays_budget_items WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
$sumRow = hol_q($pdo, "
SELECT
SUM(CASE WHEN per_person=0 THEN amount ELSE 0 END) AS fixed_total,
SUM(CASE WHEN per_person=1 THEN amount ELSE 0 END) AS per_person_total
FROM pf_holidays_budget_items WHERE idea_id = ?
", [$ideaId])[0] ?? ['fixed_total' => 0, 'per_person_total' => 0];
$fixedTotal = (float)($sumRow['fixed_total'] ?? 0);
$ppTotal = (float)($sumRow['per_person_total'] ?? 0);
?> ?>
<!-- Leaflet (carte) -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const HOL_MAP_DATA = <?= json_encode($mapIdeas, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
</script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<?php if ($isDetailView): ?>
<?php if (!$currentIdea): ?>
<div class="pf-holidays__titlebar"> <div class="pf-holidays__titlebar">
<h1><?= htmlspecialchars($idea['title']) ?></h1> <h1>Idée introuvable</h1>
<div class="hol-title-actions">
<a class="btn" href="/holidays.php">← Retour à la liste</a>
</div>
</div>
<p>Cette idée de vacances n'existe pas ou a été supprimée.</p>
<?php else: ?>
<div class="pf-holidays__titlebar">
<h1><?= htmlspecialchars($currentIdea['title']) ?></h1>
<div class="hol-title-actions"> <div class="hol-title-actions">
<a class="btn" href="/holidays.php">← Retour</a> <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-edit" id="hol-edit-open" data-edit-id="<?= (int)$ideaId ?>">Éditer</button>
@@ -75,16 +117,16 @@ if ($ideaId > 0) {
</div> </div>
<p class="hol-idea-meta"> <p class="hol-idea-meta">
<?= htmlspecialchars(trim(($idea['city'] ? $idea['city'] . ', ' : '') . ($idea['region'] ? $idea['region'] . ', ' : '') . ($idea['country'] ?? ''))) ?> <?= htmlspecialchars(trim(($currentIdea['city'] ? $currentIdea['city'] . ', ' : '') . ($currentIdea['region'] ? $currentIdea['region'] . ', ' : '') . ($currentIdea['country'] ?? ''))) ?>
<?php if (!empty($idea['desired_start_date'])): ?> <?php if (!empty($currentIdea['desired_start_date'])): ?>
• Dates: <?= htmlspecialchars($idea['desired_start_date']) ?><?= !empty($idea['desired_end_date']) ? ' → ' . htmlspecialchars($idea['desired_end_date']) : '' ?> • Dates: <?= htmlspecialchars($currentIdea['desired_start_date']) ?><?= !empty($currentIdea['desired_end_date']) ? ' → ' . htmlspecialchars($currentIdea['desired_end_date']) : '' ?>
<?php elseif (!empty($idea['season_hint'])): ?> <?php elseif (!empty($currentIdea['season_hint'])): ?>
• Saison: <?= htmlspecialchars($idea['season_hint']) ?> • Saison: <?= htmlspecialchars($currentIdea['season_hint']) ?>
<?php endif; ?> <?php endif; ?>
<?php if (!empty($idea['ideal_days'])): ?> <?php if (!empty($currentIdea['ideal_days'])): ?>
• Durée idéale: <?= (int)$idea['ideal_days'] ?> j • Durée idéale: <?= (int)$currentIdea['ideal_days'] ?> j
<?php endif; ?> <?php endif; ?>
• Statut: <strong><?= htmlspecialchars($idea['status']) ?></strong> • Statut: <strong><?= htmlspecialchars($currentIdea['status']) ?></strong>
</p> </p>
<div class="hol-grid"> <div class="hol-grid">
@@ -201,102 +243,9 @@ if ($ideaId > 0) {
</ul> </ul>
</section> </section>
</div> </div>
<?php endif; ?>
<!-- Modale édition idée (pré-remplie via JS) --> <?php else: ?>
<div class="hol-modal" id="hol-edit-modal" aria-hidden="true">
<div class="hol-backdrop"></div>
<div class="hol-dialog" role="dialog" aria-modal="true" aria-labelledby="hol-edit-title">
<form method="post" action="/modules/holidays/save.php" class="hol-form" id="hol-edit-form">
<h3 id="hol-edit-title">Éditer lidée</h3>
<input type="hidden" name="action" value="update_idea">
<input type="hidden" name="id" id="edit-id">
<label>Titre
<input type="text" name="title" id="edit-title" required>
</label>
<div class="hol-inline">
<label>Pays <input type="text" name="country" id="edit-country"></label>
<label>Région <input type="text" name="region" id="edit-region"></label>
<label>Ville <input type="text" name="city" id="edit-city"></label>
</div>
<div class="hol-inline">
<label>Lat <input type="number" step="0.000001" name="lat" id="edit-lat"></label>
<label>Lng <input type="number" step="0.000001" name="lng" id="edit-lng"></label>
<button type="button" class="btn hol-geocode-btn" data-scope="edit">Géocoder</button>
</div>
<div class="hol-inline">
<label>Début <input type="date" name="desired_start_date" id="edit-start"></label>
<label>Fin <input type="date" name="desired_end_date" id="edit-end"></label>
</div>
<div class="hol-inline">
<label>Saison <input type="text" name="season_hint" id="edit-season"></label>
<label>Durée idéale <input type="number" name="ideal_days" id="edit-days" min="1" step="1"></label>
</div>
<label>Statut
<select name="status" id="edit-status">
<option value="draft">draft</option>
<option value="shortlist">shortlist</option>
<option value="favorite">favorite</option>
<option value="planned">planned</option>
<option value="archived">archived</option>
</select>
</label>
<label>Notes <textarea name="notes" rows="4" id="edit-notes"></textarea></label>
<div class="hol-actions">
<button type="button" class="hol-cancel">Annuler</button>
<button type="submit" class="hol-ok">Enregistrer</button>
</div>
</form>
</div>
</div>
<!-- Modale carte -->
<div class="hol-modal" id="hol-map-modal" aria-hidden="true">
<div class="hol-backdrop"></div>
<div class="hol-dialog hol-dialog--map" role="dialog" aria-modal="true" aria-labelledby="hol-map-title">
<div class="hol-map-header">
<h3 id="hol-map-title">Carte des idées</h3>
<button class="hol-cancel">Fermer</button>
</div>
<div id="hol-map" style="width: 100%; height: calc(100vh - 140px);"></div>
</div>
</div>
<script src="/modules/holidays/holidays.js"></script>
<?php
return;
}
/* Vue liste + carte */
$planned = hol_q($pdo, "
SELECT * FROM pf_holidays_ideas
WHERE status = 'planned'
ORDER BY COALESCE(desired_start_date, created_at) DESC
");
$ideas = hol_q($pdo, "
SELECT * FROM pf_holidays_ideas
WHERE status IN ('draft','shortlist','favorite')
ORDER BY FIELD(status,'favorite','shortlist','draft'), created_at DESC
");
$archived = hol_q($pdo, "
SELECT * FROM pf_holidays_ideas
WHERE status = 'archived'
ORDER BY updated_at DESC
");
?>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const HOL_MAP_DATA = <?= json_encode($mapIdeas, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
</script>
<div class="pf-holidays__titlebar"> <div class="pf-holidays__titlebar">
<h1>Idées de vacances</h1> <h1>Idées de vacances</h1>
<div class="hol-title-actions"> <div class="hol-title-actions">
@@ -386,7 +335,6 @@ $archived = hol_q($pdo, "
</div> </div>
</section> </section>
<!-- Modale ajout idée -->
<div class="hol-modal" id="hol-add-modal" aria-hidden="true"> <div class="hol-modal" id="hol-add-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-add-title"> <div class="hol-dialog" role="dialog" aria-modal="true" aria-labelledby="hol-add-title">
@@ -438,7 +386,9 @@ $archived = hol_q($pdo, "
</div> </div>
</div> </div>
<!-- Modale édition (commune à la liste) --> <?php endif; ?>
<div class="hol-modal" id="hol-edit-modal" aria-hidden="true"> <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">
@@ -491,7 +441,6 @@ $archived = hol_q($pdo, "
</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">
@@ -503,4 +452,8 @@ $archived = hol_q($pdo, "
</div> </div>
</div> </div>
<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>
<script src="/modules/holidays/holidays.js"></script> <script src="/modules/holidays/holidays.js"></script>
+76 -36
View File
@@ -1,4 +1,6 @@
<?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';
@@ -11,12 +13,6 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$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
*/ */
@@ -29,7 +25,7 @@ function hol_norm_decimal($v): ?float {
} }
/** /**
* 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;
@@ -37,16 +33,33 @@ function hol_norm_date($v): ?string {
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) {
// --- CRÉATION ---
case 'create_idea': { case 'create_idea': {
$title = trim($_POST['title'] ?? '');
if ($title === '') {
throw new Exception("Le titre est obligatoire.");
}
$status = $_POST['status'] ?? 'draft'; $status = $_POST['status'] ?? 'draft';
$start = hol_norm_date($_POST['desired_start_date'] ?? null); $start = hol_norm_date($_POST['desired_start_date'] ?? null);
$end = hol_norm_date($_POST['desired_end_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); // Règle métier : si on met une date, on passe probablement en 'planned'
$lngVal = hol_norm_decimal($_POST['lng'] ?? null); if (!empty($start) && $status === 'draft') { $status = 'planned'; }
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO pf_holidays_ideas INSERT INTO pf_holidays_ideas
@@ -55,12 +68,12 @@ try {
(: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'] ?? ''),
@@ -68,20 +81,25 @@ try {
':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': { 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'; $status = $_POST['status'] ?? 'draft';
$start = hol_norm_date($_POST['desired_start_date'] ?? null); $start = hol_norm_date($_POST['desired_start_date'] ?? null);
$end = hol_norm_date($_POST['desired_end_date'] ?? null); $end = hol_norm_date($_POST['desired_end_date'] ?? null);
if (!empty($start) && $status === 'draft') { $status = 'planned'; } if (!empty($start) && $status === 'draft') { $status = 'planned'; }
$latVal = hol_norm_decimal($_POST['lat'] ?? null);
$lngVal = hol_norm_decimal($_POST['lng'] ?? null);
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
UPDATE pf_holidays_ideas UPDATE pf_holidays_ideas
SET title=:title, country=:country, region=:region, city=:city, lat=:lat, lng=:lng, SET title=:title, country=:country, region=:region, city=:city, lat=:lat, lng=:lng,
@@ -90,13 +108,13 @@ try {
WHERE id=:id WHERE id=:id
"); ");
$stmt->execute([ $stmt->execute([
':id' => (int)$_POST['id'], ':id' => $id,
':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'] ?? ''),
@@ -104,23 +122,34 @@ try {
':status' => $status, ':status' => $status,
':notes' => trim($_POST['notes'] ?? ''), ':notes' => trim($_POST['notes'] ?? ''),
]); ]);
hol_back();
hol_redirect($id);
break;
} }
// --- SUPPRESSION ---
case 'delete_idea': { 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 = $pdo->prepare("DELETE FROM pf_holidays_ideas WHERE id = :id");
$stmt->execute([':id' => (int)$_POST['id']]); $stmt->execute([':id' => $id]);
header("Location: /holidays.php");
exit;
} }
hol_redirect(0); // Retour liste
break;
}
// --- SOUS-ÉLÉMENTS (Transport, Logement, Activités, Budget) ---
case 'add_transport': { case 'add_transport': {
$id = (int)$_POST['idea_id'];
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO pf_holidays_transport (idea_id, mode, duration_min, cost, co2_kg, link, notes) INSERT INTO pf_holidays_transport (idea_id, mode, duration_min, cost, co2_kg, link, notes)
VALUES (:id,:mode,:dur,:cost,:co2,:link,:notes) VALUES (:id,:mode,:dur,:cost,:co2,:link,:notes)
"); ");
$stmt->execute([ $stmt->execute([
':id' => (int)$_POST['idea_id'], ':id' => $id,
':mode' => strtoupper($_POST['mode'] ?? 'OTHER'), ':mode' => strtoupper($_POST['mode'] ?? 'OTHER'),
':dur' => ($_POST['duration_min'] !== '' ? (int)$_POST['duration_min'] : null), ':dur' => ($_POST['duration_min'] !== '' ? (int)$_POST['duration_min'] : null),
':cost' => hol_norm_decimal($_POST['cost'] ?? null), ':cost' => hol_norm_decimal($_POST['cost'] ?? null),
@@ -128,16 +157,18 @@ try {
':link' => trim($_POST['link'] ?? ''), ':link' => trim($_POST['link'] ?? ''),
':notes' => trim($_POST['notes'] ?? ''), ':notes' => trim($_POST['notes'] ?? ''),
]); ]);
hol_back(); hol_redirect($id);
break;
} }
case 'add_lodging': { case 'add_lodging': {
$id = (int)$_POST['idea_id'];
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO pf_holidays_lodging (idea_id, type, location_text, price_per_n, nights, free_cancel, family_friendly, link, notes) 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) VALUES (:id,:type,:loc,:ppn,:n,:fc,:ff,:link,:notes)
"); ");
$stmt->execute([ $stmt->execute([
':id' => (int)$_POST['idea_id'], ':id' => $id,
':type' => strtoupper($_POST['type'] ?? 'OTHER'), ':type' => strtoupper($_POST['type'] ?? 'OTHER'),
':loc' => trim($_POST['location_text'] ?? ''), ':loc' => trim($_POST['location_text'] ?? ''),
':ppn' => hol_norm_decimal($_POST['price_per_n'] ?? null), ':ppn' => hol_norm_decimal($_POST['price_per_n'] ?? null),
@@ -147,16 +178,18 @@ try {
':link' => trim($_POST['link'] ?? ''), ':link' => trim($_POST['link'] ?? ''),
':notes'=> trim($_POST['notes'] ?? ''), ':notes'=> trim($_POST['notes'] ?? ''),
]); ]);
hol_back(); hol_redirect($id);
break;
} }
case 'add_activity': { case 'add_activity': {
$id = (int)$_POST['idea_id'];
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO pf_holidays_activities (idea_id, name, kind, cost_est, need_booking, weather, link, notes) 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) VALUES (:id,:name,:kind,:cost,:need,:weather,:link,:notes)
"); ");
$stmt->execute([ $stmt->execute([
':id' => (int)$_POST['idea_id'], ':id' => $id,
':name' => trim($_POST['name'] ?? ''), ':name' => trim($_POST['name'] ?? ''),
':kind' => trim($_POST['kind'] ?? ''), ':kind' => trim($_POST['kind'] ?? ''),
':cost' => hol_norm_decimal($_POST['cost_est'] ?? null), ':cost' => hol_norm_decimal($_POST['cost_est'] ?? null),
@@ -165,22 +198,25 @@ try {
':link' => trim($_POST['link'] ?? ''), ':link' => trim($_POST['link'] ?? ''),
':notes' => trim($_POST['notes'] ?? ''), ':notes' => trim($_POST['notes'] ?? ''),
]); ]);
hol_back(); hol_redirect($id);
break;
} }
case 'add_budget': { case 'add_budget': {
$id = (int)$_POST['idea_id'];
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO pf_holidays_budget_items (idea_id, category, label, amount, per_person) INSERT INTO pf_holidays_budget_items (idea_id, category, label, amount, per_person)
VALUES (:id,:cat,:label,:amt,:pp) VALUES (:id,:cat,:label,:amt,:pp)
"); ");
$stmt->execute([ $stmt->execute([
':id' => (int)$_POST['idea_id'], ':id' => $id,
':cat' => strtoupper($_POST['category'] ?? 'OTHER'), ':cat' => strtoupper($_POST['category'] ?? 'OTHER'),
':label' => trim($_POST['label'] ?? ''), ':label' => trim($_POST['label'] ?? ''),
':amt' => hol_norm_decimal($_POST['amount'] ?? null) ?? 0.0, ':amt' => hol_norm_decimal($_POST['amount'] ?? null) ?? 0.0,
':pp' => isset($_POST['per_person']) ? 1 : 0, ':pp' => isset($_POST['per_person']) ? 1 : 0,
]); ]);
hol_back(); hol_redirect($id);
break;
} }
default: default:
@@ -188,7 +224,11 @@ try {
echo 'Unknown action'; echo 'Unknown action';
exit; 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>';
} }
+22 -2
View File
@@ -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;
} }
try {
$st = $pdo->prepare("SELECT * FROM pf_holidays_ideas WHERE id = ?"); $st = $pdo->prepare("SELECT * FROM pf_holidays_ideas WHERE id = ?");
$st->execute([$id]); $st->execute([$id]);
$it = $st->fetch(PDO::FETCH_ASSOC); $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;
} }
// 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); echo json_encode($it, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode(['error' => 'server_error']);
}