12) { $nextM = 1; $nextY++; } $prevLink = "?tab=suivi&m=$prevM&y=$prevY"; $nextLink = "?tab=suivi&m=$nextM&y=$nextY"; $todayLink = "?tab=suivi"; // 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 ($name && $budget >= 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&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 (LIGNE PAR LIGNE AVEC CHECKBOX) if (isset($_POST['action']) && $_POST['action'] === 'save_import') { $count = 0; // On récupère le mois affiché sur l'écran lors de l'import $viewMonth = $_POST['view_month'] ?? $currentMonth; $viewYear = $_POST['view_year'] ?? $currentYear; // 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)"); foreach ($_POST['new_temp_cats'] as $tempKey => $catData) { $stmtTemp->execute([$currentMonthKey, $catData['name'], $catData['type']]); $tempCatMapping[$tempKey] = $pdo->lastInsertId(); } } $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; 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']); // NOUVEAU : GESTION DU DÉCALAGE SI LA COCHE EST SÉLECTIONNÉE $dateToSave = $line['date']; if (!empty($line['force_current'])) { $day = date('d', strtotime($dateToSave)); // On vérifie que la date existe (ex: pour éviter le 31 Février) if (checkdate((int)$viewMonth, (int)$day, (int)$viewYear)) { $dateToSave = "$viewYear-$viewMonth-$day"; } else { // Sinon, on met au dernier jour du mois visé $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; } // 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; 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) { $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 { $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&m=$currentMonth&y=$currentYear"); 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&m=$currentMonth&y=$currentYear"); 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 = []; // 1. Récupération des IDs de charges fixes déjà payées (Dépenses uniquement) $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 AND amount > 0"); $stmtIds->execute([$currentMonth, $currentYear]); $paidItemIds = $stmtIds->fetchAll(PDO::FETCH_COLUMN); // 2. Récupération libellés (Uniquement les dépenses pour éviter qu'un revenu ne valide une charge) $stmtLabels = $pdo->prepare("SELECT label FROM pf_expenses WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ? AND amount > 0"); $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 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']; $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 ($cat === 'Income') { $totals[$cat] += abs($exp['amount']); } else { if ($exp['amount'] < 0) { $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') { // Pour les revenus : $spent = revenu perçu, $bg = revenu prévu $pct = ($bg > 0) ? min(100, ($spent / $bg) * 100) : ($spent > 0 ? 100 : 0); $isOver = false; // On n'est jamais "dans le rouge" avec les revenus ! if ($bg > 0) { $text = number_format(ceil($spent), 0, ',', ' ') . ' / ' . number_format(ceil($bg), 0, ',', ' ') . ' €'; } else { $text = number_format(ceil($spent), 0, ',', ' ') . ' €'; } } else { // Pour les dépenses $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, ',', ' ') . ' €'; } } return ['pct' => $pct, 'isOver' => $isOver, 'text' => $text]; } // Nom du mois en français (Compatible PHP 8+) $moisFr = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']; $monthName = $moisFr[(int)$currentMonth] . ' ' . $currentYear; ?>

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' : ''); ?>
DateLibelléMontantCatégorie
onchange="checkValidation()">
(déjà importé)
Annuler
$conf): ?>

×

Aucune ligne.
🔗 + - ×

Nouvelle dépense

Mettre à jour le solde

Nouvelle catégorie