refacto holidays

This commit is contained in:
2026-02-09 15:29:12 +01:00
parent 43bcdc83a1
commit d557636eee
5 changed files with 704 additions and 1395 deletions
+299 -379
View File
@@ -2,15 +2,17 @@
/* --- 1. VARIABLES & BASE --- */
:root {
--primary: #2563eb; /* Bleu vibrant */
--primary: #2563eb;
--primary-hover: #1d4ed8;
--bg-page: #f1f5f9; /* Gris-bleu très pâle */
--bg-page: #f1f5f9;
--bg-card: #ffffff;
--text-main: #1e293b; /* Gris très foncé (pas noir) */
--text-main: #1e293b;
--text-muted: #64748b;
--radius-l: 16px;
--radius-m: 10px;
--radius-s: 6px;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg:
@@ -27,24 +29,10 @@
-apple-system,
sans-serif;
color: var(--text-main);
padding-bottom: 50px;
}
/* Titres */
.pf-holidays h1 {
font-size: 1.8rem;
font-weight: 800;
letter-spacing: -0.025em;
color: #0f172a;
margin-bottom: 0.5rem;
}
.pf-holidays p {
font-size: 0.95rem;
color: var(--text-muted);
line-height: 1.5;
}
/* --- 2. EN-TÊTE (TITLEBAR) --- */
/* --- 2. EN-TÊTE --- */
.pf-holidays__titlebar {
background: transparent;
padding-bottom: 1rem;
@@ -57,17 +45,24 @@
gap: 1rem;
}
.pf-holidays__titlebar h1 {
font-size: 1.8rem;
font-weight: 800;
color: #0f172a;
margin: 0;
}
.hol-title-actions {
display: flex;
gap: 12px;
}
/* --- 3. BOUTONS MODERNES --- */
.btn,
/* --- 3. BOUTONS --- */
.hol-add-btn,
.hol-map-toggle {
.hol-map-toggle,
.pf-btn {
border: none;
border-radius: 50px; /* Pill shape */
border-radius: 50px;
padding: 10px 20px;
font-size: 0.9rem;
font-weight: 600;
@@ -76,21 +71,21 @@
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
box-shadow: var(--shadow-sm);
text-decoration: none;
}
/* Bouton principal (Ajouter) */
.hol-add-btn {
.hol-add-btn,
.pf-btn {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: white;
}
.hol-add-btn:hover {
.hol-add-btn:hover,
.pf-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
}
/* Bouton Carte */
.hol-map-toggle {
background: white;
color: #0f766e;
@@ -98,53 +93,44 @@
}
.hol-map-toggle:hover {
background: #f0fdfa;
border-color: #99f6e4;
color: #0d9488;
border-color: #99f6e4;
}
/* Boutons classiques */
.btn {
.pf-btn.btn-secondary {
background: white;
color: var(--text-main);
color: var(--text-muted);
border: 1px solid #cbd5e1;
}
.btn:hover {
background: #f8fafc;
border-color: #94a3b8;
}
/* Actions contextuelles (edit/delete) dans les cards */
.btn-edit,
.btn-delete {
border-radius: var(--radius-s);
padding: 6px 12px;
font-size: 0.8rem;
box-shadow: none;
}
.btn-edit {
.pf-btn.btn-secondary:hover {
background: #f8fafc;
color: var(--text-main);
}
/* Petit bouton + pour les colonnes */
.btn-icon-small {
background: transparent;
border: 1px dashed #cbd5e1;
border-radius: 4px;
padding: 6px 12px;
font-size: 0.8rem;
color: var(--text-muted);
cursor: pointer;
width: 100%;
margin-top: 10px;
transition: 0.2s;
}
.btn-icon-small:hover {
background: #f1f5f9;
color: #475569;
border: none;
}
.btn-edit:hover {
background: #e2e8f0;
color: #1e293b;
color: var(--text-main);
border-color: var(--text-muted);
}
.btn-delete {
background: #fef2f2;
color: #dc2626;
border: none;
}
.btn-delete:hover {
background: #fee2e2;
color: #b91c1c;
}
/* --- 4. GRILLE & CARTES (Le cœur du design) --- */
/* --- 4. CARTES --- */
.hol-ideas-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 24px;
margin-bottom: 40px;
}
@@ -159,52 +145,31 @@
transition:
transform 0.25s ease,
box-shadow 0.25s ease;
border: 1px solid transparent; /* Pour éviter le saut au hover */
border: 1px solid transparent;
cursor: pointer;
position: relative;
overflow: hidden;
}
/* Effet au survol : la carte "flotte" */
.hol-idea-card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-hover);
border-color: #e2e8f0;
}
/* Bande de couleur décorative en haut de carte selon statut (Optionnel mais joli) */
.hol-idea-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: #e2e8f0; /* Default */
}
.hol-idea-card:has(.hol-status--planned)::before {
background: #22c55e;
}
.hol-idea-card:has(.hol-status--favorite)::before {
background: #f59e0b;
}
.hol-idea-card:has(.hol-status--shortlist)::before {
background: #3b82f6;
}
.hol-idea-card__head {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
margin-bottom: 8px;
}
.hol-idea-card h3 {
margin: 0;
font-size: 1.15rem;
font-weight: 700;
line-height: 1.3;
color: var(--text-main);
margin-right: 8px;
line-height: 1.3;
}
.hol-idea-meta {
@@ -216,355 +181,310 @@
gap: 4px;
}
/* Notes stylisées */
.hol-notes {
background: #fffbeb; /* Jaune très pâle, style post-it */
color: #78350f;
padding: 10px 12px;
border-radius: var(--radius-m);
font-size: 0.85rem;
font-style: italic;
margin-bottom: 16px;
flex-grow: 1;
border-left: 3px solid #fcd34d;
}
.hol-card-actions {
margin-top: auto;
display: flex;
gap: 8px;
padding-top: 16px;
border-top: 1px solid #f1f5f9;
}
/* Statuts (Badges Modernes) */
.hol-status {
padding: 4px 10px;
border-radius: 20px;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.hol-status--planned {
background: #dcfce7;
color: #15803d;
} /* Vert */
.hol-status--favorite {
background: #fff7ed;
color: #c2410c;
border: 1px solid #ffedd5;
} /* Orange */
.hol-status--shortlist {
background: #eff6ff;
color: #1d4ed8;
} /* Bleu */
.hol-status--draft {
background: #f1f5f9;
color: #64748b;
} /* Gris */
.hol-status--archived {
background: #f8fafc;
color: #94a3b8;
text-decoration: line-through;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin: 0;
font-style: italic;
}
/* Style spécifique pour les archivés */
.hol-idea-card--archived {
background: #f8fafc;
box-shadow: none;
border: 1px solid #e2e8f0;
opacity: 0.8;
/* --- 5. UTILITAIRES STATUTS --- */
.bg-green-100 {
background-color: #dcfce7;
}
/* --- 5. VUE DÉTAIL (Panels) --- */
.pf-section--panel {
background: white;
border-radius: var(--radius-l);
box-shadow: var(--shadow-sm);
padding: 24px;
margin-bottom: 24px;
border: 1px solid #f1f5f9;
.text-green-800 {
color: #166534;
}
.pf-section--panel h2 {
font-size: 1.25rem;
color: var(--text-main);
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
.bg-blue-100 {
background-color: #dbeafe;
}
/* Petite ligne décorative sous le titre h2 */
.pf-section--panel h2::after {
content: "";
flex: 1;
height: 1px;
background: #e2e8f0;
margin-left: 12px;
}
.hol-list li {
padding: 12px 0;
border-bottom: 1px solid #f1f5f9;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.hol-list li:last-child {
border-bottom: none;
}
.hol-budget-summary {
background: linear-gradient(to right, #eff6ff, #ffffff);
border: 1px solid #dbeafe;
.text-blue-800 {
color: #1e40af;
padding: 16px;
border-radius: var(--radius-m);
font-weight: 600;
display: flex;
justify-content: space-between;
margin-bottom: 16px;
}
.bg-yellow-50 {
background-color: #fefce8;
}
.text-yellow-800 {
color: #854d0e;
}
.bg-gray-100 {
background-color: #f3f4f6;
}
.text-gray-600 {
color: #4b5563;
}
/* Formulaires Inline (Détail) */
.hol-inline-form {
background: #f8fafc;
padding: 12px;
border-radius: var(--radius-m);
border: 1px dashed #cbd5e1;
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
margin-bottom: 20px;
}
.hol-inline-form input,
.hol-inline-form select {
border: 1px solid #cbd5e1;
border-radius: var(--radius-s);
padding: 8px 12px;
font-size: 0.9rem;
transition: border-color 0.2s;
}
.hol-inline-form input:focus,
.hol-inline-form select:focus {
border-color: var(--primary);
outline: 2px solid rgba(37, 99, 235, 0.1);
}
.hol-inline-form button {
background: #334155;
color: white;
border: none;
padding: 8px 16px;
border-radius: var(--radius-s);
cursor: pointer;
}
.hol-inline-form button:hover {
background: #1e293b;
}
/* --- 6. MODALES MODERNES (Glassmorphism léger) --- */
.hol-modal {
/* --- 6. MODALE & FORMULAIRE (Refonte Centrage & Largeur) --- */
.pf-modal {
display: none;
}
.hol-modal.open {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
inset: 0;
z-index: 9999;
align-items: center; /* Centrage Vertical */
justify-content: center; /* Centrage Horizontal */
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(4px);
}
.pf-modal.open {
display: flex;
}
.hol-backdrop {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.6); /* Plus sombre */
backdrop-filter: blur(4px); /* Flou d'arrière plan */
}
.hol-dialog {
.pf-modal-content {
position: relative;
background: white;
width: min(600px, 90vw);
width: 95%; /* Responsive */
max-width: 1100px; /* Largeur augmentée pour 3 colonnes */
max-height: 90vh;
overflow-y: auto;
border-radius: 20px; /* Très arrondi */
border-radius: 20px;
box-shadow: var(--shadow-lg);
z-index: 10;
animation: modalPop 0.3s cubic-bezier(0.16, 1, 0.3, 1);
padding: 32px;
display: flex;
flex-direction: column;
margin: auto; /* Sécurité centrage */
}
@keyframes modalPop {
from {
transform: scale(0.95) translateY(10px);
opacity: 0;
}
to {
transform: scale(1) translateY(0);
opacity: 1;
}
}
/* Modale Carte */
/* Cas spécifique modale carte */
.hol-dialog--map {
width: 95vw;
height: 90vh;
max-width: 1400px;
padding: 0;
background: white;
border-radius: 12px;
display: flex;
flex-direction: column;
}
/* Formulaires Modale */
.hol-form {
padding: 32px;
display: flex;
flex-direction: column;
gap: 16px;
}
.hol-form h3 {
font-size: 1.5rem;
margin-bottom: 12px;
color: var(--text-main);
}
.hol-form label {
/* Inputs généraux */
.pf-label {
display: block;
font-weight: 600;
font-size: 0.85rem;
color: #475569;
margin-bottom: 4px;
color: var(--text-muted);
margin-bottom: 6px;
}
.hol-form input,
.hol-form select,
.hol-form textarea {
.pf-input {
width: 100%;
padding: 10px 12px;
border: 1px solid #cbd5e1;
border-radius: var(--radius-m);
font-size: 1rem;
background: #f8fafc;
box-sizing: border-box; /* Important */
background: #ffffff; /* Fond blanc forcé */
color: var(--text-main);
box-sizing: border-box;
transition: all 0.2s;
}
.hol-form input:focus,
.hol-form select:focus,
.hol-form textarea:focus {
.pf-input:focus {
background: white;
border-color: var(--primary);
outline: 4px solid rgba(37, 99, 235, 0.1);
outline: none;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.hol-inline {
.form-row {
display: flex;
gap: 16px;
}
.hol-inline > label {
flex: 1;
flex-wrap: wrap;
margin-bottom: 16px;
}
.hol-actions {
margin-top: 16px;
/* --- 7. SECTIONS DE DÉTAILS (Transport/Hotel/Activité) --- */
.hol-columns-wrapper {
display: flex; /* Flexbox */
flex-direction: column; /* Alignement vertical (les uns sous les autres) */
gap: 24px; /* Espace entre les blocs */
width: 100%;
}
.hol-col {
background: #f8fafc;
padding: 20px;
border-radius: 12px;
border: 1px solid #e2e8f0;
width: 100%; /* Prend toute la largeur disponible */
box-sizing: border-box; /* Empêche le padding de casser la largeur */
}
.hol-col-header {
display: flex;
justify-content: space-between; /* Titre à gauche, bouton à droite */
align-items: center;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 8px;
margin-bottom: 12px;
}
.hol-col-header h4 {
margin: 0;
font-size: 1rem;
text-transform: uppercase;
font-weight: 700;
/* On retire les bordures/marges de l'ancien h4 car c'est le header qui gère */
border: none;
padding: 0;
}
/* Le bouton "+" style Gift List */
.btn-add-item {
/* Style de base */
background: rgba(255, 255, 255, 0.6);
color: var(--text-muted);
border: 1px solid rgba(0, 0, 0, 0.1);
/* Forme et Taille */
width: 28px;
height: 28px;
border-radius: 50%;
/* Texte */
font-size: 18px;
line-height: 1;
/* Flex pour centrer le + */
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
/* Animation fluide "Cubic Bezier" copiée de gift-list */
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.btn-add-item:hover {
background: white;
color: var(--primary); /* On ajoute la couleur primaire au survol */
/* L'effet de rotation et d'agrandissement */
transform: rotate(90deg) scale(1.1);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
border-color: currentColor;
}
/* --- 8. ITEMS DYNAMIQUES (Inputs optimisés) --- */
.savings-line-item {
display: flex;
align-items: center;
gap: 8px; /* Espace réduit */
margin-bottom: 10px;
background: white;
padding: 8px;
border-radius: 8px;
border: 1px solid #f1f5f9;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
}
/* Champ NOM : prend toute la place */
.savings-line-item input[type="text"] {
flex-grow: 1;
min-width: 120px;
padding: 6px 10px;
font-size: 0.9rem;
}
/* Champ PRIX : compact */
.savings-line-item input[type="number"] {
width: 120px !important;
padding: 6px 8px !important;
text-align: right;
font-weight: 600;
color: #059669;
}
/* Checkbox Payé */
.savings-line-item label {
display: flex;
align-items: center;
font-size: 0.75rem;
color: #64748b;
gap: 4px;
cursor: pointer;
white-space: nowrap;
}
/* Bouton Supprimer (Croix) */
.btn-remove {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
color: #ef4444;
border: none;
border-radius: 4px;
font-size: 1.2rem;
cursor: pointer;
line-height: 1;
transition: background 0.2s;
}
.btn-remove:hover {
background: #fef2f2;
color: #b91c1c;
}
/* --- 9. FOOTER MODALE --- */
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.hol-cancel {
background: transparent;
color: var(--text-muted);
border: none;
font-weight: 600;
}
.hol-cancel:hover {
color: var(--text-main);
background: #f1f5f9;
gap: 10px;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #e2e8f0;
}
.hol-ok {
background: var(--primary);
color: white;
border: none;
padding: 10px 24px;
border-radius: 50px;
font-weight: 600;
box-shadow: 0 4px 6px rgba(37, 99, 235, 0.2);
}
.hol-ok:hover {
background: var(--primary-hover);
transform: translateY(-1px);
}
/* --- 7. PICKER GEOCODE --- */
.hol-geocode-picker {
background: white;
border: 1px solid #e2e8f0;
border-radius: var(--radius-m);
box-shadow: var(--shadow-lg);
margin-top: 8px;
overflow: hidden;
}
.hol-geocode-picker__header {
background: #f8fafc;
padding: 8px 12px;
font-weight: 600;
font-size: 0.85rem;
display: flex;
justify-content: space-between;
}
.hol-geocode-picker__item {
padding: 10px 12px;
border-bottom: 1px solid #f1f5f9;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.hol-geocode-picker__item:hover {
background: #f0f9ff;
}
.hol-geocode-picker__item button {
background: white;
border: 1px solid #cbd5e1;
border-radius: 4px;
font-size: 0.75rem;
padding: 4px 8px;
}
/* --- 8. MOBILE RESPONSIVE --- */
/* --- 10. MOBILE --- */
@media (max-width: 768px) {
.hol-inline {
.pf-holidays__titlebar {
flex-direction: column;
gap: 12px;
}
.hol-inline-form {
flex-direction: column;
align-items: stretch;
}
.hol-inline-form button {
margin-top: 8px;
width: 100%;
}
.pf-holidays h1 {
font-size: 1.5rem;
}
.hol-idea-card {
padding: 16px;
}
.hol-ideas-grid {
gap: 16px;
}
.btn,
.hol-add-btn,
.hol-map-toggle {
width: 100%; /* Boutons pleine largeur sur mobile */
margin-bottom: 8px;
align-items: flex-start;
}
.hol-title-actions {
flex-direction: column;
width: 100%;
gap: 4px;
}
.hol-add-btn,
.hol-map-toggle {
width: 100%;
}
.form-row {
flex-direction: column;
gap: 10px;
}
/* Sur mobile, les colonnes s'empilent */
.hol-columns-wrapper {
grid-template-columns: 1fr;
}
.pf-modal-content {
padding: 16px;
width: 100%;
height: 100%;
max-height: 100vh;
border-radius: 0;
}
}
/* --- 11. CALENDRIER & DATES (Force Light Theme) --- */
input[type="date"] {
color-scheme: light; /* Force le popup blanc */
background-color: #ffffff;
color: #1e293b;
}
/* Cible spécifique pour l'icône calendrier (Chrome/Edge/Safari) */
input[type="date"]::-webkit-calendar-picker-indicator {
cursor: pointer;
opacity: 0.6;
filter: invert(0);
}
input[type="date"]::-webkit-calendar-picker-indicator:hover {
opacity: 1;
}
+172 -358
View File
@@ -1,360 +1,174 @@
// modules/holidays/holidays.js
document.addEventListener("DOMContentLoaded", () => {
// --- 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" && modal.classList.contains("open")) {
close();
}
});
return { modal, open, close };
}
/**
* Échappe les caractères HTML pour éviter les failles XSS simples
*/
function esc(s) {
if (s === null || s === undefined) return "";
return String(s)
.replace(/&/g, "&")
.replace(/</g, "&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" } },
);
if (!res.ok) throw new Error("Erreur HTTP " + res.status);
const it = await res.json();
const modal = editModalCtrl.modal;
// Helper pour remplir les champs
const setVal = (sel, val) => {
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-status", it.status || "draft");
setVal("#edit-notes", it.notes);
editModalCtrl.open();
} catch (err) {
console.error(err);
alert("Impossible de charger les données de l'idée.");
}
}
// Écouteurs sur les boutons "Éditer" (Liste + Détail)
document.body.addEventListener("click", (e) => {
// Utilisation de la délégation d'événement pour gérer tous les boutons (même dynamiques)
const btn = e.target.closest(".btn-edit");
if (btn && btn.hasAttribute("data-edit-id")) {
const id = btn.getAttribute("data-edit-id");
openEditForId(id);
}
// Cas spécifique du bouton dans le header de la vue détail
else if (e.target.id === "hol-edit-open") {
const id = e.target.getAttribute("data-edit-id");
openEditForId(id);
}
});
// --- 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 (
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";
form.innerHTML = `
<input type="hidden" name="action" value="delete_idea">
<input type="hidden" name="id" value="${id}">
`;
document.body.appendChild(form);
form.submit();
}
}
});
// --- 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 latInput,
lngInput,
qParts = [];
// Récupération des inputs selon le scope
if (scope === "edit") {
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 {
// 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 = qParts.filter(Boolean).join(", ");
if (!q) {
alert(
"Veuillez renseigner au moins une Ville ou un Pays pour géocoder.",
);
return;
}
// UI : chargement
removeNearbyPicker(btn);
btn.disabled = true;
const originalText = btn.textContent;
btn.textContent = "⏳...";
try {
const res = await fetch(
`/modules/holidays/geocode.php?q=${encodeURIComponent(q)}&limit=5`,
{
headers: { Accept: "application/json" },
},
);
const data = await res.json();
if (!res.ok) throw new Error(data?.error || "Erreur 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;
}
// 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 trouvé pour : " + q);
}
} catch (err) {
console.error(err);
alert("Erreur lors du géocodage.");
} finally {
btn.disabled = false;
btn.textContent = originalText;
}
});
});
function renderGeocodePicker(anchorBtn, results, onPick) {
removeNearbyPicker(anchorBtn);
const wrapper = document.createElement("div");
wrapper.className = "hol-geocode-picker";
// Header
const header = document.createElement("div");
header.className = "hol-geocode-picker__header";
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";
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(list);
// Insertion après le conteneur du bouton
const container =
anchorBtn.closest(".hol-inline") || anchorBtn.parentElement;
container.insertAdjacentElement("afterend", wrapper);
}
function removeNearbyPicker(anchorBtn) {
const container =
anchorBtn.closest(".hol-inline") || anchorBtn.parentElement;
const next = container?.nextElementSibling;
if (next && next.classList.contains("hol-geocode-picker")) {
next.remove();
}
}
// --- 6. CARTE LEAFLET ---
let mapInitialized = false;
let mapInstance;
// Fonction d'initialisation de la carte (appelée à l'ouverture de la modale)
function initMap() {
if (mapInitialized) {
// Si déjà init, on force juste le redimensionnement pour éviter les bugs d'affichage
setTimeout(() => mapInstance.invalidateSize(), 200);
return;
}
if (typeof L === "undefined") {
console.error("Leaflet n'est pas chargé.");
return;
}
// Récupération sécurisée des données injectées par PHP
const MAP_DATA = Array.isArray(window.HOL_MAP_DATA)
? window.HOL_MAP_DATA
: [];
mapInstance = L.map("hol-map", { scrollWheelZoom: true });
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap",
}).addTo(mapInstance);
const markers = [];
MAP_DATA.forEach((it) => {
const lat = parseFloat(it.lat);
const lng = parseFloat(it.lng);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
// Code couleur selon statut
const colors = {
planned: "#16a34a",
favorite: "#f59e0b",
shortlist: "#3b82f6",
default: "#6b7280",
};
const color = colors[it.status] || colors.default;
const m = L.circleMarker([lat, lng], {
radius: 7,
color: "#ffffff",
weight: 1,
fillColor: color,
fillOpacity: 0.9,
}).addTo(mapInstance);
// Construction du popup
const loc = [it.city, it.region, it.country].filter(Boolean).join(", ");
const dates = it.desired_start_date
? `${it.desired_start_date}${it.desired_end_date ? " → " + it.desired_end_date : ""}`
: null;
m.bindPopup(`
<div class="hol-map-popup">
<strong>${esc(it.title)}</strong>
<div style="font-size:0.9em; color:#666;">${esc(loc)}</div>
${dates ? `<div style="font-size:0.85em; margin-top:4px;">📅 ${esc(dates)}</div>` : ""}
<div style="margin-top:8px;">
<span class="hol-status hol-status--${esc(it.status)}">${esc(it.status)}</span>
<a href="/holidays.php?id=${it.id}" style="margin-left:8px;">Voir</a>
</div>
</div>
`);
markers.push(m);
});
// Centrage de la carte
if (markers.length > 0) {
const group = L.featureGroup(markers);
mapInstance.fitBounds(group.getBounds(), { padding: [50, 50] });
} else {
mapInstance.setView([46.603354, 1.888334], 5); // France par défaut si vide
}
mapInitialized = true;
// Hack indispensable pour que Leaflet calcule la bonne taille dans une modale
setTimeout(() => mapInstance.invalidateSize(), 200);
}
// Connexion de la carte à la modale
const mapBtn = document.getElementById("hol-map-open");
if (mapBtn) {
const mapModalCtrl = setupModal("hol-map-modal", initMap); // On passe initMap en callback d'ouverture
mapBtn.addEventListener("click", mapModalCtrl.open);
}
// Initialisation des écouteurs globaux si besoin
});
// --- 1. GESTION DE LA MODALE D'ÉDITION ---
function openHolidayModal(mode) {
const modal = document.getElementById("holidayModal");
const form = document.getElementById("holidayForm");
const btnDelete = document.getElementById("btn_delete");
// Reset complet
form.reset();
document.getElementById("inp_id").value = "";
document.getElementById("list_transport").innerHTML = "";
document.getElementById("list_accommodation").innerHTML = "";
document.getElementById("list_activity").innerHTML = "";
if (mode === "add") {
document.getElementById("modalTitle").innerText = "Nouveau Voyage";
btnDelete.style.display = "none";
} else {
document.getElementById("modalTitle").innerText = "Modifier le voyage";
btnDelete.style.display = "block";
}
modal.classList.add("open");
modal.style.display = "flex";
// --- AJOUT : FORCER LE SCROLL EN HAUT ---
const content = modal.querySelector(".pf-modal-content");
if (content) {
content.scrollTop = 0;
}
// Optionnel : Mettre le focus sur le premier champ (Titre)
// setTimeout(() => document.getElementById('inp_title').focus(), 50);
}
function closeHolidayModal() {
const modal = document.getElementById("holidayModal");
modal.classList.remove("open");
modal.style.display = "none";
}
// Fonction appelée au clic sur une carte (injectée par PHP)
function editHoliday(data) {
openHolidayModal("edit");
const h = data.main;
// Remplissage des champs principaux
document.getElementById("inp_id").value = h.id;
document.getElementById("inp_title").value = h.title;
document.getElementById("inp_status").value = h.status;
document.getElementById("inp_period").value = h.period_hint;
document.getElementById("inp_start").value = h.start_date;
document.getElementById("inp_end").value = h.end_date;
document.getElementById("inp_food").value =
h.budget_food > 0 ? h.budget_food : "";
document.getElementById("inp_extra").value =
h.budget_extra > 0 ? h.budget_extra : "";
document.getElementById("inp_notes").value = h.notes;
// Remplissage des listes dynamiques
if (data.items && data.items.length > 0) {
data.items.forEach((item) => {
addItem(item.category, item.name, item.amount, item.is_paid);
});
}
}
// --- 2. GESTION DES LISTES DYNAMIQUES (Transport, etc.) ---
function addItem(category, name = "", amount = "", isPaid = 0) {
const container = document.getElementById("list_" + category);
const div = document.createElement("div");
// Utilisation des classes CSS définies précédemment
div.className = "savings-line-item";
// Checkbox logique pour "Payé"
const checkedAttr = isPaid == 1 ? "checked" : "";
div.innerHTML = `
<input type="hidden" name="items[cat][]" value="${category}">
<input type="text" name="items[name][]" class="pf-input"
placeholder="Nom (ex: Vol)" value="${name}"
style="flex: 2; min-width: 0;">
<input type="number" step="0.01" name="items[amount][]" class="pf-input"
placeholder="€" value="${amount}"
style="width: 80px; text-align: right;">
<label title="Déjà payé ?" style="display: flex; align-items: center; cursor: pointer; padding: 0 5px;">
<input type="checkbox" ${checkedAttr} onchange="this.nextElementSibling.value = this.checked ? 1 : 0">
<input type="hidden" name="items[paid][]" value="${isPaid}">
<span style="font-size:0.8rem; margin-left:4px;">Payé</span>
</label>
<button type="button" class="btn-remove" onclick="this.parentElement.remove()" title="Supprimer">
&times;
</button>
`;
container.appendChild(div);
}
function deleteHoliday() {
if (!confirm("Supprimer définitivement ce voyage ?")) return;
const form = document.getElementById("holidayForm");
// On ajoute un input caché pour signaler la suppression au PHP
const input = document.createElement("input");
input.type = "hidden";
input.name = "action_delete";
input.value = "1";
form.appendChild(input);
form.submit();
}
// --- 3. GESTION DE LA CARTE (Leaflet) ---
let map = null;
function toggleMap() {
const modal = document.getElementById("hol-map-modal");
if (modal.style.display === "flex") {
modal.style.display = "none";
} else {
modal.style.display = "flex";
// Petit délai pour laisser le temps au DOM de s'afficher avant d'init Leaflet
setTimeout(initMap, 100);
}
}
function initMap() {
if (map) {
map.invalidateSize(); // Recalcule la taille si la fenêtre a changé
return;
}
if (typeof L === "undefined") return;
// Centré sur l'Europe par défaut
map = L.map("hol-map").setView([46.6, 2.4], 4);
L.tileLayer(
"https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png",
{
attribution: "© OpenStreetMap",
},
).addTo(map);
// On récupère les points définis en global dans le PHP
if (typeof HOL_MAP_POINTS !== "undefined") {
HOL_MAP_POINTS.forEach((pt) => {
const color =
pt.status === "planned" || pt.status === "booked" ? "green" : "blue";
// Cercle simple
L.circleMarker([pt.lat, pt.lng], {
color: color,
radius: 8,
fillOpacity: 0.8,
})
.addTo(map)
.bindPopup(`<b>${pt.title}</b><br>${pt.status}`);
});
}
}
+182 -439
View File
@@ -1,459 +1,202 @@
<?php
// modules/holidays/index.php
// ------------------------------------------------------------------
// 1. LOGIQUE PHP : RÉCUPÉRATION DES DONNÉES
// ------------------------------------------------------------------
// 1. Récupération des voyages + Calcul du coût total
$sql = "
SELECT h.*,
(
COALESCE(h.budget_food, 0) +
COALESCE(h.budget_extra, 0) +
COALESCE((SELECT SUM(amount) FROM pf_holidays_items WHERE holiday_id = h.id), 0)
) as total_cost
FROM pf_holidays h
ORDER BY FIELD(status, 'booked', 'planned', 'draft', 'passed', 'archived'),
start_date ASC
";
$holidays = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
if (!function_exists('hol_q')) {
function hol_q(PDO $pdo, string $sql, array $params = []): array {
$st = $pdo->prepare($sql);
$st->execute($params);
return $st->fetchAll(PDO::FETCH_ASSOC);
}
}
// Tri par statut
$active = array_filter($holidays, fn($h) => in_array($h['status'], ['draft', 'planned', 'booked']));
$history = array_filter($holidays, fn($h) => in_array($h['status'], ['passed', 'archived']));
?>
$ideaId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
$isDetailView = ($ideaId > 0);
$currentIdea = null; // Stockera les données si on est en vue détail
<div class="pf-holidays">
// Préparation des variables par défaut pour éviter les erreurs "undefined variable"
$transport = []; $lodging = []; $acts = []; $budget = [];
$fixedTotal = 0; $ppTotal = 0;
$planned = []; $ideas = []; $archived = [];
<div class="pf-holidays__titlebar">
<h1>Mes Vacances ✈️</h1>
<div class="hol-title-actions">
<button class="hol-add-btn" onclick="openHolidayModal('add')">+ Créer un voyage</button>
</div>
</div>
if ($isDetailView) {
// --- MODE DÉTAIL ---
<section class="pf-section">
<div class="hol-ideas-grid">
<?php if (empty($active)): ?>
<p style="color:var(--text-muted); font-style:italic;">Aucun voyage en cours. Planifions quelque chose !</p>
<?php endif; ?>
// 1. Récupérer l'idée principale
$rows = hol_q($pdo, "SELECT * FROM pf_holidays_ideas WHERE id = ?", [$ideaId]);
$currentIdea = $rows[0] ?? null;
<?php foreach ($active as $h): ?>
<?php renderHolidayCard($h, $pdo); ?>
<?php endforeach; ?>
</div>
</section>
if ($currentIdea) {
// 2. Récupérer les données pour la CARTE (un seul point)
// Le JS filtrera lat/lng invalides
$mapIdeas = hol_q($pdo, "
SELECT id, title, country, region, city,
CAST(lat AS DECIMAL(9,6)) AS lat,
CAST(lng AS DECIMAL(9,6)) AS lng,
status, desired_start_date, desired_end_date
FROM pf_holidays_ideas
WHERE id = ?
", [$ideaId]);
<?php if (!empty($history)): ?>
<section class="pf-section" style="margin-top: 40px; border-top: 1px solid #e2e8f0; padding-top: 20px;">
<h3 style="color:var(--text-muted);">Historique</h3>
<div class="hol-ideas-grid" style="opacity: 0.7;">
<?php foreach ($history as $h): ?>
<?php renderHolidayCard($h, $pdo); ?>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
// 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]);
</div>
// 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];
<div id="holidayModal" class="pf-modal">
<div class="pf-modal-content" style="max-width: 800px; width: 95%;">
<h3 id="modalTitle" class="pf-modal-title">Planifier le voyage</h3>
$fixedTotal = (float)($sumRow['fixed_total'] ?? 0);
$ppTotal = (float)($sumRow['per_person_total'] ?? 0);
<form action="/modules/holidays/save_new.php" method="POST" id="holidayForm">
<input type="hidden" name="id" id="inp_id">
<input type="hidden" name="action" value="save">
<div class="form-row">
<div style="flex: 2;">
<label class="pf-label">Nom du voyage</label>
<input type="text" name="title" id="inp_title" class="pf-input" placeholder="Ex: Octobre - Portugal" required>
</div>
<div style="flex: 1;">
<label class="pf-label">Statut</label>
<select name="status" id="inp_status" class="pf-input">
<option value="draft">Brouillon ✏️</option>
<option value="planned">Planifié 📅</option>
<option value="booked">Réservé ✅</option>
<option value="passed">Passé 👋</option>
<option value="archived">Archivé 🗄️</option>
</select>
</div>
</div>
<div class="form-row">
<div>
<label class="pf-label">Période (Texte libre)</label>
<input type="text" name="period_hint" id="inp_period" class="pf-input" placeholder="Ex: Octobre 2026">
</div>
<div style="display:flex; gap:10px;">
<div style="flex:1">
<label class="pf-label">Du</label>
<input type="date" name="start_date" id="inp_start" class="pf-input" lang="fr">
</div>
<div style="flex:1">
<label class="pf-label">Au</label>
<input type="date" name="end_date" id="inp_end" class="pf-input" lang="fr">
</div>
</div>
</div>
<hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 20px 0;">
<div class="hol-columns-wrapper">
<div class="hol-col">
<div class="hol-col-header">
<h4 style="color:#2563eb;">🚗 Transport</h4>
<button type="button" class="btn-add-item" onclick="addItem('transport')" title="Ajouter un transport"></button>
</div>
<div id="list_transport" class="dynamic-list"></div>
</div>
<div class="hol-col">
<div class="hol-col-header">
<h4 style="color:#059669;">🏨 Hébergement</h4>
<button type="button" class="btn-add-item" onclick="addItem('accommodation')" title="Ajouter un hébergement"></button>
</div>
<div id="list_accommodation" class="dynamic-list"></div>
</div>
<div class="hol-col">
<div class="hol-col-header">
<h4 style="color:#d97706;">🎫 Activité</h4>
<button type="button" class="btn-add-item" onclick="addItem('activity')" title="Ajouter une activité"></button>
</div>
<div id="list_activity" class="dynamic-list"></div>
</div>
</div>
<hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 20px 0;">
<div class="form-row">
<div>
<label class="pf-label">🍔 Budget Food & Bev (€)</label>
<input type="number" step="0.01" name="budget_food" id="inp_food" class="pf-input" placeholder="0.00">
</div>
<div>
<label class="pf-label">🎁 Budget Extras (€)</label>
<input type="number" step="0.01" name="budget_extra" id="inp_extra" class="pf-input" placeholder="0.00">
</div>
</div>
<div class="form-group">
<label class="pf-label">Notes</label>
<textarea name="notes" id="inp_notes" class="pf-input" rows="2" placeholder="Idées en vrac..."></textarea>
</div>
<div class="modal-footer">
<button type="button" onclick="deleteHoliday()" id="btn_delete" class="pf-btn btn-secondary" style="color:#ef4444; border-color:#fca5a5; margin-right:auto; display:none;">Supprimer</button>
<button type="button" onclick="document.getElementById('holidayModal').style.display='none'" class="pf-btn btn-secondary">Annuler</button>
<button type="submit" class="pf-btn">Enregistrer</button>
</div>
</form>
</div>
</div>
<?php
function renderHolidayCard($h, $pdo) {
// Récupérer le détail des items pour l'affichage JS futur
$stmt = $pdo->prepare("SELECT * FROM pf_holidays_items WHERE holiday_id = ?");
$stmt->execute([$h['id']]);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
// JSON pour le JS
$json = htmlspecialchars(json_encode(['main' => $h, 'items' => $items]), ENT_QUOTES, 'UTF-8');
// Affichage des dates
$dateDisplay = htmlspecialchars($h['period_hint'] ?? '');
// MODIFICATION ICI : Format d/m/Y (jj/mm/aaaa)
if (empty($dateDisplay) && $h['start_date']) {
$dateDisplay = date('d/m/Y', strtotime($h['start_date']));
if ($h['end_date']) $dateDisplay .= ' → ' . date('d/m/Y', strtotime($h['end_date']));
}
} else {
// --- MODE LISTE ---
$statusClass = match($h['status']) {
'booked' => 'bg-green-100 text-green-800',
'planned' => 'bg-blue-100 text-blue-800',
'passed' => 'bg-gray-100 text-gray-600',
default => 'bg-yellow-50 text-yellow-800'
};
// 1. Données pour la CARTE (tous les points valides)
$mapIdeas = hol_q($pdo, "
SELECT id, title, country, region, city,
CAST(lat AS DECIMAL(9,6)) AS lat,
CAST(lng AS DECIMAL(9,6)) AS lng,
status, desired_start_date, desired_end_date
FROM pf_holidays_ideas
WHERE status IN ('draft','shortlist','favorite','planned')
AND lat IS NOT NULL
AND lng IS NOT NULL
");
echo "
<div class='hol-idea-card' onclick='editHoliday($json)'>
<div class='hol-idea-card__head'>
<h3>".htmlspecialchars($h['title'])."</h3>
<span style='font-size:0.75rem; padding:2px 8px; border-radius:12px; font-weight:bold;' class='$statusClass'>
".strtoupper($h['status'])."
</span>
</div>
<div class='hol-idea-meta'>
<span>🗓️ ".($dateDisplay ?: 'Dates à définir')."</span>
</div>
// 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
");
<div style='margin-top:auto; padding-top:10px; border-top:1px solid #f1f5f9; display:flex; justify-content:space-between; align-items:center;'>
<span style='font-size:0.85rem; color:#64748b;'>Budget Total</span>
<span style='font-size:1.1rem; font-weight:bold; color:#1e293b;'>".number_format($h['total_cost'], 0, ',', ' ')." €</span>
</div>
</div>
";
}
?>
<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>Idée introuvable</h1>
<div class="hol-title-actions">
<a class="btn" href="/holidays.php">← Retour à la liste</a>
</div>
</div>
<p>Cette idée de vacances n'existe pas ou a été supprimée.</p>
<?php else: ?>
<div class="pf-holidays__titlebar">
<h1><?= htmlspecialchars($currentIdea['title']) ?></h1>
<div class="hol-title-actions">
<a class="btn" href="/holidays.php">← Retour</a>
<button class="btn btn-edit" id="hol-edit-open" data-edit-id="<?= (int)$ideaId ?>">Éditer</button>
<button class="btn btn-delete" id="hol-delete" data-del-id="<?= (int)$ideaId ?>">Supprimer</button>
<button class="hol-map-toggle" id="hol-map-open">🌍 Carte</button>
</div>
</div>
<p class="hol-idea-meta">
<?= htmlspecialchars(trim(($currentIdea['city'] ? $currentIdea['city'] . ', ' : '') . ($currentIdea['region'] ? $currentIdea['region'] . ', ' : '') . ($currentIdea['country'] ?? ''))) ?>
<?php if (!empty($currentIdea['desired_start_date'])): ?>
• Dates: <?= htmlspecialchars($currentIdea['desired_start_date']) ?><?= !empty($currentIdea['desired_end_date']) ? ' → ' . htmlspecialchars($currentIdea['desired_end_date']) : '' ?>
<?php elseif (!empty($currentIdea['season_hint'])): ?>
• Saison: <?= htmlspecialchars($currentIdea['season_hint']) ?>
<?php endif; ?>
<?php if (!empty($currentIdea['ideal_days'])): ?>
• Durée idéale: <?= (int)$currentIdea['ideal_days'] ?> j
<?php endif; ?>
• Statut: <strong><?= htmlspecialchars($currentIdea['status']) ?></strong>
</p>
<div class="hol-grid">
<section class="pf-section pf-section--panel">
<h2>Transport</h2>
<form method="post" action="/modules/holidays/save.php" class="hol-inline-form">
<input type="hidden" name="action" value="add_transport">
<input type="hidden" name="idea_id" value="<?= (int)$ideaId ?>">
<select name="mode" required>
<option value="TRAIN">TRAIN</option><option value="PLANE">PLANE</option>
<option value="CAR">CAR</option><option value="BUS">BUS</option><option value="BOAT">BOAT</option>
</select>
<input type="number" name="duration_min" min="0" placeholder="Durée (min)">
<input type="number" step="0.01" name="cost" placeholder="Coût (€)">
<input type="number" step="0.01" name="co2_kg" placeholder="CO₂ (kg)">
<input type="url" name="link" placeholder="Lien">
<input type="text" name="notes" placeholder="Notes">
<button type="submit" class="btn">Ajouter</button>
</form>
<ul class="hol-list">
<?php foreach ($transport as $t): ?>
<li><?= htmlspecialchars($t['mode']) ?>
<?= $t['duration_min'] !== null ? ' • ' . (int)$t['duration_min'] . ' min' : '' ?>
<?= $t['cost'] !== null ? ' • ' . number_format((float)$t['cost'], 0, ',', ' ') . ' €' : '' ?>
<?php if (!empty($t['link'])): ?> • <a href="<?= htmlspecialchars($t['link']) ?>" target="_blank" rel="noopener">🔗</a><?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</section>
<section class="pf-section pf-section--panel">
<h2>Hébergement</h2>
<form method="post" action="/modules/holidays/save.php" class="hol-inline-form">
<input type="hidden" name="action" value="add_lodging">
<input type="hidden" name="idea_id" value="<?= (int)$ideaId ?>">
<select name="type" required>
<option value="HOTEL">HOTEL</option><option value="APT">APT</option>
<option value="HOUSE">HOUSE</option><option value="CAMPING">CAMPING</option><option value="OTHER">OTHER</option>
</select>
<input type="text" name="location_text" placeholder="Localisation">
<input type="number" step="0.01" name="price_per_n" placeholder="€ / nuit">
<input type="number" name="nights" placeholder="Nuits">
<label><input type="checkbox" name="free_cancel" value="1"> Annulation gratuite</label>
<label><input type="checkbox" name="family_friendly" value="1" checked> Family-friendly</label>
<input type="url" name="link" placeholder="Lien">
<input type="text" name="notes" placeholder="Notes">
<button type="submit" class="btn">Ajouter</button>
</form>
<ul class="hol-list">
<?php foreach ($lodging as $l): ?>
<li><?= htmlspecialchars($l['type']) ?>
<?= !empty($l['location_text']) ? ' • ' . htmlspecialchars($l['location_text']) : '' ?>
<?= $l['price_per_n'] !== null ? ' • ' . number_format((float)$l['price_per_n'], 0, ',', ' ') . ' €/nuit' : '' ?>
<?= $l['nights'] !== null ? ' × ' . (int)$l['nights'] . 'n' : '' ?>
<?php if (!empty($l['link'])): ?> • <a href="<?= htmlspecialchars($l['link']) ?>" target="_blank" rel="noopener">🔗</a><?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</section>
<section class="pf-section pf-section--panel">
<h2>Activités</h2>
<form method="post" action="/modules/holidays/save.php" class="hol-inline-form">
<input type="hidden" name="action" value="add_activity">
<input type="hidden" name="idea_id" value="<?= (int)$ideaId ?>">
<input type="text" name="name" placeholder="Nom" required>
<input type="text" name="kind" placeholder="Type (ex: PARK)">
<input type="number" step="0.01" name="cost_est" placeholder="€ estimé">
<label><input type="checkbox" name="need_booking" value="1"> Réservation</label>
<select name="weather">
<option value="ANY">ANY</option><option value="GOOD">GOOD</option><option value="RAIN">RAIN</option>
</select>
<input type="url" name="link" placeholder="Lien">
<input type="text" name="notes" placeholder="Notes">
<button type="submit" class="btn">Ajouter</button>
</form>
<ul class="hol-list">
<?php foreach ($acts as $a): ?>
<li><?= htmlspecialchars($a['name']) ?><?= !empty($a['kind']) ? ' (' . htmlspecialchars($a['kind']) . ')' : '' ?>
<?= $a['cost_est'] !== null ? ' • ' . number_format((float)$a['cost_est'], 0, ',', ' ') . ' €' : '' ?>
<?= !empty($a['need_booking']) ? ' • Réservation requise' : '' ?>
<?php if (!empty($a['link'])): ?> • <a href="<?= htmlspecialchars($a['link']) ?>" target="_blank" rel="noopener">🔗</a><?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</section>
<section class="pf-section pf-section--panel">
<h2>Budget</h2>
<form method="post" action="/modules/holidays/save.php" class="hol-inline-form">
<input type="hidden" name="action" value="add_budget">
<input type="hidden" name="idea_id" value="<?= (int)$ideaId ?>">
<select name="category" required>
<option>TRANSPORT</option><option>LODGING</option><option>FOOD</option>
<option>ACTIVITIES</option><option>LOCAL</option><option>INSURANCE</option><option>VISAS</option><option>OTHER</option>
</select>
<input type="text" name="label" placeholder="Label (facultatif)">
<input type="number" step="0.01" name="amount" placeholder="Montant (€)" required>
<label><input type="checkbox" name="per_person" value="1"> Par personne</label>
<button type="submit" class="btn">Ajouter</button>
</form>
<div class="hol-budget-summary">
<strong>Fixe:</strong> <?= number_format($fixedTotal, 0, ',', ' ') ?> €
• <strong>Par personne:</strong> <?= number_format($ppTotal, 0, ',', ' ') ?> €
</div>
<ul class="hol-list">
<?php foreach ($budget as $b): ?>
<li>[<?= htmlspecialchars($b['category']) ?>] <?= htmlspecialchars($b['label'] ?? '') ?> —
<?= number_format((float)$b['amount'], 0, ',', ' ') ?> €<?= $b['per_person'] ? ' /pers.' : '' ?>
</li>
<?php endforeach; ?>
</ul>
</section>
</div>
<?php endif; ?>
<?php else: ?>
<div class="pf-holidays__titlebar">
<h1>Idées de vacances</h1>
<div class="hol-title-actions">
<button class="hol-add-btn" id="hol-add-open">+ Ajouter une idée</button>
<button class="hol-map-toggle" id="hol-map-open">🌍 Carte</button>
</div>
</div>
<section class="pf-section pf-section--panel">
<h2>Vacances planifiées</h2>
<p class="cl-legend">Dates souhaitées, prêtes à être réservées.</p>
<div class="hol-ideas-grid">
<?php foreach ($planned as $it): ?>
<div class="hol-idea-card" data-id="<?= (int)$it['id'] ?>">
<div class="hol-idea-card__head">
<h3><?= htmlspecialchars($it['title']) ?></h3>
<span class="hol-status hol-status--planned">planned</span>
</div>
<p class="hol-idea-meta">
<?= htmlspecialchars(trim(($it['city'] ? $it['city'] . ', ' : '') . ($it['region'] ? $it['region'] . ', ' : '') . ($it['country'] ?? ''))) ?>
<?php if (!empty($it['desired_start_date'])): ?>
• Dates: <?= htmlspecialchars($it['desired_start_date']) ?><?= !empty($it['desired_end_date']) ? ' → ' . htmlspecialchars($it['desired_end_date']) : '' ?>
<?php endif; ?>
<?php if (!empty($it['ideal_days'])): ?>
• Durée idéale: <?= (int)$it['ideal_days'] ?> j
<?php endif; ?>
</p>
<?php if (!empty($it['notes'])): ?>
<p class="hol-notes"><?= nl2br(htmlspecialchars(mb_strimwidth($it['notes'], 0, 160, '…'))) ?></p>
<?php endif; ?>
<div class="hol-card-actions">
<a class="btn" href="/holidays.php?id=<?= (int)$it['id'] ?>">Ouvrir</a>
<button class="btn btn-edit" data-edit-id="<?= (int)$it['id'] ?>">Éditer</button>
<button class="btn btn-delete" data-del-id="<?= (int)$it['id'] ?>">Supprimer</button>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
<section class="pf-section pf-section--panel">
<h2>Idées</h2>
<p class="cl-legend">Brouillons, favoris, shortlist.</p>
<div class="hol-ideas-grid">
<?php foreach ($ideas as $it): ?>
<div class="hol-idea-card" data-id="<?= (int)$it['id'] ?>">
<div class="hol-idea-card__head">
<h3><?= htmlspecialchars($it['title']) ?></h3>
<span class="hol-status hol-status--<?= htmlspecialchars($it['status']) ?>"><?= htmlspecialchars($it['status']) ?></span>
</div>
<p class="hol-idea-meta">
<?= htmlspecialchars(trim(($it['city'] ? $it['city'] . ', ' : '') . ($it['region'] ? $it['region'] . ', ' : '') . ($it['country'] ?? ''))) ?>
<?php if (!empty($it['desired_start_date'])): ?>
• Dates: <?= htmlspecialchars($it['desired_start_date']) ?><?= !empty($it['desired_end_date']) ? ' → ' . htmlspecialchars($it['desired_end_date']) : '' ?>
<?php elseif (!empty($it['season_hint'])): ?>
• Saison: <?= htmlspecialchars($it['season_hint']) ?>
<?php endif; ?>
<?php if (!empty($it['ideal_days'])): ?>
• Durée idéale: <?= (int)$it['ideal_days'] ?> j
<?php endif; ?>
</p>
<?php if (!empty($it['notes'])): ?>
<p class="hol-notes"><?= nl2br(htmlspecialchars(mb_strimwidth($it['notes'], 0, 160, '…'))) ?></p>
<?php endif; ?>
<div class="hol-card-actions">
<a class="btn" href="/holidays.php?id=<?= (int)$it['id'] ?>">Ouvrir</a>
<button class="btn btn-edit" data-edit-id="<?= (int)$it['id'] ?>">Éditer</button>
<button class="btn btn-delete" data-del-id="<?= (int)$it['id'] ?>">Supprimer</button>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
<section class="pf-section pf-section--panel">
<h2>Archivées</h2>
<div class="hol-ideas-grid hol-ideas-grid--archived">
<?php foreach ($archived as $it): ?>
<div class="hol-idea-card hol-idea-card--archived" data-id="<?= (int)$it['id'] ?>">
<h3><?= htmlspecialchars($it['title']) ?></h3>
<div class="hol-card-actions">
<button class="btn btn-edit" data-edit-id="<?= (int)$it['id'] ?>">Éditer</button>
<button class="btn btn-delete" data-del-id="<?= (int)$it['id'] ?>">Supprimer</button>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
<div class="hol-modal" id="hol-add-modal" aria-hidden="true">
<div class="hol-backdrop"></div>
<div class="hol-dialog" role="dialog" aria-modal="true" aria-labelledby="hol-add-title">
<form method="post" action="/modules/holidays/save.php" class="hol-form">
<h3 id="hol-add-title">Ajouter une idée</h3>
<input type="hidden" name="action" value="create_idea">
<label>Titre <input type="text" name="title" required></label>
<div class="hol-inline">
<label>Pays <input type="text" name="country"></label>
<label>Région <input type="text" name="region"></label>
<label>Ville <input type="text" name="city" placeholder="Pour la carte"></label>
</div>
<div class="hol-inline">
<label>Lat <input type="number" step="0.000001" name="lat" placeholder="41.385064"></label>
<label>Lng <input type="number" step="0.000001" name="lng" placeholder="2.173404"></label>
<button type="button" class="btn hol-geocode-btn" data-scope="add">Géocoder</button>
</div>
<div class="hol-inline">
<label>Dates souhaitées (début) <input type="date" name="desired_start_date"></label>
<label>Dates souhaitées (fin) <input type="date" name="desired_end_date"></label>
</div>
<div class="hol-inline">
<label>Saison (facultatif) <input type="text" name="season_hint" placeholder="MaiJuin"></label>
<label>Durée idéale (jours) <input type="number" name="ideal_days" min="1" step="1"></label>
</div>
<label>Statut
<select name="status">
<option value="draft">draft</option>
<option value="shortlist">shortlist</option>
<option value="favorite">favorite</option>
<option value="planned">planned</option>
<option value="archived">archived</option>
</select>
</label>
<label>Notes <textarea name="notes" rows="4" placeholder="Activités phares, contraintes, liens..."></textarea></label>
<div class="hol-actions">
<button type="button" class="hol-cancel">Annuler</button>
<button type="submit" class="hol-ok">Créer</button>
</div>
</form>
</div>
</div>
<?php endif; ?>
<div class="hol-modal" id="hol-edit-modal" aria-hidden="true">
<div class="hol-backdrop"></div>
<div class="hol-dialog" role="dialog" aria-modal="true" aria-labelledby="hol-edit-title">
<form method="post" action="/modules/holidays/save.php" class="hol-form" id="hol-edit-form">
<h3 id="hol-edit-title">Éditer 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>
<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="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>
-234
View File
@@ -1,234 +0,0 @@
<?php
// modules/holidays/save.php
require __DIR__ . '/../../includes/auth.php';
require_login('/login.php');
require __DIR__ . '/../../includes/db.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo 'Method Not Allowed';
exit;
}
$action = $_POST['action'] ?? '';
/**
* Normalise un décimal saisi (accepte virgule ou point), retourne float|NULL
*/
function hol_norm_decimal($v): ?float {
if (!isset($v)) return null;
$s = trim((string)$v);
if ($s === '') return null;
$s = str_replace(',', '.', $s);
return is_numeric($s) ? (float)$s : null;
}
/**
* Normalise une date (YYYY-MM-DD) : '' -> NULL
*/
function hol_norm_date($v): ?string {
if (!isset($v)) return null;
$s = trim((string)$v);
return $s === '' ? null : $s;
}
/**
* Redirection explicite vers la vue détail ou liste
*/
function hol_redirect(int $id = 0): void {
if ($id > 0) {
header("Location: /holidays.php?id=" . $id);
} else {
header("Location: /holidays.php");
}
exit;
}
try {
switch ($action) {
// --- 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);
// Règle métier : si on met une date, on passe probablement en 'planned'
if (!empty($start) && $status === 'draft') { $status = 'planned'; }
$stmt = $pdo->prepare("
INSERT INTO pf_holidays_ideas
(title, country, region, city, lat, lng, desired_start_date, desired_end_date, season_hint, ideal_days, status, notes)
VALUES
(:title,:country,:region,:city,:lat,:lng,:start,:end,:season,:days,:status,:notes)
");
$stmt->execute([
':title' => $title,
':country' => trim($_POST['country'] ?? ''),
':region' => trim($_POST['region'] ?? ''),
':city' => trim($_POST['city'] ?? ''),
':lat' => hol_norm_decimal($_POST['lat'] ?? null),
':lng' => hol_norm_decimal($_POST['lng'] ?? null),
':start' => $start,
':end' => $end,
':season' => trim($_POST['season_hint'] ?? ''),
':days' => ($_POST['ideal_days'] !== '' ? (int)$_POST['ideal_days'] : null),
':status' => $status,
':notes' => trim($_POST['notes'] ?? ''),
]);
// Redirection vers la nouvelle idée
hol_redirect((int)$pdo->lastInsertId());
break;
}
// --- MISE À JOUR ---
case 'update_idea': {
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) throw new Exception("ID invalide.");
$title = trim($_POST['title'] ?? '');
if ($title === '') throw new Exception("Le titre est obligatoire.");
$status = $_POST['status'] ?? 'draft';
$start = hol_norm_date($_POST['desired_start_date'] ?? null);
$end = hol_norm_date($_POST['desired_end_date'] ?? null);
if (!empty($start) && $status === 'draft') { $status = 'planned'; }
$stmt = $pdo->prepare("
UPDATE pf_holidays_ideas
SET title=:title, country=:country, region=:region, city=:city, lat=:lat, lng=:lng,
desired_start_date=:start, desired_end_date=:end,
season_hint=:season, ideal_days=:days, status=:status, notes=:notes
WHERE id=:id
");
$stmt->execute([
':id' => $id,
':title' => $title,
':country' => trim($_POST['country'] ?? ''),
':region' => trim($_POST['region'] ?? ''),
':city' => trim($_POST['city'] ?? ''),
':lat' => hol_norm_decimal($_POST['lat'] ?? null),
':lng' => hol_norm_decimal($_POST['lng'] ?? null),
':start' => $start,
':end' => $end,
':season' => trim($_POST['season_hint'] ?? ''),
':days' => ($_POST['ideal_days'] !== '' ? (int)$_POST['ideal_days'] : null),
':status' => $status,
':notes' => trim($_POST['notes'] ?? ''),
]);
hol_redirect($id);
break;
}
// --- SUPPRESSION ---
case 'delete_idea': {
$id = (int)($_POST['id'] ?? 0);
if ($id > 0) {
// On supprime l'idée (les sous-tables devraient être supprimées via ON DELETE CASCADE côté SQL,
// sinon il faudrait les supprimer ici manuellement)
$stmt = $pdo->prepare("DELETE FROM pf_holidays_ideas WHERE id = :id");
$stmt->execute([':id' => $id]);
}
hol_redirect(0); // Retour liste
break;
}
// --- SOUS-ÉLÉMENTS (Transport, Logement, Activités, Budget) ---
case 'add_transport': {
$id = (int)$_POST['idea_id'];
$stmt = $pdo->prepare("
INSERT INTO pf_holidays_transport (idea_id, mode, duration_min, cost, co2_kg, link, notes)
VALUES (:id,:mode,:dur,:cost,:co2,:link,:notes)
");
$stmt->execute([
':id' => $id,
':mode' => strtoupper($_POST['mode'] ?? 'OTHER'),
':dur' => ($_POST['duration_min'] !== '' ? (int)$_POST['duration_min'] : null),
':cost' => hol_norm_decimal($_POST['cost'] ?? null),
':co2' => hol_norm_decimal($_POST['co2_kg'] ?? null),
':link' => trim($_POST['link'] ?? ''),
':notes' => trim($_POST['notes'] ?? ''),
]);
hol_redirect($id);
break;
}
case 'add_lodging': {
$id = (int)$_POST['idea_id'];
$stmt = $pdo->prepare("
INSERT INTO pf_holidays_lodging (idea_id, type, location_text, price_per_n, nights, free_cancel, family_friendly, link, notes)
VALUES (:id,:type,:loc,:ppn,:n,:fc,:ff,:link,:notes)
");
$stmt->execute([
':id' => $id,
':type' => strtoupper($_POST['type'] ?? 'OTHER'),
':loc' => trim($_POST['location_text'] ?? ''),
':ppn' => hol_norm_decimal($_POST['price_per_n'] ?? null),
':n' => ($_POST['nights'] !== '' ? (int)$_POST['nights'] : null),
':fc' => isset($_POST['free_cancel']) ? 1 : 0,
':ff' => isset($_POST['family_friendly']) ? 1 : 0,
':link' => trim($_POST['link'] ?? ''),
':notes'=> trim($_POST['notes'] ?? ''),
]);
hol_redirect($id);
break;
}
case 'add_activity': {
$id = (int)$_POST['idea_id'];
$stmt = $pdo->prepare("
INSERT INTO pf_holidays_activities (idea_id, name, kind, cost_est, need_booking, weather, link, notes)
VALUES (:id,:name,:kind,:cost,:need,:weather,:link,:notes)
");
$stmt->execute([
':id' => $id,
':name' => trim($_POST['name'] ?? ''),
':kind' => trim($_POST['kind'] ?? ''),
':cost' => hol_norm_decimal($_POST['cost_est'] ?? null),
':need' => isset($_POST['need_booking']) ? 1 : 0,
':weather'=> strtoupper($_POST['weather'] ?? 'ANY'),
':link' => trim($_POST['link'] ?? ''),
':notes' => trim($_POST['notes'] ?? ''),
]);
hol_redirect($id);
break;
}
case 'add_budget': {
$id = (int)$_POST['idea_id'];
$stmt = $pdo->prepare("
INSERT INTO pf_holidays_budget_items (idea_id, category, label, amount, per_person)
VALUES (:id,:cat,:label,:amt,:pp)
");
$stmt->execute([
':id' => $id,
':cat' => strtoupper($_POST['category'] ?? 'OTHER'),
':label' => trim($_POST['label'] ?? ''),
':amt' => hol_norm_decimal($_POST['amount'] ?? null) ?? 0.0,
':pp' => isset($_POST['per_person']) ? 1 : 0,
]);
hol_redirect($id);
break;
}
default:
http_response_code(400);
echo 'Unknown action';
exit;
}
} catch (Throwable $e) {
http_response_code(500);
// En production, éviter d'afficher l'erreur brute à l'utilisateur
// Mais utile pour le debug actuel
echo "Erreur lors de l'enregistrement : " . htmlspecialchars($e->getMessage());
echo '<br><a href="/holidays.php">Retour</a>';
}
+66
View File
@@ -0,0 +1,66 @@
<?php
require __DIR__ . '/../../includes/auth.php';
require __DIR__ . '/../../includes/db.php';
require_login();
if (isset($_POST['action_delete']) && $_POST['action_delete'] == '1') {
$stmt = $pdo->prepare("DELETE FROM pf_holidays WHERE id = ?");
$stmt->execute([$_POST['id']]);
header("Location: /holidays.php");
exit;
}
$id = $_POST['id'] ?? '';
$title = $_POST['title'];
$period = $_POST['period_hint'];
$start = !empty($_POST['start_date']) ? $_POST['start_date'] : null;
$end = !empty($_POST['end_date']) ? $_POST['end_date'] : null;
$status = $_POST['status'];
$food = !empty($_POST['budget_food']) ? $_POST['budget_food'] : 0;
$extra = !empty($_POST['budget_extra']) ? $_POST['budget_extra'] : 0;
$notes = $_POST['notes'];
try {
$pdo->beginTransaction();
if ($id) {
// UPDATE
$sql = "UPDATE pf_holidays SET title=?, period_hint=?, start_date=?, end_date=?, status=?, budget_food=?, budget_extra=?, notes=? WHERE id=?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $id]);
} else {
// INSERT
$sql = "INSERT INTO pf_holidays (title, period_hint, start_date, end_date, status, budget_food, budget_extra, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $pdo->prepare($sql);
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes]);
$id = $pdo->lastInsertId();
}
// GESTION DES ITEMS (On supprime tout et on recrée)
$pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ?")->execute([$id]);
if (!empty($_POST['items']['name'])) {
$stmtItem = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid) VALUES (?, ?, ?, ?, ?)");
$count = count($_POST['items']['name']);
for ($i = 0; $i < $count; $i++) {
$cat = $_POST['items']['cat'][$i];
$name = trim($_POST['items']['name'][$i]);
$amount = floatval($_POST['items']['amount'][$i]);
$paid = $_POST['items']['paid'][$i];
if (!empty($name)) {
$stmtItem->execute([$id, $cat, $name, $amount, $paid]);
}
}
}
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
die("Erreur : " . $e->getMessage());
}
header("Location: /holidays.php");
exit;