opti family calendar
This commit is contained in:
+3
-3
@@ -98,8 +98,8 @@ require __DIR__ . '/header.php';
|
|||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowspan="3" class="col-month">Mois</th>
|
<th rowspan="3" class="col-month col-sticky-mois">Mois</th>
|
||||||
<th rowspan="3" class="col-month">Sem.</th>
|
<th rowspan="3" class="col-month col-sticky-mois">Sem.</th>
|
||||||
<th rowspan="3" class="col-day">Lun</th>
|
<th rowspan="3" class="col-day">Lun</th>
|
||||||
<th rowspan="3" class="col-day">Mar</th>
|
<th rowspan="3" class="col-day">Mar</th>
|
||||||
<th rowspan="3" class="col-day">Mer</th>
|
<th rowspan="3" class="col-day">Mer</th>
|
||||||
@@ -140,7 +140,7 @@ require __DIR__ . '/header.php';
|
|||||||
<h2 class="pf-card-title">Légende</h2>
|
<h2 class="pf-card-title">Légende</h2>
|
||||||
<div class="pf-card-body">
|
<div class="pf-card-body">
|
||||||
<div class="pf-legend-grid">
|
<div class="pf-legend-grid">
|
||||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-school-holiday"></div><span>Vacances</span></div>
|
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-school-holiday"></div><span>Vacances Scolaires</span></div>
|
||||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-public-holiday"></div><span>Férié</span></div>
|
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-public-holiday"></div><span>Férié</span></div>
|
||||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-off-carole"></div><span>Off Carole</span></div>
|
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-off-carole"></div><span>Off Carole</span></div>
|
||||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-extra-off-carole"></div><span>Extra Off</span></div>
|
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-extra-off-carole"></div><span>Extra Off</span></div>
|
||||||
|
|||||||
+1
-1
@@ -140,7 +140,7 @@ body {
|
|||||||
.pf-main {
|
.pf-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1200px;
|
max-width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 32px 20px;
|
padding: 32px 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -796,3 +796,88 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- SUPERPOSITION : GARDE/CONGÉS + VACANCES SCOLAIRES --- */
|
||||||
|
|
||||||
|
/* 1. OFF CAROLE + VACANCES
|
||||||
|
On force le fond orange/pêche, et on met la barre violette en bas */
|
||||||
|
.fc-day--off-carole.fc-day--school-holiday {
|
||||||
|
background: var(--c-off-carole) !important;
|
||||||
|
box-shadow: inset 0 -8px 0 0 var(--c-school-holiday) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. EXTRA OFF CAROLE + VACANCES
|
||||||
|
On force le fond rouge clair, et on met la barre violette en bas */
|
||||||
|
.fc-day--extra-off-carole.fc-day--school-holiday {
|
||||||
|
background: var(--c-extra-off) !important;
|
||||||
|
box-shadow: inset 0 -8px 0 0 var(--c-school-holiday) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. CENTRE / AVIS + VACANCES
|
||||||
|
Comme ce sont juste des icônes sur fond normal, on se contente d'ajouter la barre violette */
|
||||||
|
.fc-day--centre.fc-day--school-holiday,
|
||||||
|
.fc-day--avis.fc-day--school-holiday {
|
||||||
|
box-shadow: inset 0 -8px 0 0 var(--c-school-holiday) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
8. OPTIMISATIONS MOBILES ET ERGONOMIE (AJOUTS)
|
||||||
|
========================================================= */
|
||||||
|
|
||||||
|
/* --- A. COLONNES MOIS ET SEMAINE FIGÉES (Sticky Left) --- */
|
||||||
|
.col-sticky-mois {
|
||||||
|
position: sticky !important;
|
||||||
|
left: 0;
|
||||||
|
z-index: 15;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 1px 0 2px rgba(0, 0, 0, 0.05); /* Petite ombre pour séparer */
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-sticky-sem {
|
||||||
|
position: sticky !important;
|
||||||
|
left: 50px; /* On décale de la largeur de la colonne 'Mois' (50px) */
|
||||||
|
z-index: 15;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On s'assure que l'en-tête reste bien au-dessus du reste */
|
||||||
|
#planningTable thead .col-sticky-mois,
|
||||||
|
#planningTable thead .col-sticky-sem {
|
||||||
|
z-index: 25;
|
||||||
|
background: var(--bg-header);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- B. MENU BOTTOM SHEET (Mobile Uniquement) --- */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.fc-selection-menu {
|
||||||
|
position: fixed !important; /* Fixé à l'écran, pas au tableau */
|
||||||
|
top: auto !important;
|
||||||
|
left: 0 !important;
|
||||||
|
bottom: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
border-radius: 24px 24px 0 0; /* Bords arrondis en haut */
|
||||||
|
box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 24px 20px;
|
||||||
|
/* Padding dynamique pour les iPhone (encoche du bas) */
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 24px);
|
||||||
|
transform: translateY(100%); /* Caché en bas par défaut */
|
||||||
|
animation: slideUpSheet 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
|
max-height: 85vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUpSheet {
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optionnel : On grossit un peu les boutons pour les gros doigts */
|
||||||
|
.fc-menu-btn,
|
||||||
|
.fc-menu-danger {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* family-calendar.js (Version Sécurisée)
|
* family-calendar.js (Version Optimisée - API BDD & Décalage Vendredi)
|
||||||
*/
|
*/
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const CONGE_TYPES = ["OFF_CAROLE", "EXTRA_OFF_CAROLE"];
|
const CONGE_TYPES = ["OFF_CAROLE", "EXTRA_OFF_CAROLE"];
|
||||||
@@ -8,7 +8,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
class FamilyCalendar {
|
class FamilyCalendar {
|
||||||
constructor() {
|
constructor() {
|
||||||
// Éléments DOM (avec sécurité null)
|
|
||||||
this.planningBody = document.getElementById("planningBody");
|
this.planningBody = document.getElementById("planningBody");
|
||||||
this.selectionMenu = document.getElementById("selectionMenu");
|
this.selectionMenu = document.getElementById("selectionMenu");
|
||||||
this.schoolHolidaysTableBody = document.querySelector(
|
this.schoolHolidaysTableBody = document.querySelector(
|
||||||
@@ -19,7 +18,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
"fc-month-selectionMenu",
|
"fc-month-selectionMenu",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Déplacement des menus dans le body pour affichage correct
|
|
||||||
if (
|
if (
|
||||||
this.selectionMenu &&
|
this.selectionMenu &&
|
||||||
this.selectionMenu.parentElement !== document.body
|
this.selectionMenu.parentElement !== document.body
|
||||||
@@ -37,15 +35,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
this.currentMonth.setDate(1);
|
this.currentMonth.setDate(1);
|
||||||
this.viewMode = "1month";
|
this.viewMode = "1month";
|
||||||
this.currentSchoolYearStart = null;
|
this.currentSchoolYearStart = null;
|
||||||
|
this.modalSelectedYear = null; // Pour la modale des vacances
|
||||||
|
|
||||||
this.isSelecting = false;
|
this.isSelecting = false;
|
||||||
this.selectedCells = [];
|
this.selectedCells = [];
|
||||||
this.monthSelectedCells = [];
|
this.monthSelectedCells = [];
|
||||||
this._currentBulkInfo = null;
|
this._currentBulkInfo = null;
|
||||||
|
|
||||||
// Données
|
|
||||||
this.dbEvents = [];
|
this.dbEvents = [];
|
||||||
this.fixedEvents = [];
|
this.fixedEvents = []; // Fériés statiques
|
||||||
this.events = [];
|
this.events = [];
|
||||||
this.leaves = [];
|
this.leaves = [];
|
||||||
this.weeks = [];
|
this.weeks = [];
|
||||||
@@ -54,25 +52,51 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
3: { CP: {}, JRA: {}, JA: {} },
|
3: { CP: {}, JRA: {}, JA: {} },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Si le tableau principal n'est pas là, on arrête tout pour ne pas crasher
|
if (!this.planningBody) return;
|
||||||
if (!this.planningBody) {
|
|
||||||
console.warn("Element #planningBody introuvable. Le JS s'arrête.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.init();
|
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() {
|
async init() {
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
this.currentSchoolYearStart =
|
this.currentSchoolYearStart =
|
||||||
now.getMonth() >= 8 ? now.getFullYear() : now.getFullYear() - 1;
|
now.getMonth() >= 8 ? now.getFullYear() : now.getFullYear() - 1;
|
||||||
|
this.modalSelectedYear = this.currentSchoolYearStart;
|
||||||
|
|
||||||
|
this.setupModalUI(); // Prépare le selecteur d'année dans la modale
|
||||||
await this.refreshAllData();
|
await this.refreshAllData();
|
||||||
this.updateSchoolYearLabel();
|
this.updateSchoolYearLabel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Modifie dynamiquement le titre de la modale pour y insérer le selecteur d'année
|
||||||
|
setupModalUI() {
|
||||||
|
const headerH2 = document.querySelector(".fc-modal-header h2");
|
||||||
|
if (headerH2 && !document.getElementById("holidayYearSelect")) {
|
||||||
|
headerH2.innerHTML = `Vacances Scolaires (Zone C)
|
||||||
|
<select id="holidayYearSelect" style="margin-left:15px; font-size:1rem; padding:4px; border-radius:4px; border:1px solid #cbd5e1;">
|
||||||
|
<option value="${this.currentSchoolYearStart - 1}">${this.currentSchoolYearStart - 1} - ${this.currentSchoolYearStart}</option>
|
||||||
|
<option value="${this.currentSchoolYearStart}" selected>${this.currentSchoolYearStart} - ${this.currentSchoolYearStart + 1}</option>
|
||||||
|
<option value="${this.currentSchoolYearStart + 1}">${this.currentSchoolYearStart + 1} - ${this.currentSchoolYearStart + 2}</option>
|
||||||
|
<option value="${this.currentSchoolYearStart + 2}">${this.currentSchoolYearStart + 2} - ${this.currentSchoolYearStart + 3}</option>
|
||||||
|
</select>`;
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById("holidayYearSelect")
|
||||||
|
.addEventListener("change", (e) => {
|
||||||
|
this.modalSelectedYear = parseInt(e.target.value);
|
||||||
|
this.renderModalHolidays();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async refreshAllData() {
|
async refreshAllData() {
|
||||||
try {
|
try {
|
||||||
const weeksData = await this.fetchApi(
|
const weeksData = await this.fetchApi(
|
||||||
@@ -88,7 +112,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
duration: parseFloat(e.duration),
|
duration: parseFloat(e.duration),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.fixedEvents = await this.fetchPublicAndSchoolHolidays();
|
this.fixedEvents = this.getPublicHolidays(); // Uniquement les jours fériés maintenant
|
||||||
|
|
||||||
const leavesData = await this.fetchApi(
|
const leavesData = await this.fetchApi(
|
||||||
"/modules/family-calendar/includes/api/get-leaves.php",
|
"/modules/family-calendar/includes/api/get-leaves.php",
|
||||||
@@ -100,6 +124,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
);
|
);
|
||||||
this.leaveBalances = balancesData.balances || [];
|
this.leaveBalances = balancesData.balances || [];
|
||||||
|
|
||||||
|
// Les vacances scolaires sont maintenant dans dbEvents !
|
||||||
this.events = [...this.dbEvents, ...this.fixedEvents];
|
this.events = [...this.dbEvents, ...this.fixedEvents];
|
||||||
this.publicHolidayDates = new Set(
|
this.publicHolidayDates = new Set(
|
||||||
this.fixedEvents
|
this.fixedEvents
|
||||||
@@ -108,14 +133,23 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.reprocessAndRender();
|
this.reprocessAndRender();
|
||||||
|
this.renderModalHolidays(); // Met à jour la modale
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Erreur chargement données:", e);
|
console.error("Erreur chargement données:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchPublicAndSchoolHolidays() {
|
// Les jours fériés restent hardcodés car ils sont fixes.
|
||||||
// 1. Jours fériés (Statique)
|
getPublicHolidays() {
|
||||||
const publicHolidays = [
|
return [
|
||||||
|
"2025-01-01",
|
||||||
|
"2025-04-21",
|
||||||
|
"2025-05-01",
|
||||||
|
"2025-05-08",
|
||||||
|
"2025-05-29",
|
||||||
|
"2025-06-09",
|
||||||
|
"2025-07-14",
|
||||||
|
"2025-08-15",
|
||||||
"2025-11-01",
|
"2025-11-01",
|
||||||
"2025-11-11",
|
"2025-11-11",
|
||||||
"2025-12-25",
|
"2025-12-25",
|
||||||
@@ -127,79 +161,189 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
"2026-05-25",
|
"2026-05-25",
|
||||||
"2026-07-14",
|
"2026-07-14",
|
||||||
"2026-08-15",
|
"2026-08-15",
|
||||||
|
"2026-11-01",
|
||||||
|
"2026-11-11",
|
||||||
|
"2026-12-25",
|
||||||
].map((date, idx) => ({
|
].map((date, idx) => ({
|
||||||
id: `ph-${idx}`,
|
id: `ph-${idx}`,
|
||||||
date,
|
date,
|
||||||
type: "PUBLIC_HOLIDAY",
|
type: "PUBLIC_HOLIDAY",
|
||||||
duration: 1,
|
duration: 1,
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const schoolHolidayEvents = [];
|
// --- RECONSTRUCTION DE LA MODALE VIA LA BDD (Par blocs consécutifs) ---
|
||||||
|
renderModalHolidays() {
|
||||||
|
if (!this.schoolHolidaysTableBody) return;
|
||||||
|
|
||||||
|
// Filtrer les événements de type VACANCES_SCOLAIRES pour l'année scolaire sélectionnée
|
||||||
|
const startDate = `${this.modalSelectedYear}-09-01`;
|
||||||
|
const endDate = `${this.modalSelectedYear + 1}-08-31`;
|
||||||
|
|
||||||
|
const yearHolidays = this.events.filter(
|
||||||
|
(e) =>
|
||||||
|
e.type === "VACANCES_SCOLAIRES" &&
|
||||||
|
e.date >= startDate &&
|
||||||
|
e.date <= endDate,
|
||||||
|
);
|
||||||
|
|
||||||
|
// S'il n'y a pas de vacances en base pour cette année, on affiche le bouton "Générer"
|
||||||
|
if (yearHolidays.length === 0) {
|
||||||
|
this.schoolHolidaysTableBody.innerHTML = `
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" style="text-align:center; padding: 30px;">
|
||||||
|
<p style="color:#64748b; margin-bottom:15px;">Les vacances de cette année ne sont pas encore enregistrées.</p>
|
||||||
|
<button id="btnFetchGovHolidays" class="pf-btn">Importer depuis l'API Gouvernement</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById("btnFetchGovHolidays")
|
||||||
|
.addEventListener("click", (e) => {
|
||||||
|
e.target.innerText = "Téléchargement en cours...";
|
||||||
|
e.target.disabled = true;
|
||||||
|
this.fetchAndSaveGovHolidays(this.modalSelectedYear);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Tri chronologique strict des jours
|
||||||
|
yearHolidays.sort((a, b) => new Date(a.date) - new Date(b.date));
|
||||||
|
|
||||||
|
// 2. Regroupement par blocs de jours consécutifs
|
||||||
|
const blocks = [];
|
||||||
|
let currentBlock = null;
|
||||||
|
|
||||||
|
yearHolidays.forEach((e) => {
|
||||||
|
const d = new Date(e.date + "T00:00:00"); // Force locale
|
||||||
|
|
||||||
|
if (!currentBlock) {
|
||||||
|
currentBlock = { start: d, end: d };
|
||||||
|
blocks.push(currentBlock);
|
||||||
|
} else {
|
||||||
|
// Calcul de l'écart en jours entre la date actuelle et la fin du bloc en cours
|
||||||
|
const diffTime = d.getTime() - currentBlock.end.getTime();
|
||||||
|
const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
// Si l'écart est minime (<= 4 jours, pour absorber un éventuel week-end non stocké)
|
||||||
|
// on considère qu'on est toujours dans la même période de vacances.
|
||||||
|
if (diffDays <= 4) {
|
||||||
|
currentBlock.end = d;
|
||||||
|
} else {
|
||||||
|
// Sinon, l'écart est grand : c'est une NOUVELLE période de vacances
|
||||||
|
currentBlock = { start: d, end: d };
|
||||||
|
blocks.push(currentBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Rendu HTML et déduction des noms
|
||||||
|
let html = "";
|
||||||
|
blocks.forEach((block) => {
|
||||||
|
// Déduction intelligente du nom selon le mois de départ ET la durée
|
||||||
|
const m = block.start.getMonth() + 1;
|
||||||
|
const durationDays =
|
||||||
|
Math.round(
|
||||||
|
(block.end.getTime() - block.start.getTime()) /
|
||||||
|
(1000 * 60 * 60 * 24),
|
||||||
|
) + 1;
|
||||||
|
|
||||||
|
let name = "Vacances scolaires";
|
||||||
|
|
||||||
|
if (m === 10 || m === 11) {
|
||||||
|
name = "Vacances de la Toussaint";
|
||||||
|
} else if (m === 12 || m === 1) {
|
||||||
|
name = "Vacances de Noël";
|
||||||
|
} else if (m === 2 || m === 3) {
|
||||||
|
name = "Vacances d'Hiver";
|
||||||
|
} else if (m === 4 || (m === 5 && durationDays > 6)) {
|
||||||
|
name = "Vacances de Printemps";
|
||||||
|
} else if (m === 5 && durationDays <= 6) {
|
||||||
|
name = "Pont de Mai (Ascension)";
|
||||||
|
} else if (m === 7 || m === 8) {
|
||||||
|
name = "Vacances d'Été";
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td><strong>${name}</strong></td>
|
||||||
|
<td>${block.start.toLocaleDateString("fr-FR")}</td>
|
||||||
|
<td>${block.end.toLocaleDateString("fr-FR")}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.schoolHolidaysTableBody.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- IMPORTATION DEPUIS L'API & SAUVEGARDE EN BDD ---
|
||||||
|
async fetchAndSaveGovHolidays(yearStart) {
|
||||||
try {
|
try {
|
||||||
// API Gouv
|
const yearStr = `${yearStart}-${yearStart + 1}`;
|
||||||
const url =
|
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 C%'&limit=100`;
|
||||||
"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 res = await fetch(url);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const rawRecords = data.results || [];
|
const rawRecords = data.results || [];
|
||||||
|
|
||||||
// Dédoublonnage
|
// Dédoublonnage
|
||||||
const uniqueHolidaysMap = new Map();
|
const uniqueMap = new Map();
|
||||||
rawRecords.forEach((r) => {
|
rawRecords.forEach((r) => {
|
||||||
const key = `${r.description}|${r.start_date}`;
|
const key = `${r.description}|${r.start_date}`;
|
||||||
if (!uniqueHolidaysMap.has(key)) uniqueHolidaysMap.set(key, r);
|
if (!uniqueMap.has(key)) uniqueMap.set(key, r);
|
||||||
});
|
});
|
||||||
const uniqueRecords = Array.from(uniqueHolidaysMap.values());
|
|
||||||
|
|
||||||
// APPEL DE LA MÉTHODE D'AFFICHAGE (séparée pour la propreté)
|
const payload = [];
|
||||||
this.renderSchoolHolidaysTable(uniqueRecords);
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
// REGLE METIER : Si le 1er jour est un VENDREDI (jour=5), on décale le début au SAMEDI.
|
||||||
|
if (curr.getDay() === 5) {
|
||||||
|
curr.setDate(curr.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
// Génération des events pour le calendrier (cases colorées)
|
|
||||||
uniqueRecords.forEach((r, idx) => {
|
|
||||||
let curr = new Date(r.start_date);
|
|
||||||
const end = new Date(r.end_date);
|
|
||||||
while (curr < end) {
|
while (curr < end) {
|
||||||
const iso = curr.toISOString().split("T")[0];
|
const iso = this.getLocalIsoDate(curr);
|
||||||
schoolHolidayEvents.push({
|
payload.push({
|
||||||
id: `sh-${iso}-${idx}`,
|
|
||||||
date: iso,
|
date: iso,
|
||||||
type: "VACANCES_SCOLAIRES",
|
type: "VACANCES_SCOLAIRES",
|
||||||
duration: 1,
|
duration: 1,
|
||||||
|
person: r.description, // ASTUCE: On stocke le nom de la vacance ici !
|
||||||
});
|
});
|
||||||
curr.setDate(curr.getDate() + 1);
|
curr.setDate(curr.getDate() + 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (payload.length > 0) {
|
||||||
|
// On envoie le gros lot à la base de données via l'API existante
|
||||||
|
await this.postApi(
|
||||||
|
"/modules/family-calendar/includes/api/save-events.php",
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
await this.refreshAllData(); // Recharge tout, ce qui mettra à jour la modale
|
||||||
|
} else {
|
||||||
|
alert(
|
||||||
|
"Aucune donnée trouvée sur l'API du gouvernement pour cette année.",
|
||||||
|
);
|
||||||
|
document.getElementById("btnFetchGovHolidays").innerText =
|
||||||
|
"Réessayer";
|
||||||
|
document.getElementById("btnFetchGovHolidays").disabled = false;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Erreur API Vacances (pas grave, on continue)", e);
|
console.error("Erreur API", e);
|
||||||
|
alert("Erreur lors de la connexion à l'API gouvernementale.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// On retourne TOUJOURS les événements, même si l'affichage du tableau échoue
|
// ================================================================
|
||||||
return [...publicHolidays, ...schoolHolidayEvents];
|
// LE RESTE DU CODE (AFFICHAGE) RESTE INCHANGÉ MAIS OPTIMISÉ
|
||||||
}
|
// ================================================================
|
||||||
|
|
||||||
renderSchoolHolidaysTable(records) {
|
|
||||||
// Si le tableau HTML n'existe pas (ex: on n'est pas sur la bonne page), on ne fait rien
|
|
||||||
if (!this.schoolHolidaysTableBody) return;
|
|
||||||
|
|
||||||
// 1. Tri chronologique strict (du plus ancien au plus récent)
|
|
||||||
records.sort((a, b) => new Date(a.start_date) - new Date(b.start_date));
|
|
||||||
|
|
||||||
// 2. Génération du HTML
|
|
||||||
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>
|
|
||||||
</tr>
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================== LOGIQUE MÉTIER ==================
|
|
||||||
|
|
||||||
reprocessAndRender() {
|
reprocessAndRender() {
|
||||||
this.reprocessEvents();
|
this.reprocessEvents();
|
||||||
@@ -246,7 +390,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
let workingDays = 0;
|
let workingDays = 0;
|
||||||
Object.values(w.dayDates).forEach((d) => {
|
Object.values(w.dayDates).forEach((d) => {
|
||||||
if (!this.publicHolidayDates.has(d.toISOString().split("T")[0]))
|
if (!this.publicHolidayDates.has(this.getLocalIsoDate(d)))
|
||||||
workingDays++;
|
workingDays++;
|
||||||
});
|
});
|
||||||
w.totals.presencePep = Math.max(
|
w.totals.presencePep = Math.max(
|
||||||
@@ -304,9 +448,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const periodStart = `${refYear}-${periodStartMonth}`;
|
const periodStart = `${refYear}-${periodStartMonth}`;
|
||||||
|
|
||||||
Object.keys(usageByMonth[pid]?.[type] || {}).forEach((usedYm) => {
|
Object.keys(usageByMonth[pid]?.[type] || {}).forEach((usedYm) => {
|
||||||
if (usedYm >= periodStart && usedYm < ym) {
|
if (usedYm >= periodStart && usedYm < ym)
|
||||||
usedBefore += usageByMonth[pid][type][usedYm];
|
usedBefore += usageByMonth[pid][type][usedYm];
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const available = Math.max(0, initial - usedBefore);
|
const available = Math.max(0, initial - usedBefore);
|
||||||
@@ -346,21 +489,21 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
if (!processedMonths[w.monthKey]) {
|
if (!processedMonths[w.monthKey]) {
|
||||||
processedMonths[w.monthKey] = true;
|
processedMonths[w.monthKey] = true;
|
||||||
const td = document.createElement("td");
|
const td = document.createElement("td");
|
||||||
td.className = "col-month";
|
td.className = "col-month col-sticky-mois";
|
||||||
td.textContent = w.monthName;
|
td.textContent = w.monthName;
|
||||||
td.rowSpan = monthSpans[w.monthKey];
|
td.rowSpan = monthSpans[w.monthKey];
|
||||||
tr.appendChild(td);
|
tr.appendChild(td);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tdW = document.createElement("td");
|
const tdW = document.createElement("td");
|
||||||
tdW.className = "col-month";
|
tdW.className = "col-month col-sticky-sem";
|
||||||
tdW.textContent = w.weekLabel;
|
tdW.textContent = w.weekLabel;
|
||||||
tr.appendChild(tdW);
|
tr.appendChild(tdW);
|
||||||
|
|
||||||
["mon", "tue", "wed", "thu", "fri"].forEach((d) => {
|
["mon", "tue", "wed", "thu", "fri"].forEach((d) => {
|
||||||
const td = document.createElement("td");
|
const td = document.createElement("td");
|
||||||
const dateObj = w.dayDates[d];
|
const dateObj = w.dayDates[d];
|
||||||
const iso = dateObj.toISOString().split("T")[0];
|
const iso = this.getLocalIsoDate(dateObj);
|
||||||
td.dataset.date = iso;
|
td.dataset.date = iso;
|
||||||
td.textContent = String(dateObj.getDate()).padStart(2, "0");
|
td.textContent = String(dateObj.getDate()).padStart(2, "0");
|
||||||
td.className = "col-day";
|
td.className = "col-day";
|
||||||
@@ -470,7 +613,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
this.renderTwoMonthsView();
|
this.renderTwoMonthsView();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.monthCalendar.innerHTML = this.generateMonthHTML(y, m);
|
this.monthCalendar.innerHTML = this.generateMonthHTML(y, m);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,27 +641,36 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
generateMonthHTML(year, month) {
|
generateMonthHTML(year, month) {
|
||||||
let html = `<table class="fc-month-table"><thead><tr>`;
|
let html = `<table class="fc-month-table"><thead><tr>`;
|
||||||
["L", "M", "M", "J", "V"].forEach((d) => (html += `<th>${d}</th>`));
|
["L", "M", "M", "J", "V"].forEach((d) => (html += `<th>${d}</th>`));
|
||||||
html += `</tr></thead><tbody>`;
|
html += `</tr></thead><tbody><tr>`;
|
||||||
|
|
||||||
|
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||||
|
let currentRenderedCols = 0;
|
||||||
|
|
||||||
const firstDay = new Date(year, month, 1);
|
const firstDay = new Date(year, month, 1);
|
||||||
const startDay = (firstDay.getDay() + 6) % 7;
|
let startDay = (firstDay.getDay() + 6) % 7;
|
||||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
||||||
let dayCounter = 1;
|
|
||||||
|
|
||||||
for (let row = 0; row < 6; row++) {
|
if (startDay < 5) {
|
||||||
html += `<tr>`;
|
for (let i = 0; i < startDay; i++) {
|
||||||
for (let col = 0; col < 5; col++) {
|
|
||||||
if (
|
|
||||||
(row === 0 && col < startDay && startDay < 5) ||
|
|
||||||
dayCounter > daysInMonth
|
|
||||||
) {
|
|
||||||
html += `<td class="fc-day--other-month"></td>`;
|
html += `<td class="fc-day--other-month"></td>`;
|
||||||
} else if (row === 0 && startDay >= 5) {
|
currentRenderedCols++;
|
||||||
html += `<td class="fc-day--other-month"></td>`;
|
}
|
||||||
} else {
|
}
|
||||||
const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(dayCounter).padStart(2, "0")}`;
|
|
||||||
|
for (let dayCounter = 1; dayCounter <= daysInMonth; dayCounter++) {
|
||||||
|
const dateObj = new Date(year, month, dayCounter);
|
||||||
|
const dayOfWeek = dateObj.getDay();
|
||||||
|
|
||||||
|
if (dayOfWeek === 0 || dayOfWeek === 6) continue;
|
||||||
|
|
||||||
|
if (currentRenderedCols === 5) {
|
||||||
|
html += `</tr><tr>`;
|
||||||
|
currentRenderedCols = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iso = this.getLocalIsoDate(dateObj);
|
||||||
let cls = "fc-month-day";
|
let cls = "fc-month-day";
|
||||||
const dayEvts = this.events.filter((e) => e.date === iso);
|
const dayEvts = this.events.filter((e) => e.date === iso);
|
||||||
|
|
||||||
if (dayEvts.some((e) => e.type === "VACANCES_SCOLAIRES"))
|
if (dayEvts.some((e) => e.type === "VACANCES_SCOLAIRES"))
|
||||||
cls += " fc-day--school-holiday";
|
cls += " fc-day--school-holiday";
|
||||||
if (dayEvts.some((e) => e.type === "PUBLIC_HOLIDAY"))
|
if (dayEvts.some((e) => e.type === "PUBLIC_HOLIDAY"))
|
||||||
@@ -532,8 +683,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
cls += " fc-day--has-guard";
|
cls += " fc-day--has-guard";
|
||||||
if (dayEvts.some((e) => e.type === "CENTRE"))
|
if (dayEvts.some((e) => e.type === "CENTRE"))
|
||||||
cls += " fc-day--centre";
|
cls += " fc-day--centre";
|
||||||
if (dayEvts.some((e) => e.type === "AVIS"))
|
if (dayEvts.some((e) => e.type === "AVIS")) cls += " fc-day--avis";
|
||||||
cls += " fc-day--avis";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = `<div style="position:relative; height:100%;">${dayCounter}`;
|
let content = `<div style="position:relative; height:100%;">${dayCounter}`;
|
||||||
@@ -550,28 +700,23 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
content += `</div>`;
|
content += `</div>`;
|
||||||
html += `<td class="${cls}" data-date="${iso}">${content}</td>`;
|
html += `<td class="${cls}" data-date="${iso}">${content}</td>`;
|
||||||
dayCounter++;
|
currentRenderedCols++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
while (currentRenderedCols < 5 && currentRenderedCols > 0) {
|
||||||
|
html += `<td class="fc-day--other-month"></td>`;
|
||||||
|
currentRenderedCols++;
|
||||||
}
|
}
|
||||||
html += `</tr>`;
|
|
||||||
if (dayCounter > daysInMonth) break;
|
html += `</tr></tbody></table>`;
|
||||||
}
|
|
||||||
html += `</tbody></table>`;
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NOUVELLES MÉTHODES POUR LE RÉCAPITULATIF DYNAMIQUE ---
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialise les menus déroulants (Année / Mois)
|
|
||||||
*/
|
|
||||||
initSummaryControls() {
|
initSummaryControls() {
|
||||||
const typeSelect = document.getElementById("summType");
|
const typeSelect = document.getElementById("summType");
|
||||||
const valueSelect = document.getElementById("summValue");
|
const valueSelect = document.getElementById("summValue");
|
||||||
|
|
||||||
if (!typeSelect || !valueSelect) return;
|
if (!typeSelect || !valueSelect) return;
|
||||||
|
|
||||||
// 1. Scanner les données pour trouver les années et mois disponibles
|
|
||||||
const years = new Set();
|
const years = new Set();
|
||||||
const months = new Set();
|
const months = new Set();
|
||||||
|
|
||||||
@@ -586,12 +731,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const sortedYears = Array.from(years).sort();
|
const sortedYears = Array.from(years).sort();
|
||||||
const sortedMonths = Array.from(months).sort();
|
const sortedMonths = Array.from(months).sort();
|
||||||
|
|
||||||
// 2. Fonction interne pour remplir le 2ème menu (Valeurs)
|
|
||||||
const populateValues = () => {
|
const populateValues = () => {
|
||||||
const currentType = typeSelect.value;
|
const currentType = typeSelect.value;
|
||||||
const previousValue = valueSelect.value; // Pour essayer de garder la sélection si possible
|
const previousValue = valueSelect.value;
|
||||||
|
valueSelect.innerHTML = "";
|
||||||
valueSelect.innerHTML = ""; // On vide
|
|
||||||
|
|
||||||
if (currentType === "year") {
|
if (currentType === "year") {
|
||||||
sortedYears.forEach((y) => {
|
sortedYears.forEach((y) => {
|
||||||
@@ -600,8 +743,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
opt.textContent = y;
|
opt.textContent = y;
|
||||||
valueSelect.appendChild(opt);
|
valueSelect.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sélection intelligente : garder l'ancienne ou prendre l'année en cours
|
|
||||||
if (sortedYears.includes(parseInt(previousValue))) {
|
if (sortedYears.includes(parseInt(previousValue))) {
|
||||||
valueSelect.value = previousValue;
|
valueSelect.value = previousValue;
|
||||||
} else {
|
} else {
|
||||||
@@ -610,34 +751,28 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
valueSelect.value = currentYear;
|
valueSelect.value = currentYear;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Mode Mois
|
|
||||||
sortedMonths.forEach((m) => {
|
sortedMonths.forEach((m) => {
|
||||||
const [y, mo] = m.split("-");
|
const [y, mo] = m.split("-");
|
||||||
const dateObj = new Date(y, mo - 1, 1);
|
const dateObj = new Date(y, mo - 1, 1);
|
||||||
// Format : "octobre 2025"
|
|
||||||
const label = new Intl.DateTimeFormat("fr-FR", {
|
const label = new Intl.DateTimeFormat("fr-FR", {
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
}).format(dateObj);
|
}).format(dateObj);
|
||||||
|
|
||||||
const opt = document.createElement("option");
|
const opt = document.createElement("option");
|
||||||
opt.value = m;
|
opt.value = m;
|
||||||
opt.textContent = label;
|
opt.textContent = label;
|
||||||
valueSelect.appendChild(opt);
|
valueSelect.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sélection intelligente : mois courant
|
const now = new Date();
|
||||||
const nowIso = new Date().toISOString().slice(0, 7); // "2025-01"
|
const nowIso = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
||||||
if (sortedMonths.includes(nowIso)) {
|
if (sortedMonths.includes(nowIso)) {
|
||||||
valueSelect.value = nowIso;
|
valueSelect.value = nowIso;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Une fois rempli, on met à jour les chiffres
|
|
||||||
this.updateGlobalSummary();
|
this.updateGlobalSummary();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. Attacher les écouteurs (UNE SEULE FOIS)
|
|
||||||
if (!this.summaryListenersAttached) {
|
if (!this.summaryListenersAttached) {
|
||||||
typeSelect.addEventListener("change", populateValues);
|
typeSelect.addEventListener("change", populateValues);
|
||||||
valueSelect.addEventListener("change", () =>
|
valueSelect.addEventListener("change", () =>
|
||||||
@@ -645,49 +780,35 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
);
|
);
|
||||||
this.summaryListenersAttached = true;
|
this.summaryListenersAttached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Premier appel pour remplir dès le chargement
|
|
||||||
populateValues();
|
populateValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calcule et affiche les KPIs en fonction des filtres
|
|
||||||
*/
|
|
||||||
updateGlobalSummary() {
|
updateGlobalSummary() {
|
||||||
const div = document.getElementById("globalSummary");
|
const div = document.getElementById("globalSummary");
|
||||||
const typeSelect = document.getElementById("summType");
|
const typeSelect = document.getElementById("summType");
|
||||||
const valueSelect = document.getElementById("summValue");
|
const valueSelect = document.getElementById("summValue");
|
||||||
|
|
||||||
if (!div) return;
|
if (!div || (typeSelect && valueSelect && !valueSelect.value)) return;
|
||||||
|
|
||||||
// Si les contrôles n'ont pas encore de valeur (chargement initial), on attend
|
|
||||||
if (typeSelect && valueSelect && !valueSelect.value) return;
|
|
||||||
|
|
||||||
const filterType = typeSelect ? typeSelect.value : "year";
|
const filterType = typeSelect ? typeSelect.value : "year";
|
||||||
const filterValue = valueSelect
|
const filterValue = valueSelect
|
||||||
? valueSelect.value
|
? valueSelect.value
|
||||||
: new Date().getFullYear().toString(); // Fallback
|
: new Date().getFullYear().toString();
|
||||||
|
|
||||||
const stats = { off: 0, extra: 0, sick: 0, pep: 0, totalWorking: 0 };
|
const stats = { off: 0, extra: 0, sick: 0, pep: 0, totalWorking: 0 };
|
||||||
|
|
||||||
// Parcours précis jour par jour
|
|
||||||
this.weeks.forEach((w) => {
|
this.weeks.forEach((w) => {
|
||||||
Object.entries(w.dayDates).forEach(([dayName, dateObj]) => {
|
Object.entries(w.dayDates).forEach(([dayName, dateObj]) => {
|
||||||
// Vérification du filtre
|
|
||||||
let match = false;
|
let match = false;
|
||||||
if (filterType === "year") {
|
if (filterType === "year") {
|
||||||
if (dateObj.getFullYear().toString() === filterValue) match = true;
|
if (dateObj.getFullYear().toString() === filterValue) match = true;
|
||||||
} else {
|
} else {
|
||||||
// Month (YYYY-MM)
|
const isoMonth = this.getLocalIsoDate(dateObj).slice(0, 7);
|
||||||
const isoMonth = dateObj.toISOString().slice(0, 7);
|
|
||||||
if (isoMonth === filterValue) match = true;
|
if (isoMonth === filterValue) match = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
// Récupérer les events de CE jour spécifique
|
|
||||||
const dayEvents = w.dayFlags[dayName].events;
|
const dayEvents = w.dayFlags[dayName].events;
|
||||||
|
|
||||||
// Additions
|
|
||||||
dayEvents.forEach((e) => {
|
dayEvents.forEach((e) => {
|
||||||
const dur = parseFloat(e.duration) || 1;
|
const dur = parseFloat(e.duration) || 1;
|
||||||
if (e.type === "OFF_CAROLE") stats.off += dur;
|
if (e.type === "OFF_CAROLE") stats.off += dur;
|
||||||
@@ -695,15 +816,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
if (e.type === "PEP_SICK") stats.sick += dur;
|
if (e.type === "PEP_SICK") stats.sick += dur;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calcul présence (Jour ouvré - Absences)
|
const dateIso = this.getLocalIsoDate(dateObj);
|
||||||
// Est-ce un jour ouvré ? (Pas férié, pas we - WE déjà exclu par structure semainier lun-ven)
|
|
||||||
// NOTE : Ma structure `weeks` ne contient que Lun-Ven par défaut.
|
|
||||||
const dateIso = dateObj.toISOString().split("T")[0];
|
|
||||||
if (!this.publicHolidayDates.has(dateIso)) {
|
if (!this.publicHolidayDates.has(dateIso)) {
|
||||||
// C'est un jour potentiellement ouvré
|
|
||||||
stats.totalWorking++;
|
stats.totalWorking++;
|
||||||
|
|
||||||
// Calcul des absences sur ce jour
|
|
||||||
let dayAbsence = 0;
|
let dayAbsence = 0;
|
||||||
dayEvents.forEach((e) => {
|
dayEvents.forEach((e) => {
|
||||||
if (
|
if (
|
||||||
@@ -714,31 +829,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
dayAbsence += parseFloat(e.duration) || 1;
|
dayAbsence += parseFloat(e.duration) || 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
stats.pep += Math.max(0, 1 - dayAbsence);
|
stats.pep += Math.max(0, 1 - dayAbsence);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Affichage HTML Vertical
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<div class="fc-summary-item">
|
<div class="fc-summary-item"><span class="fc-summary-label">Off Carole</span><span class="fc-summary-value">${parseFloat(stats.off.toFixed(1))} jours</span></div>
|
||||||
<span class="fc-summary-label">Off Carole</span>
|
<div class="fc-summary-item"><span class="fc-summary-label">Extra Off Carole</span><span class="fc-summary-value">${parseFloat(stats.extra.toFixed(1))} jours</span></div>
|
||||||
<span class="fc-summary-value">${parseFloat(stats.off.toFixed(1))} jours</span>
|
<div class="fc-summary-item"><span class="fc-summary-label">Pep Malade</span><span class="fc-summary-value">${parseFloat(stats.sick.toFixed(1))} jours</span></div>
|
||||||
</div>
|
<div class="fc-summary-item"><span class="fc-summary-label">Présence Pep</span><span class="fc-summary-value">${parseFloat(stats.pep.toFixed(1))} jours</span></div>
|
||||||
<div class="fc-summary-item">
|
|
||||||
<span class="fc-summary-label">Extra Off Carole</span>
|
|
||||||
<span class="fc-summary-value">${parseFloat(stats.extra.toFixed(1))} jours</span>
|
|
||||||
</div>
|
|
||||||
<div class="fc-summary-item">
|
|
||||||
<span class="fc-summary-label">Pep Malade</span>
|
|
||||||
<span class="fc-summary-value">${parseFloat(stats.sick.toFixed(1))} jours</span>
|
|
||||||
</div>
|
|
||||||
<div class="fc-summary-item">
|
|
||||||
<span class="fc-summary-label">Présence Pep</span>
|
|
||||||
<span class="fc-summary-value">${parseFloat(stats.pep.toFixed(1))} jours</span>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -805,24 +906,41 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
await this.refreshAllData();
|
await this.refreshAllData();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== INTERACTIONS ==================
|
|
||||||
|
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// Planning Hebdo
|
|
||||||
if (this.planningBody) {
|
if (this.planningBody) {
|
||||||
|
// Souris (PC)
|
||||||
this.planningBody.addEventListener("mousedown", (e) =>
|
this.planningBody.addEventListener("mousedown", (e) =>
|
||||||
this.handleMouseDown(e),
|
this.handleMouseDown(e),
|
||||||
);
|
);
|
||||||
document.addEventListener("mousemove", (e) => this.handleMouseMove(e));
|
document.addEventListener("mousemove", (e) => this.handleMouseMove(e));
|
||||||
document.addEventListener("mouseup", (e) => this.handleMouseUp(e));
|
document.addEventListener("mouseup", (e) => this.handleMouseUp(e));
|
||||||
|
|
||||||
|
// Tactile (Mobile/Tablette) - NOUVEAU
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modale
|
|
||||||
const btnOpen = document.getElementById("btnOpenHolidays");
|
const btnOpen = document.getElementById("btnOpenHolidays");
|
||||||
const btnClose = document.getElementById("btnCloseHolidays");
|
const btnClose = document.getElementById("btnCloseHolidays");
|
||||||
const modal = document.getElementById("modalHolidays");
|
const modal = document.getElementById("modalHolidays");
|
||||||
if (btnOpen && modal)
|
|
||||||
btnOpen.addEventListener("click", () => (modal.style.display = "flex"));
|
if (btnOpen && modal) {
|
||||||
|
btnOpen.addEventListener("click", () => {
|
||||||
|
modal.style.display = "flex";
|
||||||
|
// Remet l'année par défaut sur l'année actuellement affichée dans le calendrier
|
||||||
|
const yearSelect = document.getElementById("holidayYearSelect");
|
||||||
|
if (yearSelect) yearSelect.value = this.currentSchoolYearStart;
|
||||||
|
this.modalSelectedYear = this.currentSchoolYearStart;
|
||||||
|
this.renderModalHolidays();
|
||||||
|
});
|
||||||
|
}
|
||||||
if (btnClose && modal)
|
if (btnClose && modal)
|
||||||
btnClose.addEventListener(
|
btnClose.addEventListener(
|
||||||
"click",
|
"click",
|
||||||
@@ -833,29 +951,26 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
if (e.target === modal) modal.style.display = "none";
|
if (e.target === modal) modal.style.display = "none";
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calendrier Mensuel
|
|
||||||
if (this.monthCalendar) {
|
if (this.monthCalendar) {
|
||||||
this.monthCalendar.addEventListener("click", (e) => {
|
this.monthCalendar.addEventListener("click", (e) => {
|
||||||
const td = e.target.closest("td[data-date]");
|
const td = e.target.closest("td[data-date]");
|
||||||
if (td) {
|
if (td && td.dataset.date) {
|
||||||
this.monthSelectedCells = [td];
|
this.monthSelectedCells = [td];
|
||||||
this.showMenu(e.pageX, e.pageY, [td.dataset.date], true);
|
this.showMenu(e.pageX, e.pageY, [td.dataset.date], true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menus
|
|
||||||
document.addEventListener("click", (e) => this.closeMenusIfOutside(e));
|
document.addEventListener("click", (e) => this.closeMenusIfOutside(e));
|
||||||
const handleMenu = (e) => {
|
const handleMenu = (e) => {
|
||||||
const btn = e.target.closest("button");
|
const btn = e.target.closest("button");
|
||||||
if (btn) this.handleMenuAction(btn.dataset);
|
if (btn && btn.dataset.action) this.handleMenuAction(btn.dataset);
|
||||||
};
|
};
|
||||||
if (this.selectionMenu)
|
if (this.selectionMenu)
|
||||||
this.selectionMenu.addEventListener("click", handleMenu);
|
this.selectionMenu.addEventListener("click", handleMenu);
|
||||||
if (this.monthSelectionMenu)
|
if (this.monthSelectionMenu)
|
||||||
this.monthSelectionMenu.addEventListener("click", handleMenu);
|
this.monthSelectionMenu.addEventListener("click", handleMenu);
|
||||||
|
|
||||||
// Nav Mensuelle
|
|
||||||
document
|
document
|
||||||
.getElementById("fc-prev-month")
|
.getElementById("fc-prev-month")
|
||||||
?.addEventListener("click", () => {
|
?.addEventListener("click", () => {
|
||||||
@@ -868,8 +983,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
this.currentMonth.setMonth(this.currentMonth.getMonth() + 1);
|
this.currentMonth.setMonth(this.currentMonth.getMonth() + 1);
|
||||||
this.renderMonthCalendar();
|
this.renderMonthCalendar();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Nav Année
|
|
||||||
document
|
document
|
||||||
.getElementById("fc-prev-school-year")
|
.getElementById("fc-prev-school-year")
|
||||||
?.addEventListener("click", () => this.changeSchoolYear(-1));
|
?.addEventListener("click", () => this.changeSchoolYear(-1));
|
||||||
@@ -877,7 +990,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
.getElementById("fc-next-school-year")
|
.getElementById("fc-next-school-year")
|
||||||
?.addEventListener("click", () => this.changeSchoolYear(1));
|
?.addEventListener("click", () => this.changeSchoolYear(1));
|
||||||
|
|
||||||
// Boutons Vue
|
|
||||||
document.querySelectorAll(".fc-view-button").forEach((btn) => {
|
document.querySelectorAll(".fc-view-button").forEach((btn) => {
|
||||||
btn.addEventListener("click", (e) => {
|
btn.addEventListener("click", (e) => {
|
||||||
document
|
document
|
||||||
@@ -907,15 +1019,48 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
handleMouseUp(e) {
|
handleMouseUp(e) {
|
||||||
if (!this.isSelecting) return;
|
if (!this.isSelecting) return;
|
||||||
// Fix : inclure la dernière cellule
|
|
||||||
const td = e.target.closest("#planningTable td[data-date]");
|
const td = e.target.closest("#planningTable td[data-date]");
|
||||||
if (td && !this.selectedCells.includes(td)) this.selectCell(td);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GESTION DU TACTILE (Swipe pour sélectionner) ---
|
||||||
|
handleTouchStart(e) {
|
||||||
|
const td = e.target.closest("#planningTable td[data-date]");
|
||||||
|
if (!td) return;
|
||||||
|
|
||||||
|
// Si l'utilisateur touche une case, on bloque le scroll de la page pour le laisser sélectionner
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
this.clearSelection();
|
||||||
|
this.isSelecting = true;
|
||||||
|
this.selectCell(td);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTouchMove(e) {
|
||||||
|
if (!this.isSelecting) return;
|
||||||
|
e.preventDefault(); // Bloque le défilement de l'écran pendant qu'on glisse le doigt
|
||||||
|
|
||||||
|
const touch = e.touches[0];
|
||||||
|
// elementFromPoint permet de savoir sur quelle case se trouve le doigt actuellement
|
||||||
|
const 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;
|
this.isSelecting = false;
|
||||||
if (!this.selectedCells.length) return;
|
if (!this.selectedCells.length) return;
|
||||||
|
|
||||||
const dates = this.selectedCells.map((c) => c.dataset.date);
|
const dates = this.selectedCells.map((c) => c.dataset.date);
|
||||||
this.showMenu(e.pageX, e.pageY, dates, false);
|
// Sur mobile, on s'en fiche de X et Y car le menu s'affiche en bas de l'écran
|
||||||
|
this.showMenu(0, 0, dates, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectCell(cell) {
|
selectCell(cell) {
|
||||||
@@ -950,45 +1095,23 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
dates.length > 1
|
dates.length > 1
|
||||||
? `${dates.length} jours`
|
? `${dates.length} jours`
|
||||||
: new Date(dates[0]).toLocaleDateString("fr-FR");
|
: new Date(dates[0]).toLocaleDateString("fr-FR");
|
||||||
|
|
||||||
let html = `<div class="fc-menu-section"><strong>${dateLabel}</strong></div>`;
|
let html = `<div class="fc-menu-section"><strong>${dateLabel}</strong></div>`;
|
||||||
|
html += `<div class="fc-menu-section"><strong>Congés Carole</strong><div class="fc-menu-grid"><button class="fc-menu-btn" data-action="add" data-type="OFF_CAROLE" data-person="Carole">Off Carole</button><button class="fc-menu-btn" data-action="add" data-type="EXTRA_OFF_CAROLE" data-person="Carole">Extra Off</button></div><button class="fc-menu-danger" data-action="clear-type" data-cat="CONGE">Effacer congés Carole</button></div>`;
|
||||||
// Carole
|
html += `<div class="fc-menu-section"><strong>Garde</strong><div class="fc-menu-grid"><button class="fc-menu-btn" data-action="add" data-type="CENTRE">Centre</button><button class="fc-menu-btn" data-action="add" data-type="AVIS">Avis</button></div><button class="fc-menu-danger" data-action="clear-type" data-cat="GARDE">Effacer Garde</button></div>`;
|
||||||
html += `<div class="fc-menu-section"><strong>Congés Carole</strong><div class="fc-menu-grid">`;
|
html += `<div class="fc-menu-section"><strong>Pep</strong><button class="fc-menu-btn" data-action="add" data-type="PEP_SICK" style="width:100%">Pep Malade</button><button class="fc-menu-danger" data-action="clear-type" data-cat="PEP">Effacer Pep</button></div>`;
|
||||||
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>`;
|
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) => {
|
["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 += `<tr><td><button class="fc-menu-btn" data-action="add-leave" data-pid="2" data-type="${t}">${t}</button></td><td><button class="fc-menu-btn" data-action="add-leave" data-pid="3" data-type="${t}">${t}</button></td></tr>`;
|
||||||
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 += `</tbody></table></div><button class="fc-menu-danger" data-action="clear-leaves">Effacer Alex/Laia</button></div>`;
|
||||||
html += `<button class="fc-menu-danger" data-action="clear-leaves">Effacer Alex/Laia</button></div>`;
|
|
||||||
|
|
||||||
menu.innerHTML = html;
|
menu.innerHTML = html;
|
||||||
|
|
||||||
const menuWidth = 240;
|
const menuWidth = 240;
|
||||||
let left = x + 10;
|
let left = x + 10;
|
||||||
if (left + menuWidth > window.innerWidth) left = x - menuWidth - 10;
|
if (left + menuWidth > window.innerWidth) left = x - menuWidth - 10;
|
||||||
|
|
||||||
menu.style.left = `${left}px`;
|
menu.style.left = `${left}px`;
|
||||||
menu.style.top = `${y + 10}px`;
|
menu.style.top = `${y + 10}px`;
|
||||||
menu.style.display = "block";
|
menu.style.display = "block";
|
||||||
|
|
||||||
this.menuJustOpened = true;
|
this.menuJustOpened = true;
|
||||||
setTimeout(() => (this.menuJustOpened = false), 100);
|
setTimeout(() => (this.menuJustOpened = false), 100);
|
||||||
}
|
}
|
||||||
@@ -1012,11 +1135,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
if (typesToClear.length) {
|
if (typesToClear.length) {
|
||||||
await this.postApi(
|
await this.postApi(
|
||||||
"/modules/family-calendar/includes/api/manage-event.php",
|
"/modules/family-calendar/includes/api/manage-event.php",
|
||||||
{
|
{ action: "bulk_delete_day_types", dates, types: typesToClear },
|
||||||
action: "bulk_delete_day_types",
|
|
||||||
dates,
|
|
||||||
types: typesToClear,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const payload = dates.map((d) => ({
|
const payload = dates.map((d) => ({
|
||||||
@@ -1037,20 +1156,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
await this.postApi(
|
await this.postApi(
|
||||||
"/modules/family-calendar/includes/api/manage-event.php",
|
"/modules/family-calendar/includes/api/manage-event.php",
|
||||||
{
|
{ action: "bulk_delete_day_types", dates, types: typesToClear },
|
||||||
action: "bulk_delete_day_types",
|
|
||||||
dates,
|
|
||||||
types: typesToClear,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
} else if (action === "add-leave") {
|
} else if (action === "add-leave") {
|
||||||
await this.postApi(
|
await this.postApi(
|
||||||
"/modules/family-calendar/includes/api/manage-leaf.php",
|
"/modules/family-calendar/includes/api/manage-leaf.php",
|
||||||
{
|
{ action: "bulk_delete_day_person", dates, person_id: pid },
|
||||||
action: "bulk_delete_day_person",
|
|
||||||
dates,
|
|
||||||
person_id: pid,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
const payload = dates.map((d) => ({
|
const payload = dates.map((d) => ({
|
||||||
date: d,
|
date: d,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user