diff --git a/family-calendar.php b/family-calendar.php index b6d3aed..5246cfd 100644 --- a/family-calendar.php +++ b/family-calendar.php @@ -7,9 +7,6 @@ require __DIR__ . '/includes/auth.php'; require_login(); require __DIR__ . '/includes/db.php'; -// Récupération des événements -$stmt_events = $pdo->query("SELECT * FROM pf_events"); -$dbEvents = $stmt_events->fetchAll(PDO::FETCH_ASSOC); $pageTitle = "PachaFamily - Family Calendar"; $activePage = "family-calendar"; @@ -19,18 +16,22 @@ $pageCss = "/modules/family-calendar/family-calendar.css"; require __DIR__ . '/header.php'; ?> - +

Family Calendar

- +
+ + +
+ +
@@ -77,6 +115,8 @@ require __DIR__ . '/header.php';
+ +

Planning hebdo

diff --git a/modules/family-calendar/family-calendar.css b/modules/family-calendar/family-calendar.css index 585fff9..0e05bfb 100644 --- a/modules/family-calendar/family-calendar.css +++ b/modules/family-calendar/family-calendar.css @@ -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); +} diff --git a/modules/family-calendar/family-calendar.js b/modules/family-calendar/family-calendar.js index a81cd18..3dc69bb 100644 --- a/modules/family-calendar/family-calendar.js +++ b/modules/family-calendar/family-calendar.js @@ -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 = `
${dateLabel}
`; - html += `
Congés Carole
`; - html += `
Garde
`; - html += `
Pep
`; - html += `
Congés Alex / Laia
`; + + // Uniformisation : On définit l'icône SVG une seule fois + const trashSvg = ``; + + // En-tête principal avec la date + let html = `
+ ${dateLabel} +
`; + + // Fonction utilitaire pour créer une ligne de titre avec l'icône SVG + const buildHeader = (title, action, cat) => ` +
+ ${title} + ${action ? `` : ""} +
+ `; + + // Section Carole + html += `
+ ${buildHeader("Congés Carole", "clear-type", "CONGE")} +
+ + +
+
`; + + // Section Garde + html += `
+ ${buildHeader("Garde", "clear-type", "GARDE")} +
+ + +
+
`; + + // Section Pep + html += `
+ ${buildHeader("Pep", "clear-type", "PEP")} + +
`; + + // --- SECTION ALEX & LAIA AVEC SUPPRESSION INDÉPENDANTE, UNIFORMISÉE ET ALIGNÉE À CÔTÉ (INLINE) --- + html += `
+ ${buildHeader("Congés Enfants", null, null)} +
+
AlexLaia
+ + + + + + + `; + ["CP", "JRA", "JA"].forEach((t) => { - html += ``; + html += ` + + + `; }); - html += `
+
+ Alex + +
+
+
+ Laia + +
+
`; + html += ` + +
+ `; 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 }, diff --git a/modules/family-calendar/includes/api/manage-event.php b/modules/family-calendar/includes/api/manage-event.php index cb744c3..ea3a4fb 100644 --- a/modules/family-calendar/includes/api/manage-event.php +++ b/modules/family-calendar/includes/api/manage-event.php @@ -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; } diff --git a/modules/family-calendar/includes/api/save-events.php b/modules/family-calendar/includes/api/save-events.php index f139d6a..c8b5cbf 100644 --- a/modules/family-calendar/includes/api/save-events.php +++ b/modules/family-calendar/includes/api/save-events.php @@ -1,18 +1,18 @@ '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()]); -} +} \ No newline at end of file diff --git a/modules/family-calendar/includes/api/save-leave-snapshot.php b/modules/family-calendar/includes/api/save-leave-snapshot.php new file mode 100644 index 0000000..0f9c9fb --- /dev/null +++ b/modules/family-calendar/includes/api/save-leave-snapshot.php @@ -0,0 +1,31 @@ + '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()]); +} \ No newline at end of file