refacto parent Calendrier budget et couleur profil.
Deploy HouseHub / deploy (push) Successful in 2s

This commit is contained in:
2026-05-20 17:56:25 +02:00
parent 4130b03a68
commit f8e2ad67ee
15 changed files with 1153 additions and 675 deletions
+19 -13
View File
@@ -151,12 +151,15 @@ L'architecture de HouseHub isole chaque foyer dans sa propre base de données (`
Afin de transformer complètement l'application héritée de Pacha Family en un produit générique et autonome pour chaque espace familial, les chantiers suivants doivent être menés.
🟢 Niveau 1 : Facile (Constantes et configurations globales)
[x] Configuration de base : Extraire la devise (CURRENCY) et la zone de vacances scolaires (ZONE_SCOLAIRE) du fichier config.php pour les stocker dans les paramètres d'espace en BDD.
[x] Suppression des IDs statiques : Supprimer les IDs parents fixes (ID_ALEX = 2, ID_LAIA = 3) dans config.php au profit d'une lecture dynamique basée sur les utilisateurs réels de la famille connectée (liens via la table pf_people).
[x] Charte graphique dynamique : Remplacer les couleurs codées en dur (bleu/orange) par des sélecteurs natifs dans l'espace "Paramètres", en générant les variables CSS et leurs nuances (via color-mix()) à la volée.
[ ] Filtres API Éducation Nationale : Rendre dynamique la zone de l'API (zones LIKE '%Zone C%' dans family-calendar.js) pour s'adapter à la région de chaque foyer.
[ ] Identifiant Planka : Variabiliser le project_id Planka par défaut écrit en dur dans planka/api.php.
[ ] Suppression des IDs statiques : Supprimer les IDs parents fixes (ID_ALEX = 2, ID_LAIA = 3) dans config.php au profit d'une lecture dynamique basée sur les utilisateurs réels de la famille connectée.
🟡 Niveau 2 : Intermédiaire (Listes et dictionnaires en dur)
[ ] Gestion des membres (Cadeaux) : Dans gift-list.php, remplacer la liste d'enfants ($children) et d'adultes ($baseAdults, $extraAdults) codée en dur par une lecture dynamique des membres du foyer déclarés en BDD.
[ ] Occasions festives (Cadeaux) : Rendre configurables les occasions spéciales (TIO, NOEL, ROIS, ANNIV, SANT).
[ ] Configuration des catégories (Budget) : Dans budget/views/suivi.php, migrer le tableau $categoriesConfig (FMCG, Essence, École, etc.), leurs couleurs et leurs suggestions de magasins associés vers un stockage paramétrable.
@@ -164,7 +167,9 @@ Afin de transformer complètement l'application héritée de Pacha Family en un
[ ] Types de carburants (Garage) : Variabiliser la liste statique des carburants dans garage.php.
🔴 Niveau 3 : Complexe (Logique métier et Schéma SQL)
[ ] Refonte relationnelle de la répartition (Budget) : Modifier la structure de la table pf_alloc_values pour supprimer les colonnes figées amount_alex et amount_laia et basculer sur un modèle relationnel (alloc_id, user_id, amount) capable de gérer n'importe quel nombre d'adultes (1, 2, 3 ou plus).
[x] Refonte relationnelle du Budget (Prévisionnel) : Refonte totale de budget_prev.php. Modification de la table pf_alloc_values pour basculer sur des colonnes génériques (amount_p1, amount_p2) et suppression définitive de toute logique conditionnelle liée aux prénoms des concepteurs originaux.
[ ] Refonte globale de la répartition (Budget) : Étendre le modèle relationnel (alloc_id, user_id, amount) au reste de l'application pour gérer n'importe quel nombre d'adultes (1, 2, 3 ou plus).
[ ] Destinations de transferts (Budget) : Rendre dynamiques les cibles de virements prévisionnels (vers L.Pol, vers L.Pep, etc.) en fonction des comptes épargne créés par la famille.
[ ] Onglets d'Épargne : Générer dynamiquement les sous-onglets de la vue epargne.php à partir des membres réels au lieu des blocs statiques Alex, Laia et Nens.
[ ] Types de Garde (Calendrier) : Supprimer les types d'événements et compteurs codés en dur (OFF_CAROLE, EXTRA_OFF_CAROLE, CENTRE, AVIS, PEP_SICK) pour permettre à chaque foyer de définir ses propres modalités de garde et compteurs de congés associés.
@@ -174,14 +179,15 @@ Afin de transformer complètement l'application héritée de Pacha Family en un
## 📌 Versioning
| Version | Changements |
| ------- | ------------------------------------------------------------------------------------------ |
| v2.2.0 | Multi-tenant (p.1) : Paramètres dynamiques du foyer (€, Zone) via BDD + script migration |
| v2.1.0 | Module Planka : Kanban natif connecté à Planka via API proxy PHP |
| v2.0.0 | Module PrintVault : viewer 3D STL/3MF/GCode, stockage local, trajectoires GCode illimitées |
| v1.5.0 | Modules Todo (rappels Discord), Mémo, Courses (liste partagée) |
| v1.4.0 | Synchronisation CalDAV iOS |
| v1.3.0 | Optimisation SQL — suppression des requêtes N+1, temps de réponse ÷3 |
| v1.2.0 | Modernisation UI — Toasts, confirmations sur-mesure |
| v1.1.0 | Refonte expérience mobile (Bottom Sheets) |
| v1.0.0 | Version stable initiale — Budget, Calendrier, Voyages, Cadeaux, Garage |
| Version | Changements |
| ------- | ------------------------------------------------------------------------------------------------------------------------- |
| v2.3.0 | Multi-tenant (p.2) : Refonte BDD Budget (p1, p2), suppression IDs statiques et thèmes de profils dynamiques (color-mix()) |
| v2.2.0 | Multi-tenant (p.1) : Paramètres dynamiques du foyer (€, Zone) via BDD + script migration |
| v2.1.0 | Module Planka : Kanban natif connecté à Planka via API proxy PHP |
| v2.0.0 | Module PrintVault : viewer 3D STL/3MF/GCode, stockage local, trajectoires GCode illimitées |
| v1.5.0 | Modules Todo (rappels Discord), Mémo, Courses (liste partagée) |
| v1.4.0 | Synchronisation CalDAV iOS |
| v1.3.0 | Optimisation SQL — suppression des requêtes N+1, temps de réponse ÷3 |
| v1.2.0 | Modernisation UI — Toasts, confirmations sur-mesure |
| v1.1.0 | Refonte expérience mobile (Bottom Sheets) |
| v1.0.0 | Version stable initiale — Budget, Calendrier, Voyages, Cadeaux, Garage |
+7 -7
View File
@@ -24,10 +24,13 @@ CREATE TABLE IF NOT EXISTS pf_foyer_settings (
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '', 'C');
-- ─── Personnes ────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_people (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `pf_people` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`user_id` int(11) DEFAULT NULL,
`role` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
-- ─── Calendrier familial ──────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_events (
@@ -243,9 +246,6 @@ SET FOREIGN_KEY_CHECKS = 1;
INSERT IGNORE INTO pf_users (id, username, password_hash, display_name)
VALUES (1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin');
-- Personnes (IDs fixes correspondant aux constantes dans config.php)
INSERT IGNORE INTO pf_people (id, name) VALUES (2, 'Alex');
INSERT IGNORE INTO pf_people (id, name) VALUES (3, 'Laia');
-- ─── Garage Manager ───────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_vehicles (
+24 -10
View File
@@ -8,6 +8,12 @@ require_login();
require __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/i18n.php';
$stmtPeople = $pdo->query("SELECT id, name, user_id, role FROM pf_people ORDER BY id ASC");
$familyPeople = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
$parents = array_values(array_filter($familyPeople, function($p) {
return strtolower($p['role'] ?? '') === 'parent';
}));
$pageTitle = tr('fc_page_title');
$activePage = "family-calendar";
$mainClass = "pf-family-calendar";
@@ -70,8 +76,12 @@ require __DIR__ . '/header.php';
<div class="pf-form-group">
<label class="pf-label"><?= tr('fc_label_person') ?></label>
<select id="snapPerson" class="pf-input" required>
<option value="2">Alex</option>
<option value="3">Laia</option>
<option value="" disabled selected>-- <?= tr('fc_choose_person') ?> --</option>
<?php foreach ($familyPeople as $person): ?>
<option value="<?= htmlspecialchars($person['id']) ?>">
<?= htmlspecialchars($person['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="pf-form-group">
@@ -169,20 +179,24 @@ require __DIR__ . '/header.php';
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_avis') ?></span></th>
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_pep_sick') ?></span></th>
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_presence') ?></span></th>
<th colspan="6" class="col-alex header-group">ALEX</th>
<th colspan="6" class="col-laia header-group">LAIA</th>
<th colspan="6" class="col-alex header-group"><?= htmlspecialchars(strtoupper($parents[0]['name'] ?? 'PARENT 1')) ?></th>
<th colspan="6" class="col-laia header-group"><?= htmlspecialchars(strtoupper($parents[1]['name'] ?? 'PARENT 2')) ?></th>
</tr>
<tr>
<th colspan="2" class="col-alex-sub">CP</th><th colspan="2" class="col-alex-sub">JRA</th><th colspan="2" class="col-alex-sub">JA</th>
<th colspan="2" class="col-laia-sub">CP</th><th colspan="2" class="col-laia-sub">JRA</th><th colspan="2" class="col-laia-sub">JA</th>
</tr>
<tr>
<th class="col-alex-sub col-alex-av"><?= tr('fc_col_av') ?></th><th class="col-alex-sub col-alex-use"><?= tr('fc_col_use') ?></th>
<th class="col-alex-sub col-alex-av"><?= tr('fc_col_av') ?></th><th class="col-alex-sub col-alex-use"><?= tr('fc_col_use') ?></th>
<th class="col-alex-sub col-alex-av"><?= tr('fc_col_av') ?></th><th class="col-alex-sub col-alex-use"><?= tr('fc_col_use') ?></th>
<th class="col-laia-sub col-laia-av"><?= tr('fc_col_av') ?></th><th class="col-laia-sub col-laia-use"><?= tr('fc_col_use') ?></th>
<th class="col-laia-sub col-laia-av"><?= tr('fc_col_av') ?></th><th class="col-laia-sub col-laia-use"><?= tr('fc_col_use') ?></th>
<th class="col-laia-sub col-laia-av"><?= tr('fc_col_av') ?></th><th class="col-laia-sub col-laia-use"><?= tr('fc_col_use') ?></th>
<?php foreach ($parents as $index => $parent):
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
?>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody id="planningBody"></tbody>
+261 -68
View File
@@ -307,7 +307,6 @@ body.pf-home .pf-mobile-close-btn {
.pf-muted-note {
color: var(--text-muted);
font-size: 0.85rem;
margin-bottom: 20px;
}
.pf-muted-note.pf-muted-note--tight {
margin-bottom: 8px;
@@ -359,6 +358,58 @@ body.pf-home .pf-mobile-close-btn {
gap: 8px;
}
/* --- SÉLECTEUR DE COULEUR THÉMATIQUE (SETTINGS) --- */
.pf-color-picker-group {
display: flex;
align-items: center;
gap: 12px;
margin-top: 6px;
}
.pf-input-color {
/* On réduit la taille pour le rendre plus subtil */
width: 36px;
height: 36px;
/* On enlève les marges/paddings par défaut */
padding: 0;
cursor: pointer;
/* On le rend complètement rond, c'est plus moderne */
border-radius: 50%;
border: 2px solid #e2e8f0;
/* On cache le fond par défaut du navigateur */
background: transparent;
box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.05));
transition: all 0.2s ease;
/* Optionnel : masquer la bordure interne sur Chrome/Safari */
-webkit-appearance: none;
}
/* On s'assure que le carré de couleur interne (Webkit) remplit tout et est rond */
.pf-input-color::-webkit-color-swatch-wrapper {
padding: 0;
}
.pf-input-color::-webkit-color-swatch {
border: none;
border-radius: 50%;
}
/* Même chose pour Firefox */
.pf-input-color::-moz-color-swatch {
border: none;
border-radius: 50%;
}
.pf-input-color:hover {
border-color: #94a3b8;
transform: scale(1.05);
}
.pf-color-picker-group .pf-muted-note {
font-size: 0.85rem;
color: var(--text-muted, #64748b);
line-height: 1.4;
}
/* Tuile module (checkbox) — état coché via :has(), compatible thème */
.pf-module-tile {
display: flex;
@@ -968,21 +1019,23 @@ p {
DARK MODE — [data-theme="dark"] sur <html>
══════════════════════════════════════════════════════════════ */
[data-theme="dark"] {
--text-main: #e6edf3;
--text-muted: #8b949e;
--bg-page: #0d1117;
--bg-panel: #161b22;
--text-main: #e6edf3;
--text-muted: #8b949e;
--bg-page: #0d1117;
--bg-panel: #161b22;
--border-light: #30363d;
--shadow: 0 4px 6px -1px rgba(0,0,0,.4), 0 2px 4px -2px rgba(0,0,0,.3);
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,.5), 0 4px 6px -4px rgba(0,0,0,.4);
--bg-subtle: rgba(255,255,255,.06);
--bg-soft: rgba(59,130,246,.15);
--bg-danger-soft: rgba(239,68,68,.1);
--shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3);
--shadow-lg:
0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -4px rgba(0, 0, 0, 0.4);
--bg-subtle: rgba(255, 255, 255, 0.06);
--bg-soft: rgba(59, 130, 246, 0.15);
--bg-danger-soft: rgba(239, 68, 68, 0.1);
--bg-overlay: rgba(0, 0, 0, 0.55);
--text-danger-soft: #f87171;
--border-strong: #484f58;
--border-danger-soft: rgba(239,68,68,.3);
--focus-ring: rgba(59,130,246,.25);
--border-danger-soft: rgba(239, 68, 68, 0.3);
--focus-ring: rgba(59, 130, 246, 0.25);
--alert-success-bg: rgba(34, 197, 94, 0.12);
--alert-success-border: rgba(34, 197, 94, 0.45);
--alert-success-text: #86efac;
@@ -993,67 +1046,207 @@ p {
--badge-admin-bg: rgba(59, 130, 246, 0.2);
--badge-admin-text: #93c5fd;
}
[data-theme="dark"] body { background: var(--bg-page); color: var(--text-main); }
[data-theme="dark"] .pf-header { background: var(--bg-panel); border-color: var(--border-light); }
[data-theme="dark"] .pf-logo { color: var(--primary); }
[data-theme="dark"] .pf-nav-link { color: var(--text-muted); }
[data-theme="dark"] .pf-nav-link:hover { background: rgba(255,255,255,.06); color: var(--text-main); }
[data-theme="dark"] .pf-nav-link--active { background: rgba(59,130,246,.15); color: var(--primary); }
[data-theme="dark"] .pf-user-badge { color: var(--text-muted); }
[data-theme="dark"] .pf-logout-btn { color: #f87171; }
[data-theme="dark"] .pf-logout-btn:hover { background: rgba(239,68,68,.1); border-color: rgba(239,68,68,.3); }
[data-theme="dark"] .pf-burger-btn { color: var(--text-main); }
[data-theme="dark"] .pf-mobile-menu { background: var(--bg-panel); color: var(--text-main); }
[data-theme="dark"] .pf-mobile-menu-header { background: var(--bg-page); border-color: var(--border-light); }
[data-theme="dark"] .pf-mobile-nav-link { color: var(--text-main); border-color: var(--border-light); }
[data-theme="dark"] .pf-mobile-nav-link:active { background: rgba(255,255,255,.06); }
[data-theme="dark"] .pf-module-card { background: var(--bg-panel); border-color: var(--border-light); }
[data-theme="dark"] .pf-module-card:hover { border-color: #484f58; box-shadow: var(--shadow); }
[data-theme="dark"] .pf-card-title { color: var(--text-main); }
[data-theme="dark"] .pf-card-desc { color: var(--text-muted); }
[data-theme="dark"] h1, [data-theme="dark"] h2, [data-theme="dark"] h3 { color: var(--text-main); }
[data-theme="dark"] p { color: var(--text-muted); }
[data-theme="dark"] .pf-btn.btn-secondary { background: var(--bg-panel); color: var(--text-muted); border-color: var(--border-light); }
[data-theme="dark"] .pf-btn.btn-secondary:hover { background: rgba(255,255,255,.05); color: var(--text-main); border-color: #484f58; }
[data-theme="dark"] .pf-input { background: #1c2128; border-color: var(--border-light); color: var(--text-main); }
[data-theme="dark"] .pf-input::placeholder { color: #484f58; }
[data-theme="dark"] .pf-input:focus { background: #1c2128; border-color: var(--primary); }
[data-theme="dark"] .pf-table { background: var(--bg-panel); }
[data-theme="dark"] .pf-table th { background: #1c2128; color: var(--text-muted); border-color: var(--border-light); }
[data-theme="dark"] .pf-table td { border-color: rgba(48,54,61,.5); color: var(--text-main); }
[data-theme="dark"] .pf-table tbody tr:hover { background: rgba(255,255,255,.03); }
[data-theme="dark"] .pf-modal-content { background: var(--bg-panel); border: 1px solid var(--border-light); }
[data-theme="dark"] .pf-modal-title { border-color: var(--border-light); color: var(--text-main); }
[data-theme="dark"] .modal-footer { border-color: var(--border-light); }
[data-theme="dark"] .pf-footer { background: var(--bg-panel); border-color: var(--border-light); }
[data-theme="dark"] .pf-toast { background: var(--bg-panel); color: var(--text-main); box-shadow: 0 10px 25px rgba(0,0,0,.4); }
[data-theme="dark"] .pf-login-card { background: var(--bg-panel); border-color: var(--border-light); }
[data-theme="dark"] .pf-login-error { background: rgba(239,68,68,.1); border-color: rgba(239,68,68,.3); }
[data-theme="dark"] .btn-icon-action.edit:hover { background: rgba(59,130,246,.15); }
[data-theme="dark"] .btn-icon-action.delete:hover { background: rgba(239,68,68,.1); }
[data-theme="dark"] body {
background: var(--bg-page);
color: var(--text-main);
}
[data-theme="dark"] .pf-header {
background: var(--bg-panel);
border-color: var(--border-light);
}
[data-theme="dark"] .pf-logo {
color: var(--primary);
}
[data-theme="dark"] .pf-nav-link {
color: var(--text-muted);
}
[data-theme="dark"] .pf-nav-link:hover {
background: rgba(255, 255, 255, 0.06);
color: var(--text-main);
}
[data-theme="dark"] .pf-nav-link--active {
background: rgba(59, 130, 246, 0.15);
color: var(--primary);
}
[data-theme="dark"] .pf-user-badge {
color: var(--text-muted);
}
[data-theme="dark"] .pf-logout-btn {
color: #f87171;
}
[data-theme="dark"] .pf-logout-btn:hover {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
}
[data-theme="dark"] .pf-burger-btn {
color: var(--text-main);
}
[data-theme="dark"] .pf-mobile-menu {
background: var(--bg-panel);
color: var(--text-main);
}
[data-theme="dark"] .pf-mobile-menu-header {
background: var(--bg-page);
border-color: var(--border-light);
}
[data-theme="dark"] .pf-mobile-nav-link {
color: var(--text-main);
border-color: var(--border-light);
}
[data-theme="dark"] .pf-mobile-nav-link:active {
background: rgba(255, 255, 255, 0.06);
}
[data-theme="dark"] .pf-module-card {
background: var(--bg-panel);
border-color: var(--border-light);
}
[data-theme="dark"] .pf-module-card:hover {
border-color: #484f58;
box-shadow: var(--shadow);
}
[data-theme="dark"] .pf-card-title {
color: var(--text-main);
}
[data-theme="dark"] .pf-card-desc {
color: var(--text-muted);
}
[data-theme="dark"] h1,
[data-theme="dark"] h2,
[data-theme="dark"] h3 {
color: var(--text-main);
}
[data-theme="dark"] p {
color: var(--text-muted);
}
[data-theme="dark"] .pf-btn.btn-secondary {
background: var(--bg-panel);
color: var(--text-muted);
border-color: var(--border-light);
}
[data-theme="dark"] .pf-btn.btn-secondary:hover {
background: rgba(255, 255, 255, 0.05);
color: var(--text-main);
border-color: #484f58;
}
[data-theme="dark"] .pf-input {
background: #1c2128;
border-color: var(--border-light);
color: var(--text-main);
}
[data-theme="dark"] .pf-input::placeholder {
color: #484f58;
}
[data-theme="dark"] .pf-input:focus {
background: #1c2128;
border-color: var(--primary);
}
[data-theme="dark"] .pf-table {
background: var(--bg-panel);
}
[data-theme="dark"] .pf-table th {
background: #1c2128;
color: var(--text-muted);
border-color: var(--border-light);
}
[data-theme="dark"] .pf-table td {
border-color: rgba(48, 54, 61, 0.5);
color: var(--text-main);
}
[data-theme="dark"] .pf-table tbody tr:hover {
background: rgba(255, 255, 255, 0.03);
}
[data-theme="dark"] .pf-modal-content {
background: var(--bg-panel);
border: 1px solid var(--border-light);
}
[data-theme="dark"] .pf-modal-title {
border-color: var(--border-light);
color: var(--text-main);
}
[data-theme="dark"] .modal-footer {
border-color: var(--border-light);
}
[data-theme="dark"] .pf-footer {
background: var(--bg-panel);
border-color: var(--border-light);
}
[data-theme="dark"] .pf-toast {
background: var(--bg-panel);
color: var(--text-main);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
}
[data-theme="dark"] .pf-login-card {
background: var(--bg-panel);
border-color: var(--border-light);
}
[data-theme="dark"] .pf-login-error {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
}
[data-theme="dark"] .btn-icon-action.edit:hover {
background: rgba(59, 130, 246, 0.15);
}
[data-theme="dark"] .btn-icon-action.delete:hover {
background: rgba(239, 68, 68, 0.1);
}
/* ─── Pill toggle thème ────────────────────────────────────── */
.theme-switch {
display: inline-flex; align-items: center; gap: 5px;
background: none; border: none; cursor: pointer;
padding: 3px 5px; border-radius: 8px; transition: background .15s;
display: inline-flex;
align-items: center;
gap: 5px;
background: none;
border: none;
cursor: pointer;
padding: 3px 5px;
border-radius: 8px;
transition: background 0.15s;
}
.theme-switch:hover {
background: #f1f5f9;
}
[data-theme="dark"] .theme-switch:hover {
background: rgba(255, 255, 255, 0.08);
}
.ts-sun,
.ts-moon {
font-size: 0.85rem;
line-height: 1;
transition: opacity 0.2s;
user-select: none;
}
[data-theme="dark"] .ts-sun {
opacity: 0.3;
}
.ts-moon {
opacity: 0.3;
}
[data-theme="dark"] .ts-moon {
opacity: 1;
}
.theme-switch:hover { background: #f1f5f9; }
[data-theme="dark"] .theme-switch:hover { background: rgba(255,255,255,.08); }
.ts-sun, .ts-moon { font-size: .85rem; line-height: 1; transition: opacity .2s; user-select: none; }
[data-theme="dark"] .ts-sun { opacity: .3; }
.ts-moon { opacity: .3; }
[data-theme="dark"] .ts-moon { opacity: 1; }
.ts-track {
position: relative; width: 36px; height: 20px;
background: #cbd5e1; border-radius: 999px;
flex-shrink: 0; transition: background .25s;
position: relative;
width: 36px;
height: 20px;
background: #cbd5e1;
border-radius: 999px;
flex-shrink: 0;
transition: background 0.25s;
}
[data-theme="dark"] .ts-track {
background: #2563eb;
}
[data-theme="dark"] .ts-track { background: #2563eb; }
.ts-thumb {
position: absolute; top: 3px; left: 3px;
width: 14px; height: 14px; border-radius: 50%;
background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.3);
transition: transform .25s cubic-bezier(.4,0,.2,1);
position: absolute;
top: 3px;
left: 3px;
width: 14px;
height: 14px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
[data-theme="dark"] .ts-thumb {
transform: translateX(16px);
}
[data-theme="dark"] .ts-thumb { transform: translateX(16px); }
+5 -3
View File
@@ -130,10 +130,12 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
*/
window.I18N = <?php echo json_encode($current_translations_array ?? [], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); ?>;
// ✅ Injection sécurisée (évite le crash si config.php n'est pas chargé)
// ✅ Injection sécurisée
window.CONFIG = {
ID_ALEX: <?php echo defined('ID_ALEX') ? ID_ALEX : 2; ?>,
ID_LAIA: <?php echo defined('ID_LAIA') ? ID_LAIA : 3; ?>,
// Liste complète des profils de la famille (Membres, Nounou, Enfants)
PEOPLE: <?= isset($familyPeople) ? json_encode($familyPeople) : '[]' ?>,
// ID du user global connecté (pour savoir "qui" clique)
CURRENT_USER_ID: <?= isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : 'null' ?>,
CURRENCY: '<?php echo defined('CURRENCY') ? CURRENCY : "€"; ?>',
ZONE_SCOLAIRE: '<?php echo defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : "C"; ?>'
};
+5
View File
@@ -54,3 +54,8 @@ function verify_csrf(?string $token): bool
}
return hash_equals($_SESSION['csrf_token'], $token);
}
if (!isset($familyPeople) && !empty($family_id)) {
require_once __DIR__ . '/db.php';
$stmtPeople = $pdo->query("SELECT id, name, user_id FROM pf_people ORDER BY id ASC");
$familyPeople = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
}
-7
View File
@@ -1,7 +0,0 @@
<?php
// includes/config.php
// HouseHub Config - Version dynamique
if (!defined('ID_ALEX')) {
define('ID_ALEX', 2);
define('ID_LAIA', 3);
}
-4
View File
@@ -1,8 +1,4 @@
<?php
if (file_exists(__DIR__ . '/config.php')) {
require_once __DIR__ . '/config.php';
}
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
+125 -20
View File
@@ -1,46 +1,151 @@
<?php
// Script de migration pour mettre à jour TOUTES les bases de données familiales
// Script de migration : Refonte globale Multi-Tenant (pf_people & Budget)
require_once __DIR__ . '/includes/meta_db.php';
echo "<h1>🚀 Début de la migration...</h1>";
echo "<h1>🚀 Début de la migration Globale ...</h1>";
// 1. Récupérer toutes les bases de données des familles
$stmt = $meta_pdo->query("SELECT name, db_name FROM families WHERE db_name != ''");
$stmt = $meta_pdo->query("SELECT id, name, db_name FROM families WHERE db_name != ''");
$families = $stmt->fetchAll();
$host = getenv('DB_HOST') ?: 'househub-db';
$user = getenv('DB_USER') ?: 'househub';
$pass = getenv('DB_PASS') ?: 'changeme';
// 2. Boucler sur chaque famille
foreach ($families as $f) {
$db_name = $f['db_name'];
echo "Mise à jour de la famille <strong>{$f['name']}</strong> (Base : $db_name) ... ";
$family_id = $f['id'];
echo "<h3>Mise à jour de <strong>{$f['name']}</strong> ($db_name)</h3><ul>";
try {
$fam_pdo = new PDO("mysql:host=$host;dbname=$db_name;charset=utf8mb4", $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
// 3. Injecter la nouvelle table
$sql = "
CREATE TABLE IF NOT EXISTS pf_foyer_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
currency VARCHAR(10) NOT NULL DEFAULT '€',
zone_scolaire VARCHAR(5) NOT NULL DEFAULT 'C',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
// ==========================================
// 1. GESTION DE PF_PEOPLE (user_id, role, color)
// ==========================================
echo "<li><strong>Table pf_people :</strong> ";
// user_id
try {
$fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN user_id INT NULL DEFAULT NULL");
echo "<span style='color:green'>user_id OK</span> - ";
} catch (\PDOException $e) {
if ($e->getCode() == '42S21' || strpos($e->getMessage(), '1060') !== false) {
echo "<span style='color:gray'>user_id déjà présent</span> - ";
} else { throw $e; }
}
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C');
";
// Auto-mapping
$stmtUsers = $meta_pdo->prepare("SELECT id, username FROM users WHERE family_id = ?");
$stmtUsers->execute([$family_id]);
$users = $stmtUsers->fetchAll();
$mappedCount = 0;
foreach ($users as $u) {
$stmtUpdate = $fam_pdo->prepare("UPDATE pf_people SET user_id = ? WHERE LOWER(name) = LOWER(?) AND user_id IS NULL");
$stmtUpdate->execute([$u['id'], $u['username']]);
$mappedCount += $stmtUpdate->rowCount();
}
echo "<span style='color:blue'>$mappedCount profils liés</span> - ";
$fam_pdo->exec($sql);
echo "<span style='color:green'>✅ OK</span><br>";
// role
try {
$fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN role VARCHAR(50) DEFAULT NULL");
echo "<span style='color:green'>role OK</span> - ";
} catch (\PDOException $e) {
if ($e->getCode() == '42S21' || strpos($e->getMessage(), '1060') !== false) {
echo "<span style='color:gray'>role déjà présent</span> - ";
} else { throw $e; }
}
// 🟢 NOUVEAU : Colonne color
try {
$fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN color VARCHAR(7) DEFAULT '#0891b2'");
echo "<span style='color:green'>colonne 'color' ajoutée</span> - ";
} catch (\PDOException $e) {
if ($e->getCode() == '42S21' || strpos($e->getMessage(), '1060') !== false) {
echo "<span style='color:gray'>colonne 'color' déjà présente</span> - ";
} else { throw $e; }
}
// Configuration des valeurs par défaut et des rôles
$fam_pdo->exec("
UPDATE pf_people SET role = 'parent' WHERE user_id IS NOT NULL;
UPDATE pf_people SET role = 'nounou' WHERE LOWER(name) = 'carole';
");
// On donne des couleurs distinctes par défaut aux deux parents (P1 = Cyan, P2 = Orange)
$stmtP = $fam_pdo->query("SELECT id FROM pf_people WHERE role = 'parent' ORDER BY id ASC");
$pIds = $stmtP->fetchAll(PDO::FETCH_COLUMN);
if (isset($pIds[0])) {
$fam_pdo->exec("UPDATE pf_people SET color = '#0891b2' WHERE id = " . (int)$pIds[0]);
}
if (isset($pIds[1])) {
$fam_pdo->exec("UPDATE pf_people SET color = '#f59e0b' WHERE id = " . (int)$pIds[1]);
}
echo "<span style='color:blue'>Rôles et Couleurs initialisés.</span></li>";
// ==========================================
// 2. GESTION DU BUDGET (pf_alloc_values)
// ==========================================
echo "<li><strong>Table pf_alloc_values :</strong> ";
// Vérification et renommage de amount_alex
$checkAlex = $fam_pdo->query("SHOW COLUMNS FROM pf_alloc_values LIKE 'amount_alex'")->rowCount();
if ($checkAlex > 0) {
// Utilisation de CHANGE pour compatibilité maximale avec les anciennes versions MySQL
$fam_pdo->exec("ALTER TABLE pf_alloc_values CHANGE amount_alex amount_p1 FLOAT DEFAULT 0");
echo "<span style='color:green'>amount_alex -> amount_p1</span> - ";
} else {
echo "<span style='color:gray'>amount_p1 OK</span> - ";
}
// Vérification et renommage de amount_laia
$checkLaia = $fam_pdo->query("SHOW COLUMNS FROM pf_alloc_values LIKE 'amount_laia'")->rowCount();
if ($checkLaia > 0) {
$fam_pdo->exec("ALTER TABLE pf_alloc_values CHANGE amount_laia amount_p2 FLOAT DEFAULT 0");
echo "<span style='color:green'>amount_laia -> amount_p2</span></li>";
} else {
echo "<span style='color:gray'>amount_p2 OK</span></li>";
}
// ==========================================
// 3. GESTION DU BUDGET (pf_alloc_categories)
// ==========================================
echo "<li><strong>Table pf_alloc_categories :</strong> ";
$stmtCats1 = $fam_pdo->exec("UPDATE pf_alloc_categories SET name = 'Eco P1' WHERE name LIKE 'Eco Alex%'");
$stmtCats2 = $fam_pdo->exec("UPDATE pf_alloc_categories SET name = 'Eco P2' WHERE name LIKE 'Eco Laia%'");
echo "<span style='color:green'>Catégories 'Eco' génériques mises à jour (" . ($stmtCats1 + $stmtCats2) . " lignes).</span></li>";
// ==========================================
// 4. GESTION DU BUDGET (pf_salary_config)
// ==========================================
echo "<li><strong>Table pf_salary_config :</strong> ";
// On récupère les vrais prénoms pour les remplacer dans les configs de salaires
$stmtParents = $fam_pdo->query("SELECT name FROM pf_people WHERE role = 'parent' ORDER BY id ASC");
$parents = $stmtParents->fetchAll();
if (count($parents) >= 2) {
$p1_name = $parents[0]['name'];
$p2_name = $parents[1]['name'];
$stmtSal1 = $fam_pdo->prepare("UPDATE pf_salary_config SET person = ? WHERE person = 'Alex'");
$stmtSal1->execute([$p1_name]);
$stmtSal2 = $fam_pdo->prepare("UPDATE pf_salary_config SET person = ? WHERE person = 'Laia'");
$stmtSal2->execute([$p2_name]);
echo "<span style='color:green'>Salaires mappés vers $p1_name et $p2_name.</span></li>";
} else {
echo "<span style='color:orange'>Pas assez de parents trouvés pour mapper les salaires.</span></li>";
}
} catch (\PDOException $e) {
echo "<span style='color:red'>❌ Erreur : " . $e->getMessage() . "</span><br>";
echo "<li style='color:red'>❌ Erreur : " . $e->getMessage() . "</li>";
}
echo "</ul>";
}
echo "<h2>🎉 Migration terminée ! Tu peux supprimer ce fichier.</h2>";
echo "<h2>🎉 Migration terminée avec succès !</h2>";
?>
+273 -69
View File
@@ -1,7 +1,6 @@
/* modules/budget/budget.css */
/* --- 1. VARIABLES & BASE --- */
/* Le root garde les couleurs sémantiques dont le JS/PHP a besoin localement */
.budget-view {
font-family:
"Segoe UI",
@@ -128,7 +127,7 @@
/* --- 3. BOUTONS SPÉCIFIQUES & ACTIONS --- */
.pf-btn {
border-radius: 50px; /* Surcharge le global pour le budget */
border-radius: 50px;
}
.btn-icon {
background: transparent;
@@ -263,7 +262,6 @@ td:hover .btn-cell-delete {
border-top: 1px solid #e2e8f0;
}
/* Inputs modifiés */
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -346,20 +344,94 @@ input[type="number"] {
gap: 30px;
}
.prev-section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #e2e8f0;
display: flex !important;
flex-direction: row !important;
justify-content: space-between !important;
align-items: center !important;
margin-bottom: 20px;
padding: 15px;
background: #f8fafc;
border-radius: 12px;
border: 1px solid #e2e8f0;
width: 100% !important;
box-sizing: border-box;
}
.prev-section-header h2 {
margin: 0;
font-size: 1.5rem;
font-size: 1.3rem;
font-weight: 800;
color: #0f172a;
letter-spacing: -0.025em;
}
.prev-header-left {
display: flex;
gap: 10px;
align-items: center;
}
.prev-header-actions {
display: flex;
gap: 10px;
}
/* Configuration Chromatique Parent 1 */
.txt-p1,
.txt-alex {
color: color-mix(in srgb, var(--p1-main, #0891b2) 85%, #000000) !important;
font-weight: bold;
text-align: center;
}
.col-p1,
.col-alex {
color: color-mix(in srgb, var(--p1-main, #0891b2) 85%, #000000) !important;
text-align: center;
}
th.col-p1,
td.col-p1 {
background-color: color-mix(
in srgb,
var(--p1-main, #0891b2) 8%,
#ffffff
) !important;
text-align: center;
}
input.txt-p1 {
color: color-mix(in srgb, var(--p1-main, #0891b2) 85%, #000000) !important;
text-align: center;
font-weight: 600;
}
input.txt-p1:focus {
border-color: var(--p1-main, #0891b2) !important;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--p1-main, #0891b2) 10%, #ffffff) !important;
}
/* Configuration Chromatique Parent 2 */
.txt-p2 {
color: color-mix(in srgb, var(--p2-main, #f59e0b) 85%, #000000) !important;
font-weight: bold;
text-align: center;
}
.col-p2 {
color: color-mix(in srgb, var(--p2-main, #f59e0b) 85%, #000000) !important;
text-align: center;
}
th.col-p2,
td.col-p2 {
background-color: color-mix(
in srgb,
var(--p2-main, #f59e0b) 8%,
#ffffff
) !important;
text-align: center;
}
input.txt-p2 {
color: color-mix(in srgb, var(--p2-main, #f59e0b) 85%, #000000) !important;
text-align: center;
font-weight: 600;
}
input.txt-p2:focus {
border-color: var(--p2-main, #f59e0b) !important;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--p2-main, #f59e0b) 10%, #ffffff) !important;
}
.prev-input {
width: 100%;
@@ -385,19 +457,6 @@ input[type="number"] {
font-weight: 700;
}
.prev-section-header,
.view-header,
.budget-view > div[style*="justify-content: space-between"] {
display: flex !important;
flex-direction: row !important;
justify-content: space-between !important;
align-items: center !important;
float: none !important;
position: relative !important;
width: 100% !important;
gap: 15px !important;
}
.prev-salary-table {
width: 100%;
max-width: 900px;
@@ -419,6 +478,15 @@ input[type="number"] {
text-align: center;
border-bottom: 1px solid #e2e8f0;
}
.prev-salary-table th small {
font-weight: normal;
text-transform: none;
}
.prev-salary-table th.th-family-savings,
.prev-salary-table td.td-family-savings {
background: #f0f9ff !important;
color: #0284c7 !important;
}
.prev-salary-table th:first-child {
text-align: left;
}
@@ -426,6 +494,22 @@ input[type="number"] {
padding: 8px 12px;
border-bottom: 1px solid var(--border-light);
text-align: center;
vertical-align: middle;
}
.prev-salary-table tbody tr:nth-child(1) td:first-child {
text-align: left !important;
font-weight: bold !important;
color: var(--text-main) !important;
border-left: 4px solid var(--p1-main, #0891b2) !important;
}
/* Parent 2 (Ligne 2, ex: Laia) */
.prev-salary-table tbody tr:nth-child(2) td:first-child {
text-align: left !important;
font-weight: bold !important;
color: var(--text-main) !important;
border-left: 4px solid var(--p2-main, #f59e0b) !important;
}
.prev-timeline-wrapper {
@@ -455,7 +539,7 @@ input[type="number"] {
border-right: 1px solid var(--border-light);
white-space: nowrap;
text-align: center;
font-size: 0.9rem;
vertical-align: middle;
}
.col-sticky {
@@ -490,6 +574,15 @@ input[type="number"] {
border-left: 2px solid white;
padding: 8px !important;
}
.th-month span {
text-transform: capitalize;
}
.th-month .cycle-start-label {
font-size: 0.75rem;
font-weight: normal;
color: #64748b;
margin-top: 2px;
}
.th-month.current {
background: var(--primary) !important;
}
@@ -511,16 +604,42 @@ input[type="number"] {
font-weight: 700;
border-bottom: 2px solid #bfdbfe;
}
.txt-alex {
color: #0891b2;
.row-restant td.td-separator {
border-left: 2px solid #cbd5e1;
background: #e2e8f0;
}
.txt-laia {
color: #f59e0b;
.row-indicative {
background: #f8fafc;
color: #94a3b8;
}
.row-indicative .col-sticky {
opacity: 0.8;
}
.cat-name-label {
font-weight: 600;
color: var(--text-main);
}
.row-indicative .cat-name-label {
color: #64748b;
}
.row-indicative .cat-name-label span {
font-size: 0.7rem;
border: 1px solid #cbd5e1;
border-radius: 4px;
padding: 0 4px;
margin-left: 5px;
}
.cat-target-label {
font-size: 0.75rem;
color: var(--text-muted);
font-style: italic;
text-align: right;
}
.txt-global {
color: var(--text-muted);
font-weight: 700;
background: #f8fafc;
border-left: 2px solid #e2e8f0;
}
.val-ok {
color: var(--success);
@@ -594,6 +713,9 @@ tr:hover .row-actions {
letter-spacing: 0.05em;
text-align: center;
}
.recap-header span {
text-transform: capitalize;
}
.recap-table {
width: 100%;
border-collapse: collapse;
@@ -603,6 +725,7 @@ tr:hover .row-actions {
padding: 10px 15px;
border-bottom: 1px solid var(--border-light);
text-align: center;
vertical-align: middle;
font-size: 0.95rem;
}
.recap-table th {
@@ -618,30 +741,68 @@ tr:hover .row-actions {
font-weight: 600;
color: #1e293b;
}
.col-alex {
background-color: #ecfeff;
color: #0891b2;
border-left: 1px solid #cffafe;
.recap-th-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
th.col-alex {
background-color: #cffafe;
border-bottom-color: #a5f3fc;
.recap-th-content span {
text-transform: uppercase;
}
.col-laia {
background-color: #fffbeb;
color: #d97706;
border-left: 1px solid #fef3c7;
.validation-badge {
background: #10b981;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.7rem;
display: flex;
align-items: center;
gap: 4px;
}
th.col-laia {
background-color: #fef3c7;
border-bottom-color: #fde68a;
/* --- Style de base du bouton de validation (Générique) --- */
.recap-table .pf-btn.btn-small {
background: #ffffff !important;
font-size: 0.7rem !important;
padding: 2px 8px !important;
height: auto !important;
width: auto !important;
font-weight: 600;
border-radius: 9px !important;
box-shadow: none !important;
transform: none !important;
transition: all 0.2s ease;
}
.col-global {
background-color: #f8fafc;
color: #475569;
font-weight: 700;
border-left: 2px solid #e2e8f0;
/* Couleurs dynamiques adaptées au Parent 1 (ex: Féfé) */
.col-p1 .pf-btn.btn-small {
color: var(--p1-main, #0891b2) !important;
border: 1px solid var(--p1-main, #0891b2) !important;
}
.col-p1 .pf-btn.btn-small:hover {
background: color-mix(
in srgb,
var(--p1-main, #0891b2) 12%,
#ffffff
) !important;
}
/* Couleurs dynamiques adaptées au Parent 2 (ex: Laia) */
.col-p2 .pf-btn.btn-small {
color: var(--p2-main, #f59e0b) !important;
border: 1px solid var(--p2-main, #f59e0b) !important;
}
.col-p2 .pf-btn.btn-small:hover {
background: color-mix(
in srgb,
var(--p2-main, #f59e0b) 12%,
#ffffff
) !important;
}
/* Grand Totaux Structuraux */
.row-grand-total td {
background-color: #1e293b;
color: white;
@@ -649,14 +810,22 @@ th.col-laia {
border: none;
padding: 12px 15px;
}
.row-grand-total .col-alex {
background: #164e63;
color: #67e8f9;
.row-grand-total .col-p1 {
background: color-mix(
in srgb,
var(--p1-main, #0891b2) 65%,
#000000
) !important;
color: color-mix(in srgb, var(--p1-main, #0891b2) 20%, #ffffff) !important;
border: none;
}
.row-grand-total .col-laia {
background: #78350f;
color: #fcd34d;
.row-grand-total .col-p2 {
background: color-mix(
in srgb,
var(--p2-main, #f59e0b) 65%,
#000000
) !important;
color: color-mix(in srgb, var(--p2-main, #f59e0b) 20%, #ffffff) !important;
border: none;
}
.row-grand-total .col-global {
@@ -665,7 +834,7 @@ th.col-laia {
border: none;
}
/* --- 9. STYLES SPÉCIFIQUES AU SUIVI MENSUEL --- */
/* --- 9. STYLES DU SUIVI MENSUEL --- */
.cat-card {
background: var(--bg-panel);
border-radius: var(--radius);
@@ -675,20 +844,17 @@ th.col-laia {
overflow: hidden;
position: relative;
}
.cat-card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px; /* Padding interne à l'encart coloré */
padding: 15px;
width: 100%;
}
.cat-card-title-group {
display: flex;
flex-direction: column;
}
.cat-card-subtitle {
font-size: 0.85rem;
color: var(--text-muted);
@@ -768,7 +934,52 @@ input[type="month"].pf-input {
border-radius: 6px;
}
/* --- 10. EPARGNE --- */
/* --- 10. SECTION NOTES BUDGETAIRES --- */
.budget-notes-section {
margin: 20px 0;
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: var(--shadow-sm);
border: 1px solid #e2e8f0;
}
.notes-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.notes-header h3 {
margin: 0;
font-size: 1.1rem;
color: #1e293b;
}
.notes-header h3 span {
text-transform: capitalize;
}
.note-save-indicator {
font-size: 0.85rem;
color: #10b981;
font-weight: bold;
opacity: 0;
transition: opacity 0.3s;
}
#monthNoteArea {
width: 100%;
resize: vertical;
border-color: #cbd5e1;
background: #f8fafc;
margin-bottom: 10px;
}
.notes-footer {
text-align: right;
}
.notes-footer .pf-btn {
width: auto;
display: inline-flex;
}
/* --- 11. EPARGNE --- */
.epargne-inline-input {
width: 100%;
min-width: 60px;
@@ -793,7 +1004,7 @@ input[type="month"].pf-input {
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
}
/* --- 11. CALCULATRICE FLOTTANTE --- */
/* --- 12. CALCULATRICE FLOTTANTE --- */
.pf-fab-sum {
position: fixed;
right: 30px;
@@ -883,7 +1094,7 @@ body.sum-mode-active .sum-target {
transition: background-color 0.2s;
}
/* --- 12. RESPONSIVE MOBILE BUDGET --- */
/* --- 13. RESPONSIVE MOBILE BUDGET --- */
@media (max-width: 768px) {
.budget-view {
padding: 10px !important;
@@ -917,11 +1128,6 @@ body.sum-mode-active .sum-target {
.budget-view .categories-grid > .cat-card {
padding: 0 !important;
}
.budget-view
> div[style*="justify-content: space-between"]:not(.view-header) {
flex-direction: column !important;
align-items: stretch !important;
}
.budget-view
> div[style*="justify-content: space-between"]:not(.view-header) {
flex-direction: column !important;
@@ -1021,7 +1227,6 @@ body.sum-mode-active .sum-target {
justify-content: flex-start;
width: auto !important;
}
.col-sticky,
.sticky-col {
min-width: 120px !important;
@@ -1059,8 +1264,7 @@ body.sum-mode-active .sum-target {
right: auto;
top: auto;
}
.cat-card-header {
padding: 12px 10px; /* Plus serré sur mobile */
padding: 12px 10px;
}
}
File diff suppressed because it is too large Load Diff
+117 -88
View File
@@ -5,32 +5,42 @@ document.addEventListener("DOMContentLoaded", () => {
const CONGE_TYPES = ["OFF_CAROLE", "EXTRA_OFF_CAROLE"];
const GUARDE_TYPES = ["CENTRE", "AVIS"];
const PEP_TYPES = ["PEP_SICK"];
const FAMILY = {
ALEX: { id: 2, prefix: "alex" },
LAIA: { id: 3, prefix: "laia" },
};
// 🟢 Liste globale dynamique des personnes (fini les IDs en dur !)
const peopleList = window.CONFIG.PEOPLE || [];
// On filtre pour ne garder que les parents pour la logique principale des congés
const parents = peopleList.filter(
(p) => p.role && p.role.toLowerCase() === "parent",
);
const LEAVES_CONFIG = {
CP: {
startMonth: 8, // Le cycle commence en août (après la tolérance de juillet)
startMonth: 8,
defaultBalance: 25,
},
JRA: {
// Tu pourras ajouter les années suivantes ici
yearlyTotals: {
2024: 10, // ex: 0.83 * 12 arrondi
2024: 10,
2025: 10,
2026: 11, // ex: 0.9 * 12 arrondi
2026: 11,
},
defaultBalance: 10,
toleranceMonths: 2, // Janvier et Février
toleranceMonths: 2,
maxReport: 2,
},
JA: {
[FAMILY.ALEX.id]: { startMonth: 4, startDay: 29, defaultBalance: 4 },
[FAMILY.LAIA.id]: { startMonth: 10, startDay: 1, defaultBalance: 4 }, // Date de Laia à adapter
},
JA: {}, // Ce tableau est maintenant vide par défaut, on le remplit dynamiquement juste en dessous
};
// 🟢 INITIALISATION DYNAMIQUE DES JOURS D'ANNIVERSAIRE (JA)
parents.forEach((parent) => {
LEAVES_CONFIG.JA[parent.id] = {
startMonth: 4, // Mois par défaut (à faire évoluer en BDD au Niveau 3)
startDay: 29, // Jour par défaut
defaultBalance: 4,
};
});
class FamilyCalendar {
constructor() {
this.planningBody = document.getElementById("planningBody");
@@ -455,12 +465,17 @@ document.addEventListener("DOMContentLoaded", () => {
const d = new Date(l.leave_date + "T00:00:00");
if (d >= w.dayDates.mon && d <= w.dayDates.fri) {
const dur = parseFloat(l.duration) || 1;
const prefix =
l.person_id === FAMILY.ALEX.id
? FAMILY.ALEX.prefix
: l.person_id === FAMILY.LAIA.id
? FAMILY.LAIA.prefix
: null;
// 🟢 Correspondance dynamique pour le stockage des totaux de la semaine
let prefix = null;
if (parents[0] && parseInt(l.person_id) === parseInt(parents[0].id))
prefix = "alex";
else if (
parents[1] &&
parseInt(l.person_id) === parseInt(parents[1].id)
)
prefix = "laia";
if (prefix) w.totals[`${prefix}${l.leave_type}`] += dur;
}
});
@@ -479,17 +494,16 @@ document.addEventListener("DOMContentLoaded", () => {
}
calculateMonthlyBalances() {
const balances = {
[FAMILY.ALEX.id]: { CP: {}, JRA: {}, JA: {} },
[FAMILY.LAIA.id]: { CP: {}, JRA: {}, JA: {} },
};
const balances = {};
// 🟢 On initialise le tableau des soldes pour chaque parent trouvé
parents.forEach((p) => {
balances[p.id] = { CP: {}, JRA: {}, JA: {} };
});
// Liste des mois actuellement affichés dans le planning
const ymSet = new Set();
this.weeks.forEach((w) => ymSet.add(w.monthKey));
const ymList = Array.from(ymSet).sort();
// Pré-calcul de l'utilisation par mois
const usageByMonth = {};
this.leaves.forEach((l) => {
const pid = l.person_id;
@@ -502,7 +516,8 @@ document.addEventListener("DOMContentLoaded", () => {
(usageByMonth[pid][type][ym] || 0) + parseFloat(l.duration);
});
[FAMILY.ALEX.id, FAMILY.LAIA.id].forEach((pid) => {
parents.forEach((parent) => {
const pid = parent.id;
["CP", "JRA", "JA"].forEach((type) => {
ymList.forEach((ym) => {
const [currYear, currMonth] = ym.split("-").map(Number);
@@ -510,8 +525,6 @@ document.addEventListener("DOMContentLoaded", () => {
let cycleStartStr = "";
let initialBalance = 0;
// --- 1. RECHERCHE D'UN CORRECTIF MANUEL (SNAPSHOT) ---
// On cherche le snapshot le plus récent qui est inférieur ou égal au mois en cours de calcul
const latestSnapshot = (this.leaveSnapshots || [])
.filter(
(s) =>
@@ -519,18 +532,14 @@ document.addEventListener("DOMContentLoaded", () => {
s.leave_type == type &&
s.snapshot_date.substring(0, 7) <= ym,
)
// On trie du plus récent au plus ancien pour prendre le premier
.sort((a, b) =>
b.snapshot_date.localeCompare(a.snapshot_date),
)[0];
if (latestSnapshot) {
// Si on trouve un correctif, il devient notre nouveau "point zéro"
cycleStartStr = latestSnapshot.snapshot_date.substring(0, 7);
initialBalance = parseFloat(latestSnapshot.remaining_balance); // Utilisation de VOTRE nom de colonne
}
// --- 2. SINON, CALCUL CLASSIQUE PAR DÉFAUT ---
else {
initialBalance = parseFloat(latestSnapshot.remaining_balance);
} else {
if (type === "CP") {
const refYear =
currMonth >= LEAVES_CONFIG.CP.startMonth
@@ -552,7 +561,6 @@ document.addEventListener("DOMContentLoaded", () => {
LEAVES_CONFIG.JRA.yearlyTotals[currYear] ||
LEAVES_CONFIG.JRA.defaultBalance;
// Logique de report du reliquat de l'année N-1
if (currMonth <= LEAVES_CONFIG.JRA.toleranceMonths) {
const prevYear = currYear - 1;
const prevInitial =
@@ -569,14 +577,10 @@ document.addEventListener("DOMContentLoaded", () => {
0,
prevInitial - usedPrevYear,
);
// Ajout du report plafonné à 2 jours
initialBalance += Math.min(
remainingPrevYear,
LEAVES_CONFIG.JRA.maxReport,
);
console.log(
`[JRA] Personne ${pid}, Année ${currYear}: Report de ${Math.min(remainingPrevYear, LEAVES_CONFIG.JRA.maxReport)}j inclus.`,
);
}
} else if (type === "JA") {
const configJA = LEAVES_CONFIG.JA[pid];
@@ -598,11 +602,8 @@ document.addEventListener("DOMContentLoaded", () => {
}
}
// --- 3. DÉDUCTION DES CONGÉS PRIS DEPUIS LE POINT ZÉRO ---
let usedBeforeCurrentMonth = 0;
Object.keys(usageByMonth[pid]?.[type] || {}).forEach((usedYm) => {
// On ne déduit que ce qui a été posé entre le début du cycle (ou la date du snapshot) et le mois en cours
if (usedYm >= cycleStartStr && usedYm < ym) {
usedBeforeCurrentMonth += usageByMonth[pid][type][usedYm];
}
@@ -688,10 +689,28 @@ document.addEventListener("DOMContentLoaded", () => {
const dayLeaves = this.leaves.filter((l) => l.leave_date === iso);
if (dayLeaves.length) {
let html = `<div style="position:absolute; bottom:0; left:0; width:100%; font-size:9px; line-height:1; display:flex; justify-content:center; gap:2px; pointer-events:none;">`;
if (dayLeaves.some((l) => l.person_id === window.CONFIG.ID_ALEX))
html += `<span style="color:#0f766e; font-weight:800;">A</span>`;
if (dayLeaves.some((l) => l.person_id === window.CONFIG.ID_LAIA))
html += `<span style="color:#b45309; font-weight:800;">L</span>`;
const colors = [
"#0f766e",
"#b45309",
"#047857",
"#4338ca",
"#b91c1c",
];
if (window.CONFIG.PEOPLE && window.CONFIG.PEOPLE.length > 0) {
window.CONFIG.PEOPLE.forEach((person, index) => {
if (
dayLeaves.some(
(l) => parseInt(l.person_id) === parseInt(person.id),
)
) {
const color = colors[index % colors.length];
const initial = person.name.charAt(0).toUpperCase();
html += `<span style="color:${color}; font-weight:800; margin: 0 1px;">${initial}</span>`;
}
});
}
html += `</div>`;
td.innerHTML += html;
}
@@ -731,8 +750,10 @@ document.addEventListener("DOMContentLoaded", () => {
tr.appendChild(tdUse);
});
};
renderPersonCols(FAMILY.ALEX.id, `col-${FAMILY.ALEX.prefix}`);
renderPersonCols(FAMILY.LAIA.id, `col-${FAMILY.LAIA.prefix}`);
parents.forEach((parent, index) => {
const cssPrefix = index % 2 === 0 ? "col-alex" : "col-laia";
renderPersonCols(parent.id, cssPrefix);
});
}
this.planningBody.appendChild(tr);
});
@@ -845,9 +866,9 @@ document.addEventListener("DOMContentLoaded", () => {
container.style.display = "flex";
let html = "";
[FAMILY.ALEX, FAMILY.LAIA].forEach((person) => {
parents.forEach((person) => {
html += `<div class="fc-minimal-balance-card">
<strong class="${person.prefix}">${person.prefix.toUpperCase()}</strong>
<strong class="col-alex">${person.name.toUpperCase()}</strong>
<div class="fc-minimal-chips">`;
["CP", "JRA", "JA"].forEach((type) => {
@@ -989,10 +1010,19 @@ document.addEventListener("DOMContentLoaded", () => {
const dayLeaves = this.leaves.filter((l) => l.leave_date === iso);
if (dayLeaves.length) {
content += `<div style="position:absolute; bottom:2px; left:2px; font-size:10px; font-weight:bold;">`;
if (dayLeaves.some((l) => l.person_id === 2))
content += `<span style="color:#0f766e">A</span> `;
if (dayLeaves.some((l) => l.person_id === 3))
content += `<span style="color:#b45309">L</span>`;
parents.forEach((parent, index) => {
if (
dayLeaves.some(
(l) => parseInt(l.person_id) === parseInt(parent.id),
)
) {
const color = index % 2 === 0 ? "#0f766e" : "#b45309";
const initial = parent.name.charAt(0).toUpperCase();
content += `<span style="color:${color}">${initial}</span> `;
}
});
content += `</div>`;
}
content += `</div>`;
@@ -1557,7 +1587,9 @@ document.addEventListener("DOMContentLoaded", () => {
if (dates.includes(e.date)) activeEvents.add(e.type);
});
const activeLeaves = { 2: new Set(), 3: new Set() };
const activeLeaves = {};
parents.forEach((p) => (activeLeaves[p.id] = new Set()));
this.leaves.forEach((l) => {
if (dates.includes(l.leave_date)) {
if (activeLeaves[l.person_id])
@@ -1623,32 +1655,33 @@ document.addEventListener("DOMContentLoaded", () => {
<button class="fc-menu-btn ${getBtnClass("PEP_SICK")}" data-action="add" data-type="PEP_SICK" style="width:100%">${getBtnIcon("PEP_SICK")}${tr("leg_pep_sick")} 🤒</button>
</div>`;
// 5. Section Enfants
// 5. Section Parents (Congés)
html += `<div class="fc-menu-section">
${buildHeader(tr("fc_menu_kids_leaves"), null, null)}
<div class="fc-menu-leaves-table">
<table>
<thead>
<tr>
<th>
<div class="fc-th-inline">
Alex <button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="2" title="${tr("fc_clear")} Alex">${trashSvg}</button>
</div>
</th>
<th>
<div class="fc-th-inline">
Laia <button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="3" title="${tr("fc_clear")} Laia">${trashSvg}</button>
</div>
</th>
</tr>
<tr>`;
// En-têtes dynamiques (Noms + Bouton Poubelle)
parents.forEach((parent) => {
html += `<th>
<div class="fc-th-inline">
${parent.name} <button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="${parent.id}" title="${tr("fc_clear")} ${parent.name}">${trashSvg}</button>
</div>
</th>`;
});
html += ` </tr>
</thead>
<tbody>`;
// Lignes de congés (CP, JRA, JA)
["CP", "JRA", "JA"].forEach((t) => {
html += `<tr>
<td><button class="fc-menu-btn ${getBtnClass(t, 2)}" data-action="add-leave" data-pid="2" data-type="${t}">${getBtnIcon(t, 2)}${t}</button></td>
<td><button class="fc-menu-btn ${getBtnClass(t, 3)}" data-action="add-leave" data-pid="3" data-type="${t}">${getBtnIcon(t, 3)}${t}</button></td>
</tr>`;
html += `<tr>`;
parents.forEach((parent) => {
html += `<td><button class="fc-menu-btn ${getBtnClass(t, parent.id)}" data-action="add-leave" data-pid="${parent.id}" data-type="${t}">${getBtnIcon(t, parent.id)}${t}</button></td>`;
});
html += `</tr>`;
});
html += ` </tbody>
@@ -1743,22 +1776,18 @@ document.addEventListener("DOMContentLoaded", () => {
},
);
} else if (action === "clear-leaves") {
await this.postApi(
"/modules/family-calendar/includes/api/manage-leaf.php",
{
action: "bulk_delete_day_person",
dates,
person_id: window.CONFIG.ID_ALEX,
},
);
await this.postApi(
"/modules/family-calendar/includes/api/manage-leaf.php",
{
action: "bulk_delete_day_person",
dates,
person_id: window.CONFIG.ID_LAIA,
},
);
if (window.CONFIG.PEOPLE && window.CONFIG.PEOPLE.length > 0) {
for (const person of window.CONFIG.PEOPLE) {
await this.postApi(
"/modules/family-calendar/includes/api/manage-leaf.php",
{
action: "bulk_delete_day_person",
dates,
person_id: person.id,
},
);
}
}
}
await this.refreshAllData();
@@ -51,3 +51,4 @@
[2026-05-06T20:38:24+02:00] RAW INPUT: [{"date":"2026-05-08","type":"AVIS","duration":1,"person":"Carole"}]
[2026-05-06T20:43:43+02:00] RAW INPUT: [{"date":"2026-05-06","type":"AVIS","duration":1,"person":"Carole"}]
[2026-05-06T20:44:36+02:00] RAW INPUT: [{"date":"2026-05-06","type":"PEP_SICK","duration":1,"person":"Carole"}]
[2026-05-20T16:40:17+02:00] RAW INPUT: [{"date":"2026-05-19","type":"OFF_CAROLE","duration":1,"person":"Carole"}]
+7 -7
View File
@@ -13,10 +13,13 @@ CREATE TABLE IF NOT EXISTS pf_users (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Personnes ────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_people (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `pf_people` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`user_id` int(11) DEFAULT NULL,
`role` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
-- ─── Calendrier familial ──────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_events (
@@ -232,9 +235,6 @@ SET FOREIGN_KEY_CHECKS = 1;
INSERT IGNORE INTO pf_users (id, username, password_hash, display_name)
VALUES (1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin');
-- Personnes (IDs fixes correspondant aux constantes dans config.php)
INSERT IGNORE INTO pf_people (id, name) VALUES (2, 'Alex');
INSERT IGNORE INTO pf_people (id, name) VALUES (3, 'Laia');
-- ─── Calendar iOS ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_calendar_events (
+44 -17
View File
@@ -69,13 +69,25 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
// 🟢 ENRICHISSEMENT : Prise en compte de la couleur du profil
if ($action === 'update_profile') {
$display_name = trim($_POST['display_name'] ?? '');
$color = trim($_POST['color'] ?? '#0891b2');
if (!$display_name) {
$error = "Le prénom ne peut pas être vide.";
} else {
// MÀJ dans la base Meta (users)
$meta_pdo->prepare("UPDATE users SET display_name = ? WHERE id = ?")
->execute([$display_name, $user_id]);
// MÀJ dans la base locale Foyer (pf_people) via le user_id mappé
require_once __DIR__ . '/includes/db.php';
if (isset($pdo)) {
$stmtPeopleColor = $pdo->prepare("UPDATE pf_people SET color = ? WHERE user_id = ?");
$stmtPeopleColor->execute([$color, $user_id]);
}
$_SESSION['user']['display_name'] = $display_name;
$success = "Profil mis à jour.";
}
@@ -129,7 +141,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$error = "Format non supporté (jpg, png, webp).";
} else {
$dest = $upload_dir . 'home_bg_' . $family_id . '.' . $ext;
// Supprimer anciens fichiers de ce family
foreach (glob($upload_dir . 'home_bg_' . $family_id . '.*') as $old) @unlink($old);
if (move_uploaded_file($file['tmp_name'], $dest)) {
$success = "Image d'accueil mise à jour.";
@@ -225,9 +236,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
// ─── Chargement données ───────────────────────────────────────────────────────
$user = $meta_pdo->prepare("SELECT * FROM users WHERE id = ?");
$user->execute([$user_id]);
$user = $user->fetch();
// 1. On charge la base locale AVANT pour que son $user n'écrase pas le nôtre
if ($family_id) {
require_once __DIR__ . '/includes/db.php';
}
// 2. Maintenant on peut charger ton profil en toute sécurité
$stmtUser = $meta_pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmtUser->execute([$user_id]);
$user = $stmtUser->fetch();
// 3. Récupération de la couleur actuelle depuis pf_people
$user_color = '#0891b2'; // Fallback par défaut
if ($family_id && isset($pdo)) {
$stmtColorFetch = $pdo->prepare("SELECT color FROM pf_people WHERE user_id = ?");
$stmtColorFetch->execute([$user_id]);
$fetched_color = $stmtColorFetch->fetchColumn();
if ($fetched_color) {
$user_color = $fetched_color;
}
}
$family = null;
$members = [];
@@ -276,7 +305,6 @@ require __DIR__ . '/header.php';
<div class="pf-alert pf-alert--error">✗ <?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<!-- ── Langue ─────────────────────────────────────────────────────────── -->
<section class="pf-panel-card">
<h2 class="pf-card-h2">🌐 Langue / Language</h2>
<form method="post" class="pf-lang-form">
@@ -303,7 +331,6 @@ require __DIR__ . '/header.php';
</form>
</section>
<!-- ── Modules ────────────────────────────────────────────────────────── -->
<?php if ($family_id): ?>
<section class="pf-panel-card">
<h2 class="pf-card-h2 pf-card-h2--tight">🧩 Modules actifs</h2>
@@ -413,7 +440,6 @@ require __DIR__ . '/header.php';
<?php endif; ?>
</section>
<!-- ── Fond page d'accueil ───────────────────────────────────────────── -->
<?php if ($family_id):
$bg_file = null;
foreach (glob('/uploads/home_bg_' . $family_id . '.*') as $f) { $bg_file = $f; break; }
@@ -452,15 +478,12 @@ require __DIR__ . '/header.php';
<?php endif; ?>
<!-- ── Famille ──────────────────────────────────────────────────────────── -->
<?php
<?php
$enabledMods = $_SESSION['enabled_modules'] ?? [];
// On cible précisément qui a besoin de quoi
$showCurrency = count(array_intersect(['budget', 'holidays', 'gifts', 'garage'], $enabledMods)) > 0;
$showZone = in_array('calendar', $enabledMods);
// Si au moins un des champs doit être affiché, on rend la section visible
if ($family_id && ($showCurrency || $showZone)):
$currentCurrency = defined('CURRENCY') ? CURRENCY : '€';
$currentZone = defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : 'C';
@@ -502,7 +525,6 @@ require __DIR__ . '/header.php';
<?php endif; ?>
<!-- ── Profil ──────────────────────────────────────────────────────────── -->
<section class="pf-panel-card">
<h2 class="pf-card-h2">👤 Mon profil</h2>
<form method="post">
@@ -516,11 +538,19 @@ require __DIR__ . '/header.php';
<label class="pf-label">Prénom / Pseudo affiché</label>
<input type="text" name="display_name" class="pf-input" value="<?= htmlspecialchars($user['display_name']) ?>" required>
</div>
<div class="pf-form-group">
<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) ?>">
<span class="pf-muted-note">Cette couleur sera utilisée pour vos grilles de budget, calendriers et indicateurs personnels.</span>
</div>
</div>
<button type="submit" class="pf-btn">Enregistrer</button>
</form>
</section>
<!-- ── Mot de passe ────────────────────────────────────────────────────── -->
<section class="pf-panel-card">
<h2 class="pf-card-h2">🔒 Changer le mot de passe</h2>
<form method="post">
@@ -543,7 +573,6 @@ require __DIR__ . '/header.php';
</section>
<?php if ($family): ?>
<!-- ── Espace familial ─────────────────────────────────────────────────── -->
<section class="pf-panel-card">
<h2 class="pf-card-h2">🏠 Mon espace familial</h2>
@@ -559,7 +588,6 @@ require __DIR__ . '/header.php';
</div>
</form>
<!-- Code d'invitation -->
<div class="pf-dashed-panel">
<p class="pf-muted-note pf-muted-note--tight">Code d'invitation — partagez-le pour inviter quelqu'un</p>
<div class="pf-invite-row">
@@ -582,7 +610,6 @@ require __DIR__ . '/header.php';
</p>
</div>
<!-- Membres -->
<h3 class="pf-members-heading">Membres (<?= count($members) ?>)</h3>
<div class="pf-stack-sm">
<?php foreach ($members as $m): ?>
@@ -630,4 +657,4 @@ function copyCode() {
}
</script>
<?php require __DIR__ . '/footer.php'; ?>
<?php require __DIR__ . '/footer.php'; ?>