diff --git a/modules/budget/views/suivi.php b/modules/budget/views/suivi.php index fd08bb0..998aa51 100644 --- a/modules/budget/views/suivi.php +++ b/modules/budget/views/suivi.php @@ -1,371 +1,348 @@ query("SELECT name, amount, type, category, is_estimate FROM pf_budget_items"); -$budgetItems = $stmt->fetchAll(PDO::FETCH_ASSOC); - -foreach ($budgetItems as $item) { - $rawAmount = (float)$item['amount']; - $amt = $rawAmount; // Montant lissé pour les totaux globaux - - $name = trim($item['name']); - $type = $item['type']; // 'Mensuel' ou 'Annuel' - $cat = $item['category']; // 'expense' ou 'income' - $is_est = (int)$item['is_estimate']; // 0 ou 1 - - // Gestion du lissage pour le calcul des totaux globaux (Reste à vivre) - if ($type === 'Annuel') { - $amt = $rawAmount / 12; - } - - // 1. Calcul des totaux globaux (Entrées / Sorties) - if ($cat === 'income') { - $total_income += $amt; - } else { - $total_expenses += $amt; - } - - // 2. Mappage des ESTIMATIONS spécifiques (FMCG, School, Essence) - // Ces lignes ont généralement is_estimate = 1 dans ta base - if ($name === 'Estimacio F&B & beauty') { - $budget_fmcg = $amt; - } - elseif ($name === 'Estimacio escola') { - $budget_school = $amt; - } - elseif ($name === 'Estimation gasolina') { - $budget_essence = $amt; - } - - // 3. CALCUL DU BUDGET CHARGES FIXES (Selon tes critères stricts) - // - Pas une estimation - // - Type Mensuel - // - Catégorie Dépense - if ($is_est === 0 && $type === 'Mensuel' && $cat === 'expense') { - $budget_frais += $rawAmount; +// A. AJOUT CATÉGORIE TEMPORAIRE +if (isset($_POST['action']) && $_POST['action'] === 'add_temp_cat') { + $name = trim($_POST['cat_name']); + $budget = floatval($_POST['cat_budget']); + if ($name && $budget >= 0) { + $stmt = $pdo->prepare("INSERT INTO pf_monthly_categories (month_year, name, budget) VALUES (?, ?, ?)"); + $stmt->execute([$currentMonthKey, $name, $budget]); + header("Location: ?tab=suivi"); exit; } } -// Calcul du "Reste à vivre" pour la catégorie Autres -// Formule : Revenus Totaux - Toutes les dépenses lissées -$budget_autres = $total_income - $total_expenses; -if ($budget_autres < 0) $budget_autres = 0; +// B. SUPPRESSION CATÉGORIE TEMPORAIRE +if (isset($_GET['del_cat'])) { + $id = (int)$_GET['del_cat']; + $pdo->prepare("DELETE FROM pf_monthly_categories WHERE id = ?")->execute([$id]); + header("Location: ?tab=suivi"); exit; +} +// C. SAUVEGARDE IMPORT CSV +if (isset($_POST['action']) && $_POST['action'] === 'save_import') { + $count = 0; + $stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, category, label, amount, import_ref) 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 (!empty($line['cat']) && isset($line['import_check'])) { + try { + $stmtExp->execute([$line['date'], $line['cat'], $line['label'], $line['amount'], $line['ref']]); + $stmtRule->execute([$line['label'], $line['cat']]); + $count++; + } catch (Exception $e) { continue; } + } + } + } + header("Location: ?tab=suivi&msg=imported_$count"); exit; +} + +// D. AJOUT DÉPENSE MANUELLE +if (isset($_POST['action']) && $_POST['action'] === 'add_expense') { + $cat = $_POST['category']; + $amount = floatval($_POST['amount']); + $date = $_POST['date']; + + // Logique pour le label : Soit liste fermée (School), soit texte libre + if ($cat === 'School' && !empty($_POST['label_select'])) { + $label = trim($_POST['label_select']); + } else { + $label = trim($_POST['label']); + } + + if ($label && $amount > 0) { + $uniqueRef = "MANUAL_" . uniqid(); + $pdo->prepare("INSERT INTO pf_expenses (date_exp, category, label, amount, import_ref) VALUES (?, ?, ?, ?, ?)") + ->execute([$date, $cat, $label, $amount, $uniqueRef]); + header("Location: ?tab=suivi"); exit; + } +} + +// E. 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; +} // ============================================================================ -// 2. CONFIGURATION DES CATÉGORIES +// 2. CALCUL DES BUDGETS & INDICATEURS +// ============================================================================ + +$budget_fmcg = 0; $budget_school = 0; $budget_essence = 0; $budget_frais = 0; +$total_income = 0; $total_expenses_prevues = 0; +$reste_a_venir = 0; // Somme des frais futurs +$today_day = (int)date('j'); // Jour du mois (1 à 31) + +// 2.1 Récupération Budget Fixe +$stmt = $pdo->query("SELECT name, amount, type, category, is_estimate, payment_day FROM pf_budget_items"); +while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) { + $rawAmount = (float)$item['amount']; + $amt = ($item['type'] === 'Annuel') ? $rawAmount / 12 : $rawAmount; + $name = trim($item['name']); + $pDay = (int)$item['payment_day']; + + if ($item['category'] === 'income') { + $total_income += $amt; + } else { + $total_expenses_prevues += $amt; + + // --- CALCUL DU "RESTE À VENIR" --- + // Conditions : Expense + Mensuel + (Jour > Aujourd'hui OU C'est l'école) + if ($item['category'] === 'expense' && $item['type'] === 'Mensuel') { + // Si c'est l'école, on l'ajoute toujours (selon ta demande) + if ($name === 'Estimacio escola') { + $reste_a_venir += $rawAmount; + } + // Sinon, si c'est une autre dépense avec une date future + elseif ($pDay > $today_day) { + $reste_a_venir += $rawAmount; + } + } + + // --- MAPPING CATÉGORIES --- + 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 += $rawAmount; + } + } +} + +// 2.2 Récupération 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) $total_temp_budget += $tc['budget']; +} catch (Exception $e) {} + +// 2.3 Calcul Reste à vivre +$budget_autres = $total_income - ($total_expenses_prevues + $total_temp_budget); +if ($budget_autres < 0) $budget_autres = 0; + +// ============================================================================ +// 3. CONFIGURATION CATÉGORIES // ============================================================================ $categoriesConfig = [ - 'FMCG' => [ - 'label' => 'Courses (FMCG)', - 'budget' => $budget_fmcg, - 'color' => '#3b82f6', // Bleu - 'suggestions' => ['Action', 'Boucher', 'Boulangerie', 'Carrefour', 'Grand Frais', 'Lidl', 'Zooplus', 'Pharmacie', 'Decathlon'] - ], - 'Essence' => [ - 'label' => 'Essence', - 'budget' => $budget_essence, - 'color' => '#f59e0b', // Orange - 'suggestions' => ['Audi', 'Polo'] - ], - 'School' => [ - 'label' => 'École / Garde', - 'budget' => $budget_school, - 'color' => '#10b981', // Vert - 'suggestions' => ['Pep', 'Pol'] - ], - 'Frais' => [ - 'label' => 'Charges / Frais Fixes', - 'budget' => $budget_frais, // Calculé ci-dessus (Somme des fixes mensuels) - 'color' => '#ef4444', // Rouge - 'suggestions' => ['Prêt', 'Assurance', 'Banque', 'Netflix', 'Spotify', 'Verisure', 'Internet', 'Mobile', 'Eau', 'Elec', 'Cantine'] - ], - 'Autres' => [ - 'label' => 'Autres / Imprévus', - 'budget' => $budget_autres, // Reste à vivre - 'color' => '#64748b', // Gris - 'suggestions' => ['Restaurant', 'Cadeau', 'Maison', 'Sortie', 'Santé non remboursée'] - ], - 'LivretA' => [ - 'label' => 'Epargne / Livret A', - 'budget' => 0, - 'color' => '#8b5cf6', // Violet - 'suggestions' => ['Virement mensuel'] - ] + 'FMCG' => ['label' => 'Courses (FMCG)', 'budget' => $budget_fmcg, 'color' => '#3b82f6', 'suggestions' => ['Action', 'Carrefour', 'Lidl']], + 'Essence' => ['label' => 'Essence', 'budget' => $budget_essence, 'color' => '#f59e0b', 'suggestions' => ['Audi', 'Polo']], + 'School' => ['label' => 'École / Garde', 'budget' => $budget_school, 'color' => '#10b981', 'suggestions' => []], // Liste fermée gérée en JS + 'Frais' => ['label' => 'Charges Fixes', 'budget' => $budget_frais, 'color' => '#ef4444', 'suggestions' => ['Netflix', 'Assurance', 'Prêt']], ]; +// Couleurs temporaires +$tempColors = ['#ec4899', '#06b6d4', '#84cc16', '#d946ef', '#f97316']; +$colorIdx = 0; + +foreach ($tempCats as $tc) { + $catKey = 'TEMP_' . $tc['id']; + $categoriesConfig[$catKey] = [ + 'label' => $tc['name'], + 'budget' => $tc['budget'], + 'color' => $tempColors[$colorIdx % count($tempColors)], + 'suggestions' => [], + 'is_temp' => true, + 'id' => $tc['id'] + ]; + $colorIdx++; +} + +$categoriesConfig['Autres'] = ['label' => 'Autres / Imprévus', 'budget' => $budget_autres, 'color' => '#64748b', 'suggestions' => ['Restaurant', 'Cadeau']]; +$categoriesConfig['LivretA'] = ['label' => 'Epargne', 'budget' => 0, 'color' => '#8b5cf6', 'suggestions' => ['Virement']]; // ============================================================================ -// 3. LOGIQUE D'IMPORTATION CSV (Avec Mémoire) +// 4. DONNÉES & IMPORT // ============================================================================ +// CSV PREVIEW $csvData = []; $showPreview = false; - if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) { $file = $_FILES['csv_file']['tmp_name']; $handle = fopen($file, "r"); - - // Chargement de la mémoire - $rules = []; - try { - $rulesStmt = $pdo->query("SELECT keyword, category FROM pf_import_rules"); - $rules = $rulesStmt->fetchAll(PDO::FETCH_KEY_PAIR); - } catch (Exception $e) { /* Ignore */ } - - // Sauter l'en-tête + $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) { - // Mapping (Date;Libelle;...;Debit) $rawDebit = $data[8] ?? ''; - if (!empty($rawDebit)) { $amount = abs((float)str_replace(',', '.', str_replace(' ', '', $rawDebit))); - $dateParts = explode('/', $data[0]); - if (count($dateParts) == 3) { - $dateSql = $dateParts[2] . '-' . $dateParts[1] . '-' . $dateParts[0]; - } else { - $dateSql = date('Y-m-d'); - } - - $label = trim($data[1]); - if(empty($label)) $label = trim($data[2]); - - // Intelligence + $dateSql = (count($dateParts) == 3) ? $dateParts[2].'-'.$dateParts[1].'-'.$dateParts[0] : date('Y-m-d'); + $label = trim($data[1]) ?: trim($data[2]); + $refCSV = trim($data[3]); + $uniqueKey = !empty($refCSV) ? "REF_".$refCSV : "HASH_".md5($dateSql.$label.number_format($amount, 2)); + $isDuplicate = in_array($uniqueKey, $existingRefs); $suggestedCat = ''; - foreach ($rules as $keyword => $cat) { - if (stripos($label, $keyword) !== false) { - $suggestedCat = $cat; - break; - } - } - - $csvData[] = [ - 'date' => $dateSql, - 'label' => $label, - 'amount' => $amount, - 'cat' => $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]; } } fclose($handle); $showPreview = true; } - -// ============================================================================ -// 4. TRAITEMENT DES ACTIONS (Sauvegardes) -// ============================================================================ - -// A. SAUVEGARDE IMPORT CSV -if (isset($_POST['action']) && $_POST['action'] === 'save_import') { - $count = 0; - $stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, category, label, amount) 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 (!empty($line['cat']) && isset($line['import_check'])) { - $stmtExp->execute([$line['date'], $line['cat'], $line['label'], $line['amount']]); - $stmtRule->execute([$line['label'], $line['cat']]); - $count++; - } - } - } - header("Location: ?tab=suivi&msg=imported_$count"); - exit; -} - -// B. AJOUT MANUEL -if (isset($_POST['action']) && $_POST['action'] === 'add_expense') { - $cat = $_POST['category']; - $label = trim($_POST['label']); - $amount = floatval($_POST['amount']); - $date = $_POST['date']; - - if ($label && $amount > 0 && isset($categoriesConfig[$cat])) { - $stmt = $pdo->prepare("INSERT INTO pf_expenses (date_exp, category, label, amount) VALUES (?, ?, ?, ?)"); - $stmt->execute([$date, $cat, $label, $amount]); - header("Location: ?tab=suivi"); - exit; - } -} - -// C. SUPPRESSION -if (isset($_GET['delete_expense'])) { - $id = (int)$_GET['delete_expense']; - $pdo->prepare("DELETE FROM pf_expenses WHERE id = ?")->execute([$id]); - header("Location: ?tab=suivi"); - exit; -} - - -// ============================================================================ -// 5. RÉCUPÉRATION DES DONNÉES D'AFFICHAGE DU MOIS -// ============================================================================ - -$currentMonth = date('m'); -$currentYear = date('Y'); - -$stmt = $pdo->prepare(" - SELECT * FROM pf_expenses - WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ? - ORDER BY date_exp DESC, id DESC -"); +// DÉPENSES RÉELLES +$currentMonth = date('m'); $currentYear = date('Y'); +$stmt = $pdo->prepare("SELECT * FROM pf_expenses WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ? ORDER BY date_exp DESC"); $stmt->execute([$currentMonth, $currentYear]); $allExpenses = $stmt->fetchAll(PDO::FETCH_ASSOC); -// Calcul des totaux par catégorie $totals = array_fill_keys(array_keys($categoriesConfig), 0); $expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []); foreach ($allExpenses as $exp) { $cat = $exp['category']; - if (isset($totals[$cat])) { - $totals[$cat] += $exp['amount']; - $expensesByCategory[$cat][] = $exp; - } + if (!isset($totals[$cat])) $cat = 'Autres'; + $totals[$cat] += $exp['amount']; + $expensesByCategory[$cat][] = $exp; } $globalSpent = array_sum($totals); $globalBudget = array_sum(array_column($categoriesConfig, 'budget')); ?> + +
-
-

Suivi Mensuel :

+
+
+

Suivi :

+
+ Charges à venir ce mois : +
+
- Total Dépensé - - / € Prévu + + /
-
+
$conf): - $spent = $totals[$key]; - $budget = $conf['budget']; - - if ($budget > 0) { - $percent = ($spent / $budget) * 100; - } else { - $percent = ($spent > 0) ? 100 : 0; - } - - $barColor = ($spent > $budget && $budget > 0) ? '#ef4444' : $conf['color']; - $cssPercent = min(100, $percent); + $spent = $totals[$key]; $bg = $conf['budget']; + $pct = ($bg > 0) ? min(100, ($spent/$bg)*100) : ($spent>0?100:0); + $col = ($spent > $bg && $bg > 0) ? '#ef4444' : $conf['color']; ?>
- / + /
-
+
- $budget && $budget > 0): ?> -
- + € -
-
- +
-