Auto-commit for deploy to production - 2026-05-04 18:30:44

This commit is contained in:
2026-05-04 18:30:44 +02:00
parent cc937c544c
commit 6ce49bffc0
+269 -129
View File
@@ -1,6 +1,6 @@
# 🦙 Source Code PachaFamily # 🦙 Source Code PachaFamily
> *Généré le 2026-05-04 17:27:25* > *Généré le 2026-05-04 18:29:00*
### 📄 Fichier : `budget.php` ### 📄 Fichier : `budget.php`
```php ```php
@@ -1208,6 +1208,35 @@ body.no-scroll {
margin-top: auto; margin-top: auto;
} }
/* Notifications Toasts */
.pf-toast {
position: fixed;
bottom: 20px;
right: 20px;
padding: 12px 24px;
background: white;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
border-left: 5px solid var(--primary);
z-index: 10000;
transform: translateX(120%);
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
font-weight: 600;
color: var(--text-main);
}
.pf-toast.is-visible {
transform: translateX(0);
}
.pf-toast--success {
border-left-color: #10b981;
}
.pf-toast--error {
border-left-color: #ef4444;
}
.pf-toast--warning {
border-left-color: #f59e0b;
}
/* === MOBILE RESPONSIVE === */ /* === MOBILE RESPONSIVE === */
@media (max-width: 768px) { @media (max-width: 768px) {
.pf-header { .pf-header {
@@ -1247,6 +1276,42 @@ body.no-scroll {
.pf-mobile-logout { .pf-mobile-logout {
color: var(--danger); color: var(--danger);
} }
/* ✅ Transformation de toutes les modales en "Bottom Sheets" */
.pf-modal {
align-items: flex-end; /* Aligne la modale en bas */
padding: 0;
}
.pf-modal-content {
width: 100% !important;
max-width: none !important;
border-radius: 24px 24px 0 0 !important; /* Arrondi seulement en haut */
padding: 24px 20px !important;
padding-bottom: env(
safe-area-inset-bottom,
30px
) !important; /* Gère l'encoche iPhone */
transform: translateY(100%);
animation: slideUpSheet 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
max-height: 92vh; /* Laisse un petit espace en haut */
}
/* Barre de drag visuelle en haut de la modale */
.pf-modal-content::before {
content: "";
display: block;
width: 40px;
height: 5px;
background: #cbd5e1;
border-radius: 10px;
margin: -10px auto 20px auto;
}
@keyframes slideUpSheet {
to {
transform: translateY(0);
}
}
} }
``` ```
@@ -1349,16 +1414,24 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
<script> <script>
/** /**
* Pont d'Internationalisation pour le Javascript * Pont d'Internationalisation et Configuration
*/ */
window.I18N = <?php echo json_encode($current_translations_array ?? []); ?>; window.I18N = <?php echo json_encode($current_translations_array ?? []); ?>;
// ✅ Injection sécurisée (évite le crash si config.php n'est pas chargé)
window.CONFIG = {
ID_ALEX: <?php echo defined('ID_ALEX') ? ID_ALEX : 2; ?>,
ID_LAIA: <?php echo defined('ID_LAIA') ? ID_LAIA : 3; ?>,
CURRENCY: '<?php echo defined('CURRENCY') ? CURRENCY : "€"; ?>'
};
function tr(key) { function tr(key) {
return window.I18N[key] || key; return window.I18N[key] || key;
} }
// Gestion du menu mobile (Uniquement si le bouton Burger est présent dans le DOM) // Gestion du menu mobile
<?php if (isset($_SESSION['user'])): ?> <?php if (isset($_SESSION['user'])): ?>
document.addEventListener('DOMContentLoaded', () => {
const burgerBtn = document.querySelector('.pf-burger-btn'); const burgerBtn = document.querySelector('.pf-burger-btn');
const mobileMenu = document.querySelector('.pf-mobile-menu'); const mobileMenu = document.querySelector('.pf-mobile-menu');
@@ -1366,15 +1439,81 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
burgerBtn.addEventListener('click', () => { burgerBtn.addEventListener('click', () => {
const isOpen = mobileMenu.classList.toggle('is-open'); const isOpen = mobileMenu.classList.toggle('is-open');
burgerBtn.textContent = isOpen ? '✕' : '☰'; burgerBtn.textContent = isOpen ? '✕' : '☰';
document.body.classList.toggle('no-scroll', isOpen);
if (isOpen) {
document.body.classList.add('no-scroll');
} else {
document.body.classList.remove('no-scroll');
}
}); });
} }
});
<?php endif; ?> <?php endif; ?>
/**
* pachaFetch : Utilitaire de requête robuste
*/
async function pachaFetch(url, options = {}) {
const finalUrl = url.startsWith('/') ? url.substring(1) : url;
options.credentials = 'same-origin';
options.headers = {
...options.headers,
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
};
try {
const response = await fetch(finalUrl, options);
const rawText = await response.text();
try {
return JSON.parse(rawText);
} catch (jsonErr) {
console.error("Réponse corrompue (HTML reçu au lieu de JSON) :", rawText);
throw new Error("Erreur serveur : format JSON invalide.");
}
} catch (err) {
console.error(`Erreur pachaFetch [${finalUrl}] :`, err);
throw err;
}
}
/**
* UI Utility : Toasts (Notifications)
*/
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `pf-toast pf-toast--${type}`;
toast.innerHTML = message;
document.body.appendChild(toast);
// Animation et suppression
setTimeout(() => toast.classList.add('is-visible'), 100);
setTimeout(() => {
toast.classList.remove('is-visible');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
/**
* UI Utility : Confirmation stylisée (Remplace confirm())
*/
async function pachaConfirm(title, message) {
return new Promise((resolve) => {
const modal = document.createElement('div');
modal.className = 'pf-modal open';
modal.innerHTML = `
<div class="pf-modal-content" style="max-width: 400px; align-self: center;">
<h3 style="margin-top:0;">${title}</h3>
<p style="color:var(--text-muted);">${message}</p>
<div class="modal-footer">
<button class="pf-btn btn-secondary" id="confirm-cancel">${tr('btn_cancel')}</button>
<button class="pf-btn" id="confirm-ok" style="background:var(--danger);">${tr('btn_delete')}</button>
</div>
</div>
`;
document.body.appendChild(modal);
document.getElementById('confirm-cancel').onclick = () => { modal.remove(); resolve(false); };
document.getElementById('confirm-ok').onclick = () => { modal.remove(); resolve(true); };
});
}
</script> </script>
``` ```
@@ -1444,10 +1583,14 @@ function require_login(?string $loginPage = '/login.php'): void
} }
if (empty($_SESSION['user'])) { if (empty($_SESSION['user'])) {
// URL de la page courante (ex: /gift-list.php?foo=bar) if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) || (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false)) {
$currentUrl = $_SERVER['REQUEST_URI'] ?? '/'; header('Content-Type: application/json');
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Session expirée. Veuillez vous reconnecter.']);
exit;
}
// On redirige vers la page de login en ajoutant ?redirect=... $currentUrl = $_SERVER['REQUEST_URI'] ?? '/';
$redirectParam = urlencode($currentUrl); $redirectParam = urlencode($currentUrl);
$target = ($loginPage ?? '/login.php') . '?redirect=' . $redirectParam; $target = ($loginPage ?? '/login.php') . '?redirect=' . $redirectParam;
@@ -1460,10 +1603,26 @@ function require_login(?string $loginPage = '/login.php'): void
--- ---
### 📄 Fichier : `includes/config.php`
```php
<?php
// PachaFamily Config - Version sans erreur
if (!defined('ID_ALEX')) {
define('ID_ALEX', 2);
define('ID_LAIA', 3);
define('CURRENCY', '€');
define('ZONE_SCOLAIRE', 'C');
}
```
---
### 📄 Fichier : `includes/db.php` ### 📄 Fichier : `includes/db.php`
```php ```php
<?php <?php
if (file_exists(__DIR__ . '/config.php')) {
require_once __DIR__ . '/config.php';
}
// Activer erreurs // Activer erreurs
ini_set('display_errors', 1); ini_set('display_errors', 1);
ini_set('display_startup_errors', 1); ini_set('display_startup_errors', 1);
@@ -2623,10 +2782,12 @@ return [
```php ```php
<?php <?php
// Protection de la page : nécessite d'être connecté // Protection de la page : nécessite d'être connecté
require __DIR__ . '/includes/auth.php'; require __DIR__ . '/includes/auth.php';
require_login('/login.php'); require_login('/login.php');
// Inclusion i18n (déjà fait dans header, mais par sécurité si besoin de tr() avant) // Inclusion i18n (déjà fait dans header, mais par sécurité si besoin de tr() avant)
require_once __DIR__ . '/includes/i18n.php'; require_once __DIR__ . '/includes/i18n.php';
// Configuration de la page // Configuration de la page
@@ -4873,10 +5034,14 @@ function getTranslatedMonthName($dateString) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach (['Alex', 'Laia'] as $p): <?php
// Définition des noms à partir de la config
$family_members = [ID_ALEX => 'Alex', ID_LAIA => 'Laia'];
foreach ($family_members as $id => $p):
$d = $salaryConfig[$p]; $d = $salaryConfig[$p];
$restant = $d['salary'] - ($d['mensualite'] + $d['frais_func'] + $d['eco_perso'] + $d['eco_family']); $restant = $d['salary'] - ($d['mensualite'] + $d['frais_func'] + $d['eco_perso'] + $d['eco_family']);
$borderColor = ($p === 'Alex') ? '#0891b2' : '#f59e0b'; $borderColor = ($id === ID_ALEX) ? '#0891b2' : '#f59e0b';
?> ?>
<tr data-person="<?= $p ?>"> <tr data-person="<?= $p ?>">
<td style="text-align:left; font-weight:bold; color:var(--text-main); border-left:4px solid <?= $borderColor ?>;"> <td style="text-align:left; font-weight:bold; color:var(--text-main); border-left:4px solid <?= $borderColor ?>;">
@@ -5163,7 +5328,7 @@ function getTranslatedMonthName($dateString) {
</div> </div>
</div> </div>
<div id="addCatModal" class="pf-modal" style="display:none; position:fixed; inset:0; z-index:9999; background:rgba(15, 23, 42, 0.6); backdrop-filter:blur(4px); align-items:center; justify-content:center;"> <div id="addCatModal" class="pf-modal">
<div class="pf-modal-content" style="background:white; width:95%; max-width:400px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px;"> <div class="pf-modal-content" style="background:white; width:95%; max-width:400px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;"> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 class="pf-modal-title" style="margin:0; border:none; font-size:1.2rem;"><?= tr('bud_prev_new_line_title') ?></h3> <h3 class="pf-modal-title" style="margin:0; border:none; font-size:1.2rem;"><?= tr('bud_prev_new_line_title') ?></h3>
@@ -5553,33 +5718,23 @@ function updateSumResult() {
} }
async function deleteCategory(id) { async function deleteCategory(id) {
if (!confirm(window.I18N['bud_prev_confirm_del_line'] || "Confirmer la suppression ?")) return; if (!confirm(tr('bud_prev_confirm_del_line'))) return;
const formData = new FormData(); const formData = new FormData();
formData.append('action', 'delete_category'); formData.append('action', 'delete_category');
formData.append('id', id); formData.append('id', id);
formData.append('ajax', '1'); formData.append('ajax', '1'); //
try { try {
// 💡 CORRECTION : Chemin relatif (sans le "/" au début) pour s'adapter à ton localhost const result = await pachaFetch('modules/budget/includes/api/save-budget.php', {
const response = await fetch('modules/budget/includes/api/save-budget.php', {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
if (!response.ok) throw new Error(`Erreur réseau HTTP ${response.status}`);
const result = await response.json();
if (result.success) { if (result.success) {
window.location.reload(); window.location.reload();
} else {
alert((window.I18N['bud_err_tech'] || 'Erreur technique') + " : Suppression impossible.");
}
} catch(e) {
console.error("Erreur Fetch Suppression:", e);
alert("Erreur de communication avec le serveur. Regarde la console (F12) pour plus de détails.");
} }
} catch(e) { console.error(e); }
} }
document.addEventListener('click', function(e) { document.addEventListener('click', function(e) {
@@ -5607,7 +5762,7 @@ document.addEventListener('DOMContentLoaded', recalcAllAllocations);
// --- INTERCEPTION ASYNCHRONE DES FORMULAIRES DE MODALES --- // --- INTERCEPTION ASYNCHRONE DES FORMULAIRES DE MODALES ---
document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form => { document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form => {
form.addEventListener('submit', async (e) => { form.addEventListener('submit', async (e) => {
e.preventDefault(); // 🛑 Bloque le rechargement HTML par défaut e.preventDefault();
const submitBtn = form.querySelector('button[type="submit"]'); const submitBtn = form.querySelector('button[type="submit"]');
const originalText = submitBtn.innerText; const originalText = submitBtn.innerText;
@@ -5619,31 +5774,24 @@ document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form
const formData = new FormData(form); const formData = new FormData(form);
formData.append('ajax', '1'); formData.append('ajax', '1');
// 💡 CORRECTION DU PIÈGE : On utilise getAttribute() pour forcer la lecture de l'URL !
const actionUrl = form.getAttribute('action'); const actionUrl = form.getAttribute('action');
// On s'assure du bon chemin (chemin relatif depuis budget.php) // On utilise pachaFetch au lieu de fetch
const finalUrl = actionUrl.startsWith('/') ? actionUrl.substring(1) : actionUrl; const result = await pachaFetch(actionUrl, {
const response = await fetch(finalUrl, {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
if (!response.ok) throw new Error(`Erreur HTTP: ${response.status}`);
const result = await response.json();
if (result.success) { if (result.success) {
form.closest('.pf-modal').style.display = 'none'; form.closest('.pf-modal').style.display = 'none';
document.body.classList.remove('no-scroll'); document.body.classList.remove('no-scroll');
window.location.reload(); window.location.reload();
} else { } else {
alert((window.I18N['bud_err_tech'] || 'Erreur technique') + " : " + (result.error || "Inconnue")); alert((window.I18N['bud_err_tech'] || 'Erreur') + " : " + (result.error || "Inconnue"));
} }
} catch (error) { } catch (error) {
console.error("Erreur Fetch Modale:", error); console.error("Erreur Fetch Modale:", error);
alert("Une erreur technique est survenue lors de l'enregistrement."); alert("Une erreur technique est survenue.");
} finally { } finally {
submitBtn.disabled = false; submitBtn.disabled = false;
submitBtn.innerText = originalText; submitBtn.innerText = originalText;
@@ -6235,18 +6383,30 @@ $currentMonth = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEF
$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y', strtotime($defaultActiveMonth)); $currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y', strtotime($defaultActiveMonth));
$viewMonthDate = "$currentYear-$currentMonth-01"; $viewMonthDate = "$currentYear-$currentMonth-01";
// Conservation du tableau d'origine pour la correspondance en base de données $sqlReal = "SELECT budget_item_id, SUM(amount) as total_real
$moisFr = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']; FROM pf_expenses
WHERE gestion_month = ? AND budget_item_id IS NOT NULL
GROUP BY budget_item_id";
$stmtReal = $pdo->prepare($sqlReal);
$stmtReal->execute([$viewMonthDate]);
$realTotals = $stmtReal->fetchAll(PDO::FETCH_KEY_PAIR); // Retourne un tableau [id => total]
// Affichage dynamique traduit $sqlCatReal = "SELECT category, SUM(amount) as total_real
FROM pf_expenses
WHERE gestion_month = ? AND budget_item_id IS NULL
GROUP BY category";
$stmtCatReal = $pdo->prepare($sqlCatReal);
$stmtCatReal->execute([$viewMonthDate]);
$catTotals = $stmtCatReal->fetchAll(PDO::FETCH_KEY_PAIR);
$stmtLabels = $pdo->prepare("SELECT label, amount FROM pf_expenses WHERE gestion_month = ? AND budget_item_id IS NULL");
$stmtLabels->execute([$viewMonthDate]);
$unlinkedExpenses = $stmtLabels->fetchAll(PDO::FETCH_ASSOC);
$moisFr = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
$monthTranslationKey = 'month_' . str_pad((int)$currentMonth, 2, '0', STR_PAD_LEFT); $monthTranslationKey = 'month_' . str_pad((int)$currentMonth, 2, '0', STR_PAD_LEFT);
$currentMonthName = tr($monthTranslationKey) . ' ' . $currentYear; $currentMonthName = tr($monthTranslationKey) . ' ' . $currentYear;
// 4. Récupération des Dépenses Réelles du mois (Basé sur le Mois de Gestion !)
$stmtExp = $pdo->prepare("SELECT amount, label, category, budget_item_id FROM pf_expenses WHERE gestion_month = ?");
$stmtExp->execute([$viewMonthDate]);
$allExpenses = $stmtExp->fetchAll(PDO::FETCH_ASSOC);
$totalDepenses = 0; $totalDepenses = 0;
$totalRevenus = 0; $totalRevenus = 0;
?> ?>
@@ -6272,59 +6432,50 @@ $totalRevenus = 0;
<tbody> <tbody>
<?php foreach ($items as $item): <?php foreach ($items as $item):
// --- 1. CALCUL DES TOTAUX PRÉVUS --- // --- 1. CALCUL DES TOTAUX PRÉVUS ---
$targetAbs = abs((float)$item['amount']); // On utilise l'absolu pour l'affichage $targetAbs = abs((float)$item['amount']);
$amountToAdd = ($item['type'] === 'Annuel') ? $targetAbs / 12 : $targetAbs; $amountToAdd = ($item['type'] === 'Annuel') ? $targetAbs / 12 : $targetAbs;
if ($item['category'] === 'expense') $totalDepenses += $amountToAdd; if ($item['category'] === 'income') $totalRevenus += $amountToAdd;
else $totalRevenus += $amountToAdd; else $totalDepenses += $amountToAdd;
// --- 2. CALCUL DU RÉEL (SOMME DES LIGNES ASSOCIÉES) --- // --- 2. CALCUL DU RÉEL (Logique optimisée) ---
$realSum = 0; $realSum = 0;
$hasMatchingExpense = false; $hasMatchingExpense = false;
foreach ($allExpenses as $exp) { // A. Correspondance directe par ID
$match = false; if (isset($realTotals[$item['id']])) {
$realSum = $realTotals[$item['id']];
$hasMatchingExpense = true;
}
// B. Correspondance par catégorie système (École, Essence, FMCG)
else {
$catKey = null;
if (trim($item['name']) === 'Estimacio escola') $catKey = 'School';
elseif (trim($item['name']) === 'Estimation gasolina') $catKey = 'Essence';
elseif (trim($item['name']) === 'Estimacio F&B & beauty') $catKey = 'FMCG';
// 1. Lien direct via l'ID de la charge if ($catKey && isset($catTotals[$catKey])) {
if (!empty($exp['budget_item_id']) && (int)$exp['budget_item_id'] === (int)$item['id']) { $realSum = $catTotals[$catKey];
$match = true; $hasMatchingExpense = true;
} }
// 2. Lien forcé pour l'École // C. Correspondance par mots-clés (seulement sur les dépenses non liées)
elseif ($exp['category'] === 'School' && trim($item['name']) === 'Estimacio escola') { elseif (!empty($item['mapping_keywords'])) {
$match = true;
}
elseif ($exp['category'] === 'Essence' && trim($item['name']) === 'Estimation gasolina') {
$match = true;
}
elseif ($exp['category'] === 'FMCG' && trim($item['name']) === 'Estimacio F&B & beauty') {
$match = true;
}
elseif (empty($exp['budget_item_id']) && !empty($item['mapping_keywords'])) {
$keywords = array_map('trim', explode(',', $item['mapping_keywords'])); $keywords = array_map('trim', explode(',', $item['mapping_keywords']));
foreach ($unlinkedExpenses as $uexp) {
foreach ($keywords as $kw) { foreach ($keywords as $kw) {
if (!empty($kw) && stripos($exp['label'], $kw) !== false) { if (!empty($kw) && stripos($uexp['label'], $kw) !== false) {
$match = true; $realSum += (float)$uexp['amount'];
$hasMatchingExpense = true;
break; break;
} }
} }
} }
if ($match) {
$realSum += (float)$exp['amount'];
$hasMatchingExpense = true;
} }
} }
// On convertit le résultat réel en positif pour simplifier la comparaison visuelle UI
$realAbs = abs($realSum); $realAbs = abs($realSum);
$isAutoChecked = ($hasMatchingExpense && ($realAbs >= ($targetAbs - 0.10)));
// --- 3. LOGIQUE D'ÉTAT ---
$isAutoChecked = false;
if ($hasMatchingExpense && ($realAbs >= ($targetAbs - 0.10))) {
$isAutoChecked = true;
}
// Styles
$rowClass = ($item['category'] === 'income') ? 'row-income' : 'row-expense'; $rowClass = ($item['category'] === 'income') ? 'row-income' : 'row-expense';
if ($item['is_estimate']) $rowClass .= ' row-estimate'; if ($item['is_estimate']) $rowClass .= ' row-estimate';
?> ?>
@@ -7474,29 +7625,19 @@ window.addEventListener('click', (e) => {
// --- 2. SUPPRESSION ASYNCHRONE --- // --- 2. SUPPRESSION ASYNCHRONE ---
async function deleteExpense(id) { async function deleteExpense(id) {
if (!confirm(window.I18N['bud_confirm_delete'] || "Confirmer ?")) return; const confirmed = await pachaConfirm("Suppression", tr('bud_confirm_delete'));
if (!confirmed) return;
const formData = new FormData(); const formData = new FormData();
formData.append("action", "delete_expense"); formData.append("action", "delete_expense");
formData.append("id", id); formData.append("id", id);
try { try {
// Envoi au handler PHP que nous venons de créer en haut de suivi.php await pachaFetch("budget.php?tab=suivi", { method: "POST", body: formData });
const response = await fetch("budget.php?tab=suivi", { method: "POST", body: formData }); showToast("Dépense supprimée !"); //
if (!response.ok) throw new Error(`HTTP ${response.status}`); setTimeout(() => window.location.reload(), 800);
const textResult = await response.text();
try {
const result = JSON.parse(textResult);
if (result.success) window.location.reload();
else alert((window.I18N['error_occured'] || 'Erreur') + " : " + (result.error || ""));
} catch (e) {
console.error("Réponse non-JSON :", textResult);
window.location.reload(); // Sécurité : on recharge quand même si le serveur dérive
}
} catch (err) { } catch (err) {
console.error(err); showToast("Erreur lors de la suppression", "error");
alert(window.I18N['bud_err_tech'] || 'Erreur technique');
} }
} }
@@ -7514,31 +7655,22 @@ document.addEventListener('DOMContentLoaded', () => {
submitBtn.innerText = '⏳ ...'; submitBtn.innerText = '⏳ ...';
const formData = new FormData(formExpense); const formData = new FormData(formExpense);
// Force l'action ici pour être sûr
formData.set('action', 'save_expense_manual'); formData.set('action', 'save_expense_manual');
// 💡 Chemin relatif propre ! const actionUrl = formExpense.getAttribute('action') || 'modules/budget/includes/api/manage-item.php';
const response = await fetch('modules/budget/includes/api/manage-item.php', {
const result = await pachaFetch(actionUrl, {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
if (!response.ok) throw new Error(`Erreur HTTP: ${response.status}`);
// 🛡️ Double vérification JSON contre les warnings PHP
const textResult = await response.text();
try {
const result = JSON.parse(textResult);
if (result.success) { if (result.success) {
closeSuiviModal('manualExpenseModal'); closeSuiviModal('manualExpenseModal');
formExpense.reset(); formExpense.reset();
window.location.reload(); window.location.reload();
} else { } else {
const errorMsg = result.error || 'Erreur inconnue'; alert((window.I18N['error_occured'] || 'Erreur') + ' : ' + (result.error || 'Inconnue'));
alert((window.I18N['error_occured'] || 'Erreur') + ' : ' + errorMsg);
}
} catch (jsonErr) {
console.error("Réponse non-JSON :", textResult);
alert("Erreur PHP. Vérifie la console (F12).");
} }
} catch (error) { } catch (error) {
console.error("Erreur réseau :", error); console.error("Erreur réseau :", error);
@@ -9425,9 +9557,9 @@ document.addEventListener("DOMContentLoaded", () => {
const dayLeaves = this.leaves.filter((l) => l.leave_date === iso); const dayLeaves = this.leaves.filter((l) => l.leave_date === iso);
if (dayLeaves.length) { if (dayLeaves.length) {
let html = `<div style="position:absolute; bottom:0; left:0; width:100%; font-size:9px; line-height:1; display:flex; justify-content:center; gap:2px; pointer-events:none;">`; let html = `<div style="position:absolute; bottom:0; left:0; width:100%; font-size:9px; line-height:1; display:flex; justify-content:center; gap:2px; pointer-events:none;">`;
if (dayLeaves.some((l) => l.person_id === 2)) if (dayLeaves.some((l) => l.person_id === window.CONFIG.ID_ALEX))
html += `<span style="color:#0f766e; font-weight:800;">A</span>`; html += `<span style="color:#0f766e; font-weight:800;">A</span>`;
if (dayLeaves.some((l) => l.person_id === 3)) if (dayLeaves.some((l) => l.person_id === window.CONFIG.ID_LAIA))
html += `<span style="color:#b45309; font-weight:800;">L</span>`; html += `<span style="color:#b45309; font-weight:800;">L</span>`;
html += `</div>`; html += `</div>`;
td.innerHTML += html; td.innerHTML += html;
@@ -9787,17 +9919,17 @@ document.addEventListener("DOMContentLoaded", () => {
} }
async fetchApi(url) { async fetchApi(url) {
return fetch(url).then((r) => r.json()); return pachaFetch(url);
} }
async postApi(url, data) { async postApi(url, data) {
const res = await fetch(url, { return pachaFetch(url, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
if (!res.ok) throw new Error(await res.text());
return res.json();
} }
async changeSchoolYear(delta) { async changeSchoolYear(delta) {
@@ -10214,7 +10346,6 @@ document.addEventListener("DOMContentLoaded", () => {
payload, payload,
); );
} else if (action === "clear-leaves-person") { } else if (action === "clear-leaves-person") {
// --- NOUVELLE ACTION : Suppression ciblée (Alex OU Laia selon le pid) ---
await this.postApi( await this.postApi(
"/modules/family-calendar/includes/api/manage-leaf.php", "/modules/family-calendar/includes/api/manage-leaf.php",
{ {
@@ -10224,14 +10355,21 @@ document.addEventListener("DOMContentLoaded", () => {
}, },
); );
} else if (action === "clear-leaves") { } else if (action === "clear-leaves") {
// --- ACTION GLOBALE : Gardée au cas où vous en auriez besoin ailleurs ---
await this.postApi( await this.postApi(
"/modules/family-calendar/includes/api/manage-leaf.php", "/modules/family-calendar/includes/api/manage-leaf.php",
{ action: "bulk_delete_day_person", dates, person_id: 2 }, {
action: "bulk_delete_day_person",
dates,
person_id: window.CONFIG.ID_ALEX,
},
); );
await this.postApi( await this.postApi(
"/modules/family-calendar/includes/api/manage-leaf.php", "/modules/family-calendar/includes/api/manage-leaf.php",
{ action: "bulk_delete_day_person", dates, person_id: 3 }, {
action: "bulk_delete_day_person",
dates,
person_id: window.CONFIG.ID_LAIA,
},
); );
} }
@@ -15114,12 +15252,14 @@ if ($selectedYear !== 'all') {
// 2. Récupération des voyages + Calculs // 2. Récupération des voyages + Calculs
$sql = " $sql = "
SELECT h.*, SELECT h.*,
(COALESCE(h.budget_food, 0) + COALESCE(h.budget_extra, 0) + COALESCE((SELECT SUM(amount) FROM pf_holidays_items WHERE holiday_id = h.id), 0)) as total_cost, (COALESCE(h.budget_food, 0) + COALESCE(h.budget_extra, 0) + COALESCE(SUM(hi.amount), 0)) as total_cost,
((SELECT COALESCE(SUM(ABS(amount)), 0) FROM pf_expenses WHERE holiday_id = h.id) + (SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND is_paid = 1)) as total_paid, (SELECT COALESCE(SUM(ABS(amount)), 0) FROM pf_expenses WHERE holiday_id = h.id) as real_expenses_sum,
(SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved SUM(CASE WHEN hi.is_paid = 1 THEN hi.amount ELSE 0 END) as items_paid_sum
FROM pf_holidays h FROM pf_holidays h
LEFT JOIN pf_holidays_items hi ON h.id = hi.holiday_id
$whereSQL $whereSQL
ORDER BY COALESCE(start_date, '2999-12-31') ASC, FIELD(status, 'booked', 'planned', 'draft', 'passed', 'archived') GROUP BY h.id
ORDER BY COALESCE(start_date, '2999-12-31') ASC
"; ";
$stmt = $pdo->prepare($sql); $stmt = $pdo->prepare($sql);
$stmt->execute($params); $stmt->execute($params);