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';

-
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('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';

['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';
$mod): $active = in_array($key, $enabledMods); ?> - +
+ +
+ + + +
+ $vLabel): ?> + + +
+ +
+ + + + + +
@@ -427,26 +474,6 @@ require __DIR__ . '/header.php'; - -
-

📝

-

-
- - -
- - -

-
- -
-
- -