diff --git a/includes/lang/ca.php b/includes/lang/ca.php index e758616..1e51c0b 100644 --- a/includes/lang/ca.php +++ b/includes/lang/ca.php @@ -586,7 +586,6 @@ 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 (€)', @@ -600,7 +599,16 @@ return [ '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€', + 'bud_audit_nanny_full' => '💼 %s : Fix (%.2f€) + Despeses (%dd x %.2f€ = %.2f€) - CMG (%.2f€) - %s (%.2f€) = %.2f€', + 'bud_audit_cesu_avg' => 'CESU (Mitj.)', + 'bud_audit_cesu_adj' => 'CESU (Ajustat)', + 'bud_dyn_cesu_overrides' => 'Ajustaments puntuals del CESU', + 'bud_dyn_cesu_add' => 'Afegir un mes', + 'bud_dyn_cesu_empty' => 'Cap ajustament registrat.', + 'bud_settings_dyn_title' => 'Cura i Escola', + 'bud_settings_dyn_desc' => 'Tarifes per al càlcul automatitzat de les despeses.', + 'bud_dyn_sub_nanny' => 'Cangur', + 'bud_dyn_sub_school' => 'Extraescolars i Casal', // CSV 'bs_csv_title' => 'Format de la importació CSV', diff --git a/includes/lang/en.php b/includes/lang/en.php index 3686f26..bfa6593 100644 --- a/includes/lang/en.php +++ b/includes/lang/en.php @@ -588,7 +588,6 @@ 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 (€)', @@ -602,7 +601,16 @@ return [ '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€', + 'bud_audit_nanny_full' => '💼 %s : Fixed (%.2f€) + Fees (%dd x %.2f€ = %.2f€) - Aid (%.2f€) - %s (%.2f€) = %.2f€', + 'bud_audit_cesu_avg' => 'CESU (Avg.)', + 'bud_audit_cesu_adj' => 'CESU (Adjusted)', + 'bud_dyn_cesu_overrides' => 'Specific CESU adjustments', + 'bud_dyn_cesu_add' => 'Add a month', + 'bud_dyn_cesu_empty' => 'No adjustments recorded.', + 'bud_settings_dyn_title' => 'Care & School', + 'bud_settings_dyn_desc' => 'Rates for automated childcare fee calculations.', + 'bud_dyn_sub_nanny' => 'Nanny', + 'bud_dyn_sub_school' => 'Afterschool & Center', // CSV Mapping (Budget) 'bs_csv_title' => 'CSV Import Format', diff --git a/includes/lang/fr.php b/includes/lang/fr.php index b53d154..d12abc2 100644 --- a/includes/lang/fr.php +++ b/includes/lang/fr.php @@ -588,7 +588,6 @@ 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) (€)', @@ -602,7 +601,16 @@ return [ '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€', + 'bud_audit_nanny_full' => '💼 %s : Fixe (%.2f€) + Frais (%dj x %.2f€ = %.2f€) - CMG (%.2f€) - %s (%.2f€) = %.2f€', + 'bud_audit_cesu_avg' => 'CESU (Moy.)', + 'bud_audit_cesu_adj' => 'CESU (Ajusté)', + 'bud_dyn_cesu_overrides' => 'Ajustements ponctuels du CESU', + 'bud_dyn_cesu_add' => 'Ajouter un mois', + 'bud_dyn_cesu_empty' => 'Aucun ajustement enregistré.', + 'bud_settings_dyn_title' => 'Garde & École', + 'bud_settings_dyn_desc' => 'Tarifs pour le calcul automatisé des frais de garde.', + 'bud_dyn_sub_nanny' => 'Nounou', + 'bud_dyn_sub_school' => 'Périscolaire', // CSV Mapping (Budget) 'bs_csv_title' => 'Format de l\'import CSV', diff --git a/modules/budget/budget.css b/modules/budget/budget.css index 5cde7ed..fe9916e 100644 --- a/modules/budget/budget.css +++ b/modules/budget/budget.css @@ -2701,3 +2701,36 @@ body.sum-mode-active .sum-target { text-align: right; white-space: nowrap; } + +/* --- 16. AJUSTEMENTS CESU (JSON OVERRIDES) --- */ +.bs-cesu-wrapper { + margin-top: 20px; + padding: 16px; + background: var(--bg-page); + border-radius: 8px; + border: 1px solid var(--border-light); +} +.bs-cesu-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} +.bs-cesu-list { + display: flex; + flex-direction: column; + gap: 10px; +} +.bs-cesu-row { + display: flex; + gap: 10px; + align-items: center; +} +.bs-cesu-row input[type="month"] { + width: 140px; + flex-shrink: 0; +} +.bs-cesu-row input[type="number"] { + width: 100px; + flex-shrink: 0; +} diff --git a/modules/budget/includes/DynamicBudgetCalculator.php b/modules/budget/includes/DynamicBudgetCalculator.php index 663143f..23fca7e 100644 --- a/modules/budget/includes/DynamicBudgetCalculator.php +++ b/modules/budget/includes/DynamicBudgetCalculator.php @@ -32,6 +32,10 @@ class DynamicBudgetCalculator { return isset($this->settings[$key]) ? (float)$this->settings[$key] : (float)$default; } + private function getSettingString(string $key, string $default = ''): string { + return isset($this->settings[$key]) ? (string)$this->settings[$key] : $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"); @@ -85,10 +89,11 @@ class DynamicBudgetCalculator { $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) + // 🔥 FIX STATE OF THE ART : On cible uniquement la population "Élèves" $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}%'"; + $where = "(annee_scolaire='{$prevYear}-{$this->year}' OR annee_scolaire='{$this->year}-{$nextYear}') AND zones LIKE '%Zone {$zone}%' AND population = 'Élèves'"; + $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); @@ -127,7 +132,16 @@ class DynamicBudgetCalculator { $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); + + $cesuAvg = $this->getSetting('budget_nanny_cesu_avg', 170); + $overridesJson = $this->getSettingString('budget_cesu_overrides', '{}'); + $cesuOverrides = json_decode($overridesJson, true) ?: []; + + $monthKey = sprintf("%04d-%02d", $this->year, $this->month); + $isCesuOverride = isset($cesuOverrides[$monthKey]); + $cesuApplied = $isCesuOverride ? (float)$cesuOverrides[$monthKey] : $cesuAvg; + $cesuLabelText = $isCesuOverride ? tr('bud_audit_cesu_adj') : tr('bud_audit_cesu_avg'); + // ------------------------------------ $schoolMeal = $this->getSetting('budget_school_meal', 5.62); $schoolAftercare = $this->getSetting('budget_school_aftercare', 1.97); @@ -168,6 +182,8 @@ class DynamicBudgetCalculator { if (in_array($dateStr, $offDays['feries'])) continue; $isVacances = in_array($dateStr, $offDays['vacances']); + + $events = $this->calendarEvents[$dateStr] ?? []; $nannyOffToday = false; @@ -216,13 +232,15 @@ class DynamicBudgetCalculator { } $totalNannyFees = $totalNannyDays * $nannyDaily; - $nannyGlobalCost = $nannyFixed + $totalNannyFees - $nannyAid - $cesuAvg; + // On soustrait $cesuApplied au lieu de $cesuAvg + $nannyGlobalCost = $nannyFixed + $totalNannyFees - $nannyAid - $cesuApplied; // 3. Rédaction du Bilan if ($hasNanny) { $totalCost += max(0, $nannyGlobalCost); + // On passe $cesuLabelText au sprintf $details .= sprintf(tr('bud_audit_nanny_full') . "\n", - $this->helperName, $nannyFixed, $totalNannyDays, $nannyDaily, $totalNannyFees, $nannyAid, $cesuAvg, max(0, $nannyGlobalCost)); + $this->helperName, $nannyFixed, $totalNannyDays, $nannyDaily, $totalNannyFees, $nannyAid, $cesuLabelText, $cesuApplied, max(0, $nannyGlobalCost)); if ($nannyOffDays > 0) { $details .= sprintf(tr('bud_audit_nanny_abs') . "\n", $this->helperName, $nannyOffDays); diff --git a/modules/budget/includes/api/settings.php b/modules/budget/includes/api/settings.php index bb58567..38761cf 100644 --- a/modules/budget/includes/api/settings.php +++ b/modules/budget/includes/api/settings.php @@ -205,7 +205,8 @@ try { 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' + 'budget_school_meal', 'budget_school_aftercare', 'budget_school_fullday', + 'budget_cesu_overrides' ]; $pdo->beginTransaction(); diff --git a/modules/budget/views/settings_modal.php b/modules/budget/views/settings_modal.php index dcf6628..c7974e8 100644 --- a/modules/budget/views/settings_modal.php +++ b/modules/budget/views/settings_modal.php @@ -224,10 +224,11 @@ if (!defined('CURRENCY')) {

👶

-

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

+

-
Assistante Maternelle
+ +
@@ -247,7 +248,17 @@ if (!defined('CURRENCY')) {
-
École & Centre de loisirs
+ +
+
+ + +
+
+ +
+ +
@@ -263,8 +274,8 @@ if (!defined('CURRENCY')) {
-
- +
+
@@ -341,6 +352,10 @@ async function loadBudgetSettingsData() { if (document.getElementById('budget_school_aftercare')) document.getElementById('budget_school_aftercare').value = bs.budget_school_aftercare || 1.97; if (document.getElementById('budget_school_fullday')) document.getElementById('budget_school_fullday').value = bs.budget_school_fullday || 21.89; + try { cesuOverrides = JSON.parse(bs.budget_cesu_overrides || '{}'); } + catch (e) { cesuOverrides = {}; } + renderCesuOverrides(); + } catch (err) { console.error("Erreur chargement paramètres :", err); const errorMsg = `
❌ ${tr('error_loading_settings')} : ${err.message}
`; @@ -657,4 +672,79 @@ async function saveDynamicEstimates(e) { btn.disabled = false; } } + +// --- LOGIQUE DES AJUSTEMENTS CESU --- +let cesuOverrides = {}; + +function renderCesuOverrides() { + const list = document.getElementById('cesu-overrides-list'); + const input = document.getElementById('budget_cesu_overrides_input'); + if (!list || !input) return; + + list.innerHTML = ''; + // Tri décroissant pour afficher le mois le plus lointain en haut + const keys = Object.keys(cesuOverrides).sort().reverse(); + + if (keys.length === 0) { + list.innerHTML = `
${window.I18N['bud_dyn_cesu_empty'] || 'Aucun ajustement enregistré.'}
`; + } else { + keys.forEach(k => { + const btnDelText = window.I18N['btn_delete'] || 'Supprimer'; + // UI : Design aéré type "Carte HouseHub" avec des inputs sans bordures agressives + list.innerHTML += ` +
+ + + € + +
+ `; + }); + } + input.value = JSON.stringify(cesuOverrides); +} + +function addCesuOverride() { + const keys = Object.keys(cesuOverrides).sort(); + let nextDate = new Date(); + nextDate.setMonth(nextDate.getMonth() + 1); // Base : le mois prochain + + // S'il y a déjà des mois dans la liste, on prend le plus lointain et on ajoute 1 mois + if (keys.length > 0) { + const lastKey = keys[keys.length - 1]; // ex: "2026-10" + const parts = lastKey.split('-'); + // L'astuce : Le mois en JS commence à 0. Donc en lui passant "10", on pointe déjà sur le mois d'après (Novembre) ! + nextDate = new Date(parts[0], parseInt(parts[1], 10), 1); + } + + // Sécurité anti-boucle infinie + let m = nextDate.getFullYear() + '-' + String(nextDate.getMonth() + 1).padStart(2, '0'); + let safety = 0; + while (cesuOverrides[m] !== undefined && safety < 24) { + nextDate.setMonth(nextDate.getMonth() + 1); + m = nextDate.getFullYear() + '-' + String(nextDate.getMonth() + 1).padStart(2, '0'); + safety++; + } + + cesuOverrides[m] = 0; + renderCesuOverrides(); +} + +function updateCesuOverrideKey(oldK, newK) { + if (oldK !== newK && newK) { + cesuOverrides[newK] = cesuOverrides[oldK]; + delete cesuOverrides[oldK]; + renderCesuOverrides(); + } +} + +function updateCesuOverrideValue(k, val) { + cesuOverrides[k] = parseFloat(val) || 0; + renderCesuOverrides(); +} + +function removeCesuOverride(k) { + delete cesuOverrides[k]; + renderCesuOverrides(); +} \ No newline at end of file diff --git a/modules/family-calendar/family-calendar.js b/modules/family-calendar/family-calendar.js index ea0ac76..93e0336 100644 --- a/modules/family-calendar/family-calendar.js +++ b/modules/family-calendar/family-calendar.js @@ -1078,7 +1078,7 @@ document.addEventListener("DOMContentLoaded", () => { ), this.fetchApi("/modules/family-calendar/includes/api/get-events.php"), this.fetchPublicHolidays(), - this.fetchSchoolHolidays(zone), // 🏖️ NOUVEAU : On appelle l'API + this.fetchSchoolHolidays(zone), this.fetchApi("/modules/family-calendar/includes/api/get-leaves.php"), this.fetchApi( "/modules/family-calendar/includes/api/get-leave-snapshots.php", @@ -1132,8 +1132,9 @@ document.addEventListener("DOMContentLoaded", () => { if (!zone || zone === "Autre") return []; try { const yearStr = `${this.currentSchoolYearStart}-${this.currentSchoolYearStart + 1}`; - // Ton URL qui utilise le LIKE, beaucoup plus robuste ! - const url = `https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=annee_scolaire='${yearStr}' AND zones LIKE '%Zone ${zone}%'&limit=100`; + + const whereClause = `annee_scolaire='${yearStr}' AND zones LIKE '%Zone ${zone}%' AND population='Élèves'`; + const url = `https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=${encodeURIComponent(whereClause)}&limit=100`; const res = await fetch(url); const data = await res.json(); @@ -1196,7 +1197,9 @@ document.addEventListener("DOMContentLoaded", () => { try { const year = this.modalSelectedYear || this.currentSchoolYearStart; const yearStr = `${year}-${year + 1}`; - const url = `https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=annee_scolaire='${yearStr}' AND zones LIKE '%Zone ${zone}%'&limit=100&order_by=start_date`; + + const whereClause = `annee_scolaire='${yearStr}' AND zones LIKE '%Zone ${zone}%' AND population='Élèves'`; + const url = `https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=${encodeURIComponent(whereClause)}&limit=100&order_by=start_date`; const res = await fetch(url); const data = await res.json();