914 lines
33 KiB
JavaScript
914 lines
33 KiB
JavaScript
/**
|
||
* family-calendar.js (FIXED: Selection, View Buttons, Colors)
|
||
*/
|
||
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
const CONGE_TYPES = ["OFF_CAROLE", "EXTRA_OFF_CAROLE"];
|
||
const GUARDE_TYPES = ["CENTRE", "AVIS"];
|
||
const PEP_TYPES = ["PEP_SICK"];
|
||
const MODIFIABLE_TYPES = [...CONGE_TYPES, ...GUARDE_TYPES, ...PEP_TYPES];
|
||
|
||
class FamilyCalendar {
|
||
constructor() {
|
||
this.planningBody = document.getElementById("planningBody");
|
||
this.selectionMenu = document.getElementById("selectionMenu");
|
||
// Menu dans le body pour position absolue correcte
|
||
if (
|
||
this.selectionMenu &&
|
||
this.selectionMenu.parentElement !== document.body
|
||
) {
|
||
document.body.appendChild(this.selectionMenu);
|
||
}
|
||
this.schoolHolidaysTableBody = document.querySelector(
|
||
"#schoolHolidaysTable tbody",
|
||
);
|
||
this.monthCalendar = document.getElementById("fc-month-calendar");
|
||
this.monthSelectionMenu = document.getElementById(
|
||
"fc-month-selectionMenu",
|
||
);
|
||
if (
|
||
this.monthSelectionMenu &&
|
||
this.monthSelectionMenu.parentElement !== document.body
|
||
) {
|
||
document.body.appendChild(this.monthSelectionMenu);
|
||
}
|
||
|
||
// Etat
|
||
this.currentMonth = new Date();
|
||
this.currentMonth.setDate(1);
|
||
this.viewMode = "1month";
|
||
this.currentSchoolYearStart = null;
|
||
|
||
this.isSelecting = false;
|
||
this.selectedCells = [];
|
||
this.monthSelectedCells = [];
|
||
this.isMonthSelecting = false;
|
||
this._currentBulkInfo = null;
|
||
|
||
// Données
|
||
this.dbEvents = [];
|
||
this.fixedEvents = [];
|
||
this.events = [];
|
||
this.leaves = [];
|
||
this.weeks = [];
|
||
this.monthlyLeaveBalances = {
|
||
2: { CP: {}, JRA: {}, JA: {} },
|
||
3: { CP: {}, JRA: {}, JA: {} },
|
||
};
|
||
|
||
if (!this.planningBody) return;
|
||
this.init();
|
||
}
|
||
|
||
async init() {
|
||
this.setupEventListeners();
|
||
const now = new Date();
|
||
this.currentSchoolYearStart =
|
||
now.getMonth() >= 8 ? now.getFullYear() : now.getFullYear() - 1;
|
||
await this.refreshAllData();
|
||
this.updateSchoolYearLabel();
|
||
}
|
||
|
||
async refreshAllData() {
|
||
try {
|
||
const weeksData = await this.fetchApi(
|
||
`/modules/family-calendar/includes/api/get-calendar-weeks-scolaire.php?school_year_start=${this.currentSchoolYearStart}`,
|
||
);
|
||
this.weeks = this.processWeeks(weeksData.weeks || []);
|
||
|
||
const eventsData = await this.fetchApi(
|
||
"/modules/family-calendar/includes/api/get-events.php",
|
||
);
|
||
this.dbEvents = (eventsData.events || []).map((e) => ({
|
||
...e,
|
||
duration: parseFloat(e.duration),
|
||
}));
|
||
|
||
this.fixedEvents = await this.fetchPublicAndSchoolHolidays();
|
||
|
||
const leavesData = await this.fetchApi(
|
||
"/modules/family-calendar/includes/api/get-leaves.php",
|
||
);
|
||
this.leaves = leavesData.leaves || [];
|
||
|
||
const balancesData = await this.fetchApi(
|
||
"/modules/family-calendar/includes/api/get-leave-balances.php",
|
||
);
|
||
this.leaveBalances = balancesData.balances || [];
|
||
|
||
this.events = [...this.dbEvents, ...this.fixedEvents];
|
||
this.publicHolidayDates = new Set(
|
||
this.fixedEvents
|
||
.filter((e) => e.type === "PUBLIC_HOLIDAY")
|
||
.map((e) => e.date),
|
||
);
|
||
|
||
this.reprocessAndRender();
|
||
} catch (e) {
|
||
console.error("Erreur chargement", e);
|
||
}
|
||
}
|
||
|
||
// Appel API pour les vacances scolaires (Zone C) avec dédoublonnage
|
||
async fetchPublicAndSchoolHolidays() {
|
||
// 1. Jours fériés 2025-2026 (Statique)
|
||
const publicHolidays = [
|
||
"2025-11-01",
|
||
"2025-11-11",
|
||
"2025-12-25",
|
||
"2026-01-01",
|
||
"2026-04-06",
|
||
"2026-05-01",
|
||
"2026-05-08",
|
||
"2026-05-14",
|
||
"2026-05-25",
|
||
"2026-07-14",
|
||
"2026-08-15",
|
||
].map((date, idx) => ({
|
||
id: `ph-${idx}`,
|
||
date,
|
||
type: "PUBLIC_HOLIDAY",
|
||
duration: 1,
|
||
}));
|
||
|
||
// 2. Vacances Scolaires (API Gouv)
|
||
const schoolHolidayEvents = [];
|
||
try {
|
||
const url =
|
||
"https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=annee_scolaire='2025-2026' AND zones LIKE '%Zone C%'&limit=100";
|
||
const res = await fetch(url);
|
||
const data = await res.json();
|
||
const rawRecords = data.results || [];
|
||
|
||
// --- DÉDOUBLONNAGE ---
|
||
// On utilise une Map pour ne garder qu'une entrée unique par (Description + Date Début)
|
||
const uniqueHolidaysMap = new Map();
|
||
|
||
rawRecords.forEach((r) => {
|
||
// Clé unique : "Vacances de Noël|2025-12-20"
|
||
const key = `${r.description}|${r.start_date}`;
|
||
if (!uniqueHolidaysMap.has(key)) {
|
||
uniqueHolidaysMap.set(key, r);
|
||
}
|
||
});
|
||
|
||
// Convertir la Map en tableau
|
||
const uniqueRecords = Array.from(uniqueHolidaysMap.values());
|
||
|
||
// Remplir le tableau HTML du haut (avec la liste propre)
|
||
this.renderSchoolHolidaysTable(uniqueRecords);
|
||
|
||
// Générer les événements jour par jour (uniquement sur la liste propre)
|
||
uniqueRecords.forEach((r, idx) => {
|
||
let curr = new Date(r.start_date);
|
||
const end = new Date(r.end_date);
|
||
// Boucle jour par jour
|
||
while (curr < end) {
|
||
const iso = curr.toISOString().split("T")[0];
|
||
schoolHolidayEvents.push({
|
||
id: `sh-${iso}-${idx}`,
|
||
date: iso,
|
||
type: "VACANCES_SCOLAIRES",
|
||
duration: 1,
|
||
});
|
||
curr.setDate(curr.getDate() + 1);
|
||
}
|
||
});
|
||
} catch (e) {
|
||
console.warn(
|
||
"Impossible de charger les vacances scolaires depuis l'API Gouv.",
|
||
e,
|
||
);
|
||
}
|
||
|
||
return [...publicHolidays, ...schoolHolidayEvents];
|
||
}
|
||
|
||
processWeeks(rawWeeks) {
|
||
return rawWeeks.map((w) => ({
|
||
id: `${w.week_iso_year}-W${w.week_iso_number}`,
|
||
monthKey: `${w.year}-${String(w.month).padStart(2, "0")}`,
|
||
monthName: w.month_name,
|
||
weekLabel: w.week_label,
|
||
dayDates: {
|
||
mon: new Date(w.mon_date + "T00:00:00"),
|
||
tue: new Date(w.tue_date + "T00:00:00"),
|
||
wed: new Date(w.wed_date + "T00:00:00"),
|
||
thu: new Date(w.thu_date + "T00:00:00"),
|
||
fri: new Date(w.fri_date + "T00:00:00"),
|
||
},
|
||
dayFlags: {
|
||
mon: { events: [] },
|
||
tue: { events: [] },
|
||
wed: { events: [] },
|
||
thu: { events: [] },
|
||
fri: { events: [] },
|
||
},
|
||
totals: {
|
||
offCarole: 0,
|
||
extraOffCarole: 0,
|
||
centre: 0,
|
||
avis: 0,
|
||
pepSick: 0,
|
||
presencePep: 0,
|
||
alexCP: 0,
|
||
alexJRA: 0,
|
||
alexJA: 0,
|
||
laiaCP: 0,
|
||
laiaJRA: 0,
|
||
laiaJA: 0,
|
||
},
|
||
}));
|
||
}
|
||
|
||
reprocessAndRender() {
|
||
this.reprocessEvents();
|
||
this.calculateMonthlyBalances();
|
||
this.updateGlobalSummary();
|
||
this.renderTable();
|
||
this.renderMonthCalendar();
|
||
}
|
||
|
||
reprocessEvents() {
|
||
this.weeks.forEach((w) => {
|
||
Object.keys(w.totals).forEach((k) => (w.totals[k] = 0));
|
||
Object.values(w.dayFlags).forEach((f) => (f.events = []));
|
||
|
||
this.events.forEach((e) => {
|
||
const d = new Date(e.date + "T00:00:00");
|
||
if (d >= w.dayDates.mon && d <= w.dayDates.fri) {
|
||
const dayKey = Object.keys(w.dayDates).find(
|
||
(k) => w.dayDates[k].getTime() === d.getTime(),
|
||
);
|
||
if (dayKey) w.dayFlags[dayKey].events.push(e);
|
||
const dur = parseFloat(e.duration) || 1;
|
||
const typeMap = {
|
||
OFF_CAROLE: "offCarole",
|
||
EXTRA_OFF_CAROLE: "extraOffCarole",
|
||
CENTRE: "centre",
|
||
AVIS: "avis",
|
||
PEP_SICK: "pepSick",
|
||
};
|
||
if (typeMap[e.type]) w.totals[typeMap[e.type]] += dur;
|
||
}
|
||
});
|
||
|
||
this.leaves.forEach((l) => {
|
||
const d = new Date(l.leave_date + "T00:00:00");
|
||
if (d >= w.dayDates.mon && d <= w.dayDates.fri) {
|
||
const dur = parseFloat(l.duration) || 1;
|
||
const prefix =
|
||
l.person_id === 2 ? "alex" : l.person_id === 3 ? "laia" : null;
|
||
if (prefix) w.totals[`${prefix}${l.leave_type}`] += dur;
|
||
}
|
||
});
|
||
|
||
let workingDays = 0;
|
||
Object.values(w.dayDates).forEach((d) => {
|
||
if (!this.publicHolidayDates.has(d.toISOString().split("T")[0]))
|
||
workingDays++;
|
||
});
|
||
w.totals.presencePep = Math.max(
|
||
0,
|
||
workingDays -
|
||
(w.totals.offCarole + w.totals.extraOffCarole + w.totals.pepSick),
|
||
);
|
||
});
|
||
}
|
||
|
||
calculateMonthlyBalances() {
|
||
const balances = {
|
||
2: { CP: {}, JRA: {}, JA: {} },
|
||
3: { CP: {}, JRA: {}, JA: {} },
|
||
};
|
||
const ymSet = new Set();
|
||
this.weeks.forEach((w) => ymSet.add(w.monthKey));
|
||
const ymList = Array.from(ymSet).sort();
|
||
|
||
const usageByMonth = {};
|
||
this.leaves.forEach((l) => {
|
||
const pid = l.person_id;
|
||
const type = l.leave_type;
|
||
const ym = l.leave_date.substring(0, 7);
|
||
if (!usageByMonth[pid]) usageByMonth[pid] = {};
|
||
if (!usageByMonth[pid][type]) usageByMonth[pid][type] = {};
|
||
usageByMonth[pid][type][ym] =
|
||
(usageByMonth[pid][type][ym] || 0) + parseFloat(l.duration);
|
||
});
|
||
|
||
[2, 3].forEach((pid) => {
|
||
["CP", "JRA", "JA"].forEach((type) => {
|
||
ymList.forEach((ym) => {
|
||
const [currYear, currMonth] = ym.split("-").map(Number);
|
||
let refYear = currYear;
|
||
if (type === "CP")
|
||
refYear = currMonth >= 6 ? currYear : currYear - 1;
|
||
|
||
const initialObj = this.leaveBalances.find(
|
||
(b) =>
|
||
b.person_id == pid &&
|
||
b.leave_type == type &&
|
||
b.balance_year == refYear,
|
||
);
|
||
const initial = initialObj
|
||
? parseFloat(initialObj.initial_balance)
|
||
: type === "CP"
|
||
? 25
|
||
: type === "JRA"
|
||
? 10
|
||
: 0;
|
||
|
||
let usedBefore = 0;
|
||
let periodStartMonth = type === "CP" ? "06" : "01";
|
||
const periodStart = `${refYear}-${periodStartMonth}`;
|
||
|
||
Object.keys(usageByMonth[pid]?.[type] || {}).forEach((usedYm) => {
|
||
if (usedYm >= periodStart && usedYm < ym) {
|
||
usedBefore += usageByMonth[pid][type][usedYm];
|
||
}
|
||
});
|
||
|
||
const available = Math.max(0, initial - usedBefore);
|
||
const usedInMonth = usageByMonth[pid]?.[type]?.[ym] || 0;
|
||
balances[pid][type][ym] = {
|
||
availableAtMonthStart: available,
|
||
usedInMonth: usedInMonth,
|
||
};
|
||
});
|
||
});
|
||
});
|
||
this.monthlyLeaveBalances = balances;
|
||
}
|
||
|
||
renderTable() {
|
||
this.planningBody.innerHTML = "";
|
||
const monthSpans = this.weeks.reduce((acc, w) => {
|
||
acc[w.monthKey] = (acc[w.monthKey] || 0) + 1;
|
||
return acc;
|
||
}, {});
|
||
const processedMonths = {};
|
||
const processedLeavesCols = {};
|
||
const fmt = (n) =>
|
||
n > 0 ? (Number.isInteger(n) ? n : n.toFixed(1)) : "";
|
||
|
||
this.weeks.forEach((w, idx) => {
|
||
const tr = document.createElement("tr");
|
||
if (idx === 0 || this.weeks[idx - 1].monthKey !== w.monthKey)
|
||
tr.classList.add("fc-month-first-week-row");
|
||
if (
|
||
idx === this.weeks.length - 1 ||
|
||
this.weeks[idx + 1].monthKey !== w.monthKey
|
||
)
|
||
tr.classList.add("fc-month-last-week-row");
|
||
|
||
if (!processedMonths[w.monthKey]) {
|
||
processedMonths[w.monthKey] = true;
|
||
const td = document.createElement("td");
|
||
td.className = "col-month";
|
||
td.textContent = w.monthName;
|
||
td.rowSpan = monthSpans[w.monthKey];
|
||
tr.appendChild(td);
|
||
}
|
||
|
||
const tdW = document.createElement("td");
|
||
tdW.className = "col-month";
|
||
tdW.textContent = w.weekLabel;
|
||
tr.appendChild(tdW);
|
||
|
||
["mon", "tue", "wed", "thu", "fri"].forEach((d) => {
|
||
const td = document.createElement("td");
|
||
const dateObj = w.dayDates[d];
|
||
const iso = dateObj.toISOString().split("T")[0];
|
||
td.dataset.date = iso;
|
||
td.textContent = String(dateObj.getDate()).padStart(2, "0");
|
||
td.className = "col-day";
|
||
|
||
const dayEvents = w.dayFlags[d].events;
|
||
dayEvents.forEach((evt) => {
|
||
if (evt.type === "OFF_CAROLE")
|
||
td.classList.add("fc-day--off-carole");
|
||
if (evt.type === "EXTRA_OFF_CAROLE")
|
||
td.classList.add("fc-day--extra-off-carole");
|
||
if (evt.type === "PUBLIC_HOLIDAY")
|
||
td.classList.add("fc-day--public-holiday");
|
||
if (evt.type === "VACANCES_SCOLAIRES")
|
||
td.classList.add("fc-day--school-holiday");
|
||
if (evt.type === "CENTRE") td.classList.add("fc-day--centre");
|
||
if (evt.type === "AVIS") td.classList.add("fc-day--avis");
|
||
if (evt.type === "PEP_SICK")
|
||
td.innerHTML += `<span class="fc-pep-sick-emoji">🤒</span>`;
|
||
});
|
||
|
||
const dayLeaves = this.leaves.filter((l) => l.leave_date === iso);
|
||
if (dayLeaves.length) {
|
||
let html = `<div style="position:absolute; bottom:0; left:0; width:100%; font-size:9px; line-height:1; display:flex; justify-content:center; gap:2px; pointer-events:none;">`;
|
||
// --- CORRECTION COULEURS ---
|
||
if (dayLeaves.some((l) => l.person_id === 2))
|
||
html += `<span style="color:#0f766e; font-weight:800;">A</span>`; // Teal
|
||
if (dayLeaves.some((l) => l.person_id === 3))
|
||
html += `<span style="color:#b45309; font-weight:800;">L</span>`; // Amber
|
||
html += `</div>`;
|
||
td.innerHTML += html;
|
||
}
|
||
tr.appendChild(td);
|
||
});
|
||
|
||
[
|
||
"offCarole",
|
||
"extraOffCarole",
|
||
"centre",
|
||
"avis",
|
||
"pepSick",
|
||
"presencePep",
|
||
].forEach((k) => {
|
||
const td = document.createElement("td");
|
||
td.className = "col-total";
|
||
td.textContent = fmt(w.totals[k]);
|
||
tr.appendChild(td);
|
||
});
|
||
|
||
if (!processedLeavesCols[w.monthKey]) {
|
||
processedLeavesCols[w.monthKey] = true;
|
||
const span = monthSpans[w.monthKey];
|
||
const ym = w.monthKey;
|
||
const renderPersonCols = (pid, prefix) => {
|
||
["CP", "JRA", "JA"].forEach((type) => {
|
||
const info = this.monthlyLeaveBalances[pid][type][ym];
|
||
const tdAv = document.createElement("td");
|
||
tdAv.className = `${prefix}-sub ${prefix}-av`;
|
||
tdAv.rowSpan = span;
|
||
tdAv.textContent = info ? fmt(info.availableAtMonthStart) : "-";
|
||
tr.appendChild(tdAv);
|
||
const tdUse = document.createElement("td");
|
||
tdUse.className = `${prefix}-sub ${prefix}-use`;
|
||
tdUse.rowSpan = span;
|
||
tdUse.textContent = info ? fmt(info.usedInMonth) : "";
|
||
tr.appendChild(tdUse);
|
||
});
|
||
};
|
||
renderPersonCols(2, "col-alex");
|
||
renderPersonCols(3, "col-laia");
|
||
}
|
||
this.planningBody.appendChild(tr);
|
||
});
|
||
}
|
||
|
||
renderMonthCalendar() {
|
||
if (!this.monthCalendar) return;
|
||
this.monthCalendar.innerHTML = ""; // CLEAN UP
|
||
|
||
const y = this.currentMonth.getFullYear();
|
||
const m = this.currentMonth.getMonth();
|
||
|
||
const titleEl = document.querySelector("#fc-current-month-year");
|
||
if (titleEl) {
|
||
if (this.viewMode === "year") {
|
||
titleEl.textContent = `Année scolaire ${this.currentSchoolYearStart} – ${this.currentSchoolYearStart + 1}`;
|
||
} else if (this.viewMode === "2months") {
|
||
const nextM = new Date(y, m + 1, 1);
|
||
const m1 = new Intl.DateTimeFormat("fr-FR", { month: "long" }).format(
|
||
this.currentMonth,
|
||
);
|
||
const m2 = new Intl.DateTimeFormat("fr-FR", {
|
||
month: "long",
|
||
year: "numeric",
|
||
}).format(nextM);
|
||
titleEl.textContent = `${m1} - ${m2}`;
|
||
} else {
|
||
titleEl.textContent = new Intl.DateTimeFormat("fr-FR", {
|
||
month: "long",
|
||
year: "numeric",
|
||
}).format(this.currentMonth);
|
||
}
|
||
}
|
||
|
||
if (this.viewMode === "year") {
|
||
this.renderYearView();
|
||
return;
|
||
}
|
||
if (this.viewMode === "2months") {
|
||
this.renderTwoMonthsView();
|
||
return;
|
||
}
|
||
|
||
this.monthCalendar.innerHTML = this.generateMonthHTML(y, m);
|
||
}
|
||
|
||
renderTwoMonthsView() {
|
||
const y = this.currentMonth.getFullYear();
|
||
const m = this.currentMonth.getMonth();
|
||
const nextDate = new Date(y, m + 1, 1);
|
||
let html = `<div class="fc-two-months-container">`;
|
||
html += `<div class="fc-month-container"><div class="fc-month-title">${new Intl.DateTimeFormat("fr-FR", { month: "long" }).format(this.currentMonth)}</div>${this.generateMonthHTML(y, m)}</div>`;
|
||
html += `<div class="fc-month-container"><div class="fc-month-title">${new Intl.DateTimeFormat("fr-FR", { month: "long" }).format(nextDate)}</div>${this.generateMonthHTML(nextDate.getFullYear(), nextDate.getMonth())}</div>`;
|
||
html += `</div>`;
|
||
this.monthCalendar.innerHTML = html;
|
||
}
|
||
|
||
renderYearView() {
|
||
const startYear = this.currentSchoolYearStart;
|
||
let html = '<div class="fc-year-container">';
|
||
// Sept-Dec
|
||
for (let i = 8; i < 12; i++)
|
||
html += `<div class="fc-year-month"><div class="fc-year-month-title">${new Intl.DateTimeFormat("fr-FR", { month: "long" }).format(new Date(startYear, i))}</div>${this.generateMonthHTML(startYear, i)}</div>`;
|
||
// Jan-Aug
|
||
for (let i = 0; i < 8; i++)
|
||
html += `<div class="fc-year-month"><div class="fc-year-month-title">${new Intl.DateTimeFormat("fr-FR", { month: "long" }).format(new Date(startYear + 1, i))}</div>${this.generateMonthHTML(startYear + 1, i)}</div>`;
|
||
html += "</div>";
|
||
this.monthCalendar.innerHTML = html;
|
||
}
|
||
|
||
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>`;
|
||
|
||
const firstDay = new Date(year, month, 1);
|
||
const startDay = (firstDay.getDay() + 6) % 7;
|
||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||
let dayCounter = 1;
|
||
|
||
for (let row = 0; row < 6; row++) {
|
||
html += `<tr>`;
|
||
for (let col = 0; col < 5; col++) {
|
||
if (
|
||
(row === 0 && col < startDay && startDay < 5) ||
|
||
dayCounter > daysInMonth
|
||
) {
|
||
html += `<td class="fc-day--other-month"></td>`;
|
||
} else if (row === 0 && startDay >= 5) {
|
||
html += `<td class="fc-day--other-month"></td>`;
|
||
} else {
|
||
const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(dayCounter).padStart(2, "0")}`;
|
||
let cls = "fc-month-day";
|
||
const dayEvts = this.events.filter((e) => e.date === iso);
|
||
if (dayEvts.some((e) => e.type === "VACANCES_SCOLAIRES"))
|
||
cls += " fc-day--school-holiday";
|
||
if (dayEvts.some((e) => e.type === "PUBLIC_HOLIDAY"))
|
||
cls += " fc-day--public-holiday";
|
||
if (dayEvts.some((e) => e.type === "OFF_CAROLE"))
|
||
cls += " fc-day--off-carole";
|
||
if (dayEvts.some((e) => e.type === "EXTRA_OFF_CAROLE"))
|
||
cls += " fc-day--extra-off-carole";
|
||
if (dayEvts.some((e) => ["CENTRE", "AVIS"].includes(e.type))) {
|
||
cls += " fc-day--has-guard";
|
||
if (dayEvts.some((e) => e.type === "CENTRE"))
|
||
cls += " fc-day--centre";
|
||
if (dayEvts.some((e) => e.type === "AVIS"))
|
||
cls += " fc-day--avis";
|
||
}
|
||
|
||
let content = `<div style="position:relative; height:100%;">${dayCounter}`;
|
||
if (dayEvts.some((e) => e.type === "PEP_SICK"))
|
||
content += `<span style="position:absolute; bottom:2px; right:2px;">🤒</span>`;
|
||
const dayLeaves = this.leaves.filter((l) => l.leave_date === iso);
|
||
if (dayLeaves.length) {
|
||
content += `<div style="position:absolute; bottom:2px; left:2px; font-size:10px; font-weight:bold;">`;
|
||
if (dayLeaves.some((l) => l.person_id === 2))
|
||
content += `<span style="color:#0f766e">A</span> `;
|
||
if (dayLeaves.some((l) => l.person_id === 3))
|
||
content += `<span style="color:#b45309">L</span>`;
|
||
content += `</div>`;
|
||
}
|
||
content += `</div>`;
|
||
html += `<td class="${cls}" data-date="${iso}">${content}</td>`;
|
||
dayCounter++;
|
||
}
|
||
}
|
||
html += `</tr>`;
|
||
if (dayCounter > daysInMonth) break;
|
||
}
|
||
html += `</tbody></table>`;
|
||
return html;
|
||
}
|
||
|
||
renderSchoolHolidaysTable(records) {
|
||
if (!this.schoolHolidaysTableBody) return;
|
||
records.sort((a, b) => new Date(a.start_date) - new Date(b.start_date));
|
||
this.schoolHolidaysTableBody.innerHTML = records
|
||
.map(
|
||
(r) => `
|
||
<tr>
|
||
<td>${r.description}</td>
|
||
<td>${new Date(r.start_date).toLocaleDateString("fr-FR")}</td>
|
||
<td>${new Date(r.end_date).toLocaleDateString("fr-FR")}</td>
|
||
<td>${r.zones}</td>
|
||
</tr>
|
||
`,
|
||
)
|
||
.join("");
|
||
}
|
||
|
||
updateGlobalSummary() {
|
||
const div = document.getElementById("globalSummary");
|
||
if (!div) return;
|
||
const stats = { off: 0, extra: 0, sick: 0, pep: 0, totalWorking: 0 };
|
||
this.weeks.forEach((w) => {
|
||
stats.off += w.totals.offCarole;
|
||
stats.extra += w.totals.extraOffCarole;
|
||
stats.sick += w.totals.pepSick;
|
||
stats.pep += w.totals.presencePep;
|
||
Object.values(w.dayDates).forEach((d) => {
|
||
if (!this.publicHolidayDates.has(d.toISOString().split("T")[0]))
|
||
stats.totalWorking++;
|
||
});
|
||
});
|
||
div.innerHTML = `
|
||
<p><strong>Off Carole :</strong> ${stats.off} jours</p>
|
||
<p><strong>Extra Off Carole :</strong> ${stats.extra} jours</p>
|
||
<p><strong>Pep malade :</strong> ${stats.sick} jours</p>
|
||
<p><strong>Présence Pep :</strong> ${stats.pep} jours</p>
|
||
<p style="font-size:0.8em; color:#666; margin-top:4px;">(Sur jours ouvrés totaux : ${stats.totalWorking})</p>
|
||
`;
|
||
}
|
||
|
||
updateSchoolYearLabel() {
|
||
const lbl = document.getElementById("fc-current-school-year-label");
|
||
if (lbl)
|
||
lbl.textContent = `${this.currentSchoolYearStart} – ${this.currentSchoolYearStart + 1}`;
|
||
}
|
||
|
||
// ================== INTERACTIONS ==================
|
||
|
||
setupEventListeners() {
|
||
// Planning Hebdo
|
||
this.planningBody.addEventListener("mousedown", (e) =>
|
||
this.handleMouseDown(e),
|
||
);
|
||
document.addEventListener("mousemove", (e) => this.handleMouseMove(e));
|
||
document.addEventListener("mouseup", (e) => this.handleMouseUp(e));
|
||
|
||
// Calendrier Mensuel (Clic simple pour ouvrir le menu)
|
||
if (this.monthCalendar) {
|
||
this.monthCalendar.addEventListener("click", (e) => {
|
||
const td = e.target.closest("td[data-date]");
|
||
if (td) {
|
||
this.monthSelectedCells = [td]; // Simuler sélection unique
|
||
this.showMenu(e.pageX, e.pageY, [td.dataset.date], true);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Menus Contextuels
|
||
document.addEventListener("click", (e) => this.closeMenusIfOutside(e));
|
||
const handleMenu = (e) => {
|
||
const btn = e.target.closest("button");
|
||
if (btn) this.handleMenuAction(btn.dataset);
|
||
};
|
||
if (this.selectionMenu)
|
||
this.selectionMenu.addEventListener("click", handleMenu);
|
||
if (this.monthSelectionMenu)
|
||
this.monthSelectionMenu.addEventListener("click", handleMenu);
|
||
|
||
// Boutons Navigation Mensuelle
|
||
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();
|
||
});
|
||
|
||
// Boutons Navigation Année Scolaire
|
||
document
|
||
.getElementById("fc-prev-school-year")
|
||
?.addEventListener("click", () => this.changeSchoolYear(-1));
|
||
document
|
||
.getElementById("fc-next-school-year")
|
||
?.addEventListener("click", () => this.changeSchoolYear(1));
|
||
|
||
// Boutons de Vue (1 mois / 2 mois / Année)
|
||
document.querySelectorAll(".fc-view-button").forEach((btn) => {
|
||
btn.addEventListener("click", (e) => {
|
||
document
|
||
.querySelectorAll(".fc-view-button")
|
||
.forEach((b) => b.classList.remove("fc-view-button--active"));
|
||
e.target.classList.add("fc-view-button--active");
|
||
this.viewMode = e.target.dataset.view;
|
||
this.renderMonthCalendar();
|
||
});
|
||
});
|
||
}
|
||
|
||
handleMouseDown(e) {
|
||
const td = e.target.closest("#planningTable td[data-date]");
|
||
if (!td) return;
|
||
e.preventDefault();
|
||
this.clearSelection();
|
||
this.isSelecting = true;
|
||
this.selectCell(td);
|
||
}
|
||
|
||
handleMouseMove(e) {
|
||
if (!this.isSelecting) return;
|
||
const td = e.target.closest("#planningTable td[data-date]");
|
||
if (td && !this.selectedCells.includes(td)) this.selectCell(td);
|
||
}
|
||
|
||
handleMouseUp(e) {
|
||
if (!this.isSelecting) return;
|
||
|
||
// FIX CRITIQUE : Si on relâche la souris sur une case qu'on n'a pas eu le temps de "survoler" (mouvement rapide)
|
||
// on l'ajoute maintenant pour être sûr qu'elle est dans la sélection.
|
||
const td = e.target.closest("#planningTable td[data-date]");
|
||
if (td && !this.selectedCells.includes(td)) {
|
||
this.selectCell(td);
|
||
}
|
||
|
||
this.isSelecting = false;
|
||
if (!this.selectedCells.length) return;
|
||
|
||
const dates = this.selectedCells.map((c) => c.dataset.date);
|
||
this.showMenu(e.pageX, e.pageY, dates, false);
|
||
}
|
||
|
||
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 = [];
|
||
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 dateLabel =
|
||
dates.length > 1
|
||
? `${dates.length} jours`
|
||
: new Date(dates[0]).toLocaleDateString("fr-FR");
|
||
|
||
let html = `<div class="fc-menu-section"><strong>${dateLabel}</strong></div>`;
|
||
|
||
// Carole
|
||
html += `<div class="fc-menu-section"><strong>Congés Carole</strong><div class="fc-menu-grid">`;
|
||
html += `<button class="fc-menu-btn" data-action="add" data-type="OFF_CAROLE" data-person="Carole">Off Carole</button>`;
|
||
html += `<button class="fc-menu-btn" data-action="add" data-type="EXTRA_OFF_CAROLE" data-person="Carole">Extra Off</button>`;
|
||
html += `</div><button class="fc-menu-danger" data-action="clear-type" data-cat="CONGE">Effacer congés Carole</button></div>`;
|
||
|
||
// Garde
|
||
html += `<div class="fc-menu-section"><strong>Garde</strong><div class="fc-menu-grid">`;
|
||
html += `<button class="fc-menu-btn" data-action="add" data-type="CENTRE">Centre</button>`;
|
||
html += `<button class="fc-menu-btn" data-action="add" data-type="AVIS">Avis</button>`;
|
||
html += `</div><button class="fc-menu-danger" data-action="clear-type" data-cat="GARDE">Effacer Garde</button></div>`;
|
||
|
||
// Pep
|
||
html += `<div class="fc-menu-section"><strong>Pep</strong>`;
|
||
html += `<button class="fc-menu-btn" data-action="add" data-type="PEP_SICK" style="width:100%">Pep Malade</button>`;
|
||
html += `<button class="fc-menu-danger" data-action="clear-type" data-cat="PEP">Effacer Pep</button></div>`;
|
||
|
||
// Alex/Laia
|
||
html += `<div class="fc-menu-section"><strong>Congés Alex / Laia</strong><div class="fc-menu-leaves-table"><table><thead><tr><th>Alex</th><th>Laia</th></tr></thead><tbody>`;
|
||
["CP", "JRA", "JA"].forEach((t) => {
|
||
html += `<tr><td><button class="fc-menu-btn" data-action="add-leave" data-pid="2" data-type="${t}">${t}</button></td>`;
|
||
html += `<td><button class="fc-menu-btn" data-action="add-leave" data-pid="3" data-type="${t}">${t}</button></td></tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
html += `<button class="fc-menu-danger" data-action="clear-leaves">Effacer Alex/Laia</button></div>`;
|
||
|
||
menu.innerHTML = html;
|
||
|
||
const menuWidth = 240;
|
||
let left = x + 10;
|
||
if (left + menuWidth > window.innerWidth) left = x - menuWidth - 10;
|
||
|
||
menu.style.left = `${left}px`;
|
||
menu.style.top = `${y + 10}px`;
|
||
menu.style.display = "block";
|
||
|
||
this.menuJustOpened = true;
|
||
setTimeout(() => (this.menuJustOpened = false), 100);
|
||
}
|
||
|
||
async handleMenuAction(dataset) {
|
||
this.selectionMenu.style.display = "none";
|
||
if (this.monthSelectionMenu)
|
||
this.monthSelectionMenu.style.display = "none";
|
||
|
||
const { action, type, pid, cat } = dataset;
|
||
const dates = this._currentBulkInfo.dates;
|
||
|
||
try {
|
||
if (action === "add") {
|
||
let typesToClear = [];
|
||
if (["OFF_CAROLE", "EXTRA_OFF_CAROLE"].includes(type))
|
||
typesToClear = CONGE_TYPES;
|
||
if (["CENTRE", "AVIS"].includes(type)) typesToClear = GUARDE_TYPES;
|
||
if (type === "PEP_SICK") typesToClear = PEP_TYPES;
|
||
|
||
if (typesToClear.length) {
|
||
await this.postApi(
|
||
"/modules/family-calendar/includes/api/manage-event.php",
|
||
{
|
||
action: "bulk_delete_day_types",
|
||
dates,
|
||
types: typesToClear,
|
||
},
|
||
);
|
||
}
|
||
const payload = dates.map((d) => ({
|
||
date: d,
|
||
type: type,
|
||
duration: 1,
|
||
person: "Carole",
|
||
}));
|
||
await this.postApi(
|
||
"/modules/family-calendar/includes/api/save-events.php",
|
||
payload,
|
||
);
|
||
} else if (action === "clear-type") {
|
||
let typesToClear = [];
|
||
if (cat === "CONGE") typesToClear = CONGE_TYPES;
|
||
if (cat === "GARDE") typesToClear = GUARDE_TYPES;
|
||
if (cat === "PEP") typesToClear = PEP_TYPES;
|
||
|
||
await this.postApi(
|
||
"/modules/family-calendar/includes/api/manage-event.php",
|
||
{
|
||
action: "bulk_delete_day_types",
|
||
dates,
|
||
types: typesToClear,
|
||
},
|
||
);
|
||
} else if (action === "add-leave") {
|
||
await this.postApi(
|
||
"/modules/family-calendar/includes/api/manage-leaf.php",
|
||
{
|
||
action: "bulk_delete_day_person",
|
||
dates,
|
||
person_id: pid,
|
||
},
|
||
);
|
||
const payload = dates.map((d) => ({
|
||
date: d,
|
||
person_id: pid,
|
||
leave_type: type,
|
||
duration: 1,
|
||
}));
|
||
await this.postApi(
|
||
"/modules/family-calendar/includes/api/save-leaves.php",
|
||
payload,
|
||
);
|
||
} else if (action === "clear-leaves") {
|
||
await this.postApi(
|
||
"/modules/family-calendar/includes/api/manage-leaf.php",
|
||
{ action: "bulk_delete_day_person", dates, person_id: 2 },
|
||
);
|
||
await this.postApi(
|
||
"/modules/family-calendar/includes/api/manage-leaf.php",
|
||
{ action: "bulk_delete_day_person", dates, person_id: 3 },
|
||
);
|
||
}
|
||
|
||
await this.refreshAllData();
|
||
} catch (e) {
|
||
alert("Erreur action: " + e.message);
|
||
}
|
||
}
|
||
|
||
async fetchApi(url) {
|
||
return fetch(url).then((r) => r.json());
|
||
}
|
||
|
||
async postApi(url, data) {
|
||
const res = await fetch(url, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(data),
|
||
});
|
||
if (!res.ok) throw new Error(await res.text());
|
||
return res.json();
|
||
}
|
||
|
||
async changeSchoolYear(delta) {
|
||
this.currentSchoolYearStart += delta;
|
||
this.updateSchoolYearLabel();
|
||
await this.refreshAllData();
|
||
}
|
||
}
|
||
|
||
new FamilyCalendar();
|
||
});
|