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. 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) 🟢 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] 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. [ ] 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. [ ] 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) 🟡 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. [ ] 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). [ ] 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. [ ] 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. [ ] Types de carburants (Garage) : Variabiliser la liste statique des carburants dans garage.php.
🔴 Niveau 3 : Complexe (Logique métier et Schéma SQL) 🔴 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. [ ] 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. [ ] 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. [ ] 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 ## 📌 Versioning
| Version | Changements | | Version | Changements |
| ------- | ------------------------------------------------------------------------------------------ | | ------- | ------------------------------------------------------------------------------------------------------------------------- |
| v2.2.0 | Multi-tenant (p.1) : Paramètres dynamiques du foyer (€, Zone) via BDD + script migration | | v2.3.0 | Multi-tenant (p.2) : Refonte BDD Budget (p1, p2), suppression IDs statiques et thèmes de profils dynamiques (color-mix()) |
| v2.1.0 | Module Planka : Kanban natif connecté à Planka via API proxy PHP | | v2.2.0 | Multi-tenant (p.1) : Paramètres dynamiques du foyer (€, Zone) via BDD + script migration |
| v2.0.0 | Module PrintVault : viewer 3D STL/3MF/GCode, stockage local, trajectoires GCode illimitées | | v2.1.0 | Module Planka : Kanban natif connecté à Planka via API proxy PHP |
| v1.5.0 | Modules Todo (rappels Discord), Mémo, Courses (liste partagée) | | v2.0.0 | Module PrintVault : viewer 3D STL/3MF/GCode, stockage local, trajectoires GCode illimitées |
| v1.4.0 | Synchronisation CalDAV iOS | | v1.5.0 | Modules Todo (rappels Discord), Mémo, Courses (liste partagée) |
| v1.3.0 | Optimisation SQL — suppression des requêtes N+1, temps de réponse ÷3 | | v1.4.0 | Synchronisation CalDAV iOS |
| v1.2.0 | Modernisation UI — Toasts, confirmations sur-mesure | | v1.3.0 | Optimisation SQL — suppression des requêtes N+1, temps de réponse ÷3 |
| v1.1.0 | Refonte expérience mobile (Bottom Sheets) | | v1.2.0 | Modernisation UI — Toasts, confirmations sur-mesure |
| v1.0.0 | Version stable initiale — Budget, Calendrier, Voyages, Cadeaux, Garage | | 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'); INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '', 'C');
-- ─── Personnes ──────────────────────────────────────────────────────────────── -- ─── Personnes ────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_people ( CREATE TABLE `pf_people` (
id INT AUTO_INCREMENT PRIMARY KEY, `id` int(11) NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL `name` varchar(50) NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `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 ────────────────────────────────────────────────────── -- ─── Calendrier familial ──────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_events ( 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) INSERT IGNORE INTO pf_users (id, username, password_hash, display_name)
VALUES (1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin'); 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 ─────────────────────────────────────────────────────────── -- ─── Garage Manager ───────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_vehicles ( CREATE TABLE IF NOT EXISTS pf_vehicles (
+24 -10
View File
@@ -8,6 +8,12 @@ require_login();
require __DIR__ . '/includes/db.php'; require __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/i18n.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'); $pageTitle = tr('fc_page_title');
$activePage = "family-calendar"; $activePage = "family-calendar";
$mainClass = "pf-family-calendar"; $mainClass = "pf-family-calendar";
@@ -70,8 +76,12 @@ require __DIR__ . '/header.php';
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label"><?= tr('fc_label_person') ?></label> <label class="pf-label"><?= tr('fc_label_person') ?></label>
<select id="snapPerson" class="pf-input" required> <select id="snapPerson" class="pf-input" required>
<option value="2">Alex</option> <option value="" disabled selected>-- <?= tr('fc_choose_person') ?> --</option>
<option value="3">Laia</option> <?php foreach ($familyPeople as $person): ?>
<option value="<?= htmlspecialchars($person['id']) ?>">
<?= htmlspecialchars($person['name']) ?>
</option>
<?php endforeach; ?>
</select> </select>
</div> </div>
<div class="pf-form-group"> <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_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_pep_sick') ?></span></th>
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_presence') ?></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-alex header-group"><?= htmlspecialchars(strtoupper($parents[0]['name'] ?? 'PARENT 1')) ?></th>
<th colspan="6" class="col-laia header-group">LAIA</th> <th colspan="6" class="col-laia header-group"><?= htmlspecialchars(strtoupper($parents[1]['name'] ?? 'PARENT 2')) ?></th>
</tr> </tr>
<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-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> <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>
<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> <?php foreach ($parents as $index => $parent):
<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> $parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
<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="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></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="<?= $parentClass ?>-sub <?= $parentClass ?>-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="<?= $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> </tr>
</thead> </thead>
<tbody id="planningBody"></tbody> <tbody id="planningBody"></tbody>
+261 -68
View File
@@ -307,7 +307,6 @@ body.pf-home .pf-mobile-close-btn {
.pf-muted-note { .pf-muted-note {
color: var(--text-muted); color: var(--text-muted);
font-size: 0.85rem; font-size: 0.85rem;
margin-bottom: 20px;
} }
.pf-muted-note.pf-muted-note--tight { .pf-muted-note.pf-muted-note--tight {
margin-bottom: 8px; margin-bottom: 8px;
@@ -359,6 +358,58 @@ body.pf-home .pf-mobile-close-btn {
gap: 8px; 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 */ /* Tuile module (checkbox) — état coché via :has(), compatible thème */
.pf-module-tile { .pf-module-tile {
display: flex; display: flex;
@@ -968,21 +1019,23 @@ p {
DARK MODE — [data-theme="dark"] sur <html> DARK MODE — [data-theme="dark"] sur <html>
══════════════════════════════════════════════════════════════ */ ══════════════════════════════════════════════════════════════ */
[data-theme="dark"] { [data-theme="dark"] {
--text-main: #e6edf3; --text-main: #e6edf3;
--text-muted: #8b949e; --text-muted: #8b949e;
--bg-page: #0d1117; --bg-page: #0d1117;
--bg-panel: #161b22; --bg-panel: #161b22;
--border-light: #30363d; --border-light: #30363d;
--shadow: 0 4px 6px -1px rgba(0,0,0,.4), 0 2px 4px -2px rgba(0,0,0,.3); --shadow:
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,.5), 0 4px 6px -4px rgba(0,0,0,.4); 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3);
--bg-subtle: rgba(255,255,255,.06); --shadow-lg:
--bg-soft: rgba(59,130,246,.15); 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -4px rgba(0, 0, 0, 0.4);
--bg-danger-soft: rgba(239,68,68,.1); --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); --bg-overlay: rgba(0, 0, 0, 0.55);
--text-danger-soft: #f87171; --text-danger-soft: #f87171;
--border-strong: #484f58; --border-strong: #484f58;
--border-danger-soft: rgba(239,68,68,.3); --border-danger-soft: rgba(239, 68, 68, 0.3);
--focus-ring: rgba(59,130,246,.25); --focus-ring: rgba(59, 130, 246, 0.25);
--alert-success-bg: rgba(34, 197, 94, 0.12); --alert-success-bg: rgba(34, 197, 94, 0.12);
--alert-success-border: rgba(34, 197, 94, 0.45); --alert-success-border: rgba(34, 197, 94, 0.45);
--alert-success-text: #86efac; --alert-success-text: #86efac;
@@ -993,67 +1046,207 @@ p {
--badge-admin-bg: rgba(59, 130, 246, 0.2); --badge-admin-bg: rgba(59, 130, 246, 0.2);
--badge-admin-text: #93c5fd; --badge-admin-text: #93c5fd;
} }
[data-theme="dark"] body { background: var(--bg-page); color: var(--text-main); } [data-theme="dark"] body {
[data-theme="dark"] .pf-header { background: var(--bg-panel); border-color: var(--border-light); } background: var(--bg-page);
[data-theme="dark"] .pf-logo { color: var(--primary); } color: var(--text-main);
[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-header {
[data-theme="dark"] .pf-nav-link--active { background: rgba(59,130,246,.15); color: var(--primary); } background: var(--bg-panel);
[data-theme="dark"] .pf-user-badge { color: var(--text-muted); } border-color: var(--border-light);
[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-logo {
[data-theme="dark"] .pf-burger-btn { color: var(--text-main); } color: var(--primary);
[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-nav-link {
[data-theme="dark"] .pf-mobile-nav-link { color: var(--text-main); border-color: var(--border-light); } color: var(--text-muted);
[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-nav-link:hover {
[data-theme="dark"] .pf-module-card:hover { border-color: #484f58; box-shadow: var(--shadow); } background: rgba(255, 255, 255, 0.06);
[data-theme="dark"] .pf-card-title { color: var(--text-main); } 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"] .pf-nav-link--active {
[data-theme="dark"] p { color: var(--text-muted); } background: rgba(59, 130, 246, 0.15);
[data-theme="dark"] .pf-btn.btn-secondary { background: var(--bg-panel); color: var(--text-muted); border-color: var(--border-light); } color: var(--primary);
[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-user-badge {
[data-theme="dark"] .pf-input::placeholder { color: #484f58; } color: var(--text-muted);
[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-logout-btn {
[data-theme="dark"] .pf-table th { background: #1c2128; color: var(--text-muted); border-color: var(--border-light); } color: #f87171;
[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-logout-btn:hover {
[data-theme="dark"] .pf-modal-content { background: var(--bg-panel); border: 1px solid var(--border-light); } background: rgba(239, 68, 68, 0.1);
[data-theme="dark"] .pf-modal-title { border-color: var(--border-light); color: var(--text-main); } border-color: rgba(239, 68, 68, 0.3);
[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-burger-btn {
[data-theme="dark"] .pf-toast { background: var(--bg-panel); color: var(--text-main); box-shadow: 0 10px 25px rgba(0,0,0,.4); } color: var(--text-main);
[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"] .pf-mobile-menu {
[data-theme="dark"] .btn-icon-action.edit:hover { background: rgba(59,130,246,.15); } background: var(--bg-panel);
[data-theme="dark"] .btn-icon-action.delete:hover { background: rgba(239,68,68,.1); } 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 ────────────────────────────────────── */ /* ─── Pill toggle thème ────────────────────────────────────── */
.theme-switch { .theme-switch {
display: inline-flex; align-items: center; gap: 5px; display: inline-flex;
background: none; border: none; cursor: pointer; align-items: center;
padding: 3px 5px; border-radius: 8px; transition: background .15s; 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 { .ts-track {
position: relative; width: 36px; height: 20px; position: relative;
background: #cbd5e1; border-radius: 999px; width: 36px;
flex-shrink: 0; transition: background .25s; 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 { .ts-thumb {
position: absolute; top: 3px; left: 3px; position: absolute;
width: 14px; height: 14px; border-radius: 50%; top: 3px;
background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.3); left: 3px;
transition: transform .25s cubic-bezier(.4,0,.2,1); 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); ?>; 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 = { window.CONFIG = {
ID_ALEX: <?php echo defined('ID_ALEX') ? ID_ALEX : 2; ?>, // Liste complète des profils de la famille (Membres, Nounou, Enfants)
ID_LAIA: <?php echo defined('ID_LAIA') ? ID_LAIA : 3; ?>, 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 : "€"; ?>', CURRENCY: '<?php echo defined('CURRENCY') ? CURRENCY : "€"; ?>',
ZONE_SCOLAIRE: '<?php echo defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : "C"; ?>' 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); 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 <?php
if (file_exists(__DIR__ . '/config.php')) {
require_once __DIR__ . '/config.php';
}
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_start(); session_start();
} }
+125 -20
View File
@@ -1,46 +1,151 @@
<?php <?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'; 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 id, name, db_name FROM families WHERE db_name != ''");
$stmt = $meta_pdo->query("SELECT name, db_name FROM families WHERE db_name != ''");
$families = $stmt->fetchAll(); $families = $stmt->fetchAll();
$host = getenv('DB_HOST') ?: 'househub-db'; $host = getenv('DB_HOST') ?: 'househub-db';
$user = getenv('DB_USER') ?: 'househub'; $user = getenv('DB_USER') ?: 'househub';
$pass = getenv('DB_PASS') ?: 'changeme'; $pass = getenv('DB_PASS') ?: 'changeme';
// 2. Boucler sur chaque famille
foreach ($families as $f) { foreach ($families as $f) {
$db_name = $f['db_name']; $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 { try {
$fam_pdo = new PDO("mysql:host=$host;dbname=$db_name;charset=utf8mb4", $user, $pass, [ $fam_pdo = new PDO("mysql:host=$host;dbname=$db_name;charset=utf8mb4", $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]); ]);
// 3. Injecter la nouvelle table // ==========================================
$sql = " // 1. GESTION DE PF_PEOPLE (user_id, role, color)
CREATE TABLE IF NOT EXISTS pf_foyer_settings ( // ==========================================
id INT AUTO_INCREMENT PRIMARY KEY, echo "<li><strong>Table pf_people :</strong> ";
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;
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C'); // 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; }
}
$fam_pdo->exec($sql); // Auto-mapping
echo "<span style='color:green'>✅ OK</span><br>"; $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> - ";
// 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) { } 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 */ /* modules/budget/budget.css */
/* --- 1. VARIABLES & BASE --- */ /* --- 1. VARIABLES & BASE --- */
/* Le root garde les couleurs sémantiques dont le JS/PHP a besoin localement */
.budget-view { .budget-view {
font-family: font-family:
"Segoe UI", "Segoe UI",
@@ -128,7 +127,7 @@
/* --- 3. BOUTONS SPÉCIFIQUES & ACTIONS --- */ /* --- 3. BOUTONS SPÉCIFIQUES & ACTIONS --- */
.pf-btn { .pf-btn {
border-radius: 50px; /* Surcharge le global pour le budget */ border-radius: 50px;
} }
.btn-icon { .btn-icon {
background: transparent; background: transparent;
@@ -263,7 +262,6 @@ td:hover .btn-cell-delete {
border-top: 1px solid #e2e8f0; border-top: 1px solid #e2e8f0;
} }
/* Inputs modifiés */
.form-row { .form-row {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@@ -346,20 +344,94 @@ input[type="number"] {
gap: 30px; gap: 30px;
} }
.prev-section-header { .prev-section-header {
display: flex; display: flex !important;
justify-content: space-between; flex-direction: row !important;
align-items: center; justify-content: space-between !important;
margin-bottom: 15px; align-items: center !important;
padding-bottom: 10px; margin-bottom: 20px;
border-bottom: 1px solid #e2e8f0; padding: 15px;
background: #f8fafc;
border-radius: 12px;
border: 1px solid #e2e8f0;
width: 100% !important;
box-sizing: border-box;
} }
.prev-section-header h2 { .prev-section-header h2 {
margin: 0; margin: 0;
font-size: 1.5rem; font-size: 1.3rem;
font-weight: 800; font-weight: 800;
color: #0f172a; color: #0f172a;
letter-spacing: -0.025em; 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 { .prev-input {
width: 100%; width: 100%;
@@ -385,19 +457,6 @@ input[type="number"] {
font-weight: 700; 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 { .prev-salary-table {
width: 100%; width: 100%;
max-width: 900px; max-width: 900px;
@@ -419,6 +478,15 @@ input[type="number"] {
text-align: center; text-align: center;
border-bottom: 1px solid #e2e8f0; 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 { .prev-salary-table th:first-child {
text-align: left; text-align: left;
} }
@@ -426,6 +494,22 @@ input[type="number"] {
padding: 8px 12px; padding: 8px 12px;
border-bottom: 1px solid var(--border-light); border-bottom: 1px solid var(--border-light);
text-align: center; 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 { .prev-timeline-wrapper {
@@ -455,7 +539,7 @@ input[type="number"] {
border-right: 1px solid var(--border-light); border-right: 1px solid var(--border-light);
white-space: nowrap; white-space: nowrap;
text-align: center; text-align: center;
font-size: 0.9rem; vertical-align: middle;
} }
.col-sticky { .col-sticky {
@@ -490,6 +574,15 @@ input[type="number"] {
border-left: 2px solid white; border-left: 2px solid white;
padding: 8px !important; 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 { .th-month.current {
background: var(--primary) !important; background: var(--primary) !important;
} }
@@ -511,16 +604,42 @@ input[type="number"] {
font-weight: 700; font-weight: 700;
border-bottom: 2px solid #bfdbfe; border-bottom: 2px solid #bfdbfe;
} }
.txt-alex { .row-restant td.td-separator {
color: #0891b2; border-left: 2px solid #cbd5e1;
background: #e2e8f0;
} }
.txt-laia { .row-indicative {
color: #f59e0b; 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 { .txt-global {
color: var(--text-muted); color: var(--text-muted);
font-weight: 700; font-weight: 700;
background: #f8fafc; background: #f8fafc;
border-left: 2px solid #e2e8f0;
} }
.val-ok { .val-ok {
color: var(--success); color: var(--success);
@@ -594,6 +713,9 @@ tr:hover .row-actions {
letter-spacing: 0.05em; letter-spacing: 0.05em;
text-align: center; text-align: center;
} }
.recap-header span {
text-transform: capitalize;
}
.recap-table { .recap-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
@@ -603,6 +725,7 @@ tr:hover .row-actions {
padding: 10px 15px; padding: 10px 15px;
border-bottom: 1px solid var(--border-light); border-bottom: 1px solid var(--border-light);
text-align: center; text-align: center;
vertical-align: middle;
font-size: 0.95rem; font-size: 0.95rem;
} }
.recap-table th { .recap-table th {
@@ -618,30 +741,68 @@ tr:hover .row-actions {
font-weight: 600; font-weight: 600;
color: #1e293b; color: #1e293b;
} }
.col-alex {
background-color: #ecfeff; .recap-th-content {
color: #0891b2; display: flex;
border-left: 1px solid #cffafe; flex-direction: column;
align-items: center;
gap: 5px;
} }
th.col-alex { .recap-th-content span {
background-color: #cffafe; text-transform: uppercase;
border-bottom-color: #a5f3fc;
} }
.col-laia { .validation-badge {
background-color: #fffbeb; background: #10b981;
color: #d97706; color: white;
border-left: 1px solid #fef3c7; padding: 4px 8px;
border-radius: 4px;
font-size: 0.7rem;
display: flex;
align-items: center;
gap: 4px;
} }
th.col-laia {
background-color: #fef3c7; /* --- Style de base du bouton de validation (Générique) --- */
border-bottom-color: #fde68a; .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; /* Couleurs dynamiques adaptées au Parent 1 (ex: Féfé) */
color: #475569; .col-p1 .pf-btn.btn-small {
font-weight: 700; color: var(--p1-main, #0891b2) !important;
border-left: 2px solid #e2e8f0; 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 { .row-grand-total td {
background-color: #1e293b; background-color: #1e293b;
color: white; color: white;
@@ -649,14 +810,22 @@ th.col-laia {
border: none; border: none;
padding: 12px 15px; padding: 12px 15px;
} }
.row-grand-total .col-alex { .row-grand-total .col-p1 {
background: #164e63; background: color-mix(
color: #67e8f9; in srgb,
var(--p1-main, #0891b2) 65%,
#000000
) !important;
color: color-mix(in srgb, var(--p1-main, #0891b2) 20%, #ffffff) !important;
border: none; border: none;
} }
.row-grand-total .col-laia { .row-grand-total .col-p2 {
background: #78350f; background: color-mix(
color: #fcd34d; in srgb,
var(--p2-main, #f59e0b) 65%,
#000000
) !important;
color: color-mix(in srgb, var(--p2-main, #f59e0b) 20%, #ffffff) !important;
border: none; border: none;
} }
.row-grand-total .col-global { .row-grand-total .col-global {
@@ -665,7 +834,7 @@ th.col-laia {
border: none; border: none;
} }
/* --- 9. STYLES SPÉCIFIQUES AU SUIVI MENSUEL --- */ /* --- 9. STYLES DU SUIVI MENSUEL --- */
.cat-card { .cat-card {
background: var(--bg-panel); background: var(--bg-panel);
border-radius: var(--radius); border-radius: var(--radius);
@@ -675,20 +844,17 @@ th.col-laia {
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }
.cat-card-header { .cat-card-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 15px; /* Padding interne à l'encart coloré */ padding: 15px;
width: 100%; width: 100%;
} }
.cat-card-title-group { .cat-card-title-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.cat-card-subtitle { .cat-card-subtitle {
font-size: 0.85rem; font-size: 0.85rem;
color: var(--text-muted); color: var(--text-muted);
@@ -768,7 +934,52 @@ input[type="month"].pf-input {
border-radius: 6px; 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 { .epargne-inline-input {
width: 100%; width: 100%;
min-width: 60px; min-width: 60px;
@@ -793,7 +1004,7 @@ input[type="month"].pf-input {
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1); box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
} }
/* --- 11. CALCULATRICE FLOTTANTE --- */ /* --- 12. CALCULATRICE FLOTTANTE --- */
.pf-fab-sum { .pf-fab-sum {
position: fixed; position: fixed;
right: 30px; right: 30px;
@@ -883,7 +1094,7 @@ body.sum-mode-active .sum-target {
transition: background-color 0.2s; transition: background-color 0.2s;
} }
/* --- 12. RESPONSIVE MOBILE BUDGET --- */ /* --- 13. RESPONSIVE MOBILE BUDGET --- */
@media (max-width: 768px) { @media (max-width: 768px) {
.budget-view { .budget-view {
padding: 10px !important; padding: 10px !important;
@@ -917,11 +1128,6 @@ body.sum-mode-active .sum-target {
.budget-view .categories-grid > .cat-card { .budget-view .categories-grid > .cat-card {
padding: 0 !important; padding: 0 !important;
} }
.budget-view
> div[style*="justify-content: space-between"]:not(.view-header) {
flex-direction: column !important;
align-items: stretch !important;
}
.budget-view .budget-view
> div[style*="justify-content: space-between"]:not(.view-header) { > div[style*="justify-content: space-between"]:not(.view-header) {
flex-direction: column !important; flex-direction: column !important;
@@ -1021,7 +1227,6 @@ body.sum-mode-active .sum-target {
justify-content: flex-start; justify-content: flex-start;
width: auto !important; width: auto !important;
} }
.col-sticky, .col-sticky,
.sticky-col { .sticky-col {
min-width: 120px !important; min-width: 120px !important;
@@ -1059,8 +1264,7 @@ body.sum-mode-active .sum-target {
right: auto; right: auto;
top: auto; top: auto;
} }
.cat-card-header { .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 CONGE_TYPES = ["OFF_CAROLE", "EXTRA_OFF_CAROLE"];
const GUARDE_TYPES = ["CENTRE", "AVIS"]; const GUARDE_TYPES = ["CENTRE", "AVIS"];
const PEP_TYPES = ["PEP_SICK"]; const PEP_TYPES = ["PEP_SICK"];
const FAMILY = {
ALEX: { id: 2, prefix: "alex" }, // 🟢 Liste globale dynamique des personnes (fini les IDs en dur !)
LAIA: { id: 3, prefix: "laia" }, 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 = { const LEAVES_CONFIG = {
CP: { CP: {
startMonth: 8, // Le cycle commence en août (après la tolérance de juillet) startMonth: 8,
defaultBalance: 25, defaultBalance: 25,
}, },
JRA: { JRA: {
// Tu pourras ajouter les années suivantes ici
yearlyTotals: { yearlyTotals: {
2024: 10, // ex: 0.83 * 12 arrondi 2024: 10,
2025: 10, 2025: 10,
2026: 11, // ex: 0.9 * 12 arrondi 2026: 11,
}, },
defaultBalance: 10, defaultBalance: 10,
toleranceMonths: 2, // Janvier et Février toleranceMonths: 2,
maxReport: 2, maxReport: 2,
}, },
JA: { JA: {}, // Ce tableau est maintenant vide par défaut, on le remplit dynamiquement juste en dessous
[FAMILY.ALEX.id]: { startMonth: 4, startDay: 29, defaultBalance: 4 },
[FAMILY.LAIA.id]: { startMonth: 10, startDay: 1, defaultBalance: 4 }, // Date de Laia à adapter
},
}; };
// 🟢 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 { class FamilyCalendar {
constructor() { constructor() {
this.planningBody = document.getElementById("planningBody"); this.planningBody = document.getElementById("planningBody");
@@ -455,12 +465,17 @@ document.addEventListener("DOMContentLoaded", () => {
const d = new Date(l.leave_date + "T00:00:00"); const d = new Date(l.leave_date + "T00:00:00");
if (d >= w.dayDates.mon && d <= w.dayDates.fri) { if (d >= w.dayDates.mon && d <= w.dayDates.fri) {
const dur = parseFloat(l.duration) || 1; const dur = parseFloat(l.duration) || 1;
const prefix =
l.person_id === FAMILY.ALEX.id // 🟢 Correspondance dynamique pour le stockage des totaux de la semaine
? FAMILY.ALEX.prefix let prefix = null;
: l.person_id === FAMILY.LAIA.id if (parents[0] && parseInt(l.person_id) === parseInt(parents[0].id))
? FAMILY.LAIA.prefix prefix = "alex";
: null; else if (
parents[1] &&
parseInt(l.person_id) === parseInt(parents[1].id)
)
prefix = "laia";
if (prefix) w.totals[`${prefix}${l.leave_type}`] += dur; if (prefix) w.totals[`${prefix}${l.leave_type}`] += dur;
} }
}); });
@@ -479,17 +494,16 @@ document.addEventListener("DOMContentLoaded", () => {
} }
calculateMonthlyBalances() { calculateMonthlyBalances() {
const balances = { const balances = {};
[FAMILY.ALEX.id]: { CP: {}, JRA: {}, JA: {} }, // 🟢 On initialise le tableau des soldes pour chaque parent trouvé
[FAMILY.LAIA.id]: { CP: {}, JRA: {}, JA: {} }, parents.forEach((p) => {
}; balances[p.id] = { CP: {}, JRA: {}, JA: {} };
});
// Liste des mois actuellement affichés dans le planning
const ymSet = new Set(); const ymSet = new Set();
this.weeks.forEach((w) => ymSet.add(w.monthKey)); this.weeks.forEach((w) => ymSet.add(w.monthKey));
const ymList = Array.from(ymSet).sort(); const ymList = Array.from(ymSet).sort();
// Pré-calcul de l'utilisation par mois
const usageByMonth = {}; const usageByMonth = {};
this.leaves.forEach((l) => { this.leaves.forEach((l) => {
const pid = l.person_id; const pid = l.person_id;
@@ -502,7 +516,8 @@ document.addEventListener("DOMContentLoaded", () => {
(usageByMonth[pid][type][ym] || 0) + parseFloat(l.duration); (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) => { ["CP", "JRA", "JA"].forEach((type) => {
ymList.forEach((ym) => { ymList.forEach((ym) => {
const [currYear, currMonth] = ym.split("-").map(Number); const [currYear, currMonth] = ym.split("-").map(Number);
@@ -510,8 +525,6 @@ document.addEventListener("DOMContentLoaded", () => {
let cycleStartStr = ""; let cycleStartStr = "";
let initialBalance = 0; 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 || []) const latestSnapshot = (this.leaveSnapshots || [])
.filter( .filter(
(s) => (s) =>
@@ -519,18 +532,14 @@ document.addEventListener("DOMContentLoaded", () => {
s.leave_type == type && s.leave_type == type &&
s.snapshot_date.substring(0, 7) <= ym, s.snapshot_date.substring(0, 7) <= ym,
) )
// On trie du plus récent au plus ancien pour prendre le premier
.sort((a, b) => .sort((a, b) =>
b.snapshot_date.localeCompare(a.snapshot_date), b.snapshot_date.localeCompare(a.snapshot_date),
)[0]; )[0];
if (latestSnapshot) { if (latestSnapshot) {
// Si on trouve un correctif, il devient notre nouveau "point zéro"
cycleStartStr = latestSnapshot.snapshot_date.substring(0, 7); cycleStartStr = latestSnapshot.snapshot_date.substring(0, 7);
initialBalance = parseFloat(latestSnapshot.remaining_balance); // Utilisation de VOTRE nom de colonne initialBalance = parseFloat(latestSnapshot.remaining_balance);
} } else {
// --- 2. SINON, CALCUL CLASSIQUE PAR DÉFAUT ---
else {
if (type === "CP") { if (type === "CP") {
const refYear = const refYear =
currMonth >= LEAVES_CONFIG.CP.startMonth currMonth >= LEAVES_CONFIG.CP.startMonth
@@ -552,7 +561,6 @@ document.addEventListener("DOMContentLoaded", () => {
LEAVES_CONFIG.JRA.yearlyTotals[currYear] || LEAVES_CONFIG.JRA.yearlyTotals[currYear] ||
LEAVES_CONFIG.JRA.defaultBalance; LEAVES_CONFIG.JRA.defaultBalance;
// Logique de report du reliquat de l'année N-1
if (currMonth <= LEAVES_CONFIG.JRA.toleranceMonths) { if (currMonth <= LEAVES_CONFIG.JRA.toleranceMonths) {
const prevYear = currYear - 1; const prevYear = currYear - 1;
const prevInitial = const prevInitial =
@@ -569,14 +577,10 @@ document.addEventListener("DOMContentLoaded", () => {
0, 0,
prevInitial - usedPrevYear, prevInitial - usedPrevYear,
); );
// Ajout du report plafonné à 2 jours
initialBalance += Math.min( initialBalance += Math.min(
remainingPrevYear, remainingPrevYear,
LEAVES_CONFIG.JRA.maxReport, 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") { } else if (type === "JA") {
const configJA = LEAVES_CONFIG.JA[pid]; 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; let usedBeforeCurrentMonth = 0;
Object.keys(usageByMonth[pid]?.[type] || {}).forEach((usedYm) => { 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) { if (usedYm >= cycleStartStr && usedYm < ym) {
usedBeforeCurrentMonth += usageByMonth[pid][type][usedYm]; usedBeforeCurrentMonth += usageByMonth[pid][type][usedYm];
} }
@@ -688,10 +689,28 @@ document.addEventListener("DOMContentLoaded", () => {
const dayLeaves = this.leaves.filter((l) => l.leave_date === iso); const dayLeaves = this.leaves.filter((l) => l.leave_date === iso);
if (dayLeaves.length) { 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;">`; 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>`; const colors = [
if (dayLeaves.some((l) => l.person_id === window.CONFIG.ID_LAIA)) "#0f766e",
html += `<span style="color:#b45309; font-weight:800;">L</span>`; "#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>`; html += `</div>`;
td.innerHTML += html; td.innerHTML += html;
} }
@@ -731,8 +750,10 @@ document.addEventListener("DOMContentLoaded", () => {
tr.appendChild(tdUse); tr.appendChild(tdUse);
}); });
}; };
renderPersonCols(FAMILY.ALEX.id, `col-${FAMILY.ALEX.prefix}`); parents.forEach((parent, index) => {
renderPersonCols(FAMILY.LAIA.id, `col-${FAMILY.LAIA.prefix}`); const cssPrefix = index % 2 === 0 ? "col-alex" : "col-laia";
renderPersonCols(parent.id, cssPrefix);
});
} }
this.planningBody.appendChild(tr); this.planningBody.appendChild(tr);
}); });
@@ -845,9 +866,9 @@ document.addEventListener("DOMContentLoaded", () => {
container.style.display = "flex"; container.style.display = "flex";
let html = ""; let html = "";
[FAMILY.ALEX, FAMILY.LAIA].forEach((person) => { parents.forEach((person) => {
html += `<div class="fc-minimal-balance-card"> 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">`; <div class="fc-minimal-chips">`;
["CP", "JRA", "JA"].forEach((type) => { ["CP", "JRA", "JA"].forEach((type) => {
@@ -989,10 +1010,19 @@ document.addEventListener("DOMContentLoaded", () => {
const dayLeaves = this.leaves.filter((l) => l.leave_date === iso); const dayLeaves = this.leaves.filter((l) => l.leave_date === iso);
if (dayLeaves.length) { if (dayLeaves.length) {
content += `<div style="position:absolute; bottom:2px; left:2px; font-size:10px; font-weight:bold;">`; 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> `; parents.forEach((parent, index) => {
if (dayLeaves.some((l) => l.person_id === 3)) if (
content += `<span style="color:#b45309">L</span>`; 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>`;
} }
content += `</div>`; content += `</div>`;
@@ -1557,7 +1587,9 @@ document.addEventListener("DOMContentLoaded", () => {
if (dates.includes(e.date)) activeEvents.add(e.type); 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) => { this.leaves.forEach((l) => {
if (dates.includes(l.leave_date)) { if (dates.includes(l.leave_date)) {
if (activeLeaves[l.person_id]) 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> <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>`; </div>`;
// 5. Section Enfants // 5. Section Parents (Congés)
html += `<div class="fc-menu-section"> html += `<div class="fc-menu-section">
${buildHeader(tr("fc_menu_kids_leaves"), null, null)} ${buildHeader(tr("fc_menu_kids_leaves"), null, null)}
<div class="fc-menu-leaves-table"> <div class="fc-menu-leaves-table">
<table> <table>
<thead> <thead>
<tr> <tr>`;
<th>
<div class="fc-th-inline"> // En-têtes dynamiques (Noms + Bouton Poubelle)
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> parents.forEach((parent) => {
</div> html += `<th>
</th> <div class="fc-th-inline">
<th> ${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 class="fc-th-inline"> </div>
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> </th>`;
</div> });
</th> html += ` </tr>
</tr>
</thead> </thead>
<tbody>`; <tbody>`;
// Lignes de congés (CP, JRA, JA)
["CP", "JRA", "JA"].forEach((t) => { ["CP", "JRA", "JA"].forEach((t) => {
html += `<tr> 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> parents.forEach((parent) => {
<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> 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>`;
</tr>`; });
html += `</tr>`;
}); });
html += ` </tbody> html += ` </tbody>
@@ -1743,22 +1776,18 @@ document.addEventListener("DOMContentLoaded", () => {
}, },
); );
} else if (action === "clear-leaves") { } else if (action === "clear-leaves") {
await this.postApi( if (window.CONFIG.PEOPLE && window.CONFIG.PEOPLE.length > 0) {
"/modules/family-calendar/includes/api/manage-leaf.php", for (const person of window.CONFIG.PEOPLE) {
{ await this.postApi(
action: "bulk_delete_day_person", "/modules/family-calendar/includes/api/manage-leaf.php",
dates, {
person_id: window.CONFIG.ID_ALEX, action: "bulk_delete_day_person",
}, dates,
); person_id: person.id,
await this.postApi( },
"/modules/family-calendar/includes/api/manage-leaf.php", );
{ }
action: "bulk_delete_day_person", }
dates,
person_id: window.CONFIG.ID_LAIA,
},
);
} }
await this.refreshAllData(); 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: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: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-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; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Personnes ──────────────────────────────────────────────────────────────── -- ─── Personnes ────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_people ( CREATE TABLE `pf_people` (
id INT AUTO_INCREMENT PRIMARY KEY, `id` int(11) NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL `name` varchar(50) NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `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 ────────────────────────────────────────────────────── -- ─── Calendrier familial ──────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_events ( 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) INSERT IGNORE INTO pf_users (id, username, password_hash, display_name)
VALUES (1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin'); 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 ───────────────────────────────────────────────────────────── -- ─── Calendar iOS ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_calendar_events ( CREATE TABLE IF NOT EXISTS pf_calendar_events (
+43 -16
View File
@@ -69,13 +69,25 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
} }
// 🟢 ENRICHISSEMENT : Prise en compte de la couleur du profil
if ($action === 'update_profile') { if ($action === 'update_profile') {
$display_name = trim($_POST['display_name'] ?? ''); $display_name = trim($_POST['display_name'] ?? '');
$color = trim($_POST['color'] ?? '#0891b2');
if (!$display_name) { if (!$display_name) {
$error = "Le prénom ne peut pas être vide."; $error = "Le prénom ne peut pas être vide.";
} else { } else {
// MÀJ dans la base Meta (users)
$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]);
// 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; $_SESSION['user']['display_name'] = $display_name;
$success = "Profil mis à jour."; $success = "Profil mis à jour.";
} }
@@ -129,7 +141,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$error = "Format non supporté (jpg, png, webp)."; $error = "Format non supporté (jpg, png, webp).";
} else { } else {
$dest = $upload_dir . 'home_bg_' . $family_id . '.' . $ext; $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); 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 = "Image d'accueil mise à jour.";
@@ -225,9 +236,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
// ─── Chargement données ─────────────────────────────────────────────────────── // ─── Chargement données ───────────────────────────────────────────────────────
$user = $meta_pdo->prepare("SELECT * FROM users WHERE id = ?");
$user->execute([$user_id]); // 1. On charge la base locale AVANT pour que son $user n'écrase pas le nôtre
$user = $user->fetch(); 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; $family = null;
$members = []; $members = [];
@@ -276,7 +305,6 @@ require __DIR__ . '/header.php';
<div class="pf-alert pf-alert--error">✗ <?= htmlspecialchars($error) ?></div> <div class="pf-alert pf-alert--error">✗ <?= htmlspecialchars($error) ?></div>
<?php endif; ?> <?php endif; ?>
<!-- ── Langue ─────────────────────────────────────────────────────────── -->
<section class="pf-panel-card"> <section class="pf-panel-card">
<h2 class="pf-card-h2">🌐 Langue / Language</h2> <h2 class="pf-card-h2">🌐 Langue / Language</h2>
<form method="post" class="pf-lang-form"> <form method="post" class="pf-lang-form">
@@ -303,7 +331,6 @@ require __DIR__ . '/header.php';
</form> </form>
</section> </section>
<!-- ── Modules ────────────────────────────────────────────────────────── -->
<?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">🧩 Modules actifs</h2>
@@ -413,7 +440,6 @@ require __DIR__ . '/header.php';
<?php endif; ?> <?php endif; ?>
</section> </section>
<!-- ── Fond page d'accueil ───────────────────────────────────────────── -->
<?php if ($family_id): <?php if ($family_id):
$bg_file = null; $bg_file = null;
foreach (glob('/uploads/home_bg_' . $family_id . '.*') as $f) { $bg_file = $f; break; } foreach (glob('/uploads/home_bg_' . $family_id . '.*') as $f) { $bg_file = $f; break; }
@@ -452,15 +478,12 @@ require __DIR__ . '/header.php';
<?php endif; ?> <?php endif; ?>
<!-- ── Famille ──────────────────────────────────────────────────────────── --> <?php
<?php
$enabledMods = $_SESSION['enabled_modules'] ?? []; $enabledMods = $_SESSION['enabled_modules'] ?? [];
// On cible précisément qui a besoin de quoi
$showCurrency = count(array_intersect(['budget', 'holidays', 'gifts', 'garage'], $enabledMods)) > 0; $showCurrency = count(array_intersect(['budget', 'holidays', 'gifts', 'garage'], $enabledMods)) > 0;
$showZone = in_array('calendar', $enabledMods); $showZone = in_array('calendar', $enabledMods);
// Si au moins un des champs doit être affiché, on rend la section visible
if ($family_id && ($showCurrency || $showZone)): if ($family_id && ($showCurrency || $showZone)):
$currentCurrency = defined('CURRENCY') ? CURRENCY : '€'; $currentCurrency = defined('CURRENCY') ? CURRENCY : '€';
$currentZone = defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : 'C'; $currentZone = defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : 'C';
@@ -502,7 +525,6 @@ require __DIR__ . '/header.php';
<?php endif; ?> <?php endif; ?>
<!-- ── Profil ──────────────────────────────────────────────────────────── -->
<section class="pf-panel-card"> <section class="pf-panel-card">
<h2 class="pf-card-h2">👤 Mon profil</h2> <h2 class="pf-card-h2">👤 Mon profil</h2>
<form method="post"> <form method="post">
@@ -516,11 +538,19 @@ require __DIR__ . '/header.php';
<label class="pf-label">Prénom / Pseudo affiché</label> <label class="pf-label">Prénom / Pseudo affiché</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">
<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> <button type="submit" class="pf-btn">Enregistrer</button>
</form> </form>
</section> </section>
<!-- ── Mot de passe ────────────────────────────────────────────────────── -->
<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">🔒 Changer le mot de passe</h2>
<form method="post"> <form method="post">
@@ -543,7 +573,6 @@ require __DIR__ . '/header.php';
</section> </section>
<?php if ($family): ?> <?php if ($family): ?>
<!-- ── Espace familial ─────────────────────────────────────────────────── -->
<section class="pf-panel-card"> <section class="pf-panel-card">
<h2 class="pf-card-h2">🏠 Mon espace familial</h2> <h2 class="pf-card-h2">🏠 Mon espace familial</h2>
@@ -559,7 +588,6 @@ require __DIR__ . '/header.php';
</div> </div>
</form> </form>
<!-- Code d'invitation -->
<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">Code d'invitation — partagez-le pour inviter quelqu'un</p>
<div class="pf-invite-row"> <div class="pf-invite-row">
@@ -582,7 +610,6 @@ require __DIR__ . '/header.php';
</p> </p>
</div> </div>
<!-- Membres -->
<h3 class="pf-members-heading">Membres (<?= count($members) ?>)</h3> <h3 class="pf-members-heading">Membres (<?= count($members) ?>)</h3>
<div class="pf-stack-sm"> <div class="pf-stack-sm">
<?php foreach ($members as $m): ?> <?php foreach ($members as $m): ?>