typhon
Deploy HouseHub / deploy (push) Successful in 2s

This commit is contained in:
2026-06-19 11:13:37 +02:00
parent 89d8ad114b
commit aad528d15b
18 changed files with 2991 additions and 2845 deletions
+542 -232
View File
@@ -37,6 +37,7 @@
letter-spacing: -0.025em;
}
/* Thèmes des tableaux */
.nens-title.theme-pol {
color: #1e3a8a !important;
background: #eaf2ff !important;
@@ -147,6 +148,7 @@
z-index: 10 !important;
pointer-events: auto !important;
}
.month-actions {
display: flex;
gap: 4px;
@@ -261,13 +263,13 @@ td:hover .btn-cell-delete {
padding: 18px 16px;
border-top: 1px solid #e2e8f0;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
@@ -373,7 +375,7 @@ input[type="number"] {
gap: 10px;
}
/* Configuration Chromatique Parent 1 */
/* Configuration Chromatique Parent 1 & 2 */
.txt-p1,
.txt-alex {
color: color-mix(in srgb, var(--p1-main, #0891b2) 85%, #000000) !important;
@@ -404,7 +406,6 @@ input.txt-p1:focus {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--p1-main, #0891b2) 10%, #ffffff) !important;
}
/* Configuration Chromatique Parent 2 */
.txt-p2 {
color: color-mix(in srgb, var(--p2-main, #f59e0b) 85%, #000000) !important;
font-weight: bold;
@@ -503,8 +504,6 @@ input.txt-p2:focus {
color: var(--text-main) !important;
border-left: 4px solid var(--p1-main, #0891b2) !important;
}
/* Parent 2 (Ligne 2, ex: Laia) */
.prev-salary-table tbody tr:nth-child(2) td:first-child {
text-align: left !important;
font-weight: bold !important;
@@ -762,7 +761,6 @@ tr:hover .row-actions {
gap: 4px;
}
/* --- Style de base du bouton de validation (Générique) --- */
.recap-table .pf-btn.btn-small {
background: #ffffff !important;
font-size: 0.7rem !important;
@@ -775,8 +773,6 @@ tr:hover .row-actions {
transform: none !important;
transition: all 0.2s ease;
}
/* Couleurs dynamiques adaptées au Parent 1 (ex: Féfé) */
.col-p1 .pf-btn.btn-small {
color: var(--p1-main, #0891b2) !important;
border: 1px solid var(--p1-main, #0891b2) !important;
@@ -788,8 +784,6 @@ tr:hover .row-actions {
#ffffff
) !important;
}
/* Couleurs dynamiques adaptées au Parent 2 (ex: Laia) */
.col-p2 .pf-btn.btn-small {
color: var(--p2-main, #f59e0b) !important;
border: 1px solid var(--p2-main, #f59e0b) !important;
@@ -802,7 +796,6 @@ tr:hover .row-actions {
) !important;
}
/* Grand Totaux Structuraux */
.row-grand-total td {
background-color: #1e293b;
color: white;
@@ -1094,7 +1087,544 @@ body.sum-mode-active .sum-target {
transition: background-color 0.2s;
}
/* --- 13. RESPONSIVE MOBILE BUDGET --- */
/* --- 13. MODALE PARAMÈTRES BUDGET (Design System HouseHub) --- */
.budget-settings-backdrop {
display: none;
position: fixed;
inset: 0;
z-index: 200;
background: rgba(15, 23, 42, 0.5);
backdrop-filter: blur(4px);
align-items: center;
justify-content: center;
padding: 1rem;
}
.budget-settings-backdrop.show {
display: flex;
}
.budget-settings-modal {
background: var(--bg-panel);
border: 1px solid var(--border-light);
border-radius: 14px;
width: 100%;
max-width: 800px;
height: 80vh;
max-height: 700px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
animation: modalIn 0.18s ease;
}
.bs-header {
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--border-light);
display: flex;
justify-content: space-between;
align-items: center;
}
.bs-header h3 {
margin: 0;
font-size: 1.1rem;
color: var(--text-main);
}
.bs-close {
background: none;
border: none;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
color: var(--text-muted);
border-radius: 6px;
padding: 0 0.5rem;
}
.bs-close:hover {
background: var(--bg-page);
color: var(--text-main);
}
.bs-layout {
display: flex;
flex: 1;
overflow: hidden;
}
.bs-sidebar {
width: 200px;
border-right: 1px solid var(--border-light);
background: var(--bg-page);
display: flex;
flex-direction: column;
padding: 0.75rem;
gap: 0.4rem;
overflow-y: auto;
}
.bs-tab-btn {
text-align: left;
padding: 0.6rem 1rem;
border-radius: 8px;
border: none;
background: transparent;
color: var(--text-muted);
font-weight: 600;
cursor: pointer;
transition: 0.15s;
font-size: 0.9rem;
}
.bs-tab-btn:hover {
background: var(--border-light);
color: var(--text-main);
}
.bs-tab-btn.active {
background: var(--text-main);
color: var(--bg-panel);
}
.bs-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
background: var(--bg-panel);
}
.bs-pane {
display: none;
}
.bs-pane.active {
display: block;
animation: fadeIn 0.2s ease;
}
/* Utilitaires et Formulaires Paramètres */
.bs-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border: 1px solid var(--border-light);
border-radius: 8px;
margin-bottom: 0.5rem;
background: var(--bg-panel);
}
.bs-list-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.5rem;
}
.bs-section-title {
font-size: 1.1rem;
color: var(--text-main);
margin-top: 0;
margin-bottom: 1rem;
}
.bs-section-subtitle {
font-size: 1rem;
margin-top: 0;
margin-bottom: 1rem;
}
.bs-section-subtitle.bordered {
margin: 0 0 15px 0;
border-bottom: 1px solid var(--border-light);
padding-bottom: 5px;
font-size: 0.95rem;
}
.bs-form-inline {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.bs-form-inline.align-center {
align-items: center;
}
.bs-input-flex {
flex: 1;
min-width: 150px;
padding: 0.4rem 0.75rem;
}
.bs-input-fixed {
width: 130px;
padding: 0.4rem 0.75rem;
}
.bs-input-icon {
width: 65px;
padding: 0.4rem 0.2rem;
font-size: 1.1rem;
cursor: pointer;
text-align: center;
}
.bs-input-color {
width: 38px;
height: 38px;
padding: 0;
border: 1px solid var(--border-light);
border-radius: 6px;
cursor: pointer;
}
.bs-pane-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.2rem;
border-bottom: 1px solid var(--border-light);
padding-bottom: 0.5rem;
}
.bs-currency-selector {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.8rem;
color: var(--text-muted);
font-weight: 500;
}
.bs-currency-select {
width: 75px;
padding: 3px 6px;
font-size: 0.8rem;
height: auto;
cursor: pointer;
background: var(--bg-page);
}
.bs-divider {
margin: 1.5rem 0;
border: 0;
border-bottom: 1px solid var(--border-light);
}
.bs-badge-default {
font-size: 0.7em;
background: var(--success);
color: white;
padding: 2px 6px;
border-radius: 4px;
margin-left: 8px;
}
/* Onboarding Checklist */
.bs-onboarding-card {
background: #fffbeb;
border-left: 4px solid #f59e0b;
padding: 15px;
margin-bottom: 25px;
border-radius: 8px;
}
.bs-onboarding-title {
margin: 0 0 10px 0;
color: #b45309;
display: flex;
align-items: center;
gap: 8px;
font-size: 1.1rem;
}
.bs-onboarding-desc {
margin: 0 0 10px 0;
font-size: 0.85rem;
color: #d97706;
}
.bs-onboarding-list {
margin: 0;
padding-left: 20px;
color: #b45309;
font-size: 0.9rem;
line-height: 1.8;
}
.bs-onboarding-list li {
margin-bottom: 4px;
}
.bs-onboarding-list strong {
font-size: 1.1rem;
margin-right: 5px;
}
.bs-onboarding-link {
color: #b45309;
text-decoration: underline;
font-weight: bold;
cursor: pointer;
}
/* Affichage Tag Cloud Règles */
.bs-rule-search {
width: 100%;
padding: 0.5rem 0.75rem;
border-radius: 8px;
border: 1px solid var(--border-light);
background: var(--bg-page);
margin-bottom: 1rem;
color: var(--text-main);
font-family: inherit;
}
.bs-accordion-summary {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.bs-accordion-count {
font-size: 0.8rem;
background: var(--border-light);
padding: 2px 8px;
border-radius: 10px;
color: var(--text-muted);
font-weight: normal;
margin-left: auto;
margin-right: 10px;
}
.bs-rule-tags {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
padding: 0.5rem 0;
}
.bs-rule-tag {
background: var(--bg-page);
border: 1px solid var(--border-light);
padding: 3px 10px;
border-radius: 14px;
font-size: 0.8rem;
display: flex;
align-items: center;
gap: 6px;
transition: 0.15s;
}
.bs-rule-tag:hover {
border-color: var(--danger);
background: #fef2f2;
}
.bs-rule-tag-del {
color: var(--text-muted);
cursor: pointer;
border: none;
background: none;
padding: 0;
font-size: 1rem;
line-height: 1;
margin-top: -2px;
}
.bs-rule-tag:hover .bs-rule-tag-del {
color: var(--danger);
}
/* CSV & Utils */
.bs-desc {
margin-top: 0;
margin-bottom: 15px;
color: var(--text-muted);
font-size: 0.85rem;
}
.bs-csv-dropzone {
border: 2px dashed var(--border-light);
border-radius: var(--radius);
padding: 30px;
text-align: center;
cursor: pointer;
margin-bottom: 25px;
transition: all 0.2s;
background: var(--bg-page);
}
.bs-csv-dropzone:hover {
border-color: var(--primary);
background: rgba(37, 99, 235, 0.05);
}
.bs-csv-icon {
font-size: 2rem;
margin-bottom: 10px;
}
.bs-csv-title {
margin: 0 0 5px 0;
font-size: 1.1rem;
}
.bs-csv-subtitle {
margin: 0;
color: var(--text-muted);
font-size: 0.85rem;
}
.bs-csv-preview {
display: none;
background: var(--bg-page);
padding: 15px;
border-radius: var(--radius);
border: 1px dashed var(--primary);
margin-bottom: 25px;
}
.bs-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 15px;
}
.bs-text-muted-normal {
color: var(--text-muted);
font-weight: normal;
}
.bs-card-gray {
background: var(--bg-panel);
padding: 10px;
border-radius: var(--radius);
border: 1px solid var(--border-light);
margin-bottom: 15px;
}
.bs-input-mb {
margin-bottom: 10px;
}
.bs-text-right {
text-align: right;
}
.bs-csv-table-wrapper {
overflow-x: auto;
max-width: 100%;
border: 1px solid var(--border-light);
border-radius: 8px;
background: white;
}
.bs-csv-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
text-align: left;
white-space: nowrap;
}
.bs-csv-thead {
background: var(--bg-page);
border-bottom: 2px solid var(--border-light);
}
.bs-csv-th {
padding: 10px 15px;
font-weight: 600;
color: var(--text-main);
border-right: 1px solid var(--border-light);
}
.bs-csv-th-index {
font-size: 0.75rem;
color: var(--primary);
margin-bottom: 2px;
}
.bs-csv-td-empty {
padding: 15px;
text-align: center;
color: var(--text-muted);
font-style: italic;
}
.bs-csv-tr {
border-bottom: 1px solid #e2e8f0;
}
.bs-csv-td {
padding: 10px 15px;
color: var(--text-muted);
border-right: 1px dashed #e2e8f0;
}
/* Ajouts Structure Global Budget */
.budget-hero {
text-align: center;
margin-bottom: 30px;
}
.budget-title-group {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
margin-bottom: 20px;
}
.budget-title-group h1 {
margin: 0;
}
.btn-settings-gear {
position: relative;
}
.budget-error-msg {
text-align: center;
padding: 50px;
color: var(--danger);
}
@keyframes pulse-warning {
0% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);
}
70% {
box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
}
}
/* Le bouton engrenage devient un simple conteneur fixe */
.btn-header-settings {
position: relative;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 5px;
text-decoration: none;
color: inherit;
}
/* C'est uniquement l'icône à l'intérieur qui tourne ! */
.gear-icon-anim {
display: inline-block;
transition: transform 0.4s ease-in-out;
}
.btn-header-settings:hover .gear-icon-anim {
transform: rotate(90deg);
}
.alert-dot {
display: flex;
align-items: center;
justify-content: center;
background: var(--danger, #ef4444);
color: white;
border-radius: 50%;
width: 18px;
height: 18px;
font-size: 0.85rem;
font-weight: bold;
position: absolute;
top: -4px;
right: -4px;
animation: pulse-warning 2s infinite;
border: 2px solid white;
line-height: 1;
}
.alert-dot-inline {
position: relative;
top: auto;
right: auto;
display: inline-flex;
margin-left: 6px;
width: 16px;
height: 16px;
font-size: 0.75rem;
border-width: 1px;
animation: none; /* Pas de clignotement dans le menu pour ne pas agresser l'œil */
}
/* Responsive Modale Paramètres */
@media (max-width: 650px) {
.bs-layout {
flex-direction: column;
}
.bs-sidebar {
width: 100%;
flex-direction: row;
overflow-x: auto;
border-right: none;
border-bottom: 1px solid var(--border-light);
padding: 0.5rem;
white-space: nowrap;
}
.bs-tab-btn {
flex-shrink: 0;
}
.bs-content {
padding: 1rem;
}
}
/* Responsive Mobile Budget */
@media (max-width: 768px) {
.budget-view {
padding: 10px !important;
@@ -1268,223 +1798,3 @@ body.sum-mode-active .sum-target {
padding: 12px 10px;
}
}
/* ==========================================================================
MODALE PARAMÈTRES BUDGET (Design System HouseHub)
========================================================================== */
.budget-settings-backdrop {
display: none;
position: fixed;
inset: 0;
z-index: 200;
background: rgba(15, 23, 42, 0.5);
backdrop-filter: blur(4px);
align-items: center;
justify-content: center;
padding: 1rem;
}
.budget-settings-backdrop.show {
display: flex;
}
.budget-settings-modal {
background: var(--bg-panel);
border: 1px solid var(--border-light);
border-radius: 14px;
width: 100%;
max-width: 800px;
height: 80vh;
max-height: 700px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
animation: modalIn 0.18s ease;
}
.bs-header {
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--border-light);
display: flex;
justify-content: space-between;
align-items: center;
}
.bs-header h3 {
margin: 0;
font-size: 1.1rem;
color: var(--text-main);
}
.bs-close {
background: none;
border: none;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
color: var(--text-muted);
border-radius: 6px;
padding: 0 0.5rem;
}
.bs-close:hover {
background: var(--bg-page);
color: var(--text-main);
}
.bs-layout {
display: flex;
flex: 1;
overflow: hidden;
}
.bs-sidebar {
width: 200px;
border-right: 1px solid var(--border-light);
background: var(--bg-page);
display: flex;
flex-direction: column;
padding: 0.75rem;
gap: 0.4rem;
overflow-y: auto;
}
.bs-tab-btn {
text-align: left;
padding: 0.6rem 1rem;
border-radius: 8px;
border: none;
background: transparent;
color: var(--text-muted);
font-weight: 600;
cursor: pointer;
transition: 0.15s;
font-size: 0.9rem;
}
.bs-tab-btn:hover {
background: var(--border-light);
color: var(--text-main);
}
.bs-tab-btn.active {
background: var(--text-main);
color: var(--bg-panel);
}
.bs-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
background: var(--bg-panel);
}
.bs-pane {
display: none;
}
.bs-pane.active {
display: block;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Mobile Responsive */
@media (max-width: 650px) {
.bs-layout {
flex-direction: column;
}
.bs-sidebar {
width: 100%;
flex-direction: row;
overflow-x: auto;
border-right: none;
border-bottom: 1px solid var(--border-light);
padding: 0.5rem;
white-space: nowrap;
}
.bs-tab-btn {
flex-shrink: 0;
}
.bs-content {
padding: 1rem;
}
}
/* Utilitaires pour les listes de paramètres */
.bs-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border: 1px solid var(--border-light);
border-radius: 8px;
margin-bottom: 0.5rem;
}
.bs-section-title {
font-size: 1.1rem;
color: var(--text-main);
margin-top: 0;
margin-bottom: 1rem;
}
/* --- AFFICHAGE SMART DES RÈGLES (TAG CLOUD) --- */
.bs-rule-search {
width: 100%;
padding: 0.5rem 0.75rem;
border-radius: 8px;
border: 1px solid var(--border-light);
background: var(--bg-page);
margin-bottom: 1rem;
color: var(--text-main);
font-family: inherit;
}
.bs-rule-group {
margin-bottom: 1.2rem;
}
.bs-rule-group-title {
font-size: 0.85rem;
color: var(--text-muted);
margin-bottom: 0.5rem;
display: flex;
align-items: center;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid var(--border-light);
padding-bottom: 4px;
}
.bs-rule-tags {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.bs-rule-tag {
background: var(--bg-page);
border: 1px solid var(--border-light);
padding: 3px 10px;
border-radius: 14px;
font-size: 0.8rem;
display: flex;
align-items: center;
gap: 6px;
transition: 0.15s;
}
.bs-rule-tag:hover {
border-color: var(--danger);
background: #fef2f2;
}
.bs-rule-tag-del {
color: var(--text-muted);
cursor: pointer;
border: none;
background: none;
padding: 0;
font-size: 1rem;
line-height: 1;
margin-top: -2px;
}
.bs-rule-tag:hover .bs-rule-tag-del {
color: var(--danger);
}
+10 -1
View File
@@ -29,7 +29,16 @@ try {
// 4. Salaires (Année en cours)
$currentYear = (int)date('Y');
$salaries = $pdo->query("SELECT * FROM pf_salary_config WHERE year = $currentYear ORDER BY person ASC")->fetchAll(PDO::FETCH_ASSOC);
$salaries = $pdo->query("
SELECT p.name as person,
COALESCE(s.salary, 0) as salary,
COALESCE(s.mensualite, 0) as mensualite,
s.id as id
FROM pf_people p
LEFT JOIN pf_salary_config s ON p.name = s.person AND s.year = $currentYear
WHERE p.role NOT IN ('enfant', 'nounou') AND p.is_active = 1
ORDER BY p.name ASC
")->fetchAll(PDO::FETCH_ASSOC);
// 5. Paramètres Foyer (Devise + Mapping CSV)
$foyerData = $pdo->query("SELECT currency, csv_mapping FROM pf_foyer_settings LIMIT 1")->fetch(PDO::FETCH_ASSOC);
+151 -133
View File
@@ -1,15 +1,18 @@
<?php
// modules/budget/views/budget_prev.php
// 1. Chargement dynamique des parents
$stmtPeople = $pdo->query("SELECT id, name, user_id, role, color FROM pf_people WHERE role = 'parent' ORDER BY id ASC");
// 1. Chargement dynamique des ADULTES de la famille
$stmtPeople = $pdo->query("SELECT id, name, user_id, role, color FROM pf_people WHERE role NOT IN ('enfant', 'nounou') AND is_active = 1 ORDER BY id ASC");
$budgetParents = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
$p1_name = $budgetParents[0]['name'] ?? 'Parent 1';
$p2_name = $budgetParents[1]['name'] ?? 'Parent 2';
// Sécurité : au cas où aucun adulte n'est trouvé, on évite un crash
if (empty($budgetParents)) {
$budgetParents[] = ['id' => 0, 'name' => 'Utilisateur', 'color' => '#0891b2'];
}
$currentYear = date('Y');
// Cartographie dynamique
// Cartographie dynamique (Supporte 1, 2 ou N adultes)
$parentMapping = [];
foreach ($budgetParents as $index => $parent) {
$num = $index + 1;
@@ -21,6 +24,10 @@ foreach ($budgetParents as $index => $parent) {
];
}
// 1.5 Récupération des VRAIS comptes bancaires créés dans les paramètres
$stmtAccounts = $pdo->query("SELECT name FROM pf_bank_accounts ORDER BY is_default DESC, name ASC");
$bankAccounts = $stmtAccounts->fetchAll(PDO::FETCH_COLUMN);
// 2. Récupération Config Salaires
$salaryConfig = [];
$stmt = $pdo->prepare("SELECT * FROM pf_salary_config WHERE year = ?");
@@ -29,7 +36,7 @@ while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$salaryConfig[$row['person']] = $row;
}
// Sécurité profils de salaire vides
// Sécurité profils de salaire vides (Garantit l'affichage à 0 même si non configuré)
foreach ($parentMapping as $map) {
if (!isset($salaryConfig[$map['name']])) {
$salaryConfig[$map['name']] = ['salary'=>0, 'mensualite'=>0, 'frais_func'=>0, 'eco_perso'=>0, 'eco_family'=>0];
@@ -103,21 +110,28 @@ function getTranslatedMonthName($dateString) {
return tr('month_' . $m) . ' ' . $y;
}
// --- 🧠 PREPARATION DATA TRICOUNT / AVANCES ---
// --- 🧠 PREPARATION DATA TRICOUNT / AVANCES (Dynamique) ---
$stmtAdvancesList = $pdo->query("SELECT * FROM pf_advances WHERE is_resolved = 0 ORDER BY advance_date DESC");
$activeAdvances = $stmtAdvancesList->fetchAll(PDO::FETCH_ASSOC);
$advTotal = [$p1_name => 0, $p2_name => 0];
$livretTotal = [$p1_name => 0, $p2_name => 0];
$advTotal = [];
$livretTotal = [];
$labelsCC = [];
$labelsLivret = [];
$labelsCC = [$p1_name => [], $p2_name => []];
$labelsLivret = [$p1_name => [], $p2_name => []];
foreach ($parentMapping as $map) {
$advTotal[$map['name']] = 0;
$livretTotal[$map['name']] = 0;
$labelsCC[$map['name']] = [];
$labelsLivret[$map['name']] = [];
}
foreach ($activeAdvances as $adv) {
$p = $adv['payer'];
$amt = (float)$adv['amount'];
$labelStr = htmlspecialchars($adv['description']) . ' (' . number_format($amt, 0, ',', ' ') . '€)';
// Si un ancien nom traîne en base, on l'initialise
if (!isset($advTotal[$p])) {
$advTotal[$p] = 0; $livretTotal[$p] = 0;
$labelsCC[$p] = []; $labelsLivret[$p] = [];
@@ -132,10 +146,16 @@ foreach ($activeAdvances as $adv) {
}
}
$balanceDiff = abs(($advTotal[$p1_name] ?? 0) - ($advTotal[$p2_name] ?? 0));
// Calcul de la dette croisée (Valable surtout si 2 parents)
$balanceDiff = 0;
$owedTo = '';
if (($advTotal[$p1_name] ?? 0) > ($advTotal[$p2_name] ?? 0)) $owedTo = $p1_name;
elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_name;
if (count($parentMapping) >= 2) {
$p1 = $parentMapping[0]['name'];
$p2 = $parentMapping[1]['name'];
$balanceDiff = abs(($advTotal[$p1] ?? 0) - ($advTotal[$p2] ?? 0));
if (($advTotal[$p1] ?? 0) > ($advTotal[$p2] ?? 0)) $owedTo = $p1;
elseif (($advTotal[$p2] ?? 0) > ($advTotal[$p1] ?? 0)) $owedTo = $p2;
}
?>
@@ -260,10 +280,11 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
$isIndicative = (strpos($cat['name'], 'Eco P') === 0);
$rowClass = $isIndicative ? 'row-indicative' : '';
$inputClass = $isIndicative ? 'ignore-calc' : '';
// Retrocompatibilité pour les anciens noms d'économies hardcodés
$catDisplayName = $cat['name'];
if ($catDisplayName === 'Eco P1') $catDisplayName = 'Eco ' . $p1_name;
if ($catDisplayName === 'Eco P2') $catDisplayName = 'Eco ' . $p2_name;
if (isset($parentMapping[0]) && $catDisplayName === 'Eco P1') $catDisplayName = 'Eco ' . $parentMapping[0]['name'];
if (isset($parentMapping[1]) && $catDisplayName === 'Eco P2') $catDisplayName = 'Eco ' . $parentMapping[1]['name'];
?>
<tr class="<?= $rowClass ?>">
<td class="col-sticky">
@@ -323,6 +344,7 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
</div>
</div>
<?php if (count($parentMapping) > 1): ?>
<div style="margin: 30px 0; background: var(--bg-panel); padding: 24px; border-radius: var(--radius); border: 1px solid var(--border-light); box-shadow: var(--shadow);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px;">
<h3 style="margin: 0; font-size: 1.3rem; font-weight: 800;"><?= tr('bud_adv_title') ?></h3>
@@ -332,57 +354,39 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
</div>
<div style="display: flex; gap: 20px; margin-bottom: 24px; flex-wrap: wrap;">
<div style="flex: 1; min-width: 240px; background: rgba(8, 145, 178, 0.06); border: 1px solid rgba(8, 145, 178, 0.2); padding: 18px; border-radius: 12px;">
<div style="font-size: 0.85rem; color: #0891b2; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;"><?= sprintf(tr('bud_adv_has_advanced'), htmlspecialchars($p1_name)) ?></div>
<?php foreach ($parentMapping as $idx => $map):
$pName = $map['name'];
$bgClass = ($idx === 0) ? 'rgba(8, 145, 178, 0.06)' : 'rgba(217, 119, 6, 0.06)';
$bdClass = ($idx === 0) ? 'rgba(8, 145, 178, 0.2)' : 'rgba(217, 119, 6, 0.2)';
$colorMain = ($idx === 0) ? '#0891b2' : '#d97706';
$colorDark = ($idx === 0) ? '#164e63' : '#78350f';
?>
<div style="flex: 1; min-width: 240px; background: <?= $bgClass ?>; border: 1px solid <?= $bdClass ?>; padding: 18px; border-radius: 12px;">
<div style="font-size: 0.85rem; color: <?= $colorMain ?>; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;"><?= sprintf(tr('bud_adv_has_advanced'), htmlspecialchars($pName)) ?></div>
<div style="margin-top: 10px;">
<div style="display: flex; align-items: baseline; gap: 6px;">
<span style="font-size: 1.5rem; font-weight: 800; color: #164e63; font-family: monospace;"><?= number_format($advTotal[$p1_name] ?? 0, 2, ',', ' ') ?> €</span>
<span style="font-size: 1.5rem; font-weight: 800; color: <?= $colorDark ?>; font-family: monospace;"><?= number_format($advTotal[$pName] ?? 0, 2, ',', ' ') ?> €</span>
</div>
<small style="color: var(--text-muted); font-size: 0.75rem; font-weight: 500;"><?= tr('bud_adv_cc_label') ?></small>
<?php if (!empty($labelsCC[$p1_name])): ?>
<div style="font-size: 0.75rem; color: #0e7490; margin-top: 6px; line-height: 1.4; font-style: italic;">
<?= implode(', ', $labelsCC[$p1_name]) ?>
<?php if (!empty($labelsCC[$pName])): ?>
<div style="font-size: 0.75rem; color: <?= $colorMain ?>; margin-top: 6px; line-height: 1.4; font-style: italic;">
<?= implode(', ', $labelsCC[$pName]) ?>
</div>
<?php endif; ?>
</div>
<?php if(($livretTotal[$p1_name] ?? 0) > 0): ?>
<div style="margin-top: 14px; padding-top: 10px; border-top: 1px dashed rgba(8, 145, 178, 0.3);">
<div style="font-size: 1.2rem; font-weight: 800; color: #4338ca; font-family: monospace;">+ <?= number_format($livretTotal[$p1_name], 2, ',', ' ') ?> €</div>
<small style="color: #4338ca; font-size: 0.75rem; font-weight: 600;"><?= tr('bud_adv_livret_label') ?></small>
<?php if (!empty($labelsLivret[$p1_name])): ?>
<div style="font-size: 0.75rem; color: #3730a3; margin-top: 4px; line-height: 1.4;">
<?= implode(', ', $labelsLivret[$p1_name]) ?>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div style="flex: 1; min-width: 240px; background: rgba(217, 119, 6, 0.06); border: 1px solid rgba(217, 119, 6, 0.2); padding: 18px; border-radius: 12px;">
<div style="font-size: 0.85rem; color: #d97706; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;"><?= sprintf(tr('bud_adv_has_advanced'), htmlspecialchars($p2_name)) ?></div>
<div style="margin-top: 10px;">
<div style="display: flex; align-items: baseline; gap: 6px;">
<span style="font-size: 1.5rem; font-weight: 800; color: #78350f; font-family: monospace;"><?= number_format($advTotal[$p2_name] ?? 0, 2, ',', ' ') ?> €</span>
</div>
<small style="color: var(--text-muted); font-size: 0.75rem; font-weight: 500;"><?= tr('bud_adv_cc_label') ?></small>
<?php if (!empty($labelsCC[$p2_name])): ?>
<div style="font-size: 0.75rem; color: #b45309; margin-top: 6px; line-height: 1.4; font-style: italic;">
<?= implode(', ', $labelsCC[$p2_name]) ?>
</div>
<?php endif; ?>
</div>
<?php if(($livretTotal[$p2_name] ?? 0) > 0): ?>
<div style="margin-top: 14px; padding-top: 10px; border-top: 1px dashed rgba(217, 119, 6, 0.3);">
<div style="font-size: 1.2rem; font-weight: 800; color: #b45309; font-family: monospace;">+ <?= number_format($livretTotal[$p2_name], 2, ',', ' ') ?> €</div>
<small style="color: #b45309; font-size: 0.75rem; font-weight: 600;"><?= tr('bud_adv_livret_label') ?></small>
<?php if (!empty($labelsLivret[$p2_name])): ?>
<div style="font-size: 0.75rem; color: #92400e; margin-top: 4px; line-height: 1.4;">
<?= implode(', ', $labelsLivret[$p2_name]) ?>
<?php if(($livretTotal[$pName] ?? 0) > 0): ?>
<div style="margin-top: 14px; padding-top: 10px; border-top: 1px dashed <?= $bdClass ?>;">
<div style="font-size: 1.2rem; font-weight: 800; color: <?= $colorDark ?>; font-family: monospace;">+ <?= number_format($livretTotal[$pName], 2, ',', ' ') ?> €</div>
<small style="color: <?= $colorDark ?>; font-size: 0.75rem; font-weight: 600;"><?= tr('bud_adv_livret_label') ?></small>
<?php if (!empty($labelsLivret[$pName])): ?>
<div style="font-size: 0.75rem; color: <?= $colorDark ?>; margin-top: 4px; line-height: 1.4;">
<?= implode(', ', $labelsLivret[$pName]) ?>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<div style="flex: 1; min-width: 240px; background: var(--bg-page); border: 1px solid var(--border-light); padding: 18px; border-radius: 12px; display: flex; flex-direction: column; justify-content: center;">
<div style="font-size: 0.75rem; color: var(--text-muted); font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px;">
@@ -411,10 +415,19 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
</tr>
</thead>
<tbody>
<?php foreach ($activeAdvances as $adv): ?>
<?php foreach ($activeAdvances as $adv):
// Attribution de la couleur selon l'utilisateur
$colorP = '#64748b'; // Defaut
foreach($parentMapping as $idx => $m) {
if($m['name'] === $adv['payer']) {
$colorP = ($idx === 0) ? '#0891b2' : '#d97706';
break;
}
}
?>
<tr>
<td style="color: var(--text-muted);"><?= date('d/m/Y', strtotime($adv['advance_date'])) ?></td>
<td style="font-weight: 700; color: <?= $adv['payer'] === $p1_name ? '#0891b2' : '#d97706' ?>;"><?= htmlspecialchars($adv['payer']) ?></td>
<td style="font-weight: 700; color: <?= $colorP ?>;"><?= htmlspecialchars($adv['payer']) ?></td>
<td>
<?= htmlspecialchars($adv['description']) ?>
<?php if ($adv['from_savings']): ?>
@@ -441,11 +454,19 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
</div>
<?php endif; ?>
</div>
<?php endif; // Fin du Tricount conditionnel ?>
<?php
$focusMonth = $months[0];
$targetsOrder = ['vers commune', 'vers L.Pol', 'vers L.Pep', 'vers L.Perso'];
// Génération dynamique des cibles basées UNIQUEMENT sur les comptes bancaires
$targetsOrder = [];
foreach ($bankAccounts as $accName) {
$targetsOrder[] = 'vers ' . $accName;
}
// On ajoute quand même les cibles historiques enregistrées dans les catégories
// (pour ne pas casser l'affichage si on supprime un compte plus tard)
$allTargets = $targetsOrder;
foreach($cats as $c) {
$t = trim($c['transfer_dest'] ?? '');
@@ -506,33 +527,38 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
</div>
</div>
<div id="addCatModal" class="pf-modal">
<div class="pf-modal-content">
<div class="prev-header-left">
<h3 class="pf-modal-title"><?= tr('bud_prev_new_line_title') ?></h3>
<button onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');">&times;</button>
<div id="addCatModal" class="pf-modal" onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');">
<div class="pf-modal-content" onclick="event.stopPropagation()">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h3 class="pf-modal-title" style="margin: 0; font-size: 1.25rem;"><?= tr('bud_prev_new_line_title') ?></h3>
<button type="button" onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-modal-close" style="background:none; border:none; font-size:1.5rem; cursor:pointer; color: var(--text-muted, #64748b);">×</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">
<div class="form-group" style="margin-bottom: 1rem;">
<label class="pf-label"><?= tr('bud_prev_label_name') ?></label>
<input type="text" name="name" class="pf-input" required>
</div>
<div class="form-group">
<label class="pf-label"><?= tr('bud_prev_monthly_target') ?? 'Objectif Mensuel (€)' ?></label>
<input type="number" step="1" name="target" class="pf-input" placeholder="Ex: 150">
<div class="form-group" style="margin-bottom: 1rem;">
<label class="pf-label"><?= tr('bud_prev_monthly_target') ?></label>
<input type="number" step="1" name="target" class="pf-input" placeholder="<?= tr('bud_prev_target_ph') ?>">
</div>
<div class="form-group">
<label class="pf-label"><?= tr('bud_prev_transfer_dest') ?? 'Destination Virement (Optionnel)' ?></label>
<div class="form-group" style="margin-bottom: 1rem;">
<label class="pf-label"><?= tr('bud_prev_transfer_dest') ?></label>
<select name="transfer_dest" class="pf-input">
<option value="" selected>-- <?= tr('bud_prev_none') ?? 'Aucune' ?> --</option>
<option value="vers L.Pol">vers L.Pol</option>
<option value="vers L.Pep">vers L.Pep</option>
<option value="vers L.Perso">vers L.Perso</option>
<option value="vers commune">vers commune</option>
<option value="">-- <?= tr('bud_prev_none') ?> --</option>
<?php foreach ($bankAccounts as $accName): ?>
<option value="vers <?= htmlspecialchars($accName) ?>"><?= tr('bud_prev_transfer_to') ?> <?= htmlspecialchars($accName) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<div class="form-group" style="margin-bottom: 1rem;">
<label class="pf-label">🌴 <?= tr('bud_prev_link_holiday') ?></label>
<select name="holiday_id" class="pf-input">
<option value="">-- <?= tr('bud_prev_no_link') ?> --</option>
@@ -541,7 +567,8 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
<?php endforeach; ?>
</select>
</div>
<div class="modal-footer">
<div class="modal-footer" style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 1.5rem;">
<button type="button" onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn"><?= tr('bud_add_title') ?></button>
</div>
@@ -549,34 +576,39 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
</div>
</div>
<div id="editCatModal" class="pf-modal">
<div class="pf-modal-content">
<div class="prev-header-left">
<h3 class="pf-modal-title"><?= tr('bud_prev_edit_line_title') ?></h3>
<button onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');">&times;</button>
<div id="editCatModal" class="pf-modal" onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');">
<div class="pf-modal-content" onclick="event.stopPropagation()">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h3 class="pf-modal-title" style="margin: 0; font-size: 1.25rem;"><?= tr('bud_prev_edit_line_title') ?></h3>
<button type="button" onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-modal-close" style="background:none; border:none; font-size:1.5rem; cursor:pointer; color: var(--text-muted, #64748b);">×</button>
</div>
<form action="/modules/budget/includes/api/save-budget.php" method="POST">
<input type="hidden" name="action" value="update_category">
<input type="hidden" name="cat_id" id="edit_cat_id">
<div class="form-group">
<div class="form-group" style="margin-bottom: 1rem;">
<label class="pf-label"><?= tr('bud_label_name') ?></label>
<input type="text" name="name" id="edit_cat_name" class="pf-input" required>
</div>
<div class="form-group">
<label class="pf-label"><?= tr('bud_prev_monthly_target') ?? 'Objectif Mensuel (€)' ?></label>
<input type="number" step="1" name="target" id="edit_cat_target" class="pf-input" placeholder="Ex: 150">
<div class="form-group" style="margin-bottom: 1rem;">
<label class="pf-label"><?= tr('bud_prev_monthly_target') ?></label>
<input type="number" step="1" name="target" id="edit_cat_target" class="pf-input" placeholder="<?= tr('bud_prev_target_ph') ?>">
</div>
<div class="form-group">
<label class="pf-label"><?= tr('bud_prev_transfer_dest') ?? 'Destination Virement (Optionnel)' ?></label>
<div class="form-group" style="margin-bottom: 1rem;">
<label class="pf-label"><?= tr('bud_prev_transfer_dest') ?></label>
<select name="transfer_dest" id="edit_cat_transfer_dest" class="pf-input">
<option value="">-- <?= tr('bud_prev_none') ?? 'Aucune' ?> --</option>
<option value="vers L.Pol">vers L.Pol</option>
<option value="vers L.Pep">vers L.Pep</option>
<option value="vers L.Perso">vers L.Perso</option>
<option value="vers commune">vers commune</option>
<option value="">-- <?= tr('bud_prev_none') ?> --</option>
<?php foreach ($bankAccounts as $accName): ?>
<option value="vers <?= htmlspecialchars($accName) ?>"><?= tr('bud_prev_transfer_to') ?> <?= htmlspecialchars($accName) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<div class="form-group" style="margin-bottom: 1rem;">
<label class="pf-label">🌴 <?= tr('bud_prev_link_holiday') ?></label>
<select name="holiday_id" id="edit_cat_holiday" class="pf-input">
<option value="">-- <?= tr('bud_prev_no_link') ?> --</option>
@@ -585,7 +617,8 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
<?php endforeach; ?>
</select>
</div>
<div class="modal-footer">
<div class="modal-footer" style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 1.5rem;">
<button type="button" onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
</div>
@@ -604,8 +637,9 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
<div class="pf-form-group">
<label class="pf-label"><?= tr('bud_adv_who_paid') ?></label>
<select name="payer" class="pf-input" required>
<option value="<?= htmlspecialchars($p1_name) ?>"><?= htmlspecialchars($p1_name) ?></option>
<option value="<?= htmlspecialchars($p2_name) ?>"><?= htmlspecialchars($p2_name) ?></option>
<?php foreach ($parentMapping as $map): ?>
<option value="<?= htmlspecialchars($map['name']) ?>"><?= htmlspecialchars($map['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="pf-form-group">
@@ -644,8 +678,9 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
<div class="pf-form-group">
<label class="pf-label"><?= tr('bud_adv_who_paid') ?></label>
<select name="payer" id="edit_adv_payer" class="pf-input" required>
<option value="<?= htmlspecialchars($p1_name) ?>"><?= htmlspecialchars($p1_name) ?></option>
<option value="<?= htmlspecialchars($p2_name) ?>"><?= htmlspecialchars($p2_name) ?></option>
<?php foreach ($parentMapping as $map): ?>
<option value="<?= htmlspecialchars($map['name']) ?>"><?= htmlspecialchars($map['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="pf-form-group">
@@ -684,20 +719,6 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
<script>
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
// Dictionnaire I18N injecté proprement depuis PHP
window.I18N = {
...(window.I18N || {}),
'bud_prev_label_name': <?= json_encode(tr('bud_prev_label_name')) ?>,
'bud_prev_err_no_history': <?= json_encode(tr('bud_prev_err_no_history')) ?>,
'bud_prev_confirm_copy': <?= json_encode(tr('bud_prev_confirm_copy')) ?>,
'bud_prev_confirm_transfers': <?= json_encode(tr('bud_prev_confirm_transfers')) ?>,
'bud_prev_confirm_del_line': <?= json_encode(tr('bud_prev_confirm_del_line')) ?>,
'bud_adv_confirm_resolve': <?= json_encode(tr('bud_adv_confirm_resolve') ?? 'Confirmer le remboursement ?') ?>,
'bud_adv_confirm_delete': <?= json_encode(tr('bud_adv_confirm_delete') ?? 'Supprimer définitivement ?') ?>,
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>
};
window.CONFIG = window.CONFIG || {};
window.CONFIG.parentMapping = <?= json_encode($parentMapping) ?>;
window.CONFIG.CURRENCY = '<?= defined('CURRENCY') ? CURRENCY : "€" ?>';
@@ -746,7 +767,7 @@ function duplicateMonth() {
const parentMap = window.CONFIG.parentMapping;
if (!sourceDateStr) {
alert(window.I18N['bud_prev_err_no_history']);
alert(tr('bud_prev_err_no_history'));
return;
}
@@ -757,7 +778,7 @@ function duplicateMonth() {
const sourceName = formatMonth(sourceDateStr);
const targetName = formatMonth(targetDateStr);
const message = window.I18N['bud_prev_confirm_copy'].replace('%s', sourceName).replace('%t', targetName);
const message = tr('bud_prev_confirm_copy').replace('%s', sourceName).replace('%t', targetName);
if(!confirm(message)) return;
@@ -883,10 +904,9 @@ function updateSummaryTable() {
function saveData(action, data) {
const formData = new FormData();
formData.append('action', action);
formData.append('ajax', '1'); // <-- Sécurisation pour pachaFetch / backend
formData.append('ajax', '1');
for (const key in data) formData.append(key, data[key]);
// On utilise fetch ici en mode aveugle pour ne pas bloquer l'UI lors de la frappe rapide
fetch('/modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData });
}
@@ -894,7 +914,7 @@ async function validateTransfers(personCss, month) {
const parentMap = window.CONFIG.parentMapping.find(m => m.css === personCss);
if (!parentMap) return;
const msg = window.I18N['bud_prev_confirm_transfers'].replace('%p', parentMap.name).replace('%m', month);
const msg = tr('bud_prev_confirm_transfers').replace('%p', parentMap.name).replace('%m', month);
if (!confirm(msg)) return;
const formData = new FormData();
@@ -908,10 +928,10 @@ async function validateTransfers(personCss, month) {
if(result.success) {
window.location.reload();
} else {
alert(window.I18N['bud_err_tech'] + " : " + result.error);
alert(tr('bud_err_tech') + " : " + result.error);
}
} catch(e) {
alert(window.I18N['bud_err_tech']);
alert(tr('bud_err_tech'));
}
}
@@ -932,15 +952,15 @@ async function saveGenericNote(noteType, refId, content) {
setTimeout(() => indicator.style.opacity = '0', 2000);
}
} else {
alert(window.I18N['bud_err_tech'] + " : " + data.error);
alert(tr('bud_err_tech') + " : " + data.error);
}
} catch(e) {
alert(window.I18N['bud_err_tech']);
alert(tr('bud_err_tech'));
}
}
async function deleteCategory(id) {
if (!confirm(window.I18N['bud_prev_confirm_del_line'])) return;
if (!confirm(tr('bud_prev_confirm_del_line'))) return;
const formData = new FormData();
formData.append('action', 'delete_category');
formData.append('id', id);
@@ -980,29 +1000,28 @@ async function handleAdvanceSubmit(event, form) {
const endpoint = form.getAttribute('action');
const formData = new FormData(form);
formData.append('ajax', '1'); // <-- Correction de l'erreur d'empty string
formData.append('ajax', '1');
try {
// Utilisation robuste de pachaFetch
const result = await pachaFetch(endpoint, { method: 'POST', body: formData });
if (result.success) {
window.location.reload();
} else {
alert((window.I18N['bud_err_tech'] || "Erreur") + " : " + (result.error || "Opération échouée"));
alert((tr('bud_err_tech') || "Erreur") + " : " + (result.error || "Opération échouée"));
btnSubmit.disabled = false;
btnSubmit.innerHTML = oldText;
}
} catch (err) {
console.error("AJAX Error:", err);
alert(window.I18N['bud_err_tech'] || "Une erreur technique est survenue.");
alert(tr('bud_err_tech') || "Une erreur technique est survenue.");
btnSubmit.disabled = false;
btnSubmit.innerHTML = oldText;
}
}
async function executeResolveAdvance(id) {
if (!confirm(window.I18N['bud_adv_confirm_resolve'])) return;
if (!confirm(tr('bud_adv_confirm_resolve'))) return;
const fd = new FormData();
fd.append('action', 'resolve_advance');
fd.append('id', id);
@@ -1017,7 +1036,7 @@ async function executeResolveAdvance(id) {
}
async function executeDeleteAdvance(id) {
if (!confirm(window.I18N['bud_adv_confirm_delete'])) return;
if (!confirm(tr('bud_adv_confirm_delete'))) return;
const fd = new FormData();
fd.append('action', 'delete_advance');
fd.append('id', id);
@@ -1087,7 +1106,6 @@ document.addEventListener('click', function(e) {
document.addEventListener('DOMContentLoaded', recalcAllAllocations);
// Soumission standard des modales de catégories avec pachaFetch
document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form => {
form.addEventListener('submit', async (e) => {
e.preventDefault();
@@ -1097,7 +1115,7 @@ document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form
submitBtn.disabled = true;
submitBtn.innerText = '⏳ ...';
const formData = new FormData(form);
formData.append('ajax', '1'); // Toujours sécuriser
formData.append('ajax', '1');
const actionUrl = form.getAttribute('action');
const result = await pachaFetch(actionUrl, { method: 'POST', body: formData });
@@ -1107,10 +1125,10 @@ document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form
document.body.classList.remove('no-scroll');
window.location.reload();
} else {
alert((window.I18N['bud_err_tech'] || 'Erreur') + " : " + (result.error || "Inconnue"));
alert((tr('bud_err_tech') || 'Erreur') + " : " + (result.error || "Inconnue"));
}
} catch (error) {
alert(window.I18N['bud_err_tech'] || "Une erreur technique est survenue.");
alert(tr('bud_err_tech') || "Une erreur technique est survenue.");
} finally {
submitBtn.disabled = false;
submitBtn.innerText = originalText;
+484
View File
@@ -0,0 +1,484 @@
<?php
// Sécurité : Ce fichier ne doit pas être appelé directement
if (!defined('CURRENCY')) {
define('CURRENCY', '€');
}
?>
<div class="budget-settings-backdrop" id="modal-budget-settings">
<div class="budget-settings-modal">
<div class="bs-header">
<h3>⚙️ <?= tr('budget_settings_title') ?></h3>
<button class="bs-close" onclick="closeBudgetSettings()">×</button>
</div>
<div class="bs-layout">
<div class="bs-sidebar">
<button class="bs-tab-btn active" onclick="switchBsTab('accounts', this)">
<?= tr('bs_tab_accounts') ?>
<?php if (!$hasAccounts): ?><span class="alert-dot alert-dot-inline">!</span><?php endif; ?>
</button>
<button class="bs-tab-btn" onclick="switchBsTab('categories', this)">
<?= tr('bs_tab_categories') ?>
<?php if (!$hasCategories): ?><span class="alert-dot alert-dot-inline">!</span><?php endif; ?>
</button>
<button class="bs-tab-btn" onclick="switchBsTab('rules', this)"><?= tr('bs_tab_rules') ?></button>
<button class="bs-tab-btn" onclick="switchBsTab('salaries', this)">
<?= tr('bs_tab_salaries') ?>
<?php if (!$hasSalaries): ?><span class="alert-dot alert-dot-inline">!</span><?php endif; ?>
</button>
<button class="bs-tab-btn" onclick="switchBsTab('csv', this)"><?= tr('bs_tab_csv') ?></button>
</div>
<div class="bs-content">
<?php if (!isset($isBudgetSetupOk) || !$isBudgetSetupOk || !$hasBudgetItems): ?>
<div class="bs-onboarding-card">
<h4 class="bs-onboarding-title">
<span>🚀</span> <?= tr('bs_onb_title') ?>
</h4>
<p class="bs-onboarding-desc"><?= tr('bs_onb_desc') ?></p>
<ul class="bs-onboarding-list">
<li>
<strong class="<?= $hasCategories ? 'text-success' : 'text-danger' ?>"><?= $hasCategories ? '✅' : '❌' ?></strong>
<a class="bs-onboarding-link" onclick="switchBsTab('categories', document.querySelectorAll('.bs-tab-btn')[1]); return false;"><?= tr('bs_onb_cat_link') ?></a> : <?= tr('bs_onb_cat_desc') ?>
</li>
<li>
<strong class="<?= $hasAccounts ? 'text-success' : 'text-danger' ?>"><?= $hasAccounts ? '✅' : '❌' ?></strong>
<a class="bs-onboarding-link" onclick="switchBsTab('accounts', document.querySelectorAll('.bs-tab-btn')[0]); return false;"><?= tr('bs_onb_acc_link') ?></a> : <?= tr('bs_onb_acc_desc') ?>
</li>
<li>
<strong class="<?= $hasSalaries ? 'text-success' : 'text-danger' ?>"><?= $hasSalaries ? '✅' : '❌' ?></strong>
<a class="bs-onboarding-link" onclick="switchBsTab('salaries', document.querySelectorAll('.bs-tab-btn')[3]); return false;"><?= tr('bs_onb_sal_link') ?></a> : <?= tr('bs_onb_sal_desc') ?>
</li>
<li style="margin-top: 8px; padding-top: 8px; border-top: 1px dashed rgba(180, 83, 9, 0.3);">
<strong class="<?= $hasBudgetItems ? 'text-success' : 'text-warning' ?>" style="color: <?= $hasBudgetItems ? '' : '#f59e0b' ?>"><?= $hasBudgetItems ? '✅' : '⏳' ?></strong>
<a href="?tab=recap" onclick="closeBudgetSettings();" class="bs-onboarding-link"><?= tr('bs_onb_items_link') ?></a> : <?= tr('bs_onb_items_desc') ?>
</li>
</ul>
</div>
<?php endif; ?>
<div id="pane-accounts" class="bs-pane active">
<div class="bs-pane-header">
<h4 class="bs-section-title"><?= tr('bs_tab_accounts') ?></h4>
<div class="bs-currency-selector">
<span><?= tr('bs_currency_label') ?> :</span>
<select id="bs-currency-select" class="pf-input bs-currency-select" onchange="updateBudgetCurrency(this)">
<option value="€">EUR (€)</option>
<option value="$">USD ($)</option>
<option value="£">GBP (£)</option>
<option value="CHF">CHF</option>
</select>
</div>
</div>
<div id="bs-list-accounts" class="bs-list-container">⏳ <?= tr('loading') ?></div>
<hr class="bs-divider">
<h5 class="bs-section-subtitle"><?= tr('bs_add_account_title') ?></h5>
<form id="form-add-account" class="bs-form-inline">
<input type="text" name="name" class="pf-input bs-input-flex" placeholder="<?= tr('bs_account_name_ph') ?>" required>
<select name="type" class="pf-input bs-input-fixed">
<option value="checking"><?= tr('bs_type_checking') ?></option>
<option value="savings"><?= tr('bs_type_savings') ?></option>
</select>
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
</form>
</div>
<div id="pane-categories" class="bs-pane">
<h4 class="bs-section-title"><?= tr('bs_tab_categories') ?></h4>
<div id="bs-list-categories" class="bs-list-container">⏳ <?= tr('loading') ?></div>
<hr class="bs-divider">
<h5 class="bs-section-subtitle"><?= tr('bs_add_category_title') ?></h5>
<form id="form-add-category" class="bs-form-inline align-center">
<input type="text" name="code" class="pf-input bs-input-fixed" placeholder="<?= tr('bs_cat_code_ph') ?>" required>
<input type="text" name="label" class="pf-input bs-input-flex" placeholder="<?= tr('bs_cat_label_ph') ?>" required>
<select name="type" class="pf-input bs-input-fixed">
<option value="Expense"><?= tr('bs_type_expense') ?></option>
<option value="Income"><?= tr('bs_type_income') ?></option>
<option value="Savings"><?= tr('bs_type_savings_cat') ?></option>
</select>
<select name="icon" class="pf-input bs-input-icon">
<option value="📌">📌</option><option value="🛒">🛒</option><option value="🥖">🥖</option>
<option value="🍽️">🍽️</option><option value="⛽">⛽</option><option value="🚗">🚗</option>
<option value="🚆">🚆</option><option value="🏠">🏠</option><option value="⚡">⚡</option>
<option value="💧">💧</option><option value="⚕️">⚕️</option><option value="🎒">🎒</option>
<option value="👕">👕</option><option value="📱">📱</option><option value="🎮">🎮</option>
<option value="✈️">✈️</option><option value="🎁">🎁</option><option value="🐶">🐶</option>
<option value="💵">💵</option><option value="🐷">🐷</option><option value="📈">📈</option>
</select>
<input type="color" name="color" value="#3b82f6" class="bs-input-color">
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
</form>
</div>
<div id="pane-rules" class="bs-pane">
<h4 class="bs-section-title"><?= tr('bs_tab_rules') ?></h4>
<input type="text" id="input-search-rules" class="bs-rule-search" placeholder="<?= tr('bs_rule_search_ph') ?>" onkeyup="filterRules()">
<div id="bs-list-rules" class="bs-list-container">⏳ <?= tr('loading') ?></div>
<hr class="bs-divider">
<h5 class="bs-section-subtitle"><?= tr('bs_add_rule_title') ?></h5>
<form id="form-add-rule" class="bs-form-inline align-center">
<input type="text" name="keyword" class="pf-input bs-input-flex" placeholder="<?= tr('bs_rule_keyword_ph') ?>" required>
<select name="category" id="select-rule-category" class="pf-input bs-input-fixed" required></select>
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
</form>
</div>
<div id="pane-salaries" class="bs-pane">
<h4 class="bs-section-title"><?= tr('bs_tab_salaries') ?> (<?= date('Y') ?>)</h4>
<div id="bs-list-salaries" class="bs-list-container">⏳ <?= tr('loading') ?></div>
</div>
<div id="pane-csv" class="bs-pane">
<h4 class="bs-section-title"><?= tr('bs_csv_title') ?></h4>
<p class="pf-muted-tiny bs-desc"><?= tr('bs_csv_desc') ?></p>
<div id="csv-drop-zone" class="bs-csv-dropzone">
<div class="bs-csv-icon">📥</div>
<h5 class="bs-csv-title"><?= tr('bs_csv_drop_title') ?></h5>
<p class="pf-muted-tiny bs-csv-subtitle"><?= tr('bs_csv_drop_desc') ?></p>
<input type="file" id="csv_file_input" accept=".csv" style="display: none;">
</div>
<div id="csv-preview-container" class="bs-csv-preview"></div>
<h5 class="bs-section-subtitle bordered"><?= tr('bs_csv_settings_title') ?></h5>
<form id="form-csv-mapping" onsubmit="saveCsvMapping(event)">
<div class="bs-grid-2">
<div>
<label class="pf-label"><?= tr('bs_csv_delimiter') ?></label>
<select name="csv_delimiter" id="csv_delimiter" class="pf-input">
<option value=";"><?= tr('bs_csv_delim_semi') ?></option>
<option value=","><?= tr('bs_csv_delim_comma') ?></option>
<option value="\t"><?= tr('bs_csv_delim_tab') ?></option>
</select>
</div>
<div>
<label class="pf-label"><?= tr('bs_csv_date_format') ?></label>
<select name="csv_date_format" id="csv_date_format" class="pf-input">
<option value="d/m/Y">JJ/MM/AAAA</option>
<option value="Y-m-d">AAAA-MM-JJ</option>
</select>
</div>
</div>
<h5 class="bs-section-subtitle">
<?= tr('bs_csv_col_index') ?>
<small class="bs-text-muted-normal"><?= tr('bs_csv_col_index_help') ?></small>
</h5>
<div class="bs-grid-2">
<div>
<label class="pf-label"><?= tr('bs_csv_col_date') ?></label>
<input type="number" min="0" name="csv_col_date" id="csv_col_date" class="pf-input" value="0" required>
</div>
<div>
<label class="pf-label"><?= tr('bs_csv_col_label') ?></label>
<input type="number" min="0" name="csv_col_label" id="csv_col_label" class="pf-input" value="1" required>
</div>
</div>
<div class="bs-card-gray">
<label class="pf-label"><?= tr('bs_csv_amount_mgmt') ?></label>
<select name="csv_amount_type" id="csv_amount_type" class="pf-input bs-input-mb" onchange="toggleCsvAmountCols(this.value)">
<option value="single"><?= tr('bs_csv_amount_single') ?></option>
<option value="split"><?= tr('bs_csv_amount_split') ?></option>
</select>
<div class="bs-grid-2">
<div>
<label class="pf-label" id="lbl_col_debit"><?= tr('bs_csv_col_amount') ?></label>
<input type="number" min="0" name="csv_col_debit" id="csv_col_debit" class="pf-input" value="8" required>
</div>
<div id="wrapper_col_credit" style="display:none;">
<label class="pf-label"><?= tr('bs_csv_col_credit') ?></label>
<input type="number" min="0" name="csv_col_credit" id="csv_col_credit" class="pf-input" value="9">
</div>
</div>
</div>
<div class="bs-input-mb">
<label class="pf-label"><?= tr('bs_csv_col_ref') ?></label>
<input type="number" min="0" name="csv_col_ref" id="csv_col_ref" class="pf-input" value="3">
</div>
<div class="bs-text-right">
<button type="submit" class="btn btn-secondary">💾 <?= tr('btn_save_format') ?></button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const tabsContainer = document.querySelector('.budget-tabs-container');
const activeTab = document.querySelector('.tab-item.active');
if (tabsContainer && activeTab) {
if (tabsContainer.scrollWidth > tabsContainer.clientWidth) {
const scrollPos = activeTab.offsetLeft - (tabsContainer.offsetWidth / 2) + (activeTab.offsetWidth / 2);
tabsContainer.scrollTo({ left: scrollPos, behavior: 'smooth' });
}
}
});
function openBudgetSettings() {
document.getElementById('modal-budget-settings').classList.add('show');
document.body.classList.add('no-scroll');
loadBudgetSettingsData();
}
function closeBudgetSettings() {
document.getElementById('modal-budget-settings').classList.remove('show');
document.body.classList.remove('no-scroll');
}
document.getElementById('modal-budget-settings')?.addEventListener('click', (e) => {
if (e.target.id === 'modal-budget-settings') closeBudgetSettings();
});
function switchBsTab(tabId, btnEl) {
document.querySelectorAll('.bs-tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.bs-pane').forEach(p => p.classList.remove('active'));
btnEl.classList.add('active');
document.getElementById('pane-' + tabId).classList.add('active');
}
async function loadBudgetSettingsData() {
try {
const response = await pachaFetch('/modules/budget/includes/api/settings.php?action=get_all', { method: 'GET' });
if (!response.success) throw new Error(response.error || tr('error_occured'));
renderAccounts(response.data.accounts);
renderCategories(response.data.categories);
populateRuleCategories(response.data.categories);
renderRules(response.data.rules);
renderSalaries(response.data.salaries, response.data.year);
const currencySelect = document.getElementById('bs-currency-select');
if (currencySelect && response.data.currency) currencySelect.value = response.data.currency;
} catch (err) {
console.error("Erreur chargement paramètres :", err);
const errorMsg = `<div class="pf-alert pf-alert--error bs-input-mb">❌ ${tr('error_loading_settings')} : ${err.message}</div>`;
['accounts', 'categories', 'rules', 'salaries'].forEach(id => {
const el = document.getElementById('bs-list-' + id);
if (el) el.innerHTML = errorMsg;
});
}
}
// 1. ONGLET : COMPTES
function renderAccounts(accounts) {
const container = document.getElementById('bs-list-accounts');
if (!container) return;
if (!accounts || accounts.length === 0) {
container.innerHTML = `<em class="text-muted">${tr('bs_empty_accounts')}</em>`;
return;
}
let html = '';
accounts.forEach(acc => {
const typeBadge = acc.account_type === 'checking' ? tr('bs_type_checking') : tr('bs_type_savings');
const defaultBadge = acc.is_default == 1 ? `<span class="bs-badge-default">${tr('bs_badge_default')}</span>` : '';
const ownerText = acc.owner_name ? `${tr('bs_owner')} : ${acc.owner_name}` : tr('bs_shared_account');
html += `
<div class="bs-list-item">
<div>
<strong>${acc.name}</strong> ${defaultBadge}<br>
<small class="text-muted">${typeBadge} · ${ownerText}</small>
</div>
<div class="pf-flex-gap-8">
<button type="button" class="btn-icon-action delete" onclick="deleteAccount(${acc.id})" title="${tr('btn_delete')}">🗑️</button>
</div>
</div>`;
});
container.innerHTML = html;
}
// 2. ONGLET : CATÉGORIES
function renderCategories(categories) {
const container = document.getElementById('bs-list-categories');
if (!container) return;
if (!categories || categories.length === 0) {
container.innerHTML = `<em class="text-muted">${tr('bs_empty_categories')}</em>`;
return;
}
let html = '';
categories.forEach(cat => {
let typeLabel = cat.type === 'Income' ? tr('bs_type_income') : cat.type === 'Expense' ? tr('bs_type_expense') : tr('bs_type_savings_cat');
html += `
<div class="bs-list-item" style="border-left: 4px solid ${cat.color || '#ccc'};">
<div>
<span style="font-size:1.2rem; margin-right:8px;">${cat.icon || '📌'}</span>
<strong>${cat.label}</strong> <small class="text-muted">(${cat.code} — ${typeLabel})</small>
</div>
<div class="pf-flex-gap-8">
<button type="button" class="btn-icon-action delete" onclick="deleteCategory(${cat.id})" title="${tr('btn_delete')}">🗑️</button>
</div>
</div>`;
});
container.innerHTML = html;
}
// 3. ONGLET : RÈGLES D'IMPORT
function renderRules(rules) {
const container = document.getElementById('bs-list-rules');
if (!container) return;
if (!rules || rules.length === 0) {
container.innerHTML = `<em class="text-muted">${tr('bs_empty_rules')}</em>`;
return;
}
const groupedRules = {};
rules.forEach(r => {
const catName = r.cat_label || r.category;
if (!groupedRules[catName]) groupedRules[catName] = [];
groupedRules[catName].push(r);
});
let html = '';
for (const [catName, catRules] of Object.entries(groupedRules)) {
html += `
<details class="pf-accordion js-rule-group" data-catname="${catName.toLowerCase()}">
<summary class="pf-accordion-summary bs-accordion-summary">
<span>📁 ${catName}</span>
<span class="bs-accordion-count">${catRules.length}</span>
</summary>
<div class="pf-accordion-content" style="padding: 0.5rem;">
<div class="bs-rule-tags">`;
catRules.forEach(r => {
html += `<div class="bs-rule-tag js-rule-tag" data-keyword="${r.keyword.toLowerCase()}">
${r.keyword}
<button type="button" class="bs-rule-tag-del" onclick="deleteRule(${r.id})" title="${tr('btn_delete')}">×</button>
</div>`;
});
html += ` </div>
</div>
</details>`;
}
container.innerHTML = html;
}
function filterRules() {
const searchVal = document.getElementById('input-search-rules').value.toLowerCase();
document.querySelectorAll('.js-rule-group').forEach(group => {
let hasVisibleTag = false;
const catName = group.getAttribute('data-catname');
group.querySelectorAll('.js-rule-tag').forEach(tag => {
const keyword = tag.getAttribute('data-keyword');
if (keyword.includes(searchVal)) { tag.style.display = 'inline-flex'; hasVisibleTag = true; }
else { tag.style.display = 'none'; }
});
if (catName.includes(searchVal) && searchVal.length > 0) {
group.querySelectorAll('.js-rule-tag').forEach(tag => tag.style.display = 'inline-flex');
hasVisibleTag = true;
}
if (hasVisibleTag) {
group.style.display = 'block';
if (searchVal.length > 0) group.setAttribute('open', '');
} else {
group.style.display = 'none'; group.removeAttribute('open');
}
});
}
// 5. ONGLET : SALAIRES
function renderSalaries(salaries, year) {
const container = document.getElementById('bs-list-salaries');
if (!container) return;
if (!salaries || salaries.length === 0) {
container.innerHTML = `<em class="text-muted">${tr('bs_empty_salaries')} ${year}.</em>`;
return;
}
const currency = '<?= CURRENCY ?>';
let html = `<p class="bs-desc">${tr('bs_salaries_year_desc')} <strong>${year}</strong>.</p>`;
salaries.forEach(s => {
html += `
<div class="bs-list-item" id="salary-item-${s.id}">
<div>
<strong>${s.person}</strong><br>
<small class="text-muted">
${tr('bs_salary_net')} : ${s.salary} ${currency}<br>
${tr('bs_salary_fees')} : ${s.mensualite} ${currency}
</small>
</div>
<div class="pf-flex-gap-8">
<button type="button" class="btn-icon-action edit" data-id="${s.id}" data-person="${s.person}" data-salary="${s.salary}" data-mensualite="${s.mensualite}" onclick="inlineEditSalary(this)" title="${tr('btn_edit')}">✏️</button>
</div>
</div>`;
});
container.innerHTML = html;
}
// --- CRUD ACTIONS ---
async function deleteAccount(id) { if (!await pachaConfirm(tr('btn_delete'), tr('bs_confirm_delete'))) return; try { const fd = new FormData(); fd.append('action', 'delete_account'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } }
const formAddAccount = document.getElementById('form-add-account');
if (formAddAccount) formAddAccount.addEventListener('submit', async (e) => { e.preventDefault(); const btn = formAddAccount.querySelector('button'); const oldText = btn.innerText; btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(formAddAccount); fd.append('action', 'add_account'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); formAddAccount.reset(); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } });
async function deleteCategory(id) { if (!await pachaConfirm(tr('btn_delete'), tr('bs_confirm_delete_cat'))) return; try { const fd = new FormData(); fd.append('action', 'delete_category'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } }
const formAddCategory = document.getElementById('form-add-category');
if (formAddCategory) formAddCategory.addEventListener('submit', async (e) => { e.preventDefault(); const btn = formAddCategory.querySelector('button'); const oldText = btn.innerText; btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(formAddCategory); fd.append('action', 'add_category'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); formAddCategory.reset(); formAddCategory.querySelector('input[type="color"]').value = "#3b82f6"; await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } });
function populateRuleCategories(categories) { const select = document.getElementById('select-rule-category'); if (!select) return; select.innerHTML = ''; if (!categories) return; categories.forEach(cat => { const opt = document.createElement('option'); opt.value = cat.code; opt.textContent = `${cat.icon || ''} ${cat.label}`; select.appendChild(opt); }); }
async function deleteRule(id) { if (!await pachaConfirm(tr('btn_delete'), tr('bs_confirm_delete_rule'))) return; try { const fd = new FormData(); fd.append('action', 'delete_rule'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } }
const formAddRule = document.getElementById('form-add-rule');
if (formAddRule) formAddRule.addEventListener('submit', async (e) => { e.preventDefault(); const btn = formAddRule.querySelector('button'); const oldText = btn.innerText; btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(formAddRule); fd.append('action', 'add_rule'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); formAddRule.reset(); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } });
function inlineEditSalary(btnEl) {
const id = btnEl.dataset.id; const person = btnEl.dataset.person; const salary = btnEl.dataset.salary; const mensualite = btnEl.dataset.mensualite; const currency = '<?= CURRENCY ?>';
document.getElementById(`salary-item-${id}`).innerHTML = `
<form onsubmit="submitInlineSalary(event, ${id})" class="bs-form-inline align-center">
<div class="bs-input-flex"><strong>${person}</strong></div>
<div style="display:flex; align-items:center; gap:4px;"><small class="text-muted">${tr('bs_salary_net')}:</small><input type="number" step="0.01" name="salary" class="pf-input bs-input-fixed" value="${salary}" required> ${currency}</div>
<div style="display:flex; align-items:center; gap:4px;"><small class="text-muted">${tr('bs_salary_fees')}:</small><input type="number" step="0.01" name="mensualite" class="pf-input bs-input-fixed" value="${mensualite}" required> ${currency}</div>
<button type="submit" class="btn btn-secondary">💾</button>
<button type="button" class="btn btn-ghost" onclick="loadBudgetSettingsData()">❌</button>
</form>`;
}
async function submitInlineSalary(event, id) { event.preventDefault(); const form = event.target; const btn = form.querySelector('button'); btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(form); fd.append('action', 'save_salary'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); btn.innerText = '💾'; btn.disabled = false; } }
async function updateBudgetCurrency(selectEl) { try { const fd = new FormData(); fd.append('action', 'save_currency'); fd.append('currency', selectEl.value); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); if (typeof showToast === 'function') { showToast(`${tr('bs_currency_updated')} ✅`); } await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } }
// CSV
function toggleCsvAmountCols(type) { if (type === 'split') { document.getElementById('wrapper_col_credit').style.display = 'block'; document.getElementById('lbl_col_debit').innerText = tr('bs_csv_col_debit'); } else { document.getElementById('wrapper_col_credit').style.display = 'none'; document.getElementById('lbl_col_debit').innerText = tr('bs_csv_col_amount'); } }
function renderCsvMapping(mapping) { if (!mapping) return; document.getElementById('csv_delimiter').value = mapping.delimiter || ';'; document.getElementById('csv_date_format').value = mapping.date_format || 'd/m/Y'; document.getElementById('csv_col_date').value = mapping.col_date ?? 0; document.getElementById('csv_col_label').value = mapping.col_label ?? 1; document.getElementById('csv_col_ref').value = mapping.col_ref ?? 3; const amountType = mapping.amount_type || 'split'; document.getElementById('csv_amount_type').value = amountType; document.getElementById('csv_col_debit').value = mapping.col_debit ?? 8; document.getElementById('csv_col_credit').value = mapping.col_credit ?? 9; toggleCsvAmountCols(amountType); }
async function saveCsvMapping(e) { e.preventDefault(); const form = e.target; const btn = form.querySelector('button'); const oldText = btn.innerText; btn.innerText = '⏳...'; btn.disabled = true; try { const fd = new FormData(form); fd.append('action', 'save_csv_mapping'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); if (typeof showToast === 'function') showToast(tr('bs_csv_saved')); else alert(tr('bs_csv_saved')); } catch (err) { alert(tr('error_occured') + " : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } }
const dropZone = document.getElementById('csv-drop-zone'); const fileInput = document.getElementById('csv_file_input');
if (dropZone && fileInput) {
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.style.backgroundColor = 'rgba(0, 123, 255, 0.05)'; dropZone.style.borderColor = 'var(--primary-color, #007bff)'; });
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.style.backgroundColor = ''; dropZone.style.borderColor = 'var(--border-light, #ccc)'; });
dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.style.backgroundColor = ''; dropZone.style.borderColor = 'var(--border-light, #ccc)'; if (e.dataTransfer.files.length) { fileInput.files = e.dataTransfer.files; handleCsvUpload(e.dataTransfer.files[0]); } });
fileInput.addEventListener('change', function() { if (this.files.length) { handleCsvUpload(this.files[0]); } });
}
function handleCsvUpload(file) {
if (!file.name.endsWith('.csv')) { alert(tr('bs_csv_err_type')); return; }
if (typeof showToast === 'function') { showToast(`${tr('bs_csv_file_ok')} : ${file.name}`); }
const reader = new FileReader();
reader.onload = function(e) {
const text = e.target.result; const delimiter = document.getElementById('csv_delimiter').value || ';';
const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0);
if (lines.length > 0) {
const headers = lines[0].split(delimiter).map(h => h.replace(/^"|"$/g, '').trim());
const sampleRows = [];
for (let i = 1; i < Math.min(lines.length, 3); i++) { sampleRows.push(lines[i].split(delimiter).map(d => d.replace(/^"|"$/g, '').trim())); }
renderCsvTablePreview(headers, sampleRows);
}
};
reader.readAsText(file, 'ISO-8859-1');
}
function renderCsvTablePreview(headers, sampleRows) {
const container = document.getElementById('csv-preview-container');
let html = `<h5 class="bs-section-subtitle">👁️ ${tr('bs_csv_preview_title')}</h5>`;
html += `<div class="bs-csv-table-wrapper"><table class="bs-csv-table"><thead class="bs-csv-thead"><tr>`;
headers.forEach((header, index) => {
const cleanHeader = header || `(${tr('bs_csv_empty_col')})`;
html += `<th class="bs-csv-th"><div class="bs-csv-th-index">N° ${index}</div><div>${cleanHeader}</div></th>`;
});
html += `</tr></thead><tbody>`;
if (sampleRows.length === 0) { html += `<tr><td colspan="${headers.length}" class="bs-csv-td-empty">${tr('bs_csv_no_data')}</td></tr>`; }
else { sampleRows.forEach(row => { html += `<tr class="bs-csv-tr">`; for (let i = 0; i < headers.length; i++) { const cellData = row[i] !== undefined ? row[i] : ''; const displayData = cellData.length > 40 ? cellData.substring(0, 40) + '...' : cellData; html += `<td class="bs-csv-td">${displayData}</td>`; } html += `</tr>`; }); }
html += `</tbody></table></div>`;
container.innerHTML = html; container.style.display = 'block';
}
</script>
+316 -190
View File
@@ -13,7 +13,7 @@ let localCareModes = [];
async function openCalendarSettings() {
try {
const res = await pachaFetch(
"/modules/family-calendar/includes/api/settings.php?action=get_all",
"/modules/family-calendar/includes/api/calendar-settings.php?action=get_all",
);
if (!res.success) throw new Error(res.error);
@@ -63,29 +63,34 @@ function closeCalendarSettings() {
document.body.classList.remove("no-scroll");
}
function switchCalendarTab(tabName) {
document
.querySelectorAll(".cal-settings-pane")
.forEach((p) => (p.style.display = "none"));
function switchCalendarTab(tabId) {
// 1. On réinitialise l'affichage
document
.querySelectorAll(".bs-tab-btn")
.forEach((b) => b.classList.remove("active"));
.forEach((btn) => btn.classList.remove("active"));
document
.querySelectorAll(".cal-settings-pane")
.forEach((pane) => (pane.style.display = "none"));
if (tabName === "foyer") {
document.getElementById("cal-pane-foyer").style.display = "block";
document.getElementById("tab-btn-foyer").classList.add("active");
} else if (tabName === "affichage") {
document.getElementById("cal-pane-affichage").style.display = "block";
document.getElementById("tab-btn-affichage").classList.add("active");
} else {
document.getElementById("cal-pane-membres").style.display = "block";
document.getElementById("tab-btn-membres").classList.add("active");
loadMemberConfigView();
// 2. On active l'onglet cliqué
document.getElementById(`tab-btn-${tabId}`).classList.add("active");
document.getElementById(`cal-pane-${tabId}`).style.display = "block";
// 3. Actions spécifiques
if (tabId === "foyer") {
loadLeaveCatalog();
} else if (tabId === "membres") {
// On charge la vue du membre sélectionné par défaut dans la liste
if (document.getElementById("selectCalMember").value) {
loadMemberConfigView();
}
}
}
function renderCareModeTags() {
const container = document.getElementById("careModesContainer");
if (!container) return; // 🛑 LA BARRIÈRE DE SÉCURITÉ EST ICI
container.innerHTML = localCareModes
.map(
(mode, i) => `
@@ -97,8 +102,11 @@ function renderCareModeTags() {
.join("");
}
// Ajoutons la même sécurité pour l'ajout au cas où :
function addCareModeTag() {
const input = document.getElementById("inputNewCareMode");
if (!input) return; // 🛑 SÉCURITÉ
const val = input.value.trim();
if (val && !localCareModes.includes(val)) {
localCareModes.push(val);
@@ -124,7 +132,7 @@ async function submitCalFoyer(e) {
formData.append("care_modes", JSON.stringify(localCareModes));
const res = await pachaFetch(
"/modules/family-calendar/includes/api/settings.php",
"/modules/family-calendar/includes/api/calendar-settings.php",
{ method: "POST", body: formData },
);
if (!res.success) throw new Error(res.error);
@@ -142,139 +150,195 @@ async function submitCalFoyer(e) {
}
}
window.addLeaveRowToMatrix = function () {
const tbody = document.getElementById("tbodyLeaveMatrix");
const empty = document.getElementById("emptyMatrixRow");
if (empty) empty.remove();
if (!tbody) return;
window.submitChildCareModes = async function () {
if (!window.currentSelectedMemberId) return;
const checkboxes = document.querySelectorAll(".js-care-mode-cb:checked");
const selectedModes = Array.from(checkboxes).map((cb) => cb.value);
try {
const formData = new FormData();
formData.append("action", "save_child_care_modes");
formData.append("person_id", window.currentSelectedMemberId);
formData.append("care_modes", JSON.stringify(selectedModes));
const rowTypeOptions = (calGlobalData.leave_types || [])
.map((lt) => `<option value="${lt.code}">${lt.code} - ${lt.label}</option>`)
.join("");
const res = await pachaFetch(
"/modules/family-calendar/includes/api/calendar-settings.php",
{ method: "POST", body: formData },
);
if (!res.success) throw new Error(res.error);
if (window.showToast)
showToast(tr("fc_child_saved") || "Sauvegardé !", "success");
else alert(tr("fc_child_saved") || "Sauvegardé !");
const trRow = document.createElement("tr");
trRow.className = "js-leave-row";
trRow.style.borderBottom = "1px solid var(--border-light)";
trRow.innerHTML = `
<td style="padding: 6px 2px;">
<select class="pf-input js-leave-type" required style="width:100%; padding:6px 2px; font-size:0.8rem; box-sizing: border-box;">
<option value="">${tr("fc_leave_code_placeholder") || "Code..."}</option>
${rowTypeOptions}
</select>
</td>
<td style="padding: 6px 2px;">
<select class="pf-input js-leave-method" style="width:100%; padding:6px 2px; font-size:0.8rem; box-sizing: border-box;">
<option value="FIXED">${tr("fc_leave_method_fixed") || "Fixe"}</option>
<option value="ACCUMULATED">${tr("fc_leave_method_accumulated") || "Cumul"}</option>
</select>
</td>
<td style="padding: 6px 2px;"><input type="number" step="0.5" class="pf-input js-leave-allowance" placeholder="0" required style="width:100%; padding:6px 4px; font-size:0.85rem; text-align:center; box-sizing: border-box;"></td>
<td style="padding: 6px 2px;"><input type="text" class="pf-input js-leave-date" placeholder="${tr("fc_date_format_placeholder") || "JJ/MM"}" pattern="(0[1-9]|[12][0-9]|3[01])/(0[1-9]|1[012])" maxlength="5" required style="width:100%; padding:6px 4px; font-size:0.85rem; text-align:center; box-sizing: border-box;" title="${tr("fc_date_format_title") || "Format JJ/MM"}"></td>
<td style="padding: 6px 2px; text-align:right;"><button type="button" class="btn-icon-action delete" style="padding:4px; margin:0;" onclick="this.closest('tr').remove()" title="${tr("btn_delete") || "Supprimer"}">🗑️</button></td>
`;
tbody.appendChild(trRow);
const currentPerson = calGlobalData.people.find(
(k) => parseInt(k.id) === window.currentSelectedMemberId,
);
if (currentPerson) {
currentPerson.care_modes = JSON.stringify(selectedModes);
}
} catch (err) {
alert("Erreur: " + err.message);
}
};
window.populateCalendarSettings = function (settings) {
if (!settings) return;
const viewSelect = document.getElementById("setCalView");
if (viewSelect && settings.calendar_default_view)
viewSelect.value = settings.calendar_default_view;
const firstDaySelect = document.getElementById("setCalFirstDay");
if (firstDaySelect && settings.calendar_first_day !== undefined)
firstDaySelect.value = settings.calendar_first_day;
const hoursInput = document.getElementById("setCalHours");
if (hoursInput && settings.calendar_working_hours)
hoursInput.value = settings.calendar_working_hours;
};
// ==========================================
// NOUVEAU CATALOGUE GLOBAL (FOYER)
// ==========================================
let globalLeaveCatalog = [];
window.loadAdultConfigView = function (memberId, zoneElement) {
let userLeaves = calGlobalData.leaves[memberId] || [];
async function loadLeaveCatalog() {
try {
const res = await pachaFetch(
"/modules/family-calendar/includes/api/calendar-settings.php?action=get_leave_types",
);
globalLeaveCatalog = res.data || [];
renderLeaveCatalog(globalLeaveCatalog);
} catch (err) {
console.error("Erreur chargement catalogue", err);
}
}
const tableRows = userLeaves
.map((l) => {
let dateDisp = "";
if (l.date) {
const parts = l.date.split("-");
if (parts.length >= 3) dateDisp = `${parts[2]}/${parts[1]}`;
}
function renderLeaveCatalog(types) {
const container = document.getElementById("leaveTypesContainer");
if (!container) return;
container.innerHTML = "";
let rowTypeOptions = (calGlobalData.leave_types || [])
.map(
(lt) =>
`<option value="${lt.code}" ${lt.code === l.type ? "selected" : ""}>${lt.code} - ${lt.label}</option>`,
)
.join("");
if (types.length === 0) {
container.innerHTML = `<p class="pf-muted-note">Aucun congé configuré.</p>`;
return;
}
if (!calGlobalData.leave_types.some((lt) => lt.code === l.type)) {
rowTypeOptions += `<option value="${l.type}" selected>${l.type} (${tr("fc_leave_obsolete") || "Obsolète"})</option>`;
}
return `
<tr class="js-leave-row" style="border-bottom: 1px solid var(--border-light);">
<td style="padding: 6px 2px;">
<select class="pf-input js-leave-type" required style="width:100%; padding:6px 2px; font-size:0.8rem; box-sizing: border-box;">
<option value="">${tr("fc_leave_code_placeholder") || "Code..."}</option>
${rowTypeOptions}
</select>
</td>
<td style="padding: 6px 2px;">
<select class="pf-input js-leave-method" style="width:100%; padding:6px 2px; font-size:0.8rem; box-sizing: border-box;">
<option value="FIXED" ${l.method === "FIXED" ? "selected" : ""}>${tr("fc_leave_method_fixed") || "Fixe"}</option>
<option value="ACCUMULATED" ${l.method === "ACCUMULATED" ? "selected" : ""}>${tr("fc_leave_method_accumulated") || "Cumul"}</option>
</select>
</td>
<td style="padding: 6px 2px;"><input type="number" step="0.5" class="pf-input js-leave-allowance" value="${l.allowance || 0}" placeholder="0" required style="width:100%; padding:6px 4px; font-size:0.85rem; text-align:center; box-sizing: border-box;"></td>
<td style="padding: 6px 2px;"><input type="text" class="pf-input js-leave-date" value="${dateDisp}" placeholder="${tr("fc_date_format_placeholder") || "JJ/MM"}" pattern="(0[1-9]|[12][0-9]|3[01])/(0[1-9]|1[012])" maxlength="5" required style="width:100%; padding:6px 4px; font-size:0.85rem; text-align:center; box-sizing: border-box;" title="${tr("fc_date_format_title") || "Format JJ/MM"}"></td>
<td style="padding: 6px 2px; text-align:right;"><button type="button" class="btn-icon-action delete" style="padding:4px; margin:0;" onclick="this.closest('tr').remove()" title="${tr("btn_delete") || "Supprimer"}">🗑️</button></td>
</tr>
`;
})
.join("");
zoneElement.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:12px;">
<div>
<h5 style="margin:0 0 4px 0; color:var(--text-main); font-size:1rem;">🗓️ ${tr("fc_matrix_title") || "Matrice des Congés"}</h5>
<p style="font-size:0.8rem; color:var(--text-muted); margin:0; line-height:1.3;">${tr("fc_matrix_desc") || "Définissez les quotas et la date de renouvellement."}</p>
types.forEach((lt) => {
container.innerHTML += `
<div style="display: flex; justify-content: space-between; align-items: center; background: var(--bg-page); padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border-light); margin-bottom: 6px;">
<div>
<span style="font-weight: bold; color: var(--text-main);">${lt.label}</span>
<span style="background: var(--bg-subtle); border: 1px solid var(--border-main); border-radius: 4px; padding: 2px 6px; font-size: 0.8rem; margin-left: 8px; color: var(--text-muted);">${lt.code}</span>
</div>
<div style="display: flex; gap: 4px;">
<button type="button" class="pf-btn btn-secondary" style="padding: 4px 8px;" onclick="editLeaveType('${lt.code}', '${lt.label.replace(/'/g, "\\'")}')">✏️</button>
<button type="button" class="pf-btn btn-secondary" style="padding: 4px 8px; color: var(--danger);" onclick="deleteLeaveType('${lt.code}', '${lt.label.replace(/'/g, "\\'")}')">🗑️</button>
</div>
</div>
<button type="button" class="pf-btn pf-btn-secondary" style="padding:6px 10px; font-size:0.8rem; white-space:nowrap;" onclick="addLeaveRowToMatrix()"> ${tr("btn_add") || "Ajouter"}</button>
</div>
<div style="overflow-x:hidden; margin: 0 -5px;">
<table style="width:100%; border-collapse:collapse; margin-bottom:15px; table-layout: fixed;">
<thead>
<tr style="font-size:0.75rem; text-align:left; color:var(--text-muted); text-transform:uppercase;">
<th style="padding:0 2px 8px; width: 33%;">${tr("fc_table_code") || "Code"}</th>
<th style="padding:0 2px 8px; width: 22%;">${tr("fc_table_method") || "Méthode"}</th>
<th style="padding:0 2px 8px; width: 15%;">${tr("fc_table_quota") || "Quota"}</th>
<th style="padding:0 2px 8px; width: 20%;">${tr("fc_table_renewal") || "Renouv."}</th>
<th style="padding:0 2px 8px; width: 10%;"></th>
</tr>
</thead>
<tbody id="tbodyLeaveMatrix">
${tableRows || `<tr><td colspan="5" id="emptyMatrixRow" style="text-align:center; padding:20px; color:var(--text-muted); font-style:italic; font-size:0.9rem;">${tr("fc_matrix_empty") || "Aucun compteur défini."}</td></tr>`}
</tbody>
</table>
</div>
<div style="display:flex; justify-content:flex-end; border-top:1px solid var(--border-light); padding-top:12px;">
<button type="button" class="pf-btn pf-btn-primary" style="padding:8px 16px;" onclick="submitMemberLeaves()">${tr("btn_save") || "Enregistrer"}</button>
</div>
`;
};
`;
});
}
window.loadMemberConfigView = function () {
function editLeaveType(code, label) {
document.getElementById("leaveTypeFormTitle").innerText =
"✏️ Modifier : " + label;
document.getElementById("lt-mode").value = "edit";
const codeInput = document.getElementById("lt-code");
codeInput.value = code;
codeInput.readOnly = true;
codeInput.style.backgroundColor = "var(--bg-subtle)";
codeInput.style.opacity = "0.6";
codeInput.style.cursor = "not-allowed";
document.getElementById("lt-code-note").innerText = "Non modifiable";
document.getElementById("lt-label").value = label;
}
function resetLeaveTypeForm() {
document.getElementById("leaveTypeFormTitle").innerText = "+ Ajouter";
document.getElementById("lt-mode").value = "add";
const codeInput = document.getElementById("lt-code");
codeInput.value = "";
codeInput.readOnly = false;
codeInput.style.backgroundColor = "";
codeInput.style.opacity = "1";
codeInput.style.cursor = "text";
document.getElementById("lt-code-note").innerText = "Irréversible";
document.getElementById("lt-label").value = "";
}
// NOUVELLE FONCTION DE SUPPRESSION
async function deleteLeaveType(code, label) {
if (
!confirm(
`Supprimer définitivement le congé "${label}" du catalogue ? Cela le retirera également des membres qui l'utilisent.`,
)
)
return;
const fd = new FormData();
fd.append("action", "delete_leave_type");
fd.append("code", code);
try {
const res = await pachaFetch(
"/modules/family-calendar/includes/api/calendar-settings.php",
{ method: "POST", body: fd },
);
if (!res.success) throw new Error(res.error);
if (window.showToast) showToast("Congé supprimé avec succès !", "success");
resetLeaveTypeForm();
loadLeaveCatalog();
// On recharge l'onglet membre au cas où le membre affiché utilisait ce congé
if (document.getElementById("selectCalMember").value) {
loadMemberConfigView();
}
} catch (err) {
alert("Erreur de suppression : " + err.message);
}
}
async function saveLeaveType() {
const code = document.getElementById("lt-code").value.toUpperCase();
const label = document.getElementById("lt-label").value;
if (!code || !label)
return window.showToast
? showToast("Code et Label requis", "error")
: alert("Code et Label requis");
const fd = new FormData();
fd.append("action", "save_leave_type");
fd.append("mode", document.getElementById("lt-mode").value);
fd.append("code", code);
fd.append("label", label);
try {
const res = await pachaFetch(
"/modules/family-calendar/includes/api/calendar-settings.php",
{ method: "POST", body: fd },
);
if (!res.success) throw new Error(res.error);
if (window.showToast) showToast("Catalogue mis à jour !", "success");
resetLeaveTypeForm();
loadLeaveCatalog();
} catch (err) {
alert("Erreur d'enregistrement : " + err.message);
}
}
// ==========================================
// MODALE SETTINGS : AFFECTATION (MEMBRES)
// ==========================================
// Quand on choisit un membre dans la liste déroulante
async function loadMemberConfigView() {
const select = document.getElementById("selectCalMember");
const zone = document.getElementById("memberConfigZone");
if (!select || !select.value || !zone) return;
window.currentSelectedMemberId = parseInt(select.value);
const personId = parseInt(select.value);
window.currentSelectedMemberId = personId;
const role = (
select.options[select.selectedIndex].dataset.role || ""
).toLowerCase();
zone.innerHTML = '<p class="pf-muted-note">Chargement...</p>';
// === CAS 1 : C'EST UN ENFANT (On gère les modes de garde) ===
if (role === "child" || role === "enfant") {
const currentPerson = calGlobalData.people.find(
(k) => parseInt(k.id) === window.currentSelectedMemberId,
(k) => parseInt(k.id) === personId,
);
let savedModes = [];
try {
@@ -296,83 +360,145 @@ window.loadMemberConfigView = function () {
.join("");
zone.innerHTML = `
<h5 style="margin:0 0 8px 0; color:var(--text-main);">${tr("fc_care_modes_title") || "Modes de Garde"}</h5>
<p style="font-size:0.85rem; color:var(--text-muted); margin-bottom:12px;">Quels modes de garde s'appliquent à cet enfant ?</p>
<div style="display:flex; flex-direction:column; gap:6px;">
${modesHtml || `<em>Aucun mode de garde configuré dans le foyer.</em>`}
</div>
<button class="pf-btn pf-btn-primary" style="margin-top:15px; width:100%;" onclick="submitChildCareModes()">${tr("btn_save_rights") || "Enregistrer"}</button>
`;
} else {
window.loadAdultConfigView(window.currentSelectedMemberId, zone);
<h5 style="margin:0 0 8px 0; color:var(--text-main);">${tr("fc_care_modes_title") || "Modes de Garde"}</h5>
<p style="font-size:0.85rem; color:var(--text-muted); margin-bottom:12px;">Quels modes de garde s'appliquent à cet enfant ?</p>
<div style="display:flex; flex-direction:column; gap:6px;">
${modesHtml || `<em>Aucun mode de garde configuré dans le foyer.</em>`}
</div>
<button class="pf-btn pf-btn-primary" style="margin-top:15px; width:100%;" onclick="submitChildCareModes()">${tr("btn_save_rights") || "Enregistrer"}</button>
`;
}
};
// === CAS 2 : C'EST UN ADULTE (On gère les congés) ===
else {
try {
const res = await pachaFetch(
`/modules/family-calendar/includes/api/calendar-settings.php?action=get_person_leaves&person_id=${personId}`,
);
const memberLeaves = res.data || [];
renderMemberLeavesView(personId, memberLeaves);
} catch (err) {
zone.innerHTML =
'<p class="pf-muted-note" style="color:var(--danger);">Erreur de chargement.</p>';
}
}
}
// Affichage des congés du membre et du formulaire d'ajout
function renderMemberLeavesView(personId, memberLeaves) {
const zone = document.getElementById("memberConfigZone");
let html = `<h5 style="margin: 0 0 10px 0; color: var(--text-main);">Congés attribués</h5>`;
if (memberLeaves.length === 0) {
html += `<p class="pf-muted-note" style="font-size: 0.85rem;">Aucun congé attribué à ce membre.</p>`;
} else {
html += `<div style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 15px;">`;
memberLeaves.forEach((ml) => {
// On extrait le mois de la date anniversaire (ex: "2000-06-01" -> 6)
const moisRenouv = ml.anniversary_date
? parseInt(ml.anniversary_date.split("-")[1])
: 1;
html += `
<div style="display: flex; justify-content: space-between; align-items: center; background: var(--bg-subtle); padding: 8px; border-radius: 6px; border: 1px solid var(--border-light);">
<div>
<strong style="color: var(--text-main);">${ml.leave_type}</strong>
<span style="font-size: 0.8rem; color: var(--text-muted); margin-left: 5px;">(Quota: <b>${ml.allowance}j</b> - Renouv: <b>Mois ${moisRenouv}</b>)</span>
</div>
<button class="pf-btn btn-secondary" style="padding: 2px 6px; color: var(--danger);" onclick="deleteMemberLeave(${ml.id}, ${personId})">🗑️</button>
</div>
`;
});
html += `</div>`;
}
// Ajout du selecteur de mois dans l'attribution
html += `
<hr style="border: 0; border-top: 1px solid var(--border-light); margin: 15px 0;">
<h5 style="margin: 0 0 10px 0;">+ Attribuer un congé</h5>
<div style="display: flex; gap: 8px; align-items: flex-end; flex-wrap: wrap;">
<div style="flex: 1; min-width: 120px;">
<label class="pf-label" style="font-size: 0.8rem;">Type de congé</label>
<select id="new-member-leave-type" class="pf-input" style="width: 100%;">
<option value="" disabled selected>-- Sélectionner --</option>
${globalLeaveCatalog.map((lt) => `<option value="${lt.code}">${lt.label}</option>`).join("")}
</select>
</div>
<div style="flex: 0 0 70px;">
<label class="pf-label" style="font-size: 0.8rem;">Quota</label>
<input type="number" step="0.5" id="new-member-leave-allowance" class="pf-input" value="0" style="width: 100%;">
</div>
<div style="flex: 0 0 100px;">
<label class="pf-label" style="font-size: 0.8rem;">Renouvellement</label>
<select id="new-member-leave-reset" class="pf-input" style="width: 100%;">
<option value="1">${tr("month_jan") || "Janvier"}</option>
<option value="2">${tr("month_feb") || "Février"}</option>
<option value="3">${tr("month_mar") || "Mars"}</option>
<option value="4">${tr("month_apr") || "Avril"}</option>
<option value="5">${tr("month_may") || "Mai"}</option>
<option value="6">${tr("month_jun") || "Juin"}</option>
<option value="7">${tr("month_jul") || "Juillet"}</option>
<option value="8">${tr("month_aug") || "Août"}</option>
<option value="9">${tr("month_sep") || "Septembre"}</option>
<option value="10">${tr("month_oct") || "Octobre"}</option>
<option value="11">${tr("month_nov") || "Novembre"}</option>
<option value="12">${tr("month_dec") || "Décembre"}</option>
</select>
</div>
<button class="pf-btn pf-btn-primary" onclick="addMemberLeave(${personId})">Ajouter</button>
</div>
`;
zone.innerHTML = html;
}
async function addMemberLeave(personId) {
const leaveCode = document.getElementById("new-member-leave-type").value;
const allowance = document.getElementById("new-member-leave-allowance").value;
const resetMonth = document.getElementById("new-member-leave-reset").value;
if (!leaveCode) return showToast("Veuillez sélectionner un type", "error");
const fd = new FormData();
fd.append("action", "add_person_leave");
fd.append("person_id", personId);
fd.append("leave_type", leaveCode);
fd.append("allowance", allowance);
fd.append("reset_month", resetMonth);
window.submitChildCareModes = async function () {
if (!window.currentSelectedMemberId) return;
const checkboxes = document.querySelectorAll(".js-care-mode-cb:checked");
const selectedModes = Array.from(checkboxes).map((cb) => cb.value);
try {
const formData = new FormData();
formData.append("action", "save_child_care_modes");
formData.append("person_id", window.currentSelectedMemberId);
formData.append("care_modes", JSON.stringify(selectedModes));
const res = await pachaFetch(
"/modules/family-calendar/includes/api/settings.php",
{ method: "POST", body: formData },
"/modules/family-calendar/includes/api/calendar-settings.php",
{ method: "POST", body: fd },
);
if (!res.success) throw new Error(res.error);
if (window.showToast)
showToast(tr("fc_child_saved") || "Sauvegardé !", "success");
else alert(tr("fc_child_saved") || "Sauvegardé !");
const currentPerson = calGlobalData.people.find(
(k) => parseInt(k.id) === window.currentSelectedMemberId,
);
if (currentPerson) {
currentPerson.care_modes = JSON.stringify(selectedModes);
}
if (window.showToast) showToast("Congé attribué avec succès !", "success");
loadMemberConfigView();
} catch (err) {
alert("Erreur: " + err.message);
}
};
}
async function submitMemberLeaves() {
if (!currentSelectedMemberId || isNaN(currentSelectedMemberId)) {
alert("Erreur technique : Aucun membre valide sélectionné.");
// Supprimer un congé d'un membre
async function deleteMemberLeave(leaveId, personId) {
if (!confirm(tr("confirm_delete") || "Retirer ce congé pour ce membre ?"))
return;
}
const fd = new FormData();
fd.append("action", "delete_person_leave");
fd.append("id", leaveId);
try {
const rows = document.querySelectorAll(".js-leave-row");
const leavesPayload = Array.from(rows)
.map((row) => ({
type: row.querySelector(".js-leave-type").value.trim().toUpperCase(),
method: row.querySelector(".js-leave-method").value,
allowance: row.querySelector(".js-leave-allowance").value,
date: row.querySelector(".js-leave-date").value,
}))
.filter((l) => l.type && l.date);
const formData = new FormData();
formData.append("action", "save_member_leaves");
formData.append("person_id", currentSelectedMemberId);
formData.append("leaves", JSON.stringify(leavesPayload));
const res = await pachaFetch(
"/modules/family-calendar/includes/api/settings.php",
{ method: "POST", body: formData },
"/modules/family-calendar/includes/api/calendar-settings.php",
{ method: "POST", body: fd },
);
if (!res.success) throw new Error(res.error);
if (window.showToast)
showToast(tr("fc_matrix_saved") || "Matrice enregistrée", "success");
else alert(tr("fc_matrix_saved") || "Matrice enregistrée");
closeCalendarSettings();
setTimeout(openCalendarSettings, 300);
if (window.showToast) showToast("Congé retiré !", "success");
loadMemberConfigView();
} catch (err) {
alert("Erreur : " + err.message);
alert("Erreur: " + err.message);
}
}
@@ -450,7 +576,7 @@ document.addEventListener("DOMContentLoaded", () => {
try {
const res = await pachaFetch(
"/modules/family-calendar/includes/api/settings.php?action=get_all",
"/modules/family-calendar/includes/api/calendar-settings.php?action=get_all",
);
if (res && res.success && res.data) {
window.calGlobalData = res.data;
@@ -7,19 +7,14 @@ header('Content-Type: application/json');
$action = $_REQUEST['action'] ?? '';
try {
// ─── 1. RECUPÉRATION COMPLÈTE DES PARAMÈTRES ───
// ─── 1. RECUPÉRATION COMPLÈTE DES PARAMÈTRES (INITIALISATION) ───
if ($action === 'get_all') {
// A. Options globales du foyer
$stmtFoyer = $pdo->query("SELECT zone_scolaire, care_modes FROM pf_foyer_settings LIMIT 1");
// On retire le fallback "Nounou" codé en dur, on part sur vide si rien n'est configuré
$foyer = $stmtFoyer->fetch(PDO::FETCH_ASSOC) ?: ['zone_scolaire' => 'C', 'care_modes' => '[]'];
// B. Liste des membres de la famille (Triés par rôle pour grouper Parents, Enfants, Helpers)
// 🟢 CORRECTION : Ajout de la colonne `care_modes` et `color` pour le Javascript !
$stmtPeople = $pdo->query("SELECT id, name, role, care_modes, color FROM pf_people WHERE is_active = 1 ORDER BY role ASC, name ASC");
$people = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
// C. Matrice des congés par personne
$stmtLeaves = $pdo->query("SELECT person_id, leave_type, anniversary_date, method, allowance FROM pf_person_leave_meta");
$leavesRaw = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC);
@@ -33,18 +28,16 @@ try {
];
}
// D. Paramètres dynamiques du calendrier (pf_settings)
$stmtSettings = $pdo->query("SELECT setting_key, setting_value FROM pf_settings WHERE module = 'calendar'");
$calendarSettingsRaw = $stmtSettings->fetchAll(PDO::FETCH_KEY_PAIR); // Crée un tableau [key => value]
$calendarSettingsRaw = $stmtSettings->fetchAll(PDO::FETCH_KEY_PAIR);
// Paramètres par défaut si la table est vide
$calendarSettings = [
'calendar_default_view' => $calendarSettingsRaw['calendar_default_view'] ?? 'month',
'calendar_first_day' => $calendarSettingsRaw['calendar_first_day'] ?? '1',
'calendar_working_hours' => $calendarSettingsRaw['calendar_working_hours'] ?? '08:00-19:00'
];
// E. NOUVEAU : Récupération du catalogue des types de congés de la famille
// Catalogue des congés
$stmtLeaveTypes = $pdo->query("SELECT code, label, default_allowance, reset_month, allow_carry_over FROM pf_leave_types ORDER BY label ASC");
$leaveTypes = $stmtLeaveTypes->fetchAll(PDO::FETCH_ASSOC);
@@ -58,7 +51,7 @@ try {
'people' => $people,
'leaves' => $leavesMap,
'calendar_settings' => $calendarSettings,
'leave_types' => $leaveTypes // On envoie le catalogue au JS !
'leave_types' => $leaveTypes
]
]);
exit;
@@ -70,7 +63,6 @@ try {
$zone = trim($_POST['zone_scolaire'] ?? 'C');
$modesRaw = $_POST['care_modes'] ?? '[]';
$modesArray = json_encode(array_values(array_unique(array_filter(json_decode($modesRaw, true)))));
$stmt = $pdo->prepare("UPDATE pf_foyer_settings SET zone_scolaire = ?, care_modes = ? WHERE id = 1");
@@ -80,74 +72,7 @@ try {
exit;
}
// ─── 3. SAUVEGARDE DES CONGÉS D'UN MEMBRE (ONGLET 2) ───
if ($action === 'save_member_leaves') {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
$personId = (int)($_POST['person_id'] ?? 0);
$leavesData = json_decode($_POST['leaves'] ?? '[]', true);
if ($personId <= 0) throw new Exception("Identifiant de membre invalide.");
$pdo->beginTransaction();
$stmtDelete = $pdo->prepare("DELETE FROM pf_person_leave_meta WHERE person_id = ?");
$stmtDelete->execute([$personId]);
if (!empty($leavesData)) {
$stmtInsert = $pdo->prepare("INSERT INTO pf_person_leave_meta (person_id, leave_type, method, allowance, anniversary_date) VALUES (?, ?, ?, ?, ?)");
foreach ($leavesData as $leave) {
$type = strtoupper(trim($leave['type']));
$method = in_array($leave['method'] ?? '', ['FIXED', 'ACCUMULATED']) ? $leave['method'] : 'FIXED';
$allowance = (float)($leave['allowance'] ?? 0);
$date = trim($leave['date']);
// Si le JS n'envoie que "MM-DD" (Renouvellement perpétuel), on ajoute l'année bissextile 2000 pour satisfaire le format DATE de MySQL
if (preg_match('/^\d{2}-\d{2}$/', $date)) {
$date = "2000-" . $date;
}
if (!empty($type) && !empty($date)) {
$stmtInsert->execute([$personId, $type, $method, $allowance, $date]);
}
}
}
$pdo->commit();
echo json_encode(['success' => true]);
exit;
}
// ─── 4. SAUVEGARDE DES PARAMÈTRES D'AFFICHAGE DU CALENDRIER (ONGLET 3) ───
if ($action === 'save_calendar_settings') {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
$allowed_keys = ['calendar_default_view', 'calendar_first_day', 'calendar_working_hours'];
$stmt = $pdo->prepare("
INSERT INTO pf_settings (setting_key, setting_value, module)
VALUES (:key, :val, 'calendar')
ON DUPLICATE KEY UPDATE setting_value = :val2
");
$pdo->beginTransaction();
foreach ($allowed_keys as $key) {
if (isset($_POST[$key])) {
$value = trim($_POST[$key]);
$stmt->execute([
'key' => $key,
'val' => $value,
'val2' => $value
]);
}
}
$pdo->commit();
echo json_encode(['success' => true]);
exit;
}
// ─── 5. SAUVEGARDE DES MODES DE GARDE D'UN ENFANT ───
// ─── 3. SAUVEGARDE DES MODES DE GARDE D'UN ENFANT ───
if ($action === 'save_child_care_modes') {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
@@ -164,7 +89,100 @@ try {
exit;
}
throw new Exception("Action inconnue");
// ─── 4. GESTION DU CATALOGUE DE CONGÉS (FOYER) ───
if ($action === 'get_leave_types') {
$stmt = $pdo->query("SELECT * FROM pf_leave_types ORDER BY label ASC");
echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
exit;
}
if ($action === 'save_leave_type') {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
$mode = $_POST['mode'] ?? 'add';
$code = strtoupper(trim($_POST['code'] ?? ''));
$label = trim($_POST['label'] ?? '');
if (empty($code) || empty($label)) throw new Exception("Le code et le label sont obligatoires.");
if ($mode === 'add') {
$stmt = $pdo->prepare("INSERT INTO pf_leave_types (code, label) VALUES (?, ?)");
$stmt->execute([$code, $label]);
} else {
$stmt = $pdo->prepare("UPDATE pf_leave_types SET label = ? WHERE code = ?");
$stmt->execute([$label, $code]);
}
echo json_encode(['success' => true]);
exit;
}
if ($action === 'delete_leave_type') {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
$code = strtoupper(trim($_POST['code'] ?? ''));
if (empty($code)) throw new Exception("Code du congé manquant.");
$pdo->beginTransaction();
// 1. On supprime d'abord ce congé chez tous les membres à qui on l'avait attribué
$stmtMeta = $pdo->prepare("DELETE FROM pf_person_leave_meta WHERE leave_type = ?");
$stmtMeta->execute([$code]);
// 2. Ensuite, on supprime le congé du catalogue global
$stmtCat = $pdo->prepare("DELETE FROM pf_leave_types WHERE code = ?");
$stmtCat->execute([$code]);
$pdo->commit();
echo json_encode(['success' => true]);
exit;
}
// ─── 5. GESTION DES CONGÉS INDIVIDUELS (MEMBRES) ───
if ($action === 'get_person_leaves') {
$personId = (int)($_GET['person_id'] ?? 0);
$stmt = $pdo->prepare("SELECT id, leave_type, allowance, anniversary_date FROM pf_person_leave_meta WHERE person_id = ? ORDER BY leave_type ASC");
$stmt->execute([$personId]);
echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
exit;
}
if ($action === 'add_person_leave') {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
$personId = (int)($_POST['person_id'] ?? 0);
$type = strtoupper(trim($_POST['leave_type'] ?? ''));
$allowance = (float)($_POST['allowance'] ?? 0);
$resetMonth = (int)($_POST['reset_month'] ?? 1);
if ($personId <= 0 || empty($type)) throw new Exception("Données invalides.");
$check = $pdo->prepare("SELECT id FROM pf_person_leave_meta WHERE person_id = ? AND leave_type = ?");
$check->execute([$personId, $type]);
if ($check->rowCount() > 0) throw new Exception("Ce congé est déjà attribué à cette personne.");
// On construit la date anniversaire avec le mois choisi par l'utilisateur
$anniversaryDate = "2000-" . str_pad((string)$resetMonth, 2, "0", STR_PAD_LEFT) . "-01";
$stmt = $pdo->prepare("INSERT INTO pf_person_leave_meta (person_id, leave_type, allowance, method, anniversary_date) VALUES (?, ?, ?, 'FIXED', ?)");
$stmt->execute([$personId, $type, $allowance, $anniversaryDate]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'delete_person_leave') {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
$id = (int)($_POST['id'] ?? 0);
$stmt = $pdo->prepare("DELETE FROM pf_person_leave_meta WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true]);
exit;
}
throw new Exception("Action inconnue : " . htmlspecialchars($action));
} catch (\Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
+67 -34
View File
@@ -1,51 +1,84 @@
<?php
require_once __DIR__ . '/../../includes/db.php';
require __DIR__ . '/../../includes/auth.php';
require __DIR__ . '/../../includes/db.php';
require_login();
header('Content-Type: application/json');
$action = $_POST['action'] ?? $_GET['action'] ?? '';
try {
// 1. Lire toutes les fêtes
if ($action === 'get_occasions') {
$stmt = $pdo->query("SELECT * FROM pf_gift_occasions ORDER BY month_date ASC, id ASC");
echo json_encode(['ok' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
if ($action === 'get_all') {
$occ = $pdo->query("SELECT * FROM pf_gift_occasions ORDER BY sort_order ASC, id ASC")->fetchAll();
$proches = $pdo->query("SELECT * FROM pf_people WHERE role IN ('proche_adulte', 'proche_enfant') ORDER BY name ASC")->fetchAll();
echo json_encode(['success' => true, 'data' => ['occasions' => $occ, 'proches' => $proches]]);
exit;
}
// 2. Ajouter une nouvelle fête (Renvoie l'ID pour rafraîchir la modale)
if ($action === 'add_occasion') {
$name = trim($_POST['name'] ?? '');
$month_date = trim($_POST['month_date'] ?? '');
if ($action === 'save_occasion') {
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
$name = trim($_POST['name']);
$icon = trim($_POST['icon']) ?: '🎁';
$month_date = trim($_POST['month_date']) ?: null;
if (empty($name)) throw new Exception("Name required");
$code = strtoupper(preg_replace('/[^A-Za-z0-9]/', '', $name));
$code = substr($code, 0, 20);
if (empty($name)) throw new Exception("Le nom est obligatoire.");
$stmt = $pdo->prepare("INSERT INTO pf_gift_occasions (code, name, month_date, is_active) VALUES (?, ?, ?, 1)");
$stmt->execute([$code, $name, $month_date ?: null]);
echo json_encode(['ok' => true]);
exit;
}
// 3. Sauvegarde globale des cases à cocher
if ($action === 'save_toggles') {
$states = json_decode($_POST['states'] ?? '[]', true);
if (is_array($states)) {
$stmt = $pdo->prepare("UPDATE pf_gift_occasions SET is_active = ? WHERE id = ?");
foreach ($states as $item) {
$stmt->execute([(int)$item['state'], (int)$item['id']]);
}
if ($id) {
$stmt = $pdo->prepare("UPDATE pf_gift_occasions SET name=?, icon=?, month_date=? WHERE id=?");
$stmt->execute([$name, $icon, $month_date, $id]);
} else {
$code = 'CUST_' . strtoupper(substr(preg_replace('/[^A-Za-z0-9]/', '', $name), 0, 4)) . '_' . rand(100, 999);
$stmt = $pdo->prepare("INSERT INTO pf_gift_occasions (code, name, icon, month_date) VALUES (?, ?, ?, ?)");
$stmt->execute([$code, $name, $icon, $month_date]);
}
echo json_encode(['ok' => true]);
echo json_encode(['success' => true]);
exit;
}
throw new Exception("Action inconnue.");
if ($action === 'delete_occasion') {
$id = (int)$_POST['id'];
$pdo->prepare("DELETE FROM pf_gift_occasions WHERE id=?")->execute([$id]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'toggle_occasion') {
$id = (int)$_POST['id'];
$state = (int)$_POST['state'];
$pdo->prepare("UPDATE pf_gift_occasions SET is_active=? WHERE id=?")->execute([$state, $id]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'save_proche') {
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
$name = trim($_POST['name']);
$role = $_POST['role'];
if (empty($name)) throw new Exception("Le nom est obligatoire.");
if (!in_array($role, ['proche_adulte', 'proche_enfant'])) throw new Exception("Rôle invalide.");
if ($id) {
// Sécurité : on s'assure qu'on modifie bien un proche et pas un parent
$stmt = $pdo->prepare("UPDATE pf_people SET name=?, role=? WHERE id=? AND role IN ('proche_adulte', 'proche_enfant')");
$stmt->execute([$name, $role, $id]);
} else {
$stmt = $pdo->prepare("INSERT INTO pf_people (name, role, color) VALUES (?, ?, '#94a3b8')");
$stmt->execute([$name, $role]);
}
echo json_encode(['success' => true]);
exit;
}
if ($action === 'delete_proche') {
$id = (int)$_POST['id'];
$stmt = $pdo->prepare("DELETE FROM pf_people WHERE id=? AND role IN ('proche_adulte', 'proche_enfant')");
$stmt->execute([$id]);
echo json_encode(['success' => true]);
exit;
}
} catch (Exception $e) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
}
?>
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
exit;
}
+161 -339
View File
@@ -6,7 +6,6 @@
margin: 0;
color: var(--text-main);
}
.cl-titlebar {
display: flex;
flex-wrap: wrap;
@@ -17,7 +16,6 @@
border-bottom: 1px solid var(--border-light);
padding-bottom: 15px;
}
.cl-view-switch {
display: flex;
padding: 4px;
@@ -25,7 +23,6 @@
border: 1px solid var(--border-light);
box-shadow: var(--shadow-sm);
}
.cl-view-btn {
padding: 6px 16px;
border-radius: 6px;
@@ -35,13 +32,12 @@
color: var(--text-muted);
transition: 0.2s;
}
.cl-view-btn.is-active {
background: var(--text-main);
color: var(--bg-panel);
}
/* === FILTRES STICKY === */
/* === FILTRES === */
.pf-filter-bar {
display: flex;
gap: 10px;
@@ -52,7 +48,6 @@
z-index: 50;
flex-wrap: wrap;
}
.pf-filter-label {
font-size: 0.85rem;
font-weight: 600;
@@ -62,35 +57,6 @@
gap: 6px;
}
.pf-filter-select {
padding: 6px 12px;
border-radius: 20px;
border: 1px solid var(--border-light);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(8px);
font-size: 0.85rem;
font-weight: 600;
color: var(--text-main);
outline: none;
font-family: inherit;
cursor: pointer;
box-shadow: var(--shadow-sm);
width: auto;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2364748b'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 12px;
padding-right: 28px;
}
.pf-filter-select:focus {
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
}
/* === SECTIONS ENFANTS === */
.pf-child-section {
margin-bottom: 30px;
@@ -226,18 +192,69 @@
z-index: 2;
border-right: 2px solid var(--border-light);
}
.cl-occasion-icon {
width: 20px;
height: 20px;
object-fit: contain;
vertical-align: middle;
.gift-tricount-section {
border: 1px solid var(--border-light);
margin-top: 40px;
}
.gift-tricount-title {
margin-top: 0;
color: var(--text-main);
font-size: 1.3rem;
}
.gift-tricount-success {
color: var(--success);
font-weight: 700;
margin-bottom: 0;
}
.gift-tricount-debt-amount {
color: var(--danger);
}
.gift-matrix-details {
margin-top: 20px;
padding: 12px;
border-radius: 8px;
background: var(--bg-panel);
border: 1px solid var(--border-light);
}
.gift-matrix-summary {
cursor: pointer;
font-weight: 600;
color: var(--text-muted);
outline: none;
}
.gift-matrix-wrapper {
overflow-x: auto;
margin-top: 15px;
}
.cl-debt-matrix th.sticky-col.bg-header {
background: var(--bg-page);
}
.cl-debt-matrix th.sticky-col.bg-body {
background: var(--bg-panel);
}
.gift-cell-danger {
color: var(--danger);
font-weight: 700;
background: var(--bg-danger-soft);
}
.gift-cell-muted {
color: var(--text-muted);
}
/* === TYPOGRAPHIE & ICONES === */
.cl-occasion-icon {
font-size: 1.4rem;
margin-right: 8px;
}
.cl-occasion-title {
display: flex;
align-items: center;
gap: 8px;
}
.gift-empty-state {
color: var(--text-muted);
font-size: 0.9rem;
font-style: italic;
margin: 0;
}
/* === COMPOSANT MULTI-SELECT VANILLA === */
@@ -355,79 +372,7 @@
}
}
/* Header & Settings Button */
.gift-settings-btn {
font-size: 1.5rem;
background: none;
border: none;
cursor: pointer;
}
/* Empty States */
.gift-empty-state {
color: var(--text-muted);
font-size: 0.9rem;
font-style: italic;
margin: 0;
}
/* Section Tricount (Bilan) */
.gift-tricount-section {
border: 1px solid var(--border-light);
margin-top: 40px;
}
.gift-tricount-title {
margin-top: 0;
color: var(--text-main);
font-size: 1.3rem;
}
.gift-tricount-success {
color: var(--success);
font-weight: 700;
margin-bottom: 0;
}
.gift-tricount-debt-amount {
color: var(--danger);
}
.gift-matrix-details {
margin-top: 20px;
padding: 12px;
border-radius: 8px;
}
.gift-matrix-summary {
cursor: pointer;
font-weight: 600;
color: var(--text-muted);
outline: none;
}
.gift-matrix-wrapper {
overflow-x: auto;
margin-top: 15px;
}
.cl-debt-matrix th.sticky-col,
.cl-debt-matrix td.sticky-col {
position: sticky;
left: 0;
z-index: 2;
border-right: 2px solid #e2e8f0;
}
.cl-debt-matrix th.sticky-col.bg-header {
background: #f8fafc;
}
.cl-debt-matrix th.sticky-col.bg-body {
background: white;
}
.gift-cell-danger {
color: var(--danger);
font-weight: 700;
background: #fef2f2;
}
.gift-cell-muted {
color: var(--text-muted);
}
/* Modale Edition Cadeau */
/* === MODALE EDITION CADEAU === */
.gift-modal-custom-content {
max-width: 500px;
width: 95%;
@@ -459,249 +404,126 @@
display: flex;
justify-content: flex-end;
gap: 10px;
border-top: 1px solid var(--border-light);
}
/* Modale Paramètres Occasions (Bottom Sheet style) */
.gift-settings-backdrop {
.gl-pane {
display: none;
align-items: flex-end; /* Bottom sheet on mobile */
animation: fadeIn 0.3s ease;
}
.gift-settings-modal {
width: 100%;
border-radius: 12px 12px 0 0;
margin: 0;
.gl-pane.active {
display: block;
}
.gift-settings-header {
display: flex;
justify-content: space-between;
align-items: center;
.gl-pane-title {
margin-top: 0;
color: var(--text-main);
font-size: 1.1rem;
margin-bottom: 10px;
}
.gift-settings-header h3 {
margin: 0;
}
.gift-settings-body {
padding: 1rem;
}
.gift-occasions-list {
margin-bottom: 2rem;
}
.gift-occasions-grid {
display: grid;
gap: 0.5rem;
}
.gift-occasion-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.8rem 1rem;
border: 1px solid var(--border-light);
border-radius: 8px;
}
.gift-occasion-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.gift-date-badge {
font-size: 0.8em;
background: var(--bg-page);
padding: 2px 6px;
border-radius: 4px;
margin-left: 10px;
}
.gift-add-subtitle {
margin-top: 1rem;
margin-bottom: 0.5rem;
}
.gift-add-form {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.gift-add-input-name {
flex: 1;
min-width: 150px;
}
.gift-add-input-date {
width: 120px;
}
.gift-help-text {
.gl-pane-desc {
font-size: 0.85rem;
color: var(--text-muted);
font-size: 0.85em;
margin-bottom: 15px;
}
.gl-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
background: var(--bg-panel);
border-radius: var(--radius);
border: 1px solid var(--border-light);
margin-bottom: 8px;
}
.gl-form-row {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: flex-end;
margin-top: 15px;
}
.gl-divider {
margin: 1.5rem 0;
border: 0;
border-bottom: 1px solid var(--border-light);
}
.gl-input-sm {
flex: 0 0 60px;
}
.gl-input-md {
flex: 1;
min-width: 120px;
}
.gift-loader {
text-align: center;
padding: 20px;
}
.gift-error-msg {
color: var(--danger);
padding: 10px;
}
/* ==========================================================================
BOUTONS & MODALE (Design System Todo)
========================================================================== */
.btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.4rem 0.85rem;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.15s;
white-space: nowrap;
}
.btn-icon {
padding: 0.25rem;
min-width: 28px;
justify-content: center;
font-size: 1.2rem;
}
.btn-ghost {
background: transparent;
border: none;
color: var(--text-muted);
font-style: italic;
}
.btn-ghost:hover {
color: var(--text-main);
background: var(--bg-page);
border-radius: 6px;
}
/* Correction de la surimpression de la modale Paramètres */
.gift-settings-backdrop {
display: none;
position: fixed;
inset: 0;
z-index: 200;
background: rgba(15, 23, 42, 0.5);
backdrop-filter: blur(4px);
.align-center {
align-items: center;
justify-content: center;
padding: 1rem;
}
.gift-settings-modal {
background: var(--bg-panel);
border: 1px solid var(--border-light);
border-radius: 14px;
width: 100%;
max-width: 500px;
max-height: 90vh;
/* =========================================
COMPOSANTS MODALE & EDITION IN-PLACE
========================================= */
/* Sélecteur d'émojis horizontal */
.emoji-picker {
display: flex;
flex-direction: column;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
animation: modalIn 0.18s ease;
}
@keyframes modalIn {
from {
opacity: 0;
transform: scale(0.96) translateY(-8px);
}
to {
opacity: 1;
transform: none;
}
}
/* ==========================================================================
MODALE PARAMÈTRES (Design System Todo)
========================================================================== */
.gift-settings-backdrop {
display: none;
position: fixed;
inset: 0;
z-index: 200;
background: rgba(15, 23, 42, 0.5);
backdrop-filter: blur(4px);
align-items: center;
justify-content: center;
padding: 1rem;
}
.gift-settings-backdrop.show {
display: flex;
}
.gift-settings-modal {
background: var(--bg-panel);
border: 1px solid var(--border-light);
border-radius: 14px;
width: 100%;
max-width: 500px;
max-height: 90vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
animation: giftModalIn 0.18s ease;
}
@keyframes giftModalIn {
from {
opacity: 0;
transform: scale(0.96) translateY(-8px);
}
to {
opacity: 1;
transform: none;
}
}
.gift-settings-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--border-light);
}
.gift-settings-header h3 {
font-size: 1rem;
font-weight: 700;
margin: 0;
}
.gift-settings-close {
background: none;
border: none;
cursor: pointer;
color: var(--text-muted);
font-size: 1.2rem;
border-radius: 6px;
padding: 0.2rem 0.4rem;
}
.gift-settings-close:hover {
background: var(--bg-page);
}
.gift-settings-body {
padding: 1.25rem;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.9rem;
}
.gift-add-subtitle {
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
margin: 0.5rem 0 0;
}
.gift-add-form {
display: flex;
gap: 0.5rem;
gap: 8px;
flex-wrap: wrap;
}
/* Aligner le bouton sur celui de Todo */
.gift-add-form .btn-primary {
background: var(--primary);
color: #fff;
.emoji-option {
cursor: pointer;
width: 38px;
height: 38px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border-light);
border-radius: 8px;
font-size: 1.4rem;
transition: all 0.2s;
background: transparent;
user-select: none;
}
.emoji-option:hover {
background: var(--bg-page);
border-color: #cbd5e1;
}
.emoji-option.selected {
border-color: var(--primary);
background: var(
--bg-subtle
); /* ou un bleu très clair rgba(59, 130, 246, 0.1) */
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
.gift-add-form .btn-primary:hover {
background: var(--primary-dark);
/* Alignement des boutons d'ajout */
.modal-actions-right {
display: flex;
gap: 10px;
justify-content: flex-end;
width: 100%;
margin-top: 10px;
padding-top: 15px;
border-top: 1px solid var(--border-light);
}
/* Édition In-Place sur les lignes de la liste */
.inline-edit-form {
display: flex;
gap: 8px;
align-items: center;
background: var(--bg-subtle);
padding: 8px 12px;
border-radius: 8px;
width: 100%;
flex-wrap: wrap; /* Évite que ça dépasse sur mobile */
}
.inline-edit-form .pf-input {
margin: 0;
}