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 { // 🔥 FIX : On récupère les DEUX années scolaires qui chevauchent l'année calendaire (ex: 2025-2026 ET 2026-2027) $prevYear = $this->year - 1; $nextYear = $this->year + 1; $where = "(annee_scolaire='{$prevYear}-{$this->year}' OR annee_scolaire='{$this->year}-{$nextYear}') AND zones LIKE '%Zone {$zone}%'"; $url = "https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=" . urlencode($where) . "&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)); // Sécurité : si la vacance commence le vendredi soir, on la compte à partir du samedi 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 = (int)date('t', strtotime(sprintf("%04d-%02d-01", $this->year, $this->month))); // 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) ]; } }