/**
* family-calendar.js (Version Optimisée - API BDD & Décalage Vendredi)
*/
document.addEventListener("DOMContentLoaded", () => {
const CONGE_TYPES = ["OFF_CAROLE", "EXTRA_OFF_CAROLE"];
const GUARDE_TYPES = ["CENTRE", "AVIS"];
const PEP_TYPES = ["PEP_SICK"];
// 🟢 Liste globale dynamique des personnes (fini les IDs en dur !)
const peopleList = window.CONFIG.PEOPLE || [];
// On filtre pour ne garder que les parents pour la logique principale des congés
const parents = peopleList.filter(
(p) => p.role && p.role.toLowerCase() === "parent",
);
const LEAVES_CONFIG = {
CP: {
startMonth: 8,
defaultBalance: 25,
},
JRA: {
yearlyTotals: {
2024: 10,
2025: 10,
2026: 11,
},
defaultBalance: 10,
toleranceMonths: 2,
maxReport: 2,
},
JA: {}, // Ce tableau est maintenant vide par défaut, on le remplit dynamiquement juste en dessous
};
// 🟢 INITIALISATION DYNAMIQUE DES JOURS D'ANNIVERSAIRE (JA)
parents.forEach((parent) => {
LEAVES_CONFIG.JA[parent.id] = {
startMonth: 4, // Mois par défaut (à faire évoluer en BDD au Niveau 3)
startDay: 29, // Jour par défaut
defaultBalance: 4,
};
});
class FamilyCalendar {
constructor() {
this.planningBody = document.getElementById("planningBody");
this.selectionMenu = document.getElementById("selectionMenu");
this.schoolHolidaysTableBody = document.querySelector(
"#schoolHolidaysTable tbody",
);
this.monthCalendar = document.getElementById("fc-month-calendar");
this.monthSelectionMenu = document.getElementById(
"fc-month-selectionMenu",
);
if (
this.selectionMenu &&
this.selectionMenu.parentElement !== document.body
) {
document.body.appendChild(this.selectionMenu);
}
if (
this.monthSelectionMenu &&
this.monthSelectionMenu.parentElement !== document.body
) {
document.body.appendChild(this.monthSelectionMenu);
}
this.currentMonth = new Date();
this.currentMonth.setDate(1);
this.viewMode = "1month";
this.currentSchoolYearStart = null;
this.modalSelectedYear = null; // Pour la modale des vacances
this.isSelecting = false;
this.selectedCells = [];
this.monthSelectedCells = [];
this._currentBulkInfo = null;
this.dbEvents = [];
this.fixedEvents = []; // Fériés statiques
this.events = [];
this.leaves = [];
this.weeks = [];
this.monthlyLeaveBalances = {
2: { CP: {}, JRA: {}, JA: {} },
3: { CP: {}, JRA: {}, JA: {} },
};
if (!this.planningBody) return;
this.init();
}
getLocalIsoDate(dateObj) {
const y = dateObj.getFullYear();
const m = String(dateObj.getMonth() + 1).padStart(2, "0");
const d = String(dateObj.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}
async init() {
this.setupEventListeners();
const now = new Date();
this.currentSchoolYearStart =
now.getMonth() >= 8 ? now.getFullYear() : now.getFullYear() - 1;
this.modalSelectedYear = this.currentSchoolYearStart;
this.setupModalUI();
this.initSmartSelectors();
await this.refreshAllData();
this.updateSchoolYearLabel();
setTimeout(() => this.scrollToCurrentMonth(), 100);
}
// Modifie dynamiquement le titre de la modale pour y insérer le selecteur d'année
setupModalUI() {
const headerTitle = document.querySelector(
"#modalHolidays .pf-modal-title",
);
if (headerTitle && !document.getElementById("holidayYearSelect")) {
let options = "";
const currentY = new Date().getFullYear();
// On affiche de N-2 à N+3 pour avoir un bel historique/futur
for (let y = currentY - 2; y <= currentY + 3; y++) {
const selected = y === this.currentSchoolYearStart ? "selected" : "";
options += `${y} - ${y + 1} `;
}
// --- NOUVEAU : Récupération de la zone dynamique ---
const zoneText =
window.CONFIG &&
window.CONFIG.ZONE_SCOLAIRE &&
window.CONFIG.ZONE_SCOLAIRE !== "Autre"
? `(Zone ${window.CONFIG.ZONE_SCOLAIRE})`
: "";
// Injection du texte de la zone dans le header
headerTitle.innerHTML = `🏖️ ${window.I18N["fc_modal_holidays_title"]} ${zoneText}
${options}
`;
document
.getElementById("holidayYearSelect")
.addEventListener("change", (e) => {
this.modalSelectedYear = parseInt(e.target.value);
this.renderModalHolidays();
});
}
}
initSmartSelectors() {
const selectMonth = document.getElementById("fc-select-month");
const selectYear = document.getElementById("fc-select-year");
if (!selectMonth || !selectYear) return;
const lang = window.appLang || "fr-FR";
// 1. Remplir les mois en tenant compte de la langue locale
for (let i = 0; i < 12; i++) {
const d = new Date(2000, i, 1);
const monthName = new Intl.DateTimeFormat(lang, {
month: "long",
}).format(d);
selectMonth.add(new Option(monthName, i));
}
// 2. Remplir les années (de l'année actuelle -2 à +5)
const currentY = new Date().getFullYear();
for (let y = currentY - 2; y <= currentY + 5; y++) {
selectYear.add(new Option(y, y));
}
// 3. Écouteurs d'événements pour le rechargement en direct
const handleChange = () => {
const m = parseInt(selectMonth.value);
const y = parseInt(selectYear.value);
this.currentMonth = new Date(y, m, 1);
this.renderMonthCalendar();
};
selectMonth.addEventListener("change", handleChange);
selectYear.addEventListener("change", handleChange);
}
async refreshAllData() {
try {
const weeksData = await this.fetchApi(
`/modules/family-calendar/includes/api/get-calendar-weeks-scolaire.php?school_year_start=${this.currentSchoolYearStart}`,
);
this.weeks = this.processWeeks(weeksData.weeks || []);
const eventsData = await this.fetchApi(
"/modules/family-calendar/includes/api/get-events.php",
);
this.dbEvents = (eventsData.events || []).map((e) => ({
...e,
duration: parseFloat(e.duration),
}));
this.fixedEvents = await this.fetchPublicHolidays();
const leavesData = await this.fetchApi(
"/modules/family-calendar/includes/api/get-leaves.php",
);
this.leaves = leavesData.leaves || [];
const balancesData = await this.fetchApi(
"/modules/family-calendar/includes/api/get-leave-balances.php",
);
this.leaveBalances = balancesData.balances || [];
// --- NOUVEAU : Chargement des correctifs de congés ---
const snapshotsData = await this.fetchApi(
"/modules/family-calendar/includes/api/get-leave-snapshots.php",
);
this.leaveSnapshots = snapshotsData.snapshots || [];
this.events = [...this.dbEvents, ...this.fixedEvents];
this.publicHolidayDates = new Set(
this.fixedEvents
.filter((e) => e.type === "PUBLIC_HOLIDAY")
.map((e) => e.date),
);
this.reprocessAndRender();
this.renderModalHolidays();
} catch (e) {
console.error("Erreur chargement données:", e);
}
}
// Les jours fériés restent hardcodés car ils sont fixes.
async fetchPublicHolidays() {
try {
// Appel à l'API officielle des jours fériés en métropole
const res = await fetch(
"https://calendrier.api.gouv.fr/jours-feries/metropole.json",
);
const holidays = await res.json();
// L'API renvoie un objet : { "2024-01-01": "Jour de l'an", ... }
// On le transforme en tableau compatible avec ton système d'événements
return Object.keys(holidays).map((date, idx) => ({
id: `ph-${idx}`,
date: date,
name: holidays[date], // On garde le nom au cas où tu veuilles l'afficher plus tard
type: "PUBLIC_HOLIDAY",
duration: 1,
}));
} catch (error) {
console.error(
"Erreur lors de la récupération des jours fériés :",
error,
);
return []; // Évite de casser le calendrier si l'API de l'État est indisponible
}
}
// --- RECONSTRUCTION DE LA MODALE VIA LA BDD (Par blocs consécutifs) ---
renderModalHolidays() {
if (!this.schoolHolidaysTableBody) return;
// On utilise bien l'année de la modale, indépendamment du planning de fond
const startDate = `${this.modalSelectedYear}-09-01`;
const endDate = `${this.modalSelectedYear + 1}-08-31`;
const yearHolidays = this.events.filter(
(e) =>
e.type === "VACANCES_SCOLAIRES" &&
e.date >= startDate &&
e.date <= endDate,
);
if (yearHolidays.length === 0) {
this.schoolHolidaysTableBody.innerHTML = `
${tr("fc_err_no_data_gov")}
${tr("btn_import")}
`;
document
.getElementById("btnFetchGovHolidays")
?.addEventListener("click", (e) => {
e.target.innerText = "...";
e.target.disabled = true;
this.fetchAndSaveGovHolidays(this.modalSelectedYear);
});
return;
}
yearHolidays.sort((a, b) => new Date(a.date) - new Date(b.date));
const blocks = [];
let currentBlock = null;
yearHolidays.forEach((e) => {
const d = new Date(e.date + "T00:00:00");
if (!currentBlock) {
currentBlock = { start: d, end: d };
blocks.push(currentBlock);
} else {
const diffDays = Math.round(
(d.getTime() - currentBlock.end.getTime()) / (1000 * 60 * 60 * 24),
);
if (diffDays <= 4) {
currentBlock.end = d;
} else {
currentBlock = { start: d, end: d };
blocks.push(currentBlock);
}
}
});
let html = "";
blocks.forEach((block) => {
const m = block.start.getMonth() + 1;
const durationDays =
Math.round(
(block.end.getTime() - block.start.getTime()) /
(1000 * 60 * 60 * 24),
) + 1;
let name = tr("leg_school_holidays");
if (m === 10 || m === 11) name = tr("vac_toussaint");
else if (m === 12 || m === 1) name = tr("vac_noel");
else if (m === 2 || m === 3) name = tr("vac_hiver");
else if (m === 4 || (m === 5 && durationDays > 6))
name = tr("vac_printemps");
else if (m === 5 && durationDays <= 6) name = tr("vac_ascension");
else if (m === 7 || m === 8) name = tr("vac_ete");
html += `
${name}
${block.start.toLocaleDateString(window.appLang || "fr-FR")}
${block.end.toLocaleDateString(window.appLang || "fr-FR")}
`;
});
this.schoolHolidaysTableBody.innerHTML = html;
}
// --- IMPORTATION DEPUIS L'API & SAUVEGARDE EN BDD ---
async fetchAndSaveGovHolidays(yearStart) {
try {
const yearStr = `${yearStart}-${yearStart + 1}`;
const zone = window.CONFIG?.ZONE_SCOLAIRE || "C";
if (zone === "Autre") {
alert(
"L'importation automatique n'est disponible que pour les zones A, B ou C (France).",
);
const btn = document.getElementById("btnFetchGovHolidays");
if (btn) {
btn.innerText = "Non disponible";
btn.disabled = true;
}
return;
}
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 || [];
// 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 payload = [];
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");
// REGLE METIER : Si le 1er jour est un VENDREDI (jour=5), on décale le début au SAMEDI.
if (curr.getDay() === 5) {
curr.setDate(curr.getDate() + 1);
}
while (curr < end) {
const iso = this.getLocalIsoDate(curr);
payload.push({
date: iso,
type: "VACANCES_SCOLAIRES",
duration: 1,
person: r.description, // ASTUCE: On stocke le nom de la vacance ici !
});
curr.setDate(curr.getDate() + 1);
}
});
if (payload.length > 0) {
// On envoie le gros lot à la base de données via l'API existante
await this.postApi(
"/modules/family-calendar/includes/api/save-events.php",
payload,
);
await this.refreshAllData(); // Recharge tout, ce qui mettra à jour la modale
} else {
alert(
"Aucune donnée trouvée sur l'API du gouvernement pour cette année.",
);
document.getElementById("btnFetchGovHolidays").innerText =
"Réessayer";
document.getElementById("btnFetchGovHolidays").disabled = false;
}
} catch (e) {
console.error("Erreur API", e);
alert("Erreur lors de la connexion à l'API gouvernementale.");
}
}
// ================================================================
// LE RESTE DU CODE (AFFICHAGE) RESTE INCHANGÉ MAIS OPTIMISÉ
// ================================================================
reprocessAndRender() {
this.reprocessEvents();
this.calculateMonthlyBalances();
this.initSummaryControls();
this.renderTable();
this.renderMonthCalendar();
}
reprocessEvents() {
this.weeks.forEach((w) => {
Object.keys(w.totals).forEach((k) => (w.totals[k] = 0));
Object.values(w.dayFlags).forEach((f) => (f.events = []));
this.events.forEach((e) => {
const d = new Date(e.date + "T00:00:00");
if (d >= w.dayDates.mon && d <= w.dayDates.fri) {
const dayKey = Object.keys(w.dayDates).find(
(k) => w.dayDates[k].getTime() === d.getTime(),
);
if (dayKey) w.dayFlags[dayKey].events.push(e);
const dur = parseFloat(e.duration) || 1;
const typeMap = {
OFF_CAROLE: "offCarole",
EXTRA_OFF_CAROLE: "extraOffCarole",
CENTRE: "centre",
AVIS: "avis",
PEP_SICK: "pepSick",
};
if (typeMap[e.type]) w.totals[typeMap[e.type]] += dur;
}
});
this.leaves.forEach((l) => {
const d = new Date(l.leave_date + "T00:00:00");
if (d >= w.dayDates.mon && d <= w.dayDates.fri) {
const dur = parseFloat(l.duration) || 1;
// 🟢 Correspondance dynamique pour le stockage des totaux de la semaine
let prefix = null;
if (parents[0] && parseInt(l.person_id) === parseInt(parents[0].id))
prefix = "alex";
else if (
parents[1] &&
parseInt(l.person_id) === parseInt(parents[1].id)
)
prefix = "laia";
if (prefix) w.totals[`${prefix}${l.leave_type}`] += dur;
}
});
let workingDays = 0;
Object.values(w.dayDates).forEach((d) => {
if (!this.publicHolidayDates.has(this.getLocalIsoDate(d)))
workingDays++;
});
w.totals.presencePep = Math.max(
0,
workingDays -
(w.totals.offCarole + w.totals.extraOffCarole + w.totals.pepSick),
);
});
}
calculateMonthlyBalances() {
const balances = {};
// 🟢 On initialise le tableau des soldes pour chaque parent trouvé
parents.forEach((p) => {
balances[p.id] = { CP: {}, JRA: {}, JA: {} };
});
const ymSet = new Set();
this.weeks.forEach((w) => ymSet.add(w.monthKey));
const ymList = Array.from(ymSet).sort();
const usageByMonth = {};
this.leaves.forEach((l) => {
const pid = l.person_id;
const type = l.leave_type;
const ym = l.leave_date.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);
});
parents.forEach((parent) => {
const pid = parent.id;
["CP", "JRA", "JA"].forEach((type) => {
ymList.forEach((ym) => {
const [currYear, currMonth] = ym.split("-").map(Number);
let cycleStartStr = "";
let initialBalance = 0;
const latestSnapshot = (this.leaveSnapshots || [])
.filter(
(s) =>
s.person_id == pid &&
s.leave_type == type &&
s.snapshot_date.substring(0, 7) <= ym,
)
.sort((a, b) =>
b.snapshot_date.localeCompare(a.snapshot_date),
)[0];
if (latestSnapshot) {
cycleStartStr = latestSnapshot.snapshot_date.substring(0, 7);
initialBalance = parseFloat(latestSnapshot.remaining_balance);
} else {
if (type === "CP") {
const refYear =
currMonth >= LEAVES_CONFIG.CP.startMonth
? currYear
: currYear - 1;
cycleStartStr = `${refYear}-${String(LEAVES_CONFIG.CP.startMonth).padStart(2, "0")}`;
const dbBal = this.leaveBalances.find(
(b) =>
b.person_id == pid &&
b.leave_type == "CP" &&
b.balance_year == refYear,
);
initialBalance = dbBal
? parseFloat(dbBal.initial_balance)
: LEAVES_CONFIG.CP.defaultBalance;
} else if (type === "JRA") {
cycleStartStr = `${currYear}-01`;
initialBalance =
LEAVES_CONFIG.JRA.yearlyTotals[currYear] ||
LEAVES_CONFIG.JRA.defaultBalance;
if (currMonth <= LEAVES_CONFIG.JRA.toleranceMonths) {
const prevYear = currYear - 1;
const prevInitial =
LEAVES_CONFIG.JRA.yearlyTotals[prevYear] ||
LEAVES_CONFIG.JRA.defaultBalance;
let usedPrevYear = 0;
for (let m = 1; m <= 12; m++) {
const mStr = `${prevYear}-${String(m).padStart(2, "0")}`;
usedPrevYear += usageByMonth[pid]?.[type]?.[mStr] || 0;
}
const remainingPrevYear = Math.max(
0,
prevInitial - usedPrevYear,
);
initialBalance += Math.min(
remainingPrevYear,
LEAVES_CONFIG.JRA.maxReport,
);
}
} else if (type === "JA") {
const configJA = LEAVES_CONFIG.JA[pid];
const isPastAnniversary =
currMonth > configJA.startMonth ||
(currMonth === configJA.startMonth &&
configJA.startDay === 1);
const refYear = isPastAnniversary ? currYear : currYear - 1;
cycleStartStr = `${refYear}-${String(configJA.startMonth).padStart(2, "0")}`;
const dbBal = this.leaveBalances.find(
(b) =>
b.person_id == pid &&
b.leave_type == "JA" &&
b.balance_year == refYear,
);
initialBalance = dbBal
? parseFloat(dbBal.initial_balance)
: configJA.defaultBalance;
}
}
let usedBeforeCurrentMonth = 0;
Object.keys(usageByMonth[pid]?.[type] || {}).forEach((usedYm) => {
if (usedYm >= cycleStartStr && usedYm < ym) {
usedBeforeCurrentMonth += usageByMonth[pid][type][usedYm];
}
});
const available = Math.max(
0,
initialBalance - usedBeforeCurrentMonth,
);
const usedInMonth = usageByMonth[pid]?.[type]?.[ym] || 0;
balances[pid][type][ym] = {
availableAtMonthStart: available,
usedInMonth: usedInMonth,
};
});
});
});
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)) : "";
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");
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);
}
const tdW = document.createElement("td");
tdW.className = "col-month col-sticky-sem";
tdW.textContent = w.weekLabel;
tr.appendChild(tdW);
["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.textContent = String(dateObj.getDate()).padStart(2, "0");
td.className = "col-day";
w.dayFlags[d].events.forEach((evt) => {
if (evt.type === "OFF_CAROLE")
td.classList.add("fc-day--off-carole");
if (evt.type === "EXTRA_OFF_CAROLE")
td.classList.add("fc-day--extra-off-carole");
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 === "CENTRE") td.classList.add("fc-day--centre");
if (evt.type === "AVIS") td.classList.add("fc-day--avis");
if (evt.type === "PEP_SICK")
td.innerHTML += `🤒 `;
});
const dayLeaves = this.leaves.filter((l) => l.leave_date === iso);
if (dayLeaves.length) {
let html = ``;
const colors = [
"#0f766e",
"#b45309",
"#047857",
"#4338ca",
"#b91c1c",
];
if (window.CONFIG.PEOPLE && window.CONFIG.PEOPLE.length > 0) {
window.CONFIG.PEOPLE.forEach((person, index) => {
if (
dayLeaves.some(
(l) => parseInt(l.person_id) === parseInt(person.id),
)
) {
const color = colors[index % colors.length];
const initial = person.name.charAt(0).toUpperCase();
html += `${initial} `;
}
});
}
html += `
`;
td.innerHTML += html;
}
tr.appendChild(td);
});
[
"offCarole",
"extraOffCarole",
"centre",
"avis",
"pepSick",
"presencePep",
].forEach((k) => {
const td = document.createElement("td");
td.className = "col-total";
td.textContent = fmt(w.totals[k]);
tr.appendChild(td);
});
if (!processedLeavesCols[w.monthKey]) {
processedLeavesCols[w.monthKey] = true;
const span = monthSpans[w.monthKey];
const ym = w.monthKey;
const renderPersonCols = (pid, prefix) => {
["CP", "JRA", "JA"].forEach((type) => {
const info = this.monthlyLeaveBalances[pid][type][ym];
const tdAv = document.createElement("td");
tdAv.className = `${prefix}-sub ${prefix}-av`;
tdAv.rowSpan = span;
tdAv.textContent = info ? fmt(info.availableAtMonthStart) : "-";
tr.appendChild(tdAv);
const tdUse = document.createElement("td");
tdUse.className = `${prefix}-sub ${prefix}-use`;
tdUse.rowSpan = span;
tdUse.textContent = info ? fmt(info.usedInMonth) : "";
tr.appendChild(tdUse);
});
};
parents.forEach((parent, index) => {
const cssPrefix = index % 2 === 0 ? "col-alex" : "col-laia";
renderPersonCols(parent.id, cssPrefix);
});
}
this.planningBody.appendChild(tr);
});
}
// --- Auto-scroll vers le mois en cours ---
scrollToCurrentMonth() {
const wrapper = document.getElementById("planningTable-wrapper");
const thead = document.querySelector("#planningTable thead");
if (!wrapper || !thead) return;
const now = new Date();
// Construit la clé au format "YYYY-MM"
const currentYm = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
// Cherche la TOUTE PREMIÈRE ligne qui correspond à ce mois
const targetRow = document.querySelector(
`#planningTable tbody tr[data-month="${currentYm}"]`,
);
if (targetRow) {
// On donne 50ms au navigateur pour finir son rendu graphique avant de calculer les hauteurs
setTimeout(() => {
const scrollPos = targetRow.offsetTop - thead.offsetHeight;
wrapper.scrollTo({
top: scrollPos > 0 ? scrollPos : 0,
behavior: "smooth",
});
}, 50);
}
}
renderMonthCalendar() {
if (!this.monthCalendar) return;
this.monthCalendar.innerHTML = "";
const y = this.currentMonth.getFullYear();
const m = this.currentMonth.getMonth();
const selectMonth = document.getElementById("fc-select-month");
const selectYear = document.getElementById("fc-select-year");
// On masque définitivement le suffixe encombrant
const suffixEl = document.getElementById("fc-multi-month-suffix");
if (selectMonth && selectYear) {
selectMonth.value = m;
selectYear.value = y;
if (suffixEl) suffixEl.style.display = "none";
if (this.viewMode === "3months") {
this.renderThreeMonthsView();
} else if (this.viewMode === "2months") {
this.renderTwoMonthsView();
} else {
this.monthCalendar.innerHTML = this.generateMonthHTML(y, m);
}
}
this.renderMonthBalances();
this.syncSummaryWithMonth();
}
renderTwoMonthsView() {
const y = this.currentMonth.getFullYear();
const m = this.currentMonth.getMonth();
const nextDate = new Date(y, m + 1, 1);
const lang = window.I18N_LANG || "fr-FR";
let html = ``;
html += `
${new Intl.DateTimeFormat(lang, { month: "long" }).format(this.currentMonth)}
${this.generateMonthHTML(y, m)}
`;
html += `
${new Intl.DateTimeFormat(lang, { month: "long" }).format(nextDate)}
${this.generateMonthHTML(nextDate.getFullYear(), nextDate.getMonth())}
`;
html += `
`;
this.monthCalendar.innerHTML = html;
}
renderThreeMonthsView() {
const y = this.currentMonth.getFullYear();
const m = this.currentMonth.getMonth();
const d1 = this.currentMonth;
const d2 = new Date(y, m + 1, 1);
const d3 = new Date(y, m + 2, 1);
let html = ``;
[d1, d2, d3].forEach((d) => {
html += `
${new Intl.DateTimeFormat("fr-FR", { month: "long" }).format(d)}
${this.generateMonthHTML(d.getFullYear(), d.getMonth())}
`;
});
html += `
`;
this.monthCalendar.innerHTML = html;
}
renderMonthBalances() {
const container = document.getElementById("fc-month-balances");
if (!container) return;
const monthsToDisplay = [];
const y = this.currentMonth.getFullYear();
const m = this.currentMonth.getMonth();
let numMonths =
this.viewMode === "2months" ? 2 : this.viewMode === "3months" ? 3 : 1;
for (let i = 0; i < numMonths; i++) {
const d = new Date(y, m + i, 1);
monthsToDisplay.push(
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`,
);
}
container.style.display = "flex";
let html = "";
parents.forEach((person) => {
html += `
${person.name.toUpperCase()}
`;
["CP", "JRA", "JA"].forEach((type) => {
const startInfo =
this.monthlyLeaveBalances[person.id]?.[type]?.[monthsToDisplay[0]];
const startBal = startInfo ? startInfo.availableAtMonthStart : 0;
let totalUsed = 0;
monthsToDisplay.forEach((ym) => {
const info = this.monthlyLeaveBalances[person.id]?.[type]?.[ym];
if (info) totalUsed += info.usedInMonth;
});
const endBal = Math.max(0, startBal - totalUsed);
const fmt = (n) =>
n > 0 ? (Number.isInteger(n) ? n : n.toFixed(1)) : "0";
const usedHtml =
totalUsed > 0
? `
-${fmt(totalUsed)} `
: "";
// --- LOGIQUE D'ALERTE FIRE 🔥 ---
let alertHtml = "";
const currentYm = monthsToDisplay[0];
const [cYear, cMonth] = currentYm.split("-").map(Number);
if (endBal > 0) {
if (type === "CP" && cMonth >= 6 && cMonth <= 7) {
// CP : Alerte en Juin et Juillet uniquement
let msg = (
window.I18N["fc_alert_burn_days"] || "Perte: %s j avant le %s"
)
.replace("%s", fmt(endBal))
.replace("%s", "31/07");
alertHtml = `
🔥
`;
} else if (type === "JRA" && (cMonth === 1 || cMonth === 2)) {
// JRA : Alerte Janvier/Février si > 2 jours
if (endBal > 2) {
let msg =
window.I18N["fc_alert_burn_jra"] || "Seuls 2j reportables";
alertHtml = `
🔥 ${fmt(endBal - 2)}
`;
}
} else if (type === "JA") {
const configJA = LEAVES_CONFIG.JA[person.id];
let limitMonth = configJA.startMonth - 1 || 12;
if (cMonth === limitMonth || cMonth === configJA.startMonth) {
let msg = (window.I18N["fc_alert_burn_days"] || "Perte: %s j")
.replace("%s", fmt(endBal))
.replace("%s", window.I18N["ANNIV"] || "Anniv");
alertHtml = `
🔥
`;
}
}
}
html += `
${type}
${fmt(endBal)}
${usedHtml}
${alertHtml}
`;
});
html += `
`;
});
container.innerHTML = html;
}
syncSummaryWithMonth() {
const typeSelect = document.getElementById("summType");
const valueSelect = document.getElementById("summValue");
if (typeSelect && valueSelect) {
// Le récap se synchronise avec le PREMIER mois affiché de la période
const ym = `${this.currentMonth.getFullYear()}-${String(this.currentMonth.getMonth() + 1).padStart(2, "0")}`;
if (typeSelect.value !== "month") {
typeSelect.value = "month";
typeSelect.dispatchEvent(new Event("change"));
}
valueSelect.value = ym;
this.updateGlobalSummary();
}
}
generateMonthHTML(year, month) {
let html = ``;
["L", "M", "M", "J", "V"].forEach((d) => (html += `${d} `));
html += ` `;
const daysInMonth = new Date(year, month + 1, 0).getDate();
let currentRenderedCols = 0;
const firstDay = new Date(year, month, 1);
let startDay = (firstDay.getDay() + 6) % 7;
if (startDay < 5) {
for (let i = 0; i < startDay; i++) {
html += ` `;
currentRenderedCols++;
}
}
for (let dayCounter = 1; dayCounter <= daysInMonth; dayCounter++) {
const dateObj = new Date(year, month, dayCounter);
const dayOfWeek = dateObj.getDay();
if (dayOfWeek === 0 || dayOfWeek === 6) continue;
if (currentRenderedCols === 5) {
html += ` `;
currentRenderedCols = 0;
}
const iso = this.getLocalIsoDate(dateObj);
const todayIso = this.getLocalIsoDate(new Date()); // Date du jour
let cls = "fc-month-day";
if (iso === todayIso) cls += " fc-day--today"; // On applique la classe !
const dayEvts = this.events.filter((e) => e.date === iso);
if (dayEvts.some((e) => e.type === "VACANCES_SCOLAIRES"))
cls += " fc-day--school-holiday";
if (dayEvts.some((e) => e.type === "PUBLIC_HOLIDAY"))
cls += " fc-day--public-holiday";
if (dayEvts.some((e) => e.type === "OFF_CAROLE"))
cls += " fc-day--off-carole";
if (dayEvts.some((e) => e.type === "EXTRA_OFF_CAROLE"))
cls += " fc-day--extra-off-carole";
if (dayEvts.some((e) => ["CENTRE", "AVIS"].includes(e.type))) {
cls += " fc-day--has-guard";
if (dayEvts.some((e) => e.type === "CENTRE"))
cls += " fc-day--centre";
if (dayEvts.some((e) => e.type === "AVIS")) cls += " fc-day--avis";
}
let content = `${dayCounter} `;
if (dayEvts.some((e) => e.type === "PEP_SICK"))
content += `
🤒 `;
const dayLeaves = this.leaves.filter((l) => l.leave_date === iso);
if (dayLeaves.length) {
content += `
`;
parents.forEach((parent, index) => {
if (
dayLeaves.some(
(l) => parseInt(l.person_id) === parseInt(parent.id),
)
) {
const color = index % 2 === 0 ? "#0f766e" : "#b45309";
const initial = parent.name.charAt(0).toUpperCase();
content += `${initial} `;
}
});
content += `
`;
}
content += `
`;
html += `${content} `;
currentRenderedCols++;
}
while (currentRenderedCols < 5 && currentRenderedCols > 0) {
html += ` `;
currentRenderedCols++;
}
html += `
`;
return html;
}
initSummaryControls() {
const typeSelect = document.getElementById("summType");
const valueSelect = document.getElementById("summValue");
if (!typeSelect || !valueSelect) return;
const years = new Set();
const months = new Set();
this.weeks.forEach((w) => {
Object.values(w.dayDates).forEach((d) => {
years.add(d.getFullYear());
const mKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
months.add(mKey);
});
});
const sortedYears = Array.from(years).sort();
const sortedMonths = Array.from(months).sort();
const populateValues = () => {
const currentType = typeSelect.value;
const previousValue = valueSelect.value;
valueSelect.innerHTML = "";
if (currentType === "year") {
sortedYears.forEach((y) => {
const opt = document.createElement("option");
opt.value = y;
opt.textContent = y;
valueSelect.appendChild(opt);
});
if (sortedYears.includes(parseInt(previousValue))) {
valueSelect.value = previousValue;
} else {
const currentYear = new Date().getFullYear();
if (sortedYears.includes(currentYear))
valueSelect.value = currentYear;
}
} else {
sortedMonths.forEach((m) => {
const [y, mo] = m.split("-");
const dateObj = new Date(y, mo - 1, 1);
const label = new Intl.DateTimeFormat("fr-FR", {
month: "long",
year: "numeric",
}).format(dateObj);
const opt = document.createElement("option");
opt.value = m;
opt.textContent = label;
valueSelect.appendChild(opt);
});
const now = new Date();
const nowIso = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
if (sortedMonths.includes(nowIso)) {
valueSelect.value = nowIso;
}
}
this.updateGlobalSummary();
};
if (!this.summaryListenersAttached) {
typeSelect.addEventListener("change", populateValues);
valueSelect.addEventListener("change", () =>
this.updateGlobalSummary(),
);
this.summaryListenersAttached = true;
}
populateValues();
}
updateGlobalSummary() {
const div = document.getElementById("globalSummary");
const typeSelect = document.getElementById("summType");
const valueSelect = document.getElementById("summValue");
if (!div || (typeSelect && valueSelect && !valueSelect.value)) return;
const filterType = typeSelect ? typeSelect.value : "year";
const filterValue = valueSelect
? valueSelect.value
: new Date().getFullYear().toString();
const stats = { off: 0, extra: 0, sick: 0, pep: 0, totalWorking: 0 };
this.weeks.forEach((w) => {
Object.entries(w.dayDates).forEach(([dayName, dateObj]) => {
let match = false;
if (filterType === "year") {
if (dateObj.getFullYear().toString() === filterValue) match = true;
} else {
const isoMonth = this.getLocalIsoDate(dateObj).slice(0, 7);
if (isoMonth === filterValue) match = true;
}
if (match) {
const dayEvents = w.dayFlags[dayName].events;
dayEvents.forEach((e) => {
const dur = parseFloat(e.duration) || 1;
if (e.type === "OFF_CAROLE") stats.off += dur;
if (e.type === "EXTRA_OFF_CAROLE") stats.extra += dur;
if (e.type === "PEP_SICK") stats.sick += dur;
});
const dateIso = this.getLocalIsoDate(dateObj);
if (!this.publicHolidayDates.has(dateIso)) {
stats.totalWorking++;
let dayAbsence = 0;
dayEvents.forEach((e) => {
if (
["OFF_CAROLE", "EXTRA_OFF_CAROLE", "PEP_SICK"].includes(
e.type,
)
) {
dayAbsence += parseFloat(e.duration) || 1;
}
});
stats.pep += Math.max(0, 1 - dayAbsence);
}
}
});
});
div.innerHTML = `
${tr("leg_off_carole")} ${parseFloat(stats.off.toFixed(1))} ${tr("fc_unit_days")}
${tr("leg_extra_off")} ${parseFloat(stats.extra.toFixed(1))} ${tr("fc_unit_days")}
${tr("leg_pep_sick")} ${parseFloat(stats.sick.toFixed(1))} ${tr("fc_unit_days")}
${tr("leg_presence")} ${parseFloat(stats.pep.toFixed(1))} ${tr("fc_unit_days")}
`;
}
updateSchoolYearLabel() {
const lbl = document.getElementById("fc-current-school-year-label");
if (lbl)
lbl.textContent = `${this.currentSchoolYearStart} – ${this.currentSchoolYearStart + 1}`;
}
processWeeks(rawWeeks) {
return rawWeeks.map((w) => ({
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,
dayDates: {
mon: new Date(w.mon_date + "T00:00:00"),
tue: new Date(w.tue_date + "T00:00:00"),
wed: new Date(w.wed_date + "T00:00:00"),
thu: new Date(w.thu_date + "T00:00:00"),
fri: new Date(w.fri_date + "T00:00:00"),
},
dayFlags: {
mon: { events: [] },
tue: { events: [] },
wed: { events: [] },
thu: { events: [] },
fri: { events: [] },
},
totals: {
offCarole: 0,
extraOffCarole: 0,
centre: 0,
avis: 0,
pepSick: 0,
presencePep: 0,
alexCP: 0,
alexJRA: 0,
alexJA: 0,
laiaCP: 0,
laiaJRA: 0,
laiaJA: 0,
},
}));
}
async fetchApi(url) {
return pachaFetch(url);
}
async postApi(url, data) {
return pachaFetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
}
async changeSchoolYear(delta) {
this.currentSchoolYearStart += delta;
this.updateSchoolYearLabel();
await this.refreshAllData();
}
setupEventListeners() {
if (this.planningBody) {
// Souris (PC)
this.planningBody.addEventListener("mousedown", (e) =>
this.handleMouseDown(e),
);
document.addEventListener("mousemove", (e) => this.handleMouseMove(e));
document.addEventListener("mouseup", (e) => this.handleMouseUp(e));
// Tactile (Mobile/Tablette)
this.planningBody.addEventListener(
"touchstart",
(e) => this.handleTouchStart(e),
{ passive: false },
);
document.addEventListener("touchmove", (e) => this.handleTouchMove(e), {
passive: false,
});
document.addEventListener("touchend", (e) => this.handleTouchEnd(e));
}
const scrollWrapper = document.getElementById("planningTable-wrapper");
if (scrollWrapper) {
scrollWrapper.addEventListener("scroll", async () => {
// Bloquer si on est déjà en train de charger
if (this._isAutoLoading) return;
// Détection du bas (On descend dans le temps : passage à l'année suivante)
// On met une tolérance de 5px pour les calculs de pixels décimaux
if (
scrollWrapper.scrollTop + scrollWrapper.clientHeight >=
scrollWrapper.scrollHeight - 5
) {
this._isAutoLoading = true;
await this.changeSchoolYear(1);
// On replace le scroll tout en haut pour la continuité visuelle
scrollWrapper.scrollTop = 5;
setTimeout(() => (this._isAutoLoading = false), 500);
}
// Détection du haut (On remonte le temps : passage à l'année précédente)
else if (scrollWrapper.scrollTop === 0) {
this._isAutoLoading = true;
await this.changeSchoolYear(-1);
// On replace le scroll tout en bas pour la continuité visuelle
scrollWrapper.scrollTop =
scrollWrapper.scrollHeight - scrollWrapper.clientHeight - 5;
setTimeout(() => (this._isAutoLoading = false), 500);
}
});
}
// --- GESTION MODALE VACANCES SCOLAIRES ---
const btnOpen = document.getElementById("btnOpenHolidays");
const btnClose = document.getElementById("btnCloseHolidays");
const modal = document.getElementById("modalHolidays");
if (btnOpen && modal) {
btnOpen.addEventListener("click", () => {
modal.classList.add("open"); // Utilisation propre de la classe CSS
document.body.classList.add("no-scroll");
const yearSelect = document.getElementById("holidayYearSelect");
if (yearSelect) yearSelect.value = this.currentSchoolYearStart;
this.modalSelectedYear = this.currentSchoolYearStart;
this.renderModalHolidays();
});
}
if (btnClose && modal) {
btnClose.addEventListener("click", () => {
modal.classList.remove("open");
document.body.classList.remove("no-scroll");
});
}
if (modal) {
modal.addEventListener("click", (e) => {
if (e.target === modal) {
modal.classList.remove("open");
document.body.classList.remove("no-scroll");
}
});
}
// --- GESTION MODALE CORRECTIF DES SOLDES (SNAPSHOTS) ---
const btnSnap = document.getElementById("btnOpenSnapshotModal");
const modalSnap = document.getElementById("modalSnapshot");
const btnCloseSnap = document.getElementById("btnCloseSnapshot");
const formSnap = document.getElementById("formSnapshot");
if (btnSnap && modalSnap) {
btnSnap.addEventListener("click", () => {
modalSnap.classList.add("open"); // On ouvre la modale correctement
document.body.classList.add("no-scroll");
// Pré-remplir la date avec le 1er jour du mois en cours
const today = new Date();
const y = today.getFullYear();
const m = String(today.getMonth() + 1).padStart(2, "0");
const snapDateInput = document.getElementById("snapDate");
if (snapDateInput) snapDateInput.value = `${y}-${m}-01`;
});
}
if (btnCloseSnap && modalSnap) {
btnCloseSnap.addEventListener("click", () => {
modalSnap.classList.remove("open");
document.body.classList.remove("no-scroll");
});
}
if (modalSnap) {
modalSnap.addEventListener("click", (e) => {
if (e.target === modalSnap) {
modalSnap.classList.remove("open");
document.body.classList.remove("no-scroll");
}
});
}
if (formSnap) {
formSnap.addEventListener("submit", async (e) => {
e.preventDefault();
const payload = {
person_id: document.getElementById("snapPerson").value,
leave_type: document.getElementById("snapType").value,
snapshot_date: document.getElementById("snapDate").value,
remaining_balance: document.getElementById("snapBalance").value,
};
try {
await this.postApi(
"/modules/family-calendar/includes/api/save-leave-snapshot.php",
payload,
);
modalSnap.classList.remove("open"); // Fermeture propre
document.body.classList.remove("no-scroll");
formSnap.reset();
await this.refreshAllData();
} catch (error) {
alert("Erreur lors de la sauvegarde du correctif.");
}
});
}
// --- GESTION DU RESTE DE L'INTERFACE ---
if (this.monthCalendar) {
this.monthCalendar.addEventListener("click", (e) => {
const td = e.target.closest("td[data-date]");
if (td && td.dataset.date) {
this.monthSelectedCells = [td];
this.showMenu(e.pageX, e.pageY, [td.dataset.date], true);
}
});
}
document.addEventListener("click", (e) => this.closeMenusIfOutside(e));
const handleMenu = (e) => {
const btn = e.target.closest("button");
if (btn && btn.dataset.action) this.handleMenuAction(btn.dataset);
};
if (this.selectionMenu)
this.selectionMenu.addEventListener("click", handleMenu);
if (this.monthSelectionMenu)
this.monthSelectionMenu.addEventListener("click", handleMenu);
document
.getElementById("fc-prev-month")
?.addEventListener("click", () => {
this.currentMonth.setMonth(this.currentMonth.getMonth() - 1);
this.renderMonthCalendar();
});
document
.getElementById("fc-next-month")
?.addEventListener("click", () => {
this.currentMonth.setMonth(this.currentMonth.getMonth() + 1);
this.renderMonthCalendar();
});
// --- BOUTON AUJOURD'HUI ---
document.getElementById("fc-today-btn")?.addEventListener("click", () => {
this.currentMonth = new Date();
this.currentMonth.setDate(1); // Force au 1er du mois pour éviter les bugs de fins de mois
this.renderMonthCalendar();
});
// --- GESTION DU SWIPE MENSUEL (MOBILE) ---
if (this.monthCalendar) {
let touchStartX = 0;
let touchStartY = 0;
// On écoute le début du toucher
this.monthCalendar.addEventListener(
"touchstart",
(e) => {
touchStartX = e.changedTouches[0].screenX;
touchStartY = e.changedTouches[0].screenY;
},
{ passive: true },
);
// On écoute la fin du toucher
this.monthCalendar.addEventListener(
"touchend",
(e) => {
const touchEndX = e.changedTouches[0].screenX;
const touchEndY = e.changedTouches[0].screenY;
const deltaX = touchEndX - touchStartX;
const deltaY = touchEndY - touchStartY;
// Détection d'un swipe horizontal (on exclut les swipes verticaux de défilement)
// Seuil de 50px pour éviter les déclenchements par erreur
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 50) {
const numMonths =
this.viewMode === "2months"
? 2
: this.viewMode === "3months"
? 3
: 1;
if (deltaX < 0) {
// Swipe vers la GAUCHE = Aller dans le FUTUR (Mois suivants)
this.currentMonth.setMonth(
this.currentMonth.getMonth() + numMonths,
);
} else {
// Swipe vers la DROITE = Revenir dans le PASSÉ (Mois précédents)
this.currentMonth.setMonth(
this.currentMonth.getMonth() - numMonths,
);
}
this.renderMonthCalendar(); // Rafraîchit l'UI
}
},
{ passive: true },
);
}
document
.getElementById("fc-prev-school-year")
?.addEventListener("click", () => this.changeSchoolYear(-1));
document
.getElementById("fc-next-school-year")
?.addEventListener("click", () => this.changeSchoolYear(1));
document.querySelectorAll(".fc-view-button").forEach((btn) => {
btn.addEventListener("click", (e) => {
document
.querySelectorAll(".fc-view-button")
.forEach((b) => b.classList.remove("fc-view-button--active"));
e.target.classList.add("fc-view-button--active");
this.viewMode = e.target.dataset.view;
this.renderMonthCalendar();
});
});
}
handleMouseDown(e) {
const td = e.target.closest("#planningTable td[data-date]");
if (!td) return;
e.preventDefault();
this.clearSelection();
this.isSelecting = true;
this.selectCell(td);
}
handleMouseMove(e) {
if (!this.isSelecting) return;
const td = e.target.closest("#planningTable td[data-date]");
if (td && !this.selectedCells.includes(td)) this.selectCell(td);
}
handleMouseUp(e) {
if (!this.isSelecting) return;
const td = e.target.closest("#planningTable td[data-date]");
if (td && !this.selectedCells.includes(td)) this.selectCell(td);
this.isSelecting = false;
if (!this.selectedCells.length) return;
const dates = this.selectedCells.map((c) => c.dataset.date);
this.showMenu(e.pageX, e.pageY, dates, false);
}
// --- GESTION DU TACTILE (Swipe pour sélectionner) ---
handleTouchStart(e) {
const td = e.target.closest("#planningTable td[data-date]");
if (!td) return;
// Si l'utilisateur touche une case, on bloque le scroll de la page pour le laisser sélectionner
e.preventDefault();
this.clearSelection();
this.isSelecting = true;
this.selectCell(td);
}
handleTouchMove(e) {
if (!this.isSelecting) return;
e.preventDefault(); // Bloque le défilement de l'écran pendant qu'on glisse le doigt
const touch = e.touches[0];
// elementFromPoint permet de savoir sur quelle case se trouve le doigt actuellement
const target = document.elementFromPoint(touch.clientX, touch.clientY);
if (!target) return;
const td = target.closest("#planningTable td[data-date]");
if (td && !this.selectedCells.includes(td)) this.selectCell(td);
}
handleTouchEnd(e) {
if (!this.isSelecting) return;
this.isSelecting = false;
if (!this.selectedCells.length) return;
const dates = this.selectedCells.map((c) => c.dataset.date);
// Sur mobile, on s'en fiche de X et Y car le menu s'affiche en bas de l'écran
this.showMenu(0, 0, dates, false);
}
selectCell(cell) {
cell.classList.add("fc-day--selected");
this.selectedCells.push(cell);
}
clearSelection() {
this.selectedCells.forEach((c) => c.classList.remove("fc-day--selected"));
this.selectedCells = [];
if (this.selectionMenu) this.selectionMenu.style.display = "none";
if (this.monthSelectionMenu)
this.monthSelectionMenu.style.display = "none";
}
closeMenusIfOutside(e) {
if (
!this.selectionMenu?.contains(e.target) &&
!this.monthSelectionMenu?.contains(e.target) &&
!this.menuJustOpened
) {
this.clearSelection();
}
}
showMenu(x, y, dates, isMonthView) {
const menu = isMonthView ? this.monthSelectionMenu : this.selectionMenu;
if (!menu) return;
this._currentBulkInfo = { dates };
// --- 🔍 DÉTECTION DES ÉVÉNEMENTS & CONGÉS EXISTANTS ---
const activeEvents = new Set();
this.events.forEach((e) => {
if (dates.includes(e.date)) activeEvents.add(e.type);
});
const activeLeaves = {};
parents.forEach((p) => (activeLeaves[p.id] = new Set()));
this.leaves.forEach((l) => {
if (dates.includes(l.leave_date)) {
if (activeLeaves[l.person_id])
activeLeaves[l.person_id].add(l.leave_type);
}
});
// Petites fonctions utilitaires pour gérer l'UI du bouton
const getBtnClass = (type, personId = null) => {
let isActive = personId
? activeLeaves[personId] && activeLeaves[personId].has(type)
: activeEvents.has(type);
return isActive ? "fc-menu-btn--active" : "";
};
const getBtnIcon = (type, personId = null) => {
let isActive = personId
? activeLeaves[personId] && activeLeaves[personId].has(type)
: activeEvents.has(type);
return isActive ? "✓ " : "";
};
// Gestion de la date affichée (Singulier/Pluriel)
const dateLabel =
dates.length > 1
? `${dates.length} ${tr("fc_unit_days")}`
: new Date(dates[0]).toLocaleDateString(window.I18N_LANG || "fr-FR");
// Icône SVG Poubelle
const trashSvg = ` `;
const buildHeader = (title, action, cat) => `
`;
let html = ``;
// 2. Section Carole
html += ``;
// 3. Section Garde
html += ``;
// 4. Section Pep
html += ``;
// 5. Section Parents (Congés)
html += ``;
menu.innerHTML = html;
// Positionnement (Gestion des bords d'écran)
const menuWidth = 240;
let left = x + 10;
if (left + menuWidth > window.innerWidth) left = x - menuWidth - 10;
menu.style.left = `${left}px`;
menu.style.top = `${y + 10}px`;
menu.style.display = "block";
this.menuJustOpened = true;
setTimeout(() => (this.menuJustOpened = false), 100);
}
async handleMenuAction(dataset) {
if (this.selectionMenu) this.selectionMenu.style.display = "none";
if (this.monthSelectionMenu)
this.monthSelectionMenu.style.display = "none";
const { action, type, pid, cat } = dataset;
const dates = this._currentBulkInfo.dates;
try {
if (action === "add") {
let typesToClear = [];
if (["OFF_CAROLE", "EXTRA_OFF_CAROLE"].includes(type))
typesToClear = CONGE_TYPES;
if (["CENTRE", "AVIS"].includes(type)) typesToClear = GUARDE_TYPES;
if (type === "PEP_SICK") typesToClear = PEP_TYPES;
if (typesToClear.length) {
await this.postApi(
"/modules/family-calendar/includes/api/manage-event.php",
{ action: "bulk_delete_day_types", dates, types: typesToClear },
);
}
const payload = dates.map((d) => ({
date: d,
type: type,
duration: 1,
person: "Carole",
}));
await this.postApi(
"/modules/family-calendar/includes/api/save-events.php",
payload,
);
} else if (action === "clear-type") {
let typesToClear = [];
if (cat === "CONGE") typesToClear = CONGE_TYPES;
if (cat === "GARDE") typesToClear = GUARDE_TYPES;
if (cat === "PEP") typesToClear = PEP_TYPES;
await this.postApi(
"/modules/family-calendar/includes/api/manage-event.php",
{ action: "bulk_delete_day_types", dates, types: typesToClear },
);
} else if (action === "add-leave") {
await this.postApi(
"/modules/family-calendar/includes/api/manage-leaf.php",
{
action: "bulk_delete_day_person",
dates,
person_id: parseInt(pid),
},
);
const payload = dates.map((d) => ({
date: d,
person_id: parseInt(pid), // On s'assure que c'est bien un format numérique
leave_type: type,
duration: 1,
}));
await this.postApi(
"/modules/family-calendar/includes/api/save-leaves.php",
payload,
);
} else if (action === "clear-leaves-person") {
await this.postApi(
"/modules/family-calendar/includes/api/manage-leaf.php",
{
action: "bulk_delete_day_person",
dates,
person_id: parseInt(pid),
},
);
} else if (action === "clear-leaves") {
if (window.CONFIG.PEOPLE && window.CONFIG.PEOPLE.length > 0) {
for (const person of window.CONFIG.PEOPLE) {
await this.postApi(
"/modules/family-calendar/includes/api/manage-leaf.php",
{
action: "bulk_delete_day_person",
dates,
person_id: person.id,
},
);
}
}
}
await this.refreshAllData();
} catch (e) {
alert("Erreur action: " + e.message);
}
}
}
new FamilyCalendar();
});