/** * family-calendar.js * Distinction congés / modes de garde + modif/suppression locales. */ 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() { this.planningBody = document.getElementById("planningBody"); this.selectionMenu = document.getElementById("selectionMenu"); this.schoolHolidaysTableBody = document.querySelector( "#schoolHolidaysTable tbody" ); this.monthCalendar = document.getElementById("fc-month-calendar"); this.monthSelectionMenu = document.getElementById( "fc-month-selectionMenu" ); // Initialiser avec septembre (mois 8, car 0-indexé) pour l'année scolaire 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.viewMode = "1month"; // "1month", "2months", "year" if (!this.planningBody || !this.selectionMenu) { console.error("planningBody ou selectionMenu manquant"); return; } this.isSelecting = false; this.selectedCells = []; this.monthSelectedCells = []; this.isMonthSelecting = false; this.dbEvents = []; this.fixedEvents = []; this.events = []; this.menuJustOpened = false; this.leaves = []; this.init(); window.cal = this; } // ================== INIT ================== async init() { this.setupEventListeners(); this.weeks = this.generateWeeksStructure(); this.dbEvents = this.loadDbEvents(); this.fixedEvents = await this.fetchPublicAndSchoolHolidays(); this.events = [...this.dbEvents, ...this.fixedEvents]; this.leaves = await this.fetchLeaves(); // Nouveau : set des jours fériés (dates ISO) this.publicHolidayDates = new Set( this.fixedEvents .filter((e) => e.type === "PUBLIC_HOLIDAY") .map((e) => e.date) ); this.reprocessAndRender(); this.renderMonthCalendar(); } loadDbEvents() { if (typeof serverData !== "undefined" && Array.isArray(serverData)) { return serverData.map((evt) => ({ id: evt.id, date: evt.event_date, type: evt.event_type, duration: parseFloat(evt.duration), person_id: evt.person_id, })); } return []; } async fetchLeaves() { try { const res = await fetch( "/modules/family-calendar/includes/api/get-leaves.php" ); if (!res.ok) { throw new Error("Erreur HTTP " + res.status); } const data = await res.json(); return data.leaves || []; } catch (err) { console.error("Erreur lors du chargement des congés Alex/Laia :", err); return []; } } async fetchPublicAndSchoolHolidays() { // 1. Jours fériés (fixes) const publicHolidays = [ { date: "2025-11-01", type: "PUBLIC_HOLIDAY", duration: 1 }, { date: "2025-11-11", type: "PUBLIC_HOLIDAY", duration: 1 }, { date: "2025-12-25", type: "PUBLIC_HOLIDAY", duration: 1 }, { date: "2026-01-01", type: "PUBLIC_HOLIDAY", duration: 1 }, { date: "2026-04-06", type: "PUBLIC_HOLIDAY", duration: 1 }, { date: "2026-05-01", type: "PUBLIC_HOLIDAY", duration: 1 }, { date: "2026-05-08", type: "PUBLIC_HOLIDAY", duration: 1 }, { date: "2026-05-14", type: "PUBLIC_HOLIDAY", duration: 1 }, { date: "2026-05-25", type: "PUBLIC_HOLIDAY", duration: 1 }, ].map((e, idx) => ({ id: `ph-${idx}`, ...e, })); // 2. Vacances scolaires (API) const schoolHolidayEvents = []; let holidayRecords = []; try { const response = await fetch( "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 schoolHolidaysData = await response.json(); holidayRecords = schoolHolidaysData.results || []; holidayRecords.forEach((record, index) => { let current = new Date(record.start_date); let end = new Date(record.end_date); while (current < end) { const isoDate = `${current.getFullYear()}-${String( current.getMonth() + 1 ).padStart(2, "0")}-${String(current.getDate()).padStart(2, "0")}`; schoolHolidayEvents.push({ id: `sh-${isoDate}-${index}`, date: isoDate, type: "VACANCES_SCOLAIRES", duration: 1, }); current.setDate(current.getDate() + 1); } }); // Remplir le tableau HTML des vacances this.renderSchoolHolidaysTable(holidayRecords); } catch (error) { console.error("Impossible de charger les vacances scolaires.", error); } // 3. Retourne tous les événements "fixes" 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); // Remonter au lundi le plus proche <= start while (current.getDay() !== 1) current.setDate(current.getDate() - 1); 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: [] }; }); weeks.push(weekData); current.setDate(current.getDate() + 7); } // 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() { this.reprocessEvents(); this.renderTable(); this.updateGlobalSummary(); this.renderMonthCalendar(); } reprocessEvents() { // Réinitialisation des semaines this.weeks.forEach((w) => { w.totals = { offCarole: 0, extraOffCarole: 0, centre: 0, avis: 0, pepSick: 0, presencePep: 0, // Nouveaux totaux pour Alex / Laia alexCP: 0, alexJRA: 0, alexJA: 0, laiaCP: 0, laiaJRA: 0, laiaJA: 0, }; Object.values(w.dayFlags).forEach((df) => { df.eventsOnDay = []; }); }); // ================== Événements Carole / garde / Pep ================== this.events.forEach((evt) => { const evtDate = new Date(evt.date + "T00:00:00"); // Trouver la semaine correspondant à la date de l'événement const week = this.weeks.find( (w) => evtDate >= w.dayDates.mon && evtDate <= w.dayDates.fri ); if (!week) return; // Identifier le jour (mon, tue, wed, thu, fri) const dayKey = Object.keys(week.dayDates).find( (key) => week.dayDates[key].toDateString() === evtDate.toDateString() ); if (dayKey) { week.dayFlags[dayKey].eventsOnDay.push(evt); } const dur = parseFloat(evt.duration) || 1; // Mettre à jour les totaux hebdo selon le type switch (evt.type) { case "OFF_CAROLE": week.totals.offCarole += dur; break; case "EXTRA_OFF_CAROLE": week.totals.extraOffCarole += dur; break; case "CENTRE": week.totals.centre += dur; break; case "AVIS": week.totals.avis += dur; break; case "PEP_SICK": week.totals.pepSick += dur; break; default: break; } }); // ================== Congés Alex / Laia (CP / JRA / JA) ================== (this.leaves || []).forEach((lv) => { const lvDate = new Date(lv.leave_date + "T00:00:00"); const week = this.weeks.find( (w) => lvDate >= w.dayDates.mon && lvDate <= w.dayDates.fri ); if (!week) return; const isAlex = lv.person_id === 2; const isLaia = lv.person_id === 3; const type = lv.leave_type; // "CP", "JRA" ou "JA" const dur = parseFloat(lv.duration) || 1; if (isAlex) { if (type === "CP") week.totals.alexCP += dur; if (type === "JRA") week.totals.alexJRA += dur; if (type === "JA") week.totals.alexJA += dur; } else if (isLaia) { if (type === "CP") week.totals.laiaCP += dur; if (type === "JRA") week.totals.laiaJRA += dur; if (type === "JA") week.totals.laiaJA += dur; } }); // Calcul présence Pep par semaine this.weeks.forEach((w) => { // Calcul du nombre de jours potentiels d'accueil Pep dans la semaine let workingDays = 0; ["mon", "tue", "wed", "thu", "fri"].forEach((dayKey) => { const d = w.dayDates[dayKey]; const isoDate = `${d.getFullYear()}-${String( d.getMonth() + 1 ).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; const isPublicHoliday = this.publicHolidayDates && this.publicHolidayDates.has(isoDate); if (!isPublicHoliday) { workingDays++; } }); const absencesPep = (w.totals.offCarole || 0) + (w.totals.extraOffCarole || 0) + (w.totals.pepSick || 0); w.totals.presencePep = Math.max(0, workingDays - absencesPep); w.totals.workingDays = workingDays; // si tu veux l'afficher un jour }); } updateGlobalSummary() { const summaryDiv = document.getElementById("globalSummary"); if (!summaryDiv) return; const allEvents = this.dbEvents || []; const totalOff = allEvents .filter((e) => e.type === "OFF_CAROLE") .reduce((sum, e) => sum + Number(e.duration || 1), 0); const totalExtraOff = allEvents .filter((e) => e.type === "EXTRA_OFF_CAROLE") .reduce((sum, e) => sum + Number(e.duration || 1), 0); const totalPepSick = allEvents .filter((e) => e.type === "PEP_SICK") .reduce((sum, e) => sum + Number(e.duration || 1), 0); // Calculer les jours ouvrés & présence Pep sur l'année scolaire let totalWorkingDays = 0; let totalPresencePep = 0; // On parcourt l'année scolaire en mois, en réutilisant calculateMonthTotals const start = new Date(2025, 8, 1); // 1 sept 2025 const end = new Date(2026, 7, 31); // 31 août 2026 const current = new Date(start); while (current <= end) { const y = current.getFullYear(); const m = current.getMonth(); const monthTotals = this.calculateMonthTotals(y, m); totalWorkingDays += monthTotals.workingDays || 0; totalPresencePep += monthTotals.presencePep || 0; // Passer au 1er du mois suivant current.setMonth(current.getMonth() + 1); current.setDate(1); } summaryDiv.innerHTML = `

Off Carole : ${totalOff} jours

Extra Off Carole : ${totalExtraOff} jours

Pep malade : ${totalPepSick} jours

Jours potentiels d'accueil Pep (année scolaire, hors fériés) : ${totalWorkingDays}

Présence Pep (année scolaire) : ${totalPresencePep} jours

`; } renderSchoolHolidaysTable(holidayRecords) { if (!this.schoolHolidaysTableBody) return; this.schoolHolidaysTableBody.innerHTML = ""; const uniqueHolidays = new Map(); holidayRecords.forEach((r) => uniqueHolidays.set(`${r.start_date}|${r.end_date}`, r) ); [...uniqueHolidays.values()] .sort((a, b) => new Date(a.start_date) - new Date(b.start_date)) .forEach((record) => { const tr = document.createElement("tr"); const startDate = new Date(record.start_date); const endDate = new Date(record.end_date); tr.innerHTML = ` ${record.description} ${startDate.toLocaleDateString("fr-FR")} ${endDate.toLocaleDateString("fr-FR")} ${record.zones} `; this.schoolHolidaysTableBody.appendChild(tr); }); } renderTable() { this.planningBody.innerHTML = ""; const monthSpans = this.weeks.reduce( (acc, w) => ({ ...acc, [w.monthKey]: (acc[w.monthKey] || 0) + 1 }), {} ); const monthRowRendered = {}; const formatTotal = (total) => total > 0 ? (Number.isInteger(total) ? total : total.toFixed(1)) : ""; this.weeks.forEach((week, index) => { const tr = document.createElement("tr"); // Déterminer si c'est la première ou la dernière semaine du mois const isFirstWeekOfMonth = index === 0 || this.weeks[index - 1].monthKey !== week.monthKey; const isLastWeekOfMonth = index === this.weeks.length - 1 || this.weeks[index + 1].monthKey !== week.monthKey; if (isFirstWeekOfMonth) { tr.classList.add("fc-month-first-week-row"); } if (isLastWeekOfMonth) { tr.classList.add("fc-month-last-week-row"); } // Colonne Mois (avec rowSpan) if (!monthRowRendered[week.monthKey]) { monthRowRendered[week.monthKey] = true; const tdMonth = document.createElement("td"); tdMonth.rowSpan = monthSpans[week.monthKey] || 1; tdMonth.textContent = week.monthName; tdMonth.classList.add("col-month"); tr.appendChild(tdMonth); } // Semaine const tdWeek = document.createElement("td"); tdWeek.textContent = week.weekLabel; tdWeek.classList.add("col-month"); tr.appendChild(tdWeek); // Jours Lundi–Vendredi ["mon", "tue", "wed", "thu", "fri"].forEach((dayKey) => { const td = document.createElement("td"); td.classList.add("col-day"); const dayDate = week.dayDates[dayKey]; td.dataset.date = `${dayDate.getFullYear()}-${String( dayDate.getMonth() + 1 ).padStart(2, "0")}-${String(dayDate.getDate()).padStart(2, "0")}`; td.textContent = `${String(dayDate.getDate()).padStart( 2, "0" )}/${String(dayDate.getMonth() + 1).padStart(2, "0")}`; let hasGarde = false; let gardeType = null; let hasPepSick = false; week.dayFlags[dayKey].eventsOnDay.forEach((evt) => { const classMap = { VACANCES_SCOLAIRES: "fc-day--school-holiday", PUBLIC_HOLIDAY: "fc-day--public-holiday", OFF_CAROLE: "fc-day--off-carole", EXTRA_OFF_CAROLE: "fc-day--extra-off-carole", // pas de couleur pour PEP_SICK dans l'hebdo }; if (classMap[evt.type]) { td.classList.add(classMap[evt.type]); } if (GUARDE_TYPES.includes(evt.type)) { hasGarde = true; gardeType = evt.type; } if (evt.type === "PEP_SICK") { hasPepSick = true; } }); if (hasGarde) { td.classList.add("fc-day--has-guard"); if (gardeType === "CENTRE") td.classList.add("fc-day--centre"); if (gardeType === "AVIS") td.classList.add("fc-day--avis"); } if (hasPepSick) { const pepSpan = document.createElement("span"); pepSpan.className = "fc-pep-sick-emoji"; pepSpan.textContent = "🤒"; td.appendChild(pepSpan); } // Congés Alex/Laia const isoDate = td.dataset.date; const leavesOnDay = this.leaves.filter( (lv) => lv.leave_date === isoDate ); if (leavesOnDay.length > 0) { const container = document.createElement("div"); container.className = "fc-leaves-label-container"; const hasAlex = leavesOnDay.some((lv) => lv.person_id === 2); const hasLaia = leavesOnDay.some((lv) => lv.person_id === 3); if (hasAlex) { const alexSpan = document.createElement("span"); alexSpan.className = "fc-leaves-label"; alexSpan.textContent = "AF"; container.appendChild(alexSpan); } if (hasLaia) { const laiaSpan = document.createElement("span"); laiaSpan.className = "fc-leaves-label"; laiaSpan.textContent = "LM"; container.appendChild(laiaSpan); } td.appendChild(container); } tr.appendChild(td); }); // Totaux const tdOff = document.createElement("td"); tdOff.textContent = formatTotal(week.totals.offCarole); tdOff.classList.add("col-total"); tr.appendChild(tdOff); const tdExtra = document.createElement("td"); tdExtra.textContent = formatTotal(week.totals.extraOffCarole); tdExtra.classList.add("col-total"); tr.appendChild(tdExtra); const tdCentre = document.createElement("td"); tdCentre.textContent = formatTotal(week.totals.centre); tdCentre.classList.add("col-total"); tr.appendChild(tdCentre); const tdAvis = document.createElement("td"); tdAvis.textContent = formatTotal(week.totals.avis); tdAvis.classList.add("col-total"); tr.appendChild(tdAvis); const tdPepSick = document.createElement("td"); tdPepSick.textContent = formatTotal(week.totals.pepSick); tdPepSick.classList.add("col-total"); tr.appendChild(tdPepSick); const tdPresencePep = document.createElement("td"); tdPresencePep.textContent = formatTotal(week.totals.presencePep); tdPresencePep.classList.add("col-total"); tr.appendChild(tdPresencePep); // 6 colonnes ALEX : [CP Av., CP Use, JRA Av., JRA Use, JA Av., JA Use] for (let i = 0; i < 6; i++) { const td = document.createElement("td"); td.classList.add("col-alex-sub"); if (i % 2 === 0) { // colonnes Av. (pour l'instant vides, on remplira plus tard si besoin) td.classList.add("col-alex-av"); td.textContent = ""; // ex: futur "solde" CP/JRA/JA } else { // colonnes Use : remplir selon le couple (index, type) td.classList.add("col-alex-use"); let value = ""; if (i === 1) { // CP Use value = formatTotal(week.totals.alexCP); } else if (i === 3) { // JRA Use value = formatTotal(week.totals.alexJRA); } else if (i === 5) { // JA Use value = formatTotal(week.totals.alexJA); } td.textContent = value; } tr.appendChild(td); } // 6 colonnes LAIA : [CP Av., CP Use, JRA Av., JRA Use, JA Av., JA Use] for (let i = 0; i < 6; i++) { const td = document.createElement("td"); td.classList.add("col-laia-sub"); if (i % 2 === 0) { td.classList.add("col-laia-av"); td.textContent = ""; // futur solde } else { td.classList.add("col-laia-use"); let value = ""; if (i === 1) { // CP Use value = formatTotal(week.totals.laiaCP); } else if (i === 3) { // JRA Use value = formatTotal(week.totals.laiaJRA); } else if (i === 5) { // JA Use value = formatTotal(week.totals.laiaJA); } td.textContent = value; } tr.appendChild(td); } this.planningBody.appendChild(tr); }); } // ================== INTERACTIONS ================== setupEventListeners() { this.planningBody.addEventListener("mousedown", (e) => this.handleMouseDown(e) ); document.addEventListener("mousemove", (e) => this.handleMouseMove(e)); document.addEventListener("mouseup", (e) => this.handleMouseUp(e)); document.addEventListener( "click", (e) => this.handleClickOutsideMenu(e), true ); this.selectionMenu.addEventListener("click", (e) => this.handleMenuClick(e) ); // Événements pour le calendrier mensuel if (this.monthCalendar) { this.monthCalendar.addEventListener("mousedown", (e) => this.handleMonthMouseDown(e) ); document.addEventListener("mousemove", (e) => this.handleMonthMouseMove(e) ); document.addEventListener("mouseup", (e) => this.handleMonthMouseUp(e)); if (this.monthSelectionMenu) { // Gestion add / add-single / delete / update sur le calendrier mensuel this.monthSelectionMenu.addEventListener("click", (e) => this.handleMonthMenuClick(e) ); } } // Navigation du calendrier mensuel const prevBtn = document.getElementById("fc-prev-month"); const nextBtn = document.getElementById("fc-next-month"); if (prevBtn) { prevBtn.addEventListener("click", () => this.navigateMonth(-1)); } if (nextBtn) { nextBtn.addEventListener("click", () => this.navigateMonth(1)); } // Boutons de changement de vue const viewButtons = document.querySelectorAll(".fc-view-button"); viewButtons.forEach((btn) => { btn.addEventListener("click", (e) => { const view = e.target.dataset.view; this.setViewMode(view); }); }); } handleMouseDown(e) { console.log("handleMouseDown raw target:", e.target); const cell = e.target.closest("#planningTable td[data-date]"); console.log("handleMouseDown cell:", cell); if (!cell) return; e.preventDefault(); this.clearSelection(); this.isSelecting = true; cell.classList.add("fc-day--selected"); this.selectedCells = [cell]; console.log( "handleMouseDown after select, selectedCells =", this.selectedCells ); } handleMouseMove(e) { if (!this.isSelecting) return; const cell = e.target.closest("#planningTable td[data-date]"); if (!cell) return; if (!this.selectedCells.includes(cell)) { cell.classList.add("fc-day--selected"); this.selectedCells.push(cell); } } handleMouseUp(e) { console.log( "handleMouseUp called, selectedCells.length =", this.selectedCells.length ); if (!this.isSelecting) { return; } this.isSelecting = false; if (this.selectedCells.length === 0) return; // ===== Cas 1 seule cellule ===== if (this.selectedCells.length === 1) { const date = this.selectedCells[0].dataset.date; const eventsOnDay = this.events.filter( (evt) => evt.date === date && MODIFIABLE_TYPES.includes(evt.type) ); const conge = eventsOnDay.find((ev) => CONGE_TYPES.includes(ev.type)); const garde = eventsOnDay.find((ev) => GUARDE_TYPES.includes(ev.type)); const pep = eventsOnDay.find((ev) => PEP_TYPES.includes(ev.type)); this.showEditMenuForDay(e, { conge, garde, pep, date }); return; } // ===== Multi-jours ===== const selectedDates = this.selectedCells.map((c) => c.dataset.date); const eventsOnDates = this.events.filter( (evt) => selectedDates.includes(evt.date) && MODIFIABLE_TYPES.includes(evt.type) ); const conges = eventsOnDates.filter((ev) => CONGE_TYPES.includes(ev.type) ); const gardes = eventsOnDates.filter((ev) => GUARDE_TYPES.includes(ev.type) ); const uniqueCongeTypes = new Set(conges.map((c) => c.type)); const uniqueGardeTypes = new Set(gardes.map((g) => g.type)); const allDatesHaveConge = conges.length === selectedDates.length && uniqueCongeTypes.size === 1; const allDatesHaveGarde = gardes.length === selectedDates.length && uniqueGardeTypes.size === 1; // On construit quand même un bulkInfo partiel (même si pas homogène) const bulkInfo = { selectedDates, conges, gardes, congeType: allDatesHaveConge ? conges[0].type : null, gardeType: allDatesHaveGarde ? gardes[0].type : null, }; // On affiche toujours le menu bulk pour permettre Alex/Laia, // et éventuellement les actions Carole/garde si congeType/gardeType sont définis. this.showBulkMenu(e, bulkInfo); } showBulkMenu(e, bulkInfo) { const { selectedDates } = bulkInfo; const nbDays = selectedDates.length; let html = `
Actions multi-jours
`; // Section Congés Carole html += `
Congés Carole (${nbDays} jours)
`; // Section Mode de garde – toujours visible html += `
Mode de garde (${nbDays} jours)
`; // Section bulk congés Alex / Laia – tableau identique au single html += `
Congés Alex / Laia (${nbDays} jours)
Alex Laia
`; // Après le tableau CP/JRA/JA html += ` `; this.selectionMenu.innerHTML = html; this._currentBulkInfo = bulkInfo; this.positionAndShowMenu(e); } handleClickOutsideMenu(e) { if (this.menuJustOpened) { this.menuJustOpened = false; return; } if ( this.selectionMenu && this.selectionMenu.style.display === "block" && !this.selectionMenu.contains(e.target) ) { this.clearSelection(); } if ( this.monthSelectionMenu && this.monthSelectionMenu.style.display === "block" && !this.monthSelectionMenu.contains(e.target) ) { this.clearMonthSelection(); } } clearSelection() { this.selectionMenu.style.display = "none"; this.selectedCells.forEach((cell) => cell.classList.remove("fc-day--selected") ); this.selectedCells = []; } showAddMenu(e) { this.selectionMenu.innerHTML = `
Ajouter
Mode de garde
Pep
`; this.positionAndShowMenu(e); } showEditMenuForDay(e, { conge, garde, pep, date }) { console.log( "[EDIT MENU] pour date", date, "conge =", conge, "garde =", garde ); // --- Section congé Carole --- let congeSection = ""; if (conge) { const oppositeConge = conge.type === "OFF_CAROLE" ? "EXTRA_OFF_CAROLE" : "OFF_CAROLE"; congeSection = `
Congé Carole
`; } else { congeSection = `
Ajouter un congé
`; } // --- Section mode de garde --- let gardeSection = ""; if (garde) { const oppositeGarde = garde.type === "CENTRE" ? "AVIS" : "CENTRE"; gardeSection = `
Mode de garde
`; } else { gardeSection = `
Ajouter mode de garde
`; } // --- Section Pep malade --- let pepSection = ""; if (pep) { pepSection = `
Pep
`; } else { pepSection = `
Pep
`; } // --- Section congés Alex / Laia --- console.log( "[EDIT MENU] this.leaves length =", (this.leaves || []).length ); const leavesOnDay = (this.leaves || []).filter( (lv) => lv.leave_date === date ); console.log("[EDIT MENU] leavesOnDay =", leavesOnDay); let leavesSection = `
Congés Alex / Laia
Alex Laia
`; // Bouton de suppression si congés existants ce jour-là if (leavesOnDay.length > 0) { leavesSection += ` `; } const fullHtml = congeSection + gardeSection + pepSection + leavesSection; console.log("[EDIT MENU] full menu HTML length =", fullHtml.length); this.selectionMenu.innerHTML = fullHtml; this.positionAndShowMenu(e); } positionAndShowMenu(e) { const wrapper = document.getElementById("planningTable-wrapper"); if (!wrapper) { // fallback sécurité this.selectionMenu.style.display = "block"; this.selectionMenu.style.left = `${e.clientX + 5}px`; this.selectionMenu.style.top = `${e.clientY + 5}px`; return; } const rect = wrapper.getBoundingClientRect(); this.selectionMenu.style.display = "block"; // Position relative au wrapper (qui scrolle) const x = e.clientX - rect.left + wrapper.scrollLeft; const y = e.clientY - rect.top + wrapper.scrollTop; this.selectionMenu.style.left = `${x + 5}px`; this.selectionMenu.style.top = `${y + 5}px`; this.menuJustOpened = true; setTimeout(() => { this.menuJustOpened = false; }, 0); } // ===== Helpers Carole / garde ===== // Single day – Carole async setCaroleSingle(date, type) { try { // 1) supprimer OFF_CAROLE / EXTRA_OFF_CAROLE sur ce jour await this.manageEvent({ action: "delete_day_types", date, types: CONGE_TYPES, // ["OFF_CAROLE", "EXTRA_OFF_CAROLE"] }); // 2) ajouter le nouveau congé await this.manageEvent({ action: "add_multiple", events: [ { date, type, person: "Carole", duration: 1.0, }, ], }); } catch (err) { console.error("setCaroleSingle error:", err); alert("Erreur lors de la mise à jour du congé Carole : " + err.message); } } // Single day – Garde async setGardeSingle(date, type) { try { await this.manageEvent({ action: "delete_day_types", date, types: GUARDE_TYPES, // ["CENTRE","AVIS"] }); await this.manageEvent({ action: "add_multiple", events: [ { date, type, duration: 1.0, }, ], }); } catch (err) { console.error("setGardeSingle error:", err); alert( "Erreur lors de la mise à jour du mode de garde : " + err.message ); } } // Bulk – Carole async setCaroleBulk(dates, type) { try { await this.manageEvent({ action: "bulk_delete_day_types", dates, types: CONGE_TYPES, }); await this.manageEvent({ action: "add_multiple", events: dates.map((date) => ({ date, type, person: "Carole", duration: 1.0, })), }); } catch (err) { console.error("setCaroleBulk error:", err); alert("Erreur bulk congés Carole : " + err.message); } } // Bulk – Garde async setGardeBulk(dates, type) { try { await this.manageEvent({ action: "bulk_delete_day_types", dates, types: GUARDE_TYPES, }); await this.manageEvent({ action: "add_multiple", events: dates.map((date) => ({ date, type, duration: 1.0, })), }); } catch (err) { console.error("setGardeBulk error:", err); alert("Erreur bulk mode de garde : " + err.message); } } async handleMenuClick(e) { const button = e.target.closest("button[data-action]"); if (!button) return; const { action, eventId, newType, type, person, date } = button.dataset; // ===================== SINGLE DAY – Carole / Garde / Pep ===================== // Single day Carole/garde (add-single depuis le menu d’un jour) if (action === "add-single") { const targetDate = date; // Switch congé Carole (OFF_CAROLE / EXTRA_OFF_CAROLE) if (CONGE_TYPES.includes(type)) { try { // Récupérer les events de ce jour const existingOnDate = this.dbEvents.filter( (evt) => evt.date === targetDate && CONGE_TYPES.includes(evt.type) ); // Supprimer tous les congés Carole ce jour-là for (const evt of existingOnDate) { await fetch( "/modules/family-calendar/includes/api/manage-event.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "delete", event_id: evt.id, }), } ); } // Ajouter le nouveau type const response = await fetch( "/modules/family-calendar/includes/api/save-events.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify([ { date: targetDate, type, person: "Carole", duration: 1.0, }, ]), } ); if (!response.ok) throw new Error("Erreur HTTP " + response.status); const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); const dataEvents = await resEvents.json(); this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); } catch (err) { console.error("Erreur congé Carole:", err); alert("Erreur congé Carole : " + err.message); } this.clearSelection(); return; } // Switch mode de garde (CENTRE / AVIS) if (GUARDE_TYPES.includes(type)) { try { const existingOnDate = this.dbEvents.filter( (evt) => evt.date === targetDate && GUARDE_TYPES.includes(evt.type) ); // Supprimer tous les modes de garde ce jour-là for (const evt of existingOnDate) { await fetch( "/modules/family-calendar/includes/api/manage-event.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "delete", event_id: evt.id, }), } ); } // Ajouter le nouveau type const response = await fetch( "/modules/family-calendar/includes/api/save-events.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify([ { date: targetDate, type, duration: 1.0, }, ]), } ); if (!response.ok) throw new Error("Erreur HTTP " + response.status); const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); const dataEvents = await resEvents.json(); this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); } catch (err) { console.error("Erreur mode de garde:", err); alert("Erreur mode de garde : " + err.message); } this.clearSelection(); return; } // Pep (comportement existant) const newEvent = { date: targetDate, type, person, duration: 1.0, }; this.clearSelection(); try { const response = await fetch( "/modules/family-calendar/includes/api/save-events.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify([newEvent]), } ); if (!response.ok) throw new Error("Erreur HTTP " + response.status); const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); const dataEvents = await resEvents.json(); this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); } catch (err) { console.error("Erreur ajout Pep:", err); alert("Erreur lors de l'ajout."); } return; } // ===================== BULK – Carole / Garde / Pep =========================== if (action === "add") { const selectedDates = this.selectedCells.map((c) => c.dataset.date); // Bulk congés Carole if (CONGE_TYPES.includes(type)) { try { // Supprimer tous les OFF/EXTRA_OFF sur ces dates const existingOnDates = this.dbEvents.filter( (evt) => selectedDates.includes(evt.date) && CONGE_TYPES.includes(evt.type) ); for (const evt of existingOnDates) { await fetch( "/modules/family-calendar/includes/api/manage-event.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "delete", event_id: evt.id, }), } ); } // Ajouter le type choisi sur chaque date const newEvents = selectedDates.map((d) => ({ date: d, type, person: "Carole", duration: 1.0, })); const response = await fetch( "/modules/family-calendar/includes/api/save-events.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newEvents), } ); if (!response.ok) throw new Error("Erreur HTTP " + response.status); const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); const dataEvents = await resEvents.json(); this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); } catch (err) { console.error("Erreur bulk congés Carole:", err); alert("Erreur bulk congés Carole : " + err.message); } this.clearSelection(); return; } // Bulk mode de garde if (GUARDE_TYPES.includes(type)) { try { const existingOnDates = this.dbEvents.filter( (evt) => selectedDates.includes(evt.date) && GUARDE_TYPES.includes(evt.type) ); for (const evt of existingOnDates) { await fetch( "/modules/family-calendar/includes/api/manage-event.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "delete", event_id: evt.id, }), } ); } const newEvents = selectedDates.map((d) => ({ date: d, type, duration: 1.0, })); const response = await fetch( "/modules/family-calendar/includes/api/save-events.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newEvents), } ); if (!response.ok) throw new Error("Erreur HTTP " + response.status); const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); const dataEvents = await resEvents.json(); this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); } catch (err) { console.error("Erreur bulk mode de garde:", err); alert("Erreur bulk mode de garde : " + err.message); } this.clearSelection(); return; } // Bulk Pep (comportement existant) const newEvents = this.selectedCells.map((cell) => ({ date: cell.dataset.date, type, person, duration: 1.0, })); this.clearSelection(); try { const response = await fetch( "/modules/family-calendar/includes/api/save-events.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newEvents), } ); if (!response.ok) throw new Error("Erreur HTTP " + response.status); const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); const dataEvents = await resEvents.json(); this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); } catch (err) { console.error("Erreur bulk add:", err); alert("Erreur lors de l'ajout."); } return; } // ===================== ALEX / LAIA (single / bulk) ========================== // Single day leaves Alex/Laia if (action === "add-leave") { const leaveDate = date; const personId = parseInt(button.dataset.personId, 10); const leaveType = button.dataset.leaveType; // "CP", "JRA", "JA" if (!leaveDate || !personId || !leaveType) { this.clearSelection(); return; } this.clearSelection(); try { // 1) Supprimer tous les congés Alex/Laia pour cette personne ce jour-là await fetch("/modules/family-calendar/includes/api/manage-leaf.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "delete_day_person", date: leaveDate, person_id: personId, }), }); // 2) Ajouter le type choisi const responseAdd = await fetch( "/modules/family-calendar/includes/api/save-leaves.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify([ { date: leaveDate, person_id: personId, leave_type: leaveType, duration: 1.0, }, ]), } ); if (!responseAdd.ok) { throw new Error("Erreur HTTP " + responseAdd.status); } this.leaves = await this.fetchLeaves(); this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { console.error("Erreur set leave single:", err); alert("Erreur congé Alex/Laia : " + err.message); } return; } if (action === "delete-leaves-day") { const leaveDate = date; if (!leaveDate) { this.clearSelection(); return; } try { const response = await fetch( "/modules/family-calendar/includes/api/manage-leaf.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "delete_day", date: leaveDate }), } ); if (!response.ok) { const errData = await response.json().catch(() => ({})); throw new Error( errData.message || "Erreur HTTP " + response.status ); } this.leaves = await this.fetchLeaves(); this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { console.error("Erreur delete leaves day:", err); alert( "Erreur lors de la suppression des congés Alex/Laia pour ce jour: " + err.message ); } this.clearSelection(); return; } // Bulk leaves Alex/Laia via bulk-add-leave if (action === "bulk-add-leave") { if (!this._currentBulkInfo) { this.clearSelection(); return; } const selectedDates = this._currentBulkInfo.selectedDates || []; const personId = parseInt(button.dataset.personId, 10); const leaveType = button.dataset.leaveType; this._currentBulkInfo = null; try { // Supprimer pour cette personne sur toutes les dates await fetch("/modules/family-calendar/includes/api/manage-leaf.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "bulk_delete_day_person", dates: selectedDates, person_id: personId, }), }); // Ajouter sur toutes les dates const newLeaves = selectedDates.map((d) => ({ date: d, person_id: personId, leave_type: leaveType, duration: 1.0, })); const responseAdd = await fetch( "/modules/family-calendar/includes/api/save-leaves.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newLeaves), } ); if (!responseAdd.ok) throw new Error("Erreur HTTP " + responseAdd.status); this.leaves = await this.fetchLeaves(); this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { console.error("Erreur bulk-add-leave:", err); alert("Erreur bulk congés Alex/Laia : " + err.message); } this.clearSelection(); return; } // ===================== SUPPRESSION SIMPLE EVENT ============================= if (action === "delete") { if (!eventId) { this.clearSelection(); return; } try { const response = await fetch( "/modules/family-calendar/includes/api/manage-event.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "delete", event_id: eventId }), } ); if (!response.ok) { const errData = await response.json().catch(() => ({})); throw new Error( errData.message || "Erreur HTTP " + response.status ); } const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); const dataEvents = await resEvents.json(); this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); } catch (err) { console.error("Erreur delete:", err); alert("Erreur lors de la suppression : " + err.message); } this.clearSelection(); return; } // ===================== SUPPRESSION BULK LEAVE ============================= if (action === "bulk-clear-leave") { if (!this._currentBulkInfo) { this.clearSelection(); return; } const selectedDates = this._currentBulkInfo.selectedDates || []; const personId = parseInt(button.dataset.personId, 10); try { await fetch("/modules/family-calendar/includes/api/manage-leaf.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "bulk_delete_day_person", dates: selectedDates, person_id: personId, }), }); this.leaves = await this.fetchLeaves(); this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { console.error("Erreur bulk-clear-leave:", err); alert( "Erreur lors de la suppression des congés sur ces jours : " + err.message ); } this._currentBulkInfo = null; this.clearSelection(); return; } // ===================== UPDATE SIMPLE (utilisé par les boutons "Remplacer par") ===== if (action === "update") { if (!eventId) { this.clearSelection(); return; } try { const response = await fetch( "/modules/family-calendar/includes/api/manage-event.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "update", event_id: eventId, new_type: newType, }), } ); if (!response.ok) { const errData = await response.json().catch(() => ({})); throw new Error( errData.message || "Erreur HTTP " + response.status ); } const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); const dataEvents = await resEvents.json(); this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); } catch (err) { console.error("Erreur update:", err); alert("Erreur lors de la modification : " + err.message); } this.clearSelection(); return; } } async manageEvent(payload) { try { const response = await fetch( "/modules/family-calendar/includes/api/manage-event.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), } ); if (!response.ok) { const errData = await response.json().catch(() => ({})); throw new Error(errData.message || "Erreur HTTP " + response.status); } const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); if (!resEvents.ok) { throw new Error("Erreur lors du rechargement des événements."); } const dataEvents = await resEvents.json(); this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); this.renderMonthCalendar(); } catch (error) { console.error("Erreur manageEvent :", error); alert("Erreur: " + error.message); } } // ================== CALENDRIER MENSUEL ================== setViewMode(mode) { this.viewMode = mode; // Mettre à jour les boutons actifs document.querySelectorAll(".fc-view-button").forEach((btn) => { btn.classList.remove("fc-view-button--active"); if (btn.dataset.view === mode) { btn.classList.add("fc-view-button--active"); } }); this.renderMonthCalendar(); } 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); } this.renderMonthCalendar(); } renderMonthCalendar() { if (!this.monthCalendar) return; // Mettre à jour le titre selon le mode const monthTitle = document.getElementById("fc-current-month-year"); if (monthTitle) { const year = this.currentMonth.getFullYear(); const month = this.currentMonth.getMonth(); // Calcul de l'année scolaire à partir du mois courant const schoolYearStart = month >= 8 ? year : year - 1; const schoolYearEnd = schoolYearStart + 1; if (this.viewMode === "year") { // Année scolaire complète monthTitle.textContent = `Année scolaire ${schoolYearStart}–${schoolYearEnd}`; } else if (this.viewMode === "2months") { const monthName = getMonthNameFr(month); const next = new Date(year, month + 1, 1); const nextMonthIndex = next.getMonth(); const nextYear = next.getFullYear(); const nextMonthName = getMonthNameFr(nextMonthIndex); monthTitle.textContent = `${monthName} ${year} – ${nextMonthName} ${nextYear} (${schoolYearStart}–${schoolYearEnd})`; } else { // Vue 1 mois const monthName = getMonthNameFr(month); monthTitle.textContent = `${monthName} ${year} (${schoolYearStart}–${schoolYearEnd})`; } } // Appeler la fonction appropriée selon le mode if (this.viewMode === "year") { this.renderYearView(); } else if (this.viewMode === "2months") { this.renderTwoMonthsView(); } else { this.renderSingleMonthView(); } } renderSingleMonthView() { const year = this.currentMonth.getFullYear(); const month = this.currentMonth.getMonth(); const calendarHTML = this.generateMonthHTML(year, month); const summaryHTML = this.generateMonthSummaryHTML(year, month); this.monthCalendar.innerHTML = calendarHTML + summaryHTML; } calculateMonthTotals(year, month) { const lastDay = new Date(year, month + 1, 0); const totals = { offCarole: 0, extraOffCarole: 0, centre: 0, avis: 0, pepSick: 0, presencePep: 0, workingDays: 0, // jours potentiels d'accueil Pep }; for (let day = 1; day <= lastDay.getDate(); day++) { const date = new Date(year, month, day); const dayOfWeek = date.getDay(); // 0=dim, 6=sam if (dayOfWeek === 0 || dayOfWeek === 6) { continue; // on saute samedi/dimanche } const isoDate = `${year}-${String(month + 1).padStart(2, "0")}-${String( day ).padStart(2, "0")}`; // Si jour férié, ce n'est PAS un jour potentiel d'accueil Pep const isPublicHoliday = this.publicHolidayDates && this.publicHolidayDates.has(isoDate); if (!isPublicHoliday) { totals.workingDays++; } // Événements de base (pf_events) pour ce jour const dayEvents = this.dbEvents.filter((evt) => evt.date === isoDate); dayEvents.forEach((evt) => { const dur = parseFloat(evt.duration) || 1; switch (evt.type) { case "OFF_CAROLE": totals.offCarole += dur; break; case "EXTRA_OFF_CAROLE": totals.extraOffCarole += dur; break; case "CENTRE": totals.centre += dur; break; case "AVIS": totals.avis += dur; break; case "PEP_SICK": totals.pepSick += dur; break; } }); } const absencesPep = (totals.offCarole || 0) + (totals.extraOffCarole || 0) + (totals.pepSick || 0); totals.presencePep = Math.max(0, totals.workingDays - absencesPep); return totals; } generateMonthSummaryHTML(year, month) { const totals = this.calculateMonthTotals(year, month); const formatTotal = (total) => total > 0 ? (Number.isInteger(total) ? total : total.toFixed(1)) : "0"; const monthLabel = `${getMonthNameFr(month)} ${year}`; return `
Mois Jours ouvrés # Off Carole # Extra off Carole # Pep malade # Présence Pep
${monthLabel} ${formatTotal(totals.workingDays)} ${formatTotal(totals.offCarole)} ${formatTotal(totals.extraOffCarole)} ${formatTotal(totals.pepSick)} ${formatTotal(totals.presencePep)}
`; } renderMonthSummary() { const summaryDiv = document.getElementById("fc-month-summary"); if (!summaryDiv) return; let html = '
'; html += ''; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; const formatTotal = (total) => total > 0 ? (Number.isInteger(total) ? total : total.toFixed(1)) : ""; if (this.viewMode === "year") { // Afficher tous les mois de l'année scolaire const year = this.currentMonth.getFullYear(); const schoolYearMonths = []; for (let month = 8; month < 12; month++) { schoolYearMonths.push({ year, month }); } for (let month = 0; month < 8; month++) { schoolYearMonths.push({ year: year + 1, month }); } schoolYearMonths.forEach(({ year: monthYear, month }) => { const totals = this.calculateMonthTotals(monthYear, month); html += ""; html += ``; html += ``; html += ``; html += ``; html += ``; html += ""; }); // Ligne de total const yearTotals = schoolYearMonths.reduce( (acc, { year: monthYear, month }) => { const totals = this.calculateMonthTotals(monthYear, month); acc.offCarole += totals.offCarole; acc.extraOffCarole += totals.extraOffCarole; acc.centre += totals.centre; acc.avis += totals.avis; return acc; }, { offCarole: 0, extraOffCarole: 0, centre: 0, avis: 0 } ); html += ''; html += ``; html += ``; html += ``; html += ``; html += ``; html += ""; } else if (this.viewMode === "2months") { // Afficher les 2 mois const year = this.currentMonth.getFullYear(); const month = this.currentMonth.getMonth(); const nextMonth = month + 1; const nextYear = nextMonth > 11 ? year + 1 : year; const nextMonthIndex = nextMonth > 11 ? 0 : nextMonth; // Premier mois const totals1 = this.calculateMonthTotals(year, month); html += ""; html += ``; html += ``; html += ``; html += ``; html += ``; html += ""; // Deuxième mois const totals2 = this.calculateMonthTotals(nextYear, nextMonthIndex); html += ""; html += ``; html += ``; html += ``; html += ``; html += ``; html += ""; // Ligne de total const combinedTotals = { offCarole: totals1.offCarole + totals2.offCarole, extraOffCarole: totals1.extraOffCarole + totals2.extraOffCarole, centre: totals1.centre + totals2.centre, avis: totals1.avis + totals2.avis, }; html += ''; html += ""; html += ``; html += ``; html += ``; html += ``; html += ""; } else { // Afficher 1 mois const year = this.currentMonth.getFullYear(); const month = this.currentMonth.getMonth(); const totals = this.calculateMonthTotals(year, month); html += ""; html += ``; html += ``; html += ``; html += ``; html += ``; html += ""; } html += "
Mois# Off Carole# Extra off Carole# Centre# Avis
${getMonthNameFr(month)} ${monthYear}${formatTotal(totals.offCarole)}${formatTotal(totals.extraOffCarole)}${formatTotal(totals.centre)}${formatTotal(totals.avis)}
Total ${year}-${year + 1}${formatTotal( yearTotals.offCarole )}${formatTotal( yearTotals.extraOffCarole )}${formatTotal(yearTotals.centre)}${formatTotal(yearTotals.avis)}
${getMonthNameFr(month)} ${ month >= 8 ? year : year + 1 }${formatTotal(totals1.offCarole)}${formatTotal(totals1.extraOffCarole)}${formatTotal(totals1.centre)}${formatTotal(totals1.avis)}
${getMonthNameFr(nextMonthIndex)} ${ nextMonthIndex >= 8 ? nextYear : nextYear + 1 }${formatTotal(totals2.offCarole)}${formatTotal(totals2.extraOffCarole)}${formatTotal(totals2.centre)}${formatTotal(totals2.avis)}
Total${formatTotal( combinedTotals.offCarole )}${formatTotal( combinedTotals.extraOffCarole )}${formatTotal( combinedTotals.centre )}${formatTotal(combinedTotals.avis)}
${getMonthNameFr(month)} ${ month >= 8 ? year : year + 1 }${formatTotal(totals.offCarole)}${formatTotal(totals.extraOffCarole)}${formatTotal(totals.centre)}${formatTotal(totals.avis)}
"; html += "
"; summaryDiv.innerHTML = html; } renderTwoMonthsView() { const year = this.currentMonth.getFullYear(); const month = this.currentMonth.getMonth(); const nextMonth = month + 1; const nextYear = nextMonth > 11 ? year + 1 : year; const nextMonthIndex = nextMonth > 11 ? 0 : nextMonth; let html = '
'; // Premier mois html += `
`; html += `
${getMonthNameFr(month)} ${ month >= 8 ? year : year + 1 }
`; html += this.generateMonthHTML(year, month); html += this.generateMonthSummaryHTML(year, month); html += `
`; // Deuxième mois html += `
`; html += `
${getMonthNameFr(nextMonthIndex)} ${ nextMonthIndex >= 8 ? nextYear : nextYear + 1 }
`; html += this.generateMonthHTML(nextYear, nextMonthIndex); html += this.generateMonthSummaryHTML(nextYear, nextMonthIndex); html += `
`; html += "
"; this.monthCalendar.innerHTML = html; } renderYearView() { const year = this.currentMonth.getFullYear(); let html = '
'; const schoolYearMonths = []; for (let month = 8; month < 12; month++) { schoolYearMonths.push({ year, month }); } for (let month = 0; month < 8; month++) { schoolYearMonths.push({ year: year + 1, month }); } schoolYearMonths.forEach(({ year: monthYear, month }) => { html += `
`; html += `
${getMonthNameFr( month )} ${monthYear}
`; html += this.generateMonthHTML(monthYear, month); html += this.generateMonthSummaryHTML(monthYear, month); html += `
`; }); html += "
"; this.monthCalendar.innerHTML = html; } generateMonthHTML(year, month) { // Premier jour du mois et dernier jour const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const daysInMonth = lastDay.getDate(); // Calculer le premier lundi du mois (ou avant si le 1er n'est pas un lundi) const firstDayOfWeek = firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1; // Lundi = 0 const startDayOfWeek = firstDayOfWeek; // 0 = Lundi, 4 = Vendredi // Noms des jours (uniquement semaine : Lun-Ven) const dayNames = ["Lun", "Mar", "Mer", "Jeu", "Ven"]; let html = ''; dayNames.forEach((day) => { html += ``; }); html += ""; // Jours du mois précédent (si nécessaire) - seulement les jours de semaine let dayCount = 0; html += ""; // On ne remplit que jusqu'à vendredi (5 jours max) const daysToFill = Math.min(startDayOfWeek, 5); for (let i = 0; i < daysToFill; i++) { html += ''; dayCount++; } // Jours du mois actuel (uniquement lundi à vendredi) for (let day = 1; day <= daysInMonth; day++) { const date = new Date(year, month, day); const dayOfWeek = date.getDay(); // 0 = Dimanche, 1 = Lundi, ..., 6 = Samedi // Ignorer les samedi (6) et dimanche (0) if (dayOfWeek === 0 || dayOfWeek === 6) { continue; } // Nouvelle ligne chaque lundi (quand dayCount est un multiple de 5) if (dayCount > 0 && dayCount % 5 === 0) { html += ""; } const isoDate = `${year}-${String(month + 1).padStart(2, "0")}-${String( day ).padStart(2, "0")}`; // Trouver les événements pour ce jour const dayEvents = this.events.filter((evt) => evt.date === isoDate); let classes = "fc-month-day"; let hasGarde = false; let gardeType = null; let hasPepSick = false; // <- ajouté dayEvents.forEach((evt) => { const classMap = { VACANCES_SCOLAIRES: "fc-day--school-holiday", PUBLIC_HOLIDAY: "fc-day--public-holiday", OFF_CAROLE: "fc-day--off-carole", EXTRA_OFF_CAROLE: "fc-day--extra-off-carole", // PEP_SICK: "fc-day--pep-sick", }; if (classMap[evt.type]) { classes += " " + classMap[evt.type]; } if (GUARDE_TYPES.includes(evt.type)) { hasGarde = true; gardeType = evt.type; } if (evt.type === "PEP_SICK") { hasPepSick = true; } }); if (hasGarde) { classes += " fc-day--has-guard"; if (gardeType === "CENTRE") classes += " fc-day--centre"; if (gardeType === "AVIS") classes += " fc-day--avis"; } // Construire la cellule avec indicateur congés Alex/Laia // (on génère un TD "manuel" pour pouvoir inclure le conteneur) const leavesOnDay = this.leaves.filter( (lv) => lv.leave_date === isoDate ); let cellInnerHTML = `${day}`; if (hasPepSick) { cellInnerHTML += `🤒`; } if (leavesOnDay.length > 0) { const hasAlex = leavesOnDay.some((lv) => lv.person_id === 2); // Alex const hasLaia = leavesOnDay.some((lv) => lv.person_id === 3); // Laia let leavesHtml = `
`; if (hasAlex) leavesHtml += `AF`; if (hasLaia) leavesHtml += `LM`; leavesHtml += `
`; cellInnerHTML += leavesHtml; } html += ``; dayCount++; } // Jours du mois suivant (pour compléter la dernière ligne) - seulement jusqu'à vendredi const remainingDays = 5 - (dayCount % 5); if (remainingDays < 5 && remainingDays > 0) { for (let i = 0; i < remainingDays; i++) { html += ''; } } html += "
${day}
${cellInnerHTML}
"; return html; } handleMonthMouseDown(e) { const cell = e.target.closest("td[data-date]"); if (!cell) return; e.preventDefault(); this.clearMonthSelection(); this.isMonthSelecting = true; cell.classList.add("fc-day--selected"); this.monthSelectedCells.push(cell); } handleMonthMouseMove(e) { if (!this.isMonthSelecting) return; const cell = e.target.closest("td[data-date]"); if (cell && !this.monthSelectedCells.includes(cell)) { cell.classList.add("fc-day--selected"); this.monthSelectedCells.push(cell); } } handleMonthMouseUp(e) { if (!this.isMonthSelecting) return; this.isMonthSelecting = false; if (this.monthSelectedCells.length === 0) return; if (this.monthSelectedCells.length === 1) { const date = this.monthSelectedCells[0].dataset.date; const eventsOnDay = this.events.filter( (evt) => evt.date === date && MODIFIABLE_TYPES.includes(evt.type) ); const conge = eventsOnDay.find((e) => CONGE_TYPES.includes(e.type)); const garde = eventsOnDay.find((e) => GUARDE_TYPES.includes(e.type)); const pep = eventsOnDay.find((e) => PEP_TYPES.includes(e.type)); // Toujours ouvrir le menu complet qui inclut Alex/Laia this.showMonthEditMenuForDay(e, { conge, garde, pep, date }); return; } // ===== Multi-jours : toujours ouvrir le menu bulk ===== const selectedDates = this.monthSelectedCells.map((c) => c.dataset.date); const eventsOnDates = this.events.filter( (evt) => selectedDates.includes(evt.date) && MODIFIABLE_TYPES.includes(evt.type) ); const conges = eventsOnDates.filter((ev) => CONGE_TYPES.includes(ev.type) ); const gardes = eventsOnDates.filter((ev) => GUARDE_TYPES.includes(ev.type) ); const uniqueCongeTypes = new Set(conges.map((c) => c.type)); const uniqueGardeTypes = new Set(gardes.map((g) => g.type)); const allDatesHaveConge = conges.length === selectedDates.length && uniqueCongeTypes.size === 1; const allDatesHaveGarde = gardes.length === selectedDates.length && uniqueGardeTypes.size === 1; const bulkInfo = { selectedDates, conges, gardes, congeType: allDatesHaveConge ? conges[0].type : null, gardeType: allDatesHaveGarde ? gardes[0].type : null, }; // Peu importe qu'il y ait déjà des congés ou non, on ouvre le bulk this.showMonthBulkMenu(e, bulkInfo); } clearMonthSelection() { if (this.monthSelectionMenu) { this.monthSelectionMenu.style.display = "none"; } this.monthSelectedCells.forEach((cell) => cell.classList.remove("fc-day--selected") ); this.monthSelectedCells = []; } showMonthAddMenu(e) { if (!this.monthSelectionMenu) return; this.monthSelectionMenu.innerHTML = `
Ajouter
Mode de garde
Pep
`; this.positionAndShowMonthMenu(e); } showMonthEditMenuForDay(e, { conge, garde, pep, date }) { if (!this.monthSelectionMenu) return; // --- Section congé Carole --- let congeSection = ""; if (conge) { const oppositeConge = conge.type === "OFF_CAROLE" ? "EXTRA_OFF_CAROLE" : "OFF_CAROLE"; congeSection = `
Congé Carole
`; } else { congeSection = `
Ajouter un congé
`; } // --- Section mode de garde --- let gardeSection = ""; if (garde) { const oppositeGarde = garde.type === "CENTRE" ? "AVIS" : "CENTRE"; gardeSection = `
Mode de garde
`; } else { gardeSection = `
Ajouter mode de garde
`; } // --- Section Pep malade --- let pepSection = ""; if (pep) { pepSection = `
Pep
`; } else { pepSection = `
Pep
`; } // --- Section congés Alex / Laia --- const leavesOnDay = (this.leaves || []).filter( (lv) => lv.leave_date === date ); let leavesSection = `
Congés Alex / Laia
Alex Laia
`; if (leavesOnDay.length > 0) { leavesSection += ` `; } this.monthSelectionMenu.innerHTML = congeSection + gardeSection + pepSection + leavesSection; this.positionAndShowMonthMenu(e); } showMonthBulkMenu(e, bulkInfo) { if (!this.monthSelectionMenu) return; const { selectedDates } = bulkInfo; const nbDays = selectedDates.length; let html = `
Actions multi-jours
`; // === Congés Carole (toujours visible) === html += `
Congés Carole (${nbDays} jours)
`; // === Mode de garde (toujours visible) === html += `
Mode de garde (${nbDays} jours)
`; // === Congés Alex / Laia (tableau identique au single) === html += `
Congés Alex / Laia (${nbDays} jours)
Alex Laia
`; this.monthSelectionMenu.innerHTML = html; this._currentBulkInfo = bulkInfo; this.positionAndShowMonthMenu(e); } positionAndShowMonthMenu(e) { if (!this.monthSelectionMenu) return; // parent direct qui contient le calendrier + le menu const wrapper = this.monthSelectionMenu.parentElement; // .fc-calendar-and-summary const rect = wrapper.getBoundingClientRect(); this.monthSelectionMenu.style.display = "block"; // Position relative au wrapper, comme pour l’hebdo this.monthSelectionMenu.style.left = `${e.clientX - rect.left + 5}px`; this.monthSelectionMenu.style.top = `${e.clientY - rect.top + 5}px`; this.menuJustOpened = true; setTimeout(() => { this.menuJustOpened = false; }, 0); } async handleMonthMenuClick(e) { const button = e.target.closest("button[data-action]"); if (!button) return; const { action, eventId, newType, type, person, date } = button.dataset; // === AJOUT MULTI-JOURS (Carole / garde / Pep) ============================ if (action === "add") { const selectedDates = this.monthSelectedCells.map( (c) => c.dataset.date ); // Bulk congés Carole if (CONGE_TYPES.includes(type)) { try { const existingOnDates = this.dbEvents.filter( (evt) => selectedDates.includes(evt.date) && CONGE_TYPES.includes(evt.type) ); for (const evt of existingOnDates) { await fetch( "/modules/family-calendar/includes/api/manage-event.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "delete", event_id: evt.id, }), } ); } const newEvents = selectedDates.map((d) => ({ date: d, type, person: "Carole", duration: 1.0, })); const response = await fetch( "/modules/family-calendar/includes/api/save-events.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newEvents), } ); if (!response.ok) throw new Error("Erreur HTTP " + response.status); const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); const dataEvents = await resEvents.json(); this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { console.error("Erreur bulk Carole (month):", err); alert("Erreur bulk congés Carole : " + err.message); } this.clearMonthSelection(); return; } // Bulk mode de garde if (GUARDE_TYPES.includes(type)) { try { const existingOnDates = this.dbEvents.filter( (evt) => selectedDates.includes(evt.date) && GUARDE_TYPES.includes(evt.type) ); for (const evt of existingOnDates) { await fetch( "/modules/family-calendar/includes/api/manage-event.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "delete", event_id: evt.id, }), } ); } const newEvents = selectedDates.map((d) => ({ date: d, type, duration: 1.0, })); const response = await fetch( "/modules/family-calendar/includes/api/save-events.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newEvents), } ); if (!response.ok) throw new Error("Erreur HTTP " + response.status); const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); const dataEvents = await resEvents.json(); this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { console.error("Erreur bulk garde (month):", err); alert("Erreur bulk mode de garde : " + err.message); } this.clearMonthSelection(); return; } // Bulk Pep const newEvents = this.monthSelectedCells.map((cell) => ({ date: cell.dataset.date, type, person, duration: 1.0, })); this.clearMonthSelection(); try { const response = await fetch( "/modules/family-calendar/includes/api/save-events.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newEvents), } ); if (!response.ok) throw new Error("Erreur HTTP " + response.status); const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); const dataEvents = await resEvents.json(); this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { console.error("Erreur bulk add (month):", err); alert("Erreur lors de l'ajout."); } return; } // === AJOUT SUR UNE SEULE DATE (Carole / garde / Pep) ===================== if (action === "add-single") { const targetDate = date; // Switch congé Carole if (CONGE_TYPES.includes(type)) { try { const existingOnDate = this.dbEvents.filter( (evt) => evt.date === targetDate && CONGE_TYPES.includes(evt.type) ); for (const evt of existingOnDate) { await fetch( "/modules/family-calendar/includes/api/manage-event.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "delete", event_id: evt.id, }), } ); } const response = await fetch( "/modules/family-calendar/includes/api/save-events.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify([ { date: targetDate, type, person: "Carole", duration: 1.0, }, ]), } ); if (!response.ok) throw new Error("Erreur HTTP " + response.status); const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); const dataEvents = await resEvents.json(); this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { console.error("Erreur Carole (month single):", err); alert("Erreur congé Carole : " + err.message); } this.clearMonthSelection(); return; } // Switch mode de garde if (GUARDE_TYPES.includes(type)) { try { const existingOnDate = this.dbEvents.filter( (evt) => evt.date === targetDate && GUARDE_TYPES.includes(evt.type) ); for (const evt of existingOnDate) { await fetch( "/modules/family-calendar/includes/api/manage-event.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "delete", event_id: evt.id, }), } ); } const response = await fetch( "/modules/family-calendar/includes/api/save-events.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify([ { date: targetDate, type, duration: 1.0, }, ]), } ); if (!response.ok) throw new Error("Erreur HTTP " + response.status); const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); const dataEvents = await resEvents.json(); this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { console.error("Erreur garde (month single):", err); alert("Erreur mode de garde : " + err.message); } this.clearMonthSelection(); return; } // Pep const newEvent = { date: targetDate, type, person, duration: 1.0, }; this.clearMonthSelection(); try { const response = await fetch( "/modules/family-calendar/includes/api/save-events.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify([newEvent]), } ); if (!response.ok) throw new Error("Erreur HTTP " + response.status); const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); const dataEvents = await resEvents.json(); this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { console.error("Erreur add (month single):", err); alert("Erreur lors de l'ajout."); } return; } // === SUPPRESSION SIMPLE (Carole / garde / Pep) =========================== if (action === "delete") { if (!eventId) { this.clearMonthSelection(); return; } this.clearMonthSelection(); try { const response = await fetch( "/modules/family-calendar/includes/api/manage-event.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "delete", event_id: eventId }), } ); if (!response.ok) { const errData = await response.json().catch(() => ({})); throw new Error( errData.message || "Erreur HTTP " + response.status ); } const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); const dataEvents = await resEvents.json(); this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { console.error("Erreur delete (month):", err); alert("Erreur lors de la suppression : " + err.message); } return; } // === SUPPRESSION BULK ALEX / LAIA =========================== if (action === "bulk-clear-leave") { if (!this._currentBulkInfo) { this.clearSelection(); return; } const selectedDates = this._currentBulkInfo.selectedDates || []; const personId = parseInt(button.dataset.personId, 10); try { await fetch("/modules/family-calendar/includes/api/manage-leaf.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "bulk_delete_day_person", dates: selectedDates, person_id: personId, }), }); this.leaves = await this.fetchLeaves(); this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { console.error("Erreur bulk-clear-leave:", err); alert( "Erreur lors de la suppression des congés sur ces jours : " + err.message ); } this._currentBulkInfo = null; this.clearSelection(); return; } // === MODIFICATION SIMPLE =================================================== if (action === "update") { if (!eventId) { this.clearMonthSelection(); return; } this.clearMonthSelection(); try { const response = await fetch( "/modules/family-calendar/includes/api/manage-event.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "update", event_id: eventId, new_type: newType, }), } ); if (!response.ok) { const errData = await response.json().catch(() => ({})); throw new Error( errData.message || "Erreur HTTP " + response.status ); } const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); const dataEvents = await resEvents.json(); this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { console.error("Erreur update (month):", err); alert("Erreur lors de la modification : " + err.message); } return; } // === ALEX / LAIA (logique identique à handleMenuClick) =================== if (action === "add-leave") { const leaveDate = date; const personId = parseInt(button.dataset.personId, 10); const leaveType = button.dataset.leaveType; // "CP", "JRA", "JA" if (!leaveDate || !personId || !leaveType) { this.clearMonthSelection(); return; } this.clearMonthSelection(); try { // 1) Supprimer tous les congés Alex/Laia pour cette personne ce jour-là await fetch("/modules/family-calendar/includes/api/manage-leaf.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "delete_day_person", date: leaveDate, person_id: personId, }), }); // 2) Ajouter le type choisi const responseAdd = await fetch( "/modules/family-calendar/includes/api/save-leaves.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify([ { date: leaveDate, person_id: personId, leave_type: leaveType, duration: 1.0, }, ]), } ); if (!responseAdd.ok) { throw new Error("Erreur HTTP " + responseAdd.status); } this.leaves = await this.fetchLeaves(); this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { console.error("Erreur set leave single:", err); alert("Erreur congé Alex/Laia : " + err.message); } return; } if (action === "delete-leaves-day") { const leaveDate = date; if (!leaveDate) { this.clearMonthSelection(); return; } try { const response = await fetch( "/modules/family-calendar/includes/api/manage-leaf.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "delete_day", date: leaveDate }), } ); if (!response.ok) { const errData = await response.json().catch(() => ({})); throw new Error( errData.message || "Erreur HTTP " + response.status ); } this.leaves = await this.fetchLeaves(); this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { console.error("Erreur delete_leaves_day (month):", err); alert( "Erreur lors de la suppression des congés Alex/Laia pour ce jour: " + err.message ); } this.clearMonthSelection(); return; } if (action === "bulk-add-leave") { if (!this._currentBulkInfo) { this.clearMonthSelection(); return; } const selectedDates = this._currentBulkInfo.selectedDates || []; const personId = parseInt(button.dataset.personId, 10); const leaveType = button.dataset.leaveType; this._currentBulkInfo = null; try { await fetch("/modules/family-calendar/includes/api/manage-leaf.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "bulk_delete_day_person", dates: selectedDates, person_id: personId, }), }); const newLeaves = selectedDates.map((d) => ({ date: d, person_id: personId, leave_type: leaveType, duration: 1.0, })); const responseAdd = await fetch( "/modules/family-calendar/includes/api/save-leaves.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newLeaves), } ); if (!responseAdd.ok) throw new Error("Erreur HTTP " + responseAdd.status); this.leaves = await this.fetchLeaves(); this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { console.error("Erreur bulk-add-leave (month):", err); alert("Erreur bulk congés Alex/Laia : " + err.message); } this.clearMonthSelection(); return; } } } function getWeekOfYear(date) { const d = new Date( Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) ); d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7)); const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); return Math.ceil(((d - yearStart) / 86400000 + 1) / 7); } function getMonthNameFr(monthIndex) { return ( [ "Janvier", "Fevrier", "Mars", "Avril", "Mai", "Juin", "Juillet", "Aout", "Septembre", "Octobre", "Novembre", "Decembre", ][monthIndex] || "" ); } new FamilyCalendar(); });