This commit is contained in:
2026-02-11 11:50:18 +01:00
parent 16e3eb0890
commit 71db01d159
7 changed files with 863 additions and 598 deletions
-1
View File
@@ -51,5 +51,4 @@ require __DIR__ . '/header.php';
?> ?>
</section> </section>
</div> </div>
<script src="/modules/budget/budget.js"></script>
<?php require __DIR__ . '/footer.php'; ?> <?php require __DIR__ . '/footer.php'; ?>
+3 -1
View File
@@ -157,6 +157,7 @@
color: var(--text-muted); color: var(--text-muted);
border: 1px solid #cbd5e1; border: 1px solid #cbd5e1;
box-shadow: none; box-shadow: none;
margin-bottom: 5px;
} }
.pf-btn.btn-secondary:hover { .pf-btn.btn-secondary:hover {
@@ -415,7 +416,7 @@ input[type="checkbox"]:checked::after {
animation: modalPop 0.3s cubic-bezier(0.16, 1, 0.3, 1); animation: modalPop 0.3s cubic-bezier(0.16, 1, 0.3, 1);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 10px;
} }
@keyframes modalPop { @keyframes modalPop {
@@ -512,6 +513,7 @@ input[type="checkbox"]:checked::after {
.pf-btn { .pf-btn {
width: 100%; width: 100%;
justify-content: center; justify-content: center;
margin-top: 5px;
} }
.pf-table { .pf-table {
-213
View File
@@ -1,213 +0,0 @@
/**
* Change l'état "payé/attente" d'un frais sans recharger la page
*/
function toggleCheck(id, isChecked) {
const formData = new FormData();
formData.append("action", "toggle-check");
formData.append("id", id);
formData.append("status", isChecked ? 1 : 0);
fetch("/modules/budget/includes/api/manage-item.php", {
method: "POST",
body: formData,
})
.then((response) => {
if (!response.ok) throw new Error("Erreur réseau");
// Optionnel : on pourrait actualiser un petit label ici
console.log("Statut mis à jour pour l'item " + id);
})
.catch((error) => {
alert("Erreur lors de la mise à jour");
console.error(error);
});
}
/**
* Supprime un item après confirmation
*/
function deleteItem(id) {
if (confirm("Voulez-vous vraiment supprimer cet élément ?")) {
const formData = new FormData();
formData.append("action", "delete");
formData.append("id", id);
fetch("/modules/budget/includes/api/manage-item.php", {
method: "POST",
body: formData,
}).then(() => {
window.location.reload(); // On recharge pour mettre à jour les totaux
});
}
}
/**
* Ouvre la modal et pré-remplit les champs pour la modification
*/
function editItem(item) {
// Si 'item' arrive sous forme de string JSON depuis l'attribut HTML
const data = typeof item === "string" ? JSON.parse(item) : item;
document.getElementById("modalTitle").innerText = "Modifier : " + data.name;
document.getElementById("item_id").value = data.id;
document.getElementById("item_name").value = data.name;
document.getElementById("item_amount").value = 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("budgetModal").style.display = "flex";
}
function openModal(mode) {
if (mode === "add") {
document.getElementById("modalTitle").innerText = "Ajouter un élément";
document.getElementById("item_id").value = "";
document.querySelector("#budgetModal form").reset();
}
document.getElementById("budgetModal").style.display = "flex";
}
function closeModal() {
document.getElementById("budgetModal").style.display = "none";
}
// Variable globale pour stocker les catégories existantes (passées par PHP)
let knownCategories = [];
function openSavingsModal(mode, owner) {
document.getElementById("savingsModal").style.display = "flex";
document.getElementById("sav_owner").value = owner; // On stocke le owner dans le form caché
const container = document.getElementById("linesContainer");
container.innerHTML = "";
if (mode === "add") {
document.getElementById("savingsModalTitle").innerText =
"Nouveau pour " + owner;
document.getElementById("sav_date").valueAsDate = new Date();
document.getElementById("sav_total").value = "";
// Par défaut, une ligne vide
createLine("", "");
}
}
function editMonth(monthDate, owner, dataValues, allCats) {
document.getElementById("savingsModal").style.display = "flex";
document.getElementById("savingsModalTitle").innerText =
"Modifier " + owner + " (" + monthDate + ")";
document.getElementById("sav_owner").value = owner;
document.getElementById("sav_date").value = monthDate;
knownCategories = allCats || [];
const container = document.getElementById("linesContainer");
container.innerHTML = "";
// Total
if (dataValues["TOTAL_BANQUE"]) {
document.getElementById("sav_total").value = dataValues["TOTAL_BANQUE"];
delete dataValues["TOTAL_BANQUE"];
} else {
document.getElementById("sav_total").value = "";
}
// Lignes existantes
for (const [category, amount] of Object.entries(dataValues)) {
createLine(category, amount);
}
createLine("", "");
}
function addNewLine() {
createLine("", "");
}
/**
* Crée une ligne HTML dans la modale : [ Nom de la catégorie ] [ Montant ] [ X ]
*/
function createLine(name, amount) {
const container = document.getElementById("linesContainer");
const div = document.createElement("div");
div.className = "savings-line-item";
const listId = "list_" + Math.random().toString(36).substr(2, 9);
div.innerHTML = `
<div class="input-category-wrapper">
<input type="text" name="cat_names[]" class="pf-input" placeholder="Catégorie" value="${name}" list="${listId}">
<datalist id="${listId}">${knownCategories.map((c) => `<option value="${c}">`).join("")}</datalist>
</div>
<input type="number" step="0.01" name="cat_amounts[]" class="pf-input input-amount" placeholder="0.00" value="${amount}">
<button type="button" class="btn-remove" onclick="this.parentElement.remove()" title="Supprimer">&times;</button>
`;
container.appendChild(div);
}
/**
* Supprime une entrée spécifique (Mois + Catégorie) sans recharger la page (si possible)
*/
function deleteSavingsEntry(monthDate, category, owner) {
if (!confirm(`Supprimer le montant pour "${category}" ?`)) return;
const formData = new FormData();
formData.append("action", "delete_entry");
formData.append("month_date", monthDate);
formData.append("category", category);
formData.append("owner", owner);
fetch("/modules/budget/includes/api/save-savings.php", {
method: "POST",
body: formData,
}).then(() => window.location.reload());
}
/**
* Duplique le dernier mois vers le mois suivant
*/
function duplicateLastMonth(lastMonthDate, owner) {
let dateObj = new Date(lastMonthDate);
dateObj.setMonth(dateObj.getMonth() + 1);
let nextMonthStr = dateObj.toISOString().split("T")[0];
let newTotal = prompt(
`Dupliquer pour ${owner} vers ${nextMonthStr} ?\n\nNouveau TOTAL :`,
"",
);
if (newTotal !== null && newTotal.trim() !== "") {
const formData = new FormData();
formData.append("action", "duplicate_month");
formData.append("source_date", lastMonthDate);
formData.append("target_date", nextMonthStr);
formData.append("new_total", newTotal);
formData.append("owner", owner);
fetch("/modules/budget/includes/api/save-savings.php", {
method: "POST",
body: formData,
})
.then((r) => r.json())
.then((d) => {
if (d.success) window.location.reload();
else alert(d.error);
});
}
}
/**
* Supprime toutes les données d'un mois pour le propriétaire actuel
*/
function deleteEntireMonth(monthDate, owner) {
if (!confirm(`Supprimer TOUT le mois de ${monthDate} pour ${owner} ?`))
return;
const formData = new FormData();
formData.append("action", "delete_month_global");
formData.append("month_date", monthDate);
formData.append("owner", owner);
fetch("/modules/budget/includes/api/save-savings.php", {
method: "POST",
body: formData,
}).then(() => window.location.reload());
}
+12 -7
View File
@@ -6,7 +6,6 @@ require_login();
// --- ACTION : SUPPRESSION D'UNE ENTRÉE UNIQUE --- // --- ACTION : SUPPRESSION D'UNE ENTRÉE UNIQUE ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete_entry') { if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete_entry') {
$owner = $_POST['owner']; $owner = $_POST['owner'];
$redirectTab = $_POST['redirect_tab'] ?? $owner;
$date = $_POST['month_date']; $date = $_POST['month_date'];
$cat = $_POST['category']; $cat = $_POST['category'];
@@ -40,7 +39,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'dupli
$stmtIns->execute([$owner, $targetDate, 'TOTAL_BANQUE', $newTotal]); $stmtIns->execute([$owner, $targetDate, 'TOTAL_BANQUE', $newTotal]);
// 3. Copier toutes les catégories (sauf TOTAL_BANQUE) du mois source // 3. Copier toutes les catégories (sauf TOTAL_BANQUE) du mois source
// On insère directement avec une requête INSERT SELECT
$sqlCopy = "INSERT INTO pf_savings (owner, month_date, category, amount) $sqlCopy = "INSERT INTO pf_savings (owner, month_date, category, amount)
SELECT owner, ?, category, amount SELECT owner, ?, category, amount
FROM pf_savings FROM pf_savings
@@ -76,30 +74,37 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
} }
// --- ACTION : SAUVEGARDE CLASSIQUE (MODALE) --- // --- ACTION : SAUVEGARDE CLASSIQUE (MODALE) ---
// (Le code précédent reste ici pour la sauvegarde via le formulaire classique)
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$owner = $_POST['owner']; $owner = $_POST['owner'];
// CORRECTION ICI : On récupère l'onglet de redirection (Nens, Alex ou Laia)
$redirectTab = $_POST['redirect_tab'] ?? $owner;
$dateInput = $_POST['month_date']; $dateInput = $_POST['month_date'];
$dateObj = new DateTime($dateInput); $dateObj = new DateTime($dateInput);
$monthDate = $dateObj->format('Y-m-01'); $monthDate = $dateObj->format('Y-m-01');
$values = $_POST['values'] ?? []; // Pour la modale standard $values = $_POST['values'] ?? []; // Tableau généré par nos champs JS: values[Catégorie]
// ... (Garde ton code précédent de sauvegarde ici) ...
// Note : Ajoute ce bloc TRY CATCH si tu ne l'avais pas déjà
try { try {
$pdo->beginTransaction(); $pdo->beginTransaction();
// On supprime d'abord les anciennes données du mois pour cet utilisateur
$stmtDel = $pdo->prepare("DELETE FROM pf_savings WHERE owner = ? AND month_date = ?"); $stmtDel = $pdo->prepare("DELETE FROM pf_savings WHERE owner = ? AND month_date = ?");
$stmtDel->execute([$owner, $monthDate]); $stmtDel->execute([$owner, $monthDate]);
// On réinsère les nouvelles données
$stmtIns = $pdo->prepare("INSERT INTO pf_savings (owner, month_date, category, amount) VALUES (?, ?, ?, ?)"); $stmtIns = $pdo->prepare("INSERT INTO pf_savings (owner, month_date, category, amount) VALUES (?, ?, ?, ?)");
foreach ($values as $category => $amount) { foreach ($values as $category => $amount) {
$amount = floatval($amount); $amount = floatval($amount);
// On enregistre si c'est positif OU si c'est le total banque
if ($amount > 0 || $category === 'TOTAL_BANQUE') { if ($amount > 0 || $category === 'TOTAL_BANQUE') {
$stmtIns->execute([$owner, $monthDate, $category, $amount]); $stmtIns->execute([$owner, $monthDate, $category, $amount]);
} }
} }
$pdo->commit(); $pdo->commit();
header("Location: /budget.php?tab=epargne&owner=$redirectTab");
// Redirection vers le bon onglet
header("Location: /budget.php?tab=epargne&owner=" . urlencode($redirectTab));
exit; exit;
} catch (Exception $e) { } catch (Exception $e) {
$pdo->rollBack(); $pdo->rollBack();
+253 -22
View File
@@ -1,4 +1,6 @@
<?php <?php
// modules/budget/views/epargne.php
// 1. Gestion des propriétaires à afficher // 1. Gestion des propriétaires à afficher
$requestedOwner = $_GET['owner'] ?? 'Nens'; $requestedOwner = $_GET['owner'] ?? 'Nens';
@@ -6,6 +8,18 @@ $requestedOwner = $_GET['owner'] ?? 'Nens';
$ownersToDisplay = ($requestedOwner === 'Nens') ? ['Pol', 'Pep'] : [$requestedOwner]; $ownersToDisplay = ($requestedOwner === 'Nens') ? ['Pol', 'Pep'] : [$requestedOwner];
?> ?>
<style>
/* Cacher les flèches haut/bas des champs de type number */
input[type="number"].no-spinners::-webkit-inner-spin-button,
input[type="number"].no-spinners::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"].no-spinners {
-moz-appearance: textfield; /* Firefox */
}
</style>
<div class="budget-view"> <div class="budget-view">
<div class="view-header"> <div class="view-header">
@@ -41,7 +55,6 @@ $ownersToDisplay = ($requestedOwner === 'Nens') ? ['Pol', 'Pep'] : [$requestedOw
<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 === 'Nens' && $currentOwner !== 'Pol') ? '40px' : '0' ?>;">
<div style="flex-grow: 1;"> <div style="flex-grow: 1;">
<?php if ($requestedOwner === 'Nens'): <?php if ($requestedOwner === 'Nens'):
// On détermine la classe CSS basée sur le nom (pol ou pep)
$themeClass = 'theme-' . strtolower($currentOwner); $themeClass = 'theme-' . strtolower($currentOwner);
?> ?>
<h3 class="nens-title <?= $themeClass ?>" style="margin:0; font-size:1.2rem;"> <h3 class="nens-title <?= $themeClass ?>" style="margin:0; font-size:1.2rem;">
@@ -56,11 +69,10 @@ $ownersToDisplay = ($requestedOwner === 'Nens') ? ['Pol', 'Pep'] : [$requestedOw
🔁 +1 Mois 🔁 +1 Mois
</button> </button>
<?php endif; ?> <?php endif; ?>
<button onclick="openSavingsModal('add', '<?= $currentOwner ?>')" class="pf-btn"> <button onclick="openCustomSavingsModal('<?= $currentOwner ?>')" class="pf-btn">
Saisir un mois Saisir un mois
</button> </button>
</div> </div>
</div> </div>
<div class="table-responsive" style="background:white; border-radius:16px; box-shadow:var(--shadow-sm); border:1px solid #e2e8f0;"> <div class="table-responsive" style="background:white; border-radius:16px; box-shadow:var(--shadow-sm); border:1px solid #e2e8f0;">
@@ -69,7 +81,8 @@ $ownersToDisplay = ($requestedOwner === 'Nens') ? ['Pol', 'Pep'] : [$requestedOw
<p>Aucune donnée pour <?= htmlspecialchars($currentOwner) ?>.</p> <p>Aucune donnée pour <?= htmlspecialchars($currentOwner) ?>.</p>
</div> </div>
<?php else: ?> <?php else: ?>
<table class="pf-table savings-table nens-table theme-<?= strtolower($currentOwner) ?>" style="margin-top:0; box-shadow:none; border-radius:16px;"> <thead> <table class="pf-table savings-table nens-table theme-<?= strtolower($currentOwner) ?>" style="margin-top:0; box-shadow:none; border-radius:16px;">
<thead>
<tr> <tr>
<th class="sticky-col" style="background:#f8fafc;">Poste / Mois</th> <th class="sticky-col" style="background:#f8fafc;">Poste / Mois</th>
<?php foreach ($months as $month): ?> <?php foreach ($months as $month): ?>
@@ -78,7 +91,7 @@ $ownersToDisplay = ($requestedOwner === 'Nens') ? ['Pol', 'Pep'] : [$requestedOw
<span class="month-name"><?= date('M Y', strtotime($month)) ?></span> <span class="month-name"><?= date('M Y', strtotime($month)) ?></span>
<div class="month-actions"> <div class="month-actions">
<button class="btn-icon-small" title="Modifier" <button class="btn-icon-small" title="Modifier"
onclick='editMonth("<?= $month ?>", "<?= $currentOwner ?>", <?= json_encode($data[$month] ?? []) ?>, <?= json_encode($allCategories) ?>)'> onclick='editCustomSavingsMonth("<?= $month ?>", "<?= $currentOwner ?>", <?= json_encode($data[$month] ?? []) ?>)'>
✏️ ✏️
</button> </button>
<button class="btn-icon-small" title="Supprimer" <button class="btn-icon-small" title="Supprimer"
@@ -138,40 +151,258 @@ $ownersToDisplay = ($requestedOwner === 'Nens') ? ['Pol', 'Pep'] : [$requestedOw
</table> </table>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php endforeach; ?> </div> <?php endforeach; ?>
</div>
<div id="savingsModal" class="pf-modal"> <div id="savingsModal" class="pf-modal">
<div class="pf-modal-content" style="max-width: 500px;"> <div class="pf-modal-content" style="max-width: 600px; width: 95%;">
<h3 id="savingsModalTitle" class="pf-modal-title">Saisir le mois</h3> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 id="savingsModalTitle" class="pf-modal-title" style="margin:0;">Saisir le mois</h3>
<button type="button" onclick="document.getElementById('savingsModal').style.display='none'" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">&times;</button>
</div>
<form action="/modules/budget/includes/api/save-savings.php" method="POST" id="savingsForm"> <form action="/modules/budget/includes/api/save-savings.php" method="POST" id="savingsForm">
<input type="hidden" name="owner" id="sav_owner"> <input type="hidden" name="owner" id="sav_owner">
<input type="hidden" name="redirect_tab" id="redirect_tab" value="<?= htmlspecialchars($requestedOwner) ?>">
<input type="hidden" name="redirect_tab" value="<?= $requestedOwner ?>"> <div style="display:flex; gap:15px; margin-bottom:20px;">
<div class="form-group" style="flex:1; margin:0;">
<div class="form-group">
<label class="pf-label">Mois concerné</label> <label class="pf-label">Mois concerné</label>
<input type="date" name="month_date" id="sav_date" required class="pf-input"> <input type="date" name="month_date" id="sav_date" required class="pf-input">
</div> </div>
<div class="form-group"> <div class="form-group" style="flex:1; margin:0;">
<label class="pf-label">Total (€)</label> <label class="pf-label">Total en Banque (€)</label>
<input type="number" step="0.01" name="values[TOTAL_BANQUE]" id="sav_total" required class="pf-input" style="font-weight:bold; color:#2563eb;"> <input type="number" step="0.01" name="values[TOTAL_BANQUE]" id="sav_total" required class="pf-input no-spinners" style="font-weight:bold; color:#2563eb;">
</div>
</div> </div>
<div class="separator" style="margin: 20px 0; border-bottom: 1px solid #eee;"></div> <div class="separator" style="margin: 20px 0; border-bottom: 1px solid #e2e8f0;"></div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;"> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;">
<h4 style="margin:0; font-size:0.9rem; color:#64748b;">Ventilation</h4> <div>
<button type="button" class="btn-icon" onclick="addNewLine()" title="Ajouter une ligne" style="background:#e2e8f0;"></button> <h4 style="margin:0; font-size:1rem; color:#1e293b;">Ventilation</h4>
<span style="font-size:0.8rem; color:#64748b;">Utilisez l'ajustement (+/-) pour recalculer automatiquement.</span>
</div>
<button type="button" class="pf-btn btn-secondary" onclick="addCustomEpargneLine()" style="padding:4px 10px; height:auto; width:auto; font-size:0.9rem;"> Ligne</button>
</div> </div>
<div id="linesContainer" style="max-height: 300px; overflow-y: auto; padding-right:5px; display:flex; flex-direction:column; gap:8px;"></div> <div style="display:flex; gap:10px; padding:0 5px 5px 5px; font-size:0.8rem; color:#64748b; font-weight:600;">
<div style="flex:2;">Catégorie</div>
<div style="width:100px;">Actuel</div>
<div style="width:90px;">Ajust (+/-)</div>
<div style="width:100px;">Nouveau</div>
<div style="width:28px;"></div>
</div>
<div class="modal-footer"> <div id="linesContainer" style="max-height: 350px; overflow-y: auto; padding-right:5px; display:flex; flex-direction:column; gap:10px;">
<button type="button" onclick="document.getElementById('savingsModal').style.display='none'" class="pf-btn btn-secondary">Annuler</button> </div>
<button type="submit" class="pf-btn">Enregistrer</button>
<div style="margin-top:25px; display:flex; justify-content:flex-end; gap:10px;">
<button type="button" onclick="document.getElementById('savingsModal').style.display='none'" class="pf-btn btn-secondary" style="width:auto; margin:0;">Annuler</button>
<button type="submit" class="pf-btn" style="width:auto; margin:0;">Enregistrer</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<script>
// --- LOGIQUE MODALE EPARGNE ---
function addCustomEpargneLine(catName = '', amount = '') {
const container = document.getElementById('linesContainer');
const baseAmount = amount !== '' ? parseFloat(amount).toFixed(2) : '0.00';
// Assure l'envoi de `values[Catégorie]` au serveur
const inputName = catName ? `values[${catName}]` : '';
const html = `
<div class="ventilation-line" style="display:flex; gap:10px; align-items:center; background:#f8fafc; padding:8px; border-radius:8px; border:1px solid #e2e8f0;">
<div style="flex:2;">
<input type="text" class="pf-input cat-name-input" value="${catName}" placeholder="Nom (ex: Vacances)" oninput="updateCustomFieldName(this)" style="padding:6px; font-size:0.9rem;" required>
</div>
<div style="width:100px;">
<input type="number" step="0.01" class="pf-input base-amount no-spinners" value="${baseAmount}" oninput="recalculateCustomLine(this)" style="padding:6px; font-size:0.9rem; background:#fff;">
</div>
<div style="width:90px;">
<input type="number" step="0.01" class="pf-input adjustment-amount no-spinners" placeholder="+ / -" oninput="recalculateCustomLine(this)" style="padding:6px; font-size:0.9rem; color:#f59e0b; font-weight:bold;">
</div>
<div style="width:100px;">
<input type="number" step="0.01" name="${inputName}" class="pf-input final-amount no-spinners" value="${baseAmount}" style="padding:6px; font-size:0.9rem; font-weight:bold; background:#e0f2fe; border-color:#bae6fd; color:#0369a1;" readonly>
</div>
<button type="button" onclick="this.parentElement.remove()" style="width:28px; height:28px; border:none; background:#fee2e2; color:#ef4444; border-radius:4px; cursor:pointer; display:flex; align-items:center; justify-content:center; font-weight:bold;">&times;</button>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function updateCustomFieldName(inputElement) {
const line = inputElement.closest('.ventilation-line');
const finalInput = line.querySelector('.final-amount');
const newName = inputElement.value.trim();
if (newName) {
finalInput.name = `values[${newName}]`;
} else {
finalInput.name = '';
}
}
function recalculateCustomLine(inputElement) {
const line = inputElement.closest('.ventilation-line');
const baseInput = line.querySelector('.base-amount');
const adjInput = line.querySelector('.adjustment-amount');
const finalInput = line.querySelector('.final-amount');
const base = parseFloat(baseInput.value) || 0;
const adj = parseFloat(adjInput.value) || 0;
finalInput.value = (base + adj).toFixed(2);
}
function editCustomSavingsMonth(monthDate, owner, rowData) {
document.getElementById('sav_owner').value = owner;
document.getElementById('sav_date').value = monthDate;
const dateObj = new Date(monthDate);
const monthName = dateObj.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
document.getElementById('savingsModalTitle').innerText = "Modifier : " + monthName + " (" + owner + ")";
document.getElementById('sav_total').value = rowData['TOTAL_BANQUE'] || '';
const container = document.getElementById('linesContainer');
container.innerHTML = '';
for (const [cat, val] of Object.entries(rowData)) {
if (cat !== 'TOTAL_BANQUE') {
addCustomEpargneLine(cat, val);
}
}
if (container.children.length === 0) {
addCustomEpargneLine();
}
document.getElementById('savingsModal').style.display = 'flex';
}
function openCustomSavingsModal(owner) {
document.getElementById('sav_owner').value = owner;
document.getElementById('sav_date').value = '';
document.getElementById('sav_total').value = '';
document.getElementById('savingsModalTitle').innerText = "Saisir un mois (" + owner + ")";
const container = document.getElementById('linesContainer');
container.innerHTML = '';
addCustomEpargneLine();
document.getElementById('savingsModal').style.display = 'flex';
}
window.onclick = function(event) {
const modal = document.getElementById('savingsModal');
if (event.target == modal) {
modal.style.display = 'none';
}
}
// --- INTERCEPTION DU FORMULAIRE POUR ÉVITER LE BUG DE REDIRECTION ---
document.getElementById('epargneForm').addEventListener('submit', function(e) {
e.preventDefault(); // Empêche le rechargement classique de la page
const submitBtn = this.querySelector('button[type="submit"]');
const originalText = submitBtn.innerText;
submitBtn.innerText = "Enregistrement...";
submitBtn.disabled = true;
const formData = new FormData(this);
fetch(this.action, {
method: 'POST',
body: formData
})
.then(response => {
// Peu importe comment le backend redirige en interne,
// on force le navigateur à recharger la page EXACTE où l'utilisateur se trouve
window.location.reload();
})
.catch(error => {
console.error("Erreur:", error);
alert("Une erreur est survenue lors de l'enregistrement.");
submitBtn.innerText = originalText;
submitBtn.disabled = false;
});
});
// ============================================================================
// ACTIONS AJAX (Suppression et Duplication)
// ============================================================================
// 1. Supprimer TOUT un mois (Bouton Poubelle rouge en haut)
function deleteEntireMonth(monthDate, owner) {
if (!confirm(`Supprimer TOUT le mois pour ${owner} ?`)) return;
const formData = new FormData();
formData.append("action", "delete_month_global");
formData.append("month_date", monthDate);
formData.append("owner", owner);
fetch("/modules/budget/includes/api/save-savings.php", {
method: "POST",
body: formData,
}).then(() => window.location.reload());
}
// 2. Dupliquer le mois précédent (Bouton +1 Mois)
function duplicateLastMonth(lastMonthDate, owner) {
let dateObj = new Date(lastMonthDate);
dateObj.setMonth(dateObj.getMonth() + 1);
let nextMonthStr = dateObj.toISOString().split("T")[0];
let newTotal = prompt(
`Dupliquer pour ${owner} vers ${nextMonthStr} ?\n\nNouveau TOTAL en banque :`,
""
);
if (newTotal !== null && newTotal.trim() !== "") {
const formData = new FormData();
formData.append("action", "duplicate_month");
formData.append("source_date", lastMonthDate);
formData.append("target_date", nextMonthStr);
formData.append("new_total", newTotal);
formData.append("owner", owner);
fetch("/modules/budget/includes/api/save-savings.php", {
method: "POST",
body: formData,
})
.then((r) => r.json())
.then((d) => {
if (d.success) window.location.reload();
else alert(d.error);
})
.catch(err => alert("Erreur réseau."));
}
}
// 3. Supprimer une seule ligne d'un mois (Petite croix dans la cellule)
function deleteSavingsEntry(monthDate, category, owner) {
if (!confirm(`Supprimer le montant pour "${category}" ?`)) return;
const formData = new FormData();
formData.append("action", "delete_entry");
formData.append("month_date", monthDate);
formData.append("category", category);
formData.append("owner", owner);
fetch("/modules/budget/includes/api/save-savings.php", {
method: "POST",
body: formData,
}).then(() => window.location.reload());
}
</script>
+158 -62
View File
@@ -1,4 +1,6 @@
<?php <?php
// modules/budget/views/recap.php
// 1. Récupération des données // 1. Récupération des données
$stmt = $pdo->query("SELECT * FROM pf_budget_items ORDER BY category DESC, sort_order ASC, name ASC"); $stmt = $pdo->query("SELECT * FROM pf_budget_items ORDER BY category DESC, sort_order ASC, name ASC");
$items = $stmt->fetchAll(); $items = $stmt->fetchAll();
@@ -8,13 +10,14 @@ $totalRevenusMensuels = 0;
?> ?>
<div class="budget-view"> <div class="budget-view">
<div class="view-header"> <div class="view-header" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h2>Récapitulatif Mensuel</h2> <h2 style="margin:0;">Récapitulatif Mensuel</h2>
<button onclick="openModal('add')" class="pf-btn"> Ajouter un frais / revenu</button> <button onclick="openRecapModal('add')" class="pf-btn"> Ajouter un frais / revenu</button>
</div> </div>
<table class="pf-table"> <div class="table-responsive" style="background:white; border-radius:16px; box-shadow:var(--shadow-sm); border:1px solid #e2e8f0; overflow:hidden;">
<thead> <table class="pf-table" style="margin:0; box-shadow:none;">
<thead style="background:#f8fafc;">
<tr> <tr>
<th>Nom</th> <th>Nom</th>
<th>Montant</th> <th>Montant</th>
@@ -22,7 +25,7 @@ $totalRevenusMensuels = 0;
<th>Jour</th> <th>Jour</th>
<th>État prélèvement</th> <th>État prélèvement</th>
<th>Régularisation</th> <th>Régularisation</th>
<th>Actions</th> <th style="text-align:right;">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -39,115 +42,123 @@ $totalRevenusMensuels = 0;
$rowClass = ($item['category'] === 'income') ? 'row-income' : 'row-expense'; $rowClass = ($item['category'] === 'income') ? 'row-income' : 'row-expense';
if ($item['is_estimate']) $rowClass .= ' row-estimate'; if ($item['is_estimate']) $rowClass .= ' row-estimate';
?> ?>
<tr class="<?= $rowClass ?>"> <tr class="<?= $rowClass ?>" style="border-bottom:1px solid #f1f5f9;">
<td> <td style="padding:15px;">
<strong><?= htmlspecialchars($item['name']) ?></strong> <strong><?= htmlspecialchars($item['name']) ?></strong>
<?= $item['is_estimate'] ? ' <small>(Est.)</small>' : '' ?> <?= $item['is_estimate'] ? ' <small style="color:#64748b;">(Est.)</small>' : '' ?>
</td> </td>
<td class="cell-amount"> <td class="cell-amount" style="font-weight:600; padding:15px; color:<?= $item['category']==='income'?'#10b981':'#1e293b' ?>;">
<?= number_format($item['amount'], 2, ',', ' ') ?> € <?= number_format($item['amount'], 2, ',', ' ') ?> €
</td> </td>
<td> <td style="padding:15px;">
<span class="badge-type <?= strtolower($item['type']) ?>"> <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;">
<?= $item['type'] ?> <?= $item['type'] ?>
</span> </span>
</td> </td>
<td><?= $item['payment_day'] ? $item['payment_day'] : '-' ?></td> <td style="padding:15px; color:#64748b;"><?= $item['payment_day'] ? $item['payment_day'] : '-' ?></td>
<td class="text-center"> <td style="padding:15px;">
<div style="display:flex; align-items:center; gap:8px;">
<input type="checkbox" <input type="checkbox"
<?= $item['is_checked'] ? 'checked' : '' ?> <?= $item['is_checked'] ? 'checked' : '' ?>
onclick="toggleCheck(<?= $item['id'] ?>, this.checked)" onclick="toggleItemCheck(<?= $item['id'] ?>, this.checked)"
title="Marquer comme payé"> title="Marquer comme payé"
<?= $item['is_checked'] ? ' <span class="text-success">Payé</span>' : ' <span style="color:var(--warning)">Attente</span>' ?> style="width:18px; height:18px; cursor:pointer;">
<?= $item['is_checked'] ? ' <span style="color:#10b981; font-weight:500; font-size:0.9rem;">Payé</span>' : ' <span style="color:#f59e0b; font-weight:500; font-size:0.9rem;">Attente</span>' ?>
</div>
</td> </td>
<td> <td style="padding:15px; color:#64748b; font-size:0.9rem;">
<em><?= htmlspecialchars($item['reg_month'] ?: '-') ?></em> <em><?= htmlspecialchars($item['reg_month'] ?: '-') ?></em>
</td> </td>
<td> <td style="padding:15px; text-align:right;">
<div class="action-buttons"> <div class="action-buttons" style="display:flex; gap:5px; justify-content:flex-end;">
<button class="btn-icon" onclick='editItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="Modifier">✏️</button> <button class="btn-icon" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="Modifier" style="background:none; border:none; cursor:pointer; font-size:1.1rem; filter:grayscale(1); transition:0.2s;">✏️</button>
<button class="btn-icon" onclick="deleteItem(<?= $item['id'] ?>)" title="Supprimer">🗑️</button> <button class="btn-icon" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="Supprimer" style="background:none; border:none; cursor:pointer; font-size:1.1rem; filter:grayscale(1); transition:0.2s;">🗑️</button>
</div> </div>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
<tfoot> <tfoot style="background:#f8fafc;">
<tr> <tr>
<td colspan="1"><strong>Total Revenus Mensuels</strong></td> <td colspan="1" style="padding:15px;"><strong>Total Revenus Mensuels</strong></td>
<td colspan="6" class="text-success"><strong>+ <?= number_format($totalRevenusMensuels, 2, ',', ' ') ?> €</strong></td> <td colspan="6" style="padding:15px; color:#10b981;"><strong>+ <?= number_format($totalRevenusMensuels, 2, ',', ' ') ?> €</strong></td>
</tr> </tr>
<tr> <tr>
<td colspan="1"><strong>Total Dépenses Mensuelles</strong></td> <td colspan="1" style="padding:15px;"><strong>Total Dépenses Mensuelles</strong></td>
<td colspan="6" class="text-danger"><strong>- <?= number_format($totalDepensesMensuelles, 2, ',', ' ') ?> €</strong></td> <td colspan="6" style="padding:15px; color:#ef4444;"><strong>- <?= number_format($totalDepensesMensuelles, 2, ',', ' ') ?> €</strong></td>
</tr> </tr>
<tr style="border-top: 2px solid #e2e8f0; background: white;"> <tr style="border-top: 2px solid #e2e8f0; background: white;">
<td colspan="1"><strong>Équilibre du compte</strong></td> <td colspan="1" style="padding:15px; font-size:1.1rem;"><strong>Équilibre du compte</strong></td>
<?php $balance = $totalRevenusMensuels - $totalDepensesMensuelles; ?> <?php $balance = $totalRevenusMensuels - $totalDepensesMensuelles; ?>
<td colspan="6" style="font-size: 1.3em;" class="<?= $balance >= 0 ? 'text-success' : 'text-danger' ?>"> <td colspan="6" style="padding:15px; font-size: 1.3em;" class="<?= $balance >= 0 ? 'text-success' : 'text-danger' ?>">
<strong><?= number_format($balance, 2, ',', ' ') ?> € / mois</strong> <strong style="color:<?= $balance >= 0 ? '#10b981' : '#ef4444' ?>;"><?= number_format($balance, 2, ',', ' ') ?> € / mois</strong>
</td> </td>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
</div>
<div class="budget-note"> <div class="budget-note" style="margin-top:15px; font-size:0.85rem; color:#64748b;">
<p>* Note : Les frais de type "Annuel" sont affichés pour information mais ne sont pas inclus dans le calcul de l'équilibre mensuel.</p> <p>* Note : Les frais de type "Annuel" sont affichés pour information mais ne sont pas inclus dans le calcul de l'équilibre mensuel.</p>
</div> </div>
</div> </div>
<div id="budgetModal" class="pf-modal"> <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"> <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;">
<h3 id="modalTitle" class="pf-modal-title">Ajouter un élément</h3>
<form action="/modules/budget/includes/api/manage-item.php" method="POST"> <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;">Ajouter un élément</h3>
<button type="button" onclick="closeRecapModal()" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">&times;</button>
</div>
<form action="/modules/budget/includes/api/manage-item.php" method="POST" id="recapForm">
<input type="hidden" name="action" value="save"> <input type="hidden" name="action" value="save">
<input type="hidden" name="id" id="item_id"> <input type="hidden" name="id" id="item_id">
<div class="form-group"> <div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Nom</label> <label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Nom</label>
<input type="text" name="name" id="item_name" required class="pf-input" placeholder="Ex: Loyer, Salaire..."> <input type="text" name="name" id="item_name" required class="pf-input" placeholder="Ex: Loyer, Salaire..." style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
</div> </div>
<div class="form-row"> <div style="display:flex; gap:15px; margin-bottom:15px;">
<div> <div style="flex:1;">
<label class="pf-label">Montant (€)</label> <label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Montant (€)</label>
<input type="number" step="0.01" name="amount" id="item_amount" required class="pf-input"> <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>
<div> <div style="flex:1;">
<label class="pf-label">Jour (1-31)</label> <label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Jour (1-31)</label>
<input type="number" min="1" max="31" name="payment_day" id="item_day" class="pf-input"> <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;">
</div> </div>
</div> </div>
<div class="form-row"> <div style="display:flex; gap:15px; margin-bottom:15px;">
<div> <div style="flex:1;">
<label class="pf-label">Catégorie</label> <label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Catégorie</label>
<select name="category" id="item_category" class="pf-input"> <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">Dépense (Frais)</option> <option value="expense">Dépense (Frais)</option>
<option value="income">Revenu (Salaire)</option> <option value="income">Revenu (Salaire)</option>
</select> </select>
</div> </div>
<div> <div style="flex:1;">
<label class="pf-label">Fréquence</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"> <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">Mensuel</option> <option value="Mensuel">Mensuel</option>
<option value="Annuel">Annuel</option> <option value="Annuel">Annuel</option>
</select> </select>
</div> </div>
</div> </div>
<div class="form-row"> <div style="display:flex; gap:15px; margin-bottom:25px;">
<div> <div style="flex:1;">
<label class="pf-label">Type de montant</label> <label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Type de montant</label>
<select name="is_estimate" id="item_is_estimate" class="pf-input"> <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)</option> <option value="0">Fixe (Facture)</option>
<option value="1">Variable (Estimation)</option> <option value="1">Variable (Estimation)</option>
</select> </select>
</div> </div>
<div> <div style="flex:1;">
<label class="pf-label">Régularisation</label> <label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Régularisation</label>
<select name="reg_month" id="item_reg_month" class="pf-input"> <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="">Aucune</option> <option value="">Aucune</option>
<?php <?php
$mois = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']; $mois = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
@@ -157,10 +168,95 @@ $totalRevenusMensuels = 0;
</div> </div>
</div> </div>
<div class="modal-footer"> <div style="display:flex; justify-content:flex-end; gap:10px;">
<button type="button" onclick="closeModal()" class="pf-btn btn-secondary">Annuler</button> <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">Enregistrer</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> </div>
</form> </form>
</div> </div>
</div> </div>
<script>
// ==========================================
// SCRIPTS ISOLÉS POUR L'ONGLET RECAPITULATIF
// ==========================================
// Fermer au clic extérieur
window.onclick = function(event) {
const modal = document.getElementById('budgetRecapModal');
if (event.target == modal) {
modal.style.display = 'none';
}
}
function openRecapModal(mode) {
if (mode === "add") {
document.getElementById("recapModalTitle").innerText = "Ajouter un élément";
document.getElementById("item_id").value = "";
document.getElementById("recapForm").reset();
}
document.getElementById("budgetRecapModal").style.display = "flex";
}
function closeRecapModal() {
document.getElementById("budgetRecapModal").style.display = "none";
}
function editRecapItem(item) {
// Si item est une string JSON (passée depuis PHP), on la parse
const data = typeof item === "string" ? JSON.parse(item) : item;
document.getElementById("recapModalTitle").innerText = "Modifier : " + data.name;
document.getElementById("item_id").value = data.id;
document.getElementById("item_name").value = data.name;
document.getElementById("item_amount").value = 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";
}
function deleteRecapItem(id) {
if (confirm("Voulez-vous vraiment supprimer cet élément ?")) {
const formData = new FormData();
formData.append("action", "delete");
formData.append("id", id);
fetch("/modules/budget/includes/api/manage-item.php", {
method: "POST",
body: formData,
}).then(() => {
window.location.reload();
}).catch(err => {
alert("Erreur lors de la suppression.");
console.error(err);
});
}
}
function toggleItemCheck(id, isChecked) {
const formData = new FormData();
formData.append("action", "toggle-check");
formData.append("id", id);
formData.append("status", isChecked ? 1 : 0);
fetch("/modules/budget/includes/api/manage-item.php", {
method: "POST",
body: formData,
})
.then((response) => {
if (!response.ok) throw new Error("Erreur réseau");
// Optionnel : on pourrait forcer un rechargement pour mettre l'icône à jour dynamiquement
window.location.reload();
})
.catch((error) => {
alert("Erreur lors de la mise à jour");
console.error(error);
// Si erreur, on décoche/recoche pour refléter la réalité
window.location.reload();
});
}
</script>
+301 -156
View File
@@ -7,36 +7,71 @@
$currentMonthKey = date('m-Y'); $currentMonthKey = date('m-Y');
// A. AJOUT CATÉGORIE TEMPORAIRE // 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']);
$type = $_POST['cat_type'] === 'credit' ? 'credit' : 'debit';
if ($name && $budget >= 0) { if ($name && $budget >= 0) {
$stmt = $pdo->prepare("INSERT INTO pf_monthly_categories (month_year, name, budget) VALUES (?, ?, ?)"); $stmt = $pdo->prepare("INSERT INTO pf_monthly_categories (month_year, name, type, budget) VALUES (?, ?, ?, ?)");
$stmt->execute([$currentMonthKey, $name, $budget]); $stmt->execute([$currentMonthKey, $name, $type, $budget]);
header("Location: ?tab=suivi"); exit; header("Location: ?tab=suivi"); exit;
} }
} }
// B. SUPPRESSION CATÉGORIE TEMPORAIRE // B. SUPPRESSION CATÉGORIE TEMPORAIRE
if (isset($_GET['del_cat'])) { if (isset($_GET['del_cat'])) {
$id = (int)$_GET['del_cat']; $pdo->prepare("DELETE FROM pf_monthly_categories WHERE id = ?")->execute([(int)$_GET['del_cat']]);
$pdo->prepare("DELETE FROM pf_monthly_categories WHERE id = ?")->execute([$id]);
header("Location: ?tab=suivi"); exit; header("Location: ?tab=suivi"); exit;
} }
// C. SAUVEGARDE IMPORT CSV // C. SAUVEGARDE SNAPSHOT BANCAIRE (MODIFIÉ : Remplacement de l'ancien)
if (isset($_POST['action']) && $_POST['action'] === 'save_snapshot') {
$date = $_POST['snapshot_date'];
$amount = floatval($_POST['snapshot_amount']);
// On vide la table pour que le nouveau remplace l'ancien
$pdo->query("DELETE FROM pf_bank_snapshots");
$pdo->prepare("INSERT INTO pf_bank_snapshots (snapshot_date, amount) VALUES (?, ?)")->execute([$date, $amount]);
header("Location: ?tab=suivi"); exit;
}
// D. SAUVEGARDE IMPORT CSV
if (isset($_POST['action']) && $_POST['action'] === 'save_import') { if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
$count = 0; $count = 0;
$tempCatMapping = [];
if (!empty($_POST['new_temp_cats'])) {
$stmtTemp = $pdo->prepare("INSERT INTO pf_monthly_categories (month_year, name, type, budget) VALUES (?, ?, ?, 0)");
foreach ($_POST['new_temp_cats'] as $tempKey => $catData) {
$stmtTemp->execute([$currentMonthKey, $catData['name'], $catData['type']]);
$tempCatMapping[$tempKey] = $pdo->lastInsertId();
}
}
$stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, category, label, amount, import_ref) VALUES (?, ?, ?, ?, ?)"); $stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, category, label, amount, import_ref) VALUES (?, ?, ?, ?, ?)");
$stmtRule = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category) VALUES (?, ?) ON DUPLICATE KEY UPDATE category = VALUES(category)"); $stmtRule = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category) VALUES (?, ?) ON DUPLICATE KEY UPDATE category = VALUES(category)");
if (isset($_POST['lines']) && is_array($_POST['lines'])) { if (isset($_POST['lines']) && is_array($_POST['lines'])) {
foreach ($_POST['lines'] as $line) { foreach ($_POST['lines'] as $line) {
if (!empty($line['cat']) && isset($line['import_check'])) { if (isset($line['import_check'])) {
$cat = $line['cat'];
$is_credit = isset($line['is_credit']) ? (int)$line['is_credit'] : 0;
if ($is_credit && empty($cat)) continue;
if (!$is_credit && empty($cat)) continue;
if (strpos($cat, 'NEW_TEMP_') === 0 && isset($tempCatMapping[$cat])) {
$cat = 'TEMP_' . $tempCatMapping[$cat];
}
$finalAmount = $is_credit ? -abs($line['amount']) : abs($line['amount']);
try { try {
$stmtExp->execute([$line['date'], $line['cat'], $line['label'], $line['amount'], $line['ref']]); $stmtExp->execute([$line['date'], $cat, $line['label'], $finalAmount, $line['ref']]);
$stmtRule->execute([$line['label'], $line['cat']]); $stmtRule->execute([$line['label'], $cat]);
$count++; $count++;
} catch (Exception $e) { continue; } } catch (Exception $e) { continue; }
} }
@@ -45,18 +80,13 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
header("Location: ?tab=suivi&msg=imported_$count"); exit; header("Location: ?tab=suivi&msg=imported_$count"); exit;
} }
// D. AJOUT DÉPENSE MANUELLE // E. AJOUT DÉPENSE MANUELLE
if (isset($_POST['action']) && $_POST['action'] === 'add_expense') { if (isset($_POST['action']) && $_POST['action'] === 'add_expense') {
$cat = $_POST['category']; $cat = $_POST['category'];
$amount = floatval($_POST['amount']); $amount = floatval($_POST['amount']);
$date = $_POST['date']; $date = $_POST['date'];
// Logique pour le label : Soit liste fermée (School), soit texte libre $label = ($cat === 'School' && !empty($_POST['label_select'])) ? trim($_POST['label_select']) : trim($_POST['label']);
if ($cat === 'School' && !empty($_POST['label_select'])) {
$label = trim($_POST['label_select']);
} else {
$label = trim($_POST['label']);
}
if ($label && $amount > 0) { if ($label && $amount > 0) {
$uniqueRef = "MANUAL_" . uniqid(); $uniqueRef = "MANUAL_" . uniqid();
@@ -66,22 +96,50 @@ if (isset($_POST['action']) && $_POST['action'] === 'add_expense') {
} }
} }
// E. SUPPRESSION DÉPENSE // F. SUPPRESSION DÉPENSE
if (isset($_GET['delete_expense'])) { if (isset($_GET['delete_expense'])) {
$pdo->prepare("DELETE FROM pf_expenses WHERE id = ?")->execute([(int)$_GET['delete_expense']]); $pdo->prepare("DELETE FROM pf_expenses WHERE id = ?")->execute([(int)$_GET['delete_expense']]);
header("Location: ?tab=suivi"); exit; header("Location: ?tab=suivi"); exit;
} }
// ============================================================================ // ============================================================================
// 2. CALCUL DES BUDGETS & INDICATEURS // 2. CALCUL DES BUDGETS & SNAPSHOT
// ============================================================================ // ============================================================================
$budget_fmcg = 0; $budget_school = 0; $budget_essence = 0; $budget_frais = 0; $budget_fmcg = 0; $budget_school = 0; $budget_essence = 0; $budget_frais = 0;
$total_income = 0; $total_expenses_prevues = 0; $total_income = 0; $total_expenses_prevues = 0;
$reste_a_venir = 0; // Somme des frais futurs $reste_a_venir = 0;
$today_day = (int)date('j'); // Jour du mois (1 à 31) $today_day = (int)date('j');
// 2.1 Récupération Budget Fixe // --- SNAPSHOT & SOLDE THÉORIQUE ---
$snapshot = ['date' => date('Y-m-d'), 'amount' => 0];
$solde_theorique = 0;
try {
$snapStmt = $pdo->query("SELECT * FROM pf_bank_snapshots ORDER BY snapshot_date DESC, id DESC LIMIT 1");
if ($s = $snapStmt->fetch(PDO::FETCH_ASSOC)) {
$snapshot = ['date' => $s['snapshot_date'], 'amount' => (float)$s['amount']];
}
} catch (Exception $e) {}
$solde_theorique = $snapshot['amount'];
// Calcul du solde théorique : On soustrait les opérations saisies APRÈS la date du snapshot
if (!empty($snapshot['date'])) {
try {
$stmtCalc = $pdo->prepare("SELECT SUM(amount) as total_diff FROM pf_expenses WHERE date_exp > ?");
$stmtCalc->execute([$snapshot['date']]);
$resDiff = $stmtCalc->fetch(PDO::FETCH_ASSOC);
if ($resDiff && $resDiff['total_diff'] !== null) {
// amount est positif pour les débits, négatif pour les crédits
// On soustrait donc le total diff. (Ex: 3500 - 50 = 3450)
$solde_theorique -= (float)$resDiff['total_diff'];
}
} catch (Exception $e) {}
}
// --- LECTURE BUDGET PREVISIONNEL ---
$stmt = $pdo->query("SELECT name, amount, type, category, is_estimate, payment_day FROM pf_budget_items"); $stmt = $pdo->query("SELECT name, amount, type, category, is_estimate, payment_day FROM pf_budget_items");
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) { while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
$rawAmount = (float)$item['amount']; $rawAmount = (float)$item['amount'];
@@ -94,20 +152,12 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
} else { } else {
$total_expenses_prevues += $amt; $total_expenses_prevues += $amt;
// --- CALCUL DU "RESTE À VENIR" ---
// Conditions : Expense + Mensuel + (Jour > Aujourd'hui OU C'est l'école)
if ($item['category'] === 'expense' && $item['type'] === 'Mensuel') { if ($item['category'] === 'expense' && $item['type'] === 'Mensuel') {
// Si c'est l'école, on l'ajoute toujours (selon ta demande) if ($name === 'Estimacio escola' || $pDay > $today_day) {
if ($name === 'Estimacio escola') {
$reste_a_venir += $rawAmount;
}
// Sinon, si c'est une autre dépense avec une date future
elseif ($pDay > $today_day) {
$reste_a_venir += $rawAmount; $reste_a_venir += $rawAmount;
} }
} }
// --- MAPPING CATÉGORIES ---
if ($name === 'Estimacio F&B & beauty') $budget_fmcg = $amt; if ($name === 'Estimacio F&B & beauty') $budget_fmcg = $amt;
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;
@@ -117,17 +167,21 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
} }
} }
// 2.2 Récupération Catégories Temporaires // Catégories Temporaires
$tempCats = []; $tempCats = []; $total_temp_budget = 0;
$total_temp_budget = 0;
try { try {
$stmt = $pdo->prepare("SELECT * FROM pf_monthly_categories WHERE month_year = ?"); $stmt = $pdo->prepare("SELECT * FROM pf_monthly_categories WHERE month_year = ?");
$stmt->execute([$currentMonthKey]); $stmt->execute([$currentMonthKey]);
$tempCats = $stmt->fetchAll(PDO::FETCH_ASSOC); $tempCats = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach($tempCats as $tc) $total_temp_budget += $tc['budget']; foreach($tempCats as $tc) {
// On n'ajoute au "budget prévisionnel" que si c'est un débit.
// Si c'est un crédit (réserve), l'argent est censé déjà être ou arriver sur le compte
if ($tc['type'] === 'debit') {
$total_temp_budget += $tc['budget'];
}
}
} catch (Exception $e) {} } catch (Exception $e) {}
// 2.3 Calcul Reste à vivre
$budget_autres = $total_income - ($total_expenses_prevues + $total_temp_budget); $budget_autres = $total_income - ($total_expenses_prevues + $total_temp_budget);
if ($budget_autres < 0) $budget_autres = 0; if ($budget_autres < 0) $budget_autres = 0;
@@ -136,19 +190,20 @@ if ($budget_autres < 0) $budget_autres = 0;
// ============================================================================ // ============================================================================
$categoriesConfig = [ $categoriesConfig = [
'FMCG' => ['label' => 'Courses (FMCG)', 'budget' => $budget_fmcg, 'color' => '#3b82f6', 'suggestions' => ['Action', 'Carrefour', 'Lidl']], 'FMCG' => ['type'=>'debit', 'label'=>'Courses (FMCG)', 'budget'=>$budget_fmcg, 'color'=>'#3b82f6', 'suggestions'=>['Action', 'Carrefour', 'Lidl']],
'Essence' => ['label' => 'Essence', 'budget' => $budget_essence, 'color' => '#f59e0b', 'suggestions' => ['Audi', 'Polo']], 'Essence' => ['type'=>'debit', 'label'=>'Essence', 'budget'=>$budget_essence, 'color'=>'#f59e0b', 'suggestions'=>['Audi', 'Polo']],
'School' => ['label' => 'École / Garde', 'budget' => $budget_school, 'color' => '#10b981', 'suggestions' => []], // Liste fermée gérée en JS 'School' => ['type'=>'debit', 'label'=>'École / Garde', 'budget'=>$budget_school, 'color'=>'#10b981', 'suggestions'=>[]],
'Frais' => ['label' => 'Charges Fixes', 'budget' => $budget_frais, 'color' => '#ef4444', 'suggestions' => ['Netflix', 'Assurance', 'Prêt']], 'Frais' => ['type'=>'debit', 'label'=>'Charges Fixes', 'budget'=>$budget_frais, 'color'=>'#ef4444', 'suggestions'=>['Netflix', 'Assurance', 'Prêt']],
]; ];
// Couleurs temporaires
$tempColors = ['#ec4899', '#06b6d4', '#84cc16', '#d946ef', '#f97316']; $tempColors = ['#ec4899', '#06b6d4', '#84cc16', '#d946ef', '#f97316'];
$colorIdx = 0; $colorIdx = 0;
foreach ($tempCats as $tc) { foreach ($tempCats as $tc) {
$catKey = 'TEMP_' . $tc['id']; $catKey = 'TEMP_' . $tc['id'];
$catType = isset($tc['type']) ? $tc['type'] : 'debit';
$categoriesConfig[$catKey] = [ $categoriesConfig[$catKey] = [
'type' => $catType,
'label' => $tc['name'], 'label' => $tc['name'],
'budget' => $tc['budget'], 'budget' => $tc['budget'],
'color' => $tempColors[$colorIdx % count($tempColors)], 'color' => $tempColors[$colorIdx % count($tempColors)],
@@ -159,14 +214,13 @@ foreach ($tempCats as $tc) {
$colorIdx++; $colorIdx++;
} }
$categoriesConfig['Autres'] = ['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']];
$categoriesConfig['LivretA'] = ['label' => 'Epargne', 'budget' => 0, 'color' => '#8b5cf6', 'suggestions' => ['Virement']]; $categoriesConfig['LivretA'] = ['type'=>'debit', 'label'=>'Epargne', 'budget'=>0, 'color'=>'#8b5cf6', 'suggestions'=>['Virement']];
// ============================================================================ // ============================================================================
// 4. DONNÉES & IMPORT // 4. DONNÉES RÉELLES & IMPORT
// ============================================================================ // ============================================================================
// CSV PREVIEW
$csvData = []; $csvData = [];
$showPreview = false; $showPreview = false;
if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) { if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
@@ -174,27 +228,40 @@ if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
$handle = fopen($file, "r"); $handle = fopen($file, "r");
$rules = []; try { $rules = $pdo->query("SELECT keyword, category FROM pf_import_rules")->fetchAll(PDO::FETCH_KEY_PAIR); } catch(Exception $e){} $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){} $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, ";"); fgetcsv($handle, 1000, ";");
while (($data = fgetcsv($handle, 1000, ";")) !== FALSE) { while (($data = fgetcsv($handle, 1000, ";")) !== FALSE) {
$rawDebit = $data[8] ?? ''; $rawDebit = $data[8] ?? '';
if (!empty($rawDebit)) { $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))); $amount = abs((float)str_replace(',', '.', str_replace(' ', '', $rawDebit)));
} else {
continue;
}
$dateParts = explode('/', $data[0]); $dateParts = explode('/', $data[0]);
$dateSql = (count($dateParts) == 3) ? $dateParts[2].'-'.$dateParts[1].'-'.$dateParts[0] : date('Y-m-d'); $dateSql = (count($dateParts) == 3) ? $dateParts[2].'-'.$dateParts[1].'-'.$dateParts[0] : date('Y-m-d');
$label = trim($data[1]) ?: trim($data[2]); $label = trim($data[1]) ?: trim($data[2]);
$refCSV = trim($data[3]); $refCSV = trim($data[3]);
$uniqueKey = !empty($refCSV) ? "REF_".$refCSV : "HASH_".md5($dateSql.$label.number_format($amount, 2));
$uniqueKey = !empty($refCSV) ? "REF_".$refCSV : "HASH_".md5($dateSql.$label.number_format($amount, 2).$isCredit);
$isDuplicate = in_array($uniqueKey, $existingRefs); $isDuplicate = in_array($uniqueKey, $existingRefs);
$suggestedCat = ''; $suggestedCat = '';
foreach ($rules as $kw => $c) { if (stripos($label, $kw) !== false) { $suggestedCat = $c; break; } } 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];
} $csvData[] = ['date'=>$dateSql, 'label'=>$label, 'amount'=>$amount, 'cat'=>$suggestedCat, 'ref'=>$uniqueKey, 'is_duplicate'=>$isDuplicate, 'is_credit'=>$isCredit];
} }
fclose($handle); fclose($handle);
$showPreview = true; $showPreview = true;
} }
// DÉPENSES RÉELLES // DÉPENSES EN BDD
$currentMonth = date('m'); $currentYear = date('Y'); $currentMonth = date('m'); $currentYear = date('Y');
$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");
$stmt->execute([$currentMonth, $currentYear]); $stmt->execute([$currentMonth, $currentYear]);
@@ -206,47 +273,45 @@ $expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []);
foreach ($allExpenses as $exp) { foreach ($allExpenses as $exp) {
$cat = $exp['category']; $cat = $exp['category'];
if (!isset($totals[$cat])) $cat = 'Autres'; if (!isset($totals[$cat])) $cat = 'Autres';
// Si la dépense est un crédit (montant < 0 en bdd), on augmente l'enveloppe
if ($exp['amount'] < 0) {
$categoriesConfig[$cat]['budget'] += abs($exp['amount']);
} else {
$totals[$cat] += $exp['amount']; $totals[$cat] += $exp['amount'];
}
$expensesByCategory[$cat][] = $exp; $expensesByCategory[$cat][] = $exp;
} }
$globalSpent = array_sum($totals); $globalSpent = array_sum($totals);
$globalBudget = array_sum(array_column($categoriesConfig, 'budget')); $globalBudget = array_sum(array_column($categoriesConfig, 'budget'));
// --- FONCTION D'AFFICHAGE ---
function getDisplayLogic($spent, $bg, $type) {
if ($type === 'credit') {
$remaining = $bg - $spent;
$pct = ($bg > 0) ? max(0, min(100, ($remaining / $bg) * 100)) : 0;
$isOver = ($remaining < 0);
$text = number_format(ceil($remaining), 0, ',', ' ') . ' / ' . number_format(ceil($bg), 0, ',', ' ') . ' €';
} else {
$pct = ($bg > 0) ? min(100, ($spent / $bg) * 100) : ($spent > 0 ? 100 : 0);
$isOver = ($spent > $bg && $bg > 0);
$text = number_format(ceil($spent), 0, ',', ' ') . ' / ' . number_format(ceil($bg), 0, ',', ' ') . ' €';
}
return ['pct' => $pct, 'isOver' => $isOver, 'text' => $text];
}
?> ?>
<style> <style>
.cat-card { .cat-card { background: white; border-radius: 16px; border: 1px solid #e2e8f0; display: flex; flex-direction: column; overflow: hidden; position: relative; }
background: white; border-radius: 16px; border: 1px solid #e2e8f0; .btn-add-item { background: rgba(255, 255, 255, 0.6); color: inherit; border: 1px solid rgba(0, 0, 0, 0.1); width: 28px; height: 28px; border-radius: 50%; font-size: 18px; line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
display: flex; flex-direction: column; overflow: hidden; position: relative; .btn-add-item:hover { background: white; transform: rotate(90deg) scale(1.1); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); border-color: currentColor; }
} .pf-modal { 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; }
.btn-add-item { .pf-modal-content { background: white; width: 95%; border-radius: 20px; box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1); padding: 30px; position: relative; }
background: rgba(255, 255, 255, 0.6); color: inherit; border: 1px solid rgba(0, 0, 0, 0.1); .progress-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-top: 20px; }
width: 28px; height: 28px; border-radius: 50%; font-size: 18px; line-height: 1; cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.btn-add-item:hover {
background: white; transform: rotate(90deg) scale(1.1);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); border-color: currentColor;
}
.pf-modal {
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;
}
.pf-modal-content {
background: white; width: 95%; border-radius: 20px;
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1); padding: 30px; position: relative;
}
.progress-grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-top: 20px;
}
/* Animation pour le reste à venir */
.fade-pulse { animation: pulseText 2s infinite; } .fade-pulse { animation: pulseText 2s infinite; }
@keyframes pulseText { @keyframes pulseText { 0% { opacity: 0.8; } 50% { opacity: 1; } 100% { opacity: 0.8; } }
0% { opacity: 0.8; } 50% { opacity: 1; } 100% { opacity: 0.8; }
}
</style> </style>
<div class="budget-view"> <div class="budget-view">
@@ -256,8 +321,20 @@ $globalBudget = array_sum(array_column($categoriesConfig, 'budget'));
<div> <div>
<h2 style="margin:0;">Suivi : <?= date('F Y') ?></h2> <h2 style="margin:0;">Suivi : <?= date('F Y') ?></h2>
<div style="margin-top:4px; font-size:0.9rem; color:#64748b;"> <div style="margin-top:4px; font-size:0.9rem; color:#64748b;">
Charges à venir ce mois : <strong class="fade-pulse" style="color:#f59e0b;"><?= number_format($reste_a_venir, 0, ',', ' ') ?> €</strong> Charges à venir : <strong class="fade-pulse" style="color:#f59e0b;"><?= number_format(ceil($reste_a_venir), 0, ',', ' ') ?> €</strong>
</div> </div>
<div style="margin-top:4px; font-size:0.9rem; color:#64748b; display:flex; align-items:center; gap:5px;">
Solde au <?= date('d/m', strtotime($snapshot['date'])) ?> :
<strong style="color:#1e293b;"><?= number_format($snapshot['amount'], 2, ',', ' ') ?> €</strong>
<button onclick="openSuiviModal('snapshotModal')" style="background:none; border:none; cursor:pointer; font-size:0.9rem; padding:0; filter:grayscale(1);">✏️</button>
</div>
<div style="margin-top:4px; font-size:0.9rem; color:#64748b; display:flex; align-items:center; gap:5px;">
Solde théorique au <?= date('d/m') ?> :
<strong style="color:#3b82f6;"><?= number_format($solde_theorique, 2, ',', ' ') ?> €</strong>
</div>
</div> </div>
<div style="text-align:right;"> <div style="text-align:right;">
<strong style="font-size:1.4rem; color:#1e293b;"><?= number_format(ceil($globalSpent), 0, ',', ' ') ?> €</strong> <strong style="font-size:1.4rem; color:#1e293b;"><?= number_format(ceil($globalSpent), 0, ',', ' ') ?> €</strong>
@@ -267,17 +344,16 @@ $globalBudget = array_sum(array_column($categoriesConfig, 'budget'));
<div class="progress-grid"> <div class="progress-grid">
<?php foreach ($categoriesConfig as $key => $conf): <?php foreach ($categoriesConfig as $key => $conf):
$spent = $totals[$key]; $bg = $conf['budget']; $logic = getDisplayLogic($totals[$key], $conf['budget'], $conf['type']);
$pct = ($bg > 0) ? min(100, ($spent/$bg)*100) : ($spent>0?100:0); $barCol = $logic['isOver'] ? '#ef4444' : $conf['color'];
$col = ($spent > $bg && $bg > 0) ? '#ef4444' : $conf['color'];
?> ?>
<div class="progress-card"> <div class="progress-card">
<div style="display:flex; justify-content:space-between; font-size:0.85rem; margin-bottom:5px;"> <div style="display:flex; justify-content:space-between; font-size:0.85rem; margin-bottom:5px;">
<span style="font-weight:600; color:<?= $conf['color'] ?>"><?= $conf['label'] ?></span> <span style="font-weight:600; color:<?= $conf['color'] ?>"><?= $conf['label'] ?></span>
<span><?= number_format(ceil($spent), 0, ',', ' ') ?> / <?= number_format(ceil($bg), 0, ',', ' ') ?></span> <span><?= $logic['text'] ?></span>
</div> </div>
<div style="background:#f1f5f9; height:8px; border-radius:4px; overflow:hidden;"> <div style="background:#f1f5f9; height:8px; border-radius:4px; overflow:hidden;">
<div style="width:<?= $pct ?>%; background:<?= $col ?>; height:100%;"></div> <div style="width:<?= $logic['pct'] ?>%; background:<?= $barCol ?>; height:100%;"></div>
</div> </div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
@@ -292,13 +368,20 @@ $globalBudget = array_sum(array_column($categoriesConfig, 'budget'));
<div id="addTempCatForm" style="display:none; background:#f8fafc; padding:15px; border-radius:12px; border:1px dashed #cbd5e1; margin-bottom:20px;"> <div id="addTempCatForm" style="display:none; background:#f8fafc; padding:15px; border-radius:12px; border:1px dashed #cbd5e1; margin-bottom:20px;">
<form method="POST" style="display:flex; gap:10px; align-items:end; flex-wrap:wrap;"> <form method="POST" style="display:flex; gap:10px; align-items:end; flex-wrap:wrap;">
<input type="hidden" name="action" value="add_temp_cat"> <input type="hidden" name="action" value="add_temp_cat">
<div style="flex:1; min-width:200px;"> <div style="flex:1; min-width:180px;">
<label class="pf-label">Nom</label> <label class="pf-label">Nom</label>
<input type="text" name="cat_name" class="pf-input" required> <input type="text" name="cat_name" class="pf-input" required>
</div> </div>
<div style="width:120px;"> <div style="width:120px;">
<label class="pf-label">Budget (€)</label> <label class="pf-label">Budget de base</label>
<input type="number" name="cat_budget" class="pf-input" step="1" required> <input type="number" name="cat_budget" class="pf-input" step="1" value="0" required>
</div>
<div style="width:150px;">
<label class="pf-label">Type</label>
<select name="cat_type" class="pf-input">
<option value="debit">Débit (Budget)</option>
<option value="credit">Crédit (Réserve)</option>
</select>
</div> </div>
<button type="submit" class="pf-btn" style="width:auto;">Créer</button> <button type="submit" class="pf-btn" style="width:auto;">Créer</button>
</form> </form>
@@ -316,31 +399,45 @@ $globalBudget = array_sum(array_column($categoriesConfig, 'budget'));
<?php else: ?> <?php else: ?>
<form method="POST" id="formMapping"> <form method="POST" id="formMapping">
<input type="hidden" name="action" value="save_import"> <input type="hidden" name="action" value="save_import">
<div style="display:flex; justify-content:space-between; margin-bottom:10px;"> <div id="dynamicNewCats"></div>
<h3>Valider l'importation</h3>
<span id="missingCount" style="color:red; font-weight:bold; display:none;"></span> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
<h3 style="margin:0;">Valider l'importation</h3>
<div>
<span id="missingCount" style="color:#ef4444; background:#fee2e2; padding:4px 10px; border-radius:12px; font-weight:bold; font-size:0.85rem; display:none; margin-right:10px;"></span>
<button type="button" class="btn-icon-small" onclick="openSuiviModal('newCatModal')" title="Créer une catégorie" style="display:inline-flex; vertical-align:middle; width:auto; padding:4px 10px;"> Catégorie</button>
</div> </div>
<div style="max-height:300px; overflow-y:auto; background:white; border:1px solid #eee;"> </div>
<div style="max-height:350px; overflow-y:auto; background:white; border:1px solid #eee; border-radius:8px;">
<table class="pf-table" style="margin:0;"> <table class="pf-table" style="margin:0;">
<thead><tr><th><input type="checkbox" onclick="toggleAll(this)" checked></th><th>Libellé</th><th>Montant</th><th>Catégorie</th></tr></thead> <thead style="position:sticky; top:0; z-index:10;">
<tr><th><input type="checkbox" onclick="toggleAll(this)" checked></th><th>Libellé</th><th>Montant</th><th>Catégorie</th></tr>
</thead>
<tbody> <tbody>
<?php foreach ($csvData as $idx => $row): <?php foreach ($csvData as $idx => $row):
$dup = $row['is_duplicate']; $dis = $dup?'disabled':''; ?> $dup = $row['is_duplicate']; $isCrd = $row['is_credit']; $dis = $dup?'disabled':'';
<tr style="<?= $dup?'opacity:0.5':(empty($row['cat'])?'background:#fff1f2':'') ?>"> $bgCol = $dup ? 'opacity:0.5' : (empty($row['cat']) && !$isCrd ? 'background:#fff1f2' : '');
<td><input type="checkbox" class="line-checkbox" name="lines[<?= $idx ?>][import_check]" value="1" <?= $dup?'':'checked' ?> <?= $dis ?> onchange="checkValidation()"> ?>
<tr style="<?= $bgCol ?>">
<td>
<input type="checkbox" class="line-checkbox" name="lines[<?= $idx ?>][import_check]" value="1" <?= $dup?'':'checked' ?> <?= $dis ?> onchange="checkValidation()">
<input type="hidden" name="lines[<?= $idx ?>][date]" value="<?= $row['date'] ?>"> <input type="hidden" name="lines[<?= $idx ?>][date]" value="<?= $row['date'] ?>">
<input type="hidden" name="lines[<?= $idx ?>][label]" value="<?= $row['label'] ?>"> <input type="hidden" name="lines[<?= $idx ?>][label]" value="<?= $row['label'] ?>">
<input type="hidden" name="lines[<?= $idx ?>][amount]" value="<?= $row['amount'] ?>"> <input type="hidden" name="lines[<?= $idx ?>][amount]" value="<?= $row['amount'] ?>">
<input type="hidden" name="lines[<?= $idx ?>][ref]" value="<?= $row['ref'] ?>"> <input type="hidden" name="lines[<?= $idx ?>][ref]" value="<?= $row['ref'] ?>">
<input type="hidden" class="is-credit-flag" name="lines[<?= $idx ?>][is_credit]" value="<?= $isCrd ?>">
</td> </td>
<td> <td>
<?= htmlspecialchars($row['label']) ?> <?= htmlspecialchars($row['label']) ?>
<?php if($dup): ?><span style="color:#ef4444; font-size:0.85em; font-weight:bold; margin-left:5px;">(déjà importé)</span><?php endif; ?> <?php if($dup): ?><small style="color:#ef4444; font-weight:bold; margin-left:5px;">(déjà importé)</small><?php endif; ?>
</td>
<td style="font-weight:bold; color:<?= $isCrd ? '#10b981' : '#1e293b' ?>;">
<?= $isCrd ? '+' : '-' ?> <?= number_format($row['amount'],2) ?> €
</td> </td>
<td><?= number_format($row['amount'],2) ?> €</td>
<td> <td>
<select name="lines[<?= $idx ?>][cat]" class="pf-input line-select" onchange="checkValidation()" <?= $dis ?>> <select name="lines[<?= $idx ?>][cat]" class="pf-input line-select" onchange="checkValidation()" <?= $dis ?>>
<option value="">-- ? --</option> <option value="">-- <?= $isCrd ? 'Ignorer (Crédit)' : 'À définir' ?> --</option>
<?php foreach ($categoriesConfig as $k => $c): ?> <?php foreach ($categoriesConfig as $k => $c): ?>
<option value="<?= $k ?>" <?= ($row['cat']===$k)?'selected':'' ?>><?= $c['label'] ?></option> <option value="<?= $k ?>" <?= ($row['cat']===$k)?'selected':'' ?>><?= $c['label'] ?></option>
<?php endforeach; ?> <?php endforeach; ?>
@@ -351,9 +448,9 @@ $globalBudget = array_sum(array_column($categoriesConfig, 'budget'));
</tbody> </tbody>
</table> </table>
</div> </div>
<div style="margin-top:10px; text-align:right;"> <div style="margin-top:15px; display:flex; justify-content:flex-end; gap:10px;">
<a href="?tab=suivi" class="pf-btn btn-secondary">Annuler</a> <a href="?tab=suivi" class="pf-btn btn-secondary" style="width:auto; margin:0;">Annuler</a>
<button type="submit" id="btnImport" class="pf-btn" style="width:auto;">Importer</button> <button type="submit" id="btnImport" class="pf-btn" style="width:auto; margin:0;">Importer</button>
</div> </div>
</form> </form>
<?php endif; ?> <?php endif; ?>
@@ -371,32 +468,32 @@ $globalBudget = array_sum(array_column($categoriesConfig, 'budget'));
<?php endif; ?> <?php endif; ?>
</h3> </h3>
<div style="font-size:0.85rem; color:#64748b; font-weight:600; margin-top:2px;"> <div style="font-size:0.85rem; color:#64748b; font-weight:600; margin-top:2px;">
<?= number_format(ceil($totals[$key]), 0, ',', ' ') ?> / <?= number_format(ceil($conf['budget']), 0, ',', ' ') ?> <?php $logic = getDisplayLogic($totals[$key], $conf['budget'], $conf['type']); echo $logic['text']; ?>
</div> </div>
</div> </div>
<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($conf['label']) ?>')"></button>
</div> </div>
<?php <?php $barCol = $logic['isOver'] ? '#ef4444' : $conf['color']; ?>
$bg = $conf['budget'];
$pct = ($bg > 0) ? min(100, ($totals[$key]/$bg)*100) : ($totals[$key]>0?100:0);
$barCol = ($totals[$key] > $bg && $bg > 0) ? '#ef4444' : $conf['color'];
?>
<div style="background:#f1f5f9; height:4px; width:100%;"> <div style="background:#f1f5f9; height:4px; width:100%;">
<div style="width:<?= $pct ?>%; background:<?= $barCol ?>; height:100%;"></div> <div style="width:<?= $logic['pct'] ?>%; background:<?= $barCol ?>; height:100%;"></div>
</div> </div>
<div style="flex:1; max-height:300px; overflow-y:auto; padding:0;"> <div style="flex:1; max-height:300px; overflow-y:auto; padding:0;">
<?php if (empty($expensesByCategory[$key])): ?> <?php if (empty($expensesByCategory[$key])): ?>
<div style="padding:20px; text-align:center; color:#cbd5e1; font-style:italic; font-size:0.85rem;">Aucune dépense</div> <div style="padding:20px; text-align:center; color:#cbd5e1; font-style:italic; font-size:0.85rem;">Aucune ligne.</div>
<?php else: ?> <?php else: ?>
<table style="width:100%; border-collapse:collapse; font-size:0.85rem;"> <table style="width:100%; border-collapse:collapse; font-size:0.85rem;">
<?php foreach ($expensesByCategory[$key] as $exp): ?> <?php foreach ($expensesByCategory[$key] as $exp): ?>
<tr style="border-bottom:1px solid #f8fafc;"> <tr style="border-bottom:1px solid #f8fafc;">
<td style="padding:10px 15px; color:#94a3b8;"><?= date('d/m', strtotime($exp['date_exp'])) ?></td> <td style="padding:10px 15px; color:#94a3b8;"><?= date('d/m', strtotime($exp['date_exp'])) ?></td>
<td style="padding:10px 5px; font-weight:500;"><?= htmlspecialchars($exp['label']) ?></td> <td style="padding:10px 5px; font-weight:500;"><?= htmlspecialchars($exp['label']) ?></td>
<td style="padding:10px 15px; text-align:right;">-<?= number_format($exp['amount'], 2) ?></td> <?php if($exp['amount'] < 0): ?>
<td style="width:20px; padding-right:10px;"><a href="?tab=suivi&delete_expense=<?= $exp['id'] ?>" onclick="return confirm('x ?')" style="color:#ef4444; text-decoration:none;">&times;</a></td> <td style="padding:10px 15px; text-align:right; font-weight:600; color:#10b981;">+<?= number_format(abs($exp['amount']), 2) ?></td>
<?php else: ?>
<td style="padding:10px 15px; text-align:right; font-weight:600; color:#1e293b;">-<?= number_format($exp['amount'], 2) ?></td>
<?php endif; ?>
<td style="width:20px; padding-right:10px;"><a href="?tab=suivi&delete_expense=<?= $exp['id'] ?>" onclick="return confirm('x ?')" style="color:#ef4444; text-decoration:none; font-size:1.2rem;">&times;</a></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</table> </table>
@@ -411,7 +508,7 @@ $globalBudget = array_sum(array_column($categoriesConfig, 'budget'));
<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 dépense</h3>
<button type="button" onclick="closeModal()" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">&times;</button> <button type="button" onclick="closeSuiviModal('manualExpenseModal')" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">&times;</button>
</div> </div>
<form method="POST"> <form method="POST">
@@ -442,81 +539,129 @@ $globalBudget = array_sum(array_column($categoriesConfig, 'budget'));
<input type="number" step="0.01" name="amount" class="pf-input" placeholder="0.00" required> <input type="number" step="0.01" name="amount" class="pf-input" placeholder="0.00" required>
</div> </div>
<div style="text-align:right; margin-top:20px;"> <div style="margin-top:20px; display:flex; justify-content:flex-end; gap:10px;">
<button type="button" onclick="closeModal()" class="pf-btn btn-secondary" style="margin-right:10px;">Annuler</button> <button type="button" onclick="closeSuiviModal('manualExpenseModal')" class="pf-btn btn-secondary" style="width:auto; margin:0;">Annuler</button>
<button type="submit" class="pf-btn">Ajouter</button> <button type="submit" class="pf-btn" style="width:auto; margin:0;">Ajouter</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<div id="snapshotModal" class="pf-modal">
<div class="pf-modal-content" style="max-width:350px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 style="margin:0;">Mettre à jour le solde</h3>
<button type="button" onclick="closeSuiviModal('snapshotModal')" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">&times;</button>
</div>
<form method="POST">
<input type="hidden" name="action" value="save_snapshot">
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Date du relevé</label>
<input type="date" name="snapshot_date" class="pf-input" value="<?= date('Y-m-d') ?>" required>
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Solde actuel (€)</label>
<input type="number" step="0.01" name="snapshot_amount" class="pf-input" placeholder="0.00" required>
</div>
<div style="margin-top:20px; display:flex; justify-content:flex-end; gap:10px;">
<button type="button" onclick="closeSuiviModal('snapshotModal')" class="pf-btn btn-secondary" style="width:auto; margin:0;">Annuler</button>
<button type="submit" class="pf-btn" style="width:auto; margin:0;">Enregistrer</button>
</div>
</form>
</div>
</div>
<div id="newCatModal" class="pf-modal">
<div class="pf-modal-content" style="max-width:350px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 style="margin:0;">Nouvelle catégorie</h3>
<button type="button" onclick="closeSuiviModal('newCatModal')" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">&times;</button>
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Nom (ex: Vacances)</label>
<input type="text" id="newCatName" class="pf-input">
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Type</label>
<select id="newCatType" class="pf-input">
<option value="debit">Débit (Dépense standard)</option>
<option value="credit">Crédit (Réserve d'argent)</option>
</select>
</div>
<div style="margin-top:20px; display:flex; justify-content:flex-end; gap:10px;">
<button type="button" onclick="closeSuiviModal('newCatModal')" class="pf-btn btn-secondary" style="width:auto; margin:0;">Annuler</button>
<button type="button" onclick="confirmNewCat()" class="pf-btn" style="width:auto; margin:0;">Créer</button>
</div>
</div>
</div>
<script> <script>
// UI Utils // --- UI ---
function toggleDiv(id) { const el = document.getElementById(id); el.style.display = (el.style.display === 'none') ? 'block' : 'none'; } function toggleDiv(id) { const el = document.getElementById(id); el.style.display = (el.style.display === 'none') ? 'block' : 'none'; }
function openSuiviModal(id) { document.getElementById(id).style.display = 'flex'; }
function closeSuiviModal(id) { document.getElementById(id).style.display = 'none'; }
window.onclick = function(event) { if (event.target.classList.contains('pf-modal')) event.target.style.display = 'none'; }
// MODALE LOGIQUE // --- SAISIE MANUELLE ---
const suggestions = <?= json_encode(array_map(fn($c) => $c['suggestions'], $categoriesConfig)) ?>; const suggestions = <?= json_encode(array_map(fn($c) => $c['suggestions'], $categoriesConfig)) ?>;
function openAddModal(catKey, catLabel) { function openAddModal(catKey, catLabel) {
const modal = document.getElementById('manualExpenseModal'); openSuiviModal('manualExpenseModal');
if(modal) {
modal.style.display = 'flex';
document.getElementById('modalTitle').innerText = "Dépense : " + catLabel; document.getElementById('modalTitle').innerText = "Dépense : " + catLabel;
document.getElementById('modalCatInput').value = catKey; document.getElementById('modalCatInput').value = catKey;
// GESTION DU CHAMP TITRE (Input vs Select pour School)
const blockText = document.getElementById('blockInputText'); const blockText = document.getElementById('blockInputText');
const blockSelect = document.getElementById('blockInputSelect'); const blockSelect = document.getElementById('blockInputSelect');
const inputLabel = document.getElementById('modalLabelInput'); const inputLabel = document.getElementById('modalLabelInput');
if (catKey === 'School') { if (catKey === 'School') {
// Mode Select Fermé blockText.style.display = 'none'; blockSelect.style.display = 'block'; inputLabel.required = false;
blockText.style.display = 'none';
blockSelect.style.display = 'block';
inputLabel.required = false; // On désactive le required du text
} else { } else {
// Mode Texte Libre blockText.style.display = 'block'; blockSelect.style.display = 'none'; inputLabel.required = true;
blockText.style.display = 'block';
blockSelect.style.display = 'none';
inputLabel.required = true;
// Chargement suggestions
const list = document.getElementById('modalSuggestions'); const list = document.getElementById('modalSuggestions');
list.innerHTML = ''; list.innerHTML = ''; inputLabel.value = '';
inputLabel.value = ''; if (suggestions[catKey]) suggestions[catKey].forEach(i => { const op = document.createElement('option'); op.value = i; list.appendChild(op); });
if (suggestions[catKey]) {
suggestions[catKey].forEach(item => {
const op = document.createElement('option');
op.value = item;
list.appendChild(op);
});
}
setTimeout(() => inputLabel.focus(), 100); setTimeout(() => inputLabel.focus(), 100);
} }
} }
// --- IMPORT CSV ---
let newCatIndex = 0;
function confirmNewCat() {
const name = document.getElementById('newCatName').value.trim();
const type = document.getElementById('newCatType').value;
if(name !== "") {
const key = 'NEW_TEMP_' + newCatIndex++;
document.getElementById('formMapping').insertAdjacentHTML('beforeend', `<input type="hidden" name="new_temp_cats[${key}][name]" value="${name}"><input type="hidden" name="new_temp_cats[${key}][type]" value="${type}">`);
document.querySelectorAll('.line-select').forEach(sel => {
const opt = document.createElement('option');
opt.value = key; opt.text = "🏷️ " + name + (type === 'credit' ? ' (Réserve)' : '');
sel.add(opt, sel.options[1]);
});
closeSuiviModal('newCatModal');
checkValidation();
}
} }
function closeModal() {
const modal = document.getElementById('manualExpenseModal');
if(modal) modal.style.display = 'none';
}
window.onclick = function(event) {
const modal = document.getElementById('manualExpenseModal');
if (event.target == modal) { closeModal(); }
}
// IMPORT CSV
function toggleAll(src) { document.querySelectorAll('.line-checkbox:not([disabled])').forEach(c => c.checked = src.checked); checkValidation(); } function toggleAll(src) { document.querySelectorAll('.line-checkbox:not([disabled])').forEach(c => c.checked = src.checked); checkValidation(); }
function checkValidation() { function checkValidation() {
const cbs = document.querySelectorAll('.line-checkbox:checked'); const cbs = document.querySelectorAll('.line-checkbox:checked');
let miss = 0; let miss = 0;
cbs.forEach(cb => { if(cb.closest('tr').querySelector('.line-select').value === "") miss++; }); cbs.forEach(cb => {
const row = cb.closest('tr');
const isCredit = row.querySelector('.is-credit-flag').value === '1';
if (row.querySelector('.line-select').value === "") {
if(!isCredit) miss++;
row.style.background = isCredit ? '' : '#fff1f2';
} else {
row.style.background = '';
}
});
const btn = document.getElementById('btnImport'); const btn = document.getElementById('btnImport');
const msg = document.getElementById('missingCount'); const msg = document.getElementById('missingCount');
if(miss>0) { if(miss>0) {
btn.disabled = true; btn.style.opacity=0.5; btn.style.cursor='not-allowed'; btn.disabled = true; btn.style.opacity=0.5; btn.style.cursor='not-allowed';
msg.style.display='inline'; msg.innerText=miss+' à définir'; msg.style.display='inline'; msg.innerText=miss+' à définir (débits)';
} else { } else {
btn.disabled = false; btn.style.opacity=1; btn.style.cursor='pointer'; btn.disabled = false; btn.style.opacity=1; btn.style.cursor='pointer';
msg.style.display='none'; msg.style.display='none';