Merge pull request 'Merge branch 'main' of http://nas.percolouco.com:3500/perco/HouseHub into parent_id' (#4) from parent_id into main
Deploy HouseHub / deploy (push) Successful in 2s
Deploy HouseHub / deploy (push) Successful in 2s
Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
househub_db_data
|
househub_db_data
|
||||||
.env
|
.env
|
||||||
data/.hh_app_secret
|
data/.hh_app_secret
|
||||||
|
extract_source.php
|
||||||
@@ -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.
|
||||||
@@ -175,7 +180,8 @@ Afin de transformer complètement l'application héritée de Pacha Family en un
|
|||||||
## 📌 Versioning
|
## 📌 Versioning
|
||||||
|
|
||||||
| Version | Changements |
|
| 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.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.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 |
|
| v2.0.0 | Module PrintVault : viewer 3D STL/3MF/GCode, stockage local, trajectoires GCode illimitées |
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
+257
-64
@@ -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;
|
||||||
@@ -973,16 +1024,18 @@ p {
|
|||||||
--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
@@ -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"; ?>'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
<?php
|
|
||||||
// includes/config.php
|
|
||||||
// HouseHub Config - Version dynamique
|
|
||||||
if (!defined('ID_ALEX')) {
|
|
||||||
define('ID_ALEX', 2);
|
|
||||||
define('ID_LAIA', 3);
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
+141
-20
@@ -1,46 +1,167 @@
|
|||||||
<?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>";
|
||||||
|
|
||||||
|
// 🟢 SÉCURITÉ : On vide les variables à chaque famille pour éviter les fuites (le bug f2 vers f1)
|
||||||
|
$p1_name = null;
|
||||||
|
$p2_name = null;
|
||||||
|
$parents = [];
|
||||||
|
|
||||||
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);
|
// 🟢 CORRECTION DE L'AUTO-MAPPING : Si pf_people est vide, on la remplit !
|
||||||
echo "<span style='color:green'>✅ OK</span><br>";
|
$stmtUsers = $meta_pdo->prepare("SELECT id, username, display_name FROM users WHERE family_id = ? ORDER BY id ASC");
|
||||||
|
$stmtUsers->execute([$family_id]);
|
||||||
|
$users = $stmtUsers->fetchAll();
|
||||||
|
|
||||||
|
$mappedCount = 0;
|
||||||
|
foreach ($users as $u) {
|
||||||
|
// On vérifie si l'utilisateur existe déjà dans pf_people (via user_id ou son nom)
|
||||||
|
$stmtCheck = $fam_pdo->prepare("SELECT id FROM pf_people WHERE user_id = ? OR LOWER(name) = LOWER(?)");
|
||||||
|
$stmtCheck->execute([$u['id'], $u['username']]);
|
||||||
|
$exists = $stmtCheck->fetchColumn();
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
// S'il existe, on s'assure que son user_id est bien renseigné
|
||||||
|
$stmtUpdate = $fam_pdo->prepare("UPDATE pf_people SET user_id = ? WHERE id = ?");
|
||||||
|
$stmtUpdate->execute([$u['id'], $exists]);
|
||||||
|
$mappedCount += $stmtUpdate->rowCount();
|
||||||
|
} else {
|
||||||
|
// S'il n'existe pas du tout (base f1 vide), on le crée !
|
||||||
|
$stmtInsert = $fam_pdo->prepare("INSERT INTO pf_people (name, user_id) VALUES (?, ?)");
|
||||||
|
$stmtInsert->execute([$u['display_name'] ?: $u['username'], $u['id']]);
|
||||||
|
$mappedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "<span style='color:blue'>$mappedCount profils liés ou créé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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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> ";
|
||||||
|
|
||||||
|
$checkAlex = $fam_pdo->query("SHOW COLUMNS FROM pf_alloc_values LIKE 'amount_alex'")->rowCount();
|
||||||
|
if ($checkAlex > 0) {
|
||||||
|
$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> - ";
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 fraîchement créés/mappés
|
||||||
|
$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
@@ -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
@@ -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>
|
|
||||||
|
// En-têtes dynamiques (Noms + Bouton Poubelle)
|
||||||
|
parents.forEach((parent) => {
|
||||||
|
html += `<th>
|
||||||
<div class="fc-th-inline">
|
<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>
|
${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>
|
</div>
|
||||||
</th>
|
</th>`;
|
||||||
<th>
|
});
|
||||||
<div class="fc-th-inline">
|
html += ` </tr>
|
||||||
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>
|
|
||||||
</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,23 +1776,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else if (action === "clear-leaves") {
|
} else if (action === "clear-leaves") {
|
||||||
|
if (window.CONFIG.PEOPLE && window.CONFIG.PEOPLE.length > 0) {
|
||||||
|
for (const person of window.CONFIG.PEOPLE) {
|
||||||
await this.postApi(
|
await this.postApi(
|
||||||
"/modules/family-calendar/includes/api/manage-leaf.php",
|
"/modules/family-calendar/includes/api/manage-leaf.php",
|
||||||
{
|
{
|
||||||
action: "bulk_delete_day_person",
|
action: "bulk_delete_day_person",
|
||||||
dates,
|
dates,
|
||||||
person_id: window.CONFIG.ID_ALEX,
|
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();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -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
@@ -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 (
|
||||||
|
|||||||
+42
-15
@@ -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): ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user