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) ";
+
+ try {
+ $pdo = new PDO("mysql:host=$db_host;dbname=$dbName;charset=utf8mb4", $db_user, $db_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
+
+ // On utilise REPLACE INTO pour Ă©craser proprement si certaines semaines existent dĂ©jĂ
+ $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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+ $insertStmt = $pdo->prepare($sql);
+
+ // On génÚre de l'été 2023 jusqu'à fin 2030 !
+ $startDate = new DateTime('2023-08-28'); // Un lundi
+ $endDate = new DateTime('2030-12-31');
+ $count = 0;
+
+ 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');
+
+ $year = (int)$mon->format('Y');
+ $month = (int)$mon->format('n');
+ $weekIsoYear = (int)$mon->format('o');
+ $weekIsoNumber = (int)$mon->format('W');
+ $weekLabel = "Semaine " . $weekIsoNumber;
+ $monthName = $monthsFr[$month];
+
+ $insertStmt->execute([
+ $year,
+ $weekIsoYear,
+ $weekIsoNumber,
+ $weekLabel,
+ $month,
+ $monthName,
+ $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');
+ $count++;
+ }
+ echo "â
$count semaines générées (de 2023 à 2030) ! ";
+ } catch (\PDOException $e) {
+ echo "â Erreur : " . $e->getMessage() . " ";
+ }
+ echo " ";
+ }
+ 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() {
${tr("btn_save_rights") || "Enregistrer"}
`;
- }
- // === 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'] = [