From c3eae9e1d4f155baca678b4926ea32a3b1045886 Mon Sep 17 00:00:00 2001 From: "fefe.clochette" Date: Tue, 15 Sep 2026 19:18:38 +0200 Subject: [PATCH] estimation frais --- docker/schema_family.sql | 2 + includes/lang/ca.php | 15 + includes/lang/en.php | 15 + includes/lang/fr.php | 15 + migrate_meals.php => migrate.php | 25 +- .../includes/DynamicBudgetCalculator.php | 260 ++++++++++++++++++ modules/budget/includes/api/settings.php | 42 ++- modules/budget/views/recap.php | 21 ++ modules/budget/views/settings_modal.php | 96 ++++++- modules/budget/views/suivi.php | 29 +- 10 files changed, 503 insertions(+), 17 deletions(-) rename migrate_meals.php => migrate.php (61%) create mode 100644 modules/budget/includes/DynamicBudgetCalculator.php diff --git a/docker/schema_family.sql b/docker/schema_family.sql index 72e8c93..687c1ec 100644 --- a/docker/schema_family.sql +++ b/docker/schema_family.sql @@ -155,6 +155,8 @@ CREATE TABLE IF NOT EXISTS pf_budget_items ( type VARCHAR(50), payment_day INT DEFAULT NULL, is_estimate TINYINT(1) DEFAULT 0, + is_dynamic TINYINT(1) DEFAULT 0, + dynamic_code VARCHAR(50) DEFAULT NULL, reg_month VARCHAR(7) DEFAULT NULL, mapping_keywords TEXT DEFAULT NULL, holiday_id INT DEFAULT NULL, diff --git a/includes/lang/ca.php b/includes/lang/ca.php index 9a74794..e758616 100644 --- a/includes/lang/ca.php +++ b/includes/lang/ca.php @@ -586,6 +586,21 @@ return [ 'bs_currency_label' => 'Divisa de la llar', 'bs_currency_desc' => 'Divisa principal utilitzada per mostrar els vostres comptes i pressupostos.', 'bs_currency_updated' => 'Divisa actualitzada correctament!', + 'bud_settings_dyn_title' => 'Estimacions Dinàmiques (Nens)', + 'bud_dyn_nanny_fixed' => 'Sou fix Cangur (€)', + 'bud_dyn_nanny_daily' => 'Despeses diàries de manteniment (€)', + 'bud_dyn_nanny_aid' => 'Ajuda del govern (€)', + 'bud_dyn_nanny_cesu_avg' => 'Mitjana CESU pagada (€)', + 'bud_dyn_school_meal' => 'Preu del menjador (€)', + 'bud_dyn_school_aftercare' => 'Preu acollida tarda (€)', + 'bud_dyn_school_fullday' => 'Preu acollida dia sencer (Dimecres/Vacances) (€)', + 'bud_dyn_pattern_title' => 'Setmana escolar tipus', + 'bud_audit_month' => 'Mes analitzat: %02d/%d', + 'bud_audit_nanny_abs' => 'Absències %s deduïdes : %dd', + 'bud_audit_child' => '👦 %s :', + 'bud_audit_school_fee' => 'Menjador (%dd) + Acollida (%dd) + Casal (%dd) = %.2f€', + 'bud_audit_sick' => 'Dies de malaltia deduïts : %dd', + 'bud_audit_nanny_full' => '💼 %s : Fix (%.2f€) + Despeses (%dd x %.2f€ = %.2f€) - Ajut (%.2f€) - CESU (%.2f€) = %.2f€', // CSV 'bs_csv_title' => 'Format de la importació CSV', diff --git a/includes/lang/en.php b/includes/lang/en.php index aed66fd..3686f26 100644 --- a/includes/lang/en.php +++ b/includes/lang/en.php @@ -588,6 +588,21 @@ return [ 'bs_currency_label' => 'Household Currency', 'bs_currency_desc' => 'Main currency used for displaying your accounts and budgets.', 'bs_currency_updated' => 'Currency updated successfully!', + 'bud_settings_dyn_title' => 'Dynamic Estimates (Children)', + 'bud_dyn_nanny_fixed' => 'Nanny fixed salary (€)', + 'bud_dyn_nanny_daily' => 'Daily maintenance fee (€)', + 'bud_dyn_nanny_aid' => 'Government aid (€)', + 'bud_dyn_nanny_cesu_avg' => 'Average CESU paid (€)', + 'bud_dyn_school_meal' => 'School meal price (€)', + 'bud_dyn_school_aftercare' => 'After-school care price (€)', + 'bud_dyn_school_fullday' => 'Full day care (Wednesdays/Holidays) (€)', + 'bud_dyn_pattern_title' => 'Standard school week', + 'bud_audit_month' => 'Analyzed month: %02d/%d', + 'bud_audit_nanny_abs' => '%s absences deducted : %dd', + 'bud_audit_child' => '👦 %s :', + 'bud_audit_school_fee' => 'Canteen (%dd) + Aftercare (%dd) + Center (%dd) = %.2f€', + 'bud_audit_sick' => 'Sick days deducted : %dd', + 'bud_audit_nanny_full' => '💼 %s : Fixed (%.2f€) + Fees (%dd x %.2f€ = %.2f€) - Aid (%.2f€) - CESU (%.2f€) = %.2f€', // CSV Mapping (Budget) 'bs_csv_title' => 'CSV Import Format', diff --git a/includes/lang/fr.php b/includes/lang/fr.php index 83bf913..b53d154 100644 --- a/includes/lang/fr.php +++ b/includes/lang/fr.php @@ -588,6 +588,21 @@ return [ 'bs_currency_label' => 'Devise du Foyer', 'bs_currency_desc' => 'Devise principale utilisée pour l\'affichage de vos comptes et budgets.', 'bs_currency_updated' => 'Devise mise à jour avec succès !', + 'bud_settings_dyn_title' => 'Estimations Dynamiques (Enfants)', + 'bud_dyn_nanny_fixed' => 'Salaire fixe Assistante Maternelle (€)', + 'bud_dyn_nanny_daily' => 'Frais d\'entretien journaliers (€)', + 'bud_dyn_nanny_aid' => 'Aide de l\'État (CMG) (€)', + 'bud_dyn_nanny_cesu_avg' => 'Moyenne CESU versée (€)', + 'bud_dyn_school_meal' => 'Prix repas Cantine (€)', + 'bud_dyn_school_aftercare' => 'Prix Accueil Périscolaire soir (€)', + 'bud_dyn_school_fullday' => 'Prix Accueil Journée (Mercredi/Vacances) (€)', + 'bud_dyn_pattern_title' => 'Semaine type d\'école', + 'bud_audit_month' => 'Mois analysé : %02d/%d', + 'bud_audit_nanny_abs' => 'Absences %s déduites : %dj', + 'bud_audit_child' => '👦 %s :', + 'bud_audit_school_fee' => 'Cantine (%dj) + Garderie (%dj) + Centre (%dj) = %.2f€', + 'bud_audit_sick' => 'Jours maladie déduits : %dj', + 'bud_audit_nanny_full' => '💼 %s : Fixe (%.2f€) + Frais (%dj x %.2f€ = %.2f€) - Aide (%.2f€) - CESU (%.2f€) = %.2f€', // CSV Mapping (Budget) 'bs_csv_title' => 'Format de l\'import CSV', diff --git a/migrate_meals.php b/migrate.php similarity index 61% rename from migrate_meals.php rename to migrate.php index e4cace9..d8fa4f1 100644 --- a/migrate_meals.php +++ b/migrate.php @@ -1,5 +1,5 @@ 🎉 Migration terminée avec succès."; + // À ajouter dans migrate.php +echo "Migration : Ajout des colonnes dynamiques au Budget...\n"; +$stmtFam = $meta_pdo->query("SELECT id, db_name FROM families"); +$families = $stmtFam->fetchAll(PDO::FETCH_ASSOC); + +foreach ($families as $fam) { + $dbName = $fam['db_name']; + try { + $famPdo = new PDO("mysql:host=$meta_host;dbname=$dbName;charset=utf8mb4", $meta_user, $meta_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + + // Vérification si la colonne n'existe pas déjà + $checkCol = $famPdo->query("SHOW COLUMNS FROM pf_budget_items LIKE 'is_dynamic'"); + if ($checkCol->rowCount() == 0) { + $famPdo->exec("ALTER TABLE pf_budget_items + ADD COLUMN is_dynamic TINYINT(1) DEFAULT 0 AFTER is_estimate, + ADD COLUMN dynamic_code VARCHAR(50) DEFAULT NULL AFTER is_dynamic"); + echo "Famille {$fam['id']} ($dbName) : Colonnes is_dynamic et dynamic_code ajoutées.\n"; + } + } catch (\PDOException $e) { + echo "Erreur sur la famille {$fam['id']} ($dbName) : " . $e->getMessage() . "\n"; + } +} + } catch (Exception $e) { die("❌ Erreur lors de la migration : " . $e->getMessage()); } \ No newline at end of file diff --git a/modules/budget/includes/DynamicBudgetCalculator.php b/modules/budget/includes/DynamicBudgetCalculator.php new file mode 100644 index 0000000..77e1a0a --- /dev/null +++ b/modules/budget/includes/DynamicBudgetCalculator.php @@ -0,0 +1,260 @@ +pdo = $pdo; + $this->year = (int)$year; + $this->month = str_pad((string)$month, 2, '0', STR_PAD_LEFT); + + $this->loadSettings(); + $this->loadPeople(); + $this->loadEvents(); + } + + private function loadSettings(): void { + $stmt = $this->pdo->query("SELECT setting_key, setting_value FROM pf_settings WHERE module = 'budget'"); + $this->settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); + $this->foyerSettings = $this->pdo->query("SELECT * FROM pf_foyer_settings LIMIT 1")->fetch(PDO::FETCH_ASSOC); + } + + private function getSetting(string $key, float|int $default = 0): float { + return isset($this->settings[$key]) ? (float)$this->settings[$key] : (float)$default; + } + + private function loadPeople(): void { + // 1. Chargement des enfants sans doublons + $stmt = $this->pdo->query("SELECT id, name, care_modes FROM pf_people WHERE role IN ('enfant', 'child') AND is_active = 1"); + $rawKids = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $this->kids = []; + $seenNames = []; + + foreach ($rawKids as $k) { + $nameKey = mb_strtolower(trim($k['name'])); + if (!isset($seenNames[$nameKey])) { + $this->kids[] = $k; + $seenNames[$nameKey] = true; + } + } + + // 2. Chargement dynamique de l'intervenant (ex: Carole) + $stmtHelper = $this->pdo->query("SELECT name FROM pf_people WHERE role IN ('helper', 'nounou') AND is_active = 1 LIMIT 1"); + $helper = $stmtHelper->fetchColumn(); + if ($helper) { + $this->helperName = mb_strtoupper($helper); + } + } + + private function loadEvents(): void { + $startDate = "{$this->year}-{$this->month}-01"; + $endDate = date('Y-m-t', strtotime($startDate)); + $stmt = $this->pdo->prepare("SELECT event_date, event_type, person_id FROM pf_events WHERE event_date BETWEEN ? AND ?"); + $stmt->execute([$startDate, $endDate]); + $events = $stmt->fetchAll(PDO::FETCH_ASSOC); + + foreach ($events as $e) { + $this->calendarEvents[$e['event_date']][] = $e; + } + } + + private function getOffDays(): array { + $cacheKey = "offdays_{$this->year}"; + $stmt = $this->pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'system_cache' AND reference_id = ?"); + $stmt->execute([$cacheKey]); + $cached = $stmt->fetchColumn(); + + if ($cached) return json_decode($cached, true); + + $offDays = ['feries' => [], 'vacances' => []]; + + try { + $jsonFeries = @file_get_contents("https://calendrier.api.gouv.fr/jours-feries/metropole/{$this->year}.json"); + if ($jsonFeries) $offDays['feries'] = array_keys(json_decode($jsonFeries, true)); + } catch (\Throwable $e) {} + + $zone = $this->foyerSettings['zone_scolaire'] ?? 'C'; + try { + $url = "https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=annee_scolaire='{$this->year}-" . ($this->year + 1) . "' AND zones LIKE '%Zone {$zone}%'&limit=100"; + $jsonVacances = @file_get_contents($url); + if ($jsonVacances) { + $data = json_decode($jsonVacances, true); + if (!empty($data['results'])) { + foreach ($data['results'] as $r) { + $start = new DateTime(substr($r['start_date'], 0, 10)); + $end = new DateTime(substr($r['end_date'], 0, 10)); + if ($start->format('N') == 5) $start->modify('+1 day'); + while ($start < $end) { + $offDays['vacances'][] = $start->format('Y-m-d'); + $start->modify('+1 day'); + } + } + } + } + } catch (\Throwable $e) {} + + $this->pdo->prepare("INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('system_cache', ?, ?) ON DUPLICATE KEY UPDATE content = VALUES(content)") + ->execute([$cacheKey, json_encode($offDays)]); + + return $offDays; + } + + public function getEstimate(string $dynamicCode): array { + if ($dynamicCode === 'SCHOOL_ESTIMATE') { + return $this->calculateSchoolEstimate(); + } + return ['amount' => 0.0, 'details' => tr('error_occured')]; + } + + private function calculateSchoolEstimate(): array { + $nannyFixed = $this->getSetting('budget_nanny_fixed', 700); + $nannyDaily = $this->getSetting('budget_nanny_daily', 4); + $nannyAid = $this->getSetting('budget_nanny_aid', 120); + $cesuAvg = $this->getSetting('budget_nanny_cesu_avg', 170); + + $schoolMeal = $this->getSetting('budget_school_meal', 5.62); + $schoolAftercare = $this->getSetting('budget_school_aftercare', 1.97); + $schoolFullday = $this->getSetting('budget_school_fullday', 21.89); + + $offDays = $this->getOffDays(); + + $totalCost = 0; + $details = sprintf(tr('bud_audit_month') . "\n\n", $this->month, $this->year); + + $hasNanny = false; + $nannyOffDays = 0; + $nannyGlobalCost = $nannyFixed - $nannyAid - $cesuAvg; + + $kidsStats = []; + + // 1. Initialisation stricte + foreach ($this->kids as $k) { + $modes = json_decode($k['care_modes'] ?? '[]', true) ?: []; + $isNanny = count(array_intersect(array_map('strtolower', $modes), ['nounou', 'kangur'])) > 0; + if ($isNanny) $hasNanny = true; + + $kidsStats[$k['id']] = [ + 'name' => $k['name'], 'isNanny' => $isNanny, + 'nannyDays' => 0, 'sick' => 0, + 'schoolMeal' => 0, 'schoolAftercare' => 0, 'center' => 0 + ]; + } + + $daysInMonth = cal_days_in_month(CAL_GREGORIAN, (int)$this->month, $this->year); + + // 2. Itération SANS pointeurs de référence + for ($d = 1; $d <= $daysInMonth; $d++) { + $dateStr = sprintf("%04d-%02d-%02d", $this->year, $this->month, $d); + $dayOfWeek = date('N', strtotime($dateStr)); + + if ($dayOfWeek >= 6) continue; + if (in_array($dateStr, $offDays['feries'])) continue; + + $isVacances = in_array($dateStr, $offDays['vacances']); + $events = $this->calendarEvents[$dateStr] ?? []; + + $nannyOffToday = false; + foreach ($events as $e) { + if (in_array($e['event_type'], ['HELPER_OFF', 'HELPER_EXTRA'])) $nannyOffToday = true; + } + if ($nannyOffToday) $nannyOffDays++; + + foreach ($this->kids as $kid) { + $kidId = $kid['id']; + + $isSick = false; + $hasCenterEvent = false; + foreach ($events as $e) { + if ($e['event_type'] === 'CHILD_SICK' && $e['person_id'] == $kidId) $isSick = true; + if (strtoupper($e['event_type']) === 'CENTRE' && $e['person_id'] == $kidId) $hasCenterEvent = true; + } + + if ($isSick) { + $kidsStats[$kidId]['sick']++; + continue; + } + + if ($kidsStats[$kidId]['isNanny']) { + if (!$nannyOffToday) $kidsStats[$kidId]['nannyDays']++; + } else { + if ($isVacances) { + if ($hasCenterEvent) $kidsStats[$kidId]['center']++; + } else { + if ($dayOfWeek == 3) { + $kidsStats[$kidId]['center']++; + } else { + $kidsStats[$kidId]['schoolMeal']++; + if (in_array($dayOfWeek, [2, 4])) $kidsStats[$kidId]['schoolAftercare']++; + } + } + } + } + } + + $totalNannyDays = 0; + foreach ($kidsStats as $stat) { + if ($stat['isNanny']) { + $totalNannyDays += $stat['nannyDays']; + } + } + + $totalNannyFees = $totalNannyDays * $nannyDaily; + $nannyGlobalCost = $nannyFixed + $totalNannyFees - $nannyAid - $cesuAvg; + + // 3. Rédaction du Bilan + if ($hasNanny) { + $totalCost += max(0, $nannyGlobalCost); + $details .= sprintf(tr('bud_audit_nanny_full') . "\n", + $this->helperName, $nannyFixed, $totalNannyDays, $nannyDaily, $totalNannyFees, $nannyAid, $cesuAvg, max(0, $nannyGlobalCost)); + + if ($nannyOffDays > 0) { + $details .= sprintf(tr('bud_audit_nanny_abs') . "\n", $this->helperName, $nannyOffDays); + } + $details .= "\n"; + } + + foreach ($kidsStats as $finalStat) { + if ($finalStat['isNanny']) { + // L'enfant est chez la nounou : on ne l'affiche QUE s'il a été malade + if ($finalStat['sick'] > 0) { + $details .= sprintf(tr('bud_audit_child') . "\n", mb_strtoupper($finalStat['name'])); + $details .= sprintf(tr('bud_audit_sick') . "\n", $finalStat['sick']) . "\n\n"; + } + } else { + // L'enfant va à l'école / centre + $details .= sprintf(tr('bud_audit_child') . "\n", mb_strtoupper($finalStat['name'])); + + $costM = $finalStat['schoolMeal'] * $schoolMeal; + $costA = $finalStat['schoolAftercare'] * $schoolAftercare; + $costC = $finalStat['center'] * $schoolFullday; + $kidTotal = $costM + $costA + $costC; + + $totalCost += $kidTotal; + + $details .= sprintf(tr('bud_audit_school_fee') . "\n", + $finalStat['schoolMeal'], $finalStat['schoolAftercare'], $finalStat['center'], $kidTotal); + + if ($finalStat['sick'] > 0) { + $details .= sprintf(tr('bud_audit_sick') . "\n", $finalStat['sick']) . "\n"; + } + $details .= "\n"; + } + } + + return [ + 'amount' => round($totalCost, 2), + 'details' => trim($details) + ]; + } + +} \ No newline at end of file diff --git a/modules/budget/includes/api/settings.php b/modules/budget/includes/api/settings.php index 81b1b76..bb58567 100644 --- a/modules/budget/includes/api/settings.php +++ b/modules/budget/includes/api/settings.php @@ -46,20 +46,25 @@ try { $currencySetting = $foyerData['currency'] ?? '€'; $csvMapping = !empty($foyerData['csv_mapping']) ? json_decode($foyerData['csv_mapping'], true) : null; + // --- NOUVEAU : Récupération des paramètres dynamiques --- + $stmtSettings = $pdo->query("SELECT setting_key, setting_value FROM pf_settings WHERE module = 'budget'"); + $budgetSettings = $stmtSettings->fetchAll(PDO::FETCH_KEY_PAIR); + // 6. Liste des membres (pour assigner un compte bancaire) $people = $pdo->query("SELECT id, name FROM pf_people WHERE is_active = 1 ORDER BY name ASC")->fetchAll(PDO::FETCH_ASSOC); echo json_encode([ 'success' => true, 'data' => [ - 'accounts' => $accounts, - 'categories' => $categories, - 'rules' => $rules, - 'salaries' => $salaries, - 'people' => $people, - 'year' => $currentYear, - 'currency' => $currencySetting, - 'csv_mapping' => $csvMapping + 'accounts' => $accounts, + 'categories' => $categories, + 'rules' => $rules, + 'salaries' => $salaries, + 'people' => $people, + 'year' => $currentYear, + 'currency' => $currencySetting, + 'csv_mapping' => $csvMapping, + 'budget_settings' => $budgetSettings ] ]); exit; @@ -196,6 +201,27 @@ try { exit; } + // --- GESTION DES ESTIMATIONS DYNAMIQUES --- + if ($action === 'save_dynamic_estimates') { + $keys = [ + 'budget_nanny_fixed', 'budget_nanny_daily', 'budget_nanny_aid', 'budget_nanny_cesu_avg', + 'budget_school_meal', 'budget_school_aftercare', 'budget_school_fullday' + ]; + + $pdo->beginTransaction(); + $stmt = $pdo->prepare("INSERT INTO pf_settings (setting_key, setting_value, module) VALUES (?, ?, 'budget') ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), updated_at = NOW()"); + + foreach ($keys as $k) { + if (isset($_POST[$k])) { + $stmt->execute([$k, $_POST[$k]]); + } + } + $pdo->commit(); + + echo json_encode(['success' => true]); + exit; + } + throw new Exception("Action API inconnue."); } catch (\Throwable $e) { diff --git a/modules/budget/views/recap.php b/modules/budget/views/recap.php index a69b24d..1743647 100644 --- a/modules/budget/views/recap.php +++ b/modules/budget/views/recap.php @@ -128,6 +128,23 @@ $totalRevenus = 0; foreach ($items as $item): // 1. Calculs de base $targetAbs = abs((float)$item['amount']); + + // --- 🚀 INTERCEPTION DYNAMIQUE --- + $calcDetails = ""; + if (isset($item['is_dynamic']) && $item['is_dynamic'] == 1 && !empty($item['dynamic_code'])) { + require_once __DIR__ . '/../includes/DynamicBudgetCalculator.php'; + // Facturation à terme échu : on recule d'un mois pour le calcul + $prevDate = date('Y-m-d', strtotime('-1 month', strtotime("$currentYear-$currentMonth-01"))); + $calcYear = date('Y', strtotime($prevDate)); + $calcMonth = date('m', strtotime($prevDate)); + + $calc = new DynamicBudgetCalculator($pdo, $calcYear, $calcMonth); + $estimateData = $calc->getEstimate($item['dynamic_code']); + $targetAbs = $estimateData['amount']; + $calcDetails = $estimateData['details']; + } + // -------------------------------- + $amountToAdd = ($item['type'] === 'Annuel') ? $targetAbs / 12 : $targetAbs; $totalDepenses += $amountToAdd; @@ -185,6 +202,10 @@ foreach ($items as $item): - € + + ℹ️ + + 0.05): ?> diff --git a/modules/budget/views/settings_modal.php b/modules/budget/views/settings_modal.php index 7abbb0c..dcf6628 100644 --- a/modules/budget/views/settings_modal.php +++ b/modules/budget/views/settings_modal.php @@ -1,4 +1,12 @@ ! +
@@ -212,10 +221,59 @@ if (!defined('CURRENCY')) {
- - - - + +
+

👶

+

Configurez ici les tarifs pour le calcul dynamique des frais de garde et de cantine.

+ +
+
Assistante Maternelle
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
École & Centre de loisirs
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ + + + + + \ No newline at end of file diff --git a/modules/budget/views/suivi.php b/modules/budget/views/suivi.php index 512b9ad..8818acb 100644 --- a/modules/budget/views/suivi.php +++ b/modules/budget/views/suivi.php @@ -287,13 +287,30 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) { // 2. C'est une estimation (Variable) if ((int)$item['is_estimate'] === 1) { + $calcDetails = ""; + + // --- 🚀 INTERCEPTION DYNAMIQUE --- + if (isset($item['is_dynamic']) && $item['is_dynamic'] == 1 && !empty($item['dynamic_code'])) { + require_once __DIR__ . '/../includes/DynamicBudgetCalculator.php'; + // Facturation à terme échu : on recule d'un mois + $prevDate = date('Y-m-d', strtotime('-1 month', strtotime("$viewY-$viewM-01"))); + $calcYear = date('Y', strtotime($prevDate)); + $calcMonth = date('m', strtotime($prevDate)); + + $calc = new DynamicBudgetCalculator($pdo, $calcYear, $calcMonth); + $estimateData = $calc->getEstimate($item['dynamic_code']); + $amt = $estimateData['amount']; + $calcDetails = $estimateData['details']; + } + // -------------------------------- + $estimatesList[] = [ 'name' => $name, 'amount' => $amt, - 'categories' => !empty($catCode) ? array_map('trim', explode(',', $catCode)) : [] + 'categories' => !empty($catCode) ? array_map('trim', explode(',', $catCode)) : [], + 'details' => $calcDetails ]; } - // 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)) { @@ -352,11 +369,9 @@ 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) . "€"; } @@ -367,6 +382,12 @@ foreach ($estimatesList as $est) { if ($rem > 0) { $reste_a_venir_calc += $rem; $tooltip = !empty($detailsHover) ? implode(' | ', $detailsHover) : 'Aucune dépense pour le moment'; + + // --- INJECTION DU DÉTAIL DYNAMIQUE --- + if (!empty($est['details'])) { + $tooltip .= "\n\n📊 Détail du calcul :\n" . $est['details']; + } + $pending_charges[] = [ 'name' => 'Reste ' . $est['name'], 'amount' => $rem,