diff --git a/assets/css/style.css b/assets/css/style.css
index 253def7..e8bf6ef 100644
--- a/assets/css/style.css
+++ b/assets/css/style.css
@@ -365,3 +365,143 @@ input[type="number"] {
margin-right: 10px;
flex-shrink: 0;
}
+
+/* === CALENDRIER MENSUEL === */
+.fc-month-calendar-wrapper {
+ background: #fff;
+ border-radius: 8px;
+ padding: 16px;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+ border: 1px solid #d9e2ec;
+}
+
+.fc-month-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 16px;
+ padding-bottom: 12px;
+ border-bottom: 2px solid #d9e2ec;
+}
+
+.fc-month-header h3 {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: #243b53;
+}
+
+.fc-nav-button {
+ background: #f0f4f8;
+ border: 1px solid #d9e2ec;
+ border-radius: 4px;
+ padding: 6px 12px;
+ font-size: 18px;
+ cursor: pointer;
+ color: #243b53;
+ transition: all 0.2s;
+}
+
+.fc-nav-button:hover {
+ background: #d9e2ec;
+ border-color: #bcccdc;
+}
+
+.fc-month-calendar {
+ width: 100%;
+}
+
+.fc-month-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 13px;
+}
+
+.fc-month-table thead th {
+ background: #f0f4f8;
+ padding: 8px 4px;
+ text-align: center;
+ font-weight: 600;
+ color: #243b53;
+ border: 1px solid #d9e2ec;
+}
+
+.fc-month-table tbody td {
+ border: 1px solid #d9e2ec;
+ padding: 8px 4px;
+ text-align: center;
+ vertical-align: top;
+ min-height: 60px;
+ height: 60px;
+ position: relative;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.fc-month-day {
+ background: #fff;
+}
+
+.fc-month-day:hover {
+ background: #f8fafc !important;
+}
+
+.fc-day--other-month {
+ background: #f8fafc;
+ color: #9fb3c8;
+ cursor: default;
+}
+
+.fc-day--weekend {
+ background: #f8fafc;
+}
+
+.fc-day--weekend.fc-day--school-holiday,
+.fc-day--weekend.fc-day--public-holiday,
+.fc-day--weekend.fc-day--off-carole,
+.fc-day--weekend.fc-day--extra-off-carole {
+ background: inherit;
+}
+
+/* Application des couleurs existantes au calendrier mensuel */
+.fc-month-table .fc-day--school-holiday {
+ background-color: #e5d9f2;
+}
+
+.fc-month-table .fc-day--public-holiday {
+ background-color: #e0e0e0;
+}
+
+.fc-month-table .fc-day--off-carole {
+ background-color: #ffe9a7;
+}
+
+.fc-month-table .fc-day--extra-off-carole {
+ background-color: #ffd59b;
+}
+
+.fc-month-table .fc-day--has-guard {
+ box-sizing: border-box;
+}
+
+.fc-month-table .fc-day--has-guard.fc-day--centre {
+ border: 2px solid #4caf50;
+}
+
+.fc-month-table .fc-day--has-guard.fc-day--avis {
+ border: 2px solid #2196f3;
+}
+
+/* Combinaisons avec dégradés pour le calendrier mensuel */
+.fc-month-table .fc-day--school-holiday.fc-day--off-carole {
+ background-image: linear-gradient(45deg, #e5d9f2 49%, #ffe9a7 51%);
+}
+
+.fc-month-table .fc-day--school-holiday.fc-day--extra-off-carole {
+ background-image: linear-gradient(45deg, #e5d9f2 49%, #ffd59b 51%);
+}
+
+.fc-month-table .fc-day--selected {
+ background-color: #bde4ff !important;
+ cursor: pointer;
+}
\ No newline at end of file
diff --git a/assets/js/family-calendar.js b/assets/js/family-calendar.js
index 8f72271..0250e45 100644
--- a/assets/js/family-calendar.js
+++ b/assets/js/family-calendar.js
@@ -14,6 +14,12 @@ document.addEventListener("DOMContentLoaded", () => {
this.schoolHolidaysTableBody = document.querySelector(
"#schoolHolidaysTable tbody"
);
+ this.monthCalendar = document.getElementById("fc-month-calendar");
+ this.monthSelectionMenu = document.getElementById(
+ "fc-month-selectionMenu"
+ );
+ this.currentMonth = new Date();
+ this.currentMonth.setDate(1); // Premier jour du mois
if (!this.planningBody || !this.selectionMenu) {
console.error("planningBody ou selectionMenu manquant");
@@ -22,6 +28,8 @@ document.addEventListener("DOMContentLoaded", () => {
this.isSelecting = false;
this.selectedCells = [];
+ this.monthSelectedCells = [];
+ this.isMonthSelecting = false;
this.dbEvents = [];
this.fixedEvents = [];
this.events = [];
@@ -41,6 +49,7 @@ document.addEventListener("DOMContentLoaded", () => {
this.events = [...this.dbEvents, ...this.fixedEvents];
this.reprocessAndRender();
+ this.renderMonthCalendar();
}
loadDbEvents() {
@@ -161,6 +170,7 @@ document.addEventListener("DOMContentLoaded", () => {
this.reprocessEvents();
this.renderTable();
this.updateGlobalSummary();
+ this.renderMonthCalendar();
}
reprocessEvents() {
@@ -349,6 +359,32 @@ document.addEventListener("DOMContentLoaded", () => {
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));
+ }
}
handleMouseDown(e) {
@@ -401,11 +437,19 @@ document.addEventListener("DOMContentLoaded", () => {
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() {
@@ -753,11 +797,472 @@ document.addEventListener("DOMContentLoaded", () => {
this.events = [...this.dbEvents, ...this.fixedEvents];
this.reprocessAndRender();
+ this.renderMonthCalendar();
} catch (error) {
console.error("Erreur manageEvent :", error);
alert("Erreur: " + error.message);
}
}
+
+ // ================== CALENDRIER MENSUEL ==================
+ navigateMonth(direction) {
+ this.currentMonth.setMonth(this.currentMonth.getMonth() + direction);
+ this.renderMonthCalendar();
+ }
+
+ renderMonthCalendar() {
+ if (!this.monthCalendar) return;
+
+ const year = this.currentMonth.getFullYear();
+ const month = this.currentMonth.getMonth();
+ const monthName = getMonthNameFr(month);
+
+ // Mettre à jour le titre
+ const monthTitle = document.getElementById("fc-current-month-year");
+ if (monthTitle) {
+ monthTitle.textContent = `${monthName} ${year}`;
+ }
+
+ // 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();
+ const startDayOfWeek =
+ firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1; // Lundi = 0
+
+ // Noms des jours
+ const dayNames = ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"];
+
+ let html = '
';
+ dayNames.forEach((day) => {
+ html += `${day} `;
+ });
+ html += " ";
+
+ // Jours du mois précédent (si nécessaire)
+ let dayCount = 0;
+ html += "";
+ for (let i = 0; i < startDayOfWeek; i++) {
+ html += ' ';
+ dayCount++;
+ }
+
+ // Jours du mois actuel
+ for (let day = 1; day <= daysInMonth; day++) {
+ if (dayCount % 7 === 0 && day > 1) {
+ html += " ";
+ }
+
+ const date = new Date(year, month, day);
+ 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";
+ }
+
+ // Week-end
+ const dayOfWeek = date.getDay();
+ if (dayOfWeek === 0 || dayOfWeek === 6) {
+ classes += " fc-day--weekend";
+ }
+
+ html += `${day} `;
+ dayCount++;
+ }
+
+ // Jours du mois suivant (pour compléter la dernière ligne)
+ const remainingDays = 7 - (dayCount % 7);
+ if (remainingDays < 7) {
+ for (let i = 0; i < remainingDays; i++) {
+ html += ' ';
+ }
+ }
+ html += "
";
+
+ this.monthCalendar.innerHTML = 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) {
diff --git a/family-calendar.php b/family-calendar.php
index 3036d0a..9b9c4ca 100644
--- a/family-calendar.php
+++ b/family-calendar.php
@@ -90,6 +90,25 @@ require __DIR__ . '/header.php';
+
+
+
+
+