From 2e3056967572d8a3386c1fd372729810799abdf8 Mon Sep 17 00:00:00 2001 From: "fefe.clochette" Date: Fri, 19 Jun 2026 12:30:26 +0200 Subject: [PATCH] save event --- modules/family-calendar/family-calendar.js | 151 ++++++++++-------- .../includes/api/save-events.php | 15 +- 2 files changed, 90 insertions(+), 76 deletions(-) diff --git a/modules/family-calendar/family-calendar.js b/modules/family-calendar/family-calendar.js index 85ec5f2..d9d900d 100644 --- a/modules/family-calendar/family-calendar.js +++ b/modules/family-calendar/family-calendar.js @@ -397,12 +397,15 @@ function renderMemberLeavesView(personId, memberLeaves) { const moisRenouv = ml.anniversary_date ? parseInt(ml.anniversary_date.split("-")[1]) : 1; + const methodeLabel = ml.method === "ACCUMULATED" ? "Graduel" : "Fixe"; html += `
${ml.leave_type} - (Quota: ${ml.allowance}j - Renouv: Mois ${moisRenouv}) + + (Quota: ${ml.allowance}j - Renouv: Mois ${moisRenouv} - Acquis: ${methodeLabel}) +
@@ -411,7 +414,7 @@ function renderMemberLeavesView(personId, memberLeaves) { html += ``; } - // Ajout du selecteur de mois dans l'attribution + // Ajout du formulaire d'attribution complet html += `
+ Attribuer un congé
@@ -459,55 +462,45 @@ function renderMemberLeavesView(personId, memberLeaves) { } async function addMemberLeave(personId) { + // 1. Récupération des valeurs du formulaire const leaveCode = document.getElementById("new-member-leave-type").value; const allowance = document.getElementById("new-member-leave-allowance").value; const resetMonth = document.getElementById("new-member-leave-reset").value; - const method = document.getElementById("new-member-leave-method").value; // <-- Ajout - fd.append("method", method); + const method = document.getElementById("new-member-leave-method").value; - if (!leaveCode) return showToast("Veuillez sélectionner un type", "error"); + // 2. Sécurité : vérifier qu'un type de congé a bien été sélectionné + if (!leaveCode) { + return window.showToast + ? showToast("Veuillez sélectionner un type de congé", "error") + : alert("Veuillez sélectionner un type de congé"); + } + // 3. Préparation des données pour l'API const fd = new FormData(); fd.append("action", "add_person_leave"); fd.append("person_id", personId); fd.append("leave_type", leaveCode); fd.append("allowance", allowance); fd.append("reset_month", resetMonth); + fd.append("method", method); + // 4. Appel à l'API et rafraîchissement try { const res = await pachaFetch( "/modules/family-calendar/includes/api/calendar-settings.php", { method: "POST", body: fd }, ); + if (!res.success) throw new Error(res.error); if (window.showToast) showToast("Congé attribué avec succès !", "success"); + + // Recharge la vue du membre pour faire apparaître la nouvelle ligne immédiatement loadMemberConfigView(); } catch (err) { - alert("Erreur: " + err.message); - } -} - -// Supprimer un congé d'un membre -async function deleteMemberLeave(leaveId, personId) { - if (!confirm(tr("confirm_delete") || "Retirer ce congé pour ce membre ?")) - return; - - const fd = new FormData(); - fd.append("action", "delete_person_leave"); - fd.append("id", leaveId); - - try { - const res = await pachaFetch( - "/modules/family-calendar/includes/api/calendar-settings.php", - { method: "POST", body: fd }, - ); - if (!res.success) throw new Error(res.error); - - if (window.showToast) showToast("Congé retiré !", "success"); - loadMemberConfigView(); - } catch (err) { - alert("Erreur: " + err.message); + if (window.showToast) + showToast("Erreur lors de l'attribution : " + err.message, "error"); + else alert("Erreur: " + err.message); } } @@ -837,62 +830,77 @@ document.addEventListener("DOMContentLoaded", () => { calculateMonthlyBalances() { const balances = {}; - this.parents.forEach((p) => (balances[p.id] = {})); + this.parents.forEach((p) => (balances[String(p.id)] = {})); const ymSet = new Set(); this.weeks.forEach((w) => ymSet.add(w.monthKey)); const ymList = Array.from(ymSet).sort(); const usageByMonth = {}; + + // 1. On calcule ce qui a été posé (Strictement typé en String pour éviter les bugs) this.leaves.forEach((l) => { - const pid = l.person_id, - type = l.leave_type, - ym = l.leave_date.substring(0, 7); + const pid = String(l.person_id); + const type = String(l.leave_type).trim().toUpperCase(); + const ym = String(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); + (usageByMonth[pid][type][ym] || 0) + (parseFloat(l.duration) || 1); }); + // 2. On calcule les soldes pour chaque mois this.parents.forEach((parent) => { - const pid = parent.id; + const pid = String(parent.id); const matrix = this.leaveMatrix[pid] || []; matrix.forEach((conf) => { - const type = conf.type; - balances[pid][type] = {}; + const type = String(conf.type).trim().toUpperCase(); + if (!balances[pid][type]) balances[pid][type] = {}; ymList.forEach((ym) => { const [currYear, currMonth] = ym.split("-").map(Number); - let cycleStartStr = "", - initialBalance = parseFloat(conf.allowance || 0); + let cycleStartStr = ""; + let initialBalance = parseFloat(conf.allowance || 0); + let monthRenouvellement = 1; if (conf.date) { const parts = conf.date.split("-"); - if (parts.length >= 2) { - const monthRenouvellement = parseInt(parts[1]); - const dayRenouvellement = - parts.length === 3 ? parseInt(parts[2]) : 1; - const isPastAnniversary = - currMonth > monthRenouvellement || - (currMonth === monthRenouvellement && 1 >= dayRenouvellement); - const refYear = isPastAnniversary ? currYear : currYear - 1; - cycleStartStr = `${refYear}-${String(monthRenouvellement).padStart(2, "0")}`; - } - } else { - cycleStartStr = `${currYear}-01`; + if (parts.length >= 2) monthRenouvellement = parseInt(parts[1]); + } + + const isPastAnniversary = + currMonth > monthRenouvellement || + (currMonth === monthRenouvellement && 1 >= 1); + const refYear = isPastAnniversary ? currYear : currYear - 1; + cycleStartStr = `${refYear}-${String(monthRenouvellement).padStart(2, "0")}`; + + // 🔥 LE FIX : GESTION DU MODE FIXE VS GRADUEL + let acquiredBalance = initialBalance; + if (conf.method === "ACCUMULATED") { + // On calcule le nombre de mois passés depuis la date anniversaire + let monthsPassed = + (currYear - refYear) * 12 + + (currMonth - monthRenouvellement) + + 1; + acquiredBalance = Math.min( + initialBalance, + (initialBalance / 12) * monthsPassed, + ); } let usedBeforeCurrentMonth = 0; Object.keys(usageByMonth[pid]?.[type] || {}).forEach((usedYm) => { - if (usedYm >= cycleStartStr && usedYm < ym) + if (usedYm >= cycleStartStr && usedYm < ym) { usedBeforeCurrentMonth += usageByMonth[pid][type][usedYm]; + } }); balances[pid][type][ym] = { availableAtMonthStart: Math.max( 0, - initialBalance - usedBeforeCurrentMonth, + acquiredBalance - usedBeforeCurrentMonth, ), usedInMonth: usageByMonth[pid]?.[type]?.[ym] || 0, }; @@ -1242,32 +1250,38 @@ document.addEventListener("DOMContentLoaded", () => { renderMonthBalances() { const container = document.getElementById("fc-month-balances"); if (!container) return; - const monthsToDisplay = [], - y = this.currentMonth.getFullYear(), - m = this.currentMonth.getMonth(); + + const monthsToDisplay = []; + const y = this.currentMonth.getFullYear(); + const m = this.currentMonth.getMonth(); let numMonths = this.viewMode === "2months" ? 2 : this.viewMode === "3months" ? 3 : 1; - for (let i = 0; i < numMonths; i++) + + for (let i = 0; i < numMonths; i++) { monthsToDisplay.push(`${y}-${String(m + i + 1).padStart(2, "0")}`); + } container.style.display = "flex"; container.innerHTML = this.parents .map((person) => { let cards = `
${person.name.toUpperCase()}
`; - const types = this.leaveMatrix[person.id] || []; + // Typage strict String(person.id) pour matcher avec le calcul des balances + const types = this.leaveMatrix[String(person.id)] || []; types.forEach((conf) => { const type = conf.type; const startBal = - this.monthlyLeaveBalances[person.id]?.[type]?.[monthsToDisplay[0]] - ?.availableAtMonthStart || 0; + this.monthlyLeaveBalances[String(person.id)]?.[type]?.[ + monthsToDisplay[0] + ]?.availableAtMonthStart || 0; + let totalUsed = 0; - monthsToDisplay.forEach( - (ym) => - (totalUsed += - this.monthlyLeaveBalances[person.id]?.[type]?.[ym] - ?.usedInMonth || 0), - ); + monthsToDisplay.forEach((ym) => { + totalUsed += + this.monthlyLeaveBalances[String(person.id)]?.[type]?.[ym] + ?.usedInMonth || 0; + }); + const endBal = Math.max(0, startBal - totalUsed); const fmt = (n) => n > 0 ? (Number.isInteger(n) ? n : n.toFixed(1)) : "0"; @@ -1275,14 +1289,13 @@ document.addEventListener("DOMContentLoaded", () => { let alertHtml = ""; const cMonth = parseInt(monthsToDisplay[0].split("-")[1]); - // On récupère le mois de renouvellement depuis la date (ex: "2000-06-01" -> 6) + // Gestion de l'alerte 🔥 (Mois en cours ou Mois précédant le renouvellement) if (endBal > 0 && conf.date) { const resetMonth = parseInt(conf.date.split("-")[1]); - // On alerte le mois même, ou le mois juste avant ! const alertMonth = resetMonth - 1 === 0 ? 12 : resetMonth - 1; if (cMonth === alertMonth || cMonth === resetMonth) { - alertHtml = `
🔥
`; + alertHtml = `
🔥
`; } } diff --git a/modules/family-calendar/includes/api/save-events.php b/modules/family-calendar/includes/api/save-events.php index 22dfc66..4943491 100644 --- a/modules/family-calendar/includes/api/save-events.php +++ b/modules/family-calendar/includes/api/save-events.php @@ -20,9 +20,8 @@ try { $inserted = []; foreach ($eventsToSave as $event) { - // 🔥 SÉCURITÉ : On ignore l'itération si les données vitales manquent (évite le crash SQL) if (empty($event['date']) || empty($event['type'])) { - continue; + continue; // Sécurité anti-crash } $person_id = 0; @@ -32,11 +31,13 @@ try { $person_id = (int)$event['person']; } + // 🔥 LE FIX : On tronque à 50 caractères pour éviter l'erreur "Data truncated" + $safeType = substr(trim($event['type']), 0, 50); $duration = isset($event['duration']) ? (float)$event['duration'] : 1.0; $stmt->execute([ ':event_date' => $event['date'], - ':event_type' => $event['type'], + ':event_type' => $safeType, ':person_id' => $person_id, ':duration' => $duration, ]); @@ -44,7 +45,7 @@ try { $inserted[] = [ 'id' => $pdo->lastInsertId(), 'date' => $event['date'], - 'type' => $event['type'], + 'type' => $safeType, 'duration' => $duration, 'person_id' => $person_id, ]; @@ -58,14 +59,14 @@ try { 'inserted' => $inserted, ]); -} catch (\Throwable $e) { // 🔥 LE CORRECTIF PRINCIPAL : \Throwable attrape AUSSI les erreurs fatales PHP ! +} catch (\Throwable $e) { // 🔥 LE FIX : \Throwable attrape AUSSI les fatals errors PDO if (isset($pdo) && $pdo->inTransaction()) { $pdo->rollBack(); } http_response_code(500); echo json_encode([ - 'success' => false, - 'status' => 'error', + 'success' => false, + 'status' => 'error', 'message' => 'Erreur Serveur/SQL : ' . $e->getMessage() ]); }