Auto-commit for deploy to production - 2026-05-04 18:30:44
This commit is contained in:
+286
-146
@@ -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,33 +1414,107 @@ $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'])): ?>
|
||||||
const burgerBtn = document.querySelector('.pf-burger-btn');
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const mobileMenu = document.querySelector('.pf-mobile-menu');
|
const burgerBtn = document.querySelector('.pf-burger-btn');
|
||||||
|
const mobileMenu = document.querySelector('.pf-mobile-menu');
|
||||||
if(burgerBtn && mobileMenu) {
|
|
||||||
burgerBtn.addEventListener('click', () => {
|
|
||||||
const isOpen = mobileMenu.classList.toggle('is-open');
|
|
||||||
burgerBtn.textContent = isOpen ? '✕' : '☰';
|
|
||||||
|
|
||||||
if (isOpen) {
|
if(burgerBtn && mobileMenu) {
|
||||||
document.body.classList.add('no-scroll');
|
burgerBtn.addEventListener('click', () => {
|
||||||
} else {
|
const isOpen = mobileMenu.classList.toggle('is-open');
|
||||||
document.body.classList.remove('no-scroll');
|
burgerBtn.textContent = isOpen ? '✕' : '☰';
|
||||||
|
document.body.classList.toggle('no-scroll', isOpen);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</script>
|
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -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) {
|
} catch(e) { console.error(e); }
|
||||||
console.error("Erreur Fetch Suppression:", e);
|
|
||||||
alert("Erreur de communication avec le serveur. Regarde la console (F12) pour plus de détails.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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']];
|
||||||
// 1. Lien direct via l'ID de la charge
|
$hasMatchingExpense = true;
|
||||||
if (!empty($exp['budget_item_id']) && (int)$exp['budget_item_id'] === (int)$item['id']) {
|
}
|
||||||
$match = true;
|
// B. Correspondance par catégorie système (École, Essence, FMCG)
|
||||||
}
|
else {
|
||||||
// 2. Lien forcé pour l'École
|
$catKey = null;
|
||||||
elseif ($exp['category'] === 'School' && trim($item['name']) === 'Estimacio escola') {
|
if (trim($item['name']) === 'Estimacio escola') $catKey = 'School';
|
||||||
$match = true;
|
elseif (trim($item['name']) === 'Estimation gasolina') $catKey = 'Essence';
|
||||||
|
elseif (trim($item['name']) === 'Estimacio F&B & beauty') $catKey = 'FMCG';
|
||||||
|
|
||||||
|
if ($catKey && isset($catTotals[$catKey])) {
|
||||||
|
$realSum = $catTotals[$catKey];
|
||||||
|
$hasMatchingExpense = true;
|
||||||
}
|
}
|
||||||
elseif ($exp['category'] === 'Essence' && trim($item['name']) === 'Estimation gasolina') {
|
// C. Correspondance par mots-clés (seulement sur les dépenses non liées)
|
||||||
$match = true;
|
elseif (!empty($item['mapping_keywords'])) {
|
||||||
}
|
|
||||||
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 ($keywords as $kw) {
|
foreach ($unlinkedExpenses as $uexp) {
|
||||||
if (!empty($kw) && stripos($exp['label'], $kw) !== false) {
|
foreach ($keywords as $kw) {
|
||||||
$match = true;
|
if (!empty($kw) && stripos($uexp['label'], $kw) !== false) {
|
||||||
break;
|
$realSum += (float)$uexp['amount'];
|
||||||
|
$hasMatchingExpense = true;
|
||||||
|
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}`);
|
if (result.success) {
|
||||||
|
closeSuiviModal('manualExpenseModal');
|
||||||
// 🛡️ Double vérification JSON contre les warnings PHP
|
formExpense.reset();
|
||||||
const textResult = await response.text();
|
window.location.reload();
|
||||||
try {
|
} else {
|
||||||
const result = JSON.parse(textResult);
|
alert((window.I18N['error_occured'] || 'Erreur') + ' : ' + (result.error || 'Inconnue'));
|
||||||
if (result.success) {
|
|
||||||
closeSuiviModal('manualExpenseModal');
|
|
||||||
formExpense.reset();
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
const errorMsg = result.error || 'Erreur 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);
|
||||||
|
|||||||
Reference in New Issue
Block a user