`;
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) {
if (
!confirm(
`Supprimer définitivement le congé "${label}" du catalogue ? Cela le retirera également des membres qui l'utilisent.`,
)
)
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) {
alert("Erreur de suppression : " + err.message);
}
}
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)
return window.showToast
? showToast(tr("fc_err_code_label"), "error")
: alert(tr("fc_err_code_label"));
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) {
alert(tr("error_occured") + " " + err.message);
}
}
// ==========================================
// 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 = '
`;
}
// --- 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) {
return window.showToast
? showToast("Veuillez sélectionner un type de congé", "error")
: alert("Veuillez sélectionner un type de congé");
}
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) {
if (window.showToast) showToast("Erreur : " + err.message, "error");
else alert("Erreur: " + err.message);
}
}
async function deleteMemberLeave(id, personId) {
if (
!confirm(
tr("confirm_delete") ||
"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) {
if (window.showToast)
showToast(
(tr("error_occured") || "Erreur") + " : " + err.message,
"error",
);
else alert("Erreur: " + err.message);
}
}
// ============================================================================
// 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.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 || {};
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);
}
// 🔥 LE FIX (refreshAllData avec schoolHols injecté et fusionné)
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 += `
`;
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 off = 0;
let extra = 0;
let sick = 0;
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);
dayEvents.forEach((e) => {
const dur = parseFloat(e.duration) || 1;
if (e.type === "HELPER_OFF") off += dur;
if (e.type === "HELPER_EXTRA") extra += 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;
}
});
}
// Calcul final strict : Jours ouvrés - Maladie (de l'enfant) - Extra - Off
const presence = Math.max(0, workingDays - sick - extra - off);
// Rendu HTML individuel
html += `
`;
}
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 = `
`;
}
// ⚠️ 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 = `