enable view
Deploy HouseHub / deploy (push) Successful in 2s

This commit is contained in:
FeFeClochette committed 2026-09-17 10:49:49 +02:00
1 parent 693b277002
commit a7a8244a33
12 files changed
+268 -114

No files matched your search

+22 -3
View File
@@ -16,8 +16,23 @@ $hasBudgetItems = ((int)$pdo->query("SELECT COUNT(*) FROM pf_budget_items")->fet
// Le minimum vital pour que ça tourne // Le minimum vital pour que ça tourne
$isBudgetSetupOk = ($hasCategories && $hasSalaries && $hasAccounts); $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'; $tab = $_GET['tab'] ?? 'suivi';
if (!in_array($tab, $allowedTabs)) {
$tab = 'suivi';
}
$pageTitle = tr('budget_page_title'); $pageTitle = tr('budget_page_title');
$activePage = "budget"; $activePage = "budget";
@@ -30,7 +45,6 @@ require __DIR__ . '/header.php';
<div class="pf-hero budget-hero"> <div class="pf-hero budget-hero">
<div class="budget-title-group"> <div class="budget-title-group">
<h1><?= tr('budget_main_header') ?></h1> <h1><?= tr('budget_main_header') ?></h1>
<!-- Bouton avec séparation claire entre l'engrenage animable et l'alerte fixe -->
<button class="btn-header-settings" onclick="openBudgetSettings()" title="<?= tr('settings') ?>"> <button class="btn-header-settings" onclick="openBudgetSettings()" title="<?= tr('settings') ?>">
<span class="gear-icon-anim">⚙️</span> <span class="gear-icon-anim">⚙️</span>
<?php if (!$isBudgetSetupOk || !$hasBudgetItems): ?> <?php if (!$isBudgetSetupOk || !$hasBudgetItems): ?>
@@ -45,26 +59,31 @@ require __DIR__ . '/header.php';
<span><?= tr('budget_tab_tracking') ?></span> <span><?= tr('budget_tab_tracking') ?></span>
</a> </a>
<?php if (in_array('budget_prev', $allowedTabs)): ?>
<a href="?tab=budget_prev" class="tab-item <?= $tab == 'budget_prev' ? 'active' : '' ?>"> <a href="?tab=budget_prev" class="tab-item <?= $tab == 'budget_prev' ? 'active' : '' ?>">
<span class="tab-icon">🎯</span> <span class="tab-icon">🎯</span>
<span><?= tr('budget_tab_prev') ?></span> <span><?= tr('budget_tab_prev') ?></span>
</a> </a>
<?php endif; ?>
<?php if (in_array('epargne', $allowedTabs)): ?>
<a href="?tab=epargne" class="tab-item <?= $tab == 'epargne' ? 'active' : '' ?>"> <a href="?tab=epargne" class="tab-item <?= $tab == 'epargne' ? 'active' : '' ?>">
<span class="tab-icon">🐷</span> <span class="tab-icon">🐷</span>
<span><?= tr('budget_tab_savings') ?></span> <span><?= tr('budget_tab_savings') ?></span>
</a> </a>
<?php endif; ?>
<a href="?tab=recap" class="tab-item <?= $tab == 'recap' ? 'active' : '' ?>"> <a href="?tab=recap" class="tab-item <?= $tab == 'recap' ? 'active' : '' ?>">
<span class="tab-icon">📊</span> <span class="tab-icon">📊</span>
<span><?= tr('budget_tab_recap') ?></span> <span><?= tr('budget_tab_recap') ?></span>
</a> </a>
<?php if (in_array('provisions', $allowedTabs)): ?>
<a href="?tab=provisions" class="tab-item <?= $tab == 'provisions' ? 'active' : '' ?>"> <a href="?tab=provisions" class="tab-item <?= $tab == 'provisions' ? 'active' : '' ?>">
<span class="tab-icon">🧮</span> <span class="tab-icon">🧮</span>
<span><?= tr('budget_tab_provisions') ?></span> <span><?= tr('budget_tab_provisions') ?></span>
</a> </a>
<?php endif; ?>
</nav> </nav>
</div> </div>
+12
View File
@@ -208,3 +208,15 @@
[data-theme="dark"] .month-actions .btn-icon-small:hover { [data-theme="dark"] .month-actions .btn-icon-small:hover {
background: rgba(255, 255, 255, 0.06) !important; 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);
}
+1
View File
@@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS families (
invite_code VARCHAR(32) NOT NULL UNIQUE, invite_code VARCHAR(32) NOT NULL UNIQUE,
is_active TINYINT(1) DEFAULT 1, is_active TINYINT(1) DEFAULT 1,
enabled_modules JSON DEFAULT NULL, enabled_modules JSON DEFAULT NULL,
enabled_views JSON DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+38 -6
View File
@@ -5,8 +5,26 @@ require __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/i18n.php'; require_once __DIR__ . '/includes/i18n.php';
require_login('/login.php'); require_login('/login.php');
// Gestion de l'onglet actif // 1. Détection des vues actives
$tab = $_GET['tab'] ?? 'meals'; $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'); $pageTitle = tr('mod_food_name');
$activePage = "food"; $activePage = "food";
@@ -21,30 +39,44 @@ require __DIR__ . '/header.php';
<h1>🍳 <?= tr('mod_food_name') ?></h1> <h1>🍳 <?= tr('mod_food_name') ?></h1>
</div> </div>
<?php if ($tab !== 'empty'): ?>
<nav class="budget-tabs-container"> <nav class="budget-tabs-container">
<?php if (in_array('meals', $allowedTabs)): ?>
<a href="?tab=meals" class="tab-item <?= $tab == 'meals' ? 'active' : '' ?>"> <a href="?tab=meals" class="tab-item <?= $tab == 'meals' ? 'active' : '' ?>">
<span class="tab-icon">🍽️</span> <span class="tab-icon">🍽️</span>
<span><?= tr('food_tab_meals') ?></span> <span><?= tr('food_tab_meals') ?></span>
</a> </a>
<?php endif; ?>
<?php if (in_array('liste', $allowedTabs)): ?>
<a href="?tab=liste" class="tab-item <?= $tab == 'liste' ? 'active' : '' ?>"> <a href="?tab=liste" class="tab-item <?= $tab == 'liste' ? 'active' : '' ?>">
<span class="tab-icon">🛒</span> <span class="tab-icon">🛒</span>
<span><?= tr('food_tab_list') ?></span> <span><?= tr('food_tab_list') ?></span>
</a> </a>
<?php endif; ?>
</nav> </nav>
<?php endif; ?>
</div> </div>
<section class="pf-section"> <section class="pf-section">
<?php <?php
$allowedTabs = ['meals', 'liste']; if ($tab === 'empty') {
if (in_array($tab, $allowedTabs)) { // État Vide Intégré (Zéro vue activée)
echo "
<div class='pf-empty-state' style='text-align:center; padding: 60px 20px; max-width: 600px; margin: 0 auto; background: var(--bg-panel); border-radius: 16px; border: 1px dashed var(--border-strong); box-shadow: var(--shadow-sm);'>
<div style='font-size: 4rem; margin-bottom: 20px; opacity: 0.5;'>👀</div>
<h3 style='color: var(--text-main); margin-bottom: 10px; font-size: 1.3rem;'>" . tr('food_err_no_views') . "</h3>
<p style='color: var(--text-muted); margin-bottom: 30px; font-size: 0.95rem; line-height: 1.5;'>" . tr('food_err_no_views_desc') . "</p>
<a href='/settings.php' class='pf-btn'>⚙️ " . tr('settings') . "</a>
</div>";
} else {
// Routage classique
$viewPath = __DIR__ . "/modules/food/views/{$tab}.php"; $viewPath = __DIR__ . "/modules/food/views/{$tab}.php";
if (file_exists($viewPath)) { if (file_exists($viewPath)) {
require $viewPath; require $viewPath;
} else { } else {
echo "<p>Vue introuvable : " . htmlspecialchars($tab) . "</p>"; echo "<p>Vue introuvable : " . htmlspecialchars($tab) . "</p>";
} }
} else {
echo "<p>" . tr('error_occured') . "</p>";
} }
?> ?>
</section> </section>
+112 -13
View File
@@ -410,25 +410,124 @@ body.pf-home .pf-mobile-close-btn {
line-height: 1.4; line-height: 1.4;
} }
/* Tuile module (checkbox) — état coché via :has(), compatible thème */ /* --- MODULES & SOUS-VUES UNIFIÉS (Settings) --- */
.pf-module-tile { .pf-module-card-wrapper {
background: var(--bg-panel);
border: 1px solid var(--border-light);
border-radius: 12px;
padding: 12px 12px;
display: flex; display: flex;
align-items: center; flex-direction: column;
gap: 14px; gap: 8px;
padding: 14px 16px; transition: all 0.2s ease;
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;
} }
.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); border-color: var(--primary);
background: var(--bg-soft); 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 { .pf-checkbox-lg {
width: 18px; width: 18px;
height: 18px; height: 18px;
+2 -1
View File
@@ -35,7 +35,7 @@ function require_login(?string $loginPage = '/login.php'): void
$stmt = $meta_pdo->prepare(" $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, 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 FROM users u
LEFT JOIN families f ON f.id = u.family_id LEFT JOIN families f ON f.id = u.family_id
WHERE u.id = ? WHERE u.id = ?
@@ -57,6 +57,7 @@ function require_login(?string $loginPage = '/login.php'): void
$_SESSION['family_db'] = $user['db_name']; $_SESSION['family_db'] = $user['db_name'];
$_SESSION['app_lang'] = $user['lang'] ?? 'fr'; $_SESSION['app_lang'] = $user['lang'] ?? 'fr';
$_SESSION['enabled_modules'] = json_decode($user['enabled_modules'] ?? '["calendar","budget","holidays","gifts","calendar_ios"]', true); $_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);
} }
} }
} }
+3
View File
@@ -105,6 +105,7 @@ return [
'mod_printvault_desc'=> 'Gestioneu els vostres fitxers d\'impressió 3D — STL, 3MF, GCode.', 'mod_printvault_desc'=> 'Gestioneu els vostres fitxers d\'impressió 3D — STL, 3MF, GCode.',
'mod_planka_name' => 'Planka', 'mod_planka_name' => 'Planka',
'mod_planka_desc' => 'Gestioneu els vostres projectes en mode Kanban amb 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_open' => 'Obrir',
'cta_explore' => 'Explorar', 'cta_explore' => 'Explorar',
@@ -1031,4 +1032,6 @@ return [
'menu_food' => 'Menjar', 'menu_food' => 'Menjar',
'mod_food_desc' => 'Gestioneu els vostres menús setmanals i les llistes de la compra compartides.', 'mod_food_desc' => 'Gestioneu els vostres menús setmanals i les llistes de la compra compartides.',
'cta_food' => 'Cuinar', '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.',
]; ];
+5
View File
@@ -105,6 +105,7 @@ return [
'mod_printvault_desc'=> 'Manage your 3D print files — STL, 3MF, GCode with interactive viewer.', 'mod_printvault_desc'=> 'Manage your 3D print files — STL, 3MF, GCode with interactive viewer.',
'mod_planka_name' => 'Planka', 'mod_planka_name' => 'Planka',
'mod_planka_desc' => 'Manage your projects in Kanban mode with 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_open' => 'Open',
'cta_explore' => 'Explore', 'cta_explore' => 'Explore',
@@ -542,6 +543,7 @@ return [
'bud_err_tech' => 'A technical error occurred.', 'bud_err_tech' => 'A technical error occurred.',
'bud_err_server' => 'Server error: ', 'bud_err_server' => 'Server error: ',
'bud_err_delete' => 'Error while deleting.', 'bud_err_delete' => 'Error while deleting.',
'budget_mandatory_note' => 'Tracking and Summary tabs are mandatory and always active.',
// Categories (Budget) // Categories (Budget)
'cat_income' => 'Income', 'cat_income' => 'Income',
@@ -1034,4 +1036,7 @@ return [
'menu_food' => 'Food', 'menu_food' => 'Food',
'mod_food_desc' => 'Manage your weekly menus and shared shopping lists.', 'mod_food_desc' => 'Manage your weekly menus and shared shopping lists.',
'cta_food' => 'Cook up', '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.',
]; ];
+4
View File
@@ -105,6 +105,7 @@ return [
'mod_printvault_desc'=> 'Gérez vos fichiers d\'impression 3D — STL, 3MF, GCode avec viewer interactif.', 'mod_printvault_desc'=> 'Gérez vos fichiers d\'impression 3D — STL, 3MF, GCode avec viewer interactif.',
'mod_planka_name' => 'Planka', 'mod_planka_name' => 'Planka',
'mod_planka_desc' => 'Gérez vos projets en mode Kanban avec 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_open' => 'Ouvrir',
'cta_explore' => 'Explorer', 'cta_explore' => 'Explorer',
@@ -1030,4 +1031,7 @@ return [
'menu_food' => 'Food', 'menu_food' => 'Food',
'mod_food_desc' => 'Gérez vos menus de la semaine et vos listes de courses partagées.', 'mod_food_desc' => 'Gérez vos menus de la semaine et vos listes de courses partagées.',
'cta_food' => 'Mijoter', '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.',
]; ];
+3 -2
View File
@@ -25,8 +25,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$error = tr('error_missing_fields'); $error = tr('error_missing_fields');
} else { } else {
$stmt = $meta_pdo->prepare(" $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, 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 FROM users u
LEFT JOIN families f ON f.id = u.family_id LEFT JOIN families f ON f.id = u.family_id
WHERE u.username = ? WHERE u.username = ?
@@ -51,6 +51,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$_SESSION['family_db'] = $user['db_name']; $_SESSION['family_db'] = $user['db_name'];
$_SESSION['app_lang'] = $user['lang'] ?? 'fr'; $_SESSION['app_lang'] = $user['lang'] ?? 'fr';
$_SESSION['enabled_modules'] = json_decode($user['enabled_modules'] ?? '["calendar","budget","holidays","gifts","calendar_ios"]', true); $_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" --- // --- NOUVEAU : Logique "Se souvenir de moi" ---
if (isset($_POST['remember_me']) && $_POST['remember_me'] === '1') { if (isset($_POST['remember_me']) && $_POST['remember_me'] === '1') {
+12 -62
View File
@@ -4,78 +4,28 @@ require __DIR__ . '/includes/auth.php';
require __DIR__ . '/includes/meta_db.php'; require __DIR__ . '/includes/meta_db.php';
require_login(); require_login();
// Sécurité : Accès limité aux administrateurs
if (empty($_SESSION['user']['is_admin'])) { if (empty($_SESSION['user']['is_admin'])) {
die("Accès refusé. Droits administrateur requis."); die("Accès refusé. Droits administrateur requis.");
} }
try { try {
$stmt = $meta_pdo->query("SELECT db_name FROM families"); echo "<h1>Migration de la base Meta (househub_meta)</h1>";
$families = $stmt->fetchAll(PDO::FETCH_ASSOC); echo "Vérification de la structure de la table 'families'...<br>";
foreach ($families as $fam) { // Vérification de l'existence de la colonne pour rendre le script idempotent
$dbName = $fam['db_name']; $checkMetaCol = $meta_pdo->query("SHOW COLUMNS FROM families LIKE 'enabled_views'");
echo "Migration de la base : $dbName ... <br>";
// --- DÉBUT DU BLOC MODIFIÉ --- if ($checkMetaCol->rowCount() == 0) {
$envHost = getenv('DB_HOST'); // Ajout de la colonne JSON
if ($envHost === 'househub-db') { $meta_pdo->exec("ALTER TABLE families ADD COLUMN enabled_views JSON DEFAULT NULL AFTER enabled_modules");
$host = 'househub-db'; echo "✅ <strong>Succès :</strong> Colonne 'enabled_views' ajoutée à househub_meta.families.<br><br>";
$user = getenv('DB_USER') ?: 'househub';
$pass = getenv('DB_PASS') ?: 'changeme';
} else { } else {
$host = '127.0.0.1'; echo "ℹ️ <strong>Info :</strong> La colonne 'enabled_views' est déjà présente. Aucune modification requise.<br><br>";
$user = 'root';
$pass = '';
} }
$familyPdo = new PDO( echo "🎉 Migration Meta terminée avec succès.";
"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.<br>";
}
echo "<br>🎉 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";
}
}
} catch (Exception $e) { } catch (Exception $e) {
die("❌ Erreur lors de la migration : " . $e->getMessage()); die("❌ <strong>Erreur lors de la migration :</strong> " . $e->getMessage());
} }
+54 -27
View File
@@ -41,14 +41,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? ''; $action = $_POST['action'] ?? '';
if ($action === 'set_modules' && $family_id) { 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]))); $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)) { if (empty($enabled)) {
$error = tr('set_err_min_module'); $error = tr('set_err_min_module');
} else { } else {
$meta_pdo->prepare("UPDATE families SET enabled_modules = ? WHERE id = ?") $meta_pdo->prepare("UPDATE families SET enabled_modules = ?, enabled_views = ? WHERE id = ?")
->execute([json_encode($enabled), $family_id]); ->execute([json_encode($enabled), json_encode($enabledViews), $family_id]);
$_SESSION['enabled_modules'] = $enabled; $_SESSION['enabled_modules'] = $enabled;
$_SESSION['enabled_views'] = $enabledViews;
$success = tr('set_success_modules'); $success = tr('set_success_modules');
} }
} }
@@ -396,15 +401,34 @@ require __DIR__ . '/header.php';
<p class="pf-muted-note"><?= tr('set_desc_modules') ?></p> <p class="pf-muted-note"><?= tr('set_desc_modules') ?></p>
<?php <?php
$enabledMods = $_SESSION['enabled_modules'] ?? ['calendar','budget','holidays','gifts','calendar_ios']; $enabledMods = $_SESSION['enabled_modules'] ?? ['calendar','budget','holidays','gifts','calendar_ios'];
$enabledViews = $_SESSION['enabled_views'] ?? ['food_meals', 'food_liste', 'budget_prev', 'budget_epargne', 'budget_provisions'];
$allModules = [ $allModules = [
'calendar' => ['icon' => '📅', 'label' => tr('menu_calendar')], 'calendar' => ['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')], 'holidays' => ['icon' => '🏖️', 'label' => tr('menu_holidays')],
'gifts' => ['icon' => '🎁', 'label' => tr('menu_gifts')], 'gifts' => ['icon' => '🎁', 'label' => tr('menu_gifts')],
'garage' => ['icon' => '🚗', 'label' => tr('menu_garage')], 'garage' => ['icon' => '🚗', 'label' => tr('menu_garage')],
'memo' => ['icon' => '📝', 'label' => tr('menu_memo')], 'memo' => ['icon' => '📝', 'label' => tr('menu_memo')],
'todo' => ['icon' => '✅', 'label' => tr('menu_todo')], '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')], 'calendar_ios' => ['icon' => '📱', 'label' => tr('menu_calendar_ios')],
'printvault' => ['icon' => '🖨️', 'label' => tr('menu_printvault')], 'printvault' => ['icon' => '🖨️', 'label' => tr('menu_printvault')],
'planka' => ['icon' => '📋', 'label' => tr('menu_planka')], 'planka' => ['icon' => '📋', 'label' => tr('menu_planka')],
@@ -415,11 +439,34 @@ require __DIR__ . '/header.php';
<input type="hidden" name="action" value="set_modules"> <input type="hidden" name="action" value="set_modules">
<div class="pf-stack-md"> <div class="pf-stack-md">
<?php foreach ($allModules as $key => $mod): $active = in_array($key, $enabledMods); ?> <?php foreach ($allModules as $key => $mod): $active = in_array($key, $enabledMods); ?>
<label class="pf-module-tile"> <div class="pf-module-card-wrapper">
<input type="checkbox" name="mod_<?= $key ?>" <?= $active ? 'checked' : '' ?> class="pf-checkbox-lg">
<div class="pf-module-top-row">
<label class="pf-module-tile-compact">
<input type="checkbox" name="mod_<?= $key ?>" <?= $active ? 'checked' : '' ?> class="pf-checkbox-lg js-mod-checkbox">
<span class="pf-tile-icon"><?= $mod['icon'] ?></span> <span class="pf-tile-icon"><?= $mod['icon'] ?></span>
<span class="pf-tile-label"><?= $mod['label'] ?></span> <span class="pf-tile-label"><?= $mod['label'] ?></span>
</label> </label>
<?php if (!empty($mod['views'])): ?>
<div class="pf-module-inline-pills">
<?php foreach ($mod['views'] as $vKey => $vLabel): ?>
<label class="pf-sub-module-pill">
<input type="checkbox" name="view_<?= $vKey ?>" <?= in_array($vKey, $enabledViews) ? 'checked' : '' ?>>
<span class="pf-pill-text"><?= htmlspecialchars($vLabel) ?></span>
</label>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php if (!empty($mod['mandatory_note'])): ?>
<div class="pf-module-info-footer">
ℹ️ <?= $mod['mandatory_note'] ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button> <button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
@@ -427,26 +474,6 @@ require __DIR__ . '/header.php';
</section> </section>
<?php endif; ?> <?php endif; ?>
<?php
$enabledModsForFood = $_SESSION['enabled_modules'] ?? [];
if ($family_id && in_array('food', $enabledModsForFood, true)):
?>
<section class="pf-panel-card">
<h2 class="pf-card-h2 pf-card-h2--tight">📝 <?= htmlspecialchars(tr('liste_settings_title')) ?></h2>
<p class="pf-muted-note"><?= htmlspecialchars(tr('liste_settings_intro')) ?></p>
<form method="post" class="pf-stack-md pf-mt-md">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="action" value="grocery_history_max">
<div class="pf-form-group">
<label class="pf-label" for="history_max"><?= htmlspecialchars(tr('liste_settings_history_max')) ?></label>
<input type="number" name="history_max" id="history_max" class="pf-input pf-input-sm" min="1" max="50" step="1" value="<?= (int) $groceryHistoryMaxSetting ?>" required>
<p class="pf-muted-note pf-mt-xs"><?= htmlspecialchars(tr('liste_settings_history_hint')) ?></p>
</div>
<button type="submit" class="pf-btn"><?= htmlspecialchars(tr('btn_save')) ?></button>
</form>
</section>
<?php endif; ?>
<section class="pf-panel-card"> <section class="pf-panel-card">
<h2 class="pf-card-h2 pf-card-h2--tight"><?= tr('set_title_ios') ?></h2> <h2 class="pf-card-h2 pf-card-h2--tight"><?= tr('set_title_ios') ?></h2>
<p class="pf-muted-note"><?= tr('set_desc_ios') ?></p> <p class="pf-muted-note"><?= tr('set_desc_ios') ?></p>