diff --git a/budget.php b/budget.php index e9a72f1..9b25b88 100644 --- a/budget.php +++ b/budget.php @@ -59,12 +59,18 @@ require __DIR__ . '/header.php'; 📊 + + + 🧮 + + +
'Activar/Desactivar la calculadora', 'bud_sav_selection' => 'Selecció:', 'bud_sav_ph_name' => 'Nom del compte', + + + // --- Module Budget : Provisions & Optimisation --- +'budget_provisions_title' => 'Provisions & Optimisation de Trésorerie', +'btn_optimize_cashflow' => 'Optimiser mes Économies', +'add_new_provision' => 'Anticiper une grosse dépense à venir', +'provision_label' => 'Libellé de la dépense', +'provision_placeholder_wood' => 'Ex: Commande de bois, Révision de l\'auto...', +'amount' => 'Montant', +'expected_date' => 'Date prévue', +'btn_add' => 'Ajouter', +'upcoming_provisions_list' => 'Dépenses prévues au calendrier', +'optimization_assistant_title' => 'Assistant d\'Optimisation des Quinzaines', +'error_generic' => 'Une erreur est survenue', +'success_add_provision' => 'Dépense prévisionnelle ajoutée !', +'no_provisions' => 'Aucune dépense prévisionnelle enregistrée pour le moment.', +'btn_delete' => 'Supprimer', +'savings_this_month' => 'Économies globales à placer ce mois-ci', +'calculate' => 'Calculer la stratégie optimale', +'date_past_warning' => 'La date sélectionnée est déjà passée !', +'budget_tab_provisions' => 'Provisions', // JS & Errors 'bud_sav_confirm_delete_month' => 'Estàs segur que vols suprimir totes les dades de %m per a %o?', diff --git a/includes/lang/en.php b/includes/lang/en.php index 8cfb359..ed7391b 100644 --- a/includes/lang/en.php +++ b/includes/lang/en.php @@ -824,6 +824,26 @@ return [ 'bud_prev_target_ph' => 'E.g., 150', 'bud_prev_transfer_to' => 'To', + // --- Module Budget : Provisions & Optimisation --- +'budget_provisions_title' => 'Provisions & Cashflow Optimization', +'btn_optimize_cashflow' => 'Optimize My Savings', +'add_new_provision' => 'Anticipate an upcoming major expense', +'provision_label' => 'Expense description', +'provision_placeholder_wood' => 'E.g., Firewood order, Car service...', +'amount' => 'Amount', +'expected_date' => 'Expected date', +'btn_add' => 'Add', +'upcoming_provisions_list' => 'Scheduled expenses', +'optimization_assistant_title' => 'Fortnightly Interest Optimization Assistant', +'error_generic' => 'An error occurred', +'success_add_provision' => 'Forecasted expense added successfully!', +'no_provisions' => 'No forecasted expenses registered at the moment.', +'btn_delete' => 'Delete', +'savings_this_month' => 'Total savings to allocate this month', +'calculate' => 'Calculate optimal strategy', +'date_past_warning' => 'The selected date has already passed!', +'budget_tab_provisions' => 'Forecasts', + // ========================================== // MODULE: GIFTS (gift-list) // ========================================== diff --git a/includes/lang/fr.php b/includes/lang/fr.php index d611a58..0398ce8 100644 --- a/includes/lang/fr.php +++ b/includes/lang/fr.php @@ -771,6 +771,34 @@ return [ 'bud_sav_sum_mode_title' => 'Activer/Désactiver la calculatrice', 'bud_sav_selection' => 'Sélection :', 'bud_sav_ph_name' => 'Nom du compte', + + + // --- BUDGET : PREVISION --- + + 'budget_provisions_title' => 'Provisions & Optimisation de Trésorerie', +'btn_optimize_cashflow' => 'Optimiser mes Économies', +'add_new_provision' => 'Anticiper une grosse dépense à venir', +'provision_label' => 'Libellé de la dépense', +'provision_placeholder_wood' => 'Ex: Commande bois de chauffage, Révision voiture...', +'amount' => 'Montant', +'expected_date' => 'Date prévue', +'btn_add' => 'Ajouter', +'upcoming_provisions_list' => 'Dépenses prévues au calendrier', +'optimization_assistant_title' => 'Assistant d\'Optimisation des Quinzaines', +'error_generic' => 'Une erreur est survenue', +'success_add_provision' => 'Dépense prévisionnelle ajoutée !', +'no_provisions' => 'Aucune dépense prévisionnelle enregistrée pour le moment.', +'btn_delete' => 'Supprimer', +'savings_this_month' => 'Économies globales à placer ce mois-ci', +'calculate' => 'Calculer la stratégie optimale', +'date_past_warning' => 'La date sélectionnée est déjà passée !', +'budget_tab_provisions' => 'Provisions', +'budget_opti_base' => 'Base', +'budget_opti_debt' => 'Dette compensée', +'budget_opti_no_config' => 'Aucune configuration de salaire trouvée pour cette année.', +'budget_opti_recap_apports' => 'Résumé des apports (Clearing) :', +'budget_opti_total_disp' => 'Total disponible pour l\'optimisation :', +'budget_opti_must_pay' => 'doit verser', // JS & Erreurs diff --git a/migrate.php b/migrate.php index fcdd0e2..8719a13 100644 --- a/migrate.php +++ b/migrate.php @@ -5,45 +5,87 @@ $db_host = getenv('DB_HOST') ?: '127.0.0.1'; $db_user = getenv('DB_USER') ?: 'househub'; $db_pass = getenv('DB_PASS') ?: 'househub_dev'; -echo "

🗺️ Migration : Refonte Modèle Voyages

🎉 Migrations terminées avec succès !

"; + } catch (Exception $e) { - die("Erreur fatale : " . $e->getMessage()); + die("Erreur fatale globale : " . $e->getMessage()); } ?> \ No newline at end of file diff --git a/modules/budget/budget.css b/modules/budget/budget.css index 72f2bda..33fcb4e 100644 --- a/modules/budget/budget.css +++ b/modules/budget/budget.css @@ -1798,3 +1798,121 @@ body.sum-mode-active .sum-target { padding: 12px 10px; } } + +/* ========================================================================== + 14. PROVISIONS & OPTIMISATION (CASHFLOW) + ========================================================================== */ + +/* Formulaire d'ajout */ +.provisions-form-grid { + display: grid; + grid-template-columns: 2fr 1fr 1fr auto; + gap: 15px; + align-items: end; +} +.provisions-form-grid .pf-form-group { + margin-bottom: 0; +} +.provisions-form-grid .pf-btn { + height: 42px; + margin: 0; +} + +/* Tableau des provisions */ +.budget-table-card { + background: var(--bg-panel); + border-radius: 16px; + box-shadow: var(--shadow-sm); + border: 1px solid var(--border-light); + overflow: hidden; + margin-bottom: 30px; +} +.provisions-table th { + text-align: center; +} +.provisions-table th:first-child { + text-align: left; + padding-left: 15px; +} +.provisions-table th:last-child { + text-align: right; + padding-right: 15px; +} +.provisions-table td { + border-bottom: 1px solid var(--border-light); +} +.prov-title-cell { + font-weight: 500; + color: var(--text-main); + padding-left: 15px; +} +.prov-amount-cell { + color: #2563eb; + font-weight: bold; + text-align: center; +} +.prov-date-cell { + color: var(--text-muted); + text-align: center; +} + +/* Modale d'optimisation */ +.provisions-modal-content { + max-width: 600px; + width: 95%; + max-height: 85vh; + overflow-y: auto; +} +.provision-person-card { + background: var(--bg-subtle); + padding: 10px; + border-radius: 8px; + border: 1px solid var(--border-light); + margin-bottom: 10px; +} +.provision-person-header { + display: flex; + justify-content: space-between; + margin-bottom: 6px; +} +.provision-input-group { + display: flex; + align-items: center; + gap: 10px; +} +.input-amount-highlight { + font-weight: bold; + color: #2563eb !important; +} +.input-amount-success { + font-weight: bold; + color: var(--success) !important; + flex: 1; + margin: 0; +} +.currency-symbol { + font-weight: bold; + color: var(--text-muted); +} +.optimization-hint { + text-align: center; + color: var(--text-muted); + font-size: 0.9rem; + margin-top: 15px; +} +.pf-divider { + border: 0; + border-top: 1px solid var(--border-light); + margin: 20px 0; +} +.btn-block { + width: 100%; +} + +/* --- RESPONSIVE --- */ +@media (max-width: 768px) { + .provisions-form-grid { + grid-template-columns: 1fr; + gap: 12px; + } +} diff --git a/modules/budget/includes/api/add-provision.php b/modules/budget/includes/api/add-provision.php new file mode 100644 index 0000000..ff370a6 --- /dev/null +++ b/modules/budget/includes/api/add-provision.php @@ -0,0 +1,30 @@ + false, 'message' => 'Method not allowed']); + exit; +} + +$title = trim($_POST['title'] ?? ''); +$amount = floatval($_POST['amount'] ?? 0); +$expected_date = $_POST['expected_date'] ?? ''; + +if (empty($title) || $amount <= 0 || empty($expected_date)) { + echo json_encode(['success' => false, 'message' => tr('error_generic')]); + exit; +} + +try { + // Grâce au multi-tenant par BDD, $pdo pointe déjà sur la base de la famille connectée + $stmt = $pdo->prepare("INSERT INTO pf_expected_expenses (title, amount, expected_date) VALUES (?, ?, ?)"); + $stmt->execute([$title, $amount, $expected_date]); + + echo json_encode(['success' => true, 'message' => tr('success_add_provision')]); +} catch (PDOException $e) { + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); +} \ No newline at end of file diff --git a/modules/budget/includes/api/delete-provision.php b/modules/budget/includes/api/delete-provision.php new file mode 100644 index 0000000..2c68ac0 --- /dev/null +++ b/modules/budget/includes/api/delete-provision.php @@ -0,0 +1,19 @@ + false, 'message' => 'Invalid ID']); + exit; +} + +try { + $stmt = $pdo->prepare("DELETE FROM pf_expected_expenses WHERE id = ?"); + $stmt->execute([$id]); + echo json_encode(['success' => true]); +} catch (PDOException $e) { + echo json_encode(['success' => false, 'message' => $e->getMessage()]); +} \ No newline at end of file diff --git a/modules/budget/includes/api/get-provisions.php b/modules/budget/includes/api/get-provisions.php new file mode 100644 index 0000000..b578ca4 --- /dev/null +++ b/modules/budget/includes/api/get-provisions.php @@ -0,0 +1,35 @@ +query("SELECT id, title, amount, expected_date FROM pf_expected_expenses WHERE is_paid = 0 ORDER BY expected_date ASC"); + $provisions = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (empty($provisions)) { + $html = '

' . tr('no_provisions') . '

'; + } else { + $html = ''; + $html .= ''; + $html .= ''; + $html .= ''; + + foreach ($provisions as $p) { + $dateFormated = date('d/m/Y', strtotime($p['expected_date'])); + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + $html .= '
' . tr('provision_label') . '' . tr('amount') . '' . tr('expected_date') . 'Actions
' . htmlspecialchars($p['title']) . '' . number_format($p['amount'], 2, ',', ' ') . ' €' . $dateFormated . '
'; + } + + echo json_encode(['success' => true, 'html' => $html]); +} catch (PDOException $e) { + echo json_encode(['success' => false, 'message' => $e->getMessage()]); +} \ No newline at end of file diff --git a/modules/budget/includes/api/optimize-cashflow.php b/modules/budget/includes/api/optimize-cashflow.php new file mode 100644 index 0000000..a6431e1 --- /dev/null +++ b/modules/budget/includes/api/optimize-cashflow.php @@ -0,0 +1,156 @@ +prepare(" + SELECT title, amount, expected_date + FROM pf_expected_expenses + WHERE is_paid = 0 + AND YEAR(expected_date) = ? + AND MONTH(expected_date) = ? + ORDER BY expected_date ASC + "); + $stmtExpenses->execute([$current_year, $current_month]); + $expenses = $stmtExpenses->fetchAll(PDO::FETCH_ASSOC); + + $q1_expenses_total = 0; + $q2_expenses_total = 0; + + $detail_html = ''; + + $total_expenses = $q1_expenses_total + $q2_expenses_total; + + // 2. Récupération des dettes + $stmtDebts = $pdo->query("SELECT payer, SUM(amount) as total_debt FROM pf_advances WHERE is_resolved = 0 GROUP BY payer"); + $debts = $stmtDebts->fetchAll(PDO::FETCH_KEY_PAIR); + + // 3. NOUVELLE LOGIQUE : Clearing intelligent (Compensation partielle ou totale) + $total_base_inputs = array_sum(array_map('floatval', $savings_inputs)); + $surplus_for_clearing = $total_base_inputs - $total_expenses; + $monthly_savings = 0; // Le vrai total qui sera viré après déduction des dettes + + $contributions_html = ''; + $contributions_html .= '

Total net atterrissant sur les comptes : ' . number_format($monthly_savings, 2, ',', ' ') . ' €

'; + + if(empty($expenses)) { + $detail_html = '

🎉 Aucune grosse dépense enregistrée sur ce mois.

'; + } + + // --- MOTEUR DE RÈGLES DES QUINZAINES (Remplacement des ** par ) --- + $instructions = []; + $remaining_to_allocate = $monthly_savings; + + // Règle 1 : Première quinzaine + if ($q1_expenses_total > 0) { + if ($remaining_to_allocate >= $q1_expenses_total) { + $remaining_to_allocate -= $q1_expenses_total; + $instructions[] = "💼 Dès le 1er : Laissez " . number_format($q1_expenses_total, 2, ',', ' ') . " € sur le compte commun pour honorer les dépenses de la 1ère quinzaine."; + } else { + $deficit = $q1_expenses_total - $remaining_to_allocate; + $remaining_to_allocate = 0; + $instructions[] = "💼 Dès le 1er : Gardez l'intégralité des apports sur le compte commun."; + $instructions[] = "⚠️ Retrait requis : Retirez " . number_format($deficit, 2, ',', ' ') . " € depuis le Livret A vers le commun pour couvrir la 1ère quinzaine."; + } + } + + // Règle 2 : Deuxième quinzaine + if ($q2_expenses_total > 0) { + if ($remaining_to_allocate >= $q2_expenses_total) { + $remaining_to_allocate -= $q2_expenses_total; + $instructions[] = "📈 Dès le 1er : Placez " . number_format($q2_expenses_total, 2, ',', ' ') . " € sur le Livret A pour générer des intérêts sur la 1ère quinzaine."; + $instructions[] = "🔄 Le 16 du mois : Transférez ces " . number_format($q2_expenses_total, 2, ',', ' ') . " € sur le compte commun pour payer la dépense."; + } else { + if ($remaining_to_allocate > 0) { + $instructions[] = "📈 Dès le 1er : Placez le reste des apports (" . number_format($remaining_to_allocate, 2, ',', ' ') . " €) sur le Livret A."; + $instructions[] = "🔄 Le 16 du mois : Retirez " . number_format($q2_expenses_total, 2, ',', ' ') . " € global du Livret A pour payer la fin de mois."; + $remaining_to_allocate = 0; + } else { + $instructions[] = "🔄 Le 16 du mois : Retirez " . number_format($q2_expenses_total, 2, ',', ' ') . " € du Livret A vers le commun. Ne le faites pas avant le 16 !"; + } + } + } + + // Solde résiduel long terme + if ($remaining_to_allocate > 0) { + $instructions[] = "💰 Épargne stable : Placez les " . number_format($remaining_to_allocate, 2, ',', ' ') . " € restants sur votre Livret A dès le 1er."; + } + + // --- CONSTITUTION DU RENDU HTML FINAL --- + $html = '
'; + + // Titre de l'état du Clearing simplifié et clair + $html .= '

🤝 Résumé des apports et remboursements internes :

'; + $html .= '
'; + $html .= $contributions_html; + $html .= '
'; + + $html .= '

Analyse des dépenses du mois :

'; + $html .= $detail_html; + + $html .= '
'; + $html .= '

📋 Plan d\'action recommandé :

'; + + foreach ($instructions as $ins) { + $html .= '

' . $ins . '

'; + } + + $html .= '
'; + + echo json_encode(['success' => true, 'html' => $html]); + +} catch (PDOException $e) { + echo json_encode(['success' => false, 'message' => 'Calcul impossible : ' . $e->getMessage()]); +} \ No newline at end of file diff --git a/modules/budget/views/provisions.php b/modules/budget/views/provisions.php new file mode 100644 index 0000000..30a477a --- /dev/null +++ b/modules/budget/views/provisions.php @@ -0,0 +1,265 @@ +query("SELECT id, title, amount, expected_date FROM pf_expected_expenses WHERE is_paid = 0 ORDER BY expected_date ASC"); +$provisions = $stmt->fetchAll(PDO::FETCH_ASSOC); + +// --- RÉCUPÉRATION DES APPORTS ET DETTES (CLEARING) --- +$currentYear = date('Y'); + +// 1. Apports de base (Eco Family) des parents configurés pour l'année +$stmtConfig = $pdo->prepare("SELECT person, eco_family FROM pf_salary_config WHERE year = ?"); +$stmtConfig->execute([$currentYear]); +$configs = $stmtConfig->fetchAll(PDO::FETCH_KEY_PAIR); + +// 2. Dettes non résolues par personne (issues des Avances & Tricount) +$stmtDebts = $pdo->query("SELECT payer, SUM(amount) as total_debt FROM pf_advances WHERE is_resolved = 0 GROUP BY payer"); +$debts = $stmtDebts->fetchAll(PDO::FETCH_KEY_PAIR); + +function formatDate($dateString) { + if (!$dateString) return ''; + return date('d/m/Y', strtotime($dateString)); +} +?> + +
+
+

+ + +
+ +
+

+ +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+ +
+ +
+

+
+ + + + + + + + + + + + + + + + + + + + +
+ + + € + + + + +
+ +
+
+ +
+
+ +
+

🧠

+ +
+ +
+
+ +

+ + + $ecoBase): + $debt = $debts[$person] ?? 0; + ?> +
+ +
+ + +
+
+ + +
+ + +
+ +
+ +
+

+ Vérifiez vos apports théoriques ci-dessus pour calculer la stratégie de répartition (Clearing automatique si aucune dépense). +

+
+
+
+ + \ No newline at end of file