This commit is contained in:
2026-04-09 20:42:46 +02:00
parent 6d5982d923
commit 25b1c27ba9
6 changed files with 661 additions and 169 deletions
+229 -73
View File
@@ -24,6 +24,12 @@
--text-laia: #b45309; /* Ambre foncé */
}
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Base */
.pf-family-calendar {
background-color: var(--bg-page);
@@ -707,93 +713,189 @@
}
}
/* --- 7. MENU CONTEXTUEL --- */
/* --- MENU CONTEXTUEL COMPACT ET UNIFORMISÉ (DNA Holidays) --- */
.fc-selection-menu {
position: absolute;
z-index: 9999;
background: rgba(255, 255, 255, 0.98);
border: 1px solid var(--border-color);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 8px;
min-width: 240px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(8px);
border: 1px solid rgba(226, 232, 240, 0.8);
box-shadow:
0 10px 25px -5px rgba(0, 0, 0, 0.1),
0 8px 10px -6px rgba(0, 0, 0, 0.1);
border-radius: 16px;
padding: 10px 12px; /* Base compact */
min-width: 220px; /* Base compact */
display: none;
animation: menuPopIn 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
transform-origin: top center;
}
@keyframes menuPopIn {
from {
opacity: 0;
transform: scale(0.95) translateY(10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.fc-menu-section {
padding: 8px 0;
border-bottom: 1px solid #f1f5f9;
padding: 6px 0; /* Compact */
border-bottom: 1px dashed #cbd5e1;
}
.fc-menu-section:first-child {
padding-top: 0;
}
.fc-menu-section:last-child {
border: none;
padding-bottom: 0;
}
.fc-menu-header {
/* Compact header */
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.fc-menu-section strong {
display: block;
font-size: 0.75rem;
font-size: 0.7rem; /* Base compact */
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 6px;
letter-spacing: 0.05em;
font-weight: 700;
color: #64748b;
margin-bottom: 0 !important; /* Retire l'ancienne marge */
}
.fc-menu-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.fc-menu-btn {
background: white;
border: 1px solid var(--border-color);
padding: 6px;
border-radius: 4px;
cursor: pointer;
width: 100%;
}
.fc-menu-btn:hover {
background: #eff6ff;
border-color: var(--primary);
color: var(--primary);
}
.fc-menu-danger {
width: 100%;
margin-top: 4px;
background: #fff1f2;
border: 1px solid #fda4af;
color: #be123c;
padding: 6px;
border-radius: 4px;
cursor: pointer;
}
.fc-menu-danger:hover {
background: #be123c;
color: white;
border-color: #be123c;
}
.fc-menu-leaves-table table {
width: 100%;
}
.fc-menu-leaves-table th {
font-size: 0.7rem;
color: var(--text-muted);
padding-bottom: 4px;
}
.fc-menu-leaves-table td {
padding: 2px;
gap: 4px; /* Base compact */
}
/* RESPONSIVE */
.fc-menu-btn {
background: white;
border: 1px solid #e2e8f0;
color: #1e293b;
padding: 6px 8px; /* Base compact */
border-radius: 8px;
font-size: 0.8rem; /* Base compact */
font-weight: 600;
cursor: pointer;
width: 100%;
transition: all 0.2s ease;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.fc-menu-btn:hover {
background: #f8fafc;
border-color: #3b82f6;
color: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.15);
}
/* Style des icônes de suppression unifiées */
.fc-menu-clear-icon {
background: transparent;
border: none;
color: #94a3b8;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
transition: all 0.2s ease;
}
.fc-menu-clear-icon svg {
width: 14px;
height: 14px;
}
.fc-menu-clear-icon:hover {
background: #fef2f2;
color: #dc2626;
}
/* --- TABLEAU DES CONGÉS ENFANTS --- */
.fc-menu-leaves-table table {
width: 100%;
border-spacing: 2px; /* Base compact */
border-collapse: separate;
}
.fc-menu-leaves-table th {
font-size: 0.7rem; /* Base compact */
font-weight: 700;
color: #64748b;
padding-bottom: 2px; /* Base compact */
text-align: center;
}
.fc-menu-leaves-table td {
padding: 0;
}
/* Aligner le nom et l'icône sur une ligne (Inline) */
.fc-th-inline {
display: flex;
flex-direction: row; /* Alignement horizontal */
align-items: center;
justify-content: center;
gap: 4px; /* Petit espace entre le nom et la poubelle */
}
/* --- VERSION MOBILE COMPACTE --- */
@media (max-width: 768px) {
.fc-bottom-grid {
grid-template-columns: 1fr;
.fc-selection-menu {
position: fixed !important;
top: auto !important;
bottom: 0 !important;
left: 0 !important;
width: 100% !important;
border-radius: 24px 24px 0 0;
border: none;
border-top: 1px solid rgba(226, 232, 240, 0.8);
box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.15);
padding: 12px 16px; /* Compact mobile */
padding-bottom: calc(
12px + env(safe-area-inset-bottom)
); /* Compact mobile */
max-height: 85vh;
overflow-y: auto;
animation: slideUpSheet 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
transform-origin: bottom center;
}
.fc-month-header {
flex-direction: column;
align-items: stretch;
.fc-selection-menu::before {
content: "";
display: block;
width: 40px;
height: 5px;
background: #cbd5e1;
border-radius: 10px;
margin: 0 auto 10px auto; /* Compact mobile */
}
.fc-nav-controls,
.fc-view-controls {
justify-content: center;
@keyframes slideUpSheet {
from {
opacity: 0;
transform: translateY(100%);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.pf-btn-icon-text {
width: 100%;
justify-content: center;
.fc-menu-btn {
padding: 8px; /* Compact mobile */
font-size: 0.85rem; /* Compact mobile */
}
}
@@ -821,15 +923,9 @@
}
/* =========================================================
8. OPTIMISATIONS MOBILES ET ERGONOMIE (AJOUTS)
8. OPTIMISATIONS MOBILES ET ERGONOMIE
========================================================= */
/* =========================================================
8. OPTIMISATIONS MOBILES ET ERGONOMIE
========================================================= */
/* --- A. COLONNES MOIS ET SEMAINE FIGÉES (Double Axe Blindé) --- */
/* 1. On force une largeur stricte pour éviter le décalage de la 2ème colonne */
.col-month {
box-sizing: border-box !important;
@@ -900,8 +996,7 @@
}
}
.fc-menu-btn,
.fc-menu-danger {
.fc-menu-btn {
padding: 10px;
font-size: 0.95rem;
font-weight: 600;
@@ -932,3 +1027,64 @@
right: 1px !important;
}
}
/* --- ALIGNEMENT DES EN-TÊTES DU MENU --- */
.fc-menu-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.fc-menu-header strong {
margin-bottom: 0 !important; /* Retire l'ancienne marge du bas */
}
/* Style de l'icône Poubelle */
.fc-menu-clear-icon {
background: transparent;
border: none;
color: #94a3b8; /* Gris discret par défaut */
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
transition: all 0.2s ease;
}
.fc-menu-clear-icon svg {
width: 14px;
height: 14px;
}
/* Survol de la poubelle : devient rouge clair */
.fc-menu-clear-icon:hover {
background: #fef2f2;
color: #dc2626;
}
.fc-th-stack {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding-bottom: 4px;
}
.fc-menu-btn-mini-clear {
background: #f1f5f9;
border: 1px solid #cbd5e1;
border-radius: 4px;
font-size: 10px;
padding: 2px 4px;
cursor: pointer;
transition: all 0.2s;
}
.fc-menu-btn-mini-clear:hover {
background: #fee2e2;
border-color: #fca5a5;
transform: scale(1.1);
}
+326 -71
View File
@@ -5,6 +5,31 @@ document.addEventListener("DOMContentLoaded", () => {
const CONGE_TYPES = ["OFF_CAROLE", "EXTRA_OFF_CAROLE"];
const GUARDE_TYPES = ["CENTRE", "AVIS"];
const PEP_TYPES = ["PEP_SICK"];
const FAMILY = {
ALEX: { id: 2, prefix: "alex" },
LAIA: { id: 3, prefix: "laia" },
};
const LEAVES_CONFIG = {
CP: {
startMonth: 8, // Le cycle commence en août (après la tolérance de juillet)
defaultBalance: 25,
},
JRA: {
// Tu pourras ajouter les années suivantes ici
yearlyTotals: {
2024: 10, // ex: 0.83 * 12 arrondi
2025: 10,
2026: 11, // ex: 0.9 * 12 arrondi
},
defaultBalance: 10,
toleranceMonths: 2, // Janvier et Février
maxReport: 2,
},
JA: {
[FAMILY.ALEX.id]: { startMonth: 4, startDay: 29, defaultBalance: 4 },
[FAMILY.LAIA.id]: { startMonth: 10, startDay: 1, defaultBalance: 4 }, // Date de Laia à adapter
},
};
class FamilyCalendar {
constructor() {
@@ -112,7 +137,7 @@ document.addEventListener("DOMContentLoaded", () => {
duration: parseFloat(e.duration),
}));
this.fixedEvents = this.getPublicHolidays(); // Uniquement les jours fériés maintenant
this.fixedEvents = await this.fetchPublicHolidays();
const leavesData = await this.fetchApi(
"/modules/family-calendar/includes/api/get-leaves.php",
@@ -124,7 +149,12 @@ document.addEventListener("DOMContentLoaded", () => {
);
this.leaveBalances = balancesData.balances || [];
// Les vacances scolaires sont maintenant dans dbEvents !
// --- NOUVEAU : Chargement des correctifs de congés ---
const snapshotsData = await this.fetchApi(
"/modules/family-calendar/includes/api/get-leave-snapshots.php",
);
this.leaveSnapshots = snapshotsData.snapshots || [];
this.events = [...this.dbEvents, ...this.fixedEvents];
this.publicHolidayDates = new Set(
this.fixedEvents
@@ -133,43 +163,37 @@ document.addEventListener("DOMContentLoaded", () => {
);
this.reprocessAndRender();
this.renderModalHolidays(); // Met à jour la modale
this.renderModalHolidays();
} catch (e) {
console.error("Erreur chargement données:", e);
}
}
// Les jours fériés restent hardcodés car ils sont fixes.
getPublicHolidays() {
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-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",
"2026-11-01",
"2026-11-11",
"2026-12-25",
].map((date, idx) => ({
id: `ph-${idx}`,
date,
type: "PUBLIC_HOLIDAY",
duration: 1,
}));
async fetchPublicHolidays() {
try {
// Appel à l'API officielle des jours fériés en métropole
const res = await fetch(
"https://calendrier.api.gouv.fr/jours-feries/metropole.json",
);
const holidays = await res.json();
// L'API renvoie un objet : { "2024-01-01": "Jour de l'an", ... }
// On le transforme en tableau compatible avec ton système d'événements
return Object.keys(holidays).map((date, idx) => ({
id: `ph-${idx}`,
date: date,
name: holidays[date], // On garde le nom au cas où tu veuilles l'afficher plus tard
type: "PUBLIC_HOLIDAY",
duration: 1,
}));
} catch (error) {
console.error(
"Erreur lors de la récupération des jours fériés :",
error,
);
return []; // Évite de casser le calendrier si l'API de l'État est indisponible
}
}
// --- RECONSTRUCTION DE LA MODALE VIA LA BDD (Par blocs consécutifs) ---
@@ -383,7 +407,11 @@ document.addEventListener("DOMContentLoaded", () => {
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;
l.person_id === FAMILY.ALEX.id
? FAMILY.ALEX.prefix
: l.person_id === FAMILY.LAIA.id
? FAMILY.LAIA.prefix
: null;
if (prefix) w.totals[`${prefix}${l.leave_type}`] += dur;
}
});
@@ -403,57 +431,135 @@ document.addEventListener("DOMContentLoaded", () => {
calculateMonthlyBalances() {
const balances = {
2: { CP: {}, JRA: {}, JA: {} },
3: { CP: {}, JRA: {}, JA: {} },
[FAMILY.ALEX.id]: { CP: {}, JRA: {}, JA: {} },
[FAMILY.LAIA.id]: { CP: {}, JRA: {}, JA: {} },
};
// Liste des mois actuellement affichés dans le planning
const ymSet = new Set();
this.weeks.forEach((w) => ymSet.add(w.monthKey));
const ymList = Array.from(ymSet).sort();
// Pré-calcul de l'utilisation par mois
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) => {
[FAMILY.ALEX.id, FAMILY.LAIA.id].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 cycleStartStr = "";
let initialBalance = 0;
let usedBefore = 0;
let periodStartMonth = type === "CP" ? "06" : "01";
const periodStart = `${refYear}-${periodStartMonth}`;
// --- 1. RECHERCHE D'UN CORRECTIF MANUEL (SNAPSHOT) ---
// On cherche le snapshot le plus récent qui est inférieur ou égal au mois en cours de calcul
const latestSnapshot = (this.leaveSnapshots || [])
.filter(
(s) =>
s.person_id == pid &&
s.leave_type == type &&
s.snapshot_date.substring(0, 7) <= ym,
)
// On trie du plus récent au plus ancien pour prendre le premier
.sort((a, b) =>
b.snapshot_date.localeCompare(a.snapshot_date),
)[0];
if (latestSnapshot) {
// Si on trouve un correctif, il devient notre nouveau "point zéro"
cycleStartStr = latestSnapshot.snapshot_date.substring(0, 7);
initialBalance = parseFloat(latestSnapshot.remaining_balance); // Utilisation de VOTRE nom de colonne
}
// --- 2. SINON, CALCUL CLASSIQUE PAR DÉFAUT ---
else {
if (type === "CP") {
const refYear =
currMonth >= LEAVES_CONFIG.CP.startMonth
? currYear
: currYear - 1;
cycleStartStr = `${refYear}-${String(LEAVES_CONFIG.CP.startMonth).padStart(2, "0")}`;
const dbBal = this.leaveBalances.find(
(b) =>
b.person_id == pid &&
b.leave_type == "CP" &&
b.balance_year == refYear,
);
initialBalance = dbBal
? parseFloat(dbBal.initial_balance)
: LEAVES_CONFIG.CP.defaultBalance;
} else if (type === "JRA") {
cycleStartStr = `${currYear}-01`;
initialBalance =
LEAVES_CONFIG.JRA.yearlyTotals[currYear] ||
LEAVES_CONFIG.JRA.defaultBalance;
if (currMonth <= LEAVES_CONFIG.JRA.toleranceMonths) {
const prevYear = currYear - 1;
const prevInitial =
LEAVES_CONFIG.JRA.yearlyTotals[prevYear] ||
LEAVES_CONFIG.JRA.defaultBalance;
let usedPrevYear = 0;
for (let m = 1; m <= 12; m++) {
const mStr = `${prevYear}-${String(m).padStart(2, "0")}`;
usedPrevYear += usageByMonth[pid]?.[type]?.[mStr] || 0;
}
const remainingPrevYear = Math.max(
0,
prevInitial - usedPrevYear,
);
initialBalance += Math.min(
remainingPrevYear,
LEAVES_CONFIG.JRA.maxReport,
);
}
} else if (type === "JA") {
const configJA = LEAVES_CONFIG.JA[pid];
const isPastAnniversary =
currMonth > configJA.startMonth ||
(currMonth === configJA.startMonth &&
configJA.startDay === 1);
const refYear = isPastAnniversary ? currYear : currYear - 1;
cycleStartStr = `${refYear}-${String(configJA.startMonth).padStart(2, "0")}`;
const dbBal = this.leaveBalances.find(
(b) =>
b.person_id == pid &&
b.leave_type == "JA" &&
b.balance_year == refYear,
);
initialBalance = dbBal
? parseFloat(dbBal.initial_balance)
: configJA.defaultBalance;
}
}
// --- 3. DÉDUCTION DES CONGÉS PRIS DEPUIS LE POINT ZÉRO ---
let usedBeforeCurrentMonth = 0;
Object.keys(usageByMonth[pid]?.[type] || {}).forEach((usedYm) => {
if (usedYm >= periodStart && usedYm < ym)
usedBefore += usageByMonth[pid][type][usedYm];
// On ne déduit que ce qui a été posé entre le début du cycle (ou la date du snapshot) et le mois en cours
if (usedYm >= cycleStartStr && usedYm < ym) {
usedBeforeCurrentMonth += usageByMonth[pid][type][usedYm];
}
});
const available = Math.max(0, initial - usedBefore);
const available = Math.max(
0,
initialBalance - usedBeforeCurrentMonth,
);
const usedInMonth = usageByMonth[pid]?.[type]?.[ym] || 0;
balances[pid][type][ym] = {
availableAtMonthStart: available,
usedInMonth: usedInMonth,
@@ -461,6 +567,7 @@ document.addEventListener("DOMContentLoaded", () => {
});
});
});
this.monthlyLeaveBalances = balances;
}
@@ -569,8 +676,8 @@ document.addEventListener("DOMContentLoaded", () => {
tr.appendChild(tdUse);
});
};
renderPersonCols(2, "col-alex");
renderPersonCols(3, "col-laia");
renderPersonCols(FAMILY.ALEX.id, `col-${FAMILY.ALEX.prefix}`);
renderPersonCols(FAMILY.LAIA.id, `col-${FAMILY.LAIA.prefix}`);
}
this.planningBody.appendChild(tr);
});
@@ -915,7 +1022,7 @@ document.addEventListener("DOMContentLoaded", () => {
document.addEventListener("mousemove", (e) => this.handleMouseMove(e));
document.addEventListener("mouseup", (e) => this.handleMouseUp(e));
// Tactile (Mobile/Tablette) - NOUVEAU
// Tactile (Mobile/Tablette)
this.planningBody.addEventListener(
"touchstart",
(e) => this.handleTouchStart(e),
@@ -927,6 +1034,7 @@ document.addEventListener("DOMContentLoaded", () => {
document.addEventListener("touchend", (e) => this.handleTouchEnd(e));
}
// --- GESTION MODALE VACANCES SCOLAIRES ---
const btnOpen = document.getElementById("btnOpenHolidays");
const btnClose = document.getElementById("btnCloseHolidays");
const modal = document.getElementById("modalHolidays");
@@ -951,6 +1059,67 @@ document.addEventListener("DOMContentLoaded", () => {
if (e.target === modal) modal.style.display = "none";
});
// --- GESTION MODALE CORRECTIF DES SOLDES (SNAPSHOTS) ---
const btnSnap = document.getElementById("btnOpenSnapshotModal");
const modalSnap = document.getElementById("modalSnapshot");
const btnCloseSnap = document.getElementById("btnCloseSnapshot");
const formSnap = document.getElementById("formSnapshot");
if (btnSnap && modalSnap) {
btnSnap.addEventListener("click", () => {
modalSnap.style.display = "flex";
// Pré-remplir la date avec le 1er jour du mois en cours
const today = new Date();
const y = today.getFullYear();
const m = String(today.getMonth() + 1).padStart(2, "0");
const snapDateInput = document.getElementById("snapDate");
if (snapDateInput) snapDateInput.value = `${y}-${m}-01`;
});
}
if (btnCloseSnap && modalSnap) {
btnCloseSnap.addEventListener(
"click",
() => (modalSnap.style.display = "none"),
);
}
if (modalSnap) {
modalSnap.addEventListener("click", (e) => {
if (e.target === modalSnap) modalSnap.style.display = "none";
});
}
if (formSnap) {
formSnap.addEventListener("submit", async (e) => {
e.preventDefault(); // Empêche le rechargement de la page
const payload = {
person_id: document.getElementById("snapPerson").value,
leave_type: document.getElementById("snapType").value,
snapshot_date: document.getElementById("snapDate").value,
remaining_balance: document.getElementById("snapBalance").value,
};
try {
// On envoie à la nouvelle API
await this.postApi(
"/modules/family-calendar/includes/api/save-leave-snapshot.php",
payload,
);
modalSnap.style.display = "none";
formSnap.reset(); // On vide le formulaire
// On rafraîchit tout le calendrier pour appliquer le nouveau solde immédiatement
await this.refreshAllData();
} catch (error) {
alert("Erreur lors de la sauvegarde du correctif.");
}
});
}
// --- GESTION DU RESTE DE L'INTERFACE ---
if (this.monthCalendar) {
this.monthCalendar.addEventListener("click", (e) => {
const td = e.target.closest("td[data-date]");
@@ -966,6 +1135,7 @@ document.addEventListener("DOMContentLoaded", () => {
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)
@@ -1095,23 +1265,92 @@ document.addEventListener("DOMContentLoaded", () => {
dates.length > 1
? `${dates.length} jours`
: new Date(dates[0]).toLocaleDateString("fr-FR");
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>`;
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>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 += `<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>`;
// Uniformisation : On définit l'icône SVG une seule fois
const trashSvg = `<svg viewBox="0 0 24 24" 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>`;
// En-tête principal avec la date
let html = `<div class="fc-menu-section" style="border-bottom: none; padding-bottom: 0;">
<strong style="font-size:0.85rem; color:var(--text-main); margin-bottom: 4px;">${dateLabel}</strong>
</div>`;
// Fonction utilitaire pour créer une ligne de titre avec l'icône SVG
const buildHeader = (title, action, cat) => `
<div class="fc-menu-header">
<strong>${title}</strong>
${action ? `<button class="fc-menu-clear-icon" title="Effacer" data-action="${action}" ${cat ? `data-cat="${cat}"` : ""}>${trashSvg}</button>` : ""}
</div>
`;
// Section Carole
html += `<div class="fc-menu-section">
${buildHeader("Congés Carole", "clear-type", "CONGE")}
<div class="fc-menu-grid">
<button class="fc-menu-btn" data-action="add" data-type="OFF_CAROLE" data-person="Carole">Off</button>
<button class="fc-menu-btn" data-action="add" data-type="EXTRA_OFF_CAROLE" data-person="Carole">Extra</button>
</div>
</div>`;
// Section Garde
html += `<div class="fc-menu-section">
${buildHeader("Garde", "clear-type", "GARDE")}
<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>
</div>`;
// Section Pep
html += `<div class="fc-menu-section">
${buildHeader("Pep", "clear-type", "PEP")}
<button class="fc-menu-btn" data-action="add" data-type="PEP_SICK" style="width:100%">Pep Malade 🤒</button>
</div>`;
// --- SECTION ALEX & LAIA AVEC SUPPRESSION INDÉPENDANTE, UNIFORMISÉE ET ALIGNÉE À CÔTÉ (INLINE) ---
html += `<div class="fc-menu-section">
${buildHeader("Congés Enfants", null, null)}
<div class="fc-menu-leaves-table">
<table>
<thead>
<tr>
<th>
<div class="fc-th-inline">
Alex
<button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="2" title="Effacer Alex">${trashSvg}</button>
</div>
</th>
<th>
<div class="fc-th-inline">
Laia
<button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="3" title="Effacer Laia">${trashSvg}</button>
</div>
</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><td><button class="fc-menu-btn" data-action="add-leave" data-pid="3" data-type="${t}">${t}</button></td></tr>`;
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 += `</tbody></table></div><button class="fc-menu-danger" data-action="clear-leaves">Effacer Alex/Laia</button></div>`;
html += ` </tbody>
</table>
</div>
</div>`;
menu.innerHTML = html;
// Positionnement intelligent du menu
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);
}
@@ -1161,11 +1400,16 @@ document.addEventListener("DOMContentLoaded", () => {
} 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 },
{
action: "bulk_delete_day_person",
dates,
person_id: parseInt(pid),
},
);
const payload = dates.map((d) => ({
date: d,
person_id: pid,
person_id: parseInt(pid), // On s'assure que c'est bien un format numérique
leave_type: type,
duration: 1,
}));
@@ -1173,7 +1417,18 @@ document.addEventListener("DOMContentLoaded", () => {
"/modules/family-calendar/includes/api/save-leaves.php",
payload,
);
} else if (action === "clear-leaves-person") {
// --- NOUVELLE ACTION : Suppression ciblée (Alex OU Laia selon le pid) ---
await this.postApi(
"/modules/family-calendar/includes/api/manage-leaf.php",
{
action: "bulk_delete_day_person",
dates,
person_id: parseInt(pid),
},
);
} else if (action === "clear-leaves") {
// --- ACTION GLOBALE : Gardée au cas où vous en auriez besoin ailleurs ---
await this.postApi(
"/modules/family-calendar/includes/api/manage-leaf.php",
{ action: "bulk_delete_day_person", dates, person_id: 2 },
@@ -47,9 +47,12 @@ try {
if ($action === 'bulk_delete_day_types') {
$dates = $input['dates'] ?? [];
$types = $input['types'] ?? [];
$dates = array_filter($dates, function($d) {
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
});
if (empty($dates) || empty($types)) {
echo json_encode(['status' => 'success']); // Rien à faire
echo json_encode(['status' => 'success']);
exit;
}
@@ -70,7 +73,11 @@ try {
// --- SUPPRESSION TOTALE SUR DES DATES ---
if ($action === 'bulk_delete_all') {
$dates = $input['dates'] ?? [];
if (empty($dates)) {
$dates = array_filter($dates, function($d) {
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
});
if (empty($dates) || empty($types)) {
echo json_encode(['status' => 'success']);
exit;
}
@@ -1,18 +1,18 @@
<?php
// includes/api/save-events.php
header('Content-Type: application/json');
require __DIR__ . '/../../../../includes/db.php';
$eventsToSave = json_decode(file_get_contents('php://input'), true);
$rawInput = file_get_contents('php://input');
$eventsToSave = json_decode($rawInput, true);
// Optionnel : tu peux commenter/supprimer ces logs en production pour économiser du disque
file_put_contents(
__DIR__ . '/events-debug.log',
"[" . date('c') . "] RAW INPUT: " . $rawInput . PHP_EOL,
FILE_APPEND
);
$eventsToSave = json_decode($rawInput, true);
if (empty($eventsToSave) || !is_array($eventsToSave)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Aucune donnée d\'événement reçue.']);
@@ -22,6 +22,14 @@ if (empty($eventsToSave) || !is_array($eventsToSave)) {
$inserted = [];
try {
// 1. OPTIMISATION : On récupère toutes les personnes d'un coup (Mapping)
$stmtPeople = $pdo->query("SELECT id, name FROM pf_people");
$peopleMap = [];
while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) {
// On crée un tableau associatif : ['Carole' => 1, 'Alex' => 2, etc.]
$peopleMap[$row['name']] = $row['id'];
}
$pdo->beginTransaction();
$sql = "INSERT INTO pf_events (event_date, event_type, person_id, duration)
@@ -31,14 +39,9 @@ try {
foreach ($eventsToSave as $event) {
$person_id = null;
// Si un nom de personne est fourni
if (!empty($event['person'])) {
$personStmt = $pdo->prepare("SELECT id FROM pf_people WHERE name = ?");
$personStmt->execute([$event['person']]);
$personRow = $personStmt->fetch();
if ($personRow) {
$person_id = $personRow['id'];
}
// 2. On vérifie simplement dans notre tableau (plus de requête SQL ici !)
if (!empty($event['person']) && isset($peopleMap[$event['person']])) {
$person_id = $peopleMap[$event['person']];
}
$stmt->execute([
@@ -68,4 +71,4 @@ try {
$pdo->rollBack();
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Erreur lors de la sauvegarde : ' . $e->getMessage()]);
}
}
@@ -0,0 +1,31 @@
<?php
header('Content-Type: application/json');
require __DIR__ . '/../../../../includes/db.php';
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || empty($input['person_id']) || empty($input['leave_type']) || empty($input['snapshot_date']) || !isset($input['remaining_balance'])) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Données manquantes.']);
exit;
}
try {
// Étape 1 : Par sécurité, on supprime un éventuel snapshot existant exactement à la même date pour cette personne et ce type
$stmtDel = $pdo->prepare("DELETE FROM pf_leave_snapshots WHERE person_id = ? AND leave_type = ? AND snapshot_date = ?");
$stmtDel->execute([$input['person_id'], $input['leave_type'], $input['snapshot_date']]);
// Étape 2 : On insère le nouveau solde
$stmt = $pdo->prepare("INSERT INTO pf_leave_snapshots (person_id, leave_type, snapshot_date, remaining_balance) VALUES (?, ?, ?, ?)");
$stmt->execute([
$input['person_id'],
$input['leave_type'],
$input['snapshot_date'],
$input['remaining_balance']
]);
echo json_encode(['status' => 'success']);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}