-
-
-
\ 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();