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 += ``; + }); + 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 += ``; + 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 += "
${day}
${day}
"; + + 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 = ` +
Ajouter
+ + +
Mode de Garde
+ + + `; + 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 = ` +
Congé Carole
+ + + `; + } else { + congeSection = ` +
Ajouter un congé
+ + + `; + } + + let gardeSection = ""; + if (garde) { + const oppositeGarde = garde.type === "CENTRE" ? "AVIS" : "CENTRE"; + gardeSection = ` +
Mode de garde
+ + + `; + } else { + gardeSection = ` +
Ajouter mode de garde
+ + + `; + } + + 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'; + + + +
+

Calendrier mensuel

+
+
+ +

+ +
+
+ +
+ +
+
+
+