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
+47 -133
View File
@@ -1,5 +1,7 @@
<?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);
error_reporting(E_ALL);
@@ -9,8 +11,9 @@ require_login();
require __DIR__ . '/includes/db.php';
// --- 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");
$dbEvents = $stmt_events->fetchAll();
$dbEvents = $stmt_events->fetchAll(PDO::FETCH_ASSOC);
$pageTitle = "PachaFamily - Family Calendar";
$activePage = "family-calendar";
@@ -18,75 +21,47 @@ $bodyClass = "pf-family-calendar";
$pageCss = "/modules/family-calendar/family-calendar.css";
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>
/* 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); ?>;
</script>
<h1>Family Calendar</h1>
<div class="pf-d-flex pf-align-center pf-justify-between pf-mb-4">
<h1>Family Calendar</h1>
</div>
<!-- ===================================================================== -->
<!-- PANNEAU DE CONTRÔLE : Légende, Récapitulatif et Vacances -->
<!-- ===================================================================== -->
<section class="pf-section pf-section--panel">
<div class="pf-flex pf-flex--wrap pf-gap-lg">
<!-- LÉGENDE -->
<div class="pf-card pf-card--small">
<h2 class="pf-card-title">Légende</h2>
<div class="pf-card-body">
<div class="pf-legend-item">
<div class="pf-legend-color fc-legend-school-holiday"></div>
<span>Vacances scolaires</span>
</div>
<div class="pf-legend-item">
<div class="pf-legend-color fc-legend-public-holiday"></div>
<span>Jour férié</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 class="pf-legend-grid">
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-school-holiday"></div><span>Vacances</span></div>
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-public-holiday"></div><span>Férié</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</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>
<!-- RÉCAPITULATIF ANNUEL -->
<div class="pf-card pf-card--small">
<h2 class="pf-card-title">Récapitulatif annuel</h2>
<div class="pf-card-body" id="globalSummary">
<!-- Rempli par le JavaScript -->
<span class="pf-loading-text">Chargement...</span>
</div>
</div>
<!-- VACANCES SCOLAIRES -->
<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-table-wrapper">
<table id="schoolHolidaysTable" class="fc-holidays-table">
<div class="pf-table-wrapper pf-table-wrapper--max-height">
<table id="schoolHolidaysTable" class="fc-holidays-table pf-table pf-table--compact">
<thead>
<tr>
<th>Période</th>
@@ -96,7 +71,6 @@ require __DIR__ . '/header.php';
</tr>
</thead>
<tbody>
<!-- Rempli par JS -->
</tbody>
</table>
</div>
@@ -106,12 +80,10 @@ require __DIR__ . '/header.php';
</div>
</section>
<!-- ===================================================================== -->
<!-- CALENDRIER MENSUEL -->
<!-- ===================================================================== -->
<section class="pf-section">
<h2>Calendrier mensuel</h2>
<div class="fc-month-calendar-wrapper">
<div class="fc-month-header">
<div class="fc-view-controls">
<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>
</div>
</div>
<div class="fc-calendar-and-summary">
<div class="fc-calendar-container">
<div id="fc-month-calendar" class="fc-month-calendar">
</div>
<!-- Le menu contextuel pour le calendrier mensuel -->
<div id="fc-month-selectionMenu" class="fc-selection-menu"></div>
<div id="fc-month-selectionMenu" class="fc-selection-menu" hidden></div>
</div>
</div>
</section>
<!-- ===================================================================== -->
<!-- PLANNING PRINCIPAL -->
<!-- ===================================================================== -->
<section class="pf-section">
<div class="fc-week-header">
<h2>Planning hebdo</h2>
@@ -147,29 +117,15 @@ require __DIR__ . '/header.php';
</div>
<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>
<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-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-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">
@@ -177,28 +133,27 @@ require __DIR__ . '/header.php';
<col class="col-laia-sub">
<col class="col-laia-sub">
</colgroup>
<thead>
<tr>
<th rowspan="3" class="col-month">Mois</th>
<th rowspan="3" class="col-month">Semaine</th>
<th rowspan="3" class="col-day">Lundi</th>
<th rowspan="3" class="col-day">Mardi</th>
<th rowspan="3" class="col-day">Mercredi</th>
<th rowspan="3" class="col-day">Jeudi</th>
<th rowspan="3" class="col-day">Vendredi</th>
<th rowspan="3" class="col-total"># Off Carole</th>
<th rowspan="3" class="col-total"># Extra off Carole</th>
<th rowspan="3" class="col-total"># Centre</th>
<th rowspan="3" class="col-total"># Avis</th>
<th rowspan="3" class="col-total"># Pep malade</th>
<th rowspan="3" class="col-total"># Pep Présence</th>
<th rowspan="3" class="col-month">Sem.</th>
<th rowspan="3" class="col-day">Lun</th>
<th rowspan="3" class="col-day">Mar</th>
<th rowspan="3" class="col-day">Mer</th>
<th rowspan="3" class="col-day">Jeu</th>
<th rowspan="3" class="col-day">Ven</th>
<th rowspan="3" class="col-total rotated-text"><span>Off Carole</span></th>
<th rowspan="3" class="col-total rotated-text"><span>Extra Off</span></th>
<th rowspan="3" class="col-total rotated-text"><span>Centre</span></th>
<th rowspan="3" class="col-total rotated-text"><span>Avis</span></th>
<th rowspan="3" class="col-total rotated-text"><span>Pep Malade</span></th>
<th rowspan="3" class="col-total rotated-text"><span>Présence</span></th>
<!-- Ligne 1 : ALEX / LAIA -->
<th colspan="6" class="col-alex">ALEX</th>
<th colspan="6" class="col-laia">LAIA</th>
<th colspan="6" class="col-alex header-group">ALEX</th>
<th colspan="6" class="col-laia header-group">LAIA</th>
</tr>
<tr>
<!-- Ligne 2 : CP / JRA / JA (regroupement) -->
<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">JA</th>
@@ -208,15 +163,12 @@ require __DIR__ . '/header.php';
<th colspan="2" class="col-laia-sub">JA</th>
</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-use">Use</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-av">Av.</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-use">Use</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>
</tr>
</thead>
</table>
<table id="planningTable" class="pf-table pf-table--compact">
<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>
</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"></div>
<div id="selectionMenu" class="fc-selection-menu" hidden></div>
</div>
</section>
<!-- ===================================================================== -->
<!-- CHARGEMENT DU SCRIPT JAVASCRIPT PRINCIPAL -->
<!-- ===================================================================== -->
<script src="/modules/family-calendar/family-calendar.js"></script>
<?php
// Inclusion du pied de page
require __DIR__ . '/footer.php';
?>
<?php require __DIR__ . '/footer.php'; ?>
+182 -274
View File
@@ -1,42 +1,38 @@
<?php
// modules/gift-list/gift-list.php
// Protection : nécessite d'être connecté
require __DIR__ . '/includes/auth.php';
require_login('/login.php');
// Connexion DB
require __DIR__ . '/includes/db.php';
if (session_status() === PHP_SESSION_NONE) { session_start(); }
// Config page
// --- 1. CONFIGURATION & DONNÉES ---
$year = (int)date('Y');
$pageTitle = "PachaFamily - Llista de regals";
$activePage = "gift-list";
$bodyClass = "pf-gift-list";
$pageCss = "/modules/gift-list/gift-list.css";
require __DIR__ . '/header.php';
// Personne
// Personnes
$baseAdults = ['Laia', 'Laura', 'Avi Iaia'];
$adults = $baseAdults;
$children = ['Pol', 'Pep', 'Elna', 'Bru', 'Guim'];
// Vues et mapping doccasions
// Configuration des Vues
$VIEWS = [
'nadal' => ['TIO','NOEL','ROIS'],
'anniversary' => ['ANNIV','SANT'],
'nadal' => ['TIO', 'NOEL', 'ROIS'],
'anniversary' => ['ANNIV', 'SANT'],
];
// Vue courante (URL > Session > défaut)
// Vue courante
$currentView = strtolower($_GET['view'] ?? ($_SESSION['gift_view'] ?? 'nadal'));
if (!isset($VIEWS[$currentView])) $currentView = 'nadal';
$_SESSION['gift_view'] = $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'];
$adultsByChildForAnniv = [
'Pol' => array_merge($baseAdults, $extraAdults),
@@ -46,9 +42,7 @@ $adultsByChildForAnniv = [
'Guim' => $baseAdults,
];
// Labels doccasion (affichage)
// Labels & Icônes
$allOccasionLabels = [
'TIO' => 'Tió',
'NOEL' => 'Nadal',
@@ -56,8 +50,6 @@ $allOccasionLabels = [
'ANNIV' => 'Anniversary',
'SANT' => 'Sant',
];
// Icônes (pense à ajouter les 2 nouvelles images)
$occasionIcons = [
'TIO' => '/modules/gift-list/assets/img/tio.png',
'NOEL' => '/modules/gift-list/assets/img/santa.png',
@@ -66,61 +58,99 @@ $occasionIcons = [
'SANT' => '/modules/gift-list/assets/img/sant.png',
];
// Nom de table (renommée -> pf_gifts), fallback si pas encore migrée
$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), '?'));
$sql = "
SELECT *
FROM {$tableGifts}
WHERE year = ? AND occasion IN ($inMarks)
ORDER BY adult_name, child_name, occasion, created_at
";
$sql = "SELECT * FROM {$tableGifts} WHERE year = ? AND occasion IN ($inMarks) ORDER BY adult_name, child_name, occasion, created_at";
$stmt = $pdo->prepare($sql);
$params = array_merge([$year], $allowedOccasions);
$stmt->execute($params);
$gifts = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
// Index [occasion][child][adult]
// Indexation [occasion][child][adult] pour la matrice
$byOccasion = [];
foreach ($gifts as $gift) {
$o = $gift['occasion'];
$c = $gift['child_name'];
$a = $gift['adult_name'];
$byOccasion[$o][$c][$a][] = $gift;
$byOccasion[$gift['occasion']][$gift['child_name']][$gift['adult_name']][] = $gift;
}
// Occasions à afficher pour la vue
// Occasions à afficher
$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="cl-titlebar">
<h1>Llista de regals <?= htmlspecialchars($year) ?></h1>
<div class="cl-view-switch" aria-label="Canvia la vista">
<a href="?view=nadal"
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=nadal" class="cl-view-btn <?= $currentView === 'nadal' ? 'is-active' : '' ?>">Nadal</a>
<a href="?view=anniversary" class="cl-view-btn <?= $currentView === 'anniversary' ? 'is-active' : '' ?>">Anniversary</a>
</div>
</div>
<!-- VUE TABLEAU PAR FÊTE -->
<section class="pf-section pf-section--panel">
<h2>Vista per festa</h2>
<?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 foreach ($occasionsToShow as $occCode): ?>
<div class="cl-occasion-block">
<h3 class="cl-occasion-title">
<?php if (!empty($occasionIcons[$occCode])): ?>
<img class="cl-occasion-icon"
src="<?= htmlspecialchars($occasionIcons[$occCode]) ?>"
alt=""
aria-hidden="true">
<img class="cl-occasion-icon" src="<?= htmlspecialchars($occasionIcons[$occCode]) ?>" alt="" aria-hidden="true">
<?php endif; ?>
<?= htmlspecialchars($allOccasionLabels[$occCode] ?? $occCode) ?>
</h3>
@@ -128,6 +158,7 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
<div class="cl-occasion-children-tables">
<?php foreach ($children as $childName): ?>
<?php
// Détermine quels adultes participent pour cet enfant
$adultsForChild = ($currentView === 'anniversary')
? ($adultsByChildForAnniv[$childName] ?? $baseAdults)
: $baseAdults;
@@ -137,12 +168,14 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
$totals = [];
foreach ($adultsForChild as $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);
$maxRowsChild = !empty($counts) ? max($counts) : 0;
?>
<table class="cl-child-table child-<?= strtolower($childName) ?>">
<colgroup>
<?php foreach ($adultsForChild as $_): ?>
@@ -152,15 +185,13 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
<caption>
<?= htmlspecialchars($childName) ?>
<button
type="button"
class="cl-child-add-btn"
title="Afegeix un regal"
<button type="button" class="cl-child-add-btn" title="Afegeix un regal"
data-year="<?= $year ?>"
data-child="<?= htmlspecialchars($childName) ?>"
data-occasion="<?= htmlspecialchars($occCode) ?>"
data-adults="<?= htmlspecialchars(json_encode($adultsForChild), ENT_QUOTES) ?>"
>+</button>
data-adults="<?= htmlspecialchars(json_encode(array_values($adultsForChild)), ENT_QUOTES) ?>">
+
</button>
</caption>
<thead>
@@ -169,9 +200,7 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
<th>
<div class="cl-th-inner">
<span class="cl-th-label"><?= htmlspecialchars($adultName) ?></span>
<span class="cl-summary-adult-total">
<?= number_format($totals[$adultName], 0, ',', ' ') ?> €
</span>
<span class="cl-summary-adult-total"><?= number_format($totals[$adultName], 0, ',', ' ') ?> €</span>
</div>
</th>
<?php endforeach; ?>
@@ -206,14 +235,11 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
<?php else: ?>
<span class="cl-gift-desc"><?= $desc ?></span>
<?php endif; ?>
<div class="cl-gift-right">
<span class="cl-gift-amount">(<?= number_format($amt, 0, ',', ' ') ?> €)</span>
<span class="cl-gift-actions">
<button
type="button"
class="cl-gift-action-btn cl-gift-edit"
title="Edita"
aria-label="Edita"
<button type="button" class="cl-gift-action-btn cl-gift-edit" aria-label="Edita"
data-id="<?= $giftId ?>"
data-year="<?= $year ?>"
data-child="<?= htmlspecialchars($childName) ?>"
@@ -222,31 +248,18 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
data-payer="<?= htmlspecialchars($payer) ?>"
data-desc="<?= htmlspecialchars($gift['gift_description']) ?>"
data-amount="<?= htmlspecialchars($gift['amount']) ?>"
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>
data-link="<?= htmlspecialchars($gift['product_link'] ?? '') ?>">
</button>
<!-- Delete -->
<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 type="button" class="cl-gift-action-btn cl-gift-delete" aria-label="Eliminar" data-id="<?= $giftId ?>">
×
</button>
</span>
</div>
</div>
<?php if (!empty($payer) && $payer !== $gift['adult_name']): ?>
<small style="color:#b91c1c; font-style:italic;">
(pagat per <?= htmlspecialchars($payer) ?>)
</small>
<small style="color:#b91c1c; font-style:italic; display:block; font-size:0.75em;">(pagat per <?= htmlspecialchars($payer) ?>)</small>
<?php endif; ?>
</div>
<?php else: ?>
@@ -260,24 +273,16 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
</tbody>
</table>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</section>
<!-- RÉSUMÉ DU BUDGET (AGGRÉGÉ) -->
<section class="pf-section pf-section--panel">
<h2>Resum del pressupost</h2>
<?php
$sqlSum = "
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
";
$stmtSum = $pdo->prepare($sqlSum);
// Calcul agrégé SQL pour vérification
$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");
$stmtSum->execute($params);
$sums = $stmtSum->fetchAll(PDO::FETCH_ASSOC);
?>
@@ -300,47 +305,13 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
</div>
</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">
<h2>Tricount</h2>
<div class="cl-budget-wrapper">
<table class="pf-table pf-table--compact cl-debt-matrix">
<thead>
<tr>
<th class="cl-matrix-corner">
<span>Deutor ↓</span><span>Creditor →</span>
</th>
<th class="cl-matrix-corner"><span>Deutor ↓</span><span>Creditor →</span></th>
<?php foreach ($people as $p): ?><th><?= htmlspecialchars($p) ?></th><?php endforeach; ?>
</tr>
</thead>
@@ -352,8 +323,8 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
<?php
$val = $matrix[$debtor][$creditor] ?? 0;
$isDiag = ($debtor === $creditor);
$display = $isDiag || $val == 0 ? '—' : number_format($val, 0, ',', ' ') . ' €';
$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>
<?php endforeach; ?>
@@ -362,19 +333,19 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
</tbody>
</table>
</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)): ?>
<p class="cl-legend">Cap deute pendent.</p>
<?php else: ?>
<ul>
<?php foreach ($settlements as [$from, $to, $amt]): ?>
<li><?= htmlspecialchars($from) ?> ha de pagar <?= number_format($amt, 0, ',', ' ') ?> € a <?= htmlspecialchars($to) ?></li>
<ul class="hol-list">
<?php foreach ($settlements as $s): ?>
<li><strong><?= htmlspecialchars($s['from']) ?></strong> ha de pagar <strong><?= number_format($s['amount'], 2, ',', ' ') ?> €</strong> a <?= htmlspecialchars($s['to']) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</section>
<!-- LISTE DÉTAILLÉE -->
<section class="pf-section pf-section--panel">
<h2>Llista detallada de regals</h2>
<div class="cl-detail-wrapper">
@@ -403,196 +374,133 @@ $occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels),
</section>
</div>
<!-- JS inline : modale d'ajout/édition -->
<script>
document.addEventListener('DOMContentLoaded', function () {
// Références modale
const modal = document.getElementById('cl-gift-modal');
const backdrop = modal ? modal.querySelector('.cl-modal-backdrop') : null;
const cancelBtn = modal ? modal.querySelector('.clm-cancel') : null;
function openModal() { if (modal) modal.classList.add('cl-open'); }
function closeModal() { if (modal) modal.classList.remove('cl-open'); }
function toggleModal(show) {
if (!modal) return;
modal.classList.toggle('cl-open', show);
}
// Helpers
function setOptions(select, values) {
if (!select) return;
select.innerHTML = '';
values.forEach(v => {
// Gestion des options de select (Adultes dynamiques selon l'enfant)
function populateSelects(adults) {
const selects = [document.getElementById('clm-adult'), document.getElementById('clm-payer')];
selects.forEach(sel => {
if (!sel) return;
sel.innerHTML = '';
adults.forEach(name => {
const opt = document.createElement('option');
opt.value = v;
opt.textContent = v;
select.appendChild(opt);
opt.value = name;
opt.textContent = name;
sel.appendChild(opt);
});
});
}
function ensureOption(select, value) {
if (!select || !value) return;
const exists = Array.from(select.options).some(o => o.value === value);
if (!exists) {
const opt = document.createElement('option');
opt.value = value;
opt.textContent = value;
select.appendChild(opt);
}
}
// --- BOUTON AJOUT (+) ---
document.querySelectorAll('.cl-child-add-btn').forEach(btn => {
btn.addEventListener('click', () => {
const d = btn.dataset;
let adults = [];
try { adults = JSON.parse(d.adults || '[]'); } catch(e) {}
// Ouverture modale en mode création (bouton "+")
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');
populateSelects(adults);
// Liste d'adultes spécifique à l'enfant (si fournie)
let allowedAdults = [];
const adultsAttr = btn.getAttribute('data-adults');
if (adultsAttr) {
try { allowedAdults = JSON.parse(adultsAttr); } catch (e) { allowedAdults = []; }
}
openModal();
// Mode création
// Valeurs par défaut
document.getElementById('clm-action').value = 'create';
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-amount').value = '';
document.getElementById('clm-link').value = '';
// Titre
document.getElementById('cl-modal-title').textContent = `Afegeix un regal per ${child}`;
document.getElementById('cl-modal-title').textContent = `Afegeix un regal per ${d.child}`;
toggleModal(true);
});
});
// Ouverture modale en mode édition (icône crayon)
document.querySelectorAll('.cl-gift-edit').forEach(function (btn) {
btn.addEventListener('click', function () {
// --- BOUTON ÉDITION (Crayon) ---
document.body.addEventListener('click', (e) => {
const btn = e.target.closest('.cl-gift-edit');
if (!btn) return;
const d = btn.dataset;
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
// Pour l'édition, on s'assure que l'adulte actuel est dans la liste (même si pas standard)
const adultSelect = document.getElementById('clm-adult');
const payerSelect = document.getElementById('clm-payer');
ensureOption(adultSelect, d.adult);
ensureOption(payerSelect, d.payer || d.adult);
if (adultSelect) adultSelect.value = d.adult || '';
if (payerSelect) payerSelect.value = d.payer || d.adult || '';
// Champs
document.getElementById('clm-gift').value = d.desc || '';
document.getElementById('clm-amount').value = d.amount || '';
document.getElementById('clm-link').value = d.link || '';
// Titre
document.getElementById('cl-modal-title').textContent = `Edita un regal per ${d.child || ''}`;
});
// On ne recharge pas toute la liste adults ici car complexe à récupérer du DOM,
// on ajoute juste l'option si manquante.
[adultSelect, payerSelect].forEach(sel => {
const val = (sel === adultSelect) ? d.adult : (d.payer || d.adult);
if (!Array.from(sel.options).some(o => o.value === val)) {
const opt = document.createElement('option');
opt.value = val;
opt.textContent = val;
sel.appendChild(opt);
}
sel.value = val;
});
// Suppression (icône poubelle)
const deleteForm = document.getElementById('cl-delete-form');
const deleteIdInput = document.getElementById('cld-id');
document.querySelectorAll('.cl-gift-delete').forEach(function (btn) {
btn.addEventListener('click', function () {
const giftId = btn.getAttribute('data-id');
if (!giftId) return;
document.getElementById('clm-action').value = 'update';
document.getElementById('clm-id').value = d.id;
document.getElementById('clm-year').value = d.year;
document.getElementById('clm-child').value = d.child;
document.getElementById('clm-occasion').value = d.occasion;
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 (deleteIdInput) deleteIdInput.value = giftId;
if (deleteForm) deleteForm.submit();
const form = document.getElementById('cl-delete-form');
document.getElementById('cld-id').value = btn.dataset.id;
form.submit();
}
});
});
// Fermeture modale
if (cancelBtn) cancelBtn.addEventListener('click', closeModal);
if (backdrop) backdrop.addEventListener('click', closeModal);
document.addEventListener('keydown', function (e) {
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);
// Fermeture
if(cancelBtn) cancelBtn.addEventListener('click', () => toggleModal(false));
if(backdrop) backdrop.addEventListener('click', () => toggleModal(false));
document.addEventListener('keydown', (e) => { if(e.key === 'Escape') toggleModal(false); });
});
</script>
<!-- Modal d'ajout de cadeau -->
<div id="cl-gift-modal" class="cl-modal" aria-hidden="true">
<div class="cl-modal-backdrop"></div>
<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">
<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="child_name" id="clm-child" value="">
<input type="hidden" name="occasion" id="clm-occasion" value="">
<input type="hidden" name="action" id="clm-action" value="create">
<input type="hidden" name="gift_id" id="clm-id" value="">
<input type="hidden" name="year" id="clm-year">
<input type="hidden" name="child_name" id="clm-child">
<input type="hidden" name="occasion" id="clm-occasion">
<input type="hidden" name="action" id="clm-action">
<input type="hidden" name="gift_id" id="clm-id">
<label class="clm-label">
Adult
<select name="adult_name" id="clm-adult" required>
<?php foreach ($adults as $adultName): ?>
<option value="<?= htmlspecialchars($adultName) ?>"><?= htmlspecialchars($adultName) ?></option>
<?php endforeach; ?>
</select>
<select name="adult_name" id="clm-adult" required></select>
</label>
<label class="clm-label">
Pagat per
<select name="payer_name" id="clm-payer" required>
<?php foreach ($adults as $adultName): ?>
<option value="<?= htmlspecialchars($adultName) ?>"><?= htmlspecialchars($adultName) ?></option>
<?php endforeach; ?>
</select>
<select name="payer_name" id="clm-payer" required></select>
</label>
<label class="clm-label">
@@ -602,17 +510,17 @@ document.addEventListener('DOMContentLoaded', function () {
<label class="clm-label">
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 class="clm-label">
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>
<div class="cl-modal-actions">
<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>
</form>
</div>
@@ -624,4 +532,4 @@ document.addEventListener('DOMContentLoaded', function () {
<input type="hidden" name="gift_id" id="cld-id" value="">
</form>
<?php require __DIR__ . '/footer.php';
<?php require __DIR__ . '/footer.php'; ?>
+5
View File
@@ -1,16 +1,21 @@
<?php
// holidays.php
require __DIR__ . '/includes/auth.php';
require_login('/login.php');
require __DIR__ . '/includes/db.php';
if (session_status() === PHP_SESSION_NONE) { session_start(); }
$pageTitle = "PachaFamily - Idées de vacances";
$activePage = "holidays";
$bodyClass = "pf-holidays";
// On charge le CSS spécifique au module
$pageCss = "/modules/holidays/holidays.css";
require __DIR__ . '/header.php';
// Inclusion de la logique et de la vue unifiées
require __DIR__ . '/modules/holidays/index.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:24:00+01:00] RAW INPUT: [{"date":"2025-12-23","type":"CENTRE","duration":1}]
[2026-01-08T09:24:02+01:00] RAW INPUT: [{"date":"2025-12-24","type":"CENTRE","duration":1}]
[2026-01-30T22:49:07+01:00] RAW INPUT: [{"date":"2025-10-24","type":"AVIS","duration":1,"person":null}]
[2026-01-30T22:49:50+01:00] RAW INPUT: [{"date":"2025-10-16","type":"OFF_CAROLE","duration":1,"person":null}]
[2026-01-30T22:50:00+01:00] RAW INPUT: [{"date":"2025-09-11","type":"OFF_CAROLE","duration":1,"person":null}]
[2026-01-30T22:51:16+01:00] RAW INPUT: [{"date":"2025-09-05","type":"AVIS","duration":1,"person":null}]
[2026-01-30T22:53:23+01:00] RAW INPUT: [{"date":"2025-09-03","type":"AVIS","duration":1,"person":null}]
[2026-01-30T23:16:56+01:00] RAW INPUT: [{"date":"2025-09-28","type":"OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-09-29","type":"OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-09-30","type":"OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-01","type":"OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-02","type":"OFF_CAROLE","duration":1,"person":"Carole"}]
[2026-01-30T23:17:02+01:00] RAW INPUT: [{"date":"2025-10-05","type":"EXTRA_OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-06","type":"EXTRA_OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-07","type":"EXTRA_OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-08","type":"EXTRA_OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-09","type":"EXTRA_OFF_CAROLE","duration":1,"person":"Carole"}]
[2026-01-30T23:17:09+01:00] RAW INPUT: [{"date":"2025-10-05","type":"AVIS","duration":1,"person":"Carole"},{"date":"2025-10-06","type":"AVIS","duration":1,"person":"Carole"},{"date":"2025-10-07","type":"AVIS","duration":1,"person":"Carole"},{"date":"2025-10-08","type":"AVIS","duration":1,"person":"Carole"}]
@@ -1,102 +1,93 @@
<?php
// modules/family-calendar/includes/api/manage-event.php
header('Content-Type: application/json');
require __DIR__ . '/../../../../includes/db.php';
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? '';
if (!isset($input['action'])) {
if (!$action) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Action manquante.']);
exit;
}
$action = $input['action'];
try {
// --- SUPPRESSION UNITAIRE ---
if ($action === 'delete') {
// Suppression d'un seul événement
if (!isset($input['event_id'])) {
$eventId = (int)($input['event_id'] ?? 0);
if ($eventId <= 0) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'ID manquant pour la suppression.']);
echo json_encode(['status' => 'error', 'message' => 'ID manquant.']);
exit;
}
$eventId = (int)$input['event_id'];
$stmt = $pdo->prepare("DELETE FROM pf_events WHERE id = ?");
$stmt->execute([$eventId]);
echo json_encode(['status' => 'success', '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.']);
echo json_encode(['status' => 'success']);
exit;
}
$eventId = (int)$input['event_id'];
$newType = $input['new_type'];
// --- MISE À JOUR UNITAIRE ---
if ($action === 'update') {
$eventId = (int)($input['event_id'] ?? 0);
$newType = $input['new_type'] ?? '';
if ($eventId <= 0 || !$newType) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Données manquantes.']);
exit;
}
$stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id = ?");
$stmt->execute([$newType, $eventId]);
echo json_encode(['status' => 'success', 'message' => 'Événement mis à jour.']);
} elseif ($action === 'bulk_delete') {
// Suppression en masse
if (empty($input['event_ids']) || !is_array($input['event_ids'])) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Liste event_ids manquante pour bulk_delete.']);
echo json_encode(['status' => 'success']);
exit;
}
$eventIds = array_map('intval', $input['event_ids']);
$eventIds = array_filter($eventIds, fn($id) => $id > 0);
// --- SUPPRESSION DE MASSE (Par date et type) ---
// Utilisé quand on ajoute un événement pour nettoyer les doublons potentiels (ex: Off vs Extra)
if ($action === 'bulk_delete_day_types') {
$dates = $input['dates'] ?? [];
$types = $input['types'] ?? [];
if (empty($eventIds)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Aucun ID valide pour bulk_delete.']);
if (empty($dates) || empty($types)) {
echo json_encode(['status' => 'success']); // Rien à faire
exit;
}
$placeholders = implode(',', array_fill(0, count($eventIds), '?'));
$stmt = $pdo->prepare("DELETE FROM pf_events WHERE id IN ($placeholders)");
$stmt->execute($eventIds);
// Création des placeholders IN (?,?,?)
$datePlaceholders = implode(',', array_fill(0, count($dates), '?'));
$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') {
// Mise à jour en masse
if (empty($input['event_ids']) || !is_array($input['event_ids']) || !isset($input['new_type'])) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'event_ids ou new_type manquant pour bulk_update.']);
// Fusion des tableaux pour l'exécution
$stmt->execute(array_merge($dates, $types));
echo json_encode(['status' => 'success']);
exit;
}
$eventIds = array_map('intval', $input['event_ids']);
$eventIds = array_filter($eventIds, fn($id) => $id > 0);
$newType = $input['new_type'];
if (empty($eventIds)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Aucun ID valide pour bulk_update.']);
// --- SUPPRESSION TOTALE SUR DES DATES ---
if ($action === 'bulk_delete_all') {
$dates = $input['dates'] ?? [];
if (empty($dates)) {
echo json_encode(['status' => 'success']);
exit;
}
$placeholders = implode(',', array_fill(0, count($eventIds), '?'));
$params = array_merge([$newType], $eventIds);
$datePlaceholders = implode(',', array_fill(0, count($dates), '?'));
$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)");
$stmt->execute($params);
echo json_encode(['status' => 'success', 'message' => 'Événements mis à jour en masse.']);
} else {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Action non valide.']);
echo json_encode(['status' => 'success']);
exit;
}
// Action inconnue
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Action non reconnue : ' . $action]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
File diff suppressed because it is too large Load Diff
+52 -27
View File
@@ -1,42 +1,69 @@
<?php
require __DIR__ . '/../../includes/auth.php';
require_login('/gift-list.php');
// modules/gift-list/save-gift.php
require __DIR__ . '/../../includes/auth.php';
require_login('/login.php');
require __DIR__ . '/../../includes/db.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = trim($_POST['action'] ?? 'create');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /gift-list.php');
exit;
}
// Configuration
$tableName = 'pf_gifts'; // Harmonisé avec gift-list.php
// Récupération des données
$action = $_POST['action'] ?? 'create';
$gift_id = (int)($_POST['gift_id'] ?? 0);
// Logique de redirection (pour rester sur la bonne vue)
// Par défaut on renvoie vers le referer ou vers la page principale
$redirectUrl = '/gift-list.php';
$occasionForView = $_POST['occasion'] ?? '';
if (in_array($occasionForView, ['ANNIV', 'SANT'])) {
$redirectUrl .= '?view=anniversary';
} else {
$redirectUrl .= '?view=nadal';
}
try {
// --- SUPPRESSION ---
if ($action === 'delete') {
$gift_id = (int)($_POST['gift_id'] ?? 0);
if ($gift_id > 0) {
$stmt = $pdo->prepare("DELETE FROM pf_gift_gifts WHERE id = :id");
$stmt = $pdo->prepare("DELETE FROM {$tableName} WHERE id = :id");
$stmt->execute(['id' => $gift_id]);
}
header('Location: /gift-list.php');
header("Location: $redirectUrl");
exit;
}
// Champs communs
$year = (int)($_POST['year'] ?? date('Y'));
$adult_name = trim($_POST['adult_name'] ?? '');
$payer_name = trim($_POST['payer_name'] ?? ''); // nouveau champ
$payer_name = trim($_POST['payer_name'] ?? '');
$child_name = trim($_POST['child_name'] ?? '');
$occasion = trim($_POST['occasion'] ?? '');
$gift_desc = trim($_POST['gift_description'] ?? '');
$product_link = trim($_POST['product_link'] ?? '');
$amount = $_POST['amount'] !== '' ? (float)$_POST['amount'] : 0.0;
$prod_link = trim($_POST['product_link'] ?? '');
$amount = ($_POST['amount'] !== '') ? (float)$_POST['amount'] : 0.0;
// Si payeur vide, c'est l'adulte responsable qui paye
if ($payer_name === '') {
$payer_name = $adult_name;
}
if ($action === 'update') {
$gift_id = (int)($_POST['gift_id'] ?? 0);
if ($gift_id > 0 && $adult_name && $payer_name && $child_name && $occasion && $gift_desc) {
// Validation minimale
if (!$adult_name || !$child_name || !$occasion || !$gift_desc) {
// En cas d'erreur, on redirige sans rien faire (ou on pourrait gérer une erreur)
header("Location: $redirectUrl");
exit;
}
// --- UPDATE ---
if ($action === 'update' && $gift_id > 0) {
$stmt = $pdo->prepare("
UPDATE pf_gift_gifts
UPDATE {$tableName}
SET year = :year,
adult_name = :adult_name,
payer_name = :payer_name,
@@ -55,19 +82,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'child_name' => $child_name,
'occasion' => $occasion,
'gift_description' => $gift_desc,
'product_link' => $product_link ?: null,
'product_link' => $prod_link ?: null,
'amount' => $amount,
]);
}
header('Location: /gift-list.php');
exit;
}
// create (par défaut)
if ($adult_name && $payer_name && $child_name && $occasion && $gift_desc) {
// --- CREATE ---
else {
$stmt = $pdo->prepare("
INSERT INTO pf_gift_gifts
INSERT INTO {$tableName}
(year, adult_name, payer_name, child_name, occasion, gift_description, product_link, amount)
VALUES
(:year, :adult_name, :payer_name, :child_name, :occasion, :gift_description, :product_link, :amount)
@@ -79,11 +101,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'child_name' => $child_name,
'occasion' => $occasion,
'gift_description' => $gift_desc,
'product_link' => $product_link ?: null,
'product_link' => $prod_link ?: null,
'amount' => $amount,
]);
}
header('Location: /gift-list.php');
exit;
} catch (PDOException $e) {
// Log l'erreur si besoin
}
header("Location: $redirectUrl");
exit;
+64 -22
View File
@@ -6,23 +6,43 @@ require __DIR__ . '/../../includes/db.php';
header('Content-Type: application/json; charset=utf-8');
// 1. Validation de l'entrée
$q = trim($_GET['q'] ?? '');
if ($q === '') {
http_response_code(400);
echo json_encode(['error' => 'missing q']);
echo json_encode(['error' => 'missing_q']);
exit;
}
// borne la limite entre 1 et 5 (usage perso)
// Borner la limite
$limit = (int)($_GET['limit'] ?? 1);
if ($limit < 1) $limit = 1;
if ($limit > 5) $limit = 5;
if ($limit > 10) $limit = 10; // Nominatim bloque souvent au-dessus de 10-50
// Cache local (silencieux si table absente)
// 2. Normalisation pour le cache
$qNorm = mb_strtolower($q);
$qHash = hash('sha256', $qNorm);
// 3. Création automatique de la table cache si elle n'existe pas (Sécurité)
try {
$pdo->exec("
CREATE TABLE IF NOT EXISTS pf_geocode_cache (
q_hash CHAR(64) PRIMARY KEY,
q VARCHAR(255),
lat DECIMAL(10, 7),
lng DECIMAL(10, 7),
display_name TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
} catch (Throwable $e) {
// On continue même si ça échoue (l'admin devra créer la table manuellement)
}
// 4. Vérification du cache (Même si limit > 1)
// Stratégie : Si on a déjà cherché exactement "Paris", on renvoie le résultat stocké
// pour économiser l'API et aller plus vite. Le JS gérera le résultat unique.
try {
if ($limit === 1) {
$st = $pdo->prepare("SELECT lat, lng, display_name FROM pf_geocode_cache WHERE q_hash = ?");
$st->execute([$qHash]);
if ($row = $st->fetch(PDO::FETCH_ASSOC)) {
@@ -31,15 +51,14 @@ try {
'lng' => (float)$row['lng'],
'display_name' => $row['display_name'],
'cached' => true
]);
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
}
} catch (Throwable $e) {
// pas bloquant
// Erreur SQL silencieuse sur le cache
}
// Appel Nominatim (respect des règles d'usage)
// 5. Appel Nominatim (Si pas en cache)
$endpoint = 'https://nominatim.openstreetmap.org/search';
$params = http_build_query([
'format' => 'jsonv2',
@@ -56,7 +75,8 @@ curl_setopt_array($ch, [
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 10,
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)'
],
]);
@@ -65,41 +85,63 @@ $http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
// Gestion erreur cURL / HTTP
if ($body === false || $http !== 200) {
http_response_code(502);
echo json_encode(['error' => 'geocode_failed', 'details' => $err ?: ('HTTP '.$http)]);
http_response_code(502); // Bad Gateway
echo json_encode([
'error' => 'geocode_failed',
'details' => $err ?: ('HTTP ' . $http)
]);
exit;
}
$data = json_decode($body, true);
// Gestion erreur JSON ou vide
if (!is_array($data) || empty($data)) {
// On renvoie 200 avec une liste vide ou 404, le JS gère les deux.
// 404 est plus sémantique "Not Found".
http_response_code(404);
echo json_encode(['error' => 'not_found']);
exit;
}
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];
$lat = round((float)$r['lat'], 6);
$lng = round((float)$r['lon'], 6);
$display = $r['display_name'] ?? null;
$display = $r['display_name'] ?? '';
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]);
} catch (Throwable $e) {}
echo json_encode(['lat' => $lat, 'lng' => $lng, 'display_name' => $display]);
exit;
}
// Multi-résultats
$results = array_map(function ($r) {
// 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) {
return [
'lat' => round((float)$r['lat'], 6),
'lng' => round((float)$r['lon'], 6),
'display_name' => (string)($r['display_name'] ?? ''),
];
}, $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
document.addEventListener("DOMContentLoaded", () => {
// --- Modale "Ajouter une idée"
const addBtn = document.getElementById("hol-add-open");
const addModal = document.getElementById("hol-add-modal");
if (addBtn && addModal) {
const backdrop = addModal.querySelector(".hol-backdrop");
const cancel = addModal.querySelector(".hol-cancel");
const open = () => addModal.classList.add("open");
const close = () => addModal.classList.remove("open");
addBtn.addEventListener("click", open);
backdrop.addEventListener("click", close);
cancel.addEventListener("click", close);
// --- 1. FONCTIONS UTILITAIRES ---
/**
* Configure les comportements de fermeture d'une modale (Backdrop, Cancel, Escape)
*/
function setupModal(modalId, openAction = null) {
const modal = document.getElementById(modalId);
if (!modal) return null;
const backdrop = modal.querySelector(".hol-backdrop");
const cancelBtn = modal.querySelector(".hol-cancel");
const close = () => modal.classList.remove("open");
const open = () => {
modal.classList.add("open");
if (openAction) openAction();
};
if (backdrop) backdrop.addEventListener("click", close);
if (cancelBtn) cancelBtn.addEventListener("click", close);
// Fermeture avec ECHAP (uniquement si cette modale est ouverte)
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") close();
if (e.key === "Escape" && modal.classList.contains("open")) {
close();
}
});
return { modal, open, close };
}
// --- Helpers modale "Éditer"
const editModal = document.getElementById("hol-edit-modal");
const openEditModal = () => {
if (editModal) editModal.classList.add("open");
};
const closeEditModal = () => {
if (editModal) editModal.classList.remove("open");
};
/**
* Échappe les caractères HTML pour éviter les failles XSS simples
*/
function esc(s) {
if (s === null || s === undefined) return "";
return String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// --- 2. GESTION MODALE "AJOUTER" ---
const addModalCtrl = setupModal("hol-add-modal");
const addBtn = document.getElementById("hol-add-open");
if (addBtn && addModalCtrl) {
addBtn.addEventListener("click", addModalCtrl.open);
}
// --- 3. GESTION MODALE "ÉDITER" ---
const editModalCtrl = setupModal("hol-edit-modal");
// Fonction pour charger et ouvrir l'édition via AJAX
async function openEditForId(id) {
if (!editModalCtrl) return;
try {
const res = await fetch(
`/modules/holidays/view.php?id=${encodeURIComponent(id)}`,
{
headers: { Accept: "application/json" },
},
{ headers: { Accept: "application/json" } },
);
if (!res.ok) throw new Error("HTTP " + res.status);
const it = await res.json();
if (!res.ok) throw new Error("Erreur HTTP " + res.status);
if (!editModal) {
alert("Modale édition introuvable");
return;
}
// Scope les sélecteurs à la modale pour éviter les null
const $ = (sel) => editModal.querySelector(sel);
const it = await res.json();
const modal = editModalCtrl.modal;
// Helper pour remplir les champs
const setVal = (sel, val) => {
const el = $(sel);
const el = modal.querySelector(sel);
if (el) el.value = val ?? "";
};
// Remplissage du formulaire
setVal("#edit-id", it.id);
setVal("#edit-title", it.title || "");
setVal("#edit-country", it.country || "");
setVal("#edit-region", it.region || "");
setVal("#edit-city", it.city || "");
setVal("#edit-lat", it.lat ?? "");
setVal("#edit-lng", it.lng ?? "");
setVal("#edit-start", it.desired_start_date ?? "");
setVal("#edit-end", it.desired_end_date ?? "");
setVal("#edit-season", it.season_hint || "");
setVal("#edit-days", it.ideal_days ?? "");
setVal("#edit-title", it.title);
setVal("#edit-country", it.country);
setVal("#edit-region", it.region);
setVal("#edit-city", it.city);
setVal("#edit-lat", it.lat);
setVal("#edit-lng", it.lng);
setVal("#edit-start", it.desired_start_date);
setVal("#edit-end", it.desired_end_date);
setVal("#edit-season", it.season_hint);
setVal("#edit-days", it.ideal_days);
setVal("#edit-status", it.status || "draft");
setVal("#edit-notes", it.notes || "");
setVal("#edit-notes", it.notes);
openEditModal();
} catch {
alert("Impossible de charger lidée.");
editModalCtrl.open();
} catch (err) {
console.error(err);
alert("Impossible de charger les données de l'idée.");
}
}
// --- Modale "Éditer" (liste + page détail)
if (editModal) {
const backdrop = editModal.querySelector(".hol-backdrop");
const cancel = editModal.querySelector(".hol-cancel");
if (backdrop) backdrop.addEventListener("click", closeEditModal);
if (cancel) cancel.addEventListener("click", closeEditModal);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeEditModal();
});
// Boutons "Éditer" des cards (liste/planifiées/archivées)
document.querySelectorAll(".btn-edit[data-edit-id]").forEach((btn) => {
btn.addEventListener("click", () => {
// Écouteurs sur les boutons "Éditer" (Liste + Détail)
document.body.addEventListener("click", (e) => {
// Utilisation de la délégation d'événement pour gérer tous les boutons (même dynamiques)
const btn = e.target.closest(".btn-edit");
if (btn && btn.hasAttribute("data-edit-id")) {
const id = btn.getAttribute("data-edit-id");
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)
const headerEditBtn = document.getElementById("hol-edit-open");
if (headerEditBtn) {
headerEditBtn.addEventListener("click", () => {
const id = headerEditBtn.getAttribute("data-edit-id");
if (id) openEditForId(id);
});
}
}
// --- Suppression (liste + planifiées + page détail)
document.querySelectorAll(".btn-delete[data-del-id]").forEach((btn) => {
btn.addEventListener("click", () => {
// --- 4. GESTION SUPPRESSION ---
document.body.addEventListener("click", (e) => {
const btn = e.target.closest(".btn-delete");
if (btn && btn.hasAttribute("data-del-id")) {
const id = btn.getAttribute("data-del-id");
if (!id) return;
if (confirm("Supprimer cette idée ?")) {
if (
confirm(
"Voulez-vous vraiment supprimer cette idée ?\nCette action est irréversible.",
)
) {
// Création d'un formulaire temporaire pour le POST
const form = document.createElement("form");
form.method = "post";
form.action = "/modules/holidays/save.php";
@@ -112,45 +133,51 @@ document.addEventListener("DOMContentLoaded", () => {
document.body.appendChild(form);
form.submit();
}
});
}
});
// --- Géocodage via Nominatim + UI multi-résultats
// --- 5. GÉOCODAGE (Nominatim) ---
document.querySelectorAll(".hol-geocode-btn").forEach((btn) => {
btn.addEventListener("click", async () => {
const scope = btn.getAttribute("data-scope"); // 'add' ou 'edit'
let form, city, region, country, latInput, lngInput;
let latInput,
lngInput,
qParts = [];
// Récupération des inputs selon le scope
if (scope === "edit") {
form = btn.closest("form") || document;
city = (document.getElementById("edit-city")?.value || "").trim();
region = (document.getElementById("edit-region")?.value || "").trim();
country = (document.getElementById("edit-country")?.value || "").trim();
const getVal = (id) =>
(document.getElementById(id)?.value || "").trim();
qParts = [
getVal("edit-city"),
getVal("edit-region"),
getVal("edit-country"),
];
latInput = document.getElementById("edit-lat");
lngInput = document.getElementById("edit-lng");
} else {
form = btn.closest("form");
city = (form?.querySelector('input[name="city"]')?.value || "").trim();
region = (
form?.querySelector('input[name="region"]')?.value || ""
).trim();
country = (
form?.querySelector('input[name="country"]')?.value || ""
).trim();
// Scope 'add' : on cherche dans le formulaire parent
const form = btn.closest("form");
const getVal = (name) =>
(form?.querySelector(`input[name="${name}"]`)?.value || "").trim();
qParts = [getVal("city"), getVal("region"), getVal("country")];
latInput = form?.querySelector('input[name="lat"]');
lngInput = form?.querySelector('input[name="lng"]');
}
const q = [city, region, country].filter(Boolean).join(", ");
const q = qParts.filter(Boolean).join(", ");
if (!q) {
alert("Renseigne au moins Ville/Pays.");
alert(
"Veuillez renseigner au moins une Ville ou un Pays pour géocoder.",
);
return;
}
// UI : chargement
removeNearbyPicker(btn);
btn.disabled = true;
const original = btn.textContent;
btn.textContent = "Recherche...";
const originalText = btn.textContent;
btn.textContent = "...";
try {
const res = await fetch(
@@ -160,28 +187,30 @@ document.addEventListener("DOMContentLoaded", () => {
},
);
const data = await res.json();
if (!res.ok) throw new Error(data?.error || "Erreur géocodage");
if ("lat" in data && "lng" in data) {
if (!res.ok) throw new Error(data?.error || "Erreur inconnue");
// Cas 1: Résultat direct (lat/lng uniques)
if (data.lat && data.lng) {
if (latInput) latInput.value = data.lat;
if (lngInput) lngInput.value = data.lng;
return;
}
if (Array.isArray(data.results) && data.results.length > 0) {
// Cas 2: Liste de choix
else if (Array.isArray(data.results) && data.results.length > 0) {
renderGeocodePicker(btn, data.results, (choice) => {
if (latInput) latInput.value = choice.lat;
if (lngInput) lngInput.value = choice.lng;
removeNearbyPicker(btn);
});
} else {
alert("Aucun résultat.");
alert("Aucun résultat trouvé pour : " + q);
}
} catch {
alert("Impossible de trouver les coordonnées pour: " + q);
} catch (err) {
console.error(err);
alert("Erreur lors du géocodage.");
} finally {
btn.disabled = false;
btn.textContent = original;
btn.textContent = originalText;
}
});
});
@@ -192,46 +221,36 @@ document.addEventListener("DOMContentLoaded", () => {
const wrapper = document.createElement("div");
wrapper.className = "hol-geocode-picker";
// Header
const header = document.createElement("div");
header.className = "hol-geocode-picker__header";
header.textContent = "Plusieurs résultats trouvés";
const closeBtn = document.createElement("button");
closeBtn.type = "button";
closeBtn.className = "hol-geocode-picker__close";
closeBtn.textContent = "×";
closeBtn.addEventListener("click", () => removeNearbyPicker(anchorBtn));
header.appendChild(closeBtn);
header.innerHTML = `<span>Choix multiples</span><button type="button" class="hol-close">×</button>`;
header
.querySelector(".hol-close")
.addEventListener("click", () => removeNearbyPicker(anchorBtn));
wrapper.appendChild(header);
// Liste
const list = document.createElement("ul");
list.className = "hol-geocode-picker__list";
results.forEach((r) => {
const li = document.createElement("li");
li.className = "hol-geocode-picker__item";
const label = document.createElement("div");
label.className = "hol-geocode-picker__label";
label.textContent = r.display_name || `${r.lat}, ${r.lng}`;
const coords = document.createElement("div");
coords.className = "hol-geocode-picker__coords";
coords.textContent = `(${r.lat}, ${r.lng})`;
const pickBtn = document.createElement("button");
pickBtn.type = "button";
pickBtn.className = "hol-geocode-picker__pick";
pickBtn.textContent = "Choisir";
pickBtn.addEventListener("click", () => onPick(r));
li.appendChild(label);
li.appendChild(coords);
li.appendChild(pickBtn);
li.innerHTML = `
<div class="hol-info">
<span class="hol-label">${esc(r.display_name)}</span>
<span class="hol-coords">(${r.lat}, ${r.lng})</span>
</div>
<button type="button">Choisir</button>
`;
li.querySelector("button").addEventListener("click", () => onPick(r));
list.appendChild(li);
});
wrapper.appendChild(header);
wrapper.appendChild(list);
// Insertion après le conteneur du bouton
const container =
anchorBtn.closest(".hol-inline") || anchorBtn.parentElement;
container.insertAdjacentElement("afterend", wrapper);
@@ -246,123 +265,96 @@ document.addEventListener("DOMContentLoaded", () => {
}
}
// --- Carte (Leaflet)
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");
// --- 6. CARTE LEAFLET ---
let mapInitialized = false;
let map;
let mapInstance;
// Fonction d'initialisation de la carte (appelée à l'ouverture de la modale)
function initMap() {
// Évite double initialisation
if (mapInitialized) return;
mapInitialized = true;
// Données carte (safe fallback)
const MAP_DATA = Array.isArray(window.HOL_MAP_DATA)
? window.HOL_MAP_DATA
: [];
console.log("HOL_MAP_DATA (safe):", MAP_DATA);
// Leaflet dispo ?
if (typeof L === "undefined") {
console.error("Leaflet non chargé");
if (mapInitialized) {
// Si déjà init, on force juste le redimensionnement pour éviter les bugs d'affichage
setTimeout(() => mapInstance.invalidateSize(), 200);
return;
}
// Init carte une seule fois (ne pas faire ça dans la boucle)
map = L.map("hol-map", { scrollWheelZoom: true });
if (typeof L === "undefined") {
console.error("Leaflet n'est pas chargé.");
return;
}
// Récupération sécurisée des données injectées par PHP
const MAP_DATA = Array.isArray(window.HOL_MAP_DATA)
? window.HOL_MAP_DATA
: [];
mapInstance = L.map("hol-map", { scrollWheelZoom: true });
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap",
}).addTo(map);
}).addTo(mapInstance);
// Corrige la taille après affichage de la modale
setTimeout(() => map.invalidateSize(), 0);
// Ajout des marqueurs
const markers = [];
MAP_DATA.forEach((it) => {
const lat = parseFloat(it.lat);
const lng = parseFloat(it.lng);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
const color =
it.status === "planned"
? "#16a34a"
: it.status === "favorite"
? "#f59e0b"
: it.status === "shortlist"
? "#3b82f6"
: "#6b7280";
// Code couleur selon statut
const colors = {
planned: "#16a34a",
favorite: "#f59e0b",
shortlist: "#3b82f6",
default: "#6b7280",
};
const color = colors[it.status] || colors.default;
const m = L.circleMarker([lat, lng], {
radius: 6,
color,
radius: 7,
color: "#ffffff",
weight: 1,
fillColor: color,
fillOpacity: 0.85,
}).addTo(map);
fillOpacity: 0.9,
}).addTo(mapInstance);
// Construction du popup
const loc = [it.city, it.region, it.country].filter(Boolean).join(", ");
const dates = it.desired_start_date
? `${it.desired_start_date}${it.desired_end_date ? " → " + it.desired_end_date : ""}`
: "";
: null;
m.bindPopup(`
<strong>${esc(it.title || "")}</strong><br/>
${esc(loc)}<br/>
${dates ? "Dates: " + esc(dates) + "<br/>" : ""}
Statut: ${esc(it.status || "")}<br/>
<a href="/holidays.php?id=${it.id}">Ouvrir</a>
<div class="hol-map-popup">
<strong>${esc(it.title)}</strong>
<div style="font-size:0.9em; color:#666;">${esc(loc)}</div>
${dates ? `<div style="font-size:0.85em; margin-top:4px;">📅 ${esc(dates)}</div>` : ""}
<div style="margin-top:8px;">
<span class="hol-status hol-status--${esc(it.status)}">${esc(it.status)}</span>
<a href="/holidays.php?id=${it.id}" style="margin-left:8px;">Voir</a>
</div>
</div>
`);
markers.push(m);
});
// Vue par défaut selon nombre de points
if (markers.length === 1) {
map.setView(markers[0].getLatLng(), 7);
} else if (markers.length > 1) {
// Centrage de la carte
if (markers.length > 0) {
const group = L.featureGroup(markers);
map.fitBounds(group.getBounds(), { padding: [20, 20] });
mapInstance.fitBounds(group.getBounds(), { padding: [50, 50] });
} else {
console.warn(
"HOL_MAP_DATA vide ou coordonnées non valides.",
window.HOL_MAP_DATA,
);
map.setView([20, 0], 2);
mapInstance.setView([46.603354, 1.888334], 5); // France par défaut si vide
}
// Re-valider la taille après rendu complet
setTimeout(() => map.invalidateSize(), 100);
mapInitialized = true;
// Hack indispensable pour que Leaflet calcule la bonne taille dans une modale
setTimeout(() => mapInstance.invalidateSize(), 200);
}
function esc(s) {
return String(s).replace(
/[&<>"']/g,
(c) =>
({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
})[c],
);
}
mapBtn.addEventListener("click", () => {
open();
setTimeout(initMap, 0);
});
if (backdrop) backdrop.addEventListener("click", close);
if (cancel) cancel.addEventListener("click", close);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") close();
});
// Connexion de la carte à la modale
const mapBtn = document.getElementById("hol-map-open");
if (mapBtn) {
const mapModalCtrl = setupModal("hol-map-modal", initMap); // On passe initMap en callback d'ouverture
mapBtn.addEventListener("click", mapModalCtrl.open);
}
});
+101 -148
View File
@@ -1,6 +1,10 @@
<?php
// modules/holidays/index.php
// ------------------------------------------------------------------
// 1. LOGIQUE PHP : RÉCUPÉRATION DES DONNÉES
// ------------------------------------------------------------------
if (!function_exists('hol_q')) {
function hol_q(PDO $pdo, string $sql, array $params = []): array {
$st = $pdo->prepare($sql);
@@ -10,10 +14,24 @@ if (!function_exists('hol_q')) {
}
$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) */
if ($ideaId > 0) {
// Vue détail: renvoyer l'idée courante (le JS filtrera lat/lng invalides)
// Préparation des variables par défaut pour éviter les erreurs "undefined variable"
$transport = []; $lodging = []; $acts = []; $budget = [];
$fixedTotal = 0; $ppTotal = 0;
$planned = []; $ideas = []; $archived = [];
if ($isDetailView) {
// --- MODE DÉTAIL ---
// 1. Récupérer l'idée principale
$rows = hol_q($pdo, "SELECT * FROM pf_holidays_ideas WHERE id = ?", [$ideaId]);
$currentIdea = $rows[0] ?? null;
if ($currentIdea) {
// 2. Récupérer les données pour la CARTE (un seul point)
// Le JS filtrera lat/lng invalides
$mapIdeas = hol_q($pdo, "
SELECT id, title, country, region, city,
CAST(lat AS DECIMAL(9,6)) AS lat,
@@ -22,8 +40,29 @@ if ($ideaId > 0) {
FROM pf_holidays_ideas
WHERE id = ?
", [$ideaId]);
// 3. Récupérer les sous-éléments (Transport, Logement, etc.)
$transport = hol_q($pdo, "SELECT * FROM pf_holidays_transport WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
$lodging = hol_q($pdo, "SELECT * FROM pf_holidays_lodging WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
$acts = hol_q($pdo, "SELECT * FROM pf_holidays_activities WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
$budget = hol_q($pdo, "SELECT category, label, amount, per_person FROM pf_holidays_budget_items WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
// 4. Calculs totaux budget
$sumRow = hol_q($pdo, "
SELECT
SUM(CASE WHEN per_person=0 THEN amount ELSE 0 END) AS fixed_total,
SUM(CASE WHEN per_person=1 THEN amount ELSE 0 END) AS per_person_total
FROM pf_holidays_budget_items WHERE idea_id = ?
", [$ideaId])[0] ?? ['fixed_total' => 0, 'per_person_total' => 0];
$fixedTotal = (float)($sumRow['fixed_total'] ?? 0);
$ppTotal = (float)($sumRow['per_person_total'] ?? 0);
}
} 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, "
SELECT id, title, country, region, city,
CAST(lat AS DECIMAL(9,6)) AS lat,
@@ -34,38 +73,41 @@ if ($ideaId > 0) {
AND lat IS NOT NULL
AND lng IS NOT NULL
");
// 2. Récupérer les listes
$planned = hol_q($pdo, "
SELECT * FROM pf_holidays_ideas
WHERE status = 'planned'
ORDER BY COALESCE(desired_start_date, created_at) DESC
");
$ideas = hol_q($pdo, "
SELECT * FROM pf_holidays_ideas
WHERE status IN ('draft','shortlist','favorite')
ORDER BY FIELD(status,'favorite','shortlist','draft'), created_at DESC
");
$archived = hol_q($pdo, "
SELECT * FROM pf_holidays_ideas
WHERE status = 'archived'
ORDER BY updated_at DESC
");
}
?>
if ($ideaId > 0) {
// Vue détail d'une idée
$rows = hol_q($pdo, "SELECT * FROM pf_holidays_ideas WHERE id = ?", [$ideaId]);
$idea = $rows[0] ?? null;
if (!$idea) {
echo '<p>Idée introuvable.</p>';
return;
}
$transport = hol_q($pdo, "SELECT * FROM pf_holidays_transport WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
$lodging = hol_q($pdo, "SELECT * FROM pf_holidays_lodging WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
$acts = hol_q($pdo, "SELECT * FROM pf_holidays_activities WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
$budget = hol_q($pdo, "SELECT category, label, amount, per_person FROM pf_holidays_budget_items WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
$sumRow = hol_q($pdo, "
SELECT
SUM(CASE WHEN per_person=0 THEN amount ELSE 0 END) AS fixed_total,
SUM(CASE WHEN per_person=1 THEN amount ELSE 0 END) AS per_person_total
FROM pf_holidays_budget_items WHERE idea_id = ?
", [$ideaId])[0] ?? ['fixed_total' => 0, 'per_person_total' => 0];
$fixedTotal = (float)($sumRow['fixed_total'] ?? 0);
$ppTotal = (float)($sumRow['per_person_total'] ?? 0);
?>
<!-- Leaflet (carte) -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const HOL_MAP_DATA = <?= json_encode($mapIdeas, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
</script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<?php if ($isDetailView): ?>
<?php if (!$currentIdea): ?>
<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">
<a class="btn" href="/holidays.php">← Retour</a>
<button class="btn btn-edit" id="hol-edit-open" data-edit-id="<?= (int)$ideaId ?>">Éditer</button>
@@ -75,16 +117,16 @@ if ($ideaId > 0) {
</div>
<p class="hol-idea-meta">
<?= htmlspecialchars(trim(($idea['city'] ? $idea['city'] . ', ' : '') . ($idea['region'] ? $idea['region'] . ', ' : '') . ($idea['country'] ?? ''))) ?>
<?php if (!empty($idea['desired_start_date'])): ?>
• Dates: <?= htmlspecialchars($idea['desired_start_date']) ?><?= !empty($idea['desired_end_date']) ? ' → ' . htmlspecialchars($idea['desired_end_date']) : '' ?>
<?php elseif (!empty($idea['season_hint'])): ?>
• Saison: <?= htmlspecialchars($idea['season_hint']) ?>
<?= htmlspecialchars(trim(($currentIdea['city'] ? $currentIdea['city'] . ', ' : '') . ($currentIdea['region'] ? $currentIdea['region'] . ', ' : '') . ($currentIdea['country'] ?? ''))) ?>
<?php if (!empty($currentIdea['desired_start_date'])): ?>
• Dates: <?= htmlspecialchars($currentIdea['desired_start_date']) ?><?= !empty($currentIdea['desired_end_date']) ? ' → ' . htmlspecialchars($currentIdea['desired_end_date']) : '' ?>
<?php elseif (!empty($currentIdea['season_hint'])): ?>
• Saison: <?= htmlspecialchars($currentIdea['season_hint']) ?>
<?php endif; ?>
<?php if (!empty($idea['ideal_days'])): ?>
• Durée idéale: <?= (int)$idea['ideal_days'] ?> j
<?php if (!empty($currentIdea['ideal_days'])): ?>
• Durée idéale: <?= (int)$currentIdea['ideal_days'] ?> j
<?php endif; ?>
• Statut: <strong><?= htmlspecialchars($idea['status']) ?></strong>
• Statut: <strong><?= htmlspecialchars($currentIdea['status']) ?></strong>
</p>
<div class="hol-grid">
@@ -201,111 +243,18 @@ if ($ideaId > 0) {
</ul>
</section>
</div>
<?php endif; ?>
<!-- Modale édition idée (pré-remplie via JS) -->
<div class="hol-modal" id="hol-edit-modal" aria-hidden="true">
<div class="hol-backdrop"></div>
<div class="hol-dialog" role="dialog" aria-modal="true" aria-labelledby="hol-edit-title">
<form method="post" action="/modules/holidays/save.php" class="hol-form" id="hol-edit-form">
<h3 id="hol-edit-title">Éditer 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">
<?php else: ?>
<div class="pf-holidays__titlebar">
<h1>Idées de vacances</h1>
<div class="hol-title-actions">
<button class="hol-add-btn" id="hol-add-open">+ Ajouter une idée</button>
<button class="hol-map-toggle" id="hol-map-open">🌍 Carte</button>
</div>
</div>
</div>
<section class="pf-section pf-section--panel">
<section class="pf-section pf-section--panel">
<h2>Vacances planifiées</h2>
<p class="cl-legend">Dates souhaitées, prêtes à être réservées.</p>
<div class="hol-ideas-grid">
@@ -335,9 +284,9 @@ $archived = hol_q($pdo, "
</div>
<?php endforeach; ?>
</div>
</section>
</section>
<section class="pf-section pf-section--panel">
<section class="pf-section pf-section--panel">
<h2>Idées</h2>
<p class="cl-legend">Brouillons, favoris, shortlist.</p>
<div class="hol-ideas-grid">
@@ -369,9 +318,9 @@ $archived = hol_q($pdo, "
</div>
<?php endforeach; ?>
</div>
</section>
</section>
<section class="pf-section pf-section--panel">
<section class="pf-section pf-section--panel">
<h2>Archivées</h2>
<div class="hol-ideas-grid hol-ideas-grid--archived">
<?php foreach ($archived as $it): ?>
@@ -384,10 +333,9 @@ $archived = hol_q($pdo, "
</div>
<?php endforeach; ?>
</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-dialog" role="dialog" aria-modal="true" aria-labelledby="hol-add-title">
<form method="post" action="/modules/holidays/save.php" class="hol-form">
@@ -436,9 +384,11 @@ $archived = hol_q($pdo, "
</div>
</form>
</div>
</div>
</div>
<?php endif; ?>
<!-- Modale édition (commune à la liste) -->
<div class="hol-modal" id="hol-edit-modal" aria-hidden="true">
<div class="hol-backdrop"></div>
<div class="hol-dialog" role="dialog" aria-modal="true" aria-labelledby="hol-edit-title">
@@ -491,7 +441,6 @@ $archived = hol_q($pdo, "
</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">
@@ -503,4 +452,8 @@ $archived = hol_q($pdo, "
</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>
+79 -39
View File
@@ -1,4 +1,6 @@
<?php
// modules/holidays/save.php
require __DIR__ . '/../../includes/auth.php';
require_login('/login.php');
require __DIR__ . '/../../includes/db.php';
@@ -11,12 +13,6 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$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
*/
@@ -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 {
if (!isset($v)) return null;
@@ -37,16 +33,33 @@ function hol_norm_date($v): ?string {
return $s === '' ? null : $s;
}
/**
* Redirection explicite vers la vue détail ou liste
*/
function hol_redirect(int $id = 0): void {
if ($id > 0) {
header("Location: /holidays.php?id=" . $id);
} else {
header("Location: /holidays.php");
}
exit;
}
try {
switch ($action) {
// --- CRÉATION ---
case 'create_idea': {
$title = trim($_POST['title'] ?? '');
if ($title === '') {
throw new Exception("Le titre est obligatoire.");
}
$status = $_POST['status'] ?? 'draft';
$start = hol_norm_date($_POST['desired_start_date'] ?? null);
$end = hol_norm_date($_POST['desired_end_date'] ?? null);
if (!empty($start) && $status === 'draft') { $status = 'planned'; }
$latVal = hol_norm_decimal($_POST['lat'] ?? null);
$lngVal = hol_norm_decimal($_POST['lng'] ?? null);
// Règle métier : si on met une date, on passe probablement en 'planned'
if (!empty($start) && $status === 'draft') { $status = 'planned'; }
$stmt = $pdo->prepare("
INSERT INTO pf_holidays_ideas
@@ -55,12 +68,12 @@ try {
(:title,:country,:region,:city,:lat,:lng,:start,:end,:season,:days,:status,:notes)
");
$stmt->execute([
':title' => trim($_POST['title'] ?? ''),
':country'=> trim($_POST['country'] ?? ''),
':title' => $title,
':country' => trim($_POST['country'] ?? ''),
':region' => trim($_POST['region'] ?? ''),
':city' => trim($_POST['city'] ?? ''),
':lat' => $latVal,
':lng' => $lngVal,
':lat' => hol_norm_decimal($_POST['lat'] ?? null),
':lng' => hol_norm_decimal($_POST['lng'] ?? null),
':start' => $start,
':end' => $end,
':season' => trim($_POST['season_hint'] ?? ''),
@@ -68,20 +81,25 @@ try {
':status' => $status,
':notes' => trim($_POST['notes'] ?? ''),
]);
$newId = (int)$pdo->lastInsertId();
header("Location: /holidays.php?id={$newId}");
exit;
// Redirection vers la nouvelle idée
hol_redirect((int)$pdo->lastInsertId());
break;
}
// --- MISE À JOUR ---
case 'update_idea': {
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) throw new Exception("ID invalide.");
$title = trim($_POST['title'] ?? '');
if ($title === '') throw new Exception("Le titre est obligatoire.");
$status = $_POST['status'] ?? 'draft';
$start = hol_norm_date($_POST['desired_start_date'] ?? null);
$end = hol_norm_date($_POST['desired_end_date'] ?? null);
if (!empty($start) && $status === 'draft') { $status = 'planned'; }
$latVal = hol_norm_decimal($_POST['lat'] ?? null);
$lngVal = hol_norm_decimal($_POST['lng'] ?? null);
$stmt = $pdo->prepare("
UPDATE pf_holidays_ideas
SET title=:title, country=:country, region=:region, city=:city, lat=:lat, lng=:lng,
@@ -90,13 +108,13 @@ try {
WHERE id=:id
");
$stmt->execute([
':id' => (int)$_POST['id'],
':title' => trim($_POST['title'] ?? ''),
':country'=> trim($_POST['country'] ?? ''),
':id' => $id,
':title' => $title,
':country' => trim($_POST['country'] ?? ''),
':region' => trim($_POST['region'] ?? ''),
':city' => trim($_POST['city'] ?? ''),
':lat' => $latVal,
':lng' => $lngVal,
':lat' => hol_norm_decimal($_POST['lat'] ?? null),
':lng' => hol_norm_decimal($_POST['lng'] ?? null),
':start' => $start,
':end' => $end,
':season' => trim($_POST['season_hint'] ?? ''),
@@ -104,40 +122,53 @@ try {
':status' => $status,
':notes' => trim($_POST['notes'] ?? ''),
]);
hol_back();
hol_redirect($id);
break;
}
// --- SUPPRESSION ---
case 'delete_idea': {
$id = (int)($_POST['id'] ?? 0);
if ($id > 0) {
// On supprime l'idée (les sous-tables devraient être supprimées via ON DELETE CASCADE côté SQL,
// sinon il faudrait les supprimer ici manuellement)
$stmt = $pdo->prepare("DELETE FROM pf_holidays_ideas WHERE id = :id");
$stmt->execute([':id' => (int)$_POST['id']]);
header("Location: /holidays.php");
exit;
$stmt->execute([':id' => $id]);
}
hol_redirect(0); // Retour liste
break;
}
// --- SOUS-ÉLÉMENTS (Transport, Logement, Activités, Budget) ---
case 'add_transport': {
$id = (int)$_POST['idea_id'];
$stmt = $pdo->prepare("
INSERT INTO pf_holidays_transport (idea_id, mode, duration_min, cost, co2_kg, link, notes)
VALUES (:id,:mode,:dur,:cost,:co2,:link,:notes)
");
$stmt->execute([
':id' => (int)$_POST['idea_id'],
':id' => $id,
':mode' => strtoupper($_POST['mode'] ?? 'OTHER'),
':dur' => ($_POST['duration_min'] !== '' ? (int)$_POST['duration_min'] : null),
':cost' => hol_norm_decimal($_POST['cost'] ?? null),
':co2' => hol_norm_decimal($_POST['co2_kg'] ?? null),
':link' => trim($_POST['link'] ?? ''),
':notes'=> trim($_POST['notes'] ?? ''),
':notes' => trim($_POST['notes'] ?? ''),
]);
hol_back();
hol_redirect($id);
break;
}
case 'add_lodging': {
$id = (int)$_POST['idea_id'];
$stmt = $pdo->prepare("
INSERT INTO pf_holidays_lodging (idea_id, type, location_text, price_per_n, nights, free_cancel, family_friendly, link, notes)
VALUES (:id,:type,:loc,:ppn,:n,:fc,:ff,:link,:notes)
");
$stmt->execute([
':id' => (int)$_POST['idea_id'],
':id' => $id,
':type' => strtoupper($_POST['type'] ?? 'OTHER'),
':loc' => trim($_POST['location_text'] ?? ''),
':ppn' => hol_norm_decimal($_POST['price_per_n'] ?? null),
@@ -147,16 +178,18 @@ try {
':link' => trim($_POST['link'] ?? ''),
':notes'=> trim($_POST['notes'] ?? ''),
]);
hol_back();
hol_redirect($id);
break;
}
case 'add_activity': {
$id = (int)$_POST['idea_id'];
$stmt = $pdo->prepare("
INSERT INTO pf_holidays_activities (idea_id, name, kind, cost_est, need_booking, weather, link, notes)
VALUES (:id,:name,:kind,:cost,:need,:weather,:link,:notes)
");
$stmt->execute([
':id' => (int)$_POST['idea_id'],
':id' => $id,
':name' => trim($_POST['name'] ?? ''),
':kind' => trim($_POST['kind'] ?? ''),
':cost' => hol_norm_decimal($_POST['cost_est'] ?? null),
@@ -165,22 +198,25 @@ try {
':link' => trim($_POST['link'] ?? ''),
':notes' => trim($_POST['notes'] ?? ''),
]);
hol_back();
hol_redirect($id);
break;
}
case 'add_budget': {
$id = (int)$_POST['idea_id'];
$stmt = $pdo->prepare("
INSERT INTO pf_holidays_budget_items (idea_id, category, label, amount, per_person)
VALUES (:id,:cat,:label,:amt,:pp)
");
$stmt->execute([
':id' => (int)$_POST['idea_id'],
':id' => $id,
':cat' => strtoupper($_POST['category'] ?? 'OTHER'),
':label' => trim($_POST['label'] ?? ''),
':amt' => hol_norm_decimal($_POST['amount'] ?? null) ?? 0.0,
':pp' => isset($_POST['per_person']) ? 1 : 0,
]);
hol_back();
hol_redirect($id);
break;
}
default:
@@ -188,7 +224,11 @@ try {
echo 'Unknown action';
exit;
}
} catch (Throwable $e) {
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>';
}
+28 -8
View File
@@ -1,4 +1,6 @@
<?php
// modules/holidays/view.php
require __DIR__ . '/../../includes/auth.php';
require_login('/login.php');
require __DIR__ . '/../../includes/db.php';
@@ -6,20 +8,38 @@ require __DIR__ . '/../../includes/db.php';
header('Content-Type: application/json; charset=utf-8');
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) {
http_response_code(400);
echo json_encode(['error' => 'bad id']);
echo json_encode(['error' => 'invalid_id']);
exit;
}
$st = $pdo->prepare("SELECT * FROM pf_holidays_ideas WHERE id = ?");
$st->execute([$id]);
$it = $st->fetch(PDO::FETCH_ASSOC);
try {
$st = $pdo->prepare("SELECT * FROM pf_holidays_ideas WHERE id = ?");
$st->execute([$id]);
$it = $st->fetch(PDO::FETCH_ASSOC);
if (!$it) {
if (!$it) {
http_response_code(404);
echo json_encode(['error' => 'not found']);
echo json_encode(['error' => 'not_found']);
exit;
}
}
echo json_encode($it, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// Amélioration : Typage explicite pour le JSON
// Cela évite que JS reçoive "4" (string) au lieu de 4 (int) pour les calculs
$it['id'] = (int)$it['id'];
if (isset($it['lat'])) $it['lat'] = (float)$it['lat'];
if (isset($it['lng'])) $it['lng'] = (float)$it['lng'];
if (isset($it['ideal_days'])) $it['ideal_days'] = (int)$it['ideal_days'];
// On s'assure que les null restent null et pas des chaines vides si la DB est stricte
// (Optionnel mais propre)
echo json_encode($it, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode(['error' => 'server_error']);
}