= 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; } } // 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; } // 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"); exit; } // D. SAUVEGARDE IMPORT CSV (AVEC BUDGET_ITEM_ID ET HOLIDAY_ID) if (isset($_POST['action']) && $_POST['action'] === 'save_import') { $count = 0; // 1. Catégories temporaires (Inchangé) $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(); } } // 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)"); 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; // NOUVEAU if ($is_credit && empty($cat)) continue; if (!$is_credit && empty($cat)) continue; if (strpos($cat, 'NEW_TEMP_') === 0 && isset($tempCatMapping[$cat])) { $cat = 'TEMP_' . $tempCatMapping[$cat]; } $finalAmount = $is_credit ? -abs($line['amount']) : abs($line['amount']); try { $stmtExp->execute([$line['date'], $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; } // 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']; $label = trim($_POST['label']); $budgetItemId = null; $holidayId = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null; // NOUVEAU 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; $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; } } // 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; } // 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 = []; $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"); $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) = ?"); $stmtLabels->execute([$currentMonth, $currentYear]); $realExpensesLabels = $stmtLabels->fetchAll(PDO::FETCH_COLUMN); // Snapshot $snapshot = ['date' => date('Y-m-d'), 'amount' => 0]; $solde_theorique = 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_theorique = $snapshot['amount']; 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) $solde_theorique -= (float)$resDiff['total_diff']; } catch (Exception $e) {} } // Lecture Budget $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']; $amt = ($item['type'] === 'Annuel') ? $rawAmount / 12 : $rawAmount; $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[] = $item; } if ($item['category'] === 'income') { $incomeList[] = $item; $total_income += $amt; $budget_income_prevu += $amt; } else { $total_expenses_prevues += $amt; // Calcul intelligent Reste à venir 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 += $rawAmount; } 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; } } } // 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 += $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']], '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' => $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 & IMPORT // ============================================================================ $csvData = []; $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){} fgetcsv($handle, 1000, ";", "\"", "\\"); while (($data = fgetcsv($handle, 1000, ";", "\"", "\\")) !== FALSE) { $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; $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); $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]); $allExpenses = $stmt->fetchAll(PDO::FETCH_ASSOC); $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])) $cat = 'Autres'; if ($exp['amount'] < 0) { if ($cat === 'Income') $totals[$cat] += abs($exp['amount']); else $categoriesConfig[$cat]['budget'] += abs($exp['amount']); } else { $totals[$cat] += $exp['amount']; } $expensesByCategory[$cat][] = $exp; } $globalSpent = array_sum($totals); $globalBudget = array_sum(array_column($categoriesConfig, 'budget')); function getDisplayLogic($spent, $bg, $type) { if ($type === 'credit') { $pct = ($bg > 0) ? max(0, min(100, ($spent / $bg) * 100)) : 0; $isOver = false; $text = number_format(ceil($spent), 0, ',', ' ') . ' / ' . number_format(ceil($bg), 0, ',', ' ') . ' €'; } else { $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 { $text = number_format(ceil($spent), 0, ',', ' ') . ' €'; } } return ['pct' => $pct, 'isOver' => $isOver, 'text' => $text]; } ?>

Suivi :

Charges à venir :
Solde au :
Solde théorique au :
Total Mouvements
$conf): $logic = getDisplayLogic($totals[$key], $conf['budget'], $conf['type']); $barCol = ($key === 'Income') ? '#10b981' : ($logic['isOver'] ? '#ef4444' : $conf['color']); ?>

Valider l'importation

$row): $dup = $row['is_duplicate']; $isCrd = $row['is_credit']; $dis = $dup?'disabled':''; $bgCol = $dup ? 'opacity:0.5' : (empty($row['cat']) && !$isCrd ? 'background:#fff1f2' : ''); ?>
LibelléMontantCatégorie
onchange="checkValidation()"> (déjà importé)
Annuler
$conf): ?>

×

Aucune ligne.
🔗 + - ×

Nouvelle dépense

Mettre à jour le solde

Nouvelle catégorie