ajust calcul estim
Deploy HouseHub / deploy (push) Successful in 3s

This commit is contained in:
FeFeClochette committed 2026-09-15 21:05:16 +02:00
1 parent a69a8950b3
commit 693b277002
8 files changed
+190 -21

No files matched your search

+10 -2
View File
@@ -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',
+10 -2
View File
@@ -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',
+10 -2
View File
@@ -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',
+33
View File
@@ -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;
}
@@ -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);
+2 -1
View File
@@ -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();
+95 -5
View File
@@ -224,10 +224,11 @@ if (!defined('CURRENCY')) {
<!-- NOUVEAU PANE ENFANTS -->
<div id="pane-children" class="bs-pane">
<h4 class="bs-section-title">👶 <?= tr('bud_settings_dyn_title') ?></h4>
<p class="pf-muted-tiny bs-desc">Configurez ici les tarifs pour le calcul dynamique des frais de garde et de cantine.</p>
<p class="pf-muted-tiny bs-desc"><?= tr('bud_settings_dyn_desc') ?></p>
<form id="form-dynamic-estimates" onsubmit="saveDynamicEstimates(event)">
<h5 class="bs-section-subtitle bordered">Assistante Maternelle</h5>
<h5 class="bs-section-subtitle bordered"><?= tr('bud_dyn_sub_nanny') ?></h5>
<div class="bs-grid-2">
<div>
<label class="pf-label"><?= tr('bud_dyn_nanny_fixed') ?></label>
@@ -247,7 +248,17 @@ if (!defined('CURRENCY')) {
</div>
</div>
<h5 class="bs-section-subtitle bordered" style="margin-top: 20px;">École & Centre de loisirs</h5>
<!-- Ajustements ponctuels du CESU -->
<div style="margin-top: 20px;">
<div class="bs-cesu-header" style="display:flex; justify-content:space-between; margin-bottom:12px;">
<label class="pf-label pf-m-0"><?= tr('bud_dyn_cesu_overrides') ?></label>
<button type="button" class="pf-btn btn-secondary pf-btn-sm" onclick="addCesuOverride()">+ <?= tr('bud_dyn_cesu_add') ?></button>
</div>
<div id="cesu-overrides-list" class="bs-cesu-list"></div>
<input type="hidden" name="budget_cesu_overrides" id="budget_cesu_overrides_input" value="{}">
</div>
<h5 class="bs-section-subtitle bordered pf-mt-md"><?= tr('bud_dyn_sub_school') ?></h5>
<div class="bs-grid-2">
<div>
<label class="pf-label"><?= tr('bud_dyn_school_meal') ?></label>
@@ -263,8 +274,8 @@ if (!defined('CURRENCY')) {
</div>
</div>
<div class="bs-text-right" style="margin-top: 20px;">
<button type="submit" class="btn btn-secondary">💾 <?= tr('btn_save') ?></button>
<div class="bs-text-right pf-mt-md">
<button type="submit" class="pf-btn btn-secondary">💾 <?= tr('btn_save') ?></button>
</div>
</form>
</div>
@@ -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 = `<div class="pf-alert pf-alert--error bs-input-mb">❌ ${tr('error_loading_settings')} : ${err.message}</div>`;
@@ -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 = `<div class="pf-empty-dashed" style="margin:0; padding:12px;"><em class="pf-muted-tiny">${window.I18N['bud_dyn_cesu_empty'] || 'Aucun ajustement enregistré.'}</em></div>`;
} 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 += `
<div style="display: flex; gap: 10px; align-items: center; background: var(--bg-panel); padding: 8px 12px; border-radius: 8px; border: 1px solid var(--border-light); box-shadow: var(--shadow-sm);">
<input type="month" class="pf-input" style="width: 150px; margin: 0; padding: 6px 10px;" value="${k}" onchange="updateCesuOverrideKey('${k}', this.value)">
<input type="number" step="0.01" class="pf-input" style="flex: 1; margin: 0; padding: 6px 10px; text-align: right;" value="${cesuOverrides[k]}" onchange="updateCesuOverrideValue('${k}', this.value)">
<span style="color: var(--text-muted); font-weight: 600; margin-right: 5px;">€</span>
<button type="button" class="btn-icon-action delete" style="margin: 0; background: var(--bg-danger-soft); width: 32px; height: 32px;" onclick="removeCesuOverride('${k}')" title="${btnDelText}">🗑️</button>
</div>
`;
});
}
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();
}
</script>
+7 -4
View File
@@ -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();