17 Commits
Author SHA1 Message Date
perco 8e3e7fe3d8 Merge pull request 'Deploiement staging' (#5) from deploiement-staging into main
Deploy HouseHub / deploy (push) Successful in 2s
Reviewed-on: #5
2026-06-04 14:14:19 +02:00
FeFeClochette 2c8bbbb3e1 Merge branch 'parent_id' of http://nas.percolouco.com:3500/perco/HouseHub into parent_id 2026-06-04 13:09:50 +02:00
FeFeClochette 0ff3c42d35 fix bêtises et update schema sql 2026-06-04 13:09:47 +02:00
perco ea04831451 fix(garage): tous les champs optionnels vides → NULL dans INSERT maintenance
Deploy HouseHub / deploy (push) Successful in 1s
2026-05-29 15:05:11 +02:00
perco d9ae5308ea fix(garage): cost vide → null au lieu de chaîne vide (erreur DECIMAL SQL)
Deploy HouseHub / deploy (push) Successful in 1s
2026-05-29 14:56:40 +02:00
perco 2c5fb9da6b 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
Reviewed-on: #4
2026-05-21 11:22:12 +02:00
perco 751082308a Merge pull request 'feat(voyage): base de données péages 2026 (24 680 paires, 5 opérateurs)' (#3) from main into parent_id
Reviewed-on: #3
2026-05-21 11:20:16 +02:00
FeFeClochette b44688a654 fix migrate
Deploy HouseHub / deploy (push) Successful in 2s
2026-05-20 19:20:16 +02:00
FeFeClochette f8e2ad67ee refacto parent Calendrier budget et couleur profil.
Deploy HouseHub / deploy (push) Successful in 2s
2026-05-20 17:56:25 +02:00
percoandClaude Sonnet 4.6 6aab112ecb feat(voyage): affichage détaillé des gares de péage par segment
Deploy HouseHub / deploy (push) Successful in 1s
- Chaque segment affiche maintenant la gare d'entrée et de sortie
- Affichage de l'opérateur (APRR, COFIROUTE, SANEF...)
- Style amélioré : distance + gares sur la même ligne
- Ajout du champ 'op' dans la réponse API

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 16:23:34 +02:00
percoandClaude Sonnet 4.6 6e409fb3a1 feat(voyage): calculateur de coût de trajet avec péages et carburant
Deploy HouseHub / deploy (push) Successful in 1s
- GPS database (toll_gps.json): 414 péages géocodés via Nominatim (88.5% coverage, 468 plazas)
- API PHP (voyage/api.php): estimation péages + carburant via OSRM routing
  - Matching opérateur/entrée/sortie par proximité GPS (80km radius)
  - Fallback haversine si OSRM indisponible
- UI holidays/detail.php: panneau "Coût du trajet" (≥2 étapes)
  - Inputs conso L/100km et prix carburant configurables
  - Affichage distance, péages, carburant, total aller
  - Détail par segment en accordéon
- CSS: classes hol-cost-stat pour les stats chiffrées

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 16:17:21 +02:00
percoandClaude Sonnet 4.6 535f1b7dcf feat(voyage): base de données péages 2026 (24 680 paires, 5 opérateurs)
Deploy HouseHub / deploy (push) Successful in 2s
Données extraites depuis les PDFs officiels 2026 :
- APRR : 20 964 paires (réseau A6, A31, A36, A40...)
- Cofiroute : 1 296 paires (A10, A11, A28...)
- SANEF : 1 620 paires (A1, A2, A26...)
- AREA : 674 paires (A41, A43...)
- SAPN : 126 paires (A13, A29...)

Script de parsing : /tmp/build_tolls.py (à conserver pour mises à jour)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 15:54:26 +02:00
FeFeClochette 4130b03a68 reverse
Deploy HouseHub / deploy (push) Successful in 2s
2026-05-20 12:17:37 +02:00
FeFeClochette 315e1b5fb1 headerfefe
Deploy HouseHub / deploy (push) Successful in 2s
2026-05-20 12:12:12 +02:00
FeFeClochette b1d3db4892 Merge branch 'main' of http://nas.percolouco.com:3500/perco/HouseHub into parent_id 2026-05-20 12:12:03 +02:00
FeFeClochette 5fe9046a63 gitignore extract source 2026-05-20 11:22:06 +02:00
FeFeClochette 5b70cd20e6 Merge branch 'main' of http://nas.percolouco.com:3500/perco/HouseHub into parent_id 2026-05-20 11:19:05 +02:00
25 changed files with 4129 additions and 877 deletions
+1
View File
@@ -1,3 +1,4 @@
househub_db_data
.env
data/.hh_app_secret
extract_source.php
+19 -13
View File
@@ -151,12 +151,15 @@ L'architecture de HouseHub isole chaque foyer dans sa propre base de données (`
Afin de transformer complètement l'application héritée de Pacha Family en un produit générique et autonome pour chaque espace familial, les chantiers suivants doivent être menés.
🟢 Niveau 1 : Facile (Constantes et configurations globales)
[x] Configuration de base : Extraire la devise (CURRENCY) et la zone de vacances scolaires (ZONE_SCOLAIRE) du fichier config.php pour les stocker dans les paramètres d'espace en BDD.
[x] Suppression des IDs statiques : Supprimer les IDs parents fixes (ID_ALEX = 2, ID_LAIA = 3) dans config.php au profit d'une lecture dynamique basée sur les utilisateurs réels de la famille connectée (liens via la table pf_people).
[x] Charte graphique dynamique : Remplacer les couleurs codées en dur (bleu/orange) par des sélecteurs natifs dans l'espace "Paramètres", en générant les variables CSS et leurs nuances (via color-mix()) à la volée.
[ ] Filtres API Éducation Nationale : Rendre dynamique la zone de l'API (zones LIKE '%Zone C%' dans family-calendar.js) pour s'adapter à la région de chaque foyer.
[ ] Identifiant Planka : Variabiliser le project_id Planka par défaut écrit en dur dans planka/api.php.
[ ] Suppression des IDs statiques : Supprimer les IDs parents fixes (ID_ALEX = 2, ID_LAIA = 3) dans config.php au profit d'une lecture dynamique basée sur les utilisateurs réels de la famille connectée.
🟡 Niveau 2 : Intermédiaire (Listes et dictionnaires en dur)
[ ] Gestion des membres (Cadeaux) : Dans gift-list.php, remplacer la liste d'enfants ($children) et d'adultes ($baseAdults, $extraAdults) codée en dur par une lecture dynamique des membres du foyer déclarés en BDD.
[ ] Occasions festives (Cadeaux) : Rendre configurables les occasions spéciales (TIO, NOEL, ROIS, ANNIV, SANT).
[ ] Configuration des catégories (Budget) : Dans budget/views/suivi.php, migrer le tableau $categoriesConfig (FMCG, Essence, École, etc.), leurs couleurs et leurs suggestions de magasins associés vers un stockage paramétrable.
@@ -164,7 +167,9 @@ Afin de transformer complètement l'application héritée de Pacha Family en un
[ ] Types de carburants (Garage) : Variabiliser la liste statique des carburants dans garage.php.
🔴 Niveau 3 : Complexe (Logique métier et Schéma SQL)
[ ] Refonte relationnelle de la répartition (Budget) : Modifier la structure de la table pf_alloc_values pour supprimer les colonnes figées amount_alex et amount_laia et basculer sur un modèle relationnel (alloc_id, user_id, amount) capable de gérer n'importe quel nombre d'adultes (1, 2, 3 ou plus).
[x] Refonte relationnelle du Budget (Prévisionnel) : Refonte totale de budget_prev.php. Modification de la table pf_alloc_values pour basculer sur des colonnes génériques (amount_p1, amount_p2) et suppression définitive de toute logique conditionnelle liée aux prénoms des concepteurs originaux.
[ ] Refonte globale de la répartition (Budget) : Étendre le modèle relationnel (alloc_id, user_id, amount) au reste de l'application pour gérer n'importe quel nombre d'adultes (1, 2, 3 ou plus).
[ ] Destinations de transferts (Budget) : Rendre dynamiques les cibles de virements prévisionnels (vers L.Pol, vers L.Pep, etc.) en fonction des comptes épargne créés par la famille.
[ ] Onglets d'Épargne : Générer dynamiquement les sous-onglets de la vue epargne.php à partir des membres réels au lieu des blocs statiques Alex, Laia et Nens.
[ ] Types de Garde (Calendrier) : Supprimer les types d'événements et compteurs codés en dur (OFF_CAROLE, EXTRA_OFF_CAROLE, CENTRE, AVIS, PEP_SICK) pour permettre à chaque foyer de définir ses propres modalités de garde et compteurs de congés associés.
@@ -174,14 +179,15 @@ Afin de transformer complètement l'application héritée de Pacha Family en un
## 📌 Versioning
| Version | Changements |
| ------- | ------------------------------------------------------------------------------------------ |
| v2.2.0 | Multi-tenant (p.1) : Paramètres dynamiques du foyer (€, Zone) via BDD + script migration |
| v2.1.0 | Module Planka : Kanban natif connecté à Planka via API proxy PHP |
| v2.0.0 | Module PrintVault : viewer 3D STL/3MF/GCode, stockage local, trajectoires GCode illimitées |
| v1.5.0 | Modules Todo (rappels Discord), Mémo, Courses (liste partagée) |
| v1.4.0 | Synchronisation CalDAV iOS |
| v1.3.0 | Optimisation SQL — suppression des requêtes N+1, temps de réponse ÷3 |
| v1.2.0 | Modernisation UI — Toasts, confirmations sur-mesure |
| v1.1.0 | Refonte expérience mobile (Bottom Sheets) |
| v1.0.0 | Version stable initiale — Budget, Calendrier, Voyages, Cadeaux, Garage |
| Version | Changements |
| ------- | ------------------------------------------------------------------------------------------------------------------------- |
| v2.3.0 | Multi-tenant (p.2) : Refonte BDD Budget (p1, p2), suppression IDs statiques et thèmes de profils dynamiques (color-mix()) |
| v2.2.0 | Multi-tenant (p.1) : Paramètres dynamiques du foyer (€, Zone) via BDD + script migration |
| v2.1.0 | Module Planka : Kanban natif connecté à Planka via API proxy PHP |
| v2.0.0 | Module PrintVault : viewer 3D STL/3MF/GCode, stockage local, trajectoires GCode illimitées |
| v1.5.0 | Modules Todo (rappels Discord), Mémo, Courses (liste partagée) |
| v1.4.0 | Synchronisation CalDAV iOS |
| v1.3.0 | Optimisation SQL — suppression des requêtes N+1, temps de réponse ÷3 |
| v1.2.0 | Modernisation UI — Toasts, confirmations sur-mesure |
| v1.1.0 | Refonte expérience mobile (Bottom Sheets) |
| v1.0.0 | Version stable initiale — Budget, Calendrier, Voyages, Cadeaux, Garage |
+2 -1
View File
@@ -36,6 +36,7 @@ CREATE TABLE IF NOT EXISTS user_calendar_integrations (
calendar_url VARCHAR(1024) DEFAULT NULL,
status VARCHAR(30) DEFAULT 'connected',
last_sync_at DATETIME DEFAULT NULL,
calendar_prefs_json TEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_user_provider (user_id, provider),
@@ -44,4 +45,4 @@ CREATE TABLE IF NOT EXISTS user_calendar_integrations (
-- Donner au user applicatif le droit de créer des DBs famille
GRANT ALL PRIVILEGES ON `househub_f%`.* TO 'househub'@'%';
FLUSH PRIVILEGES;
FLUSH PRIVILEGES;
+69 -62
View File
@@ -1,18 +1,8 @@
-- HouseHub — Schéma MySQL
-- Créer la base : CREATE DATABASE househub CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- HouseHub — Schéma MySQL (Modèle pour une base famille)
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ─── Utilisateurs ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(100) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Familles ─────────────────────────────────────────────────────────────
-- ─── Configuration du foyer ───────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_foyer_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
currency VARCHAR(10) NOT NULL DEFAULT '',
@@ -20,13 +10,24 @@ CREATE TABLE IF NOT EXISTS pf_foyer_settings (
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Initialisation de la ligne unique par défaut
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '', 'C');
-- ─── Utilisateurs (Legacy) ────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(100) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Personnes ────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_people (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL
CREATE TABLE `pf_people` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`user_id` int(11) DEFAULT NULL,
`role` varchar(50) DEFAULT NULL,
`color` varchar(7) DEFAULT '#0891b2',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Calendrier familial ──────────────────────────────────────────────────────
@@ -112,20 +113,22 @@ CREATE TABLE IF NOT EXISTS pf_expenses (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_alloc_categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
target DECIMAL(10,2) DEFAULT 0,
holiday_id INT DEFAULT NULL,
sort_order INT DEFAULT 0
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
target DECIMAL(10,2) DEFAULT 0.00,
transfer_dest VARCHAR(50) DEFAULT NULL,
holiday_id INT DEFAULT NULL,
sort_order INT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_alloc_values (
id INT AUTO_INCREMENT PRIMARY KEY,
month_date VARCHAR(10) NOT NULL,
cat_id INT NOT NULL,
amount_alex DECIMAL(10,2) DEFAULT 0,
amount_laia DECIMAL(10,2) DEFAULT 0,
UNIQUE KEY uq_alloc (month_date, cat_id)
person_id INT NOT NULL,
amount DECIMAL(10,2) DEFAULT 0.00,
UNIQUE KEY uq_alloc_person (month_date, cat_id, person_id),
FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_savings (
@@ -158,9 +161,22 @@ CREATE TABLE IF NOT EXISTS pf_salary_config (
CREATE TABLE IF NOT EXISTS pf_import_rules (
id INT AUTO_INCREMENT PRIMARY KEY,
keyword VARCHAR(255) NOT NULL UNIQUE,
category VARCHAR(100) NOT NULL
category VARCHAR(100) NOT NULL,
budget_item_id INT(11) NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_advances (
id int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
advance_date date NOT NULL,
payer varchar(50) NOT NULL,
description varchar(255) NOT NULL,
amount decimal(10,2) DEFAULT 0.00,
from_savings tinyint(1) DEFAULT 0,
is_resolved tinyint(1) DEFAULT 0,
created_at datetime DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_notes (
id INT AUTO_INCREMENT PRIMARY KEY,
note_type VARCHAR(100) NOT NULL,
@@ -169,6 +185,29 @@ CREATE TABLE IF NOT EXISTS pf_notes (
UNIQUE KEY uq_note (note_type, reference_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_memo_notes (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content LONGTEXT DEFAULT '',
tags VARCHAR(1000) DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FULLTEXT KEY ft_notes (title, content, tags)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_memo_attachments (
id INT AUTO_INCREMENT PRIMARY KEY,
note_id INT NOT NULL,
type ENUM('image','file','url') NOT NULL DEFAULT 'file',
filename VARCHAR(255) DEFAULT NULL,
original_name VARCHAR(255) DEFAULT NULL,
url TEXT DEFAULT NULL,
label VARCHAR(255) DEFAULT NULL,
size INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (note_id) REFERENCES pf_memo_notes(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Voyages / Holidays ───────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_holidays (
id INT AUTO_INCREMENT PRIMARY KEY,
@@ -236,17 +275,6 @@ CREATE TABLE IF NOT EXISTS pf_gifts (
amount DECIMAL(8,2) DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SET FOREIGN_KEY_CHECKS = 1;
-- ─── Données initiales ────────────────────────────────────────────────────────
-- Utilisateur admin (mot de passe : changeme → à modifier !)
INSERT IGNORE INTO pf_users (id, username, password_hash, display_name)
VALUES (1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin');
-- Personnes (IDs fixes correspondant aux constantes dans config.php)
INSERT IGNORE INTO pf_people (id, name) VALUES (2, 'Alex');
INSERT IGNORE INTO pf_people (id, name) VALUES (3, 'Laia');
-- ─── Garage Manager ───────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_vehicles (
id INT AUTO_INCREMENT PRIMARY KEY,
@@ -321,30 +349,6 @@ CREATE TABLE IF NOT EXISTS pf_garage_documents (
FOREIGN KEY (maintenance_id) REFERENCES pf_maintenances(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_memo_notes (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content LONGTEXT DEFAULT '',
tags VARCHAR(1000) DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FULLTEXT KEY ft_notes (title, content, tags)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_memo_attachments (
id INT AUTO_INCREMENT PRIMARY KEY,
note_id INT NOT NULL,
type ENUM('image','file','url') NOT NULL DEFAULT 'file',
filename VARCHAR(255) DEFAULT NULL,
original_name VARCHAR(255) DEFAULT NULL,
url TEXT DEFAULT NULL,
label VARCHAR(255) DEFAULT NULL,
size INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (note_id) REFERENCES pf_memo_notes(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Todo ─────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_todo_lists (
id INT AUTO_INCREMENT PRIMARY KEY,
@@ -372,7 +376,7 @@ CREATE TABLE IF NOT EXISTS pf_todos (
FOREIGN KEY (list_id) REFERENCES pf_todo_lists(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Module Liste (multi-listes) ──────────────────────────────────────────────
-- ─── Module Liste (Courses partagées) ─────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_lists (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL DEFAULT 'Ma liste',
@@ -382,7 +386,7 @@ CREATE TABLE IF NOT EXISTS pf_lists (
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO pf_lists (id, name, position) VALUES (1, 'Ma liste', 0);
INSERT IGNORE INTO pf_lists (id, name, position) VALUES (1, 'Ma liste', 0);
CREATE TABLE IF NOT EXISTS pf_grocery_items (
id INT AUTO_INCREMENT PRIMARY KEY,
@@ -447,6 +451,7 @@ CREATE TABLE IF NOT EXISTS pf_calendar_event_links (
external_uid VARCHAR(255) NOT NULL,
external_etag VARCHAR(255) DEFAULT NULL,
calendar_url VARCHAR(1024) DEFAULT NULL,
external_href VARCHAR(2048) DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_calendar_event (calendar_event_id),
@@ -498,3 +503,5 @@ CREATE TABLE IF NOT EXISTS pf_planka_config (
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SET FOREIGN_KEY_CHECKS = 1;
+24 -10
View File
@@ -8,6 +8,12 @@ require_login();
require __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/i18n.php';
$stmtPeople = $pdo->query("SELECT id, name, user_id, role FROM pf_people ORDER BY id ASC");
$familyPeople = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
$parents = array_values(array_filter($familyPeople, function($p) {
return strtolower($p['role'] ?? '') === 'parent';
}));
$pageTitle = tr('fc_page_title');
$activePage = "family-calendar";
$mainClass = "pf-family-calendar";
@@ -70,8 +76,12 @@ require __DIR__ . '/header.php';
<div class="pf-form-group">
<label class="pf-label"><?= tr('fc_label_person') ?></label>
<select id="snapPerson" class="pf-input" required>
<option value="2">Alex</option>
<option value="3">Laia</option>
<option value="" disabled selected>-- <?= tr('fc_choose_person') ?> --</option>
<?php foreach ($familyPeople as $person): ?>
<option value="<?= htmlspecialchars($person['id']) ?>">
<?= htmlspecialchars($person['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="pf-form-group">
@@ -169,20 +179,24 @@ require __DIR__ . '/header.php';
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_avis') ?></span></th>
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_pep_sick') ?></span></th>
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_presence') ?></span></th>
<th colspan="6" class="col-alex header-group">ALEX</th>
<th colspan="6" class="col-laia header-group">LAIA</th>
<th colspan="6" class="col-alex header-group"><?= htmlspecialchars(strtoupper($parents[0]['name'] ?? 'PARENT 1')) ?></th>
<th colspan="6" class="col-laia header-group"><?= htmlspecialchars(strtoupper($parents[1]['name'] ?? 'PARENT 2')) ?></th>
</tr>
<tr>
<th colspan="2" class="col-alex-sub">CP</th><th colspan="2" class="col-alex-sub">JRA</th><th colspan="2" class="col-alex-sub">JA</th>
<th colspan="2" class="col-laia-sub">CP</th><th colspan="2" class="col-laia-sub">JRA</th><th colspan="2" class="col-laia-sub">JA</th>
</tr>
<tr>
<th class="col-alex-sub col-alex-av"><?= tr('fc_col_av') ?></th><th class="col-alex-sub col-alex-use"><?= tr('fc_col_use') ?></th>
<th class="col-alex-sub col-alex-av"><?= tr('fc_col_av') ?></th><th class="col-alex-sub col-alex-use"><?= tr('fc_col_use') ?></th>
<th class="col-alex-sub col-alex-av"><?= tr('fc_col_av') ?></th><th class="col-alex-sub col-alex-use"><?= tr('fc_col_use') ?></th>
<th class="col-laia-sub col-laia-av"><?= tr('fc_col_av') ?></th><th class="col-laia-sub col-laia-use"><?= tr('fc_col_use') ?></th>
<th class="col-laia-sub col-laia-av"><?= tr('fc_col_av') ?></th><th class="col-laia-sub col-laia-use"><?= tr('fc_col_use') ?></th>
<th class="col-laia-sub col-laia-av"><?= tr('fc_col_av') ?></th><th class="col-laia-sub col-laia-use"><?= tr('fc_col_use') ?></th>
<?php foreach ($parents as $index => $parent):
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
?>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th>
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody id="planningBody"></tbody>
+261 -68
View File
@@ -307,7 +307,6 @@ body.pf-home .pf-mobile-close-btn {
.pf-muted-note {
color: var(--text-muted);
font-size: 0.85rem;
margin-bottom: 20px;
}
.pf-muted-note.pf-muted-note--tight {
margin-bottom: 8px;
@@ -359,6 +358,58 @@ body.pf-home .pf-mobile-close-btn {
gap: 8px;
}
/* --- SÉLECTEUR DE COULEUR THÉMATIQUE (SETTINGS) --- */
.pf-color-picker-group {
display: flex;
align-items: center;
gap: 12px;
margin-top: 6px;
}
.pf-input-color {
/* On réduit la taille pour le rendre plus subtil */
width: 36px;
height: 36px;
/* On enlève les marges/paddings par défaut */
padding: 0;
cursor: pointer;
/* On le rend complètement rond, c'est plus moderne */
border-radius: 50%;
border: 2px solid #e2e8f0;
/* On cache le fond par défaut du navigateur */
background: transparent;
box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.05));
transition: all 0.2s ease;
/* Optionnel : masquer la bordure interne sur Chrome/Safari */
-webkit-appearance: none;
}
/* On s'assure que le carré de couleur interne (Webkit) remplit tout et est rond */
.pf-input-color::-webkit-color-swatch-wrapper {
padding: 0;
}
.pf-input-color::-webkit-color-swatch {
border: none;
border-radius: 50%;
}
/* Même chose pour Firefox */
.pf-input-color::-moz-color-swatch {
border: none;
border-radius: 50%;
}
.pf-input-color:hover {
border-color: #94a3b8;
transform: scale(1.05);
}
.pf-color-picker-group .pf-muted-note {
font-size: 0.85rem;
color: var(--text-muted, #64748b);
line-height: 1.4;
}
/* Tuile module (checkbox) — état coché via :has(), compatible thème */
.pf-module-tile {
display: flex;
@@ -968,21 +1019,23 @@ p {
DARK MODE — [data-theme="dark"] sur <html>
══════════════════════════════════════════════════════════════ */
[data-theme="dark"] {
--text-main: #e6edf3;
--text-muted: #8b949e;
--bg-page: #0d1117;
--bg-panel: #161b22;
--text-main: #e6edf3;
--text-muted: #8b949e;
--bg-page: #0d1117;
--bg-panel: #161b22;
--border-light: #30363d;
--shadow: 0 4px 6px -1px rgba(0,0,0,.4), 0 2px 4px -2px rgba(0,0,0,.3);
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,.5), 0 4px 6px -4px rgba(0,0,0,.4);
--bg-subtle: rgba(255,255,255,.06);
--bg-soft: rgba(59,130,246,.15);
--bg-danger-soft: rgba(239,68,68,.1);
--shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3);
--shadow-lg:
0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -4px rgba(0, 0, 0, 0.4);
--bg-subtle: rgba(255, 255, 255, 0.06);
--bg-soft: rgba(59, 130, 246, 0.15);
--bg-danger-soft: rgba(239, 68, 68, 0.1);
--bg-overlay: rgba(0, 0, 0, 0.55);
--text-danger-soft: #f87171;
--border-strong: #484f58;
--border-danger-soft: rgba(239,68,68,.3);
--focus-ring: rgba(59,130,246,.25);
--border-danger-soft: rgba(239, 68, 68, 0.3);
--focus-ring: rgba(59, 130, 246, 0.25);
--alert-success-bg: rgba(34, 197, 94, 0.12);
--alert-success-border: rgba(34, 197, 94, 0.45);
--alert-success-text: #86efac;
@@ -993,67 +1046,207 @@ p {
--badge-admin-bg: rgba(59, 130, 246, 0.2);
--badge-admin-text: #93c5fd;
}
[data-theme="dark"] body { background: var(--bg-page); color: var(--text-main); }
[data-theme="dark"] .pf-header { background: var(--bg-panel); border-color: var(--border-light); }
[data-theme="dark"] .pf-logo { color: var(--primary); }
[data-theme="dark"] .pf-nav-link { color: var(--text-muted); }
[data-theme="dark"] .pf-nav-link:hover { background: rgba(255,255,255,.06); color: var(--text-main); }
[data-theme="dark"] .pf-nav-link--active { background: rgba(59,130,246,.15); color: var(--primary); }
[data-theme="dark"] .pf-user-badge { color: var(--text-muted); }
[data-theme="dark"] .pf-logout-btn { color: #f87171; }
[data-theme="dark"] .pf-logout-btn:hover { background: rgba(239,68,68,.1); border-color: rgba(239,68,68,.3); }
[data-theme="dark"] .pf-burger-btn { color: var(--text-main); }
[data-theme="dark"] .pf-mobile-menu { background: var(--bg-panel); color: var(--text-main); }
[data-theme="dark"] .pf-mobile-menu-header { background: var(--bg-page); border-color: var(--border-light); }
[data-theme="dark"] .pf-mobile-nav-link { color: var(--text-main); border-color: var(--border-light); }
[data-theme="dark"] .pf-mobile-nav-link:active { background: rgba(255,255,255,.06); }
[data-theme="dark"] .pf-module-card { background: var(--bg-panel); border-color: var(--border-light); }
[data-theme="dark"] .pf-module-card:hover { border-color: #484f58; box-shadow: var(--shadow); }
[data-theme="dark"] .pf-card-title { color: var(--text-main); }
[data-theme="dark"] .pf-card-desc { color: var(--text-muted); }
[data-theme="dark"] h1, [data-theme="dark"] h2, [data-theme="dark"] h3 { color: var(--text-main); }
[data-theme="dark"] p { color: var(--text-muted); }
[data-theme="dark"] .pf-btn.btn-secondary { background: var(--bg-panel); color: var(--text-muted); border-color: var(--border-light); }
[data-theme="dark"] .pf-btn.btn-secondary:hover { background: rgba(255,255,255,.05); color: var(--text-main); border-color: #484f58; }
[data-theme="dark"] .pf-input { background: #1c2128; border-color: var(--border-light); color: var(--text-main); }
[data-theme="dark"] .pf-input::placeholder { color: #484f58; }
[data-theme="dark"] .pf-input:focus { background: #1c2128; border-color: var(--primary); }
[data-theme="dark"] .pf-table { background: var(--bg-panel); }
[data-theme="dark"] .pf-table th { background: #1c2128; color: var(--text-muted); border-color: var(--border-light); }
[data-theme="dark"] .pf-table td { border-color: rgba(48,54,61,.5); color: var(--text-main); }
[data-theme="dark"] .pf-table tbody tr:hover { background: rgba(255,255,255,.03); }
[data-theme="dark"] .pf-modal-content { background: var(--bg-panel); border: 1px solid var(--border-light); }
[data-theme="dark"] .pf-modal-title { border-color: var(--border-light); color: var(--text-main); }
[data-theme="dark"] .modal-footer { border-color: var(--border-light); }
[data-theme="dark"] .pf-footer { background: var(--bg-panel); border-color: var(--border-light); }
[data-theme="dark"] .pf-toast { background: var(--bg-panel); color: var(--text-main); box-shadow: 0 10px 25px rgba(0,0,0,.4); }
[data-theme="dark"] .pf-login-card { background: var(--bg-panel); border-color: var(--border-light); }
[data-theme="dark"] .pf-login-error { background: rgba(239,68,68,.1); border-color: rgba(239,68,68,.3); }
[data-theme="dark"] .btn-icon-action.edit:hover { background: rgba(59,130,246,.15); }
[data-theme="dark"] .btn-icon-action.delete:hover { background: rgba(239,68,68,.1); }
[data-theme="dark"] body {
background: var(--bg-page);
color: var(--text-main);
}
[data-theme="dark"] .pf-header {
background: var(--bg-panel);
border-color: var(--border-light);
}
[data-theme="dark"] .pf-logo {
color: var(--primary);
}
[data-theme="dark"] .pf-nav-link {
color: var(--text-muted);
}
[data-theme="dark"] .pf-nav-link:hover {
background: rgba(255, 255, 255, 0.06);
color: var(--text-main);
}
[data-theme="dark"] .pf-nav-link--active {
background: rgba(59, 130, 246, 0.15);
color: var(--primary);
}
[data-theme="dark"] .pf-user-badge {
color: var(--text-muted);
}
[data-theme="dark"] .pf-logout-btn {
color: #f87171;
}
[data-theme="dark"] .pf-logout-btn:hover {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
}
[data-theme="dark"] .pf-burger-btn {
color: var(--text-main);
}
[data-theme="dark"] .pf-mobile-menu {
background: var(--bg-panel);
color: var(--text-main);
}
[data-theme="dark"] .pf-mobile-menu-header {
background: var(--bg-page);
border-color: var(--border-light);
}
[data-theme="dark"] .pf-mobile-nav-link {
color: var(--text-main);
border-color: var(--border-light);
}
[data-theme="dark"] .pf-mobile-nav-link:active {
background: rgba(255, 255, 255, 0.06);
}
[data-theme="dark"] .pf-module-card {
background: var(--bg-panel);
border-color: var(--border-light);
}
[data-theme="dark"] .pf-module-card:hover {
border-color: #484f58;
box-shadow: var(--shadow);
}
[data-theme="dark"] .pf-card-title {
color: var(--text-main);
}
[data-theme="dark"] .pf-card-desc {
color: var(--text-muted);
}
[data-theme="dark"] h1,
[data-theme="dark"] h2,
[data-theme="dark"] h3 {
color: var(--text-main);
}
[data-theme="dark"] p {
color: var(--text-muted);
}
[data-theme="dark"] .pf-btn.btn-secondary {
background: var(--bg-panel);
color: var(--text-muted);
border-color: var(--border-light);
}
[data-theme="dark"] .pf-btn.btn-secondary:hover {
background: rgba(255, 255, 255, 0.05);
color: var(--text-main);
border-color: #484f58;
}
[data-theme="dark"] .pf-input {
background: #1c2128;
border-color: var(--border-light);
color: var(--text-main);
}
[data-theme="dark"] .pf-input::placeholder {
color: #484f58;
}
[data-theme="dark"] .pf-input:focus {
background: #1c2128;
border-color: var(--primary);
}
[data-theme="dark"] .pf-table {
background: var(--bg-panel);
}
[data-theme="dark"] .pf-table th {
background: #1c2128;
color: var(--text-muted);
border-color: var(--border-light);
}
[data-theme="dark"] .pf-table td {
border-color: rgba(48, 54, 61, 0.5);
color: var(--text-main);
}
[data-theme="dark"] .pf-table tbody tr:hover {
background: rgba(255, 255, 255, 0.03);
}
[data-theme="dark"] .pf-modal-content {
background: var(--bg-panel);
border: 1px solid var(--border-light);
}
[data-theme="dark"] .pf-modal-title {
border-color: var(--border-light);
color: var(--text-main);
}
[data-theme="dark"] .modal-footer {
border-color: var(--border-light);
}
[data-theme="dark"] .pf-footer {
background: var(--bg-panel);
border-color: var(--border-light);
}
[data-theme="dark"] .pf-toast {
background: var(--bg-panel);
color: var(--text-main);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
}
[data-theme="dark"] .pf-login-card {
background: var(--bg-panel);
border-color: var(--border-light);
}
[data-theme="dark"] .pf-login-error {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
}
[data-theme="dark"] .btn-icon-action.edit:hover {
background: rgba(59, 130, 246, 0.15);
}
[data-theme="dark"] .btn-icon-action.delete:hover {
background: rgba(239, 68, 68, 0.1);
}
/* ─── Pill toggle thème ────────────────────────────────────── */
.theme-switch {
display: inline-flex; align-items: center; gap: 5px;
background: none; border: none; cursor: pointer;
padding: 3px 5px; border-radius: 8px; transition: background .15s;
display: inline-flex;
align-items: center;
gap: 5px;
background: none;
border: none;
cursor: pointer;
padding: 3px 5px;
border-radius: 8px;
transition: background 0.15s;
}
.theme-switch:hover {
background: #f1f5f9;
}
[data-theme="dark"] .theme-switch:hover {
background: rgba(255, 255, 255, 0.08);
}
.ts-sun,
.ts-moon {
font-size: 0.85rem;
line-height: 1;
transition: opacity 0.2s;
user-select: none;
}
[data-theme="dark"] .ts-sun {
opacity: 0.3;
}
.ts-moon {
opacity: 0.3;
}
[data-theme="dark"] .ts-moon {
opacity: 1;
}
.theme-switch:hover { background: #f1f5f9; }
[data-theme="dark"] .theme-switch:hover { background: rgba(255,255,255,.08); }
.ts-sun, .ts-moon { font-size: .85rem; line-height: 1; transition: opacity .2s; user-select: none; }
[data-theme="dark"] .ts-sun { opacity: .3; }
.ts-moon { opacity: .3; }
[data-theme="dark"] .ts-moon { opacity: 1; }
.ts-track {
position: relative; width: 36px; height: 20px;
background: #cbd5e1; border-radius: 999px;
flex-shrink: 0; transition: background .25s;
position: relative;
width: 36px;
height: 20px;
background: #cbd5e1;
border-radius: 999px;
flex-shrink: 0;
transition: background 0.25s;
}
[data-theme="dark"] .ts-track {
background: #2563eb;
}
[data-theme="dark"] .ts-track { background: #2563eb; }
.ts-thumb {
position: absolute; top: 3px; left: 3px;
width: 14px; height: 14px; border-radius: 50%;
background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.3);
transition: transform .25s cubic-bezier(.4,0,.2,1);
position: absolute;
top: 3px;
left: 3px;
width: 14px;
height: 14px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
[data-theme="dark"] .ts-thumb {
transform: translateX(16px);
}
[data-theme="dark"] .ts-thumb { transform: translateX(16px); }
+5 -3
View File
@@ -130,10 +130,12 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
*/
window.I18N = <?php echo json_encode($current_translations_array ?? [], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); ?>;
// ✅ Injection sécurisée (évite le crash si config.php n'est pas chargé)
// ✅ Injection sécurisée
window.CONFIG = {
ID_ALEX: <?php echo defined('ID_ALEX') ? ID_ALEX : 2; ?>,
ID_LAIA: <?php echo defined('ID_LAIA') ? ID_LAIA : 3; ?>,
// Liste complète des profils de la famille (Membres, Nounou, Enfants)
PEOPLE: <?= isset($familyPeople) ? json_encode($familyPeople) : '[]' ?>,
// ID du user global connecté (pour savoir "qui" clique)
CURRENT_USER_ID: <?= isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : 'null' ?>,
CURRENCY: '<?php echo defined('CURRENCY') ? CURRENCY : "€"; ?>',
ZONE_SCOLAIRE: '<?php echo defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : "C"; ?>'
};
+5
View File
@@ -54,3 +54,8 @@ function verify_csrf(?string $token): bool
}
return hash_equals($_SESSION['csrf_token'], $token);
}
if (!isset($familyPeople) && !empty($family_id)) {
require_once __DIR__ . '/db.php';
$stmtPeople = $pdo->query("SELECT id, name, user_id FROM pf_people ORDER BY id ASC");
$familyPeople = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
}
-7
View File
@@ -1,7 +0,0 @@
<?php
// includes/config.php
// HouseHub Config - Version dynamique
if (!defined('ID_ALEX')) {
define('ID_ALEX', 2);
define('ID_LAIA', 3);
}
-4
View File
@@ -1,8 +1,4 @@
<?php
if (file_exists(__DIR__ . '/config.php')) {
require_once __DIR__ . '/config.php';
}
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
+191 -19
View File
@@ -1,46 +1,218 @@
<?php
// Script de migration pour mettre à jour TOUTES les bases de données familiales
// Script de migration : Refonte globale Multi-Tenant & Synchronisation des schémas
require_once __DIR__ . '/includes/meta_db.php';
echo "<h1>🚀 Début de la migration...</h1>";
echo "<h1>🚀 Début de la migration Globale ...</h1>";
// 1. Récupérer toutes les bases de données des familles
$stmt = $meta_pdo->query("SELECT name, db_name FROM families WHERE db_name != ''");
// ==========================================
// 0. MISE À JOUR DE LA META DB
// ==========================================
echo "<h3>Mise à jour Meta DB (househub_meta)</h3><ul>";
try {
$meta_pdo->exec("ALTER TABLE user_calendar_integrations ADD COLUMN calendar_prefs_json TEXT DEFAULT NULL");
echo "<li><span style='color:green'>Colonne 'calendar_prefs_json' ajoutée à user_calendar_integrations.</span></li>";
} catch (\PDOException $e) {
if ($e->getCode() == '42S21' || strpos($e->getMessage(), '1060') !== false) {
echo "<li><span style='color:gray'>Colonne 'calendar_prefs_json' déjà présente.</span></li>";
} else { throw $e; }
}
echo "</ul>";
// ==========================================
// MIGRATIONS PAR FAMILLE
// ==========================================
$stmt = $meta_pdo->query("SELECT id, name, db_name FROM families WHERE db_name != ''");
$families = $stmt->fetchAll();
$host = getenv('DB_HOST') ?: 'househub-db';
$user = getenv('DB_USER') ?: 'househub';
$pass = getenv('DB_PASS') ?: 'changeme';
// 2. Boucler sur chaque famille
foreach ($families as $f) {
$db_name = $f['db_name'];
echo "Mise à jour de la famille <strong>{$f['name']}</strong> (Base : $db_name) ... ";
$family_id = $f['id'];
echo "<h3>Mise à jour de <strong>{$f['name']}</strong> ($db_name)</h3><ul>";
$p1_name = null;
$p2_name = null;
try {
$fam_pdo = new PDO("mysql:host=$host;dbname=$db_name;charset=utf8mb4", $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
// 3. Injecter la nouvelle table
$sql = "
CREATE TABLE IF NOT EXISTS pf_foyer_settings (
// 1. Table pf_foyer_settings
$fam_pdo->exec("CREATE TABLE IF NOT EXISTS pf_foyer_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
currency VARCHAR(10) NOT NULL DEFAULT '€',
zone_scolaire VARCHAR(5) NOT NULL DEFAULT 'C',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
$fam_pdo->exec("INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C');");
echo "<li><span style='color:green'>Table pf_foyer_settings vérifiée/créée.</span></li>";
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C');
";
$fam_pdo->exec($sql);
echo "<span style='color:green'>✅ OK</span><br>";
// 2. Colonne external_href pour le Calendrier iOS
try {
$fam_pdo->exec("ALTER TABLE pf_calendar_event_links ADD COLUMN external_href VARCHAR(2048) DEFAULT NULL");
echo "<li><span style='color:green'>Colonne 'external_href' ajoutée à pf_calendar_event_links.</span></li>";
} catch (\PDOException $e) {
if ($e->getCode() != '42S21' && strpos($e->getMessage(), '1060') === false) throw $e;
}
// 3. Colonne budget_item_id pour les règles d'import
try {
$fam_pdo->exec("ALTER TABLE pf_import_rules ADD COLUMN budget_item_id INT(11) DEFAULT NULL");
echo "<li><span style='color:green'>Colonne 'budget_item_id' ajoutée à pf_import_rules.</span></li>";
} catch (\PDOException $e) {
if ($e->getCode() != '42S21' && strpos($e->getMessage(), '1060') === false) throw $e;
}
// 4. Uniformisation des VARCHAR de dates pour le budget (YYYY-MM-01 = 10 chars)
$fam_pdo->exec("ALTER TABLE pf_expenses MODIFY gestion_month VARCHAR(10) NOT NULL");
$fam_pdo->exec("ALTER TABLE pf_alloc_values MODIFY month_date VARCHAR(10) NOT NULL");
$fam_pdo->exec("ALTER TABLE pf_savings MODIFY month_date VARCHAR(10) NOT NULL");
echo "<li><span style='color:green'>Formats de dates (VARCHAR 10) uniformisés pour le budget.</span></li>";
// 5. GESTION DE PF_PEOPLE (user_id, role, color)
echo "<li><strong>Mise à jour pf_people :</strong> ";
try { $fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN user_id INT NULL DEFAULT NULL"); } catch (\Exception $e) {}
try { $fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN role VARCHAR(50) DEFAULT NULL"); } catch (\Exception $e) {}
try { $fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN color VARCHAR(7) DEFAULT '#0891b2'"); } catch (\Exception $e) {}
$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();
foreach ($users as $u) {
$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) {
$fam_pdo->prepare("UPDATE pf_people SET user_id = ? WHERE id = ?")->execute([$u['id'], $exists]);
} else {
$fam_pdo->prepare("INSERT INTO pf_people (name, user_id) VALUES (?, ?)")->execute([$u['display_name'] ?: $u['username'], $u['id']]);
}
}
$fam_pdo->exec("UPDATE pf_people SET role = 'parent' WHERE user_id IS NOT NULL AND role IS NULL;");
$fam_pdo->exec("UPDATE pf_people SET role = 'nounou' WHERE LOWER(name) = 'carole';");
$pIds = $fam_pdo->query("SELECT id FROM pf_people WHERE role = 'parent' ORDER BY id ASC")->fetchAll(PDO::FETCH_COLUMN);
if (isset($pIds[0])) $fam_pdo->exec("UPDATE pf_people SET color = '#0891b2' WHERE id = " . (int)$pIds[0] . " AND color IS NULL");
if (isset($pIds[1])) $fam_pdo->exec("UPDATE pf_people SET color = '#f59e0b' WHERE id = " . (int)$pIds[1] . " AND color IS NULL");
echo "<span style='color:blue'>OK</span></li>";
// ==========================================
// 6. REFONTE RELATIONNELLE : pf_alloc_values (Colonnes -> Lignes)
// ==========================================
echo "<li><strong>Table pf_alloc_values (Normalisation) :</strong> ";
// Vérifier si la table est déjà convertie
$checkNewFormat = $fam_pdo->query("SHOW COLUMNS FROM pf_alloc_values LIKE 'person_id'")->rowCount();
if ($checkNewFormat > 0) {
echo "<span style='color:gray'>Déjà convertie au format relationnel.</span></li>";
} else {
// A. Récupérer l'ordre des parents réels pour faire le mapping d'index
$stmtParents = $fam_pdo->query("SELECT id, name FROM pf_people WHERE role = 'parent' ORDER BY id ASC");
$orderedParents = $stmtParents->fetchAll();
// B. Sauvegarder les anciennes données à migrer
$oldAllocations = $fam_pdo->query("SELECT * FROM pf_alloc_values")->fetchAll(PDO::FETCH_ASSOC);
// C. Supprimer l'ancienne table
$fam_pdo->exec("DROP TABLE IF EXISTS pf_alloc_values");
// D. Créer la nouvelle table normalisée
$fam_pdo->exec("CREATE TABLE pf_alloc_values (
id INT AUTO_INCREMENT PRIMARY KEY,
month_date VARCHAR(10) NOT NULL,
cat_id INT NOT NULL,
person_id INT NOT NULL,
amount DECIMAL(10,2) DEFAULT 0.00,
UNIQUE KEY uq_alloc_person (month_date, cat_id, person_id),
FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
// E. Migration des données
$stmtInsert = $fam_pdo->prepare("INSERT INTO pf_alloc_values (month_date, cat_id, person_id, amount) VALUES (?, ?, ?, ?)");
$migratedRows = 0;
foreach ($oldAllocations as $oldRow) {
// Mapping des anciennes colonnes potentielles vers les index de parents
$possibleColumns = [
0 => ['amount_p1', 'amount_alex'],
1 => ['amount_p2', 'amount_laia'],
2 => ['amount_p3'],
3 => ['amount_p4']
];
foreach ($possibleColumns as $parentIndex => $colNames) {
if (!isset($orderedParents[$parentIndex])) continue;
$targetParentId = $orderedParents[$parentIndex]['id'];
foreach ($colNames as $col) {
if (isset($oldRow[$col]) && (float)$oldRow[$col] > 0) {
$stmtInsert->execute([
$oldRow['month_date'],
$oldRow['cat_id'],
$targetParentId,
(float)$oldRow[$col]
]);
$migratedRows++;
break; // Passer à l'index parent suivant dès qu'on a trouvé une valeur
}
}
}
}
echo "<span style='color:green'>Succès ! Nouvelle table créée, $migratedRows lignes migrées.</span></li>";
}
// 7. GESTION DU BUDGET (pf_alloc_categories & pf_salary_config)
$fam_pdo->exec("UPDATE pf_alloc_categories SET name = 'Eco P1' WHERE name LIKE 'Eco Alex%'");
$fam_pdo->exec("UPDATE pf_alloc_categories SET name = 'Eco P2' WHERE name LIKE 'Eco Laia%'");
$stmtParents = $fam_pdo->query("SELECT name FROM pf_people WHERE role = 'parent' ORDER BY id ASC");
$parents = $stmtParents->fetchAll();
if (count($parents) >= 2) {
$fam_pdo->prepare("UPDATE pf_salary_config SET person = ? WHERE person = 'Alex'")->execute([$parents[0]['name']]);
$fam_pdo->prepare("UPDATE pf_salary_config SET person = ? WHERE person = 'Laia'")->execute([$parents[1]['name']]);
}
} catch (\PDOException $e) {
echo "<span style='color:red'>❌ Erreur : " . $e->getMessage() . "</span><br>";
echo "<li style='color:red'>❌ Erreur : " . $e->getMessage() . "</li>";
}
echo "</ul>";
// ==========================================
// 8. SEPARATION CIBLE BUDGET / DESTINATION VIREMENT
// ==========================================
echo "<li><strong>Table pf_alloc_categories (Fix Collision) :</strong> ";
try {
$fam_pdo->exec("ALTER TABLE pf_alloc_categories ADD COLUMN transfer_dest VARCHAR(50) DEFAULT NULL AFTER target");
echo "<span style='color:green'>Succès ! Colonne 'transfer_dest' ajoutée pour préserver vos objectifs chiffrés.</span></li>";
} catch (\PDOException $e) {
echo "<span style='color:gray'>La colonne transfer_dest existe déjà.</span></li>";
}
// ==========================================
// 9. TRANSFERT DES DONNÉES (Cible -> Destination)
// ==========================================
echo "<li><strong>Table pf_alloc_categories (Récupération des données) :</strong> ";
try {
// Déplacer les textes (qui commencent par "vers") dans transfer_dest
$updated = $fam_pdo->exec("UPDATE pf_alloc_categories SET transfer_dest = target, target = '0' WHERE target LIKE 'vers %'");
echo "<span style='color:green'>$updated destinations récupérées et déplacées proprement.</span></li>";
// Remettre la colonne target en format numérique maintenant qu'elle est propre
$fam_pdo->exec("ALTER TABLE pf_alloc_categories MODIFY target DECIMAL(10,2) DEFAULT 0.00");
echo "<li><span style='color:green'>Colonne 'target' re-sécurisée en format monétaire DECIMAL(10,2).</span></li>";
} catch (\PDOException $e) {
echo "<span style='color:red'>Erreur : " . $e->getMessage() . "</span></li>";
}
}
echo "<h2>🎉 Migration terminée ! Tu peux supprimer ce fichier.</h2>";
echo "<h2>🎉 Migration terminée avec succès !</h2>";
?>
+273 -69
View File
@@ -1,7 +1,6 @@
/* modules/budget/budget.css */
/* --- 1. VARIABLES & BASE --- */
/* Le root garde les couleurs sémantiques dont le JS/PHP a besoin localement */
.budget-view {
font-family:
"Segoe UI",
@@ -128,7 +127,7 @@
/* --- 3. BOUTONS SPÉCIFIQUES & ACTIONS --- */
.pf-btn {
border-radius: 50px; /* Surcharge le global pour le budget */
border-radius: 50px;
}
.btn-icon {
background: transparent;
@@ -263,7 +262,6 @@ td:hover .btn-cell-delete {
border-top: 1px solid #e2e8f0;
}
/* Inputs modifiés */
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -346,20 +344,94 @@ input[type="number"] {
gap: 30px;
}
.prev-section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #e2e8f0;
display: flex !important;
flex-direction: row !important;
justify-content: space-between !important;
align-items: center !important;
margin-bottom: 20px;
padding: 15px;
background: #f8fafc;
border-radius: 12px;
border: 1px solid #e2e8f0;
width: 100% !important;
box-sizing: border-box;
}
.prev-section-header h2 {
margin: 0;
font-size: 1.5rem;
font-size: 1.3rem;
font-weight: 800;
color: #0f172a;
letter-spacing: -0.025em;
}
.prev-header-left {
display: flex;
gap: 10px;
align-items: center;
}
.prev-header-actions {
display: flex;
gap: 10px;
}
/* Configuration Chromatique Parent 1 */
.txt-p1,
.txt-alex {
color: color-mix(in srgb, var(--p1-main, #0891b2) 85%, #000000) !important;
font-weight: bold;
text-align: center;
}
.col-p1,
.col-alex {
color: color-mix(in srgb, var(--p1-main, #0891b2) 85%, #000000) !important;
text-align: center;
}
th.col-p1,
td.col-p1 {
background-color: color-mix(
in srgb,
var(--p1-main, #0891b2) 8%,
#ffffff
) !important;
text-align: center;
}
input.txt-p1 {
color: color-mix(in srgb, var(--p1-main, #0891b2) 85%, #000000) !important;
text-align: center;
font-weight: 600;
}
input.txt-p1:focus {
border-color: var(--p1-main, #0891b2) !important;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--p1-main, #0891b2) 10%, #ffffff) !important;
}
/* Configuration Chromatique Parent 2 */
.txt-p2 {
color: color-mix(in srgb, var(--p2-main, #f59e0b) 85%, #000000) !important;
font-weight: bold;
text-align: center;
}
.col-p2 {
color: color-mix(in srgb, var(--p2-main, #f59e0b) 85%, #000000) !important;
text-align: center;
}
th.col-p2,
td.col-p2 {
background-color: color-mix(
in srgb,
var(--p2-main, #f59e0b) 8%,
#ffffff
) !important;
text-align: center;
}
input.txt-p2 {
color: color-mix(in srgb, var(--p2-main, #f59e0b) 85%, #000000) !important;
text-align: center;
font-weight: 600;
}
input.txt-p2:focus {
border-color: var(--p2-main, #f59e0b) !important;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--p2-main, #f59e0b) 10%, #ffffff) !important;
}
.prev-input {
width: 100%;
@@ -385,19 +457,6 @@ input[type="number"] {
font-weight: 700;
}
.prev-section-header,
.view-header,
.budget-view > div[style*="justify-content: space-between"] {
display: flex !important;
flex-direction: row !important;
justify-content: space-between !important;
align-items: center !important;
float: none !important;
position: relative !important;
width: 100% !important;
gap: 15px !important;
}
.prev-salary-table {
width: 100%;
max-width: 900px;
@@ -419,6 +478,15 @@ input[type="number"] {
text-align: center;
border-bottom: 1px solid #e2e8f0;
}
.prev-salary-table th small {
font-weight: normal;
text-transform: none;
}
.prev-salary-table th.th-family-savings,
.prev-salary-table td.td-family-savings {
background: #f0f9ff !important;
color: #0284c7 !important;
}
.prev-salary-table th:first-child {
text-align: left;
}
@@ -426,6 +494,22 @@ input[type="number"] {
padding: 8px 12px;
border-bottom: 1px solid var(--border-light);
text-align: center;
vertical-align: middle;
}
.prev-salary-table tbody tr:nth-child(1) td:first-child {
text-align: left !important;
font-weight: bold !important;
color: var(--text-main) !important;
border-left: 4px solid var(--p1-main, #0891b2) !important;
}
/* Parent 2 (Ligne 2, ex: Laia) */
.prev-salary-table tbody tr:nth-child(2) td:first-child {
text-align: left !important;
font-weight: bold !important;
color: var(--text-main) !important;
border-left: 4px solid var(--p2-main, #f59e0b) !important;
}
.prev-timeline-wrapper {
@@ -455,7 +539,7 @@ input[type="number"] {
border-right: 1px solid var(--border-light);
white-space: nowrap;
text-align: center;
font-size: 0.9rem;
vertical-align: middle;
}
.col-sticky {
@@ -490,6 +574,15 @@ input[type="number"] {
border-left: 2px solid white;
padding: 8px !important;
}
.th-month span {
text-transform: capitalize;
}
.th-month .cycle-start-label {
font-size: 0.75rem;
font-weight: normal;
color: #64748b;
margin-top: 2px;
}
.th-month.current {
background: var(--primary) !important;
}
@@ -511,16 +604,42 @@ input[type="number"] {
font-weight: 700;
border-bottom: 2px solid #bfdbfe;
}
.txt-alex {
color: #0891b2;
.row-restant td.td-separator {
border-left: 2px solid #cbd5e1;
background: #e2e8f0;
}
.txt-laia {
color: #f59e0b;
.row-indicative {
background: #f8fafc;
color: #94a3b8;
}
.row-indicative .col-sticky {
opacity: 0.8;
}
.cat-name-label {
font-weight: 600;
color: var(--text-main);
}
.row-indicative .cat-name-label {
color: #64748b;
}
.row-indicative .cat-name-label span {
font-size: 0.7rem;
border: 1px solid #cbd5e1;
border-radius: 4px;
padding: 0 4px;
margin-left: 5px;
}
.cat-target-label {
font-size: 0.75rem;
color: var(--text-muted);
font-style: italic;
text-align: right;
}
.txt-global {
color: var(--text-muted);
font-weight: 700;
background: #f8fafc;
border-left: 2px solid #e2e8f0;
}
.val-ok {
color: var(--success);
@@ -594,6 +713,9 @@ tr:hover .row-actions {
letter-spacing: 0.05em;
text-align: center;
}
.recap-header span {
text-transform: capitalize;
}
.recap-table {
width: 100%;
border-collapse: collapse;
@@ -603,6 +725,7 @@ tr:hover .row-actions {
padding: 10px 15px;
border-bottom: 1px solid var(--border-light);
text-align: center;
vertical-align: middle;
font-size: 0.95rem;
}
.recap-table th {
@@ -618,30 +741,68 @@ tr:hover .row-actions {
font-weight: 600;
color: #1e293b;
}
.col-alex {
background-color: #ecfeff;
color: #0891b2;
border-left: 1px solid #cffafe;
.recap-th-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
th.col-alex {
background-color: #cffafe;
border-bottom-color: #a5f3fc;
.recap-th-content span {
text-transform: uppercase;
}
.col-laia {
background-color: #fffbeb;
color: #d97706;
border-left: 1px solid #fef3c7;
.validation-badge {
background: #10b981;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.7rem;
display: flex;
align-items: center;
gap: 4px;
}
th.col-laia {
background-color: #fef3c7;
border-bottom-color: #fde68a;
/* --- Style de base du bouton de validation (Générique) --- */
.recap-table .pf-btn.btn-small {
background: #ffffff !important;
font-size: 0.7rem !important;
padding: 2px 8px !important;
height: auto !important;
width: auto !important;
font-weight: 600;
border-radius: 9px !important;
box-shadow: none !important;
transform: none !important;
transition: all 0.2s ease;
}
.col-global {
background-color: #f8fafc;
color: #475569;
font-weight: 700;
border-left: 2px solid #e2e8f0;
/* Couleurs dynamiques adaptées au Parent 1 (ex: Féfé) */
.col-p1 .pf-btn.btn-small {
color: var(--p1-main, #0891b2) !important;
border: 1px solid var(--p1-main, #0891b2) !important;
}
.col-p1 .pf-btn.btn-small:hover {
background: color-mix(
in srgb,
var(--p1-main, #0891b2) 12%,
#ffffff
) !important;
}
/* Couleurs dynamiques adaptées au Parent 2 (ex: Laia) */
.col-p2 .pf-btn.btn-small {
color: var(--p2-main, #f59e0b) !important;
border: 1px solid var(--p2-main, #f59e0b) !important;
}
.col-p2 .pf-btn.btn-small:hover {
background: color-mix(
in srgb,
var(--p2-main, #f59e0b) 12%,
#ffffff
) !important;
}
/* Grand Totaux Structuraux */
.row-grand-total td {
background-color: #1e293b;
color: white;
@@ -649,14 +810,22 @@ th.col-laia {
border: none;
padding: 12px 15px;
}
.row-grand-total .col-alex {
background: #164e63;
color: #67e8f9;
.row-grand-total .col-p1 {
background: color-mix(
in srgb,
var(--p1-main, #0891b2) 65%,
#000000
) !important;
color: color-mix(in srgb, var(--p1-main, #0891b2) 20%, #ffffff) !important;
border: none;
}
.row-grand-total .col-laia {
background: #78350f;
color: #fcd34d;
.row-grand-total .col-p2 {
background: color-mix(
in srgb,
var(--p2-main, #f59e0b) 65%,
#000000
) !important;
color: color-mix(in srgb, var(--p2-main, #f59e0b) 20%, #ffffff) !important;
border: none;
}
.row-grand-total .col-global {
@@ -665,7 +834,7 @@ th.col-laia {
border: none;
}
/* --- 9. STYLES SPÉCIFIQUES AU SUIVI MENSUEL --- */
/* --- 9. STYLES DU SUIVI MENSUEL --- */
.cat-card {
background: var(--bg-panel);
border-radius: var(--radius);
@@ -675,20 +844,17 @@ th.col-laia {
overflow: hidden;
position: relative;
}
.cat-card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px; /* Padding interne à l'encart coloré */
padding: 15px;
width: 100%;
}
.cat-card-title-group {
display: flex;
flex-direction: column;
}
.cat-card-subtitle {
font-size: 0.85rem;
color: var(--text-muted);
@@ -768,7 +934,52 @@ input[type="month"].pf-input {
border-radius: 6px;
}
/* --- 10. EPARGNE --- */
/* --- 10. SECTION NOTES BUDGETAIRES --- */
.budget-notes-section {
margin: 20px 0;
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: var(--shadow-sm);
border: 1px solid #e2e8f0;
}
.notes-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.notes-header h3 {
margin: 0;
font-size: 1.1rem;
color: #1e293b;
}
.notes-header h3 span {
text-transform: capitalize;
}
.note-save-indicator {
font-size: 0.85rem;
color: #10b981;
font-weight: bold;
opacity: 0;
transition: opacity 0.3s;
}
#monthNoteArea {
width: 100%;
resize: vertical;
border-color: #cbd5e1;
background: #f8fafc;
margin-bottom: 10px;
}
.notes-footer {
text-align: right;
}
.notes-footer .pf-btn {
width: auto;
display: inline-flex;
}
/* --- 11. EPARGNE --- */
.epargne-inline-input {
width: 100%;
min-width: 60px;
@@ -793,7 +1004,7 @@ input[type="month"].pf-input {
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
}
/* --- 11. CALCULATRICE FLOTTANTE --- */
/* --- 12. CALCULATRICE FLOTTANTE --- */
.pf-fab-sum {
position: fixed;
right: 30px;
@@ -883,7 +1094,7 @@ body.sum-mode-active .sum-target {
transition: background-color 0.2s;
}
/* --- 12. RESPONSIVE MOBILE BUDGET --- */
/* --- 13. RESPONSIVE MOBILE BUDGET --- */
@media (max-width: 768px) {
.budget-view {
padding: 10px !important;
@@ -917,11 +1128,6 @@ body.sum-mode-active .sum-target {
.budget-view .categories-grid > .cat-card {
padding: 0 !important;
}
.budget-view
> div[style*="justify-content: space-between"]:not(.view-header) {
flex-direction: column !important;
align-items: stretch !important;
}
.budget-view
> div[style*="justify-content: space-between"]:not(.view-header) {
flex-direction: column !important;
@@ -1021,7 +1227,6 @@ body.sum-mode-active .sum-target {
justify-content: flex-start;
width: auto !important;
}
.col-sticky,
.sticky-col {
min-width: 120px !important;
@@ -1059,8 +1264,7 @@ body.sum-mode-active .sum-target {
right: auto;
top: auto;
}
.cat-card-header {
padding: 12px 10px; /* Plus serré sur mobile */
padding: 12px 10px;
}
}
+87 -75
View File
@@ -60,59 +60,85 @@ if ($action === 'update_salary_config') {
// 2. MISE A JOUR TABLEAU REPARTITION (AJAX)
if ($action === 'update_allocation') {
header('Content-Type: application/json');
$date = $_POST['month_date'];
$catId = $_POST['cat_id'];
$person = $_POST['person']; // 'amount_alex' ou 'amount_laia'
$value = floatval($_POST['value']);
header('Content-Type: application/json');
$date = $_POST['month_date'];
$catId = (int)$_POST['cat_id'];
$personId = (int)$_POST['person_id']; // 🟢 Reçoit l'ID de la personne directement !
$value = floatval($_POST['value']);
$stmt = $pdo->prepare("INSERT INTO pf_alloc_values (month_date, cat_id, $person) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE $person = VALUES($person)");
$stmt->execute([$date, $catId, $value]);
if ($catId <= 0 || $personId <= 0) {
echo json_encode(['success' => false, 'error' => 'Données invalides.']);
exit;
}
$stmt = $pdo->prepare("INSERT INTO pf_alloc_values (month_date, cat_id, person_id, amount)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE amount = VALUES(amount)");
$stmt->execute([$date, $catId, $personId, $value]);
echo json_encode(['success' => true]);
exit;
}
// 3. GESTION DES CATEGORIES (Ajout)
if ($action === 'add_category') {
$name = trim($_POST['name']);
$target = trim($_POST['target']);
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
if (!empty($name)) {
$stmt = $pdo->prepare("INSERT INTO pf_alloc_categories (name, target, holiday_id) VALUES (?, ?, ?)");
$stmt->execute([$name, $target, $holiday_id]);
}
// --- NOUVEAU : Réponse AJAX ---
if (isset($_POST['ajax'])) {
header('Content-Type: application/json');
echo json_encode(['success' => true]);
try {
$name = trim($_POST['name']);
$transfer_dest = trim($_POST['transfer_dest'] ?? '');
$target = floatval($_POST['target'] ?? 0);
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
if (!empty($name)) {
$stmt = $pdo->prepare("INSERT INTO pf_alloc_categories (name, target, transfer_dest, holiday_id) VALUES (?, ?, ?, ?)");
$stmt->execute([$name, $target, $transfer_dest, $holiday_id]);
}
if (isset($_POST['ajax'])) {
header('Content-Type: application/json');
echo json_encode(['success' => true]);
exit;
}
header("Location: " . $_SERVER['HTTP_REFERER']);
exit;
} catch (Exception $e) {
if (isset($_POST['ajax'])) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
exit;
}
die($e->getMessage());
}
header("Location: " . $_SERVER['HTTP_REFERER']);
exit;
}
// 4. MODIFICATION D'UNE CATEGORIE
if ($action === 'update_category') {
$id = (int)$_POST['cat_id'];
$name = trim($_POST['name']);
$target = trim($_POST['target']);
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
try {
$id = (int)$_POST['cat_id'];
$name = trim($_POST['name']);
$transfer_dest = trim($_POST['transfer_dest'] ?? '');
$target = floatval($_POST['target'] ?? 0);
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
if ($id > 0 && !empty($name)) {
$stmt = $pdo->prepare("UPDATE pf_alloc_categories SET name = ?, target = ?, holiday_id = ? WHERE id = ?");
$stmt->execute([$name, $target, $holiday_id, $id]);
}
if ($id > 0 && !empty($name)) {
$stmt = $pdo->prepare("UPDATE pf_alloc_categories SET name = ?, target = ?, transfer_dest = ?, holiday_id = ? WHERE id = ?");
$stmt->execute([$name, $target, $transfer_dest, $holiday_id, $id]);
}
// --- NOUVEAU : Réponse AJAX ---
if (isset($_POST['ajax'])) {
header('Content-Type: application/json');
echo json_encode(['success' => true]);
if (isset($_POST['ajax'])) {
header('Content-Type: application/json');
echo json_encode(['success' => true]);
exit;
}
header("Location: " . $_SERVER['HTTP_REFERER']);
exit;
} catch (Exception $e) {
if (isset($_POST['ajax'])) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
exit;
}
die($e->getMessage());
}
header("Location: " . $_SERVER['HTTP_REFERER']);
exit;
}
// 5. SUPPRESSION CATEGORIE
@@ -136,43 +162,48 @@ if ($action === 'delete_category') {
// 6. VALIDATION DES VIREMENTS (Complex Business Logic)
if ($action === 'validate_transfers') {
header('Content-Type: application/json');
$person = $_POST['person'];
$personId = (int)$_POST['person_id']; // Reçoit l'ID parent
$monthDate = $_POST['month_date'];
try {
$pdo->beginTransaction();
$stmtP = $pdo->prepare("SELECT name FROM pf_people WHERE id = ?");
$stmtP->execute([$personId]);
$dbPersonName = $stmtP->fetchColumn();
if (!$dbPersonName) throw new Exception("Parent introuvable.");
$stmt = $pdo->prepare("
SELECT v.*, c.name as cat_name, c.target, c.holiday_id
FROM pf_alloc_values v
SELECT v.amount, c.name as cat_name, c.transfer_dest, c.holiday_id
FROM pf_alloc_values v
JOIN pf_alloc_categories c ON v.cat_id = c.id
WHERE v.month_date = ?
WHERE v.month_date = ? AND v.person_id = ?
");
$stmt->execute([$monthDate]);
$stmt->execute([$monthDate, $personId]);
$budgetLines = $stmt->fetchAll(PDO::FETCH_ASSOC);
$transfersToDo = [];
foreach ($budgetLines as $line) {
$amount = ($person === 'Alex') ? $line['amount_alex'] : $line['amount_laia'];
if ($amount <= 0) continue;
$amount = (float)$line['amount'];
if ($amount <= 0) continue;
$target = trim($line['target']);
$dest = trim($line['transfer_dest']);
$catName = trim($line['cat_name']);
$holidayId = $line['holiday_id'];
$holidayId = $line['holiday_id'];
$targetOwner = null;
if ($target === 'vers L.Perso') { $targetOwner = $person; }
elseif ($target === 'vers L.Pol') { $targetOwner = 'Pol'; }
elseif ($target === 'vers L.Pep') { $targetOwner = 'Pep'; }
elseif ($target === 'vers commune') { continue; }
if ($dest === 'vers L.Perso') { $targetOwner = $dbPersonName; }
elseif ($dest === 'vers L.Pol') { $targetOwner = 'Pol'; }
elseif ($dest === 'vers L.Pep') { $targetOwner = 'Pep'; }
elseif ($dest === 'vers commune') { continue; }
if ($targetOwner) {
if (!isset($transfersToDo[$targetOwner])) {
$transfersToDo[$targetOwner] = ['total_add' => 0, 'cats' => []];
}
$transfersToDo[$targetOwner]['total_add'] += $amount;
if (!isset($transfersToDo[$targetOwner]['cats'][$catName])) {
$transfersToDo[$targetOwner]['cats'][$catName] = ['amount' => 0, 'holiday_id' => $holidayId];
}
@@ -181,7 +212,6 @@ if ($action === 'validate_transfers') {
}
foreach ($transfersToDo as $owner => $data) {
// A. VERIFIER EXISTENCE (Inchangé)
$stmtCheck = $pdo->prepare("SELECT COUNT(*) FROM pf_savings WHERE owner = ? AND month_date = ? AND category = 'TOTAL_BANQUE'");
$stmtCheck->execute([$owner, $monthDate]);
$exists = $stmtCheck->fetchColumn() > 0;
@@ -200,54 +230,38 @@ if ($action === 'validate_transfers') {
}
}
// B. UPDATE TOTAL (Inchangé)
$stmtUpdTotal = $pdo->prepare("UPDATE pf_savings SET amount = amount + ? WHERE owner = ? AND month_date = ? AND category = 'TOTAL_BANQUE'");
$stmtUpdTotal->execute([$data['total_add'], $owner, $monthDate]);
// C. UPDATE CATÉGORIES (Modifié pour gérer le holiday_id)
foreach ($data['cats'] as $catName => $catInfo) {
$catAmount = $catInfo['amount'];
$catHolidayId = $catInfo['holiday_id']; // NOUVEAU
$catHolidayId = $catInfo['holiday_id'];
if ($catName === 'Eco Alex' || $catName === 'Eco Laia') { continue; }
if (strpos($catName, 'Eco P') === 0) { continue; }
$stmtCheckCat = $pdo->prepare("SELECT id FROM pf_savings WHERE owner = ? AND month_date = ? AND category = ?");
$stmtCheckCat->execute([$owner, $monthDate, $catName]);
$catId = $stmtCheckCat->fetchColumn();
if ($catId) {
// Update : On actualise aussi le holiday_id au cas où il aurait changé
$stmtUpdateCat = $pdo->prepare("UPDATE pf_savings SET amount = amount + ?, holiday_id = ? WHERE id = ?");
$stmtUpdateCat->execute([$catAmount, $catHolidayId, $catId]);
} else {
// Insert
$stmtInsertCat = $pdo->prepare("INSERT INTO pf_savings (month_date, owner, category, amount, holiday_id) VALUES (?, ?, ?, ?, ?)");
$stmtInsertCat->execute([$monthDate, $owner, $catName, $catAmount, $catHolidayId]);
}
}
}
// 3. ENREGISTRER LA VALIDATION (Mise à jour table existante)
// a. Trouver l'ID de la catégorie système
$stmtSys = $pdo->prepare("SELECT id FROM pf_alloc_categories WHERE name = 'SYSTEM_VALIDATION' LIMIT 1");
$stmtSys->execute();
$sysCatId = $stmtSys->fetchColumn();
if ($sysCatId) {
if ($person === 'Alex') {
$sql = "INSERT INTO pf_alloc_values (month_date, cat_id, amount_alex, amount_laia)
VALUES (?, ?, 1, 0)
ON DUPLICATE KEY UPDATE amount_alex = 1";
} else {
$sql = "INSERT INTO pf_alloc_values (month_date, cat_id, amount_alex, amount_laia)
VALUES (?, ?, 0, 1)
ON DUPLICATE KEY UPDATE amount_laia = 1";
}
$stmtVal = $pdo->prepare($sql);
$stmtVal->execute([$monthDate, $sysCatId]);
$stmtVal = $pdo->prepare("INSERT INTO pf_alloc_values (month_date, cat_id, person_id, amount)
VALUES (?, ?, ?, 1)
ON DUPLICATE KEY UPDATE amount = 1");
$stmtVal->execute([$monthDate, $sysCatId, $personId]);
}
$pdo->commit();
@@ -258,6 +272,4 @@ if ($action === 'validate_transfers') {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
File diff suppressed because it is too large Load Diff
+39 -13
View File
@@ -57,12 +57,11 @@ if (isset($_POST['action']) && $_POST['action'] === 'reopen_month') {
exit;
}
if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
$count = 0;
$stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmtRule = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category) VALUES (?, ?) ON DUPLICATE KEY UPDATE category = VALUES(category)");
// Mémorisation du mapping étendu (incluant budget_item_id)
$stmtRule = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category, budget_item_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE category = VALUES(category), budget_item_id = VALUES(budget_item_id)");
if (isset($_POST['lines']) && is_array($_POST['lines'])) {
foreach ($_POST['lines'] as $line) {
@@ -81,7 +80,7 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
try {
$stmtExp->execute([$dateToSave, $gestionMonthLine, $cat, $line['label'], $finalAmount, $line['ref'], $budgetItemId, $holidayId]);
$stmtRule->execute([$line['label'], $cat]);
$stmtRule->execute([$line['label'], $cat, $budgetItemId]);
$count++;
} catch (Exception $e) { continue; }
}
@@ -110,7 +109,15 @@ $showPreview = false;
if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
$file = $_FILES['csv_file']['tmp_name'];
$handle = fopen($file, "r");
$rules = []; try { $rules = $pdo->query("SELECT keyword, category FROM pf_import_rules")->fetchAll(PDO::FETCH_KEY_PAIR); } catch(Exception $e){}
$rules = [];
try {
$stmtRules = $pdo->query("SELECT keyword, category, budget_item_id FROM pf_import_rules");
while ($r = $stmtRules->fetch(PDO::FETCH_ASSOC)) {
$rules[$r['keyword']] = ['cat' => $r['category'], 'budget_item_id' => $r['budget_item_id']];
}
} catch(Exception $e){}
$existingRefs = []; try { $existingRefs = $pdo->query("SELECT import_ref FROM pf_expenses WHERE import_ref IS NOT NULL")->fetchAll(PDO::FETCH_COLUMN); } catch(Exception $e){}
fgetcsv($handle, 1000, ";", "\"", "\\");
@@ -129,9 +136,16 @@ if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
$isDuplicate = in_array($uniqueKey, $existingRefs);
$suggestedCat = '';
foreach ($rules as $kw => $c) { if (stripos($label, $kw) !== false) { $suggestedCat = $c; break; } }
$suggestedItemId = null;
foreach ($rules as $kw => $ruleData) {
if (stripos($label, $kw) !== false) {
$suggestedCat = $ruleData['cat'];
$suggestedItemId = $ruleData['budget_item_id'];
break;
}
}
$csvData[] = ['date'=>$dateSql, 'label'=>$label, 'amount'=>$amount, 'cat'=>$suggestedCat, 'ref'=>$uniqueKey, 'is_duplicate'=>$isDuplicate, 'is_credit'=>$isCredit];
$csvData[] = ['date'=>$dateSql, 'label'=>$label, 'amount'=>$amount, 'cat'=>$suggestedCat, 'suggested_item_id'=>$suggestedItemId, 'ref'=>$uniqueKey, 'is_duplicate'=>$isDuplicate, 'is_credit'=>$isCredit];
}
fclose($handle);
$showPreview = true;
@@ -224,6 +238,7 @@ $categoriesConfig = [
'School' => ['type'=>'debit', 'label'=>tr('cat_school'), 'budget'=>$budget_school, 'color'=>'#10b981', 'suggestions'=>[]],
'Frais' => ['type'=>'debit', 'label'=>tr('cat_fixed'), 'budget'=>$budget_frais, 'color'=>'#ef4444', 'suggestions'=>[]],
'Autres' => ['type'=>'debit', 'label'=>tr('cat_others'), 'budget'=>$budget_autres, 'color'=>'#64748b', 'suggestions'=>['Restaurant', 'Cadeau']],
'Apports' => ['type'=>'debit', 'label'=>tr('cat_contributions') ?? 'Apports & Projets', 'budget'=>0, 'color'=>'#0ea5e9', 'suggestions'=>['Alex', 'Laia', 'Remboursement']],
'LivretA' => ['type'=>'debit', 'label'=>tr('cat_savings'),'budget'=>0, 'color'=>'#8b5cf6', 'suggestions'=>['Virement']]
];
@@ -695,11 +710,15 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
</select>
<select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-frais" onchange="checkValidation()" style="display:none; padding:4px; font-size:0.85rem; flex:1;" disabled>
<option value="">-- <?= tr('bud_is_charge') ?> --</option>
<?php foreach ($fixedChargesList as $fc): ?><option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['name']) ?></option><?php endforeach; ?>
<?php foreach ($fixedChargesList as $fc): ?>
<option value="<?= $fc['id'] ?>" <?= ($row['suggested_item_id'] == $fc['id']) ? 'selected' : '' ?>><?= htmlspecialchars($fc['name']) ?></option>
<?php endforeach; ?>
</select>
<select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-income" onchange="checkValidation()" style="display:none; padding:4px; font-size:0.85rem; flex:1;" disabled>
<option value="">-- <?= tr('bud_is_income') ?> --</option>
<?php foreach ($incomeList as $inc): ?><option value="<?= $inc['id'] ?>"><?= htmlspecialchars($inc['name']) ?></option><?php endforeach; ?>
<?php foreach ($incomeList as $inc): ?>
<option value="<?= $inc['id'] ?>" <?= ($row['suggested_item_id'] == $inc['id']) ? 'selected' : '' ?>><?= htmlspecialchars($inc['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</td>
@@ -792,10 +811,14 @@ function openEditModal(e) {
else if (e.category === 'Income') document.getElementById('incomeSelect').value = e.budget_item_id;
}
function handleLineCatChange(select) {
function handleLineCatChange(select, isInit = false) {
const row = select.closest('tr');
const fSel = row.querySelector('.select-frais'); const iSel = row.querySelector('.select-income');
fSel.style.display = 'none'; iSel.style.display = 'none'; fSel.value = ''; iSel.value = ''; fSel.disabled = true; iSel.disabled = true;
fSel.style.display = 'none'; iSel.style.display = 'none';
if (!isInit) { fSel.value = ''; iSel.value = ''; }
fSel.disabled = true; iSel.disabled = true;
if (select.value === 'Frais') { fSel.style.display = 'block'; fSel.disabled = false; }
else if (select.value === 'Income') { iSel.style.display = 'block'; iSel.disabled = false; }
checkValidation();
@@ -815,7 +838,10 @@ function checkValidation() {
else { btn.disabled = false; btn.style.opacity=1; msg.style.display='none'; }
}
if(document.getElementById('formMapping')) { document.querySelectorAll('.line-select').forEach(s => handleLineCatChange(s)); checkValidation(); }
if(document.getElementById('formMapping')) {
document.querySelectorAll('.line-select').forEach(s => handleLineCatChange(s, true));
checkValidation();
}
window.addEventListener('click', (e) => {
if (e.target.classList.contains('pf-modal')) {
@@ -826,7 +852,7 @@ window.addEventListener('click', (e) => {
// --- 2. SUPPRESSION ASYNCHRONE ---
async function deleteExpense(id) {
const confirmed = await pachaConfirm("Suppression", tr('bud_confirm_delete'));
const confirmed = await pachaConfirm("Suppression", window.I18N['bud_confirm_delete']);
if (!confirmed) return;
const formData = new FormData();
+117 -88
View File
@@ -5,32 +5,42 @@ document.addEventListener("DOMContentLoaded", () => {
const CONGE_TYPES = ["OFF_CAROLE", "EXTRA_OFF_CAROLE"];
const GUARDE_TYPES = ["CENTRE", "AVIS"];
const PEP_TYPES = ["PEP_SICK"];
const FAMILY = {
ALEX: { id: 2, prefix: "alex" },
LAIA: { id: 3, prefix: "laia" },
};
// 🟢 Liste globale dynamique des personnes (fini les IDs en dur !)
const peopleList = window.CONFIG.PEOPLE || [];
// On filtre pour ne garder que les parents pour la logique principale des congés
const parents = peopleList.filter(
(p) => p.role && p.role.toLowerCase() === "parent",
);
const LEAVES_CONFIG = {
CP: {
startMonth: 8, // Le cycle commence en août (après la tolérance de juillet)
startMonth: 8,
defaultBalance: 25,
},
JRA: {
// Tu pourras ajouter les années suivantes ici
yearlyTotals: {
2024: 10, // ex: 0.83 * 12 arrondi
2024: 10,
2025: 10,
2026: 11, // ex: 0.9 * 12 arrondi
2026: 11,
},
defaultBalance: 10,
toleranceMonths: 2, // Janvier et Février
toleranceMonths: 2,
maxReport: 2,
},
JA: {
[FAMILY.ALEX.id]: { startMonth: 4, startDay: 29, defaultBalance: 4 },
[FAMILY.LAIA.id]: { startMonth: 10, startDay: 1, defaultBalance: 4 }, // Date de Laia à adapter
},
JA: {}, // Ce tableau est maintenant vide par défaut, on le remplit dynamiquement juste en dessous
};
// 🟢 INITIALISATION DYNAMIQUE DES JOURS D'ANNIVERSAIRE (JA)
parents.forEach((parent) => {
LEAVES_CONFIG.JA[parent.id] = {
startMonth: 4, // Mois par défaut (à faire évoluer en BDD au Niveau 3)
startDay: 29, // Jour par défaut
defaultBalance: 4,
};
});
class FamilyCalendar {
constructor() {
this.planningBody = document.getElementById("planningBody");
@@ -455,12 +465,17 @@ document.addEventListener("DOMContentLoaded", () => {
const d = new Date(l.leave_date + "T00:00:00");
if (d >= w.dayDates.mon && d <= w.dayDates.fri) {
const dur = parseFloat(l.duration) || 1;
const prefix =
l.person_id === FAMILY.ALEX.id
? FAMILY.ALEX.prefix
: l.person_id === FAMILY.LAIA.id
? FAMILY.LAIA.prefix
: null;
// 🟢 Correspondance dynamique pour le stockage des totaux de la semaine
let prefix = null;
if (parents[0] && parseInt(l.person_id) === parseInt(parents[0].id))
prefix = "alex";
else if (
parents[1] &&
parseInt(l.person_id) === parseInt(parents[1].id)
)
prefix = "laia";
if (prefix) w.totals[`${prefix}${l.leave_type}`] += dur;
}
});
@@ -479,17 +494,16 @@ document.addEventListener("DOMContentLoaded", () => {
}
calculateMonthlyBalances() {
const balances = {
[FAMILY.ALEX.id]: { CP: {}, JRA: {}, JA: {} },
[FAMILY.LAIA.id]: { CP: {}, JRA: {}, JA: {} },
};
const balances = {};
// 🟢 On initialise le tableau des soldes pour chaque parent trouvé
parents.forEach((p) => {
balances[p.id] = { CP: {}, JRA: {}, JA: {} };
});
// Liste des mois actuellement affichés dans le planning
const ymSet = new Set();
this.weeks.forEach((w) => ymSet.add(w.monthKey));
const ymList = Array.from(ymSet).sort();
// Pré-calcul de l'utilisation par mois
const usageByMonth = {};
this.leaves.forEach((l) => {
const pid = l.person_id;
@@ -502,7 +516,8 @@ document.addEventListener("DOMContentLoaded", () => {
(usageByMonth[pid][type][ym] || 0) + parseFloat(l.duration);
});
[FAMILY.ALEX.id, FAMILY.LAIA.id].forEach((pid) => {
parents.forEach((parent) => {
const pid = parent.id;
["CP", "JRA", "JA"].forEach((type) => {
ymList.forEach((ym) => {
const [currYear, currMonth] = ym.split("-").map(Number);
@@ -510,8 +525,6 @@ document.addEventListener("DOMContentLoaded", () => {
let cycleStartStr = "";
let initialBalance = 0;
// --- 1. RECHERCHE D'UN CORRECTIF MANUEL (SNAPSHOT) ---
// On cherche le snapshot le plus récent qui est inférieur ou égal au mois en cours de calcul
const latestSnapshot = (this.leaveSnapshots || [])
.filter(
(s) =>
@@ -519,18 +532,14 @@ document.addEventListener("DOMContentLoaded", () => {
s.leave_type == type &&
s.snapshot_date.substring(0, 7) <= ym,
)
// On trie du plus récent au plus ancien pour prendre le premier
.sort((a, b) =>
b.snapshot_date.localeCompare(a.snapshot_date),
)[0];
if (latestSnapshot) {
// Si on trouve un correctif, il devient notre nouveau "point zéro"
cycleStartStr = latestSnapshot.snapshot_date.substring(0, 7);
initialBalance = parseFloat(latestSnapshot.remaining_balance); // Utilisation de VOTRE nom de colonne
}
// --- 2. SINON, CALCUL CLASSIQUE PAR DÉFAUT ---
else {
initialBalance = parseFloat(latestSnapshot.remaining_balance);
} else {
if (type === "CP") {
const refYear =
currMonth >= LEAVES_CONFIG.CP.startMonth
@@ -552,7 +561,6 @@ document.addEventListener("DOMContentLoaded", () => {
LEAVES_CONFIG.JRA.yearlyTotals[currYear] ||
LEAVES_CONFIG.JRA.defaultBalance;
// Logique de report du reliquat de l'année N-1
if (currMonth <= LEAVES_CONFIG.JRA.toleranceMonths) {
const prevYear = currYear - 1;
const prevInitial =
@@ -569,14 +577,10 @@ document.addEventListener("DOMContentLoaded", () => {
0,
prevInitial - usedPrevYear,
);
// Ajout du report plafonné à 2 jours
initialBalance += Math.min(
remainingPrevYear,
LEAVES_CONFIG.JRA.maxReport,
);
console.log(
`[JRA] Personne ${pid}, Année ${currYear}: Report de ${Math.min(remainingPrevYear, LEAVES_CONFIG.JRA.maxReport)}j inclus.`,
);
}
} else if (type === "JA") {
const configJA = LEAVES_CONFIG.JA[pid];
@@ -598,11 +602,8 @@ document.addEventListener("DOMContentLoaded", () => {
}
}
// --- 3. DÉDUCTION DES CONGÉS PRIS DEPUIS LE POINT ZÉRO ---
let usedBeforeCurrentMonth = 0;
Object.keys(usageByMonth[pid]?.[type] || {}).forEach((usedYm) => {
// On ne déduit que ce qui a été posé entre le début du cycle (ou la date du snapshot) et le mois en cours
if (usedYm >= cycleStartStr && usedYm < ym) {
usedBeforeCurrentMonth += usageByMonth[pid][type][usedYm];
}
@@ -688,10 +689,28 @@ document.addEventListener("DOMContentLoaded", () => {
const dayLeaves = this.leaves.filter((l) => l.leave_date === iso);
if (dayLeaves.length) {
let html = `<div style="position:absolute; bottom:0; left:0; width:100%; font-size:9px; line-height:1; display:flex; justify-content:center; gap:2px; pointer-events:none;">`;
if (dayLeaves.some((l) => l.person_id === window.CONFIG.ID_ALEX))
html += `<span style="color:#0f766e; font-weight:800;">A</span>`;
if (dayLeaves.some((l) => l.person_id === window.CONFIG.ID_LAIA))
html += `<span style="color:#b45309; font-weight:800;">L</span>`;
const colors = [
"#0f766e",
"#b45309",
"#047857",
"#4338ca",
"#b91c1c",
];
if (window.CONFIG.PEOPLE && window.CONFIG.PEOPLE.length > 0) {
window.CONFIG.PEOPLE.forEach((person, index) => {
if (
dayLeaves.some(
(l) => parseInt(l.person_id) === parseInt(person.id),
)
) {
const color = colors[index % colors.length];
const initial = person.name.charAt(0).toUpperCase();
html += `<span style="color:${color}; font-weight:800; margin: 0 1px;">${initial}</span>`;
}
});
}
html += `</div>`;
td.innerHTML += html;
}
@@ -731,8 +750,10 @@ document.addEventListener("DOMContentLoaded", () => {
tr.appendChild(tdUse);
});
};
renderPersonCols(FAMILY.ALEX.id, `col-${FAMILY.ALEX.prefix}`);
renderPersonCols(FAMILY.LAIA.id, `col-${FAMILY.LAIA.prefix}`);
parents.forEach((parent, index) => {
const cssPrefix = index % 2 === 0 ? "col-alex" : "col-laia";
renderPersonCols(parent.id, cssPrefix);
});
}
this.planningBody.appendChild(tr);
});
@@ -845,9 +866,9 @@ document.addEventListener("DOMContentLoaded", () => {
container.style.display = "flex";
let html = "";
[FAMILY.ALEX, FAMILY.LAIA].forEach((person) => {
parents.forEach((person) => {
html += `<div class="fc-minimal-balance-card">
<strong class="${person.prefix}">${person.prefix.toUpperCase()}</strong>
<strong class="col-alex">${person.name.toUpperCase()}</strong>
<div class="fc-minimal-chips">`;
["CP", "JRA", "JA"].forEach((type) => {
@@ -989,10 +1010,19 @@ document.addEventListener("DOMContentLoaded", () => {
const dayLeaves = this.leaves.filter((l) => l.leave_date === iso);
if (dayLeaves.length) {
content += `<div style="position:absolute; bottom:2px; left:2px; font-size:10px; font-weight:bold;">`;
if (dayLeaves.some((l) => l.person_id === 2))
content += `<span style="color:#0f766e">A</span> `;
if (dayLeaves.some((l) => l.person_id === 3))
content += `<span style="color:#b45309">L</span>`;
parents.forEach((parent, index) => {
if (
dayLeaves.some(
(l) => parseInt(l.person_id) === parseInt(parent.id),
)
) {
const color = index % 2 === 0 ? "#0f766e" : "#b45309";
const initial = parent.name.charAt(0).toUpperCase();
content += `<span style="color:${color}">${initial}</span> `;
}
});
content += `</div>`;
}
content += `</div>`;
@@ -1557,7 +1587,9 @@ document.addEventListener("DOMContentLoaded", () => {
if (dates.includes(e.date)) activeEvents.add(e.type);
});
const activeLeaves = { 2: new Set(), 3: new Set() };
const activeLeaves = {};
parents.forEach((p) => (activeLeaves[p.id] = new Set()));
this.leaves.forEach((l) => {
if (dates.includes(l.leave_date)) {
if (activeLeaves[l.person_id])
@@ -1623,32 +1655,33 @@ document.addEventListener("DOMContentLoaded", () => {
<button class="fc-menu-btn ${getBtnClass("PEP_SICK")}" data-action="add" data-type="PEP_SICK" style="width:100%">${getBtnIcon("PEP_SICK")}${tr("leg_pep_sick")} 🤒</button>
</div>`;
// 5. Section Enfants
// 5. Section Parents (Congés)
html += `<div class="fc-menu-section">
${buildHeader(tr("fc_menu_kids_leaves"), null, null)}
<div class="fc-menu-leaves-table">
<table>
<thead>
<tr>
<th>
<div class="fc-th-inline">
Alex <button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="2" title="${tr("fc_clear")} Alex">${trashSvg}</button>
</div>
</th>
<th>
<div class="fc-th-inline">
Laia <button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="3" title="${tr("fc_clear")} Laia">${trashSvg}</button>
</div>
</th>
</tr>
<tr>`;
// En-têtes dynamiques (Noms + Bouton Poubelle)
parents.forEach((parent) => {
html += `<th>
<div class="fc-th-inline">
${parent.name} <button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="${parent.id}" title="${tr("fc_clear")} ${parent.name}">${trashSvg}</button>
</div>
</th>`;
});
html += ` </tr>
</thead>
<tbody>`;
// Lignes de congés (CP, JRA, JA)
["CP", "JRA", "JA"].forEach((t) => {
html += `<tr>
<td><button class="fc-menu-btn ${getBtnClass(t, 2)}" data-action="add-leave" data-pid="2" data-type="${t}">${getBtnIcon(t, 2)}${t}</button></td>
<td><button class="fc-menu-btn ${getBtnClass(t, 3)}" data-action="add-leave" data-pid="3" data-type="${t}">${getBtnIcon(t, 3)}${t}</button></td>
</tr>`;
html += `<tr>`;
parents.forEach((parent) => {
html += `<td><button class="fc-menu-btn ${getBtnClass(t, parent.id)}" data-action="add-leave" data-pid="${parent.id}" data-type="${t}">${getBtnIcon(t, parent.id)}${t}</button></td>`;
});
html += `</tr>`;
});
html += ` </tbody>
@@ -1743,22 +1776,18 @@ document.addEventListener("DOMContentLoaded", () => {
},
);
} else if (action === "clear-leaves") {
await this.postApi(
"/modules/family-calendar/includes/api/manage-leaf.php",
{
action: "bulk_delete_day_person",
dates,
person_id: window.CONFIG.ID_ALEX,
},
);
await this.postApi(
"/modules/family-calendar/includes/api/manage-leaf.php",
{
action: "bulk_delete_day_person",
dates,
person_id: window.CONFIG.ID_LAIA,
},
);
if (window.CONFIG.PEOPLE && window.CONFIG.PEOPLE.length > 0) {
for (const person of window.CONFIG.PEOPLE) {
await this.postApi(
"/modules/family-calendar/includes/api/manage-leaf.php",
{
action: "bulk_delete_day_person",
dates,
person_id: person.id,
},
);
}
}
}
await this.refreshAllData();
@@ -51,3 +51,4 @@
[2026-05-06T20:38:24+02:00] RAW INPUT: [{"date":"2026-05-08","type":"AVIS","duration":1,"person":"Carole"}]
[2026-05-06T20:43:43+02:00] RAW INPUT: [{"date":"2026-05-06","type":"AVIS","duration":1,"person":"Carole"}]
[2026-05-06T20:44:36+02:00] RAW INPUT: [{"date":"2026-05-06","type":"PEP_SICK","duration":1,"person":"Carole"}]
[2026-05-20T16:40:17+02:00] RAW INPUT: [{"date":"2026-05-19","type":"OFF_CAROLE","duration":1,"person":"Carole"}]
+17 -1
View File
@@ -259,7 +259,23 @@ if ($action === 'maintenances') {
$photo = handleUpload('invoice_photo', $UPLOAD_DIR); $d = $_POST ?: gBody();
if (!($d['vehicle_id'] ?? null)) gErr('vehicle_id manquant');
$stmt = $pdo->prepare("INSERT INTO pf_maintenances (vehicle_id,type,description,date,km,cost,mechanic,garage_name,next_km,next_date,invoice_photo,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)");
$stmt->execute([$d['vehicle_id'], $d['type']??'', $d['description']??null, $d['date']??date('Y-m-d'), $d['km']??null, $d['cost']??0, $d['mechanic']??null, $d['garage_name']??null, $d['next_km']??null, $d['next_date']??null, $photo, $d['notes']??null]);
$nullInt = fn($v) => ($v ?? '') !== '' ? (int)$v : null;
$nullFloat = fn($v) => ($v ?? '') !== '' ? (float)$v : null;
$nullStr = fn($v) => ($v ?? '') !== '' ? $v : null;
$stmt->execute([
$d['vehicle_id'],
$d['type'] ?? '',
$nullStr($d['description'] ?? null),
$d['date'] ?? date('Y-m-d'),
$nullInt($d['km'] ?? null),
$nullFloat($d['cost'] ?? null),
$nullStr($d['mechanic'] ?? null),
$nullStr($d['garage_name'] ?? null),
$nullInt($d['next_km'] ?? null),
$nullStr($d['next_date'] ?? null),
$photo,
$nullStr($d['notes'] ?? null),
]);
$mid = $pdo->lastInsertId();
if (!empty($d['km'])) { $pdo->prepare("UPDATE pf_vehicles SET current_km = GREATEST(current_km, ?), updated_at = NOW() WHERE id = ?")->execute([$d['km'], $d['vehicle_id']]); }
gOk(['id' => $mid]);
+7
View File
@@ -1398,3 +1398,10 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
#tripMap {
touch-action: none;
}
/* --- Toll cost estimator panel --- */
.hol-cost-stat { background: var(--bg-page, #f8fafc); border: 1px solid var(--border-light, #e2e8f0); border-radius: 10px; padding: .75rem 1rem; text-align: center; }
.hol-cost-stat-val { font-size: 1.2rem; font-weight: 700; color: var(--text-main, #0f172a); }
.hol-cost-stat-label { font-size: .72rem; color: var(--text-muted, #64748b); margin-top: 2px; text-transform: uppercase; letter-spacing: .04em; }
.hol-cost-total { border-color: var(--primary, #4361ee); }
.hol-cost-total .hol-cost-stat-val { color: var(--primary, #4361ee); }
+73
View File
@@ -261,6 +261,22 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
</div>
</div>
<?php if (count($mapPoints) >= 2): ?>
<div class="hol-summary-card" id="hol-cost-panel" style="margin-top:24px;">
<div style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px; margin-bottom:16px;">
<h3 style="margin:0; display:flex; align-items:center; gap:8px;">🚗 Coût du trajet</h3>
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
<label style="font-size:.82rem; color:var(--text-muted);">Conso (L/100km)</label>
<input type="number" id="fuel-l100" value="7" min="3" max="30" step="0.5" style="width:65px; padding:.3rem .5rem; border:1px solid var(--border-light); border-radius:8px; font-size:.85rem;">
<label style="font-size:.82rem; color:var(--text-muted);">Prix carburant (€/L)</label>
<input type="number" id="fuel-price" value="1.85" min="1" max="4" step="0.01" style="width:65px; padding:.3rem .5rem; border:1px solid var(--border-light); border-radius:8px; font-size:.85rem;">
<button onclick="estimateTripCost()" class="pf-btn pf-btn-small">Calculer</button>
</div>
</div>
<div id="hol-cost-result" style="color:var(--text-muted); font-size:.9rem;">Cliquez sur Calculer pour estimer le coût du trajet.</div>
</div>
<?php endif; ?>
<?php if (!empty($holiday['notes'])): ?>
<div class="hol-summary-card" style="margin-top: 24px; padding: 25px; border-left: 5px solid #f59e0b;">
<h3 style="margin: 0 0 15px 0; font-size: 1.2rem; color: #0f172a; display: flex; align-items: center; gap: 8px;">
@@ -415,6 +431,63 @@ window.closePlanningModal = window.closePlanningModal || function() {
if(modal) modal.style.display = 'none';
document.body.classList.remove('no-scroll');
};
async function estimateTripCost() {
const l100 = parseFloat(document.getElementById('fuel-l100').value) || 7;
const price = parseFloat(document.getElementById('fuel-price').value) || 1.85;
const result = document.getElementById('hol-cost-result');
result.innerHTML = '⏳ Calcul en cours…';
const stops = (window.MAP_POINTS || []).map(p => ({lat: p.lat, lng: p.lng, name: p.location_name}));
if (stops.length < 2) { result.innerHTML = 'Pas assez d\'étapes pour calculer.'; return; }
try {
const resp = await fetch('/modules/voyage/api.php?action=estimate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({stops, fuel_l100: l100, fuel_price: price})
});
const data = await resp.json();
if (!data.ok) { result.innerHTML = 'Erreur : ' + (data.error || 'inconnue'); return; }
let html = '<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:12px; margin-bottom:16px;">';
html += `<div class="hol-cost-stat"><div class="hol-cost-stat-val">${Math.round(data.total_km)} km</div><div class="hol-cost-stat-label">Distance totale</div></div>`;
html += `<div class="hol-cost-stat"><div class="hol-cost-stat-val">${data.total_toll.toFixed(2)} €</div><div class="hol-cost-stat-label">Péages estimés</div></div>`;
html += `<div class="hol-cost-stat"><div class="hol-cost-stat-val">${data.fuel_cost.toFixed(2)} €</div><div class="hol-cost-stat-label">Carburant</div></div>`;
html += `<div class="hol-cost-stat hol-cost-total"><div class="hol-cost-stat-val">${data.grand_total.toFixed(2)} €</div><div class="hol-cost-stat-label">Total aller</div></div>`;
html += '</div>';
if (data.segments && data.segments.length) {
html += '<div style="border-top:1px solid var(--border-light); padding-top:12px; margin-top:4px;">';
html += '<div style="font-size:.75rem; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--text-muted); margin-bottom:10px;">Détail du trajet</div>';
data.segments.forEach(s => {
html += `<div style="padding:8px 0; border-bottom:1px solid var(--border-light);">`;
html += `<div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:4px;">
<span style="font-weight:600; font-size:.88rem;">📍 ${esc(s.from)} → ${esc(s.to)}</span>
<span style="font-size:.88rem; color:var(--primary);">péage : <strong>${s.toll.toFixed(2)} €</strong></span>
</div>`;
html += `<div style="font-size:.78rem; color:var(--text-muted);">🛣️ ${Math.round(s.distance_km)} km`;
if (s.entry_plaza && s.exit_plaza) {
html += ` · ${esc(s.entry_plaza)} → ${esc(s.exit_plaza)}`;
if (s.op) html += ` <span style="background:var(--bg-page); border:1px solid var(--border-light); border-radius:4px; padding:0 4px; font-size:.7rem;">${esc(s.op)}</span>`;
} else if (s.note) {
html += ` · <em>${esc(s.note)}</em>`;
}
html += '</div></div>';
});
html += '</div>';
}
function esc(s) {
return String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
result.innerHTML = html;
} catch(e) {
result.innerHTML = 'Erreur de connexion.';
console.error(e);
}
}
</script>
<script src="/modules/holidays/holidays.js"></script>
+243
View File
@@ -0,0 +1,243 @@
<?php
// modules/voyage/api.php
// Toll cost estimator API for HouseHub
require_once dirname(__DIR__, 2) . '/includes/auth.php';
require_login();
header('Content-Type: application/json; charset=utf-8');
$action = $_GET['action'] ?? '';
if ($action !== 'estimate') {
echo json_encode(['ok' => false, 'error' => 'Action inconnue']);
exit;
}
// --- Read POST body ---
$body = file_get_contents('php://input');
$params = json_decode($body, true);
if (!$params || empty($params['stops']) || count($params['stops']) < 2) {
echo json_encode(['ok' => false, 'error' => 'Paramètres invalides (stops requis, min 2)']);
exit;
}
$stops = $params['stops'];
$fuelL100 = (float)($params['fuel_l100'] ?? 7);
$fuelPrice= (float)($params['fuel_price'] ?? 1.85);
// --- Load databases ---
$tollsPath = __DIR__ . '/data/tolls.json';
$gpsPath = __DIR__ . '/data/toll_gps.json';
if (!file_exists($tollsPath)) {
echo json_encode(['ok' => false, 'error' => 'Base de péages introuvable']);
exit;
}
$tollsRaw = json_decode(file_get_contents($tollsPath), true);
$tollData = $tollsRaw['data'] ?? [];
$gpsData = [];
if (file_exists($gpsPath)) {
$gpsData = json_decode(file_get_contents($gpsPath), true) ?? [];
}
// --- Build lookup index: [op][entry][exit] => c1_price ---
$tollIndex = [];
foreach ($tollData as $row) {
$op = $row['op'];
$e = $row['e'];
$x = $row['x'];
$c1 = (float)$row['c1'];
$tollIndex[$op][$e][$x] = $c1;
}
// --- Helper: great-circle distance (km) ---
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float {
$R = 6371.0;
$dLat = deg2rad($lat2 - $lat1);
$dLng = deg2rad($lng2 - $lng1);
$a = sin($dLat/2)**2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng/2)**2;
return $R * 2 * atan2(sqrt($a), sqrt(1 - $a));
}
// --- Find plazas near a coordinate (within $maxKm) ---
// Returns array of [plaza_name => distance_km]
function plazasNear(array $gpsData, float $lat, float $lng, float $maxKm): array {
$result = [];
foreach ($gpsData as $name => $info) {
$d = haversine($lat, $lng, $info['lat'], $info['lng']);
if ($d <= $maxKm) {
$result[$name] = $d;
}
}
asort($result);
return $result;
}
// --- Look up toll price for (op, entry, exit) with fallback ---
function lookupToll(array $tollIndex, string $op, string $entry, string $exit): ?float {
// Direct lookup
if (isset($tollIndex[$op][$entry][$exit])) {
return $tollIndex[$op][$entry][$exit];
}
// Reversed
if (isset($tollIndex[$op][$exit][$entry])) {
return $tollIndex[$op][$exit][$entry];
}
return null;
}
// --- OSRM route distance ---
function getRouteDistanceKm(float $lat1, float $lng1, float $lat2, float $lng2): ?float {
$url = sprintf(
'https://router.project-osrm.org/route/v1/driving/%f,%f;%f,%f?overview=false',
$lng1, $lat1, $lng2, $lat2
);
$ctx = stream_context_create([
'http' => [
'timeout' => 10,
'header' => "User-Agent: HouseHub/1.0\r\n"
]
]);
$resp = @file_get_contents($url, false, $ctx);
if ($resp === false) return null;
$data = json_decode($resp, true);
if (!empty($data['routes'][0]['distance'])) {
return (float)$data['routes'][0]['distance'] / 1000.0;
}
return null;
}
// --- Main: process each consecutive stop pair ---
$segments = [];
$totalKm = 0.0;
$totalToll = 0.0;
for ($i = 0; $i < count($stops) - 1; $i++) {
$orig = $stops[$i];
$dest = $stops[$i + 1];
$oLat = (float)$orig['lat'];
$oLng = (float)$orig['lng'];
$dLat = (float)$dest['lat'];
$dLng = (float)$dest['lng'];
$fromName = $orig['name'] ?? "Étape " . ($i + 1);
$toName = $dest['name'] ?? "Étape " . ($i + 2);
// Distance via OSRM
$distKm = getRouteDistanceKm($oLat, $oLng, $dLat, $dLng);
if ($distKm === null) {
// Fallback: straight-line distance * 1.25
$distKm = haversine($oLat, $oLng, $dLat, $dLng) * 1.25;
}
$totalKm += $distKm;
// --- Find toll ---
$segToll = 0.0;
$entryPlaza = null;
$exitPlaza = null;
$tollNote = null;
if (!empty($gpsData)) {
// Find plazas near origin (80km) and destination (80km)
$nearOrig = plazasNear($gpsData, $oLat, $oLng, 80.0);
$nearDest = plazasNear($gpsData, $dLat, $dLng, 80.0);
$bestToll = null;
$bestDistSum = PHP_FLOAT_MAX;
$bestEntry = null;
$bestExit = null;
$bestOp = null;
// Try each operator
foreach ($tollIndex as $op => $opEntries) {
// Find candidate entry plazas (near origin, present in this operator)
$candidateEntries = [];
foreach ($nearOrig as $pName => $dist) {
if (isset($opEntries[$pName]) || isset(array_flip(array_keys($opEntries))[$pName])) {
// Plaza exists as entry in this operator
if (isset($opEntries[$pName])) {
$candidateEntries[$pName] = $dist;
}
}
// Also check if it's an exit plaza (reversed lookup later)
// Include any plaza from this operator that is within range
}
// Build set of all plazas for this operator
$opPlazas = [];
foreach ($opEntries as $entry => $exits) {
$opPlazas[$entry] = true;
foreach ($exits as $exit => $price) {
$opPlazas[$exit] = true;
}
}
// Filter nearOrig and nearDest to only plazas in this operator
$opNearOrig = array_intersect_key($nearOrig, $opPlazas);
$opNearDest = array_intersect_key($nearDest, $opPlazas);
if (empty($opNearOrig) || empty($opNearDest)) continue;
// Try best 5 entry and best 5 exit candidates
$topEntries = array_slice($opNearOrig, 0, 5, true);
$topExits = array_slice($opNearDest, 0, 5, true);
foreach ($topEntries as $ePlaza => $eDist) {
foreach ($topExits as $xPlaza => $xDist) {
if ($ePlaza === $xPlaza) continue;
$price = lookupToll($tollIndex, $op, $ePlaza, $xPlaza);
if ($price !== null) {
$distSum = $eDist + $xDist;
if ($distSum < $bestDistSum) {
$bestDistSum = $distSum;
$bestToll = $price;
$bestEntry = $ePlaza;
$bestExit = $xPlaza;
$bestOp = $op;
}
}
}
}
}
if ($bestToll !== null) {
$segToll = $bestToll;
$entryPlaza = $bestEntry;
$exitPlaza = $bestExit;
} else {
$tollNote = 'péages non estimés pour ce trajet';
}
} else {
$tollNote = 'base GPS des péages non disponible';
}
$totalToll += $segToll;
$seg = [
'from' => $fromName,
'to' => $toName,
'distance_km' => round($distKm, 1),
'toll' => round($segToll, 2),
];
if ($entryPlaza) $seg['entry_plaza'] = $entryPlaza;
if ($exitPlaza) $seg['exit_plaza'] = $exitPlaza;
if ($bestOp) $seg['op'] = $bestOp;
if ($tollNote) $seg['note'] = $tollNote;
$segments[] = $seg;
}
$fuelCost = round(($totalKm / 100.0) * $fuelL100 * $fuelPrice, 2);
$grandTotal = round($totalToll + $fuelCost, 2);
echo json_encode([
'ok' => true,
'segments' => $segments,
'total_km' => round($totalKm, 1),
'total_toll' => round($totalToll, 2),
'fuel_cost' => $fuelCost,
'grand_total' => $grandTotal,
], JSON_UNESCAPED_UNICODE);
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+263 -27
View File
@@ -1,10 +1,18 @@
-- HouseHub — Schéma MySQL
-- Créer la base : CREATE DATABASE househub CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- HouseHub — Schéma MySQL (Modèle pour une base famille)
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ─── Utilisateurs ─────────────────────────────────────────────────────────────
-- ─── Configuration du foyer ───────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_foyer_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
currency VARCHAR(10) NOT NULL DEFAULT '',
zone_scolaire VARCHAR(5) NOT NULL DEFAULT 'C',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '', 'C');
-- ─── Utilisateurs (Legacy) ────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
@@ -13,9 +21,13 @@ CREATE TABLE IF NOT EXISTS pf_users (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Personnes ────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_people (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL
CREATE TABLE `pf_people` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`user_id` int(11) DEFAULT NULL,
`role` varchar(50) DEFAULT NULL,
`color` varchar(7) DEFAULT '#0891b2',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Calendrier familial ──────────────────────────────────────────────────────
@@ -91,7 +103,7 @@ CREATE TABLE IF NOT EXISTS pf_budget_items (
CREATE TABLE IF NOT EXISTS pf_expenses (
id INT AUTO_INCREMENT PRIMARY KEY,
date_exp DATE NOT NULL,
gestion_month VARCHAR(7) NOT NULL,
gestion_month VARCHAR(10) NOT NULL,
category VARCHAR(100),
label VARCHAR(255),
amount DECIMAL(10,2) DEFAULT 0,
@@ -101,26 +113,28 @@ CREATE TABLE IF NOT EXISTS pf_expenses (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_alloc_categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
target DECIMAL(10,2) DEFAULT 0,
holiday_id INT DEFAULT NULL,
sort_order INT DEFAULT 0
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
target DECIMAL(10,2) DEFAULT 0.00,
transfer_dest VARCHAR(50) DEFAULT NULL,
holiday_id INT DEFAULT NULL,
sort_order INT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_alloc_values (
id INT AUTO_INCREMENT PRIMARY KEY,
month_date VARCHAR(7) NOT NULL,
month_date VARCHAR(10) NOT NULL,
cat_id INT NOT NULL,
amount_alex DECIMAL(10,2) DEFAULT 0,
amount_laia DECIMAL(10,2) DEFAULT 0,
UNIQUE KEY uq_alloc (month_date, cat_id)
person_id INT NOT NULL,
amount DECIMAL(10,2) DEFAULT 0.00,
UNIQUE KEY uq_alloc_person (month_date, cat_id, person_id),
FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_savings (
id INT AUTO_INCREMENT PRIMARY KEY,
owner VARCHAR(50) NOT NULL,
month_date VARCHAR(7) NOT NULL,
month_date VARCHAR(10) NOT NULL,
category VARCHAR(100) NOT NULL,
amount DECIMAL(10,2) DEFAULT 0,
holiday_id INT DEFAULT NULL
@@ -147,9 +161,22 @@ CREATE TABLE IF NOT EXISTS pf_salary_config (
CREATE TABLE IF NOT EXISTS pf_import_rules (
id INT AUTO_INCREMENT PRIMARY KEY,
keyword VARCHAR(255) NOT NULL UNIQUE,
category VARCHAR(100) NOT NULL
category VARCHAR(100) NOT NULL,
budget_item_id INT(11) NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_advances (
id int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
advance_date date NOT NULL,
payer varchar(50) NOT NULL,
description varchar(255) NOT NULL,
amount decimal(10,2) DEFAULT 0.00,
from_savings tinyint(1) DEFAULT 0,
is_resolved tinyint(1) DEFAULT 0,
created_at datetime DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_notes (
id INT AUTO_INCREMENT PRIMARY KEY,
note_type VARCHAR(100) NOT NULL,
@@ -158,6 +185,29 @@ CREATE TABLE IF NOT EXISTS pf_notes (
UNIQUE KEY uq_note (note_type, reference_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_memo_notes (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content LONGTEXT DEFAULT '',
tags VARCHAR(1000) DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FULLTEXT KEY ft_notes (title, content, tags)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_memo_attachments (
id INT AUTO_INCREMENT PRIMARY KEY,
note_id INT NOT NULL,
type ENUM('image','file','url') NOT NULL DEFAULT 'file',
filename VARCHAR(255) DEFAULT NULL,
original_name VARCHAR(255) DEFAULT NULL,
url TEXT DEFAULT NULL,
label VARCHAR(255) DEFAULT NULL,
size INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (note_id) REFERENCES pf_memo_notes(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Voyages / Holidays ───────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_holidays (
id INT AUTO_INCREMENT PRIMARY KEY,
@@ -225,16 +275,153 @@ CREATE TABLE IF NOT EXISTS pf_gifts (
amount DECIMAL(8,2) DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SET FOREIGN_KEY_CHECKS = 1;
-- ─── Garage Manager ───────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_vehicles (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
brand VARCHAR(100) NOT NULL,
model VARCHAR(100) NOT NULL,
year INT DEFAULT NULL,
license_plate VARCHAR(50) DEFAULT NULL,
vin VARCHAR(100) DEFAULT NULL,
fuel_type VARCHAR(50) DEFAULT 'Essence',
color VARCHAR(50) DEFAULT NULL,
purchase_date DATE DEFAULT NULL,
purchase_price DECIMAL(10,2) DEFAULT NULL,
current_km INT DEFAULT 0,
photo VARCHAR(255) DEFAULT NULL,
notes TEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Données initiales ────────────────────────────────────────────────────────
-- Utilisateur admin (mot de passe : changeme → à modifier !)
INSERT IGNORE INTO pf_users (id, username, password_hash, display_name)
VALUES (1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin');
CREATE TABLE IF NOT EXISTS pf_maintenances (
id INT AUTO_INCREMENT PRIMARY KEY,
vehicle_id INT NOT NULL,
type VARCHAR(100) NOT NULL,
description TEXT DEFAULT NULL,
date DATE NOT NULL,
km INT DEFAULT NULL,
cost DECIMAL(10,2) DEFAULT 0,
mechanic VARCHAR(100) DEFAULT NULL,
garage_name VARCHAR(100) DEFAULT NULL,
next_km INT DEFAULT NULL,
next_date DATE DEFAULT NULL,
invoice_photo VARCHAR(255) DEFAULT NULL,
notes TEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 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');
CREATE TABLE IF NOT EXISTS pf_parts (
id INT AUTO_INCREMENT PRIMARY KEY,
vehicle_id INT DEFAULT NULL,
maintenance_id INT DEFAULT NULL,
brand VARCHAR(100) DEFAULT NULL,
reference VARCHAR(100) DEFAULT NULL,
name VARCHAR(255) NOT NULL,
category VARCHAR(100) DEFAULT 'Autre',
price DECIMAL(10,2) DEFAULT 0,
quantity INT DEFAULT 1,
unit VARCHAR(50) DEFAULT 'pièce',
supplier VARCHAR(100) DEFAULT NULL,
purchase_date DATE DEFAULT NULL,
photo VARCHAR(255) DEFAULT NULL,
notes TEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE SET NULL,
FOREIGN KEY (maintenance_id) REFERENCES pf_maintenances(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_garage_documents (
id INT AUTO_INCREMENT PRIMARY KEY,
vehicle_id INT NOT NULL,
maintenance_id INT DEFAULT NULL,
label VARCHAR(255) DEFAULT NULL,
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(255) DEFAULT NULL,
mime VARCHAR(120) DEFAULT NULL,
size INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
KEY idx_gd_vehicle (vehicle_id),
KEY idx_gd_maint (maintenance_id),
FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE CASCADE,
FOREIGN KEY (maintenance_id) REFERENCES pf_maintenances(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Todo ─────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_todo_lists (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
color VARCHAR(20) DEFAULT '#3b82f6',
icon VARCHAR(10) DEFAULT '📋',
position INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_todos (
id INT AUTO_INCREMENT PRIMARY KEY,
list_id INT DEFAULT NULL,
title VARCHAR(500) NOT NULL,
notes TEXT DEFAULT NULL,
due_date DATE DEFAULT NULL,
due_time TIME DEFAULT NULL,
notified TINYINT(1) DEFAULT 0,
notified_date DATE DEFAULT NULL,
priority ENUM('none','low','medium','high') DEFAULT 'none',
done TINYINT(1) DEFAULT 0,
done_at DATETIME DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (list_id) REFERENCES pf_todo_lists(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Module Liste (Courses partagées) ─────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_lists (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL DEFAULT 'Ma liste',
color VARCHAR(20) DEFAULT NULL,
list_type VARCHAR(50) DEFAULT NULL,
position INT NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO pf_lists (id, name, position) VALUES (1, 'Ma liste', 0);
CREATE TABLE IF NOT EXISTS pf_grocery_items (
id INT AUTO_INCREMENT PRIMARY KEY,
list_id INT NOT NULL DEFAULT 1,
category_id INT DEFAULT NULL,
label VARCHAR(500) NOT NULL,
in_cart TINYINT(1) NOT NULL DEFAULT 0,
position INT NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_items_list (list_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_grocery_history (
id INT AUTO_INCREMENT PRIMARY KEY,
label_hash CHAR(64) NOT NULL,
label_display VARCHAR(500) NOT NULL,
last_used_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_grocery_hist_hash (label_hash),
KEY idx_hist_last (last_used_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_list_categories (
id INT AUTO_INCREMENT PRIMARY KEY,
icon VARCHAR(10) NOT NULL DEFAULT '🏷️',
name VARCHAR(100) NOT NULL,
position INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_item_category_rules (
id INT AUTO_INCREMENT PRIMARY KEY,
keyword VARCHAR(255) NOT NULL,
category_id INT NOT NULL,
UNIQUE KEY uq_keyword (keyword)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Calendar iOS ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_calendar_events (
@@ -264,8 +451,57 @@ CREATE TABLE IF NOT EXISTS pf_calendar_event_links (
external_uid VARCHAR(255) NOT NULL,
external_etag VARCHAR(255) DEFAULT NULL,
calendar_url VARCHAR(1024) DEFAULT NULL,
external_href VARCHAR(2048) DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_calendar_event (calendar_event_id),
UNIQUE KEY uq_external_link (external_uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── PrintVault ───────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_pv_models (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT DEFAULT '',
category VARCHAR(100) DEFAULT 'Non classé',
tags VARCHAR(500) DEFAULT '',
file_type VARCHAR(20) NOT NULL,
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(255) NOT NULL,
file_size INT DEFAULT 0,
dim_x DECIMAL(10,3) DEFAULT 0,
dim_y DECIMAL(10,3) DEFAULT 0,
dim_z DECIMAL(10,3) DEFAULT 0,
volume DECIMAL(10,3) DEFAULT 0,
gcode_time VARCHAR(50) DEFAULT '',
gcode_filament VARCHAR(50) DEFAULT '',
gcode_nozzle VARCHAR(20) DEFAULT '',
gcode_bed VARCHAR(20) DEFAULT '',
thumb VARCHAR(255) DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_pv_categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
color VARCHAR(20) DEFAULT '#8b5cf6'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO pf_pv_categories (name, color) VALUES
('Non classé','#64748b'),('Déco','#ec4899'),('Fonctionnel','#3b82f6'),
('Mécanique','#f59e0b'),('Jouets','#10b981'),('Outils','#ef4444'),
('Architecture','#8b5cf6'),('Art','#06b6d4');
-- ─── Planka ───────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_planka_config (
id INT NOT NULL DEFAULT 1,
project_id VARCHAR(32),
admin_token TEXT,
token_expires_at DATETIME,
active_board_id VARCHAR(32),
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SET FOREIGN_KEY_CHECKS = 1;
+44 -17
View File
@@ -69,13 +69,25 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
// 🟢 ENRICHISSEMENT : Prise en compte de la couleur du profil
if ($action === 'update_profile') {
$display_name = trim($_POST['display_name'] ?? '');
$color = trim($_POST['color'] ?? '#0891b2');
if (!$display_name) {
$error = "Le prénom ne peut pas être vide.";
} else {
// MÀJ dans la base Meta (users)
$meta_pdo->prepare("UPDATE users SET display_name = ? WHERE id = ?")
->execute([$display_name, $user_id]);
// MÀJ dans la base locale Foyer (pf_people) via le user_id mappé
require_once __DIR__ . '/includes/db.php';
if (isset($pdo)) {
$stmtPeopleColor = $pdo->prepare("UPDATE pf_people SET color = ? WHERE user_id = ?");
$stmtPeopleColor->execute([$color, $user_id]);
}
$_SESSION['user']['display_name'] = $display_name;
$success = "Profil mis à jour.";
}
@@ -129,7 +141,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$error = "Format non supporté (jpg, png, webp).";
} else {
$dest = $upload_dir . 'home_bg_' . $family_id . '.' . $ext;
// Supprimer anciens fichiers de ce family
foreach (glob($upload_dir . 'home_bg_' . $family_id . '.*') as $old) @unlink($old);
if (move_uploaded_file($file['tmp_name'], $dest)) {
$success = "Image d'accueil mise à jour.";
@@ -225,9 +236,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
// ─── Chargement données ───────────────────────────────────────────────────────
$user = $meta_pdo->prepare("SELECT * FROM users WHERE id = ?");
$user->execute([$user_id]);
$user = $user->fetch();
// 1. On charge la base locale AVANT pour que son $user n'écrase pas le nôtre
if ($family_id) {
require_once __DIR__ . '/includes/db.php';
}
// 2. Maintenant on peut charger ton profil en toute sécurité
$stmtUser = $meta_pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmtUser->execute([$user_id]);
$user = $stmtUser->fetch();
// 3. Récupération de la couleur actuelle depuis pf_people
$user_color = '#0891b2'; // Fallback par défaut
if ($family_id && isset($pdo)) {
$stmtColorFetch = $pdo->prepare("SELECT color FROM pf_people WHERE user_id = ?");
$stmtColorFetch->execute([$user_id]);
$fetched_color = $stmtColorFetch->fetchColumn();
if ($fetched_color) {
$user_color = $fetched_color;
}
}
$family = null;
$members = [];
@@ -276,7 +305,6 @@ require __DIR__ . '/header.php';
<div class="pf-alert pf-alert--error">✗ <?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<!-- ── Langue ─────────────────────────────────────────────────────────── -->
<section class="pf-panel-card">
<h2 class="pf-card-h2">🌐 Langue / Language</h2>
<form method="post" class="pf-lang-form">
@@ -303,7 +331,6 @@ require __DIR__ . '/header.php';
</form>
</section>
<!-- ── Modules ────────────────────────────────────────────────────────── -->
<?php if ($family_id): ?>
<section class="pf-panel-card">
<h2 class="pf-card-h2 pf-card-h2--tight">🧩 Modules actifs</h2>
@@ -413,7 +440,6 @@ require __DIR__ . '/header.php';
<?php endif; ?>
</section>
<!-- ── Fond page d'accueil ───────────────────────────────────────────── -->
<?php if ($family_id):
$bg_file = null;
foreach (glob('/uploads/home_bg_' . $family_id . '.*') as $f) { $bg_file = $f; break; }
@@ -452,15 +478,12 @@ require __DIR__ . '/header.php';
<?php endif; ?>
<!-- ── Famille ──────────────────────────────────────────────────────────── -->
<?php
<?php
$enabledMods = $_SESSION['enabled_modules'] ?? [];
// On cible précisément qui a besoin de quoi
$showCurrency = count(array_intersect(['budget', 'holidays', 'gifts', 'garage'], $enabledMods)) > 0;
$showZone = in_array('calendar', $enabledMods);
// Si au moins un des champs doit être affiché, on rend la section visible
if ($family_id && ($showCurrency || $showZone)):
$currentCurrency = defined('CURRENCY') ? CURRENCY : '€';
$currentZone = defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : 'C';
@@ -502,7 +525,6 @@ require __DIR__ . '/header.php';
<?php endif; ?>
<!-- ── Profil ──────────────────────────────────────────────────────────── -->
<section class="pf-panel-card">
<h2 class="pf-card-h2">👤 Mon profil</h2>
<form method="post">
@@ -516,11 +538,19 @@ require __DIR__ . '/header.php';
<label class="pf-label">Prénom / Pseudo affiché</label>
<input type="text" name="display_name" class="pf-input" value="<?= htmlspecialchars($user['display_name']) ?>" required>
</div>
<div class="pf-form-group">
<label class="pf-label" for="profile_color">Couleur thématique de mon profil</label>
<div class="pf-color-picker-group">
<input type="color" name="color" id="profile_color" class="pf-input-color" value="<?= htmlspecialchars($user_color) ?>">
<span class="pf-muted-note">Cette couleur sera utilisée pour vos grilles de budget, calendriers et indicateurs personnels.</span>
</div>
</div>
<button type="submit" class="pf-btn">Enregistrer</button>
</form>
</section>
<!-- ── Mot de passe ────────────────────────────────────────────────────── -->
<section class="pf-panel-card">
<h2 class="pf-card-h2">🔒 Changer le mot de passe</h2>
<form method="post">
@@ -543,7 +573,6 @@ require __DIR__ . '/header.php';
</section>
<?php if ($family): ?>
<!-- ── Espace familial ─────────────────────────────────────────────────── -->
<section class="pf-panel-card">
<h2 class="pf-card-h2">🏠 Mon espace familial</h2>
@@ -559,7 +588,6 @@ require __DIR__ . '/header.php';
</div>
</form>
<!-- Code d'invitation -->
<div class="pf-dashed-panel">
<p class="pf-muted-note pf-muted-note--tight">Code d'invitation — partagez-le pour inviter quelqu'un</p>
<div class="pf-invite-row">
@@ -582,7 +610,6 @@ require __DIR__ . '/header.php';
</p>
</div>
<!-- Membres -->
<h3 class="pf-members-heading">Membres (<?= count($members) ?>)</h3>
<div class="pf-stack-sm">
<?php foreach ($members as $m): ?>
@@ -630,4 +657,4 @@ function copyCode() {
}
</script>
<?php require __DIR__ . '/footer.php'; ?>
<?php require __DIR__ . '/footer.php'; ?>