budget +
This commit is contained in:
@@ -157,6 +157,7 @@
|
||||
color: var(--text-muted);
|
||||
border: 1px solid #cbd5e1;
|
||||
box-shadow: none;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.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);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@keyframes modalPop {
|
||||
@@ -512,6 +513,7 @@ input[type="checkbox"]:checked::after {
|
||||
.pf-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.pf-table {
|
||||
|
||||
@@ -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">×</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());
|
||||
}
|
||||
@@ -6,7 +6,6 @@ require_login();
|
||||
// --- ACTION : SUPPRESSION D'UNE ENTRÉE UNIQUE ---
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete_entry') {
|
||||
$owner = $_POST['owner'];
|
||||
$redirectTab = $_POST['redirect_tab'] ?? $owner;
|
||||
$date = $_POST['month_date'];
|
||||
$cat = $_POST['category'];
|
||||
|
||||
@@ -40,7 +39,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'dupli
|
||||
$stmtIns->execute([$owner, $targetDate, 'TOTAL_BANQUE', $newTotal]);
|
||||
|
||||
// 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)
|
||||
SELECT owner, ?, category, amount
|
||||
FROM pf_savings
|
||||
@@ -76,30 +74,37 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
|
||||
}
|
||||
|
||||
// --- ACTION : SAUVEGARDE CLASSIQUE (MODALE) ---
|
||||
// (Le code précédent reste ici pour la sauvegarde via le formulaire classique)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$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'];
|
||||
$dateObj = new DateTime($dateInput);
|
||||
$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 {
|
||||
$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->execute([$owner, $monthDate]);
|
||||
|
||||
// On réinsère les nouvelles données
|
||||
$stmtIns = $pdo->prepare("INSERT INTO pf_savings (owner, month_date, category, amount) VALUES (?, ?, ?, ?)");
|
||||
foreach ($values as $category => $amount) {
|
||||
$amount = floatval($amount);
|
||||
// On enregistre si c'est positif OU si c'est le total banque
|
||||
if ($amount > 0 || $category === 'TOTAL_BANQUE') {
|
||||
$stmtIns->execute([$owner, $monthDate, $category, $amount]);
|
||||
}
|
||||
}
|
||||
$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;
|
||||
} catch (Exception $e) {
|
||||
$pdo->rollBack();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
// modules/budget/views/epargne.php
|
||||
|
||||
// 1. Gestion des propriétaires à afficher
|
||||
$requestedOwner = $_GET['owner'] ?? 'Nens';
|
||||
|
||||
@@ -6,6 +8,18 @@ $requestedOwner = $_GET['owner'] ?? 'Nens';
|
||||
$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="view-header">
|
||||
@@ -14,7 +28,7 @@ $ownersToDisplay = ($requestedOwner === 'Nens') ? ['Pol', 'Pep'] : [$requestedOw
|
||||
<a href="?tab=epargne&owner=Laia" class="owner-tab <?= $requestedOwner === 'Laia' ? 'active' : '' ?>">Laia</a>
|
||||
<a href="?tab=epargne&owner=Nens" class="owner-tab <?= $requestedOwner === 'Nens' ? 'active' : '' ?>">Nens 👶</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php foreach ($ownersToDisplay as $currentOwner):
|
||||
// --- Récupération des données pour $currentOwner ---
|
||||
@@ -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="flex-grow: 1;">
|
||||
<?php if ($requestedOwner === 'Nens'):
|
||||
// On détermine la classe CSS basée sur le nom (pol ou pep)
|
||||
$themeClass = 'theme-' . strtolower($currentOwner);
|
||||
?>
|
||||
<h3 class="nens-title <?= $themeClass ?>" style="margin:0; font-size:1.2rem;">
|
||||
@@ -56,122 +69,340 @@ $ownersToDisplay = ($requestedOwner === 'Nens') ? ['Pol', 'Pep'] : [$requestedOw
|
||||
🔁 +1 Mois
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button onclick="openSavingsModal('add', '<?= $currentOwner ?>')" class="pf-btn">
|
||||
<button onclick="openCustomSavingsModal('<?= $currentOwner ?>')" class="pf-btn">
|
||||
+ Saisir un mois
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="table-responsive" style="background:white; border-radius:16px; box-shadow:var(--shadow-sm); border:1px solid #e2e8f0;">
|
||||
<?php if (empty($months)): ?>
|
||||
<div class="table-responsive" style="background:white; border-radius:16px; box-shadow:var(--shadow-sm); border:1px solid #e2e8f0;">
|
||||
<?php if (empty($months)): ?>
|
||||
<div style="padding: 30px; text-align: center; color: #64748b;">
|
||||
<p>Aucune donnée pour <?= htmlspecialchars($currentOwner) ?>.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<table class="pf-table savings-table nens-table theme-<?= strtolower($currentOwner) ?>" style="margin-top:0; box-shadow:none; border-radius:16px;"> <thead>
|
||||
<tr>
|
||||
<th class="sticky-col" style="background:#f8fafc;">Poste / Mois</th>
|
||||
<?php foreach ($months as $month): ?>
|
||||
<th>
|
||||
<div class="month-header-container">
|
||||
<span class="month-name"><?= date('M Y', strtotime($month)) ?></span>
|
||||
<div class="month-actions">
|
||||
<button class="btn-icon-small" title="Modifier"
|
||||
onclick='editMonth("<?= $month ?>", "<?= $currentOwner ?>", <?= json_encode($data[$month] ?? []) ?>, <?= json_encode($allCategories) ?>)'>
|
||||
✏️
|
||||
</button>
|
||||
<button class="btn-icon-small" title="Supprimer"
|
||||
onclick="deleteEntireMonth('<?= $month ?>', '<?= $currentOwner ?>')"
|
||||
style="color: #ef4444; border-color: #fca5a5; background: #fef2f2;">
|
||||
🗑️
|
||||
</button>
|
||||
<table class="pf-table savings-table nens-table theme-<?= strtolower($currentOwner) ?>" style="margin-top:0; box-shadow:none; border-radius:16px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sticky-col" style="background:#f8fafc;">Poste / Mois</th>
|
||||
<?php foreach ($months as $month): ?>
|
||||
<th>
|
||||
<div class="month-header-container">
|
||||
<span class="month-name"><?= date('M Y', strtotime($month)) ?></span>
|
||||
<div class="month-actions">
|
||||
<button class="btn-icon-small" title="Modifier"
|
||||
onclick='editCustomSavingsMonth("<?= $month ?>", "<?= $currentOwner ?>", <?= json_encode($data[$month] ?? []) ?>)'>
|
||||
✏️
|
||||
</button>
|
||||
<button class="btn-icon-small" title="Supprimer"
|
||||
onclick="deleteEntireMonth('<?= $month ?>', '<?= $currentOwner ?>')"
|
||||
style="color: #ef4444; border-color: #fca5a5; background: #fef2f2;">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="row-total">
|
||||
<td class="sticky-col"><strong>Total</strong></td>
|
||||
<?php foreach ($months as $month): ?>
|
||||
<td class="text-center font-bold" style="color: #2563eb;">
|
||||
<?= number_format($data[$month]['TOTAL_BANQUE'] ?? 0, 0, ',', ' ') ?> €
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="row-total">
|
||||
<td class="sticky-col"><strong>Total</strong></td>
|
||||
<?php foreach ($months as $month): ?>
|
||||
<td class="text-center font-bold" style="color: #2563eb;">
|
||||
<?= number_format($data[$month]['TOTAL_BANQUE'] ?? 0, 0, ',', ' ') ?> €
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
|
||||
<?php foreach ($allCategories as $cat): ?>
|
||||
<tr>
|
||||
<td class="sticky-col"><?= htmlspecialchars($cat) ?></td>
|
||||
<?php foreach ($months as $month): $amount = $data[$month][$cat] ?? 0; ?>
|
||||
<td class="text-center text-muted">
|
||||
<?php if ($amount != 0): ?>
|
||||
<div class="cell-content">
|
||||
<span>- <?= number_format($amount, 0, ',', ' ') ?> €</span>
|
||||
<button class="btn-cell-delete"
|
||||
onclick="deleteSavingsEntry('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= $currentOwner ?>')">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<?php else: ?> - <?php endif; ?>
|
||||
</td>
|
||||
<?php foreach ($allCategories as $cat): ?>
|
||||
<tr>
|
||||
<td class="sticky-col"><?= htmlspecialchars($cat) ?></td>
|
||||
<?php foreach ($months as $month): $amount = $data[$month][$cat] ?? 0; ?>
|
||||
<td class="text-center text-muted">
|
||||
<?php if ($amount != 0): ?>
|
||||
<div class="cell-content">
|
||||
<span>- <?= number_format($amount, 0, ',', ' ') ?> €</span>
|
||||
<button class="btn-cell-delete"
|
||||
onclick="deleteSavingsEntry('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= $currentOwner ?>')">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<?php else: ?> - <?php endif; ?>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<tr class="row-extres">
|
||||
<td class="sticky-col"><strong>Extra</strong></td>
|
||||
<?php foreach ($months as $month):
|
||||
$total = $data[$month]['TOTAL_BANQUE'] ?? 0;
|
||||
$sum = 0;
|
||||
foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0);
|
||||
$extra = $total - $sum;
|
||||
?>
|
||||
<td class="text-center font-bold" style="color: <?= $extra >= 0 ? '#10b981' : '#ef4444' ?>">
|
||||
<?= number_format($extra, 0, ',', ' ') ?> €
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<tr class="row-extres">
|
||||
<td class="sticky-col"><strong>Extra</strong></td>
|
||||
<?php foreach ($months as $month):
|
||||
$total = $data[$month]['TOTAL_BANQUE'] ?? 0;
|
||||
$sum = 0;
|
||||
foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0);
|
||||
$extra = $total - $sum;
|
||||
?>
|
||||
<td class="text-center font-bold" style="color: <?= $extra >= 0 ? '#10b981' : '#ef4444' ?>">
|
||||
<?= number_format($extra, 0, ',', ' ') ?> €
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?> </div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div id="savingsModal" class="pf-modal">
|
||||
<div class="pf-modal-content" style="max-width: 500px;">
|
||||
<h3 id="savingsModalTitle" class="pf-modal-title">Saisir le mois</h3>
|
||||
<div class="pf-modal-content" style="max-width: 600px; width: 95%;">
|
||||
<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;">×</button>
|
||||
</div>
|
||||
|
||||
<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="redirect_tab" id="redirect_tab" value="<?= htmlspecialchars($requestedOwner) ?>">
|
||||
|
||||
<div style="display:flex; gap:15px; margin-bottom:20px;">
|
||||
<div class="form-group" style="flex:1; margin:0;">
|
||||
<label class="pf-label">Mois concerné</label>
|
||||
<input type="date" name="month_date" id="sav_date" required class="pf-input">
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="flex:1; margin:0;">
|
||||
<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 no-spinners" style="font-weight:bold; color:#2563eb;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="separator" style="margin: 20px 0; border-bottom: 1px solid #e2e8f0;"></div>
|
||||
|
||||
<input type="hidden" name="redirect_tab" value="<?= $requestedOwner ?>">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="pf-label">Mois concerné</label>
|
||||
<input type="date" name="month_date" id="sav_date" required class="pf-input">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;">
|
||||
<div>
|
||||
<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 class="form-group">
|
||||
<label class="pf-label">Total (€)</label>
|
||||
<input type="number" step="0.01" name="values[TOTAL_BANQUE]" id="sav_total" required class="pf-input" style="font-weight:bold; color:#2563eb;">
|
||||
<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="separator" style="margin: 20px 0; border-bottom: 1px solid #eee;"></div>
|
||||
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
|
||||
<h4 style="margin:0; font-size:0.9rem; color:#64748b;">Ventilation</h4>
|
||||
<button type="button" class="btn-icon" onclick="addNewLine()" title="Ajouter une ligne" style="background:#e2e8f0;">+</button>
|
||||
</div>
|
||||
<div id="linesContainer" style="max-height: 350px; overflow-y: auto; padding-right:5px; display:flex; flex-direction:column; gap:10px;">
|
||||
</div>
|
||||
|
||||
<div id="linesContainer" style="max-height: 300px; overflow-y: auto; padding-right:5px; display:flex; flex-direction:column; gap:8px;"></div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" onclick="document.getElementById('savingsModal').style.display='none'" class="pf-btn btn-secondary">Annuler</button>
|
||||
<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>
|
||||
</form>
|
||||
</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;">×</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>
|
||||
+208
-112
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
// modules/budget/views/recap.php
|
||||
|
||||
// 1. Récupération des données
|
||||
$stmt = $pdo->query("SELECT * FROM pf_budget_items ORDER BY category DESC, sort_order ASC, name ASC");
|
||||
$items = $stmt->fetchAll();
|
||||
@@ -8,146 +10,155 @@ $totalRevenusMensuels = 0;
|
||||
?>
|
||||
|
||||
<div class="budget-view">
|
||||
<div class="view-header">
|
||||
<h2>Récapitulatif Mensuel</h2>
|
||||
<button onclick="openModal('add')" class="pf-btn">+ Ajouter un frais / revenu</button>
|
||||
<div class="view-header" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||
<h2 style="margin:0;">Récapitulatif Mensuel</h2>
|
||||
<button onclick="openRecapModal('add')" class="pf-btn">+ Ajouter un frais / revenu</button>
|
||||
</div>
|
||||
|
||||
<table class="pf-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>Montant</th>
|
||||
<th>Type</th>
|
||||
<th>Jour</th>
|
||||
<th>État prélèvement</th>
|
||||
<th>Régularisation</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($items as $item):
|
||||
// Calcul des totaux (Mensuel uniquement)
|
||||
if ($item['type'] === 'Mensuel') {
|
||||
if ($item['category'] === 'expense') {
|
||||
$totalDepensesMensuelles += $item['amount'];
|
||||
} else {
|
||||
$totalRevenusMensuels += $item['amount'];
|
||||
<div class="table-responsive" style="background:white; border-radius:16px; box-shadow:var(--shadow-sm); border:1px solid #e2e8f0; overflow:hidden;">
|
||||
<table class="pf-table" style="margin:0; box-shadow:none;">
|
||||
<thead style="background:#f8fafc;">
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>Montant</th>
|
||||
<th>Type</th>
|
||||
<th>Jour</th>
|
||||
<th>État prélèvement</th>
|
||||
<th>Régularisation</th>
|
||||
<th style="text-align:right;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($items as $item):
|
||||
// Calcul des totaux (Mensuel uniquement)
|
||||
if ($item['type'] === 'Mensuel') {
|
||||
if ($item['category'] === 'expense') {
|
||||
$totalDepensesMensuelles += $item['amount'];
|
||||
} else {
|
||||
$totalRevenusMensuels += $item['amount'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rowClass = ($item['category'] === 'income') ? 'row-income' : 'row-expense';
|
||||
if ($item['is_estimate']) $rowClass .= ' row-estimate';
|
||||
?>
|
||||
<tr class="<?= $rowClass ?>">
|
||||
<td>
|
||||
<strong><?= htmlspecialchars($item['name']) ?></strong>
|
||||
<?= $item['is_estimate'] ? ' <small>(Est.)</small>' : '' ?>
|
||||
</td>
|
||||
<td class="cell-amount">
|
||||
<?= number_format($item['amount'], 2, ',', ' ') ?> €
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge-type <?= strtolower($item['type']) ?>">
|
||||
<?= $item['type'] ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?= $item['payment_day'] ? $item['payment_day'] : '-' ?></td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox"
|
||||
<?= $item['is_checked'] ? 'checked' : '' ?>
|
||||
onclick="toggleCheck(<?= $item['id'] ?>, this.checked)"
|
||||
title="Marquer comme payé">
|
||||
<?= $item['is_checked'] ? ' <span class="text-success">Payé</span>' : ' <span style="color:var(--warning)">Attente</span>' ?>
|
||||
</td>
|
||||
<td>
|
||||
<em><?= htmlspecialchars($item['reg_month'] ?: '-') ?></em>
|
||||
</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn-icon" onclick='editItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="Modifier">✏️</button>
|
||||
<button class="btn-icon" onclick="deleteItem(<?= $item['id'] ?>)" title="Supprimer">🗑️</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="1"><strong>Total Revenus Mensuels</strong></td>
|
||||
<td colspan="6" class="text-success"><strong>+ <?= number_format($totalRevenusMensuels, 2, ',', ' ') ?> €</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1"><strong>Total Dépenses Mensuelles</strong></td>
|
||||
<td colspan="6" class="text-danger"><strong>- <?= number_format($totalDepensesMensuelles, 2, ',', ' ') ?> €</strong></td>
|
||||
</tr>
|
||||
<tr style="border-top: 2px solid #e2e8f0; background: white;">
|
||||
<td colspan="1"><strong>Équilibre du compte</strong></td>
|
||||
<?php $balance = $totalRevenusMensuels - $totalDepensesMensuelles; ?>
|
||||
<td colspan="6" style="font-size: 1.3em;" class="<?= $balance >= 0 ? 'text-success' : 'text-danger' ?>">
|
||||
<strong><?= number_format($balance, 2, ',', ' ') ?> € / mois</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
$rowClass = ($item['category'] === 'income') ? 'row-income' : 'row-expense';
|
||||
if ($item['is_estimate']) $rowClass .= ' row-estimate';
|
||||
?>
|
||||
<tr class="<?= $rowClass ?>" style="border-bottom:1px solid #f1f5f9;">
|
||||
<td style="padding:15px;">
|
||||
<strong><?= htmlspecialchars($item['name']) ?></strong>
|
||||
<?= $item['is_estimate'] ? ' <small style="color:#64748b;">(Est.)</small>' : '' ?>
|
||||
</td>
|
||||
<td class="cell-amount" style="font-weight:600; padding:15px; color:<?= $item['category']==='income'?'#10b981':'#1e293b' ?>;">
|
||||
<?= number_format($item['amount'], 2, ',', ' ') ?> €
|
||||
</td>
|
||||
<td style="padding:15px;">
|
||||
<span class="badge-type <?= strtolower($item['type']) ?>" style="background:#e2e8f0; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600; color:#475569;">
|
||||
<?= $item['type'] ?>
|
||||
</span>
|
||||
</td>
|
||||
<td style="padding:15px; color:#64748b;"><?= $item['payment_day'] ? $item['payment_day'] : '-' ?></td>
|
||||
<td style="padding:15px;">
|
||||
<div style="display:flex; align-items:center; gap:8px;">
|
||||
<input type="checkbox"
|
||||
<?= $item['is_checked'] ? 'checked' : '' ?>
|
||||
onclick="toggleItemCheck(<?= $item['id'] ?>, this.checked)"
|
||||
title="Marquer comme payé"
|
||||
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 style="padding:15px; color:#64748b; font-size:0.9rem;">
|
||||
<em><?= htmlspecialchars($item['reg_month'] ?: '-') ?></em>
|
||||
</td>
|
||||
<td style="padding:15px; text-align:right;">
|
||||
<div class="action-buttons" style="display:flex; gap:5px; justify-content:flex-end;">
|
||||
<button class="btn-icon" 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="deleteRecapItem(<?= $item['id'] ?>)" title="Supprimer" style="background:none; border:none; cursor:pointer; font-size:1.1rem; filter:grayscale(1); transition:0.2s;">🗑️</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot style="background:#f8fafc;">
|
||||
<tr>
|
||||
<td colspan="1" style="padding:15px;"><strong>Total Revenus Mensuels</strong></td>
|
||||
<td colspan="6" style="padding:15px; color:#10b981;"><strong>+ <?= number_format($totalRevenusMensuels, 2, ',', ' ') ?> €</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1" style="padding:15px;"><strong>Total Dépenses Mensuelles</strong></td>
|
||||
<td colspan="6" style="padding:15px; color:#ef4444;"><strong>- <?= number_format($totalDepensesMensuelles, 2, ',', ' ') ?> €</strong></td>
|
||||
</tr>
|
||||
<tr style="border-top: 2px solid #e2e8f0; background: white;">
|
||||
<td colspan="1" style="padding:15px; font-size:1.1rem;"><strong>Équilibre du compte</strong></td>
|
||||
<?php $balance = $totalRevenusMensuels - $totalDepensesMensuelles; ?>
|
||||
<td colspan="6" style="padding:15px; font-size: 1.3em;" class="<?= $balance >= 0 ? 'text-success' : 'text-danger' ?>">
|
||||
<strong style="color:<?= $balance >= 0 ? '#10b981' : '#ef4444' ?>;"><?= number_format($balance, 2, ',', ' ') ?> € / mois</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="budgetModal" class="pf-modal">
|
||||
<div class="pf-modal-content">
|
||||
<h3 id="modalTitle" class="pf-modal-title">Ajouter un élément</h3>
|
||||
<div id="budgetRecapModal" class="pf-modal" style="display:none; position:fixed; inset:0; z-index:9999; background:rgba(15, 23, 42, 0.6); backdrop-filter:blur(4px); align-items:center; justify-content:center;">
|
||||
<div class="pf-modal-content" style="background:white; width:95%; max-width:500px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px; position:relative;">
|
||||
|
||||
<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;">×</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="id" id="item_id">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="pf-label">Nom</label>
|
||||
<input type="text" name="name" id="item_name" required class="pf-input" placeholder="Ex: Loyer, Salaire...">
|
||||
<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;">Nom</label>
|
||||
<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 class="form-row">
|
||||
<div>
|
||||
<label class="pf-label">Montant (€)</label>
|
||||
<input type="number" step="0.01" name="amount" id="item_amount" required class="pf-input">
|
||||
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Montant (€)</label>
|
||||
<input type="number" step="0.01" name="amount" id="item_amount" required class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="pf-label">Jour (1-31)</label>
|
||||
<input type="number" min="1" max="31" name="payment_day" id="item_day" class="pf-input">
|
||||
<div style="flex:1;">
|
||||
<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" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div>
|
||||
<label class="pf-label">Catégorie</label>
|
||||
<select name="category" id="item_category" class="pf-input">
|
||||
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Catégorie</label>
|
||||
<select name="category" id="item_category" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value="expense">Dépense (Frais)</option>
|
||||
<option value="income">Revenu (Salaire)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="pf-label">Fréquence</label>
|
||||
<select name="type" id="item_type" class="pf-input">
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Fréquence</label>
|
||||
<select name="type" id="item_type" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value="Mensuel">Mensuel</option>
|
||||
<option value="Annuel">Annuel</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div>
|
||||
<label class="pf-label">Type de montant</label>
|
||||
<select name="is_estimate" id="item_is_estimate" class="pf-input">
|
||||
<div style="display:flex; gap:15px; margin-bottom:25px;">
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Type de montant</label>
|
||||
<select name="is_estimate" id="item_is_estimate" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value="0">Fixe (Facture)</option>
|
||||
<option value="1">Variable (Estimation)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="pf-label">Régularisation</label>
|
||||
<select name="reg_month" id="item_reg_month" class="pf-input">
|
||||
<div style="flex:1;">
|
||||
<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" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value="">Aucune</option>
|
||||
<?php
|
||||
$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 class="modal-footer">
|
||||
<button type="button" onclick="closeModal()" class="pf-btn btn-secondary">Annuler</button>
|
||||
<button type="submit" class="pf-btn">Enregistrer</button>
|
||||
<div style="display:flex; justify-content:flex-end; gap:10px;">
|
||||
<button type="button" onclick="closeRecapModal()" class="pf-btn btn-secondary" style="width:auto; margin:0; background:#f1f5f9; color:#475569; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;">Annuler</button>
|
||||
<button type="submit" class="pf-btn" style="width:auto; margin:0; background:#2563eb; color:white; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
+317
-172
@@ -7,36 +7,71 @@
|
||||
|
||||
$currentMonthKey = date('m-Y');
|
||||
|
||||
// A. AJOUT CATÉGORIE TEMPORAIRE
|
||||
// A. AJOUT CATÉGORIE TEMPORAIRE MANUELLE
|
||||
if (isset($_POST['action']) && $_POST['action'] === 'add_temp_cat') {
|
||||
$name = trim($_POST['cat_name']);
|
||||
$budget = floatval($_POST['cat_budget']);
|
||||
$type = $_POST['cat_type'] === 'credit' ? 'credit' : 'debit';
|
||||
|
||||
if ($name && $budget >= 0) {
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_monthly_categories (month_year, name, budget) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$currentMonthKey, $name, $budget]);
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_monthly_categories (month_year, name, type, budget) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$currentMonthKey, $name, $type, $budget]);
|
||||
header("Location: ?tab=suivi"); exit;
|
||||
}
|
||||
}
|
||||
|
||||
// B. SUPPRESSION CATÉGORIE TEMPORAIRE
|
||||
if (isset($_GET['del_cat'])) {
|
||||
$id = (int)$_GET['del_cat'];
|
||||
$pdo->prepare("DELETE FROM pf_monthly_categories WHERE id = ?")->execute([$id]);
|
||||
$pdo->prepare("DELETE FROM pf_monthly_categories WHERE id = ?")->execute([(int)$_GET['del_cat']]);
|
||||
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') {
|
||||
$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 (?, ?, ?, ?, ?)");
|
||||
$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'])) {
|
||||
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 {
|
||||
$stmtExp->execute([$line['date'], $line['cat'], $line['label'], $line['amount'], $line['ref']]);
|
||||
$stmtRule->execute([$line['label'], $line['cat']]);
|
||||
$stmtExp->execute([$line['date'], $cat, $line['label'], $finalAmount, $line['ref']]);
|
||||
$stmtRule->execute([$line['label'], $cat]);
|
||||
$count++;
|
||||
} catch (Exception $e) { continue; }
|
||||
}
|
||||
@@ -45,18 +80,13 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
|
||||
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') {
|
||||
$cat = $_POST['category'];
|
||||
$amount = floatval($_POST['amount']);
|
||||
$date = $_POST['date'];
|
||||
|
||||
// Logique pour le label : Soit liste fermée (School), soit texte libre
|
||||
if ($cat === 'School' && !empty($_POST['label_select'])) {
|
||||
$label = trim($_POST['label_select']);
|
||||
} else {
|
||||
$label = trim($_POST['label']);
|
||||
}
|
||||
$label = ($cat === 'School' && !empty($_POST['label_select'])) ? trim($_POST['label_select']) : trim($_POST['label']);
|
||||
|
||||
if ($label && $amount > 0) {
|
||||
$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'])) {
|
||||
$pdo->prepare("DELETE FROM pf_expenses WHERE id = ?")->execute([(int)$_GET['delete_expense']]);
|
||||
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;
|
||||
$total_income = 0; $total_expenses_prevues = 0;
|
||||
$reste_a_venir = 0; // Somme des frais futurs
|
||||
$today_day = (int)date('j'); // Jour du mois (1 à 31)
|
||||
$reste_a_venir = 0;
|
||||
$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");
|
||||
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$rawAmount = (float)$item['amount'];
|
||||
@@ -94,20 +152,12 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
} else {
|
||||
$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') {
|
||||
// Si c'est l'école, on l'ajoute toujours (selon ta demande)
|
||||
if ($name === 'Estimacio escola') {
|
||||
$reste_a_venir += $rawAmount;
|
||||
}
|
||||
// Sinon, si c'est une autre dépense avec une date future
|
||||
elseif ($pDay > $today_day) {
|
||||
if ($name === 'Estimacio escola' || $pDay > $today_day) {
|
||||
$reste_a_venir += $rawAmount;
|
||||
}
|
||||
}
|
||||
|
||||
// --- MAPPING CATÉGORIES ---
|
||||
if ($name === 'Estimacio F&B & beauty') $budget_fmcg = $amt;
|
||||
elseif ($name === 'Estimacio escola') $budget_school = $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
|
||||
$tempCats = [];
|
||||
$total_temp_budget = 0;
|
||||
// Catégories Temporaires
|
||||
$tempCats = []; $total_temp_budget = 0;
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT * FROM pf_monthly_categories WHERE month_year = ?");
|
||||
$stmt->execute([$currentMonthKey]);
|
||||
$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) {}
|
||||
|
||||
// 2.3 Calcul Reste à vivre
|
||||
$budget_autres = $total_income - ($total_expenses_prevues + $total_temp_budget);
|
||||
if ($budget_autres < 0) $budget_autres = 0;
|
||||
|
||||
@@ -136,19 +190,20 @@ if ($budget_autres < 0) $budget_autres = 0;
|
||||
// ============================================================================
|
||||
|
||||
$categoriesConfig = [
|
||||
'FMCG' => ['label' => 'Courses (FMCG)', 'budget' => $budget_fmcg, 'color' => '#3b82f6', 'suggestions' => ['Action', 'Carrefour', 'Lidl']],
|
||||
'Essence' => ['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
|
||||
'Frais' => ['label' => 'Charges Fixes', 'budget' => $budget_frais, 'color' => '#ef4444', 'suggestions' => ['Netflix', 'Assurance', 'Prêt']],
|
||||
'FMCG' => ['type'=>'debit', 'label'=>'Courses (FMCG)', 'budget'=>$budget_fmcg, 'color'=>'#3b82f6', 'suggestions'=>['Action', 'Carrefour', 'Lidl']],
|
||||
'Essence' => ['type'=>'debit', 'label'=>'Essence', 'budget'=>$budget_essence, 'color'=>'#f59e0b', 'suggestions'=>['Audi', 'Polo']],
|
||||
'School' => ['type'=>'debit', 'label'=>'École / Garde', 'budget'=>$budget_school, 'color'=>'#10b981', 'suggestions'=>[]],
|
||||
'Frais' => ['type'=>'debit', 'label'=>'Charges Fixes', 'budget'=>$budget_frais, 'color'=>'#ef4444', 'suggestions'=>['Netflix', 'Assurance', 'Prêt']],
|
||||
];
|
||||
|
||||
// Couleurs temporaires
|
||||
$tempColors = ['#ec4899', '#06b6d4', '#84cc16', '#d946ef', '#f97316'];
|
||||
$colorIdx = 0;
|
||||
|
||||
foreach ($tempCats as $tc) {
|
||||
$catKey = 'TEMP_' . $tc['id'];
|
||||
$catType = isset($tc['type']) ? $tc['type'] : 'debit';
|
||||
$categoriesConfig[$catKey] = [
|
||||
'type' => $catType,
|
||||
'label' => $tc['name'],
|
||||
'budget' => $tc['budget'],
|
||||
'color' => $tempColors[$colorIdx % count($tempColors)],
|
||||
@@ -159,14 +214,13 @@ foreach ($tempCats as $tc) {
|
||||
$colorIdx++;
|
||||
}
|
||||
|
||||
$categoriesConfig['Autres'] = ['label' => 'Autres / Imprévus', 'budget' => $budget_autres, 'color' => '#64748b', 'suggestions' => ['Restaurant', 'Cadeau']];
|
||||
$categoriesConfig['LivretA'] = ['label' => 'Epargne', 'budget' => 0, 'color' => '#8b5cf6', 'suggestions' => ['Virement']];
|
||||
$categoriesConfig['Autres'] = ['type'=>'debit', 'label'=>'Autres / Imprévus', 'budget'=>$budget_autres, 'color'=>'#64748b', 'suggestions'=>['Restaurant', 'Cadeau']];
|
||||
$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 = [];
|
||||
$showPreview = false;
|
||||
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");
|
||||
$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] ?? '';
|
||||
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)));
|
||||
$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));
|
||||
$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];
|
||||
} 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 RÉELLES
|
||||
// DÉPENSES EN BDD
|
||||
$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->execute([$currentMonth, $currentYear]);
|
||||
@@ -206,47 +273,45 @@ $expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []);
|
||||
foreach ($allExpenses as $exp) {
|
||||
$cat = $exp['category'];
|
||||
if (!isset($totals[$cat])) $cat = 'Autres';
|
||||
$totals[$cat] += $exp['amount'];
|
||||
|
||||
// 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'];
|
||||
}
|
||||
|
||||
$expensesByCategory[$cat][] = $exp;
|
||||
}
|
||||
|
||||
$globalSpent = array_sum($totals);
|
||||
$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>
|
||||
.cat-card {
|
||||
background: white; border-radius: 16px; border: 1px solid #e2e8f0;
|
||||
display: flex; flex-direction: column; overflow: hidden; position: relative;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
.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 */
|
||||
.cat-card { background: white; border-radius: 16px; border: 1px solid #e2e8f0; display: flex; flex-direction: column; overflow: hidden; position: relative; }
|
||||
.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); }
|
||||
.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; }
|
||||
.fade-pulse { animation: pulseText 2s infinite; }
|
||||
@keyframes pulseText {
|
||||
0% { opacity: 0.8; } 50% { opacity: 1; } 100% { opacity: 0.8; }
|
||||
}
|
||||
@keyframes pulseText { 0% { opacity: 0.8; } 50% { opacity: 1; } 100% { opacity: 0.8; } }
|
||||
</style>
|
||||
|
||||
<div class="budget-view">
|
||||
@@ -256,8 +321,20 @@ $globalBudget = array_sum(array_column($categoriesConfig, 'budget'));
|
||||
<div>
|
||||
<h2 style="margin:0;">Suivi : <?= date('F Y') ?></h2>
|
||||
<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 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 style="text-align:right;">
|
||||
<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">
|
||||
<?php foreach ($categoriesConfig as $key => $conf):
|
||||
$spent = $totals[$key]; $bg = $conf['budget'];
|
||||
$pct = ($bg > 0) ? min(100, ($spent/$bg)*100) : ($spent>0?100:0);
|
||||
$col = ($spent > $bg && $bg > 0) ? '#ef4444' : $conf['color'];
|
||||
$logic = getDisplayLogic($totals[$key], $conf['budget'], $conf['type']);
|
||||
$barCol = $logic['isOver'] ? '#ef4444' : $conf['color'];
|
||||
?>
|
||||
<div class="progress-card">
|
||||
<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><?= number_format(ceil($spent), 0, ',', ' ') ?> / <?= number_format(ceil($bg), 0, ',', ' ') ?> €</span>
|
||||
<span><?= $logic['text'] ?></span>
|
||||
</div>
|
||||
<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>
|
||||
<?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;">
|
||||
<form method="POST" style="display:flex; gap:10px; align-items:end; flex-wrap:wrap;">
|
||||
<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>
|
||||
<input type="text" name="cat_name" class="pf-input" required>
|
||||
</div>
|
||||
<div style="width:120px;">
|
||||
<label class="pf-label">Budget (€)</label>
|
||||
<input type="number" name="cat_budget" class="pf-input" step="1" required>
|
||||
<label class="pf-label">Budget de base</label>
|
||||
<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>
|
||||
<button type="submit" class="pf-btn" style="width:auto;">Créer</button>
|
||||
</form>
|
||||
@@ -316,31 +399,45 @@ $globalBudget = array_sum(array_column($categoriesConfig, 'budget'));
|
||||
<?php else: ?>
|
||||
<form method="POST" id="formMapping">
|
||||
<input type="hidden" name="action" value="save_import">
|
||||
<div style="display:flex; justify-content:space-between; margin-bottom:10px;">
|
||||
<h3>Valider l'importation</h3>
|
||||
<span id="missingCount" style="color:red; font-weight:bold; display:none;"></span>
|
||||
<div id="dynamicNewCats"></div>
|
||||
|
||||
<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 style="max-height:350px; overflow-y:auto; background:white; border:1px solid #eee; border-radius:8px;">
|
||||
<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>
|
||||
<?php foreach ($csvData as $idx => $row):
|
||||
$dup = $row['is_duplicate']; $dis = $dup?'disabled':''; ?>
|
||||
<tr style="<?= $dup?'opacity:0.5':(empty($row['cat'])?'background:#fff1f2':'') ?>">
|
||||
<td><input type="checkbox" class="line-checkbox" name="lines[<?= $idx ?>][import_check]" value="1" <?= $dup?'':'checked' ?> <?= $dis ?> onchange="checkValidation()">
|
||||
$dup = $row['is_duplicate']; $isCrd = $row['is_credit']; $dis = $dup?'disabled':'';
|
||||
$bgCol = $dup ? 'opacity:0.5' : (empty($row['cat']) && !$isCrd ? 'background:#fff1f2' : '');
|
||||
?>
|
||||
<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 ?>][label]" value="<?= $row['label'] ?>">
|
||||
<input type="hidden" name="lines[<?= $idx ?>][amount]" value="<?= $row['amount'] ?>">
|
||||
<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>
|
||||
<?= 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><?= number_format($row['amount'],2) ?> €</td>
|
||||
<td>
|
||||
<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): ?>
|
||||
<option value="<?= $k ?>" <?= ($row['cat']===$k)?'selected':'' ?>><?= $c['label'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
@@ -351,9 +448,9 @@ $globalBudget = array_sum(array_column($categoriesConfig, 'budget'));
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="margin-top:10px; text-align:right;">
|
||||
<a href="?tab=suivi" class="pf-btn btn-secondary">Annuler</a>
|
||||
<button type="submit" id="btnImport" class="pf-btn" style="width:auto;">Importer</button>
|
||||
<div style="margin-top:15px; display:flex; justify-content:flex-end; gap:10px;">
|
||||
<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; margin:0;">Importer</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
@@ -371,32 +468,32 @@ $globalBudget = array_sum(array_column($categoriesConfig, 'budget'));
|
||||
<?php endif; ?>
|
||||
</h3>
|
||||
<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>
|
||||
<button class="btn-add-item" style="color:<?= $conf['color'] ?>;" onclick="openAddModal('<?= $key ?>', '<?= addslashes($conf['label']) ?>')">+</button>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$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'];
|
||||
?>
|
||||
<?php $barCol = $logic['isOver'] ? '#ef4444' : $conf['color']; ?>
|
||||
<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 style="flex:1; max-height:300px; overflow-y:auto; padding:0;">
|
||||
<?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: ?>
|
||||
<table style="width:100%; border-collapse:collapse; font-size:0.85rem;">
|
||||
<?php foreach ($expensesByCategory[$key] as $exp): ?>
|
||||
<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 5px; font-weight:500;"><?= htmlspecialchars($exp['label']) ?></td>
|
||||
<td style="padding:10px 15px; text-align:right;">-<?= number_format($exp['amount'], 2) ?></td>
|
||||
<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;">×</a></td>
|
||||
<?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>
|
||||
<?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;">×</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
@@ -411,7 +508,7 @@ $globalBudget = array_sum(array_column($categoriesConfig, 'budget'));
|
||||
<div class="pf-modal-content" style="max-width:400px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||
<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;">×</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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div style="text-align:right; margin-top:20px;">
|
||||
<button type="button" onclick="closeModal()" class="pf-btn btn-secondary" style="margin-right:10px;">Annuler</button>
|
||||
<button type="submit" class="pf-btn">Ajouter</button>
|
||||
<div style="margin-top:20px; display:flex; justify-content:flex-end; gap:10px;">
|
||||
<button type="button" onclick="closeSuiviModal('manualExpenseModal')" class="pf-btn btn-secondary" style="width:auto; margin:0;">Annuler</button>
|
||||
<button type="submit" class="pf-btn" style="width:auto; margin:0;">Ajouter</button>
|
||||
</div>
|
||||
</form>
|
||||
</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;">×</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;">×</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>
|
||||
// UI Utils
|
||||
// --- UI ---
|
||||
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)) ?>;
|
||||
|
||||
function openAddModal(catKey, catLabel) {
|
||||
const modal = document.getElementById('manualExpenseModal');
|
||||
if(modal) {
|
||||
modal.style.display = 'flex';
|
||||
document.getElementById('modalTitle').innerText = "Dépense : " + catLabel;
|
||||
document.getElementById('modalCatInput').value = catKey;
|
||||
|
||||
// GESTION DU CHAMP TITRE (Input vs Select pour School)
|
||||
const blockText = document.getElementById('blockInputText');
|
||||
const blockSelect = document.getElementById('blockInputSelect');
|
||||
const inputLabel = document.getElementById('modalLabelInput');
|
||||
openSuiviModal('manualExpenseModal');
|
||||
document.getElementById('modalTitle').innerText = "Dépense : " + catLabel;
|
||||
document.getElementById('modalCatInput').value = catKey;
|
||||
|
||||
const blockText = document.getElementById('blockInputText');
|
||||
const blockSelect = document.getElementById('blockInputSelect');
|
||||
const inputLabel = document.getElementById('modalLabelInput');
|
||||
|
||||
if (catKey === 'School') {
|
||||
// Mode Select Fermé
|
||||
blockText.style.display = 'none';
|
||||
blockSelect.style.display = 'block';
|
||||
inputLabel.required = false; // On désactive le required du text
|
||||
} else {
|
||||
// Mode Texte Libre
|
||||
blockText.style.display = 'block';
|
||||
blockSelect.style.display = 'none';
|
||||
inputLabel.required = true;
|
||||
|
||||
// Chargement suggestions
|
||||
const list = document.getElementById('modalSuggestions');
|
||||
list.innerHTML = '';
|
||||
inputLabel.value = '';
|
||||
if (suggestions[catKey]) {
|
||||
suggestions[catKey].forEach(item => {
|
||||
const op = document.createElement('option');
|
||||
op.value = item;
|
||||
list.appendChild(op);
|
||||
});
|
||||
}
|
||||
setTimeout(() => inputLabel.focus(), 100);
|
||||
}
|
||||
if (catKey === 'School') {
|
||||
blockText.style.display = 'none'; blockSelect.style.display = 'block'; inputLabel.required = false;
|
||||
} else {
|
||||
blockText.style.display = 'block'; blockSelect.style.display = 'none'; inputLabel.required = true;
|
||||
const list = document.getElementById('modalSuggestions');
|
||||
list.innerHTML = ''; inputLabel.value = '';
|
||||
if (suggestions[catKey]) suggestions[catKey].forEach(i => { const op = document.createElement('option'); op.value = i; list.appendChild(op); });
|
||||
setTimeout(() => inputLabel.focus(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const modal = document.getElementById('manualExpenseModal');
|
||||
if(modal) modal.style.display = 'none';
|
||||
// --- 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();
|
||||
}
|
||||
}
|
||||
|
||||
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 checkValidation() {
|
||||
const cbs = document.querySelectorAll('.line-checkbox:checked');
|
||||
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 msg = document.getElementById('missingCount');
|
||||
if(miss>0) {
|
||||
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 {
|
||||
btn.disabled = false; btn.style.opacity=1; btn.style.cursor='pointer';
|
||||
msg.style.display='none';
|
||||
|
||||
Reference in New Issue
Block a user