save event
Deploy HouseHub / deploy (push) Successful in 2s

This commit is contained in:
2026-06-19 12:30:26 +02:00
parent f5c5320189
commit 2e30569675
2 changed files with 90 additions and 76 deletions
+82 -69
View File
@@ -397,12 +397,15 @@ function renderMemberLeavesView(personId, memberLeaves) {
const moisRenouv = ml.anniversary_date const moisRenouv = ml.anniversary_date
? parseInt(ml.anniversary_date.split("-")[1]) ? parseInt(ml.anniversary_date.split("-")[1])
: 1; : 1;
const methodeLabel = ml.method === "ACCUMULATED" ? "Graduel" : "Fixe";
html += ` html += `
<div style="display: flex; justify-content: space-between; align-items: center; background: var(--bg-subtle); padding: 8px; border-radius: 6px; border: 1px solid var(--border-light);"> <div style="display: flex; justify-content: space-between; align-items: center; background: var(--bg-subtle); padding: 8px; border-radius: 6px; border: 1px solid var(--border-light);">
<div> <div>
<strong style="color: var(--text-main);">${ml.leave_type}</strong> <strong style="color: var(--text-main);">${ml.leave_type}</strong>
<span style="font-size: 0.8rem; color: var(--text-muted); margin-left: 5px;">(Quota: <b>${ml.allowance}j</b> - Renouv: <b>Mois ${moisRenouv}</b>)</span> <span style="font-size: 0.8rem; color: var(--text-muted); margin-left: 5px;">
(Quota: <b>${ml.allowance}j</b> - Renouv: <b>Mois ${moisRenouv}</b> - Acquis: <b>${methodeLabel}</b>)
</span>
</div> </div>
<button class="pf-btn btn-secondary" style="padding: 2px 6px; color: var(--danger);" onclick="deleteMemberLeave(${ml.id}, ${personId})">🗑️</button> <button class="pf-btn btn-secondary" style="padding: 2px 6px; color: var(--danger);" onclick="deleteMemberLeave(${ml.id}, ${personId})">🗑️</button>
</div> </div>
@@ -411,7 +414,7 @@ function renderMemberLeavesView(personId, memberLeaves) {
html += `</div>`; html += `</div>`;
} }
// Ajout du selecteur de mois dans l'attribution // Ajout du formulaire d'attribution complet
html += ` html += `
<hr style="border: 0; border-top: 1px solid var(--border-light); margin: 15px 0;"> <hr style="border: 0; border-top: 1px solid var(--border-light); margin: 15px 0;">
<h5 style="margin: 0 0 10px 0;">+ Attribuer un congé</h5> <h5 style="margin: 0 0 10px 0;">+ Attribuer un congé</h5>
@@ -459,55 +462,45 @@ function renderMemberLeavesView(personId, memberLeaves) {
} }
async function addMemberLeave(personId) { async function addMemberLeave(personId) {
// 1. Récupération des valeurs du formulaire
const leaveCode = document.getElementById("new-member-leave-type").value; const leaveCode = document.getElementById("new-member-leave-type").value;
const allowance = document.getElementById("new-member-leave-allowance").value; const allowance = document.getElementById("new-member-leave-allowance").value;
const resetMonth = document.getElementById("new-member-leave-reset").value; const resetMonth = document.getElementById("new-member-leave-reset").value;
const method = document.getElementById("new-member-leave-method").value; // <-- Ajout const method = document.getElementById("new-member-leave-method").value;
fd.append("method", method);
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(); const fd = new FormData();
fd.append("action", "add_person_leave"); fd.append("action", "add_person_leave");
fd.append("person_id", personId); fd.append("person_id", personId);
fd.append("leave_type", leaveCode); fd.append("leave_type", leaveCode);
fd.append("allowance", allowance); fd.append("allowance", allowance);
fd.append("reset_month", resetMonth); fd.append("reset_month", resetMonth);
fd.append("method", method);
// 4. Appel à l'API et rafraîchissement
try { try {
const res = await pachaFetch( const res = await pachaFetch(
"/modules/family-calendar/includes/api/calendar-settings.php", "/modules/family-calendar/includes/api/calendar-settings.php",
{ method: "POST", body: fd }, { method: "POST", body: fd },
); );
if (!res.success) throw new Error(res.error); if (!res.success) throw new Error(res.error);
if (window.showToast) showToast("Congé attribué avec succès !", "success"); 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(); loadMemberConfigView();
} catch (err) { } catch (err) {
alert("Erreur: " + err.message); if (window.showToast)
} showToast("Erreur lors de l'attribution : " + err.message, "error");
} else 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);
} }
} }
@@ -837,62 +830,77 @@ document.addEventListener("DOMContentLoaded", () => {
calculateMonthlyBalances() { calculateMonthlyBalances() {
const balances = {}; const balances = {};
this.parents.forEach((p) => (balances[p.id] = {})); this.parents.forEach((p) => (balances[String(p.id)] = {}));
const ymSet = new Set(); const ymSet = new Set();
this.weeks.forEach((w) => ymSet.add(w.monthKey)); this.weeks.forEach((w) => ymSet.add(w.monthKey));
const ymList = Array.from(ymSet).sort(); const ymList = Array.from(ymSet).sort();
const usageByMonth = {}; const usageByMonth = {};
// 1. On calcule ce qui a été posé (Strictement typé en String pour éviter les bugs)
this.leaves.forEach((l) => { this.leaves.forEach((l) => {
const pid = l.person_id, const pid = String(l.person_id);
type = l.leave_type, const type = String(l.leave_type).trim().toUpperCase();
ym = l.leave_date.substring(0, 7); const ym = String(l.leave_date).substring(0, 7);
if (!usageByMonth[pid]) usageByMonth[pid] = {}; if (!usageByMonth[pid]) usageByMonth[pid] = {};
if (!usageByMonth[pid][type]) usageByMonth[pid][type] = {}; if (!usageByMonth[pid][type]) usageByMonth[pid][type] = {};
usageByMonth[pid][type][ym] = 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) => { this.parents.forEach((parent) => {
const pid = parent.id; const pid = String(parent.id);
const matrix = this.leaveMatrix[pid] || []; const matrix = this.leaveMatrix[pid] || [];
matrix.forEach((conf) => { matrix.forEach((conf) => {
const type = conf.type; const type = String(conf.type).trim().toUpperCase();
balances[pid][type] = {}; if (!balances[pid][type]) balances[pid][type] = {};
ymList.forEach((ym) => { ymList.forEach((ym) => {
const [currYear, currMonth] = ym.split("-").map(Number); const [currYear, currMonth] = ym.split("-").map(Number);
let cycleStartStr = "", let cycleStartStr = "";
initialBalance = parseFloat(conf.allowance || 0); let initialBalance = parseFloat(conf.allowance || 0);
let monthRenouvellement = 1;
if (conf.date) { if (conf.date) {
const parts = conf.date.split("-"); const parts = conf.date.split("-");
if (parts.length >= 2) { if (parts.length >= 2) monthRenouvellement = parseInt(parts[1]);
const monthRenouvellement = parseInt(parts[1]); }
const dayRenouvellement =
parts.length === 3 ? parseInt(parts[2]) : 1; const isPastAnniversary =
const isPastAnniversary = currMonth > monthRenouvellement ||
currMonth > monthRenouvellement || (currMonth === monthRenouvellement && 1 >= 1);
(currMonth === monthRenouvellement && 1 >= dayRenouvellement); const refYear = isPastAnniversary ? currYear : currYear - 1;
const refYear = isPastAnniversary ? currYear : currYear - 1; cycleStartStr = `${refYear}-${String(monthRenouvellement).padStart(2, "0")}`;
cycleStartStr = `${refYear}-${String(monthRenouvellement).padStart(2, "0")}`;
} // 🔥 LE FIX : GESTION DU MODE FIXE VS GRADUEL
} else { let acquiredBalance = initialBalance;
cycleStartStr = `${currYear}-01`; 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; let usedBeforeCurrentMonth = 0;
Object.keys(usageByMonth[pid]?.[type] || {}).forEach((usedYm) => { Object.keys(usageByMonth[pid]?.[type] || {}).forEach((usedYm) => {
if (usedYm >= cycleStartStr && usedYm < ym) if (usedYm >= cycleStartStr && usedYm < ym) {
usedBeforeCurrentMonth += usageByMonth[pid][type][usedYm]; usedBeforeCurrentMonth += usageByMonth[pid][type][usedYm];
}
}); });
balances[pid][type][ym] = { balances[pid][type][ym] = {
availableAtMonthStart: Math.max( availableAtMonthStart: Math.max(
0, 0,
initialBalance - usedBeforeCurrentMonth, acquiredBalance - usedBeforeCurrentMonth,
), ),
usedInMonth: usageByMonth[pid]?.[type]?.[ym] || 0, usedInMonth: usageByMonth[pid]?.[type]?.[ym] || 0,
}; };
@@ -1242,32 +1250,38 @@ document.addEventListener("DOMContentLoaded", () => {
renderMonthBalances() { renderMonthBalances() {
const container = document.getElementById("fc-month-balances"); const container = document.getElementById("fc-month-balances");
if (!container) return; if (!container) return;
const monthsToDisplay = [],
y = this.currentMonth.getFullYear(), const monthsToDisplay = [];
m = this.currentMonth.getMonth(); const y = this.currentMonth.getFullYear();
const m = this.currentMonth.getMonth();
let numMonths = let numMonths =
this.viewMode === "2months" ? 2 : this.viewMode === "3months" ? 3 : 1; 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")}`); monthsToDisplay.push(`${y}-${String(m + i + 1).padStart(2, "0")}`);
}
container.style.display = "flex"; container.style.display = "flex";
container.innerHTML = this.parents container.innerHTML = this.parents
.map((person) => { .map((person) => {
let cards = `<div class="fc-minimal-balance-card"><strong style="color:${person.color || "#333"}">${person.name.toUpperCase()}</strong><div class="fc-minimal-chips">`; let cards = `<div class="fc-minimal-balance-card"><strong style="color:${person.color || "#333"}">${person.name.toUpperCase()}</strong><div class="fc-minimal-chips">`;
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) => { types.forEach((conf) => {
const type = conf.type; const type = conf.type;
const startBal = const startBal =
this.monthlyLeaveBalances[person.id]?.[type]?.[monthsToDisplay[0]] this.monthlyLeaveBalances[String(person.id)]?.[type]?.[
?.availableAtMonthStart || 0; monthsToDisplay[0]
]?.availableAtMonthStart || 0;
let totalUsed = 0; let totalUsed = 0;
monthsToDisplay.forEach( monthsToDisplay.forEach((ym) => {
(ym) => totalUsed +=
(totalUsed += this.monthlyLeaveBalances[String(person.id)]?.[type]?.[ym]
this.monthlyLeaveBalances[person.id]?.[type]?.[ym] ?.usedInMonth || 0;
?.usedInMonth || 0), });
);
const endBal = Math.max(0, startBal - totalUsed); const endBal = Math.max(0, startBal - totalUsed);
const fmt = (n) => const fmt = (n) =>
n > 0 ? (Number.isInteger(n) ? n : n.toFixed(1)) : "0"; n > 0 ? (Number.isInteger(n) ? n : n.toFixed(1)) : "0";
@@ -1275,14 +1289,13 @@ document.addEventListener("DOMContentLoaded", () => {
let alertHtml = ""; let alertHtml = "";
const cMonth = parseInt(monthsToDisplay[0].split("-")[1]); 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) { if (endBal > 0 && conf.date) {
const resetMonth = parseInt(conf.date.split("-")[1]); 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; const alertMonth = resetMonth - 1 === 0 ? 12 : resetMonth - 1;
if (cMonth === alertMonth || cMonth === resetMonth) { if (cMonth === alertMonth || cMonth === resetMonth) {
alertHtml = `<div class="fc-burn-alert" title="Alerte : Perte imminente !">🔥</div>`; alertHtml = `<div class="fc-burn-alert" title="Alerte : ${fmt(endBal)} jour(s) perdu(s) à la fin du cycle !">🔥</div>`;
} }
} }
@@ -20,9 +20,8 @@ try {
$inserted = []; $inserted = [];
foreach ($eventsToSave as $event) { 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'])) { if (empty($event['date']) || empty($event['type'])) {
continue; continue; // Sécurité anti-crash
} }
$person_id = 0; $person_id = 0;
@@ -32,11 +31,13 @@ try {
$person_id = (int)$event['person']; $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; $duration = isset($event['duration']) ? (float)$event['duration'] : 1.0;
$stmt->execute([ $stmt->execute([
':event_date' => $event['date'], ':event_date' => $event['date'],
':event_type' => $event['type'], ':event_type' => $safeType,
':person_id' => $person_id, ':person_id' => $person_id,
':duration' => $duration, ':duration' => $duration,
]); ]);
@@ -44,7 +45,7 @@ try {
$inserted[] = [ $inserted[] = [
'id' => $pdo->lastInsertId(), 'id' => $pdo->lastInsertId(),
'date' => $event['date'], 'date' => $event['date'],
'type' => $event['type'], 'type' => $safeType,
'duration' => $duration, 'duration' => $duration,
'person_id' => $person_id, 'person_id' => $person_id,
]; ];
@@ -58,14 +59,14 @@ try {
'inserted' => $inserted, '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()) { if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollBack(); $pdo->rollBack();
} }
http_response_code(500); http_response_code(500);
echo json_encode([ echo json_encode([
'success' => false, 'success' => false,
'status' => 'error', 'status' => 'error',
'message' => 'Erreur Serveur/SQL : ' . $e->getMessage() 'message' => 'Erreur Serveur/SQL : ' . $e->getMessage()
]); ]);
} }