signe expense
This commit is contained in:
@@ -10,18 +10,26 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if ($action === 'save') {
|
if ($action === 'save') {
|
||||||
$id = $_POST['id'] ?? '';
|
$id = $_POST['id'] ?? '';
|
||||||
$name = $_POST['name'];
|
$name = $_POST['name'];
|
||||||
$amount = $_POST['amount'];
|
|
||||||
$category = $_POST['category'];
|
$category = $_POST['category'];
|
||||||
$type = $_POST['type'];
|
$type = $_POST['type'];
|
||||||
$payment_day = $_POST['payment_day'];
|
$payment_day = empty($_POST['payment_day']) ? null : (int)$_POST['payment_day'];
|
||||||
$is_estimate = $_POST['is_estimate'];
|
$is_estimate = $_POST['is_estimate'];
|
||||||
$reg_month = $_POST['reg_month'];
|
$reg_month = $_POST['reg_month'];
|
||||||
|
|
||||||
$keywords = $_POST['mapping_keywords'] ?? '';
|
$keywords = $_POST['mapping_keywords'] ?? '';
|
||||||
|
|
||||||
// NOUVEAU : Récupération de l'ID des vacances
|
|
||||||
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
|
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// NOUVELLE NORME COMPTABLE : Dépense = Négatif
|
||||||
|
// ==========================================
|
||||||
|
// On récupère le montant en valeur absolue (pour éviter les erreurs si l'utilisateur a tapé un '-')
|
||||||
|
$amount = abs((float)$_POST['amount']);
|
||||||
|
|
||||||
|
// Si c'est un frais, on l'enregistre en négatif
|
||||||
|
if ($category === 'expense') {
|
||||||
|
$amount = -$amount;
|
||||||
|
}
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
if ($id) {
|
if ($id) {
|
||||||
// UPDATE
|
// UPDATE
|
||||||
$stmt = $pdo->prepare("UPDATE pf_budget_items SET name=?, amount=?, category=?, type=?, payment_day=?, is_estimate=?, reg_month=?, mapping_keywords=?, holiday_id=? WHERE id=?");
|
$stmt = $pdo->prepare("UPDATE pf_budget_items SET name=?, amount=?, category=?, type=?, payment_day=?, is_estimate=?, reg_month=?, mapping_keywords=?, holiday_id=? WHERE id=?");
|
||||||
@@ -31,6 +39,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$stmt = $pdo->prepare("INSERT INTO pf_budget_items (name, amount, category, type, payment_day, is_estimate, reg_month, mapping_keywords, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
$stmt = $pdo->prepare("INSERT INTO pf_budget_items (name, amount, category, type, payment_day, is_estimate, reg_month, mapping_keywords, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
$stmt->execute([$name, $amount, $category, $type, $payment_day, $is_estimate, $reg_month, $keywords, $holiday_id]);
|
$stmt->execute([$name, $amount, $category, $type, $payment_day, $is_estimate, $reg_month, $keywords, $holiday_id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Redirection
|
||||||
header('Location: /budget.php?tab=recap');
|
header('Location: /budget.php?tab=recap');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ input[type="number"].no-spinners {
|
|||||||
<td class="text-center text-muted">
|
<td class="text-center text-muted">
|
||||||
<?php if ($amount != 0): ?>
|
<?php if ($amount != 0): ?>
|
||||||
<div class="cell-content">
|
<div class="cell-content">
|
||||||
<span>- <?= number_format($amount, 0, ',', ' ') ?> €</span>
|
<span style="color:#1e293b; font-weight:500;"><?= number_format($amount, 0, ',', ' ') ?> €</span>
|
||||||
<button class="btn-cell-delete"
|
<button class="btn-cell-delete"
|
||||||
onclick="deleteSavingsEntry('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= $currentOwner ?>')">
|
onclick="deleteSavingsEntry('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= $currentOwner ?>')">
|
||||||
×
|
×
|
||||||
@@ -140,6 +140,7 @@ input[type="number"].no-spinners {
|
|||||||
<?php foreach ($months as $month):
|
<?php foreach ($months as $month):
|
||||||
$total = $data[$month]['TOTAL_BANQUE'] ?? 0;
|
$total = $data[$month]['TOTAL_BANQUE'] ?? 0;
|
||||||
$sum = 0;
|
$sum = 0;
|
||||||
|
// Le calcul reste identique : Total de la banque - Somme des enveloppes
|
||||||
foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0);
|
foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0);
|
||||||
$extra = $total - $sum;
|
$extra = $total - $sum;
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ $totalRevenus = 0;
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($items as $item):
|
<?php foreach ($items as $item):
|
||||||
// --- 1. CALCUL DES TOTAUX PRÉVUS ---
|
// --- 1. CALCUL DES TOTAUX PRÉVUS ---
|
||||||
$amountToAdd = ($item['type'] === 'Annuel') ? $item['amount'] / 12 : $item['amount'];
|
$targetAbs = abs((float)$item['amount']); // On utilise l'absolu pour l'affichage
|
||||||
|
$amountToAdd = ($item['type'] === 'Annuel') ? $targetAbs / 12 : $targetAbs;
|
||||||
|
|
||||||
if ($item['category'] === 'expense') $totalDepenses += $amountToAdd;
|
if ($item['category'] === 'expense') $totalDepenses += $amountToAdd;
|
||||||
else $totalRevenus += $amountToAdd;
|
else $totalRevenus += $amountToAdd;
|
||||||
|
|
||||||
@@ -63,19 +65,20 @@ $totalRevenus = 0;
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($match) {
|
if ($match) {
|
||||||
if ($item['category'] === 'income') {
|
// Avec la nouvelle norme, on additionne simplement les montants.
|
||||||
$realSum += abs((float)$exp['amount']);
|
// Si c'est une dépense, ça s'additionne en négatif.
|
||||||
} else {
|
// S'il y a un remboursement partiel, le "+" vient réduire le "-". C'est mathématiquement parfait.
|
||||||
$realSum += (float)$exp['amount'];
|
$realSum += (float)$exp['amount'];
|
||||||
}
|
|
||||||
$hasMatchingExpense = true;
|
$hasMatchingExpense = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 3. LOGIQUE D'ÉTAT (Uniquement dynamique) ---
|
// On convertit le résultat réel en positif pour simplifier la comparaison visuelle UI
|
||||||
$gap = $realSum - $item['amount'];
|
$realAbs = abs($realSum);
|
||||||
|
|
||||||
|
// --- 3. LOGIQUE D'ÉTAT ---
|
||||||
$isAutoChecked = false;
|
$isAutoChecked = false;
|
||||||
if ($hasMatchingExpense && ($realSum >= ($item['amount'] - 0.10))) {
|
if ($hasMatchingExpense && ($realAbs >= ($targetAbs - 0.10))) {
|
||||||
$isAutoChecked = true;
|
$isAutoChecked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,12 +103,17 @@ $totalRevenus = 0;
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="cell-amount" style="font-weight:600; padding:15px; color:<?= $item['category']==='income'?'#10b981':'#1e293b' ?>;">
|
<td class="cell-amount" style="font-weight:600; padding:15px; color:<?= $item['category']==='income'?'#10b981':'#1e293b' ?>;">
|
||||||
<?= number_format($item['amount'], 2, ',', ' ') ?> €
|
<?= number_format($targetAbs, 2, ',', ' ') ?> €
|
||||||
|
|
||||||
<?php if ($hasMatchingExpense): ?>
|
<?php if ($hasMatchingExpense): ?>
|
||||||
<?php if ($gap > 0.05): ?>
|
<?php
|
||||||
|
// Le "Gap" est la différence visuelle.
|
||||||
|
// S'il y a plus de $realAbs que prévu, c'est un dépassement ou un bonus.
|
||||||
|
$gap = $realAbs - $targetAbs;
|
||||||
|
|
||||||
|
if ($gap > 0.05): ?>
|
||||||
<div style="font-size:0.75rem; color:#ef4444; font-weight:bold;">
|
<div style="font-size:0.75rem; color:#ef4444; font-weight:bold;">
|
||||||
Dépassement : +<?= number_format($gap, 2, ',', ' ') ?> €
|
<?= $item['category'] === 'income' ? 'Bonus :' : 'Dépassement :' ?> +<?= number_format($gap, 2, ',', ' ') ?> €
|
||||||
</div>
|
</div>
|
||||||
<?php elseif ($gap < -0.05): ?>
|
<?php elseif ($gap < -0.05): ?>
|
||||||
<div style="font-size:0.75rem; color:#f59e0b; font-weight:normal;">
|
<div style="font-size:0.75rem; color:#f59e0b; font-weight:normal;">
|
||||||
@@ -198,7 +206,7 @@ $totalRevenus = 0;
|
|||||||
<div class="form-group" style="margin-bottom:15px;">
|
<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 (Détection Auto)</label>
|
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Mots-clés (Détection Auto)</label>
|
||||||
<input type="text" name="mapping_keywords" id="item_keywords" class="pf-input" placeholder="Ex: NETFLIX, PRIME VIDEO (séparés par virgule)" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:#f0f9ff; border-color:#bae6fd;">
|
<input type="text" name="mapping_keywords" id="item_keywords" class="pf-input" placeholder="Ex: NETFLIX, PRIME VIDEO (séparés par virgule)" 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;">Si une dépense du mois contient ce mot, la ligne passera en "Payé".</small>
|
<small style="color:#64748b; font-size:0.75rem;">Si une dépense du mois contient ce mot, la ligne passera en "Validé".</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
||||||
@@ -285,7 +293,8 @@ function editRecapItem(item) {
|
|||||||
document.getElementById("item_id").value = data.id;
|
document.getElementById("item_id").value = data.id;
|
||||||
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 = data.amount;
|
// On affiche toujours la valeur en positif dans le formulaire
|
||||||
|
document.getElementById("item_amount").value = Math.abs(data.amount);
|
||||||
document.getElementById("item_category").value = data.category;
|
document.getElementById("item_category").value = data.category;
|
||||||
document.getElementById("item_type").value = data.type;
|
document.getElementById("item_type").value = data.type;
|
||||||
document.getElementById("item_day").value = data.payment_day;
|
document.getElementById("item_day").value = data.payment_day;
|
||||||
|
|||||||
@@ -5,12 +5,10 @@
|
|||||||
// 1. GESTION DES ACTIONS ET DE LA NAVIGATION
|
// 1. GESTION DES ACTIONS ET DE LA NAVIGATION
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// --- NAVIGATION PAR MOIS ---
|
|
||||||
$currentMonth = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEFT) : date('m');
|
$currentMonth = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEFT) : date('m');
|
||||||
$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y');
|
$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y');
|
||||||
$currentMonthKey = $currentMonth . '-' . $currentYear;
|
$currentMonthKey = $currentMonth . '-' . $currentYear;
|
||||||
|
|
||||||
// Calcul des liens de navigation
|
|
||||||
$prevM = (int)$currentMonth - 1; $prevY = $currentYear;
|
$prevM = (int)$currentMonth - 1; $prevY = $currentYear;
|
||||||
if ($prevM < 1) { $prevM = 12; $prevY--; }
|
if ($prevM < 1) { $prevM = 12; $prevY--; }
|
||||||
$nextM = (int)$currentMonth + 1; $nextY = $currentYear;
|
$nextM = (int)$currentMonth + 1; $nextY = $currentYear;
|
||||||
@@ -23,10 +21,13 @@ $todayLink = "?tab=suivi";
|
|||||||
// A. AJOUT CATÉGORIE TEMPORAIRE MANUELLE
|
// A. AJOUT CATÉGORIE TEMPORAIRE MANUELLE
|
||||||
if (isset($_POST['action']) && $_POST['action'] === 'add_temp_cat') {
|
if (isset($_POST['action']) && $_POST['action'] === 'add_temp_cat') {
|
||||||
$name = trim($_POST['cat_name']);
|
$name = trim($_POST['cat_name']);
|
||||||
$budget = floatval($_POST['cat_budget']);
|
$budget = floatval($_POST['cat_budget']); // Saisi en positif par l'utilisateur
|
||||||
$type = $_POST['cat_type'] === 'credit' ? 'credit' : 'debit';
|
$type = $_POST['cat_type'] === 'credit' ? 'credit' : 'debit';
|
||||||
|
|
||||||
if ($name && $budget >= 0) {
|
// Si c'est un budget alloué (débit), on le stocke en négatif
|
||||||
|
if ($type === 'debit') $budget = -$budget;
|
||||||
|
|
||||||
|
if ($name) {
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_monthly_categories (month_year, name, type, budget) VALUES (?, ?, ?, ?)");
|
$stmt = $pdo->prepare("INSERT INTO pf_monthly_categories (month_year, name, type, budget) VALUES (?, ?, ?, ?)");
|
||||||
$stmt->execute([$currentMonthKey, $name, $type, $budget]);
|
$stmt->execute([$currentMonthKey, $name, $type, $budget]);
|
||||||
header("Location: ?tab=suivi&m=$currentMonth&y=$currentYear"); exit;
|
header("Location: ?tab=suivi&m=$currentMonth&y=$currentYear"); exit;
|
||||||
@@ -48,7 +49,7 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_snapshot') {
|
|||||||
header("Location: ?tab=suivi&m=$currentMonth&y=$currentYear"); exit;
|
header("Location: ?tab=suivi&m=$currentMonth&y=$currentYear"); exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// D. SAUVEGARDE IMPORT CSV (LIGNE PAR LIGNE AVEC CHECKBOX)
|
// D. SAUVEGARDE IMPORT CSV
|
||||||
if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
|
if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
|
||||||
$count = 0;
|
$count = 0;
|
||||||
$viewMonth = $_POST['view_month'] ?? $currentMonth;
|
$viewMonth = $_POST['view_month'] ?? $currentMonth;
|
||||||
@@ -81,7 +82,8 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
|
|||||||
$cat = 'TEMP_' . $tempCatMapping[$cat];
|
$cat = 'TEMP_' . $tempCatMapping[$cat];
|
||||||
}
|
}
|
||||||
|
|
||||||
$finalAmount = $is_credit ? -abs($line['amount']) : abs($line['amount']);
|
// LOGIQUE INVERSÉE : Crédit = Positif, Débit = Négatif
|
||||||
|
$finalAmount = $is_credit ? abs($line['amount']) : -abs($line['amount']);
|
||||||
|
|
||||||
$dateToSave = $line['date'];
|
$dateToSave = $line['date'];
|
||||||
if (!empty($line['force_current'])) {
|
if (!empty($line['force_current'])) {
|
||||||
@@ -125,7 +127,9 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_expense_manual') {
|
|||||||
|
|
||||||
if ($label && $amount > 0) {
|
if ($label && $amount > 0) {
|
||||||
$is_credit = isset($_POST['is_credit']) ? (int)$_POST['is_credit'] : 0;
|
$is_credit = isset($_POST['is_credit']) ? (int)$_POST['is_credit'] : 0;
|
||||||
$finalAmount = $is_credit ? -abs($amount) : abs($amount);
|
|
||||||
|
// LOGIQUE INVERSÉE : Crédit = Positif, Débit = Négatif
|
||||||
|
$finalAmount = $is_credit ? abs($amount) : -abs($amount);
|
||||||
|
|
||||||
if ($id) {
|
if ($id) {
|
||||||
$pdo->prepare("UPDATE pf_expenses SET date_exp=?, category=?, label=?, amount=?, budget_item_id=?, holiday_id=? WHERE id=?")
|
$pdo->prepare("UPDATE pf_expenses SET date_exp=?, category=?, label=?, amount=?, budget_item_id=?, holiday_id=? WHERE id=?")
|
||||||
@@ -160,15 +164,17 @@ $today_day = (int)date('j');
|
|||||||
$fixedChargesList = [];
|
$fixedChargesList = [];
|
||||||
$incomeList = [];
|
$incomeList = [];
|
||||||
|
|
||||||
$stmtIds = $pdo->prepare("SELECT DISTINCT budget_item_id FROM pf_expenses WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ? AND budget_item_id IS NOT NULL AND amount > 0");
|
// On sélectionne les charges qui ont un montant NÉGATIF ou POSITIF selon la nouvelle règle (plus besoin de filtrer sur > 0)
|
||||||
|
$stmtIds = $pdo->prepare("SELECT DISTINCT budget_item_id FROM pf_expenses WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ? AND budget_item_id IS NOT NULL");
|
||||||
$stmtIds->execute([$currentMonth, $currentYear]);
|
$stmtIds->execute([$currentMonth, $currentYear]);
|
||||||
$paidItemIds = $stmtIds->fetchAll(PDO::FETCH_COLUMN);
|
$paidItemIds = $stmtIds->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
$stmtLabels = $pdo->prepare("SELECT label FROM pf_expenses WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ? AND amount > 0");
|
// On récupère uniquement les libellés des dépenses (montant < 0)
|
||||||
|
$stmtLabels = $pdo->prepare("SELECT label FROM pf_expenses WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ? AND amount < 0");
|
||||||
$stmtLabels->execute([$currentMonth, $currentYear]);
|
$stmtLabels->execute([$currentMonth, $currentYear]);
|
||||||
$realExpensesLabels = $stmtLabels->fetchAll(PDO::FETCH_COLUMN);
|
$realExpensesLabels = $stmtLabels->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
// Snapshot Bancaire (Sert uniquement pour l'affichage "Actuel", plus pour le "Théorique")
|
// Snapshot Bancaire
|
||||||
$snapshot = ['date' => date('Y-m-d'), 'amount' => 0];
|
$snapshot = ['date' => date('Y-m-d'), 'amount' => 0];
|
||||||
try {
|
try {
|
||||||
$snapStmt = $pdo->query("SELECT * FROM pf_bank_snapshots ORDER BY id DESC LIMIT 1");
|
$snapStmt = $pdo->query("SELECT * FROM pf_bank_snapshots ORDER BY id DESC LIMIT 1");
|
||||||
@@ -179,20 +185,22 @@ try {
|
|||||||
|
|
||||||
$solde_actuel = $snapshot['amount'];
|
$solde_actuel = $snapshot['amount'];
|
||||||
|
|
||||||
// Lecture Budget Prévisionnel (Sert pour les cartes et pour calculer le Reste à venir)
|
// Lecture Budget Prévisionnel
|
||||||
$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, is_checked, mapping_keywords FROM pf_budget_items ORDER BY name ASC");
|
$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, is_checked, mapping_keywords FROM pf_budget_items ORDER BY name ASC");
|
||||||
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
// Les montants en base ont leur vrai signe (- pour dépense, + pour revenu)
|
||||||
$rawAmount = (float)$item['amount'];
|
$rawAmount = (float)$item['amount'];
|
||||||
$amt = ($item['type'] === 'Annuel') ? $rawAmount / 12 : $rawAmount;
|
$absAmount = abs($rawAmount); // On utilise l'absolu pour le visuel des jauges
|
||||||
|
$amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount;
|
||||||
$name = trim($item['name']);
|
$name = trim($item['name']);
|
||||||
$pDay = (int)$item['payment_day'];
|
$pDay = (int)$item['payment_day'];
|
||||||
$isChecked = (int)$item['is_checked'];
|
$isChecked = (int)$item['is_checked'];
|
||||||
|
|
||||||
if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
||||||
$fixedChargesList[] = $item;
|
$fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount]; // Passé en array propre pour l'HTML
|
||||||
}
|
}
|
||||||
if ($item['category'] === 'income') {
|
if ($item['category'] === 'income') {
|
||||||
$incomeList[] = $item;
|
$incomeList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
||||||
$total_income += $amt;
|
$total_income += $amt;
|
||||||
$budget_income_prevu += $amt;
|
$budget_income_prevu += $amt;
|
||||||
} else {
|
} else {
|
||||||
@@ -215,9 +223,8 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Somme de toutes les charges encore à venir
|
|
||||||
if (!$isPaid) {
|
if (!$isPaid) {
|
||||||
$reste_a_venir += $rawAmount;
|
$reste_a_venir += $absAmount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +232,7 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
|||||||
elseif ($name === 'Estimacio escola') $budget_school = $amt;
|
elseif ($name === 'Estimacio escola') $budget_school = $amt;
|
||||||
elseif ($name === 'Estimation gasolina') $budget_essence = $amt;
|
elseif ($name === 'Estimation gasolina') $budget_essence = $amt;
|
||||||
elseif ((int)$item['is_estimate'] === 0 && $item['type'] === 'Mensuel' && $item['category'] === 'expense') {
|
elseif ((int)$item['is_estimate'] === 0 && $item['type'] === 'Mensuel' && $item['category'] === 'expense') {
|
||||||
$budget_frais += $rawAmount;
|
$budget_frais += $absAmount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,7 +245,7 @@ try {
|
|||||||
$tempCats = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$tempCats = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
foreach($tempCats as $tc) {
|
foreach($tempCats as $tc) {
|
||||||
if ($tc['type'] === 'debit') {
|
if ($tc['type'] === 'debit') {
|
||||||
$total_temp_budget += $tc['budget'];
|
$total_temp_budget += abs($tc['budget']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {}
|
} catch (Exception $e) {}
|
||||||
@@ -263,7 +270,7 @@ $colorIdx = 0;
|
|||||||
foreach ($tempCats as $tc) {
|
foreach ($tempCats as $tc) {
|
||||||
$catKey = 'TEMP_' . $tc['id'];
|
$catKey = 'TEMP_' . $tc['id'];
|
||||||
$categoriesConfig[$catKey] = [
|
$categoriesConfig[$catKey] = [
|
||||||
'type' => $tc['type'], 'label' => $tc['name'], 'budget' => $tc['budget'], 'color' => $tempColors[$colorIdx++ % count($tempColors)], 'suggestions' => [], 'is_temp' => true, 'id' => $tc['id']
|
'type' => $tc['type'], 'label' => $tc['name'], 'budget' => abs($tc['budget']), 'color' => $tempColors[$colorIdx++ % count($tempColors)], 'suggestions' => [], 'is_temp' => true, 'id' => $tc['id']
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
$categoriesConfig['Autres'] = ['type'=>'debit', 'label'=>'Autres / Imprévus', 'budget'=>$budget_autres, 'color'=>'#64748b', 'suggestions'=>['Restaurant', 'Cadeau']];
|
$categoriesConfig['Autres'] = ['type'=>'debit', 'label'=>'Autres / Imprévus', 'budget'=>$budget_autres, 'color'=>'#64748b', 'suggestions'=>['Restaurant', 'Cadeau']];
|
||||||
@@ -275,7 +282,32 @@ $categoriesConfig['LivretA'] = ['type'=>'debit', 'label'=>'Epargne', 'budget'=>0
|
|||||||
|
|
||||||
$csvData = [];
|
$csvData = [];
|
||||||
$showPreview = false;
|
$showPreview = false;
|
||||||
// ... (Gestion de l'upload CSV inchangée, elle se fait plus haut) ...
|
if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
|
||||||
|
$file = $_FILES['csv_file']['tmp_name'];
|
||||||
|
$handle = fopen($file, "r");
|
||||||
|
$rules = []; try { $rules = $pdo->query("SELECT keyword, category FROM pf_import_rules")->fetchAll(PDO::FETCH_KEY_PAIR); } catch(Exception $e){}
|
||||||
|
$existingRefs = []; try { $existingRefs = $pdo->query("SELECT import_ref FROM pf_expenses WHERE import_ref IS NOT NULL")->fetchAll(PDO::FETCH_COLUMN); } catch(Exception $e){}
|
||||||
|
fgetcsv($handle, 1000, ";", "\"", "\\");
|
||||||
|
while (($data = fgetcsv($handle, 1000, ";", "\"", "\\")) !== FALSE) {
|
||||||
|
$rawDebit = $data[8] ?? ''; $rawCredit = $data[9] ?? '';
|
||||||
|
$amount = 0; $isCredit = 0;
|
||||||
|
if (!empty(trim($rawCredit))) { $amount = abs((float)str_replace(',', '.', str_replace(' ', '', $rawCredit))); $isCredit = 1; }
|
||||||
|
elseif (!empty(trim($rawDebit))) { $amount = abs((float)str_replace(',', '.', str_replace(' ', '', $rawDebit))); }
|
||||||
|
else continue;
|
||||||
|
|
||||||
|
$dateParts = explode('/', $data[0]);
|
||||||
|
$dateSql = (count($dateParts) == 3) ? $dateParts[2].'-'.$dateParts[1].'-'.$dateParts[0] : date('Y-m-d');
|
||||||
|
$label = trim($data[1]) ?: trim($data[2]);
|
||||||
|
$refCSV = trim($data[3]);
|
||||||
|
$uniqueKey = !empty($refCSV) ? "REF_".$refCSV : "HASH_".md5($dateSql.$label.number_format($amount, 2).$isCredit);
|
||||||
|
$isDuplicate = in_array($uniqueKey, $existingRefs);
|
||||||
|
$suggestedCat = '';
|
||||||
|
foreach ($rules as $kw => $c) { if (stripos($label, $kw) !== false) { $suggestedCat = $c; break; } }
|
||||||
|
$csvData[] = ['date'=>$dateSql, 'label'=>$label, 'amount'=>$amount, 'cat'=>$suggestedCat, 'ref'=>$uniqueKey, 'is_duplicate'=>$isDuplicate, 'is_credit'=>$isCredit];
|
||||||
|
}
|
||||||
|
fclose($handle);
|
||||||
|
$showPreview = true;
|
||||||
|
}
|
||||||
|
|
||||||
// DÉPENSES EN BDD
|
// DÉPENSES EN BDD
|
||||||
$stmt = $pdo->prepare("SELECT * FROM pf_expenses WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ? ORDER BY date_exp DESC");
|
$stmt = $pdo->prepare("SELECT * FROM pf_expenses WHERE MONTH(date_exp) = ? AND YEAR(date_exp) = ? ORDER BY date_exp DESC");
|
||||||
@@ -285,7 +317,6 @@ $allExpenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
$totals = array_fill_keys(array_keys($categoriesConfig), 0);
|
$totals = array_fill_keys(array_keys($categoriesConfig), 0);
|
||||||
$expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []);
|
$expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []);
|
||||||
|
|
||||||
// --- PREPARATION SOLDE THEORIQUE ---
|
|
||||||
$capacite_max = 0;
|
$capacite_max = 0;
|
||||||
$depenses_reelles = 0;
|
$depenses_reelles = 0;
|
||||||
|
|
||||||
@@ -293,55 +324,43 @@ foreach ($allExpenses as $exp) {
|
|||||||
$cat = $exp['category'];
|
$cat = $exp['category'];
|
||||||
if (!isset($totals[$cat])) $cat = 'Autres';
|
if (!isset($totals[$cat])) $cat = 'Autres';
|
||||||
|
|
||||||
$val = (float)$exp['amount'];
|
$val = (float)$exp['amount']; // NOUVELLE RÈGLE : + = Revenu/Remboursement, - = Dépense
|
||||||
|
|
||||||
// 1. Remplissage pour l'affichage visuel des cartes
|
// 1. Remplissage Visuel des cartes
|
||||||
if ($cat === 'Income') {
|
if ($cat === 'Income') {
|
||||||
$totals[$cat] += abs($val);
|
$totals[$cat] += $val;
|
||||||
} else {
|
} else {
|
||||||
if ($val < 0) {
|
if ($val > 0) {
|
||||||
$categoriesConfig[$cat]['budget'] += abs($val);
|
// C'est un remboursement d'une dépense (+), il augmente le budget
|
||||||
|
$categoriesConfig[$cat]['budget'] += $val;
|
||||||
} else {
|
} else {
|
||||||
$totals[$cat] += $val;
|
// C'est une dépense normale (-), on l'ajoute au total dépensé (en valeur absolue pour l'UI)
|
||||||
|
$totals[$cat] += abs($val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$expensesByCategory[$cat][] = $exp;
|
$expensesByCategory[$cat][] = $exp;
|
||||||
|
|
||||||
// 2. Calcul des briques du Solde Théorique (Respect des signes BDD)
|
// 2. Calcul du Théorique
|
||||||
if ($val < 0) {
|
if ($val > 0) {
|
||||||
// L'argent qui RENTRE sur le compte est stocké en négatif
|
|
||||||
if ($cat === 'Income' || $cat !== 'Frais') {
|
if ($cat === 'Income' || $cat !== 'Frais') {
|
||||||
// Règle 1 et 2 : Somme Revenus + Tout autre virement positif HORS charges fixes
|
$capacite_max += $val;
|
||||||
$capacite_max += abs($val);
|
|
||||||
} else {
|
} else {
|
||||||
// C'est un remboursement d'une charge fixe, il vient diminuer les dépenses
|
$depenses_reelles -= $val;
|
||||||
$depenses_reelles -= abs($val);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// L'argent qui SORT (dépense) est stocké en positif
|
$depenses_reelles += abs($val);
|
||||||
// Règle 3 : Somme de tous les autres virements sortants
|
|
||||||
$depenses_reelles += $val;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// CALCUL FINAL DU SOLDE THÉORIQUE
|
|
||||||
// ============================================================================
|
|
||||||
// On applique ta règle exacte :
|
|
||||||
$solde_theorique = $capacite_max - $depenses_reelles - $reste_a_venir;
|
$solde_theorique = $capacite_max - $depenses_reelles - $reste_a_venir;
|
||||||
|
|
||||||
// Sécurité : Si 0 injection ce mois-ci, on se base sur le prévisionnel pour avoir une jauge visuelle
|
|
||||||
if ($capacite_max <= 0) {
|
if ($capacite_max <= 0) {
|
||||||
$capacite_max = $budget_income_prevu > 0 ? $budget_income_prevu : 1;
|
$capacite_max = $budget_income_prevu > 0 ? $budget_income_prevu : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Calculs pour la Barre de Déchargement ---
|
$solde_net = max(0, $solde_actuel - $reste_a_venir);
|
||||||
$solde_actuel = max(0, $snapshot['amount']);
|
$charges_visibles = min($solde_actuel, $reste_a_venir);
|
||||||
$charges_a_venir = max(0, $reste_a_venir);
|
|
||||||
$solde_net = max(0, $solde_actuel - $charges_a_venir);
|
|
||||||
$charges_visibles = min($solde_actuel, $charges_a_venir);
|
|
||||||
|
|
||||||
// L'échelle de la jauge (Le Max) (+10% visuel)
|
|
||||||
$max_scale = max($solde_actuel, $solde_theorique, $capacite_max, 1) * 1.1;
|
$max_scale = max($solde_actuel, $solde_theorique, $capacite_max, 1) * 1.1;
|
||||||
|
|
||||||
$pct_net = min(100, max(0, ($solde_net / $max_scale) * 100));
|
$pct_net = min(100, max(0, ($solde_net / $max_scale) * 100));
|
||||||
@@ -393,7 +412,7 @@ $monthName = $moisFr[(int)$currentMonth] . ' ' . $currentYear;
|
|||||||
|
|
||||||
<div style="padding:15px; background:#f8fafc; border-radius:12px; border:1px solid #e2e8f0;">
|
<div style="padding:15px; background:#f8fafc; border-radius:12px; border:1px solid #e2e8f0;">
|
||||||
<div style="font-size:0.85rem; color:#64748b; margin-bottom:4px;">Solde théorique <span style="font-size:0.75rem;">(au <?= date('d/m') ?>)</span></div>
|
<div style="font-size:0.85rem; color:#64748b; margin-bottom:4px;">Solde théorique <span style="font-size:0.75rem;">(au <?= date('d/m') ?>)</span></div>
|
||||||
<div style="font-size:1.4rem; font-weight:700; color:<?= $solde_theorique < 0 ? '#ef4444' : '#334155' ?>;" title="Calcul : Solde du relevé + Apports depuis le relevé - Dépenses depuis le relevé - Charges en attente.">
|
<div style="font-size:1.4rem; font-weight:700; color:<?= $solde_theorique < 0 ? '#ef4444' : '#334155' ?>;" title="Calcul : Capacité Max - Dépenses Réelles - Charges en attente.">
|
||||||
<?= number_format($solde_theorique, 2, ',', ' ') ?> €
|
<?= number_format($solde_theorique, 2, ',', ' ') ?> €
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -610,10 +629,10 @@ $monthName = $moisFr[(int)$currentMonth] . ' ' . $currentYear;
|
|||||||
<span title="Lié à une charge/revenu" style="font-size:0.7rem; cursor:help;">🔗</span>
|
<span title="Lié à une charge/revenu" style="font-size:0.7rem; cursor:help;">🔗</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<?php if($exp['amount'] < 0): ?>
|
<?php if($exp['amount'] > 0): ?>
|
||||||
<td style="padding:10px 15px; text-align:right; font-weight:600; color:#10b981;">+<?= number_format(abs($exp['amount']), 2) ?></td>
|
<td style="padding:10px 15px; text-align:right; font-weight:600; color:#10b981;">+<?= number_format($exp['amount'], 2) ?></td>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<td style="padding:10px 15px; text-align:right; font-weight:600; color:#1e293b;">-<?= number_format($exp['amount'], 2) ?></td>
|
<td style="padding:10px 15px; text-align:right; font-weight:600; color:#1e293b;">-<?= number_format(abs($exp['amount']), 2) ?></td>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<td style="width:60px; padding-right:10px; text-align:right; white-space:nowrap;">
|
<td style="width:60px; padding-right:10px; text-align:right; white-space:nowrap;">
|
||||||
<button onclick='openEditModal(<?= json_encode($exp) ?>)'
|
<button onclick='openEditModal(<?= json_encode($exp) ?>)'
|
||||||
@@ -640,7 +659,7 @@ $monthName = $moisFr[(int)$currentMonth] . ' ' . $currentYear;
|
|||||||
<div id="manualExpenseModal" class="pf-modal">
|
<div id="manualExpenseModal" class="pf-modal">
|
||||||
<div class="pf-modal-content" style="max-width:400px;">
|
<div class="pf-modal-content" style="max-width:400px;">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||||
<h3 style="margin:0;" id="modalTitle">Nouvelle dépense</h3>
|
<h3 style="margin:0;" id="modalTitle">Nouvelle transaction</h3>
|
||||||
<button type="button" onclick="closeSuiviModal('manualExpenseModal')" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">×</button>
|
<button type="button" onclick="closeSuiviModal('manualExpenseModal')" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -799,7 +818,6 @@ function handleModalCatChange(select) {
|
|||||||
const selectFrais = document.getElementById('fraisSelect');
|
const selectFrais = document.getElementById('fraisSelect');
|
||||||
const selectIncome = document.getElementById('incomeSelect');
|
const selectIncome = document.getElementById('incomeSelect');
|
||||||
|
|
||||||
// Reset visibility
|
|
||||||
blockText.style.display = 'none';
|
blockText.style.display = 'none';
|
||||||
blockSelect.style.display = 'none';
|
blockSelect.style.display = 'none';
|
||||||
blockFrais.style.display = 'none';
|
blockFrais.style.display = 'none';
|
||||||
@@ -810,11 +828,10 @@ function handleModalCatChange(select) {
|
|||||||
selectFrais.required = false;
|
selectFrais.required = false;
|
||||||
selectIncome.required = false;
|
selectIncome.required = false;
|
||||||
|
|
||||||
// Affichage conditionnel des Vacances (Si Epargne/LivretA OU si la catégorie contient le mot vacance)
|
|
||||||
if (catKey === 'LivretA' || catText.includes('vacance')) {
|
if (catKey === 'LivretA' || catText.includes('vacance')) {
|
||||||
blockHoliday.style.display = 'block';
|
blockHoliday.style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('modalHolidayId').value = ''; // On reset si on cache
|
document.getElementById('modalHolidayId').value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (catKey === 'School') {
|
if (catKey === 'School') {
|
||||||
@@ -869,14 +886,15 @@ function openAddModal(catKey, catLabel) {
|
|||||||
// Ouvre la modale pour MODIFIER
|
// Ouvre la modale pour MODIFIER
|
||||||
function openEditModal(expenseData) {
|
function openEditModal(expenseData) {
|
||||||
openSuiviModal('manualExpenseModal');
|
openSuiviModal('manualExpenseModal');
|
||||||
document.getElementById('modalTitle').innerText = "Modifier la dépense";
|
document.getElementById('modalTitle').innerText = "Modifier la transaction";
|
||||||
|
|
||||||
document.getElementById('modalExpenseId').value = expenseData.id;
|
document.getElementById('modalExpenseId').value = expenseData.id;
|
||||||
document.getElementById('modalDate').value = expenseData.date_exp;
|
document.getElementById('modalDate').value = expenseData.date_exp;
|
||||||
document.getElementById('modalLabelInput').value = expenseData.label;
|
document.getElementById('modalLabelInput').value = expenseData.label;
|
||||||
|
|
||||||
const rawAmount = parseFloat(expenseData.amount);
|
const rawAmount = parseFloat(expenseData.amount);
|
||||||
document.getElementById('modalIsCredit').value = rawAmount < 0 ? "1" : "0";
|
// LOGIQUE INVERSÉE POUR L'INTERFACE : Si > 0 c'est un crédit
|
||||||
|
document.getElementById('modalIsCredit').value = rawAmount > 0 ? "1" : "0";
|
||||||
document.getElementById('modalAmount').value = Math.abs(rawAmount);
|
document.getElementById('modalAmount').value = Math.abs(rawAmount);
|
||||||
document.getElementById('modalHolidayId').value = expenseData.holiday_id || "";
|
document.getElementById('modalHolidayId').value = expenseData.holiday_id || "";
|
||||||
|
|
||||||
@@ -918,7 +936,6 @@ function handleLineCatChange(select) {
|
|||||||
const catKey = select.value;
|
const catKey = select.value;
|
||||||
const catText = select.options[select.selectedIndex].text.toLowerCase();
|
const catText = select.options[select.selectedIndex].text.toLowerCase();
|
||||||
|
|
||||||
// Reset
|
|
||||||
fraisSelect.style.display = 'none';
|
fraisSelect.style.display = 'none';
|
||||||
incomeSelect.style.display = 'none';
|
incomeSelect.style.display = 'none';
|
||||||
holidaySelect.style.display = 'none';
|
holidaySelect.style.display = 'none';
|
||||||
@@ -926,7 +943,6 @@ function handleLineCatChange(select) {
|
|||||||
fraisSelect.value = '';
|
fraisSelect.value = '';
|
||||||
incomeSelect.value = '';
|
incomeSelect.value = '';
|
||||||
|
|
||||||
// Condition d'affichage pour les vacances
|
|
||||||
if (catKey === 'LivretA' || catText.includes('vacance')) {
|
if (catKey === 'LivretA' || catText.includes('vacance')) {
|
||||||
holidaySelect.style.display = 'block';
|
holidaySelect.style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user