color select
Deploy HouseHub / deploy (push) Successful in 2s

This commit is contained in:
FeFeClochette committed 2026-09-02 19:13:51 +02:00
1 parent c05802fc79
commit 21c9b37ae0
11 files changed
+2688 -471

No files matched your search

+196 -113
View File
@@ -1,127 +1,210 @@
/* ════════════════════════════════════════════════════════════════════ /* ════════════════════════════════════════════════════════════════════
dark-mode.css — Dark mode overrides (all modules) dark-mode.css — Dark mode overrides (Cleaned & Optimized)
Chargé en dernier dans le head pour garantir la cascade. Chargé en dernier dans le head pour garantir la cascade.
════════════════════════════════════════════════════════════════════ */ ════════════════════════════════════════════════════════════════════ */
/* ─── HOME PAGE ─────────────────────────────────────────────────────────────── */ /* ─── FONDS & CONTENEURS PRINCIPAUX ────────────────────────────────── */
[data-theme="dark"] .pf-card,
[data-theme="dark"] .pf-modal-content,
[data-theme="dark"] .budget-tabs-container,
[data-theme="dark"] .fc-header-row,
[data-theme="dark"] .fc-month-nav-row,
[data-theme="dark"] .fc-month-calendar-wrapper,
[data-theme="dark"] .fc-month-balances,
[data-theme="dark"] .hol-idea-card,
[data-theme="dark"] .pf-holidays__titlebar,
[data-theme="dark"] .hol-filters-row,
[data-theme="dark"] .cl-titlebar,
[data-theme="dark"] .pf-child-section,
[data-theme="dark"] .pf-child-totals-bar,
[data-theme="dark"] .pf-gift-card-compact,
[data-theme="dark"] .epargne-grid-table,
[data-theme="dark"] .sticky-col {
background: var(--bg-panel) !important;
border-color: var(--border-light) !important;
color: var(--text-main) !important;
}
[data-theme="dark"] .owner-tabs,
[data-theme="dark"] .fc-view-controls,
[data-theme="dark"] .fc-day--other-month,
[data-theme="dark"] .fc-minimal-balance-card,
[data-theme="dark"] .pf-summary-pill,
[data-theme="dark"] .pf-filter-bar {
background: var(--bg-page) !important;
border-color: var(--border-light) !important;
color: var(--text-muted) !important;
}
/* ─── TYPOGRAPHIE GLOBALE ──────────────────────────────────────────── */
[data-theme="dark"] h1,
[data-theme="dark"] h2,
[data-theme="dark"] h3,
[data-theme="dark"] .pf-modal-title,
[data-theme="dark"] .pf-card-title,
[data-theme="dark"] .hol-main-title,
[data-theme="dark"] .pf-gift-title,
[data-theme="dark"] .budget-view h2,
[data-theme="dark"] .view-header h2,
[data-theme="dark"] .month-header-container {
color: var(--text-main) !important;
}
[data-theme="dark"] .pf-card-desc,
[data-theme="dark"] .tab-item {
color: var(--text-muted);
}
/* ─── INPUTS & SÉLECTEURS ──────────────────────────────────────────── */
[data-theme="dark"] .pf-input,
[data-theme="dark"] .pf-filter-select,
[data-theme="dark"] .hol-year-select,
[data-theme="dark"] .budget-view input,
[data-theme="dark"] .budget-view select,
[data-theme="dark"] .budget-view textarea,
[data-theme="dark"] .hol-idea-card input,
[data-theme="dark"] .hol-idea-card textarea,
[data-theme="dark"] .hol-idea-card select {
background: #1c2128 !important;
border-color: var(--border-light) !important;
color: var(--text-main) !important;
}
[data-theme="dark"] .pf-input::placeholder {
color: #484f58 !important;
}
/* ─── BOUTONS & ÉTATS ACTIFS ───────────────────────────────────────── */
[data-theme="dark"] .pf-btn.btn-secondary,
[data-theme="dark"] .fc-nav-button,
[data-theme="dark"] .fc-view-button,
[data-theme="dark"] .hol-map-toggle,
[data-theme="dark"] .cl-view-btn,
[data-theme="dark"] .pf-btn-icon-text,
[data-theme="dark"] .owner-tab {
background: var(--bg-panel) !important;
color: var(--text-muted) !important;
border-color: var(--border-light) !important;
}
[data-theme="dark"] .pf-btn.btn-secondary:hover,
[data-theme="dark"] .fc-nav-button:hover,
[data-theme="dark"] .hol-map-toggle:hover,
[data-theme="dark"] .pf-btn-icon-text:hover {
background: rgba(255, 255, 255, 0.06) !important;
color: var(--text-main) !important;
}
[data-theme="dark"] .tab-item.active,
[data-theme="dark"] .fc-view-button--active,
[data-theme="dark"] .cl-view-btn.is-active {
background: rgba(59, 130, 246, 0.15) !important;
color: var(--primary) !important;
border-color: rgba(59, 130, 246, 0.3) !important;
}
[data-theme="dark"] .owner-tab.active {
background: var(--primary) !important;
color: #fff !important;
}
/* ─── TABLES ───────────────────────────────────────────────────────── */
[data-theme="dark"] .pf-table,
[data-theme="dark"] .fc-month-table,
[data-theme="dark"] #planningTable-wrapper {
background: var(--bg-panel) !important;
color: var(--text-main);
border-color: var(--border-light) !important;
}
[data-theme="dark"] .pf-table thead th,
[data-theme="dark"] .fc-month-table th,
[data-theme="dark"] #planningTable thead th,
[data-theme="dark"] th.sticky-col,
[data-theme="dark"] .pf-child-header {
background: #1c2128 !important;
color: var(--text-muted) !important;
border-color: var(--border-light) !important;
}
[data-theme="dark"] .pf-table tbody td,
[data-theme="dark"] .fc-month-table td,
[data-theme="dark"] #planningTable tbody td {
border-color: rgba(48, 54, 61, 0.5) !important;
}
[data-theme="dark"] .pf-table tbody tr:hover td,
[data-theme="dark"] .fc-month-table td:hover:not(.fc-day--other-month) {
background: rgba(255, 255, 255, 0.04) !important;
}
[data-theme="dark"] .pf-table tfoot td {
background: #1c2128 !important;
border-color: rgba(48, 54, 61, 0.5) !important;
}
/* ─── MODULES SPÉCIFIQUES ──────────────────────────────────────────── */
/* Accueil */
[data-theme="dark"] body[data-page="home"] .pf-module-card { [data-theme="dark"] body[data-page="home"] .pf-module-card {
background: rgba(22,27,34,.92) !important; background: rgba(22, 27, 34, 0.92) !important;
border: 1px solid var(--border-light) !important; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4) !important;
box-shadow: 0 4px 15px rgba(0,0,0,.4) !important;
} }
[data-theme="dark"] body[data-page="home"] .pf-module-card:hover { [data-theme="dark"] body[data-page="home"] .pf-module-card:hover {
border-color: var(--primary) !important; border-color: var(--primary) !important;
} }
[data-theme="dark"] body[data-page="home"] .pf-card-title { color: var(--text-main); }
[data-theme="dark"] body[data-page="home"] .pf-card-desc { color: var(--text-muted); }
[data-theme="dark"] body[data-page="home"] .pf-card-cta { color: var(--primary); }
/* ─── SHARED / GLOBAL PATTERNS ──────────────────────────────────────────────── */ /* Épargne & Grilles */
[data-theme="dark"] .pf-card { background: var(--bg-panel) !important; border-color: var(--border-light) !important; color: var(--text-main) !important; } [data-theme="dark"] .eg-header-row {
[data-theme="dark"] .pf-table { background: var(--bg-panel); color: var(--text-main); } background: #111827 !important;
[data-theme="dark"] .pf-table thead th { background: #1c2128 !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; } border-color: #30363d !important;
[data-theme="dark"] .pf-table tbody td { background: var(--bg-panel); color: var(--text-main); border-color: rgba(48,54,61,.5); } color: #e6edf3 !important;
[data-theme="dark"] .pf-table tbody tr:hover td { background: rgba(255,255,255,.03); } }
[data-theme="dark"] .pf-table tfoot, [data-theme="dark"] .eg-row:nth-child(even) {
[data-theme="dark"] .pf-table tfoot td { background: #1c2128; color: var(--text-main); border-color: rgba(48,54,61,.5); } background: rgba(255, 255, 255, 0.02);
[data-theme="dark"] .pf-modal-content { background: var(--bg-panel) !important; border: 1px solid var(--border-light) !important; color: var(--text-main) !important; } }
[data-theme="dark"] .pf-modal-title { color: var(--text-main) !important; border-color: var(--border-light) !important; } [data-theme="dark"] .eg-row:hover {
[data-theme="dark"] .pf-input, background: rgba(255, 255, 255, 0.05);
[data-theme="dark"] .pf-filter-select, }
[data-theme="dark"] .hol-year-select { background: #1c2128 !important; border-color: var(--border-light) !important; color: var(--text-main) !important; }
[data-theme="dark"] .pf-input::placeholder { color: #484f58 !important; }
[data-theme="dark"] .pf-btn.btn-secondary { background: var(--bg-panel) !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; }
[data-theme="dark"] .pf-btn.btn-secondary:hover { background: rgba(255,255,255,.06) !important; color: var(--text-main) !important; }
/* ─── BUDGET MODULE ─────────────────────────────────────────────────────────── */ /* Éléments colorés spécifiques */
[data-theme="dark"] .budget-view { color: var(--text-main); } [data-theme="dark"] .savings-table .row-extres td,
[data-theme="dark"] .budget-view h2, [data-theme="dark"] .eg-extres-row {
[data-theme="dark"] .view-header h2 { color: var(--text-main) !important; } background: rgba(245, 158, 11, 0.08) !important;
[data-theme="dark"] .budget-tabs-container { background: var(--bg-panel) !important; border-color: var(--border-light) !important; } }
[data-theme="dark"] .tab-item { background: var(--bg-panel); color: var(--text-muted); border-color: var(--border-light); }
[data-theme="dark"] .tab-item:hover { background: rgba(255,255,255,.06); color: var(--text-main); }
[data-theme="dark"] .tab-item.active { background: rgba(59,130,246,.15) !important; color: var(--primary) !important; border-color: rgba(59,130,246,.3) !important; }
[data-theme="dark"] .owner-tabs { background: var(--bg-page); border-color: var(--border-light); }
[data-theme="dark"] .owner-tab { background: var(--bg-panel) !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; }
[data-theme="dark"] .owner-tab.active { background: var(--primary) !important; color: #fff !important; }
[data-theme="dark"] .month-header-container { color: var(--text-main); }
/* sticky col: uses var(--bg-panel) already but th.sticky-col uses hardcoded #f8fafc */
[data-theme="dark"] th.sticky-col { background: #1c2128 !important; color: var(--text-muted) !important; }
[data-theme="dark"] .sticky-col { background: var(--bg-panel) !important; color: var(--text-main) !important; }
[data-theme="dark"] .month-actions .btn-icon-small { background: #1c2128 !important; border-color: var(--border-light) !important; color: var(--text-main) !important; }
[data-theme="dark"] .month-actions .btn-icon-small:hover { background: rgba(255,255,255,.06) !important; }
[data-theme="dark"] .savings-table .row-extres td { background: rgba(245,158,11,.08) !important; }
[data-theme="dark"] .nens-title.theme-pol { background: rgba(30,58,138,.2) !important; color: #93c5fd !important; border-color: #3b82f6 !important; }
[data-theme="dark"] .nens-title.theme-pep { background: rgba(20,83,45,.2) !important; color: #86efac !important; border-color: #22c55e !important; }
[data-theme="dark"] .nens-table.theme-pol thead th { background: rgba(30,58,138,.15) !important; color: #93c5fd !important; }
[data-theme="dark"] .nens-table.theme-pep thead th { background: rgba(20,83,45,.15) !important; color: #86efac !important; }
/* Specific input/selects in budget */
[data-theme="dark"] .budget-view input,
[data-theme="dark"] .budget-view select,
[data-theme="dark"] .budget-view textarea { background: #1c2128 !important; border-color: var(--border-light) !important; color: var(--text-main) !important; }
/* sum selected highlight */
[data-theme="dark"] .sum-selected { background: rgba(59,130,246,.1) !important; }
/* ─── FAMILY CALENDAR MODULE ────────────────────────────────────────────────── */ [data-theme="dark"] .nens-title.theme-pol {
[data-theme="dark"] .pf-family-calendar { color: var(--text-main); } background: rgba(30, 58, 138, 0.2) !important;
[data-theme="dark"] .fc-header-row { background: var(--bg-panel); border-color: var(--border-light); } color: #93c5fd !important;
[data-theme="dark"] .fc-view-controls { background: var(--bg-page) !important; border-color: var(--border-light) !important; } border-color: #3b82f6 !important;
[data-theme="dark"] .fc-view-button { background: var(--bg-panel) !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; } }
[data-theme="dark"] .fc-view-button--active { background: rgba(59,130,246,.15) !important; color: var(--primary) !important; border-color: rgba(59,130,246,.3) !important; } [data-theme="dark"] .nens-table.theme-pol thead th {
[data-theme="dark"] .fc-month-nav-row { background: var(--bg-panel); border-color: var(--border-light); } background: rgba(30, 58, 138, 0.15) !important;
[data-theme="dark"] .fc-month-nav-row h3 { color: var(--text-main) !important; } color: #93c5fd !important;
[data-theme="dark"] .fc-nav-button { background: var(--bg-panel) !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; } }
[data-theme="dark"] .fc-nav-button:hover { background: rgba(255,255,255,.06) !important; }
/* Calendar table — lots of !important in source */
[data-theme="dark"] .fc-month-table { border-color: var(--border-light) !important; }
[data-theme="dark"] .fc-month-table th { background: #1c2128 !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; }
[data-theme="dark"] .fc-month-table td { background: var(--bg-panel) !important; border-color: rgba(48,54,61,.5) !important; }
[data-theme="dark"] .fc-month-table td:hover:not(.fc-day--other-month) { background: rgba(255,255,255,.04) !important; }
[data-theme="dark"] .fc-day--other-month { background: var(--bg-page) !important; color: var(--text-muted) !important; }
[data-theme="dark"] .fc-month-calendar-wrapper,
[data-theme="dark"] .fc-month-balances { background: var(--bg-panel) !important; border-color: var(--border-light) !important; }
[data-theme="dark"] .fc-minimal-balance-card { background: var(--bg-page) !important; border-color: var(--border-light) !important; color: var(--text-main) !important; }
[data-theme="dark"] .fc-min-chip { background: var(--bg-panel) !important; border-color: var(--border-light) !important; }
[data-theme="dark"] .fc-burn-alert { background: rgba(239,68,68,.1) !important; color: #f87171 !important; }
/* Planning table (vue planning) */
[data-theme="dark"] #planningTable-wrapper { background: var(--bg-panel) !important; border-color: var(--border-light) !important; }
[data-theme="dark"] #planningTable thead th { background: #1c2128 !important; color: var(--text-muted) !important; }
[data-theme="dark"] #planningTable tbody td { border-color: rgba(48,54,61,.5) !important; }
[data-theme="dark"] #planningTable tbody td.col-sticky-mois,
[data-theme="dark"] #planningTable tbody td.col-sticky-sem { background: var(--bg-panel) !important; }
[data-theme="dark"] .col-month { background: var(--bg-panel) !important; color: var(--text-main) !important; }
[data-theme="dark"] .fc-week-nav-controls { background: var(--bg-panel) !important; border-color: var(--border-light) !important; }
[data-theme="dark"] .pf-btn-icon-text { background: var(--bg-panel) !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; }
[data-theme="dark"] .pf-btn-icon-text:hover { background: rgba(255,255,255,.06) !important; }
/* ─── HOLIDAYS MODULE ───────────────────────────────────────────────────────── */ [data-theme="dark"] .nens-title.theme-pep {
[data-theme="dark"] .pf-holidays { color: var(--text-main); } background: rgba(20, 83, 45, 0.2) !important;
[data-theme="dark"] .pf-holidays__titlebar { background: var(--bg-panel) !important; border-color: var(--border-light) !important; } color: #86efac !important;
[data-theme="dark"] .hol-main-title { color: var(--text-main) !important; } border-color: #22c55e !important;
[data-theme="dark"] .hol-idea-card { background: var(--bg-panel) !important; border-color: var(--border-light) !important; color: var(--text-main) !important; box-shadow: var(--shadow) !important; } }
[data-theme="dark"] .hol-idea-card:hover { border-color: var(--primary) !important; } [data-theme="dark"] .nens-table.theme-pep thead th {
[data-theme="dark"] .hol-idea-card input, background: rgba(20, 83, 45, 0.15) !important;
[data-theme="dark"] .hol-idea-card textarea, color: #86efac !important;
[data-theme="dark"] .hol-idea-card select { background: #1c2128 !important; border-color: var(--border-light) !important; color: var(--text-main) !important; } }
[data-theme="dark"] .hol-filters-row { background: var(--bg-panel) !important; border-color: var(--border-light) !important; }
[data-theme="dark"] .hol-map-toggle { background: var(--bg-panel) !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; }
[data-theme="dark"] .hol-map-toggle:hover { background: rgba(255,255,255,.06) !important; }
[data-theme="dark"] .hol-add-btn { background: var(--primary) !important; color: #fff !important; }
/* ─── GIFT-LIST MODULE ──────────────────────────────────────────────────────── */ [data-theme="dark"] .sum-selected {
[data-theme="dark"] .pf-gift-list { color: var(--text-main); } background: rgba(59, 130, 246, 0.1) !important;
[data-theme="dark"] .pf-gift-list h1 { color: var(--text-main) !important; } }
[data-theme="dark"] .cl-titlebar { background: var(--bg-panel) !important; border-color: var(--border-light) !important; } [data-theme="dark"] .fc-burn-alert {
[data-theme="dark"] .cl-view-btn { background: var(--bg-panel) !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; } background: rgba(239, 68, 68, 0.1) !important;
[data-theme="dark"] .cl-view-btn.is-active { background: rgba(59,130,246,.15) !important; color: var(--primary) !important; border-color: rgba(59,130,246,.3) !important; } color: #f87171 !important;
[data-theme="dark"] .pf-filter-bar { background: var(--bg-page) !important; border-color: var(--border-light) !important; } }
[data-theme="dark"] .pf-filter-select { background: #1c2128 !important; color: var(--text-main) !important; border-color: var(--border-light) !important; } [data-theme="dark"] .month-actions .btn-icon-small {
[data-theme="dark"] .pf-child-section { background: var(--bg-panel) !important; border-color: var(--border-light) !important; } background: #1c2128 !important;
[data-theme="dark"] .pf-child-header { background: #1c2128 !important; border-color: var(--border-light) !important; } border-color: var(--border-light) !important;
[data-theme="dark"] .pf-child-header h3 { color: var(--text-main) !important; } color: var(--text-main) !important;
[data-theme="dark"] .pf-child-totals-bar { background: var(--bg-panel) !important; border-color: var(--border-light) !important; } }
[data-theme="dark"] .pf-summary-pill { background: var(--bg-page) !important; border-color: var(--border-light) !important; color: var(--text-main) !important; } [data-theme="dark"] .month-actions .btn-icon-small:hover {
[data-theme="dark"] .pf-gift-card-compact { background: var(--bg-panel) !important; border-color: var(--border-light) !important; } background: rgba(255, 255, 255, 0.06) !important;
[data-theme="dark"] .pf-gift-card-compact:hover { border-color: var(--primary) !important; } }
[data-theme="dark"] .pf-gift-title { color: var(--text-main) !important; }
[data-theme="dark"] .pf-gift-footer { border-color: var(--border-light) !important; }
+482
View File
@@ -445,6 +445,81 @@ body.pf-home .pf-mobile-close-btn {
font-size: 0.95rem; font-size: 0.95rem;
color: var(--text-main); color: var(--text-main);
} }
/* --- PALETTES DE COULEURS CUSTOM --- */
.pf-color-dropdown {
position: relative;
display: inline-block;
}
.pf-color-trigger {
width: 24px;
height: 24px;
border-radius: 50%;
border: 1px solid var(--border-light);
cursor: pointer;
padding: 0;
transition: transform 0.2s;
}
.pf-color-trigger:hover {
transform: scale(1.1);
}
.pf-color-menu {
display: none;
position: absolute;
right: 0;
top: 100%;
margin-top: 8px;
background: var(--bg-panel);
border: 1px solid var(--border-light);
border-radius: 12px;
padding: 12px;
box-shadow: var(--shadow-lg);
z-index: 100;
width: 180px;
gap: 8px;
flex-wrap: wrap;
}
.pf-color-menu.is-open {
display: flex;
animation: pfModalPop 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
.pf-color-swatch {
width: 28px;
height: 28px;
border-radius: 50%;
border: none;
cursor: pointer;
transition: transform 0.2s;
}
.pf-color-swatch:hover {
transform: scale(1.15);
}
/* Palette Radio (Pour le formulaire d'ajout) */
.pf-radio-swatch {
width: 24px;
height: 24px;
border-radius: 50%;
cursor: pointer;
display: inline-block;
transition:
transform 0.2s,
box-shadow 0.2s;
border: 2px solid transparent;
}
.pf-radio-swatch:hover {
transform: scale(1.1);
}
.pf-radio-swatch:has(input[type="radio"]:checked) {
transform: scale(1.1);
box-shadow:
0 0 0 2px var(--bg-panel),
0 0 0 4px var(--text-main);
}
[data-theme="dark"] .pf-radio-swatch:has(input[type="radio"]:checked) {
box-shadow:
0 0 0 2px var(--bg-panel),
0 0 0 4px var(--text-main);
}
/* Encadré pointillé (code d’invitation, etc.) */ /* Encadré pointillé (code d’invitation, etc.) */
.pf-dashed-panel { .pf-dashed-panel {
@@ -1326,3 +1401,410 @@ details[open].pf-accordion > .pf-accordion-summary::after {
90deg 90deg
); /* 90deg rend mieux sur une roue crantée qu'un 45deg ! */ ); /* 90deg rend mieux sur une roue crantée qu'un 45deg ! */
} }
/* ==========================================================================
UTILITAIRES GLOBAUX (Remplacement des styles en ligne)
========================================================================== */
.pf-mt-xs {
margin-top: 0.35rem;
}
.pf-mt-md {
margin-top: 1rem;
}
.pf-mt-2 {
margin-top: 2rem;
}
.pf-mb-md {
margin-bottom: 1rem;
}
.pf-m-0 {
margin: 0;
}
.pf-py-8-16 {
padding: 8px 16px;
}
.pf-input-sm {
max-width: 120px;
}
.pf-flex-0 {
flex: 0;
}
.pf-flex-1-5 {
flex: 1.5;
}
.pf-flex-2 {
flex: 2;
}
.pf-min-w-120 {
min-width: 120px;
}
.pf-label-sm {
font-size: 0.8rem;
margin-bottom: 4px;
}
.pf-flex-gap-12-center {
display: flex;
gap: 12px;
align-items: center;
}
/* Composants Spécifiques Paramètres */
.pf-empty-dashed {
padding: 10px;
text-align: center;
border: 1px dashed var(--border-light);
border-radius: 8px;
}
.pf-border-left-dynamic {
border-left: 4px solid var(--owner-color, var(--primary));
}
.pf-attached-form {
display: flex;
gap: 8px;
background: var(--bg-soft);
padding: 12px;
border-radius: 8px;
border: 1px dashed var(--border-light);
align-items: flex-end;
flex-wrap: wrap;
}
.pf-input-color-sm {
width: 28px !important;
height: 28px !important;
}
/* --- SÉLECTEUR DE COULEUR (PALETTE) --- */
.pf-color-palette {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 6px;
}
.pf-color-swatch {
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
transition:
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.2s;
padding: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.pf-color-swatch:hover {
transform: scale(1.15);
}
.pf-color-swatch.selected {
transform: scale(1.15);
border-color: var(--bg-panel);
box-shadow: 0 0 0 3px var(--text-main); /* Un anneau extérieur pour marquer la sélection */
}
/* Variante Dark Mode */
[data-theme="dark"] .pf-color-swatch.selected {
border-color: var(--bg-panel);
box-shadow: 0 0 0 3px var(--text-main);
}
/* --- PALETTE DE COULEURS CUSTOM (HouseHub) --- */
.pf-palette-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.pf-palette-sm {
gap: 6px;
}
.pf-color-swatch {
position: relative;
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
display: inline-block;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition:
transform 0.2s,
box-shadow 0.2s;
}
.pf-color-swatch:hover {
transform: scale(1.1);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
}
.pf-color-swatch-sm {
width: 24px;
height: 24px;
}
.pf-color-swatch input[type="radio"] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.pf-color-swatch:has(input[type="radio"]:checked) {
box-shadow:
0 0 0 2px var(--bg-panel),
0 0 0 4px var(--primary);
transform: scale(1.1);
}
[data-theme="dark"] .pf-color-swatch:has(input[type="radio"]:checked) {
box-shadow:
0 0 0 2px var(--bg-panel),
0 0 0 4px var(--primary);
}
/* --- PROFILS RATTACHÉS (Settings) --- */
.pf-attached-card {
display: flex;
flex-direction: column;
gap: 12px;
align-items: stretch;
padding: 14px;
background: var(--bg-panel);
border: 1px solid var(--border-light);
border-radius: 8px;
}
.pf-attached-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.pf-attached-name {
font-size: 1.1rem;
font-weight: 700;
}
.pf-attached-color-form {
display: flex;
align-items: center;
gap: 10px;
background: var(--bg-page);
padding: 8px 12px;
border-radius: 8px;
}
.pf-attached-add-form {
display: flex;
flex-direction: column;
gap: 12px;
background: var(--bg-soft);
padding: 16px;
border-radius: 8px;
border: 1px dashed var(--border-light);
}
.pf-attached-add-row {
display: flex;
gap: 12px;
align-items: flex-end;
flex-wrap: wrap;
}
.pf-attached-add-col {
margin-bottom: 0;
flex: 1;
min-width: 150px;
}
.pf-attached-btn-add {
align-self: flex-end;
margin-top: 4px;
}
/* --- UTILS --- */
.pf-mt-lg {
margin-top: 2rem;
}
.pf-mb-lg {
margin-bottom: 1.5rem;
}
.pf-mb-md {
margin-bottom: 1rem;
}
.pf-m-0 {
margin: 0;
}
.pf-label-sm {
font-size: 0.8rem;
margin-bottom: 4px;
}
.pf-empty-box {
padding: 10px;
text-align: center;
border: 1px dashed var(--border-light);
border-radius: 8px;
}
/* Couleurs Prédéfinies */
.pf-color-red {
background-color: #ef4444;
}
.pf-color-orange {
background-color: #f97316;
}
.pf-color-amber {
background-color: #f59e0b;
}
.pf-color-lime {
background-color: #84cc16;
}
.pf-color-green {
background-color: #22c55e;
}
.pf-color-teal {
background-color: #14b8a6;
}
.pf-color-cyan {
background-color: #06b6d4;
}
.pf-color-sky {
background-color: #0ea5e9;
}
.pf-color-blue {
background-color: #3b82f6;
}
.pf-color-indigo {
background-color: #6366f1;
}
.pf-color-violet {
background-color: #8b5cf6;
}
.pf-color-fuchsia {
background-color: #d946ef;
}
.pf-color-rose {
background-color: #f43f5e;
}
.pf-color-slate {
background-color: #64748b;
}
/* Popover Palette (Profils rattachés) */
.pf-palette-dropdown {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
}
.pf-palette-popover {
display: none;
position: absolute;
right: 0;
top: 120%;
background: var(--bg-panel);
border: 1px solid var(--border-light);
border-radius: 12px;
padding: 12px;
box-shadow: var(--shadow-lg);
width: 200px;
gap: 8px;
flex-wrap: wrap;
z-index: 100;
}
.pf-palette-dropdown.is-open .pf-palette-popover {
display: flex;
animation: pfModalPop 0.2s ease-out;
}
.pf-profile-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.pf-profile-actions {
display: flex;
align-items: center;
gap: 12px;
}
.pf-m-0 {
margin: 0;
}
.pf-empty-dashed {
padding: 10px;
text-align: center;
border: 1px dashed var(--border-light);
border-radius: 8px;
color: var(--text-muted);
}
/* --- UTILITAIRES & PROFILS RATTACHÉS (SETTINGS) --- */
.pf-mt-lg {
margin-top: 2rem;
}
.pf-mb-md {
margin-bottom: 1rem;
}
.pf-mb-lg {
margin-bottom: 1.5rem;
}
.pf-mb-0 {
margin-bottom: 0;
}
.pf-m-0 {
margin: 0;
}
.pf-profile-color-note {
display: block;
margin-top: 8px;
}
.pf-attached-card {
border-left: 4px solid var(--owner-color, #3b82f6);
}
.pf-attached-actions {
display: flex;
align-items: center;
gap: 12px;
}
.pf-color-picker-sm {
width: 24px !important;
height: 24px !important;
min-width: 24px !important;
}
.pf-btn-delete-sm {
font-size: 1.2rem;
padding: 4px;
}
.pf-empty-dashed {
padding: 10px;
text-align: center;
border: 1px dashed var(--border-light);
border-radius: 8px;
}
.pf-attached-form {
display: flex;
gap: 8px;
background: var(--bg-soft);
padding: 12px;
border-radius: 8px;
border: 1px dashed var(--border-light);
align-items: flex-end;
flex-wrap: wrap;
}
.pf-flex-2 {
flex: 2;
min-width: 120px;
}
.pf-flex-1-5 {
flex: 1.5;
min-width: 120px;
}
.pf-col-center {
display: flex;
flex-direction: column;
align-items: center;
}
.pf-label-sm {
font-size: 0.8rem;
margin-bottom: 4px;
}
.pf-btn-pad-sm {
padding: 8px 16px;
}
+105
View File
@@ -170,6 +170,109 @@ return [
'set_care_modes' => 'Modalitats de cura habituals', 'set_care_modes' => 'Modalitats de cura habituals',
'set_add_care_mode' => 'Afegir una modalitat', 'set_add_care_mode' => 'Afegir una modalitat',
'btn_save' => 'Desar', 'btn_save' => 'Desar',
'settings_profile_added' => 'Perfil de %s afegit amb èxit.',
'settings_err_name_required' => 'El nom és obligatori.',
'settings_attached_profiles_title' => 'Perfils adjunts',
'settings_attached_profiles_desc' => 'Membres de la llar (Nens, mainaderes, familiars) sense accés directe.',
'settings_no_attached_profiles' => 'Cap perfil adjunt.',
'label_firstname' => 'Nom',
'placeholder_firstname_ex' => 'Ex: Pol, Carole...',
'label_role' => 'Rol',
'role_child_icon' => '👶 Nen/a',
'role_helper_icon' => '💼 Ajudant',
'role_relative_icon' => '👵 Familiar',
'role_unknown' => 'Desconegut',
'label_color' => 'Color',
'confirm_delete_profile' => 'Voleu eliminar definitivament aquest perfil?',
'change_color' => 'Canviar el color',
'color_updated' => 'Color actualitzat!',
'set_success_color_profile' => 'Color del perfil actualitzat.',
'set_success_color_update' => 'Color actualitzat!',
'set_desc_profile_color' => 'Aquest color s\'utilitzarà per a les teves graelles de pressupost, calendaris i indicadors personals.',
'placeholder_apple_id' => 'nom@icloud.com',
'placeholder_apple_pwd' => 'xxxx-xxxx-xxxx-xxxx',
// --- SETTINGS ---
'error_csrf' => 'Sessió invàlida (CSRF). Si us plau, recarrega la pàgina.',
'set_err_min_module' => 'Has de mantenir almenys un mòdul actiu.',
'set_success_modules' => 'Mòduls actualitzats amb èxit.',
'set_success_lang' => 'Idioma actualitzat.',
'set_err_empty_name' => 'El nom no pot estar buit.',
'set_success_profile' => 'Perfil actualitzat amb èxit.',
'set_err_wrong_pwd' => 'Contrasenya actual incorrecta.',
'set_err_pwd_length' => 'La nova contrasenya ha de tenir almenys 6 caràcters.',
'set_err_pwd_match' => 'Les contrasenyes no coincideixen.',
'set_success_pwd' => 'Contrasenya canviada amb èxit.',
'set_success_family' => 'Nom de l\'espai actualitzat.',
'set_success_invite' => 'Nou codi d\'invitació generat.',
'set_err_bg_format' => 'Format no compatible (jpg, png, webp).',
'set_success_bg' => 'Imatge d\'inici actualitzada.',
'set_err_upload' => 'Error durant la càrrega.',
'set_success_bg_reset' => 'Imatge d\'inici restablerta.',
'set_err_db_unavailable' => 'Base de dades familiar no disponible.',
'set_err_ios_missing' => 'Si us plau, proporciona un ID d\'iCloud, contrasenya d\'aplicació i URL CalDAV.',
'set_success_ios' => 'Connexió de calendari iOS desada.',
'set_success_ios_auto' => ' URL del calendari detectada automàticament.',
'set_err_ios_save' => 'No es pot desar la connexió iOS: ',
'set_err_ios_none' => 'Cap connexió iOS configurada.',
'set_success_ios_test' => 'Connexió iCloud CalDAV vàlida (HTTP %s).',
'set_err_ios_test' => 'La prova de connexió ha fallat (HTTP %s). Revisa les teves credencials.',
'set_err_ios_test_fail' => 'Prova de connexió no disponible: ',
'set_success_ios_del' => 'Connexió iOS eliminada.',
'set_success_profile_add' => 'Perfil de %s afegit amb èxit.',
'set_err_name_required' => 'El nom és obligatori.',
'set_success_profile_del' => 'Perfil suprimit.',
'set_title_page' => '⚙️ Configuració',
'set_title_lang' => '🌐 Idioma',
'set_title_modules' => '🧩 Mòduls Actius',
'set_desc_modules' => 'Tria els mòduls visibles a la navegació (compartits amb tots els membres de l\'espai).',
'set_title_ios' => '📱 Integració de Calendari iOS (CalDAV)',
'set_desc_ios' => 'Configura aquí el teu calendari iCloud per sincronitzar els esdeveniments creats a HouseHub.',
'set_label_apple_id' => 'ID d\'Apple (correu iCloud)',
'set_label_apple_pwd' => 'Contrasenya d\'aplicació d\'Apple',
'set_label_apple_url' => 'URL del calendari CalDAV',
'set_btn_test_ios' => 'Prova la connexió',
'set_btn_disconnect_ios' => 'Desconnecta',
'set_title_bg' => '🖼️ Imatge d\'Inici',
'set_desc_bg' => 'Foto de fons mostrada a la pàgina d\'inici (compartida amb tots els membres).',
'set_badge_custom_bg' => 'Imatge personalitzada',
'set_label_new_bg' => 'Nova imatge (jpg, png, webp — màx 10 MB)',
'set_btn_reset_bg' => '🔄 Restableix la imatge per defecte',
'set_confirm_reset_bg' => 'Tornar a la imatge per defecte?',
'set_title_profile' => '👤 El meu Perfil',
'set_label_username' => 'Nom d\'usuari',
'set_label_display_name' => 'Nom / Àlies mostrat',
'set_label_profile_color' => 'Color temàtic del meu perfil',
'set_desc_profile_color' => 'Aquest color s\'utilitzarà per a les teves graelles de pressupost, calendaris i indicadors personals.',
'set_title_pwd' => '🔒 Canviar Contrasenya',
'set_label_cur_pwd' => 'Contrasenya actual',
'set_label_new_pwd' => 'Nova contrasenya',
'set_label_confirm_pwd' => 'Confirma',
'set_btn_edit' => 'Modifica',
'set_title_family' => '🏠 El meu Espai Familiar',
'set_label_family_name' => 'Nom de l\'espai',
'set_desc_invite' => 'Codi d\'invitació — comparteix-lo per convidar algú',
'set_title_copy' => 'Fes clic per copiar',
'set_msg_copied' => '✓ Copiat!',
'set_btn_new_code' => '🔄 Nou codi',
'set_confirm_new_code' => 'Regenerar codi? L\'antic serà invalidat.',
'set_desc_register_link' => 'Enllaç de registre:',
'set_title_members' => 'Membres',
'set_badge_admin' => 'Admin',
'set_title_attached' => '🧸 Perfils Adjunts',
'set_desc_attached' => 'Membres gestionats per la llar (Nens, Mainaderes, Familiars) sense accés directe.',
'role_unknown' => 'Desconegut',
'role_child' => '👶 Nen/a',
'role_helper' => '💼 Ajudant',
'role_relative' => '👵 Familiar',
'set_confirm_del_profile' => 'Eliminar definitivament aquest perfil?',
'set_empty_attached' => 'Cap perfil adjunt.',
'set_label_firstname' => 'Nom',
'set_label_role' => 'Rol',
'set_label_color' => 'Color',
'set_link_admin' => '→ Tauler d\'Administració',
'set_success_color' => 'Color actualitzat!',
'error_network' => 'Error de xarxa',
// ========================================== // ==========================================
// FAMILY CALENDAR // FAMILY CALENDAR
@@ -736,6 +839,8 @@ return [
'bud_prev_monthly_target' => 'Import objectiu / Mensualitat (€)', 'bud_prev_monthly_target' => 'Import objectiu / Mensualitat (€)',
'bud_prev_transfer_dest' => 'Destinació de la transferència', 'bud_prev_transfer_dest' => 'Destinació de la transferència',
'bud_prev_none' => 'Cap destinació (Despesa estàndard)', 'bud_prev_none' => 'Cap destinació (Despesa estàndard)',
'bud_sav_new_line' => 'New line',
'bud_sav_prompt_new_line' => 'Name of the new savings line:',
// --- BUDGET : EPARGNE --- // --- BUDGET : EPARGNE ---
'bud_sav_add_month' => 'Introduir un mes', 'bud_sav_add_month' => 'Introduir un mes',
+105
View File
@@ -75,6 +75,88 @@ return [
'login_subtitle' => 'Please sign in to continue', 'login_subtitle' => 'Please sign in to continue',
'login_remember_me' => 'Remember me', 'login_remember_me' => 'Remember me',
// --- SETTINGS ---
'error_csrf' => 'Invalid session (CSRF). Please reload the page.',
'set_err_min_module' => 'You must keep at least one active module.',
'set_success_modules' => 'Modules successfully updated.',
'set_success_lang' => 'Language updated.',
'set_err_empty_name' => 'First name cannot be empty.',
'set_success_profile' => 'Profile updated successfully.',
'set_err_wrong_pwd' => 'Incorrect current password.',
'set_err_pwd_length' => 'New password must be at least 6 characters long.',
'set_err_pwd_match' => 'Passwords do not match.',
'set_success_pwd' => 'Password successfully changed.',
'set_success_family' => 'Space name updated.',
'set_success_invite' => 'New invitation code generated.',
'set_err_bg_format' => 'Unsupported format (jpg, png, webp).',
'set_success_bg' => 'Home image updated.',
'set_err_upload' => 'Error during upload.',
'set_success_bg_reset' => 'Home image reset.',
'set_err_db_unavailable' => 'Family database unavailable.',
'set_err_ios_missing' => 'Please provide an iCloud ID, app password, and CalDAV URL.',
'set_success_ios' => 'iOS calendar connection saved.',
'set_success_ios_auto' => ' Calendar URL automatically detected.',
'set_err_ios_save' => 'Cannot save iOS connection: ',
'set_err_ios_none' => 'No iOS connection configured.',
'set_success_ios_test' => 'Valid iCloud CalDAV connection (HTTP %s).',
'set_err_ios_test' => 'Connection test failed (HTTP %s). Check your credentials.',
'set_err_ios_test_fail' => 'Connection test unavailable: ',
'set_success_ios_del' => 'iOS connection removed.',
'set_success_profile_add' => 'Profile for %s successfully added.',
'set_err_name_required' => 'First name is required.',
'set_success_profile_del' => 'Profile deleted.',
'set_title_page' => '⚙️ Settings',
'set_title_lang' => '🌐 Language',
'set_title_modules' => '🧩 Active Modules',
'set_desc_modules' => 'Choose the modules visible in the navigation (shared with all space members).',
'set_title_ios' => '📱 iOS Calendar Integration (CalDAV)',
'set_desc_ios' => 'Configure your iCloud calendar here to sync events created in HouseHub.',
'set_label_apple_id' => 'Apple ID (iCloud email)',
'set_label_apple_pwd' => 'Apple app password',
'set_label_apple_url' => 'CalDAV calendar URL',
'set_btn_test_ios' => 'Test connection',
'set_btn_disconnect_ios' => 'Disconnect',
'set_title_bg' => '🖼️ Home Image',
'set_desc_bg' => 'Background photo displayed on the homepage (shared with all members).',
'set_badge_custom_bg' => 'Custom image',
'set_label_new_bg' => 'New image (jpg, png, webp — max 10 MB)',
'set_btn_reset_bg' => '🔄 Reset to default image',
'set_confirm_reset_bg' => 'Revert to default image?',
'set_title_profile' => '👤 My Profile',
'set_label_username' => 'Username',
'set_label_display_name' => 'First name / Display name',
'set_label_profile_color' => 'My profile theme color',
'set_desc_profile_color' => 'This color will be used for your budget grids, calendars, and personal indicators.',
'set_title_pwd' => '🔒 Change Password',
'set_label_cur_pwd' => 'Current password',
'set_label_new_pwd' => 'New password',
'set_label_confirm_pwd' => 'Confirm',
'set_btn_edit' => 'Edit',
'set_title_family' => '🏠 My Family Space',
'set_label_family_name' => 'Space name',
'set_desc_invite' => 'Invitation code — share it to invite someone',
'set_title_copy' => 'Click to copy',
'set_msg_copied' => '✓ Copied!',
'set_btn_new_code' => '🔄 New code',
'set_confirm_new_code' => 'Regenerate code? The old one will be invalidated.',
'set_desc_register_link' => 'Registration link:',
'set_title_members' => 'Members',
'set_badge_admin' => 'Admin',
'set_title_attached' => '🧸 Attached Profiles',
'set_desc_attached' => 'Members managed by the household (Children, Nannies, Relatives) without direct access.',
'role_unknown' => 'Unknown',
'role_child' => '👶 Child',
'role_helper' => '💼 Helper',
'role_relative' => '👵 Relative',
'set_confirm_del_profile' => 'Permanently delete this profile?',
'set_empty_attached' => 'No attached profiles.',
'set_label_firstname' => 'First name',
'set_label_role' => 'Role',
'set_label_color' => 'Color',
'set_link_admin' => '→ Administration Panel',
'set_success_color' => 'Color updated!',
'error_network' => 'Network error',
// ========================================== // ==========================================
// HOME / INDEX // HOME / INDEX
// ========================================== // ==========================================
@@ -171,6 +253,27 @@ return [
'set_care_modes' => 'Common childcare arrangements', 'set_care_modes' => 'Common childcare arrangements',
'set_add_care_mode' => 'Ajouter un modeAdd a mode', 'set_add_care_mode' => 'Ajouter un modeAdd a mode',
'btn_save' => 'Save', 'btn_save' => 'Save',
'settings_profile_added' => 'Profile of %s added successfully.',
'settings_err_name_required' => 'First name is required.',
'settings_attached_profiles_title' => 'Attached Profiles',
'settings_attached_profiles_desc' => 'Household members (Children, Nannies, Relatives) without direct access.',
'settings_no_attached_profiles' => 'No attached profiles.',
'label_firstname' => 'First Name',
'placeholder_firstname_ex' => 'e.g., Pol, Carole...',
'label_role' => 'Role',
'role_child_icon' => '👶 Child',
'role_helper_icon' => '💼 Helper',
'role_relative_icon' => '👵 Relative',
'role_unknown' => 'Unknown',
'label_color' => 'Color',
'confirm_delete_profile' => 'Permanently delete this profile?',
'change_color' => 'Change color',
'color_updated' => 'Color updated!',
'set_success_color_profile' => 'Profile color updated.',
'set_success_color_update' => 'Color updated!',
'set_desc_profile_color' => 'This color will be used for your budget grids, calendars, and personal indicators.',
'placeholder_apple_id' => 'name@icloud.com',
'placeholder_apple_pwd' => 'xxxx-xxxx-xxxx-xxxx',
// ========================================== // ==========================================
// FAMILY CALENDAR // FAMILY CALENDAR
@@ -768,6 +871,8 @@ return [
'bud_sav_sum_mode_title' => 'Toggle calculator', 'bud_sav_sum_mode_title' => 'Toggle calculator',
'bud_sav_selection' => 'Selection:', 'bud_sav_selection' => 'Selection:',
'bud_sav_ph_name' => 'Account name', 'bud_sav_ph_name' => 'Account name',
'bud_sav_new_line' => 'New line',
'bud_sav_prompt_new_line' => 'Name of the new savings line:',
// JS & Errors // JS & Errors
'bud_sav_confirm_delete_month' => 'Are you sure you want to delete all data of %m for %o?', 'bud_sav_confirm_delete_month' => 'Are you sure you want to delete all data of %m for %o?',
+105
View File
@@ -172,6 +172,109 @@ return [
'set_care_modes' => 'Modes de garde usuels', 'set_care_modes' => 'Modes de garde usuels',
'set_add_care_mode' => 'Ajouter un mode', 'set_add_care_mode' => 'Ajouter un mode',
'btn_save_config' => 'Enregistrer', 'btn_save_config' => 'Enregistrer',
'settings_profile_added' => 'Profil de %s ajouté avec succès.',
'settings_err_name_required' => 'Le prénom est obligatoire.',
'settings_attached_profiles_title' => 'Profils rattachés',
'settings_attached_profiles_desc' => 'Membres gérés par le foyer (Enfants, Nounous, Proches) n\'ayant pas d\'accès direct.',
'settings_no_attached_profiles' => 'Aucun profil rattaché.',
'label_firstname' => 'Prénom',
'placeholder_firstname_ex' => 'Ex: Pol, Carole...',
'label_role' => 'Rôle',
'role_child_icon' => '👶 Enfant',
'role_helper_icon' => '💼 Intervenant',
'role_relative_icon' => '👵 Proche',
'role_unknown' => 'Inconnu',
'label_color' => 'Couleur',
'confirm_delete_profile' => 'Supprimer définitivement ce profil ?',
'change_color' => 'Changer la couleur',
'color_updated' => 'Couleur mise à jour !',
'set_success_color_profile' => 'Couleur du profil mise à jour.',
'set_success_color_update' => 'Couleur mise à jour !',
'set_desc_profile_color' => 'Cette couleur sera utilisée pour vos grilles de budget, calendriers et indicateurs personnels.',
'placeholder_apple_id' => 'nom@icloud.com',
'placeholder_apple_pwd' => 'xxxx-xxxx-xxxx-xxxx',
// Fichier : includes/lang/fr.php
'error_csrf' => 'Session invalide (CSRF). Rechargez la page.',
'set_err_min_module' => 'Vous devez garder au moins un module actif.',
'set_success_modules' => 'Modules mis à jour avec succès.',
'set_success_lang' => 'Langue mise à jour.',
'set_err_empty_name' => 'Le prénom ne peut pas être vide.',
'set_success_profile' => 'Profil mis à jour avec succès.',
'set_err_wrong_pwd' => 'Mot de passe actuel incorrect.',
'set_err_pwd_length' => 'Le nouveau mot de passe doit faire au moins 6 caractères.',
'set_err_pwd_match' => 'Les mots de passe ne correspondent pas.',
'set_success_pwd' => 'Mot de passe modifié avec succès.',
'set_success_family' => 'Nom de l\'espace mis à jour.',
'set_success_invite' => 'Nouveau code d\'invitation généré.',
'set_err_bg_format' => 'Format non supporté (jpg, png, webp).',
'set_success_bg' => 'Image d\'accueil mise à jour.',
'set_err_upload' => 'Erreur lors de l\'upload.',
'set_success_bg_reset' => 'Image d\'accueil réinitialisée.',
'set_err_db_unavailable' => 'Base famille indisponible.',
'set_err_ios_missing' => 'Merci de renseigner identifiant iCloud, mot de passe d\'app et URL CalDAV.',
'set_success_ios' => 'Connexion calendrier iOS enregistrée.',
'set_success_ios_auto' => ' URL du calendrier détectée automatiquement.',
'set_err_ios_save' => 'Impossible d\'enregistrer la connexion iOS : ',
'set_err_ios_none' => 'Aucune connexion iOS configurée.',
'set_success_ios_test' => 'Connexion iCloud CalDAV valide (HTTP %s).',
'set_err_ios_test' => 'Test connexion échoué (HTTP %s). Vérifiez vos identifiants.',
'set_err_ios_test_fail' => 'Test connexion impossible : ',
'set_success_ios_del' => 'Connexion iOS supprimée.',
'set_success_profile_add' => 'Profil de %s ajouté avec succès.',
'set_err_name_required' => 'Le prénom est obligatoire.',
'set_success_profile_del' => 'Profil supprimé.',
'set_title_page' => '⚙️ Paramètres',
'set_title_lang' => '🌐 Langue / Language',
'set_title_modules' => '🧩 Modules actifs',
'set_desc_modules' => 'Choisissez les modules visibles dans la navigation (partagé avec tous les membres de l\'espace).',
'set_title_ios' => '📱 Intégration Calendrier iOS (CalDAV)',
'set_desc_ios' => 'Configurez ici votre calendrier iCloud pour synchroniser les événements créés dans HouseHub.',
'set_label_apple_id' => 'Identifiant Apple (email iCloud)',
'set_label_apple_pwd' => 'Mot de passe d\'app Apple',
'set_label_apple_url' => 'URL du calendrier CalDAV',
'set_btn_test_ios' => 'Tester la connexion',
'set_btn_disconnect_ios' => 'Déconnecter',
'set_title_bg' => '🖼️ Image d\'accueil',
'set_desc_bg' => 'Photo de fond affichée sur la page d\'accueil (partagée avec tous les membres).',
'set_badge_custom_bg' => 'Image personnalisée',
'set_label_new_bg' => 'Nouvelle image (jpg, png, webp — max 10 Mo)',
'set_btn_reset_bg' => '🔄 Remettre l\'image par défaut',
'set_confirm_reset_bg' => 'Revenir à l\'image par défaut ?',
'set_title_profile' => '👤 Mon profil',
'set_label_username' => 'Nom d\'utilisateur',
'set_label_display_name' => 'Prénom / Pseudo affiché',
'set_label_profile_color' => 'Couleur thématique de mon profil',
'set_desc_profile_color' => 'Cette couleur sera utilisée pour vos grilles de budget, calendriers et indicateurs personnels.',
'set_title_pwd' => '🔒 Changer le mot de passe',
'set_label_cur_pwd' => 'Mot de passe actuel',
'set_label_new_pwd' => 'Nouveau mot de passe',
'set_label_confirm_pwd' => 'Confirmer',
'set_btn_edit' => 'Modifier',
'set_title_family' => '🏠 Mon espace familial',
'set_label_family_name' => 'Nom de l\'espace',
'set_desc_invite' => 'Code d\'invitation — partagez-le pour inviter quelqu\'un',
'set_title_copy' => 'Cliquer pour copier',
'set_msg_copied' => '✓ Copié !',
'set_btn_new_code' => '🔄 Nouveau code',
'set_confirm_new_code' => 'Regénérer le code ? L\'ancien sera invalidé.',
'set_desc_register_link' => 'Lien d\'inscription :',
'set_title_members' => 'Membres',
'set_badge_admin' => 'Admin',
'set_title_attached' => '🧸 Profils rattachés',
'set_desc_attached' => 'Membres gérés par le foyer (Enfants, Nounous, Proches) n\'ayant pas d\'accès direct.',
'role_unknown' => 'Inconnu',
'role_child' => '👶 Enfant',
'role_helper' => '💼 Intervenant',
'role_relative' => '👵 Proche',
'set_confirm_del_profile' => 'Supprimer définitivement ce profil ?',
'set_empty_attached' => 'Aucun profil rattaché.',
'set_label_firstname' => 'Prénom',
'set_label_role' => 'Rôle',
'set_label_color' => 'Couleur',
'set_link_admin' => '→ Panneau d\'administration',
'set_success_color' => 'Couleur mise à jour !',
'error_network' => 'Erreur réseau',
// ========================================== // ==========================================
// FAMILY CALENDAR // FAMILY CALENDAR
@@ -789,6 +892,8 @@ return [
'bud_sav_sum_mode_title' => 'Activer/Désactiver la calculatrice', 'bud_sav_sum_mode_title' => 'Activer/Désactiver la calculatrice',
'bud_sav_selection' => 'Sélection :', 'bud_sav_selection' => 'Sélection :',
'bud_sav_ph_name' => 'Nom du compte', 'bud_sav_ph_name' => 'Nom du compte',
'bud_sav_new_line' => 'Nouvelle ligne',
'bud_sav_prompt_new_line' => 'Nom de la nouvelle ligne d\'épargne :',
// --- BUDGET : PREVISION --- // --- BUDGET : PREVISION ---
+334
View File
@@ -0,0 +1,334 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mockup Épargne HouseHub</title>
<style>
:root {
--primary: #3b82f6;
--bg-page: #f8fafc;
--bg-panel: #ffffff;
--text-main: #0f172a;
--text-muted: #64748b;
--border-light: #e2e8f0;
--radius: 12px;
--shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--focus-ring: rgba(37, 99, 235, 0.1);
}
body {
font-family: system-ui, sans-serif;
background: var(--bg-page);
color: var(--text-main);
padding: 40px;
}
.controls {
margin-bottom: 20px;
display: flex;
gap: 10px;
}
button {
padding: 8px 16px;
cursor: pointer;
border-radius: 8px;
border: 1px solid var(--primary);
background: white;
color: var(--primary);
font-weight: bold;
}
button.active {
background: var(--primary);
color: white;
}
/* BASE GRID (Ta structure actuelle simplifiée) */
.epargne-grid-table {
display: flex;
flex-direction: column;
background: var(--bg-panel);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
border: 1px solid var(--border-light);
width: 100%;
max-width: 900px;
}
.eg-row,
.eg-header-row {
display: grid;
grid-template-columns: 180px repeat(3, minmax(115px, 1fr));
transition: 0.2s;
}
.eg-cell {
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9rem;
}
.eg-sticky {
position: sticky;
left: 0;
justify-content: flex-start;
z-index: 10;
font-weight: 600;
}
.eg-input {
width: 70px;
text-align: center;
border-radius: 6px;
border: 1px solid transparent;
background: transparent;
font-family: inherit;
font-size: 0.9rem;
padding: 4px;
transition: 0.2s;
}
.eg-input:focus {
background: var(--bg-panel);
border-color: var(--primary);
outline: none;
box-shadow: 0 0 0 3px var(--focus-ring);
}
/* ========================================= */
/* STYLE A : COMPACT & AÉRIEN */
/* ========================================= */
.style-a .eg-row,
.style-a .eg-header-row {
border-bottom: 1px solid var(--border-light);
}
.style-a .eg-cell {
padding: 6px 8px;
}
.style-a .eg-header-row {
background: #f8fafc;
font-size: 0.75rem;
text-transform: uppercase;
color: var(--text-muted);
}
.style-a .eg-header-row .eg-sticky {
background: #f8fafc;
}
.style-a .eg-row .eg-sticky {
background: var(--bg-panel);
border-right: 1px solid var(--border-light);
}
.style-a .eg-row:hover {
background: #f1f5f9;
}
.style-a .eg-row:hover .eg-sticky {
background: #f1f5f9;
}
.style-a .eg-input:hover {
background: #e2e8f0;
}
/* ========================================= */
/* STYLE B : ZÉBRÉ DOUX */
/* ========================================= */
.style-b .eg-cell {
padding: 8px 10px;
border-right: 1px dashed #e2e8f0;
}
.style-b .eg-header-row {
background: #1e293b;
color: white;
font-size: 0.8rem;
text-transform: uppercase;
}
.style-b .eg-header-row .eg-sticky {
background: #1e293b;
border-right: 2px solid #334155;
}
.style-b .eg-row:nth-child(even) {
background: #f8fafc;
}
.style-b .eg-row:nth-child(even) .eg-sticky {
background: #f8fafc;
}
.style-b .eg-row .eg-sticky {
background: var(--bg-panel);
border-right: 2px solid var(--border-light);
}
.style-b .eg-input {
background: #f1f5f9;
border: 1px solid #e2e8f0;
}
.style-b .eg-input:hover {
border-color: #cbd5e1;
}
/* ========================================= */
/* STYLE C : SAAS DASHBOARD */
/* ========================================= */
.style-c .eg-row,
.style-c .eg-header-row {
border-bottom: 1px solid #f1f5f9;
}
.style-c .eg-cell {
padding: 10px 12px;
}
.style-c .eg-header-row {
background: #ffffff;
color: var(--text-muted);
font-size: 0.8rem;
border-bottom: 2px solid var(--border-light);
}
.style-c .eg-header-row .eg-sticky {
background: #ffffff;
}
.style-c .eg-row .eg-sticky {
background: var(--bg-panel);
position: relative;
}
/* Barre de surbrillance au hover */
.style-c .eg-row:hover {
background: #eff6ff;
}
.style-c .eg-row:hover .eg-sticky {
background: #eff6ff;
}
.style-c .eg-row:hover .eg-sticky::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: var(--primary);
border-radius: 0 4px 4px 0;
}
.style-c .eg-input {
background: transparent;
border-bottom: 1px dashed #cbd5e1;
border-radius: 0;
padding: 2px 4px;
}
.style-c .eg-input:focus {
border-bottom: 1px solid var(--primary);
border-radius: 4px;
}
</style>
</head>
<body>
<div class="controls">
<button onclick="setStyle('style-a')" id="btn-a" class="active">
Style A : Compact
</button>
<button onclick="setStyle('style-b')" id="btn-b">Style B : Zébré</button>
<button onclick="setStyle('style-c')" id="btn-c">Style C : SaaS</button>
</div>
<!-- Grille Mockup -->
<div class="epargne-grid-table style-a" id="grid-container">
<div class="eg-header-row">
<div class="eg-cell eg-sticky">Poste / Mois</div>
<div class="eg-cell">Août 2026</div>
<div class="eg-cell">Juillet 2026</div>
<div class="eg-cell">Juin 2026</div>
</div>
<div class="eg-row" style="font-weight: bold; background: #f0f9ff">
<div
class="eg-cell eg-sticky"
style="background: #f0f9ff; color: #0369a1"
>
TOTAL BANQUE
</div>
<div class="eg-cell">
<input
type="number"
class="eg-input"
style="color: #0369a1; font-weight: bold"
value="7947"
/>
€
</div>
<div class="eg-cell">
<input
type="number"
class="eg-input"
style="color: #0369a1; font-weight: bold"
value="6807"
/>
€
</div>
<div class="eg-cell">
<input
type="number"
class="eg-input"
style="color: #0369a1; font-weight: bold"
value="7846"
/>
€
</div>
</div>
<div class="eg-row">
<div class="eg-cell eg-sticky">Ecole P&P</div>
<div class="eg-cell">
<input type="number" class="eg-input" value="480" />
</div>
<div class="eg-cell">
<input type="number" class="eg-input" value="480" />
</div>
<div class="eg-cell">
<input type="number" class="eg-input" value="480" />
</div>
</div>
<div class="eg-row">
<div class="eg-cell eg-sticky">Extra casa</div>
<div class="eg-cell">
<input type="number" class="eg-input" value="635" />
</div>
<div class="eg-cell">
<input type="number" class="eg-input" value="635" />
</div>
<div class="eg-cell">
<input type="number" class="eg-input" value="635" />
</div>
</div>
<div class="eg-row">
<div class="eg-cell eg-sticky">Vacances</div>
<div class="eg-cell">
<input type="number" class="eg-input" value="1002" />
</div>
<div class="eg-cell">
<input type="number" class="eg-input" value="1002" />
</div>
<div class="eg-cell">
<input type="number" class="eg-input" value="2002" />
</div>
</div>
<div class="eg-row" style="background: #fffbeb">
<div
class="eg-cell eg-sticky"
style="background: #fffbeb; color: #b45309"
>
NON ALLOUÉ
</div>
<div class="eg-cell" style="color: #10b981; font-weight: bold">
1230 €
</div>
<div class="eg-cell" style="color: #10b981; font-weight: bold">
660 €
</div>
<div class="eg-cell" style="color: #10b981; font-weight: bold">
699 €
</div>
</div>
</div>
<script>
function setStyle(styleClass) {
const grid = document.getElementById("grid-container");
grid.className = "epargne-grid-table " + styleClass;
document
.querySelectorAll(".controls button")
.forEach((btn) => btn.classList.remove("active"));
document
.getElementById("btn-" + styleClass.split("-")[1])
.classList.add("active");
}
</script>
</body>
</html>
+920 -69
View File
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -20,14 +20,14 @@ if ($action === 'update_single_entry') {
$cat = $_POST['category'] ?? ''; $cat = $_POST['category'] ?? '';
$owner = $_POST['owner'] ?? ''; $owner = $_POST['owner'] ?? '';
$amount = (float)($_POST['amount'] ?? 0); $amount = (float)($_POST['amount'] ?? 0);
$forceInsert = !empty($_POST['force_insert']);
try { try {
if ($amount == 0 && $cat !== 'TOTAL_BANQUE') { // On supprime la ligne si elle est à 0, SAUF si on demande explicitement son initialisation (force_insert)
// Si on met à 0 une ligne (autre que le total), on supprime l'entrée pour garder la base propre if ($amount == 0 && $cat !== 'TOTAL_BANQUE' && !$forceInsert) {
$stmt = $pdo->prepare("DELETE FROM pf_savings WHERE month_date=? AND owner=? AND category=?"); $stmt = $pdo->prepare("DELETE FROM pf_savings WHERE month_date=? AND owner=? AND category=?");
$stmt->execute([$month, $owner, $cat]); $stmt->execute([$month, $owner, $cat]);
} else { } else {
// Sinon on insère ou on met à jour
$stmt = $pdo->prepare("INSERT INTO pf_savings (month_date, owner, category, amount) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE amount = VALUES(amount)"); $stmt = $pdo->prepare("INSERT INTO pf_savings (month_date, owner, category, amount) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE amount = VALUES(amount)");
$stmt->execute([$month, $owner, $cat, $amount]); $stmt->execute([$month, $owner, $cat, $amount]);
} }
+140 -138
View File
@@ -2,24 +2,24 @@
// modules/budget/views/epargne.php // modules/budget/views/epargne.php
// --- 🧠 CONFIGURATION AGNOSTIQUE DES MEMBRES (MULTI-TENANT) --- // --- 🧠 CONFIGURATION AGNOSTIQUE DES MEMBRES (MULTI-TENANT) ---
$stmtPeople = $pdo->query("SELECT name, role FROM pf_people ORDER BY id ASC"); $stmtPeople = $pdo->query("SELECT name, role, color FROM pf_people ORDER BY id ASC");
$familyParents = []; $familyParents = [];
$familyKids = []; $familyKids = [];
$personColors = [];
while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) { while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) {
$role = strtolower(trim($row['role'] ?? '')); $role = strtolower(trim($row['role'] ?? ''));
$personColors[$row['name']] = $row['color'] ?? '#3b82f6';
if ($role === 'parent') { if ($role === 'parent') {
$familyParents[] = $row['name']; $familyParents[] = $row['name'];
} elseif ($role === 'nounou' || $role === 'helper') { } elseif ($role === 'nounou' || $role === 'helper') {
// On ignore la nounou dans l'épargne
continue; continue;
} else { } else {
$familyKids[] = $row['name']; $familyKids[] = $row['name'];
} }
} }
// Sécurité anti-page blanche si la BDD est mal configurée
if (empty($familyParents)) $familyParents = ['Parent 1', 'Parent 2']; if (empty($familyParents)) $familyParents = ['Parent 1', 'Parent 2'];
$requestedOwner = $_GET['owner'] ?? (!empty($familyKids) ? 'KIDS' : ($familyParents[0] ?? 'KIDS')); $requestedOwner = $_GET['owner'] ?? (!empty($familyKids) ? 'KIDS' : ($familyParents[0] ?? 'KIDS'));
@@ -46,10 +46,9 @@ function getMonthName($dateString) {
<div class="budget-view"> <div class="budget-view">
<div class="view-header"> <div class="view-header">
<div class="owner-tabs"> <div class="owner-tabs">
<?php if (!empty($familyKids)): ?> <?php if (!empty($familyKids)): ?>
<a href="?tab=epargne&owner=KIDS" class="owner-tab <?= $requestedOwner === 'KIDS' ? 'active' : '' ?>"> <a href="?tab=epargne&owner=KIDS" class="owner-tab <?= $requestedOwner === 'KIDS' ? 'active' : '' ?>">
<?= tr('budget_tab_kids') ?? 'Enfants 👶' ?> <?= tr('budget_tab_kids') ?>
</a> </a>
<?php endif; ?> <?php endif; ?>
@@ -81,181 +80,174 @@ function getMonthName($dateString) {
$months = array_slice($months, 0, 7); $months = array_slice($months, 0, 7);
sort($allCategories); sort($allCategories);
$ownerTextClass = 'txt-global';
// 🔄 CORRECTION ICI : Création d'un nom "safe" sans espace pour les classes CSS
$safeOwnerCls = htmlspecialchars(str_replace(' ', '_', $currentOwner), ENT_QUOTES); $safeOwnerCls = htmlspecialchars(str_replace(' ', '_', $currentOwner), ENT_QUOTES);
$ownerColor = $personColors[$currentOwner] ?? '#1e293b';
?> ?>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'KIDS' && $index > 0) ? '40px' : '0' ?>;"> <div class="epargne-header-bar <?= ($requestedOwner === 'KIDS' && $index > 0) ? 'pf-mt-40' : '' ?>">
<div style="flex-grow: 1;"> <div class="pf-flex-1">
<?php if ($requestedOwner === 'KIDS'): <?php if ($requestedOwner === 'KIDS'): ?>
$themeClass = 'theme-' . strtolower($safeOwnerCls); <h3 class="nens-title" style="--owner-color: <?= htmlspecialchars($ownerColor) ?>;">
?>
<h3 class="nens-title <?= $themeClass ?>" style="margin:0; font-size:1.2rem;">
<?= htmlspecialchars($currentOwner) ?> <?= htmlspecialchars($currentOwner) ?>
</h3> </h3>
<?php endif; ?> <?php endif; ?>
</div> </div>
<div style="display: flex; gap: 10px; align-items: center;"> <div class="pf-flex-gap-12-center">
<?php if (!empty($months)): ?> <?php if (!empty($months)): ?>
<button onclick="duplicateLastMonth('<?= $months[0] ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')" class="pf-btn btn-secondary"> <button onclick="duplicateLastMonth('<?= $months[0] ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')" class="pf-btn btn-secondary">
🔁 <?= tr('bud_sav_add_one_month') ?> 🔁 <?= tr('bud_sav_add_one_month') ?>
</button> </button>
<?php endif; ?> <button onclick="promptNewSavingsLine('<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', '<?= $months[0] ?>')" class="pf-btn">
+ <?= tr('bud_sav_new_line') ?>
</button>
<?php else: ?>
<button onclick="openCustomSavingsModal('<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')" class="pf-btn"> <button onclick="openCustomSavingsModal('<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')" class="pf-btn">
+ <?= tr('bud_sav_add_month') ?> + <?= tr('bud_sav_add_month') ?>
</button> </button>
<?php endif; ?>
</div> </div>
</div> </div>
<div class="table-responsive" style="background:white; border-radius:16px; box-shadow:var(--shadow-sm); border:1px solid #e2e8f0;"> <div class="table-responsive pf-card-epargne">
<?php if (empty($months)): ?> <?php if (empty($months)): ?>
<div style="padding: 30px; text-align: center; color: #64748b;"> <div class="pf-empty-dashed">
<p><?= sprintf(tr('bud_sav_no_data'), htmlspecialchars($currentOwner)) ?></p> <p><?= sprintf(tr('bud_sav_no_data'), htmlspecialchars($currentOwner)) ?></p>
</div> </div>
<?php else: ?> <?php else: ?>
<table class="pf-table savings-table nens-table theme-<?= strtolower($safeOwnerCls) ?>" style="margin-top:0; box-shadow:none; border-radius:16px;"> <div class="epargne-grid-table" style="--cols: <?= count($months) ?>; --owner-color: <?= htmlspecialchars($ownerColor) ?>;">
<thead>
<tr> <div class="eg-header-row">
<th class="sticky-col" style="background:#f8fafc;"><?= tr('bud_sav_post_month') ?></th> <div class="eg-cell eg-sticky"><?= tr('bud_sav_post_month') ?></div>
<?php foreach ($months as $month): ?> <?php foreach ($months as $month): ?>
<th> <div class="eg-cell">
<div class="month-header-container" style="display: flex; flex-direction: column; align-items: center; gap: 6px;"> <div class="month-header-container">
<div style="display:flex; flex-direction:column; text-align:center;"> <div class="month-header-title">
<span class="month-name" style="text-transform:capitalize;"><?= getMonthName($month) ?></span> <span class="month-name text-inherit"><?= getMonthName($month) ?></span>
<?php <?php
if (isset($cycleConfigs[$month]) && !empty($cycleConfigs[$month]['start_date'])) { if (isset($cycleConfigs[$month]) && !empty($cycleConfigs[$month]['start_date'])) {
$cStart = date('d/m', strtotime($cycleConfigs[$month]['start_date'])); $cStart = date('d/m', strtotime($cycleConfigs[$month]['start_date']));
echo "<span style='font-size:0.75rem; font-weight:normal; color:#64748b;'>" . sprintf(tr('bud_sav_from_date'), $cStart) . "</span>"; echo "<span class='month-cycle-start'>" . sprintf(tr('bud_sav_from_date'), $cStart) . "</span>";
} }
?> ?>
</div> </div>
<div class="month-actions" style="justify-content: center; width: 100%;"> <div class="month-actions">
<button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_edit_modal') ?>" <button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_edit_modal') ?>"
data-json="<?= htmlspecialchars(json_encode($data[$month] ?? []), ENT_QUOTES, 'UTF-8') ?>" data-json="<?= htmlspecialchars(json_encode($data[$month] ?? []), ENT_QUOTES, 'UTF-8') ?>"
onclick='editCustomSavingsMonth("<?= $month ?>", "<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>", JSON.parse(this.getAttribute("data-json")))'> onclick='editCustomSavingsMonth("<?= $month ?>", "<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>", JSON.parse(this.getAttribute("data-json")))'>
✏️ ✏️
</button> </button>
<button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_delete_month') ?>" <button class="btn-icon-small btn-safe-click btn-danger-soft" title="<?= tr('bud_sav_delete_month') ?>"
onclick="deleteEntireMonth('<?= $month ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')" onclick="deleteEntireMonth('<?= $month ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')">
style="color: #ef4444; border-color: #fca5a5; background: #fef2f2;">
🗑️ 🗑️
</button> </button>
</div> </div>
</div> </div>
</th> </div>
<?php endforeach; ?> <?php endforeach; ?>
</tr> </div>
</thead>
<tbody> <div class="eg-row eg-total-row">
<tr class="row-total"> <div class="eg-cell eg-sticky"><strong><?= tr('bud_sav_total_bank') ?></strong></div>
<td class="sticky-col"><strong><?= tr('bud_sav_total_bank') ?></strong></td>
<?php foreach ($months as $month): <?php foreach ($months as $month):
$val = $data[$month]['TOTAL_BANQUE'] ?? 0; $val = $data[$month]['TOTAL_BANQUE'] ?? 0;
?> ?>
<td class="text-center" style="padding:4px;"> <div class="eg-cell">
<div style="display:flex; align-items:center; justify-content:center; gap:2px;"> <div class="eg-cell-content">
<input type="number" step="0.01" <input type="number" step="0.01"
class="prev-input total-input-<?= $safeOwnerCls ?>-<?= $month ?>" class="prev-input eg-input eg-input-total total-input-<?= $safeOwnerCls ?>-<?= $month ?>"
style="width: 70px; font-weight:bold; color:#2563eb;"
value="<?= $val != 0 ? round($val) : '' ?>" value="<?= $val != 0 ? round($val) : '' ?>"
placeholder="0" placeholder="0"
onchange="updateEpargneCell('<?= $month ?>', 'TOTAL_BANQUE', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', this)"> onchange="updateEpargneCell('<?= $month ?>', 'TOTAL_BANQUE', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', this)">
<span style="color:#2563eb; font-weight:bold; font-size:0.9rem;">€</span> <span class="eg-currency-symbol">€</span>
</div>
</div> </div>
</td>
<?php endforeach; ?> <?php endforeach; ?>
</tr> </div>
<?php foreach ($allCategories as $cat): ?> <?php foreach ($allCategories as $cat): ?>
<tr> <div class="eg-row">
<td class="sticky-col"><?= htmlspecialchars($cat) ?></td> <div class="eg-cell eg-sticky"><?= htmlspecialchars($cat) ?></div>
<?php foreach ($months as $month): <?php foreach ($months as $month):
$amount = $data[$month][$cat] ?? 0; $amount = $data[$month][$cat] ?? 0;
?> ?>
<td class="text-center" style="padding:4px;"> <div class="eg-cell">
<div style="display:flex; align-items:center; justify-content:center; gap:2px;">
<input type="number" step="0.01" <input type="number" step="0.01"
class="prev-input <?= $ownerTextClass ?> cat-input-<?= $safeOwnerCls ?>-<?= $month ?>" class="prev-input eg-input cat-input-<?= $safeOwnerCls ?>-<?= $month ?>"
style="width: 70px;"
value="<?= $amount != 0 ? round($amount) : '' ?>" value="<?= $amount != 0 ? round($amount) : '' ?>"
placeholder="-" placeholder="-"
onchange="updateEpargneCell('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', this)"> onchange="updateEpargneCell('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', this)">
</div> </div>
</td>
<?php endforeach; ?> <?php endforeach; ?>
</tr> </div>
<?php endforeach; ?> <?php endforeach; ?>
<tr class="row-extres"> <div class="eg-row eg-extres-row">
<td class="sticky-col"><strong><?= tr('bud_sav_extra') ?></strong></td> <div class="eg-cell eg-sticky"><strong><?= tr('bud_sav_extra') ?></strong></div>
<?php foreach ($months as $month): <?php foreach ($months as $month):
$total = $data[$month]['TOTAL_BANQUE'] ?? 0; $total = $data[$month]['TOTAL_BANQUE'] ?? 0;
$sum = 0; $sum = 0;
foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0); foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0);
$extra = $total - $sum; $extra = $total - $sum;
?> ?>
<td class="text-center font-bold sum-target" id="extra_<?= $safeOwnerCls ?>_<?= $month ?>" style="color: <?= $extra >= 0 ? '#10b981' : '#ef4444' ?>; padding:12px;"> <div class="eg-cell font-bold sum-target <?= $extra >= 0 ? 'text-success' : 'text-danger' ?>" id="extra_<?= $safeOwnerCls ?>_<?= $month ?>">
<?= number_format($extra, 0, ',', ' ') ?> € <?= number_format($extra, 0, ',', ' ') ?> €
</td> </div>
<?php endforeach; ?> <?php endforeach; ?>
</tr> </div>
</tbody> </div>
</table>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<div id="savingsModal" class="pf-modal"> <div id="savingsModal" class="pf-modal">
<div class="pf-modal-content" style="max-width: 600px; width: 95%;"> <div class="pf-modal-content modal-epargne">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;"> <div class="pf-modal-header">
<h3 id="savingsModalTitle" class="pf-modal-title" style="margin:0;"><?= tr('bud_sav_modal_title_add') ?></h3> <h3 id="savingsModalTitle" class="pf-modal-title"><?= tr('bud_sav_modal_title_add') ?></h3>
<button type="button" onclick="document.getElementById('savingsModal').style.display='none'; document.body.classList.remove('no-scroll');" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">&times;</button> <button type="button" onclick="document.getElementById('savingsModal').classList.remove('open'); document.body.classList.remove('no-scroll');" class="pf-modal-close">&times;</button>
</div> </div>
<form action="modules/budget/includes/api/save-savings.php" method="POST" id="savingsForm"> <form action="/modules/budget/includes/api/save-savings.php" method="POST" id="savingsForm">
<input type="hidden" name="owner" id="sav_owner"> <input type="hidden" name="owner" id="sav_owner">
<input type="hidden" name="redirect_tab" id="redirect_tab" value="<?= htmlspecialchars($requestedOwner) ?>"> <input type="hidden" name="redirect_tab" id="redirect_tab" value="<?= htmlspecialchars($requestedOwner) ?>">
<input type="hidden" name="month_date" id="sav_date_hidden"> <input type="hidden" name="month_date" id="sav_date_hidden">
<div style="display:flex; gap:15px; margin-bottom:20px;"> <div class="pf-form-row">
<div class="form-group" style="flex:1; margin:0;"> <div class="pf-form-group pf-flex-1 pf-m-0">
<label class="pf-label"><?= tr('bud_sav_month_concerned') ?></label> <label class="pf-label"><?= tr('bud_sav_month_concerned') ?></label>
<input type="month" id="sav_month" required class="pf-input"> <input type="month" id="sav_month" required class="pf-input">
</div> </div>
<div class="form-group" style="flex:1; margin:0;"> <div class="pf-form-group pf-flex-1 pf-m-0">
<label class="pf-label"><?= tr('bud_sav_total_bank_eur') ?></label> <label class="pf-label"><?= tr('bud_sav_total_bank_eur') ?></label>
<input type="number" step="0.01" name="values[TOTAL_BANQUE]" id="sav_total" required class="pf-input no-spinners" style="font-weight:bold; color:#2563eb;"> <input type="number" step="0.01" name="values[TOTAL_BANQUE]" id="sav_total" required class="pf-input no-spinners pf-input-total">
</div> </div>
</div> </div>
<div class="separator" style="margin: 20px 0; border-bottom: 1px solid #e2e8f0;"></div> <hr class="pf-divider">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;"> <div class="ventilation-header">
<div> <div>
<h4 style="margin:0; font-size:1rem; color:#1e293b;"><?= tr('bud_sav_ventilation') ?></h4> <h4 class="pf-m-0 text-main"><?= tr('bud_sav_ventilation') ?></h4>
<span style="font-size:0.8rem; color:#64748b;"><?= tr('bud_sav_adj_help') ?></span> <span class="pf-muted-note"><?= tr('bud_sav_adj_help') ?></span>
</div> </div>
<button type="button" class="pf-btn btn-secondary" onclick="addCustomEpargneLine()" style="padding:4px 10px; height:auto; width:auto; font-size:0.9rem;">+ <?= tr('bud_sav_add_line') ?></button> <button type="button" class="pf-btn btn-secondary pf-btn-sm" onclick="addCustomEpargneLine()">+ <?= tr('bud_sav_add_line') ?></button>
</div> </div>
<div style="display:flex; gap:10px; padding:0 5px 5px 5px; font-size:0.8rem; color:#64748b; font-weight:600;"> <div class="ventilation-cols">
<div style="flex:2;"><?= tr('bud_category') ?></div> <div class="vl-col-name"><?= tr('bud_category') ?></div>
<div style="width:100px;"><?= tr('bud_sav_current') ?></div> <div class="vl-col-base"><?= tr('bud_sav_current') ?></div>
<div style="width:90px;"><?= tr('bud_sav_adjust') ?></div> <div class="vl-col-adj"><?= tr('bud_sav_adjust') ?></div>
<div style="width:100px;"><?= tr('bud_sav_new') ?></div> <div class="vl-col-final"><?= tr('bud_sav_new') ?></div>
<div style="width:28px;"></div> <div class="vl-col-btn"></div>
</div> </div>
<div id="linesContainer" style="max-height: 350px; overflow-y: auto; padding-right:5px; display:flex; flex-direction:column; gap:10px;"> <div id="linesContainer" class="ventilation-list"></div>
</div>
<div style="margin-top:25px; display:flex; justify-content:flex-end; gap:10px;"> <div class="modal-footer">
<button type="button" onclick="document.getElementById('savingsModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary" style="width:auto; margin:0;"><?= tr('btn_cancel') ?></button> <button type="button" onclick="document.getElementById('savingsModal').classList.remove('open'); document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn" style="width:auto; margin:0;"><?= tr('btn_save') ?></button> <button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
</div> </div>
</form> </form>
</div> </div>
@@ -272,7 +264,6 @@ function getMonthName($dateString) {
</div> </div>
<script> <script>
// --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR"; window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
window.I18N = { window.I18N = {
...(window.I18N || {}), ...(window.I18N || {}),
@@ -281,19 +272,47 @@ window.I18N = {
'bud_sav_ph_name': <?= json_encode(tr('bud_sav_ph_name')) ?>, 'bud_sav_ph_name': <?= json_encode(tr('bud_sav_ph_name')) ?>,
'bud_sav_confirm_delete_month': <?= json_encode(tr('bud_sav_confirm_delete_month')) ?>, 'bud_sav_confirm_delete_month': <?= json_encode(tr('bud_sav_confirm_delete_month')) ?>,
'bud_sav_prompt_duplicate': <?= json_encode(tr('bud_sav_prompt_duplicate')) ?>, 'bud_sav_prompt_duplicate': <?= json_encode(tr('bud_sav_prompt_duplicate')) ?>,
'bud_sav_prompt_new_line': <?= json_encode(tr('bud_sav_prompt_new_line')) ?>,
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>, 'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>,
'bud_err_server': <?= json_encode(tr('bud_err_server')) ?>, 'bud_err_server': <?= json_encode(tr('bud_err_server')) ?>,
'bud_err_network_dup': <?= json_encode(tr('bud_err_network_dup')) ?>, 'bud_err_network_dup': <?= json_encode(tr('bud_err_network_dup')) ?>,
'bud_sav_saving': <?= json_encode(tr('bud_sav_saving')) ?>, 'bud_sav_saving': <?= json_encode(tr('bud_sav_saving')) ?>,
'bud_err_delete': <?= json_encode(tr('bud_err_delete')) ?> 'bud_err_delete': <?= json_encode(tr('bud_err_delete')) ?>,
'btn_delete': <?= json_encode(tr('btn_delete')) ?>
}; };
// Sécurisation de la devise pour la calculatrice
const systemCurrency = (typeof window.CONFIG !== 'undefined' && window.CONFIG.CURRENCY) ? window.CONFIG.CURRENCY : '€'; const systemCurrency = (typeof window.CONFIG !== 'undefined' && window.CONFIG.CURRENCY) ? window.CONFIG.CURRENCY : '€';
// --- 2. GESTION DE L'ÉDITION INVISIBLE EN DIRECT (CORRIGÉE 🚀) ---
const cycleConfigs = <?= json_encode($cycleConfigs ?? []) ?>; const cycleConfigs = <?= json_encode($cycleConfigs ?? []) ?>;
async function promptNewSavingsLine(owner, latestMonth) {
const catName = prompt(window.I18N['bud_sav_prompt_new_line']);
if (!catName || catName.trim() === "") return;
const formData = new FormData();
formData.append('action', 'update_single_entry');
formData.append('month_date', latestMonth);
formData.append('category', catName.trim());
formData.append('owner', owner);
formData.append('amount', 0);
formData.append('force_insert', '1'); // 🛡️ Empêche la suppression auto par l'API
formData.append('ajax', '1');
try {
const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', {
method: 'POST',
body: formData
});
if (result.success) {
window.location.reload();
} else {
alert(window.I18N['bud_err_tech'] + " : " + (result.error || "Erreur"));
}
} catch (err) {
alert(window.I18N['bud_err_tech']);
}
}
async function updateEpargneCell(month, category, owner, inputEl) { async function updateEpargneCell(month, category, owner, inputEl) {
const val = parseFloat(inputEl.value) || 0; const val = parseFloat(inputEl.value) || 0;
const formData = new FormData(); const formData = new FormData();
@@ -302,27 +321,19 @@ async function updateEpargneCell(month, category, owner, inputEl) {
formData.append('category', category); formData.append('category', category);
formData.append('owner', owner); formData.append('owner', owner);
formData.append('amount', val); formData.append('amount', val);
formData.append('ajax', '1'); // 🚀 CRITIQUE : Informe l'API formData.append('ajax', '1');
try { try {
// 🚀 CRITIQUE : pachaFetch + Chemin absolu const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', { method: 'POST', body: formData });
const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', {
method: 'POST',
body: formData
});
if (!result.success) { if (!result.success) {
console.error("Erreur de sauvegarde :", result.error);
alert(window.I18N['bud_err_tech'] + " : " + (result.error || "Erreur serveur")); alert(window.I18N['bud_err_tech'] + " : " + (result.error || "Erreur serveur"));
return; // On stoppe la mise à jour visuelle si la BDD a refusé return;
} }
} catch (err) { } catch (err) {
console.error("Erreur réseau :", err); alert(window.I18N['bud_err_tech']);
alert(window.I18N['bud_err_tech'] || 'Erreur technique');
return; return;
} }
// Si on arrive ici, la BDD est bien à jour ! On met à jour le visuel.
const safeOwnerClass = owner.replace(/\s+/g, '_'); const safeOwnerClass = owner.replace(/\s+/g, '_');
const totalInput = document.querySelector(`.total-input-${CSS.escape(safeOwnerClass)}-${month}`); const totalInput = document.querySelector(`.total-input-${CSS.escape(safeOwnerClass)}-${month}`);
const totalVal = parseFloat(totalInput ? totalInput.value : 0) || 0; const totalVal = parseFloat(totalInput ? totalInput.value : 0) || 0;
@@ -337,7 +348,13 @@ async function updateEpargneCell(month, category, owner, inputEl) {
if (extraCell) { if (extraCell) {
extraCell.innerText = Math.round(extra).toLocaleString(window.appLang) + ' €'; extraCell.innerText = Math.round(extra).toLocaleString(window.appLang) + ' €';
extraCell.style.color = extra >= 0 ? '#10b981' : '#ef4444'; if (extra >= 0) {
extraCell.classList.remove('text-danger');
extraCell.classList.add('text-success');
} else {
extraCell.classList.remove('text-success');
extraCell.classList.add('text-danger');
}
} }
if(isSumModeActive) updateSumResult(); if(isSumModeActive) updateSumResult();
@@ -349,20 +366,20 @@ function addCustomEpargneLine(catName = '', amount = '') {
const inputName = catName ? `values[${catName}]` : ''; const inputName = catName ? `values[${catName}]` : '';
const html = ` const html = `
<div class="ventilation-line" style="display:flex; gap:10px; align-items:center; background:#f8fafc; padding:8px; border-radius:8px; border:1px solid #e2e8f0;"> <div class="ventilation-line">
<div style="flex:2;"> <div class="vl-col-name">
<input type="text" class="pf-input cat-name-input" value="${catName}" placeholder="${window.I18N['bud_sav_ph_name'] || 'Catégorie'}" oninput="updateCustomFieldName(this)" style="padding:6px; font-size:0.9rem;" required> <input type="text" class="pf-input cat-name-input pf-input-sm" value="${catName}" placeholder="${window.I18N['bud_sav_ph_name'] || 'Catégorie'}" oninput="updateCustomFieldName(this)" required>
</div> </div>
<div style="width:100px;"> <div class="vl-col-base">
<input type="number" step="0.01" class="pf-input base-amount no-spinners" value="${baseAmount}" oninput="recalculateCustomLine(this)" style="padding:6px; font-size:0.9rem; background:#fff;"> <input type="number" step="0.01" class="pf-input base-amount no-spinners pf-input-sm bg-white" value="${baseAmount}" oninput="recalculateCustomLine(this)">
</div> </div>
<div style="width:90px;"> <div class="vl-col-adj">
<input type="number" step="0.01" class="pf-input adjustment-amount no-spinners" placeholder="+ / -" oninput="recalculateCustomLine(this)" style="padding:6px; font-size:0.9rem; color:#f59e0b; font-weight:bold;"> <input type="number" step="0.01" class="pf-input adjustment-amount no-spinners pf-input-sm vl-input-adj" placeholder="+ / -" oninput="recalculateCustomLine(this)">
</div> </div>
<div style="width:100px;"> <div class="vl-col-final">
<input type="number" step="0.01" name="${inputName}" class="pf-input final-amount no-spinners" value="${baseAmount}" style="padding:6px; font-size:0.9rem; font-weight:bold; background:#e0f2fe; border-color:#bae6fd; color:#0369a1;" readonly> <input type="number" step="0.01" name="${inputName}" class="pf-input final-amount no-spinners pf-input-sm vl-input-final" value="${baseAmount}" readonly>
</div> </div>
<button type="button" onclick="this.parentElement.remove()" style="width:28px; height:28px; border:none; background:#fee2e2; color:#ef4444; border-radius:4px; cursor:pointer; display:flex; align-items:center; justify-content:center; font-weight:bold;">&times;</button> <button type="button" class="btn-remove-line" onclick="this.parentElement.remove()" title="${window.I18N['btn_delete']}">&times;</button>
</div> </div>
`; `;
container.insertAdjacentHTML('beforeend', html); container.insertAdjacentHTML('beforeend', html);
@@ -408,7 +425,7 @@ function editCustomSavingsMonth(monthDate, owner, rowData) {
if (container.children.length === 0) addCustomEpargneLine(); if (container.children.length === 0) addCustomEpargneLine();
document.getElementById('savingsModal').style.display = 'flex'; document.getElementById('savingsModal').classList.add('open');
document.body.classList.add('no-scroll'); document.body.classList.add('no-scroll');
} }
@@ -423,18 +440,10 @@ function openCustomSavingsModal(owner) {
container.innerHTML = ''; container.innerHTML = '';
addCustomEpargneLine(); addCustomEpargneLine();
document.getElementById('savingsModal').style.display = 'flex'; document.getElementById('savingsModal').classList.add('open');
document.body.classList.add('no-scroll'); document.body.classList.add('no-scroll');
} }
window.onclick = function(event) {
const modal = document.getElementById('savingsModal');
if (event.target == modal) {
modal.style.display = 'none';
document.body.classList.remove('no-scroll');
}
}
const savingsForm = document.getElementById('savingsForm'); const savingsForm = document.getElementById('savingsForm');
if (savingsForm) { if (savingsForm) {
savingsForm.addEventListener('submit', async function(e) { savingsForm.addEventListener('submit', async function(e) {
@@ -451,25 +460,19 @@ if (savingsForm) {
submitBtn.disabled = true; submitBtn.disabled = true;
const formData = new FormData(this); const formData = new FormData(this);
formData.append('ajax', '1'); // 🚀 CRITIQUE formData.append('ajax', '1');
try { try {
// 🚀 CRITIQUE : pachaFetch + Chemin absolu forcé const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', { method: 'POST', body: formData });
const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', {
method: 'POST',
body: formData
});
if (result.success) { if (result.success) {
window.location.reload(); // Ne recharge QUE si le serveur a dit OK window.location.reload();
} else { } else {
alert((window.I18N['bud_err_server'] || 'Erreur serveur : ') + (result.error || "Inconnue")); alert((window.I18N['bud_err_server'] || 'Erreur serveur : ') + (result.error || "Inconnue"));
submitBtn.innerText = originalText; submitBtn.innerText = originalText;
submitBtn.disabled = false; submitBtn.disabled = false;
} }
} catch (error) { } catch (error) {
console.error("Erreur catch:", error); alert(window.I18N['bud_err_tech']);
alert(window.I18N['bud_err_tech'] || 'Erreur Technique');
submitBtn.innerText = originalText; submitBtn.innerText = originalText;
submitBtn.disabled = false; submitBtn.disabled = false;
} }
@@ -477,7 +480,7 @@ if (savingsForm) {
} }
async function deleteEntireMonth(monthDate, owner) { async function deleteEntireMonth(monthDate, owner) {
const rawMsg = window.I18N['bud_sav_confirm_delete_month'] || "Supprimer %m pour %o ?"; const rawMsg = window.I18N['bud_sav_confirm_delete_month'];
const msg = rawMsg.replace('%m', monthDate).replace('%o', owner); const msg = rawMsg.replace('%m', monthDate).replace('%o', owner);
if (!confirm(msg)) return; if (!confirm(msg)) return;
@@ -485,14 +488,14 @@ async function deleteEntireMonth(monthDate, owner) {
formData.append("action", "delete_month_global"); formData.append("action", "delete_month_global");
formData.append("month_date", monthDate); formData.append("month_date", monthDate);
formData.append("owner", owner); formData.append("owner", owner);
formData.append("ajax", "1"); // 🚀 CRITIQUE formData.append("ajax", "1");
try { try {
const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', { method: "POST", body: formData }); const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', { method: "POST", body: formData });
if (result.success) window.location.reload(); if (result.success) window.location.reload();
else alert(result.error || window.I18N['bud_err_delete']); else alert(result.error || window.I18N['bud_err_delete']);
} catch(err) { } catch(err) {
alert(window.I18N['bud_err_delete'] || 'Erreur lors de la suppression'); alert(window.I18N['bud_err_delete']);
} }
} }
@@ -513,7 +516,7 @@ async function duplicateLastMonth(lastMonthDate, owner) {
defaultTotal = cycleConfigs[nextMonthStr].start_balance; defaultTotal = cycleConfigs[nextMonthStr].start_balance;
} }
const rawMsg = window.I18N['bud_sav_prompt_duplicate'] || "Dupliquer vers le mois suivant ?"; const rawMsg = window.I18N['bud_sav_prompt_duplicate'];
const message = rawMsg.replace('%s', formatMonth(lastMonthDate)).replace(/%t[12]/g, formatMonth(nextMonthStr)); const message = rawMsg.replace('%s', formatMonth(lastMonthDate)).replace(/%t[12]/g, formatMonth(nextMonthStr));
let newTotal = prompt(message, defaultTotal); let newTotal = prompt(message, defaultTotal);
@@ -525,7 +528,7 @@ async function duplicateLastMonth(lastMonthDate, owner) {
formData.append("target_date", nextMonthStr); formData.append("target_date", nextMonthStr);
formData.append("new_total", newTotal); formData.append("new_total", newTotal);
formData.append("owner", owner); formData.append("owner", owner);
formData.append("ajax", "1"); // 🚀 CRITIQUE formData.append("ajax", "1");
try { try {
const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', { method: "POST", body: formData }); const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', { method: "POST", body: formData });
@@ -535,8 +538,7 @@ async function duplicateLastMonth(lastMonthDate, owner) {
alert((window.I18N['bud_err_server'] || 'Erreur serveur : ') + (result.error || "Inconnue")); alert((window.I18N['bud_err_server'] || 'Erreur serveur : ') + (result.error || "Inconnue"));
} }
} catch(err) { } catch(err) {
console.error(err); alert(window.I18N['bud_err_network_dup']);
alert(window.I18N['bud_err_network_dup'] || 'Erreur réseau.');
} }
} }
} }
+48 -49
View File
@@ -425,23 +425,23 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
<div class="budget-view"> <div class="budget-view">
<div style="background:white; padding:20px; border-radius:16px; box-shadow:var(--shadow-sm); margin-bottom:24px; border:1px solid #e2e8f0; position:relative;"> <div class="bud-summary-card">
<?php if ($isClosed): ?> <?php if ($isClosed): ?>
<div style="position:absolute; top:0; left:0; right:0; background:#f1f5f9; padding:5px; text-align:center; font-size:0.8rem; font-weight:bold; color:#64748b; border-radius:16px 16px 0 0; border-bottom:1px solid #e2e8f0;"> <div class="bud-closed-banner">
🔒 <?= tr('bud_closed_archived') ?> 🔒 <?= tr('bud_closed_archived') ?>
</div> </div>
<div style="height:20px;"></div> <div style="height:20px;"></div>
<?php endif; ?> <?php endif; ?>
<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px; margin-bottom: 20px;"> <div class="bud-header-group">
<div style="display:flex; align-items:center; gap:15px;"> <div class="bud-header-title-wrapper">
<h2 style="margin:0; font-size:1.3rem; color:#0f172a; text-transform:capitalize;"><?= tr('bud_budget_of') ?> <?= $monthName ?></h2> <h2 class="bud-header-title"><?= tr('bud_budget_of') ?> <?= $monthName ?></h2>
</div> </div>
<div class="action-toolbar" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;"> <div class="action-toolbar bud-toolbar">
<?php if (!$isClosed): ?> <?php if (!$isClosed): ?>
<button type="button" class="pf-btn btn-secondary" onclick="openSuiviModal('importCsvModal')" style="padding:6px 12px; height:auto; width:auto; font-size:0.85rem;"> <button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="openSuiviModal('importCsvModal')">
📂 <?= tr('bud_import_csv') ?? 'Importer CSV' ?> 📂 <?= tr('bud_import_csv') ?? 'Importer CSV' ?>
</button> </button>
@@ -452,12 +452,12 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
<input type="hidden" name="freeze_capacite_max" value="<?= $capacite_max ?>"> <input type="hidden" name="freeze_capacite_max" value="<?= $capacite_max ?>">
<input type="hidden" name="freeze_solde_theorique" value="<?= $solde_theorique ?>"> <input type="hidden" name="freeze_solde_theorique" value="<?= $solde_theorique ?>">
<input type="hidden" name="freeze_reste_a_venir" value="<?= $reste_a_venir ?>"> <input type="hidden" name="freeze_reste_a_venir" value="<?= $reste_a_venir ?>">
<button type="submit" class="pf-btn" style="background:linear-gradient(135deg, #10b981, #059669); padding:6px 12px; height:auto; width:auto; font-size:0.85rem;">🔒 <?= tr('bud_close_btn') ?></button> <button type="submit" class="pf-btn bud-btn-lock">🔒 <?= tr('bud_close_btn') ?></button>
</form> </form>
<?php else: ?> <?php else: ?>
<form method="POST" onsubmit="return confirm('<?= tr('bud_confirm_reopen') ?>');"> <form method="POST" onsubmit="return confirm('<?= tr('bud_confirm_reopen') ?>');">
<input type="hidden" name="action" value="reopen_month"> <input type="hidden" name="action" value="reopen_month">
<button type="submit" class="pf-btn btn-secondary" style="padding:6px 12px; height:auto; width:auto; font-size:0.85rem;">🔓 <?= tr('bud_reopen_btn') ?></button> <button type="submit" class="pf-btn btn-secondary pf-btn-small">🔓 <?= tr('bud_reopen_btn') ?></button>
</form> </form>
<?php endif; ?> <?php endif; ?>
@@ -469,38 +469,38 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
</div> </div>
</div> </div>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:15px; margin-bottom:25px;"> <div class="bud-kpi-grid">
<div style="padding:15px; background:#f8fafc; border-radius:12px; position:relative; border:1px solid #e2e8f0;"> <div class="bud-kpi-card">
<div style="font-size:0.85rem; color:#64748b; margin-bottom:4px;"><?= tr('bud_bank_balance') ?> <?= $isClosed ? "(".tr('bud_frozen').")" : "(".tr('bud_current').")" ?></div> <div class="bud-kpi-label"><?= tr('bud_bank_balance') ?> <?= $isClosed ? "(".tr('bud_frozen').")" : "(".tr('bud_current').")" ?></div>
<div style="font-size:1.4rem; font-weight:700; color:#0f172a;"> <div class="bud-kpi-value">
<?= number_format($solde_actuel, 2, ',', ' ') ?> € <?= number_format($solde_actuel, 2, ',', ' ') ?> €
</div> </div>
<?php if (!$isClosed): ?> <?php if (!$isClosed): ?>
<button onclick="openSuiviModal('snapshotModal')" style="position:absolute; top:12px; right:12px; background:white; border:1px solid #cbd5e1; border-radius:6px; padding:4px 8px; cursor:pointer; font-size:0.8rem; color:#475569; transition:0.2s;" onmouseover="this.style.background='#f1f5f9'" onmouseout="this.style.background='white'">✏️ <?= tr('bud_update_btn') ?></button> <button onclick="openSuiviModal('snapshotModal')" class="bud-kpi-edit-btn">✏️ <?= tr('bud_update_btn') ?></button>
<?php endif; ?> <?php endif; ?>
</div> </div>
<div style="padding:15px; background:#f8fafc; border-radius:12px; border:1px solid #e2e8f0;"> <div class="bud-kpi-card">
<div style="font-size:0.85rem; color:#64748b; margin-bottom:4px;"><?= tr('bud_theoretical_balance') ?></div> <div class="bud-kpi-label"><?= tr('bud_theoretical_balance') ?></div>
<div style="font-size:1.4rem; font-weight:700; color:<?= $solde_theorique < 0 ? '#ef4444' : '#334155' ?>;"> <div class="bud-kpi-value <?= $solde_theorique < 0 ? 'bud-kpi-value--danger' : '' ?>">
<?= number_format($solde_theorique, 2, ',', ' ') ?> € <?= number_format($solde_theorique, 2, ',', ' ') ?> €
</div> </div>
</div> </div>
<div style="padding:15px; background:#fef2f2; border-radius:12px; border:1px solid #fecaca; position: relative;"> <div class="bud-kpi-card bud-kpi-card--danger">
<div style="font-size:0.85rem; color:#991b1b; margin-bottom:4px; display:flex; justify-content:space-between; align-items:center;"> <div class="bud-kpi-label bud-kpi-label--danger">
<span><?= tr('bud_upcoming_charges') ?></span> <span><?= tr('bud_upcoming_charges') ?></span>
<button onclick="toggleDiv('pendingDetailsList')" style="background:none; border:none; cursor:pointer; font-size:1rem; padding:0; filter: grayscale(1); opacity: 0.7; transition: 0.2s;" onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.7">👁️</button> <button onclick="toggleDiv('pendingDetailsList')" class="bud-pending-toggle">👁️</button>
</div> </div>
<div style="font-size:1.4rem; font-weight:700; color:#b91c1c;"> <div class="bud-kpi-value bud-kpi-value--danger">
- <?= number_format($reste_a_venir, 2, ',', ' ') ?> € - <?= number_format($reste_a_venir, 2, ',', ' ') ?> €
</div> </div>
<div id="pendingDetailsList" style="display:none; margin-top: 12px; padding-top: 12px; border-top: 1px dashed #fca5a5; font-size: 0.8rem; color: #7f1d1d;"> <div id="pendingDetailsList" class="bud-pending-list" style="display:none;">
<?php if (empty($pending_charges)): ?> <?php if (empty($pending_charges)): ?>
<div style="text-align:center; font-style:italic; opacity:0.8;"><?= tr('bud_all_paid') ?></div> <div style="text-align:center; font-style:italic; opacity:0.8;"><?= tr('bud_all_paid') ?></div>
<?php else: ?> <?php else: ?>
<?php foreach($pending_charges as $pc): ?> <?php foreach($pending_charges as $pc): ?>
<div style="display:flex; justify-content:space-between; margin-bottom:6px;" <?= !empty($pc['tooltip']) ? 'title="'.htmlspecialchars($pc['tooltip']).'" style="cursor:help;"' : '' ?>> <div class="bud-pending-item" <?= !empty($pc['tooltip']) ? 'title="'.htmlspecialchars($pc['tooltip']).'" style="cursor:help;"' : '' ?>>
<span><?= htmlspecialchars($pc['name']) ?></span> <span><?= htmlspecialchars($pc['name']) ?></span>
<strong><?= number_format($pc['amount'], 0, ',', ' ') ?> €</strong> <strong><?= number_format($pc['amount'], 0, ',', ' ') ?> €</strong>
</div> </div>
@@ -534,28 +534,28 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
} }
?> ?>
<div style="margin-top: 65px; margin-bottom: 75px; position: relative; font-family: inherit;"> <div class="bud-scale-wrapper">
<div class="bud-scale-track">
<div style="position: relative; height: 24px; background: #e2e8f0; border-radius: 12px; width: 100%; box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);">
<?php if ($posZero > 0): ?> <?php if ($posZero > 0): ?>
<div style="position: absolute; left: 0; width: <?= $posZero ?>%; height: 100%; background: #fef2f2; border-radius: 12px 0 0 12px;"></div> <div class="bud-scale-zero" style="width: <?= $posZero ?>%;"></div>
<?php endif; ?> <?php endif; ?>
<div style="position: absolute; left: <?= $leftActuel ?>%; width: <?= $widthActuel ?>%; height: 100%; background: repeating-linear-gradient(45deg, #fbbf24, #fbbf24 10px, #f59e0b 10px, #f59e0b 20px); border-radius: <?= $solde_actuel >= 0 ? '0 12px 12px 0' : '12px' ?>; transition: width 0.5s ease-out;"></div> <div class="bud-scale-bar" style="left: <?= $leftActuel ?>%; width: <?= $widthActuel ?>%; border-radius: <?= $solde_actuel >= 0 ? '0 12px 12px 0' : '12px' ?>;"></div>
<?php if ($solde_theorique < 0): ?> <?php if ($solde_theorique < 0): ?>
<div style="position: absolute; left: <?= $leftDanger ?>%; width: <?= $widthDanger ?>%; height: 100%; background: repeating-linear-gradient(45deg, #ef4444, #ef4444 10px, #dc2626 10px, #dc2626 20px); border-radius: 12px 0 0 12px; opacity: 0.95;"></div> <div class="bud-scale-bar--danger" style="left: <?= $leftDanger ?>%; width: <?= $widthDanger ?>%;"></div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<div style="position: absolute; left: <?= $posZero ?>%; top: -5px; height: 35px; width: 2px; background: #94a3b8; z-index: 10;"></div> <div class="bud-scale-marker bud-scale-marker--zero" style="left: <?= $posZero ?>%;"></div>
<div style="position: absolute; left: <?= $posZero ?>%; top: 32px; transform: translateX(-50%); font-size: 0.85rem; font-weight: 800; color: #64748b;">0 €</div> <div class="bud-scale-label bud-scale-label--zero" style="left: <?= $posZero ?>%;">0 €</div>
<div style="position: absolute; left: <?= $posActuel ?>%; top: -15px; height: 40px; width: 3px; background: #1e293b; z-index: 15;"></div> <div class="bud-scale-marker bud-scale-marker--actual" style="left: <?= $posActuel ?>%;"></div>
<div style="position: absolute; left: <?= $posActuel ?>%; top: -35px; transform: translateX(-50%); font-size: 0.8rem; font-weight: bold; color: #1e293b;"><?= tr('bud_bar_actual') ?? 'Actuel' ?></div> <div class="bud-scale-label bud-scale-label--actual" style="left: <?= $posActuel ?>%;"><?= tr('bud_bar_actual') ?? 'Actuel' ?></div>
<div style="position: absolute; left: <?= $posTheorique ?>%; top: 24px; height: 30px; width: 3px; background: <?= $solde_theorique >= 0 ? '#8b5cf6' : '#ef4444' ?>; z-index: 12;"></div> <?php $colorTheo = $solde_theorique >= 0 ? 'var(--primary)' : 'var(--danger)'; ?>
<div style="position: absolute; left: <?= $posTheorique ?>%; top: 54px; transform: translateX(-50%); font-size: 0.85rem; font-weight: bold; color: <?= $solde_theorique >= 0 ? '#8b5cf6' : '#ef4444' ?>; white-space: nowrap;"><?= tr('bud_bar_theoretical') ?? 'Théorique' ?></div> <div class="bud-scale-marker bud-scale-marker--theo" style="left: <?= $posTheorique ?>%; background: <?= $colorTheo ?>;"></div>
<div class="bud-scale-label bud-scale-label--theo" style="left: <?= $posTheorique ?>%; color: <?= $colorTheo ?>;"><?= tr('bud_bar_theoretical') ?? 'Théorique' ?></div>
<?php <?php
$overlapMax = abs($posMax - $posActuel) < 35; $overlapMax = abs($posMax - $posActuel) < 35;
@@ -563,17 +563,16 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
$heightMaxLine = $overlapMax ? '65px' : '40px'; $heightMaxLine = $overlapMax ? '65px' : '40px';
$topMaxLine = $overlapMax ? '-40px' : '-15px'; $topMaxLine = $overlapMax ? '-40px' : '-15px';
?> ?>
<div style="position: absolute; left: <?= $posMax ?>%; top: <?= $topMaxLine ?>; height: <?= $heightMaxLine ?>; width: 2px; background: #cbd5e1; z-index: 10;"></div> <div class="bud-scale-marker bud-scale-marker--max" style="left: <?= $posMax ?>%; top: <?= $topMaxLine ?>; height: <?= $heightMaxLine ?>;"></div>
<div style="position: absolute; left: <?= $posMax ?>%; top: <?= $topMaxText ?>; transform: translateX(calc(-100% + 10px)); font-size: 0.8rem; font-weight: bold; color: #64748b; white-space: nowrap; cursor: help;" title="<?= sprintf(tr('bud_capacity_tooltip'), number_format($solde_initial, 0), number_format($salaires_retenus, 0), number_format($rentrees_autres, 0)) ?>"> <div class="bud-scale-label bud-scale-label--max" style="left: <?= $posMax ?>%; top: <?= $topMaxText ?>; transform: translateX(calc(-100% + 10px));" title="<?= sprintf(tr('bud_capacity_tooltip'), number_format($solde_initial, 0), number_format($salaires_retenus, 0), number_format($rentrees_autres, 0)) ?>">
<?= tr('bud_bar_max_cap') ?? 'Capacité Max' ?> : <?= number_format($capacite_max, 0, ',', ' ') ?> € ℹ️ <?= tr('bud_bar_max_cap') ?? 'Capacité Max' ?> : <?= number_format($capacite_max, 0, ',', ' ') ?> € ℹ️
</div> </div>
</div>
</div> </div>
</div>
<div class="categories-grid" style="display:grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap:24px;"> <div class="bud-categories-grid">
<?php foreach ($categoriesConfig as $key => $conf): ?> <?php foreach ($categoriesConfig as $key => $conf): ?>
<div class="cat-card" <?= $isClosed ? 'style="opacity:0.8;"' : '' ?>> <div class="cat-card" <?= $isClosed ? 'style="opacity:0.8;"' : '' ?>>
<div class="cat-card-header" style="background:<?= $conf['color'] ?>15; border-bottom:1px solid <?= $conf['color'] ?>30;"> <div class="cat-card-header" style="background:<?= $conf['color'] ?>15; border-bottom:1px solid <?= $conf['color'] ?>30;">
@@ -588,27 +587,27 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php $barCol = ($conf['db_type'] === 'Income') ? '#10b981' : ($logic['isOver'] ? '#ef4444' : $conf['color']); ?> <?php $barCol = ($conf['db_type'] === 'Income') ? 'var(--success)' : ($logic['isOver'] ? 'var(--danger)' : $conf['color']); ?>
<div style="background:#f1f5f9; height:4px; width:100%;"> <div style="background:var(--bg-page); height:4px; width:100%;">
<div style="width:<?= $logic['pct'] ?>%; background:<?= $barCol ?>; height:100%;"></div> <div style="width:<?= $logic['pct'] ?>%; background:<?= $barCol ?>; height:100%;"></div>
</div> </div>
<div style="flex:1; max-height:300px; overflow-y:auto; padding:0;"> <div style="flex:1; max-height:300px; overflow-y:auto; padding:0;">
<?php if (empty($expensesByCategory[$key])): ?> <?php if (empty($expensesByCategory[$key])): ?>
<div style="padding:20px; text-align:center; color:#cbd5e1; font-style:italic; font-size:0.85rem;"><?= tr('bud_no_lines') ?></div> <div style="padding:20px; text-align:center; color:var(--text-muted); font-style:italic; font-size:0.85rem;"><?= tr('bud_no_lines') ?></div>
<?php else: ?> <?php else: ?>
<table style="width:100%; border-collapse:collapse; font-size:0.85rem;"> <table class="bud-expense-table">
<?php foreach ($expensesByCategory[$key] as $exp): ?> <?php foreach ($expensesByCategory[$key] as $exp): ?>
<tr style="border-bottom:1px solid #f8fafc;"> <tr class="bud-expense-row">
<td style="padding:10px 15px; color:#94a3b8;"><?= date('d/m', strtotime($exp['date_exp'])) ?></td> <td class="bud-expense-date"><?= date('d/m', strtotime($exp['date_exp'])) ?></td>
<td style="padding:10px 5px; font-weight:500;"> <td class="bud-expense-label">
<?= htmlspecialchars($exp['label']) ?> <?= htmlspecialchars($exp['label']) ?>
</td> </td>
<td style="padding:10px 15px; text-align:right; font-weight:600; color:<?= $exp['amount'] > 0 ? '#10b981' : '#1e293b' ?>;"> <td class="bud-expense-amount" style="color:<?= $exp['amount'] > 0 ? 'var(--success)' : 'var(--text-main)' ?>;">
<?= $exp['amount'] > 0 ? '+' : '-' ?><?= number_format(abs($exp['amount']), 2) ?> <?= $exp['amount'] > 0 ? '+' : '-' ?><?= number_format(abs($exp['amount']), 2) ?>
</td> </td>
<?php if (!$isClosed): ?> <?php if (!$isClosed): ?>
<td style="width:70px; padding-right:10px; text-align:right; white-space:nowrap;"> <td class="bud-expense-actions">
<button class="btn-icon-action edit" onclick='openEditModal(<?= json_encode($exp, JSON_HEX_APOS | JSON_HEX_QUOT) ?>)' title="<?= tr('edit') ?>">✏️</button> <button class="btn-icon-action edit" onclick='openEditModal(<?= json_encode($exp, JSON_HEX_APOS | JSON_HEX_QUOT) ?>)' title="<?= tr('edit') ?>">✏️</button>
<button class="btn-icon-action delete btn-safe-click" onclick="deleteExpense(<?= $exp['id'] ?>)" title="<?= tr('delete') ?>">🗑️</button> <button class="btn-icon-action delete btn-safe-click" onclick="deleteExpense(<?= $exp['id'] ?>)" title="<?= tr('delete') ?>">🗑️</button>
</td> </td>
+250 -99
View File
@@ -33,10 +33,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf($_POST['csrf_token'] ?? null)) { if (!verify_csrf($_POST['csrf_token'] ?? null)) {
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') { if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') {
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Session invalide (CSRF). Rechargez la page.']); echo json_encode(['success' => false, 'error' => tr('error_csrf')]);
exit; exit;
} }
$error = "Session invalide (CSRF). Rechargez la page."; $error = tr('error_csrf');
} else { } else {
$action = $_POST['action'] ?? ''; $action = $_POST['action'] ?? '';
@@ -44,12 +44,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'liste', 'calendar_ios', 'printvault', 'planka']; $all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'liste', 'calendar_ios', 'printvault', 'planka'];
$enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m]))); $enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m])));
if (empty($enabled)) { if (empty($enabled)) {
$error = "Vous devez garder au moins un module actif."; $error = tr('set_err_min_module');
} else { } else {
$meta_pdo->prepare("UPDATE families SET enabled_modules = ? WHERE id = ?") $meta_pdo->prepare("UPDATE families SET enabled_modules = ? WHERE id = ?")
->execute([json_encode($enabled), $family_id]); ->execute([json_encode($enabled), $family_id]);
$_SESSION['enabled_modules'] = $enabled; $_SESSION['enabled_modules'] = $enabled;
$success = "Modules mis à jour avec succès."; $success = tr('set_success_modules');
} }
} }
@@ -59,7 +59,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$_SESSION['app_lang'] = $lang; $_SESSION['app_lang'] = $lang;
$meta_pdo->prepare("UPDATE users SET lang = ? WHERE id = ?") $meta_pdo->prepare("UPDATE users SET lang = ? WHERE id = ?")
->execute([$lang, $user_id]); ->execute([$lang, $user_id]);
$success = "Langue mise à jour."; $success = tr('set_success_lang');
} }
} }
@@ -68,7 +68,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$color = trim($_POST['color'] ?? '#0891b2'); $color = trim($_POST['color'] ?? '#0891b2');
if (!$display_name) { if (!$display_name) {
$error = "Le prénom ne peut pas être vide."; $error = tr('set_err_empty_name');
} else { } else {
$meta_pdo->prepare("UPDATE users SET display_name = ? WHERE id = ?") $meta_pdo->prepare("UPDATE users SET display_name = ? WHERE id = ?")
->execute([$display_name, $user_id]); ->execute([$display_name, $user_id]);
@@ -80,7 +80,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
$_SESSION['user']['display_name'] = $display_name; $_SESSION['user']['display_name'] = $display_name;
$success = "Profil mis à jour avec succès."; $success = tr('set_success_profile');
} }
} }
@@ -94,15 +94,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$row = $row->fetch(); $row = $row->fetch();
if (!password_verify($current, $row['password_hash'])) { if (!password_verify($current, $row['password_hash'])) {
$error = "Mot de passe actuel incorrect."; $error = tr('set_err_wrong_pwd');
} elseif (strlen($new) < 6) { } elseif (strlen($new) < 6) {
$error = "Le nouveau mot de passe doit faire au moins 6 caractères."; $error = tr('set_err_pwd_length');
} elseif ($new !== $confirm) { } elseif ($new !== $confirm) {
$error = "Les mots de passe ne correspondent pas."; $error = tr('set_err_pwd_match');
} else { } else {
$meta_pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?") $meta_pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?")
->execute([password_hash($new, PASSWORD_BCRYPT), $user_id]); ->execute([password_hash($new, PASSWORD_BCRYPT), $user_id]);
$success = "Mot de passe modifié avec succès."; $success = tr('set_success_pwd');
} }
} }
@@ -111,7 +111,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($name) { if ($name) {
$meta_pdo->prepare("UPDATE families SET name = ? WHERE id = ?") $meta_pdo->prepare("UPDATE families SET name = ? WHERE id = ?")
->execute([$name, $family_id]); ->execute([$name, $family_id]);
$success = "Nom de l'espace mis à jour."; $success = tr('set_success_family');
} }
} }
@@ -119,7 +119,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$new_code = bin2hex(random_bytes(8)); $new_code = bin2hex(random_bytes(8));
$meta_pdo->prepare("UPDATE families SET invite_code = ? WHERE id = ?") $meta_pdo->prepare("UPDATE families SET invite_code = ? WHERE id = ?")
->execute([$new_code, $family_id]); ->execute([$new_code, $family_id]);
$success = "Nouveau code d'invitation généré."; $success = tr('set_success_invite');
} }
if ($action === 'upload_home_bg' && $family_id) { if ($action === 'upload_home_bg' && $family_id) {
@@ -129,14 +129,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($file && $file['error'] === 0) { if ($file && $file['error'] === 0) {
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, ['jpg','jpeg','png','webp'])) { if (!in_array($ext, ['jpg','jpeg','png','webp'])) {
$error = "Format non supporté (jpg, png, webp)."; $error = tr('set_err_bg_format');
} else { } else {
$dest = $upload_dir . 'home_bg_' . $family_id . '.' . $ext; $dest = $upload_dir . 'home_bg_' . $family_id . '.' . $ext;
foreach (glob($upload_dir . 'home_bg_' . $family_id . '.*') as $old) @unlink($old); foreach (glob($upload_dir . 'home_bg_' . $family_id . '.*') as $old) @unlink($old);
if (move_uploaded_file($file['tmp_name'], $dest)) { if (move_uploaded_file($file['tmp_name'], $dest)) {
$success = "Image d'accueil mise à jour."; $success = tr('set_success_bg');
} else { } else {
$error = "Erreur lors de l'upload."; $error = tr('set_err_upload');
} }
} }
} }
@@ -144,13 +144,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($action === 'reset_home_bg' && $family_id) { if ($action === 'reset_home_bg' && $family_id) {
foreach (glob('/uploads/home_bg_' . $family_id . '.*') as $old) @unlink($old); foreach (glob('/uploads/home_bg_' . $family_id . '.*') as $old) @unlink($old);
$success = "Image d'accueil réinitialisée."; $success = tr('set_success_bg_reset');
} }
if ($action === 'grocery_history_max' && $family_id) { if ($action === 'grocery_history_max' && $family_id) {
require_once __DIR__ . '/includes/db.php'; require_once __DIR__ . '/includes/db.php';
if (!isset($pdo)) { if (!isset($pdo)) {
$error = "Base famille indisponible."; $error = tr('set_err_db_unavailable');
} else { } else {
$n = (int) ($_POST['history_max'] ?? 20); $n = (int) ($_POST['history_max'] ?? 20);
$n = max(1, min(50, $n)); $n = max(1, min(50, $n));
@@ -171,7 +171,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$calendarUrl = trim($_POST['icloud_calendar_url'] ?? ''); $calendarUrl = trim($_POST['icloud_calendar_url'] ?? '');
if (!$username || !$appPassword || !$calendarUrl) { if (!$username || !$appPassword || !$calendarUrl) {
$error = "Merci de renseigner identifiant iCloud, mot de passe d'app et URL CalDAV."; $error = tr('set_err_ios_missing');
} else { } else {
try { try {
$resolvedUrl = hh_icloud_resolve_calendar_url_if_needed($username, $appPassword, $calendarUrl); $resolvedUrl = hh_icloud_resolve_calendar_url_if_needed($username, $appPassword, $calendarUrl);
@@ -181,13 +181,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
VALUES (?, 'icloud_caldav', ?, ?, ?, 'connected', NOW()) VALUES (?, 'icloud_caldav', ?, ?, ?, 'connected', NOW())
ON DUPLICATE KEY UPDATE username=VALUES(username), secret_encrypted=VALUES(secret_encrypted), calendar_url=VALUES(calendar_url), status='connected', updated_at=NOW() ON DUPLICATE KEY UPDATE username=VALUES(username), secret_encrypted=VALUES(secret_encrypted), calendar_url=VALUES(calendar_url), status='connected', updated_at=NOW()
")->execute([$user_id, $username, $encrypted, $resolvedUrl]); ")->execute([$user_id, $username, $encrypted, $resolvedUrl]);
$msg = "Connexion calendrier iOS enregistrée."; $msg = tr('set_success_ios');
if (rtrim($resolvedUrl, '/') !== rtrim($calendarUrl, '/')) { if (rtrim($resolvedUrl, '/') !== rtrim($calendarUrl, '/')) {
$msg .= " URL du calendrier détectée automatiquement."; $msg .= tr('set_success_ios_auto');
} }
$success = $msg; $success = $msg;
} catch (\Throwable $e) { } catch (\Throwable $e) {
$error = "Impossible d'enregistrer la connexion iOS: " . $e->getMessage(); $error = tr('set_err_ios_save') . $e->getMessage();
} }
} }
} }
@@ -197,7 +197,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$row->execute([$user_id]); $row->execute([$user_id]);
$integration = $row->fetch(); $integration = $row->fetch();
if (!$integration) { if (!$integration) {
$error = "Aucune connexion iOS configurée."; $error = tr('set_err_ios_none');
} else { } else {
try { try {
$pwd = hh_normalize_apple_app_password(hh_decrypt_secret($integration['secret_encrypted'])); $pwd = hh_normalize_apple_app_password(hh_decrypt_secret($integration['secret_encrypted']));
@@ -209,31 +209,32 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$meta_pdo->prepare("UPDATE user_calendar_integrations SET calendar_url = ? WHERE id = ?") $meta_pdo->prepare("UPDATE user_calendar_integrations SET calendar_url = ? WHERE id = ?")
->execute([$resolvedUrl, $integration['id']]); ->execute([$resolvedUrl, $integration['id']]);
} }
$success = "Connexion iCloud CalDAV valide (HTTP $code)."; $success = sprintf(tr('set_success_ios_test'), $code);
} else { } else {
$error = "Test connexion échoué (HTTP $code). Vérifiez vos identifiants."; $error = sprintf(tr('set_err_ios_test'), $code);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
$error = "Test connexion impossible: " . $e->getMessage(); $error = tr('set_err_ios_test_fail') . $e->getMessage();
} }
} }
} }
if ($action === 'calendar_ios_disconnect') { if ($action === 'calendar_ios_disconnect') {
$meta_pdo->prepare("DELETE FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'")->execute([$user_id]); $meta_pdo->prepare("DELETE FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'")->execute([$user_id]);
$success = "Connexion iOS supprimée."; $success = tr('set_success_ios_del');
} }
// --- GESTION DES PROFILS RATTACHÉS (Enfants, Nounous, etc.) --- // --- GESTION DES PROFILS RATTACHÉS (Enfants, Nounous, etc.) ---
if ($action === 'add_attached_profile') { if ($action === 'add_attached_profile') {
$name = trim($_POST['profile_name'] ?? ''); $name = trim($_POST['profile_name'] ?? '');
$role = trim($_POST['profile_role'] ?? 'child'); $role = trim($_POST['profile_role'] ?? 'child');
$color = trim($_POST['profile_color'] ?? '#3b82f6');
if ($name) { if ($name) {
require_once __DIR__ . '/includes/db.php'; require_once __DIR__ . '/includes/db.php';
if (isset($pdo)) { if (isset($pdo)) {
$stmt = $pdo->prepare("INSERT INTO pf_people (name, role) VALUES (?, ?)"); $stmt = $pdo->prepare("INSERT INTO pf_people (name, role, color) VALUES (?, ?, ?)");
$stmt->execute([$name, $role]); $stmt->execute([$name, $role, $color]);
$success = "Profil de {$name} ajouté avec succès."; $success = "Profil de {$name} ajouté avec succès.";
} }
} else { } else {
@@ -241,15 +242,60 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
} }
if ($action === 'update_attached_profile_color') {
$profile_id = (int)($_POST['profile_id'] ?? 0);
$color = trim($_POST['color'] ?? '#3b82f6');
if ($profile_id > 0) {
require_once __DIR__ . '/includes/db.php';
if (isset($pdo)) {
$stmt = $pdo->prepare("UPDATE pf_people SET color = ? WHERE id = ? AND user_id IS NULL");
$stmt->execute([$color, $profile_id]);
$success = "Couleur mise à jour.";
}
}
}
if ($action === 'delete_attached_profile') { if ($action === 'delete_attached_profile') {
$profile_id = (int)($_POST['profile_id'] ?? 0); $profile_id = (int)($_POST['profile_id'] ?? 0);
if ($profile_id > 0) { if ($profile_id > 0) {
require_once __DIR__ . '/includes/db.php'; require_once __DIR__ . '/includes/db.php';
if (isset($pdo)) { if (isset($pdo)) {
// On protège les vrais utilisateurs en s'assurant que user_id est NULL
$stmt = $pdo->prepare("DELETE FROM pf_people WHERE id = ? AND user_id IS NULL"); $stmt = $pdo->prepare("DELETE FROM pf_people WHERE id = ? AND user_id IS NULL");
$stmt->execute([$profile_id]); $stmt->execute([$profile_id]);
$success = "Profil supprimé."; $success = tr('set_success_profile_del');
}
}
}
if ($action === 'update_profile_color') {
$profile_id = (int)($_POST['profile_id'] ?? 0);
$color = trim($_POST['color'] ?? '#0891b2');
if ($profile_id > 0) {
require_once __DIR__ . '/includes/db.php';
if (isset($pdo)) {
$stmt = $pdo->prepare("UPDATE pf_people SET color = ? WHERE id = ? AND user_id IS NULL");
$stmt->execute([$color, $profile_id]);
$success = tr('set_success_color_profile');
}
}
}
if ($action === 'update_attached_profile_color') {
$profile_id = (int)($_POST['profile_id'] ?? 0);
$color = trim($_POST['color'] ?? '');
// Vérification stricte format Hexadécimal
if ($profile_id > 0 && preg_match('/^#[a-f0-9]{6}$/i', $color)) {
require_once __DIR__ . '/includes/db.php';
if (isset($pdo)) {
// user_id IS NULL garantit qu'on ne modifie pas un vrai compte Parent par accident
$stmt = $pdo->prepare("UPDATE pf_people SET color = ? WHERE id = ? AND user_id IS NULL");
$stmt->execute([$color, $profile_id]);
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode(['success' => true]);
exit;
}
$success = tr('set_success_color_update');
} }
} }
} }
@@ -289,9 +335,8 @@ if ($family_id) {
$mem->execute([$family_id]); $mem->execute([$family_id]);
$members = $mem->fetchAll(); $members = $mem->fetchAll();
// Récupération des profils rattachés (ceux qui n'ont pas de compte de connexion, donc user_id IS NULL)
if (isset($pdo)) { if (isset($pdo)) {
$stmtAttached = $pdo->query("SELECT id, name, role FROM pf_people WHERE user_id IS NULL ORDER BY role ASC, name ASC"); $stmtAttached = $pdo->query("SELECT id, name, role, color FROM pf_people WHERE user_id IS NULL ORDER BY role ASC, name ASC");
$attached_profiles = $stmtAttached->fetchAll(PDO::FETCH_ASSOC); $attached_profiles = $stmtAttached->fetchAll(PDO::FETCH_ASSOC);
} }
} }
@@ -310,17 +355,17 @@ if ($family_id && isset($pdo)) {
} catch (\Throwable $e) {} } catch (\Throwable $e) {}
} }
$pageTitle = "Paramètres — HouseHub"; $pageTitle = tr('set_title_page');
$activePage = "settings"; $activePage = "settings";
require __DIR__ . '/header.php'; require __DIR__ . '/header.php';
?> ?>
<div class="pf-container pf-settings-page"> <div class="pf-container pf-settings-page">
<h1 class="pf-settings-title">⚙️ Paramètres</h1> <h1 class="pf-settings-title"><?= tr('set_title_page') ?></h1>
<section class="pf-panel-card"> <section class="pf-panel-card">
<h2 class="pf-card-h2">🌐 Langue / Language</h2> <h2 class="pf-card-h2"><?= tr('set_title_lang') ?></h2>
<form method="post" class="pf-lang-form"> <form method="post" class="pf-lang-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="action" value="set_lang"> <input type="hidden" name="action" value="set_lang">
@@ -347,8 +392,8 @@ require __DIR__ . '/header.php';
<?php if ($family_id): ?> <?php if ($family_id): ?>
<section class="pf-panel-card"> <section class="pf-panel-card">
<h2 class="pf-card-h2 pf-card-h2--tight">🧩 Modules actifs</h2> <h2 class="pf-card-h2 pf-card-h2--tight"><?= tr('set_title_modules') ?></h2>
<p class="pf-muted-note">Choisissez les modules visibles dans la navigation (partagé avec tous les membres de l'espace).</p> <p class="pf-muted-note"><?= tr('set_desc_modules') ?></p>
<?php <?php
$enabledMods = $_SESSION['enabled_modules'] ?? ['calendar','budget','holidays','gifts','calendar_ios']; $enabledMods = $_SESSION['enabled_modules'] ?? ['calendar','budget','holidays','gifts','calendar_ios'];
$allModules = [ $allModules = [
@@ -389,13 +434,13 @@ require __DIR__ . '/header.php';
<section class="pf-panel-card"> <section class="pf-panel-card">
<h2 class="pf-card-h2 pf-card-h2--tight">📝 <?= htmlspecialchars(tr('liste_settings_title')) ?></h2> <h2 class="pf-card-h2 pf-card-h2--tight">📝 <?= htmlspecialchars(tr('liste_settings_title')) ?></h2>
<p class="pf-muted-note"><?= htmlspecialchars(tr('liste_settings_intro')) ?></p> <p class="pf-muted-note"><?= htmlspecialchars(tr('liste_settings_intro')) ?></p>
<form method="post" class="pf-stack-md" style="margin-top:1rem"> <form method="post" class="pf-stack-md pf-mt-md">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="action" value="grocery_history_max"> <input type="hidden" name="action" value="grocery_history_max">
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label" for="history_max"><?= htmlspecialchars(tr('liste_settings_history_max')) ?></label> <label class="pf-label" for="history_max"><?= htmlspecialchars(tr('liste_settings_history_max')) ?></label>
<input type="number" name="history_max" id="history_max" class="pf-input" style="max-width:120px" min="1" max="50" step="1" value="<?= (int) $groceryHistoryMaxSetting ?>" required> <input type="number" name="history_max" id="history_max" class="pf-input pf-input-sm" min="1" max="50" step="1" value="<?= (int) $groceryHistoryMaxSetting ?>" required>
<p class="pf-muted-note" style="margin-top:0.35rem"><?= htmlspecialchars(tr('liste_settings_history_hint')) ?></p> <p class="pf-muted-note pf-mt-xs"><?= htmlspecialchars(tr('liste_settings_history_hint')) ?></p>
</div> </div>
<button type="submit" class="pf-btn"><?= htmlspecialchars(tr('btn_save')) ?></button> <button type="submit" class="pf-btn"><?= htmlspecialchars(tr('btn_save')) ?></button>
</form> </form>
@@ -403,21 +448,21 @@ require __DIR__ . '/header.php';
<?php endif; ?> <?php endif; ?>
<section class="pf-panel-card"> <section class="pf-panel-card">
<h2 class="pf-card-h2 pf-card-h2--tight">📱 Intégration Calendrier iOS (CalDAV)</h2> <h2 class="pf-card-h2 pf-card-h2--tight"><?= tr('set_title_ios') ?></h2>
<p class="pf-muted-note">Configurez ici votre calendrier iCloud pour synchroniser les événements créés dans HouseHub.</p> <p class="pf-muted-note"><?= tr('set_desc_ios') ?></p>
<form method="post" class="pf-stack-md"> <form method="post" class="pf-stack-md">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="action" value="calendar_ios_save"> <input type="hidden" name="action" value="calendar_ios_save">
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label">Identifiant Apple (email iCloud)</label> <label class="pf-label"><?= tr('set_label_apple_id') ?></label>
<input type="text" name="icloud_username" class="pf-input" value="<?= htmlspecialchars($calendarIntegration['username'] ?? '') ?>" placeholder="nom@icloud.com" required> <input type="text" name="icloud_username" class="pf-input" value="<?= htmlspecialchars($calendarIntegration['username'] ?? '') ?>" placeholder="<?= tr('placeholder_apple_id') ?>" required>
</div> </div>
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label">Mot de passe d'app Apple</label> <label class="pf-label"><?= tr('set_label_apple_pwd') ?></label>
<input type="password" name="icloud_app_password" class="pf-input" placeholder="xxxx-xxxx-xxxx-xxxx" required> <input type="password" name="icloud_app_password" class="pf-input" placeholder="<?= tr('placeholder_apple_pwd') ?>" required>
</div> </div>
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label">URL du calendrier CalDAV</label> <label class="pf-label"><?= tr('set_label_apple_url') ?></label>
<input type="url" name="icloud_calendar_url" class="pf-input" value="<?= htmlspecialchars($calendarIntegration['calendar_url'] ?? '') ?>" placeholder="https://caldav.icloud.com/..." required> <input type="url" name="icloud_calendar_url" class="pf-input" value="<?= htmlspecialchars($calendarIntegration['calendar_url'] ?? '') ?>" placeholder="https://caldav.icloud.com/..." required>
</div> </div>
<div class="pf-flex-gap-8"> <div class="pf-flex-gap-8">
@@ -429,12 +474,12 @@ require __DIR__ . '/header.php';
<form method="post"> <form method="post">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="action" value="calendar_ios_test"> <input type="hidden" name="action" value="calendar_ios_test">
<button type="submit" class="pf-btn btn-secondary">Tester la connexion</button> <button type="submit" class="pf-btn btn-secondary"><?= tr('set_btn_test_ios') ?></button>
</form> </form>
<form method="post"> <form method="post">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="action" value="calendar_ios_disconnect"> <input type="hidden" name="action" value="calendar_ios_disconnect">
<button type="submit" class="pf-btn btn-secondary">Déconnecter</button> <button type="submit" class="pf-btn btn-secondary"><?= tr('set_btn_disconnect_ios') ?></button>
</form> </form>
</div> </div>
</section> </section>
@@ -445,13 +490,13 @@ require __DIR__ . '/header.php';
$has_custom_bg = $bg_file !== null; $has_custom_bg = $bg_file !== null;
?> ?>
<section class="pf-panel-card"> <section class="pf-panel-card">
<h2 class="pf-card-h2 pf-card-h2--tight">🖼️ Image d'accueil</h2> <h2 class="pf-card-h2 pf-card-h2--tight"><?= tr('set_title_bg') ?></h2>
<p class="pf-muted-note">Photo de fond affichée sur la page d'accueil (partagée avec tous les membres).</p> <p class="pf-muted-note"><?= tr('set_desc_bg') ?></p>
<?php if ($has_custom_bg): ?> <?php if ($has_custom_bg): ?>
<div class="pf-home-bg-preview"> <div class="pf-home-bg-preview">
<img src="/home-bg.php" alt="Fond actuel"> <img src="/home-bg.php" alt="Fond actuel">
<span class="pf-badge-overlay">Image personnalisée</span> <span class="pf-badge-overlay"><?= tr('set_badge_custom_bg') ?></span>
</div> </div>
<?php endif; ?> <?php endif; ?>
@@ -459,10 +504,10 @@ require __DIR__ . '/header.php';
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="action" value="upload_home_bg"> <input type="hidden" name="action" value="upload_home_bg">
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label">Nouvelle image (jpg, png, webp — max 10 Mo)</label> <label class="pf-label"><?= tr('set_label_new_bg') ?></label>
<input type="file" name="home_bg" class="pf-input" accept="image/jpeg,image/png,image/webp" required> <input type="file" name="home_bg" class="pf-input" accept="image/jpeg,image/png,image/webp" required>
</div> </div>
<button type="submit" class="pf-btn pf-shrink-0">Enregistrer</button> <button type="submit" class="pf-btn pf-shrink-0"><?= tr('btn_save') ?></button>
</form> </form>
<?php if ($has_custom_bg): ?> <?php if ($has_custom_bg): ?>
@@ -470,32 +515,49 @@ require __DIR__ . '/header.php';
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="action" value="reset_home_bg"> <input type="hidden" name="action" value="reset_home_bg">
<button type="submit" class="pf-btn btn-secondary pf-btn-sm-text" <button type="submit" class="pf-btn btn-secondary pf-btn-sm-text"
onclick="return confirm('Revenir à l\'image par défaut ?')">🔄 Remettre l'image par défaut</button> onclick="return confirm('<?= addslashes(tr('set_confirm_reset_bg')) ?>')"><?= tr('set_btn_reset_bg') ?></button>
</form> </form>
<?php endif; ?> <?php endif; ?>
</section> </section>
<?php endif; ?> <?php endif; ?>
<section class="pf-panel-card"> <section class="pf-panel-card">
<h2 class="pf-card-h2">👤 Mon profil</h2> <h2 class="pf-card-h2"><?= tr('set_title_profile') ?></h2>
<form method="post"> <form method="post">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="action" value="update_profile"> <input type="hidden" name="action" value="update_profile">
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label">Nom d'utilisateur</label> <label class="pf-label"><?= tr('set_label_username') ?></label>
<input type="text" class="pf-input pf-input--disabled-muted" value="<?= htmlspecialchars($user['username']) ?>" disabled> <input type="text" class="pf-input pf-input--disabled-muted" value="<?= htmlspecialchars($user['username']) ?>" disabled>
</div> </div>
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label">Prénom / Pseudo affiché</label> <label class="pf-label"><?= tr('set_label_display_name') ?></label>
<input type="text" name="display_name" class="pf-input" value="<?= htmlspecialchars($user['display_name']) ?>" required> <input type="text" name="display_name" class="pf-input" value="<?= htmlspecialchars($user['display_name']) ?>" required>
</div> </div>
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label" for="profile_color">Couleur thématique de mon profil</label> <label class="pf-label" for="profile_color">Couleur thématique de mon profil</label>
<div class="pf-color-picker-group">
<input type="color" name="color" id="profile_color" class="pf-input-color" value="<?= htmlspecialchars($user_color) ?>"> <!-- Le champ caché qui va stocker la valeur -->
<span class="pf-muted-note">Cette couleur sera utilisée pour vos grilles de budget, calendriers et indicateurs personnels.</span> <input type="hidden" name="color" id="profile_color" value="<?= htmlspecialchars($user_color) ?>">
<!-- Le panel de couleurs prédéfinies -->
<div class="pf-color-palette" id="color-palette">
<?php
$palette = ['#ef4444', '#f97316', '#f59e0b', '#84cc16', '#10b981', '#14b8a6', '#06b6d4', '#0ea5e9', '#3b82f6', '#6366f1', '#8b5cf6', '#d946ef', '#f43f5e', '#64748b'];
foreach ($palette as $c):
$isSelected = (strtolower($user_color) === $c) ? 'selected' : '';
?>
<button type="button"
class="pf-color-swatch <?= $isSelected ?>"
style="background-color: <?= $c ?>;"
data-color="<?= $c ?>"
onclick="selectProfileColor(this, '<?= $c ?>')">
</button>
<?php endforeach; ?>
</div> </div>
<span class="pf-muted-note pf-profile-color-note"><?= tr('set_desc_profile_color') ?></span>
</div> </div>
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button> <button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
@@ -503,65 +565,65 @@ require __DIR__ . '/header.php';
</section> </section>
<section class="pf-panel-card"> <section class="pf-panel-card">
<h2 class="pf-card-h2">🔒 Changer le mot de passe</h2> <h2 class="pf-card-h2"><?= tr('set_title_pwd') ?></h2>
<form method="post"> <form method="post">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="action" value="change_password"> <input type="hidden" name="action" value="change_password">
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label">Mot de passe actuel</label> <label class="pf-label"><?= tr('set_label_cur_pwd') ?></label>
<input type="password" name="current_password" class="pf-input" required placeholder="••••••"> <input type="password" name="current_password" class="pf-input" required placeholder="••••••">
</div> </div>
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label">Nouveau mot de passe</label> <label class="pf-label"><?= tr('set_label_new_pwd') ?></label>
<input type="password" name="new_password" class="pf-input" required placeholder="••••••" autocomplete="new-password"> <input type="password" name="new_password" class="pf-input" required placeholder="••••••" autocomplete="new-password">
</div> </div>
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label">Confirmer</label> <label class="pf-label"><?= tr('set_label_confirm_pwd') ?></label>
<input type="password" name="confirm_password" class="pf-input" required placeholder="••••••" autocomplete="new-password"> <input type="password" name="confirm_password" class="pf-input" required placeholder="••••••" autocomplete="new-password">
</div> </div>
<button type="submit" class="pf-btn">Modifier</button> <button type="submit" class="pf-btn"><?= tr('set_btn_edit') ?></button>
</form> </form>
</section> </section>
<?php if ($family): ?> <?php if ($family): ?>
<section class="pf-panel-card"> <section class="pf-panel-card">
<h2 class="pf-card-h2">🏠 Mon espace familial</h2> <h2 class="pf-card-h2"><?= tr('set_title_family') ?></h2>
<form method="post" class="pf-mb-section"> <form method="post" class="pf-mb-section">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="action" value="update_family_name"> <input type="hidden" name="action" value="update_family_name">
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label">Nom de l'espace</label> <label class="pf-label"><?= tr('set_label_family_name') ?></label>
<div class="pf-flex-gap-8"> <div class="pf-flex-gap-8">
<input type="text" name="family_name" class="pf-input" value="<?= htmlspecialchars($family['name']) ?>" required> <input type="text" name="family_name" class="pf-input" value="<?= htmlspecialchars($family['name']) ?>" required>
<button type="submit" class="pf-btn pf-shrink-0">Enregistrer</button> <button type="submit" class="pf-btn pf-shrink-0"><?= tr('btn_save') ?></button>
</div> </div>
</div> </div>
</form> </form>
<div class="pf-dashed-panel"> <div class="pf-dashed-panel">
<p class="pf-muted-note pf-muted-note--tight">Code d'invitation — partagez-le pour inviter quelqu'un</p> <p class="pf-muted-note pf-muted-note--tight"><?= tr('set_desc_invite') ?></p>
<div class="pf-invite-row"> <div class="pf-invite-row">
<code id="invite-code" class="pf-invite-code" <code id="invite-code" class="pf-invite-code"
onclick="copyCode()" title="Cliquer pour copier"> onclick="copyCode()" title="<?= tr('set_title_copy') ?>">
<?= htmlspecialchars($family['invite_code']) ?> <?= htmlspecialchars($family['invite_code']) ?>
</code> </code>
<span id="copy-msg" class="pf-copy-msg">✓ Copié !</span> <span id="copy-msg" class="pf-copy-msg"><?= tr('set_msg_copied') ?></span>
<form method="post" class="pf-ml-auto"> <form method="post" class="pf-ml-auto">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="action" value="regen_invite"> <input type="hidden" name="action" value="regen_invite">
<button type="submit" class="pf-btn btn-secondary pf-btn-sm-text" <button type="submit" class="pf-btn btn-secondary pf-btn-sm-text"
onclick="return confirm('Regénérer le code ? L\'ancien sera invalidé.')"> onclick="return confirm('<?= addslashes(tr('set_confirm_new_code')) ?>')">
🔄 Nouveau code <?= tr('set_btn_new_code') ?>
</button> </button>
</form> </form>
</div> </div>
<p class="pf-invite-footnote"> <p class="pf-invite-footnote">
Lien d'inscription : <strong><?= $_SERVER['HTTP_HOST'] ?>/register.php</strong> <?= tr('set_desc_register_link') ?> <strong><?= $_SERVER['HTTP_HOST'] ?>/register.php</strong>
</p> </p>
</div> </div>
<h3 class="pf-members-heading">Membres (<?= count($members) ?>)</h3> <h3 class="pf-members-heading"><?= tr('set_title_members') ?> (<?= count($members) ?>)</h3>
<div class="pf-stack-sm"> <div class="pf-stack-sm">
<?php foreach ($members as $m): ?> <?php foreach ($members as $m): ?>
<div class="pf-member-card"> <div class="pf-member-card">
@@ -569,7 +631,7 @@ require __DIR__ . '/header.php';
<strong><?= htmlspecialchars($m['display_name']) ?></strong> <strong><?= htmlspecialchars($m['display_name']) ?></strong>
<span class="pf-muted-inline">@<?= htmlspecialchars($m['username']) ?></span> <span class="pf-muted-inline">@<?= htmlspecialchars($m['username']) ?></span>
<?php if ($m['is_admin']): ?> <?php if ($m['is_admin']): ?>
<span class="pf-admin-badge">Admin</span> <span class="pf-admin-badge"><?= tr('set_badge_admin') ?></span>
<?php endif; ?> <?php endif; ?>
</div> </div>
<span class="pf-muted-tiny"><?= date('d/m/Y', strtotime($m['created_at'])) ?></span> <span class="pf-muted-tiny"><?= date('d/m/Y', strtotime($m['created_at'])) ?></span>
@@ -577,54 +639,79 @@ require __DIR__ . '/header.php';
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<h3 class="pf-members-heading" style="margin-top: 2rem;">🧸 Profils rattachés</h3> <h3 class="pf-members-heading pf-mt-lg">🧸 <?= tr('settings_attached_profiles_title') ?></h3>
<p class="pf-muted-note pf-muted-note--tight" style="margin-bottom: 1rem;">Membres gérés par le foyer (Enfants, Nounous, Proches) n'ayant pas d'accès direct.</p> <p class="pf-muted-note pf-muted-note--tight pf-mb-md"><?= tr('settings_attached_profiles_desc') ?></p>
<div class="pf-stack-sm" style="margin-bottom: 1.5rem;"> <div class="pf-stack-sm" style="margin-bottom: 1.5rem;">
<?php foreach ($attached_profiles as $p): <?php foreach ($attached_profiles as $p):
$roleBadge = 'Inconnu'; $roleBadge = tr('role_unknown');
if (in_array($p['role'], ['child', 'enfant'])) $roleBadge = '👶 Enfant'; if (in_array($p['role'], ['child', 'enfant'])) $roleBadge = tr('role_child_icon');
elseif ($p['role'] === 'helper') $roleBadge = '💼 Intervenant'; elseif ($p['role'] === 'helper') $roleBadge = tr('role_helper_icon');
elseif ($p['role'] === 'relative') $roleBadge = '👵 Proche'; elseif ($p['role'] === 'relative') $roleBadge = tr('role_relative_icon');
$pColor = htmlspecialchars($p['color'] ?? '#3b82f6');
?> ?>
<div class="pf-member-card" style="display: flex; justify-content: space-between; align-items: center;"> <div class="pf-member-card" style="display: flex; justify-content: space-between; align-items: center; border-left: 4px solid <?= $pColor ?>;">
<div> <div>
<strong><?= htmlspecialchars($p['name']) ?></strong> <strong><?= htmlspecialchars($p['name']) ?></strong>
<span class="pf-muted-inline"><?= $roleBadge ?></span> <span class="pf-muted-inline"><?= $roleBadge ?></span>
</div> </div>
<div style="display: flex; align-items: center; gap: 12px;">
<div class="pf-color-dropdown">
<button type="button" class="pf-color-trigger" style="background-color: <?= $pColor ?>;" title="<?= tr('change_color') ?>" onclick="toggleColorMenu(this)"></button>
<div class="pf-color-menu">
<?php foreach(['#ef4444', '#f97316', '#f59e0b', '#84cc16', '#10b981', '#14b8a6', '#06b6d4', '#0ea5e9', '#3b82f6', '#6366f1', '#8b5cf6', '#d946ef', '#f43f5e', '#64748b'] as $c): ?>
<button type="button" class="pf-color-swatch" style="background-color: <?= $c ?>;" onclick="updateAttachedProfileColor(<?= $p['id'] ?>, '<?= $c ?>', this)"></button>
<?php endforeach; ?>
</div>
</div>
<form method="post" style="margin:0;"> <form method="post" style="margin:0;">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="action" value="delete_attached_profile"> <input type="hidden" name="action" value="delete_attached_profile">
<input type="hidden" name="profile_id" value="<?= $p['id'] ?>"> <input type="hidden" name="profile_id" value="<?= $p['id'] ?>">
<button type="submit" class="btn-icon-action delete" title="Supprimer" style="background: none; border: none; cursor: pointer; font-size: 1.2rem; padding: 4px;" onclick="return confirm('Supprimer définitivement ce profil ?')">🗑️</button> <button type="submit" class="btn-icon-action delete" title="<?= tr('btn_delete') ?>" style="background: none; border: none; cursor: pointer; font-size: 1.2rem; padding: 4px;" onclick="return confirm('<?= addslashes(tr('confirm_delete_profile')) ?>')">🗑️</button>
</form> </form>
</div> </div>
</div>
<?php endforeach; ?> <?php endforeach; ?>
<?php if (empty($attached_profiles)): ?> <?php if (empty($attached_profiles)): ?>
<div class="pf-muted-note" style="padding: 10px; text-align: center; border: 1px dashed var(--border-light); border-radius: 8px;">Aucun profil rattaché.</div> <div class="pf-muted-note" style="padding: 10px; text-align: center; border: 1px dashed var(--border-light); border-radius: 8px;"><?= tr('settings_no_attached_profiles') ?></div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<form method="post" style="display: flex; gap: 8px; background: var(--bg-soft); padding: 12px; border-radius: 8px; border: 1px dashed var(--border-light); align-items: flex-end;"> <form method="post" style="display: flex; gap: 8px; background: var(--bg-soft); padding: 12px; border-radius: 8px; border: 1px dashed var(--border-light); align-items: flex-end; flex-wrap: wrap;">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="action" value="add_attached_profile"> <input type="hidden" name="action" value="add_attached_profile">
<div class="pf-form-group" style="margin-bottom: 0; flex: 2;"> <div class="pf-form-group" style="margin-bottom: 0; flex: 2; min-width: 120px;">
<label class="pf-label" style="font-size: 0.8rem; margin-bottom: 4px;">Prénom</label> <label class="pf-label" style="font-size: 0.8rem; margin-bottom: 4px;"><?= tr('label_firstname') ?></label>
<input type="text" name="profile_name" class="pf-input" placeholder="Ex: Pol, Carole..." required> <input type="text" name="profile_name" class="pf-input" placeholder="<?= tr('placeholder_firstname_ex') ?>" required>
</div> </div>
<div class="pf-form-group" style="margin-bottom: 0; flex: 1.5;"> <div class="pf-form-group" style="margin-bottom: 0; flex: 1.5; min-width: 120px;">
<label class="pf-label" style="font-size: 0.8rem; margin-bottom: 4px;">Rôle</label> <label class="pf-label" style="font-size: 0.8rem; margin-bottom: 4px;"><?= tr('label_role') ?></label>
<select name="profile_role" class="pf-input" required> <select name="profile_role" class="pf-input" required>
<option value="child">👶 Enfant</option> <option value="child"><?= tr('role_child_icon') ?></option>
<option value="helper">💼 Intervenant</option> <option value="helper"><?= tr('role_helper_icon') ?></option>
<option value="relative">👵 Proche</option> <option value="relative"><?= tr('role_relative_icon') ?></option>
</select> </select>
</div> </div>
<button type="submit" class="pf-btn pf-shrink-0" style="padding: 8px 16px;">Ajouter</button> <div class="pf-form-group" style="margin-bottom: 0; display: flex; flex-direction: column; align-items: flex-start;">
<label class="pf-label" style="font-size: 0.8rem; margin-bottom: 4px;"><?= tr('label_color') ?></label>
<div style="display: flex; gap: 4px; flex-wrap: wrap; padding-bottom: 4px;">
<?php foreach(['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#64748b'] as $i => $c): ?>
<label class="pf-radio-swatch" style="background-color: <?= $c ?>;" title="<?= $c ?>">
<input type="radio" name="profile_color" value="<?= $c ?>" <?= $i === 0 ? 'checked' : '' ?> style="display:none;">
</label>
<?php endforeach; ?>
</div>
</div>
<button type="submit" class="pf-btn pf-shrink-0" style="padding: 8px 16px; margin-bottom: 2px;"><?= tr('btn_add') ?></button>
</form> </form>
</section> </section>
@@ -632,7 +719,7 @@ require __DIR__ . '/header.php';
<?php if (!empty($_SESSION['user']['is_admin'])): ?> <?php if (!empty($_SESSION['user']['is_admin'])): ?>
<div class="pf-settings-admin-link"> <div class="pf-settings-admin-link">
<a href="/admin/">→ Panneau d'administration</a> <a href="/admin/"><?= tr('set_link_admin') ?></a>
</div> </div>
<?php endif; ?> <?php endif; ?>
@@ -660,7 +747,7 @@ function copyCode() {
}); });
} }
// --- GESTION DES NOTIFICATIONS TOAST (Remplaçant l'ancien bandeau vert) --- // --- GESTION DES NOTIFICATIONS TOAST ---
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
<?php if ($success): ?> <?php if ($success): ?>
if (typeof showToast === 'function') { if (typeof showToast === 'function') {
@@ -678,6 +765,70 @@ document.addEventListener('DOMContentLoaded', () => {
} }
<?php endif; ?> <?php endif; ?>
}); });
function selectProfileColor(btn, hexColor) {
// 1. Mettre à jour l'input caché
document.getElementById('profile_color').value = hexColor;
// 2. Mettre à jour l'UI (enlever la classe selected partout, l'ajouter sur le bouton cliqué)
const swatches = document.querySelectorAll('#color-palette .pf-color-swatch');
swatches.forEach(el => el.classList.remove('selected'));
btn.classList.add('selected');
}
function toggleColorMenu(btn) {
// Fermer tous les autres popovers
document.querySelectorAll('.pf-color-menu').forEach(menu => {
if (menu !== btn.nextElementSibling) menu.classList.remove('is-open');
});
// Ouvrir/Fermer celui cliqué
btn.nextElementSibling.classList.toggle('is-open');
}
// Fermer le menu si on clique en dehors
document.addEventListener('click', (e) => {
if (!e.target.closest('.pf-color-dropdown')) {
document.querySelectorAll('.pf-color-menu').forEach(menu => menu.classList.remove('is-open'));
}
});
// --- MISE À JOUR DYNAMIQUE COULEUR PROFIL RATTACHÉ ---
async function updateAttachedProfileColor(profileId, newColor, btnElement) {
const fd = new FormData();
fd.append('action', 'update_attached_profile_color');
fd.append('profile_id', profileId);
fd.append('color', newColor);
fd.append('csrf_token', window.CSRF_TOKEN); // Sécurité absolue
try {
const res = await pachaFetch('/settings.php', { method: 'POST', body: fd });
if (res && res.success) {
if (typeof showToast === 'function') {
showToast(window.I18N['color_updated'] || 'Couleur mise à jour !', 'success');
}
const dropdown = btnElement.closest('.pf-color-dropdown');
if (dropdown) {
// Màj visuelle de la pastille déclencheur
dropdown.querySelector('.pf-color-trigger').style.backgroundColor = newColor;
dropdown.querySelector('.pf-color-menu').classList.remove('is-open');
}
// Màj de la bordure gauche de la carte
const card = btnElement.closest('.pf-member-card');
if (card) {
card.style.borderLeftColor = newColor;
}
} else {
if (typeof showToast === 'function') showToast(window.I18N['error_occured'] || 'Erreur', 'error');
}
} catch (err) {
console.error(err);
if (typeof showToast === 'function') showToast(window.I18N['error_occured'] || 'Erreur', 'error');
}
}
</script> </script>
<?php require __DIR__ . '/footer.php'; ?> <?php require __DIR__ . '/footer.php'; ?>