From f5822f26947fe1998ed4d8f78f7c2def6d75d7cb Mon Sep 17 00:00:00 2001 From: "fefe.clochette" Date: Sat, 31 Jan 2026 15:57:25 +0100 Subject: [PATCH] calendar --- family-calendar.php | 291 +++++------ modules/family-calendar/assets/img/avis.svg | 125 +++++ modules/family-calendar/family-calendar.css | 529 ++++++++++++++------ modules/family-calendar/family-calendar.js | 467 +++++++++++------ 4 files changed, 963 insertions(+), 449 deletions(-) create mode 100644 modules/family-calendar/assets/img/avis.svg diff --git a/family-calendar.php b/family-calendar.php index 0686738..85ef246 100644 --- a/family-calendar.php +++ b/family-calendar.php @@ -1,17 +1,13 @@ query("SELECT * FROM pf_events"); $dbEvents = $stmt_events->fetchAll(PDO::FETCH_ASSOC); @@ -24,169 +20,156 @@ require __DIR__ . '/header.php'; ?> -
-

Family Calendar

+
+
+

Family Calendar

+ +
-
-
- -
-

Légende

-
-
-
Vacances
-
Férié
-
Off Carole
-
Extra Off
-
Centre
-
Avis
-
Pep Malade
+ + +
+
+
+
+ + + +
+

+
+ + +
+
+ +
+
+
-
+
-
-

Récapitulatif annuel

-
- Chargement... -
-
- -
-

Vacances scolaires - Zone C

-
-
- - - - - - - - - - - -
PériodeDuAuZones
+
+
+

Planning hebdo

+
+ + +
-
-
- - -
-

Calendrier mensuel

-
- -
-
- - - +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MoisSem.LunMarMerJeuVenOff CaroleExtra OffCentreAvisPep MaladePrésenceALEXLAIA
CPJRAJACPJRAJA
Av.UseAv.UseAv.UseAv.UseAv.UseAv.Use
+
-
- -

- -
-
+
+ +
+
+ +
+

Légende

+
+
+
Vacances
+
Férié
+
Off Carole
+
Extra Off
+
Centre
+
Avis
+
Pep Malade
+
+
+
+ +
+
+ Récapitulatif +
+ + +
+
+
+
+
+
+
-
-
- -
- -
-
- -
-
-

Planning hebdo

-
- - - -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MoisSem.LunMarMerJeuVenOff CaroleExtra OffCentreAvisPep MaladePrésenceALEXLAIA
CPJRAJACPJRAJA
Av.UseAv.UseAv.UseAv.UseAv.UseAv.Use
- - -
-
+ +
- \ No newline at end of file diff --git a/modules/family-calendar/assets/img/avis.svg b/modules/family-calendar/assets/img/avis.svg new file mode 100644 index 0000000..614b76e --- /dev/null +++ b/modules/family-calendar/assets/img/avis.svg @@ -0,0 +1,125 @@ + \ No newline at end of file diff --git a/modules/family-calendar/family-calendar.css b/modules/family-calendar/family-calendar.css index 530efc5..092732c 100644 --- a/modules/family-calendar/family-calendar.css +++ b/modules/family-calendar/family-calendar.css @@ -24,147 +24,117 @@ --text-laia: #b45309; /* Ambre foncé */ } +/* Base */ .pf-family-calendar { background-color: var(--bg-page); font-family: "Inter", system-ui, sans-serif; color: var(--text-main); font-size: 13px; + line-height: 1.4; } -/* --- 2. LÉGENDE --- */ -.pf-legend-grid { - display: flex; - flex-wrap: wrap; - gap: 12px; +/* OVERRIDES STRUCTURAUX */ +.pf-family-calendar .pf-container { + max-width: 100% !important; + padding: 0 20px; } -.pf-legend-item { +.pf-family-calendar .pf-main { + padding-top: 20px; + padding-bottom: 40px; +} + +/* Titres */ +.pf-card-title { + padding: 12px 16px; + margin: 0; + font-size: 0.9rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + background-color: var(--bg-header); + border-bottom: 1px solid var(--border-color); + color: var(--text-muted); +} + +.pf-card-body { + padding: 16px; +} + +/* --- 2. HEADER PAGE & BOUTON VACANCES --- */ +.fc-header-row { display: flex; + justify-content: space-between; align-items: center; - gap: 6px; + margin-bottom: 24px; +} + +.pf-btn-icon-text { + background: white; + border: 1px solid var(--border-color); + border-radius: 20px; /* Pill */ + padding: 8px 16px; + display: inline-flex; + align-items: center; + gap: 8px; font-size: 0.85rem; + font-weight: 600; + color: var(--text-main); + cursor: pointer; + transition: all 0.2s; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } -.pf-legend-color { - width: 18px; - height: 18px; - border-radius: 4px; - border: 1px solid rgba(0, 0, 0, 0.1); - display: flex; - align-items: center; - justify-content: center; +.pf-btn-icon-text:hover { + border-color: var(--primary); + color: var(--primary); + background: #f0f9ff; + transform: translateY(-1px); } - -.fc-legend-school-holiday { - background: var(--c-school-holiday); -} -.fc-legend-public-holiday { - background: var(--c-public-holiday); -} -.fc-legend-off-carole { - background: var(--c-off-carole); -} -.fc-legend-extra-off-carole { - background: var(--c-extra-off); -} -.fc-legend-centre { - background-color: white; -} -.fc-legend-centre::before { - content: "🏫"; - font-size: 14px; -} -.fc-legend-pep-sick { - background-color: white; -} -.fc-legend-pep-sick::before { - content: "🤒"; - font-size: 14px; -} -.fc-legend-avis { - width: 24px; - height: 16px; - background: repeating-linear-gradient( - to bottom, - #fcd116, - #fcd116 2px, - #ce1126 2px, - #ce1126 4px - ); - border: 1px solid #ce1126; +.pf-btn-icon-text .icon { + font-size: 1.2em; } /* --- 3. PLANNING HEBDO --- */ -/* --- HEADER PLANNING HEBDO (Harmonisé avec Calendrier Mensuel) --- */ - .fc-week-header { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 24px; /* Même marge que le calendrier mensuel */ - padding-bottom: 0; + margin-bottom: 16px; gap: 16px; flex-wrap: wrap; } - -/* Le titre "Planning hebdo" */ .fc-week-header h2 { margin: 0; - font-size: 1.25rem; /* Même taille que le titre du mois */ + font-size: 1.25rem; font-weight: 800; color: var(--text-main); - text-transform: none; /* On garde la casse normale ou uppercase selon préférence */ - border: none; - padding: 0; } -/* Conteneur des flèches et de la date */ +/* Contrôles Année Scolaire */ .fc-week-nav-controls { display: flex; align-items: center; - gap: 16px; /* Espace identique au header mensuel */ + gap: 12px; background: white; padding: 4px 12px; - border-radius: 50px; /* Petit conteneur pilule optionnel pour lier le tout */ - border: 1px solid transparent; /* ou var(--border-color) si tu veux un cadre */ + border-radius: 50px; } - -/* LE TEXTE DE L'ANNÉE (Cible de ta demande) */ #fc-current-school-year-label { - font-size: 1.1rem; + font-size: 1rem; font-weight: 800; color: var(--text-main); + min-width: 120px; text-align: center; letter-spacing: -0.02em; - min-width: 120px; /* Empêche le saut quand les chiffres changent */ - - /* Pour l'alignement vertical parfait avec les boutons */ - display: inline-block; - line-height: 1; - padding-top: 2px; } -/* Ajustement spécifique pour les boutons de navigation dans ce header */ -.fc-week-nav-controls .fc-nav-button { - /* On s'assure qu'ils ont bien la taille définie globalement */ - width: 32px; - height: 32px; - font-size: 16px; - background: white; /* Fond blanc */ - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); -} - -.fc-week-nav-controls .fc-nav-button:hover { - background: var(--primary); - color: white; - border-color: var(--primary); -} #planningTable-wrapper { max-height: 80vh; overflow: auto; border: 1px solid var(--border-color); - border-radius: 8px; + border-radius: 12px; background: white; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05); position: relative; } @@ -192,10 +162,10 @@ font-size: 0.75rem; color: var(--text-main); z-index: 20; - box-shadow: 0 1px 0 var(--border-color); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05); } -/* Gestion hauteurs Sticky */ +/* Hauteurs */ #planningTable thead tr:nth-child(1) th { top: 0; height: 32px; @@ -207,12 +177,12 @@ } #planningTable thead tr:nth-child(3) th { top: 60px; - height: 24px; + height: 26px; z-index: 19; border-bottom: 2px solid var(--border-color); } -/* En-têtes Alex / Laia (Couleurs mises à jour) */ +/* Couleurs Headers */ .col-alex.header-group { background: var(--bg-alex) !important; color: var(--text-alex); @@ -222,7 +192,7 @@ color: var(--text-laia); } -/* Cellules */ +/* Corps */ #planningTable tbody td { border-right: 1px solid #f1f5f9; border-bottom: 1px solid #f1f5f9; @@ -256,7 +226,7 @@ z-index: 5; } -/* Indicateurs */ +/* Icones */ .fc-day--centre::after { content: "🏫"; position: absolute; @@ -264,35 +234,28 @@ right: 2px; font-size: 12px; line-height: 1; - left: auto; - transform: none; } + .fc-day--avis::after { content: ""; position: absolute; - top: 4px; - right: 4px; + top: 2px; + right: 2px; width: 14px; - height: 10px; - border-radius: 2px; - background: repeating-linear-gradient( - to bottom, - #fcd116, - #fcd116 2px, - #ce1126 2px, - #ce1126 4px - ); - border: 1px solid #b91c1c; + height: 14px; + background: url("/modules/family-calendar/assets/img/avis.svg") no-repeat + center center; + background-size: contain; + border: none; } .fc-pep-sick-emoji { position: absolute; bottom: 2px; right: 2px; font-size: 12px; - line-height: 1; } -/* Colonnes spécifiques */ +/* Colonnes */ .col-alex-av, .col-laia-av { background: #f8fafc; @@ -305,7 +268,6 @@ .col-laia-sub { background: var(--bg-laia); } - .col-month { width: 50px; background: white; @@ -332,13 +294,13 @@ color: var(--text-muted); } -/* --- 4. CALENDRIER MENSUEL (Modernisé) --- */ +/* --- 4. CALENDRIER MENSUEL (MODERNISÉ) --- */ .fc-month-calendar-wrapper { background: white; border: 1px solid var(--border-color); - border-radius: 16px; /* Plus arrondi */ + border-radius: 16px; padding: 24px; - margin-bottom: 24px; + margin-bottom: 32px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); } @@ -346,19 +308,18 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 20px; + margin-bottom: 24px; flex-wrap: wrap; gap: 16px; } -/* NAVIGATION (Flèches) */ +/* Navigation Flèches */ .fc-nav-controls { display: flex; align-items: center; gap: 16px; - order: 2; /* Pour s'assurer qu'il est bien placé */ + order: 2; } - .fc-nav-button { width: 36px; height: 36px; @@ -371,22 +332,16 @@ align-items: center; justify-content: center; cursor: pointer; - transition: all 0.2s ease; + transition: all 0.2s; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } - .fc-nav-button:hover { border-color: var(--primary); color: var(--primary); transform: translateY(-1px); - box-shadow: 0 4px 6px rgba(59, 130, 246, 0.15); } -.fc-nav-button:active { - transform: translateY(0); -} - -/* TITRE DU MOIS */ +/* Titre Mois */ #fc-current-month-year { margin: 0; font-size: 1.25rem; @@ -398,40 +353,37 @@ letter-spacing: -0.02em; } -/* BOUTONS DE VUE (Segmented Control Style) */ +/* Boutons Vue (Segmented) */ .fc-view-controls { display: inline-flex; - background: #f1f5f9; /* Gris clair de fond */ + background: #e2e8f0; padding: 4px; - border-radius: 12px; - gap: 0; /* Collés */ + border-radius: 8px; + gap: 0; order: 1; } - .fc-view-button { background: transparent; border: none; - color: var(--text-muted); - padding: 8px 16px; - border-radius: 8px; /* Un peu moins que le conteneur */ + border-radius: 6px; + padding: 6px 16px; font-size: 0.85rem; font-weight: 600; + color: var(--text-muted); cursor: pointer; - transition: all 0.2s ease; + transition: all 0.2s; } - .fc-view-button:hover { color: var(--text-main); } - .fc-view-button--active { background: white; color: var(--primary); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); font-weight: 700; } -/* RESTE DU CALENDRIER (Inchangé mais nécessaire pour le contexte) */ +/* Table Mensuelle */ .fc-month-calendar { width: 100%; display: block; @@ -447,7 +399,6 @@ border: 1px solid var(--border-color); font-size: 0.8rem; text-transform: uppercase; - letter-spacing: 0.05em; color: var(--text-muted); } .fc-month-table td { @@ -462,7 +413,7 @@ background: #fcfcfc; } -/* Conteneurs Vues */ +/* Grilles Vues */ .fc-two-months-container { display: grid; grid-template-columns: 1fr 1fr; @@ -490,7 +441,272 @@ text-transform: capitalize; } -/* --- 5. MENU CONTEXTUEL --- */ +/* --- 5. NOUVELLE SECTION BASSE (CARTES) --- */ +.pf-section--bottom-panels { + margin-top: 40px; +} +.fc-bottom-grid { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 24px; + align-items: start; +} + +.pf-card { + background: white; + border: 1px solid var(--border-color); + border-radius: 12px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + overflow: hidden; +} + +/* Légende Badge Style */ + +.pf-legend-grid { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: flex-start; +} + +.pf-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 0.75rem; + padding: 4px 8px; + background-color: white; + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-muted); +} +.pf-legend-color { + width: 16px; + height: 16px; + border-radius: 3px; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + line-height: 1; +} + +/* Couleurs & Contenus Légende */ +.fc-legend-school-holiday { + background: var(--c-school-holiday); +} +.fc-legend-public-holiday { + background: var(--c-public-holiday); +} +.fc-legend-off-carole { + background: var(--c-off-carole); +} +.fc-legend-extra-off-carole { + background: var(--c-extra-off); +} + +/* Légende : Emojis */ +.fc-legend-centre { + background: white; +} +.fc-legend-centre::after { + content: "🏫"; +} + +.fc-legend-pep-sick { + background: white; +} +.fc-legend-pep-sick::after { + content: "🤒"; +} + +/* Légende : Icone SVG Avis */ +.fc-legend-avis { + background: white url("/modules/family-calendar/assets/img/avis.svg") + no-repeat center center; + background-size: contain; + border: none; /* Pas de bordure pour le SVG propre */ +} + +/* Récapitulatif Vertical */ +.fc-summary-vertical { + display: flex; + flex-direction: column; + gap: 0; +} +.fc-summary-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 4px; + border-bottom: 1px solid #f1f5f9; +} +.fc-summary-item:last-child { + border-bottom: none; +} +.fc-summary-label { + font-size: 0.85rem; + color: var(--text-muted); + font-weight: 500; +} +.fc-summary-value { + font-size: 1rem; + font-weight: 700; + color: var(--text-main); +} +.fc-summary-sub { + font-size: 0.75rem; + color: #94a3b8; + margin-left: 8px; + font-style: italic; +} + +/* --- CONTROLES RÉCAPITULATIF --- */ + +/* On transforme le titre de la card en Flexbox pour y mettre les boutons */ +.fc-summary-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 16px; /* Un peu plus compact verticalement */ +} + +.fc-summary-controls { + display: flex; + gap: 8px; +} + +.fc-summ-select { + background: white; + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 2px 8px; + font-size: 0.75rem; /* Petit texte */ + color: var(--text-main); + cursor: pointer; + outline: none; + height: 24px; +} + +.fc-summ-select:hover { + border-color: var(--primary); +} + +/* Ajustement mobile pour les sélecteurs */ +@media (max-width: 768px) { + .fc-summary-header { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } +} + +/* --- 6. MODALE VACANCES --- */ +.fc-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(2px); + z-index: 99999; + display: flex; + justify-content: center; + align-items: center; + animation: fadeIn 0.2s ease-out; +} +.fc-modal-content { + background: white; + width: 90%; + max-width: 600px; + max-height: 80vh; + border-radius: 16px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); + display: flex; + flex-direction: column; + animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1); +} +.fc-modal-header { + padding: 16px 24px; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; +} +.fc-modal-header h2 { + margin: 0; + font-size: 1.1rem; + color: var(--text-main); +} +.fc-modal-close { + background: none; + border: none; + font-size: 24px; + color: var(--text-muted); + cursor: pointer; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: background 0.1s; +} +.fc-modal-close:hover { + background: #f1f5f9; + color: #ef4444; +} +.fc-modal-body { + padding: 0; + overflow-y: auto; +} + +/* Table Modale */ +.fc-holidays-table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; +} +.fc-holidays-table th { + background: #f9fafb; + padding: 12px 16px; + text-align: left; + font-weight: 600; + color: var(--text-muted); + position: sticky; + top: 0; + border-bottom: 1px solid var(--border-color); +} +.fc-holidays-table td { + padding: 10px 16px; + border-bottom: 1px solid #f1f5f9; + color: var(--text-main); +} +.fc-holidays-table tr:nth-child(even) { + background: #fafafa; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* --- 7. MENU CONTEXTUEL --- */ .fc-selection-menu { position: absolute; z-index: 9999; @@ -560,3 +776,22 @@ .fc-menu-leaves-table td { padding: 2px; } + +/* RESPONSIVE */ +@media (max-width: 768px) { + .fc-bottom-grid { + grid-template-columns: 1fr; + } + .fc-month-header { + flex-direction: column; + align-items: stretch; + } + .fc-nav-controls, + .fc-view-controls { + justify-content: center; + } + .pf-btn-icon-text { + width: 100%; + justify-content: center; + } +} diff --git a/modules/family-calendar/family-calendar.js b/modules/family-calendar/family-calendar.js index f227ec4..5ead82a 100644 --- a/modules/family-calendar/family-calendar.js +++ b/modules/family-calendar/family-calendar.js @@ -1,24 +1,16 @@ /** - * family-calendar.js (FIXED: Selection, View Buttons, Colors) + * family-calendar.js (Version Sécurisée) */ - document.addEventListener("DOMContentLoaded", () => { const CONGE_TYPES = ["OFF_CAROLE", "EXTRA_OFF_CAROLE"]; const GUARDE_TYPES = ["CENTRE", "AVIS"]; const PEP_TYPES = ["PEP_SICK"]; - const MODIFIABLE_TYPES = [...CONGE_TYPES, ...GUARDE_TYPES, ...PEP_TYPES]; class FamilyCalendar { constructor() { + // Éléments DOM (avec sécurité null) this.planningBody = document.getElementById("planningBody"); this.selectionMenu = document.getElementById("selectionMenu"); - // Menu dans le body pour position absolue correcte - if ( - this.selectionMenu && - this.selectionMenu.parentElement !== document.body - ) { - document.body.appendChild(this.selectionMenu); - } this.schoolHolidaysTableBody = document.querySelector( "#schoolHolidaysTable tbody", ); @@ -26,6 +18,14 @@ document.addEventListener("DOMContentLoaded", () => { this.monthSelectionMenu = document.getElementById( "fc-month-selectionMenu", ); + + // Déplacement des menus dans le body pour affichage correct + if ( + this.selectionMenu && + this.selectionMenu.parentElement !== document.body + ) { + document.body.appendChild(this.selectionMenu); + } if ( this.monthSelectionMenu && this.monthSelectionMenu.parentElement !== document.body @@ -33,7 +33,6 @@ document.addEventListener("DOMContentLoaded", () => { document.body.appendChild(this.monthSelectionMenu); } - // Etat this.currentMonth = new Date(); this.currentMonth.setDate(1); this.viewMode = "1month"; @@ -42,7 +41,6 @@ document.addEventListener("DOMContentLoaded", () => { this.isSelecting = false; this.selectedCells = []; this.monthSelectedCells = []; - this.isMonthSelecting = false; this._currentBulkInfo = null; // Données @@ -56,7 +54,12 @@ document.addEventListener("DOMContentLoaded", () => { 3: { CP: {}, JRA: {}, JA: {} }, }; - if (!this.planningBody) return; + // Si le tableau principal n'est pas là, on arrête tout pour ne pas crasher + if (!this.planningBody) { + console.warn("Element #planningBody introuvable. Le JS s'arrête."); + return; + } + this.init(); } @@ -65,6 +68,7 @@ document.addEventListener("DOMContentLoaded", () => { const now = new Date(); this.currentSchoolYearStart = now.getMonth() >= 8 ? now.getFullYear() : now.getFullYear() - 1; + await this.refreshAllData(); this.updateSchoolYearLabel(); } @@ -105,13 +109,12 @@ document.addEventListener("DOMContentLoaded", () => { this.reprocessAndRender(); } catch (e) { - console.error("Erreur chargement", e); + console.error("Erreur chargement données:", e); } } - // Appel API pour les vacances scolaires (Zone C) avec dédoublonnage async fetchPublicAndSchoolHolidays() { - // 1. Jours fériés 2025-2026 (Statique) + // 1. Jours fériés (Statique) const publicHolidays = [ "2025-11-01", "2025-11-11", @@ -131,38 +134,31 @@ document.addEventListener("DOMContentLoaded", () => { duration: 1, })); - // 2. Vacances Scolaires (API Gouv) const schoolHolidayEvents = []; + try { + // API Gouv const url = "https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=annee_scolaire='2025-2026' AND zones LIKE '%Zone C%'&limit=100"; const res = await fetch(url); const data = await res.json(); const rawRecords = data.results || []; - // --- DÉDOUBLONNAGE --- - // On utilise une Map pour ne garder qu'une entrée unique par (Description + Date Début) + // Dédoublonnage const uniqueHolidaysMap = new Map(); - rawRecords.forEach((r) => { - // Clé unique : "Vacances de Noël|2025-12-20" const key = `${r.description}|${r.start_date}`; - if (!uniqueHolidaysMap.has(key)) { - uniqueHolidaysMap.set(key, r); - } + if (!uniqueHolidaysMap.has(key)) uniqueHolidaysMap.set(key, r); }); - - // Convertir la Map en tableau const uniqueRecords = Array.from(uniqueHolidaysMap.values()); - // Remplir le tableau HTML du haut (avec la liste propre) + // APPEL DE LA MÉTHODE D'AFFICHAGE (séparée pour la propreté) this.renderSchoolHolidaysTable(uniqueRecords); - // Générer les événements jour par jour (uniquement sur la liste propre) + // Génération des events pour le calendrier (cases colorées) uniqueRecords.forEach((r, idx) => { let curr = new Date(r.start_date); const end = new Date(r.end_date); - // Boucle jour par jour while (curr < end) { const iso = curr.toISOString().split("T")[0]; schoolHolidayEvents.push({ @@ -175,56 +171,40 @@ document.addEventListener("DOMContentLoaded", () => { } }); } catch (e) { - console.warn( - "Impossible de charger les vacances scolaires depuis l'API Gouv.", - e, - ); + console.warn("Erreur API Vacances (pas grave, on continue)", e); } + // On retourne TOUJOURS les événements, même si l'affichage du tableau échoue return [...publicHolidays, ...schoolHolidayEvents]; } - processWeeks(rawWeeks) { - return rawWeeks.map((w) => ({ - id: `${w.week_iso_year}-W${w.week_iso_number}`, - monthKey: `${w.year}-${String(w.month).padStart(2, "0")}`, - monthName: w.month_name, - weekLabel: w.week_label, - dayDates: { - mon: new Date(w.mon_date + "T00:00:00"), - tue: new Date(w.tue_date + "T00:00:00"), - wed: new Date(w.wed_date + "T00:00:00"), - thu: new Date(w.thu_date + "T00:00:00"), - fri: new Date(w.fri_date + "T00:00:00"), - }, - dayFlags: { - mon: { events: [] }, - tue: { events: [] }, - wed: { events: [] }, - thu: { events: [] }, - fri: { events: [] }, - }, - totals: { - offCarole: 0, - extraOffCarole: 0, - centre: 0, - avis: 0, - pepSick: 0, - presencePep: 0, - alexCP: 0, - alexJRA: 0, - alexJA: 0, - laiaCP: 0, - laiaJRA: 0, - laiaJA: 0, - }, - })); + renderSchoolHolidaysTable(records) { + // Si le tableau HTML n'existe pas (ex: on n'est pas sur la bonne page), on ne fait rien + if (!this.schoolHolidaysTableBody) return; + + // 1. Tri chronologique strict (du plus ancien au plus récent) + records.sort((a, b) => new Date(a.start_date) - new Date(b.start_date)); + + // 2. Génération du HTML + this.schoolHolidaysTableBody.innerHTML = records + .map( + (r) => ` + + ${r.description} + ${new Date(r.start_date).toLocaleDateString("fr-FR")} + ${new Date(r.end_date).toLocaleDateString("fr-FR")} + + `, + ) + .join(""); } + // ================== LOGIQUE MÉTIER ================== + reprocessAndRender() { this.reprocessEvents(); this.calculateMonthlyBalances(); - this.updateGlobalSummary(); + this.initSummaryControls(); this.renderTable(); this.renderMonthCalendar(); } @@ -241,6 +221,7 @@ document.addEventListener("DOMContentLoaded", () => { (k) => w.dayDates[k].getTime() === d.getTime(), ); if (dayKey) w.dayFlags[dayKey].events.push(e); + const dur = parseFloat(e.duration) || 1; const typeMap = { OFF_CAROLE: "offCarole", @@ -341,6 +322,7 @@ document.addEventListener("DOMContentLoaded", () => { } renderTable() { + if (!this.planningBody) return; this.planningBody.innerHTML = ""; const monthSpans = this.weeks.reduce((acc, w) => { acc[w.monthKey] = (acc[w.monthKey] || 0) + 1; @@ -383,8 +365,7 @@ document.addEventListener("DOMContentLoaded", () => { td.textContent = String(dateObj.getDate()).padStart(2, "0"); td.className = "col-day"; - const dayEvents = w.dayFlags[d].events; - dayEvents.forEach((evt) => { + w.dayFlags[d].events.forEach((evt) => { if (evt.type === "OFF_CAROLE") td.classList.add("fc-day--off-carole"); if (evt.type === "EXTRA_OFF_CAROLE") @@ -402,11 +383,10 @@ document.addEventListener("DOMContentLoaded", () => { const dayLeaves = this.leaves.filter((l) => l.leave_date === iso); if (dayLeaves.length) { let html = `
`; - // --- CORRECTION COULEURS --- if (dayLeaves.some((l) => l.person_id === 2)) - html += `A`; // Teal + html += `A`; if (dayLeaves.some((l) => l.person_id === 3)) - html += `L`; // Amber + html += `L`; html += `
`; td.innerHTML += html; } @@ -455,7 +435,7 @@ document.addEventListener("DOMContentLoaded", () => { renderMonthCalendar() { if (!this.monthCalendar) return; - this.monthCalendar.innerHTML = ""; // CLEAN UP + this.monthCalendar.innerHTML = ""; const y = this.currentMonth.getFullYear(); const m = this.currentMonth.getMonth(); @@ -508,10 +488,8 @@ document.addEventListener("DOMContentLoaded", () => { renderYearView() { const startYear = this.currentSchoolYearStart; let html = '
'; - // Sept-Dec for (let i = 8; i < 12; i++) html += `
${new Intl.DateTimeFormat("fr-FR", { month: "long" }).format(new Date(startYear, i))}
${this.generateMonthHTML(startYear, i)}
`; - // Jan-Aug for (let i = 0; i < 8; i++) html += `
${new Intl.DateTimeFormat("fr-FR", { month: "long" }).format(new Date(startYear + 1, i))}
${this.generateMonthHTML(startYear + 1, i)}
`; html += "
"; @@ -582,43 +560,185 @@ document.addEventListener("DOMContentLoaded", () => { return html; } - renderSchoolHolidaysTable(records) { - if (!this.schoolHolidaysTableBody) return; - records.sort((a, b) => new Date(a.start_date) - new Date(b.start_date)); - this.schoolHolidaysTableBody.innerHTML = records - .map( - (r) => ` - - ${r.description} - ${new Date(r.start_date).toLocaleDateString("fr-FR")} - ${new Date(r.end_date).toLocaleDateString("fr-FR")} - ${r.zones} - - `, - ) - .join(""); - } + // --- NOUVELLES MÉTHODES POUR LE RÉCAPITULATIF DYNAMIQUE --- + + /** + * Initialise les menus déroulants (Année / Mois) + */ + initSummaryControls() { + const typeSelect = document.getElementById("summType"); + const valueSelect = document.getElementById("summValue"); + + if (!typeSelect || !valueSelect) return; + + // 1. Scanner les données pour trouver les années et mois disponibles + const years = new Set(); + const months = new Set(); - updateGlobalSummary() { - const div = document.getElementById("globalSummary"); - if (!div) return; - const stats = { off: 0, extra: 0, sick: 0, pep: 0, totalWorking: 0 }; this.weeks.forEach((w) => { - stats.off += w.totals.offCarole; - stats.extra += w.totals.extraOffCarole; - stats.sick += w.totals.pepSick; - stats.pep += w.totals.presencePep; Object.values(w.dayDates).forEach((d) => { - if (!this.publicHolidayDates.has(d.toISOString().split("T")[0])) - stats.totalWorking++; + years.add(d.getFullYear()); + const mKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`; + months.add(mKey); }); }); + + const sortedYears = Array.from(years).sort(); + const sortedMonths = Array.from(months).sort(); + + // 2. Fonction interne pour remplir le 2ème menu (Valeurs) + const populateValues = () => { + const currentType = typeSelect.value; + const previousValue = valueSelect.value; // Pour essayer de garder la sélection si possible + + valueSelect.innerHTML = ""; // On vide + + if (currentType === "year") { + sortedYears.forEach((y) => { + const opt = document.createElement("option"); + opt.value = y; + opt.textContent = y; + valueSelect.appendChild(opt); + }); + + // Sélection intelligente : garder l'ancienne ou prendre l'année en cours + if (sortedYears.includes(parseInt(previousValue))) { + valueSelect.value = previousValue; + } else { + const currentYear = new Date().getFullYear(); + if (sortedYears.includes(currentYear)) + valueSelect.value = currentYear; + } + } else { + // Mode Mois + sortedMonths.forEach((m) => { + const [y, mo] = m.split("-"); + const dateObj = new Date(y, mo - 1, 1); + // Format : "octobre 2025" + const label = new Intl.DateTimeFormat("fr-FR", { + month: "long", + year: "numeric", + }).format(dateObj); + + const opt = document.createElement("option"); + opt.value = m; + opt.textContent = label; + valueSelect.appendChild(opt); + }); + + // Sélection intelligente : mois courant + const nowIso = new Date().toISOString().slice(0, 7); // "2025-01" + if (sortedMonths.includes(nowIso)) { + valueSelect.value = nowIso; + } + } + + // Une fois rempli, on met à jour les chiffres + this.updateGlobalSummary(); + }; + + // 3. Attacher les écouteurs (UNE SEULE FOIS) + if (!this.summaryListenersAttached) { + typeSelect.addEventListener("change", populateValues); + valueSelect.addEventListener("change", () => + this.updateGlobalSummary(), + ); + this.summaryListenersAttached = true; + } + + // 4. Premier appel pour remplir dès le chargement + populateValues(); + } + + /** + * Calcule et affiche les KPIs en fonction des filtres + */ + updateGlobalSummary() { + const div = document.getElementById("globalSummary"); + const typeSelect = document.getElementById("summType"); + const valueSelect = document.getElementById("summValue"); + + if (!div) return; + + // Si les contrôles n'ont pas encore de valeur (chargement initial), on attend + if (typeSelect && valueSelect && !valueSelect.value) return; + + const filterType = typeSelect ? typeSelect.value : "year"; + const filterValue = valueSelect + ? valueSelect.value + : new Date().getFullYear().toString(); // Fallback + + const stats = { off: 0, extra: 0, sick: 0, pep: 0, totalWorking: 0 }; + + // Parcours précis jour par jour + this.weeks.forEach((w) => { + Object.entries(w.dayDates).forEach(([dayName, dateObj]) => { + // Vérification du filtre + let match = false; + if (filterType === "year") { + if (dateObj.getFullYear().toString() === filterValue) match = true; + } else { + // Month (YYYY-MM) + const isoMonth = dateObj.toISOString().slice(0, 7); + if (isoMonth === filterValue) match = true; + } + + if (match) { + // Récupérer les events de CE jour spécifique + const dayEvents = w.dayFlags[dayName].events; + + // Additions + dayEvents.forEach((e) => { + const dur = parseFloat(e.duration) || 1; + if (e.type === "OFF_CAROLE") stats.off += dur; + if (e.type === "EXTRA_OFF_CAROLE") stats.extra += dur; + if (e.type === "PEP_SICK") stats.sick += dur; + }); + + // Calcul présence (Jour ouvré - Absences) + // Est-ce un jour ouvré ? (Pas férié, pas we - WE déjà exclu par structure semainier lun-ven) + // NOTE : Ma structure `weeks` ne contient que Lun-Ven par défaut. + const dateIso = dateObj.toISOString().split("T")[0]; + if (!this.publicHolidayDates.has(dateIso)) { + // C'est un jour potentiellement ouvré + stats.totalWorking++; + + // Calcul des absences sur ce jour + let dayAbsence = 0; + dayEvents.forEach((e) => { + if ( + ["OFF_CAROLE", "EXTRA_OFF_CAROLE", "PEP_SICK"].includes( + e.type, + ) + ) { + dayAbsence += parseFloat(e.duration) || 1; + } + }); + + stats.pep += Math.max(0, 1 - dayAbsence); + } + } + }); + }); + + // Affichage HTML Vertical div.innerHTML = ` -

Off Carole : ${stats.off} jours

-

Extra Off Carole : ${stats.extra} jours

-

Pep malade : ${stats.sick} jours

-

Présence Pep : ${stats.pep} jours

-

(Sur jours ouvrés totaux : ${stats.totalWorking})

+
+ Off Carole + ${parseFloat(stats.off.toFixed(1))} jours +
+
+ Extra Off Carole + ${parseFloat(stats.extra.toFixed(1))} jours +
+
+ Pep Malade + ${parseFloat(stats.sick.toFixed(1))} jours +
+
+ Présence Pep + ${parseFloat(stats.pep.toFixed(1))} jours +
`; } @@ -628,28 +748,103 @@ document.addEventListener("DOMContentLoaded", () => { lbl.textContent = `${this.currentSchoolYearStart} – ${this.currentSchoolYearStart + 1}`; } + processWeeks(rawWeeks) { + return rawWeeks.map((w) => ({ + id: `${w.week_iso_year}-W${w.week_iso_number}`, + monthKey: `${w.year}-${String(w.month).padStart(2, "0")}`, + monthName: w.month_name, + weekLabel: w.week_label, + dayDates: { + mon: new Date(w.mon_date + "T00:00:00"), + tue: new Date(w.tue_date + "T00:00:00"), + wed: new Date(w.wed_date + "T00:00:00"), + thu: new Date(w.thu_date + "T00:00:00"), + fri: new Date(w.fri_date + "T00:00:00"), + }, + dayFlags: { + mon: { events: [] }, + tue: { events: [] }, + wed: { events: [] }, + thu: { events: [] }, + fri: { events: [] }, + }, + totals: { + offCarole: 0, + extraOffCarole: 0, + centre: 0, + avis: 0, + pepSick: 0, + presencePep: 0, + alexCP: 0, + alexJRA: 0, + alexJA: 0, + laiaCP: 0, + laiaJRA: 0, + laiaJA: 0, + }, + })); + } + + async fetchApi(url) { + return fetch(url).then((r) => r.json()); + } + + async postApi(url, data) { + const res = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new Error(await res.text()); + return res.json(); + } + + async changeSchoolYear(delta) { + this.currentSchoolYearStart += delta; + this.updateSchoolYearLabel(); + await this.refreshAllData(); + } + // ================== INTERACTIONS ================== setupEventListeners() { // Planning Hebdo - this.planningBody.addEventListener("mousedown", (e) => - this.handleMouseDown(e), - ); - document.addEventListener("mousemove", (e) => this.handleMouseMove(e)); - document.addEventListener("mouseup", (e) => this.handleMouseUp(e)); + if (this.planningBody) { + this.planningBody.addEventListener("mousedown", (e) => + this.handleMouseDown(e), + ); + document.addEventListener("mousemove", (e) => this.handleMouseMove(e)); + document.addEventListener("mouseup", (e) => this.handleMouseUp(e)); + } - // Calendrier Mensuel (Clic simple pour ouvrir le menu) + // Modale + const btnOpen = document.getElementById("btnOpenHolidays"); + const btnClose = document.getElementById("btnCloseHolidays"); + const modal = document.getElementById("modalHolidays"); + if (btnOpen && modal) + btnOpen.addEventListener("click", () => (modal.style.display = "flex")); + if (btnClose && modal) + btnClose.addEventListener( + "click", + () => (modal.style.display = "none"), + ); + if (modal) + modal.addEventListener("click", (e) => { + if (e.target === modal) modal.style.display = "none"; + }); + + // Calendrier Mensuel if (this.monthCalendar) { this.monthCalendar.addEventListener("click", (e) => { const td = e.target.closest("td[data-date]"); if (td) { - this.monthSelectedCells = [td]; // Simuler sélection unique + this.monthSelectedCells = [td]; this.showMenu(e.pageX, e.pageY, [td.dataset.date], true); } }); } - // Menus Contextuels + // Menus document.addEventListener("click", (e) => this.closeMenusIfOutside(e)); const handleMenu = (e) => { const btn = e.target.closest("button"); @@ -660,7 +855,7 @@ document.addEventListener("DOMContentLoaded", () => { if (this.monthSelectionMenu) this.monthSelectionMenu.addEventListener("click", handleMenu); - // Boutons Navigation Mensuelle + // Nav Mensuelle document .getElementById("fc-prev-month") ?.addEventListener("click", () => { @@ -674,7 +869,7 @@ document.addEventListener("DOMContentLoaded", () => { this.renderMonthCalendar(); }); - // Boutons Navigation Année Scolaire + // Nav Année document .getElementById("fc-prev-school-year") ?.addEventListener("click", () => this.changeSchoolYear(-1)); @@ -682,7 +877,7 @@ document.addEventListener("DOMContentLoaded", () => { .getElementById("fc-next-school-year") ?.addEventListener("click", () => this.changeSchoolYear(1)); - // Boutons de Vue (1 mois / 2 mois / Année) + // Boutons Vue document.querySelectorAll(".fc-view-button").forEach((btn) => { btn.addEventListener("click", (e) => { document @@ -712,13 +907,9 @@ document.addEventListener("DOMContentLoaded", () => { handleMouseUp(e) { if (!this.isSelecting) return; - - // FIX CRITIQUE : Si on relâche la souris sur une case qu'on n'a pas eu le temps de "survoler" (mouvement rapide) - // on l'ajoute maintenant pour être sûr qu'elle est dans la sélection. + // Fix : inclure la dernière cellule const td = e.target.closest("#planningTable td[data-date]"); - if (td && !this.selectedCells.includes(td)) { - this.selectCell(td); - } + if (td && !this.selectedCells.includes(td)) this.selectCell(td); this.isSelecting = false; if (!this.selectedCells.length) return; @@ -735,14 +926,14 @@ document.addEventListener("DOMContentLoaded", () => { clearSelection() { this.selectedCells.forEach((c) => c.classList.remove("fc-day--selected")); this.selectedCells = []; - this.selectionMenu.style.display = "none"; + if (this.selectionMenu) this.selectionMenu.style.display = "none"; if (this.monthSelectionMenu) this.monthSelectionMenu.style.display = "none"; } closeMenusIfOutside(e) { if ( - !this.selectionMenu.contains(e.target) && + !this.selectionMenu?.contains(e.target) && !this.monthSelectionMenu?.contains(e.target) && !this.menuJustOpened ) { @@ -803,7 +994,7 @@ document.addEventListener("DOMContentLoaded", () => { } async handleMenuAction(dataset) { - this.selectionMenu.style.display = "none"; + if (this.selectionMenu) this.selectionMenu.style.display = "none"; if (this.monthSelectionMenu) this.monthSelectionMenu.style.display = "none"; @@ -887,26 +1078,6 @@ document.addEventListener("DOMContentLoaded", () => { alert("Erreur action: " + e.message); } } - - async fetchApi(url) { - return fetch(url).then((r) => r.json()); - } - - async postApi(url, data) { - const res = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(data), - }); - if (!res.ok) throw new Error(await res.text()); - return res.json(); - } - - async changeSchoolYear(delta) { - this.currentSchoolYearStart += delta; - this.updateSchoolYearLabel(); - await this.refreshAllData(); - } } new FamilyCalendar();