Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b44688a654 | ||
|
|
f8e2ad67ee | ||
|
|
4130b03a68 | ||
|
|
315e1b5fb1 | ||
|
|
b1d3db4892 | ||
|
|
99ed0e31de | ||
|
|
a1e7bc62fd | ||
|
|
3675f0c01a | ||
|
|
836533fcf6 | ||
|
|
5fe9046a63 | ||
|
|
5b70cd20e6 | ||
|
|
ed2d5a7c9e | ||
|
|
6bbee271e3 | ||
|
|
7b294f300a |
@@ -2,7 +2,7 @@ name: Deploy HouseHub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, staging-perco, staging-fefe]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -15,6 +15,21 @@ jobs:
|
||||
username: perco
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
script: |
|
||||
cd /opt/container/househub
|
||||
git pull
|
||||
echo "✓ HouseHub mis à jour"
|
||||
BRANCH="${{ github.ref_name }}"
|
||||
case "$BRANCH" in
|
||||
main)
|
||||
cd /opt/container/househub
|
||||
git pull
|
||||
echo "✓ Production mise à jour"
|
||||
;;
|
||||
staging-perco)
|
||||
cd /opt/container/househub-perco
|
||||
git pull origin staging-perco
|
||||
echo "✓ Staging Perco mis à jour"
|
||||
;;
|
||||
staging-fefe)
|
||||
cd /opt/container/househub-fefe
|
||||
git pull origin staging-fefe
|
||||
echo "✓ Staging Fefe mis à jour"
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
househub_db_data
|
||||
.env
|
||||
data/.hh_app_secret
|
||||
extract_source.php
|
||||
@@ -151,12 +151,15 @@ L'architecture de HouseHub isole chaque foyer dans sa propre base de données (`
|
||||
Afin de transformer complètement l'application héritée de Pacha Family en un produit générique et autonome pour chaque espace familial, les chantiers suivants doivent être menés.
|
||||
|
||||
🟢 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 |
|
||||
|
||||
+12
-10
@@ -24,10 +24,13 @@ CREATE TABLE IF NOT EXISTS pf_foyer_settings (
|
||||
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C');
|
||||
|
||||
-- ─── Personnes ────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_people (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
CREATE TABLE `pf_people` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(50) NOT NULL,
|
||||
`user_id` int(11) DEFAULT NULL,
|
||||
`role` varchar(50) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
||||
|
||||
-- ─── Calendrier familial ──────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_events (
|
||||
@@ -243,9 +246,6 @@ SET FOREIGN_KEY_CHECKS = 1;
|
||||
INSERT IGNORE INTO pf_users (id, username, password_hash, display_name)
|
||||
VALUES (1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin');
|
||||
|
||||
-- Personnes (IDs fixes correspondant aux constantes dans config.php)
|
||||
INSERT IGNORE INTO pf_people (id, name) VALUES (2, 'Alex');
|
||||
INSERT IGNORE INTO pf_people (id, name) VALUES (3, 'Laia');
|
||||
|
||||
-- ─── Garage Manager ───────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_vehicles (
|
||||
@@ -374,9 +374,11 @@ CREATE TABLE IF NOT EXISTS pf_todos (
|
||||
|
||||
-- ─── Module Liste (multi-listes) ──────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_lists (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL DEFAULT 'Ma liste',
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
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;
|
||||
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* HouseHub OS - Source Extractor 🦙🚀
|
||||
* Génère un fichier Markdown contenant tout le code source du projet.
|
||||
* Idéal pour alimenter une GEM ou un LLM.
|
||||
*/
|
||||
|
||||
// 1. 🚨 SÉCURITÉ : Ne jamais laisser ce fichier accessible publiquement sans protection !
|
||||
// Appelle le script via : https://househub.nas.../extract_source.php?key=TON_MOT_DE_PASSE
|
||||
$secretKey = 'mR83s7MmXbP$jer$9C4HnGkry6xhGL6';
|
||||
|
||||
if (!isset($_GET['key']) || $_GET['key'] !== $secretKey) {
|
||||
http_response_code(403);
|
||||
die("⛔ Accès refusé. Veuillez fournir la clé de sécurité.");
|
||||
}
|
||||
|
||||
// Forcer l'affichage en texte brut dans le navigateur pour un copier-coller facile
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
|
||||
$root = __DIR__;
|
||||
|
||||
// 2. CONFIGURATION DU FILTRAGE
|
||||
// Dossiers à exclure (ne mets pas de slash au début)
|
||||
$excludeDirs = [
|
||||
'.git',
|
||||
'.gitea',
|
||||
'.devtools',
|
||||
'vendor',
|
||||
'node_modules',
|
||||
'assets/img',
|
||||
'modules/holidays/assets/img',
|
||||
'modules/family-calendar/assets/img',
|
||||
'modules/gift-list/assets/img'
|
||||
];
|
||||
|
||||
// Fichiers spécifiques à exclure (Sécurité & Bruit)
|
||||
$excludeFiles = [
|
||||
'.env',
|
||||
'.env.example',
|
||||
'extract_source.php', // On s'exclut soi-même
|
||||
'pachafamily_source.md',
|
||||
'docker-compose.override.yml'
|
||||
];
|
||||
|
||||
// Extensions autorisées (on ignore les images, pdf, zip, etc.)
|
||||
$allowedExtensions = ['php', 'js', 'css', 'sql', 'json', 'yml', 'yaml', 'md', 'sh'];
|
||||
|
||||
// 3. INITIALISATION DU PARCOURS
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
$report = "# 🦙 Source Code HouseHub OS\n\n";
|
||||
$report .= "> *Généré le " . date('Y-m-d H:i:s') . "*\n\n";
|
||||
$report .= "---\n\n";
|
||||
|
||||
// 4. BOUCLE D'EXTRACTION
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isDir()) continue;
|
||||
|
||||
$filePath = $file->getPathname();
|
||||
$relativePath = str_replace($root . DIRECTORY_SEPARATOR, '', $filePath);
|
||||
$relativePath = str_replace('\\', '/', $relativePath); // Uniformisation Windows/Linux
|
||||
|
||||
// Extraction de l'extension
|
||||
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
||||
|
||||
// A. Filtrage par dossier
|
||||
$skip = false;
|
||||
foreach ($excludeDirs as $dir) {
|
||||
// Si le chemin commence par ce dossier, ou contient /ce_dossier/
|
||||
if (strpos($relativePath, $dir . '/') === 0 || strpos($relativePath, '/' . $dir . '/') !== false) {
|
||||
$skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($skip) continue;
|
||||
|
||||
// B. Filtrage par nom de fichier
|
||||
if (in_array(basename($filePath), $excludeFiles)) continue;
|
||||
|
||||
// C. Filtrage par extension
|
||||
if (!in_array($ext, $allowedExtensions)) continue;
|
||||
|
||||
// 5. LECTURE ET FORMATAGE
|
||||
$content = file_get_contents($filePath);
|
||||
|
||||
if ($content === false) {
|
||||
$content = "// Erreur lors de la lecture de ce fichier.";
|
||||
}
|
||||
|
||||
// Ajustement du tag de langage pour le Markdown
|
||||
$lang = $ext;
|
||||
if ($ext === 'js') $lang = 'javascript';
|
||||
if ($ext === 'yml') $lang = 'yaml';
|
||||
|
||||
$report .= "### 📄 Fichier : `$relativePath`\n";
|
||||
$report .= "```$lang\n";
|
||||
$report .= trim($content) . "\n";
|
||||
$report .= "```\n\n---\n\n";
|
||||
}
|
||||
|
||||
// 6. SORTIE
|
||||
echo $report;
|
||||
+24
-10
@@ -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
@@ -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
@@ -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"; ?>'
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
// includes/config.php
|
||||
// HouseHub Config - Version dynamique
|
||||
if (!defined('ID_ALEX')) {
|
||||
define('ID_ALEX', 2);
|
||||
define('ID_LAIA', 3);
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
<?php
|
||||
if (file_exists(__DIR__ . '/config.php')) {
|
||||
require_once __DIR__ . '/config.php';
|
||||
}
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
@@ -18,14 +18,6 @@ require __DIR__ . '/header.php';
|
||||
<button class="liste-tab-add" id="btn-add-list" title="<?= htmlspecialchars(tr('liste_new_list')) ?>">+</button>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire de création de liste inline -->
|
||||
<div class="liste-new-form hidden" id="liste-new-form">
|
||||
<input type="text" id="new-list-name" maxlength="100"
|
||||
placeholder="<?= htmlspecialchars(tr('liste_list_name_placeholder')) ?>" autocomplete="off">
|
||||
<button id="btn-confirm-new-list"><?= htmlspecialchars(tr('liste_create')) ?></button>
|
||||
<button id="btn-cancel-new-list"><?= htmlspecialchars(tr('cancel')) ?></button>
|
||||
</div>
|
||||
|
||||
<!-- Corps de la liste active -->
|
||||
<div class="liste-body" id="liste-body">
|
||||
|
||||
|
||||
+141
-20
@@ -1,46 +1,167 @@
|
||||
<?php
|
||||
// Script de migration pour mettre à jour TOUTES les bases de données familiales
|
||||
// Script de migration : Refonte globale Multi-Tenant (pf_people & Budget)
|
||||
require_once __DIR__ . '/includes/meta_db.php';
|
||||
|
||||
echo "<h1>🚀 Début de la migration...</h1>";
|
||||
echo "<h1>🚀 Début de la migration Globale ...</h1>";
|
||||
|
||||
// 1. Récupérer toutes les bases de données des familles
|
||||
$stmt = $meta_pdo->query("SELECT name, db_name FROM families WHERE db_name != ''");
|
||||
$stmt = $meta_pdo->query("SELECT id, name, db_name FROM families WHERE db_name != ''");
|
||||
$families = $stmt->fetchAll();
|
||||
|
||||
$host = getenv('DB_HOST') ?: 'househub-db';
|
||||
$user = getenv('DB_USER') ?: 'househub';
|
||||
$pass = getenv('DB_PASS') ?: 'changeme';
|
||||
|
||||
// 2. Boucler sur chaque famille
|
||||
foreach ($families as $f) {
|
||||
$db_name = $f['db_name'];
|
||||
echo "Mise à jour de la famille <strong>{$f['name']}</strong> (Base : $db_name) ... ";
|
||||
$family_id = $f['id'];
|
||||
echo "<h3>Mise à jour de <strong>{$f['name']}</strong> ($db_name)</h3><ul>";
|
||||
|
||||
// 🟢 SÉCURITÉ : On vide les variables à chaque famille pour éviter les fuites (le bug f2 vers f1)
|
||||
$p1_name = null;
|
||||
$p2_name = null;
|
||||
$parents = [];
|
||||
|
||||
try {
|
||||
$fam_pdo = new PDO("mysql:host=$host;dbname=$db_name;charset=utf8mb4", $user, $pass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
||||
]);
|
||||
|
||||
// 3. Injecter la nouvelle table
|
||||
$sql = "
|
||||
CREATE TABLE IF NOT EXISTS pf_foyer_settings (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
currency VARCHAR(10) NOT NULL DEFAULT '€',
|
||||
zone_scolaire VARCHAR(5) NOT NULL DEFAULT 'C',
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
// ==========================================
|
||||
// 1. GESTION DE PF_PEOPLE (user_id, role, color)
|
||||
// ==========================================
|
||||
echo "<li><strong>Table pf_people :</strong> ";
|
||||
|
||||
// user_id
|
||||
try {
|
||||
$fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN user_id INT NULL DEFAULT NULL");
|
||||
echo "<span style='color:green'>user_id OK</span> - ";
|
||||
} catch (\PDOException $e) {
|
||||
if ($e->getCode() == '42S21' || strpos($e->getMessage(), '1060') !== false) {
|
||||
echo "<span style='color:gray'>user_id déjà présent</span> - ";
|
||||
} else { throw $e; }
|
||||
}
|
||||
|
||||
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C');
|
||||
";
|
||||
// 🟢 CORRECTION DE L'AUTO-MAPPING : Si pf_people est vide, on la remplit !
|
||||
$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();
|
||||
|
||||
$fam_pdo->exec($sql);
|
||||
echo "<span style='color:green'>✅ OK</span><br>";
|
||||
$mappedCount = 0;
|
||||
foreach ($users as $u) {
|
||||
// On vérifie si l'utilisateur existe déjà dans pf_people (via user_id ou son nom)
|
||||
$stmtCheck = $fam_pdo->prepare("SELECT id FROM pf_people WHERE user_id = ? OR LOWER(name) = LOWER(?)");
|
||||
$stmtCheck->execute([$u['id'], $u['username']]);
|
||||
$exists = $stmtCheck->fetchColumn();
|
||||
|
||||
if ($exists) {
|
||||
// S'il existe, on s'assure que son user_id est bien renseigné
|
||||
$stmtUpdate = $fam_pdo->prepare("UPDATE pf_people SET user_id = ? WHERE id = ?");
|
||||
$stmtUpdate->execute([$u['id'], $exists]);
|
||||
$mappedCount += $stmtUpdate->rowCount();
|
||||
} else {
|
||||
// S'il n'existe pas du tout (base f1 vide), on le crée !
|
||||
$stmtInsert = $fam_pdo->prepare("INSERT INTO pf_people (name, user_id) VALUES (?, ?)");
|
||||
$stmtInsert->execute([$u['display_name'] ?: $u['username'], $u['id']]);
|
||||
$mappedCount++;
|
||||
}
|
||||
}
|
||||
echo "<span style='color:blue'>$mappedCount profils liés ou créés</span> - ";
|
||||
|
||||
// role
|
||||
try {
|
||||
$fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN role VARCHAR(50) DEFAULT NULL");
|
||||
echo "<span style='color:green'>role OK</span> - ";
|
||||
} catch (\PDOException $e) {
|
||||
if ($e->getCode() == '42S21' || strpos($e->getMessage(), '1060') !== false) {
|
||||
echo "<span style='color:gray'>role déjà présent</span> - ";
|
||||
} else { throw $e; }
|
||||
}
|
||||
|
||||
// color
|
||||
try {
|
||||
$fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN color VARCHAR(7) DEFAULT '#0891b2'");
|
||||
echo "<span style='color:green'>colonne 'color' ajoutée</span> - ";
|
||||
} catch (\PDOException $e) {
|
||||
if ($e->getCode() == '42S21' || strpos($e->getMessage(), '1060') !== false) {
|
||||
echo "<span style='color:gray'>colonne 'color' déjà présente</span> - ";
|
||||
} else { throw $e; }
|
||||
}
|
||||
|
||||
// Configuration des valeurs par défaut et des rôles
|
||||
$fam_pdo->exec("
|
||||
UPDATE pf_people SET role = 'parent' WHERE user_id IS NOT NULL;
|
||||
UPDATE pf_people SET role = 'nounou' WHERE LOWER(name) = 'carole';
|
||||
");
|
||||
|
||||
// On donne des couleurs distinctes par défaut aux deux parents (P1 = Cyan, P2 = Orange)
|
||||
$stmtP = $fam_pdo->query("SELECT id FROM pf_people WHERE role = 'parent' ORDER BY id ASC");
|
||||
$pIds = $stmtP->fetchAll(PDO::FETCH_COLUMN);
|
||||
if (isset($pIds[0])) {
|
||||
$fam_pdo->exec("UPDATE pf_people SET color = '#0891b2' WHERE id = " . (int)$pIds[0]);
|
||||
}
|
||||
if (isset($pIds[1])) {
|
||||
$fam_pdo->exec("UPDATE pf_people SET color = '#f59e0b' WHERE id = " . (int)$pIds[1]);
|
||||
}
|
||||
|
||||
echo "<span style='color:blue'>Rôles et Couleurs initialisés.</span></li>";
|
||||
|
||||
// ==========================================
|
||||
// 2. GESTION DU BUDGET (pf_alloc_values)
|
||||
// ==========================================
|
||||
echo "<li><strong>Table pf_alloc_values :</strong> ";
|
||||
|
||||
$checkAlex = $fam_pdo->query("SHOW COLUMNS FROM pf_alloc_values LIKE 'amount_alex'")->rowCount();
|
||||
if ($checkAlex > 0) {
|
||||
$fam_pdo->exec("ALTER TABLE pf_alloc_values CHANGE amount_alex amount_p1 FLOAT DEFAULT 0");
|
||||
echo "<span style='color:green'>amount_alex -> amount_p1</span> - ";
|
||||
} else {
|
||||
echo "<span style='color:gray'>amount_p1 OK</span> - ";
|
||||
}
|
||||
|
||||
$checkLaia = $fam_pdo->query("SHOW COLUMNS FROM pf_alloc_values LIKE 'amount_laia'")->rowCount();
|
||||
if ($checkLaia > 0) {
|
||||
$fam_pdo->exec("ALTER TABLE pf_alloc_values CHANGE amount_laia amount_p2 FLOAT DEFAULT 0");
|
||||
echo "<span style='color:green'>amount_laia -> amount_p2</span></li>";
|
||||
} else {
|
||||
echo "<span style='color:gray'>amount_p2 OK</span></li>";
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 3. GESTION DU BUDGET (pf_alloc_categories)
|
||||
// ==========================================
|
||||
echo "<li><strong>Table pf_alloc_categories :</strong> ";
|
||||
$stmtCats1 = $fam_pdo->exec("UPDATE pf_alloc_categories SET name = 'Eco P1' WHERE name LIKE 'Eco Alex%'");
|
||||
$stmtCats2 = $fam_pdo->exec("UPDATE pf_alloc_categories SET name = 'Eco P2' WHERE name LIKE 'Eco Laia%'");
|
||||
echo "<span style='color:green'>Catégories 'Eco' génériques mises à jour (" . ($stmtCats1 + $stmtCats2) . " lignes).</span></li>";
|
||||
|
||||
// ==========================================
|
||||
// 4. GESTION DU BUDGET (pf_salary_config)
|
||||
// ==========================================
|
||||
echo "<li><strong>Table pf_salary_config :</strong> ";
|
||||
// On récupère les vrais prénoms fraîchement créés/mappés
|
||||
$stmtParents = $fam_pdo->query("SELECT name FROM pf_people WHERE role = 'parent' ORDER BY id ASC");
|
||||
$parents = $stmtParents->fetchAll();
|
||||
|
||||
if (count($parents) >= 2) {
|
||||
$p1_name = $parents[0]['name'];
|
||||
$p2_name = $parents[1]['name'];
|
||||
|
||||
$stmtSal1 = $fam_pdo->prepare("UPDATE pf_salary_config SET person = ? WHERE person = 'Alex'");
|
||||
$stmtSal1->execute([$p1_name]);
|
||||
|
||||
$stmtSal2 = $fam_pdo->prepare("UPDATE pf_salary_config SET person = ? WHERE person = 'Laia'");
|
||||
$stmtSal2->execute([$p2_name]);
|
||||
|
||||
echo "<span style='color:green'>Salaires mappés vers $p1_name et $p2_name.</span></li>";
|
||||
} else {
|
||||
echo "<span style='color:orange'>Pas assez de parents trouvés pour mapper les salaires.</span></li>";
|
||||
}
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
echo "<span style='color:red'>❌ Erreur : " . $e->getMessage() . "</span><br>";
|
||||
echo "<li style='color:red'>❌ Erreur : " . $e->getMessage() . "</li>";
|
||||
}
|
||||
echo "</ul>";
|
||||
}
|
||||
|
||||
echo "<h2>🎉 Migration terminée ! Tu peux supprimer ce fichier.</h2>";
|
||||
echo "<h2>🎉 Migration terminée avec succès !</h2>";
|
||||
?>
|
||||
+273
-69
@@ -1,7 +1,6 @@
|
||||
/* modules/budget/budget.css */
|
||||
|
||||
/* --- 1. VARIABLES & BASE --- */
|
||||
/* Le root garde les couleurs sémantiques dont le JS/PHP a besoin localement */
|
||||
.budget-view {
|
||||
font-family:
|
||||
"Segoe UI",
|
||||
@@ -128,7 +127,7 @@
|
||||
|
||||
/* --- 3. BOUTONS SPÉCIFIQUES & ACTIONS --- */
|
||||
.pf-btn {
|
||||
border-radius: 50px; /* Surcharge le global pour le budget */
|
||||
border-radius: 50px;
|
||||
}
|
||||
.btn-icon {
|
||||
background: transparent;
|
||||
@@ -263,7 +262,6 @@ td:hover .btn-cell-delete {
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
/* Inputs modifiés */
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -346,20 +344,94 @@ input[type="number"] {
|
||||
gap: 30px;
|
||||
}
|
||||
.prev-section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.prev-section-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
.prev-header-left {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
.prev-header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Configuration Chromatique Parent 1 */
|
||||
.txt-p1,
|
||||
.txt-alex {
|
||||
color: color-mix(in srgb, var(--p1-main, #0891b2) 85%, #000000) !important;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
.col-p1,
|
||||
.col-alex {
|
||||
color: color-mix(in srgb, var(--p1-main, #0891b2) 85%, #000000) !important;
|
||||
text-align: center;
|
||||
}
|
||||
th.col-p1,
|
||||
td.col-p1 {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--p1-main, #0891b2) 8%,
|
||||
#ffffff
|
||||
) !important;
|
||||
text-align: center;
|
||||
}
|
||||
input.txt-p1 {
|
||||
color: color-mix(in srgb, var(--p1-main, #0891b2) 85%, #000000) !important;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
input.txt-p1:focus {
|
||||
border-color: var(--p1-main, #0891b2) !important;
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--p1-main, #0891b2) 10%, #ffffff) !important;
|
||||
}
|
||||
|
||||
/* Configuration Chromatique Parent 2 */
|
||||
.txt-p2 {
|
||||
color: color-mix(in srgb, var(--p2-main, #f59e0b) 85%, #000000) !important;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
.col-p2 {
|
||||
color: color-mix(in srgb, var(--p2-main, #f59e0b) 85%, #000000) !important;
|
||||
text-align: center;
|
||||
}
|
||||
th.col-p2,
|
||||
td.col-p2 {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--p2-main, #f59e0b) 8%,
|
||||
#ffffff
|
||||
) !important;
|
||||
text-align: center;
|
||||
}
|
||||
input.txt-p2 {
|
||||
color: color-mix(in srgb, var(--p2-main, #f59e0b) 85%, #000000) !important;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
input.txt-p2:focus {
|
||||
border-color: var(--p2-main, #f59e0b) !important;
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--p2-main, #f59e0b) 10%, #ffffff) !important;
|
||||
}
|
||||
|
||||
.prev-input {
|
||||
width: 100%;
|
||||
@@ -385,19 +457,6 @@ input[type="number"] {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.prev-section-header,
|
||||
.view-header,
|
||||
.budget-view > div[style*="justify-content: space-between"] {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
float: none !important;
|
||||
position: relative !important;
|
||||
width: 100% !important;
|
||||
gap: 15px !important;
|
||||
}
|
||||
|
||||
.prev-salary-table {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
@@ -419,6 +478,15 @@ input[type="number"] {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
.prev-salary-table th small {
|
||||
font-weight: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
.prev-salary-table th.th-family-savings,
|
||||
.prev-salary-table td.td-family-savings {
|
||||
background: #f0f9ff !important;
|
||||
color: #0284c7 !important;
|
||||
}
|
||||
.prev-salary-table th:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -426,6 +494,22 @@ input[type="number"] {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.prev-salary-table tbody tr:nth-child(1) td:first-child {
|
||||
text-align: left !important;
|
||||
font-weight: bold !important;
|
||||
color: var(--text-main) !important;
|
||||
border-left: 4px solid var(--p1-main, #0891b2) !important;
|
||||
}
|
||||
|
||||
/* Parent 2 (Ligne 2, ex: Laia) */
|
||||
.prev-salary-table tbody tr:nth-child(2) td:first-child {
|
||||
text-align: left !important;
|
||||
font-weight: bold !important;
|
||||
color: var(--text-main) !important;
|
||||
border-left: 4px solid var(--p2-main, #f59e0b) !important;
|
||||
}
|
||||
|
||||
.prev-timeline-wrapper {
|
||||
@@ -455,7 +539,7 @@ input[type="number"] {
|
||||
border-right: 1px solid var(--border-light);
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.col-sticky {
|
||||
@@ -490,6 +574,15 @@ input[type="number"] {
|
||||
border-left: 2px solid white;
|
||||
padding: 8px !important;
|
||||
}
|
||||
.th-month span {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.th-month .cycle-start-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: normal;
|
||||
color: #64748b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.th-month.current {
|
||||
background: var(--primary) !important;
|
||||
}
|
||||
@@ -511,16 +604,42 @@ input[type="number"] {
|
||||
font-weight: 700;
|
||||
border-bottom: 2px solid #bfdbfe;
|
||||
}
|
||||
.txt-alex {
|
||||
color: #0891b2;
|
||||
.row-restant td.td-separator {
|
||||
border-left: 2px solid #cbd5e1;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
.txt-laia {
|
||||
color: #f59e0b;
|
||||
.row-indicative {
|
||||
background: #f8fafc;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.row-indicative .col-sticky {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.cat-name-label {
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
}
|
||||
.row-indicative .cat-name-label {
|
||||
color: #64748b;
|
||||
}
|
||||
.row-indicative .cat-name-label span {
|
||||
font-size: 0.7rem;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 4px;
|
||||
padding: 0 4px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.cat-target-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
text-align: right;
|
||||
}
|
||||
.txt-global {
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
background: #f8fafc;
|
||||
border-left: 2px solid #e2e8f0;
|
||||
}
|
||||
.val-ok {
|
||||
color: var(--success);
|
||||
@@ -594,6 +713,9 @@ tr:hover .row-actions {
|
||||
letter-spacing: 0.05em;
|
||||
text-align: center;
|
||||
}
|
||||
.recap-header span {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.recap-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -603,6 +725,7 @@ tr:hover .row-actions {
|
||||
padding: 10px 15px;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.recap-table th {
|
||||
@@ -618,30 +741,68 @@ tr:hover .row-actions {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
.col-alex {
|
||||
background-color: #ecfeff;
|
||||
color: #0891b2;
|
||||
border-left: 1px solid #cffafe;
|
||||
|
||||
.recap-th-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
th.col-alex {
|
||||
background-color: #cffafe;
|
||||
border-bottom-color: #a5f3fc;
|
||||
.recap-th-content span {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.col-laia {
|
||||
background-color: #fffbeb;
|
||||
color: #d97706;
|
||||
border-left: 1px solid #fef3c7;
|
||||
.validation-badge {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
th.col-laia {
|
||||
background-color: #fef3c7;
|
||||
border-bottom-color: #fde68a;
|
||||
|
||||
/* --- Style de base du bouton de validation (Générique) --- */
|
||||
.recap-table .pf-btn.btn-small {
|
||||
background: #ffffff !important;
|
||||
font-size: 0.7rem !important;
|
||||
padding: 2px 8px !important;
|
||||
height: auto !important;
|
||||
width: auto !important;
|
||||
font-weight: 600;
|
||||
border-radius: 9px !important;
|
||||
box-shadow: none !important;
|
||||
transform: none !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.col-global {
|
||||
background-color: #f8fafc;
|
||||
color: #475569;
|
||||
font-weight: 700;
|
||||
border-left: 2px solid #e2e8f0;
|
||||
|
||||
/* Couleurs dynamiques adaptées au Parent 1 (ex: Féfé) */
|
||||
.col-p1 .pf-btn.btn-small {
|
||||
color: var(--p1-main, #0891b2) !important;
|
||||
border: 1px solid var(--p1-main, #0891b2) !important;
|
||||
}
|
||||
.col-p1 .pf-btn.btn-small:hover {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--p1-main, #0891b2) 12%,
|
||||
#ffffff
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Couleurs dynamiques adaptées au Parent 2 (ex: Laia) */
|
||||
.col-p2 .pf-btn.btn-small {
|
||||
color: var(--p2-main, #f59e0b) !important;
|
||||
border: 1px solid var(--p2-main, #f59e0b) !important;
|
||||
}
|
||||
.col-p2 .pf-btn.btn-small:hover {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--p2-main, #f59e0b) 12%,
|
||||
#ffffff
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Grand Totaux Structuraux */
|
||||
.row-grand-total td {
|
||||
background-color: #1e293b;
|
||||
color: white;
|
||||
@@ -649,14 +810,22 @@ th.col-laia {
|
||||
border: none;
|
||||
padding: 12px 15px;
|
||||
}
|
||||
.row-grand-total .col-alex {
|
||||
background: #164e63;
|
||||
color: #67e8f9;
|
||||
.row-grand-total .col-p1 {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--p1-main, #0891b2) 65%,
|
||||
#000000
|
||||
) !important;
|
||||
color: color-mix(in srgb, var(--p1-main, #0891b2) 20%, #ffffff) !important;
|
||||
border: none;
|
||||
}
|
||||
.row-grand-total .col-laia {
|
||||
background: #78350f;
|
||||
color: #fcd34d;
|
||||
.row-grand-total .col-p2 {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--p2-main, #f59e0b) 65%,
|
||||
#000000
|
||||
) !important;
|
||||
color: color-mix(in srgb, var(--p2-main, #f59e0b) 20%, #ffffff) !important;
|
||||
border: none;
|
||||
}
|
||||
.row-grand-total .col-global {
|
||||
@@ -665,7 +834,7 @@ th.col-laia {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* --- 9. STYLES SPÉCIFIQUES AU SUIVI MENSUEL --- */
|
||||
/* --- 9. STYLES DU SUIVI MENSUEL --- */
|
||||
.cat-card {
|
||||
background: var(--bg-panel);
|
||||
border-radius: var(--radius);
|
||||
@@ -675,20 +844,17 @@ th.col-laia {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cat-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px; /* Padding interne à l'encart coloré */
|
||||
padding: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cat-card-title-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cat-card-subtitle {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
@@ -768,7 +934,52 @@ input[type="month"].pf-input {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* --- 10. EPARGNE --- */
|
||||
/* --- 10. SECTION NOTES BUDGETAIRES --- */
|
||||
.budget-notes-section {
|
||||
margin: 20px 0;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
.notes-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.notes-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
color: #1e293b;
|
||||
}
|
||||
.notes-header h3 span {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.note-save-indicator {
|
||||
font-size: 0.85rem;
|
||||
color: #10b981;
|
||||
font-weight: bold;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
#monthNoteArea {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
border-color: #cbd5e1;
|
||||
background: #f8fafc;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.notes-footer {
|
||||
text-align: right;
|
||||
}
|
||||
.notes-footer .pf-btn {
|
||||
width: auto;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* --- 11. EPARGNE --- */
|
||||
.epargne-inline-input {
|
||||
width: 100%;
|
||||
min-width: 60px;
|
||||
@@ -793,7 +1004,7 @@ input[type="month"].pf-input {
|
||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
/* --- 11. CALCULATRICE FLOTTANTE --- */
|
||||
/* --- 12. CALCULATRICE FLOTTANTE --- */
|
||||
.pf-fab-sum {
|
||||
position: fixed;
|
||||
right: 30px;
|
||||
@@ -883,7 +1094,7 @@ body.sum-mode-active .sum-target {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
/* --- 12. RESPONSIVE MOBILE BUDGET --- */
|
||||
/* --- 13. RESPONSIVE MOBILE BUDGET --- */
|
||||
@media (max-width: 768px) {
|
||||
.budget-view {
|
||||
padding: 10px !important;
|
||||
@@ -917,11 +1128,6 @@ body.sum-mode-active .sum-target {
|
||||
.budget-view .categories-grid > .cat-card {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.budget-view
|
||||
> div[style*="justify-content: space-between"]:not(.view-header) {
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
.budget-view
|
||||
> div[style*="justify-content: space-between"]:not(.view-header) {
|
||||
flex-direction: column !important;
|
||||
@@ -1021,7 +1227,6 @@ body.sum-mode-active .sum-target {
|
||||
justify-content: flex-start;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.col-sticky,
|
||||
.sticky-col {
|
||||
min-width: 120px !important;
|
||||
@@ -1059,8 +1264,7 @@ body.sum-mode-active .sum-target {
|
||||
right: auto;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
.cat-card-header {
|
||||
padding: 12px 10px; /* Plus serré sur mobile */
|
||||
padding: 12px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"}]
|
||||
|
||||
+19
-7
@@ -15,6 +15,7 @@ require_once dirname(__DIR__, 2) . '/includes/db.php';
|
||||
// ── Auto-create tables ────────────────────────────────────────────────────────
|
||||
$pdo->exec("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");
|
||||
|
||||
@@ -48,6 +49,8 @@ $pdo->exec("CREATE TABLE IF NOT EXISTS pf_item_category_rules (
|
||||
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD COLUMN list_id INT NOT NULL DEFAULT 1 AFTER id"); } catch (\Exception $e) {}
|
||||
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD COLUMN category_id INT DEFAULT NULL AFTER list_id"); } catch (\Exception $e) {}
|
||||
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD KEY idx_items_list (list_id)"); } catch (\Exception $e) {}
|
||||
try { $pdo->exec("ALTER TABLE pf_lists ADD COLUMN color VARCHAR(20) DEFAULT NULL AFTER name"); } catch (\Exception $e) {}
|
||||
try { $pdo->exec("ALTER TABLE pf_lists ADD COLUMN list_type VARCHAR(50) DEFAULT NULL AFTER color"); } catch (\Exception $e) {}
|
||||
|
||||
// ── Seed categories if empty ──────────────────────────────────────────────────
|
||||
if ((int)$pdo->query("SELECT COUNT(*) FROM pf_list_categories")->fetchColumn() === 0) {
|
||||
@@ -313,23 +316,32 @@ if ($action === 'set_category' && $method === 'POST') {
|
||||
if ($action === 'lists') {
|
||||
if ($method === 'GET') {
|
||||
$firstId = liste_ensure_default($pdo);
|
||||
$rows = $pdo->query("SELECT id, name, position FROM pf_lists ORDER BY position, id")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$rows = $pdo->query("SELECT id, name, color, list_type, position FROM pf_lists ORDER BY position, id")->fetchAll(PDO::FETCH_ASSOC);
|
||||
echo json_encode(['lists' => $rows, 'default_id' => $firstId]);
|
||||
exit;
|
||||
}
|
||||
if ($method === 'POST') {
|
||||
$name = mb_substr(trim($body['name'] ?? ''), 0, 100);
|
||||
$name = mb_substr(trim($body['name'] ?? ''), 0, 100);
|
||||
$color = mb_substr(trim($body['color'] ?? ''), 0, 20) ?: null;
|
||||
$list_type = mb_substr(trim($body['list_type'] ?? ''), 0, 50) ?: null;
|
||||
if (!$name) { http_response_code(400); echo json_encode(['error' => 'name required']); exit; }
|
||||
$pos = (int)$pdo->query("SELECT COALESCE(MAX(position),0)+1 FROM pf_lists")->fetchColumn();
|
||||
$pdo->prepare("INSERT INTO pf_lists (name, position) VALUES (?,?)")->execute([$name, $pos]);
|
||||
echo json_encode(['id' => (int)$pdo->lastInsertId(), 'name' => $name, 'position' => $pos]);
|
||||
$pdo->prepare("INSERT INTO pf_lists (name, color, list_type, position) VALUES (?,?,?,?)")->execute([$name, $color, $list_type, $pos]);
|
||||
echo json_encode(['id' => (int)$pdo->lastInsertId(), 'name' => $name, 'color' => $color, 'list_type' => $list_type, 'position' => $pos]);
|
||||
exit;
|
||||
}
|
||||
if ($method === 'PUT') {
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$name = mb_substr(trim($body['name'] ?? ''), 0, 100);
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$name = mb_substr(trim($body['name'] ?? ''), 0, 100);
|
||||
$color = array_key_exists('color', $body) ? (mb_substr(trim($body['color'] ?? ''), 0, 20) ?: null) : false;
|
||||
$list_type = array_key_exists('list_type', $body) ? (mb_substr(trim($body['list_type'] ?? ''), 0, 50) ?: null) : false;
|
||||
if (!$id || !$name) { http_response_code(400); echo json_encode(['error' => 'invalid']); exit; }
|
||||
$pdo->prepare("UPDATE pf_lists SET name=? WHERE id=?")->execute([$name, $id]);
|
||||
$sets = ['name=?'];
|
||||
$vals = [$name];
|
||||
if ($color !== false) { $sets[] = 'color=?'; $vals[] = $color; }
|
||||
if ($list_type !== false) { $sets[] = 'list_type=?'; $vals[] = $list_type; }
|
||||
$vals[] = $id;
|
||||
$pdo->prepare("UPDATE pf_lists SET " . implode(', ', $sets) . " WHERE id=?")->execute($vals);
|
||||
echo json_encode(['ok' => true]); exit;
|
||||
}
|
||||
if ($method === 'DELETE') {
|
||||
|
||||
@@ -310,6 +310,135 @@
|
||||
}
|
||||
.liste-empty-icon { font-size: 3rem; margin-bottom: .75rem; }
|
||||
|
||||
/* ── List modal ────────────────────────────────────────────────────────────── */
|
||||
|
||||
.liste-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.liste-modal {
|
||||
background: var(--card-bg, #fff);
|
||||
border: 1px solid var(--border, #dee2e6);
|
||||
border-radius: 14px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,.2);
|
||||
}
|
||||
|
||||
.liste-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--border, #dee2e6);
|
||||
}
|
||||
.liste-modal-header h3 { margin: 0; font-size: .95rem; font-weight: 700; color: var(--text, #212529); }
|
||||
|
||||
.liste-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.3rem;
|
||||
cursor: pointer;
|
||||
color: var(--muted, #6c757d);
|
||||
line-height: 1;
|
||||
padding: 0 .25rem;
|
||||
transition: color .15s;
|
||||
}
|
||||
.liste-modal-close:hover { color: var(--text, #212529); }
|
||||
|
||||
.liste-modal-body {
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .85rem;
|
||||
}
|
||||
|
||||
.liste-modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: .5rem;
|
||||
padding: .85rem 1.25rem;
|
||||
border-top: 1px solid var(--border, #dee2e6);
|
||||
}
|
||||
.liste-modal-footer .btn-tool-danger { margin-right: auto; }
|
||||
|
||||
.liste-form-group { display: flex; flex-direction: column; gap: .3rem; }
|
||||
.liste-form-label {
|
||||
font-size: .73rem;
|
||||
font-weight: 700;
|
||||
color: var(--muted, #6c757d);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
|
||||
.liste-modal-name, .liste-modal-type {
|
||||
padding: .5rem .7rem;
|
||||
border: 1px solid var(--border, #dee2e6);
|
||||
border-radius: 8px;
|
||||
background: var(--input-bg, #fff);
|
||||
color: var(--text, #212529);
|
||||
font-size: .875rem;
|
||||
font-family: inherit;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
transition: border-color .15s;
|
||||
}
|
||||
.liste-modal-name:focus, .liste-modal-type:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary, #4361ee);
|
||||
box-shadow: 0 0 0 3px rgba(67,97,238,.15);
|
||||
}
|
||||
|
||||
.btn-tool-primary {
|
||||
background: var(--primary, #4361ee);
|
||||
color: #fff;
|
||||
border-color: var(--primary, #4361ee);
|
||||
}
|
||||
.btn-tool-primary:hover { filter: brightness(1.1); background: var(--primary, #4361ee); }
|
||||
|
||||
/* ── Color picker ──────────────────────────────────────────────────────────── */
|
||||
|
||||
.liste-color-picker {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .45rem;
|
||||
}
|
||||
|
||||
.liste-color-swatch {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
background: var(--swatch, transparent);
|
||||
cursor: pointer;
|
||||
transition: transform .1s;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.liste-color-swatch:hover { transform: scale(1.2); }
|
||||
.liste-color-swatch.active {
|
||||
outline-color: var(--swatch, var(--primary, #4361ee));
|
||||
border-color: var(--card-bg, #fff);
|
||||
}
|
||||
.liste-color-swatch-none {
|
||||
background: transparent !important;
|
||||
border: 2px dashed var(--border, #dee2e6) !important;
|
||||
}
|
||||
.liste-color-swatch-none.active { outline-color: var(--primary, #4361ee); }
|
||||
|
||||
/* ── Tab type emoji ────────────────────────────────────────────────────────── */
|
||||
|
||||
.liste-tab-type { font-size: .85rem; line-height: 1; }
|
||||
|
||||
/* ── Category section headers ──────────────────────────────────────────────── */
|
||||
|
||||
.liste-cat-section-header {
|
||||
@@ -429,6 +558,13 @@
|
||||
[data-theme='dark'] .btn-tool:hover { background: #2a2a3e; }
|
||||
[data-theme='dark'] .liste-new-form input { background: #1e1e2e; color: #cdd6f4; border-color: #4361ee; }
|
||||
[data-theme='dark'] .liste-new-form #btn-cancel-new-list { background: #2a2a3e; color: #cdd6f4; }
|
||||
[data-theme='dark'] .liste-modal { background: #1e1e2e; border-color: #3a3a4e; }
|
||||
[data-theme='dark'] .liste-modal-header,
|
||||
[data-theme='dark'] .liste-modal-footer { border-color: #3a3a4e; }
|
||||
[data-theme='dark'] .liste-modal-header h3 { color: #cdd6f4; }
|
||||
[data-theme='dark'] .liste-modal-name,
|
||||
[data-theme='dark'] .liste-modal-type { background: #2a2a3e; border-color: #3a3a4e; color: #cdd6f4; }
|
||||
[data-theme='dark'] .liste-color-swatch.active { border-color: #1e1e2e; }
|
||||
[data-theme='dark'] .liste-cat-badge { border-color: #3a3a4e; }
|
||||
[data-theme='dark'] .liste-cat-badge:hover { background: #313155; border-color: #4361ee; }
|
||||
[data-theme='dark'] .liste-cat-section-count { background: #2a2a3e; color: #8892b0; }
|
||||
|
||||
+169
-66
@@ -11,6 +11,35 @@ const state = {
|
||||
catMap: {},
|
||||
};
|
||||
|
||||
// ── Constants ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const LIST_COLORS = [
|
||||
{ id: 'none', hex: null },
|
||||
{ id: 'blue', hex: '#3b82f6' },
|
||||
{ id: 'red', hex: '#ef4444' },
|
||||
{ id: 'amber', hex: '#f59e0b' },
|
||||
{ id: 'purple', hex: '#8b5cf6' },
|
||||
{ id: 'pink', hex: '#ec4899' },
|
||||
{ id: 'emerald', hex: '#10b981' },
|
||||
{ id: 'orange', hex: '#f97316' },
|
||||
{ id: 'cyan', hex: '#06b6d4' },
|
||||
{ id: 'indigo', hex: '#6366f1' },
|
||||
{ id: 'teal', hex: '#14b8a6' },
|
||||
{ id: 'gray', hex: '#6b7280' },
|
||||
];
|
||||
|
||||
const LIST_TYPES = [
|
||||
{ value: '', label: '— Aucun —', emoji: '' },
|
||||
{ value: 'courses', label: 'Courses', emoji: '🛒' },
|
||||
{ value: 'todo', label: 'To-do', emoji: '✅' },
|
||||
{ value: 'voyage', label: 'Voyage', emoji: '✈️' },
|
||||
{ value: 'travail', label: 'Travail', emoji: '💼' },
|
||||
{ value: 'maison', label: 'Maison', emoji: '🏠' },
|
||||
{ value: 'sante', label: 'Santé', emoji: '💊' },
|
||||
{ value: 'loisirs', label: 'Loisirs', emoji: '🎮' },
|
||||
{ value: 'autre', label: 'Autre', emoji: '📋' },
|
||||
];
|
||||
|
||||
// ── Utilities ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function esc(s) {
|
||||
@@ -42,6 +71,116 @@ async function api(action, method = 'GET', body = null, params = {}) {
|
||||
return r.json();
|
||||
}
|
||||
|
||||
// ── List modal ─────────────────────────────────────────────────────────────────
|
||||
|
||||
let activeModal = null;
|
||||
|
||||
function openListModal(listId = null) {
|
||||
if (activeModal) { activeModal.remove(); activeModal = null; }
|
||||
const list = listId ? state.lists.find(l => l.id === listId) : null;
|
||||
const isNew = !listId;
|
||||
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.className = 'liste-modal-backdrop';
|
||||
backdrop.innerHTML = `
|
||||
<div class="liste-modal" role="dialog" aria-modal="true">
|
||||
<div class="liste-modal-header">
|
||||
<h3>${isNew ? 'Nouvelle liste' : 'Modifier la liste'}</h3>
|
||||
<button class="liste-modal-close" aria-label="Fermer">×</button>
|
||||
</div>
|
||||
<div class="liste-modal-body">
|
||||
<div class="liste-form-group">
|
||||
<label class="liste-form-label">Nom</label>
|
||||
<input class="liste-modal-name" type="text" maxlength="100"
|
||||
value="${esc(list?.name ?? '')}" placeholder="Ma liste…" autocomplete="off">
|
||||
</div>
|
||||
<div class="liste-form-group">
|
||||
<label class="liste-form-label">Couleur</label>
|
||||
<div class="liste-color-picker">
|
||||
${LIST_COLORS.map(c => {
|
||||
const isActive = (c.hex === null && !list?.color) || (list?.color === c.hex);
|
||||
const cls = 'liste-color-swatch' + (c.hex === null ? ' liste-color-swatch-none' : '') + (isActive ? ' active' : '');
|
||||
const style = c.hex ? `style="--swatch:${c.hex}"` : '';
|
||||
return `<button class="${cls}" data-color="${c.hex ?? ''}" ${style} title="${c.id}"></button>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="liste-form-group">
|
||||
<label class="liste-form-label">Type de liste</label>
|
||||
<select class="liste-modal-type">
|
||||
${LIST_TYPES.map(t =>
|
||||
`<option value="${esc(t.value)}" ${list?.list_type === t.value ? 'selected' : ''}>${t.emoji ? t.emoji + ' ' : ''}${esc(t.label)}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="liste-modal-footer">
|
||||
${!isNew && state.lists.length > 1
|
||||
? `<button class="btn-tool btn-tool-danger liste-modal-delete">Supprimer</button>`
|
||||
: ''}
|
||||
<button class="btn-tool liste-modal-cancel">Annuler</button>
|
||||
<button class="btn-tool btn-tool-primary liste-modal-save">Enregistrer</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(backdrop);
|
||||
activeModal = backdrop;
|
||||
|
||||
const nameInput = backdrop.querySelector('.liste-modal-name');
|
||||
nameInput.focus();
|
||||
nameInput.setSelectionRange(nameInput.value.length, nameInput.value.length);
|
||||
|
||||
// Color swatches
|
||||
backdrop.querySelectorAll('.liste-color-swatch').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
backdrop.querySelectorAll('.liste-color-swatch').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
const close = () => { backdrop.remove(); activeModal = null; };
|
||||
backdrop.querySelector('.liste-modal-close').addEventListener('click', close);
|
||||
backdrop.querySelector('.liste-modal-cancel').addEventListener('click', close);
|
||||
backdrop.addEventListener('click', e => { if (e.target === backdrop) close(); });
|
||||
|
||||
backdrop.querySelector('.liste-modal-delete')?.addEventListener('click', () => { close(); deleteList(listId); });
|
||||
backdrop.querySelector('.liste-modal-save').addEventListener('click', () => saveListModal(backdrop, listId));
|
||||
nameInput.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') saveListModal(backdrop, listId);
|
||||
if (e.key === 'Escape') close();
|
||||
});
|
||||
}
|
||||
|
||||
async function saveListModal(backdrop, listId) {
|
||||
const name = backdrop.querySelector('.liste-modal-name').value.trim();
|
||||
if (!name) { backdrop.querySelector('.liste-modal-name').focus(); return; }
|
||||
|
||||
const activeSwatch = backdrop.querySelector('.liste-color-swatch.active');
|
||||
const color = activeSwatch?.dataset.color || null;
|
||||
const list_type = backdrop.querySelector('.liste-modal-type').value || null;
|
||||
|
||||
if (listId) {
|
||||
const r = await api('lists', 'PUT', { name, color, list_type }, { id: listId });
|
||||
if (r.ok) {
|
||||
const list = state.lists.find(l => l.id === listId);
|
||||
if (list) { list.name = name; list.color = color; list.list_type = list_type; }
|
||||
backdrop.remove(); activeModal = null;
|
||||
renderTabs();
|
||||
toast('Liste mise à jour');
|
||||
}
|
||||
} else {
|
||||
const r = await api('lists', 'POST', { name, color, list_type });
|
||||
if (r.id) {
|
||||
state.lists.push({ id: r.id, name, color: r.color, list_type: r.list_type, position: r.position });
|
||||
state.currentListId = r.id;
|
||||
backdrop.remove(); activeModal = null;
|
||||
await loadItems();
|
||||
renderTabs();
|
||||
toast('Liste créée : ' + name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Categories ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async function loadCategories() {
|
||||
@@ -104,49 +243,54 @@ function closeCategoryPicker() {
|
||||
|
||||
// ── Tabs ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
function typeEmoji(list_type) {
|
||||
return LIST_TYPES.find(t => t.value === list_type)?.emoji || '';
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
const container = document.getElementById('liste-tabs');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
state.lists.forEach(list => {
|
||||
const isActive = list.id === state.currentListId;
|
||||
const tab = document.createElement('div');
|
||||
tab.className = 'liste-tab' + (list.id === state.currentListId ? ' active' : '');
|
||||
tab.className = 'liste-tab' + (isActive ? ' active' : '');
|
||||
tab.dataset.id = list.id;
|
||||
|
||||
if (list.id === state.currentListId) {
|
||||
// Apply custom color to entire tab
|
||||
if (list.color) {
|
||||
if (isActive) {
|
||||
tab.style.cssText = `background:${list.color};border-color:${list.color};color:#fff`;
|
||||
} else {
|
||||
tab.style.cssText = `border-color:${list.color};color:${list.color}`;
|
||||
}
|
||||
}
|
||||
|
||||
const emoji = typeEmoji(list.list_type);
|
||||
const prefix = emoji ? `<span class="liste-tab-type">${emoji}</span>` : '';
|
||||
|
||||
if (isActive) {
|
||||
tab.innerHTML = `
|
||||
${prefix}
|
||||
<span class="liste-tab-name">${esc(list.name)}</span>
|
||||
<button class="liste-tab-btn liste-tab-rename" title="${esc(T.rename_list)}" data-id="${list.id}">✏</button>
|
||||
${state.lists.length > 1 ? `<button class="liste-tab-btn liste-tab-delete" title="Supprimer" data-id="${list.id}">×</button>` : ''}
|
||||
<button class="liste-tab-btn liste-tab-edit" title="Modifier" data-id="${list.id}">✏</button>
|
||||
`;
|
||||
} else {
|
||||
tab.innerHTML = `<span class="liste-tab-name">${esc(list.name)}</span>`;
|
||||
tab.innerHTML = `${prefix}<span class="liste-tab-name">${esc(list.name)}</span>`;
|
||||
tab.addEventListener('click', () => switchList(list.id));
|
||||
}
|
||||
container.appendChild(tab);
|
||||
});
|
||||
|
||||
container.querySelectorAll('.liste-tab-rename').forEach(btn => {
|
||||
btn.addEventListener('click', e => { e.stopPropagation(); startRename(parseInt(btn.dataset.id)); });
|
||||
});
|
||||
container.querySelectorAll('.liste-tab-delete').forEach(btn => {
|
||||
btn.addEventListener('click', e => { e.stopPropagation(); deleteList(parseInt(btn.dataset.id)); });
|
||||
container.querySelectorAll('.liste-tab-edit').forEach(btn => {
|
||||
btn.addEventListener('click', e => { e.stopPropagation(); openListModal(parseInt(btn.dataset.id)); });
|
||||
});
|
||||
}
|
||||
|
||||
function startRename(listId) {
|
||||
const list = state.lists.find(l => l.id === listId);
|
||||
if (!list) return;
|
||||
const name = prompt(T.new_name || 'Nouveau nom :', list.name);
|
||||
if (name === null || name.trim() === '') return;
|
||||
api('lists', 'PUT', { name: name.trim() }, { id: listId }).then(r => {
|
||||
if (r.ok) {
|
||||
list.name = name.trim();
|
||||
renderTabs();
|
||||
toast(T.list_renamed || 'Liste renommée');
|
||||
}
|
||||
});
|
||||
}
|
||||
// ── List management ─────────────────────────────────────────────────────────────
|
||||
|
||||
document.getElementById('btn-add-list')?.addEventListener('click', () => openListModal(null));
|
||||
|
||||
async function deleteList(listId) {
|
||||
if (!confirm(T.confirm_delete_list || 'Supprimer cette liste et tous ses articles ?')) return;
|
||||
@@ -161,41 +305,6 @@ async function deleteList(listId) {
|
||||
toast(T.list_deleted || 'Liste supprimée');
|
||||
}
|
||||
|
||||
// ── List management ─────────────────────────────────────────────────────────────
|
||||
|
||||
document.getElementById('btn-add-list')?.addEventListener('click', () => {
|
||||
document.getElementById('liste-new-form')?.classList.remove('hidden');
|
||||
document.getElementById('btn-add-list')?.classList.add('hidden');
|
||||
document.getElementById('new-list-name')?.focus();
|
||||
});
|
||||
document.getElementById('btn-cancel-new-list')?.addEventListener('click', cancelNewList);
|
||||
document.getElementById('btn-confirm-new-list')?.addEventListener('click', confirmNewList);
|
||||
document.getElementById('new-list-name')?.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') confirmNewList();
|
||||
if (e.key === 'Escape') cancelNewList();
|
||||
});
|
||||
|
||||
function cancelNewList() {
|
||||
document.getElementById('liste-new-form')?.classList.add('hidden');
|
||||
document.getElementById('btn-add-list')?.classList.remove('hidden');
|
||||
if (document.getElementById('new-list-name')) document.getElementById('new-list-name').value = '';
|
||||
}
|
||||
|
||||
async function confirmNewList() {
|
||||
const input = document.getElementById('new-list-name');
|
||||
const name = input?.value.trim();
|
||||
if (!name) return;
|
||||
const r = await api('lists', 'POST', { name });
|
||||
if (r.id) {
|
||||
state.lists.push({ id: r.id, name: r.name, position: r.position });
|
||||
state.currentListId = r.id;
|
||||
cancelNewList();
|
||||
await loadItems();
|
||||
renderTabs();
|
||||
toast((T.list_created || 'Liste créée') + ' : ' + r.name);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Switch list ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async function switchList(listId) {
|
||||
@@ -207,10 +316,7 @@ async function switchList(listId) {
|
||||
// ── Items ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function loadItems() {
|
||||
if (!state.currentListId) {
|
||||
renderItems();
|
||||
return;
|
||||
}
|
||||
if (!state.currentListId) { renderItems(); return; }
|
||||
const r = await api('items', 'GET', null, { list_id: state.currentListId });
|
||||
state.items = r.items || [];
|
||||
renderItems();
|
||||
@@ -236,15 +342,12 @@ function renderItems() {
|
||||
let html = '';
|
||||
|
||||
if (hasCategories && state.categories.length) {
|
||||
// Group pending items by category
|
||||
const groups = new Map();
|
||||
pending.forEach(item => {
|
||||
const key = item.category_id ?? 0;
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key).push(item);
|
||||
});
|
||||
|
||||
// Render in category order, uncategorized last
|
||||
const catIds = state.categories.map(c => parseInt(c.id));
|
||||
const orderedKeys = catIds.filter(id => groups.has(id));
|
||||
if (groups.has(0)) orderedKeys.push(0);
|
||||
@@ -284,7 +387,7 @@ function rowHtml(item) {
|
||||
const checked = item.in_cart ? 'checked' : '';
|
||||
const cls = item.in_cart ? 'liste-row in-cart' : 'liste-row';
|
||||
const cat = item.category_id ? state.catMap[item.category_id] : null;
|
||||
const badgeCls = 'liste-cat-badge' + (cat ? '' : ' liste-cat-badge-empty');
|
||||
const badgeCls = 'liste-cat-badge' + (cat ? '' : ' liste-cat-badge-empty');
|
||||
const badgeIcon = cat ? esc(cat.icon) : '🏷️';
|
||||
const badgeTip = cat ? esc(cat.name) : 'Non classé';
|
||||
return `<div class="${cls}" data-id="${item.id}">
|
||||
|
||||
+7
-7
@@ -13,10 +13,13 @@ CREATE TABLE IF NOT EXISTS pf_users (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ─── Personnes ────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_people (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
CREATE TABLE `pf_people` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(50) NOT NULL,
|
||||
`user_id` int(11) DEFAULT NULL,
|
||||
`role` varchar(50) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
||||
|
||||
-- ─── Calendrier familial ──────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_events (
|
||||
@@ -232,9 +235,6 @@ SET FOREIGN_KEY_CHECKS = 1;
|
||||
INSERT IGNORE INTO pf_users (id, username, password_hash, display_name)
|
||||
VALUES (1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin');
|
||||
|
||||
-- Personnes (IDs fixes correspondant aux constantes dans config.php)
|
||||
INSERT IGNORE INTO pf_people (id, name) VALUES (2, 'Alex');
|
||||
INSERT IGNORE INTO pf_people (id, name) VALUES (3, 'Laia');
|
||||
|
||||
-- ─── Calendar iOS ─────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_calendar_events (
|
||||
|
||||
+44
-17
@@ -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'; ?>
|
||||
Reference in New Issue
Block a user