This commit is contained in:
2026-02-09 13:07:39 +01:00
parent f5822f2694
commit 43bcdc83a1
13 changed files with 1480 additions and 8 deletions
+55
View File
@@ -0,0 +1,55 @@
<?php
require __DIR__ . '/includes/auth.php';
require __DIR__ . '/includes/db.php';
require_login('/login.php');
// Gestion de l'onglet actif (par défaut 'recap')
$tab = $_GET['tab'] ?? 'recap';
$pageTitle = "PachaFamily - Budget";
$activePage = "budget";
$pageCss = "/modules/budget/budget.css";
$pageJs = "/modules/budget/budget.js";
require __DIR__ . '/header.php';
?>
<div class="pf-container">
<div class="pf-hero" style="text-align: center; margin-bottom: 30px;">
<h1 style="margin-bottom: 20px;">Gestion du Budget</h1>
<nav class="budget-tabs-container">
<a href="?tab=recap" class="tab-item <?= $tab == 'recap' ? 'active' : '' ?>">
<span class="tab-icon">📊</span>
<span>Récapitulatif</span>
</a>
<a href="?tab=suivi" class="tab-item <?= $tab == 'suivi' ? 'active' : '' ?>">
<span class="tab-icon">🗓️</span>
<span>Suivi Mensuel</span>
</a>
<a href="?tab=epargne" class="tab-item <?= $tab == 'epargne' ? 'active' : '' ?>">
<span class="tab-icon">🐷</span>
<span>Épargne</span>
</a>
</nav>
</div>
<section class="pf-section">
<?php
// On définit le chemin relatif par rapport à la racine du projet
$viewPath = __DIR__ . "/modules/budget/views/" . $tab . ".php";
if (file_exists($viewPath)) {
include $viewPath;
} else {
// Petit debug pour voir où PHP cherche exactement le fichier
echo "<p>Erreur : Le fichier est introuvable dans " . htmlspecialchars($viewPath) . "</p>";
echo "<p>Onglet en cours de développement...</p>";
}
?>
</section>
</div>
<script src="/modules/budget/budget.js"></script>
<?php require __DIR__ . '/footer.php'; ?>
+1
View File
@@ -21,6 +21,7 @@ $activePage = $activePage ?? "";
<nav class="pf-nav"> <nav class="pf-nav">
<a href="/index.php" class="pf-nav-link <?= $activePage === 'home' ? 'pf-nav-link--active' : '' ?>">Accueil</a> <a href="/index.php" class="pf-nav-link <?= $activePage === 'home' ? 'pf-nav-link--active' : '' ?>">Accueil</a>
<a href="/family-calendar.php" class="pf-nav-link <?= $activePage === 'family-calendar' ? 'pf-nav-link--active' : '' ?>">Calendrier</a> <a href="/family-calendar.php" class="pf-nav-link <?= $activePage === 'family-calendar' ? 'pf-nav-link--active' : '' ?>">Calendrier</a>
<a href="/budget.php" class="pf-nav-link <?= $activePage === 'budget' ? 'pf-nav-link--active' : '' ?>">Budget</a>
<a href="/holidays.php" class="pf-nav-link <?= $activePage === 'holidays' ? 'pf-nav-link--active' : '' ?>">Vacances</a> <a href="/holidays.php" class="pf-nav-link <?= $activePage === 'holidays' ? 'pf-nav-link--active' : '' ?>">Vacances</a>
<a href="/gift-list.php" class="pf-nav-link <?= $activePage === 'gift-list' ? 'pf-nav-link--active' : '' ?>">Cadeaux</a> <a href="/gift-list.php" class="pf-nav-link <?= $activePage === 'gift-list' ? 'pf-nav-link--active' : '' ?>">Cadeaux</a>
</nav> </nav>
+4 -4
View File
@@ -58,14 +58,14 @@ require __DIR__ . '/header.php';
<span class="pf-card-cta">Voir les listes</span> <span class="pf-card-cta">Voir les listes</span>
</a> </a>
<div class="pf-module-card pf-card--disabled"> <a href="/budget.php" class="pf-module-card">
<div class="pf-card-icon">💰</div> <div class="pf-card-icon">💰</div>
<h3 class="pf-card-title">Budget</h3> <h3 class="pf-card-title">Budget</h3>
<div class="pf-card-desc"> <div class="pf-card-desc">
Bientôt disponible : suivi des dépenses, catégories et budget mensuel. Suivi des dépenses fixes, revenus et équilibre du compte familial.
</div>
<span class="pf-card-cta">À venir</span>
</div> </div>
<span class="pf-card-cta">Gérer</span>
</a>
</div> </div>
</section> </section>
+663
View File
@@ -0,0 +1,663 @@
/* modules/budget/budget.css */
/* --- 1. VARIABLES & BASE --- */
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--success: #10b981; /* Vert */
--danger: #ef4444; /* Rouge */
--warning: #f59e0b; /* Jaune */
--bg-page: #f1f5f9;
--bg-card: #ffffff;
--text-main: #1e293b;
--text-muted: #64748b;
--radius-l: 16px;
--radius-m: 10px;
--radius-s: 6px;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg:
0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
/* --- 2. LAYOUT & CONTAINER --- */
.budget-view {
font-family:
"Segoe UI",
system-ui,
-apple-system,
sans-serif;
color: var(--text-main);
animation: fadeIn 0.4s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* En-tête de la vue */
.view-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
gap: 16px;
}
.view-header h2 {
font-size: 1.5rem;
font-weight: 800;
color: #0f172a; /* Très sombre, quasi noir */
margin: 0;
letter-spacing: -0.025em;
}
/* --- 3. NAVIGATION TABS (Pills) --- */
.budget-tabs-container {
display: inline-flex;
background: var(--bg-card);
padding: 6px;
border-radius: 50px;
box-shadow: var(--shadow-md);
gap: 8px;
position: relative;
z-index: 10;
}
.tab-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 24px;
border-radius: 40px;
text-decoration: none;
color: var(--text-muted);
font-weight: 600;
font-size: 0.95rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid transparent;
}
.tab-item:hover {
background-color: var(--bg-page);
color: var(--text-main);
}
.tab-item.active {
background: var(--primary);
color: white;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
transform: scale(1.02);
}
.tab-icon {
font-size: 1.1em;
line-height: 1;
}
/* Sous-onglets propriétaire (Alex/Laia) */
.owner-tabs {
display: inline-flex;
background: #e2e8f0;
border-radius: 8px;
padding: 4px;
}
.owner-tab {
padding: 6px 20px;
border-radius: 6px;
text-decoration: none;
color: var(--text-muted);
font-weight: 600;
transition: 0.2s;
}
.owner-tab.active {
background: var(--bg-card);
color: var(--primary);
box-shadow: var(--shadow-sm);
}
/* --- 4. BOUTONS --- */
.pf-btn {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: white;
border: none;
border-radius: 50px;
padding: 10px 24px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
box-shadow: var(--shadow-sm);
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 8px;
}
.pf-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
}
.pf-btn.btn-secondary {
background: var(--bg-card);
color: var(--text-muted);
border: 1px solid #cbd5e1;
box-shadow: none;
}
.pf-btn.btn-secondary:hover {
background: var(--bg-page);
color: var(--text-main);
border-color: #94a3b8;
}
.btn-icon,
.btn-icon-small {
background: transparent;
border: none;
cursor: pointer;
padding: 6px;
border-radius: var(--radius-s);
transition: background 0.2s;
}
.btn-icon {
font-size: 1.1rem;
}
.btn-icon-small {
font-size: 0.9rem;
padding: 4px;
}
.btn-icon:hover,
.btn-icon-small:hover {
background: #e2e8f0;
transform: scale(1.1);
}
/* Actions groupées */
.action-buttons {
display: flex;
gap: 8px;
}
/* --- 5. TABLEAU MODERNE --- */
.pf-table {
width: 100%;
background: var(--bg-card);
border-radius: var(--radius-l);
box-shadow: var(--shadow-md);
border-collapse: separate;
border-spacing: 0;
overflow: hidden;
}
.pf-table th {
background: #f8fafc;
padding: 16px;
text-align: left;
font-weight: 600;
color: var(--text-muted);
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid #e2e8f0;
white-space: nowrap;
}
.pf-table td {
padding: 3px 16px; /* Ajusté pour être moins haut */
border-bottom: 1px solid var(--bg-page);
vertical-align: middle;
font-size: 0.95rem;
}
.pf-table tbody tr:last-child td {
border-bottom: none;
}
.pf-table tbody tr:hover {
background-color: #f8fafc;
}
/* Styles spécifiques aux lignes */
.row-income td:first-child {
border-left: 4px solid var(--success);
}
.row-expense td:first-child {
border-left: 4px solid var(--danger);
}
.row-estimate td:first-child {
border-left: 4px solid var(--warning);
}
.row-estimate td {
color: var(--text-muted);
font-style: italic;
}
/* Styles tableau épargne */
.savings-table .row-total td {
background: var(--bg-page);
border-bottom: 2px solid #cbd5e1;
}
.savings-table .row-extres td {
background: #fffbeb;
border-top: 2px solid #e2e8f0;
}
.table-responsive {
overflow-x: auto;
}
.sticky-col {
position: sticky;
left: 0;
background: var(--bg-card);
z-index: 2;
border-right: 1px solid var(--bg-page);
min-width: 150px;
}
/* Cellules spéciales */
.cell-amount {
font-family: "Consolas", "Monaco", monospace;
font-weight: 700;
}
.cell-content {
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
height: 100%;
}
/* Bouton supprimer au survol */
.btn-cell-delete {
display: none;
background: rgba(239, 68, 68, 0.1);
color: var(--danger);
border: none;
border-radius: 4px;
width: 20px;
height: 20px;
font-size: 14px;
line-height: 1;
cursor: pointer;
padding: 0;
align-items: center;
justify-content: center;
position: absolute;
right: -5px;
}
.btn-cell-delete:hover {
background: var(--danger);
color: white;
}
td:hover .btn-cell-delete {
display: flex;
}
/* Footer du tableau */
.pf-table tfoot {
background: #f8fafc;
border-top: 2px solid #e2e8f0;
}
.pf-table tfoot td {
padding: 18px 16px;
border-top: 1px solid #e2e8f0;
}
/* --- 6. FORMULAIRES & INPUTS --- */
.pf-label {
display: block;
color: var(--text-muted);
font-weight: 600;
font-size: 0.85rem;
margin-bottom: 6px;
}
.pf-input {
width: 100%;
padding: 10px 12px;
border: 1px solid #cbd5e1;
border-radius: var(--radius-m);
font-size: 1rem;
background: #f8fafc;
color: var(--text-main);
box-sizing: border-box;
transition: all 0.2s;
}
.pf-input:focus {
background: var(--bg-card);
border-color: var(--primary);
outline: none;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.form-group {
margin-bottom: 16px;
}
/* Checkbox Custom */
input[type="checkbox"] {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid #cbd5e1;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
vertical-align: middle;
margin-right: 8px;
}
input[type="checkbox"]:checked {
background-color: var(--success);
border-color: var(--success);
}
input[type="checkbox"]:checked::after {
content: "✓";
color: white;
font-size: 14px;
font-weight: bold;
}
/* --- 7. MODALE --- */
.pf-modal {
display: none;
position: fixed;
inset: 0;
z-index: 9999;
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px); /* Safari support */
align-items: center;
justify-content: center;
padding: 20px;
}
.pf-modal-content {
background: var(--bg-card);
width: 100%;
max-width: 500px;
border-radius: 20px;
box-shadow: var(--shadow-lg);
padding: 32px;
position: relative;
animation: modalPop 0.3s cubic-bezier(0.16, 1, 0.3, 1);
display: flex;
flex-direction: column;
gap: 16px;
}
@keyframes modalPop {
from {
transform: scale(0.95) translateY(10px);
opacity: 0;
}
to {
transform: scale(1) translateY(0);
opacity: 1;
}
}
.pf-modal-title {
margin-top: 0;
font-size: 1.5rem;
font-weight: 800;
color: var(--text-main);
border-bottom: 1px solid #e2e8f0;
padding-bottom: 15px;
margin-bottom: 8px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #e2e8f0;
}
/* --- 8. UTILITAIRES & BADGES --- */
.text-success {
color: var(--success);
}
.text-danger {
color: var(--danger);
}
.text-muted {
color: var(--text-muted);
}
.font-bold {
font-weight: 700;
}
.badge-type {
display: inline-block;
padding: 4px 10px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
}
.badge-type.mensuel {
background: #eff6ff;
color: var(--primary);
}
.badge-type.annuel {
background: #f3e8ff;
color: #9333ea;
}
.budget-note {
margin-top: 15px;
font-size: 0.85em;
opacity: 0.8;
color: var(--text-muted);
}
/* --- 9. MOBILE RESPONSIVE --- */
@media (max-width: 768px) {
.budget-tabs-container {
display: flex;
width: 100%;
overflow-x: auto;
border-radius: 15px;
justify-content: space-between;
}
.tab-item {
padding: 10px 15px;
flex: 1;
justify-content: center;
white-space: nowrap;
font-size: 0.85rem;
}
.view-header {
flex-direction: column;
align-items: flex-start;
}
.pf-btn {
width: 100%;
justify-content: center;
}
.pf-table {
display: block;
overflow-x: auto;
white-space: nowrap;
}
/* Sur mobile, les formulaires passent en 1 colonne */
.form-row {
grid-template-columns: 1fr;
}
}
/* --- EN-TÊTE DES COLONNES MOIS (Optimisé) --- */
/* Conteneur principal dans le <th> */
.month-header-container {
display: flex;
flex-direction: column; /* Empile texte et boutons */
align-items: center; /* Centre horizontalement */
justify-content: center;
gap: 6px; /* Espace entre le texte et les boutons */
min-width: 100px; /* Largeur min pour éviter l'écrasement */
}
/* Le texte du mois */
.month-name {
font-weight: 700;
text-transform: uppercase;
font-size: 0.85rem;
color: var(--text-main);
letter-spacing: 0.05em;
line-height: 1.2;
}
/* Conteneur des boutons */
.month-actions {
display: flex;
gap: 4px;
opacity: 0.5; /* Un peu transparent par défaut pour ne pas surcharger */
transition: opacity 0.2s ease;
}
/* On affiche les boutons pleinement au survol de la colonne */
th:hover .month-actions {
opacity: 1;
}
/* Style spécifique pour réduire la taille des boutons d'en-tête */
.month-actions .btn-icon-small {
font-size: 0.75rem; /* Icône plus petite */
padding: 2px 6px; /* Padding réduit */
height: 24px; /* Hauteur fixe et petite */
width: 28px;
display: flex;
align-items: center;
justify-content: center;
background: white;
border: 1px solid #cbd5e1;
border-radius: 4px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.month-actions .btn-icon-small:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* --- LIGNES DYNAMIQUES (Modale Épargne) --- */
.savings-line-item {
display: flex; /* Flexbox pour aligner horizontalement */
align-items: center; /* Centrage vertical */
gap: 10px; /* Espace entre les éléments */
margin-bottom: 8px;
width: 100%;
}
/* La catégorie prend tout l'espace disponible */
.savings-line-item .input-category-wrapper {
flex: 1;
position: relative;
}
/* Le montant a une largeur fixe pour être aligné proprement */
.savings-line-item .input-amount {
width: 110px !important; /* Force la largeur */
text-align: right;
}
/* Le bouton de suppression */
.savings-line-item .btn-remove {
background: transparent;
border: 1px solid transparent;
color: #ef4444; /* Rouge */
cursor: pointer;
font-size: 1.2rem;
line-height: 1;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.savings-line-item .btn-remove:hover {
background: #fef2f2;
border-color: #fca5a5;
}
/* --- THÈMES NENS (POL & PEP) --- */
/* Style du Titre pour Pol (Bleu) */
.nens-title.theme-pol {
color: #1e3a8a !important; /* Bleu foncé */
background: #eaf2ff !important; /* Bleu très pâle */
border-left: 4px solid #1e3a8a !important;
border-radius: 0 8px 8px 0; /* Petit effet onglet */
padding: 8px 12px;
}
/* Style du Tableau pour Pol */
.nens-table.theme-pol thead th {
background: #f5f9ff !important; /* Bleu glace */
color: #1e3a8a !important;
}
.nens-table.theme-pol {
border-top: 2px solid #bcd3ff !important; /* Bordure bleue */
}
/* Style du Titre pour Pep (Vert) */
.nens-title.theme-pep {
color: #14532d !important; /* Vert foncé */
background: #eaf7ea !important; /* Vert très pâle */
border-left: 4px solid #14532d !important;
border-radius: 0 8px 8px 0;
padding: 8px 12px;
}
/* Style du Tableau pour Pep */
.nens-table.theme-pep thead th {
background: #f6fdf6 !important; /* Vert glace */
color: #14532d !important;
}
.nens-table.theme-pep {
border-top: 2px solid #b9e3b9 !important; /* Bordure verte */
}
+213
View File
@@ -0,0 +1,213 @@
/**
* 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());
}
@@ -0,0 +1,66 @@
<?php
require __DIR__ . '/../../../../includes/auth.php';
require __DIR__ . '/../../../../includes/db.php'; // On suppose que $pdo est défini ici
require_login();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
// --- ACTION : SAUVEGARDER (AJOUT OU MODIF) ---
if ($action === 'save') {
$id = !empty($_POST['id']) ? $_POST['id'] : null;
$name = $_POST['name'];
$amount = str_replace(',', '.', $_POST['amount']);
$type = $_POST['type'];
$day = !empty($_POST['payment_day']) ? $_POST['payment_day'] : null;
$month = $_POST['reg_month'] ?: null;
$cat = $_POST['category'];
$is_estimate = isset($_POST['is_estimate']) ? (int)$_POST['is_estimate'] : 0;
if ($id) {
// Update : on ajoute is_estimate
$sql = "UPDATE pf_budget_items SET name=?, amount=?, type=?, payment_day=?, reg_month=?, category=?, is_estimate=? WHERE id=?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$name, $amount, $type, $day, $month, $cat, $is_estimate, $id]);
} else {
// Insert : on ajoute is_estimate
$sql = "INSERT INTO pf_budget_items (name, amount, type, payment_day, reg_month, category, is_estimate) VALUES (?, ?, ?, ?, ?, ?, ?)";
$stmt = $pdo->prepare($sql);
$stmt->execute([$name, $amount, $type, $day, $month, $cat, $is_estimate]);
}
// Redirection après sauvegarde classique
header('Location: /budget.php?tab=recap');
exit;
}
// --- ACTION : COCHER/DÉCOCHER RAPIDE (VIA JS FETCH) ---
if ($action === 'toggle-check') {
$id = $_POST['id'];
$status = $_POST['status']; // 1 ou 0
$sql = "UPDATE pf_budget_items SET is_checked = ? WHERE id = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$status, $id]);
// On renvoie du JSON car c'est un appel JS (pas de redirection)
header('Content-Type: application/json');
echo json_encode(['success' => true]);
exit;
}
// --- ACTION : SUPPRIMER ---
if ($action === 'delete') {
$id = $_POST['id'];
$pdo->prepare("DELETE FROM pf_budget_items WHERE id = ?")->execute([$id]);
// On peut répondre en JSON pour le JS ou rediriger
if (isset($_POST['ajax'])) {
header('Content-Type: application/json');
echo json_encode(['success' => true]);
} else {
header('Location: /budget.php?tab=recap');
}
exit;
}
}
@@ -0,0 +1,108 @@
<?php
require __DIR__ . '/../../../../includes/auth.php';
require __DIR__ . '/../../../../includes/db.php';
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'];
$stmt = $pdo->prepare("DELETE FROM pf_savings WHERE owner = ? AND month_date = ? AND category = ?");
$stmt->execute([$owner, $date, $cat]);
echo json_encode(['success' => true]);
exit;
}
// --- ACTION : DUPLICATION DE MOIS ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'duplicate_month') {
$owner = $_POST['owner'];
$sourceDate = $_POST['source_date'];
$targetDate = $_POST['target_date'];
$newTotal = floatval($_POST['new_total']);
try {
$pdo->beginTransaction();
// 1. Vérifier si le mois cible existe déjà
$check = $pdo->prepare("SELECT id FROM pf_savings WHERE owner = ? AND month_date = ? LIMIT 1");
$check->execute([$owner, $targetDate]);
if ($check->fetch()) {
echo json_encode(['success' => false, 'error' => 'Ce mois existe déjà !']);
exit;
}
// 2. Insérer le nouveau TOTAL BANQUE
$stmtIns = $pdo->prepare("INSERT INTO pf_savings (owner, month_date, category, amount) VALUES (?, ?, ?, ?)");
$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
WHERE owner = ? AND month_date = ? AND category != 'TOTAL_BANQUE'";
$stmtCopy = $pdo->prepare($sqlCopy);
$stmtCopy->execute([$targetDate, $owner, $sourceDate]);
$pdo->commit();
echo json_encode(['success' => true]);
} catch (Exception $e) {
$pdo->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
// --- ACTION : SUPPRESSION GLOBALE D'UN MOIS ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete_month_global') {
$owner = $_POST['owner'];
$date = $_POST['month_date'];
try {
$stmt = $pdo->prepare("DELETE FROM pf_savings WHERE owner = ? AND month_date = ?");
$stmt->execute([$owner, $date]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
// --- 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'];
$dateInput = $_POST['month_date'];
$dateObj = new DateTime($dateInput);
$monthDate = $dateObj->format('Y-m-01');
$values = $_POST['values'] ?? []; // Pour la modale standard
// ... (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();
$stmtDel = $pdo->prepare("DELETE FROM pf_savings WHERE owner = ? AND month_date = ?");
$stmtDel->execute([$owner, $monthDate]);
$stmtIns = $pdo->prepare("INSERT INTO pf_savings (owner, month_date, category, amount) VALUES (?, ?, ?, ?)");
foreach ($values as $category => $amount) {
$amount = floatval($amount);
if ($amount > 0 || $category === 'TOTAL_BANQUE') {
$stmtIns->execute([$owner, $monthDate, $category, $amount]);
}
}
$pdo->commit();
header("Location: /budget.php?tab=epargne&owner=$redirectTab");
exit;
} catch (Exception $e) {
$pdo->rollBack();
die("Erreur : " . $e->getMessage());
}
}
+177
View File
@@ -0,0 +1,177 @@
<?php
// 1. Gestion des propriétaires à afficher
$requestedOwner = $_GET['owner'] ?? 'Nens';
// Si l'onglet est "Nens", on affiche Pol et Pep. Sinon, on affiche juste la personne demandée.
$ownersToDisplay = ($requestedOwner === 'Nens') ? ['Pol', 'Pep'] : [$requestedOwner];
?>
<div class="budget-view">
<div class="view-header">
<div class="owner-tabs">
<a href="?tab=epargne&owner=Alex" class="owner-tab <?= $requestedOwner === 'Alex' ? 'active' : '' ?>">Alex</a>
<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>
<?php foreach ($ownersToDisplay as $currentOwner):
// --- Récupération des données pour $currentOwner ---
$stmt = $pdo->prepare("SELECT month_date, category, amount FROM pf_savings WHERE owner = ? ORDER BY month_date DESC");
$stmt->execute([$currentOwner]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$data = [];
$months = [];
$allCategories = [];
foreach ($rows as $row) {
$m = $row['month_date'];
$cat = $row['category'];
$val = $row['amount'];
$data[$m][$cat] = $val;
if (!in_array($m, $months)) $months[] = $m;
if ($cat !== 'TOTAL_BANQUE' && !in_array($cat, $allCategories)) $allCategories[] = $cat;
}
$months = array_slice($months, 0, 7); // 7 derniers mois
sort($allCategories);
?>
<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;">
<?= $currentOwner ?>
</h3>
<?php endif; ?>
</div>
<div style="display: flex; gap: 10px; align-items: center;">
<?php if (!empty($months)): ?>
<button onclick="duplicateLastMonth('<?= $months[0] ?>', '<?= $currentOwner ?>')" class="pf-btn btn-secondary">
🔁 +1 Mois
</button>
<?php endif; ?>
<button onclick="openSavingsModal('add', '<?= $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 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>
</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>
<?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 ?>')">
&times;
</button>
</div>
<?php else: ?> - <?php endif; ?>
</td>
<?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>
<?php endif; ?>
</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>
<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" 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>
<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>
<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: 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>
</form>
</div>
</div>
+166
View File
@@ -0,0 +1,166 @@
<?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();
$totalDepensesMensuelles = 0;
$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>
<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'];
}
}
$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>
<div class="budget-note">
<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>
<form action="/modules/budget/includes/api/manage-item.php" method="POST">
<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>
<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>
<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>
</div>
<div class="form-row">
<div>
<label class="pf-label">Catégorie</label>
<select name="category" id="item_category" class="pf-input">
<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">
<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">
<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">
<option value="">Aucune</option>
<?php
$mois = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
foreach($mois as $m) echo "<option value='$m'>$m</option>";
?>
</select>
</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>
</form>
</div>
</div>
View File
+23
View File
@@ -466,6 +466,29 @@
background: #fef2f2; background: #fef2f2;
} }
@media (hover: none) {
/* Sur écran tactile, on désactive l'effet de masquage */
.cl-gift-amount {
/* Option A: On garde le prix visible */
opacity: 1 !important;
transform: scale(1) !important;
margin-right: 30px; /* Faire de la place pour le bouton supprimer */
}
.cl-gift-actions {
/* On rend les actions toujours visibles et clicables */
opacity: 1 !important;
transform: scale(1) !important;
pointer-events: auto !important;
background: rgba(
255,
255,
255,
0.8
); /* Fond pour lisibilité si superposition */
}
}
/* --- 8. BUDGET & TRICOUNT --- */ /* --- 8. BUDGET & TRICOUNT --- */
.pf-section--panel { .pf-section--panel {
margin-bottom: 32px; margin-bottom: 32px;