// assets/js/family-calendar.js // === 1. Utilitaires dates === function formatDayMonth(date) { const d = String(date.getDate()).padStart(2, "0"); const m = String(date.getMonth() + 1).padStart(2, "0"); return `${d}/${m}`; } function getMonthNameFr(monthIndex) { const map = { 0: "Janvier", 1: "Fevrier", 2: "Mars", 3: "Avril", 4: "Mai", 5: "Juin", 6: "Juillet", 7: "Aout", 8: "Septembre", 9: "Octobre", 10: "Novembre", 11: "Decembre", }; return map[monthIndex] || ""; } /** * Calcule le n° de semaine dans l'année (1..), * Semaine 1 = semaine contenant le 1er janvier, début au lundi. */ function getWeekOfYear(date) { const year = date.getFullYear(); const startOfYear = new Date(year, 0, 1); const dayOfWeek = startOfYear.getDay() || 7; // dimanche = 0 -> 7 const diffToMonday = dayOfWeek === 1 ? 0 : dayOfWeek - 1; const firstWeekMonday = new Date(year, 0, 1 - diffToMonday); const diffMillis = date - firstWeekMonday; const diffDays = Math.floor(diffMillis / (1000 * 60 * 60 * 24)); return Math.floor(diffDays / 7) + 1; } // === 2. Génération des semaines === function generateWeeks() { const weeks = []; const start = new Date(2025, 8, 1); // 1er sept 2025 const end = new Date(2026, 7, 31); // 31 aout 2026 let current = new Date(start); // se placer sur le premier lundi >= 1er septembre 2025 while (current.getDay() !== 1) { // 1 = lundi current.setDate(current.getDate() + 1); } while (current <= end) { const monday = new Date(current); const tuesday = new Date(current); tuesday.setDate(monday.getDate() + 1); const wednesday = new Date(current); wednesday.setDate(monday.getDate() + 2); const thursday = new Date(current); thursday.setDate(monday.getDate() + 3); const friday = new Date(current); friday.setDate(monday.getDate() + 4); const year = monday.getFullYear(); const weekOfYear = getWeekOfYear(monday); const weekId = `${year}-W${String(weekOfYear).padStart(2, "0")}`; const monthIndex = monday.getMonth(); const monthKey = `${year}-${String(monthIndex + 1).padStart(2, "0")}`; const monthName = getMonthNameFr(monthIndex); weeks.push({ id: weekId, year, weekOfYear, monthKey, monthName, weekLabel: `W${weekOfYear}`, days: { mon: formatDayMonth(monday), tue: formatDayMonth(tuesday), wed: formatDayMonth(wednesday), thu: formatDayMonth(thursday), fri: formatDayMonth(friday), }, dayDates: { mon: monday, tue: tuesday, wed: wednesday, thu: thursday, fri: friday, }, dayFlags: { mon: { schoolHoliday: false }, tue: { schoolHoliday: false }, wed: { schoolHoliday: false }, thu: { schoolHoliday: false }, fri: { schoolHoliday: false }, }, totals: { offCarole: 0, extraOffCarole: 0, centre: 0, avis: 0, }, isSchoolHolidayWeek: false, alex: { total: 0, detail: "", }, laia: { total: 0, detail: "", }, }); current.setDate(current.getDate() + 7); } return weeks; } const weeks = generateWeeks(); const events = []; // tous les evenements (vacances, off Carole, centre, etc.) // === 3. Vacances scolaires (API gouv) === async function fetchSchoolHolidays(anneeScolaire, zoneLabel) { const baseUrl = "https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records"; const where = `annee_scolaire='${anneeScolaire}' AND zones LIKE '%${zoneLabel}%'`; const params = new URLSearchParams({ where, limit: "100", order_by: "start_date", }); const url = `${baseUrl}?${params.toString()}`; const res = await fetch(url); if (!res.ok) { console.error("Erreur API vacances scolaires", res.status, res.statusText); return []; } const data = await res.json(); return data.results || []; } /** * Regroupe les records par période, remplit le tableau recap, * et crée les événements VACANCES_SCOLAIRES. */ function addSchoolHolidayEventsFromRecords(records, events) { const tbody = document.querySelector("#schoolHolidaysTable tbody"); if (tbody) tbody.innerHTML = ""; // 1) Regrouper par période pour éviter les doublons d'académies const groups = new Map(); records.forEach((record) => { const startIso = record.start_date; const endIso = record.end_date; const desc = record.description || ""; const zones = record.zones || ""; const key = `${startIso}|${endIso}|${desc}`; if (!groups.has(key)) { groups.set(key, { startIso, endIso, description: desc, zones }); } }); // 2) Remplir le tableau récap ET créer les événements pour la coloration if (tbody) { Array.from(groups.values()) .sort((a, b) => new Date(a.startIso) - new Date(b.startIso)) .forEach((g) => { // On travaille avec des dates UTC pour éviter les décalages de fuseau horaire const startDate = new Date(g.startIso); const endDate = new Date(g.endIso); // --- Logique pour le tableau récap --- // Le 1er jour de vacances est le lendemain de start_date (ex: "vendredi soir" -> samedi) const displayStartDate = new Date(startDate); displayStartDate.setUTCDate(startDate.getUTCDate() + 1); // La date de reprise est le lendemain de end_date const repriseDate = new Date(endDate); repriseDate.setUTCDate(endDate.getUTCDate() + 1); const startStr = formatDayMonth(displayStartDate) + "/" + displayStartDate.getFullYear(); const endStr = formatDayMonth(repriseDate) + "/" + repriseDate.getFullYear(); const tr = document.createElement("tr"); tr.innerHTML = ` ${g.description} ${startStr} ${endStr} ${g.zones} `; tbody.appendChild(tr); // --- Logique pour la coloration du calendrier --- // On boucle du 1er jour de vacances (start_date + 1) au dernier jour inclus (end_date) let current = new Date(displayStartDate); while (current <= endDate) { const year = current.getFullYear(); const month = String(current.getMonth() + 1).padStart(2, "0"); const day = String(current.getDate()).padStart(2, "0"); const isoDate = `${year}-${month}-${day}`; events.push({ date: isoDate, type: "VACANCES_SCOLAIRES", duration: 1, }); current.setDate(current.getDate() + 1); } }); } } /** * Marque les jours et les semaines de vacances scolaires dans weeks. */ function markSchoolHolidayDaysAndWeeks(weeks, events) { // reset weeks.forEach((w) => { w.isSchoolHolidayWeek = false; Object.keys(w.dayFlags).forEach((dayKey) => { w.dayFlags[dayKey].schoolHoliday = false; }); }); events.forEach((evt) => { if (evt.type !== "VACANCES_SCOLAIRES") return; const [y, m, d] = evt.date.split("-").map(Number); const evtDate = new Date(y, m - 1, d); weeks.forEach((w) => { const dayKeys = ["mon", "tue", "wed", "thu", "fri"]; let hasHolidayInWeek = false; dayKeys.forEach((dayKey) => { const dayDate = w.dayDates[dayKey]; if ( dayDate.getFullYear() === evtDate.getFullYear() && dayDate.getMonth() === evtDate.getMonth() && dayDate.getDate() === evtDate.getDate() ) { w.dayFlags[dayKey].schoolHoliday = true; hasHolidayInWeek = true; } }); if (hasHolidayInWeek) { w.isSchoolHolidayWeek = true; } }); }); } // === 4. Calcul des rowspans pour la colonne Mois === function computeMonthSpans(weeks) { const counts = {}; weeks.forEach((w) => { counts[w.monthKey] = (counts[w.monthKey] || 0) + 1; }); return counts; } const monthSpans = computeMonthSpans(weeks); // === 5. Rendu du tableau principal === function renderTable() { const planningBody = document.getElementById("planningBody"); if (!planningBody) return; planningBody.innerHTML = ""; const showOnlyCaroleOff = document.getElementById("showOnlyCaroleOff")?.checked; const showOnlySchoolHoliday = document.getElementById( "showOnlySchoolHoliday" )?.checked; const monthRowRendered = {}; weeks.forEach((week) => { if (showOnlyCaroleOff && week.totals.offCarole === 0) return; if (showOnlySchoolHoliday && !week.isSchoolHolidayWeek) return; const tr = document.createElement("tr"); // Colonne Mois if (!monthRowRendered[week.monthKey]) { monthRowRendered[week.monthKey] = true; const tdMonth = document.createElement("td"); tdMonth.rowSpan = monthSpans[week.monthKey] || 1; tdMonth.textContent = week.monthName; tr.appendChild(tdMonth); } // Colonne Semaine const tdWeek = document.createElement("td"); tdWeek.textContent = week.weekLabel; tr.appendChild(tdWeek); // Jours (Lun -> Ven) avec coloration jours de vacances ["mon", "tue", "wed", "thu", "fri"].forEach((dayKey) => { const td = document.createElement("td"); td.textContent = week.days[dayKey]; if (week.dayFlags[dayKey].schoolHoliday) { td.classList.add("fc-day--school-holiday"); } tr.appendChild(td); }); // Totaux # Off / Extra / Centre / Avis (pour l'instant à 0) const tdOff = document.createElement("td"); tdOff.textContent = week.totals.offCarole.toFixed(2).replace(/\.00$/, ""); tr.appendChild(tdOff); const tdExtraOff = document.createElement("td"); tdExtraOff.textContent = week.totals.extraOffCarole .toFixed(2) .replace(/\.00$/, ""); tr.appendChild(tdExtraOff); const tdCentre = document.createElement("td"); tdCentre.textContent = week.totals.centre.toFixed(2).replace(/\.00$/, ""); tr.appendChild(tdCentre); const tdAvis = document.createElement("td"); tdAvis.textContent = week.totals.avis.toFixed(2).replace(/\.00$/, ""); tr.appendChild(tdAvis); // Alex total & détail const tdAlexTotal = document.createElement("td"); tdAlexTotal.textContent = week.alex.total.toFixed(2).replace(/\.00$/, ""); tr.appendChild(tdAlexTotal); const tdAlexDetail = document.createElement("td"); tdAlexDetail.textContent = week.alex.detail || ""; tr.appendChild(tdAlexDetail); // Laia total & détail const tdLaiaTotal = document.createElement("td"); tdLaiaTotal.textContent = week.laia.total.toFixed(2).replace(/\.00$/, ""); tr.appendChild(tdLaiaTotal); const tdLaiaDetail = document.createElement("td"); tdLaiaDetail.textContent = week.laia.detail || ""; tr.appendChild(tdLaiaDetail); planningBody.appendChild(tr); }); updateSummary(); } // === 6. Résumé (pour l'instant, 0 utilisés) === function updateSummary() { const summaryDiv = document.getElementById("summaryText"); if (!summaryDiv) return; const alexCpInit = parseFloat( document.getElementById("alexCpInit")?.value || "0" ); const alexRttInit = parseFloat( document.getElementById("alexRttInit")?.value || "0" ); const alexJaInit = parseFloat( document.getElementById("alexJaInit")?.value || "0" ); const laiaCpInit = parseFloat( document.getElementById("laiaCpInit")?.value || "0" ); const laiaRttInit = parseFloat( document.getElementById("laiaRttInit")?.value || "0" ); const laiaJaInit = parseFloat( document.getElementById("laiaJaInit")?.value || "0" ); const alexCpUsed = 0; const alexRttUsed = 0; const alexJaUsed = 0; const laiaCpUsed = 0; const laiaRttUsed = 0; const laiaJaUsed = 0; const alexCpLeft = alexCpInit - alexCpUsed; const alexRttLeft = alexRttInit - alexRttUsed; const alexJaLeft = alexJaInit - alexJaUsed; const laiaCpLeft = laiaCpInit - laiaCpUsed; const laiaRttLeft = laiaRttInit - laiaRttUsed; const laiaJaLeft = laiaJaInit - laiaJaUsed; summaryDiv.innerHTML = `

Alex
CP utilises : ${alexCpUsed.toFixed(2)} / ${alexCpInit.toFixed( 2 )} (reste ${alexCpLeft.toFixed(2)})
RTT utilises : ${alexRttUsed.toFixed(2)} / ${alexRttInit.toFixed( 2 )} (reste ${alexRttLeft.toFixed(2)})
JA utilises : ${alexJaUsed.toFixed(2)} / ${alexJaInit.toFixed( 2 )} (reste ${alexJaLeft.toFixed(2)})

Laia
CP utilises : ${laiaCpUsed.toFixed(2)} / ${laiaCpInit.toFixed( 2 )} (reste ${laiaCpLeft.toFixed(2)})
RTT utilises : ${laiaRttUsed.toFixed(2)} / ${laiaRttInit.toFixed( 2 )} (reste ${laiaRttLeft.toFixed(2)})
JA utilises : ${laiaJaUsed.toFixed(2)} / ${laiaJaInit.toFixed( 2 )} (reste ${laiaJaLeft.toFixed(2)})

`; } // === 7. Init === async function initFamilyCalendar() { if (!document.getElementById("planningBody")) return; [ "alexCpInit", "alexRttInit", "alexJaInit", "laiaCpInit", "laiaRttInit", "laiaJaInit", ].forEach((id) => { const el = document.getElementById(id); if (el) el.addEventListener("change", updateSummary); }); const showOnlyCaroleOff = document.getElementById("showOnlyCaroleOff"); if (showOnlyCaroleOff) { showOnlyCaroleOff.addEventListener("change", renderTable); } const showOnlySchoolHoliday = document.getElementById( "showOnlySchoolHoliday" ); if (showOnlySchoolHoliday) { showOnlySchoolHoliday.addEventListener("change", renderTable); } // Charger les vacances scolaires 2025-2026, Zone C const holidaysRecords = await fetchSchoolHolidays("2025-2026", "Zone C"); addSchoolHolidayEventsFromRecords(holidaysRecords, events); markSchoolHolidayDaysAndWeeks(weeks, events); renderTable(); } document.addEventListener("DOMContentLoaded", () => { initFamilyCalendar().catch((err) => console.error(err)); });