diff --git a/modules/budget/views/suivi.php b/modules/budget/views/suivi.php index fd36c7c..4fb4f4d 100644 --- a/modules/budget/views/suivi.php +++ b/modules/budget/views/suivi.php @@ -26,22 +26,20 @@ if (isset($_GET['del_cat'])) { header("Location: ?tab=suivi"); exit; } -// C. SAUVEGARDE SNAPSHOT BANCAIRE (MODIFIÉ : Remplacement de l'ancien) +// C. SAUVEGARDE SNAPSHOT BANCAIRE if (isset($_POST['action']) && $_POST['action'] === 'save_snapshot') { $date = $_POST['snapshot_date']; $amount = floatval($_POST['snapshot_amount']); - - // On vide la table pour que le nouveau remplace l'ancien - $pdo->query("DELETE FROM pf_bank_snapshots"); - + $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; } -// D. SAUVEGARDE IMPORT CSV +// D. SAUVEGARDE IMPORT CSV (AVEC BUDGET_ITEM_ID) if (isset($_POST['action']) && $_POST['action'] === 'save_import') { $count = 0; + // 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)"); @@ -51,7 +49,8 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') { } } - $stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, category, label, amount, import_ref) VALUES (?, ?, ?, ?, ?)"); + // 2. Insertion avec budget_item_id + $stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, category, label, amount, import_ref, budget_item_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'])) { @@ -59,6 +58,7 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') { 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; if ($is_credit && empty($cat)) continue; if (!$is_credit && empty($cat)) continue; @@ -70,7 +70,7 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') { $finalAmount = $is_credit ? -abs($line['amount']) : abs($line['amount']); try { - $stmtExp->execute([$line['date'], $cat, $line['label'], $finalAmount, $line['ref']]); + $stmtExp->execute([$line['date'], $cat, $line['label'], $finalAmount, $line['ref'], $budgetItemId]); $stmtRule->execute([$line['label'], $cat]); $count++; } catch (Exception $e) { continue; } @@ -80,18 +80,26 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') { header("Location: ?tab=suivi&msg=imported_$count"); exit; } -// E. AJOUT DÉPENSE MANUELLE +// E. AJOUT DÉPENSE MANUELLE (AVEC BUDGET_ITEM_ID) if (isset($_POST['action']) && $_POST['action'] === 'add_expense') { $cat = $_POST['category']; $amount = floatval($_POST['amount']); $date = $_POST['date']; - $label = ($cat === 'School' && !empty($_POST['label_select'])) ? trim($_POST['label_select']) : trim($_POST['label']); + $label = trim($_POST['label']); + $budgetItemId = null; + + if ($cat === 'School' && !empty($_POST['label_select'])) { + $label = trim($_POST['label_select']); + } + elseif ($cat === 'Frais' && !empty($_POST['budget_item_id'])) { + $budgetItemId = (int)$_POST['budget_item_id']; + } 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]); + $pdo->prepare("INSERT INTO pf_expenses (date_exp, category, label, amount, import_ref, budget_item_id) VALUES (?, ?, ?, ?, ?, ?)") + ->execute([$date, $cat, $label, $amount, $uniqueRef, $budgetItemId]); header("Location: ?tab=suivi"); exit; } } @@ -103,7 +111,7 @@ if (isset($_GET['delete_expense'])) { } // ============================================================================ -// 2. CALCUL DES BUDGETS & SNAPSHOT +// 2. CALCUL DES BUDGETS & CHARGES FIXES // ============================================================================ $budget_fmcg = 0; $budget_school = 0; $budget_essence = 0; $budget_frais = 0; @@ -111,49 +119,51 @@ $total_income = 0; $total_expenses_prevues = 0; $reste_a_venir = 0; $today_day = (int)date('j'); -// --- SNAPSHOT & SOLDE THÉORIQUE --- +// Liste des charges pour le selecteur +$fixedChargesList = []; + +// Snapshot $snapshot = ['date' => date('Y-m-d'), 'amount' => 0]; $solde_theorique = 0; - try { - $snapStmt = $pdo->query("SELECT * FROM pf_bank_snapshots ORDER BY snapshot_date DESC, id DESC LIMIT 1"); + $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_theorique = $snapshot['amount']; - -// Calcul du solde théorique : On soustrait les opérations saisies APRÈS la date du snapshot if (!empty($snapshot['date'])) { try { $stmtCalc = $pdo->prepare("SELECT SUM(amount) as total_diff FROM pf_expenses WHERE date_exp > ?"); $stmtCalc->execute([$snapshot['date']]); $resDiff = $stmtCalc->fetch(PDO::FETCH_ASSOC); - if ($resDiff && $resDiff['total_diff'] !== null) { - // amount est positif pour les débits, négatif pour les crédits - // On soustrait donc le total diff. (Ex: 3500 - 50 = 3450) - $solde_theorique -= (float)$resDiff['total_diff']; - } + if ($resDiff && $resDiff['total_diff'] !== null) $solde_theorique -= (float)$resDiff['total_diff']; } catch (Exception $e) {} } - -// --- LECTURE BUDGET PREVISIONNEL --- -$stmt = $pdo->query("SELECT name, amount, type, category, is_estimate, payment_day FROM pf_budget_items"); +// Lecture Budget - Ajout de 'is_checked' à la requête +$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, is_checked FROM pf_budget_items ORDER BY name ASC"); 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']; + $isChecked = (int)$item['is_checked']; + // Remplissage liste Charges Fixes + if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) { + $fixedChargesList[] = $item; + } + if ($item['category'] === 'income') { $total_income += $amt; } else { $total_expenses_prevues += $amt; + // --- NOUVEAU CALCUL RESTE A VENIR --- + // On additionne si c'est une dépense mensuelle NON cochée if ($item['category'] === 'expense' && $item['type'] === 'Mensuel') { - if ($name === 'Estimacio escola' || $pDay > $today_day) { + if ($isChecked === 0) { $reste_a_venir += $rawAmount; } } @@ -173,13 +183,7 @@ 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) { - // On n'ajoute au "budget prévisionnel" que si c'est un débit. - // Si c'est un crédit (réserve), l'argent est censé déjà être ou arriver sur le compte - if ($tc['type'] === 'debit') { - $total_temp_budget += $tc['budget']; - } - } + foreach($tempCats as $tc) if ($tc['type'] === 'debit') $total_temp_budget += $tc['budget']; } catch (Exception $e) {} $budget_autres = $total_income - ($total_expenses_prevues + $total_temp_budget); @@ -193,27 +197,17 @@ $categoriesConfig = [ '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'=>['Netflix', 'Assurance', 'Prêt']], + 'Frais' => ['type'=>'debit', 'label'=>'Charges Fixes', 'budget'=>$budget_frais, 'color'=>'#ef4444', 'suggestions'=>[]], // Suggestions vides car gérées par ID ]; $tempColors = ['#ec4899', '#06b6d4', '#84cc16', '#d946ef', '#f97316']; $colorIdx = 0; - foreach ($tempCats as $tc) { $catKey = 'TEMP_' . $tc['id']; - $catType = isset($tc['type']) ? $tc['type'] : 'debit'; $categoriesConfig[$catKey] = [ - 'type' => $catType, - 'label' => $tc['name'], - 'budget' => $tc['budget'], - 'color' => $tempColors[$colorIdx % count($tempColors)], - 'suggestions' => [], - 'is_temp' => true, - 'id' => $tc['id'] + 'type' => $tc['type'], 'label' => $tc['name'], 'budget' => $tc['budget'], 'color' => $tempColors[$colorIdx++ % count($tempColors)], 'suggestions' => [], 'is_temp' => true, 'id' => $tc['id'] ]; - $colorIdx++; } - $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']]; @@ -226,41 +220,24 @@ $showPreview = false; if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) { $file = $_FILES['csv_file']['tmp_name']; $handle = fopen($file, "r"); - - $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){} - - // CORRECTION ICI : Ajout des paramètres explicitement : fgetcsv(stream, length, separator, enclosure, escape) + $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] ?? ''; - + $rawDebit = $data[8] ?? ''; $rawCredit = $data[9] ?? ''; $amount = 0; $isCredit = 0; - if (!empty(trim($rawCredit))) { - $amount = abs((float)str_replace(',', '.', str_replace(' ', '', $rawCredit))); - $isCredit = 1; - } elseif (!empty(trim($rawDebit))) { - $amount = abs((float)str_replace(',', '.', str_replace(' ', '', $rawDebit))); - } else { - continue; - } + if (!empty(trim($rawCredit))) { $amount = abs((float)str_replace(',', '.', str_replace(' ', '', $rawCredit))); $isCredit = 1; } + elseif (!empty(trim($rawDebit))) { $amount = abs((float)str_replace(',', '.', str_replace(' ', '', $rawDebit))); } + else continue; $dateParts = explode('/', $data[0]); $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).$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); @@ -280,12 +257,8 @@ foreach ($allExpenses as $exp) { $cat = $exp['category']; if (!isset($totals[$cat])) $cat = 'Autres'; - // Si la dépense est un crédit (montant < 0 en bdd), on augmente l'enveloppe - if ($exp['amount'] < 0) { - $categoriesConfig[$cat]['budget'] += abs($exp['amount']); - } else { - $totals[$cat] += $exp['amount']; - } + if ($exp['amount'] < 0) $categoriesConfig[$cat]['budget'] += abs($exp['amount']); + else $totals[$cat] += $exp['amount']; $expensesByCategory[$cat][] = $exp; } @@ -293,7 +266,6 @@ foreach ($allExpenses as $exp) { $globalSpent = array_sum($totals); $globalBudget = array_sum(array_column($categoriesConfig, 'budget')); -// --- FONCTION D'AFFICHAGE --- function getDisplayLogic($spent, $bg, $type) { if ($type === 'credit') { $remaining = $bg - $spent; @@ -340,7 +312,6 @@ function getDisplayLogic($spent, $bg, $type) { Solde théorique au : -
@@ -442,12 +413,21 @@ function getDisplayLogic($spent, $bg, $type) { € - +
+ + + +
@@ -493,7 +473,12 @@ function getDisplayLogic($spent, $bg, $type) { - + + + + 🔗 + + + @@ -539,6 +524,16 @@ function getDisplayLogic($spent, $bg, $type) {
+ +
@@ -617,12 +612,29 @@ function openAddModal(catKey, catLabel) { const blockText = document.getElementById('blockInputText'); const blockSelect = document.getElementById('blockInputSelect'); + const blockFrais = document.getElementById('blockInputFrais'); const inputLabel = document.getElementById('modalLabelInput'); + const selectFrais = document.getElementById('fraisSelect'); + + // Reset visibility + blockText.style.display = 'none'; + blockSelect.style.display = 'none'; + blockFrais.style.display = 'none'; + inputLabel.required = false; + selectFrais.required = false; if (catKey === 'School') { - blockText.style.display = 'none'; blockSelect.style.display = 'block'; inputLabel.required = false; - } else { - blockText.style.display = 'block'; blockSelect.style.display = 'none'; inputLabel.required = true; + blockSelect.style.display = 'block'; + } + else if (catKey === 'Frais') { + blockText.style.display = 'block'; + blockFrais.style.display = 'block'; + selectFrais.required = true; + } + else { + blockText.style.display = 'block'; + inputLabel.required = true; + const list = document.getElementById('modalSuggestions'); list.innerHTML = ''; inputLabel.value = ''; if (suggestions[catKey]) suggestions[catKey].forEach(i => { const op = document.createElement('option'); op.value = i; list.appendChild(op); }); @@ -648,15 +660,41 @@ function confirmNewCat() { } } +function handleLineCatChange(select) { + const row = select.closest('tr'); + const secondarySelect = row.querySelector('.budget-item-select'); + + if (select.value === 'Frais') { + secondarySelect.style.display = 'block'; + } else { + secondarySelect.style.display = 'none'; + secondarySelect.value = ''; + } + checkValidation(); +} + function toggleAll(src) { document.querySelectorAll('.line-checkbox:not([disabled])').forEach(c => c.checked = src.checked); checkValidation(); } + function checkValidation() { const cbs = document.querySelectorAll('.line-checkbox:checked'); let miss = 0; cbs.forEach(cb => { const row = cb.closest('tr'); const isCredit = row.querySelector('.is-credit-flag').value === '1'; - if (row.querySelector('.line-select').value === "") { - if(!isCredit) miss++; + const mainCat = row.querySelector('.line-select').value; + const subCatSelect = row.querySelector('.budget-item-select'); + + let rowValid = true; + + if (mainCat === "") { + if(!isCredit) rowValid = false; + } + else if (mainCat === 'Frais') { + if (subCatSelect.value === "") rowValid = false; + } + + if (!rowValid) { + miss++; row.style.background = isCredit ? '' : '#fff1f2'; } else { row.style.background = ''; @@ -667,11 +705,15 @@ function checkValidation() { const msg = document.getElementById('missingCount'); if(miss>0) { btn.disabled = true; btn.style.opacity=0.5; btn.style.cursor='not-allowed'; - msg.style.display='inline'; msg.innerText=miss+' à définir (débits)'; + msg.style.display='inline'; msg.innerText=miss+' à définir'; } else { btn.disabled = false; btn.style.opacity=1; btn.style.cursor='pointer'; msg.style.display='none'; } } -if(document.getElementById('formMapping')) checkValidation(); + +if(document.getElementById('formMapping')) { + document.querySelectorAll('.line-select').forEach(s => handleLineCatChange(s)); + checkValidation(); +} \ No newline at end of file