fix recap et suivi
Deploy HouseHub / deploy (push) Successful in 2s

This commit is contained in:
2026-06-17 16:55:34 +02:00
parent f36bfa828e
commit 89d8ad114b
3 changed files with 104 additions and 37 deletions
+15 -8
View File
@@ -114,13 +114,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
$label = trim($_POST['label'] ?? ''); $label = trim($_POST['label'] ?? '');
$rawItemId = $_POST['budget_item_id'] ?? '';
$budgetItemId = null; $budgetItemId = null;
$salaryId = null;
$holidayId = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null; $holidayId = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
if ($cat === 'School' && !empty($_POST['label_select'])) { if ($cat === 'School' && !empty($_POST['label_select'])) {
$label = trim($_POST['label_select']); $label = trim($_POST['label_select']);
} elseif (($cat === 'Frais' || $cat === 'Income') && !empty($_POST['budget_item_id'])) { } elseif (!empty($rawItemId)) {
$budgetItemId = (int)$_POST['budget_item_id']; // 🔥 On intercepte si c'est un Salaire (SAL_x) ou une Charge Fixe classique (x)
if (strpos($rawItemId, 'SAL_') === 0) {
$salaryId = (int)str_replace('SAL_', '', $rawItemId);
} else {
$budgetItemId = (int)$rawItemId;
}
} }
// Vérification de sécurité // Vérification de sécurité
@@ -133,14 +140,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$finalAmount = $is_credit ? abs($amount) : -abs($amount); $finalAmount = $is_credit ? abs($amount) : -abs($amount);
if ($id) { if ($id) {
// UPDATE // UPDATE (Ajout de salary_id)
$pdo->prepare("UPDATE pf_expenses SET date_exp=?, gestion_month=?, category=?, label=?, amount=?, budget_item_id=?, holiday_id=? WHERE id=?") $pdo->prepare("UPDATE pf_expenses SET date_exp=?, gestion_month=?, category=?, label=?, amount=?, budget_item_id=?, holiday_id=?, salary_id=? WHERE id=?")
->execute([$date, $gestionMonth, $cat, $label, $finalAmount, $budgetItemId, $holidayId, $id]); ->execute([$date, $gestionMonth, $cat, $label, $finalAmount, $budgetItemId, $holidayId, $salaryId, $id]);
} else { } else {
// INSERT // INSERT (Ajout de salary_id)
$uniqueRef = "MANUAL_" . uniqid(); $uniqueRef = "MANUAL_" . uniqid();
$pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)") $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id, salary_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
->execute([$date, $gestionMonth, $cat, $label, $finalAmount, $uniqueRef, $budgetItemId, $holidayId]); ->execute([$date, $gestionMonth, $cat, $label, $finalAmount, $uniqueRef, $budgetItemId, $holidayId, $salaryId]);
} }
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
+29 -6
View File
@@ -7,7 +7,7 @@ $dbCategories = $stmtCats->fetchAll(PDO::FETCH_ASSOC);
// 2. Récupération des Salaires/Mensualités configurés (Revenus automatiques) // 2. Récupération des Salaires/Mensualités configurés (Revenus automatiques)
$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y'); $currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y');
$stmtSalaries = $pdo->prepare("SELECT person, mensualite FROM pf_salary_config WHERE year = ?"); $stmtSalaries = $pdo->prepare("SELECT id, person, mensualite FROM pf_salary_config WHERE year = ?");
$stmtSalaries->execute([$currentYear]); $stmtSalaries->execute([$currentYear]);
$salaries = $stmtSalaries->fetchAll(PDO::FETCH_ASSOC); $salaries = $stmtSalaries->fetchAll(PDO::FETCH_ASSOC);
@@ -44,6 +44,10 @@ foreach ($allExpenses as $row) {
} }
} }
$stmtSalariesReal = $pdo->prepare("SELECT salary_id, SUM(amount) as total_salary FROM pf_expenses WHERE gestion_month = ? AND salary_id IS NOT NULL GROUP BY salary_id");
$stmtSalariesReal->execute([$viewMonthDate]);
$realTotalsBySalary = $stmtSalariesReal->fetchAll(PDO::FETCH_KEY_PAIR);
// Récupération des détails pour le mapping par mots-clés (si besoin de précision) // Récupération des détails pour le mapping par mots-clés (si besoin de précision)
$stmtLabels = $pdo->prepare("SELECT label, amount, category FROM pf_expenses WHERE gestion_month = ? AND budget_item_id IS NULL"); $stmtLabels = $pdo->prepare("SELECT label, amount, category FROM pf_expenses WHERE gestion_month = ? AND budget_item_id IS NULL");
$stmtLabels->execute([$viewMonthDate]); $stmtLabels->execute([$viewMonthDate]);
@@ -77,23 +81,42 @@ $totalRevenus = 0;
<?php foreach ($salaries as $salary): <?php foreach ($salaries as $salary):
$mensualite = (float)$salary['mensualite']; $mensualite = (float)$salary['mensualite'];
$totalRevenus += $mensualite; $totalRevenus += $mensualite;
// Calcul du réel perçu
$realPerçu = isset($realTotalsBySalary[$salary['id']]) ? (float)$realTotalsBySalary[$salary['id']] : 0;
$gap = $realPerçu - $mensualite;
$isAutoChecked = ($realPerçu >= ($mensualite - 0.10));
?> ?>
<tr class="row-income" style="border-bottom:1px solid var(--border-light); background:#f0fdf4;"> <tr class="row-income" style="border-bottom:1px solid var(--border-light); background:#f0fdf4;">
<td style="padding:15px;"> <td style="padding:15px;">
<strong>Apport <?= htmlspecialchars($salary['person']) ?></strong> <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> <span title="Le montant cible est défini dans les paramètres" style="font-size:0.8rem; cursor:help;">⚙️</span>
</td> </td>
<td class="cell-amount" style="font-weight:600; padding:15px; color:#10b981;"> <td class="cell-amount" style="font-weight:600; padding:15px; color:#10b981;">
+ <?= number_format($mensualite, 2, ',', ' ') ?> € + <?= number_format($mensualite, 2, ',', ' ') ?> €
<?php if ($realPerçu > 0): ?>
<?php if ($gap > 0.05): ?>
<div style="font-size:0.75rem; color:#10b981; font-weight:bold;">Bonus : +<?= number_format($gap, 2, ',', ' ') ?> €</div>
<?php elseif ($gap < -0.05): ?>
<div style="font-size:0.75rem; color:#f59e0b;">Manque : <?= number_format(abs($gap), 2, ',', ' ') ?> €</div>
<?php else: ?>
<div style="font-size:0.75rem; color:#10b981;">Atteint ✓</div>
<?php endif; ?>
<?php endif; ?>
</td> </td>
<td style="padding:15px;"> <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> <span class="badge-type" style="background:#dcfce7; color:#16a34a; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600;">Revenu Mensuel</span>
</td> </td>
<td style="padding:15px; color:#64748b; font-weight:bold;">-</td> <td style="padding:15px; color:#64748b; font-weight:bold;">-</td>
<td style="padding:15px;"> <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;"> <?php if ($isAutoChecked): ?>
Auto <div style="display:inline-flex; align-items:center; gap:5px; background:#dcfce7; color:#16a34a; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #bbf7d0;"><span>✓</span> Reçu</div>
</div> <?php elseif ($realPerçu > 0): ?>
<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> En attente</div>
<?php endif; ?>
</td> </td>
<td style="padding:15px; text-align:right;"> <td style="padding:15px; text-align:right;">
<small style="color:#94a3b8;">Via Paramètres</small> <small style="color:#94a3b8;">Via Paramètres</small>
+59 -22
View File
@@ -59,7 +59,7 @@ if (isset($_POST['action']) && $_POST['action'] === 'reopen_month') {
if (isset($_POST['action']) && $_POST['action'] === 'save_import') { if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
$count = 0; $count = 0;
$stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); $stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id, salary_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$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)"); $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'])) { if (isset($_POST['lines']) && is_array($_POST['lines'])) {
@@ -67,7 +67,19 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
if (isset($line['import_check'])) { if (isset($line['import_check'])) {
$cat = $line['cat']; $cat = $line['cat'];
$is_credit = isset($line['is_credit']) ? (int)$line['is_credit'] : 0; $is_credit = isset($line['is_credit']) ? (int)$line['is_credit'] : 0;
$budgetItemId = !empty($line['budget_item_id']) ? (int)$line['budget_item_id'] : null;
// 🔥 NOUVEAU TRAITEMENT DU BUDGET_ITEM_ID
$rawItemId = $line['budget_item_id'] ?? '';
$budgetItemId = null;
$salaryId = null;
if (!empty($rawItemId)) {
if (strpos($rawItemId, 'SAL_') === 0) {
$salaryId = (int)str_replace('SAL_', '', $rawItemId);
} else {
$budgetItemId = (int)$rawItemId;
}
}
$holidayId = !empty($line['holiday_id']) ? (int)$line['holiday_id'] : null; $holidayId = !empty($line['holiday_id']) ? (int)$line['holiday_id'] : null;
$gestionMonthLine = !empty($line['gestion_month']) ? $line['gestion_month'] . '-01' : $viewMonthDate; $gestionMonthLine = !empty($line['gestion_month']) ? $line['gestion_month'] . '-01' : $viewMonthDate;
@@ -78,7 +90,7 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
$dateToSave = $line['date']; $dateToSave = $line['date'];
try { try {
$stmtExp->execute([$dateToSave, $gestionMonthLine, $cat, $line['label'], $finalAmount, $line['ref'], $budgetItemId, $holidayId]); $stmtExp->execute([$dateToSave, $gestionMonthLine, $cat, $line['label'], $finalAmount, $line['ref'], $budgetItemId, $holidayId, $salaryId]);
$stmtRule->execute([$line['label'], $cat, $budgetItemId]); $stmtRule->execute([$line['label'], $cat, $budgetItemId]);
$count++; $count++;
} catch (Exception $e) { continue; } } catch (Exception $e) { continue; }
@@ -232,7 +244,7 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
$name = trim($item['name']); $name = trim($item['name']);
$catCode = $item['category']; // Ex: FIXED, FMCG, INCOME ou income $catCode = $item['category']; // Ex: FIXED, FMCG, INCOME ou income
$isIncome = (strtoupper($catCode) === 'INCOME' || (float)$item['amount'] > 0); $isIncome = (strtoupper($catCode) === 'INCOME');
if ($isIncome) { if ($isIncome) {
$incomeList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount]; $incomeList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
@@ -278,6 +290,15 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
} }
} }
$stmtSalaries = $pdo->query("SELECT id, person, salary FROM pf_salary_config WHERE year = " . $viewY);
while ($sal = $stmtSalaries->fetch(PDO::FETCH_ASSOC)) {
$incomeList[] = [
'id' => 'SAL_' . $sal['id'],
'name' => 'Salaire ' . $sal['person'],
'amount' => (float)$sal['salary']
];
}
// L'enveloppe "Autres" prend tout le reste du budget non alloué // L'enveloppe "Autres" prend tout le reste du budget non alloué
$categoriesConfig['AUTRES']['budget'] = max(0, $total_income - $total_expenses_prevues); $categoriesConfig['AUTRES']['budget'] = max(0, $total_income - $total_expenses_prevues);
@@ -621,12 +642,13 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
</select> </select>
</div> </div>
<!-- 🛠️ FIX VISUEL : On demande la personne concernée (Bénéficiaire) -->
<div class="form-group" id="blockInputIncome" style="margin-bottom:15px; display:none;"> <div class="form-group" id="blockInputIncome" style="margin-bottom:15px; display:none;">
<label class="pf-label"><?= tr('bud_beneficiary') ?></label> <label class="pf-label">Lier à un Revenu Prévu (Optionnel)</label>
<select name="budget_item_id" id="incomeSelect" class="pf-input" disabled> <select name="budget_item_id" id="incomeSelect" class="pf-input" disabled>
<option value=""><?= tr('gift_filter_all_adults') ?></option> <option value="">-- Aucun --</option>
<?php foreach ($incomeList as $inc): ?><option value="<?= $inc['id'] ?>"><?= htmlspecialchars($inc['name']) ?></option><?php endforeach; ?> <?php foreach ($incomeList as $inc): ?>
<option value="<?= $inc['id'] ?>"><?= htmlspecialchars($inc['name']) ?></option>
<?php endforeach; ?>
</select> </select>
</div> </div>
@@ -792,8 +814,8 @@ function closeSuiviModal(id) { document.getElementById(id).classList.remove('ope
// Gestion dynamique du formulaire modal // Gestion dynamique du formulaire modal
function handleModalCatChange(select) { function handleModalCatChange(select) {
const catKey = select.value; const catKey = select.value.toUpperCase(); // Sécurité sur la casse
const conf = catConfigs[catKey] || {db_type: 'Expense', suggestions: []}; const conf = catConfigs[select.value] || {db_type: 'Expense', suggestions: []};
document.getElementById('blockInputText').style.display = 'block'; document.getElementById('blockInputText').style.display = 'block';
document.getElementById('blockInputFrais').style.display = 'none'; document.getElementById('blockInputFrais').style.display = 'none';
@@ -806,13 +828,17 @@ function handleModalCatChange(select) {
document.getElementById('incomeSelect').disabled = true; document.getElementById('incomeSelect').disabled = true;
if (conf.db_type === 'Income') { if (conf.db_type === 'Income') {
document.getElementById('blockInputIncome').style.display = 'block'; const incSel = document.getElementById('incomeSelect');
document.getElementById('incomeSelect').disabled = false; // 👁️ Ne s'affiche que s'il y a des revenus prévus à lier, sinon reste invisible
document.getElementById('incomeSelect').required = true; if (incSel.options.length > 1) {
} else { document.getElementById('blockInputIncome').style.display = 'block';
// Optionnel : lier l'Expense à une charge fixe incSel.disabled = false;
}
}
else if (catKey === 'FIXED') { // 🔒 Strictement pour les Charges Fixes
document.getElementById('blockInputFrais').style.display = 'block'; document.getElementById('blockInputFrais').style.display = 'block';
document.getElementById('fraisSelect').disabled = false; document.getElementById('fraisSelect').disabled = false;
document.getElementById('fraisSelect').required = true; // 🔥 Devient OBLIGATOIRE
} }
const list = document.getElementById('modalSuggestions'); list.innerHTML = ''; const list = document.getElementById('modalSuggestions'); list.innerHTML = '';
@@ -857,6 +883,7 @@ function handleLineCatChange(select, isInit = false) {
const row = select.closest('tr'); const row = select.closest('tr');
const fSel = row.querySelector('.select-frais'); const fSel = row.querySelector('.select-frais');
const iSel = row.querySelector('.select-income'); const iSel = row.querySelector('.select-income');
const catKey = select.value.toUpperCase();
const conf = catConfigs[select.value] || null; const conf = catConfigs[select.value] || null;
fSel.style.display = 'none'; iSel.style.display = 'none'; fSel.style.display = 'none'; iSel.style.display = 'none';
@@ -864,8 +891,12 @@ function handleLineCatChange(select, isInit = false) {
fSel.disabled = true; iSel.disabled = true; fSel.disabled = true; iSel.disabled = true;
if (conf && conf.db_type === 'Income') { if (conf && conf.db_type === 'Income') {
iSel.style.display = 'block'; iSel.disabled = false; // 👁️ Ne s'affiche que s'il y a des revenus à lier
} else if (conf) { if (iSel.options.length > 1) {
iSel.style.display = 'block'; iSel.disabled = false;
}
}
else if (catKey === 'FIXED') { // 🔒 Strictement pour les Charges Fixes
fSel.style.display = 'block'; fSel.disabled = false; fSel.style.display = 'block'; fSel.disabled = false;
} }
checkValidation(); checkValidation();
@@ -879,18 +910,23 @@ function checkValidation() {
const row = cb.closest('tr'); const row = cb.closest('tr');
const isCrd = row.querySelector('.is-credit-flag').value === '1'; const isCrd = row.querySelector('.is-credit-flag').value === '1';
const cat = row.querySelector('.line-select').value; const cat = row.querySelector('.line-select').value;
const conf = catConfigs[cat] || null; const catKey = cat.toUpperCase();
const fSel = row.querySelector('.select-frais');
let v = true; let v = true;
if (cat === "") { if (cat === "") {
// La catégorie est obligatoire pour les dépenses (les revenus sont acceptés sans cat si souhaité)
if (!isCrd) v = false; if (!isCrd) v = false;
} else if (conf && conf.db_type === 'Income' && row.querySelector('.select-income').value === "") {
v = false;
} }
else if (catKey === 'FIXED' && fSel.value === "") {
v = false; // 🔥 Bloque l'import si une Charge Fixe n'est pas liée à une ligne du budget !
}
if (!v) { miss++; row.style.background = '#fff1f2'; } else row.style.background = ''; if (!v) { miss++; row.style.background = '#fff1f2'; } else row.style.background = '';
}); });
const btn = document.getElementById('btnImport'); const msg = document.getElementById('missingCount'); 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'] || ''); } if(miss > 0) { btn.disabled = true; btn.style.opacity = 0.5; msg.style.display = 'inline'; msg.innerText = miss + ' ' + (window.I18N['bud_to_define_js'] || 'à définir'); }
else { btn.disabled = false; btn.style.opacity = 1; msg.style.display = 'none'; } else { btn.disabled = false; btn.style.opacity = 1; msg.style.display = 'none'; }
} }
@@ -901,7 +937,8 @@ if (document.getElementById('formMapping')) {
window.addEventListener('click', (e) => { window.addEventListener('click', (e) => {
if (e.target.classList.contains('pf-modal')) { if (e.target.classList.contains('pf-modal')) {
e.target.style.display = 'none'; e.target.classList.remove('open', 'is-active');
e.target.style.display = '';
document.body.classList.remove('no-scroll'); document.body.classList.remove('no-scroll');
} }
}); });