budget 2026

This commit is contained in:
2026-02-17 16:00:03 +01:00
parent 36b54ea953
commit 6e51a9215c
4 changed files with 758 additions and 261 deletions
+22 -7
View File
@@ -8,8 +8,10 @@ $tab = $_GET['tab'] ?? 'recap';
$pageTitle = "PachaFamily - Budget";
$activePage = "budget";
// Note: budget.css est conservé pour le style global,
// mais budget.js n'est plus requis si tu l'as supprimé comme prévu.
$pageCss = "/modules/budget/budget.css";
$pageJs = "/modules/budget/budget.js";
// $pageJs = "/modules/budget/budget.js"; // Ligne commentée ou supprimée
require __DIR__ . '/header.php';
?>
@@ -24,6 +26,11 @@ require __DIR__ . '/header.php';
<span>Récapitulatif</span>
</a>
<a href="?tab=budget_prev" class="tab-item <?= $tab == 'budget_prev' ? 'active' : '' ?>">
<span class="tab-icon">🎯</span>
<span>Budget 2026</span>
</a>
<a href="?tab=suivi" class="tab-item <?= $tab == 'suivi' ? 'active' : '' ?>">
<span class="tab-icon">🗓️</span>
<span>Suivi Mensuel</span>
@@ -34,21 +41,29 @@ require __DIR__ . '/header.php';
<span>Épargne</span>
</a>
</nav>
</div>
</div>
<section class="pf-section">
<?php
// On définit le chemin relatif par rapport à la racine du projet
// ROUTAGE DYNAMIQUE
// Sécurisation basique pour éviter d'inclure n'importe quoi
$allowedTabs = ['recap', 'suivi', 'epargne', 'budget_prev'];
if (in_array($tab, $allowedTabs)) {
$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>";
echo "<div style='text-align:center; padding:50px; color:#ef4444;'>";
echo "<h3>Oups ! Fichier introuvable.</h3>";
echo "<p>Vérifiez que le fichier <code>views/" . htmlspecialchars($tab) . ".php</code> existe bien.</p>";
echo "</div>";
}
} else {
echo "<p>Page non autorisée.</p>";
}
?>
</section>
</section>
</div>
<?php require __DIR__ . '/footer.php'; ?>
+303 -234
View File
@@ -4,9 +4,9 @@
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--success: #10b981; /* Vert */
--danger: #ef4444; /* Rouge */
--warning: #f59e0b; /* Jaune */
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
--bg-page: #f1f5f9;
--bg-card: #ffffff;
@@ -46,7 +46,6 @@
}
}
/* En-tête de la vue */
.view-header {
display: flex;
flex-wrap: wrap;
@@ -55,15 +54,48 @@
margin-bottom: 24px;
gap: 16px;
}
.view-header h2 {
font-size: 1.5rem;
font-weight: 800;
color: #0f172a; /* Très sombre, quasi noir */
color: #0f172a;
margin: 0;
letter-spacing: -0.025em;
}
.nens-title.theme-pol {
color: #1e3a8a !important;
background: #eaf2ff !important;
border-left: 4px solid #1e3a8a !important;
border-radius: 0 8px 8px 0;
padding: 8px 12px;
}
.nens-table.theme-pol thead th {
background: #f5f9ff !important;
color: #1e3a8a !important;
}
.nens-table.theme-pol {
border-top: 2px solid #bcd3ff !important;
}
.nens-title.theme-pep {
color: #14532d !important;
background: #eaf7ea !important;
border-left: 4px solid #14532d !important;
border-radius: 0 8px 8px 0;
padding: 8px 12px;
}
.nens-table.theme-pep thead th {
background: #f6fdf6 !important;
color: #14532d !important;
}
.nens-table.theme-pep {
border-top: 2px solid #b9e3b9 !important;
}
/* --- 3. NAVIGATION TABS (Pills) --- */
.budget-tabs-container {
display: inline-flex;
@@ -75,7 +107,6 @@
position: relative;
z-index: 10;
}
.tab-item {
display: flex;
align-items: center;
@@ -89,32 +120,27 @@
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;
@@ -123,28 +149,35 @@
font-weight: 600;
transition: 0.2s;
}
.owner-tab.active {
background: var(--bg-card);
color: var(--primary);
box-shadow: var(--shadow-sm);
}
/* --- 4. BOUTONS --- */
/* --- 4. BOUTONS UNIFIÉS (Global) --- */
/* Style de Base (Taille Standard : 40px) */
.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;
justify-content: center;
gap: 8px;
height: 40px; /* Hauteur Fixe */
padding: 0 24px;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: white;
border: 1px solid transparent;
border-radius: 50px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
text-decoration: none;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
}
.pf-btn:hover {
@@ -152,22 +185,23 @@
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
}
/* Variante Secondaire (Fond Blanc) */
.pf-btn.btn-secondary {
background: var(--bg-card);
background: white;
color: var(--text-muted);
border: 1px solid #cbd5e1;
box-shadow: none;
margin-bottom: 5px;
}
.pf-btn.btn-secondary:hover {
background: var(--bg-page);
background: #f8fafc;
color: var(--text-main);
border-color: #94a3b8;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.btn-icon,
.btn-icon-small {
/* Boutons Icônes seuls */
.btn-icon {
background: transparent;
border: none;
cursor: pointer;
@@ -175,27 +209,41 @@
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 {
.btn-icon:hover:hover {
background: #e2e8f0;
transform: scale(1.1);
}
/* Actions groupées */
.action-buttons {
display: flex;
gap: 8px;
}
.month-actions {
display: flex;
gap: 4px;
opacity: 0.5;
transition: opacity 0.2s ease;
}
.month-actions .btn-icon-small {
font-size: 0.75rem;
padding: 2px 6px;
height: 24px;
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);
}
/* --- 5. TABLEAU MODERNE --- */
.pf-table {
width: 100%;
@@ -206,7 +254,6 @@
border-spacing: 0;
overflow: hidden;
}
.pf-table th {
background: #f8fafc;
padding: 16px;
@@ -219,14 +266,12 @@
border-bottom: 1px solid #e2e8f0;
white-space: nowrap;
}
.pf-table td {
padding: 3px 16px; /* Ajusté pour être moins haut */
padding: 3px 16px;
border-bottom: 1px solid var(--bg-page);
vertical-align: middle;
font-size: 0.95rem;
}
.pf-table tbody tr:last-child td {
border-bottom: none;
}
@@ -234,7 +279,6 @@
background-color: #f8fafc;
}
/* Styles spécifiques aux lignes */
.row-income td:first-child {
border-left: 4px solid var(--success);
}
@@ -249,16 +293,6 @@
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;
}
@@ -271,12 +305,10 @@
min-width: 150px;
}
/* Cellules spéciales */
.cell-amount {
font-family: "Consolas", "Monaco", monospace;
font-weight: 700;
}
.cell-content {
position: relative;
display: flex;
@@ -286,35 +318,41 @@
height: 100%;
}
/* Bouton supprimer au survol */
.savings-table .row-extres td {
background: #fffbeb;
border-top: 2px solid #e2e8f0;
}
/* Bouton supprimer au survol (Style Epargne) */
.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;
width: 24px;
height: 24px;
font-size: 1.1rem;
line-height: 1;
cursor: pointer;
padding: 0;
align-items: center;
justify-content: center;
position: absolute;
right: -5px;
right: 5px;
top: 50%;
transform: translateY(-50%);
text-decoration: none;
transition: all 0.2s;
}
.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;
@@ -332,7 +370,6 @@ td:hover .btn-cell-delete {
font-size: 0.85rem;
margin-bottom: 6px;
}
.pf-input {
width: 100%;
padding: 10px 12px;
@@ -344,26 +381,22 @@ td:hover .btn-cell-delete {
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;
@@ -378,12 +411,10 @@ input[type="checkbox"] {
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;
@@ -391,6 +422,17 @@ input[type="checkbox"]:checked::after {
font-weight: bold;
}
/* Suppression flèches input number (Cross-browser) */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}
/* --- 7. MODALE --- */
.pf-modal {
display: none;
@@ -399,12 +441,11 @@ input[type="checkbox"]:checked::after {
z-index: 9999;
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px); /* Safari support */
-webkit-backdrop-filter: blur(4px);
align-items: center;
justify-content: center;
padding: 20px;
}
.pf-modal-content {
background: var(--bg-card);
width: 100%;
@@ -418,7 +459,6 @@ input[type="checkbox"]:checked::after {
flex-direction: column;
gap: 10px;
}
@keyframes modalPop {
from {
transform: scale(0.95) translateY(10px);
@@ -429,7 +469,6 @@ input[type="checkbox"]:checked::after {
opacity: 1;
}
}
.pf-modal-title {
margin-top: 0;
font-size: 1.5rem;
@@ -439,7 +478,6 @@ input[type="checkbox"]:checked::after {
padding-bottom: 15px;
margin-bottom: 8px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
@@ -462,7 +500,6 @@ input[type="checkbox"]:checked::after {
.font-bold {
font-weight: 700;
}
.badge-type {
display: inline-block;
padding: 4px 10px;
@@ -479,7 +516,6 @@ input[type="checkbox"]:checked::after {
background: #f3e8ff;
color: #9333ea;
}
.budget-note {
margin-top: 15px;
font-size: 0.85em;
@@ -496,7 +532,6 @@ input[type="checkbox"]:checked::after {
border-radius: 15px;
justify-content: space-between;
}
.tab-item {
padding: 10px 15px;
flex: 1;
@@ -504,196 +539,230 @@ input[type="checkbox"]:checked::after {
white-space: nowrap;
font-size: 0.85rem;
}
.view-header {
flex-direction: column;
align-items: flex-start;
}
.pf-btn {
width: 100%;
justify-content: center;
margin-top: 5px;
}
.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é) --- */
/* --- 10. BUDGET PREVISIONNEL (TIMELINE 2025) --- */
/* Conteneur principal dans le <th> */
.month-header-container {
.prev-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 */
flex-direction: column;
gap: 30px;
}
/* Le texte du mois */
.month-name {
.prev-section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #e2e8f0;
}
.prev-section-header h2 {
margin: 0;
font-size: 1.2rem;
color: var(--text-main);
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;
/* Inputs "Invisible" */
.prev-input {
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;
background: transparent;
text-align: center;
font-family: inherit;
font-size: 0.9rem;
color: var(--text-main);
font-weight: 500;
padding: 4px 0;
border-radius: 4px;
transition: all 0.2s;
}
.prev-input:hover,
.prev-input:focus {
background: #eef2ff;
border-color: #6366f1;
outline: none;
}
.prev-input.bold-blue {
color: #0284c7;
font-weight: 700;
}
/* Tableaux Config & Alloc */
.prev-salary-table {
width: 100%;
max-width: 900px;
background: var(--bg-card);
border-radius: var(--radius-l);
box-shadow: var(--shadow-sm);
border-collapse: separate;
border-spacing: 0;
overflow: hidden;
border: 1px solid #e2e8f0;
}
.prev-salary-table th {
background: #f8fafc;
color: var(--text-muted);
font-weight: 600;
text-transform: uppercase;
font-size: 0.8rem;
padding: 12px;
text-align: center;
border-bottom: 1px solid #e2e8f0;
}
.prev-salary-table th:first-child {
text-align: left;
}
.prev-salary-table td {
padding: 8px 12px;
border-bottom: 1px solid #f1f5f9;
text-align: center;
}
.prev-timeline-wrapper {
overflow-x: auto;
border: 1px solid #e2e8f0;
border-radius: var(--radius-l);
background: var(--bg-card);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
margin-top: 15px;
}
.prev-alloc-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
min-width: 100%;
}
.prev-alloc-table th {
padding: 6px 10px;
border-right: 1px solid #f1f5f9;
white-space: nowrap;
text-align: center;
font-size: 0.9rem;
}
.prev-alloc-table td {
padding: 3px 14px;
border-bottom: 1px solid #f1f5f9;
border-right: 1px solid #f1f5f9;
white-space: nowrap;
text-align: center;
font-size: 0.9rem;
}
/* Colonne Sticky */
.col-sticky {
position: sticky !important;
left: 0 !important;
z-index: 10;
background-color: var(--bg-card);
border-right: 2px solid #e2e8f0;
text-align: left !important;
min-width: 220px;
font-weight: 600;
color: var(--text-main);
}
.col-sticky.header-cell {
top: 0;
z-index: 30 !important;
background-color: #f8fafc;
color: var(--text-muted);
}
.col-sticky small {
display: block;
font-weight: normal;
color: var(--text-muted);
font-style: italic;
font-size: 0.75rem;
}
/* En-têtes Mois */
.th-month {
background: #1e293b !important;
color: white !important;
font-size: 0.85rem !important;
border-left: 2px solid white;
padding: 8px !important;
}
.th-month.current {
background: var(--primary) !important;
}
.th-sub {
font-size: 0.7rem !important;
text-transform: uppercase;
background: #f8fafc;
color: var(--text-muted);
}
/* Lignes */
.row-total td {
background: #f8fafc;
font-weight: 700;
color: #334155;
border-top: 2px solid #e2e8f0;
}
.row-restant td {
background: #eff6ff;
font-weight: 700;
border-bottom: 2px solid #bfdbfe;
}
/* Couleurs */
.txt-alex {
color: #0891b2;
}
.txt-laia {
color: #f59e0b;
}
.txt-global {
color: var(--text-muted);
font-weight: 700;
background: #f8fafc;
}
.val-ok {
color: var(--success);
}
.val-ko {
color: var(--danger);
}
/* Navigation */
.nav-group {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.savings-line-item .btn-remove:hover {
background: #fef2f2;
border-color: #fca5a5;
.btn-nav {
background: white;
border: 1px solid #cbd5e1;
padding: 4px 10px;
border-radius: var(--radius-s);
text-decoration: none;
color: var(--text-muted);
font-size: 0.85rem;
font-weight: 500;
transition: 0.2s;
}
/* --- 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 */
}
/* Styles spécifiques Suivi Mensuel */
/* Animation d'apparition */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Scrollbar fine pour les listes */
.cat-card div::-webkit-scrollbar {
width: 6px;
}
.cat-card div::-webkit-scrollbar-track {
.btn-nav:hover {
background: #f1f5f9;
}
.cat-card div::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
.cat-card div::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Input datalist chrome fix */
input::-webkit-calendar-picker-indicator {
opacity: 100;
color: var(--text-main);
border-color: #94a3b8;
}
@@ -0,0 +1,54 @@
<?php
require __DIR__ . '/../../../../includes/auth.php';
require __DIR__ . '/../../../../includes/db.php';
require_login();
header('Content-Type: application/json');
$action = $_POST['action'] ?? '';
// 1. MISE A JOUR TABLEAU SALAIRES
if ($action === 'update_salary_config') {
$year = $_POST['year'];
$person = $_POST['person'];
$field = $_POST['field']; // salary, mensualite, etc.
$value = floatval($_POST['value']);
// Liste des champs autorisés pour éviter les injections
$allowed = ['salary', 'mensualite', 'frais_func', 'eco_perso', 'eco_family'];
if (!in_array($field, $allowed)) { echo json_encode(['error'=>'Champ invalide']); exit; }
$stmt = $pdo->prepare("INSERT INTO pf_salary_config (year, person, $field) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE $field = VALUES($field)");
$stmt->execute([$year, $person, $value]);
echo json_encode(['success' => true]);
exit;
}
// 2. MISE A JOUR TABLEAU REPARTITION
if ($action === 'update_allocation') {
$date = $_POST['month_date'];
$catId = $_POST['cat_id'];
$person = $_POST['person']; // 'amount_alex' ou 'amount_laia'
$value = floatval($_POST['value']);
$stmt = $pdo->prepare("INSERT INTO pf_alloc_values (month_date, cat_id, $person) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE $person = VALUES($person)");
$stmt->execute([$date, $catId, $value]);
echo json_encode(['success' => true]);
exit;
}
// 3. GESTION DES CATEGORIES (Ajout / Suppression)
if ($action === 'add_category') {
$name = trim($_POST['name']);
$target = trim($_POST['target']);
$stmt = $pdo->prepare("INSERT INTO pf_alloc_categories (name, target) VALUES (?, ?)");
$stmt->execute([$name, $target]);
header("Location: /budget.php?tab=budget_prev"); exit;
}
if ($action === 'delete_category') {
$id = (int)$_POST['id'];
$pdo->prepare("DELETE FROM pf_alloc_categories WHERE id = ?")->execute([$id]);
$pdo->prepare("DELETE FROM pf_alloc_values WHERE cat_id = ?")->execute([$id]); // Nettoyage valeurs
header("Location: /budget.php?tab=budget_prev"); exit;
}
+359
View File
@@ -0,0 +1,359 @@
<?php
// modules/budget/views/budget_prev.php
$currentYear = date('Y');
// 1. Récupération Config Salaires
$salaryConfig = [];
$stmt = $pdo->prepare("SELECT * FROM pf_salary_config WHERE year = ?");
$stmt->execute([$currentYear]);
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$salaryConfig[$row['person']] = $row;
}
// Init si vide
foreach (['Alex', 'Laia'] as $p) {
if (!isset($salaryConfig[$p])) {
$salaryConfig[$p] = ['salary'=>0, 'mensualite'=>0, 'frais_func'=>0, 'eco_perso'=>0, 'eco_family'=>0];
}
}
// 2. Récupération Catégories
$cats = $pdo->query("SELECT * FROM pf_alloc_categories ORDER BY sort_order ASC, id ASC")->fetchAll(PDO::FETCH_ASSOC);
// 3. Gestion du mois FOCUS et Navigation
$focusDate = isset($_GET['focus_date']) ? $_GET['focus_date'] : date('Y-m-01');
$focusTs = strtotime($focusDate);
// On affiche 6 mois (Timeline inversée)
$months = [];
for ($i = 0; $i < 6; $i++) {
$months[] = date('Y-m-01', strtotime("-$i months", $focusTs));
}
// Liens navigation
$prevMonthLink = date('Y-m-01', strtotime("-1 month", $focusTs));
$nextMonthLink = date('Y-m-01', strtotime("+1 month", $focusTs));
// 4. Récupération Valeurs Répartition
$inQuery = implode(',', array_fill(0, count($months), '?'));
$stmt = $pdo->prepare("SELECT * FROM pf_alloc_values WHERE month_date IN ($inQuery)");
$stmt->execute($months);
$allocs = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$allocs[$row['month_date']][$row['cat_id']] = $row;
}
?>
<div class="prev-container">
<div>
<div class="prev-section-header">
<h2>Revenus <?= $currentYear ?></h2>
</div>
<table class="prev-salary-table">
<thead>
<tr>
<th>Personne</th>
<th>Salaire</th>
<th>Mensualité<br><small style="font-weight:normal; text-transform:none;">(Cpt Commun)</small></th>
<th>Frais Func.</th>
<th>Eco Perso</th>
<th style="background:#f0f9ff; color:#0284c7;">Eco Family<br><small style="font-weight:normal; text-transform:none;">(Dispo)</small></th>
<th>Restant Perso</th>
</tr>
</thead>
<tbody>
<?php foreach (['Alex', 'Laia'] as $p):
$d = $salaryConfig[$p];
$restant = $d['salary'] - ($d['mensualite'] + $d['frais_func'] + $d['eco_perso'] + $d['eco_family']);
$borderColor = ($p === 'Alex') ? '#0891b2' : '#f59e0b';
?>
<tr data-person="<?= $p ?>">
<td style="text-align:left; font-weight:bold; color:var(--text-main); border-left:4px solid <?= $borderColor ?>;">
<?= $p ?>
</td>
<td><input type="number" class="prev-input" data-field="salary" value="<?= round($d['salary']) ?>" onchange="updateSalary('<?= $p ?>', this)"></td>
<td><input type="number" class="prev-input" data-field="mensualite" value="<?= round($d['mensualite']) ?>" onchange="updateSalary('<?= $p ?>', this)"></td>
<td><input type="number" class="prev-input" data-field="frais_func" value="<?= round($d['frais_func']) ?>" onchange="updateSalary('<?= $p ?>', this)"></td>
<td><input type="number" class="prev-input" data-field="eco_perso" value="<?= round($d['eco_perso']) ?>" onchange="updateSalary('<?= $p ?>', this)"></td>
<td style="background:#f0f9ff;">
<input type="number" class="prev-input bold-blue" id="eco_family_<?= $p ?>" data-field="eco_family" value="<?= round($d['eco_family']) ?>" onchange="updateSalary('<?= $p ?>', this)">
</td>
<td id="restant_<?= $p ?>" style="font-weight:bold; color:var(--text-muted); text-align:center;">
<?= number_format($restant, 0, ',', ' ') ?> €
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div>
<div class="prev-section-header">
<div style="display:flex; gap:10px; align-items:center;">
<h2>Répartition Budgétaire</h2>
<div class="nav-group">
<a href="?tab=budget_prev&focus_date=<?= $prevMonthLink ?>" class="btn-nav">◀</a>
<a href="?tab=budget_prev&focus_date=<?= date('Y-m-01') ?>" class="btn-nav">Auj.</a>
<a href="?tab=budget_prev&focus_date=<?= $nextMonthLink ?>" class="btn-nav">▶</a>
</div>
</div>
<div style="display:flex; gap:10px;">
<button class="pf-btn btn-secondary" onclick="duplicateMonth()">
🔁 +1 Mois
</button>
<button class="pf-btn" onclick="document.getElementById('addCatModal').style.display='flex'">
Nouvelle Ligne
</button>
</div>
</div>
<div class="prev-timeline-wrapper">
<table class="prev-alloc-table">
<thead>
<tr>
<th class="col-sticky header-cell"></th>
<?php foreach ($months as $month):
$isCurrent = ($month == date('Y-m-01'));
$cls = $isCurrent ? 'current' : '';
?>
<th colspan="3" class="th-month <?= $cls ?>">
<?= date('F Y', strtotime($month)) ?>
</th>
<?php endforeach; ?>
</tr>
<tr>
<th class="col-sticky header-cell">
</th>
<?php foreach ($months as $month): ?>
<th class="th-sub">Global</th>
<th class="th-sub txt-alex">Alex</th>
<th class="th-sub txt-laia">Laia</th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<tr class="row-total">
<td class="col-sticky" style="text-align:right !important;">Total</td>
<?php foreach ($months as $m): ?>
<td class="txt-global" style="border-left:2px solid #e2e8f0;" id="total_global_<?= $m ?>">0</td>
<td class="txt-alex" id="total_alex_<?= $m ?>">0</td>
<td class="txt-laia" id="total_laia_<?= $m ?>">0</td>
<?php endforeach; ?>
</tr>
<tr class="row-restant">
<td class="col-sticky" style="text-align:right !important;">Restant (Eco Family)</td>
<?php foreach ($months as $m): ?>
<td style="border-left:2px solid #cbd5e1; background:#e2e8f0;">-</td>
<td class="val-ok" id="restant_alloc_alex_<?= $m ?>">0</td>
<td class="val-ok" id="restant_alloc_laia_<?= $m ?>">0</td>
<?php endforeach; ?>
</tr>
<?php foreach ($cats as $cat): ?>
<tr>
<td class="col-sticky" style="position:relative;">
<div style="font-weight:600; color:var(--text-main); "><?= htmlspecialchars($cat['name']) ?></div>
<div style="font-size:0.75rem; color:var(--text-muted); font-style:italic; text-align:right;"><?= htmlspecialchars($cat['target']) ?></div>
<a href="?tab=budget_prev&id=<?= $cat['id'] ?>&action=delete_category"
onclick="return confirm('Supprimer cette ligne et tout son historique ?')"
class="btn-cell-delete"
title="Supprimer la ligne">
&times;
</a>
</td>
<?php foreach ($months as $m):
$val = $allocs[$m][$cat['id']] ?? ['amount_alex'=>0, 'amount_laia'=>0];
?>
<td class="txt-global" style="border-left:2px solid #e2e8f0;" id="g_<?= $m ?>_<?= $cat['id'] ?>">0</td>
<td>
<input type="number" step="1" class="prev-input txt-alex inp-alex-<?= $m ?>"
value="<?= $val['amount_alex'] == 0 ? '' : round($val['amount_alex']) ?>"
placeholder="-"
onchange="updateAlloc('<?= $m ?>', <?= $cat['id'] ?>, 'amount_alex', this)">
</td>
<td>
<input type="number" step="1" class="prev-input txt-laia inp-laia-<?= $m ?>"
value="<?= $val['amount_laia'] == 0 ? '' : round($val['amount_laia']) ?>"
placeholder="-"
onchange="updateAlloc('<?= $m ?>', <?= $cat['id'] ?>, 'amount_laia', this)">
</td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<div id="addCatModal" 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:400px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 class="pf-modal-title" style="margin:0; border:none; font-size:1.2rem;">Nouvelle Ligne de Budget</h3>
<button onclick="document.getElementById('addCatModal').style.display='none'" style="border:none; background:none; font-size:1.5rem; cursor:pointer; color:var(--text-muted);">&times;</button>
</div>
<form action="/modules/budget/includes/api/save-budget.php" method="POST">
<input type="hidden" name="action" value="add_category">
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Nom (ex: Vacances)</label>
<input type="text" name="name" class="pf-input" required>
</div>
<div class="form-group" style="margin-bottom:20px;">
<label class="pf-label">Cible (ex: vers L.Pol)</label>
<input type="text" name="target" class="pf-input" list="targets">
<datalist id="targets">
<option value="vers L.Pol">
<option value="vers L.Perso">
<option value="vers commune">
</datalist>
</div>
<div class="modal-footer">
<button type="button" onclick="document.getElementById('addCatModal').style.display='none'" class="pf-btn btn-secondary">Annuler</button>
<button type="submit" class="pf-btn">Ajouter</button>
</div>
</form>
</div>
</div>
<script>
const currentYear = <?= $currentYear ?>;
const months = <?= json_encode($months) ?>;
// --- SALAIRES ---
function updateSalary(person, input) {
const row = input.closest('tr');
const salary = parseFloat(row.querySelector('[data-field="salary"]').value) || 0;
const mens = parseFloat(row.querySelector('[data-field="mensualite"]').value) || 0;
const frais = parseFloat(row.querySelector('[data-field="frais_func"]').value) || 0;
const ecoP = parseFloat(row.querySelector('[data-field="eco_perso"]').value) || 0;
const ecoF = parseFloat(row.querySelector('[data-field="eco_family"]').value) || 0;
const restant = salary - (mens + frais + ecoP + ecoF);
// Affichage sans décimale
document.getElementById('restant_' + person).innerText = Math.round(restant).toLocaleString('fr-FR') + ' €';
saveData('update_salary_config', { year: currentYear, person: person, field: input.dataset.field, value: input.value });
recalcAllAllocations();
}
// --- ALLOCATIONS ---
function updateAlloc(month, catId, personField, input) {
saveData('update_allocation', { month_date: month, cat_id: catId, person: personField, value: input.value || 0 });
recalcAllAllocations();
}
// Fonction utilitaire : Retourne le mois en MAJUSCULES (ex: "FÉVRIER")
function getMonthName(dateStr) {
const date = new Date(dateStr);
return date.toLocaleDateString('fr-FR', { month: 'long' }).toUpperCase();
}
function duplicateMonth() {
// months[0] = Le mois affiché le plus à gauche (Cible)
// months[1] = Le mois juste à droite (Source)
const targetDateStr = months[0];
const sourceDateStr = months[1];
if (!sourceDateStr) {
alert("Impossible : pas d'historique disponible pour copier.");
return;
}
const targetName = getMonthName(targetDateStr); // ex: MARS
const sourceName = getMonthName(sourceDateStr); // ex: FÉVRIER
// On utilise des tirets ou étoiles pour attirer l'attention à défaut de gras
const message = `Voulez-vous copier les valeurs de ${sourceName} vers ${targetName} ?\n\n` +
`Cela écrasera toutes les valeurs présentes sur ${targetName}.`;
if(!confirm(message)) return;
// Le reste du code de copie reste identique...
document.querySelectorAll('.inp-alex-' + sourceDateStr).forEach(sourceInput => {
const catIdMatch = sourceInput.getAttribute('onchange').match(/, (\d+),/);
if(!catIdMatch) return;
const catId = catIdMatch[1];
const row = sourceInput.closest('tr');
// Copie Alex
const valAlex = sourceInput.value;
const targetAlex = row.querySelector('.inp-alex-' + targetDateStr);
if(targetAlex) {
targetAlex.value = valAlex;
updateAlloc(targetDateStr, catId, 'amount_alex', targetAlex);
}
// Copie Laia
const valLaia = row.querySelector('.inp-laia-' + sourceDateStr).value;
const targetLaia = row.querySelector('.inp-laia-' + targetDateStr);
if(targetLaia) {
targetLaia.value = valLaia;
updateAlloc(targetDateStr, catId, 'amount_laia', targetLaia);
}
});
}
function recalcAllAllocations() {
const budgetAlex = parseFloat(document.getElementById('eco_family_Alex').value) || 0;
const budgetLaia = parseFloat(document.getElementById('eco_family_Laia').value) || 0;
months.forEach(m => {
let sumAlex = 0;
let sumLaia = 0;
document.querySelectorAll('.inp-alex-' + m).forEach(inp => {
const val = parseFloat(inp.value) || 0;
sumAlex += val;
const row = inp.closest('tr');
const laiaVal = parseFloat(row.querySelector('.inp-laia-' + m).value) || 0;
const globalSum = val + laiaVal;
const onchangeStr = inp.getAttribute('onchange');
const matches = onchangeStr.match(/, (\d+),/);
if(matches && matches[1]) {
const gCell = document.getElementById('g_' + m + '_' + matches[1]);
if(gCell) gCell.innerText = globalSum > 0 ? Math.round(globalSum) : '-';
}
});
document.querySelectorAll('.inp-laia-' + m).forEach(inp => sumLaia += (parseFloat(inp.value) || 0));
// Totaux arrondis
document.getElementById('total_alex_' + m).innerText = Math.round(sumAlex) + ' €';
document.getElementById('total_laia_' + m).innerText = Math.round(sumLaia) + ' €';
document.getElementById('total_global_' + m).innerText = Math.round(sumAlex + sumLaia) + ' €';
const restAlex = budgetAlex - sumAlex;
const restLaia = budgetLaia - sumLaia;
const elRestAlex = document.getElementById('restant_alloc_alex_' + m);
const elRestLaia = document.getElementById('restant_alloc_laia_' + m);
elRestAlex.innerText = Math.round(restAlex) + ' €';
elRestAlex.className = 'val-' + (restAlex >= 0 ? 'ok' : 'ko');
elRestLaia.innerText = Math.round(restLaia) + ' €';
elRestLaia.className = 'val-' + (restLaia >= 0 ? 'ok' : 'ko');
});
}
function saveData(action, data) {
const formData = new FormData();
formData.append('action', action);
for (const key in data) formData.append(key, data[key]);
fetch('/modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData });
}
document.addEventListener('DOMContentLoaded', recalcAllAllocations);
</script>