/**
* 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 `${p.name} (${roleDisplay}) `;
})
.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) => `
${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. `}
${tr("btn_save_rights") || "Enregistrer"}
`;
} 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 += `
Type de congé
-- Sélectionner --
${globalLeaveCatalog.map((lt) => `${lt.label} `).join("")}
Acquisition
Fixe (Annuelle)
Graduelle (Mensuelle)
Quota
Renouvellement
${tr("month_01") || "Janvier"}
${tr("month_02") || "Février"}
${tr("month_03") || "Mars"}
${tr("month_04") || "Avril"}
${tr("month_05") || "Mai"}
${tr("month_06") || "Juin"}
${tr("month_07") || "Juillet"}
${tr("month_08") || "Août"}
${tr("month_09") || "Septembre"}
${tr("month_10") || "Octobre"}
${tr("month_11") || "Novembre"}
${tr("month_12") || "Décembre"}
⚙️ Règles de report (Optionnel)
${tr("btn_cancel") || "Annuler"}
Ajouter / Enregistrer
`;
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 += `${y} - ${y + 1} `;
}
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}
${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";
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 = `${trLang.fc_full_year || "Année complète"} `;
optionsHtml += `${trLang.fc_custom_period || "Période Spécifique..."} `;
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 += `${capitalizedMonth} `;
}
let yearOptionsHtml = "";
const currentY = new Date().getFullYear();
for (let yr = currentY - 2; yr <= currentY + 5; yr++) {
yearOptionsHtml += `${yr} `;
}
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), // 🏖️ NOUVEAU : On appelle l'API
this.fetchApi("/modules/family-calendar/includes/api/get-leaves.php"),
this.fetchApi(
"/modules/family-calendar/includes/api/get-leave-snapshots.php",
),
]);
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}`;
// Ton URL qui utilise le LIKE, beaucoup plus robuste !
const url = `https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=annee_scolaire='${yearStr}' AND zones LIKE '%Zone ${zone}%'&limit=100`;
const res = await fetch(url);
const data = await res.json();
const rawRecords = data.results || [];
// Ton système de dédoublonnage
const uniqueMap = new Map();
rawRecords.forEach((r) => {
const key = `${r.description}|${r.start_date}`;
if (!uniqueMap.has(key)) uniqueMap.set(key, r);
});
const holidays = [];
Array.from(uniqueMap.values()).forEach((r) => {
const startDateStr = r.start_date.split("T")[0];
const endDateStr = r.end_date.split("T")[0];
let curr = new Date(startDateStr + "T00:00:00");
const end = new Date(endDateStr + "T00:00:00");
// TA REGLE METIER : Si vendredi, on passe au samedi
if (curr.getDay() === 5) {
curr.setDate(curr.getDate() + 1);
}
while (curr < end) {
holidays.push({
id: `sh-${curr.getTime()}`,
date: this.getLocalIsoDate(curr),
name: r.description,
type: "VACANCES_SCOLAIRES",
duration: 1,
});
curr.setDate(curr.getDate() + 1);
}
});
return holidays;
} catch (e) {
console.error("Erreur API Vacances:", e);
return [];
}
}
async renderModalHolidays() {
const tbody = document.querySelector("#schoolHolidaysTable tbody");
if (!tbody) return;
const zone = window.calGlobalData?.foyer?.zone_scolaire || "C";
if (zone === "Autre") {
tbody.innerHTML =
"Zone 'Autre' sélectionnée. Pas de données auto. ";
return;
}
tbody.innerHTML =
"Chargement des données du Ministère... ⏳ ";
try {
const year = this.modalSelectedYear || this.currentSchoolYearStart;
const yearStr = `${year}-${year + 1}`;
const url = `https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=annee_scolaire='${yearStr}' AND zones LIKE '%Zone ${zone}%'&limit=100&order_by=start_date`;
const res = await fetch(url);
const data = await res.json();
if (data.results && data.results.length > 0) {
// Ton système de dédoublonnage pour la modale
const uniqueMap = new Map();
data.results.forEach((r) => {
const key = `${r.description}|${r.start_date}`;
if (!uniqueMap.has(key)) uniqueMap.set(key, r);
});
let rows = "";
Array.from(uniqueMap.values()).forEach((p) => {
// On applique aussi le décalage du vendredi pour l'affichage propre dans la modale
let d1Date = new Date(p.start_date.split("T")[0] + "T00:00:00");
if (d1Date.getDay() === 5) d1Date.setDate(d1Date.getDate() + 1);
const d1 = d1Date.toLocaleDateString(window.appLang || "fr-FR");
const d2 = new Date(p.end_date.split("T")[0]).toLocaleDateString(
window.appLang || "fr-FR",
);
rows += `${p.description} ${d1} ${d2} `;
});
tbody.innerHTML = rows;
} else {
tbody.innerHTML = `Aucune vacance trouvée pour ${yearStr}. `;
}
} catch (e) {
tbody.innerHTML =
"Erreur de connexion à l'API du gouvernement. ";
}
}
reprocessAndRender() {
this.reprocessEvents();
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 += `${d} `));
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 += `
${content} `;
currentRenderedCols++;
}
while (currentRenderedCols < 5 && currentRenderedCols > 0) {
html += `
`;
currentRenderedCols++;
}
return html + `
`;
}
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 ? `${trashSvg} ` : ""}
`;
let html = ``;
// 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 += ``;
}
});
}
// 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 += ``;
}
}
// 3. Maladie Enfants
if (this.kids.length > 0) {
html += ``;
}
// 4. Congés Parents (Répartition en 2 colonnes !)
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();
});