diff --git a/modules/budget/views/recap.php b/modules/budget/views/recap.php
index d9bd92a..a69b24d 100644
--- a/modules/budget/views/recap.php
+++ b/modules/budget/views/recap.php
@@ -154,6 +154,20 @@ foreach ($items as $item):
}
}
+ // C. Absorption des orphelines par catégories (Le lien Macro/Micro)
+ if (!empty($item['category'])) {
+ $catCodes = array_map('trim', explode(',', $item['category']));
+ foreach ($catCodes as $cCode) {
+ // Si la catégorie existe dans les orphelines et n'a pas encore été consommée
+ if (!empty($cCode) && isset($realTotalsByCat[$cCode]) && $realTotalsByCat[$cCode] != 0) {
+ $realSum += $realTotalsByCat[$cCode];
+ $hasMatchingExpense = true;
+ // On la vide pour ne pas la compter 2 fois si une autre ligne utilise la même catégorie
+ $realTotalsByCat[$cCode] = 0;
+ }
+ }
+ }
+
// 3. Finalisation des variables d'affichage
$realAbs = abs($realSum);
$isAutoChecked = ($hasMatchingExpense && ($realAbs >= ($targetAbs - 0.10)));
@@ -265,11 +279,12 @@ foreach ($items as $item):
-
Catégorie Cible (La jauge)
-
- -- Choisir --
+
+ Catégorie(s) Cible(s) (Maintenez Ctrl/Cmd pour choix multiple)
+
+
-
+
= htmlspecialchars($c['icon'] . ' ' . $c['label']) ?>
@@ -327,7 +342,11 @@ function editRecapItem(item) {
document.getElementById("item_name").value = data.name;
document.getElementById("item_keywords").value = data.mapping_keywords || '';
document.getElementById("item_amount").value = Math.abs(data.amount);
- document.getElementById("item_category").value = data.category;
+ const selectedCats = data.category ? data.category.split(',') : [];
+ const selectEl = document.getElementById("item_category");
+ Array.from(selectEl.options).forEach(opt => {
+ opt.selected = selectedCats.includes(opt.value);
+ });
document.getElementById("item_type").value = data.type;
document.getElementById("item_is_estimate").value = data.is_estimate;
@@ -341,6 +360,11 @@ document.getElementById('recapForm').addEventListener('submit', async function(e
btn.disabled = true; btn.innerText = '⏳ ...';
const formData = new FormData(this);
+
+ const cats = formData.getAll('category_multi[]');
+ formData.delete('category_multi[]');
+ formData.append('category', cats.join(','));
+
formData.append('ajax', '1');
const url = this.getAttribute('action');
diff --git a/modules/budget/views/suivi.php b/modules/budget/views/suivi.php
index 8b598f9..438695a 100644
--- a/modules/budget/views/suivi.php
+++ b/modules/budget/views/suivi.php
@@ -237,12 +237,15 @@ $fixedChargesList = []; $incomeList = []; $pending_charges = [];
// ============================================================================
// MAPPING DYNAMIQUE DES BUDGETS PRÉVISIONNELS (NOUVELLE LOGIQUE)
// ============================================================================
-$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, mapping_keywords FROM pf_budget_items ORDER BY name ASC");
+// On exclut "SAVINGS" pour s'aligner à 100% avec le recap.php
+$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, mapping_keywords FROM pf_budget_items WHERE category != 'SAVINGS' ORDER BY name ASC");
+$estimatesList = []; // On prépare la liste pour retenir nos estimations multi-catégories
+
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
$absAmount = abs((float)$item['amount']);
$amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount;
$name = trim($item['name']);
- $catCode = $item['category']; // Ex: FIXED, FMCG, INCOME ou income
+ $catCode = $item['category'];
$isIncome = (strtoupper($catCode) === 'INCOME');
@@ -251,18 +254,16 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
$total_income += $amt;
$budget_income_prevu += $amt;
- // Attribution au compte de revenu défini, sinon au premier disponible
if (!empty($catCode) && isset($categoriesConfig[$catCode])) {
$categoriesConfig[$catCode]['budget'] += $amt;
} else {
$incomeCatKey = array_key_first(array_filter($categoriesConfig, fn($c) => $c['db_type'] === 'Income'));
if ($incomeCatKey) $categoriesConfig[$incomeCatKey]['budget'] += $amt;
}
-
} else {
$total_expenses_prevues += $amt;
- // C'est une charge fixe (Mensuel + Is_Estimate = 0)
+ // 1. C'est une charge fixe (Non-variable)
if ($item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
$fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
@@ -279,13 +280,28 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
}
if (!$isPaid) {
$reste_a_venir_calc += $absAmount;
- $pending_charges[] = ['name' => $name, 'amount' => $absAmount];
+ // Ajouté dans le tableau avec un tooltip vide
+ $pending_charges[] = ['name' => $name, 'amount' => $absAmount, 'tooltip' => 'Charge fixe en attente'];
}
}
- // NOUVEAU : Attribution du budget prévisionnel (le Plafond) via la catégorie dynamique
- if (!empty($catCode) && isset($categoriesConfig[$catCode])) {
- $categoriesConfig[$catCode]['budget'] += $amt;
+ // 2. C'est une estimation (Variable)
+ if ((int)$item['is_estimate'] === 1) {
+ $estimatesList[] = [
+ 'name' => $name,
+ 'amount' => $amt,
+ 'categories' => !empty($catCode) ? array_map('trim', explode(',', $catCode)) : []
+ ];
+ }
+
+ // 3. Attribution du budget aux jauges visuelles de la page
+ // Pour ne pas tout casser, on donne tout le plafond visuel à la 1ère catégorie de la liste
+ if (!empty($catCode)) {
+ $catCodesArray = array_map('trim', explode(',', $catCode));
+ $firstCat = $catCodesArray[0];
+ if (isset($categoriesConfig[$firstCat])) {
+ $categoriesConfig[$firstCat]['budget'] += $amt;
+ }
}
}
}
@@ -331,16 +347,32 @@ foreach ($allExpenses as $exp) {
}
}
-// RESTE A VENIR PAR CATEGORIE (Remplace les variables codées en dur)
-foreach ($categoriesConfig as $code => $conf) {
- if ($conf['db_type'] === 'Expense' && $conf['budget'] > 0) {
- $spent = $totals[$code] ?? 0;
- $rem = max(0, $conf['budget'] - $spent);
- if ($rem > 0) {
- $reste_a_venir_calc += $rem;
- $pending_charges[] = ['name' => 'Reste ' . strip_tags($conf['label']), 'amount' => $rem];
+// CALCUL DES RESTES À VENIR BASÉ SUR LES ESTIMATIONS (MACRO)
+foreach ($estimatesList as $est) {
+ $spentForEstimate = 0;
+ $detailsHover = [];
+
+ // On additionne les dépenses de toutes les catégories liées à cette estimation
+ foreach ($est['categories'] as $cCode) {
+ if (!empty($cCode) && isset($totals[$cCode])) {
+ $spentForEstimate += $totals[$cCode];
+ // On prépare le détail pour la petite bulle d'info (hover)
+ if ($totals[$cCode] > 0 && isset($categoriesConfig[$cCode])) {
+ $detailsHover[] = strip_tags($categoriesConfig[$cCode]['label']) . " : " . number_format($totals[$cCode], 0) . "€";
+ }
}
}
+
+ $rem = max(0, $est['amount'] - $spentForEstimate);
+ if ($rem > 0) {
+ $reste_a_venir_calc += $rem;
+ $tooltip = !empty($detailsHover) ? implode(' | ', $detailsHover) : 'Aucune dépense pour le moment';
+ $pending_charges[] = [
+ 'name' => 'Reste ' . $est['name'],
+ 'amount' => $rem,
+ 'tooltip' => $tooltip
+ ];
+ }
}
// F. Calculs des KPIs finaux
@@ -468,7 +500,7 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
= tr('bud_all_paid') ?>
-
+
>
= htmlspecialchars($pc['name']) ?>
= number_format($pc['amount'], 0, ',', ' ') ?> €