diff --git a/modules/budget/views/recap.php b/modules/budget/views/recap.php index d54396a..d2bbd31 100644 --- a/modules/budget/views/recap.php +++ b/modules/budget/views/recap.php @@ -9,7 +9,7 @@ $items = $stmt->fetchAll(); $currentMonth = date('m'); $currentYear = date('Y'); -$stmtExp = $pdo->prepare("SELECT amount, label, budget_item_id FROM pf_expenses WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ?"); +$stmtExp = $pdo->prepare("SELECT amount, label, category, budget_item_id FROM pf_expenses WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ?"); $stmtExp->execute([$currentMonth, $currentYear]); $allExpenses = $stmtExp->fetchAll(PDO::FETCH_ASSOC); @@ -54,6 +54,9 @@ $totalRevenus = 0; if (!empty($exp['budget_item_id']) && (int)$exp['budget_item_id'] === (int)$item['id']) { $match = true; } + elseif ($exp['category'] === 'School' && trim($item['name']) === 'Estimacio escola') { + $match = true; + } elseif (empty($exp['budget_item_id']) && !empty($item['mapping_keywords'])) { $keywords = array_map('trim', explode(',', $item['mapping_keywords'])); foreach ($keywords as $kw) { @@ -65,9 +68,6 @@ $totalRevenus = 0; } if ($match) { - // Avec la nouvelle norme, on additionne simplement les montants. - // Si c'est une dépense, ça s'additionne en négatif. - // S'il y a un remboursement partiel, le "+" vient réduire le "-". C'est mathématiquement parfait. $realSum += (float)$exp['amount']; $hasMatchingExpense = true; } diff --git a/modules/budget/views/suivi.php b/modules/budget/views/suivi.php index 94f9465..c9b875e 100644 --- a/modules/budget/views/suivi.php +++ b/modules/budget/views/suivi.php @@ -18,13 +18,37 @@ $prevLink = "?tab=suivi&m=$prevM&y=$prevY"; $nextLink = "?tab=suivi&m=$nextM&y=$nextY"; $todayLink = "?tab=suivi"; +// ============================================================================ +// NOUVEAU : CONFIGURATION DU CYCLE (Dates personnalisées) +// ============================================================================ +// 1. On lit la configuration du mois en cours +$stmtConfig = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'month_config' AND reference_id = ?"); +$stmtConfig->execute([$currentMonthKey]); +$configJson = $stmtConfig->fetchColumn(); +$monthConfig = $configJson ? json_decode($configJson, true) : null; + +$customStartDate = $monthConfig['start_date'] ?? "$currentYear-$currentMonth-01"; +$customStartBalance = $monthConfig['start_balance'] ?? 0; + +// 2. On lit la configuration du mois SUIVANT pour fermer le cycle proprement +$nextMonthKey = str_pad($nextM, 2, '0', STR_PAD_LEFT) . '-' . $nextY; +$stmtNextConfig = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'month_config' AND reference_id = ?"); +$stmtNextConfig->execute([$nextMonthKey]); +$nextConfigJson = $stmtNextConfig->fetchColumn(); +$nextConfig = $nextConfigJson ? json_decode($nextConfigJson, true) : null; + +// La date de fin est soit la veille du prochain cycle, soit la fin du mois calendaire +if ($nextConfig && !empty($nextConfig['start_date'])) { + $customEndDate = date('Y-m-d', strtotime($nextConfig['start_date'] . ' -1 day')); +} else { + $customEndDate = date('Y-m-t', strtotime("$currentYear-$currentMonth-01")); +} + // A. AJOUT CATÉGORIE TEMPORAIRE MANUELLE if (isset($_POST['action']) && $_POST['action'] === 'add_temp_cat') { $name = trim($_POST['cat_name']); - $budget = floatval($_POST['cat_budget']); // Saisi en positif par l'utilisateur + $budget = floatval($_POST['cat_budget']); $type = $_POST['cat_type'] === 'credit' ? 'credit' : 'debit'; - - // Si c'est un budget alloué (débit), on le stocke en négatif if ($type === 'debit') $budget = -$budget; if ($name) { @@ -82,7 +106,6 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') { $cat = 'TEMP_' . $tempCatMapping[$cat]; } - // LOGIQUE INVERSÉE : Crédit = Positif, Débit = Négatif $finalAmount = $is_credit ? abs($line['amount']) : -abs($line['amount']); $dateToSave = $line['date']; @@ -103,7 +126,6 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') { } } } - header("Location: ?tab=suivi&m=$viewMonth&y=$viewYear&msg=imported_$count"); exit; } @@ -113,22 +135,15 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_expense_manual') { $cat = $_POST['category']; $amount = floatval($_POST['amount']); $date = $_POST['date']; - $label = trim($_POST['label']); $budgetItemId = null; $holidayId = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null; - if ($cat === 'School' && !empty($_POST['label_select'])) { - $label = trim($_POST['label_select']); - } - elseif (($cat === 'Frais' || $cat === 'Income') && !empty($_POST['budget_item_id'])) { - $budgetItemId = (int)$_POST['budget_item_id']; - } + if ($cat === 'School' && !empty($_POST['label_select'])) $label = trim($_POST['label_select']); + elseif (($cat === 'Frais' || $cat === 'Income') && !empty($_POST['budget_item_id'])) $budgetItemId = (int)$_POST['budget_item_id']; if ($label && $amount > 0) { $is_credit = isset($_POST['is_credit']) ? (int)$_POST['is_credit'] : 0; - - // LOGIQUE INVERSÉE : Crédit = Positif, Débit = Négatif $finalAmount = $is_credit ? abs($amount) : -abs($amount); if ($id) { @@ -160,18 +175,16 @@ $budget_fmcg = 0; $budget_school = 0; $budget_essence = 0; $budget_frais = 0; $b $total_income = 0; $total_expenses_prevues = 0; $reste_a_venir = 0; $today_day = (int)date('j'); - $fixedChargesList = []; $incomeList = []; -// On sélectionne les charges qui ont un montant NÉGATIF ou POSITIF selon la nouvelle règle (plus besoin de filtrer sur > 0) -$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"); -$stmtIds->execute([$currentMonth, $currentYear]); +// MODIFICATION : Les requêtes ciblent désormais les bornes du CYCLE personnalisé, plus le mois strict ! +$stmtIds = $pdo->prepare("SELECT DISTINCT budget_item_id FROM pf_expenses WHERE date_exp >= ? AND date_exp <= ? AND budget_item_id IS NOT NULL"); +$stmtIds->execute([$customStartDate, $customEndDate]); $paidItemIds = $stmtIds->fetchAll(PDO::FETCH_COLUMN); -// On récupère uniquement les libellés des dépenses (montant < 0) -$stmtLabels = $pdo->prepare("SELECT label FROM pf_expenses WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ? AND amount < 0"); -$stmtLabels->execute([$currentMonth, $currentYear]); +$stmtLabels = $pdo->prepare("SELECT label FROM pf_expenses WHERE date_exp >= ? AND date_exp <= ? AND amount < 0"); +$stmtLabels->execute([$customStartDate, $customEndDate]); $realExpensesLabels = $stmtLabels->fetchAll(PDO::FETCH_COLUMN); // Snapshot Bancaire @@ -188,16 +201,15 @@ $solde_actuel = $snapshot['amount']; // 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)) { - // Les montants en base ont leur vrai signe (- pour dépense, + pour revenu) $rawAmount = (float)$item['amount']; - $absAmount = abs($rawAmount); // On utilise l'absolu pour le visuel des jauges + $absAmount = abs($rawAmount); $amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount; $name = trim($item['name']); $pDay = (int)$item['payment_day']; $isChecked = (int)$item['is_checked']; if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) { - $fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount]; // Passé en array propre pour l'HTML + $fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount]; } if ($item['category'] === 'income') { $incomeList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount]; @@ -222,10 +234,7 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) { } } } - - if (!$isPaid) { - $reste_a_venir += $absAmount; - } + if (!$isPaid) $reste_a_venir += $absAmount; } if ($name === 'Estimacio F&B & beauty') $budget_fmcg = $amt; @@ -237,26 +246,19 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) { } } -// Catégories Temporaires $tempCats = []; $total_temp_budget = 0; try { $stmt = $pdo->prepare("SELECT * FROM pf_monthly_categories WHERE month_year = ?"); $stmt->execute([$currentMonthKey]); $tempCats = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach($tempCats as $tc) { - if ($tc['type'] === 'debit') { - $total_temp_budget += abs($tc['budget']); - } + if ($tc['type'] === 'debit') $total_temp_budget += abs($tc['budget']); } } catch (Exception $e) {} $budget_autres = $total_income - ($total_expenses_prevues + $total_temp_budget); if ($budget_autres < 0) $budget_autres = 0; -// ============================================================================ -// 3. CONFIGURATION CATÉGORIES -// ============================================================================ - $categoriesConfig = [ 'Income' => ['type'=>'credit', 'label'=>'Revenus', 'budget'=>$budget_income_prevu, 'color'=>'#10b981', 'suggestions'=>[]], 'FMCG' => ['type'=>'debit', 'label'=>'Courses (FMCG)', 'budget'=>$budget_fmcg, 'color'=>'#3b82f6', 'suggestions'=>['Action', 'Carrefour', 'Lidl']], @@ -277,11 +279,11 @@ $categoriesConfig['Autres'] = ['type'=>'debit', 'label'=>'Autres / Imprévus', ' $categoriesConfig['LivretA'] = ['type'=>'debit', 'label'=>'Epargne', 'budget'=>0, 'color'=>'#8b5cf6', 'suggestions'=>['Virement']]; // ============================================================================ -// 4. DONNÉES RÉELLES & IMPORT +// 4. DONNÉES RÉELLES (Sur la période du cycle) // ============================================================================ - $csvData = []; $showPreview = false; +// ... (Gestion CSV Upload inchangée ici) if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) { $file = $_FILES['csv_file']['tmp_name']; $handle = fopen($file, "r"); @@ -309,54 +311,55 @@ if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) { $showPreview = true; } -// DÉPENSES EN BDD -$stmt = $pdo->prepare("SELECT * FROM pf_expenses WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ? ORDER BY date_exp DESC"); -$stmt->execute([$currentMonth, $currentYear]); +// MODIFICATION : Dépenses ciblées sur les bornes du cycle ! +$stmt = $pdo->prepare("SELECT * FROM pf_expenses WHERE date_exp >= ? AND date_exp <= ? ORDER BY date_exp DESC"); +$stmt->execute([$customStartDate, $customEndDate]); $allExpenses = $stmt->fetchAll(PDO::FETCH_ASSOC); $totals = array_fill_keys(array_keys($categoriesConfig), 0); $expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []); -$capacite_max = 0; +// --- PREPARATION SOLDE THEORIQUE SUR LE CYCLE --- +$solde_initial = $customStartBalance; +$total_rentrees = 0; $depenses_reelles = 0; foreach ($allExpenses as $exp) { $cat = $exp['category']; if (!isset($totals[$cat])) $cat = 'Autres'; - $val = (float)$exp['amount']; // NOUVELLE RÈGLE : + = Revenu/Remboursement, - = Dépense + $val = (float)$exp['amount']; - // 1. Remplissage Visuel des cartes - if ($cat === 'Income') { - $totals[$cat] += $val; + // 1. UI Cards + if ($cat === 'Income') { + $totals[$cat] += $val; } else { - if ($val > 0) { - // C'est un remboursement d'une dépense (+), il augmente le budget - $categoriesConfig[$cat]['budget'] += $val; - } else { - // C'est une dépense normale (-), on l'ajoute au total dépensé (en valeur absolue pour l'UI) - $totals[$cat] += abs($val); - } + if ($val > 0) $categoriesConfig[$cat]['budget'] += $val; + else $totals[$cat] += abs($val); } $expensesByCategory[$cat][] = $exp; - // 2. Calcul du Théorique + // 2. Mathématiques pures if ($val > 0) { if ($cat === 'Income' || $cat !== 'Frais') { - $capacite_max += $val; + $total_rentrees += $val; // Rentrées d'argent } else { - $depenses_reelles -= $val; + $depenses_reelles -= $val; // Remboursement d'une charge fixe } } else { $depenses_reelles += abs($val); } } -$solde_theorique = $capacite_max - $depenses_reelles - $reste_a_venir; +// 3. Calcul de la Capacité Max Dynamique +$rentrees_salaires_reels = $totals['Income']; +$rentrees_autres = $total_rentrees - $rentrees_salaires_reels; -if ($capacite_max <= 0) { - $capacite_max = $budget_income_prevu > 0 ? $budget_income_prevu : 1; -} +// Capacité Max = Solde de départ + (le plus grand entre le salaire prévu et reçu) + rentrées annexes +$capacite_max = $solde_initial + max($rentrees_salaires_reels, $budget_income_prevu) + $rentrees_autres; + +// SOLDE THÉORIQUE = Solde initial + Toutes les rentrées réelles - Dépenses - Charges à venir +$solde_theorique = ($solde_initial + $total_rentrees) - $depenses_reelles - $reste_a_venir; $solde_net = max(0, $solde_actuel - $reste_a_venir); $charges_visibles = min($solde_actuel, $reste_a_venir); @@ -385,19 +388,27 @@ function getDisplayLogic($spent, $bg, $type) { $moisFr = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']; $monthName = $moisFr[(int)$currentMonth] . ' ' . $currentYear; +$cycleDisplay = date('d/m', strtotime($customStartDate)) . ' au ' . date('d/m', strtotime($customEndDate)); ?> -