refacto holidays
This commit is contained in:
+299
-379
@@ -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
@@ -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, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// --- 2. GESTION MODALE "AJOUTER" ---
|
||||
const addModalCtrl = setupModal("hol-add-modal");
|
||||
const addBtn = document.getElementById("hol-add-open");
|
||||
if (addBtn && addModalCtrl) {
|
||||
addBtn.addEventListener("click", addModalCtrl.open);
|
||||
}
|
||||
|
||||
// --- 3. GESTION MODALE "ÉDITER" ---
|
||||
const editModalCtrl = setupModal("hol-edit-modal");
|
||||
|
||||
// Fonction pour charger et ouvrir l'édition via AJAX
|
||||
async function openEditForId(id) {
|
||||
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">
|
||||
×
|
||||
</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}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+166
-423
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
$ideaId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
$isDetailView = ($ideaId > 0);
|
||||
$currentIdea = null; // Stockera les données si on est en vue détail
|
||||
|
||||
// Préparation des variables par défaut pour éviter les erreurs "undefined variable"
|
||||
$transport = []; $lodging = []; $acts = []; $budget = [];
|
||||
$fixedTotal = 0; $ppTotal = 0;
|
||||
$planned = []; $ideas = []; $archived = [];
|
||||
|
||||
if ($isDetailView) {
|
||||
// --- MODE DÉTAIL ---
|
||||
|
||||
// 1. Récupérer l'idée principale
|
||||
$rows = hol_q($pdo, "SELECT * FROM pf_holidays_ideas WHERE id = ?", [$ideaId]);
|
||||
$currentIdea = $rows[0] ?? null;
|
||||
|
||||
if ($currentIdea) {
|
||||
// 2. Récupérer les données pour la CARTE (un seul point)
|
||||
// Le JS filtrera lat/lng invalides
|
||||
$mapIdeas = hol_q($pdo, "
|
||||
SELECT id, title, country, region, city,
|
||||
CAST(lat AS DECIMAL(9,6)) AS lat,
|
||||
CAST(lng AS DECIMAL(9,6)) AS lng,
|
||||
status, desired_start_date, desired_end_date
|
||||
FROM pf_holidays_ideas
|
||||
WHERE id = ?
|
||||
", [$ideaId]);
|
||||
|
||||
// 3. Récupérer les sous-éléments (Transport, Logement, etc.)
|
||||
$transport = hol_q($pdo, "SELECT * FROM pf_holidays_transport WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
|
||||
$lodging = hol_q($pdo, "SELECT * FROM pf_holidays_lodging WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
|
||||
$acts = hol_q($pdo, "SELECT * FROM pf_holidays_activities WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
|
||||
$budget = hol_q($pdo, "SELECT category, label, amount, per_person FROM pf_holidays_budget_items WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]);
|
||||
|
||||
// 4. Calculs totaux budget
|
||||
$sumRow = hol_q($pdo, "
|
||||
SELECT
|
||||
SUM(CASE WHEN per_person=0 THEN amount ELSE 0 END) AS fixed_total,
|
||||
SUM(CASE WHEN per_person=1 THEN amount ELSE 0 END) AS per_person_total
|
||||
FROM pf_holidays_budget_items WHERE idea_id = ?
|
||||
", [$ideaId])[0] ?? ['fixed_total' => 0, 'per_person_total' => 0];
|
||||
|
||||
$fixedTotal = (float)($sumRow['fixed_total'] ?? 0);
|
||||
$ppTotal = (float)($sumRow['per_person_total'] ?? 0);
|
||||
}
|
||||
|
||||
} else {
|
||||
// --- 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,
|
||||
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
|
||||
");
|
||||
|
||||
// 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
|
||||
");
|
||||
}
|
||||
// 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']));
|
||||
?>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||
<div class="pf-holidays">
|
||||
|
||||
<?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 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>
|
||||
|
||||
<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']) ?>
|
||||
<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; ?>
|
||||
<?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>
|
||||
<?php foreach ($active as $h): ?>
|
||||
<?php renderHolidayCard($h, $pdo); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?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; ?>
|
||||
|
||||
<?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">
|
||||
<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 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>
|
||||
|
||||
<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>
|
||||
<?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="Mai–Juin"></label>
|
||||
<label>Durée idéale (jours) <input type="number" name="ideal_days" min="1" step="1"></label>
|
||||
</div>
|
||||
|
||||
<label>Statut
|
||||
<select name="status">
|
||||
<option value="draft">draft</option>
|
||||
<option value="shortlist">shortlist</option>
|
||||
<option value="favorite">favorite</option>
|
||||
<option value="planned">planned</option>
|
||||
<option value="archived">archived</option>
|
||||
<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>
|
||||
</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 l’idée</h3>
|
||||
<input type="hidden" name="action" value="update_idea">
|
||||
<input type="hidden" name="id" id="edit-id">
|
||||
|
||||
<label>Titre <input type="text" name="title" id="edit-title" required></label>
|
||||
|
||||
<div class="hol-inline">
|
||||
<label>Pays <input type="text" name="country" id="edit-country"></label>
|
||||
<label>Région <input type="text" name="region" id="edit-region"></label>
|
||||
<label>Ville <input type="text" name="city" id="edit-city"></label>
|
||||
</div>
|
||||
|
||||
<div class="hol-inline">
|
||||
<label>Lat <input type="number" step="0.000001" name="lat" id="edit-lat"></label>
|
||||
<label>Lng <input type="number" step="0.000001" name="lng" id="edit-lng"></label>
|
||||
<button type="button" class="btn hol-geocode-btn" data-scope="edit">Géocoder</button>
|
||||
<div 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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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 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>
|
||||
|
||||
<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>
|
||||
<?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']));
|
||||
}
|
||||
|
||||
$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'
|
||||
};
|
||||
|
||||
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 id="hol-map" style="width: 100%; height: calc(100vh - 140px);"></div>
|
||||
<div class='hol-idea-meta'>
|
||||
<span>🗓️ ".($dateDisplay ?: 'Dates à définir')."</span>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
@@ -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>';
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user