diff --git a/generate-weeks.php b/generate-weeks.php new file mode 100644 index 0000000..d933fc6 --- /dev/null +++ b/generate-weeks.php @@ -0,0 +1,77 @@ +📅 GĂ©nĂ©ration du squelette du calendrier (pf_calendar_weeks)"; + +$monthsFr = [1=>'Janvier', 2=>'FĂ©vrier', 3=>'Mars', 4=>'Avril', 5=>'Mai', 6=>'Juin', 7=>'Juillet', 8=>'AoĂ»t', 9=>'Septembre', 10=>'Octobre', 11=>'Novembre', 12=>'DĂ©cembre']; + +try { + $stmt = $meta_pdo->query("SELECT db_name, name FROM families WHERE is_active = 1"); + $families = $stmt->fetchAll(PDO::FETCH_ASSOC); + + foreach ($families as $family) { + $dbName = $family['db_name']; + echo "

Famille : {$family['name']} ($dbName)

"; + } + echo "

🎉 TerminĂ© ! Tu peux supprimer ce fichier et rafraĂźchir ton calendrier.

"; + +} catch (Exception $e) { + die("Erreur fatale Meta DB : " . $e->getMessage()); +} +?> \ No newline at end of file diff --git a/modules/family-calendar/family-calendar.css b/modules/family-calendar/family-calendar.css index 2a99601..9f9be8b 100644 --- a/modules/family-calendar/family-calendar.css +++ b/modules/family-calendar/family-calendar.css @@ -464,6 +464,32 @@ td.col-laia-sub { color: #64748b; } +/* ========================================================================== + OPTIMISATION DES LARGEURS DU TABLEAU HEBDOMADAIRE + ========================================================================== */ + +/* 1. On limite les colonnes des jours (Lun-Ven) pour qu'elles prennent ~55% de la page max */ +#planningTable th.col-day, +#planningTable td.col-day { + width: 11%; + max-width: 90px; +} + +/* 2. On compacte la colonne "Sem." qui n'affiche plus que 2 chiffres */ +#planningTable th.col-sticky-sem, +#planningTable td.col-sticky-sem { + width: 40px; + min-width: 40px; + text-align: center; + padding: 4px; /* RĂ©duit les marges internes */ +} + +/* 3. On s'assure que le tableau utilise tout l'espace disponible intelligemment */ +#planningTable { + table-layout: auto; + width: 100%; +} + /* GESTION DES COLONNES STICKY (Mois & Semaine) */ #planningTable tbody td.col-sticky-mois { position: sticky !important; @@ -927,3 +953,87 @@ td.col-laia-sub { transform: translateY(0); } } +/* ========================================================================== + AFFINAGE EXTRÊME DES COLONNES MOIS ET SEMAINE (Mise Ă  jour) + ========================================================================== */ + +/* 1. Colonne Mois (Texte vertical) : On rĂ©duit Ă  35px */ +.col-month, +#planningTable th.col-sticky-mois, +#planningTable td.col-sticky-mois { + width: 15px !important; + min-width: 15px !important; + max-width: 15px !important; + padding: 2px !important; +} + +/* 2. Colonne Semaine (NumĂ©ro court) : On rĂ©duit Ă  30px */ +#planningTable th.col-sticky-sem, +#planningTable td.col-sticky-sem { + width: 10px !important; + min-width: 10px !important; + max-width: 10px !important; + padding: 2px !important; + font-size: 0.85rem !important; +} + +/* 3. CORRECTION CRITIQUE : Le dĂ©calage "Sticky" de la colonne Semaine */ +/* Puisque la colonne Mois fait 35px, la colonne Semaine doit dĂ©marrer Ă  35px (et non plus 54px) */ +#planningTable tbody td.col-sticky-sem, +#planningTable thead tr th.col-sticky-sem { + left: 15px !important; +} + +/* ========================================================================== + RÉGLAGES MILLIMÉTRÉS (FIX ALIGNEMENTS & STICKY) + ========================================================================== */ + +/* 1. On force un alignement strict pour Ă©viter que les bordures de "Av." et "Use" ne se dĂ©salignent */ +#planningTable { + table-layout: fixed !important; + width: max-content !important; + min-width: 100% !important; +} + +/* 2. Colonne Mois : Box-sizing inclut le padding pour un calcul mathĂ©matique exact */ +.col-month, +#planningTable th.col-sticky-mois, +#planningTable td.col-sticky-mois { + box-sizing: border-box !important; + width: 25px !important; /* 15px + marges de sĂ©curitĂ© */ + min-width: 25px !important; + max-width: 25px !important; + padding: 2px !important; + overflow: hidden; +} + +/* 3. Colonne Semaine : Le dĂ©calage (left) correspond EXACTEMENT Ă  la largeur du Mois */ +#planningTable th.col-sticky-sem, +#planningTable td.col-sticky-sem { + box-sizing: border-box !important; + left: 25px !important; /* Doit ĂȘtre rigoureusement Ă©gal au width du Mois ! */ + width: 20px !important; /* 10px + marges */ + min-width: 20px !important; + max-width: 20px !important; + padding: 2px !important; + font-size: 0.8rem !important; + overflow: hidden; +} + +/* 4. SĂ©curitĂ© Anti-Trous pour les headers collants du haut (EmpĂȘche de voir Ă  travers lors du scroll) */ +#planningTable thead tr { + height: 32px !important; +} +#planningTable thead th { + height: 32px !important; + box-sizing: border-box !important; +} +#planningTable thead tr:nth-child(1) th { + top: 0 !important; +} +#planningTable thead tr:nth-child(2) th { + top: 32px !important; +} +#planningTable thead tr:nth-child(3) th { + top: 64px !important; +} diff --git a/modules/family-calendar/family-calendar.js b/modules/family-calendar/family-calendar.js index d9d900d..6916b0b 100644 --- a/modules/family-calendar/family-calendar.js +++ b/modules/family-calendar/family-calendar.js @@ -64,7 +64,6 @@ function closeCalendarSettings() { } function switchCalendarTab(tabId) { - // 1. On rĂ©initialise l'affichage document .querySelectorAll(".bs-tab-btn") .forEach((btn) => btn.classList.remove("active")); @@ -72,15 +71,12 @@ function switchCalendarTab(tabId) { .querySelectorAll(".cal-settings-pane") .forEach((pane) => (pane.style.display = "none")); - // 2. On active l'onglet cliquĂ© document.getElementById(`tab-btn-${tabId}`).classList.add("active"); document.getElementById(`cal-pane-${tabId}`).style.display = "block"; - // 3. Actions spĂ©cifiques if (tabId === "foyer") { loadLeaveCatalog(); } else if (tabId === "membres") { - // On charge la vue du membre sĂ©lectionnĂ© par dĂ©faut dans la liste if (document.getElementById("selectCalMember").value) { loadMemberConfigView(); } @@ -89,7 +85,7 @@ function switchCalendarTab(tabId) { function renderCareModeTags() { const container = document.getElementById("careModesContainer"); - if (!container) return; // 🛑 LA BARRIÈRE DE SÉCURITÉ EST ICI + if (!container) return; container.innerHTML = localCareModes .map( @@ -102,10 +98,9 @@ function renderCareModeTags() { .join(""); } -// Ajoutons la mĂȘme sĂ©curitĂ© pour l'ajout au cas oĂč : function addCareModeTag() { const input = document.getElementById("inputNewCareMode"); - if (!input) return; // 🛑 SÉCURITÉ + if (!input) return; const val = input.value.trim(); if (val && !localCareModes.includes(val)) { @@ -254,7 +249,6 @@ function resetLeaveTypeForm() { document.getElementById("lt-label").value = ""; } -// NOUVELLE FONCTION DE SUPPRESSION async function deleteLeaveType(code, label) { if ( !confirm( @@ -278,7 +272,6 @@ async function deleteLeaveType(code, label) { resetLeaveTypeForm(); loadLeaveCatalog(); - // On recharge l'onglet membre au cas oĂč le membre affichĂ© utilisait ce congĂ© if (document.getElementById("selectCalMember").value) { loadMemberConfigView(); } @@ -320,8 +313,6 @@ async function saveLeaveType() { // ========================================== // MODALE SETTINGS : AFFECTATION (MEMBRES) // ========================================== - -// Quand on choisit un membre dans la liste dĂ©roulante async function loadMemberConfigView() { const select = document.getElementById("selectCalMember"); const zone = document.getElementById("memberConfigZone"); @@ -335,7 +326,6 @@ async function loadMemberConfigView() { zone.innerHTML = '

Chargement...

'; - // === CAS 1 : C'EST UN ENFANT (On gÚre les modes de garde) === if (role === "child" || role === "enfant") { const currentPerson = calGlobalData.people.find( (k) => parseInt(k.id) === personId, @@ -367,9 +357,7 @@ async function loadMemberConfigView() { `; - } - // === CAS 2 : C'EST UN ADULTE (On gÚre les congés) === - else { + } else { try { const res = await pachaFetch( `/modules/family-calendar/includes/api/calendar-settings.php?action=get_person_leaves&person_id=${personId}`, @@ -383,7 +371,6 @@ async function loadMemberConfigView() { } } -// Affichage des congés du membre et du formulaire d'ajout function renderMemberLeavesView(personId, memberLeaves) { const zone = document.getElementById("memberConfigZone"); let html = `
Congés attribués
`; @@ -393,7 +380,6 @@ function renderMemberLeavesView(personId, memberLeaves) { } else { html += `
`; memberLeaves.forEach((ml) => { - // On extrait le mois de la date anniversaire (ex: "2000-06-01" -> 6) const moisRenouv = ml.anniversary_date ? parseInt(ml.anniversary_date.split("-")[1]) : 1; @@ -414,7 +400,6 @@ function renderMemberLeavesView(personId, memberLeaves) { html += `
`; } - // Ajout du formulaire d'attribution complet html += `
+ Attribuer un congé
@@ -462,20 +447,17 @@ function renderMemberLeavesView(personId, memberLeaves) { } async function addMemberLeave(personId) { - // 1. RĂ©cupĂ©ration des valeurs du formulaire const leaveCode = document.getElementById("new-member-leave-type").value; const allowance = document.getElementById("new-member-leave-allowance").value; const resetMonth = document.getElementById("new-member-leave-reset").value; const method = document.getElementById("new-member-leave-method").value; - // 2. SĂ©curitĂ© : vĂ©rifier qu'un type de congĂ© a bien Ă©tĂ© sĂ©lectionnĂ© if (!leaveCode) { return window.showToast ? showToast("Veuillez sĂ©lectionner un type de congĂ©", "error") : alert("Veuillez sĂ©lectionner un type de congĂ©"); } - // 3. PrĂ©paration des donnĂ©es pour l'API const fd = new FormData(); fd.append("action", "add_person_leave"); fd.append("person_id", personId); @@ -484,7 +466,6 @@ async function addMemberLeave(personId) { fd.append("reset_month", resetMonth); fd.append("method", method); - // 4. Appel Ă  l'API et rafraĂźchissement try { const res = await pachaFetch( "/modules/family-calendar/includes/api/calendar-settings.php", @@ -492,10 +473,7 @@ async function addMemberLeave(personId) { ); if (!res.success) throw new Error(res.error); - if (window.showToast) showToast("CongĂ© attribuĂ© avec succĂšs !", "success"); - - // Recharge la vue du membre pour faire apparaĂźtre la nouvelle ligne immĂ©diatement loadMemberConfigView(); } catch (err) { if (window.showToast) @@ -675,12 +653,15 @@ document.addEventListener("DOMContentLoaded", () => { selectYear.addEventListener("change", handleChange); } + // đŸ”„ LE FIX (refreshAllData avec schoolHols injectĂ© et fusionnĂ©) async refreshAllData() { try { + const zone = window.calGlobalData?.foyer?.zone_scolaire || "C"; const [ weeksData, eventsData, - fixedEventsData, + publicHols, + schoolHols, // đŸ–ïž NOUVEAU : On rĂ©ceptionne les vacances leavesData, snapshotsData, ] = await Promise.all([ @@ -689,6 +670,7 @@ document.addEventListener("DOMContentLoaded", () => { ), this.fetchApi("/modules/family-calendar/includes/api/get-events.php"), this.fetchPublicHolidays(), + this.fetchSchoolHolidays(zone), // đŸ–ïž NOUVEAU : On appelle l'API this.fetchApi("/modules/family-calendar/includes/api/get-leaves.php"), this.fetchApi( "/modules/family-calendar/includes/api/get-leave-snapshots.php", @@ -700,7 +682,9 @@ document.addEventListener("DOMContentLoaded", () => { ...e, duration: parseFloat(e.duration), })); - this.fixedEvents = fixedEventsData; + + // đŸ”„ LE FIX : On fusionne les jours fĂ©riĂ©s ET les vacances + this.fixedEvents = [...publicHols, ...schoolHols]; this.leaves = leavesData.leaves || []; this.leaveSnapshots = snapshotsData.snapshots || []; @@ -736,8 +720,108 @@ document.addEventListener("DOMContentLoaded", () => { } } - renderModalHolidays() {} - async fetchAndSaveGovHolidays(yearStart) {} + async fetchSchoolHolidays(zone) { + if (!zone || zone === "Autre") return []; + try { + const yearStr = `${this.currentSchoolYearStart}-${this.currentSchoolYearStart + 1}`; + // Ton URL qui utilise le LIKE, beaucoup plus robuste ! + const url = `https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=annee_scolaire='${yearStr}' AND zones LIKE '%Zone ${zone}%'&limit=100`; + + const res = await fetch(url); + const data = await res.json(); + const rawRecords = data.results || []; + + // Ton systĂšme de dĂ©doublonnage + const uniqueMap = new Map(); + rawRecords.forEach((r) => { + const key = `${r.description}|${r.start_date}`; + if (!uniqueMap.has(key)) uniqueMap.set(key, r); + }); + + const holidays = []; + + Array.from(uniqueMap.values()).forEach((r) => { + const startDateStr = r.start_date.split("T")[0]; + const endDateStr = r.end_date.split("T")[0]; + + let curr = new Date(startDateStr + "T00:00:00"); + const end = new Date(endDateStr + "T00:00:00"); + + // TA REGLE METIER : Si vendredi, on passe au samedi + if (curr.getDay() === 5) { + curr.setDate(curr.getDate() + 1); + } + + while (curr < end) { + holidays.push({ + id: `sh-${curr.getTime()}`, + date: this.getLocalIsoDate(curr), + name: r.description, + type: "VACANCES_SCOLAIRES", + duration: 1, + }); + curr.setDate(curr.getDate() + 1); + } + }); + + return holidays; + } catch (e) { + console.error("Erreur API Vacances:", e); + return []; + } + } + + async renderModalHolidays() { + const tbody = document.querySelector("#schoolHolidaysTable tbody"); + if (!tbody) return; + + const zone = window.calGlobalData?.foyer?.zone_scolaire || "C"; + if (zone === "Autre") { + tbody.innerHTML = + "Zone 'Autre' sĂ©lectionnĂ©e. Pas de donnĂ©es auto."; + return; + } + + tbody.innerHTML = + "Chargement des donnĂ©es du MinistĂšre... ⏳"; + + try { + const year = this.modalSelectedYear || this.currentSchoolYearStart; + const yearStr = `${year}-${year + 1}`; + const url = `https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=annee_scolaire='${yearStr}' AND zones LIKE '%Zone ${zone}%'&limit=100&order_by=start_date`; + + const res = await fetch(url); + const data = await res.json(); + + if (data.results && data.results.length > 0) { + // Ton systĂšme de dĂ©doublonnage pour la modale + const uniqueMap = new Map(); + data.results.forEach((r) => { + const key = `${r.description}|${r.start_date}`; + if (!uniqueMap.has(key)) uniqueMap.set(key, r); + }); + + let rows = ""; + Array.from(uniqueMap.values()).forEach((p) => { + // On applique aussi le dĂ©calage du vendredi pour l'affichage propre dans la modale + let d1Date = new Date(p.start_date.split("T")[0] + "T00:00:00"); + if (d1Date.getDay() === 5) d1Date.setDate(d1Date.getDate() + 1); + + const d1 = d1Date.toLocaleDateString(window.appLang || "fr-FR"); + const d2 = new Date(p.end_date.split("T")[0]).toLocaleDateString( + window.appLang || "fr-FR", + ); + rows += `${p.description}${d1}${d2}`; + }); + tbody.innerHTML = rows; + } else { + tbody.innerHTML = `Aucune vacance trouvĂ©e pour ${yearStr}.`; + } + } catch (e) { + tbody.innerHTML = + "Erreur de connexion Ă  l'API du gouvernement."; + } + } reprocessAndRender() { this.reprocessEvents(); @@ -832,54 +916,68 @@ document.addEventListener("DOMContentLoaded", () => { const balances = {}; this.parents.forEach((p) => (balances[String(p.id)] = {})); - const ymSet = new Set(); - this.weeks.forEach((w) => ymSet.add(w.monthKey)); - const ymList = Array.from(ymSet).sort(); + const ymList = []; + const tempDate = new Date(); + tempDate.setFullYear(tempDate.getFullYear() - 2); + for (let i = 0; i < 60; i++) { + ymList.push( + `${tempDate.getFullYear()}-${String(tempDate.getMonth() + 1).padStart(2, "0")}`, + ); + tempDate.setMonth(tempDate.getMonth() + 1); + } const usageByMonth = {}; + const allPlacedLeaves = [...(this.leaves || []), ...(this.events || [])]; - // 1. On calcule ce qui a Ă©tĂ© posĂ© (Strictement typĂ© en String pour Ă©viter les bugs) - this.leaves.forEach((l) => { - const pid = String(l.person_id); - const type = String(l.leave_type).trim().toUpperCase(); - const ym = String(l.leave_date).substring(0, 7); + allPlacedLeaves.forEach((l) => { + const rawType = l.leave_type || l.event_type || l.type; + const rawDate = l.leave_date || l.event_date || l.date; + if (!rawType || !rawDate) return; + + const pid = String(l.person_id || l.person); + const type = String(rawType).trim().toUpperCase(); + const ym = String(rawDate).substring(0, 7); if (!usageByMonth[pid]) usageByMonth[pid] = {}; if (!usageByMonth[pid][type]) usageByMonth[pid][type] = {}; - usageByMonth[pid][type][ym] = - (usageByMonth[pid][type][ym] || 0) + (parseFloat(l.duration) || 1); + + let dur = parseFloat(l.duration); + if (isNaN(dur)) dur = 1; + + usageByMonth[pid][type][ym] = (usageByMonth[pid][type][ym] || 0) + dur; }); - // 2. On calcule les soldes pour chaque mois this.parents.forEach((parent) => { const pid = String(parent.id); - const matrix = this.leaveMatrix[pid] || []; + const matrix = + this.leaveMatrix[pid] || this.leaveMatrix[Number(pid)] || []; matrix.forEach((conf) => { - const type = String(conf.type).trim().toUpperCase(); + const type = String(conf.leave_type || conf.type) + .trim() + .toUpperCase(); if (!balances[pid][type]) balances[pid][type] = {}; + let monthRenouvellement = 1; + const dateVal = conf.anniversary_date || conf.date; + if (dateVal && dateVal.includes("-")) { + monthRenouvellement = parseInt(dateVal.split("-")[1], 10) || 1; + } + + let initialBalance = parseFloat(conf.allowance); + if (isNaN(initialBalance)) initialBalance = 0; + ymList.forEach((ym) => { - const [currYear, currMonth] = ym.split("-").map(Number); - let cycleStartStr = ""; - let initialBalance = parseFloat(conf.allowance || 0); - let monthRenouvellement = 1; + const [currYearStr, currMonthStr] = ym.split("-"); + const currYear = parseInt(currYearStr, 10); + const currMonth = parseInt(currMonthStr, 10); - if (conf.date) { - const parts = conf.date.split("-"); - if (parts.length >= 2) monthRenouvellement = parseInt(parts[1]); - } - - const isPastAnniversary = - currMonth > monthRenouvellement || - (currMonth === monthRenouvellement && 1 >= 1); + const isPastAnniversary = currMonth >= monthRenouvellement; const refYear = isPastAnniversary ? currYear : currYear - 1; - cycleStartStr = `${refYear}-${String(monthRenouvellement).padStart(2, "0")}`; + const cycleStartStr = `${refYear}-${String(monthRenouvellement).padStart(2, "0")}`; - // đŸ”„ LE FIX : GESTION DU MODE FIXE VS GRADUEL let acquiredBalance = initialBalance; if (conf.method === "ACCUMULATED") { - // On calcule le nombre de mois passĂ©s depuis la date anniversaire let monthsPassed = (currYear - refYear) * 12 + (currMonth - monthRenouvellement) + @@ -907,142 +1005,190 @@ document.addEventListener("DOMContentLoaded", () => { }); }); }); + this.monthlyLeaveBalances = balances; } renderTable() { if (!this.planningBody) return; - this.planningBody.innerHTML = ""; - const monthSpans = this.weeks.reduce((acc, w) => { - acc[w.monthKey] = (acc[w.monthKey] || 0) + 1; - return acc; - }, {}); - const processedMonths = {}; - const processedLeavesCols = {}; - const fmt = (n) => - n > 0 ? (Number.isInteger(n) ? n : n.toFixed(1)) : ""; - const upperCareModes = this.careModes.map((m) => m.toUpperCase()); - this.weeks.forEach((w, idx) => { - const tr = document.createElement("tr"); - tr.setAttribute("data-month", w.monthKey); - if (idx === 0 || this.weeks[idx - 1].monthKey !== w.monthKey) - tr.classList.add("fc-month-first-week-row"); - if ( - idx === this.weeks.length - 1 || - this.weeks[idx + 1].monthKey !== w.monthKey - ) - tr.classList.add("fc-month-last-week-row"); + try { + this.planningBody.innerHTML = ""; - if (!processedMonths[w.monthKey]) { - processedMonths[w.monthKey] = true; - const td = document.createElement("td"); - td.className = "col-month col-sticky-mois"; - td.innerHTML = `${w.monthName}`; - td.rowSpan = monthSpans[w.monthKey]; - tr.appendChild(td); + if (!this.weeks || this.weeks.length === 0) { + this.planningBody.innerHTML = + "Aucune donnĂ©e pour cette annĂ©e scolaire."; + return; } - const tdW = document.createElement("td"); - tdW.className = "col-month col-sticky-sem"; - tdW.textContent = w.weekLabel; - tr.appendChild(tdW); + const monthSpans = this.weeks.reduce((acc, w) => { + acc[w.monthKey] = (acc[w.monthKey] || 0) + 1; + return acc; + }, {}); - ["mon", "tue", "wed", "thu", "fri"].forEach((d) => { - const td = document.createElement("td"); - const dateObj = w.dayDates[d]; - const iso = this.getLocalIsoDate(dateObj); - td.dataset.date = iso; - td.className = "col-day"; + const processedMonths = {}; + const processedLeavesCols = {}; + const fmt = (n) => + n > 0 ? (Number.isInteger(n) ? n : n.toFixed(1)) : ""; - w.dayFlags[d].events.forEach((evt) => { - if (evt.type === "PUBLIC_HOLIDAY") - td.classList.add("fc-day--public-holiday"); - if (evt.type === "VACANCES_SCOLAIRES") - td.classList.add("fc-day--school-holiday"); - if (evt.type === "HELPER_OFF") - td.classList.add("fc-day--off-carole"); - if (evt.type === "HELPER_EXTRA") - td.classList.add("fc-day--extra-off-carole"); - if (upperCareModes.includes(evt.type)) - td.classList.add("fc-day--has-guard"); - }); + const upperCareModes = (this.careModes || []).map((m) => + String(m).toUpperCase(), + ); - let content = `
- ${String(dateObj.getDate()).padStart(2, "0")}`; + this.weeks.forEach((w, idx) => { + const tr = document.createElement("tr"); + tr.setAttribute("data-month", w.monthKey); - let iconsHtml = `
`; - w.dayFlags[d].events.forEach((evt) => { - if (upperCareModes.includes(evt.type)) { - const modeName = evt.type.toLowerCase(); - if (modeName === "avis") - iconsHtml += ``; - else if (modeName === "centre") - iconsHtml += `đŸ«`; - else - iconsHtml += `${modeName.substring(0, 3)}`; - } - }); - content += iconsHtml + `
`; + if (idx === 0 || this.weeks[idx - 1].monthKey !== w.monthKey) + tr.classList.add("fc-month-first-week-row"); + if ( + idx === this.weeks.length - 1 || + this.weeks[idx + 1].monthKey !== w.monthKey + ) + tr.classList.add("fc-month-last-week-row"); - let sickHtml = `
`; - w.dayFlags[d].events.forEach((evt) => { - if (evt.type === "CHILD_SICK") { - const k = this.kids.find( - (x) => parseInt(x.id) === parseInt(evt.person_id), - ); - if (k) { - sickHtml += `${k.name}đŸ€’`; - } - } - }); - content += sickHtml + `
`; - - const dayLeaves = this.leaves.filter((l) => l.leave_date === iso); - if (dayLeaves.length) { - let html = `
`; - this.parents.forEach((person) => { - if ( - dayLeaves.some( - (l) => parseInt(l.person_id) === parseInt(person.id), - ) - ) { - html += `${person.name.charAt(0).toUpperCase()}`; - } - }); - content += html + `
`; + if (!processedMonths[w.monthKey]) { + processedMonths[w.monthKey] = true; + const td = document.createElement("td"); + td.className = "col-month col-sticky-mois"; + td.innerHTML = `${w.monthName || ""}`; + td.rowSpan = monthSpans[w.monthKey]; + tr.appendChild(td); } - td.innerHTML = content + `
`; - tr.appendChild(td); - }); - this.careModes.forEach((mode) => { - const td = document.createElement("td"); - td.className = "col-total"; - td.textContent = fmt(w.totals["mode_" + mode] || 0); - tr.appendChild(td); - }); - this.kids.forEach((kid) => { - const td = document.createElement("td"); - td.className = "col-total"; - td.textContent = fmt(w.totals["sick_" + kid.id] || 0); - tr.appendChild(td); - }); + const tdW = document.createElement("td"); + tdW.className = "col-month col-sticky-sem"; + tdW.textContent = w.weekLabel || ""; + tr.appendChild(tdW); - if (!processedLeavesCols[w.monthKey]) { - processedLeavesCols[w.monthKey] = true; - this.parents.forEach((parent, index) => { - const cssPrefix = index % 2 === 0 ? "col-alex" : "col-laia"; - (this.leaveMatrix[parent.id] || []).forEach((conf) => { - const info = - this.monthlyLeaveBalances[parent.id]?.[conf.type]?.[w.monthKey]; - tr.innerHTML += `${info ? fmt(info.availableAtMonthStart) : "-"}`; - tr.innerHTML += `${info ? fmt(info.usedInMonth) : ""}`; + ["mon", "tue", "wed", "thu", "fri"].forEach((d) => { + const td = document.createElement("td"); + const dateObj = w.dayDates[d]; + if (!dateObj) return; + + const iso = this.getLocalIsoDate(dateObj); + td.dataset.date = iso; + td.className = "col-day"; + + const events = w.dayFlags?.[d]?.events || []; + + events.forEach((evt) => { + if (evt.type === "PUBLIC_HOLIDAY") + td.classList.add("fc-day--public-holiday"); + if (evt.type === "VACANCES_SCOLAIRES") + td.classList.add("fc-day--school-holiday"); + if (evt.type === "HELPER_OFF") + td.classList.add("fc-day--off-carole"); + if (evt.type === "HELPER_EXTRA") + td.classList.add("fc-day--extra-off-carole"); + if (upperCareModes.includes(evt.type)) + td.classList.add("fc-day--has-guard"); }); + + let content = `
+ ${String(dateObj.getDate()).padStart(2, "0")}`; + + let iconsHtml = `
`; + events.forEach((evt) => { + if (upperCareModes.includes(evt.type)) { + const modeName = String(evt.type).toLowerCase(); + if (modeName === "avis") + iconsHtml += ``; + else if (modeName === "centre") + iconsHtml += `đŸ«`; + else + iconsHtml += `${modeName.substring(0, 3)}`; + } + }); + content += iconsHtml + `
`; + + let sickHtml = `
`; + events.forEach((evt) => { + if (evt.type === "CHILD_SICK") { + const k = (this.kids || []).find( + (x) => parseInt(x.id) === parseInt(evt.person_id), + ); + if (k) { + sickHtml += `${k.name}đŸ€’`; + } + } + }); + content += sickHtml + `
`; + + const dayLeaves = (this.leaves || []).filter( + (l) => l.leave_date === iso || l.date === iso, + ); + if (dayLeaves.length) { + let html = `
`; + (this.parents || []).forEach((person) => { + if ( + dayLeaves.some( + (l) => + parseInt(l.person_id || l.person) === parseInt(person.id), + ) + ) { + html += `${String(person.name).charAt(0).toUpperCase()}`; + } + }); + content += html + `
`; + } + td.innerHTML = content + `
`; + tr.appendChild(td); }); - } - this.planningBody.appendChild(tr); - }); + + (this.careModes || []).forEach((mode) => { + const td = document.createElement("td"); + td.className = "col-total"; + td.textContent = fmt(w.totals["mode_" + mode] || 0); + tr.appendChild(td); + }); + + (this.kids || []).forEach((kid) => { + const td = document.createElement("td"); + td.className = "col-total"; + td.textContent = fmt(w.totals["sick_" + kid.id] || 0); + tr.appendChild(td); + }); + + if (!processedLeavesCols[w.monthKey]) { + processedLeavesCols[w.monthKey] = true; + (this.parents || []).forEach((parent, index) => { + const cssPrefix = index % 2 === 0 ? "col-alex" : "col-laia"; + const matrix = + this.leaveMatrix[String(parent.id)] || + this.leaveMatrix[Number(parent.id)] || + []; + + matrix.forEach((conf) => { + const type = String(conf.leave_type || conf.type) + .trim() + .toUpperCase(); + const info = + this.monthlyLeaveBalances[String(parent.id)]?.[type]?.[ + w.monthKey + ]; + + const tdAv = document.createElement("td"); + tdAv.className = `${cssPrefix}-sub ${cssPrefix}-av`; + tdAv.rowSpan = monthSpans[w.monthKey]; + tdAv.textContent = info ? fmt(info.availableAtMonthStart) : "-"; + tr.appendChild(tdAv); + + const tdUse = document.createElement("td"); + tdUse.className = `${cssPrefix}-sub ${cssPrefix}-use`; + tdUse.rowSpan = monthSpans[w.monthKey]; + tdUse.textContent = info ? fmt(info.usedInMonth) : ""; + tr.appendChild(tdUse); + }); + }); + } + + this.planningBody.appendChild(tr); + }); + } catch (e) { + console.error("đŸ”„ Erreur fatale dans renderTable :", e); + this.planningBody.innerHTML = `Erreur d'affichage : ${e.message}
Regarde la console pour plus de détails.`; + } } generateMonthHTML(year, month) { @@ -1251,6 +1397,8 @@ document.addEventListener("DOMContentLoaded", () => { const container = document.getElementById("fc-month-balances"); if (!container) return; + this.calculateMonthlyBalances(); + const monthsToDisplay = []; const y = this.currentMonth.getFullYear(); const m = this.currentMonth.getMonth(); @@ -1265,11 +1413,15 @@ document.addEventListener("DOMContentLoaded", () => { container.innerHTML = this.parents .map((person) => { let cards = `
${person.name.toUpperCase()}
`; - // Typage strict String(person.id) pour matcher avec le calcul des balances - const types = this.leaveMatrix[String(person.id)] || []; + const types = + this.leaveMatrix[String(person.id)] || + this.leaveMatrix[Number(person.id)] || + []; types.forEach((conf) => { - const type = conf.type; + const type = String(conf.leave_type || conf.type) + .trim() + .toUpperCase(); const startBal = this.monthlyLeaveBalances[String(person.id)]?.[type]?.[ monthsToDisplay[0] @@ -1287,19 +1439,19 @@ document.addEventListener("DOMContentLoaded", () => { n > 0 ? (Number.isInteger(n) ? n : n.toFixed(1)) : "0"; let alertHtml = ""; - const cMonth = parseInt(monthsToDisplay[0].split("-")[1]); + const cMonth = parseInt(monthsToDisplay[0].split("-")[1], 10); + const dateVal = conf.anniversary_date || conf.date; - // Gestion de l'alerte đŸ”„ (Mois en cours ou Mois prĂ©cĂ©dant le renouvellement) - if (endBal > 0 && conf.date) { - const resetMonth = parseInt(conf.date.split("-")[1]); - const alertMonth = resetMonth - 1 === 0 ? 12 : resetMonth - 1; + if (endBal > 0 && dateVal && dateVal.includes("-")) { + const resetMonth = parseInt(dateVal.split("-")[1], 10) || 1; + const alertMonth = resetMonth === 1 ? 12 : resetMonth - 1; if (cMonth === alertMonth || cMonth === resetMonth) { alertHtml = `
đŸ”„
`; } } - cards += `
${type}${fmt(endBal)}${totalUsed > 0 ? `-${fmt(totalUsed)}` : ""}${alertHtml}
`; + cards += `
${conf.type || type}${fmt(endBal)}${totalUsed > 0 ? `-${fmt(totalUsed)}` : ""}${alertHtml}
`; }); return cards + `
`; }) @@ -1317,7 +1469,7 @@ document.addEventListener("DOMContentLoaded", () => { id: `${w.week_iso_year}-W${w.week_iso_number}`, monthKey: `${w.year}-${String(w.month).padStart(2, "0")}`, monthName: w.month_name, - weekLabel: w.week_label, + weekLabel: w.week_iso_number, dayDates: { mon: new Date(w.mon_date + "T00:00:00"), tue: new Date(w.tue_date + "T00:00:00"), @@ -1354,9 +1506,63 @@ document.addEventListener("DOMContentLoaded", () => { setupEventListeners() { const btnSettings = document.getElementById("btnOpenCalendarSettings"); + + const btnSnapshot = document.getElementById("btnOpenSnapshotModal"); + if (btnSnapshot) { + btnSnapshot.addEventListener("click", () => { + const modal = + document.getElementById("modalSnapshot") || + document.getElementById("snapshotModal"); + if (modal) modal.style.display = "flex"; + }); + } + + const btnHolidays = document.getElementById("btnOpenHolidays"); + if (btnHolidays) { + btnHolidays.addEventListener("click", () => { + const modal = + document.getElementById("modalHolidays") || + document.getElementById("schoolHolidaysModal"); + if (modal) modal.style.display = "flex"; + }); + } + if (btnSettings) btnSettings.addEventListener("click", openCalendarSettings); + window.addEventListener("click", (event) => { + if ( + event.target.classList.contains("pf-modal") || + event.target.classList.contains("modal-overlay") + ) { + event.target.style.display = "none"; + event.target.classList.remove("open"); + document.body.classList.remove("no-scroll"); + } + }); + + const btnCloseSnap = document.getElementById("btnCloseSnapshot"); + if (btnCloseSnap) { + btnCloseSnap.addEventListener("click", () => { + const m = + document.getElementById("modalSnapshot") || + document.getElementById("snapshotModal"); + if (m) m.style.display = "none"; + document.body.classList.remove("no-scroll"); + }); + } + + const btnCloseHol = document.getElementById("btnCloseHolidays"); + if (btnCloseHol) { + btnCloseHol.addEventListener("click", () => { + const m = + document.getElementById("modalHolidays") || + document.getElementById("schoolHolidaysModal"); + if (m) m.style.display = "none"; + document.body.classList.remove("no-scroll"); + }); + } + if (this.planningBody) { this.planningBody.addEventListener("mousedown", (e) => this.handleMouseDown(e), diff --git a/modules/family-calendar/includes/api/calendar-settings.php b/modules/family-calendar/includes/api/calendar-settings.php index ad7bdb7..c20aa26 100644 --- a/modules/family-calendar/includes/api/calendar-settings.php +++ b/modules/family-calendar/includes/api/calendar-settings.php @@ -141,7 +141,7 @@ try { // ─── 5. GESTION DES CONGÉS INDIVIDUELS (MEMBRES) ─── if ($action === 'get_person_leaves') { $personId = (int)($_GET['person_id'] ?? 0); - $stmt = $pdo->prepare("SELECT id, leave_type, allowance, anniversary_date FROM pf_person_leave_meta WHERE person_id = ? ORDER BY leave_type ASC"); + $stmt = $pdo->prepare("SELECT id, leave_type, allowance, method, anniversary_date FROM pf_person_leave_meta WHERE person_id = ? ORDER BY leave_type ASC"); $stmt->execute([$personId]); echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]); exit; diff --git a/modules/family-calendar/includes/api/get-leave-balances.php b/modules/family-calendar/includes/api/get-leave-balances.php index 7200c0c..559890d 100644 --- a/modules/family-calendar/includes/api/get-leave-balances.php +++ b/modules/family-calendar/includes/api/get-leave-balances.php @@ -3,13 +3,22 @@ header('Content-Type: application/json'); require __DIR__ . '/../../../../includes/db.php'; try { + // đŸ”„ LE FIX : On interroge la NOUVELLE table des quotas individuels ! $stmt = $pdo->query(" - SELECT person_id, leave_type, initial_balance, balance_year - FROM pf_leave_balances + SELECT + person_id, + leave_type AS type, + allowance, + method, + anniversary_date AS date + FROM pf_person_leave_meta "); $balances = $stmt->fetchAll(PDO::FETCH_ASSOC); - echo json_encode(['balances' => $balances]); + echo json_encode([ + 'status' => 'success', + 'balances' => $balances + ]); } catch (PDOException $e) { http_response_code(500); echo json_encode([ @@ -17,3 +26,4 @@ try { 'message' => $e->getMessage(), ]); } +?> \ No newline at end of file diff --git a/register.php b/register.php index 8f88974..461fcf6 100644 --- a/register.php +++ b/register.php @@ -38,6 +38,47 @@ function createFamilyDb(PDO $meta, string $db_host, string $db_user, string $db_ return $db_name; } +function generateCalendarWeeks(PDO $familyPdo) { + $monthsFr = [1=>'Janvier', 2=>'FĂ©vrier', 3=>'Mars', 4=>'Avril', 5=>'Mai', 6=>'Juin', 7=>'Juillet', 8=>'AoĂ»t', 9=>'Septembre', 10=>'Octobre', 11=>'Novembre', 12=>'DĂ©cembre']; + + $sql = "REPLACE INTO pf_calendar_weeks + (year, week_iso_year, week_iso_number, week_label, month, month_name, week_start_date, mon_date, tue_date, wed_date, thu_date, fri_date) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + $stmt = $familyPdo->prepare($sql); + + // On commence de l'annĂ©e prĂ©cĂ©dente jusqu'Ă  +10 ans + $currentYear = (int)date('Y'); + $startDate = new DateTime(($currentYear - 1) . '-08-28'); // On s'assure de couvrir la rentrĂ©e scolaire prĂ©cĂ©dente + $endDate = new DateTime(($currentYear + 10) . '-12-31'); + + while ($startDate <= $endDate) { + $mon = clone $startDate; + $tue = clone $startDate; $tue->modify('+1 day'); + $wed = clone $startDate; $wed->modify('+2 days'); + $thu = clone $startDate; $thu->modify('+3 days'); + $fri = clone $startDate; $fri->modify('+4 days'); + + $month = (int)$mon->format('n'); + + $stmt->execute([ + (int)$mon->format('Y'), + (int)$mon->format('o'), + (int)$mon->format('W'), + "Semaine " . (int)$mon->format('W'), + $month, + $monthsFr[$month], + $mon->format('Y-m-d'), + $mon->format('Y-m-d'), + $tue->format('Y-m-d'), + $wed->format('Y-m-d'), + $thu->format('Y-m-d'), + $fri->format('Y-m-d') + ]); + + $startDate->modify('+1 week'); + } +} + // ─── Traitement du formulaire ───────────────────────────────────────────────── if ($_SERVER['REQUEST_METHOD'] === 'POST') { @@ -137,11 +178,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { )->execute([$username, $hash, $display_name, $family_id]); $user_id = (int)$meta_pdo->lastInsertId(); - // đŸ”„ INJECTION ICI : Ajouter le crĂ©ateur comme premier parent de la famille (Couleur bleue) $tenantPdo = new PDO("mysql:host=$db_host;dbname=$db_name;charset=utf8mb4", $db_user, $db_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); $stmtPerson = $tenantPdo->prepare("INSERT INTO pf_people (name, user_id, role, color, is_active) VALUES (?, ?, 'parent', '#0891b2', 1)"); $stmtPerson->execute([$display_name, $user_id]); + generateCalendarWeeks($tenantPdo); + $meta_pdo->commit(); $_SESSION['user'] = [