/** * family-calendar.js (Version Multi-tenant 100% Purifiée) * 100% Dynamique - Zero nom hardcodé. */ // ============================================================================ // 1. VARIABLES GLOBALES & LOGIQUE DE CONFIGURATION (SETTINGS) // ============================================================================ let calGlobalData = null; let currentSelectedMemberId = null; let localCareModes = []; async function openCalendarSettings() { try { const res = await pachaFetch( "/modules/family-calendar/includes/api/calendar-settings.php?action=get_all", ); if (!res.success) throw new Error(res.error); calGlobalData = res.data; const zoneInput = document.getElementById("setZoneScolaire"); if (zoneInput) zoneInput.value = calGlobalData.foyer.zone_scolaire; localCareModes = calGlobalData.foyer.care_modes || []; renderCareModeTags(); const selectMember = document.getElementById("selectCalMember"); if (selectMember) { selectMember.innerHTML = calGlobalData.people .map((p) => { let roleDisplay = tr("fc_role_unknown") || "Inconnu"; if (p.role === "parent") roleDisplay = "👨‍👩‍👦 " + (tr("fc_role_adult") || "Adulte"); else if (p.role === "child" || p.role === "enfant") roleDisplay = "👶 " + (tr("fc_role_child") || "Enfant"); else if (p.role === "helper") roleDisplay = "💼 " + (tr("fc_role_helper") || "Intervenant"); return ``; }) .join(""); } if (typeof populateCalendarSettings === "function") { populateCalendarSettings(calGlobalData.calendar_settings); } switchCalendarTab("foyer"); const modalSettings = document.getElementById("modalCalendarSettings"); if (modalSettings) { modalSettings.classList.add("open"); document.body.classList.add("no-scroll"); } } catch (err) { showToast((tr("fc_error") || "Erreur : ") + err.message, "error"); } } function closeCalendarSettings() { document.getElementById("modalCalendarSettings").classList.remove("open"); document.body.classList.remove("no-scroll"); } function switchCalendarTab(tabId) { document .querySelectorAll(".bs-tab-btn") .forEach((btn) => btn.classList.remove("active")); document .querySelectorAll(".cal-settings-pane") .forEach((pane) => (pane.style.display = "none")); document.getElementById(`tab-btn-${tabId}`).classList.add("active"); document.getElementById(`cal-pane-${tabId}`).style.display = "block"; if (tabId === "foyer") { loadLeaveCatalog(); } else if (tabId === "membres") { if (document.getElementById("selectCalMember").value) { loadMemberConfigView(); } } } function renderCareModeTags() { const container = document.getElementById("careModesContainer"); if (!container) return; container.innerHTML = localCareModes .map( (mode, i) => ` ${mode} × `, ) .join(""); } function addCareModeTag() { const input = document.getElementById("inputNewCareMode"); if (!input) return; const val = input.value.trim(); if (val && !localCareModes.includes(val)) { localCareModes.push(val); renderCareModeTags(); input.value = ""; } } function removeCareModeTag(index) { localCareModes.splice(index, 1); renderCareModeTags(); } async function submitCalFoyer(e) { e.preventDefault(); try { const formData = new FormData(); formData.append("action", "save_foyer"); formData.append( "zone_scolaire", document.getElementById("setZoneScolaire").value, ); formData.append("care_modes", JSON.stringify(localCareModes)); const res = await pachaFetch( "/modules/family-calendar/includes/api/calendar-settings.php", { method: "POST", body: formData }, ); if (!res.success) throw new Error(res.error); if (window.showToast) showToast( tr("fc_settings_updated") || "Configuration enregistrée !", "success", ); setTimeout(() => window.location.reload(), 800); } catch (err) { showToast((tr("fc_error") || "Erreur : ") + err.message, "error"); } } window.submitChildCareModes = async function () { if (!window.currentSelectedMemberId) return; const checkboxes = document.querySelectorAll(".js-care-mode-cb:checked"); const selectedModes = Array.from(checkboxes).map((cb) => cb.value); try { const formData = new FormData(); formData.append("action", "save_child_care_modes"); formData.append("person_id", window.currentSelectedMemberId); formData.append("care_modes", JSON.stringify(selectedModes)); const res = await pachaFetch( "/modules/family-calendar/includes/api/calendar-settings.php", { method: "POST", body: formData }, ); if (!res.success) throw new Error(res.error); if (window.showToast) showToast(tr("fc_child_saved") || "Sauvegardé !", "success"); const currentPerson = calGlobalData.people.find( (k) => parseInt(k.id) === window.currentSelectedMemberId, ); if (currentPerson) { currentPerson.care_modes = JSON.stringify(selectedModes); } } catch (err) { showToast((tr("fc_error") || "Erreur : ") + err.message, "error"); } }; // ========================================== // NOUVEAU CATALOGUE GLOBAL (FOYER) // ========================================== let globalLeaveCatalog = []; async function loadLeaveCatalog() { try { const res = await pachaFetch( "/modules/family-calendar/includes/api/calendar-settings.php?action=get_leave_types", ); globalLeaveCatalog = res.data || []; renderLeaveCatalog(globalLeaveCatalog); } catch (err) { console.error("Erreur chargement catalogue", err); } } function renderLeaveCatalog(types) { const container = document.getElementById("leaveTypesContainer"); if (!container) return; container.innerHTML = ""; if (types.length === 0) { container.innerHTML = `

${tr("fc_no_leave_configured") || "Aucun congé configuré."}

`; return; } types.forEach((lt) => { // Fini l'injection des variables de report ici, elles n'existent plus au niveau global ! container.innerHTML += `
${lt.label} ${lt.code}
`; }); } function editLeaveType(code, label) { document.getElementById("leaveTypeFormTitle").innerText = (tr("fc_edit_leave") || "✏️ Modifier :") + " " + label; document.getElementById("lt-mode").value = "edit"; const codeInput = document.getElementById("lt-code"); codeInput.value = code; codeInput.readOnly = true; codeInput.style.backgroundColor = "var(--bg-subtle)"; codeInput.style.opacity = "0.6"; codeInput.style.cursor = "not-allowed"; document.getElementById("lt-code-note").innerText = tr("fc_not_modifiable") || "Non modifiable"; document.getElementById("lt-label").value = label; } function resetLeaveTypeForm() { document.getElementById("leaveTypeFormTitle").innerText = tr("fc_add_leave") || "+ Ajouter"; document.getElementById("lt-mode").value = "add"; const codeInput = document.getElementById("lt-code"); codeInput.value = ""; codeInput.readOnly = false; codeInput.style.backgroundColor = ""; codeInput.style.opacity = "1"; codeInput.style.cursor = "text"; document.getElementById("lt-code-note").innerText = tr("fc_code_irreversible") || "Irréversible"; document.getElementById("lt-label").value = ""; } async function deleteLeaveType(code, label) { const msg = ( tr("fc_confirm_del_catalog") || "Supprimer définitivement le congé %s du catalogue ?" ).replace("%s", `"${label}"`); if (!(await pachaConfirm(tr("btn_delete") || "Supprimer", msg))) return; const fd = new FormData(); fd.append("action", "delete_leave_type"); fd.append("code", code); try { const res = await pachaFetch( "/modules/family-calendar/includes/api/calendar-settings.php", { method: "POST", body: fd }, ); if (!res.success) throw new Error(res.error); if (window.showToast) showToast("Congé supprimé avec succès !", "success"); resetLeaveTypeForm(); loadLeaveCatalog(); if (document.getElementById("selectCalMember").value) { loadMemberConfigView(); } } catch (err) { showToast((tr("fc_error") || "Erreur : ") + err.message, "error"); } } async function saveLeaveType() { const code = document.getElementById("lt-code").value.toUpperCase(); const label = document.getElementById("lt-label").value; const carryMax = document.getElementById("lt-carry-max").value; const carryDeadline = document.getElementById("lt-carry-deadline").value; if (!code || !label) { showToast(tr("fc_err_code_label"), "error"); return; } const fd = new FormData(); fd.append("action", "save_leave_type"); fd.append("mode", document.getElementById("lt-mode").value); fd.append("code", code); fd.append("label", label); fd.append("carry_over_max_days", carryMax); fd.append("carry_over_deadline_month", carryDeadline); try { const res = await pachaFetch( "/modules/family-calendar/includes/api/calendar-settings.php", { method: "POST", body: fd }, ); if (!res.success) throw new Error(res.error); if (window.showToast) showToast(tr("fc_catalog_updated"), "success"); resetLeaveTypeForm(); loadLeaveCatalog(); } catch (err) { showToast((tr("fc_error") || "Erreur : ") + err.message, "error"); } } // ========================================== // MODALE SETTINGS : AFFECTATION (MEMBRES) // ========================================== async function loadMemberConfigView() { const select = document.getElementById("selectCalMember"); const zone = document.getElementById("memberConfigZone"); if (!select || !select.value || !zone) return; const personId = parseInt(select.value); window.currentSelectedMemberId = personId; const role = ( select.options[select.selectedIndex].dataset.role || "" ).toLowerCase(); zone.innerHTML = '

Chargement...

'; if (role === "child" || role === "enfant") { const currentPerson = calGlobalData.people.find( (k) => parseInt(k.id) === personId, ); let savedModes = []; try { if (currentPerson.modes && Array.isArray(currentPerson.modes)) savedModes = currentPerson.modes; else if (typeof currentPerson.care_modes === "string") savedModes = JSON.parse(currentPerson.care_modes); } catch (e) {} const activeModesInFoyer = calGlobalData.foyer.care_modes || []; const modesHtml = activeModesInFoyer .map( (m) => ` `, ) .join(""); zone.innerHTML = `
${tr("fc_care_modes_title") || "Modes de Garde"}

Quels modes de garde s'appliquent à cet enfant ?

${modesHtml || `Aucun mode de garde configuré dans le foyer.`}
`; } else { try { const res = await pachaFetch( `/modules/family-calendar/includes/api/calendar-settings.php?action=get_person_leaves&person_id=${personId}`, ); const memberLeaves = res.data || []; renderMemberLeavesView(personId, memberLeaves); } catch (err) { zone.innerHTML = '

Erreur de chargement.

'; } } } function renderMemberLeavesView(personId, memberLeaves) { const zone = document.getElementById("memberConfigZone"); let html = `
Congés attribués
`; if (memberLeaves.length === 0) { html += `

Aucun congé attribué à ce membre.

`; } else { html += `
`; memberLeaves.forEach((ml) => { const moisRenouv = ml.anniversary_date ? parseInt(ml.anniversary_date.split("-")[1]) : 1; const isGraduel = ml.method === "ACCUMULATED"; const methodeLabel = isGraduel ? "Graduel" : "Fixe"; // Construction des petits Badges let badgesHtml = ` ⚖️ ${ml.allowance} j ${isGraduel ? "/ mois" : "/ an"} 🔄 Mois ${moisRenouv} `; if (ml.carry_over_max_days > 0) { badgesHtml += ` ⚠️ Report Max: ${ml.carry_over_max_days}j (Exp. M${ml.carry_over_deadline_month}) `; } html += `
${ml.leave_type} — ${methodeLabel}
${badgesHtml}
`; }); html += `
`; } // --- LE FORMULAIRE RESTRUCTURÉ EN GRILLE --- html += `
+ Attribuer un congé

⚙️ Règles de report (Optionnel)

`; zone.innerHTML = html; } // === NOUVELLES FONCTIONS : ÉDITION ET RESET === window.editMemberLeave = function ( id, type, allowance, resetMonth, method, carryMax, carryDeadline, ) { document.getElementById("formMemberLeaveTitle").innerText = "✏️ Modifier le congé"; document.getElementById("new-member-leave-id").value = id; const typeSelect = document.getElementById("new-member-leave-type"); typeSelect.value = type; typeSelect.disabled = true; // On bloque le type car il définit la clé unique typeSelect.style.backgroundColor = "var(--bg-subtle)"; document.getElementById("new-member-leave-allowance").value = allowance; document.getElementById("new-member-leave-reset").value = resetMonth; document.getElementById("new-member-leave-method").value = method; document.getElementById("new-member-leave-carry-max").value = carryMax || 0; document.getElementById("new-member-leave-carry-deadline").value = carryDeadline || 12; document.getElementById("btn-submit-member-leave").innerText = tr("btn_save") || "Enregistrer"; document.getElementById("btn-cancel-member-leave").style.display = "inline-flex"; }; window.resetMemberLeaveForm = function () { document.getElementById("formMemberLeaveTitle").innerText = "+ Attribuer un congé"; document.getElementById("new-member-leave-id").value = ""; const typeSelect = document.getElementById("new-member-leave-type"); typeSelect.value = ""; typeSelect.disabled = false; typeSelect.style.backgroundColor = ""; document.getElementById("new-member-leave-allowance").value = "0"; document.getElementById("new-member-leave-reset").value = "1"; document.getElementById("new-member-leave-method").value = "FIXED"; document.getElementById("new-member-leave-carry-max").value = "0"; document.getElementById("new-member-leave-carry-deadline").value = "12"; document.getElementById("btn-submit-member-leave").innerText = "Ajouter"; document.getElementById("btn-cancel-member-leave").style.display = "none"; }; async function addMemberLeave(personId) { const id = document.getElementById("new-member-leave-id").value; const typeSelect = document.getElementById("new-member-leave-type"); const leaveCode = typeSelect.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; const carryMax = document.getElementById("new-member-leave-carry-max").value; const carryDeadline = document.getElementById( "new-member-leave-carry-deadline", ).value; if (!leaveCode) { showToast( tr("fc_err_select_leave") || "Veuillez sélectionner un type de congé", "error", ); return; } const fd = new FormData(); fd.append("action", "add_person_leave"); fd.append("meta_id", id); fd.append("person_id", personId); fd.append("leave_type", leaveCode); fd.append("allowance", allowance); fd.append("reset_month", resetMonth); fd.append("method", method); fd.append("carry_over_max_days", carryMax); fd.append("carry_over_deadline_month", carryDeadline); try { const res = await pachaFetch( "/modules/family-calendar/includes/api/calendar-settings.php", { method: "POST", body: fd }, ); if (!res.success) throw new Error(res.error); if (window.showToast) showToast("Opération réussie !", "success"); loadMemberConfigView(); } catch (err) { showToast((tr("error_occured") || "Erreur : ") + err.message, "error"); } } async function deleteMemberLeave(id, personId) { if ( !(await pachaConfirm( tr("btn_delete") || "Supprimer", tr("fc_confirm_del_member_leave") || "Voulez-vous vraiment retirer ce congé pour ce membre ?", )) ) return; const fd = new FormData(); fd.append("action", "delete_person_leave"); fd.append("id", id); try { const res = await pachaFetch( "/modules/family-calendar/includes/api/calendar-settings.php", { method: "POST", body: fd }, ); if (!res.success) throw new Error(res.error); if (window.showToast) showToast( tr("fc_settings_updated") || "Congé retiré avec succès !", "success", ); loadMemberConfigView(); // Recharge la vue des attributions du membre } catch (err) { showToast((tr("error_occured") || "Erreur : ") + err.message, "error"); } } // ============================================================================ // 2. MOTEUR PRINCIPAL DU CALENDRIER (DOM CONTENT LOADED) // ============================================================================ document.addEventListener("DOMContentLoaded", () => { 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; this.isSelecting = false; this.selectedCells = []; this.monthSelectedCells = []; this._currentBulkInfo = null; this.dbEvents = []; this.fixedEvents = []; this.events = []; this.leaves = []; this.weeks = []; this.helperColorMap = {}; this.parents = []; this.kids = []; this.helpers = []; this.careModes = []; this.leaveMatrix = {}; this.monthlyLeaveBalances = {}; 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(); try { const res = await pachaFetch( "/modules/family-calendar/includes/api/calendar-settings.php?action=get_all", ); if (res && res.success && res.data) { window.calGlobalData = res.data; this.parents = (res.data.people || []).filter( (p) => p.role === "parent", ); this.kids = (res.data.people || []).filter((p) => ["child", "enfant"].includes(p.role), ); this.helpers = (res.data.people || []).filter( (p) => p.role === "helper", ); this.careModes = res.data.foyer.care_modes || []; this.leaveMatrix = res.data.leaves || {}; let colorCounter = 0; this.helpers.forEach((h) => { const hLeaves = this.leaveMatrix[h.id] || this.leaveMatrix[Number(h.id)] || []; hLeaves.forEach((lt) => { const typeName = typeof lt === "object" ? lt.type : lt; this.helperColorMap[`${h.id}_${typeName}`] = colorCounter % 8; colorCounter++; }); }); const settings = res.data.calendar_settings || {}; if (settings.calendar_default_view) { this.viewMode = settings.calendar_default_view; document .querySelectorAll(".fc-view-button") .forEach((b) => b.classList.remove("fc-view-button--active")); const activeBtn = document.querySelector( `.fc-view-button[data-view="${this.viewMode}"]`, ); if (activeBtn) activeBtn.classList.add("fc-view-button--active"); } } } catch (e) { console.warn("Erreur chargement de l'annuaire familial:", e); } await this.refreshAllData(); this.updateSchoolYearLabel(); setTimeout(() => this.scrollToCurrentMonth(), 100); } setupModalUI() { const headerTitle = document.querySelector( "#modalHolidays .pf-modal-title", ); if (headerTitle && !document.getElementById("holidayYearSelect")) { let options = ""; const currentY = new Date().getFullYear(); for (let y = currentY - 2; y <= currentY + 3; y++) { options += ``; } const zoneText = window.calGlobalData?.foyer?.zone_scolaire && window.calGlobalData.foyer.zone_scolaire !== "Autre" ? `(Zone ${window.calGlobalData.foyer.zone_scolaire})` : ""; headerTitle.innerHTML = `🏖️ ${tr("fc_modal_holidays_title") || "Vacances"} ${zoneText} `; 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"; for (let i = 0; i < 12; i++) { selectMonth.add( new Option( new Intl.DateTimeFormat(lang, { month: "long" }).format( new Date(2000, i, 1), ), i, ), ); } const currentY = new Date().getFullYear(); for (let y = currentY - 2; y <= currentY + 5; y++) { selectYear.add(new Option(y, y)); } const handleChange = () => { this.currentMonth = new Date( parseInt(selectYear.value), parseInt(selectMonth.value), 1, ); this.renderMonthCalendar(); }; selectMonth.addEventListener("change", handleChange); selectYear.addEventListener("change", handleChange); // --- Selecteurs Recap Intervenant (Logique masquée, pilotée par le nouveau DOM généré) --- const recapMonth = document.getElementById("recapHelperMonth"); const recapYear = document.getElementById("recapHelperYear"); if (recapMonth && recapYear) { recapMonth.add(new Option("Année complète", "all")); recapMonth.add(new Option("Période Spécifique...", "custom")); for (let i = 0; i < 12; i++) { recapMonth.add( new Option( new Intl.DateTimeFormat(lang, { month: "long" }).format( new Date(2000, i, 1), ), i + 1, ), ); } for (let y = currentY - 2; y <= currentY + 5; y++) { recapYear.add(new Option(y, y)); } recapMonth.value = new Date().getMonth() + 1; recapYear.value = currentY; // L'écouteur est conservé si appelé manuellement, mais l'interaction se fera via les selecteurs injectés } } renderHelperRecap() { const content = document.getElementById("recapHelperContent"); const hiddenMonthSel = document.getElementById("recapHelperMonth"); const hiddenYearSel = document.getElementById("recapHelperYear"); const hiddenStartInp = document.getElementById("recapHelperStart"); const hiddenEndInp = document.getElementById("recapHelperEnd"); if ( !content || !hiddenMonthSel || !hiddenYearSel || this.helpers.length === 0 ) return; const mStr = hiddenMonthSel.value; const y = parseInt(hiddenYearSel.value); let startDateStr, endDateStr; let isCustom = mStr === "custom"; if (isCustom) { startDateStr = hiddenStartInp.value; endDateStr = hiddenEndInp.value; } else if (mStr === "all") { startDateStr = `${y}-01-01`; endDateStr = `${y}-12-31`; } else { const m = parseInt(mStr); startDateStr = `${y}-${String(m).padStart(2, "0")}-01`; const lastDay = new Date(y, m, 0).getDate(); endDateStr = `${y}-${String(m).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}`; } let theoreticalWorkingDays = 0; let hasValidDates = true; if ( isCustom && (!startDateStr || !endDateStr || startDateStr > endDateStr) ) { hasValidDates = false; } else { let dObj = new Date(startDateStr + "T00:00:00"); const endObj = new Date(endDateStr + "T00:00:00"); while (dObj <= endObj) { const dayOfWeek = dObj.getDay(); if (dayOfWeek !== 0 && dayOfWeek !== 6) { const iso = this.getLocalIsoDate(dObj); if (!this.publicHolidayDates.has(iso)) theoreticalWorkingDays++; } dObj.setDate(dObj.getDate() + 1); } } let html = ""; this.helpers.forEach((h) => { const hLeaves = this.leaveMatrix[h.id] || this.leaveMatrix[Number(h.id)] || []; const helperEvents = [...this.leaves, ...this.events] .filter((e) => { const eDate = e.leave_date || e.date; const eType = String(e.leave_type || e.type).toUpperCase(); const isOldEvent = eType.includes("OFF") || eType.includes("EXTRA"); if (parseInt(e.person_id) !== parseInt(h.id)) return false; if (!e.leave_date && !isOldEvent) return false; // Ignore les événements standards sauf l'historique if (isCustom && !hasValidDates) return false; return eDate >= startDateStr && eDate <= endDateStr; }) .map((e) => ({ type: e.leave_type || e.type, duration: parseFloat(e.duration) || 1, })); const totals = {}; hLeaves.forEach( (lt) => (totals[typeof lt === "object" ? lt.type : lt] = 0), ); totals["OFF"] = 0; totals["EXTRA"] = 0; let totalLeavesTaken = 0; helperEvents.forEach((e) => { const rawType = String(e.type).toUpperCase(); let mappedType = e.type; if (rawType.includes("EXTRA")) mappedType = "EXTRA"; else if (rawType.includes("OFF")) mappedType = "OFF"; if (totals[mappedType] === undefined) totals[mappedType] = 0; totals[mappedType] += e.duration; totalLeavesTaken += e.duration; }); const workedDays = hasValidDates ? Math.max(0, theoreticalWorkingDays - totalLeavesTaken) : 0; // Construction des options du sélecteur const lang = window.appLang || "fr-FR"; const trLang = window.I18N || {}; let optionsHtml = ``; optionsHtml += ``; for (let i = 0; i < 12; i++) { const monthName = new Intl.DateTimeFormat(lang, { month: "long", }).format(new Date(2000, i, 1)); const capitalizedMonth = monthName.charAt(0).toUpperCase() + monthName.slice(1); optionsHtml += ``; } let yearOptionsHtml = ""; const currentY = new Date().getFullYear(); for (let yr = currentY - 2; yr <= currentY + 5; yr++) { yearOptionsHtml += ``; } const isCustomDisplay = isCustom ? "flex" : "none"; const isYearDisplay = isCustom ? "none" : "block"; // --- GÉNÉRATION DU HTML (Design Ultra-Compact + Micro Pilules + Flexbox Corrigé) --- let cardHtml = `
📅 ${h.name} ${window.I18N?.fc_recap_title || "Récapitulatif"}
${window.I18N?.fc_worked_est || "Travaillés (Est.)"} ${hasValidDates ? workedDays : "-"} j
`; if (!hasValidDates && isCustom) { cardHtml += `
${window.I18N?.fc_invalid_dates || "Sélectionnez des dates valides."}
`; } else { let hasLeaves = false; for (const [t, val] of Object.entries(totals)) { if ( val > 0 || hLeaves.some( (lt) => (typeof lt === "object" ? lt.type : lt) === t, ) ) { hasLeaves = true; const rawT = String(t).toUpperCase(); let label = t; if (rawT.includes("EXTRA")) label = window.I18N?.btn_extra || "Extra"; else if (rawT.includes("OFF")) label = window.I18N?.btn_off || "Off"; // 🎨 INJECTION DYNAMIQUE DE LA COULEUR CSS (La même que le calendrier !) const colorIndex = this.helperColorMap[`${h.id}_${t}`] ?? 0; const borderColor = `var(--fc-palette-text-${colorIndex})`; cardHtml += `
${label} ${val}j
`; } } if (!hasLeaves) cardHtml += `
${window.I18N?.fc_no_leaves_taken || "Aucun congé posé."}
`; } cardHtml += `
➔
`; html += cardHtml; }); content.innerHTML = html; // Attachement de la fonction globale pour les selecteurs injectés window.updateHiddenRecap = (el, type) => { if (type === "month") hiddenMonthSel.value = el.value; if (type === "year") hiddenYearSel.value = el.value; if (type === "start") hiddenStartInp.value = el.value; if (type === "end") hiddenEndInp.value = el.value; this.renderHelperRecap(); }; } async refreshAllData() { try { const zone = window.calGlobalData?.foyer?.zone_scolaire || "C"; const [ weeksData, eventsData, publicHols, schoolHols, // 🏖️ NOUVEAU : On réceptionne les vacances leavesData, snapshotsData, ] = await Promise.all([ this.fetchApi( `/modules/family-calendar/includes/api/get-calendar-weeks-scolaire.php?school_year_start=${this.currentSchoolYearStart}`, ), this.fetchApi("/modules/family-calendar/includes/api/get-events.php"), this.fetchPublicHolidays(), this.fetchSchoolHolidays(zone), this.fetchApi("/modules/family-calendar/includes/api/get-leaves.php"), this.fetchApi( "/modules/family-calendar/includes/api/get-leave-snapshots.php", ), ]); this.weeks = this.processWeeks(weeksData.weeks || []); this.dbEvents = (eventsData.events || []).map((e) => ({ ...e, duration: parseFloat(e.duration), })); // 🔥 LE FIX : On fusionne les jours fériés ET les vacances this.fixedEvents = [...publicHols, ...schoolHols]; this.leaves = leavesData.leaves || []; 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); } } async fetchPublicHolidays() { try { const res = await fetch( "https://calendrier.api.gouv.fr/jours-feries/metropole.json", ); const holidays = await res.json(); return Object.keys(holidays).map((date, idx) => ({ id: `ph-${idx}`, date: date, name: holidays[date], type: "PUBLIC_HOLIDAY", duration: 1, })); } catch (error) { return []; } } async fetchSchoolHolidays(zone) { if (!zone || zone === "Autre") return []; try { const yearStr = `${this.currentSchoolYearStart}-${this.currentSchoolYearStart + 1}`; const whereClause = `annee_scolaire='${yearStr}' AND zones LIKE '%Zone ${zone}%' AND population='Élèves'`; const url = `https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=${encodeURIComponent(whereClause)}&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 whereClause = `annee_scolaire='${yearStr}' AND zones LIKE '%Zone ${zone}%' AND population='Élèves'`; const url = `https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=${encodeURIComponent(whereClause)}&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(); this.calculateMonthlyBalances(); this.renderTable(); this.renderMonthCalendar(); if (typeof this.renderHelperRecap === "function") this.renderHelperRecap(); } reprocessEvents() { const upperCareModes = this.careModes.map((m) => m.toUpperCase()); this.weeks.forEach((w) => { w.totals = {}; this.careModes.forEach((m) => (w.totals["mode_" + m] = 0)); this.kids.forEach((k) => (w.totals["sick_" + k.id] = 0)); this.helpers.forEach((h) => { const hLeaves = this.leaveMatrix[h.id] || this.leaveMatrix[Number(h.id)] || []; hLeaves.forEach((lt) => { const typeName = typeof lt === "object" ? lt.type : lt; w.totals[`leave_${h.id}_${typeName}`] = 0; }); }); this.parents.forEach((p) => { const types = this.leaveMatrix[p.id] || []; types.forEach((t) => (w.totals[`leave_${p.id}_${t.type}`] = 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; if (e.type === "CHILD_SICK") { w.totals["sick_" + e.person_id] = (w.totals["sick_" + e.person_id] || 0) + dur; } else { const rawType = String(e.type).toUpperCase(); const isOldHelperEvent = rawType.includes("OFF") || rawType.includes("EXTRA"); if (isOldHelperEvent) { const pId = this.helpers.some( (h) => parseInt(h.id) === parseInt(e.person_id), ) ? e.person_id : this.helpers[0]?.id || 0; let mappedType = e.type; if (rawType.includes("EXTRA")) mappedType = "EXTRA"; else if (rawType.includes("OFF")) mappedType = "OFF"; w.totals[`leave_${pId}_${mappedType}`] = (w.totals[`leave_${pId}_${mappedType}`] || 0) + dur; } } // Modes de garde dynamiques if (upperCareModes.includes(e.type)) { const originalMode = this.careModes.find((m) => m.toUpperCase() === e.type) || e.type; w.totals["mode_" + originalMode] = (w.totals["mode_" + originalMode] || 0) + 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; w.totals[`leave_${l.person_id}_${l.leave_type}`] = (w.totals[`leave_${l.person_id}_${l.leave_type}`] || 0) + dur; } }); let workingDays = 0; Object.values(w.dayDates).forEach((d) => { if (!this.publicHolidayDates.has(this.getLocalIsoDate(d))) workingDays++; }); let helperAbsences = 0; this.helpers.forEach((h) => { const hLeaves = this.leaveMatrix[h.id] || this.leaveMatrix[Number(h.id)] || []; hLeaves.forEach((lt) => { const typeName = typeof lt === "object" ? lt.type : lt; helperAbsences += w.totals[`leave_${h.id}_${typeName}`] || 0; }); }); if (this.kids.length > 0) { w.totals.presenceKid = Math.max( 0, workingDays - (helperAbsences + (w.totals["sick_" + this.kids[0].id] || 0)), ); } }); } calculateMonthlyBalances() { const balances = {}; this.parents.forEach((p) => (balances[String(p.id)] = {})); // 1. Préparation d'une chronologie glissante (60 mois) 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); } // 2. Indexation des jours consommés par mois const usageByMonth = {}; const allPlacedLeaves = [...(this.leaves || []), ...(this.events || [])]; 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] = {}; let dur = parseFloat(l.duration); if (isNaN(dur)) dur = 1; usageByMonth[pid][type][ym] = (usageByMonth[pid][type][ym] || 0) + dur; }); // 3. Parcours Chronologique this.parents.forEach((parent) => { const pid = String(parent.id); const matrix = this.leaveMatrix[pid] || this.leaveMatrix[Number(pid)] || []; matrix.forEach((conf) => { const type = String(conf.leave_type || conf.type) .trim() .toUpperCase(); if (!balances[pid][type]) balances[pid][type] = {}; // Paramètres du congé const resetMonth = parseInt( conf.date ? conf.date.split("-")[1] : 1, 10, ); const maxCarry = parseFloat(conf.carry_over_max_days || 0); const deadlineMonth = parseInt( conf.carry_over_deadline_month || 12, 10, ); const method = conf.method || "FIXED"; const allowance = parseFloat(conf.allowance || 0); let mainBalance = 0; let carryBalance = 0; // Filtrer les snapshots de cette personne et de ce congé const snapshots = (this.leaveSnapshots || []).filter( (s) => String(s.person_id) === pid && s.leave_type === type, ); ymList.forEach((ym) => { const currM = parseInt(ym.split("-")[1], 10); const snap = snapshots.find((s) => s.snapshot_date.startsWith(ym)); if (snap) { // A. LE MUR DE BÉTON : Le Snapshot écrase tout le passé mainBalance = parseFloat(snap.remaining_balance || 0); carryBalance = parseFloat(snap.carry_over_balance || 0); } else { // B. LA GUILLOTINE : Effacement du report au mois M+1 de la deadline let wipeMonth = (deadlineMonth % 12) + 1; if (currM === wipeMonth && maxCarry > 0) { carryBalance = 0; } // C. RENOUVELLEMENT ANNUEL (Bascule vers le Report) if (currM === resetMonth) { carryBalance = Math.min(mainBalance, maxCarry); mainBalance = 0; } // D. ACQUISITION if (method === "FIXED" && currM === resetMonth) { mainBalance += allowance; } else if (method === "ACCUMULATED") { // Acquisition graduelle stricte (ex: +0.64/mois) mainBalance += allowance; } } // E. CONSOMMATION DU MOIS const totalAvailableAtStart = mainBalance + carryBalance; const carryAtStart = carryBalance; const usedInMonth = usageByMonth[pid]?.[type]?.[ym] || 0; // Priorité absolue : On tape d'abord dans les jours périssables ! let usedFromCarry = Math.min(usedInMonth, carryBalance); carryBalance -= usedFromCarry; let usedFromMain = usedInMonth - usedFromCarry; mainBalance -= usedFromMain; // F. SAUVEGARDE DE L'ÉTAT DU MOIS balances[pid][type][ym] = { availableAtMonthStart: totalAvailableAtStart, carryAtStart: carryAtStart, usedInMonth: usedInMonth, }; }); }); }); this.monthlyLeaveBalances = balances; } renderTable() { if (!this.planningBody) return; try { this.planningBody.innerHTML = ""; if (!this.weeks || this.weeks.length === 0) { this.planningBody.innerHTML = `${window.I18N?.["fc_no_data"] || "Aucune donnée pour cette année scolaire."}`; return; } 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) => String(m).toUpperCase(), ); const helperIds = (this.helpers || []).map((h) => parseInt(h.id)); 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"); } // 1. Colonne Sticky MOIS if (!processedMonths[w.monthKey]) { processedMonths[w.monthKey] = true; const tdMonth = document.createElement("td"); tdMonth.className = "col-month col-sticky-mois rotated-text"; tdMonth.innerHTML = `${w.monthName || ""}`; tdMonth.rowSpan = monthSpans[w.monthKey]; tr.appendChild(tdMonth); } // 2. Colonne Sticky SEMAINE const tdW = document.createElement("td"); tdW.className = "col-month col-sticky-sem rotated-text"; tdW.innerHTML = `${w.weekLabel || ""}`; tr.appendChild(tdW); // 3. Jours de la semaine (Lundi à Vendredi) ["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 || []; const dayLeaves = (this.leaves || []).filter( (l) => (l.leave_date || l.date) === iso, ); // A. Application des classes de coloration de fond CSS 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 (upperCareModes.includes(evt.type)) td.classList.add("fc-day--has-guard"); }); // Détection dynamique et globale des congés de l'intervenant let appliedColorIndex = -1; const helperLeaf = dayLeaves.find((l) => helperIds.includes(parseInt(l.person_id)), ); if (helperLeaf) { appliedColorIndex = this.helperColorMap[ `${helperLeaf.person_id}_${helperLeaf.leave_type}` ]; } else { const helperEvent = events.find( (e) => helperIds.includes(parseInt(e.person_id)) || String(e.type).toUpperCase().includes("OFF") || String(e.type).toUpperCase().includes("EXTRA"), ); if (helperEvent) { const pId = helperIds.includes(parseInt(helperEvent.person_id)) ? helperEvent.person_id : helperIds[0]; const rawType = String(helperEvent.type).toUpperCase(); let mappedType = helperEvent.type; if (rawType.includes("EXTRA")) mappedType = "EXTRA"; else if (rawType.includes("OFF")) mappedType = "OFF"; appliedColorIndex = this.helperColorMap[`${pId}_${mappedType}`] ?? this.helperColorMap[`${pId}_${helperEvent.type}`] ?? 0; } } if (appliedColorIndex !== -1 && appliedColorIndex !== undefined) { td.classList.add(`fc-bg-palette-${appliedColorIndex}`); } // B. Contenu HTML de la cellule (Numéro du jour, Icônes de garde, Maladie Enfants, Initiales Parents) let content = `
${String(dateObj.getDate()).padStart(2, "0")}`; // Icônes des modes de garde 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 + `
`; // Icônes Enfant Malade 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 + `
`; // Initiales des Parents en congé if (dayLeaves.length) { let parentLeavesHtml = `
`; (this.parents || []).forEach((person) => { if ( dayLeaves.some( (l) => parseInt(l.person_id || l.person) === parseInt(person.id), ) ) { parentLeavesHtml += `${String(person.name).charAt(0).toUpperCase()}`; } }); content += parentLeavesHtml + `
`; } td.innerHTML = content + `
`; tr.appendChild(td); }); // 4. Totaux : Congés de l'intervenant / Helper (Nombres bruts) (this.helpers || []).forEach((h) => { const hLeaves = this.leaveMatrix[h.id] || this.leaveMatrix[Number(h.id)] || []; hLeaves.forEach((lt) => { const typeName = typeof lt === "object" ? lt.type : lt; const td = document.createElement("td"); td.className = "col-total"; const val = w.totals[`leave_${h.id}_${typeName}`] || 0; if (val > 0) { td.textContent = fmt(val); } else { td.textContent = ""; } tr.appendChild(td); }); }); // 5. Totaux : Modes de garde (On masque "Nounou" si un intervenant existe) (this.careModes || []).forEach((mode) => { if (mode.toLowerCase() === "nounou" && this.helpers.length > 0) return; const td = document.createElement("td"); td.className = "col-total"; const val = w.totals["mode_" + mode] || 0; td.textContent = fmt(val); tr.appendChild(td); }); // 6. Totaux : Maladie par enfant (this.kids || []).forEach((kid) => { const td = document.createElement("td"); td.className = "col-total"; const val = w.totals["sick_" + kid.id] || 0; td.textContent = fmt(val); tr.appendChild(td); }); // 7. Colonnes de suivi des soldes mensuels (Parents : Av. / Use) if (!processedLeavesCols[w.monthKey]) { processedLeavesCols[w.monthKey] = true; (this.parents || []).forEach((parent, index) => { const cssPrefix = index % 2 === 0 ? "col-p1" : "col-p2"; 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}`; } } generateMonthHTML(year, month) { let html = ``; ["L", "M", "M", "J", "V"].forEach((d) => (html += ``)); html += ``; const daysInMonth = new Date(year, month + 1, 0).getDate(); let currentRenderedCols = 0, startDay = (new Date(year, month, 1).getDay() + 6) % 7; const upperCareModes = this.careModes.map((m) => m.toUpperCase()); if (startDay < 5) { for (let i = 0; i < startDay; i++) { html += ``; currentRenderedCols++; } } for (let d = 1; d <= daysInMonth; d++) { const dateObj = new Date(year, month, d), dayOfWeek = dateObj.getDay(); if (dayOfWeek === 0 || dayOfWeek === 6) continue; if (currentRenderedCols === 5) { html += ``; currentRenderedCols = 0; } const iso = this.getLocalIsoDate(dateObj), todayIso = this.getLocalIsoDate(new Date()); let cls = "fc-month-day" + (iso === todayIso ? " fc-day--today" : ""); 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) => upperCareModes.includes(e.type))) cls += " fc-day--has-guard"; const helperIds = (this.helpers || []).map((h) => parseInt(h.id)); const dayLeaves = this.leaves.filter((l) => l.leave_date === iso); let appliedColorIndex = -1; const helperLeaf = dayLeaves.find((l) => helperIds.includes(parseInt(l.person_id)), ); if (helperLeaf) { appliedColorIndex = this.helperColorMap[ `${helperLeaf.person_id}_${helperLeaf.leave_type}` ]; } else { const helperEvent = dayEvts.find( (e) => helperIds.includes(parseInt(e.person_id)) || e.type === "HELPER_OFF" || e.type === "HELPER_EXTRA", ); if (helperEvent) { const mappedType = helperEvent.type === "HELPER_OFF" ? "OFF" : helperEvent.type === "HELPER_EXTRA" ? "EXTRA" : helperEvent.type; appliedColorIndex = this.helperColorMap[`${helperEvent.person_id}_${mappedType}`] ?? this.helperColorMap[ `${helperEvent.person_id}_${helperEvent.type}` ] ?? 0; } } if (appliedColorIndex !== -1 && appliedColorIndex !== undefined) { cls += ` fc-bg-palette-${appliedColorIndex}`; } let content = `
${d}`; let iconsHtml = `
`; dayEvts.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 + `
`; let sickHtml = `
`; dayEvts.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 + `
`; if (dayLeaves.length) { content += `
`; this.parents.forEach((parent) => { if ( dayLeaves.some( (l) => parseInt(l.person_id) === parseInt(parent.id), ) ) { content += `${parent.name.charAt(0).toUpperCase()} `; } }); content += `
`; } html += `
`; currentRenderedCols++; } while (currentRenderedCols < 5 && currentRenderedCols > 0) { html += ``; currentRenderedCols++; } return html + `
${d}
${content}
`; } generateMonthSummaryHTML(year, month) { // 1. Identifier UNIQUEMENT les enfants qui ont le mode de garde "Nounou" const nounouKids = (window.FAMILY_CONFIG.kids || []).filter( (k) => k.modes && k.modes.some((m) => m.toLowerCase() === "nounou"), ); // S'il n'y a pas d'enfant avec ce mode, on ne retourne rien pour ce bloc if (nounouKids.length === 0) return ""; const daysInMonth = new Date(year, month + 1, 0).getDate(); let html = ""; // 2. Boucler sur chaque enfant concerné pour un calcul individuel nounouKids.forEach((kid) => { let workingDays = 0; let absencesNounou = 0; let sick = 0; const helperIds = this.helpers.map((h) => parseInt(h.id)); for (let d = 1; d <= daysInMonth; d++) { const dateObj = new Date(year, month, d); const dayOfWeek = dateObj.getDay(); // Exclure les week-ends des jours ouvrés (0 = Dimanche, 6 = Samedi) if (dayOfWeek === 0 || dayOfWeek === 6) continue; const iso = this.getLocalIsoDate(dateObj); // Exclure les jours fériés des jours ouvrés if (this.publicHolidayDates.has(iso)) continue; // C'est un jour ouvré valide workingDays += 1; const dayEvents = this.events.filter((e) => e.date === iso); const dayLeaves = this.leaves.filter((l) => l.leave_date === iso); dayEvents.forEach((e) => { const dur = parseFloat(e.duration) || 1; const rawType = String(e.type).toUpperCase(); // Si c'est un vieil événement historisé OFF/EXTRA if ( rawType.includes("OFF") || rawType.includes("EXTRA") || helperIds.includes(parseInt(e.person_id)) ) { absencesNounou += dur; } // On vérifie que la maladie concerne BIEN cet enfant précis ! if ( e.type === "CHILD_SICK" && Number(e.person_id) === Number(kid.id) ) { sick += dur; } }); // Prise en compte des nouveaux congés propres aux intervenants dayLeaves.forEach((l) => { const dur = parseFloat(l.duration) || 1; if (helperIds.includes(parseInt(l.person_id))) { absencesNounou += dur; } }); } // Récupération dynamique du nom de l'intervenant avec i18n const fallbackHelper = window.I18N?.fc_role_helper || "Intervenant"; const helperName = this.helpers && this.helpers.length > 0 ? this.helpers.map((h) => h.name).join(" & ") : fallbackHelper; // Calcul final strict : Jours ouvrés - Maladie (de l'enfant) - Absences Nounou const presence = Math.max(0, workingDays - sick - absencesNounou); // Textes traduits const trPresence = window.I18N?.fc_presence || "Présence"; const trWorking = window.I18N?.fc_working_days || "Ouvrés"; const trAbs = window.I18N?.fc_absences || "Abs."; const trSick = window.I18N?.fc_menu_sick || "Maladie"; // Rendu HTML individuel html += `
👧👦 ${trPresence} ${kid.name} (${helperName})
${trWorking} ${workingDays} j
${trAbs} ${helperName} ${parseFloat(absencesNounou.toFixed(1))} j
${trSick} ${parseFloat(sick.toFixed(1))} j
${trPresence} ${parseFloat(presence.toFixed(1))} j
`; }); return html; } scrollToCurrentMonth() { const wrapper = document.getElementById("planningTable-wrapper"); const targetRow = document.querySelector( `#planningTable tbody tr[data-month="${this.getLocalIsoDate(new Date()).slice(0, 7)}"]`, ); if (wrapper && targetRow) setTimeout( () => wrapper.scrollTo({ top: Math.max(0, targetRow.offsetTop - 50), behavior: "smooth", }), 50, ); } renderMonthCalendar() { if (!this.monthCalendar) return; const y = this.currentMonth.getFullYear(), m = this.currentMonth.getMonth(); const selectMonth = document.getElementById("fc-select-month"); const selectYear = document.getElementById("fc-select-year"); if (selectMonth && selectYear) { selectMonth.value = m; selectYear.value = y; if (this.viewMode === "3months") this.renderThreeMonthsView(); else if (this.viewMode === "2months") this.renderTwoMonthsView(); else this.monthCalendar.innerHTML = `
${this.generateMonthHTML(y, m)}${this.generateMonthSummaryHTML(y, m)}
`; } this.renderMonthBalances(); } renderTwoMonthsView() { const y = this.currentMonth.getFullYear(), m = this.currentMonth.getMonth(); const nextDate = new Date(y, m + 1, 1); const lang = window.appLang || "fr-FR"; this.monthCalendar.innerHTML = `
${new Intl.DateTimeFormat(lang, { month: "long" }).format(this.currentMonth)}
${this.generateMonthHTML(y, m)}${this.generateMonthSummaryHTML(y, m)}
${new Intl.DateTimeFormat(lang, { month: "long" }).format(nextDate)}
${this.generateMonthHTML(nextDate.getFullYear(), nextDate.getMonth())}${this.generateMonthSummaryHTML(nextDate.getFullYear(), nextDate.getMonth())}
`; } renderThreeMonthsView() { const y = this.currentMonth.getFullYear(), m = this.currentMonth.getMonth(); const [d1, d2, d3] = [ this.currentMonth, new Date(y, m + 1, 1), new Date(y, m + 2, 1), ]; let html = `
`; const lang = window.appLang || "fr-FR"; [d1, d2, d3].forEach( (d) => (html += `
${new Intl.DateTimeFormat(lang, { month: "long" }).format(d)}
${this.generateMonthHTML(d.getFullYear(), d.getMonth())}${this.generateMonthSummaryHTML(d.getFullYear(), d.getMonth())}
`), ); this.monthCalendar.innerHTML = html + `
`; } renderMonthBalances() { const container = document.getElementById("fc-month-balances"); if (!container) return; this.calculateMonthlyBalances(); 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++) { monthsToDisplay.push(`${y}-${String(m + i + 1).padStart(2, "0")}`); } container.style.display = "flex"; container.innerHTML = this.parents .map((person) => { let cards = `
${person.name.toUpperCase()}
`; const types = this.leaveMatrix[String(person.id)] || this.leaveMatrix[Number(person.id)] || []; types.forEach((conf) => { const type = String(conf.leave_type || conf.type) .trim() .toUpperCase(); const startBal = this.monthlyLeaveBalances[String(person.id)]?.[type]?.[ monthsToDisplay[0] ]?.availableAtMonthStart || 0; const carryAtStart = this.monthlyLeaveBalances[String(person.id)]?.[type]?.[ monthsToDisplay[0] ]?.carryAtStart || 0; let totalUsed = 0; monthsToDisplay.forEach((ym) => { totalUsed += this.monthlyLeaveBalances[String(person.id)]?.[type]?.[ym] ?.usedInMonth || 0; }); const endBal = Math.max(0, startBal - totalUsed); const remainingCarry = Math.max(0, carryAtStart - totalUsed); const fmt = (n) => n > 0 ? (Number.isInteger(n) ? n : n.toFixed(2)) : "0"; let alertHtml = ""; const cMonth = parseInt(monthsToDisplay[0].split("-")[1], 10); const resetMonth = parseInt( conf.date ? conf.date.split("-")[1] : 1, 10, ); const deadline = parseInt(conf.carry_over_deadline_month || 12, 10); const alertMonthEndCycle = resetMonth === 1 ? 12 : resetMonth - 1; // ⚠️ Alerte 1 : Le Report expire à la fin de ce mois ! if (remainingCarry > 0 && cMonth === deadline) { alertHtml = `
🔥
`; } // ⚠️ Alerte 2 : Le solde de base expire à la fin du cycle annuel ! else if ( endBal > 0 && (cMonth === alertMonthEndCycle || cMonth === resetMonth) ) { const alertMsg = window.I18N && window.I18N["fc_alert_burn_days"] ? window.I18N["fc_alert_burn_days"] .replace("%s", fmt(endBal)) .replace("%s", "la fin de cycle") : `Alerte : ${fmt(endBal)} j perdu(s) à la fin du cycle !`; alertHtml = `
🔥
`; } cards += `
${conf.type || type}${fmt(endBal)}${totalUsed > 0 ? `-${fmt(totalUsed)}` : ""}${alertHtml}
`; }); return cards + `
`; }) .join(""); } 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_iso_number, 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: {}, })); } 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() { 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), ); document.addEventListener("mousemove", (e) => this.handleMouseMove(e)); document.addEventListener("mouseup", (e) => this.handleMouseUp(e)); 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 () => { if (this._isAutoLoading) return; if ( scrollWrapper.scrollTop + scrollWrapper.clientHeight >= scrollWrapper.scrollHeight - 5 ) { this._isAutoLoading = true; await this.changeSchoolYear(1); scrollWrapper.scrollTop = 5; setTimeout(() => (this._isAutoLoading = false), 500); } else if (scrollWrapper.scrollTop === 0) { this._isAutoLoading = true; await this.changeSchoolYear(-1); scrollWrapper.scrollTop = scrollWrapper.scrollHeight - scrollWrapper.clientHeight - 5; setTimeout(() => (this._isAutoLoading = false), 500); } }); } 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(); }); document.getElementById("fc-today-btn")?.addEventListener("click", () => { this.currentMonth = new Date(); this.currentMonth.setDate(1); this.renderMonthCalendar(); }); 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(); }), ); // Câblage sécurisé du formulaire d'ajustement de solde const formSnap = document.getElementById("formSnapshot"); if (formSnap) { formSnap.addEventListener("submit", async (e) => { e.preventDefault(); const btn = document.getElementById("btnSubmitSnapshot") || e.target.querySelector('button[type="submit"]'); const originalText = btn ? btn.innerHTML : ""; if (btn) { btn.innerHTML = "⏳..."; btn.disabled = true; } try { const payload = { person_id: document.getElementById("snapPerson").value, leave_type: document.getElementById("snapType").value, snapshot_date: document.getElementById("snapDate").value, remaining_balance: parseFloat( document.getElementById("snapBalance").value, ), carry_over_balance: parseFloat( document.getElementById("snapCarryOver").value || 0, ), }; const res = await this.postApi( "/modules/family-calendar/includes/api/save-leave-snapshot.php", payload, ); if (!res.status || res.status !== "success") throw new Error(res.message || "Erreur"); if (window.showToast) showToast( tr("fc_settings_updated") || "Solde corrigé !", "success", ); const modal = document.getElementById("modalSnapshot") || document.getElementById("snapshotModal"); if (modal) { modal.style.display = "none"; modal.classList.remove("open"); } document.body.classList.remove("no-scroll"); formSnap.reset(); await this.refreshAllData(); // Recharge les graphiques en direct } catch (err) { showToast((tr("fc_error") || "Erreur : ") + err.message, "error"); } finally { if (btn) { btn.innerHTML = originalText; btn.disabled = false; } } }); } } 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) this.showMenu( e.pageX, e.pageY, this.selectedCells.map((c) => c.dataset.date), false, ); } handleTouchStart(e) { const td = e.target.closest("#planningTable td[data-date]"); if (!td) return; e.preventDefault(); this.clearSelection(); this.isSelecting = true; this.selectCell(td); } handleTouchMove(e) { if (!this.isSelecting) return; e.preventDefault(); const touch = e.touches[0], 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) this.showMenu( 0, 0, this.selectedCells.map((c) => c.dataset.date), 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 }; const activeEventMap = new Set(); this.events.forEach((e) => { if (dates.includes(e.date)) { const personKey = e.person_id !== null && e.person_id !== undefined ? e.person_id.toString() : "0"; activeEventMap.add(`${e.type}_${personKey}`); } }); const activeLeaves = {}; [...this.parents, ...this.helpers].forEach( (p) => (activeLeaves[p.id] = new Set()), ); this.leaves.forEach((l) => { if (dates.includes(l.leave_date) && activeLeaves[l.person_id]) activeLeaves[l.person_id].add(l.leave_type); }); const getActiveStyleE = (type, personStr, color) => activeEventMap.has(`${type}_${personStr}`) ? `border: 1px solid ${color} !important; background: var(--bg-soft) !important; color: ${color} !important; font-weight: 700;` : ""; const getActiveStyleL = (type, pid, color) => activeLeaves[pid]?.has(type) ? `border: 1px solid ${color} !important; background: var(--bg-soft) !important; color: ${color} !important; font-weight: 700;` : ""; const trLang = window.I18N || {}; const dateLabel = dates.length > 1 ? `${dates.length} Jours sélectionnés` : new Date(dates[0]).toLocaleDateString(window.appLang || "fr-FR", { weekday: "long", day: "numeric", month: "long", }); const trashSvg = ``; // Ajout de la classe "btn-icon-action delete" pour activer le hover natif (rouge clair) const buildHeader = (title, action, cat, pid) => `
${title}${action ? `` : ""}
`; let html = `
${dateLabel}
`; // 1. Congés Intervenants if (this.helpers.length > 0) { this.helpers.forEach((h) => { const hLeaves = this.leaveMatrix[h.id] || this.leaveMatrix[Number(h.id)] || []; if (hLeaves.length > 0) { html += `
${buildHeader(h.name, "clear-leaves-person", null, h.id)}
`; hLeaves.forEach((lt) => { const lType = typeof lt === "object" ? lt.type : lt; const pIdx = this.helperColorMap[`${h.id}_${lType}`] ?? 0; const hColor = `var(--fc-palette-text-${pIdx})`; html += ``; }); html += `
`; } }); } // 2. Modes de Garde if (this.careModes.length > 0) { const displayModes = this.careModes.filter( (m) => !(m.toLowerCase() === "nounou" && this.helpers.length > 0), ); if (displayModes.length > 0) { html += `
${buildHeader(trLang.fc_care_modes_title || "Modes de garde", "clear-type", "CARE_MODE", null)}
`; displayModes.forEach((m) => { let iconHtml = m.toLowerCase() === "avis" ? ` ` : m.toLowerCase() === "centre" ? `🏫 ` : ""; const modeType = m.toUpperCase(); html += ``; }); html += `
`; } } // 3. Maladie Enfants if (this.kids.length > 0) { html += `
${buildHeader(trLang.fc_menu_sick || "Maladie", "clear-type", "CHILD_SICK", null)}
`; this.kids.forEach((k) => { const color = k.color || "var(--danger)"; html += ``; }); html += `
`; } // 4. Congés Parents (Répartition en 2 colonnes !) html += `
${buildHeader(trLang.fc_menu_adult_leaves || "Congés Parents", null, null, null)}`; html += `
`; const allParentLeaveTypes = new Set(); this.parents.forEach((p) => { (this.leaveMatrix[p.id] || []).forEach((l) => allParentLeaveTypes.add(typeof l === "object" ? l.type : l), ); }); this.parents.forEach((p) => { const pColor = p.color || "var(--primary)"; html += `
`; html += `
${p.name}
`; html += `
`; Array.from(allParentLeaveTypes).forEach((t) => { if ( (this.leaveMatrix[p.id] || []).some( (l) => (typeof l === "object" ? l.type : l) === t, ) ) { html += ``; } }); html += `
`; }); html += `
`; menu.innerHTML = html; 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, person, pid, cat } = dataset; const dates = this._currentBulkInfo.dates; const upperCareModes = this.careModes.map((m) => m.toUpperCase()); try { if (action === "add") { let typesToClear = []; if (["HELPER_OFF", "HELPER_EXTRA"].includes(type)) typesToClear = ["HELPER_OFF", "HELPER_EXTRA"]; if (upperCareModes.includes(type)) typesToClear = [...upperCareModes]; if (type === "CHILD_SICK") typesToClear = ["CHILD_SICK"]; if (typesToClear.length) { await this.postApi( "/modules/family-calendar/includes/api/manage-event.php", { action: "bulk_delete_day_types_person", dates, types: typesToClear, person_id: parseInt(person) || 0, }, ); } await this.postApi( "/modules/family-calendar/includes/api/save-events.php", dates.map((d) => ({ date: d, type: type, duration: 1, person_id: parseInt(person) || 0, })), ); } else if (action === "clear-type") { let typesToClear = []; let personToClear = null; if (cat.startsWith("HELPER_")) { typesToClear = ["HELPER_OFF", "HELPER_EXTRA"]; personToClear = parseInt(cat.split("_")[1]) || 0; } else if (cat === "CARE_MODE") { typesToClear = [...upperCareModes]; personToClear = 0; } else if (cat === "CHILD_SICK") { typesToClear = ["CHILD_SICK"]; } await this.postApi( "/modules/family-calendar/includes/api/manage-event.php", { action: "bulk_delete_day_types_person", dates, types: typesToClear, person_id: personToClear, }, ); } 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), }, ); await this.postApi( "/modules/family-calendar/includes/api/save-leaves.php", dates.map((d) => ({ date: d, person_id: parseInt(pid), leave_type: type, duration: 1, })), ); } 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), }, ); } await this.refreshAllData(); } catch (e) { showToast((tr("error_occured") || "Erreur : ") + e.message, "error"); } } } new FamilyCalendar(); });