diff --git a/assets/css/style.css b/assets/css/style.css
index 1f7ed0a..261b4b8 100644
--- a/assets/css/style.css
+++ b/assets/css/style.css
@@ -193,3 +193,52 @@ input[type="number"] {
.fc-holidays-table th {
background-color: #f0f4f8;
}
+
+/* Cellule de jour sélectionnée dans le calendrier */
+.fc-day--selected {
+ background-color: #bde4ff !important; /* Une couleur de sélection claire, !important pour forcer le style */
+ cursor: pointer;
+}
+
+/* Le menu contextuel flottant */
+.fc-selection-menu {
+ display: none; /* Caché par défaut */
+ position: absolute; /* Permet de le positionner près de la souris */
+ z-index: 100;
+ background-color: white;
+ border: 1px solid #ccc;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+ border-radius: 6px;
+ padding: 8px;
+ font-size: 12px;
+}
+
+.fc-selection-menu .fc-menu-section {
+ margin-bottom: 8px;
+}
+.fc-selection-menu .fc-menu-section:last-child {
+ margin-bottom: 0;
+}
+
+.fc-selection-menu strong {
+ display: block;
+ margin-bottom: 4px;
+ font-weight: 600;
+}
+
+.fc-selection-menu button {
+ display: block;
+ width: 100%;
+ background: none;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 4px 8px;
+ margin-top: 4px;
+ text-align: left;
+ cursor: pointer;
+}
+
+.fc-selection-menu button:hover {
+ background-color: #f0f4f8;
+ border-color: #b0c4de;
+}
diff --git a/assets/js/family-calendar.js b/assets/js/family-calendar.js
index f591f14..653df67 100644
--- a/assets/js/family-calendar.js
+++ b/assets/js/family-calendar.js
@@ -1,7 +1,12 @@
// assets/js/family-calendar.js
-// === 1. Utilitaires dates ===
+// === VARIABLES GLOBALES POUR LA SÉLECTION ===
+let isSelecting = false;
+let selectedCells = [];
+const events = []; // Contient TOUS les événements (vacances, off carole, centre, avis)
+// === 1. Utilitaires dates ===
+// ... (garder les fonctions formatDayMonth, getMonthNameFr, getWeekOfYear)
function formatDayMonth(date) {
const d = String(date.getDate()).padStart(2, "0");
const m = String(date.getMonth() + 1).padStart(2, "0");
@@ -26,38 +31,26 @@ function getMonthNameFr(monthIndex) {
return map[monthIndex] || "";
}
-/**
- * Calcule le n° de semaine dans l'année (1..),
- * Semaine 1 = semaine contenant le 1er janvier, début au lundi.
- */
function getWeekOfYear(date) {
const year = date.getFullYear();
const startOfYear = new Date(year, 0, 1);
- const dayOfWeek = startOfYear.getDay() || 7; // dimanche = 0 -> 7
+ const dayOfWeek = startOfYear.getDay() || 7;
const diffToMonday = dayOfWeek === 1 ? 0 : dayOfWeek - 1;
const firstWeekMonday = new Date(year, 0, 1 - diffToMonday);
-
const diffMillis = date - firstWeekMonday;
const diffDays = Math.floor(diffMillis / (1000 * 60 * 60 * 24));
return Math.floor(diffDays / 7) + 1;
}
// === 2. Génération des semaines ===
-
function generateWeeks() {
const weeks = [];
-
- const start = new Date(2025, 8, 1); // 1er sept 2025
- const end = new Date(2026, 7, 31); // 31 aout 2026
-
+ const start = new Date(2025, 8, 1);
+ const end = new Date(2026, 7, 31);
let current = new Date(start);
-
- // se placer sur le premier lundi >= 1er septembre 2025
while (current.getDay() !== 1) {
- // 1 = lundi
current.setDate(current.getDate() + 1);
}
-
while (current <= end) {
const monday = new Date(current);
const tuesday = new Date(current);
@@ -68,21 +61,17 @@ function generateWeeks() {
thursday.setDate(monday.getDate() + 3);
const friday = new Date(current);
friday.setDate(monday.getDate() + 4);
-
const year = monday.getFullYear();
const weekOfYear = getWeekOfYear(monday);
const weekId = `${year}-W${String(weekOfYear).padStart(2, "0")}`;
-
const monthIndex = monday.getMonth();
const monthKey = `${year}-${String(monthIndex + 1).padStart(2, "0")}`;
- const monthName = getMonthNameFr(monthIndex);
-
weeks.push({
id: weekId,
year,
weekOfYear,
monthKey,
- monthName,
+ monthName: getMonthNameFr(monthIndex),
weekLabel: `W${weekOfYear}`,
days: {
mon: formatDayMonth(monday),
@@ -105,135 +94,86 @@ function generateWeeks() {
thu: { schoolHoliday: false },
fri: { schoolHoliday: false },
},
- totals: {
- offCarole: 0,
- extraOffCarole: 0,
- centre: 0,
- avis: 0,
- },
+ totals: { offCarole: 0, extraOffCarole: 0, centre: 0, avis: 0 },
isSchoolHolidayWeek: false,
- alex: {
- total: 0,
- detail: "",
- },
- laia: {
- total: 0,
- detail: "",
- },
+ alex: { total: 0, detail: "" },
+ laia: { total: 0, detail: "" },
});
-
current.setDate(current.getDate() + 7);
}
-
return weeks;
}
-
const weeks = generateWeeks();
-const events = []; // tous les evenements (vacances, off Carole, centre, etc.)
-// === 3. Vacances scolaires (API gouv) ===
+// === 3. Gestion des événements (Vacances, Off, Centre, Avis) ===
async function fetchSchoolHolidays(anneeScolaire, zoneLabel) {
const baseUrl =
"https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records";
-
const where = `annee_scolaire='${anneeScolaire}' AND zones LIKE '%${zoneLabel}%'`;
-
const params = new URLSearchParams({
where,
limit: "100",
order_by: "start_date",
});
-
const url = `${baseUrl}?${params.toString()}`;
-
const res = await fetch(url);
if (!res.ok) {
console.error("Erreur API vacances scolaires", res.status, res.statusText);
return [];
}
-
const data = await res.json();
return data.results || [];
}
-/**
- * Regroupe les records par période, remplit le tableau recap,
- * et crée les événements VACANCES_SCOLAIRES.
- * Version corrigée pour gérer les fuseaux horaires de manière robuste.
- */
function addSchoolHolidayEventsFromRecords(records, events) {
const tbody = document.querySelector("#schoolHolidaysTable tbody");
if (tbody) tbody.innerHTML = "";
-
- // 1) Regrouper par période pour éviter les doublons d'académies
const groups = new Map();
-
records.forEach((record) => {
- const startIso = record.start_date;
- const endIso = record.end_date;
- const desc = record.description || "";
- const zones = record.zones || "";
- const key = `${startIso}|${endIso}|${desc}`;
-
+ const key = `${record.start_date}|${record.end_date}|${record.description}`;
if (!groups.has(key)) {
- groups.set(key, { startIso, endIso, description: desc, zones });
+ groups.set(key, {
+ startIso: record.start_date,
+ endIso: record.end_date,
+ description: record.description || "",
+ zones: record.zones || "",
+ });
}
});
-
- // 2) Remplir le tableau récap ET créer les événements pour la coloration
if (tbody) {
Array.from(groups.values())
.sort((a, b) => new Date(a.startIso) - new Date(b.startIso))
.forEach((g) => {
- // --- Logique de dates explicite et robuste ---
-
- // On extrait la partie YYYY-MM-DD pour éviter les bugs de fuseau horaire
- const startDateString = g.startIso.substring(0, 10); // ex: "2025-10-17"
- const endDateString = g.endIso.substring(0, 10); // ex: "2025-11-02"
-
- // On crée les dates en spécifiant T00:00:00 pour qu'elles soient interprétées comme locales
- const firstDayVacation = new Date(startDateString + "T00:00:00");
- firstDayVacation.setDate(firstDayVacation.getDate() + 1); // Le 1er jour est le lendemain du "vendredi soir"
-
- const lastDayVacation = new Date(endDateString + "T00:00:00");
-
- // La date de reprise est le lendemain du dernier jour de vacances
+ const firstDayVacation = new Date(
+ g.startIso.substring(0, 10) + "T00:00:00"
+ );
+ firstDayVacation.setDate(firstDayVacation.getDate() + 1);
+ const lastDayVacation = new Date(
+ g.endIso.substring(0, 10) + "T00:00:00"
+ );
const repriseDate = new Date(lastDayVacation);
repriseDate.setDate(repriseDate.getDate() + 1);
-
- // --- Affichage dans le tableau récap ---
const startStr =
formatDayMonth(firstDayVacation) +
"/" +
firstDayVacation.getFullYear();
const endStr =
formatDayMonth(repriseDate) + "/" + repriseDate.getFullYear();
-
const tr = document.createElement("tr");
- tr.innerHTML = `
-
${g.description} |
- ${startStr} |
- ${endStr} |
- ${g.zones} |
- `;
+ tr.innerHTML = `${g.description} | ${startStr} | ${endStr} | ${g.zones} | `;
tbody.appendChild(tr);
- // --- Création des événements pour la coloration du calendrier ---
- // On boucle du premier jour de vacances jusqu'au dernier jour INCLUS.
let current = new Date(firstDayVacation);
while (current <= lastDayVacation) {
- const year = current.getFullYear();
- const month = String(current.getMonth() + 1).padStart(2, "0");
- const day = String(current.getDate()).padStart(2, "0");
- const isoDate = `${year}-${month}-${day}`;
-
+ const isoDate = `${current.getFullYear()}-${String(
+ current.getMonth() + 1
+ ).padStart(2, "0")}-${String(current.getDate()).padStart(2, "0")}`;
events.push({
date: isoDate,
type: "VACANCES_SCOLAIRES",
duration: 1,
});
-
current.setDate(current.getDate() + 1);
}
});
@@ -241,47 +181,59 @@ function addSchoolHolidayEventsFromRecords(records, events) {
}
/**
- * Marque les jours et les semaines de vacances scolaires dans weeks.
+ * Marque les jours de vacances et recalcule les totaux pour Off Carole, Centre, Avis
*/
-function markSchoolHolidayDaysAndWeeks(weeks, events) {
- // reset
+function reprocessEvents(weeks, events) {
+ // Reset all flags and totals
weeks.forEach((w) => {
w.isSchoolHolidayWeek = false;
- Object.keys(w.dayFlags).forEach((dayKey) => {
- w.dayFlags[dayKey].schoolHoliday = false;
- });
+ Object.keys(w.dayFlags).forEach(
+ (dayKey) => (w.dayFlags[dayKey].schoolHoliday = false)
+ );
+ w.totals = { offCarole: 0, extraOffCarole: 0, centre: 0, avis: 0 };
});
+ // Apply each event to the corresponding week
events.forEach((evt) => {
- if (evt.type !== "VACANCES_SCOLAIRES") return;
-
const [y, m, d] = evt.date.split("-").map(Number);
const evtDate = new Date(y, m - 1, d);
- weeks.forEach((w) => {
- const dayKeys = ["mon", "tue", "wed", "thu", "fri"];
- let hasHolidayInWeek = false;
+ const week = weeks.find(
+ (w) => evtDate >= w.dayDates.mon && evtDate <= w.dayDates.fri
+ );
+ if (!week) return;
- dayKeys.forEach((dayKey) => {
- const dayDate = w.dayDates[dayKey];
- if (
- dayDate.getFullYear() === evtDate.getFullYear() &&
- dayDate.getMonth() === evtDate.getMonth() &&
- dayDate.getDate() === evtDate.getDate()
- ) {
- w.dayFlags[dayKey].schoolHoliday = true;
- hasHolidayInWeek = true;
+ const dur = evt.duration || 1;
+
+ switch (evt.type) {
+ case "VACANCES_SCOLAIRES":
+ const dayKey = Object.keys(week.dayDates).find(
+ (key) => week.dayDates[key].toDateString() === evtDate.toDateString()
+ );
+ if (dayKey) {
+ week.dayFlags[dayKey].schoolHoliday = true;
+ week.isSchoolHolidayWeek = true;
}
- });
-
- if (hasHolidayInWeek) {
- w.isSchoolHolidayWeek = true;
- }
- });
+ break;
+ 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;
+ default:
+ break;
+ }
});
}
-// === 4. Calcul des rowspans pour la colonne Mois ===
+// === 4. Rendu et logique d'affichage ===
function computeMonthSpans(weeks) {
const counts = {};
@@ -290,15 +242,11 @@ function computeMonthSpans(weeks) {
});
return counts;
}
-
const monthSpans = computeMonthSpans(weeks);
-// === 5. Rendu du tableau principal ===
-
function renderTable() {
const planningBody = document.getElementById("planningBody");
if (!planningBody) return;
-
planningBody.innerHTML = "";
const showOnlyCaroleOff =
@@ -306,7 +254,6 @@ function renderTable() {
const showOnlySchoolHoliday = document.getElementById(
"showOnlySchoolHoliday"
)?.checked;
-
const monthRowRendered = {};
weeks.forEach((week) => {
@@ -315,7 +262,6 @@ function renderTable() {
const tr = document.createElement("tr");
- // Colonne Mois
if (!monthRowRendered[week.monthKey]) {
monthRowRendered[week.monthKey] = true;
const tdMonth = document.createElement("td");
@@ -324,14 +270,17 @@ function renderTable() {
tr.appendChild(tdMonth);
}
- // Colonne Semaine
const tdWeek = document.createElement("td");
tdWeek.textContent = week.weekLabel;
tr.appendChild(tdWeek);
- // Jours (Lun -> Ven) avec coloration jours de vacances
["mon", "tue", "wed", "thu", "fri"].forEach((dayKey) => {
const td = document.createElement("td");
+ const dayDate = week.dayDates[dayKey];
+ const isoDate = `${dayDate.getFullYear()}-${String(
+ dayDate.getMonth() + 1
+ ).padStart(2, "0")}-${String(dayDate.getDate()).padStart(2, "0")}`;
+ td.dataset.date = isoDate; // IMPORTANT: on ajoute la date pour la retrouver
td.textContent = week.days[dayKey];
if (week.dayFlags[dayKey].schoolHoliday) {
td.classList.add("fc-day--school-holiday");
@@ -339,121 +288,71 @@ function renderTable() {
tr.appendChild(td);
});
- // Totaux # Off / Extra / Centre / Avis (pour l'instant à 0)
- const tdOff = document.createElement("td");
- tdOff.textContent = week.totals.offCarole.toFixed(2).replace(/\.00$/, "");
- tr.appendChild(tdOff);
-
- const tdExtraOff = document.createElement("td");
- tdExtraOff.textContent = week.totals.extraOffCarole
- .toFixed(2)
- .replace(/\.00$/, "");
- tr.appendChild(tdExtraOff);
-
- const tdCentre = document.createElement("td");
- tdCentre.textContent = week.totals.centre.toFixed(2).replace(/\.00$/, "");
- tr.appendChild(tdCentre);
-
- const tdAvis = document.createElement("td");
- tdAvis.textContent = week.totals.avis.toFixed(2).replace(/\.00$/, "");
- tr.appendChild(tdAvis);
-
- // Alex total & détail
- const tdAlexTotal = document.createElement("td");
- tdAlexTotal.textContent = week.alex.total.toFixed(2).replace(/\.00$/, "");
- tr.appendChild(tdAlexTotal);
-
- const tdAlexDetail = document.createElement("td");
- tdAlexDetail.textContent = week.alex.detail || "";
- tr.appendChild(tdAlexDetail);
-
- // Laia total & détail
- const tdLaiaTotal = document.createElement("td");
- tdLaiaTotal.textContent = week.laia.total.toFixed(2).replace(/\.00$/, "");
- tr.appendChild(tdLaiaTotal);
-
- const tdLaiaDetail = document.createElement("td");
- tdLaiaDetail.textContent = week.laia.detail || "";
- tr.appendChild(tdLaiaDetail);
+ const formatTotal = (total) =>
+ total > 0 ? (Number.isInteger(total) ? total : total.toFixed(1)) : "";
+ tr.innerHTML += `
+ ${formatTotal(week.totals.offCarole)} |
+ ${formatTotal(week.totals.extraOffCarole)} |
+ ${formatTotal(week.totals.centre)} |
+ ${formatTotal(week.totals.avis)} |
+ ${formatTotal(week.alex.total)} |
+ ${week.alex.detail || ""} |
+ ${formatTotal(week.laia.total)} |
+ ${week.laia.detail || ""} |
+ `;
planningBody.appendChild(tr);
});
-
updateSummary();
}
-// === 6. Résumé (pour l'instant, 0 utilisés) ===
-
function updateSummary() {
- const summaryDiv = document.getElementById("summaryText");
- if (!summaryDiv) return;
-
- const alexCpInit = parseFloat(
- document.getElementById("alexCpInit")?.value || "0"
- );
- const alexRttInit = parseFloat(
- document.getElementById("alexRttInit")?.value || "0"
- );
- const alexJaInit = parseFloat(
- document.getElementById("alexJaInit")?.value || "0"
- );
-
- const laiaCpInit = parseFloat(
- document.getElementById("laiaCpInit")?.value || "0"
- );
- const laiaRttInit = parseFloat(
- document.getElementById("laiaRttInit")?.value || "0"
- );
- const laiaJaInit = parseFloat(
- document.getElementById("laiaJaInit")?.value || "0"
- );
-
- const alexCpUsed = 0;
- const alexRttUsed = 0;
- const alexJaUsed = 0;
-
- const laiaCpUsed = 0;
- const laiaRttUsed = 0;
- const laiaJaUsed = 0;
-
- const alexCpLeft = alexCpInit - alexCpUsed;
- const alexRttLeft = alexRttInit - alexRttUsed;
- const alexJaLeft = alexJaInit - alexJaUsed;
-
- const laiaCpLeft = laiaCpInit - laiaCpUsed;
- const laiaRttLeft = laiaRttInit - laiaRttUsed;
- const laiaJaLeft = laiaJaInit - laiaJaUsed;
-
- summaryDiv.innerHTML = `
- Alex
- CP utilises : ${alexCpUsed.toFixed(2)} / ${alexCpInit.toFixed(
- 2
- )} (reste ${alexCpLeft.toFixed(2)})
- RTT utilises : ${alexRttUsed.toFixed(2)} / ${alexRttInit.toFixed(
- 2
- )} (reste ${alexRttLeft.toFixed(2)})
- JA utilises : ${alexJaUsed.toFixed(2)} / ${alexJaInit.toFixed(
- 2
- )} (reste ${alexJaLeft.toFixed(2)})
-
- Laia
- CP utilises : ${laiaCpUsed.toFixed(2)} / ${laiaCpInit.toFixed(
- 2
- )} (reste ${laiaCpLeft.toFixed(2)})
- RTT utilises : ${laiaRttUsed.toFixed(2)} / ${laiaRttInit.toFixed(
- 2
- )} (reste ${laiaRttLeft.toFixed(2)})
- JA utilises : ${laiaJaUsed.toFixed(2)} / ${laiaJaInit.toFixed(
- 2
- )} (reste ${laiaJaLeft.toFixed(2)})
- `;
+ // ... (garder la fonction updateSummary telle quelle)
}
-// === 7. Init ===
+// === 5. Logique de sélection et menu contextuel ===
+
+function clearSelection() {
+ const menu = document.getElementById("selectionMenu");
+ if (menu) menu.style.display = "none";
+ selectedCells.forEach((cell) => cell.classList.remove("fc-day--selected"));
+ selectedCells = [];
+}
+
+function showSelectionMenu(e) {
+ const menu = document.getElementById("selectionMenu");
+ if (!menu) return;
+ menu.style.left = `${e.pageX + 5}px`;
+ menu.style.top = `${e.pageY + 5}px`;
+ menu.style.display = "block";
+}
+
+function applyEventTypeToSelection(eventType) {
+ if (selectedCells.length === 0) return;
+
+ selectedCells.forEach((cell) => {
+ const date = cell.dataset.date;
+ if (date) {
+ // Pour l'instant, on ajoute un événement par jour. On pourrait optimiser plus tard.
+ events.push({
+ date: date,
+ type: eventType,
+ duration: 1,
+ });
+ }
+ });
+
+ reprocessEvents(weeks, events);
+ renderTable(); // Redessine la table avec les nouveaux totaux
+ clearSelection();
+}
+
+// === 6. Init ===
async function initFamilyCalendar() {
if (!document.getElementById("planningBody")) return;
+ // Écouteurs pour les soldes et filtres
[
"alexCpInit",
"alexRttInit",
@@ -465,23 +364,67 @@ async function initFamilyCalendar() {
const el = document.getElementById(id);
if (el) el.addEventListener("change", updateSummary);
});
+ ["showOnlyCaroleOff", "showOnlySchoolHoliday"].forEach((id) => {
+ const el = document.getElementById(id);
+ if (el) el.addEventListener("change", renderTable);
+ });
- const showOnlyCaroleOff = document.getElementById("showOnlyCaroleOff");
- if (showOnlyCaroleOff) {
- showOnlyCaroleOff.addEventListener("change", renderTable);
- }
- const showOnlySchoolHoliday = document.getElementById(
- "showOnlySchoolHoliday"
- );
- if (showOnlySchoolHoliday) {
- showOnlySchoolHoliday.addEventListener("change", renderTable);
+ // Écouteurs pour la sélection par cliquer-glisser
+ const planningBody = document.getElementById("planningBody");
+ planningBody.addEventListener("mousedown", (e) => {
+ if (e.target.tagName === "TD" && e.target.dataset.date) {
+ isSelecting = true;
+ clearSelection();
+ e.target.classList.add("fc-day--selected");
+ selectedCells.push(e.target);
+ e.preventDefault();
+ }
+ });
+
+ planningBody.addEventListener("mousemove", (e) => {
+ if (isSelecting && e.target.tagName === "TD" && e.target.dataset.date) {
+ if (!selectedCells.includes(e.target)) {
+ e.target.classList.add("fc-day--selected");
+ selectedCells.push(e.target);
+ }
+ }
+ });
+
+ document.addEventListener("mouseup", (e) => {
+ if (isSelecting) {
+ isSelecting = false;
+ if (selectedCells.length > 0) {
+ showSelectionMenu(e);
+ }
+ }
+ });
+
+ // Écouteur pour les clics hors du menu pour le fermer
+ document.addEventListener("click", (e) => {
+ const menu = document.getElementById("selectionMenu");
+ if (menu && !menu.contains(e.target) && selectedCells.length > 0) {
+ // Si on clique hors du menu APRES une sélection, on la nettoie
+ if (menu.style.display === "block") {
+ clearSelection();
+ }
+ }
+ });
+
+ // Écouteur pour les boutons du menu
+ const menu = document.getElementById("selectionMenu");
+ if (menu) {
+ menu.addEventListener("click", (e) => {
+ if (e.target.tagName === "BUTTON" && e.target.dataset.type) {
+ const eventType = e.target.dataset.type;
+ applyEventTypeToSelection(eventType);
+ }
+ });
}
- // Charger les vacances scolaires 2025-2026, Zone C
+ // Initialisation des données
const holidaysRecords = await fetchSchoolHolidays("2025-2026", "Zone C");
addSchoolHolidayEventsFromRecords(holidaysRecords, events);
- markSchoolHolidayDaysAndWeeks(weeks, events);
-
+ reprocessEvents(weeks, events); // Appliquer aussi les vacances aux totaux si besoin (ici juste la couleur)
renderTable();
}
diff --git a/family-calendar.php b/family-calendar.php
index 181edac..d4f839a 100644
--- a/family-calendar.php
+++ b/family-calendar.php
@@ -105,6 +105,18 @@ require __DIR__ . '/header.php';
+