2692 lines
107 KiB
JavaScript
2692 lines
107 KiB
JavaScript
/**
|
||
* 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 `<option value="${p.id}" data-role="${p.role}">${p.name} (${roleDisplay})</option>`;
|
||
})
|
||
.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) {
|
||
alert((tr("fc_error") || "Erreur : ") + err.message);
|
||
}
|
||
}
|
||
|
||
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) => `
|
||
<span class="bs-rule-tag" style="background:var(--bg-panel); font-size:0.85rem; padding: 4px 10px; display:inline-flex; align-items:center; gap:6px; border-radius:12px;">
|
||
${mode} <b style="cursor:pointer; color:var(--danger); font-size:1.1rem; line-height:1;" onclick="removeCareModeTag(${i})">×</b>
|
||
</span>
|
||
`,
|
||
)
|
||
.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",
|
||
);
|
||
else alert(tr("fc_settings_updated") || "Configuration enregistrée !");
|
||
|
||
setTimeout(() => window.location.reload(), 800);
|
||
} catch (err) {
|
||
alert("Erreur : " + err.message);
|
||
}
|
||
}
|
||
|
||
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");
|
||
else alert(tr("fc_child_saved") || "Sauvegardé !");
|
||
|
||
const currentPerson = calGlobalData.people.find(
|
||
(k) => parseInt(k.id) === window.currentSelectedMemberId,
|
||
);
|
||
if (currentPerson) {
|
||
currentPerson.care_modes = JSON.stringify(selectedModes);
|
||
}
|
||
} catch (err) {
|
||
alert("Erreur: " + err.message);
|
||
}
|
||
};
|
||
|
||
// ==========================================
|
||
// 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 = `<p class="pf-muted-note">${tr("fc_no_leave_configured") || "Aucun congé configuré."}</p>`;
|
||
return;
|
||
}
|
||
|
||
types.forEach((lt) => {
|
||
// Fini l'injection des variables de report ici, elles n'existent plus au niveau global !
|
||
container.innerHTML += `
|
||
<div style="display: flex; justify-content: space-between; align-items: center; background: var(--bg-page); padding: 8px 12px; border-radius: 8px; border: 1px solid var(--border-light); margin-bottom: 8px;">
|
||
<div>
|
||
<span style="font-weight: 700; color: var(--text-main); font-size: 1.05rem;">${lt.label}</span>
|
||
<span style="background: var(--bg-subtle); border: 1px solid var(--border-strong); border-radius: 6px; padding: 2px 8px; font-size: 0.75rem; margin-left: 8px; color: var(--text-muted); font-weight: bold;">${lt.code}</span>
|
||
</div>
|
||
<div style="display: flex; gap: 8px;">
|
||
<button type="button" class="btn-icon-action edit" onclick="editLeaveType('${lt.code}', '${lt.label.replace(/'/g, "\\'")}')" title="${tr("fc_edit_leave") || "Modifier"}">✏️</button>
|
||
<button type="button" class="btn-icon-action delete" onclick="deleteLeaveType('${lt.code}', '${lt.label.replace(/'/g, "\\'")}')" title="${tr("btn_delete") || "Supprimer"}">🗑️</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
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 = '<p class="pf-muted-note">Chargement...</p>';
|
||
|
||
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) => `
|
||
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; font-size:0.9rem;">
|
||
<input type="checkbox" class="js-care-mode-cb" value="${m}" ${savedModes.includes(m) ? "checked" : ""}> ${m}
|
||
</label>
|
||
`,
|
||
)
|
||
.join("");
|
||
|
||
zone.innerHTML = `
|
||
<h5 style="margin:0 0 8px 0; color:var(--text-main);">${tr("fc_care_modes_title") || "Modes de Garde"}</h5>
|
||
<p style="font-size:0.85rem; color:var(--text-muted); margin-bottom:12px;">Quels modes de garde s'appliquent à cet enfant ?</p>
|
||
<div style="display:flex; flex-direction:column; gap:6px;">
|
||
${modesHtml || `<em>Aucun mode de garde configuré dans le foyer.</em>`}
|
||
</div>
|
||
<button class="pf-btn pf-btn-primary" style="margin-top:15px; width:100%;" onclick="submitChildCareModes()">${tr("btn_save_rights") || "Enregistrer"}</button>
|
||
`;
|
||
} 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 =
|
||
'<p class="pf-muted-note" style="color:var(--danger);">Erreur de chargement.</p>';
|
||
}
|
||
}
|
||
}
|
||
|
||
function renderMemberLeavesView(personId, memberLeaves) {
|
||
const zone = document.getElementById("memberConfigZone");
|
||
let html = `<h5 style="margin: 0 0 15px 0; color: var(--text-main); font-size: 1.05rem;">Congés attribués</h5>`;
|
||
|
||
if (memberLeaves.length === 0) {
|
||
html += `<p class="pf-muted-note" style="font-size: 0.9rem; font-style: italic;">Aucun congé attribué à ce membre.</p>`;
|
||
} else {
|
||
html += `<div style="display: flex; flex-direction: column; gap: 12px; margin-bottom: 25px;">`;
|
||
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 = `
|
||
<span style="background: var(--bg-page); border: 1px solid var(--border-light); padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; color: var(--text-muted); font-weight: 600;">
|
||
⚖️ ${ml.allowance} j ${isGraduel ? "/ mois" : "/ an"}
|
||
</span>
|
||
<span style="background: var(--bg-page); border: 1px solid var(--border-light); padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; color: var(--text-muted); font-weight: 600;">
|
||
🔄 Mois ${moisRenouv}
|
||
</span>
|
||
`;
|
||
|
||
if (ml.carry_over_max_days > 0) {
|
||
badgesHtml += `
|
||
<span style="background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.3); padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; color: #d97706; font-weight: 600;">
|
||
⚠️ Report Max: ${ml.carry_over_max_days}j (Exp. M${ml.carry_over_deadline_month})
|
||
</span>`;
|
||
}
|
||
|
||
html += `
|
||
<div style="display: flex; justify-content: space-between; align-items: flex-start; background: var(--bg-subtle); padding: 12px 15px; border-radius: 10px; border: 1px solid var(--border-light);">
|
||
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||
<strong style="color: var(--text-main); font-size: 1rem; line-height: 1;">${ml.leave_type} <span style="font-weight: normal; font-size: 0.8rem; color: var(--text-muted);">— ${methodeLabel}</span></strong>
|
||
<div style="display: flex; gap: 6px; flex-wrap: wrap;">
|
||
${badgesHtml}
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; gap: 6px; margin-left: 15px;">
|
||
<button class="pf-btn btn-secondary" style="padding: 6px; height: 32px; width: 32px; display: flex; align-items: center; justify-content: center;" onclick="editMemberLeave(${ml.id}, '${ml.leave_type}', ${ml.allowance}, ${moisRenouv}, '${ml.method}', ${ml.carry_over_max_days}, ${ml.carry_over_deadline_month})" title="Modifier">✏️</button>
|
||
<button class="pf-btn btn-secondary" style="padding: 6px; height: 32px; width: 32px; display: flex; align-items: center; justify-content: center; color: var(--danger);" onclick="deleteMemberLeave(${ml.id}, ${personId})" title="Supprimer">🗑️</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
html += `</div>`;
|
||
}
|
||
|
||
// --- LE FORMULAIRE RESTRUCTURÉ EN GRILLE ---
|
||
html += `
|
||
<hr style="border: 0; border-top: 1px solid var(--border-light); margin: 20px 0;">
|
||
<h5 id="formMemberLeaveTitle" style="margin: 0 0 15px 0; font-size: 1.05rem; color: var(--text-main);">+ Attribuer un congé</h5>
|
||
<input type="hidden" id="new-member-leave-id" value="">
|
||
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 15px;">
|
||
<div>
|
||
<label class="pf-label" style="font-size: 0.85rem;">Type de congé</label>
|
||
<select id="new-member-leave-type" class="pf-input">
|
||
<option value="" disabled selected>-- Sélectionner --</option>
|
||
${globalLeaveCatalog.map((lt) => `<option value="${lt.code}">${lt.label}</option>`).join("")}
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="pf-label" style="font-size: 0.85rem;">Acquisition</label>
|
||
<select id="new-member-leave-method" class="pf-input">
|
||
<option value="FIXED">Fixe (Annuelle)</option>
|
||
<option value="ACCUMULATED">Graduelle (Mensuelle)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div title="${tr("fc_allowance_help") || "Fixe: Quota annuel. Graduel: Ajout par mois."}">
|
||
<label class="pf-label" style="font-size: 0.85rem;">Quota</label>
|
||
<input type="number" step="0.01" id="new-member-leave-allowance" class="pf-input" value="0">
|
||
</div>
|
||
|
||
<div>
|
||
<label class="pf-label" style="font-size: 0.85rem;">Renouvellement</label>
|
||
<select id="new-member-leave-reset" class="pf-input">
|
||
<option value="1">${tr("month_01") || "Janvier"}</option>
|
||
<option value="2">${tr("month_02") || "Février"}</option>
|
||
<option value="3">${tr("month_03") || "Mars"}</option>
|
||
<option value="4">${tr("month_04") || "Avril"}</option>
|
||
<option value="5">${tr("month_05") || "Mai"}</option>
|
||
<option value="6">${tr("month_06") || "Juin"}</option>
|
||
<option value="7">${tr("month_07") || "Juillet"}</option>
|
||
<option value="8">${tr("month_08") || "Août"}</option>
|
||
<option value="9">${tr("month_09") || "Septembre"}</option>
|
||
<option value="10">${tr("month_10") || "Octobre"}</option>
|
||
<option value="11">${tr("month_11") || "Novembre"}</option>
|
||
<option value="12">${tr("month_12") || "Décembre"}</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- BLOC AVANCÉ : RÈGLES DE REPORT -->
|
||
<div style="margin-top: 15px; padding: 12px; background: var(--bg-page); border-left: 3px solid var(--border-strong); border-radius: 4px 8px 8px 4px;">
|
||
<p style="margin: 0 0 10px 0; font-size: 0.85rem; font-weight: 600; color: var(--text-muted);">⚙️ Règles de report (Optionnel)</p>
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 15px;">
|
||
<div>
|
||
<label class="pf-label" style="font-size: 0.8rem;">${tr("fc_carry_max") || "Max Reportable"}</label>
|
||
<input type="number" step="0.5" id="new-member-leave-carry-max" class="pf-input" value="0" placeholder="ex: 2">
|
||
</div>
|
||
<div>
|
||
<label class="pf-label" style="font-size: 0.8rem;">${tr("fc_carry_deadline") || "Mois Expiration"}</label>
|
||
<select id="new-member-leave-carry-deadline" class="pf-input">
|
||
<option value="1">${tr("month_01") || "Janvier"}</option>
|
||
<option value="2">${tr("month_02") || "Février"}</option>
|
||
<option value="3">${tr("month_03") || "Mars"}</option>
|
||
<option value="4">${tr("month_04") || "Avril"}</option>
|
||
<option value="12" selected>${tr("fc_deadline_none") || "Aucune"}</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
|
||
<button class="pf-btn btn-secondary" style="display: none;" id="btn-cancel-member-leave" onclick="resetMemberLeaveForm()">${tr("btn_cancel") || "Annuler"}</button>
|
||
<button class="pf-btn pf-btn-primary" id="btn-submit-member-leave" onclick="addMemberLeave(${personId})">Ajouter / Enregistrer</button>
|
||
</div>
|
||
`;
|
||
|
||
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.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 += `<option value="${y}" ${y === this.currentSchoolYearStart ? "selected" : ""}>${y} - ${y + 1}</option>`;
|
||
}
|
||
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}
|
||
<select id="holidayYearSelect" class="pf-input" style="width:auto; display:inline-block; margin-left:10px; padding:4px 10px; height:auto;">
|
||
${options}
|
||
</select>`;
|
||
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 = `<option value="all" ${mStr === "all" ? "selected" : ""}>${trLang.fc_full_year || "Année complète"}</option>`;
|
||
optionsHtml += `<option value="custom" ${mStr === "custom" ? "selected" : ""}>${trLang.fc_custom_period || "Période Spécifique..."}</option>`;
|
||
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 += `<option value="${i + 1}" ${mStr == i + 1 ? "selected" : ""}>${capitalizedMonth}</option>`;
|
||
}
|
||
|
||
let yearOptionsHtml = "";
|
||
const currentY = new Date().getFullYear();
|
||
for (let yr = currentY - 2; yr <= currentY + 5; yr++) {
|
||
yearOptionsHtml += `<option value="${yr}" ${y == yr ? "selected" : ""}>${yr}</option>`;
|
||
}
|
||
|
||
const isCustomDisplay = isCustom ? "flex" : "none";
|
||
const isYearDisplay = isCustom ? "none" : "block";
|
||
|
||
// --- GÉNÉRATION DU HTML (Design Ultra-Compact + Micro Pilules + Flexbox Corrigé) ---
|
||
let cardHtml = `
|
||
<div style="background: var(--bg-panel); border: 1px solid var(--border-light); border-radius: 12px; padding: 12px 20px; margin-bottom: 20px; box-shadow: var(--pf-shadow-sm); display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 15px; width: 100%;">
|
||
|
||
<!-- Zone Gauche : Identité + KPI + Pilules -->
|
||
<div style="display: flex; align-items: center; gap: 20px; flex-wrap: wrap; flex: 1;">
|
||
|
||
<!-- Identité -->
|
||
<div style="display: flex; flex-direction: column; gap: 2px;">
|
||
<span style="font-size: 0.95rem; font-weight: 700; color: var(--text-main); display: flex; align-items: center; gap: 6px;">📅 ${h.name}</span>
|
||
<span style="font-size: 0.65rem; color: var(--text-muted); text-transform: uppercase; font-weight: 700; letter-spacing: 0.05em;">${window.I18N?.fc_recap_title || "Récapitulatif"}</span>
|
||
</div>
|
||
|
||
<!-- Jours Travaillés -->
|
||
<div style="display: flex; align-items: baseline; gap: 6px; padding-right: 20px; border-right: 1px solid var(--border-light);">
|
||
<span style="font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; font-weight: 700;">${window.I18N?.fc_worked_est || "Travaillés (Est.)"}</span>
|
||
<span style="font-size: 1.4rem; font-weight: 800; color: var(--primary); line-height: 1;">${hasValidDates ? workedDays : "-"} <span style="font-size: 0.85rem;">j</span></span>
|
||
</div>
|
||
|
||
<!-- Les Micro-Pilules de Congés -->
|
||
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">`;
|
||
|
||
if (!hasValidDates && isCustom) {
|
||
cardHtml += `<div style="font-size: 0.85rem; color: var(--danger); font-weight: bold;">${window.I18N?.fc_invalid_dates || "Sélectionnez des dates valides."}</div>`;
|
||
} 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 += `
|
||
<div style="display: flex; align-items: center; gap: 8px; background: var(--bg-page); border: 1px solid var(--border-light); border-left: 3px solid ${borderColor}; border-radius: 6px; padding: 4px 10px;">
|
||
<span style="font-size: 0.75rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase;">${label}</span>
|
||
<span style="font-size: 1rem; font-weight: 800; color: var(--text-main); line-height: 1;">${val}<span style="font-size: 0.8rem; margin-left: 2px;">j</span></span>
|
||
</div>`;
|
||
}
|
||
}
|
||
if (!hasLeaves)
|
||
cardHtml += `<div style="font-size: 0.85rem; color: var(--text-muted); font-style: italic;">${window.I18N?.fc_no_leaves_taken || "Aucun congé posé."}</div>`;
|
||
}
|
||
|
||
cardHtml += `
|
||
</div>
|
||
</div> <!-- /Zone Gauche -->
|
||
|
||
<!-- Zone Droite : Sélecteurs de Période Dynamiques -->
|
||
<div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap;">
|
||
<select class="pf-input" style="padding: 4px 8px; width: auto; height: 32px; font-size: 0.85rem; border-color: var(--border-strong); background: var(--bg-page);" onchange="updateHiddenRecap(this, 'month')">
|
||
${optionsHtml}
|
||
</select>
|
||
<select class="pf-input" style="padding: 4px 8px; width: auto; height: 32px; font-size: 0.85rem; border-color: var(--border-strong); background: var(--bg-page); display: ${isYearDisplay};" onchange="updateHiddenRecap(this, 'year')">
|
||
${yearOptionsHtml}
|
||
</select>
|
||
|
||
<div style="display: ${isCustomDisplay}; align-items: center; gap: 6px; background: var(--bg-page); border: 1px solid var(--border-strong); border-radius: 6px; padding: 2px 6px;">
|
||
<input type="date" style="border: none; background: transparent; padding: 2px; height: 26px; font-size: 0.8rem; width: 115px; color: var(--text-main); font-family: inherit;" value="${hiddenStartInp.value}" onchange="updateHiddenRecap(this, 'start')">
|
||
<span style="color: var(--text-muted); font-size: 0.8rem;">➔</span>
|
||
<input type="date" style="border: none; background: transparent; padding: 2px; height: 26px; font-size: 0.8rem; width: 115px; color: var(--text-main); font-family: inherit;" value="${hiddenEndInp.value}" onchange="updateHiddenRecap(this, 'end')">
|
||
</div>
|
||
</div>
|
||
|
||
</div>`;
|
||
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 =
|
||
"<tr><td colspan='3' style='text-align:center;'>Zone 'Autre' sélectionnée. Pas de données auto.</td></tr>";
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML =
|
||
"<tr><td colspan='3' style='text-align:center; padding: 20px;'>Chargement des données du Ministère... ⏳</td></tr>";
|
||
|
||
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 += `<tr><td><strong>${p.description}</strong></td><td>${d1}</td><td>${d2}</td></tr>`;
|
||
});
|
||
tbody.innerHTML = rows;
|
||
} else {
|
||
tbody.innerHTML = `<tr><td colspan='3' style='text-align:center; padding: 20px;'>Aucune vacance trouvée pour ${yearStr}.</td></tr>`;
|
||
}
|
||
} catch (e) {
|
||
tbody.innerHTML =
|
||
"<tr><td colspan='3' style='color:red; text-align:center;'>Erreur de connexion à l'API du gouvernement.</td></tr>";
|
||
}
|
||
}
|
||
|
||
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 = `<tr><td colspan='15' style='text-align:center; padding: 20px; color: var(--text-muted);'>${window.I18N?.["fc_no_data"] || "Aucune donnée pour cette année scolaire."}</td></tr>`;
|
||
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 = `<span>${w.monthName || ""}</span>`;
|
||
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 = `<span>${w.weekLabel || ""}</span>`;
|
||
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 = `<div style="position:relative; height:100%; width:100%; min-height:40px;">
|
||
<span style="display:block; padding:2px;">${String(dateObj.getDate()).padStart(2, "0")}</span>`;
|
||
|
||
// Icônes des modes de garde
|
||
let iconsHtml = `<div style="position:absolute; top:2px; right:2px; display:flex; gap:2px;">`;
|
||
events.forEach((evt) => {
|
||
if (upperCareModes.includes(evt.type)) {
|
||
const modeName = String(evt.type).toLowerCase();
|
||
if (modeName === "avis") {
|
||
iconsHtml += `<img src="/modules/family-calendar/assets/img/avis.svg" class="fc-icon-avis" title="Avis" style="width:14px; height:14px; object-fit:contain;">`;
|
||
} else if (modeName === "centre") {
|
||
iconsHtml += `<span class="fc-icon-centre" title="Centre" style="font-size:1.1rem; line-height:1;">🏫</span>`;
|
||
} else {
|
||
iconsHtml += `<span style="background:var(--primary); color:#fff; border-radius:3px; padding:0 3px; font-size:9px;">${modeName.substring(0, 3)}</span>`;
|
||
}
|
||
}
|
||
});
|
||
content += iconsHtml + `</div>`;
|
||
|
||
// Icônes Enfant Malade
|
||
let sickHtml = `<div style="position:absolute; bottom:2px; right:2px; display:flex; flex-direction:column; align-items:flex-end; gap:1px; font-size:11px; font-weight:bold; line-height:1;">`;
|
||
events.forEach((evt) => {
|
||
if (evt.type === "CHILD_SICK") {
|
||
const k = (this.kids || []).find(
|
||
(x) => parseInt(x.id) === parseInt(evt.person_id),
|
||
);
|
||
if (k) {
|
||
sickHtml += `<span style="color:${k.color || "#e11d48"};">${k.name}<span style="font-size:10px;">🤒</span></span>`;
|
||
}
|
||
}
|
||
});
|
||
content += sickHtml + `</div>`;
|
||
|
||
// Initiales des Parents en congé
|
||
if (dayLeaves.length) {
|
||
let parentLeavesHtml = `<div style="position:absolute; bottom:0; left:0; width:100%; font-size:9px; display:flex; justify-content:center; gap:2px; pointer-events:none;">`;
|
||
(this.parents || []).forEach((person) => {
|
||
if (
|
||
dayLeaves.some(
|
||
(l) =>
|
||
parseInt(l.person_id || l.person) === parseInt(person.id),
|
||
)
|
||
) {
|
||
parentLeavesHtml += `<span style="color:${person.color || "#000"}; font-weight:800; margin: 0 1px;">${String(person.name).charAt(0).toUpperCase()}</span>`;
|
||
}
|
||
});
|
||
content += parentLeavesHtml + `</div>`;
|
||
}
|
||
|
||
td.innerHTML = content + `</div>`;
|
||
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 = `<tr><td colspan="15" style="color:red; font-weight:bold; padding:20px; text-align:center;">Erreur d'affichage : ${e.message}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
generateMonthHTML(year, month) {
|
||
let html = `<table class="fc-month-table"><thead><tr>`;
|
||
["L", "M", "M", "J", "V"].forEach((d) => (html += `<th>${d}</th>`));
|
||
html += `</tr></thead><tbody><tr>`;
|
||
|
||
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 += `<td class="fc-day--other-month"></td>`;
|
||
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 += `</tr><tr>`;
|
||
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 = `<div style="position:relative; height:100%; min-height:55px;"><span class="fc-day-number">${d}</span>`;
|
||
|
||
let iconsHtml = `<div style="position:absolute; top:2px; right:2px; display:flex; gap:2px;">`;
|
||
dayEvts.forEach((evt) => {
|
||
if (upperCareModes.includes(evt.type)) {
|
||
const modeName = evt.type.toLowerCase();
|
||
if (modeName === "avis")
|
||
iconsHtml += `<img src="/modules/family-calendar/assets/img/avis.svg" class="fc-icon-avis" title="Avis" style="width:14px; height:14px; object-fit:contain;">`;
|
||
else if (modeName === "centre")
|
||
iconsHtml += `<span class="fc-icon-centre" title="Centre" style="font-size:1.1rem; line-height:1;">🏫</span>`;
|
||
else
|
||
iconsHtml += `<span style="background:var(--primary); color:#fff; border-radius:3px; padding:0 3px; font-size:9px;">${modeName.substring(0, 3)}</span>`;
|
||
}
|
||
});
|
||
content += iconsHtml + `</div>`;
|
||
|
||
let sickHtml = `<div style="position:absolute; bottom:2px; right:2px; display:flex; flex-direction:column; align-items:flex-end; gap:1px; font-weight:bold; line-height:1;">`;
|
||
dayEvts.forEach((evt) => {
|
||
if (evt.type === "CHILD_SICK") {
|
||
const k = this.kids.find(
|
||
(x) => parseInt(x.id) === parseInt(evt.person_id),
|
||
);
|
||
if (k) {
|
||
sickHtml += `<span style="color:${k.color || "#e11d48"}; font-size:10px;">${k.name} 🤒</span>`;
|
||
}
|
||
}
|
||
});
|
||
content += sickHtml + `</div>`;
|
||
|
||
if (dayLeaves.length) {
|
||
content += `<div style="position:absolute; bottom:2px; left:2px; font-size:10px; font-weight:bold;">`;
|
||
this.parents.forEach((parent) => {
|
||
if (
|
||
dayLeaves.some(
|
||
(l) => parseInt(l.person_id) === parseInt(parent.id),
|
||
)
|
||
) {
|
||
content += `<span style="color:${parent.color || "#333"}">${parent.name.charAt(0).toUpperCase()}</span> `;
|
||
}
|
||
});
|
||
content += `</div>`;
|
||
}
|
||
html += `<td class="${cls}" data-date="${iso}">${content}</div></td>`;
|
||
currentRenderedCols++;
|
||
}
|
||
|
||
while (currentRenderedCols < 5 && currentRenderedCols > 0) {
|
||
html += `<td class="fc-day--other-month"></td>`;
|
||
currentRenderedCols++;
|
||
}
|
||
return html + `</tr></tbody></table>`;
|
||
}
|
||
|
||
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 += `
|
||
<div style="margin-top: 10px;">
|
||
<div style="font-size: 0.8rem; font-weight: bold; color: var(--text-muted); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.05em;">
|
||
👧👦 ${trPresence} ${kid.name} (${helperName})
|
||
</div>
|
||
<div class="fc-month-summary-inline" style="display:flex; justify-content:space-around; gap:10px; font-size:0.75rem; background: var(--bg-panel); padding: 8px; border-radius: 8px; border: 1px solid var(--border-light);">
|
||
<div class="fc-summ-pill" style="display:flex; flex-direction:column; align-items:center; flex:1;"><span>${trWorking}</span> <strong style="color:var(--text-main); font-size:0.95rem;">${workingDays} j</strong></div>
|
||
<div class="fc-summ-pill" style="display:flex; flex-direction:column; align-items:center; flex:1;"><span>${trAbs} ${helperName}</span> <strong style="color:var(--text-main); font-size:0.95rem;">${parseFloat(absencesNounou.toFixed(1))} j</strong></div>
|
||
<div class="fc-summ-pill" style="display:flex; flex-direction:column; align-items:center; flex:1;"><span>${trSick}</span> <strong style="color:var(--danger); font-size:0.95rem;">${parseFloat(sick.toFixed(1))} j</strong></div>
|
||
<div class="fc-summ-pill pf-presence-pill" style="display:flex; flex-direction:column; align-items:center; flex:1;"><span>${trPresence}</span> <strong style="color:var(--primary); font-size:0.95rem;">${parseFloat(presence.toFixed(1))} j</strong></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
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 = `<div class="fc-month-container">${this.generateMonthHTML(y, m)}${this.generateMonthSummaryHTML(y, m)}</div>`;
|
||
}
|
||
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 = `<div class="fc-two-months-container">
|
||
<div class="fc-month-container"><div class="fc-month-title">${new Intl.DateTimeFormat(lang, { month: "long" }).format(this.currentMonth)}</div>${this.generateMonthHTML(y, m)}${this.generateMonthSummaryHTML(y, m)}</div>
|
||
<div class="fc-month-container"><div class="fc-month-title">${new Intl.DateTimeFormat(lang, { month: "long" }).format(nextDate)}</div>${this.generateMonthHTML(nextDate.getFullYear(), nextDate.getMonth())}${this.generateMonthSummaryHTML(nextDate.getFullYear(), nextDate.getMonth())}</div>
|
||
</div>`;
|
||
}
|
||
|
||
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 = `<div class="fc-three-months-container">`;
|
||
const lang = window.appLang || "fr-FR";
|
||
[d1, d2, d3].forEach(
|
||
(d) =>
|
||
(html += `<div class="fc-month-container"><div class="fc-month-title">${new Intl.DateTimeFormat(lang, { month: "long" }).format(d)}</div>${this.generateMonthHTML(d.getFullYear(), d.getMonth())}${this.generateMonthSummaryHTML(d.getFullYear(), d.getMonth())}</div>`),
|
||
);
|
||
this.monthCalendar.innerHTML = html + `</div>`;
|
||
}
|
||
|
||
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 = `<div class="fc-minimal-balance-card"><strong style="color:${person.color || "#333"}">${person.name.toUpperCase()}</strong><div class="fc-minimal-chips">`;
|
||
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 = `<div class="fc-burn-alert" title="${tr("fc_alert_burn_jra") || "Alerte : Jours reportés expirent à la fin de ce mois !"}">🔥</div>`;
|
||
}
|
||
// ⚠️ 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 = `<div class="fc-burn-alert" title="${alertMsg}">🔥</div>`;
|
||
}
|
||
|
||
cards += `<div class="fc-min-chip" title="Solde: ${fmt(startBal)}"><span class="type">${conf.type || type}</span><span class="val">${fmt(endBal)}</span>${totalUsed > 0 ? `<span class="fc-used-badge">-${fmt(totalUsed)}</span>` : ""}${alertHtml}</div>`;
|
||
});
|
||
return cards + `</div></div>`;
|
||
})
|
||
.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) {
|
||
alert(tr("error_occured") + " " + err.message);
|
||
} 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 = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path></svg>`;
|
||
|
||
// Ajout de la classe "btn-icon-action delete" pour activer le hover natif (rouge clair)
|
||
const buildHeader = (title, action, cat, pid) =>
|
||
`<div style="display:flex; justify-content:space-between; align-items:center; font-size:0.65rem; font-weight:700; color:var(--text-muted); text-transform:uppercase; margin: 8px 0 4px 0;"><span>${title}</span>${action ? `<button type="button" class="btn-icon-action delete" style="width:24px; height:24px; padding:4px;" data-action="${action}" data-cat="${cat}" ${pid ? `data-pid="${pid}"` : ""}>${trashSvg}</button>` : ""}</div>`;
|
||
|
||
let html = `<div class="fc-menu-section" style="padding-bottom: 4px;"><strong style="font-size:0.85rem; color:var(--text-main); text-transform:capitalize;">${dateLabel}</strong></div>`;
|
||
|
||
// 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 += `<div class="fc-menu-section">${buildHeader(h.name, "clear-leaves-person", null, h.id)}<div class="fc-menu-grid">`;
|
||
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 += `<button class="fc-menu-btn" style="${getActiveStyleL(lType, h.id, hColor)}" data-action="add-leave" data-pid="${h.id}" data-type="${lType}">${lType}</button>`;
|
||
});
|
||
html += `</div></div>`;
|
||
}
|
||
});
|
||
}
|
||
|
||
// 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 += `<div class="fc-menu-section">${buildHeader(trLang.fc_care_modes_title || "Modes de garde", "clear-type", "CARE_MODE", null)}<div class="fc-menu-grid">`;
|
||
displayModes.forEach((m) => {
|
||
let iconHtml =
|
||
m.toLowerCase() === "avis"
|
||
? `<img src="/modules/family-calendar/assets/img/avis.svg" class="fc-icon-avis" title="Avis" style="width:14px; height:14px; object-fit:contain; margin-right:4px;"> `
|
||
: m.toLowerCase() === "centre"
|
||
? `<span class="fc-icon-centre" style="font-size:1.1rem; line-height:1; margin-right:4px;">🏫</span> `
|
||
: "";
|
||
const modeType = m.toUpperCase();
|
||
html += `<button class="fc-menu-btn" style="display:flex; align-items:center; ${getActiveStyleE(modeType, "0", "var(--primary)")}" data-action="add" data-type="${modeType}" data-person="0">${iconHtml}${m}</button>`;
|
||
});
|
||
html += `</div></div>`;
|
||
}
|
||
}
|
||
|
||
// 3. Maladie Enfants
|
||
if (this.kids.length > 0) {
|
||
html += `<div class="fc-menu-section">${buildHeader(trLang.fc_menu_sick || "Maladie", "clear-type", "CHILD_SICK", null)}<div class="fc-menu-grid" style="grid-template-columns: 1fr;">`;
|
||
this.kids.forEach((k) => {
|
||
const color = k.color || "var(--danger)";
|
||
html += `<button class="fc-menu-btn" style="${getActiveStyleE("CHILD_SICK", k.id, color)}" data-action="add" data-type="CHILD_SICK" data-person="${k.id}">${k.name} 🤒</button>`;
|
||
});
|
||
html += `</div></div>`;
|
||
}
|
||
|
||
// 4. Congés Parents (Répartition en 2 colonnes !)
|
||
html += `<div class="fc-menu-section" style="border-bottom:none;">${buildHeader(trLang.fc_menu_adult_leaves || "Congés Parents", null, null, null)}`;
|
||
html += `<div style="display:grid; grid-template-columns: 1fr 1fr; gap:12px; align-items:start; font-size:0.8rem;">`;
|
||
|
||
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 += `<div style="display:flex; flex-direction:column; gap:6px; background:var(--bg-subtle); padding:6px; border-radius:8px; border:1px solid var(--border-light);">`;
|
||
html += `<div style="font-weight:600; color:${pColor}; display:flex; align-items:center; justify-content:space-between;">
|
||
${p.name} <button type="button" class="btn-icon-action delete" style="width:24px; height:24px; padding:4px;" data-action="clear-leaves-person" data-pid="${p.id}">${trashSvg}</button>
|
||
</div>`;
|
||
html += `<div style="display:flex; gap:4px; flex-wrap:wrap;">`;
|
||
Array.from(allParentLeaveTypes).forEach((t) => {
|
||
if (
|
||
(this.leaveMatrix[p.id] || []).some(
|
||
(l) => (typeof l === "object" ? l.type : l) === t,
|
||
)
|
||
) {
|
||
html += `<button class="fc-menu-btn" style="padding: 4px 6px; font-size:0.75rem; width:auto; flex-grow:1; text-align:center; ${getActiveStyleL(t, p.id, pColor)}" data-action="add-leave" data-pid="${p.id}" data-type="${t}">${t}</button>`;
|
||
}
|
||
});
|
||
html += `</div></div>`;
|
||
});
|
||
html += `</div></div>`;
|
||
|
||
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) {
|
||
alert("Erreur: " + e.message);
|
||
}
|
||
}
|
||
}
|
||
|
||
new FamilyCalendar();
|
||
});
|