calendar
This commit is contained in:
+46
-6
@@ -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,19 +16,23 @@ $pageCss = "/modules/family-calendar/family-calendar.css";
|
||||
require __DIR__ . '/header.php';
|
||||
?>
|
||||
|
||||
<script>
|
||||
const serverData = <?php echo json_encode($dbEvents, JSON_NUMERIC_CHECK); ?>;
|
||||
</script>
|
||||
|
||||
|
||||
<div class="pf-container" style="max-width:100%; padding:0;">
|
||||
<div class="fc-header-row">
|
||||
<h1>Family Calendar</h1>
|
||||
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button id="btnOpenSnapshotModal" class="pf-btn-icon-text">
|
||||
<span class="icon">⚖️</span>
|
||||
<span class="text">Corriger les soldes</span>
|
||||
</button>
|
||||
<button id="btnOpenHolidays" class="pf-btn-icon-text">
|
||||
<span class="icon">🏖️</span>
|
||||
<span class="text">Voir les vacances scolaires</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modalHolidays" class="fc-modal-overlay" style="display: none;">
|
||||
<div class="fc-modal-content">
|
||||
@@ -55,6 +56,43 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modalSnapshot" class="fc-modal-overlay" style="display: none;">
|
||||
<div class="fc-modal-content" style="max-width: 400px;">
|
||||
<div class="fc-modal-header">
|
||||
<h2>Ajuster un solde</h2>
|
||||
<button id="btnCloseSnapshot" class="fc-modal-close">×</button>
|
||||
</div>
|
||||
<div class="fc-modal-body" style="padding: 24px;">
|
||||
<form id="formSnapshot">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<label class="pf-label">Personne</label>
|
||||
<select id="snapPerson" class="pf-input" required>
|
||||
<option value="2">Alex</option>
|
||||
<option value="3">Laia</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="margin-bottom: 16px;">
|
||||
<label class="pf-label">Type de congé</label>
|
||||
<select id="snapType" class="pf-input" required>
|
||||
<option value="CP">CP</option>
|
||||
<option value="JRA">JRA</option>
|
||||
<option value="JA">JA</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="margin-bottom: 16px;">
|
||||
<label class="pf-label">Date d'application</label>
|
||||
<input type="date" id="snapDate" class="pf-input" required title="Choisissez de préférence le 1er jour d'un mois">
|
||||
</div>
|
||||
<div style="margin-bottom: 24px;">
|
||||
<label class="pf-label">Solde restant à cette date</label>
|
||||
<input type="number" step="0.5" id="snapBalance" class="pf-input" placeholder="ex: 12.5" required>
|
||||
</div>
|
||||
<button type="submit" class="pf-btn" style="width: 100%;">Enregistrer le correctif</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="pf-section">
|
||||
<div class="fc-month-calendar-wrapper">
|
||||
<div class="fc-month-header">
|
||||
@@ -77,6 +115,8 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
<section class="pf-section">
|
||||
<div class="fc-week-header">
|
||||
<h2>Planning hebdo</h2>
|
||||
|
||||
@@ -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 */
|
||||
@media (max-width: 768px) {
|
||||
.fc-bottom-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.fc-month-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.fc-nav-controls,
|
||||
.fc-view-controls {
|
||||
justify-content: center;
|
||||
}
|
||||
.pf-btn-icon-text {
|
||||
.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-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-selection-menu::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 5px;
|
||||
background: #cbd5e1;
|
||||
border-radius: 10px;
|
||||
margin: 0 auto 10px auto; /* Compact mobile */
|
||||
}
|
||||
|
||||
@keyframes slideUpSheet {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fc-menu-btn {
|
||||
padding: 8px; /* Compact mobile */
|
||||
font-size: 0.85rem; /* Compact mobile */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,16 +922,10 @@
|
||||
box-shadow: inset 0 -8px 0 0 var(--c-school-holiday) !important;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
8. OPTIMISATIONS MOBILES ET ERGONOMIE (AJOUTS)
|
||||
========================================================= */
|
||||
|
||||
/* =========================================================
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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) => ({
|
||||
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: 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(
|
||||
let cycleStartStr = "";
|
||||
let initialBalance = 0;
|
||||
|
||||
// --- 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 == type &&
|
||||
b.leave_type == "CP" &&
|
||||
b.balance_year == refYear,
|
||||
);
|
||||
const initial = initialObj
|
||||
? parseFloat(initialObj.initial_balance)
|
||||
: type === "CP"
|
||||
? 25
|
||||
: type === "JRA"
|
||||
? 10
|
||||
: 0;
|
||||
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;
|
||||
|
||||
let usedBefore = 0;
|
||||
let periodStartMonth = type === "CP" ? "06" : "01";
|
||||
const periodStart = `${refYear}-${periodStartMonth}`;
|
||||
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([
|
||||
|
||||
@@ -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()]);
|
||||
}
|
||||
Reference in New Issue
Block a user