@@ -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 >= dayRenouvellement);
|
(currMonth === monthRenouvellement && 1 >= 1);
|
||||||
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")}`;
|
||||||
}
|
|
||||||
} else {
|
// 🔥 LE FIX : GESTION DU MODE FIXE VS GRADUEL
|
||||||
cycleStartStr = `${currYear}-01`;
|
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;
|
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,7 +59,7 @@ 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();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user