From 796d3b3fcc542e087287bbe44a6a19acae607425 Mon Sep 17 00:00:00 2001 From: "fefe.clochette" Date: Thu, 8 Jan 2026 17:25:42 +0100 Subject: [PATCH] calendar storage --- family-calendar.php | 10 +- .../admin/generate-calendar-year.php | 157 ++++++++++++++++ modules/family-calendar/family-calendar.css | 28 ++- modules/family-calendar/family-calendar.js | 175 ++++++++++++------ .../includes/api/events-debug.log | 7 + .../api/get-calendar-weeks-scolaire.php | 68 +++++++ .../includes/api/get-calendar-weeks.php | 36 ++++ 7 files changed, 421 insertions(+), 60 deletions(-) create mode 100644 modules/family-calendar/admin/generate-calendar-year.php create mode 100644 modules/family-calendar/includes/api/get-calendar-weeks-scolaire.php create mode 100644 modules/family-calendar/includes/api/get-calendar-weeks.php diff --git a/family-calendar.php b/family-calendar.php index f978d07..31c8dfb 100644 --- a/family-calendar.php +++ b/family-calendar.php @@ -137,7 +137,15 @@ require __DIR__ . '/header.php';
-

Planning par semaine

+
+

Planning hebdo

+
+ + + +
+
+
diff --git a/modules/family-calendar/admin/generate-calendar-year.php b/modules/family-calendar/admin/generate-calendar-year.php new file mode 100644 index 0000000..2718831 --- /dev/null +++ b/modules/family-calendar/admin/generate-calendar-year.php @@ -0,0 +1,157 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +} else { + die("Erreur : PDO non initialisé dans db.php"); +} + +$year = isset($_GET['year']) ? (int)$_GET['year'] : (int)date('Y'); +if ($year < 2000 || $year > 2100) { + die("Année invalide."); +} + +try { + $pdo->beginTransaction(); + + // On supprime d'abord les semaines de cette année calendaire + $stmtDel = $pdo->prepare("DELETE FROM pf_calendar_weeks WHERE year = :year"); + $stmtDel->execute([':year' => $year]); + + // 1er janvier de l'année calendaire + $start = new DateTime("$year-01-01"); + + // On remonte au lundi précédent (ou on reste dessus si déjà lundi) + while ($start->format('N') != 1) { // 1 = lundi + $start->modify('-1 day'); + } + + // Dernier jour de l'année + $end = new DateTime("$year-12-31"); + + // Préparation de l'INSERT avec gestion des doublons + // (en supposant une contrainte UNIQUE, par ex. sur (year, week_iso_year, week_iso_number)) + $sql = " + INSERT INTO pf_calendar_weeks ( + year, week_iso_year, week_iso_number, week_label, + month, month_name, + week_start_date, mon_date, tue_date, wed_date, thu_date, fri_date, sat_date, sun_date + ) VALUES ( + :year, :week_iso_year, :week_iso_number, :week_label, + :month, :month_name, + :week_start_date, :mon_date, :tue_date, :wed_date, :thu_date, :fri_date, :sat_date, :sun_date + ) + ON DUPLICATE KEY UPDATE + week_label = VALUES(week_label), + month = VALUES(month), + month_name = VALUES(month_name), + week_start_date = VALUES(week_start_date), + mon_date = VALUES(mon_date), + tue_date = VALUES(tue_date), + wed_date = VALUES(wed_date), + thu_date = VALUES(thu_date), + fri_date = VALUES(fri_date), + sat_date = VALUES(sat_date), + sun_date = VALUES(sun_date) + "; + $stmt = $pdo->prepare($sql); + + // fonction mois FR + function getMonthNameFr($monthIndexZeroBased) { + $months = [ + "Janvier", "Fevrier", "Mars", "Avril", "Mai", "Juin", + "Juillet", "Aout", "Septembre", "Octobre", "Novembre", "Decembre", + ]; + return $months[$monthIndexZeroBased] ?? ""; + } + + $current = clone $start; + $insertCount = 0; + + while ($current <= $end) { + $monday = clone $current; + + // Calcul des 7 jours de la semaine + $mon = clone $monday; + $tue = (clone $monday)->modify('+1 day'); + $wed = (clone $monday)->modify('+2 days'); + $thu = (clone $monday)->modify('+3 days'); + $fri = (clone $monday)->modify('+4 days'); + $sat = (clone $monday)->modify('+5 days'); + $sun = (clone $monday)->modify('+6 days'); + + // Année / semaine ISO + $weekIsoYear = (int)$monday->format('o'); // année ISO + $weekIsoNumber = (int)$monday->format('W'); + $weekLabel = 'W' . str_pad($weekIsoNumber, 2, '0', STR_PAD_LEFT); + + // Détermination du mois d'affectation de la semaine + // -> on compte combien de jours de la semaine tombent dans chaque mois + $days = [$mon, $tue, $wed, $thu, $fri, $sat, $sun]; + + // compteur mois => nb de jours (clé = numéro de mois 1..12) + $monthCounts = []; + + foreach ($days as $d) { + $m = (int)$d->format('n'); // 1-12 + if (!isset($monthCounts[$m])) { + $monthCounts[$m] = 0; + } + $monthCounts[$m]++; + } + + // On prend le mois ayant le plus de jours + // (si égalité, le mois ayant la plus petite valeur numérique gagnera, + // ce qui est raisonnable, mais on peut changer si besoin) + $chosenMonth = null; + $maxDays = -1; + foreach ($monthCounts as $m => $count) { + if ($count > $maxDays) { + $maxDays = $count; + $chosenMonth = $m; + } + } + + $month = $chosenMonth; + $monthName = getMonthNameFr($month - 1); + + $stmt->execute([ + ':year' => $weekIsoYear, // <-- au lieu de $year + ':week_iso_year' => $weekIsoYear, + ':week_iso_number' => $weekIsoNumber, + ':week_label' => $weekLabel, + ':month' => $month, + ':month_name' => $monthName, + ':week_start_date' => $monday->format('Y-m-d'), + ':mon_date' => $mon->format('Y-m-d'), + ':tue_date' => $tue->format('Y-m-d'), + ':wed_date' => $wed->format('Y-m-d'), + ':thu_date' => $thu->format('Y-m-d'), + ':fri_date' => $fri->format('Y-m-d'), + ':sat_date' => $sat->format('Y-m-d'), + ':sun_date' => $sun->format('Y-m-d'), + ]); + + + $insertCount++; + $current->modify('+7 days'); + } + + $pdo->commit(); + + echo "Calendrier $year généré. Semaines traitées : " . $insertCount; + +} catch (PDOException $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + die("Erreur PDO : " . $e->getMessage()); +} catch (Exception $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + die("Erreur générale : " . $e->getMessage()); +} diff --git a/modules/family-calendar/family-calendar.css b/modules/family-calendar/family-calendar.css index c54ddc7..c3f7cd0 100644 --- a/modules/family-calendar/family-calendar.css +++ b/modules/family-calendar/family-calendar.css @@ -92,8 +92,34 @@ white-space: normal; } +/* Header du planning hebdo : même esprit que fc-month-header */ +.fc-week-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + padding-bottom: 8px; + gap: 12px; + flex-wrap: wrap; +} + +.fc-week-nav-controls { + display: flex; + align-items: center; + gap: 8px; +} +#fc-current-school-year-label { + font-weight: 600; + color: #243b53; +} + +#fc-current-school-year-label { + font-weight: 600; + color: #243b53; +} + #planningTable-wrapper { - max-height: 500px; + max-height: 80vh; overflow-y: auto; overflow-x: auto; position: relative; diff --git a/modules/family-calendar/family-calendar.js b/modules/family-calendar/family-calendar.js index 0649a6d..ca0adc7 100644 --- a/modules/family-calendar/family-calendar.js +++ b/modules/family-calendar/family-calendar.js @@ -19,15 +19,11 @@ document.addEventListener("DOMContentLoaded", () => { this.monthSelectionMenu = document.getElementById( "fc-month-selectionMenu" ); - // Initialiser avec septembre (mois 8, car 0-indexé) pour l'année scolaire + + // Mois courant réel pour le calendrier mensuel this.currentMonth = new Date(); - const currentMonthIndex = this.currentMonth.getMonth(); - // Si on est avant septembre, on affiche l'année scolaire précédente - if (currentMonthIndex < 8) { - this.currentMonth.setFullYear(this.currentMonth.getFullYear() - 1); - } - this.currentMonth.setMonth(8); // Septembre - this.currentMonth.setDate(1); // Premier jour du mois + this.currentMonth.setDate(1); // premier jour du mois courant + this.viewMode = "1month"; // "1month", "2months", "year" if (!this.planningBody || !this.selectionMenu) { @@ -52,7 +48,20 @@ document.addEventListener("DOMContentLoaded", () => { // ================== INIT ================== async init() { this.setupEventListeners(); - this.weeks = this.generateWeeksStructure(); + + // Déterminer l'année scolaire en cours à partir de la date du jour + const now = new Date(); + const nowMonth = now.getMonth(); // 0-11 + const nowYear = now.getFullYear(); + this.currentSchoolYearStart = nowMonth >= 8 ? nowYear : nowYear - 1; + + // Charger les semaines pour l'année scolaire courante + this.weeks = await this.fetchWeeksStructureScolaire( + this.currentSchoolYearStart + ); + + // Mettre à jour le label d'année scolaire dans l'UI + this.updateSchoolYearLabel(); this.dbEvents = this.loadDbEvents(); this.fixedEvents = await this.fetchPublicAndSchoolHolidays(); @@ -211,47 +220,50 @@ document.addEventListener("DOMContentLoaded", () => { return [...publicHolidays, ...schoolHolidayEvents]; } - generateWeeksStructure() { - const weeks = []; - const start = new Date(2025, 8, 1); // 1er septembre 2025 - const end = new Date(2026, 7, 31); // 31 août 2026 - let current = new Date(start); + async fetchWeeksStructureScolaire(schoolYearStart) { + try { + const year = schoolYearStart || new Date().getFullYear(); + const res = await fetch( + `/modules/family-calendar/includes/api/get-calendar-weeks-scolaire.php?school_year_start=${year}` + ); + if (!res.ok) { + throw new Error("Erreur HTTP " + res.status); + } + const data = await res.json(); + const weeks = data.weeks || []; - // Remonter au lundi le plus proche <= start - while (current.getDay() !== 1) current.setDate(current.getDate() - 1); + return weeks.map((w) => { + const mon = new Date(w.mon_date + "T00:00:00"); + const tue = new Date(w.tue_date + "T00:00:00"); + const wed = new Date(w.wed_date + "T00:00:00"); + const thu = new Date(w.thu_date + "T00:00:00"); + const fri = new Date(w.fri_date + "T00:00:00"); - while (current <= end) { - const monday = new Date(current); - - // C’est CETTE date qui doit servir de référence pour le mois - const weekMonthDate = monday; - - const weekData = { - id: `${monday.getFullYear()}-W${getWeekOfYear(monday)}`, - monthKey: `${weekMonthDate.getFullYear()}-${String( - weekMonthDate.getMonth() + 1 - ).padStart(2, "0")}`, - monthName: getMonthNameFr(weekMonthDate.getMonth()), - weekLabel: `W${getWeekOfYear(monday)}`, - dayDates: {}, - dayFlags: {}, - }; - - ["mon", "tue", "wed", "thu", "fri"].forEach((dayKey, index) => { - const dayDate = new Date(monday); - dayDate.setDate(monday.getDate() + index); - weekData.dayDates[dayKey] = dayDate; - weekData.dayFlags[dayKey] = { eventsOnDay: [] }; + return { + id: `${w.week_iso_year}-W${w.week_iso_number}`, // ex: 2026-W01 + monthKey: `${w.year}-${String(w.month).padStart(2, "0")}`, // année calendaire + mois + monthName: w.month_name, + weekLabel: w.week_label, + dayDates: { + mon, + tue, + wed, + thu, + fri, + }, + dayFlags: { + mon: { eventsOnDay: [] }, + tue: { eventsOnDay: [] }, + wed: { eventsOnDay: [] }, + thu: { eventsOnDay: [] }, + fri: { eventsOnDay: [] }, + }, + }; }); - - weeks.push(weekData); - current.setDate(current.getDate() + 7); + } catch (err) { + console.error("Erreur chargement calendar weeks scolaire:", err); + return []; } - - // Juste pour verrouiller l’ordre si un jour tu modifies current ailleurs - weeks.sort((a, b) => a.dayDates.mon - b.dayDates.mon); - - return weeks; } reprocessAndRender() { @@ -1032,6 +1044,20 @@ document.addEventListener("DOMContentLoaded", () => { nextBtn.addEventListener("click", () => this.navigateMonth(1)); } + // Navigation année scolaire (planning hebdo) + const prevSchoolYearBtn = document.getElementById("fc-prev-school-year"); + const nextSchoolYearBtn = document.getElementById("fc-next-school-year"); + if (prevSchoolYearBtn) { + prevSchoolYearBtn.addEventListener("click", () => + this.changeSchoolYear(-1) + ); + } + if (nextSchoolYearBtn) { + nextSchoolYearBtn.addEventListener("click", () => + this.changeSchoolYear(1) + ); + } + // Boutons de changement de vue const viewButtons = document.querySelectorAll(".fc-view-button"); viewButtons.forEach((btn) => { @@ -2501,24 +2527,28 @@ document.addEventListener("DOMContentLoaded", () => { btn.classList.add("fc-view-button--active"); } }); + + // Si on passe en vue "year", caler currentMonth sur septembre + if (mode === "year" && this.currentSchoolYearStart != null) { + this.currentMonth = new Date(this.currentSchoolYearStart, 8, 1); + } + this.renderMonthCalendar(); } - navigateMonth(direction) { + async navigateMonth(direction) { if (this.viewMode === "year") { - // Navigation par année scolaire : on avance/recul d'un an, - // en gardant currentMonth fixé sur septembre - const year = this.currentMonth.getFullYear() + direction; - this.currentMonth = new Date(year, 8, 1); // 8 = septembre - } else { - // Navigation simple par mois (1 ou 2 mois) - const year = this.currentMonth.getFullYear(); - const month = this.currentMonth.getMonth() + direction; - - // new Date gère automatiquement le dépassement (ex: 2025, 12 => jan 2026) - this.currentMonth = new Date(year, month, 1); + // Navigation par année scolaire via les flèches : + // on réutilise changeSchoolYear + await this.changeSchoolYear(direction); + return; } + // Navigation simple par mois (1 ou 2 mois) + const year = this.currentMonth.getFullYear(); + const month = this.currentMonth.getMonth() + direction; + + this.currentMonth = new Date(year, month, 1); this.renderMonthCalendar(); } @@ -2563,6 +2593,35 @@ document.addEventListener("DOMContentLoaded", () => { } } + updateSchoolYearLabel() { + const label = document.getElementById("fc-current-school-year-label"); + if (!label || this.currentSchoolYearStart == null) return; + const start = this.currentSchoolYearStart; + const end = start + 1; + label.textContent = `Année scolaire ${start}–${end}`; + } + + async changeSchoolYear(delta) { + // delta = -1 ou +1 + this.currentSchoolYearStart = (this.currentSchoolYearStart || 0) + delta; + + // Mettre currentMonth sur septembre de cette nouvelle année pour la vue "year" + // (pour le mensuel, on garde currentMonth tel quel, sauf si tu passes explicitement en vue year) + if (this.viewMode === "year") { + this.currentMonth = new Date(this.currentSchoolYearStart, 8, 1); // septembre + } + + // Recharger les semaines de cette année scolaire + this.weeks = await this.fetchWeeksStructureScolaire( + this.currentSchoolYearStart + ); + + // Mettre à jour données / affichage + this.reprocessAndRender(); + this.updateSchoolYearLabel(); + this.renderMonthCalendar(); + } + renderSingleMonthView() { const year = this.currentMonth.getFullYear(); const month = this.currentMonth.getMonth(); diff --git a/modules/family-calendar/includes/api/events-debug.log b/modules/family-calendar/includes/api/events-debug.log index 294814b..0e6de9e 100644 --- a/modules/family-calendar/includes/api/events-debug.log +++ b/modules/family-calendar/includes/api/events-debug.log @@ -24,3 +24,10 @@ [2026-01-07T14:26:31+01:00] RAW INPUT: [{"date":"2025-09-08","type":"OFF_CAROLE","person":"Carole","duration":1}] [2026-01-07T14:26:36+01:00] RAW INPUT: [{"date":"2025-09-08","type":"OFF_CAROLE","person":"Carole","duration":1},{"date":"2025-09-09","type":"OFF_CAROLE","person":"Carole","duration":1}] [2026-01-07T14:30:24+01:00] RAW INPUT: [{"date":"2025-10-06","type":"CENTRE","duration":1},{"date":"2025-10-07","type":"CENTRE","duration":1}] +[2026-01-08T09:21:34+01:00] RAW INPUT: [{"date":"2025-09-15","type":"EXTRA_OFF_CAROLE","person":"Carole","duration":1}] +[2026-01-08T09:21:38+01:00] RAW INPUT: [{"date":"2025-09-17","type":"EXTRA_OFF_CAROLE","person":"Carole","duration":1}] +[2026-01-08T09:21:58+01:00] RAW INPUT: [{"date":"2025-10-20","type":"CENTRE","duration":1},{"date":"2025-10-21","type":"CENTRE","duration":1},{"date":"2025-10-22","type":"CENTRE","duration":1},{"date":"2025-10-23","type":"CENTRE","duration":1},{"date":"2025-10-24","type":"CENTRE","duration":1}] +[2026-01-08T09:22:43+01:00] RAW INPUT: [{"date":"2025-12-22","type":"EXTRA_OFF_CAROLE","person":"Carole","duration":1},{"date":"2025-12-23","type":"EXTRA_OFF_CAROLE","person":"Carole","duration":1},{"date":"2025-12-24","type":"EXTRA_OFF_CAROLE","person":"Carole","duration":1}] +[2026-01-08T09:23:59+01:00] RAW INPUT: [{"date":"2025-12-22","type":"CENTRE","duration":1}] +[2026-01-08T09:24:00+01:00] RAW INPUT: [{"date":"2025-12-23","type":"CENTRE","duration":1}] +[2026-01-08T09:24:02+01:00] RAW INPUT: [{"date":"2025-12-24","type":"CENTRE","duration":1}] diff --git a/modules/family-calendar/includes/api/get-calendar-weeks-scolaire.php b/modules/family-calendar/includes/api/get-calendar-weeks-scolaire.php new file mode 100644 index 0000000..ff05ade --- /dev/null +++ b/modules/family-calendar/includes/api/get-calendar-weeks-scolaire.php @@ -0,0 +1,68 @@ + 08/2026) + * - défaut: année courante si non fourni + */ +$schoolYearStart = isset($_GET['school_year_start']) + ? (int)$_GET['school_year_start'] + : (int)date('Y'); + +if ($schoolYearStart < 2000 || $schoolYearStart > 2100) { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Année scolaire invalide.']); + exit; +} + +$yearStart = $schoolYearStart; +$yearEnd = $schoolYearStart + 1; + +// On veut toutes les semaines: +// - de septembre (month >= 9) de yearStart +// - à août (month <= 8) de yearEnd + +try { + $sql = " + SELECT + year, + week_iso_year, + week_iso_number, + week_label, + month, + month_name, + week_start_date, + mon_date, + tue_date, + wed_date, + thu_date, + fri_date + FROM pf_calendar_weeks + WHERE + (year = :year_start AND month >= 9) + OR (year = :year_end AND month <= 8) + ORDER BY week_start_date ASC + "; + + $stmt = $pdo->prepare($sql); + $stmt->execute([ + ':year_start' => $yearStart, + ':year_end' => $yearEnd, + ]); + + $weeks = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo json_encode([ + 'status' => 'success', + 'school_year' => $schoolYearStart, + 'weeks' => $weeks, + ]); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode([ + 'status' => 'error', + 'message' => $e->getMessage(), + ]); +} diff --git a/modules/family-calendar/includes/api/get-calendar-weeks.php b/modules/family-calendar/includes/api/get-calendar-weeks.php new file mode 100644 index 0000000..fd0dcb6 --- /dev/null +++ b/modules/family-calendar/includes/api/get-calendar-weeks.php @@ -0,0 +1,36 @@ +prepare(" + SELECT + year, + week_iso_year, + week_iso_number, + week_label, + month, + month_name, + week_start_date, + mon_date, + tue_date, + wed_date, + thu_date, + fri_date + FROM pf_calendar_weeks + WHERE year = :year + ORDER BY week_start_date ASC + "); + $stmt->execute([':year' => $year]); + $weeks = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo json_encode(['weeks' => $weeks]); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode([ + 'status' => 'error', + 'message' => $e->getMessage(), + ]); +}