diff --git a/budget.php b/budget.php
index 9b25b88..f255cbc 100644
--- a/budget.php
+++ b/budget.php
@@ -16,8 +16,23 @@ $hasBudgetItems = ((int)$pdo->query("SELECT COUNT(*) FROM pf_budget_items")->fet
// Le minimum vital pour que ça tourne
$isBudgetSetupOk = ($hasCategories && $hasSalaries && $hasAccounts);
-// Gestion de l'onglet actif (par défaut 'suivi')
+// ============================================================================
+// GESTION DES VUES (MANDATORY VS OPTIONAL)
+// ============================================================================
+$enabledViews = $_SESSION['enabled_views'] ?? ['budget_prev', 'budget_epargne', 'budget_provisions'];
+
+// Vues obligatoires pour le cœur du budget
+$allowedTabs = ['suivi', 'recap'];
+
+if (in_array('budget_prev', $enabledViews)) $allowedTabs[] = 'budget_prev';
+if (in_array('budget_epargne', $enabledViews)) $allowedTabs[] = 'epargne';
+if (in_array('budget_provisions', $enabledViews)) $allowedTabs[] = 'provisions';
+
+// Gestion de l'onglet actif avec fallback de sécurité
$tab = $_GET['tab'] ?? 'suivi';
+if (!in_array($tab, $allowedTabs)) {
+ $tab = 'suivi';
+}
$pageTitle = tr('budget_page_title');
$activePage = "budget";
@@ -30,7 +45,6 @@ require __DIR__ . '/header.php';
= tr('budget_main_header') ?>
-
diff --git a/dark-mode.css b/dark-mode.css
index 39fb6b3..f7e93a4 100644
--- a/dark-mode.css
+++ b/dark-mode.css
@@ -208,3 +208,15 @@
[data-theme="dark"] .month-actions .btn-icon-small:hover {
background: rgba(255, 255, 255, 0.06) !important;
}
+
+[data-theme="dark"] .pf-module-card-wrapper:has(input.js-mod-checkbox:checked) {
+ background: rgba(59, 130, 246, 0.1);
+}
+[data-theme="dark"] .pf-sub-module-pill {
+ background: var(--bg-page);
+ border-color: var(--border-strong);
+}
+[data-theme="dark"] .pf-sub-module-pill:has(input:checked) {
+ background: var(--bg-panel);
+ border-color: var(--primary);
+}
diff --git a/docker/init/00-setup.sql b/docker/init/00-setup.sql
index 8fdc672..b04589d 100644
--- a/docker/init/00-setup.sql
+++ b/docker/init/00-setup.sql
@@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS families (
invite_code VARCHAR(32) NOT NULL UNIQUE,
is_active TINYINT(1) DEFAULT 1,
enabled_modules JSON DEFAULT NULL,
+ enabled_views JSON DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/food.php b/food.php
index f0bc58c..d86a976 100644
--- a/food.php
+++ b/food.php
@@ -5,8 +5,26 @@ require __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/i18n.php';
require_login('/login.php');
-// Gestion de l'onglet actif
-$tab = $_GET['tab'] ?? 'meals';
+// 1. Détection des vues actives
+$enabledViews = $_SESSION['enabled_views'] ?? ['food_meals', 'food_liste'];
+$allowedTabs = [];
+
+if (in_array('food_meals', $enabledViews)) $allowedTabs[] = 'meals';
+if (in_array('food_liste', $enabledViews)) $allowedTabs[] = 'liste';
+
+// 2. Fallback intelligent de l'onglet actif
+if (empty($allowedTabs)) {
+ $tab = 'empty'; // L'utilisateur a désactivé les deux vues
+} else {
+ // Par défaut, on prend le premier onglet disponible (Meals, ou Liste si Meals est désactivé)
+ $defaultTab = $allowedTabs[0];
+ $tab = $_GET['tab'] ?? $defaultTab;
+
+ // Sécurité URL : Si on force un onglet non autorisé
+ if (!in_array($tab, $allowedTabs)) {
+ $tab = $defaultTab;
+ }
+}
$pageTitle = tr('mod_food_name');
$activePage = "food";
@@ -21,30 +39,44 @@ require __DIR__ . '/header.php';
🍳 = tr('mod_food_name') ?>
+
+
+ 👀
+ " . tr('food_err_no_views') . "
+ " . tr('food_err_no_views_desc') . "
+ ⚙️ " . tr('settings') . "
+ ";
+ } else {
+ // Routage classique
$viewPath = __DIR__ . "/modules/food/views/{$tab}.php";
if (file_exists($viewPath)) {
require $viewPath;
} else {
echo "Vue introuvable : " . htmlspecialchars($tab) . "
";
}
- } else {
- echo "" . tr('error_occured') . "
";
}
?>
diff --git a/global.css b/global.css
index 00915d7..3c70753 100644
--- a/global.css
+++ b/global.css
@@ -410,25 +410,124 @@ body.pf-home .pf-mobile-close-btn {
line-height: 1.4;
}
-/* Tuile module (checkbox) — état coché via :has(), compatible thème */
-.pf-module-tile {
+/* --- MODULES & SOUS-VUES UNIFIÉS (Settings) --- */
+.pf-module-card-wrapper {
+ background: var(--bg-panel);
+ border: 1px solid var(--border-light);
+ border-radius: 12px;
+ padding: 12px 12px;
display: flex;
- align-items: center;
- gap: 14px;
- padding: 14px 16px;
- border: 2px solid var(--border-light);
- border-radius: 10px;
- cursor: pointer;
- background: var(--bg-page);
- transition:
- border-color 0.15s ease,
- background 0.15s ease;
+ flex-direction: column;
+ gap: 8px;
+ transition: all 0.2s ease;
}
-.pf-module-tile:has(input:checked) {
+
+/* Le fond bleu léger s'applique à TOUTE la carte si le module est actif */
+.pf-module-card-wrapper:has(input.js-mod-checkbox:checked) {
border-color: var(--primary);
background: var(--bg-soft);
}
+/* Ligne supérieure : Titre + Pills (Alignement horizontal + Wrap auto) */
+.pf-module-top-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 12px;
+}
+
+.pf-module-tile-compact {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ cursor: pointer;
+ user-select: none;
+ flex-shrink: 0;
+}
+
+.pf-tile-icon {
+ font-size: 1.3rem;
+ line-height: 1;
+}
+
+.pf-tile-label {
+ font-weight: 600;
+ font-size: 1rem;
+ color: var(--text-main);
+}
+
+/* --- Sous-Vues (Pills) --- */
+.pf-module-inline-pills {
+ display: none;
+ flex-wrap: wrap;
+ gap: 6px;
+ align-items: center;
+ margin-left: auto;
+}
+
+.pf-module-card-wrapper:has(input.js-mod-checkbox:checked)
+ .pf-module-inline-pills {
+ display: flex;
+ animation: fadeIn 0.2s ease-out;
+}
+
+.pf-sub-module-pill {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ padding: 4px 12px;
+ border-radius: 50px;
+ background: var(--bg-page);
+ border: 1px solid var(--border-strong);
+ transition: all 0.2s ease;
+ user-select: none;
+}
+
+.pf-sub-module-pill input[type="checkbox"] {
+ display: none;
+}
+
+.pf-sub-module-pill .pf-pill-text {
+ font-size: 0.75rem;
+ font-weight: 500;
+ color: var(--text-muted);
+ transition: color 0.2s ease;
+}
+
+.pf-sub-module-pill:hover {
+ border-color: var(--text-main);
+}
+
+/* État actif de la Pill */
+.pf-sub-module-pill:has(input:checked) {
+ background: var(--bg-panel);
+ border-color: var(--primary);
+ box-shadow: 0 1px 2px rgba(59, 130, 246, 0.15);
+}
+
+.pf-sub-module-pill:has(input:checked) .pf-pill-text {
+ color: var(--primary);
+ font-weight: 700;
+}
+
+/* Note informative en bas de carte */
+.pf-module-info-footer {
+ display: none;
+ font-size: 0.8rem;
+ color: var(--text-muted);
+ font-style: italic;
+ margin-top: 4px;
+ padding-top: 8px;
+ border-top: 1px dashed var(--border-strong);
+}
+
+.pf-module-card-wrapper:has(input.js-mod-checkbox:checked)
+ .pf-module-info-footer {
+ display: block;
+ animation: fadeIn 0.2s ease-out;
+}
+
.pf-checkbox-lg {
width: 18px;
height: 18px;
diff --git a/includes/auth.php b/includes/auth.php
index f3d56b0..dd26590 100644
--- a/includes/auth.php
+++ b/includes/auth.php
@@ -35,7 +35,7 @@ function require_login(?string $loginPage = '/login.php'): void
$stmt = $meta_pdo->prepare("
SELECT u.id, u.username, u.password_hash, u.display_name, u.family_id, u.is_admin, u.is_active, u.lang, u.remember_token,
- f.db_name, f.is_active as family_active, f.enabled_modules
+ f.db_name, f.is_active as family_active, f.enabled_modules, f.enabled_views
FROM users u
LEFT JOIN families f ON f.id = u.family_id
WHERE u.id = ?
@@ -57,6 +57,7 @@ function require_login(?string $loginPage = '/login.php'): void
$_SESSION['family_db'] = $user['db_name'];
$_SESSION['app_lang'] = $user['lang'] ?? 'fr';
$_SESSION['enabled_modules'] = json_decode($user['enabled_modules'] ?? '["calendar","budget","holidays","gifts","calendar_ios"]', true);
+ $_SESSION['enabled_views'] = json_decode($user['enabled_views'] ?? '["food_meals","food_liste"]', true);
}
}
}
diff --git a/includes/lang/ca.php b/includes/lang/ca.php
index 1e51c0b..ce0dfd5 100644
--- a/includes/lang/ca.php
+++ b/includes/lang/ca.php
@@ -105,6 +105,7 @@ return [
'mod_printvault_desc'=> 'Gestioneu els vostres fitxers d\'impressió 3D — STL, 3MF, GCode.',
'mod_planka_name' => 'Planka',
'mod_planka_desc' => 'Gestioneu els vostres projectes en mode Kanban amb Planka.',
+ 'budget_mandatory_note' => 'Les pestanyes de Seguiment i Resum són obligatòries i sempre actives.',
'cta_open' => 'Obrir',
'cta_explore' => 'Explorar',
@@ -1031,4 +1032,6 @@ return [
'menu_food' => 'Menjar',
'mod_food_desc' => 'Gestioneu els vostres menús setmanals i les llistes de la compra compartides.',
'cta_food' => 'Cuinar',
+ 'food_err_no_views' => "Cap vista activada per a aquest mòdul. Modifiqueu la vostra configuració d'espai.",
+ 'food_err_no_views_desc' => 'Heu desactivat totes les funcions d\'aquest mòdul. Aneu a la configuració per reactivar-les.',
];
\ No newline at end of file
diff --git a/includes/lang/en.php b/includes/lang/en.php
index bfa6593..f982b68 100644
--- a/includes/lang/en.php
+++ b/includes/lang/en.php
@@ -105,6 +105,7 @@ return [
'mod_printvault_desc'=> 'Manage your 3D print files — STL, 3MF, GCode with interactive viewer.',
'mod_planka_name' => 'Planka',
'mod_planka_desc' => 'Manage your projects in Kanban mode with Planka.',
+ 'budget_mandatory_note' => 'Les onglets Suivi et Récapitulatif sont indispensables et toujours actifs.',
'cta_open' => 'Open',
'cta_explore' => 'Explore',
@@ -542,6 +543,7 @@ return [
'bud_err_tech' => 'A technical error occurred.',
'bud_err_server' => 'Server error: ',
'bud_err_delete' => 'Error while deleting.',
+ 'budget_mandatory_note' => 'Tracking and Summary tabs are mandatory and always active.',
// Categories (Budget)
'cat_income' => 'Income',
@@ -1034,4 +1036,7 @@ return [
'menu_food' => 'Food',
'mod_food_desc' => 'Manage your weekly menus and shared shopping lists.',
'cta_food' => 'Cook up',
+ 'food_err_no_views' => "No view is enabled for this module. Please check your space settings.",
+ 'food_err_no_views_desc' => 'You have disabled all features for this module. Please visit your settings to re-enable them.',
+
];
\ No newline at end of file
diff --git a/includes/lang/fr.php b/includes/lang/fr.php
index d12abc2..4a5fb98 100644
--- a/includes/lang/fr.php
+++ b/includes/lang/fr.php
@@ -105,6 +105,7 @@ return [
'mod_printvault_desc'=> 'Gérez vos fichiers d\'impression 3D — STL, 3MF, GCode avec viewer interactif.',
'mod_planka_name' => 'Planka',
'mod_planka_desc' => 'Gérez vos projets en mode Kanban avec Planka.',
+ 'budget_mandatory_note' => 'Les onglets Suivi et Récapitulatif sont indispensables et toujours actifs.',
'cta_open' => 'Ouvrir',
'cta_explore' => 'Explorer',
@@ -1030,4 +1031,7 @@ return [
'menu_food' => 'Food',
'mod_food_desc' => 'Gérez vos menus de la semaine et vos listes de courses partagées.',
'cta_food' => 'Mijoter',
+ 'food_err_no_views' => "Aucune vue n'est activée pour ce module. Modifiez vos paramètres d'espace.",
+ 'food_err_no_views_desc' => 'Vous avez désactivé toutes les fonctionnalités de ce module. Rendez-vous dans vos paramètres pour les réactiver.',
+
];
\ No newline at end of file
diff --git a/login.php b/login.php
index a11def5..02c3964 100644
--- a/login.php
+++ b/login.php
@@ -25,8 +25,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$error = tr('error_missing_fields');
} else {
$stmt = $meta_pdo->prepare("
- SELECT u.id, u.username, u.password_hash, u.display_name, u.family_id, u.is_admin, u.is_active, u.lang,
- f.db_name, f.is_active as family_active, f.enabled_modules
+ SELECT u.id, u.username, u.password_hash, u.display_name, u.family_id, u.is_admin, u.is_active, u.lang, u.remember_token,
+ f.db_name, f.is_active as family_active, f.enabled_modules, f.enabled_views
FROM users u
LEFT JOIN families f ON f.id = u.family_id
WHERE u.username = ?
@@ -51,6 +51,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$_SESSION['family_db'] = $user['db_name'];
$_SESSION['app_lang'] = $user['lang'] ?? 'fr';
$_SESSION['enabled_modules'] = json_decode($user['enabled_modules'] ?? '["calendar","budget","holidays","gifts","calendar_ios"]', true);
+ $_SESSION['enabled_views'] = json_decode($user['enabled_views'] ?? '["food_meals","food_liste"]', true);
// --- NOUVEAU : Logique "Se souvenir de moi" ---
if (isset($_POST['remember_me']) && $_POST['remember_me'] === '1') {
diff --git a/migrate.php b/migrate.php
index d8fa4f1..37ebc8b 100644
--- a/migrate.php
+++ b/migrate.php
@@ -4,78 +4,28 @@ require __DIR__ . '/includes/auth.php';
require __DIR__ . '/includes/meta_db.php';
require_login();
+// Sécurité : Accès limité aux administrateurs
if (empty($_SESSION['user']['is_admin'])) {
die("Accès refusé. Droits administrateur requis.");
}
try {
- $stmt = $meta_pdo->query("SELECT db_name FROM families");
- $families = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ echo "Migration de la base Meta (househub_meta)
";
+ echo "Vérification de la structure de la table 'families'...
";
- foreach ($families as $fam) {
- $dbName = $fam['db_name'];
- echo "Migration de la base : $dbName ...
";
-
- // --- DÉBUT DU BLOC MODIFIÉ ---
- $envHost = getenv('DB_HOST');
- if ($envHost === 'househub-db') {
- $host = 'househub-db';
- $user = getenv('DB_USER') ?: 'househub';
- $pass = getenv('DB_PASS') ?: 'changeme';
- } else {
- $host = '127.0.0.1';
- $user = 'root';
- $pass = '';
- }
-
- $familyPdo = new PDO(
- "mysql:host=$host;dbname=$dbName;charset=utf8mb4",
- $user,
- $pass,
- [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
- );
- // --- FIN DU BLOC MODIFIÉ ---
-
- $sql = "
- CREATE TABLE IF NOT EXISTS pf_meals_plan (
- id INT AUTO_INCREMENT PRIMARY KEY,
- plan_date DATE NOT NULL,
- service ENUM('lunch', 'dinner') NOT NULL,
- person_id INT NOT NULL,
- meal_name VARCHAR(500) DEFAULT NULL,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- UNIQUE KEY uq_meal_plan (plan_date, service, person_id)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
- ";
-
- $familyPdo->exec($sql);
- echo "✅ Table pf_meals_plan créée pour $dbName.
";
+ // Vérification de l'existence de la colonne pour rendre le script idempotent
+ $checkMetaCol = $meta_pdo->query("SHOW COLUMNS FROM families LIKE 'enabled_views'");
+
+ if ($checkMetaCol->rowCount() == 0) {
+ // Ajout de la colonne JSON
+ $meta_pdo->exec("ALTER TABLE families ADD COLUMN enabled_views JSON DEFAULT NULL AFTER enabled_modules");
+ echo "✅ Succès : Colonne 'enabled_views' ajoutée à househub_meta.families.
";
+ } else {
+ echo "ℹ️ Info : La colonne 'enabled_views' est déjà présente. Aucune modification requise.
";
}
- echo "
🎉 Migration terminée avec succès.";
- // À ajouter dans migrate.php
-echo "Migration : Ajout des colonnes dynamiques au Budget...\n";
-$stmtFam = $meta_pdo->query("SELECT id, db_name FROM families");
-$families = $stmtFam->fetchAll(PDO::FETCH_ASSOC);
-
-foreach ($families as $fam) {
- $dbName = $fam['db_name'];
- try {
- $famPdo = new PDO("mysql:host=$meta_host;dbname=$dbName;charset=utf8mb4", $meta_user, $meta_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
-
- // Vérification si la colonne n'existe pas déjà
- $checkCol = $famPdo->query("SHOW COLUMNS FROM pf_budget_items LIKE 'is_dynamic'");
- if ($checkCol->rowCount() == 0) {
- $famPdo->exec("ALTER TABLE pf_budget_items
- ADD COLUMN is_dynamic TINYINT(1) DEFAULT 0 AFTER is_estimate,
- ADD COLUMN dynamic_code VARCHAR(50) DEFAULT NULL AFTER is_dynamic");
- echo "Famille {$fam['id']} ($dbName) : Colonnes is_dynamic et dynamic_code ajoutées.\n";
- }
- } catch (\PDOException $e) {
- echo "Erreur sur la famille {$fam['id']} ($dbName) : " . $e->getMessage() . "\n";
- }
-}
+ echo "🎉 Migration Meta terminée avec succès.";
} catch (Exception $e) {
- die("❌ Erreur lors de la migration : " . $e->getMessage());
+ die("❌ Erreur lors de la migration : " . $e->getMessage());
}
\ No newline at end of file
diff --git a/settings.php b/settings.php
index 3a94d1a..dc69a29 100644
--- a/settings.php
+++ b/settings.php
@@ -41,14 +41,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'set_modules' && $family_id) {
- $all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'food', 'calendar_ios', 'printvault', 'planka'];
+ $all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'liste', 'calendar_ios', 'printvault', 'planka', 'food'];
+ $allViews = ['food_meals', 'food_liste', 'budget_prev', 'budget_epargne', 'budget_provisions'];
+
$enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m])));
+ $enabledViews = array_values(array_filter($allViews, fn($v) => isset($_POST['view_' . $v])));
+
if (empty($enabled)) {
$error = tr('set_err_min_module');
} else {
- $meta_pdo->prepare("UPDATE families SET enabled_modules = ? WHERE id = ?")
- ->execute([json_encode($enabled), $family_id]);
+ $meta_pdo->prepare("UPDATE families SET enabled_modules = ?, enabled_views = ? WHERE id = ?")
+ ->execute([json_encode($enabled), json_encode($enabledViews), $family_id]);
$_SESSION['enabled_modules'] = $enabled;
+ $_SESSION['enabled_views'] = $enabledViews;
$success = tr('set_success_modules');
}
}
@@ -396,15 +401,34 @@ require __DIR__ . '/header.php';
= tr('set_desc_modules') ?>
['icon' => '📅', 'label' => tr('menu_calendar')],
- 'budget' => ['icon' => '💰', 'label' => tr('menu_budget')],
+ 'budget' => [
+ 'icon' => '💰',
+ 'label' => tr('menu_budget'),
+ 'mandatory_note' => tr('budget_mandatory_note'),
+ 'views' => [
+ 'budget_prev' => tr('budget_tab_prev'),
+ 'budget_epargne' => tr('budget_tab_savings'),
+ 'budget_provisions' => tr('budget_tab_provisions')
+ ]
+ ],
'holidays' => ['icon' => '🏖️', 'label' => tr('menu_holidays')],
'gifts' => ['icon' => '🎁', 'label' => tr('menu_gifts')],
'garage' => ['icon' => '🚗', 'label' => tr('menu_garage')],
'memo' => ['icon' => '📝', 'label' => tr('menu_memo')],
'todo' => ['icon' => '✅', 'label' => tr('menu_todo')],
- 'food' => ['icon' => '🍳', 'label' => tr('menu_food')],
+ 'liste' => ['icon' => '🛒', 'label' => tr('menu_liste')],
+ 'food' => [
+ 'icon' => '🍳',
+ 'label' => tr('mod_food_name'),
+ 'views' => [
+ 'food_meals' => tr('food_tab_meals'),
+ 'food_liste' => tr('food_tab_list')
+ ]
+ ],
'calendar_ios' => ['icon' => '📱', 'label' => tr('menu_calendar_ios')],
'printvault' => ['icon' => '🖨️', 'label' => tr('menu_printvault')],
'planka' => ['icon' => '📋', 'label' => tr('menu_planka')],
@@ -415,11 +439,34 @@ require __DIR__ . '/header.php';
@@ -427,26 +474,6 @@ require __DIR__ . '/header.php';
-
-
- 📝 = htmlspecialchars(tr('liste_settings_title')) ?>
- = htmlspecialchars(tr('liste_settings_intro')) ?>
-
-
-
-
= tr('set_title_ios') ?>
= tr('set_desc_ios') ?>