diff --git a/modules/budget/views/suivi.php b/modules/budget/views/suivi.php index 207a7c9..54860f5 100644 --- a/modules/budget/views/suivi.php +++ b/modules/budget/views/suivi.php @@ -2,136 +2,68 @@ // modules/budget/views/suivi.php // ============================================================================ -// 1. GESTION DES ACTIONS ET DE LA NAVIGATION +// 1. GESTION DU MOIS ACTIF ET DE LA NAVIGATION // ============================================================================ -$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; +// A. Déterminer quel est le mois "ouvert" par défaut +$stmtActive = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'active_gestion_month' LIMIT 1"); +$defaultActiveMonth = $stmtActive->fetchColumn(); +if (!$defaultActiveMonth) { + $defaultActiveMonth = date('Y-m-01'); +} -$prevM = (int)$currentMonth - 1; $prevY = $currentYear; -if ($prevM < 1) { $prevM = 12; $prevY--; } -$nextM = (int)$currentMonth + 1; $nextY = $currentYear; -if ($nextM > 12) { $nextM = 1; $nextY++; } +// B. Mois affiché à l'écran +$viewM = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEFT) : date('m', strtotime($defaultActiveMonth)); +$viewY = isset($_GET['y']) ? (int)$_GET['y'] : date('Y', strtotime($defaultActiveMonth)); +$viewMonthDate = "$viewY-$viewM-01"; -$prevLink = "?tab=suivi&m=$prevM&y=$prevY"; -$nextLink = "?tab=suivi&m=$nextM&y=$nextY"; -$todayLink = "?tab=suivi"; +$prevDate = date('Y-m-01', strtotime('-1 month', strtotime($viewMonthDate))); +$nextDate = date('Y-m-01', strtotime('+1 month', strtotime($viewMonthDate))); + +$prevLink = "?tab=suivi&m=" . date('m', strtotime($prevDate)) . "&y=" . date('Y', strtotime($prevDate)); +$nextLink = "?tab=suivi&m=" . date('m', strtotime($nextDate)) . "&y=" . date('Y', strtotime($nextDate)); +$defaultLink = "?tab=suivi"; // ============================================================================ -// CONFIGURATION DU CYCLE (Dates personnalisées) +// 2. GESTION DES ACTIONS POST (Clôture, Ajouts, CSV) // ============================================================================ -$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; - -$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; - -if ($nextConfig && !empty($nextConfig['start_date'])) { - $customEndDate = date('Y-m-d', strtotime($nextConfig['start_date'] . ' -1 day')); -} else { - $customEndDate = date('Y-m-d', strtotime($customStartDate . ' +1 month -1 day')); -} - -// 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']); - $type = $_POST['cat_type'] === 'credit' ? 'credit' : 'debit'; - if ($type === 'debit') $budget = -$budget; - - if ($name) { - $stmt = $pdo->prepare("INSERT INTO pf_monthly_categories (month_year, name, type, budget) VALUES (?, ?, ?, ?)"); - $stmt->execute([$currentMonthKey, $name, $type, $budget]); - 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&m=$currentMonth&y=$currentYear"); exit; -} - -// C. SAUVEGARDE SNAPSHOT BANCAIRE -if (isset($_POST['action']) && $_POST['action'] === 'save_snapshot') { - $date = $_POST['snapshot_date']; - $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&m=$currentMonth&y=$currentYear"); exit; -} - -// D. SAUVEGARDE IMPORT CSV -if (isset($_POST['action']) && $_POST['action'] === 'save_import') { - $count = 0; - $viewMonth = $_POST['view_month'] ?? $currentMonth; - $viewYear = $_POST['view_year'] ?? $currentYear; +if (isset($_POST['action']) && $_POST['action'] === 'close_month') { + $monthToClose = $_POST['close_month_date']; + $nextMonthToOpen = date('Y-m-01', strtotime('+1 month', strtotime($monthToClose))); - $tempCatMapping = []; - if (!empty($_POST['new_temp_cats'])) { - $stmtTemp = $pdo->prepare("INSERT INTO pf_monthly_categories (month_year, name, type, budget) VALUES (?, ?, ?, 0)"); - foreach ($_POST['new_temp_cats'] as $tempKey => $catData) { - $stmtTemp->execute([$currentMonthKey, $catData['name'], $catData['type']]); - $tempCatMapping[$tempKey] = $pdo->lastInsertId(); - } - } + $frozenData = [ + 'is_closed' => true, + 'solde_actuel' => (float)$_POST['freeze_solde_actuel'], + 'capacite_max' => (float)$_POST['freeze_capacite_max'], + 'solde_theorique' => (float)$_POST['freeze_solde_theorique'], + 'reste_a_venir' => (float)$_POST['freeze_reste_a_venir'], + 'closed_at' => date('Y-m-d H:i:s') + ]; + $pdo->prepare("INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('month_closure', ?, ?) ON DUPLICATE KEY UPDATE content = VALUES(content)") + ->execute([$monthToClose, json_encode($frozenData)]); - $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)"); + $pdo->prepare("INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('active_gestion_month', 'GLOBAL', ?) ON DUPLICATE KEY UPDATE content = VALUES(content)") + ->execute([$nextMonthToOpen]); - if (isset($_POST['lines']) && is_array($_POST['lines'])) { - foreach ($_POST['lines'] as $line) { - if (isset($line['import_check'])) { - $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; - - if ($is_credit && empty($cat)) continue; - if (!$is_credit && empty($cat)) continue; + header("Location: ?tab=suivi&m=" . date('m', strtotime($nextMonthToOpen)) . "&y=" . date('Y', strtotime($nextMonthToOpen))); + exit; +} - if (strpos($cat, 'NEW_TEMP_') === 0 && isset($tempCatMapping[$cat])) { - $cat = 'TEMP_' . $tempCatMapping[$cat]; - } - - $finalAmount = $is_credit ? abs($line['amount']) : -abs($line['amount']); - - $dateToSave = $line['date']; - if (!empty($line['force_current'])) { - $day = date('d', strtotime($dateToSave)); - if (checkdate((int)$viewMonth, (int)$day, (int)$viewYear)) { - $dateToSave = "$viewYear-$viewMonth-$day"; - } else { - $dateToSave = date('Y-m-t', strtotime("$viewYear-$viewMonth-01")); - } - } - - try { - $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&m=$viewMonth&y=$viewYear&msg=imported_$count"); exit; +if (isset($_POST['action']) && $_POST['action'] === 'reopen_month') { + $pdo->prepare("DELETE FROM pf_notes WHERE note_type = 'month_closure' AND reference_id = ?")->execute([$viewMonthDate]); + $pdo->prepare("INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('active_gestion_month', 'GLOBAL', ?) ON DUPLICATE KEY UPDATE content = VALUES(content)") + ->execute([$viewMonthDate]); + header("Location: ?tab=suivi&m=$viewM&y=$viewY"); + exit; } -// E. AJOUT OU MODIFICATION DÉPENSE (MANUELLE) if (isset($_POST['action']) && $_POST['action'] === 'save_expense_manual') { $id = !empty($_POST['expense_id']) ? (int)$_POST['expense_id'] : null; $cat = $_POST['category']; $amount = floatval($_POST['amount']); $date = $_POST['date']; + $gestionMonth = $_POST['gestion_month']; $label = trim($_POST['label']); $budgetItemId = null; $holidayId = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null; @@ -144,142 +76,60 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_expense_manual') { $finalAmount = $is_credit ? abs($amount) : -abs($amount); if ($id) { - $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]); + $pdo->prepare("UPDATE pf_expenses SET date_exp=?, gestion_month=?, category=?, label=?, amount=?, budget_item_id=?, holiday_id=? WHERE id=?") + ->execute([$date, $gestionMonth, $cat, $label, $finalAmount, $budgetItemId, $holidayId, $id]); } else { $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]); + $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)") + ->execute([$date, $gestionMonth, $cat, $label, $finalAmount, $uniqueRef, $budgetItemId, $holidayId]); } - header("Location: ?tab=suivi&m=$currentMonth&y=$currentYear"); exit; + header("Location: ?tab=suivi&m=$viewM&y=$viewY"); exit; } } -// F. SUPPRESSION DÉPENSE +if (isset($_POST['action']) && $_POST['action'] === 'save_import') { + $count = 0; + $stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, 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)"); + + if (isset($_POST['lines']) && is_array($_POST['lines'])) { + foreach ($_POST['lines'] as $line) { + if (isset($line['import_check'])) { + $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; + $gestionMonthLine = !empty($line['gestion_month']) ? $line['gestion_month'] . '-01' : $viewMonthDate; + + if ($is_credit && empty($cat)) continue; + if (!$is_credit && empty($cat)) continue; + + $finalAmount = $is_credit ? abs($line['amount']) : -abs($line['amount']); + $dateToSave = $line['date']; + + try { + $stmtExp->execute([$dateToSave, $gestionMonthLine, $cat, $line['label'], $finalAmount, $line['ref'], $budgetItemId, $holidayId]); + $stmtRule->execute([$line['label'], $cat]); + $count++; + } catch (Exception $e) { continue; } + } + } + } + header("Location: ?tab=suivi&m=$viewM&y=$viewY&msg=imported_$count"); exit; +} + +if (isset($_POST['action']) && $_POST['action'] === 'save_snapshot') { + $pdo->query("DELETE FROM pf_bank_snapshots"); + $pdo->prepare("INSERT INTO pf_bank_snapshots (snapshot_date, amount) VALUES (?, ?)")->execute([$_POST['snapshot_date'], floatval($_POST['snapshot_amount'])]); + header("Location: ?tab=suivi&m=$viewM&y=$viewY"); exit; +} if (isset($_GET['delete_expense'])) { $pdo->prepare("DELETE FROM pf_expenses WHERE id = ?")->execute([(int)$_GET['delete_expense']]); - header("Location: ?tab=suivi&m=$currentMonth&y=$currentYear"); exit; + header("Location: ?tab=suivi&m=$viewM&y=$viewY"); exit; } -// G. RECUPERATION VACANCES -$activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN ('draft', 'planned', 'booked') ORDER BY start_date ASC")->fetchAll(PDO::FETCH_ASSOC); - // ============================================================================ -// 2. CALCUL DES BUDGETS & CHARGES FIXES -// ============================================================================ - -$budget_fmcg = 0; $budget_school = 0; $budget_essence = 0; $budget_frais = 0; $budget_income_prevu = 0; -$total_income = 0; $total_expenses_prevues = 0; -$reste_a_venir = 0; -$today_day = (int)date('j'); -$fixedChargesList = []; -$incomeList = []; -$pending_charges = []; // Stockage détaillé des charges à venir - -$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); - -$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 -$snapshot = ['date' => date('Y-m-d'), 'amount' => 0]; -try { - $snapStmt = $pdo->query("SELECT * FROM pf_bank_snapshots ORDER BY id DESC LIMIT 1"); - if ($s = $snapStmt->fetch(PDO::FETCH_ASSOC)) { - $snapshot = ['date' => $s['snapshot_date'], 'amount' => (float)$s['amount']]; - } -} catch (Exception $e) {} - -$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)) { - $rawAmount = (float)$item['amount']; - $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]; - } - if ($item['category'] === 'income') { - $incomeList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount]; - $total_income += $amt; - $budget_income_prevu += $amt; - } else { - $total_expenses_prevues += $amt; - - if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) { - $isPaid = false; - if ($isChecked === 1) $isPaid = true; - elseif (in_array($item['id'], $paidItemIds)) $isPaid = true; - elseif (!empty($item['mapping_keywords'])) { - $keywords = array_map('trim', explode(',', $item['mapping_keywords'])); - foreach ($keywords as $kw) { - if (empty($kw)) continue; - foreach ($realExpensesLabels as $realLabel) { - if (stripos($realLabel, $kw) !== false) { - $isPaid = true; - break 2; - } - } - } - } - if (!$isPaid) { - $reste_a_venir += $absAmount; - $pending_charges[] = ['name' => $name, 'amount' => $absAmount]; - } - } - - if ($name === 'Estimacio F&B & beauty') $budget_fmcg = $amt; - elseif ($name === 'Estimacio escola') $budget_school = $amt; - elseif ($name === 'Estimation gasolina') $budget_essence = $amt; - elseif ((int)$item['is_estimate'] === 0 && $item['type'] === 'Mensuel' && $item['category'] === 'expense') { - $budget_frais += $absAmount; - } - } -} - -$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']); - } -} catch (Exception $e) {} - -$budget_autres = $total_income - ($total_expenses_prevues + $total_temp_budget); -if ($budget_autres < 0) $budget_autres = 0; - -$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']], - 'Essence' => ['type'=>'debit', 'label'=>'Essence', 'budget'=>$budget_essence, 'color'=>'#f59e0b', 'suggestions'=>['Audi', 'Polo']], - 'School' => ['type'=>'debit', 'label'=>'École / Garde', 'budget'=>$budget_school, 'color'=>'#10b981', 'suggestions'=>[]], - 'Frais' => ['type'=>'debit', 'label'=>'Charges Fixes', 'budget'=>$budget_frais, 'color'=>'#ef4444', 'suggestions'=>[]], -]; - -$tempColors = ['#ec4899', '#06b6d4', '#84cc16', '#d946ef', '#f97316']; -$colorIdx = 0; -foreach ($tempCats as $tc) { - $catKey = 'TEMP_' . $tc['id']; - $categoriesConfig[$catKey] = [ - 'type' => $tc['type'], 'label' => $tc['name'], 'budget' => abs($tc['budget']), 'color' => $tempColors[$colorIdx++ % count($tempColors)], 'suggestions' => [], 'is_temp' => true, 'id' => $tc['id'] - ]; -} -$categoriesConfig['Autres'] = ['type'=>'debit', 'label'=>'Autres / Imprévus', 'budget'=>$budget_autres, 'color'=>'#64748b', 'suggestions'=>['Restaurant', 'Cadeau']]; -$categoriesConfig['LivretA'] = ['type'=>'debit', 'label'=>'Epargne', 'budget'=>0, 'color'=>'#8b5cf6', 'suggestions'=>['Virement']]; - -// ============================================================================ -// 4. DONNÉES RÉELLES (Sur la période du cycle) +// E-Bis. LECTURE ET PRÉVISUALISATION DU FICHIER CSV // ============================================================================ $csvData = []; $showPreview = false; @@ -290,6 +140,7 @@ if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) { $rules = []; try { $rules = $pdo->query("SELECT keyword, category FROM pf_import_rules")->fetchAll(PDO::FETCH_KEY_PAIR); } catch(Exception $e){} $existingRefs = []; try { $existingRefs = $pdo->query("SELECT import_ref FROM pf_expenses WHERE import_ref IS NOT NULL")->fetchAll(PDO::FETCH_COLUMN); } catch(Exception $e){} fgetcsv($handle, 1000, ";", "\"", "\\"); + while (($data = fgetcsv($handle, 1000, ";", "\"", "\\")) !== FALSE) { $rawDebit = $data[8] ?? ''; $rawCredit = $data[9] ?? ''; $amount = 0; $isCredit = 0; @@ -303,23 +154,114 @@ if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) { $refCSV = trim($data[3]); $uniqueKey = !empty($refCSV) ? "REF_".$refCSV : "HASH_".md5($dateSql.$label.number_format($amount, 2).$isCredit); $isDuplicate = in_array($uniqueKey, $existingRefs); + $suggestedCat = ''; foreach ($rules as $kw => $c) { if (stripos($label, $kw) !== false) { $suggestedCat = $c; break; } } + $csvData[] = ['date'=>$dateSql, 'label'=>$label, 'amount'=>$amount, 'cat'=>$suggestedCat, 'ref'=>$uniqueKey, 'is_duplicate'=>$isDuplicate, 'is_credit'=>$isCredit]; } fclose($handle); $showPreview = true; } -$stmt = $pdo->prepare("SELECT * FROM pf_expenses WHERE date_exp >= ? AND date_exp <= ? ORDER BY date_exp DESC"); -$stmt->execute([$customStartDate, $customEndDate]); +// ============================================================================ +// 3. RECUPERATION DES DONNEES ET CALCULS +// ============================================================================ + +// A. Vérifier si le mois affiché est clôturé +$stmtCheckClose = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'month_closure' AND reference_id = ?"); +$stmtCheckClose->execute([$viewMonthDate]); +$closureJson = $stmtCheckClose->fetchColumn(); +$monthState = $closureJson ? json_decode($closureJson, true) : null; +$isClosed = ($monthState && isset($monthState['is_closed']) && $monthState['is_closed'] === true); + +// B. Récupérer le solde bancaire (Snapshot actuel) +$snapshot = ['date' => date('Y-m-d'), 'amount' => 0]; +$snapStmt = $pdo->query("SELECT * FROM pf_bank_snapshots ORDER BY id DESC LIMIT 1"); +if ($s = $snapStmt->fetch(PDO::FETCH_ASSOC)) { + $snapshot = ['date' => $s['snapshot_date'], 'amount' => (float)$s['amount']]; +} + +// C. Récupérer le solde final du mois PRÉCÉDENT pour avoir la base de ce mois-ci +$stmtPrevClose = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'month_closure' AND reference_id = ?"); +$stmtPrevClose->execute([$prevDate]); +$prevClosureJson = $stmtPrevClose->fetchColumn(); +$prevMonthState = $prevClosureJson ? json_decode($prevClosureJson, true) : null; + +$solde_initial = ($prevMonthState && isset($prevMonthState['solde_actuel'])) ? (float)$prevMonthState['solde_actuel'] : 0; +if ($solde_initial === 0 && !$prevMonthState) { + $solde_initial = $snapshot['amount']; +} + +// D. Les Dépenses assignées à ce MOIS DE GESTION +$stmt = $pdo->prepare("SELECT * FROM pf_expenses WHERE gestion_month = ? ORDER BY date_exp DESC"); +$stmt->execute([$viewMonthDate]); $allExpenses = $stmt->fetchAll(PDO::FETCH_ASSOC); +$paidItemIds = array_column(array_filter($allExpenses, fn($e) => !empty($e['budget_item_id'])), 'budget_item_id'); +$realExpensesLabels = array_column(array_filter($allExpenses, fn($e) => $e['amount'] < 0), 'label'); + +// E. Lecture du Budget +$budget_fmcg = 0; $budget_school = 0; $budget_essence = 0; $budget_frais = 0; $budget_income_prevu = 0; +$total_income = 0; $total_expenses_prevues = 0; +$reste_a_venir_calc = 0; +$fixedChargesList = []; $incomeList = []; $pending_charges = []; + +// CORRECTION BUG N°2 : On supprime la vérification "is_checked" de la base, on ne se fie qu'aux dépenses réelles du mois. +$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, mapping_keywords FROM pf_budget_items ORDER BY name ASC"); +while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) { + $absAmount = abs((float)$item['amount']); + $amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount; + $name = trim($item['name']); + + if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) { + $fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount]; + } + if ($item['category'] === 'income') { + $incomeList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount]; + $total_income += $amt; + $budget_income_prevu += $amt; + } else { + $total_expenses_prevues += $amt; + if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) { + $isPaid = false; + if (in_array($item['id'], $paidItemIds)) $isPaid = true; + elseif (!empty($item['mapping_keywords'])) { + $keywords = array_map('trim', explode(',', $item['mapping_keywords'])); + foreach ($keywords as $kw) { + if (empty($kw)) continue; + foreach ($realExpensesLabels as $realLabel) { + if (stripos($realLabel, $kw) !== false) { $isPaid = true; break 2; } + } + } + } + if (!$isPaid) { + $reste_a_venir_calc += $absAmount; + $pending_charges[] = ['name' => $name, 'amount' => $absAmount]; + } + } + if ($name === 'Estimacio F&B & beauty') $budget_fmcg = $amt; + elseif ($name === 'Estimacio escola') $budget_school = $amt; + elseif ($name === 'Estimation gasolina') $budget_essence = $amt; + elseif ((int)$item['is_estimate'] === 0 && $item['type'] === 'Mensuel' && $item['category'] === 'expense') { $budget_frais += $absAmount; } + } +} + +$budget_autres = max(0, $total_income - $total_expenses_prevues); + +$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']], + 'Essence' => ['type'=>'debit', 'label'=>'Essence', 'budget'=>$budget_essence, 'color'=>'#f59e0b', 'suggestions'=>['Audi', 'Polo']], + 'School' => ['type'=>'debit', 'label'=>'École / Garde', 'budget'=>$budget_school, 'color'=>'#10b981', 'suggestions'=>[]], + 'Frais' => ['type'=>'debit', 'label'=>'Charges Fixes', 'budget'=>$budget_frais, 'color'=>'#ef4444', 'suggestions'=>[]], + 'Autres' => ['type'=>'debit', 'label'=>'Autres / Imprévus', 'budget'=>$budget_autres, 'color'=>'#64748b', 'suggestions'=>['Restaurant', 'Cadeau']], + 'LivretA' => ['type'=>'debit', 'label'=>'Epargne', 'budget'=>0, 'color'=>'#8b5cf6', 'suggestions'=>['Virement']] +]; + $totals = array_fill_keys(array_keys($categoriesConfig), 0); $expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []); -// --- PREPARATION SOLDE THEORIQUE SUR LE CYCLE --- -$solde_initial = $customStartBalance; $total_rentrees = 0; $depenses_reelles = 0; @@ -329,42 +271,53 @@ foreach ($allExpenses as $exp) { $val = (float)$exp['amount']; - if ($cat === 'Income') { - $totals[$cat] += $val; - } else { + if ($cat === 'Income') { $totals[$cat] += $val; } + else { if ($val > 0) $categoriesConfig[$cat]['budget'] += $val; else $totals[$cat] += abs($val); } $expensesByCategory[$cat][] = $exp; if ($val > 0) { - if ($cat === 'Income' || $cat !== 'Frais') { - $total_rentrees += $val; - } else { - $depenses_reelles -= $val; - } + if ($cat === 'Income' || $cat !== 'Frais') $total_rentrees += $val; + else $depenses_reelles -= $val; } else { $depenses_reelles += abs($val); } } -$rentrees_salaires_reels = $totals['Income']; -$rentrees_autres = $total_rentrees - $rentrees_salaires_reels; -$capacite_max = $solde_initial + max($rentrees_salaires_reels, $budget_income_prevu) + $rentrees_autres; - -// Ajout du reste estimé pour l'École dans les charges à venir +// CORRECTION BUG N°1 : On calcule et on ajoute l'école aux charges à venir AVANT la condition de clôture +// Ainsi, le détail de l'école sera toujours affiché dans le petit 👁️ $ecole_depense = isset($totals['School']) ? $totals['School'] : 0; $reste_ecole = max(0, $budget_school - $ecole_depense); if ($reste_ecole > 0) { - $reste_a_venir += $reste_ecole; - $pending_charges[] = ['name' => 'École / Garde (Reste estimé)', 'amount' => $reste_ecole]; + $reste_a_venir_calc += $reste_ecole; + $pending_charges[] = ['name' => 'École (Reste estimé)', 'amount' => $reste_ecole]; } -$solde_theorique = ($solde_initial + $total_rentrees) - $depenses_reelles - $reste_a_venir; +// F. Calculs des KPIs finaux +$rentrees_salaires_reels = $totals['Income'] ?? 0; +$rentrees_autres = $total_rentrees - $rentrees_salaires_reels; +$salaires_retenus = max($rentrees_salaires_reels, $budget_income_prevu); + +// Capacité Max : Solde fin de mois précédent + Salaire retenu + Autres rentrées +$capacite_max_calc = $solde_initial + $salaires_retenus + $rentrees_autres; +$solde_theorique_calc = ($solde_initial + $total_rentrees) - $depenses_reelles - $reste_a_venir_calc; + +if ($isClosed) { + $solde_actuel = $monthState['solde_actuel']; + $capacite_max = $monthState['capacite_max']; + $solde_theorique = $monthState['solde_theorique']; + $reste_a_venir = $monthState['reste_a_venir']; +} else { + $solde_actuel = $snapshot['amount']; + $capacite_max = $capacite_max_calc; + $solde_theorique = $solde_theorique_calc; + $reste_a_venir = $reste_a_venir_calc; +} $solde_net = max(0, $solde_actuel - $reste_a_venir); $charges_visibles = min($solde_actuel, $reste_a_venir); - $max_scale = max($solde_actuel, $solde_theorique, $capacite_max, 1) * 1.1; $pct_net = min(100, max(0, ($solde_net / $max_scale) * 100)); @@ -373,41 +326,53 @@ $pct_actuel = min(100, max(0, ($solde_actuel / $max_scale) * 100)); $pct_theorique = min(100, max(0, ($solde_theorique / $max_scale) * 100)); function getDisplayLogic($spent, $bg, $type) { - if ($type === 'credit') { - $pct = ($bg > 0) ? min(100, ($spent / $bg) * 100) : ($spent > 0 ? 100 : 0); - $isOver = false; - if ($bg > 0) { $text = number_format(ceil($spent), 0, ',', ' ') . ' / ' . number_format(ceil($bg), 0, ',', ' ') . ' €'; } - else { $text = number_format(ceil($spent), 0, ',', ' ') . ' €'; } - } else { - $pct = ($bg > 0) ? min(100, ($spent / $bg) * 100) : ($spent > 0 ? 100 : 0); - $isOver = ($spent > $bg && $bg > 0); - if ($bg > 0) { $text = number_format(ceil($spent), 0, ',', ' ') . ' / ' . number_format(ceil($bg), 0, ',', ' ') . ' €'; } - else { $text = number_format(ceil($spent), 0, ',', ' ') . ' €'; } - } + $pct = ($bg > 0) ? min(100, ($spent / $bg) * 100) : ($spent > 0 ? 100 : 0); + $isOver = ($type === 'debit' && $spent > $bg && $bg > 0); + $text = ($bg > 0) ? number_format(ceil($spent), 0, ',', ' ') . ' / ' . number_format(ceil($bg), 0, ',', ' ') . ' €' : number_format(ceil($spent), 0, ',', ' ') . ' €'; return ['pct' => $pct, 'isOver' => $isOver, 'text' => $text]; } $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)); +$monthName = $moisFr[(int)$viewM] . ' ' . $viewY; ?>