This commit is contained in:
@@ -154,6 +154,20 @@ foreach ($items as $item):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// C. Absorption des orphelines par catégories (Le lien Macro/Micro)
|
||||||
|
if (!empty($item['category'])) {
|
||||||
|
$catCodes = array_map('trim', explode(',', $item['category']));
|
||||||
|
foreach ($catCodes as $cCode) {
|
||||||
|
// Si la catégorie existe dans les orphelines et n'a pas encore été consommée
|
||||||
|
if (!empty($cCode) && isset($realTotalsByCat[$cCode]) && $realTotalsByCat[$cCode] != 0) {
|
||||||
|
$realSum += $realTotalsByCat[$cCode];
|
||||||
|
$hasMatchingExpense = true;
|
||||||
|
// On la vide pour ne pas la compter 2 fois si une autre ligne utilise la même catégorie
|
||||||
|
$realTotalsByCat[$cCode] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Finalisation des variables d'affichage
|
// 3. Finalisation des variables d'affichage
|
||||||
$realAbs = abs($realSum);
|
$realAbs = abs($realSum);
|
||||||
$isAutoChecked = ($hasMatchingExpense && ($realAbs >= ($targetAbs - 0.10)));
|
$isAutoChecked = ($hasMatchingExpense && ($realAbs >= ($targetAbs - 0.10)));
|
||||||
@@ -265,11 +279,12 @@ foreach ($items as $item):
|
|||||||
|
|
||||||
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<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>
|
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">
|
||||||
<select name="category" id="item_category" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;" required>
|
Catégorie(s) Cible(s) <small style="font-weight:normal;">(Maintenez Ctrl/Cmd pour choix multiple)</small>
|
||||||
<option value="" disabled selected>-- Choisir --</option>
|
</label>
|
||||||
|
<select name="category_multi[]" id="item_category" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white; min-height: 120px;" multiple required>
|
||||||
<?php foreach ($dbCategories as $c): ?>
|
<?php foreach ($dbCategories as $c): ?>
|
||||||
<?php if(strtolower($c['type']) === 'expense'): // On n'affiche que les catégories de dépenses ?>
|
<?php if(strtolower($c['type']) === 'expense'): ?>
|
||||||
<option value="<?= htmlspecialchars($c['code']) ?>">
|
<option value="<?= htmlspecialchars($c['code']) ?>">
|
||||||
<?= htmlspecialchars($c['icon'] . ' ' . $c['label']) ?>
|
<?= htmlspecialchars($c['icon'] . ' ' . $c['label']) ?>
|
||||||
</option>
|
</option>
|
||||||
@@ -327,7 +342,11 @@ function editRecapItem(item) {
|
|||||||
document.getElementById("item_name").value = data.name;
|
document.getElementById("item_name").value = data.name;
|
||||||
document.getElementById("item_keywords").value = data.mapping_keywords || '';
|
document.getElementById("item_keywords").value = data.mapping_keywords || '';
|
||||||
document.getElementById("item_amount").value = Math.abs(data.amount);
|
document.getElementById("item_amount").value = Math.abs(data.amount);
|
||||||
document.getElementById("item_category").value = data.category;
|
const selectedCats = data.category ? data.category.split(',') : [];
|
||||||
|
const selectEl = document.getElementById("item_category");
|
||||||
|
Array.from(selectEl.options).forEach(opt => {
|
||||||
|
opt.selected = selectedCats.includes(opt.value);
|
||||||
|
});
|
||||||
document.getElementById("item_type").value = data.type;
|
document.getElementById("item_type").value = data.type;
|
||||||
document.getElementById("item_is_estimate").value = data.is_estimate;
|
document.getElementById("item_is_estimate").value = data.is_estimate;
|
||||||
|
|
||||||
@@ -341,6 +360,11 @@ document.getElementById('recapForm').addEventListener('submit', async function(e
|
|||||||
btn.disabled = true; btn.innerText = '⏳ ...';
|
btn.disabled = true; btn.innerText = '⏳ ...';
|
||||||
|
|
||||||
const formData = new FormData(this);
|
const formData = new FormData(this);
|
||||||
|
|
||||||
|
const cats = formData.getAll('category_multi[]');
|
||||||
|
formData.delete('category_multi[]');
|
||||||
|
formData.append('category', cats.join(','));
|
||||||
|
|
||||||
formData.append('ajax', '1');
|
formData.append('ajax', '1');
|
||||||
const url = this.getAttribute('action');
|
const url = this.getAttribute('action');
|
||||||
|
|
||||||
|
|||||||
@@ -237,12 +237,15 @@ $fixedChargesList = []; $incomeList = []; $pending_charges = [];
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// MAPPING DYNAMIQUE DES BUDGETS PRÉVISIONNELS (NOUVELLE LOGIQUE)
|
// 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");
|
// On exclut "SAVINGS" pour s'aligner à 100% avec le recap.php
|
||||||
|
$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, mapping_keywords FROM pf_budget_items WHERE category != 'SAVINGS' ORDER BY name ASC");
|
||||||
|
$estimatesList = []; // On prépare la liste pour retenir nos estimations multi-catégories
|
||||||
|
|
||||||
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
$absAmount = abs((float)$item['amount']);
|
$absAmount = abs((float)$item['amount']);
|
||||||
$amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount;
|
$amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount;
|
||||||
$name = trim($item['name']);
|
$name = trim($item['name']);
|
||||||
$catCode = $item['category']; // Ex: FIXED, FMCG, INCOME ou income
|
$catCode = $item['category'];
|
||||||
|
|
||||||
$isIncome = (strtoupper($catCode) === 'INCOME');
|
$isIncome = (strtoupper($catCode) === 'INCOME');
|
||||||
|
|
||||||
@@ -251,18 +254,16 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
|||||||
$total_income += $amt;
|
$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])) {
|
if (!empty($catCode) && isset($categoriesConfig[$catCode])) {
|
||||||
$categoriesConfig[$catCode]['budget'] += $amt;
|
$categoriesConfig[$catCode]['budget'] += $amt;
|
||||||
} else {
|
} else {
|
||||||
$incomeCatKey = array_key_first(array_filter($categoriesConfig, fn($c) => $c['db_type'] === 'Income'));
|
$incomeCatKey = array_key_first(array_filter($categoriesConfig, fn($c) => $c['db_type'] === 'Income'));
|
||||||
if ($incomeCatKey) $categoriesConfig[$incomeCatKey]['budget'] += $amt;
|
if ($incomeCatKey) $categoriesConfig[$incomeCatKey]['budget'] += $amt;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
$total_expenses_prevues += $amt;
|
$total_expenses_prevues += $amt;
|
||||||
|
|
||||||
// C'est une charge fixe (Mensuel + Is_Estimate = 0)
|
// 1. C'est une charge fixe (Non-variable)
|
||||||
if ($item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
if ($item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
||||||
$fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
$fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
||||||
|
|
||||||
@@ -279,13 +280,28 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
|||||||
}
|
}
|
||||||
if (!$isPaid) {
|
if (!$isPaid) {
|
||||||
$reste_a_venir_calc += $absAmount;
|
$reste_a_venir_calc += $absAmount;
|
||||||
$pending_charges[] = ['name' => $name, 'amount' => $absAmount];
|
// Ajouté dans le tableau avec un tooltip vide
|
||||||
|
$pending_charges[] = ['name' => $name, 'amount' => $absAmount, 'tooltip' => 'Charge fixe en attente'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOUVEAU : Attribution du budget prévisionnel (le Plafond) via la catégorie dynamique
|
// 2. C'est une estimation (Variable)
|
||||||
if (!empty($catCode) && isset($categoriesConfig[$catCode])) {
|
if ((int)$item['is_estimate'] === 1) {
|
||||||
$categoriesConfig[$catCode]['budget'] += $amt;
|
$estimatesList[] = [
|
||||||
|
'name' => $name,
|
||||||
|
'amount' => $amt,
|
||||||
|
'categories' => !empty($catCode) ? array_map('trim', explode(',', $catCode)) : []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)) {
|
||||||
|
$catCodesArray = array_map('trim', explode(',', $catCode));
|
||||||
|
$firstCat = $catCodesArray[0];
|
||||||
|
if (isset($categoriesConfig[$firstCat])) {
|
||||||
|
$categoriesConfig[$firstCat]['budget'] += $amt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,16 +347,32 @@ foreach ($allExpenses as $exp) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RESTE A VENIR PAR CATEGORIE (Remplace les variables codées en dur)
|
// CALCUL DES RESTES À VENIR BASÉ SUR LES ESTIMATIONS (MACRO)
|
||||||
foreach ($categoriesConfig as $code => $conf) {
|
foreach ($estimatesList as $est) {
|
||||||
if ($conf['db_type'] === 'Expense' && $conf['budget'] > 0) {
|
$spentForEstimate = 0;
|
||||||
$spent = $totals[$code] ?? 0;
|
$detailsHover = [];
|
||||||
$rem = max(0, $conf['budget'] - $spent);
|
|
||||||
if ($rem > 0) {
|
// On additionne les dépenses de toutes les catégories liées à cette estimation
|
||||||
$reste_a_venir_calc += $rem;
|
foreach ($est['categories'] as $cCode) {
|
||||||
$pending_charges[] = ['name' => 'Reste ' . strip_tags($conf['label']), 'amount' => $rem];
|
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) . "€";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$rem = max(0, $est['amount'] - $spentForEstimate);
|
||||||
|
if ($rem > 0) {
|
||||||
|
$reste_a_venir_calc += $rem;
|
||||||
|
$tooltip = !empty($detailsHover) ? implode(' | ', $detailsHover) : 'Aucune dépense pour le moment';
|
||||||
|
$pending_charges[] = [
|
||||||
|
'name' => 'Reste ' . $est['name'],
|
||||||
|
'amount' => $rem,
|
||||||
|
'tooltip' => $tooltip
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// F. Calculs des KPIs finaux
|
// F. Calculs des KPIs finaux
|
||||||
@@ -468,7 +500,7 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
|||||||
<div style="text-align:center; font-style:italic; opacity:0.8;"><?= tr('bud_all_paid') ?></div>
|
<div style="text-align:center; font-style:italic; opacity:0.8;"><?= tr('bud_all_paid') ?></div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach($pending_charges as $pc): ?>
|
<?php foreach($pending_charges as $pc): ?>
|
||||||
<div style="display:flex; justify-content:space-between; margin-bottom:6px;">
|
<div style="display:flex; justify-content:space-between; margin-bottom:6px;" <?= !empty($pc['tooltip']) ? 'title="'.htmlspecialchars($pc['tooltip']).'" style="cursor:help;"' : '' ?>>
|
||||||
<span><?= htmlspecialchars($pc['name']) ?></span>
|
<span><?= htmlspecialchars($pc['name']) ?></span>
|
||||||
<strong><?= number_format($pc['amount'], 0, ',', ' ') ?> €</strong>
|
<strong><?= number_format($pc['amount'], 0, ',', ' ') ?> €</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user