/**
* 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 MODIFIABLE_TYPES = [...CONGE_TYPES, ...GUARDE_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.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.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 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() {
this.weeks.forEach((w) => {
w.totals = { offCarole: 0, extraOffCarole: 0, centre: 0, avis: 0 };
Object.values(w.dayFlags).forEach((df) => (df.eventsOnDay = []));
});
this.events.forEach((evt) => {
const evtDate = new Date(evt.date + "T00:00:00");
const week = this.weeks.find(
(w) => evtDate >= w.dayDates.mon && evtDate <= w.dayDates.fri
);
if (!week) return;
const dayKey = Object.keys(week.dayDates).find(
(k) => week.dayDates[k].toDateString() === evtDate.toDateString()
);
if (dayKey) {
week.dayFlags[dayKey].eventsOnDay.push(evt);
}
const dur = parseFloat(evt.duration) || 1;
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;
}
});
}
updateGlobalSummary() {
const summaryDiv = document.getElementById("globalSummary");
if (!summaryDiv) return;
// On filtre uniquement sur les événements "DB" pour le récap (pas les vacances/fériés)
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);
summaryDiv.innerHTML = `
Off Carole : ${totalOff} jours
Extra Off Carole : ${totalExtraOff} 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) => {
const tr = document.createElement("tr");
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);
}
const tdWeek = document.createElement("td");
tdWeek.textContent = week.weekLabel;
tr.appendChild(tdWeek);
["mon", "tue", "wed", "thu", "fri"].forEach((dayKey) => {
const td = document.createElement("td");
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")}`;
td.className = "";
let hasGarde = false;
let gardeType = null;
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",
};
if (classMap[evt.type]) {
td.classList.add(classMap[evt.type]);
}
if (GUARDE_TYPES.includes(evt.type)) {
hasGarde = true;
gardeType = evt.type;
}
});
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");
}
tr.appendChild(td);
});
const tdOff = document.createElement("td");
tdOff.textContent = formatTotal(week.totals.offCarole);
tr.appendChild(tdOff);
const tdExtra = document.createElement("td");
tdExtra.textContent = formatTotal(week.totals.extraOffCarole);
tr.appendChild(tdExtra);
const tdCentre = document.createElement("td");
tdCentre.textContent = formatTotal(week.totals.centre);
tr.appendChild(tdCentre);
const tdAvis = document.createElement("td");
tdAvis.textContent = formatTotal(week.totals.avis);
tr.appendChild(tdAvis);
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) {
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) {
const cell = e.target.closest("td[data-date]");
if (!cell) return;
e.preventDefault();
this.clearSelection();
this.isSelecting = true;
cell.classList.add("fc-day--selected");
this.selectedCells.push(cell);
}
handleMouseMove(e) {
if (!this.isSelecting) return;
const cell = e.target.closest("td[data-date]");
if (cell && !this.selectedCells.includes(cell)) {
cell.classList.add("fc-day--selected");
this.selectedCells.push(cell);
}
}
handleMouseUp(e) {
if (!this.isSelecting) return;
this.isSelecting = false;
if (this.selectedCells.length === 0) return;
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((e) => CONGE_TYPES.includes(e.type));
const garde = eventsOnDay.find((e) => GUARDE_TYPES.includes(e.type));
if (!conge && !garde) {
this.showAddMenu(e);
} else {
this.showEditMenuForDay(e, { conge, garde, date });
}
} else {
// Multi-jours : on reste en mode ajout, avec contrôles plus tard
this.showAddMenu(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 = `
Off Carole
Extra Off Carole
Centre
Avis
`;
this.positionAndShowMenu(e);
}
showEditMenuForDay(e, { conge, garde, date }) {
console.log(
"[EDIT MENU] pour date",
date,
"conge =",
conge,
"garde =",
garde
);
let congeSection = "";
if (conge) {
const oppositeConge =
conge.type === "OFF_CAROLE" ? "EXTRA_OFF_CAROLE" : "OFF_CAROLE";
congeSection = `
Remplacer par ${oppositeConge.replace(/_/g, " ")}
Supprimer le congé
`;
} else {
congeSection = `
Off Carole
Extra Off Carole
`;
}
let gardeSection = "";
if (garde) {
const oppositeGarde = garde.type === "CENTRE" ? "AVIS" : "CENTRE";
gardeSection = `
Remplacer par ${oppositeGarde}
Supprimer le mode de garde
`;
} else {
gardeSection = `
Centre
Avis
`;
}
this.selectionMenu.innerHTML = congeSection + gardeSection;
this.positionAndShowMenu(e);
}
positionAndShowMenu(e) {
this.selectionMenu.style.display = "block";
this.selectionMenu.style.left = `${e.pageX + 5}px`;
this.selectionMenu.style.top = `${e.pageY + 5}px`;
this.menuJustOpened = true;
}
async handleMenuClick(e) {
const button = e.target.closest("button[data-action]");
if (!button) return;
const { action, eventId, newType, type, person, date } = button.dataset;
// === AJOUT MULTI-JOURS (sélection de plusieurs cellules) ===
if (action === "add") {
const selectedDates = this.selectedCells.map((c) => c.dataset.date);
const existingOnDates = this.dbEvents.filter((evt) =>
selectedDates.includes(evt.date)
);
// Contrôles métier : pas de double congé / pas de double mode de garde
if (CONGE_TYPES.includes(type)) {
const hasCongeConflict = existingOnDates.some((evt) =>
CONGE_TYPES.includes(evt.type)
);
if (hasCongeConflict) {
alert(
"Impossible d'ajouter un congé : au moins une date a déjà un congé (Off/Extra Off)."
);
this.clearSelection();
return;
}
} else if (GUARDE_TYPES.includes(type)) {
const hasGardeConflict = existingOnDates.some((evt) =>
GUARDE_TYPES.includes(evt.type)
);
if (hasGardeConflict) {
alert(
"Impossible d'ajouter un mode de garde : au moins une date a déjà Centre/Avis."
);
this.clearSelection();
return;
}
}
const newEvents = this.selectedCells.map((cell) => ({
date: cell.dataset.date,
type,
person,
duration: 1.0,
}));
this.clearSelection();
try {
const response = await fetch("/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);
}
// Resync DB events
const resEvents = await fetch("/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();
} catch (err) {
console.error(err);
alert("Erreur lors de l'ajout.");
}
return;
}
// === AJOUT SUR UNE SEULE DATE (menu combiné d'un jour) ===
if (action === "add-single") {
const targetDate = date;
const existingOnDate = this.dbEvents.filter(
(evt) => evt.date === targetDate
);
if (CONGE_TYPES.includes(type)) {
const hasCongeConflict = existingOnDate.some((evt) =>
CONGE_TYPES.includes(evt.type)
);
if (hasCongeConflict) {
alert("Un congé existe déjà ce jour-là.");
this.clearSelection();
return;
}
} else if (GUARDE_TYPES.includes(type)) {
const hasGardeConflict = existingOnDate.some((evt) =>
GUARDE_TYPES.includes(evt.type)
);
if (hasGardeConflict) {
alert("Un mode de garde existe déjà ce jour-là.");
this.clearSelection();
return;
}
}
const newEvent = {
date: targetDate,
type,
person,
duration: 1.0,
};
this.clearSelection();
try {
const response = await fetch("/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);
}
// Resync DB events
const resEvents = await fetch("/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();
} catch (err) {
console.error(err);
alert("Erreur lors de l'ajout.");
}
return;
}
// === SUPPRESSION ===
if (action === "delete") {
if (!eventId) {
console.warn("Bouton delete sans eventId, action ignorée.");
this.clearSelection();
return;
}
this.clearSelection();
try {
const response = await fetch("/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
);
}
// Resync DB events
const resEvents = await fetch("/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();
} catch (err) {
console.error(err);
alert("Erreur lors de la suppression : " + err.message);
}
return;
}
// === MODIFICATION (change OFF <-> EXTRA ou CENTRE <-> AVIS) ===
if (action === "update") {
console.log("[UPDATE] click sur bouton update", { eventId, newType });
if (!eventId) {
console.warn("Bouton update sans eventId, action ignorée.");
this.clearSelection();
return;
}
this.clearSelection();
try {
const response = await fetch("/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
);
}
// Resync DB events
const resEvents = await fetch("/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();
} catch (err) {
console.error("[UPDATE] Erreur lors de la modification :", err);
alert("Erreur lors de la modification : " + err.message);
}
return;
}
}
async manageEvent(payload) {
try {
const response = await fetch("/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("/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 (septembre à août)
this.currentMonth.setFullYear(
this.currentMonth.getFullYear() + direction
);
} else if (this.viewMode === "2months") {
this.currentMonth.setMonth(this.currentMonth.getMonth() + direction);
// S'assurer qu'on reste dans une année scolaire valide
this.adjustToSchoolYear();
} else {
this.currentMonth.setMonth(this.currentMonth.getMonth() + direction);
// S'assurer qu'on reste dans une année scolaire valide
this.adjustToSchoolYear();
}
this.renderMonthCalendar();
}
adjustToSchoolYear() {
const month = this.currentMonth.getMonth();
const year = this.currentMonth.getFullYear();
// Si on dépasse août (mois 7), on passe à septembre de l'année suivante
if (month > 7) {
this.currentMonth.setFullYear(year + 1);
this.currentMonth.setMonth(8); // Septembre
}
// Si on est avant septembre (mois 8), on reste dans l'année scolaire
// mais on pourrait vouloir revenir à septembre de l'année en cours
if (month < 8) {
// On garde l'année mais on pourrait vouloir ajuster
// Pour l'instant, on laisse comme ça car ça peut être août de l'année scolaire
}
}
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();
if (this.viewMode === "year") {
// Afficher l'année scolaire (ex: 2025-2026)
monthTitle.textContent = `${year}-${year + 1}`;
} else if (this.viewMode === "2months") {
const month = this.currentMonth.getMonth();
const monthName = getMonthNameFr(month);
const nextMonth = new Date(year, month + 1, 1);
const nextMonthName = getMonthNameFr(nextMonth.getMonth());
// Ajuster l'année si on passe de décembre à janvier
const displayYear = month >= 8 ? year : year + 1;
monthTitle.textContent = `${monthName} - ${nextMonthName} ${displayYear}`;
} else {
const month = this.currentMonth.getMonth();
const monthName = getMonthNameFr(month);
// Afficher l'année scolaire si on est après août
if (month >= 8) {
monthTitle.textContent = `${monthName} ${year} (${year}-${
year + 1
})`;
} else {
monthTitle.textContent = `${monthName} ${year} (${
year - 1
}-${year})`;
}
}
}
// 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();
this.monthCalendar.innerHTML = this.generateMonthHTML(year, month);
}
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 = '';
html += `
`;
html += `
${getMonthNameFr(month)} ${
month >= 8 ? year : year + 1
}
`;
html += this.generateMonthHTML(year, month);
html += `
`;
html += `
`;
html += `
${getMonthNameFr(nextMonthIndex)} ${
nextMonthIndex >= 8 ? nextYear : nextYear + 1
}
`;
html += this.generateMonthHTML(nextYear, nextMonthIndex);
html += `
`;
html += "
";
this.monthCalendar.innerHTML = html;
}
renderYearView() {
const year = this.currentMonth.getFullYear();
let html = '';
// Année scolaire : septembre (8) à août (7) de l'année suivante
const schoolYearMonths = [];
// Septembre à décembre de l'année en cours
for (let month = 8; month < 12; month++) {
schoolYearMonths.push({ year, month });
}
// Janvier à août de l'année suivante
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 += `
`;
});
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 += `${day} `;
});
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;
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",
};
if (classMap[evt.type]) {
classes += " " + classMap[evt.type];
}
if (GUARDE_TYPES.includes(evt.type)) {
hasGarde = true;
gardeType = evt.type;
}
});
if (hasGarde) {
classes += " fc-day--has-guard";
if (gardeType === "CENTRE") classes += " fc-day--centre";
if (gardeType === "AVIS") classes += " fc-day--avis";
}
html += `${day} `;
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 += "
";
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));
if (!conge && !garde) {
this.showMonthAddMenu(e);
} else {
this.showMonthEditMenuForDay(e, { conge, garde, date });
}
} else {
this.showMonthAddMenu(e);
}
}
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 = `
Off Carole
Extra Off Carole
Centre
Avis
`;
this.positionAndShowMonthMenu(e);
}
showMonthEditMenuForDay(e, { conge, garde, date }) {
if (!this.monthSelectionMenu) return;
let congeSection = "";
if (conge) {
const oppositeConge =
conge.type === "OFF_CAROLE" ? "EXTRA_OFF_CAROLE" : "OFF_CAROLE";
congeSection = `
Remplacer par ${oppositeConge.replace(/_/g, " ")}
Supprimer le congé
`;
} else {
congeSection = `
Off Carole
Extra Off Carole
`;
}
let gardeSection = "";
if (garde) {
const oppositeGarde = garde.type === "CENTRE" ? "AVIS" : "CENTRE";
gardeSection = `
Remplacer par ${oppositeGarde}
Supprimer le mode de garde
`;
} else {
gardeSection = `
Centre
Avis
`;
}
this.monthSelectionMenu.innerHTML = congeSection + gardeSection;
this.positionAndShowMonthMenu(e);
}
positionAndShowMonthMenu(e) {
if (!this.monthSelectionMenu) return;
this.monthSelectionMenu.style.display = "block";
this.monthSelectionMenu.style.left = `${e.pageX + 5}px`;
this.monthSelectionMenu.style.top = `${e.pageY + 5}px`;
this.menuJustOpened = true;
}
async handleMonthMenuClick(e) {
const button = e.target.closest("button[data-action]");
if (!button) return;
const { action, eventId, newType, type, person, date } = button.dataset;
// Réutiliser la même logique que handleMenuClick mais avec monthSelectedCells
if (action === "add") {
const selectedDates = this.monthSelectedCells.map(
(c) => c.dataset.date
);
const existingOnDates = this.dbEvents.filter((evt) =>
selectedDates.includes(evt.date)
);
if (CONGE_TYPES.includes(type)) {
const hasCongeConflict = existingOnDates.some((evt) =>
CONGE_TYPES.includes(evt.type)
);
if (hasCongeConflict) {
alert(
"Impossible d'ajouter un congé : au moins une date a déjà un congé (Off/Extra Off)."
);
this.clearMonthSelection();
return;
}
} else if (GUARDE_TYPES.includes(type)) {
const hasGardeConflict = existingOnDates.some((evt) =>
GUARDE_TYPES.includes(evt.type)
);
if (hasGardeConflict) {
alert(
"Impossible d'ajouter un mode de garde : au moins une date a déjà Centre/Avis."
);
this.clearMonthSelection();
return;
}
}
const newEvents = this.monthSelectedCells.map((cell) => ({
date: cell.dataset.date,
type,
person,
duration: 1.0,
}));
this.clearMonthSelection();
try {
const response = await fetch("/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("/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 (err) {
console.error(err);
alert("Erreur lors de l'ajout.");
}
return;
}
if (action === "add-single") {
const targetDate = date;
const existingOnDate = this.dbEvents.filter(
(evt) => evt.date === targetDate
);
if (CONGE_TYPES.includes(type)) {
const hasCongeConflict = existingOnDate.some((evt) =>
CONGE_TYPES.includes(evt.type)
);
if (hasCongeConflict) {
alert("Un congé existe déjà ce jour-là.");
this.clearMonthSelection();
return;
}
} else if (GUARDE_TYPES.includes(type)) {
const hasGardeConflict = existingOnDate.some((evt) =>
GUARDE_TYPES.includes(evt.type)
);
if (hasGardeConflict) {
alert("Un mode de garde existe déjà ce jour-là.");
this.clearMonthSelection();
return;
}
}
const newEvent = {
date: targetDate,
type,
person,
duration: 1.0,
};
this.clearMonthSelection();
try {
const response = await fetch("/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("/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 (err) {
console.error(err);
alert("Erreur lors de l'ajout.");
}
return;
}
if (action === "delete") {
if (!eventId) {
this.clearMonthSelection();
return;
}
this.clearMonthSelection();
try {
const response = await fetch("/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("/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 (err) {
console.error(err);
alert("Erreur lors de la suppression : " + err.message);
}
return;
}
if (action === "update") {
if (!eventId) {
this.clearMonthSelection();
return;
}
this.clearMonthSelection();
try {
const response = await fetch("/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("/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 (err) {
console.error("[UPDATE] Erreur lors de la modification :", err);
alert("Erreur lors de la modification : " + err.message);
}
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();
});