estimation frais
Deploy HouseHub / deploy (push) Successful in 1s

This commit is contained in:
FeFeClochette committed 2026-09-15 19:18:38 +02:00
1 parent 4cfef07dc6
commit c3eae9e1d4
10 files changed
+503 -17

No files matched your search

+2
View File
@@ -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,
+15
View File
@@ -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',
+15
View File
@@ -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',
+15
View File
@@ -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',
+24 -1
View File
@@ -1,5 +1,5 @@
<?php
// migrate_meals.php
// migrate.php
require __DIR__ . '/includes/auth.php';
require __DIR__ . '/includes/meta_db.php';
require_login();
@@ -53,6 +53,29 @@ try {
}
echo "<br>🎉 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());
}
@@ -0,0 +1,260 @@
<?php
// modules/budget/includes/DynamicBudgetCalculator.php
class DynamicBudgetCalculator {
private \PDO $pdo;
private int $year;
private string $month;
private array $settings = [];
private array $foyerSettings = [];
private array $kids = [];
private string $helperName = 'Intervenant'; // Valeur par défaut
private array $calendarEvents = [];
public function __construct(\PDO $pdo, int|string $year, int|string $month) {
$this->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)
];
}
}
+34 -8
View File
@@ -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) {
+21
View File
@@ -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):
<td class="cell-amount" style="font-weight:600; padding:15px; color:#1e293b;">
- <?= number_format($targetAbs, 2, ',', ' ') ?> €
<?php if (!empty($calcDetails)): ?>
<span title="<?= htmlspecialchars($calcDetails) ?>" style="cursor:help; margin-left:4px; font-size:0.85rem;">ℹ️</span>
<?php endif; ?>
<?php if ($hasMatchingExpense): ?>
<?php $gap = $realAbs - $targetAbs;
if ($gap > 0.05): ?>
+92 -4
View File
@@ -1,4 +1,12 @@
<?php
/**
* @var bool $hasAccounts
* @var bool $hasCategories
* @var bool $hasSalaries
* @var bool $hasBudgetItems
* @var bool $isBudgetSetupOk
*/
// Sécurité : Ce fichier ne doit pas être appelé directement
if (!defined('CURRENCY')) {
define('CURRENCY', '€');
@@ -26,6 +34,7 @@ if (!defined('CURRENCY')) {
<?php if (!$hasSalaries): ?><span class="alert-dot alert-dot-inline">!</span><?php endif; ?>
</button>
<button class="bs-tab-btn" onclick="switchBsTab('csv', this)"><?= tr('bs_tab_csv') ?></button>
<button class="bs-tab-btn" onclick="switchBsTab('children', this)">👶 <?= tr('bud_settings_dyn_title') ?></button>
</div>
<div class="bs-content">
@@ -212,10 +221,59 @@ if (!defined('CURRENCY')) {
</form>
</div>
</div>
</div>
</div>
</div>
<!-- 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>
<form id="form-dynamic-estimates" onsubmit="saveDynamicEstimates(event)">
<h5 class="bs-section-subtitle bordered">Assistante Maternelle</h5>
<div class="bs-grid-2">
<div>
<label class="pf-label"><?= tr('bud_dyn_nanny_fixed') ?></label>
<input type="number" step="0.01" name="budget_nanny_fixed" id="budget_nanny_fixed" class="pf-input" value="700">
</div>
<div>
<label class="pf-label"><?= tr('bud_dyn_nanny_daily') ?></label>
<input type="number" step="0.01" name="budget_nanny_daily" id="budget_nanny_daily" class="pf-input" value="4">
</div>
<div>
<label class="pf-label"><?= tr('bud_dyn_nanny_aid') ?></label>
<input type="number" step="0.01" name="budget_nanny_aid" id="budget_nanny_aid" class="pf-input" value="120">
</div>
<div>
<label class="pf-label"><?= tr('bud_dyn_nanny_cesu_avg') ?></label>
<input type="number" step="0.01" name="budget_nanny_cesu_avg" id="budget_nanny_cesu_avg" class="pf-input" value="170">
</div>
</div>
<h5 class="bs-section-subtitle bordered" style="margin-top: 20px;">École & Centre de loisirs</h5>
<div class="bs-grid-2">
<div>
<label class="pf-label"><?= tr('bud_dyn_school_meal') ?></label>
<input type="number" step="0.01" name="budget_school_meal" id="budget_school_meal" class="pf-input" value="5.62">
</div>
<div>
<label class="pf-label"><?= tr('bud_dyn_school_aftercare') ?></label>
<input type="number" step="0.01" name="budget_school_aftercare" id="budget_school_aftercare" class="pf-input" value="1.97">
</div>
<div>
<label class="pf-label"><?= tr('bud_dyn_school_fullday') ?></label>
<input type="number" step="0.01" name="budget_school_fullday" id="budget_school_fullday" class="pf-input" value="21.89">
</div>
</div>
<div class="bs-text-right" style="margin-top: 20px;">
<button type="submit" class="btn btn-secondary">💾 <?= tr('btn_save') ?></button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
@@ -273,6 +331,16 @@ async function loadBudgetSettingsData() {
renderSalaries(response.data.salaries, response.data.year);
const currencySelect = document.getElementById('bs-currency-select');
if (currencySelect && response.data.currency) currencySelect.value = response.data.currency;
const bs = response.data.budget_settings || {};
if (document.getElementById('budget_nanny_fixed')) document.getElementById('budget_nanny_fixed').value = bs.budget_nanny_fixed || 700;
if (document.getElementById('budget_nanny_daily')) document.getElementById('budget_nanny_daily').value = bs.budget_nanny_daily || 4;
if (document.getElementById('budget_nanny_aid')) document.getElementById('budget_nanny_aid').value = bs.budget_nanny_aid || 120;
if (document.getElementById('budget_nanny_cesu_avg')) document.getElementById('budget_nanny_cesu_avg').value = bs.budget_nanny_cesu_avg || 170;
if (document.getElementById('budget_school_meal')) document.getElementById('budget_school_meal').value = bs.budget_school_meal || 5.62;
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;
} 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>`;
@@ -569,4 +637,24 @@ function handleCsvUpload(file) {
};
reader.readAsText(file, 'ISO-8859-1');
}
async function saveDynamicEstimates(e) {
e.preventDefault();
const form = e.target;
const btn = form.querySelector('button');
const oldText = btn.innerText;
btn.innerText = '⏳...';
btn.disabled = true;
try {
const fd = new FormData(form);
fd.append('action', 'save_dynamic_estimates');
await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd });
if (typeof showToast === 'function') showToast(window.I18N['fc_settings_updated'] || 'Paramètres enregistrés', 'success');
} catch (err) {
if (typeof showToast === 'function') showToast((window.I18N['error_occured'] || 'Erreur') + " : " + err.message, 'error');
} finally {
btn.innerText = oldText;
btn.disabled = false;
}
}
</script>
+25 -4
View File
@@ -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,