12) { $nextM = 1; $nextY++; } $prevLink = "?tab=suivi&m=$prevM&y=$prevY"; $nextLink = "?tab=suivi&m=$nextM&y=$nextY"; $todayLink = "?tab=suivi"; // ============================================================================ // CONFIGURATION DU CYCLE (Dates personnalisées) // ============================================================================ $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-t', strtotime("$currentYear-$currentMonth-01")); } // 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; $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']); $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; } // 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 = []; $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) // ============================================================================ $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; } $stmt = $pdo->prepare("SELECT * FROM pf_expenses WHERE date_exp >= ? AND date_exp <= ? ORDER BY date_exp DESC"); $stmt->execute([$customStartDate, $customEndDate]); $allExpenses = $stmt->fetchAll(PDO::FETCH_ASSOC); $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; foreach ($allExpenses as $exp) { $cat = $exp['category']; if (!isset($totals[$cat])) $cat = 'Autres'; $val = (float)$exp['amount']; 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; } } 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 $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]; } $solde_theorique = ($solde_initial + $total_rentrees) - $depenses_reelles - $reste_a_venir; $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)); $pct_charges = min(100 - $pct_net, max(0, ($charges_visibles / $max_scale) * 100)); $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, ',', ' ') . ' €'; } } 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)); ?>

Suivi :

Cycle du
Solde bancaire (au )
Solde théorique (Fin de cycle)
Charges à venir
-
0 €
Capacité Max :
Actuel
Théorique
Dispo Net : Provisions :

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.
0): ?>
🔗 + - ×

Nouvelle transaction

Mettre à jour le solde

Configuration du Cycle

Le cycle démarre généralement à la réception de votre paie principale. Le solde initial que vous déclarez deviendra votre Capacité Max de départ pour ce mois.
Solde bancaire exact avant toute nouvelle rentrée d'argent de ce mois.

Nouvelle catégorie