@@ -11,11 +11,10 @@ while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) {
|
||||
|
||||
if ($role === 'parent') {
|
||||
$familyParents[] = $row['name'];
|
||||
} elseif ($role === 'nounou') {
|
||||
} elseif ($role === 'nounou' || $role === 'helper') {
|
||||
// On ignore la nounou dans l'épargne
|
||||
continue;
|
||||
} else {
|
||||
// Fallback : Si c'est 'enfant', 'user', ou vide, ça va dans l'onglet Enfants
|
||||
$familyKids[] = $row['name'];
|
||||
}
|
||||
}
|
||||
@@ -23,8 +22,8 @@ while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) {
|
||||
// Sécurité anti-page blanche si la BDD est mal configurée
|
||||
if (empty($familyParents)) $familyParents = ['Parent 1', 'Parent 2'];
|
||||
|
||||
$requestedOwner = $_GET['owner'] ?? ($familyParents[0] ?? 'Nens');
|
||||
$ownersToDisplay = ($requestedOwner === 'Nens') ? $familyKids : [$requestedOwner];
|
||||
$requestedOwner = $_GET['owner'] ?? ($familyParents[0] ?? 'KIDS');
|
||||
$ownersToDisplay = ($requestedOwner === 'KIDS') ? $familyKids : [$requestedOwner];
|
||||
|
||||
// --- RÉCUPÉRATION CONFIGURATION DES MOIS ---
|
||||
$cycleConfigs = [];
|
||||
@@ -47,23 +46,21 @@ function getMonthName($dateString) {
|
||||
<div class="budget-view">
|
||||
<div class="view-header">
|
||||
<div class="owner-tabs">
|
||||
<!-- Boucle dynamique sur les parents -->
|
||||
<?php foreach ($familyParents as $p): ?>
|
||||
<a href="?tab=epargne&owner=<?= urlencode($p) ?>" class="owner-tab <?= $requestedOwner === $p ? 'active' : '' ?>">
|
||||
<?= htmlspecialchars($p) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<!-- Onglet Enfants -->
|
||||
<?php if (!empty($familyKids)): ?>
|
||||
<a href="?tab=epargne&owner=Nens" class="owner-tab <?= $requestedOwner === 'Nens' ? 'active' : '' ?>">
|
||||
<?= tr('budget_tab_kids') ?? 'Nens 👶' ?>
|
||||
<a href="?tab=epargne&owner=KIDS" class="owner-tab <?= $requestedOwner === 'KIDS' ? 'active' : '' ?>">
|
||||
<?= tr('budget_tab_kids') ?? 'Enfants 👶' ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php foreach ($ownersToDisplay as $currentOwner):
|
||||
<?php foreach ($ownersToDisplay as $index => $currentOwner):
|
||||
$stmt = $pdo->prepare("SELECT month_date, category, amount FROM pf_savings WHERE owner = ? ORDER BY month_date DESC");
|
||||
$stmt->execute([$currentOwner]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
@@ -83,28 +80,29 @@ function getMonthName($dateString) {
|
||||
$months = array_slice($months, 0, 7);
|
||||
sort($allCategories);
|
||||
|
||||
// Définition de la classe couleur selon le propriétaire
|
||||
$ownerTextClass = 'txt-global';
|
||||
// 🔄 CORRECTION ICI : Création d'un nom "safe" sans espace pour les classes CSS
|
||||
$safeOwnerCls = htmlspecialchars(str_replace(' ', '_', $currentOwner), ENT_QUOTES);
|
||||
?>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'Nens' && $currentOwner !== 'Pol') ? '40px' : '0' ?>;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'KIDS' && $index > 0) ? '40px' : '0' ?>;">
|
||||
<div style="flex-grow: 1;">
|
||||
<?php if ($requestedOwner === 'Nens'):
|
||||
$themeClass = 'theme-' . strtolower($currentOwner);
|
||||
<?php if ($requestedOwner === 'KIDS'):
|
||||
$themeClass = 'theme-' . strtolower($safeOwnerCls);
|
||||
?>
|
||||
<h3 class="nens-title <?= $themeClass ?>" style="margin:0; font-size:1.2rem;">
|
||||
<?= $currentOwner ?>
|
||||
<?= htmlspecialchars($currentOwner) ?>
|
||||
</h3>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<?php if (!empty($months)): ?>
|
||||
<button onclick="duplicateLastMonth('<?= $months[0] ?>', '<?= $currentOwner ?>')" class="pf-btn btn-secondary">
|
||||
<button onclick="duplicateLastMonth('<?= $months[0] ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')" class="pf-btn btn-secondary">
|
||||
🔁 <?= tr('bud_sav_add_one_month') ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button onclick="openCustomSavingsModal('<?= $currentOwner ?>')" class="pf-btn">
|
||||
<button onclick="openCustomSavingsModal('<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')" class="pf-btn">
|
||||
+ <?= tr('bud_sav_add_month') ?>
|
||||
</button>
|
||||
</div>
|
||||
@@ -116,7 +114,7 @@ function getMonthName($dateString) {
|
||||
<p><?= sprintf(tr('bud_sav_no_data'), htmlspecialchars($currentOwner)) ?></p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<table class="pf-table savings-table nens-table theme-<?= strtolower($currentOwner) ?>" style="margin-top:0; box-shadow:none; border-radius:16px;">
|
||||
<table class="pf-table savings-table nens-table theme-<?= strtolower($safeOwnerCls) ?>" style="margin-top:0; box-shadow:none; border-radius:16px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sticky-col" style="background:#f8fafc;"><?= tr('bud_sav_post_month') ?></th>
|
||||
@@ -135,11 +133,11 @@ function getMonthName($dateString) {
|
||||
<div class="month-actions" style="justify-content: center; width: 100%;">
|
||||
<button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_edit_modal') ?>"
|
||||
data-json="<?= htmlspecialchars(json_encode($data[$month] ?? []), ENT_QUOTES, 'UTF-8') ?>"
|
||||
onclick='editCustomSavingsMonth("<?= $month ?>", "<?= $currentOwner ?>", JSON.parse(this.getAttribute("data-json")))'>
|
||||
onclick='editCustomSavingsMonth("<?= $month ?>", "<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>", JSON.parse(this.getAttribute("data-json")))'>
|
||||
✏️
|
||||
</button>
|
||||
<button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_delete_month') ?>"
|
||||
onclick="deleteEntireMonth('<?= $month ?>', '<?= $currentOwner ?>')"
|
||||
onclick="deleteEntireMonth('<?= $month ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')"
|
||||
style="color: #ef4444; border-color: #fca5a5; background: #fef2f2;">
|
||||
🗑️
|
||||
</button>
|
||||
@@ -158,11 +156,11 @@ function getMonthName($dateString) {
|
||||
<td class="text-center" style="padding:4px;">
|
||||
<div style="display:flex; align-items:center; justify-content:center; gap:2px;">
|
||||
<input type="number" step="0.01"
|
||||
class="prev-input total-input-<?= $currentOwner ?>-<?= $month ?>"
|
||||
class="prev-input total-input-<?= $safeOwnerCls ?>-<?= $month ?>"
|
||||
style="width: 70px; font-weight:bold; color:#2563eb;"
|
||||
value="<?= $val != 0 ? round($val) : '' ?>"
|
||||
placeholder="0"
|
||||
onchange="updateEpargneCell('<?= $month ?>', 'TOTAL_BANQUE', '<?= $currentOwner ?>', this)">
|
||||
onchange="updateEpargneCell('<?= $month ?>', 'TOTAL_BANQUE', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', this)">
|
||||
<span style="color:#2563eb; font-weight:bold; font-size:0.9rem;">€</span>
|
||||
</div>
|
||||
</td>
|
||||
@@ -178,11 +176,11 @@ function getMonthName($dateString) {
|
||||
<td class="text-center" style="padding:4px;">
|
||||
<div style="display:flex; align-items:center; justify-content:center; gap:2px;">
|
||||
<input type="number" step="0.01"
|
||||
class="prev-input <?= $ownerTextClass ?> cat-input-<?= $currentOwner ?>-<?= $month ?>"
|
||||
class="prev-input <?= $ownerTextClass ?> cat-input-<?= $safeOwnerCls ?>-<?= $month ?>"
|
||||
style="width: 70px;"
|
||||
value="<?= $amount != 0 ? round($amount) : '' ?>"
|
||||
placeholder="-"
|
||||
onchange="updateEpargneCell('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= $currentOwner ?>', this)">
|
||||
onchange="updateEpargneCell('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', this)">
|
||||
</div>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
@@ -197,7 +195,7 @@ function getMonthName($dateString) {
|
||||
foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0);
|
||||
$extra = $total - $sum;
|
||||
?>
|
||||
<td class="text-center font-bold sum-target" id="extra_<?= $currentOwner ?>_<?= $month ?>" style="color: <?= $extra >= 0 ? '#10b981' : '#ef4444' ?>; padding:12px;">
|
||||
<td class="text-center font-bold sum-target" id="extra_<?= $safeOwnerCls ?>_<?= $month ?>" style="color: <?= $extra >= 0 ? '#10b981' : '#ef4444' ?>; padding:12px;">
|
||||
<?= number_format($extra, 0, ',', ' ') ?> €
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
@@ -289,6 +287,9 @@ window.I18N = {
|
||||
'bud_err_delete': <?= json_encode(tr('bud_err_delete')) ?>
|
||||
};
|
||||
|
||||
// Sécurisation de la devise pour la calculatrice
|
||||
const systemCurrency = (typeof window.CONFIG !== 'undefined' && window.CONFIG.CURRENCY) ? window.CONFIG.CURRENCY : '€';
|
||||
|
||||
// --- 2. GESTION DE L'ÉDITION INVISIBLE EN DIRECT ---
|
||||
const cycleConfigs = <?= json_encode($cycleConfigs ?? []) ?>;
|
||||
|
||||
@@ -306,16 +307,19 @@ function updateEpargneCell(month, category, owner, inputEl) {
|
||||
body: formData
|
||||
}).catch(err => alert(window.I18N['bud_err_tech'] || 'Erreur technique'));
|
||||
|
||||
const totalInput = document.querySelector(`.total-input-${owner}-${month}`);
|
||||
// Gère les IDs proprement
|
||||
const safeOwnerClass = owner.replace(/\s+/g, '_');
|
||||
|
||||
const totalInput = document.querySelector(`.total-input-${CSS.escape(safeOwnerClass)}-${month}`);
|
||||
const totalVal = parseFloat(totalInput ? totalInput.value : 0) || 0;
|
||||
|
||||
let sumCats = 0;
|
||||
document.querySelectorAll(`.cat-input-${owner}-${month}`).forEach(inp => {
|
||||
document.querySelectorAll(`.cat-input-${CSS.escape(safeOwnerClass)}-${month}`).forEach(inp => {
|
||||
sumCats += parseFloat(inp.value) || 0;
|
||||
});
|
||||
|
||||
const extra = totalVal - sumCats;
|
||||
const extraCell = document.getElementById(`extra_${owner}_${month}`);
|
||||
const extraCell = document.getElementById(`extra_${safeOwnerClass}_${month}`);
|
||||
|
||||
if (extraCell) {
|
||||
extraCell.innerText = Math.round(extra).toLocaleString(window.appLang) + ' €';
|
||||
@@ -557,7 +561,7 @@ function updateSumResult() {
|
||||
total += val;
|
||||
});
|
||||
|
||||
document.getElementById('sumResultValue').innerText = Math.round(total).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
||||
document.getElementById('sumResultValue').innerText = Math.round(total).toLocaleString(window.appLang) + ' ' + systemCurrency;
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
|
||||
+147
-247
@@ -1,35 +1,32 @@
|
||||
<?php
|
||||
// modules/budget/views/recap.php
|
||||
|
||||
// 1. Récupération des Items du Budget
|
||||
$stmt = $pdo->query("SELECT * FROM pf_budget_items ORDER BY category DESC, sort_order ASC, name ASC");
|
||||
$items = $stmt->fetchAll();
|
||||
// 1. Récupération des Catégories dynamiques
|
||||
$stmtCats = $pdo->query("SELECT code, label, icon, type FROM pf_budget_categories ORDER BY type DESC, label ASC");
|
||||
$dbCategories = $stmtCats->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 2. Déterminer quel est le mois de gestion "ouvert" par défaut
|
||||
// 2. Récupération des Salaires/Mensualités configurés (Revenus automatiques)
|
||||
$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y');
|
||||
$stmtSalaries = $pdo->prepare("SELECT person, mensualite FROM pf_salary_config WHERE year = ?");
|
||||
$stmtSalaries->execute([$currentYear]);
|
||||
$salaries = $stmtSalaries->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 3. Récupération des Items du Budget (Charges Fixes et Estimations)
|
||||
$stmt = $pdo->query("SELECT * FROM pf_budget_items ORDER BY is_estimate ASC, sort_order ASC, name ASC");
|
||||
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 4. Gestion du mois actif
|
||||
$stmtActive = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'active_gestion_month' LIMIT 1");
|
||||
$defaultActiveMonth = $stmtActive->fetchColumn();
|
||||
if (!$defaultActiveMonth) {
|
||||
$defaultActiveMonth = date('Y-m-01');
|
||||
}
|
||||
|
||||
// 3. Assigner le mois et l'année en fonction du mois ouvert
|
||||
$defaultActiveMonth = $stmtActive->fetchColumn() ?: date('Y-m-01');
|
||||
$currentMonth = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEFT) : date('m', strtotime($defaultActiveMonth));
|
||||
$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y', strtotime($defaultActiveMonth));
|
||||
$viewMonthDate = "$currentYear-$currentMonth-01";
|
||||
|
||||
$sqlReal = "SELECT budget_item_id, SUM(amount) as total_real
|
||||
FROM pf_expenses
|
||||
WHERE gestion_month = ? AND budget_item_id IS NOT NULL
|
||||
GROUP BY budget_item_id";
|
||||
$stmtReal = $pdo->prepare($sqlReal);
|
||||
// 5. Récupération du Réel (Dépenses)
|
||||
$stmtReal = $pdo->prepare("SELECT budget_item_id, SUM(amount) as total_real FROM pf_expenses WHERE gestion_month = ? AND budget_item_id IS NOT NULL GROUP BY budget_item_id");
|
||||
$stmtReal->execute([$viewMonthDate]);
|
||||
$realTotals = $stmtReal->fetchAll(PDO::FETCH_KEY_PAIR); // Retourne un tableau [id => total]
|
||||
$realTotals = $stmtReal->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
$sqlCatReal = "SELECT category, SUM(amount) as total_real
|
||||
FROM pf_expenses
|
||||
WHERE gestion_month = ? AND budget_item_id IS NULL
|
||||
GROUP BY category";
|
||||
$stmtCatReal = $pdo->prepare($sqlCatReal);
|
||||
$stmtCatReal = $pdo->prepare("SELECT category, SUM(amount) as total_real FROM pf_expenses WHERE gestion_month = ? AND budget_item_id IS NULL GROUP BY category");
|
||||
$stmtCatReal->execute([$viewMonthDate]);
|
||||
$catTotals = $stmtCatReal->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
@@ -38,8 +35,7 @@ $stmtLabels->execute([$viewMonthDate]);
|
||||
$unlinkedExpenses = $stmtLabels->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$moisFr = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
||||
$monthTranslationKey = 'month_' . str_pad((int)$currentMonth, 2, '0', STR_PAD_LEFT);
|
||||
$currentMonthName = tr($monthTranslationKey) . ' ' . $currentYear;
|
||||
$currentMonthName = tr('month_' . str_pad((int)$currentMonth, 2, '0', STR_PAD_LEFT)) . ' ' . $currentYear;
|
||||
|
||||
$totalDepenses = 0;
|
||||
$totalRevenus = 0;
|
||||
@@ -64,46 +60,60 @@ $totalRevenus = 0;
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($salaries as $salary):
|
||||
$mensualite = (float)$salary['mensualite'];
|
||||
$totalRevenus += $mensualite;
|
||||
?>
|
||||
<tr class="row-income" style="border-bottom:1px solid var(--border-light); background:#f0fdf4;">
|
||||
<td style="padding:15px;">
|
||||
<strong>Apport <?= htmlspecialchars($salary['person']) ?></strong>
|
||||
<span title="Géré automatiquement depuis les paramètres des Salaires" style="font-size:0.8rem; cursor:help;">⚙️</span>
|
||||
</td>
|
||||
<td class="cell-amount" style="font-weight:600; padding:15px; color:#10b981;">
|
||||
+ <?= number_format($mensualite, 2, ',', ' ') ?> €
|
||||
</td>
|
||||
<td style="padding:15px;">
|
||||
<span class="badge-type" style="background:#dcfce7; color:#16a34a; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600;">Fixe (Auto)</span>
|
||||
</td>
|
||||
<td style="padding:15px; color:#64748b; font-weight:bold;">-</td>
|
||||
<td style="padding:15px;">
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#f8fafc; color:#94a3b8; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #e2e8f0;">
|
||||
Auto
|
||||
</div>
|
||||
</td>
|
||||
<td style="padding:15px; text-align:right;">
|
||||
<small style="color:#94a3b8;">Via Paramètres</small>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php foreach ($items as $item):
|
||||
// --- 1. CALCUL DES TOTAUX PRÉVUS ---
|
||||
$targetAbs = abs((float)$item['amount']);
|
||||
$amountToAdd = ($item['type'] === 'Annuel') ? $targetAbs / 12 : $targetAbs;
|
||||
$totalDepenses += $amountToAdd;
|
||||
|
||||
if ($item['category'] === 'income') $totalRevenus += $amountToAdd;
|
||||
else $totalDepenses += $amountToAdd;
|
||||
|
||||
// --- 2. CALCUL DU RÉEL (Logique optimisée) ---
|
||||
$realSum = 0;
|
||||
$hasMatchingExpense = false;
|
||||
|
||||
// A. Correspondance directe par ID
|
||||
// A. Correspondance par ID direct
|
||||
if (isset($realTotals[$item['id']])) {
|
||||
$realSum = $realTotals[$item['id']];
|
||||
$hasMatchingExpense = true;
|
||||
}
|
||||
// B. Correspondance par catégorie système (Multi-tenant via Mots-clés)
|
||||
else {
|
||||
$catKey = null;
|
||||
if (!empty($item['mapping_keywords'])) {
|
||||
if (stripos($item['mapping_keywords'], 'School') !== false) $catKey = 'School';
|
||||
elseif (stripos($item['mapping_keywords'], 'Essence') !== false) $catKey = 'Essence';
|
||||
elseif (stripos($item['mapping_keywords'], 'FMCG') !== false) $catKey = 'FMCG';
|
||||
}
|
||||
|
||||
if ($catKey && isset($catTotals[$catKey])) {
|
||||
$realSum = $catTotals[$catKey];
|
||||
$hasMatchingExpense = true;
|
||||
}
|
||||
// C. Correspondance par mots-clés classiques (sur les dépenses non liées)
|
||||
elseif (!empty($item['mapping_keywords'])) {
|
||||
$keywords = array_map('trim', explode(',', $item['mapping_keywords']));
|
||||
foreach ($unlinkedExpenses as $uexp) {
|
||||
foreach ($keywords as $kw) {
|
||||
if (!empty($kw) && stripos($uexp['label'], $kw) !== false) {
|
||||
$realSum += (float)$uexp['amount'];
|
||||
$hasMatchingExpense = true;
|
||||
break;
|
||||
}
|
||||
// B. Correspondance par Catégorie système
|
||||
elseif (!empty($item['category']) && isset($catTotals[$item['category']])) {
|
||||
$realSum = $catTotals[$item['category']];
|
||||
$hasMatchingExpense = true;
|
||||
}
|
||||
// C. Correspondance par mots-clés bancaires
|
||||
elseif (!empty($item['mapping_keywords'])) {
|
||||
$keywords = array_map('trim', explode(',', $item['mapping_keywords']));
|
||||
foreach ($unlinkedExpenses as $uexp) {
|
||||
foreach ($keywords as $kw) {
|
||||
if (!empty($kw) && stripos($uexp['label'], $kw) !== false) {
|
||||
$realSum += (float)$uexp['amount'];
|
||||
$hasMatchingExpense = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,79 +121,55 @@ $totalRevenus = 0;
|
||||
|
||||
$realAbs = abs($realSum);
|
||||
$isAutoChecked = ($hasMatchingExpense && ($realAbs >= ($targetAbs - 0.10)));
|
||||
|
||||
$rowClass = ($item['category'] === 'income') ? 'row-income' : 'row-expense';
|
||||
if ($item['is_estimate']) $rowClass .= ' row-estimate';
|
||||
$rowClass = 'row-expense' . ($item['is_estimate'] ? ' row-estimate' : '');
|
||||
?>
|
||||
<tr class="<?= $rowClass ?>" style="border-bottom:1px solid var(--border-light);">
|
||||
<td style="padding:15px;">
|
||||
<strong><?= htmlspecialchars($item['name']) ?></strong>
|
||||
<?= $item['is_estimate'] ? ' <small style="color:#64748b;">('.tr('bud_est_short').')</small>' : '' ?>
|
||||
|
||||
<?= $item['is_estimate'] ? ' <small style="color:#64748b;">(Variable)</small>' : '' ?>
|
||||
<?php if(!empty($item['mapping_keywords'])): ?>
|
||||
<span title="<?= htmlspecialchars($item['mapping_keywords']) ?>" style="font-size:0.7rem; cursor:help;">🔗</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if(!empty($item['reg_month'])): ?>
|
||||
<div style="font-size:0.75rem; color:#94a3b8; font-style:italic; margin-top:2px;">
|
||||
📅 <?= sprintf(tr('bud_reg_planned_in'), tr('month_'.str_pad(array_search($item['reg_month'], $moisFr), 2, '0', STR_PAD_LEFT))) ?>
|
||||
</div>
|
||||
<span title="Reconnaissance bancaire : <?= htmlspecialchars($item['mapping_keywords']) ?>" style="font-size:0.7rem; cursor:help;">🔗</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<td class="cell-amount" style="font-weight:600; padding:15px; color:<?= $item['category']==='income'?'#10b981':'#1e293b' ?>;">
|
||||
<?= number_format($targetAbs, 2, ',', ' ') ?> €
|
||||
<td class="cell-amount" style="font-weight:600; padding:15px; color:#1e293b;">
|
||||
- <?= number_format($targetAbs, 2, ',', ' ') ?> €
|
||||
|
||||
<?php if ($hasMatchingExpense): ?>
|
||||
<?php
|
||||
// Le "Gap" est la différence visuelle.
|
||||
$gap = $realAbs - $targetAbs;
|
||||
|
||||
<?php $gap = $realAbs - $targetAbs;
|
||||
if ($gap > 0.05): ?>
|
||||
<div style="font-size:0.75rem; color:#ef4444; font-weight:bold;">
|
||||
<?= $item['category'] === 'income' ? tr('bud_bonus') : tr('bud_overrun') ?> : +<?= number_format($gap, 2, ',', ' ') ?> €
|
||||
</div>
|
||||
<div style="font-size:0.75rem; color:#ef4444; font-weight:bold;">Dépassé : +<?= number_format($gap, 2, ',', ' ') ?> €</div>
|
||||
<?php elseif ($gap < -0.05): ?>
|
||||
<div style="font-size:0.75rem; color:#f59e0b; font-weight:normal;">
|
||||
<?= tr('bud_remaining') ?> : <?= number_format(abs($gap), 2, ',', ' ') ?> €
|
||||
</div>
|
||||
<div style="font-size:0.75rem; color:#f59e0b;">Reste : <?= number_format(abs($gap), 2, ',', ' ') ?> €</div>
|
||||
<?php else: ?>
|
||||
<div style="font-size:0.75rem; color:#10b981; font-weight:normal;">
|
||||
<?= tr('bud_exact_amount') ?> ✓
|
||||
</div>
|
||||
<div style="font-size:0.75rem; color:#10b981;">Atteint ✓</div>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($item['type'] === 'Annuel'): ?>
|
||||
<div style="font-size:0.75rem; color:#94a3b8; font-weight:normal;"><?= tr('bud_per_month_short') ?> <?= number_format($amountToAdd, 2, ',', ' ') ?>/<?= tr('bud_month_short') ?></div>
|
||||
<div style="font-size:0.75rem; color:#94a3b8;"><?= number_format($amountToAdd, 2, ',', ' ') ?>/mois</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<td style="padding:15px;">
|
||||
<span class="badge-type <?= strtolower($item['type']) ?>" style="background:#e2e8f0; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600; color:#475569;">
|
||||
<?= tr('bud_freq_'.strtolower($item['type'])) ?>
|
||||
<span class="badge-type" style="background:#e2e8f0; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600; color:#475569;">
|
||||
<?= htmlspecialchars($item['type']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td style="padding:15px; color:#64748b; font-weight:bold;"><?= $item['payment_day'] ? $item['payment_day'] : '-' ?></td>
|
||||
<td style="padding:15px; color:#64748b; font-weight:bold;"><?= $item['payment_day'] ?: '-' ?></td>
|
||||
|
||||
<td style="padding:15px;">
|
||||
<?php if ($isAutoChecked): ?>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#f0fdf4; color:#16a34a; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #bbf7d0;">
|
||||
<span>✓</span> <?= tr('bud_state_validated') ?>
|
||||
</div>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#f0fdf4; color:#16a34a; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #bbf7d0;"><span>✓</span> Validé</div>
|
||||
<?php elseif($hasMatchingExpense): ?>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#fffbeb; color:#d97706; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #fde68a;">
|
||||
<span>⏳</span> <?= tr('bud_state_partial') ?>
|
||||
</div>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#fffbeb; color:#d97706; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #fde68a;"><span>⏳</span> Partiel</div>
|
||||
<?php else: ?>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#f8fafc; color:#94a3b8; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #e2e8f0;">
|
||||
<span>○</span> <?= tr('bud_state_waiting') ?>
|
||||
</div>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#f8fafc; color:#94a3b8; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #e2e8f0;"><span>○</span> En attente</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<td style="padding:15px; text-align:right;">
|
||||
<div class="action-buttons" style="display:flex; gap:5px; justify-content:flex-end;">
|
||||
<button class="btn-icon-action edit" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="<?= tr('edit') ?>">✏️</button>
|
||||
<button class="btn-icon-action delete" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="<?= tr('delete') ?>">🗑️</button>
|
||||
<button class="btn-icon-action edit" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="Modifier">✏️</button>
|
||||
<button class="btn-icon-action delete" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="Supprimer">🗑️</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -191,34 +177,30 @@ $totalRevenus = 0;
|
||||
</tbody>
|
||||
<tfoot style="background:#f8fafc;">
|
||||
<tr>
|
||||
<td colspan="1" style="padding:15px;"><strong><?= tr('bud_total_income_smoothed') ?></strong></td>
|
||||
<td colspan="1" style="padding:15px;"><strong>Total Revenus Lissés</strong></td>
|
||||
<td colspan="5" style="padding:15px; color:#10b981;"><strong>+ <?= number_format($totalRevenus, 2, ',', ' ') ?> €</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1" style="padding:15px;"><strong><?= tr('bud_total_expenses_smoothed') ?></strong></td>
|
||||
<td colspan="1" style="padding:15px;"><strong>Total Dépenses & Estimations</strong></td>
|
||||
<td colspan="5" style="padding:15px; color:#ef4444;"><strong>- <?= number_format($totalDepenses, 2, ',', ' ') ?> €</strong></td>
|
||||
</tr>
|
||||
<tr style="border-top: 2px solid #e2e8f0; background: white;">
|
||||
<td colspan="1" style="padding:15px; font-size:1.1rem;"><strong><?= tr('bud_theoretical_balance_recap') ?></strong></td>
|
||||
<td colspan="1" style="padding:15px; font-size:1.1rem;"><strong>Reste à Vivre (Équilibre)</strong></td>
|
||||
<?php $balance = $totalRevenus - $totalDepenses; ?>
|
||||
<td colspan="5" style="padding:15px; font-size: 1.3em;" class="<?= $balance >= 0 ? 'text-success' : 'text-danger' ?>">
|
||||
<strong style="color:<?= $balance >= 0 ? '#10b981' : '#ef4444' ?>;"><?= number_format($balance, 2, ',', ' ') ?> € / <?= tr('bud_month_short') ?></strong>
|
||||
<td colspan="5" style="padding:15px; font-size: 1.3em;">
|
||||
<strong style="color:<?= $balance >= 0 ? '#10b981' : '#ef4444' ?>;"><?= number_format($balance, 2, ',', ' ') ?> € / mois</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="budget-note" style="margin-top:15px; font-size:0.85rem; color:#64748b;">
|
||||
<p>* <?= tr('bud_recap_footer_note') ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="budgetRecapModal" class="pf-modal" style="display:none; position:fixed; inset:0; z-index:9999; background:rgba(15, 23, 42, 0.6); backdrop-filter:blur(4px); align-items:center; justify-content:center;">
|
||||
<div class="pf-modal-content" style="background:white; width:95%; max-width:500px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px; position:relative;">
|
||||
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||
<h3 id="recapModalTitle" style="margin:0; font-size:1.2rem; color:#1e293b;"><?= tr('bud_recap_modal_add') ?></h3>
|
||||
<h3 id="recapModalTitle" style="margin:0; font-size:1.2rem; color:#1e293b;">Ajouter une ligne</h3>
|
||||
<button type="button" onclick="closeRecapModal()" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">×</button>
|
||||
</div>
|
||||
|
||||
@@ -227,90 +209,65 @@ $totalRevenus = 0;
|
||||
<input type="hidden" name="id" id="item_id">
|
||||
|
||||
<div class="form-group" style="margin-bottom:15px;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_name') ?></label>
|
||||
<input type="text" name="name" id="item_name" required class="pf-input" placeholder="<?= tr('bud_ph_item_name') ?>" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom:15px;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_keywords') ?></label>
|
||||
<input type="text" name="mapping_keywords" id="item_keywords" class="pf-input" placeholder="<?= tr('bud_ph_keywords') ?>" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:#f0f9ff; border-color:#bae6fd;">
|
||||
<small style="color:#64748b; font-size:0.75rem;"><?= tr('bud_help_keywords') ?></small>
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Nom de la ligne</label>
|
||||
<input type="text" name="name" id="item_name" required class="pf-input" placeholder="ex: Assurance Auto, Estimation Courses..." style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_amount_eur') ?></label>
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Montant Prévu (€)</label>
|
||||
<input type="number" step="0.01" name="amount" id="item_amount" required class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_day') ?></label>
|
||||
<input type="number" min="1" max="31" name="payment_day" id="item_day" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Type de Dépense</label>
|
||||
<select name="is_estimate" id="item_is_estimate" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value="0">Fixe (Facture, Prélèvement)</option>
|
||||
<option value="1">Variable (Estimation, Enveloppe)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_category') ?></label>
|
||||
<select name="category" id="item_category" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value="expense"><?= tr('bud_cat_expense') ?></option>
|
||||
<option value="income"><?= tr('bud_cat_income') ?></option>
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Catégorie Cible (La jauge)</label>
|
||||
<select name="category" id="item_category" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;" required>
|
||||
<option value="" disabled selected>-- Choisir --</option>
|
||||
<?php foreach ($dbCategories as $c): ?>
|
||||
<?php if(strtolower($c['type']) === 'expense'): // On n'affiche que les catégories de dépenses ?>
|
||||
<option value="<?= htmlspecialchars($c['code']) ?>">
|
||||
<?= htmlspecialchars($c['icon'] . ' ' . $c['label']) ?>
|
||||
</option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_frequency') ?></label>
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Fréquence</label>
|
||||
<select name="type" id="item_type" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value="Mensuel"><?= tr('bud_freq_mensuel') ?></option>
|
||||
<option value="Annuel"><?= tr('bud_freq_annuel') ?></option>
|
||||
<option value="Mensuel">Mensuel</option>
|
||||
<option value="Annuel">Annuel</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:15px; margin-bottom:25px;">
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_amount_type') ?></label>
|
||||
<select name="is_estimate" id="item_is_estimate" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value="0"><?= tr('bud_type_fixed') ?></option>
|
||||
<option value="1"><?= tr('bud_type_variable') ?></option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_regularization') ?></label>
|
||||
<select name="reg_month" id="item_reg_month" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value=""><?= tr('bud_reg_none') ?></option>
|
||||
<?php
|
||||
foreach($moisFr as $index => $m) {
|
||||
if($index == 0) continue;
|
||||
echo "<option value='$m'>" . tr('month_'.str_pad($index, 2, '0', STR_PAD_LEFT)) . "</option>";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom:15px;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Mots-clés (Reconnaissance Bancaire)</label>
|
||||
<input type="text" name="mapping_keywords" id="item_keywords" class="pf-input" placeholder="ex: NETFLIX, DOMOFINANCE..." style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:#f0f9ff; border-color:#bae6fd;">
|
||||
<small style="color:#64748b; font-size:0.75rem;">Optionnel. Permet au système de lier automatiquement un import CSV à cette ligne.</small>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; justify-content:flex-end; gap:10px;">
|
||||
<button type="button" onclick="closeRecapModal()" class="pf-btn btn-secondary" style="width:auto; margin:0; background:#f1f5f9; color:#475569; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;"><?= tr('btn_cancel') ?></button>
|
||||
<button type="submit" class="pf-btn" style="width:auto; margin:0; background:#2563eb; color:white; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;"><?= tr('btn_save') ?></button>
|
||||
<div style="display:flex; justify-content:flex-end; gap:10px; margin-top: 25px;">
|
||||
<button type="button" onclick="closeRecapModal()" class="pf-btn btn-secondary" style="width:auto; margin:0; background:#f1f5f9; color:#475569; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;">Annuler</button>
|
||||
<button type="submit" class="pf-btn" style="width:auto; margin:0; background:#2563eb; color:white; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
|
||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||
window.I18N = {
|
||||
...(window.I18N || {}),
|
||||
'bud_recap_modal_add': <?= json_encode(tr('bud_recap_modal_add')) ?>,
|
||||
'bud_recap_modal_edit': <?= json_encode(tr('bud_recap_modal_edit')) ?>,
|
||||
'bud_recap_confirm_delete': <?= json_encode(tr('bud_recap_confirm_delete')) ?>,
|
||||
'bud_err_delete': <?= json_encode(tr('bud_err_delete')) ?>,
|
||||
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>,
|
||||
'bud_saving': <?= json_encode(tr('bud_sav_saving') ?? 'Sauvegarde...') ?>
|
||||
};
|
||||
|
||||
function openRecapModal(mode) {
|
||||
if (mode === "add") {
|
||||
document.getElementById("recapModalTitle").innerText = window.I18N['bud_recap_modal_add'] || 'Ajouter';
|
||||
document.getElementById("recapModalTitle").innerText = 'Ajouter une ligne';
|
||||
document.getElementById("item_id").value = "";
|
||||
document.getElementById("recapForm").reset();
|
||||
}
|
||||
@@ -324,113 +281,56 @@ function closeRecapModal() {
|
||||
}
|
||||
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('budgetRecapModal');
|
||||
if (event.target == modal) {
|
||||
closeRecapModal();
|
||||
}
|
||||
if (event.target == document.getElementById('budgetRecapModal')) closeRecapModal();
|
||||
}
|
||||
|
||||
function editRecapItem(item) {
|
||||
const data = typeof item === "string" ? JSON.parse(item) : item;
|
||||
|
||||
document.getElementById("recapModalTitle").innerText = (window.I18N['bud_recap_modal_edit'] || 'Editer') + " : " + data.name;
|
||||
document.getElementById("recapModalTitle").innerText = "Editer : " + data.name;
|
||||
document.getElementById("item_id").value = data.id;
|
||||
document.getElementById("item_name").value = data.name;
|
||||
document.getElementById("item_keywords").value = data.mapping_keywords || '';
|
||||
document.getElementById("item_amount").value = Math.abs(data.amount);
|
||||
document.getElementById("item_category").value = data.category;
|
||||
document.getElementById("item_type").value = data.type;
|
||||
document.getElementById("item_day").value = data.payment_day;
|
||||
document.getElementById("item_reg_month").value = data.reg_month || "";
|
||||
document.getElementById("item_is_estimate").value = data.is_estimate;
|
||||
|
||||
document.getElementById("budgetRecapModal").style.display = "flex";
|
||||
document.body.classList.add('no-scroll');
|
||||
}
|
||||
|
||||
// --- 2. INTERCEPTION ASYNCHRONE DU FORMULAIRE ---
|
||||
const recapForm = document.getElementById('recapForm');
|
||||
if (recapForm) {
|
||||
recapForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = this.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.innerText;
|
||||
submitBtn.innerText = window.I18N['bud_saving'] || '⏳ ...';
|
||||
submitBtn.disabled = true;
|
||||
document.getElementById('recapForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const btn = this.querySelector('button[type="submit"]');
|
||||
btn.disabled = true; btn.innerText = '⏳ ...';
|
||||
|
||||
const formData = new FormData(this);
|
||||
formData.append('ajax', '1');
|
||||
|
||||
// 💡 Utilisation sécurisée de getAttribute
|
||||
const actionUrl = this.getAttribute('action');
|
||||
const finalUrl = actionUrl.startsWith('/') ? actionUrl.substring(1) : actionUrl;
|
||||
|
||||
try {
|
||||
const response = await fetch(finalUrl, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
// 💡 Lecture robuste (anti-Warnings PHP)
|
||||
const textResult = await response.text();
|
||||
try {
|
||||
const result = JSON.parse(textResult);
|
||||
if (result.success) {
|
||||
closeRecapModal();
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert((window.I18N['bud_err_tech'] || 'Erreur') + " : " + (result.error || "Inconnue"));
|
||||
}
|
||||
} catch (jsonError) {
|
||||
console.error("Réponse non-JSON :", textResult);
|
||||
alert("Le serveur a renvoyé une erreur PHP. Regarde la console (F12).");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur Fetch:", error);
|
||||
alert(window.I18N['bud_err_tech'] || 'Erreur technique');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerText = originalText;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- 3. SUPPRESSION ASYNCHRONE ---
|
||||
async function deleteRecapItem(id) {
|
||||
if (!confirm(window.I18N['bud_recap_confirm_delete'] || "Confirmer la suppression ?")) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("action", "delete");
|
||||
formData.append("id", id);
|
||||
formData.append("ajax", "1"); // Signale à l'API qu'on attend du JSON
|
||||
const formData = new FormData(this);
|
||||
formData.append('ajax', '1');
|
||||
const url = this.getAttribute('action');
|
||||
|
||||
try {
|
||||
const response = await fetch("modules/budget/includes/api/manage-item.php", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const textResult = await response.text();
|
||||
try {
|
||||
const result = JSON.parse(textResult);
|
||||
// On s'assure que si l'API ne renvoie pas success, on le signale
|
||||
if (result.success !== false) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert((window.I18N['bud_err_delete'] || 'Erreur de suppression') + " : " + (result.error || ""));
|
||||
}
|
||||
} catch (jsonErr) {
|
||||
console.error("Réponse non-JSON lors de la suppression :", textResult);
|
||||
window.location.reload(); // Fallback si l'API redirige au lieu de répondre en JSON
|
||||
}
|
||||
const res = await fetch(url.startsWith('/') ? url.substring(1) : url, { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.success) window.location.reload();
|
||||
else alert("Erreur : " + data.error);
|
||||
} catch (err) {
|
||||
console.error("Erreur réseau Suppression:", err);
|
||||
alert(window.I18N['bud_err_delete'] || 'Erreur réseau');
|
||||
alert("Erreur technique lors de la sauvegarde.");
|
||||
} finally {
|
||||
btn.disabled = false; btn.innerText = 'Enregistrer';
|
||||
}
|
||||
});
|
||||
|
||||
async function deleteRecapItem(id) {
|
||||
if (!confirm("Confirmer la suppression ?")) return;
|
||||
const formData = new FormData();
|
||||
formData.append("action", "delete"); formData.append("id", id); formData.append("ajax", "1");
|
||||
try {
|
||||
const res = await fetch("modules/budget/includes/api/manage-item.php", { method: "POST", body: formData });
|
||||
const data = await res.json();
|
||||
if (data.success !== false) window.location.reload();
|
||||
else alert("Erreur : " + data.error);
|
||||
} catch (err) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+145
-107
@@ -60,7 +60,6 @@ if (isset($_POST['action']) && $_POST['action'] === 'reopen_month') {
|
||||
if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
|
||||
$count = 0;
|
||||
$stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
// Mémorisation du mapping étendu (incluant budget_item_id)
|
||||
$stmtRule = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category, budget_item_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE category = VALUES(category), budget_item_id = VALUES(budget_item_id)");
|
||||
|
||||
if (isset($_POST['lines']) && is_array($_POST['lines'])) {
|
||||
@@ -152,9 +151,43 @@ if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 3. RECUPERATION DES DONNEES ET CALCULS
|
||||
// 3. RECUPERATION DES DONNEES ET CALCULS (100% DYNAMIQUE)
|
||||
// ============================================================================
|
||||
|
||||
// --- LECTURE DYNAMIQUE DES CATEGORIES ---
|
||||
$stmtCats = $pdo->query("SELECT * FROM pf_budget_categories ORDER BY type DESC, label ASC");
|
||||
$dbCategories = $stmtCats->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$categoriesConfig = [];
|
||||
foreach ($dbCategories as $c) {
|
||||
$catType = ($c['type'] === 'Income') ? 'credit' : 'debit';
|
||||
$categoriesConfig[$c['code']] = [
|
||||
'type' => $catType,
|
||||
'db_type' => $c['type'],
|
||||
'label' => ($c['icon'] ? $c['icon'] . ' ' : '') . $c['label'],
|
||||
'budget' => 0, // Sera rempli par les règles
|
||||
'color' => $c['color'] ?: '#64748b',
|
||||
'suggestions' => []
|
||||
];
|
||||
}
|
||||
|
||||
// Fallback "Autres" au cas où la BDD serait vide ou pour les dépenses non classées
|
||||
if (!isset($categoriesConfig['AUTRES'])) {
|
||||
$categoriesConfig['AUTRES'] = [
|
||||
'type'=>'debit', 'db_type'=>'Expense', 'label'=>'📁 Autres / Divers',
|
||||
'budget'=>0, 'color'=>'#94a3b8', 'suggestions'=>[]
|
||||
];
|
||||
}
|
||||
|
||||
// Peuplement dynamique des suggestions via les règles existantes
|
||||
$stmtRules = $pdo->query("SELECT keyword, category FROM pf_import_rules");
|
||||
while ($rule = $stmtRules->fetch(PDO::FETCH_ASSOC)) {
|
||||
if (isset($categoriesConfig[$rule['category']])) {
|
||||
$categoriesConfig[$rule['category']]['suggestions'][] = $rule['keyword'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$stmtCheckClose = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'month_closure' AND reference_id = ?");
|
||||
$stmtCheckClose->execute([$viewMonthDate]);
|
||||
$closureJson = $stmtCheckClose->fetchColumn();
|
||||
@@ -184,27 +217,42 @@ $allExpenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$paidItemIds = array_column(array_filter($allExpenses, fn($e) => !empty($e['budget_item_id'])), 'budget_item_id');
|
||||
$realExpensesLabels = array_column(array_filter($allExpenses, fn($e) => $e['amount'] < 0), 'label');
|
||||
|
||||
$budget_fmcg = 0; $budget_school = 0; $budget_essence = 0; $budget_frais = 0; $budget_income_prevu = 0;
|
||||
$budget_income_prevu = 0;
|
||||
$total_income = 0; $total_expenses_prevues = 0;
|
||||
$reste_a_venir_calc = 0;
|
||||
$fixedChargesList = []; $incomeList = []; $pending_charges = [];
|
||||
|
||||
// ============================================================================
|
||||
// MAPPING DYNAMIQUE DES BUDGETS PRÉVISIONNELS (NOUVELLE LOGIQUE)
|
||||
// ============================================================================
|
||||
$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, mapping_keywords FROM pf_budget_items ORDER BY name ASC");
|
||||
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$absAmount = abs((float)$item['amount']);
|
||||
$amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount;
|
||||
$name = trim($item['name']);
|
||||
$catCode = $item['category']; // Ex: FIXED, FMCG, INCOME...
|
||||
$isIncome = ((float)$item['amount'] > 0); // La norme est désormais définie par le signe du montant
|
||||
|
||||
if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
||||
$fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
||||
}
|
||||
if ($item['category'] === 'income') {
|
||||
if ($isIncome) {
|
||||
$incomeList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
||||
$total_income += $amt;
|
||||
$budget_income_prevu += $amt;
|
||||
$budget_income_prevu += $amt;
|
||||
|
||||
// Attribution au compte de revenu défini, sinon au premier disponible
|
||||
if (!empty($catCode) && isset($categoriesConfig[$catCode])) {
|
||||
$categoriesConfig[$catCode]['budget'] += $amt;
|
||||
} else {
|
||||
$incomeCatKey = array_key_first(array_filter($categoriesConfig, fn($c) => $c['db_type'] === 'Income'));
|
||||
if ($incomeCatKey) $categoriesConfig[$incomeCatKey]['budget'] += $amt;
|
||||
}
|
||||
|
||||
} else {
|
||||
$total_expenses_prevues += $amt;
|
||||
if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
||||
|
||||
// C'est une charge fixe (Mensuel + Is_Estimate = 0)
|
||||
if ($item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
||||
$fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
||||
|
||||
$isPaid = false;
|
||||
if (in_array($item['id'], $paidItemIds)) $isPaid = true;
|
||||
elseif (!empty($item['mapping_keywords'])) {
|
||||
@@ -221,90 +269,68 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$pending_charges[] = ['name' => $name, 'amount' => $absAmount];
|
||||
}
|
||||
}
|
||||
if (!empty($item['mapping_keywords'])) {
|
||||
if (stripos($item['mapping_keywords'], 'FMCG') !== false) $budget_fmcg += $amt;
|
||||
if (stripos($item['mapping_keywords'], 'School') !== false) $budget_school += $amt;
|
||||
if (stripos($item['mapping_keywords'], 'Essence') !== false) $budget_essence += $amt;
|
||||
}
|
||||
|
||||
if ((int)$item['is_estimate'] === 0 && $item['type'] === 'Mensuel' && $item['category'] === 'expense') {
|
||||
$budget_frais += $absAmount;
|
||||
// NOUVEAU : Attribution du budget prévisionnel (le Plafond) via la catégorie dynamique
|
||||
if (!empty($catCode) && isset($categoriesConfig[$catCode])) {
|
||||
$categoriesConfig[$catCode]['budget'] += $amt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$budget_autres = max(0, $total_income - $total_expenses_prevues);
|
||||
|
||||
// TRADUCTION DES CATÉGORIES
|
||||
$categoriesConfig = [
|
||||
'Income' => ['type'=>'credit', 'label'=>tr('cat_income'), 'budget'=>$budget_income_prevu, 'color'=>'#10b981', 'suggestions'=>[]],
|
||||
'FMCG' => ['type'=>'debit', 'label'=>tr('cat_fmcg'), 'budget'=>$budget_fmcg, 'color'=>'#3b82f6', 'suggestions'=>['Action', 'Carrefour', 'Lidl']],
|
||||
'Essence' => ['type'=>'debit', 'label'=>tr('cat_fuel'), 'budget'=>$budget_essence, 'color'=>'#f59e0b', 'suggestions'=>['Audi', 'Polo']],
|
||||
'School' => ['type'=>'debit', 'label'=>tr('cat_school'), 'budget'=>$budget_school, 'color'=>'#10b981', 'suggestions'=>[]],
|
||||
'Frais' => ['type'=>'debit', 'label'=>tr('cat_fixed'), 'budget'=>$budget_frais, 'color'=>'#ef4444', 'suggestions'=>[]],
|
||||
'Autres' => ['type'=>'debit', 'label'=>tr('cat_others'), 'budget'=>$budget_autres, 'color'=>'#64748b', 'suggestions'=>['Restaurant', 'Cadeau']],
|
||||
'Apports' => ['type'=>'debit', 'label'=>tr('cat_contributions') ?? 'Apports & Projets', 'budget'=>0, 'color'=>'#0ea5e9', 'suggestions'=>['Alex', 'Laia', 'Remboursement']],
|
||||
'LivretA' => ['type'=>'debit', 'label'=>tr('cat_savings'),'budget'=>0, 'color'=>'#8b5cf6', 'suggestions'=>['Virement']]
|
||||
];
|
||||
// L'enveloppe "Autres" prend tout le reste du budget non alloué
|
||||
$categoriesConfig['AUTRES']['budget'] = max(0, $total_income - $total_expenses_prevues);
|
||||
|
||||
$totals = array_fill_keys(array_keys($categoriesConfig), 0);
|
||||
$expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []);
|
||||
$total_rentrees = 0;
|
||||
$depenses_reelles = 0;
|
||||
|
||||
// VENTILATION DYNAMIQUE DES DÉPENSES
|
||||
foreach ($allExpenses as $exp) {
|
||||
$cat = $exp['category'];
|
||||
if (!isset($totals[$cat])) $cat = 'Autres';
|
||||
if (!isset($categoriesConfig[$cat])) $cat = 'AUTRES';
|
||||
|
||||
$val = (float)$exp['amount'];
|
||||
|
||||
if ($cat === 'Income') { $totals[$cat] += $val; }
|
||||
else {
|
||||
if ($val > 0) $categoriesConfig[$cat]['budget'] += $val;
|
||||
if ($categoriesConfig[$cat]['db_type'] === 'Income') {
|
||||
$totals[$cat] += $val;
|
||||
} else {
|
||||
if ($val > 0) $categoriesConfig[$cat]['budget'] += $val; // Remboursement
|
||||
else $totals[$cat] += abs($val);
|
||||
}
|
||||
|
||||
$expensesByCategory[$cat][] = $exp;
|
||||
|
||||
if ($val > 0) {
|
||||
if ($cat === 'Income' || $cat !== 'Frais') $total_rentrees += $val;
|
||||
else $depenses_reelles -= $val;
|
||||
if ($categoriesConfig[$cat]['db_type'] === 'Income') { $total_rentrees += $val; }
|
||||
else { $depenses_reelles -= $val; }
|
||||
} else {
|
||||
$depenses_reelles += abs($val);
|
||||
}
|
||||
}
|
||||
|
||||
// Reste à venir dynamiques
|
||||
$ecole_depense = isset($totals['School']) ? $totals['School'] : 0;
|
||||
$reste_ecole = max(0, $budget_school - $ecole_depense);
|
||||
if ($reste_ecole > 0) {
|
||||
$reste_a_venir_calc += $reste_ecole;
|
||||
$pending_charges[] = ['name' => tr('bud_rem_school'), 'amount' => $reste_ecole];
|
||||
}
|
||||
|
||||
$fmcg_depense = isset($totals['FMCG']) ? $totals['FMCG'] : 0;
|
||||
$reste_fmcg = max(0, $budget_fmcg - $fmcg_depense);
|
||||
if ($reste_fmcg > 0) {
|
||||
$reste_a_venir_calc += $reste_fmcg;
|
||||
$pending_charges[] = ['name' => tr('bud_rem_fmcg'), 'amount' => $reste_fmcg];
|
||||
}
|
||||
|
||||
$essence_depense = isset($totals['Essence']) ? $totals['Essence'] : 0;
|
||||
$reste_essence = max(0, $budget_essence - $essence_depense);
|
||||
if ($reste_essence > 0) {
|
||||
$reste_a_venir_calc += $reste_essence;
|
||||
$pending_charges[] = ['name' => tr('bud_rem_fuel'), 'amount' => $reste_essence];
|
||||
// RESTE A VENIR PAR CATEGORIE (Remplace les variables codées en dur)
|
||||
foreach ($categoriesConfig as $code => $conf) {
|
||||
if ($conf['db_type'] === 'Expense' && $conf['budget'] > 0) {
|
||||
$spent = $totals[$code] ?? 0;
|
||||
$rem = max(0, $conf['budget'] - $spent);
|
||||
if ($rem > 0) {
|
||||
$reste_a_venir_calc += $rem;
|
||||
$pending_charges[] = ['name' => 'Reste ' . strip_tags($conf['label']), 'amount' => $rem];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// F. Calculs des KPIs finaux
|
||||
$rentrees_salaires_reels = $totals['Income'] ?? 0;
|
||||
$rentrees_salaires_reels = 0;
|
||||
foreach($categoriesConfig as $code => $conf) { if($conf['db_type'] === 'Income') $rentrees_salaires_reels += ($totals[$code] ?? 0); }
|
||||
|
||||
$rentrees_autres = $total_rentrees - $rentrees_salaires_reels;
|
||||
$salaires_retenus = max($rentrees_salaires_reels, $budget_income_prevu);
|
||||
|
||||
$capacite_max_calc = $solde_initial + $salaires_retenus + $rentrees_autres;
|
||||
|
||||
// 1. On calcule s'il reste des salaires/revenus prévus à encaisser
|
||||
$revenus_a_venir = max(0, $budget_income_prevu - $rentrees_salaires_reels);
|
||||
|
||||
// 2. Le solde théorique part de ta vraie saisie bancaire actuelle
|
||||
$solde_theorique_calc = $snapshot['amount'] + $revenus_a_venir - $reste_a_venir_calc;
|
||||
|
||||
if ($isClosed) {
|
||||
@@ -335,7 +361,6 @@ function getDisplayLogic($spent, $bg, $type) {
|
||||
return ['pct' => $pct, 'isOver' => $isOver, 'text' => $text];
|
||||
}
|
||||
|
||||
// Noms des mois traduits
|
||||
$monthNames = [
|
||||
1 => tr('month_01'), 2 => tr('month_02'), 3 => tr('month_03'), 4 => tr('month_04'),
|
||||
5 => tr('month_05'), 6 => tr('month_06'), 7 => tr('month_07'), 8 => tr('month_08'),
|
||||
@@ -505,11 +530,11 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!$isClosed): ?>
|
||||
<button class="btn-add-item" style="color:<?= $conf['color'] ?>;" onclick="openAddModal('<?= $key ?>', '<?= addslashes($conf['label']) ?>')">+</button>
|
||||
<button class="btn-add-item" style="color:<?= $conf['color'] ?>;" onclick="openAddModal('<?= $key ?>', '<?= addslashes(strip_tags($conf['label'])) ?>')">+</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php $barCol = ($key === 'Income') ? '#10b981' : ($logic['isOver'] ? '#ef4444' : $conf['color']); ?>
|
||||
<?php $barCol = ($conf['db_type'] === 'Income') ? '#10b981' : ($logic['isOver'] ? '#ef4444' : $conf['color']); ?>
|
||||
<div style="background:#f1f5f9; height:4px; width:100%;">
|
||||
<div style="width:<?= $logic['pct'] ?>%; background:<?= $barCol ?>; height:100%;"></div>
|
||||
</div>
|
||||
@@ -568,7 +593,7 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
<label class="pf-label"><?= tr('bud_category') ?></label>
|
||||
<select name="category" id="modalCatSelect" class="pf-input" onchange="handleModalCatChange(this)">
|
||||
<?php foreach($categoriesConfig as $key => $conf): ?>
|
||||
<option value="<?= $key ?>"><?= $conf['label'] ?></option>
|
||||
<option value="<?= $key ?>"><?= strip_tags($conf['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
@@ -587,23 +612,10 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
<datalist id="modalSuggestions"></datalist>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="blockInputSelect" style="margin-bottom:15px; display:none;">
|
||||
<label class="pf-label"><?= tr('bud_beneficiary') ?></label>
|
||||
<select name="label_select" id="schoolSelect" class="pf-input">
|
||||
<?php
|
||||
$stmtSchoolLabel = $pdo->query("SELECT name FROM pf_people WHERE role IN ('enfant', 'nounou') OR role IS NULL ORDER BY id ASC");
|
||||
while($k = $stmtSchoolLabel->fetch(PDO::FETCH_ASSOC)) {
|
||||
$pName = htmlspecialchars($k['name']);
|
||||
echo "<option value='{$pName}'>{$pName}</option>";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="blockInputFrais" style="margin-bottom:15px; display:none;">
|
||||
<label class="pf-label"><?= tr('bud_fixed_charge') ?></label>
|
||||
<label class="pf-label">Lier à une Charge Fixe (Optionnel)</label>
|
||||
<select name="budget_item_id" id="fraisSelect" class="pf-input" disabled>
|
||||
<option value=""><?= tr('bud_select_beneficiary') ?></option>
|
||||
<option value="">-- Aucune --</option>
|
||||
<?php foreach ($fixedChargesList as $fc): ?><option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['name']) ?></option><?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
@@ -720,11 +732,11 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
<select name="lines[<?= $idx ?>][cat]" class="pf-input line-select" onchange="handleLineCatChange(this)" <?= $dis ?> style="padding:4px; font-size:0.85rem; flex:1;">
|
||||
<option value="">-- <?= $isCrd ? tr('bud_ignore') : tr('bud_to_define') ?> --</option>
|
||||
<?php foreach ($categoriesConfig as $k => $c): ?>
|
||||
<option value="<?= $k ?>" <?= ($row['cat']===$k)?'selected':'' ?>><?= $c['label'] ?></option>
|
||||
<option value="<?= $k ?>" <?= ($row['cat']===$k)?'selected':'' ?>><?= strip_tags($c['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-frais" onchange="checkValidation()" style="display:none; padding:4px; font-size:0.85rem; flex:1;" disabled>
|
||||
<option value="">-- <?= tr('bud_is_charge') ?> --</option>
|
||||
<option value="">-- Lier à une Charge Fixe --</option>
|
||||
<?php foreach ($fixedChargesList as $fc): ?>
|
||||
<option value="<?= $fc['id'] ?>" <?= ($row['suggested_item_id'] == $fc['id']) ? 'selected' : '' ?>><?= htmlspecialchars($fc['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
@@ -757,7 +769,6 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
document.body.classList.add('no-scroll');
|
||||
<?php endif; ?>
|
||||
|
||||
// --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
|
||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||
window.I18N = {
|
||||
...(window.I18N || {}),
|
||||
@@ -769,34 +780,42 @@ window.I18N = {
|
||||
'btn_delete': <?= json_encode(tr('btn_delete')) ?>,
|
||||
};
|
||||
|
||||
// --- DICTIONNAIRE JS DYNAMIQUE ---
|
||||
const catConfigs = <?= json_encode($categoriesConfig) ?>;
|
||||
|
||||
const activeViewMonth = '<?= substr($viewMonthDate, 0, 7) ?>';
|
||||
function toggleDiv(id) { const el = document.getElementById(id); el.style.display = (el.style.display === 'none') ? 'block' : 'none'; }
|
||||
function openSuiviModal(id) { document.getElementById(id).classList.add('open'); document.body.classList.add('no-scroll'); }
|
||||
function closeSuiviModal(id) { document.getElementById(id).classList.remove('open'); document.body.classList.remove('no-scroll');}
|
||||
|
||||
const suggestions = <?= json_encode(array_map(fn($c) => $c['suggestions'], $categoriesConfig)) ?>;
|
||||
|
||||
// Gestion dynamique du formulaire modal
|
||||
function handleModalCatChange(select) {
|
||||
const catKey = select.value;
|
||||
const conf = catConfigs[catKey] || {db_type: 'Expense', suggestions: []};
|
||||
|
||||
document.getElementById('blockInputText').style.display = 'none';
|
||||
document.getElementById('blockInputSelect').style.display = 'none';
|
||||
document.getElementById('blockInputText').style.display = 'block';
|
||||
document.getElementById('blockInputFrais').style.display = 'none';
|
||||
document.getElementById('blockInputIncome').style.display = 'none';
|
||||
|
||||
document.getElementById('modalLabelInput').required = false;
|
||||
document.getElementById('modalLabelInput').required = true;
|
||||
document.getElementById('fraisSelect').required = false;
|
||||
document.getElementById('incomeSelect').required = false;
|
||||
document.getElementById('fraisSelect').disabled = true;
|
||||
document.getElementById('incomeSelect').disabled = true;
|
||||
|
||||
if (catKey === 'School') { document.getElementById('blockInputSelect').style.display = 'block'; }
|
||||
else if (catKey === 'Frais') { document.getElementById('blockInputText').style.display = 'block'; document.getElementById('blockInputFrais').style.display = 'block'; document.getElementById('fraisSelect').required = true; document.getElementById('fraisSelect').disabled = false; }
|
||||
else if (catKey === 'Income') { document.getElementById('blockInputText').style.display = 'block'; document.getElementById('blockInputIncome').style.display = 'block'; document.getElementById('incomeSelect').required = true; document.getElementById('incomeSelect').disabled = false; }
|
||||
else {
|
||||
document.getElementById('blockInputText').style.display = 'block'; document.getElementById('modalLabelInput').required = true;
|
||||
const list = document.getElementById('modalSuggestions'); list.innerHTML = '';
|
||||
if (suggestions[catKey]) { suggestions[catKey].forEach(i => { const op = document.createElement('option'); op.value = i; list.appendChild(op); }); }
|
||||
if (conf.db_type === 'Income') {
|
||||
document.getElementById('blockInputIncome').style.display = 'block';
|
||||
document.getElementById('incomeSelect').disabled = false;
|
||||
document.getElementById('incomeSelect').required = true;
|
||||
} else {
|
||||
// Optionnel : lier l'Expense à une charge fixe
|
||||
document.getElementById('blockInputFrais').style.display = 'block';
|
||||
document.getElementById('fraisSelect').disabled = false;
|
||||
}
|
||||
|
||||
const list = document.getElementById('modalSuggestions'); list.innerHTML = '';
|
||||
if (conf.suggestions) {
|
||||
conf.suggestions.forEach(i => { const op = document.createElement('option'); op.value = i; list.appendChild(op); });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,8 +827,10 @@ function openAddModal(catKey, catLabel) {
|
||||
document.getElementById('modalGestionMonth').value = activeViewMonth;
|
||||
document.getElementById('modalLabelInput').value = "";
|
||||
document.getElementById('modalAmount').value = "";
|
||||
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = catKey; handleModalCatChange(catSelect);
|
||||
document.getElementById('modalIsCredit').value = (catKey === 'Income') ? "1" : "0";
|
||||
|
||||
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = catKey;
|
||||
handleModalCatChange(catSelect);
|
||||
document.getElementById('modalIsCredit').value = (catConfigs[catKey]?.db_type === 'Income') ? "1" : "0";
|
||||
}
|
||||
|
||||
function openEditModal(e) {
|
||||
@@ -821,21 +842,30 @@ function openEditModal(e) {
|
||||
document.getElementById('modalLabelInput').value = e.label;
|
||||
document.getElementById('modalIsCredit').value = parseFloat(e.amount) > 0 ? "1" : "0";
|
||||
document.getElementById('modalAmount').value = Math.abs(parseFloat(e.amount));
|
||||
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = e.category; handleModalCatChange(catSelect);
|
||||
if (e.category === 'Frais') document.getElementById('fraisSelect').value = e.budget_item_id;
|
||||
else if (e.category === 'Income') document.getElementById('incomeSelect').value = e.budget_item_id;
|
||||
|
||||
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = e.category;
|
||||
handleModalCatChange(catSelect);
|
||||
|
||||
if (catConfigs[e.category]?.db_type === 'Income') document.getElementById('incomeSelect').value = e.budget_item_id;
|
||||
else document.getElementById('fraisSelect').value = e.budget_item_id;
|
||||
}
|
||||
|
||||
// Gestion dynamique du CSV Import
|
||||
function handleLineCatChange(select, isInit = false) {
|
||||
const row = select.closest('tr');
|
||||
const fSel = row.querySelector('.select-frais'); const iSel = row.querySelector('.select-income');
|
||||
const fSel = row.querySelector('.select-frais');
|
||||
const iSel = row.querySelector('.select-income');
|
||||
const conf = catConfigs[select.value] || null;
|
||||
|
||||
fSel.style.display = 'none'; iSel.style.display = 'none';
|
||||
if (!isInit) { fSel.value = ''; iSel.value = ''; }
|
||||
fSel.disabled = true; iSel.disabled = true;
|
||||
|
||||
if (select.value === 'Frais') { fSel.style.display = 'block'; fSel.disabled = false; }
|
||||
else if (select.value === 'Income') { iSel.style.display = 'block'; iSel.disabled = false; }
|
||||
if (conf && conf.db_type === 'Income') {
|
||||
iSel.style.display = 'block'; iSel.disabled = false;
|
||||
} else if (conf) {
|
||||
fSel.style.display = 'block'; fSel.disabled = false;
|
||||
}
|
||||
checkValidation();
|
||||
}
|
||||
|
||||
@@ -844,16 +874,25 @@ function toggleAll(src) { document.querySelectorAll('.line-checkbox:not([disable
|
||||
function checkValidation() {
|
||||
let miss = 0;
|
||||
document.querySelectorAll('.line-checkbox:checked').forEach(cb => {
|
||||
const row = cb.closest('tr'); const isCrd = row.querySelector('.is-credit-flag').value === '1'; const cat = row.querySelector('.line-select').value;
|
||||
let v = true; if (cat==="") { if(!isCrd) v = false; } else if (cat==='Frais' && row.querySelector('.select-frais').value==="") v=false; else if (cat==='Income' && row.querySelector('.select-income').value==="") v=false;
|
||||
const row = cb.closest('tr');
|
||||
const isCrd = row.querySelector('.is-credit-flag').value === '1';
|
||||
const cat = row.querySelector('.line-select').value;
|
||||
const conf = catConfigs[cat] || null;
|
||||
|
||||
let v = true;
|
||||
if (cat === "") {
|
||||
if (!isCrd) v = false;
|
||||
} else if (conf && conf.db_type === 'Income' && row.querySelector('.select-income').value === "") {
|
||||
v = false;
|
||||
}
|
||||
if (!v) { miss++; row.style.background = '#fff1f2'; } else row.style.background = '';
|
||||
});
|
||||
const btn = document.getElementById('btnImport'); const msg = document.getElementById('missingCount');
|
||||
if(miss>0) { btn.disabled = true; btn.style.opacity=0.5; msg.style.display='inline'; msg.innerText = miss + ' ' + (window.I18N['bud_to_define_js'] || ''); }
|
||||
else { btn.disabled = false; btn.style.opacity=1; msg.style.display='none'; }
|
||||
if(miss > 0) { btn.disabled = true; btn.style.opacity = 0.5; msg.style.display = 'inline'; msg.innerText = miss + ' ' + (window.I18N['bud_to_define_js'] || ''); }
|
||||
else { btn.disabled = false; btn.style.opacity = 1; msg.style.display = 'none'; }
|
||||
}
|
||||
|
||||
if(document.getElementById('formMapping')) {
|
||||
if (document.getElementById('formMapping')) {
|
||||
document.querySelectorAll('.line-select').forEach(s => handleLineCatChange(s, true));
|
||||
checkValidation();
|
||||
}
|
||||
@@ -897,7 +936,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
submitBtn.innerText = '⏳ ...';
|
||||
|
||||
const formData = new FormData(formExpense);
|
||||
// Force l'action ici pour être sûr
|
||||
formData.set('action', 'save_expense_manual');
|
||||
|
||||
const actionUrl = formExpense.getAttribute('action') || 'modules/budget/includes/api/manage-item.php';
|
||||
|
||||
Reference in New Issue
Block a user