diff --git a/modules/budget/budget.css b/modules/budget/budget.css index 1761df2..473a681 100644 --- a/modules/budget/budget.css +++ b/modules/budget/budget.css @@ -1016,3 +1016,98 @@ th.col-laia { align-items: center; } } + +/* ============================================================================ + 13. STYLES SPÉCIFIQUES AU SUIVI MENSUEL (suivi.php) + ============================================================================ */ + +.cat-card { + background: var(--bg-card); + border-radius: var(--radius-l); + border: 1px solid #e2e8f0; + display: flex; + flex-direction: column; + overflow: hidden; + position: relative; +} + +.btn-add-item { + background: rgba(255, 255, 255, 0.6); + color: inherit; + border: 1px solid rgba(0, 0, 0, 0.1); + width: 28px; + height: 28px; + border-radius: 50%; + font-size: 18px; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +.btn-add-item:hover { + background: var(--bg-card); + transform: rotate(90deg) scale(1.1); + box-shadow: var(--shadow-sm); + border-color: currentColor; +} + +.progress-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + margin-top: 20px; +} + +/* Animation "Charges à venir" */ +.fade-pulse { + animation: pulseText 2s infinite; +} +@keyframes pulseText { + 0% { + opacity: 0.8; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.8; + } +} + +/* Navigation par mois (Si elle n'est pas déjà gérée par .nav-group au-dessus) */ +.suivi-nav-group { + display: flex; + background: #e2e8f0; + border-radius: var(--radius-s); + overflow: hidden; +} +.suivi-btn-nav { + padding: 6px 12px; + color: var(--text-muted); + text-decoration: none; + font-size: 0.85rem; + font-weight: bold; + border-right: 1px solid #cbd5e1; + transition: + background 0.2s, + color 0.2s; +} +.suivi-btn-nav:last-child { + border-right: none; +} +.suivi-btn-nav:hover { + background: var(--bg-card); + color: var(--primary); +} + +/* Sélecteur dynamique de mois (dans l'import CSV) */ +input[type="month"].pf-input { + font-family: inherit; + color: var(--text-main); + background: white; + border: 1px solid #cbd5e1; + border-radius: var(--radius-s); +} diff --git a/modules/budget/includes/api/save-budget.php b/modules/budget/includes/api/save-budget.php index 05b2a4f..8975785 100644 --- a/modules/budget/includes/api/save-budget.php +++ b/modules/budget/includes/api/save-budget.php @@ -8,6 +8,38 @@ require_login(); $action = $_POST['action'] ?? ''; +// ================================================================= +// 7. SAUVEGARDE D'UNE NOTE GÉNÉRIQUE (pf_notes) +// ================================================================= +if ($action === 'save_note') { + // On affiche les erreurs s'il y a un souci SQL pour pouvoir déboguer + ini_set('display_errors', 1); + error_reporting(E_ALL); + header('Content-Type: application/json'); + + try { + $noteType = $_POST['note_type'] ?? ''; + $refId = $_POST['reference_id'] ?? ''; + $content = $_POST['content'] ?? ''; + + if (empty($noteType) || empty($refId)) { + throw new Exception("Le type et la référence de la note sont requis."); + } + + // Insère ou met à jour la note si elle existe déjà + $stmt = $pdo->prepare("INSERT INTO pf_notes (note_type, reference_id, content) + VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE content = VALUES(content)"); + $stmt->execute([$noteType, $refId, $content]); + + echo json_encode(['success' => true]); + } catch (\Throwable $e) { + echo json_encode(['success' => false, 'error' => 'Erreur SQL: ' . $e->getMessage()]); + } + exit; +} +// ================================================================= + // 1. MISE A JOUR TABLEAU SALAIRES (AJAX) if ($action === 'update_salary_config') { header('Content-Type: application/json'); // On précise JSON ici @@ -207,4 +239,6 @@ if ($action === 'validate_transfers') { echo json_encode(['success' => false, 'error' => $e->getMessage()]); } exit; + + } \ No newline at end of file diff --git a/modules/budget/views/budget_prev.php b/modules/budget/views/budget_prev.php index 2e551f5..3ff2a5e 100644 --- a/modules/budget/views/budget_prev.php +++ b/modules/budget/views/budget_prev.php @@ -69,6 +69,10 @@ if ($sysCatId && isset($allocs[$focusDate][$sysCatId])) { $isValidatedLaia = ($row['amount_laia'] == 1); } +$stmtNote = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'budget_prev' AND reference_id = ?"); +$stmtNote->execute([$focusDate]); +$currentNote = $stmtNote->fetchColumn(); + // 7. Récupération des vacances actives pour les lier au budget $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN ('draft', 'planned', 'booked') ORDER BY start_date ASC")->fetchAll(PDO::FETCH_ASSOC); ?> @@ -249,13 +253,42 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN + +
+
+

+ 📝 Notes pour +

+ + ✓ Enregistré + +
+ + + +
+ +
+
+ + + alert("Erreur technique")); } + +// --- SAUVEGARDE GÉNÉRIQUE DES NOTES --- +function saveGenericNote(noteType, refId, content) { + const formData = new FormData(); + formData.append('action', 'save_note'); + formData.append('note_type', noteType); // ex: 'budget_prev' + formData.append('reference_id', refId); // ex: '2026-03-01' + formData.append('content', content); // Le texte + + fetch('/modules/budget/includes/api/save-budget.php', { + method: 'POST', + body: formData + }) + .then(async r => { + const text = await r.text(); + if (!r.ok) throw new Error(`Erreur HTTP ${r.status} : ${text}`); + if (!text) throw new Error("Le serveur a renvoyé une réponse vide."); + try { + return JSON.parse(text); + } catch(e) { + throw new Error("Réponse inattendue (non JSON) : " + text); + } + }) + .then(data => { + if(data.success) { + const indicator = document.getElementById('note-save-indicator'); + if(indicator) { + indicator.style.opacity = '1'; + setTimeout(() => indicator.style.opacity = '0', 2000); + } + } else { + alert("Erreur lors de la sauvegarde : " + data.error); + } + }) + .catch(e => { + console.error('Erreur technique:', e); + alert(e.message); + }); +} \ No newline at end of file diff --git a/modules/budget/views/recap.php b/modules/budget/views/recap.php index a0d29cb..885574f 100644 --- a/modules/budget/views/recap.php +++ b/modules/budget/views/recap.php @@ -5,11 +5,10 @@ $stmt = $pdo->query("SELECT * FROM pf_budget_items ORDER BY category DESC, sort_order ASC, name ASC"); $items = $stmt->fetchAll(); -// 2. Récupération des Dépenses Réelles du mois +// 2. Récupération des Dépenses Réelles du mois (Pour valider les états) $currentMonth = date('m'); $currentYear = date('Y'); -// On récupère TOUTES les dépenses du mois pour faire le calcul en PHP $stmtExp = $pdo->prepare("SELECT amount, label, budget_item_id FROM pf_expenses WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ?"); $stmtExp->execute([$currentMonth, $currentYear]); $allExpenses = $stmtExp->fetchAll(PDO::FETCH_ASSOC); @@ -32,8 +31,7 @@ $totalRevenus = 0; Montant Prévu Type Jour - État - Régularisation + État du mois () Actions @@ -51,11 +49,9 @@ $totalRevenus = 0; foreach ($allExpenses as $exp) { $match = false; - // A. Matching par ID (Prioritaire) if (!empty($exp['budget_item_id']) && (int)$exp['budget_item_id'] === (int)$item['id']) { $match = true; } - // B. Matching par Mots-clés elseif (empty($exp['budget_item_id']) && !empty($item['mapping_keywords'])) { $keywords = array_map('trim', explode(',', $item['mapping_keywords'])); foreach ($keywords as $kw) { @@ -67,31 +63,22 @@ $totalRevenus = 0; } if ($match) { - // CORRECTION : Gestion des signes if ($item['category'] === 'income') { - // Pour les revenus stockés en négatif, on prend la valeur absolue $realSum += abs((float)$exp['amount']); } else { - // Pour les dépenses, on garde le signe (Débit positif, Remboursement négatif) $realSum += (float)$exp['amount']; } $hasMatchingExpense = true; } } - // --- 3. LOGIQUE D'ÉTAT (AUTO-CHECK) --- - // On coche si la somme réelle atteint au moins 98% du montant prévu (tolérance petit écart) - // ou si c'est coché manuellement. - $isAutoChecked = false; + // --- 3. LOGIQUE D'ÉTAT (Uniquement dynamique) --- $gap = $realSum - $item['amount']; - - // Seuil de tolérance (ex: 0.10€) pour considérer que c'est payé + $isAutoChecked = false; if ($hasMatchingExpense && ($realSum >= ($item['amount'] - 0.10))) { $isAutoChecked = true; } - $isPaid = $item['is_checked'] || $isAutoChecked; - // Styles $rowClass = ($item['category'] === 'income') ? 'row-income' : 'row-expense'; if ($item['is_estimate']) $rowClass .= ' row-estimate'; @@ -100,9 +87,16 @@ $totalRevenus = 0; (Est.)' : '' ?> + 🔗 + + +
+ 📅 Régul. prévue en +
+ @@ -122,7 +116,6 @@ $totalRevenus = 0; Montant exact ✓ -
Soit /mois
@@ -133,33 +126,24 @@ $totalRevenus = 0; - + -
- - - Auto ✅ - - - onclick="toggleItemCheck(, this.checked)" - title="Marquer comme payé" - style="width:18px; height:18px; cursor:pointer;"> - - Payé - - Partiel - - Attente - - -
+ +
+ Validé +
+ +
+ Partiel +
+ +
+ En attente +
+ - - -
@@ -172,16 +156,16 @@ $totalRevenus = 0; Total Revenus (Lissés) - + + + Total Dépenses (Lissées) - - + - Équilibre théorique - + € / mois @@ -190,7 +174,7 @@ $totalRevenus = 0;
-

* Les lignes Auto ✅ sont validées quand la somme des transactions atteint le montant prévu.

+

* L'état de paiement se met à jour automatiquement en fonction des opérations importées dans l'onglet "Suivi Mensuel".

@@ -274,10 +258,6 @@ $totalRevenus = 0; \ No newline at end of file diff --git a/modules/budget/views/suivi.php b/modules/budget/views/suivi.php index 018a3aa..16c2a9c 100644 --- a/modules/budget/views/suivi.php +++ b/modules/budget/views/suivi.php @@ -2,10 +2,24 @@ // modules/budget/views/suivi.php // ============================================================================ -// 1. GESTION DES ACTIONS (POST/GET) +// 1. GESTION DES ACTIONS ET DE LA NAVIGATION // ============================================================================ -$currentMonthKey = date('m-Y'); +// --- NAVIGATION PAR MOIS --- +$currentMonth = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEFT) : date('m'); +$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y'); +$currentMonthKey = $currentMonth . '-' . $currentYear; + +// Calcul des liens de navigation +$prevM = (int)$currentMonth - 1; $prevY = $currentYear; +if ($prevM < 1) { $prevM = 12; $prevY--; } +$nextM = (int)$currentMonth + 1; $nextY = $currentYear; +if ($nextM > 12) { $nextM = 1; $nextY++; } + +$prevLink = "?tab=suivi&m=$prevM&y=$prevY"; +$nextLink = "?tab=suivi&m=$nextM&y=$nextY"; +$todayLink = "?tab=suivi"; + // A. AJOUT CATÉGORIE TEMPORAIRE MANUELLE if (isset($_POST['action']) && $_POST['action'] === 'add_temp_cat') { @@ -16,14 +30,14 @@ if (isset($_POST['action']) && $_POST['action'] === 'add_temp_cat') { if ($name && $budget >= 0) { $stmt = $pdo->prepare("INSERT INTO pf_monthly_categories (month_year, name, type, budget) VALUES (?, ?, ?, ?)"); $stmt->execute([$currentMonthKey, $name, $type, $budget]); - header("Location: ?tab=suivi"); exit; + header("Location: ?tab=suivi&m=$currentMonth&y=$currentYear"); exit; } } // B. SUPPRESSION CATÉGORIE TEMPORAIRE if (isset($_GET['del_cat'])) { $pdo->prepare("DELETE FROM pf_monthly_categories WHERE id = ?")->execute([(int)$_GET['del_cat']]); - header("Location: ?tab=suivi"); exit; + header("Location: ?tab=suivi&m=$currentMonth&y=$currentYear"); exit; } // C. SAUVEGARDE SNAPSHOT BANCAIRE @@ -32,14 +46,18 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_snapshot') { $amount = floatval($_POST['snapshot_amount']); $pdo->query("DELETE FROM pf_bank_snapshots"); $pdo->prepare("INSERT INTO pf_bank_snapshots (snapshot_date, amount) VALUES (?, ?)")->execute([$date, $amount]); - header("Location: ?tab=suivi"); exit; + header("Location: ?tab=suivi&m=$currentMonth&y=$currentYear"); exit; } -// D. SAUVEGARDE IMPORT CSV (AVEC BUDGET_ITEM_ID ET HOLIDAY_ID) +// D. SAUVEGARDE IMPORT CSV (LIGNE PAR LIGNE AVEC CHECKBOX) if (isset($_POST['action']) && $_POST['action'] === 'save_import') { $count = 0; - // 1. Catégories temporaires (Inchangé) + // On récupère le mois affiché sur l'écran lors de l'import + $viewMonth = $_POST['view_month'] ?? $currentMonth; + $viewYear = $_POST['view_year'] ?? $currentYear; + + // 1. Catégories temporaires $tempCatMapping = []; if (!empty($_POST['new_temp_cats'])) { $stmtTemp = $pdo->prepare("INSERT INTO pf_monthly_categories (month_year, name, type, budget) VALUES (?, ?, ?, 0)"); @@ -49,7 +67,6 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') { } } - // 2. Insertion avec budget_item_id ET holiday_id $stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?)"); $stmtRule = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category) VALUES (?, ?) ON DUPLICATE KEY UPDATE category = VALUES(category)"); @@ -59,7 +76,7 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') { $cat = $line['cat']; $is_credit = isset($line['is_credit']) ? (int)$line['is_credit'] : 0; $budgetItemId = !empty($line['budget_item_id']) ? (int)$line['budget_item_id'] : null; - $holidayId = !empty($line['holiday_id']) ? (int)$line['holiday_id'] : null; // NOUVEAU + $holidayId = !empty($line['holiday_id']) ? (int)$line['holiday_id'] : null; if ($is_credit && empty($cat)) continue; if (!$is_credit && empty($cat)) continue; @@ -69,16 +86,30 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') { } $finalAmount = $is_credit ? -abs($line['amount']) : abs($line['amount']); + + // NOUVEAU : GESTION DU DÉCALAGE SI LA COCHE EST SÉLECTIONNÉE + $dateToSave = $line['date']; + if (!empty($line['force_current'])) { + $day = date('d', strtotime($dateToSave)); + // On vérifie que la date existe (ex: pour éviter le 31 Février) + if (checkdate((int)$viewMonth, (int)$day, (int)$viewYear)) { + $dateToSave = "$viewYear-$viewMonth-$day"; + } else { + // Sinon, on met au dernier jour du mois visé + $dateToSave = date('Y-m-t', strtotime("$viewYear-$viewMonth-01")); + } + } try { - $stmtExp->execute([$line['date'], $cat, $line['label'], $finalAmount, $line['ref'], $budgetItemId, $holidayId]); + $stmtExp->execute([$dateToSave, $cat, $line['label'], $finalAmount, $line['ref'], $budgetItemId, $holidayId]); $stmtRule->execute([$line['label'], $cat]); $count++; } catch (Exception $e) { continue; } } } } - header("Location: ?tab=suivi&msg=imported_$count"); exit; + + header("Location: ?tab=suivi&m=$viewMonth&y=$viewYear&msg=imported_$count"); exit; } // E. AJOUT OU MODIFICATION DÉPENSE (MANUELLE) @@ -90,7 +121,7 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_expense_manual') { $label = trim($_POST['label']); $budgetItemId = null; - $holidayId = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null; // NOUVEAU + $holidayId = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null; if ($cat === 'School' && !empty($_POST['label_select'])) { $label = trim($_POST['label_select']); @@ -102,24 +133,23 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_expense_manual') { if ($label && $amount > 0) { $is_credit = isset($_POST['is_credit']) ? (int)$_POST['is_credit'] : 0; $finalAmount = $is_credit ? -abs($amount) : abs($amount); + if ($id) { - // UPDATE $pdo->prepare("UPDATE pf_expenses SET date_exp=?, category=?, label=?, amount=?, budget_item_id=?, holiday_id=? WHERE id=?") ->execute([$date, $cat, $label, $finalAmount, $budgetItemId, $holidayId, $id]); } else { - // INSERT $uniqueRef = "MANUAL_" . uniqid(); $pdo->prepare("INSERT INTO pf_expenses (date_exp, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?)") ->execute([$date, $cat, $label, $finalAmount, $uniqueRef, $budgetItemId, $holidayId]); } - header("Location: ?tab=suivi"); exit; + header("Location: ?tab=suivi&m=$currentMonth&y=$currentYear"); exit; } } // F. SUPPRESSION DÉPENSE if (isset($_GET['delete_expense'])) { $pdo->prepare("DELETE FROM pf_expenses WHERE id = ?")->execute([(int)$_GET['delete_expense']]); - header("Location: ?tab=suivi"); exit; + header("Location: ?tab=suivi&m=$currentMonth&y=$currentYear"); exit; } // G. RECUPERATION VACANCES @@ -137,15 +167,13 @@ $today_day = (int)date('j'); $fixedChargesList = []; $incomeList = []; -$currentMonth = date('m'); $currentYear = date('Y'); - -// 1. Récupération des IDs payés -$stmtIds = $pdo->prepare("SELECT DISTINCT budget_item_id FROM pf_expenses WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ? AND budget_item_id IS NOT NULL"); +// 1. Récupération des IDs de charges fixes déjà payées (Dépenses uniquement) +$stmtIds = $pdo->prepare("SELECT DISTINCT budget_item_id FROM pf_expenses WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ? AND budget_item_id IS NOT NULL AND amount > 0"); $stmtIds->execute([$currentMonth, $currentYear]); $paidItemIds = $stmtIds->fetchAll(PDO::FETCH_COLUMN); -// 2. Récupération libellés -$stmtLabels = $pdo->prepare("SELECT label FROM pf_expenses WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ?"); +// 2. Récupération libellés (Uniquement les dépenses pour éviter qu'un revenu ne valide une charge) +$stmtLabels = $pdo->prepare("SELECT label FROM pf_expenses WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ? AND amount > 0"); $stmtLabels->execute([$currentMonth, $currentYear]); $realExpensesLabels = $stmtLabels->fetchAll(PDO::FETCH_COLUMN); @@ -168,7 +196,7 @@ if (!empty($snapshot['date'])) { } catch (Exception $e) {} } -// Lecture Budget +// Lecture Budget Prévisionnel $stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, is_checked, mapping_keywords FROM pf_budget_items ORDER BY name ASC"); while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) { $rawAmount = (float)$item['amount']; @@ -322,7 +350,6 @@ function getDisplayLogic($spent, $bg, $type) { $pct = ($bg > 0) ? min(100, ($spent / $bg) * 100) : ($spent > 0 ? 100 : 0); $isOver = ($spent > $bg && $bg > 0); - // MODIFICATION ICI : On cache le "/ 0 €" si le budget est à 0 if ($bg > 0) { $text = number_format(ceil($spent), 0, ',', ' ') . ' / ' . number_format(ceil($bg), 0, ',', ' ') . ' €'; } else { @@ -331,25 +358,27 @@ function getDisplayLogic($spent, $bg, $type) { } return ['pct' => $pct, 'isOver' => $isOver, 'text' => $text]; } + +// Nom du mois en français (Compatible PHP 8+) +$moisFr = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']; +$monthName = $moisFr[(int)$currentMonth] . ' ' . $currentYear; ?> -
-
+
-

Suivi :

+
+

Suivi :

+
+ + Auj. + +
+
+
Charges à venir :
@@ -427,10 +456,16 @@ function getDisplayLogic($spent, $bg, $type) {
+ +
-
-

Valider l'importation

+
+
+

Valider l'importation

+ +
+
@@ -440,7 +475,7 @@ function getDisplayLogic($spent, $bg, $type) {
- + $row): @@ -456,11 +491,27 @@ function getDisplayLogic($spent, $bg, $type) { + -
LibelléMontantCatégorie
DateLibelléMontantCatégorie
+
+ + + + +
(déjà importé) + @@ -485,7 +536,8 @@ function getDisplayLogic($spent, $bg, $type) { - > @@ -514,7 +566,7 @@ function getDisplayLogic($spent, $bg, $type) {

- × + ×

@@ -554,7 +606,7 @@ function getDisplayLogic($spent, $bg, $type) { ✏️ - × @@ -637,7 +689,7 @@ function getDisplayLogic($spent, $bg, $type) {
-
+