diff --git a/docker/schema_family.sql b/docker/schema_family.sql index f28caa5..72e8c93 100644 --- a/docker/schema_family.sql +++ b/docker/schema_family.sql @@ -660,4 +660,15 @@ CREATE TABLE IF NOT EXISTS pf_planka_config ( PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file +SET FOREIGN_KEY_CHECKS = 1; + +-- ─── Planificateur de Repas ─────────────────────────────────────────────────── +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; \ No newline at end of file diff --git a/food.php b/food.php new file mode 100644 index 0000000..f0bc58c --- /dev/null +++ b/food.php @@ -0,0 +1,53 @@ + + +
+
+
+

🍳

+
+ + +
+ +
+ Vue introuvable : " . htmlspecialchars($tab) . "

"; + } + } else { + echo "

" . tr('error_occured') . "

"; + } + ?> +
+
+ + \ No newline at end of file diff --git a/global.css b/global.css index 04a43dd..00915d7 100644 --- a/global.css +++ b/global.css @@ -1808,3 +1808,80 @@ details[open].pf-accordion > .pf-accordion-summary::after { .pf-btn-pad-sm { padding: 8px 16px; } + +/* === COMPOSANT : NAVIGATION TABS (Pills) === */ +.budget-tabs-container { + display: inline-flex; + background: var(--bg-panel); + padding: 6px; + border-radius: 50px; + box-shadow: var(--shadow); + gap: 8px; + position: relative; + z-index: 10; +} +.tab-item { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 24px; + border-radius: 40px; + text-decoration: none; + color: var(--text-muted); + font-weight: 600; + font-size: 0.95rem; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border: 1px solid transparent; +} +.tab-item:hover { + background-color: var(--bg-page); + color: var(--text-main); +} +.tab-item.active { + background: var(--primary); + color: white; + box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); + transform: scale(1.02); +} +.tab-icon { + font-size: 1.1em; + line-height: 1; +} + +@media (max-width: 768px) { + .budget-tabs-container { + display: flex !important; + flex-wrap: nowrap !important; + overflow-x: auto !important; + justify-content: flex-start !important; + -webkit-overflow-scrolling: touch; + width: 100%; + padding-bottom: 5px; + scrollbar-width: none; + } + .budget-tabs-container::-webkit-scrollbar { + display: none; + } + .tab-item { + flex: 0 0 auto !important; + padding: 8px 16px !important; + } +} + +/* === COMPOSANT : EN-TÊTE DE MODULE CENTRÉ === */ +.budget-hero { + text-align: center; + margin-bottom: 30px; +} + +.budget-title-group { + display: flex; + justify-content: center; + align-items: center; + gap: 15px; + margin-bottom: 20px; +} + +.budget-title-group h1 { + margin: 0; +} diff --git a/header.php b/header.php index 4d2cd9b..61a21c4 100644 --- a/header.php +++ b/header.php @@ -55,7 +55,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr'; - + @@ -106,7 +106,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr'; 🚗 📝 ✅ - 🛒 + 🍳 📱 🖨️ 📋 diff --git a/includes/lang/ca.php b/includes/lang/ca.php index 39059fa..9a74794 100644 --- a/includes/lang/ca.php +++ b/includes/lang/ca.php @@ -967,4 +967,45 @@ return [ 'groceries_settings_saved' => 'Preferències desades.', 'groceries_settings_error' => 'Impossible de desar:', 'groceries_err_duplicate' => 'Aquest producte ja és a la llista.', + + // --- MODULE MEALS --- + 'menu_meals' => 'Menús', + 'mod_meals_name' => 'Planificació de Menús', + 'mod_meals_desc' => 'Planifica els àpats de la setmana per a tota la família.', + 'meals_board_title' => 'Planificació de Menús', + 'meals_copy_lunch' => 'Copiar tot el dinar', + 'meals_copy_dinner' => 'Copiar tot el sopar', + 'meal_lunch' => 'Migdia', + 'meal_dinner' => 'Sopar', + 'btn_apply_all' => 'Tots', + 'btn_copy_to' => 'Copiar cap a...', + 'modal_copy_title' => 'Copiar cap a...', + 'source_individual' => 'Plat individual', + 'source_row' => 'Àpat complet', + 'week_current' => 'Setmana actual', + 'week_next' => 'Setmana vinent', + 'meals_no_members' => 'Si us plau, configureu els membres de la vostra família a la configuració.', + 'day_full_mon' => 'Dilluns', 'day_full_tue' => 'Dimarts', 'day_full_wed' => 'Dimecres', + 'day_full_thu' => 'Dijous', 'day_full_fri' => 'Divendres', 'day_full_sat' => 'Dissabte', 'day_full_sun' => 'Diumenge', + 'meal_title' => 'Planificador de Menjars', + 'meal_edit_title' => 'Modificar el menjar', + 'meal_label_name' => 'Menú previst', + 'meal_week' => 'Setmana', + 'meal_ph_name' => 'Ex: Lasanya casolana', + 'meal_empty_slot' => 'Per definir...', + 'meal_current_week' => 'Setmana actual', + 'meal_saved' => 'Menjar guardat!', + 'meal_btn_import_pdf' => 'Import Menu PDF', + 'meal_import_title' => 'Import canteen menu', + 'meal_import_child' => 'For which child?', + 'meal_select_child' => 'Select child', + 'meal_pdf_file' => 'PDF File (Convivio)', + 'meal_drop_pdf' => 'Click to add PDF', + 'meal_import_success' => 'Menus imported successfully!', + 'mod_food_name' => 'Cuina i Compres', + 'food_tab_meals' => 'Menú de la setmana', + 'food_tab_list' => 'Llista de la compra', + 'menu_food' => 'Menjar', + 'mod_food_desc' => 'Gestioneu els vostres menús setmanals i les llistes de la compra compartides.', + 'cta_food' => 'Cuinar', ]; \ No newline at end of file diff --git a/includes/lang/en.php b/includes/lang/en.php index 0fb75f5..aed66fd 100644 --- a/includes/lang/en.php +++ b/includes/lang/en.php @@ -970,4 +970,45 @@ return [ 'groceries_settings_saved' => 'Preferences saved.', 'groceries_settings_error' => 'Could not save:', 'groceries_err_duplicate' => 'This product is already on the list.', + + // --- MODULE MEALS --- + 'menu_meals' => 'Meals', + 'mod_meals_name' => 'Meal Plan', + 'mod_meals_desc' => 'Plan weekly meals for the whole family.', + 'meals_board_title' => 'Meal Planner', + 'meals_copy_lunch' => 'Copy entire lunch', + 'meals_copy_dinner' => 'Copy entire dinner', + 'meal_lunch' => 'Lunch', + 'meal_dinner' => 'Dinner', + 'btn_apply_all' => 'All', + 'btn_copy_to' => 'Copy to...', + 'modal_copy_title' => 'Copy to...', + 'source_individual' => 'Individual meal', + 'source_row' => 'Full meal', + 'week_current' => 'Current week', + 'week_next' => 'Next week', + 'meals_no_members' => 'Please configure family members in the settings.', + 'day_full_mon' => 'Monday', 'day_full_tue' => 'Tuesday', 'day_full_wed' => 'Wednesday', + 'day_full_thu' => 'Thursday', 'day_full_fri' => 'Friday', 'day_full_sat' => 'Saturday', 'day_full_sun' => 'Sunday', + 'meal_title' => 'Meal Planner', + 'meal_edit_title' => 'Edit meal', + 'meal_label_name' => 'Planned menu', + 'meal_ph_name' => 'E.g. Homemade lasagna', + 'meal_week' => 'Week', + 'meal_empty_slot' => 'To be defined...', + 'meal_current_week' => 'Current week', + 'meal_saved' => 'Meal saved!', + 'meal_btn_import_pdf' => 'Import Menu PDF', + 'meal_import_title' => 'Import canteen menu', + 'meal_import_child' => 'For which child?', + 'meal_select_child' => 'Select child', + 'meal_pdf_file' => 'PDF File (Convivio)', + 'meal_drop_pdf' => 'Click to add PDF', + 'meal_import_success' => 'Menus imported successfully!', + 'mod_food_name' => 'Kitchen & Groceries', + 'food_tab_meals' => 'Weekly Menu', + 'food_tab_list' => 'Shopping List', + 'menu_food' => 'Food', + 'mod_food_desc' => 'Manage your weekly menus and shared shopping lists.', + 'cta_food' => 'Cook up', ]; \ No newline at end of file diff --git a/includes/lang/fr.php b/includes/lang/fr.php index 1b1a9d5..83bf913 100644 --- a/includes/lang/fr.php +++ b/includes/lang/fr.php @@ -966,4 +966,45 @@ return [ 'groceries_settings_saved' => 'Préférences enregistrées.', 'groceries_settings_error' => 'Impossible d\'enregistrer :', 'groceries_err_duplicate' => 'Ce produit est déjà dans la liste.', + + // --- MODULE MEALS --- + 'menu_meals' => 'Repas', + 'mod_meals_name' => 'Menus', + 'mod_meals_desc' => 'Planifiez les repas de la semaine pour toute la famille.', + 'meals_board_title' => 'Planning Repas', + 'meals_copy_lunch' => 'Copier le midi entier', + 'meals_copy_dinner' => 'Copier le soir entier', + 'meal_lunch' => 'Midi', + 'meal_dinner' => 'Soir', + 'btn_apply_all' => 'Tous', + 'btn_copy_to' => 'Copier vers...', + 'modal_copy_title' => 'Copier vers...', + 'source_individual' => 'Plat individuel', + 'source_row' => 'Repas complet', + 'week_current' => 'Semaine actuelle', + 'week_next' => 'Semaine prochaine', + 'meals_no_members' => 'Veuillez configurer les membres de votre famille dans les paramètres.', + 'day_full_mon' => 'Lundi', 'day_full_tue' => 'Mardi', 'day_full_wed' => 'Mercredi', + 'day_full_thu' => 'Jeudi', 'day_full_fri' => 'Vendredi', 'day_full_sat' => 'Samedi', 'day_full_sun' => 'Dimanche', + 'meal_title' => 'Planificateur de Repas', + 'meal_edit_title' => 'Modifier le repas', + 'meal_label_name' => 'Menu prévu', + 'meal_ph_name' => 'Ex: Lasagnes maison', + 'meal_week' => 'Semaine', + 'meal_empty_slot' => 'À définir...', + 'meal_current_week' => 'Semaine en cours', + 'meal_saved' => 'Repas enregistré !', + 'meal_btn_import_pdf' => 'Importer Menu PDF', + 'meal_import_title' => 'Importer un menu de cantine', + 'meal_import_child' => 'Pour quel enfant ?', + 'meal_select_child' => 'Sélectionner l\'enfant', + 'meal_pdf_file' => 'Fichier PDF (Convivio)', + 'meal_drop_pdf' => 'Cliquez pour ajouter le PDF', + 'meal_import_success' => 'Menus importés avec succès !', + 'mod_food_name' => 'Cuisine & Courses', + 'food_tab_meals' => 'Menu de la semaine', + 'food_tab_list' => 'Liste de courses', + 'menu_food' => 'Food', + 'mod_food_desc' => 'Gérez vos menus de la semaine et vos listes de courses partagées.', + 'cta_food' => 'Mijoter', ]; \ No newline at end of file diff --git a/index.php b/index.php index 6051351..6d71190 100644 --- a/index.php +++ b/index.php @@ -43,7 +43,7 @@ if ($_has_custom_bg): ?>

- +
@@ -91,6 +91,15 @@ if ($_has_custom_bg): ?> + + +
🍳
+

+
+ +
+ +
📝
@@ -109,14 +118,6 @@ if ($_has_custom_bg): ?>
- - -
🛒
-

-
- -
- diff --git a/liste.php b/liste.php deleted file mode 100644 index 2b2d936..0000000 --- a/liste.php +++ /dev/null @@ -1,62 +0,0 @@ - - - - -
- - -
-
- -
- - -
- -
- - -
- -
- - - -
- -
-
- -
- -
- - - - diff --git a/migrate.php b/migrate.php deleted file mode 100644 index 744aea8..0000000 --- a/migrate.php +++ /dev/null @@ -1,19 +0,0 @@ -exec("ALTER TABLE pf_leave_snapshots ADD PRIMARY KEY (id);"); - echo "Clé primaire ajoutée avec succès sur pf_leave_snapshots.
"; -} catch (PDOException $e) { - // Si la clé primaire existe déjà d'une façon ou d'une autre, on ignore l'erreur - echo "Info (Clé primaire) : " . $e->getMessage() . "
"; -} - -try { - // 2. Maintenant que la clé est garantie, on peut ajouter l'AUTO_INCREMENT en toute sécurité - $pdo->exec("ALTER TABLE pf_leave_snapshots MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT;"); - echo "AUTO_INCREMENT rétabli avec succès sur pf_leave_snapshots. 🚀
"; -} catch (PDOException $e) { - echo "Erreur (Auto-increment) : " . $e->getMessage() . "
"; -} \ No newline at end of file diff --git a/migrate_meals.php b/migrate_meals.php new file mode 100644 index 0000000..e4cace9 --- /dev/null +++ b/migrate_meals.php @@ -0,0 +1,58 @@ +query("SELECT db_name FROM families"); + $families = $stmt->fetchAll(PDO::FETCH_ASSOC); + + 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.
"; + } + echo "
🎉 Migration terminée avec succès."; + +} catch (Exception $e) { + die("❌ Erreur lors de la migration : " . $e->getMessage()); +} \ No newline at end of file diff --git a/mockup.html b/mockup.html new file mode 100644 index 0000000..ef8a13f --- /dev/null +++ b/mockup.html @@ -0,0 +1,626 @@ + + + + + + HouseHub - Planning Repas + + + +
+

+ 🍽️ + Planning Repas +

+ +
+ + +
+ + +
+ +
+ + + + + + + + + + + + + +
PAPAMAMANE1E2
+
+ + +
+
+ + + +
+ Semaine actuelle +
+
+ +
+ Semaine prochaine +
+
+
+
+ + + + diff --git a/modules/budget/budget.css b/modules/budget/budget.css index cc9c797..5cde7ed 100644 --- a/modules/budget/budget.css +++ b/modules/budget/budget.css @@ -48,43 +48,6 @@ } /* --- 2. NAVIGATION TABS (Pills) --- */ -.budget-tabs-container { - display: inline-flex; - background: var(--bg-panel); - padding: 6px; - border-radius: 50px; - box-shadow: var(--shadow); - gap: 8px; - position: relative; - z-index: 10; -} -.tab-item { - display: flex; - align-items: center; - gap: 8px; - padding: 10px 24px; - border-radius: 40px; - text-decoration: none; - color: var(--text-muted); - font-weight: 600; - font-size: 0.95rem; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - border: 1px solid transparent; -} -.tab-item:hover { - background-color: var(--bg-page); - color: var(--text-main); -} -.tab-item.active { - background: var(--primary); - color: white; - box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); - transform: scale(1.02); -} -.tab-icon { - font-size: 1.1em; - line-height: 1; -} .owner-tabs { display: inline-flex; @@ -1486,20 +1449,7 @@ body.sum-mode-active .sum-target { } /* Ajouts Structure Global Budget */ -.budget-hero { - text-align: center; - margin-bottom: 30px; -} -.budget-title-group { - display: flex; - justify-content: center; - align-items: center; - gap: 15px; - margin-bottom: 20px; -} -.budget-title-group h1 { - margin: 0; -} + .btn-settings-gear { position: relative; } @@ -1582,23 +1532,7 @@ body.sum-mode-active .sum-target { max-width: 100vw !important; box-sizing: border-box; } - .budget-tabs-container { - display: flex !important; - flex-wrap: nowrap !important; - overflow-x: auto !important; - justify-content: flex-start !important; - -webkit-overflow-scrolling: touch; - width: 100%; - padding-bottom: 5px; - scrollbar-width: none; - } - .budget-tabs-container::-webkit-scrollbar { - display: none; - } - .tab-item { - flex: 0 0 auto !important; - padding: 8px 16px !important; - } + .budget-view > div[style*="display:grid"] { gap: 10px !important; margin-bottom: 15px !important; diff --git a/modules/food/food.css b/modules/food/food.css new file mode 100644 index 0000000..b98d822 --- /dev/null +++ b/modules/food/food.css @@ -0,0 +1,1129 @@ +/* ── Module Liste ─────────────────────────────────────────────────────────── */ + +.liste-main { + max-width: 680px; + margin: 0 auto; + padding: 1rem 1rem 4rem; +} + +/* ── Tabs bar ──────────────────────────────────────────────────────────────── */ + +.liste-tabs-bar { + display: flex; + align-items: center; + gap: 0.4rem; + margin-bottom: 0.75rem; + overflow-x: auto; + padding-bottom: 2px; + scrollbar-width: none; +} +.liste-tabs-bar::-webkit-scrollbar { + display: none; +} + +.liste-tabs { + display: flex; + gap: 0.35rem; + flex-wrap: wrap; +} + +.liste-tab { + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0.4rem 0.75rem; + border-radius: 20px; + border: 1px solid var(--border, #dee2e6); + background: var(--card-bg, #fff); + cursor: pointer; + font-size: 0.875rem; + white-space: nowrap; + transition: + background 0.15s, + border-color 0.15s; + user-select: none; +} +.liste-tab:hover { + background: var(--hover-bg, #f8f9fa); +} +.liste-tab.active { + background: var(--primary, #4361ee); + color: #fff; + border-color: var(--primary, #4361ee); + font-weight: 600; +} + +.liste-tab-name { + pointer-events: none; +} + +.liste-tab-btn { + background: none; + border: none; + padding: 0 0.1rem; + cursor: pointer; + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.75); + line-height: 1; + transition: color 0.15s; +} +.liste-tab-btn:hover { + color: #fff; +} + +.liste-tab-add { + flex-shrink: 0; + width: 2rem; + height: 2rem; + border-radius: 50%; + border: 2px dashed var(--border, #dee2e6); + background: none; + cursor: pointer; + font-size: 1.1rem; + color: var(--muted, #6c757d); + display: flex; + align-items: center; + justify-content: center; + transition: + border-color 0.15s, + color 0.15s; + line-height: 1; +} +.liste-tab-add:hover { + border-color: var(--primary, #4361ee); + color: var(--primary, #4361ee); +} + +/* ── New list form ─────────────────────────────────────────────────────────── */ + +.liste-new-form { + display: flex; + gap: 0.5rem; + align-items: center; + margin-bottom: 0.75rem; + flex-wrap: wrap; +} +.liste-new-form input { + flex: 1; + min-width: 160px; + padding: 0.45rem 0.75rem; + border: 1px solid var(--primary, #4361ee); + border-radius: 8px; + font-size: 0.9rem; + background: var(--input-bg, #fff); + color: var(--text, #212529); +} +.liste-new-form button { + padding: 0.45rem 0.9rem; + border-radius: 8px; + border: none; + cursor: pointer; + font-size: 0.875rem; +} +.liste-new-form #btn-confirm-new-list { + background: var(--primary, #4361ee); + color: #fff; +} +.liste-new-form #btn-cancel-new-list { + background: var(--muted-bg, #e9ecef); + color: var(--text, #212529); +} + +.hidden { + display: none !important; +} + +/* ── Add card ──────────────────────────────────────────────────────────────── */ + +.liste-add-card { + display: flex; + gap: 0.5rem; + background: var(--card-bg, #fff); + border: 1px solid var(--border, #dee2e6); + border-radius: 12px; + padding: 0.6rem 0.75rem; + margin-bottom: 0.75rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} +.liste-add-card input { + flex: 1; + border: none; + outline: none; + font-size: 0.95rem; + background: transparent; + color: var(--text, #212529); +} +.liste-add-card button { + background: var(--primary, #4361ee); + color: #fff; + border: none; + border-radius: 8px; + padding: 0.4rem 0.9rem; + cursor: pointer; + font-size: 0.875rem; + font-weight: 600; + white-space: nowrap; +} + +/* ── Toolbar ───────────────────────────────────────────────────────────────── */ + +.liste-toolbar { + display: flex; + gap: 0.4rem; + flex-wrap: wrap; + margin-bottom: 0.75rem; +} +.btn-tool { + padding: 0.35rem 0.75rem; + border-radius: 8px; + border: 1px solid var(--border, #dee2e6); + background: var(--card-bg, #fff); + cursor: pointer; + font-size: 0.8rem; + color: var(--text, #212529); + transition: background 0.15s; +} +.btn-tool:hover { + background: var(--hover-bg, #f8f9fa); +} +.btn-tool-danger { + color: var(--danger, #e63946); + border-color: var(--danger, #e63946); +} +.btn-tool-danger:hover { + background: rgba(230, 57, 70, 0.08); +} + +/* ── Items ─────────────────────────────────────────────────────────────────── */ + +.liste-row { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.55rem 0.75rem; + background: var(--card-bg, #fff); + border: 1px solid var(--border, #dee2e6); + border-radius: 10px; + margin-bottom: 0.4rem; + transition: opacity 0.2s; +} +.liste-row.in-cart { + opacity: 0.55; +} +.liste-row.in-cart .liste-label { + text-decoration: line-through; +} + +.liste-check-label { + display: flex; + align-items: center; + gap: 0.6rem; + flex: 1; + cursor: pointer; +} +.liste-check { + width: 1.1rem; + height: 1.1rem; + accent-color: var(--primary, #4361ee); + cursor: pointer; + flex-shrink: 0; +} +.liste-label { + font-size: 0.95rem; +} + +.liste-row-actions { + display: flex; + gap: 0.25rem; + opacity: 0; + transition: opacity 0.15s; +} +.liste-row:hover .liste-row-actions, +.liste-row:focus-within .liste-row-actions { + opacity: 1; +} + +.btn-edit-item, +.btn-delete-item { + background: none; + border: none; + cursor: pointer; + padding: 0.2rem 0.3rem; + border-radius: 6px; + font-size: 0.9rem; + transition: background 0.15s; +} +.btn-edit-item:hover { + background: var(--hover-bg, #f0f0f0); +} +.btn-delete-item:hover { + background: rgba(230, 57, 70, 0.1); +} + +.liste-cart-divider { + font-size: 0.78rem; + color: var(--muted, #6c757d); + text-transform: uppercase; + letter-spacing: 0.04em; + padding: 0.5rem 0.25rem 0.25rem; + margin-top: 0.25rem; +} + +.liste-empty-items { + text-align: center; + padding: 2rem 1rem; + color: var(--muted, #6c757d); +} +.liste-empty-items span { + font-size: 2rem; + display: block; + margin-bottom: 0.5rem; +} + +/* ── History ───────────────────────────────────────────────────────────────── */ + +.liste-history-card { + margin-top: 1.25rem; + padding: 0.75rem; + background: var(--card-bg, #fff); + border: 1px solid var(--border, #dee2e6); + border-radius: 12px; +} +.liste-history-title { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--muted, #6c757d); + margin-bottom: 0.5rem; +} +.liste-history-chips { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} +.liste-chip { + padding: 0.25rem 0.65rem; + border-radius: 20px; + border: 1px solid var(--border, #dee2e6); + background: var(--muted-bg, #f8f9fa); + cursor: pointer; + font-size: 0.82rem; + color: var(--text, #212529); + transition: + background 0.15s, + border-color 0.15s; +} +.liste-chip:hover { + background: var(--primary-light, #eef0fd); + border-color: var(--primary, #4361ee); + color: var(--primary, #4361ee); +} + +/* ── Toast ─────────────────────────────────────────────────────────────────── */ + +.liste-toast-container { + position: fixed; + bottom: 1.5rem; + right: 1rem; + display: flex; + flex-direction: column; + gap: 0.4rem; + z-index: 9999; +} +.liste-toast { + padding: 0.55rem 1rem; + border-radius: 8px; + font-size: 0.875rem; + background: #333; + color: #fff; + box-shadow: 0 3px 12px rgba(0, 0, 0, 0.2); + opacity: 0; + transform: translateY(8px); + transition: + opacity 0.2s, + transform 0.2s; +} +.liste-toast.show { + opacity: 1; + transform: translateY(0); +} +.liste-toast-warn { + background: #f0a500; +} +.liste-toast-error { + background: #e63946; +} + +/* ── Empty state (no lists at all) ────────────────────────────────────────── */ + +.liste-empty-state { + text-align: center; + padding: 3rem 1rem; + color: var(--muted, #6c757d); +} +.liste-empty-icon { + font-size: 3rem; + margin-bottom: 0.75rem; +} + +/* ── List modal ────────────────────────────────────────────────────────────── */ + +.liste-modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 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, 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: 0.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 0.25rem; + transition: color 0.15s; +} +.liste-modal-close:hover { + color: var(--text, #212529); +} + +.liste-modal-body { + padding: 1.25rem; + display: flex; + flex-direction: column; + gap: 0.85rem; +} + +.liste-modal-footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 0.5rem; + padding: 0.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: 0.3rem; +} +.liste-form-label { + font-size: 0.73rem; + font-weight: 700; + color: var(--muted, #6c757d); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.liste-modal-name, +.liste-modal-type { + padding: 0.5rem 0.7rem; + border: 1px solid var(--border, #dee2e6); + border-radius: 8px; + background: var(--input-bg, #fff); + color: var(--text, #212529); + font-size: 0.875rem; + font-family: inherit; + width: 100%; + box-sizing: border-box; + transition: border-color 0.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, 0.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: 0.45rem; +} + +.liste-color-swatch { + width: 26px; + height: 26px; + border-radius: 50%; + border: 3px solid transparent; + background: var(--swatch, transparent); + cursor: pointer; + transition: transform 0.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: 0.85rem; + line-height: 1; +} + +/* ── Category section headers ──────────────────────────────────────────────── */ + +.liste-cat-section-header { + font-size: 0.75rem; + font-weight: 700; + color: var(--muted, #6c757d); + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 0.6rem 0.25rem 0.2rem; + display: flex; + align-items: center; + gap: 0.4rem; +} +.liste-cat-section-header:first-child { + padding-top: 0.1rem; +} + +.liste-cat-section-count { + background: var(--muted-bg, #e9ecef); + color: var(--muted, #6c757d); + border-radius: 999px; + font-size: 0.7rem; + padding: 0.05rem 0.4rem; + font-weight: 600; + letter-spacing: 0; + text-transform: none; +} + +/* ── Category badge (per-item) ─────────────────────────────────────────────── */ + +.liste-cat-badge { + flex-shrink: 0; + background: none; + border: 1px solid var(--border, #dee2e6); + border-radius: 6px; + padding: 0.1rem 0.3rem; + cursor: pointer; + font-size: 0.85rem; + line-height: 1.4; + color: inherit; + transition: + border-color 0.15s, + background 0.15s; +} +.liste-cat-badge:hover { + border-color: var(--primary, #4361ee); + background: var(--primary-light, #eef0fd); +} +.liste-cat-badge-empty { + opacity: 0.35; + border-style: dashed; +} +.liste-row:hover .liste-cat-badge-empty { + opacity: 0.7; +} + +/* ── Category picker popover ───────────────────────────────────────────────── */ + +.liste-cat-picker { + position: fixed; + z-index: 9999; + background: var(--card-bg, #fff); + border: 1px solid var(--border, #dee2e6); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); + width: 216px; + overflow: hidden; +} + +.liste-cat-picker-title { + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--muted, #6c757d); + padding: 0.6rem 0.75rem 0.35rem; + border-bottom: 1px solid var(--border, #dee2e6); +} + +.liste-cat-picker-list { + overflow-y: auto; + max-height: 300px; + padding: 0.3rem; +} + +.liste-cat-picker-item { + display: flex; + align-items: center; + gap: 0.4rem; + width: 100%; + text-align: left; + background: none; + border: none; + border-radius: 6px; + padding: 0.4rem 0.6rem; + font-size: 0.83rem; + color: var(--text, #212529); + cursor: pointer; + transition: background 0.1s; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.liste-cat-picker-item:hover { + background: var(--hover-bg, #f8f9fa); +} +.liste-cat-picker-item.active { + background: var(--primary-light, #eef0fd); + color: var(--primary, #4361ee); + font-weight: 600; +} + +/* ── Dark mode ─────────────────────────────────────────────────────────────── */ + +[data-theme="dark"] .liste-tab { + background: #1e1e2e; + border-color: #3a3a4e; + color: #cdd6f4; +} +[data-theme="dark"] .liste-tab:hover { + background: #2a2a3e; +} +[data-theme="dark"] .liste-tab.active { + background: #4361ee; + color: #fff; + border-color: #4361ee; +} +[data-theme="dark"] .liste-add-card, +[data-theme="dark"] .liste-row, +[data-theme="dark"] .liste-history-card { + background: #1e1e2e; + border-color: #3a3a4e; +} +[data-theme="dark"] .liste-add-card input { + color: #cdd6f4; +} +[data-theme="dark"] .liste-chip { + background: #2a2a3e; + border-color: #3a3a4e; + color: #cdd6f4; +} +[data-theme="dark"] .liste-chip:hover { + background: #313155; + border-color: #4361ee; + color: #89b4fa; +} +[data-theme="dark"] .btn-tool { + background: #1e1e2e; + border-color: #3a3a4e; + color: #cdd6f4; +} +[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; +} +[data-theme="dark"] .liste-cat-picker { + background: #1e1e2e; + border-color: #3a3a4e; +} +[data-theme="dark"] .liste-cat-picker-title { + border-color: #3a3a4e; +} +[data-theme="dark"] .liste-cat-picker-item { + color: #cdd6f4; +} +[data-theme="dark"] .liste-cat-picker-item:hover { + background: #2a2a3e; +} +[data-theme="dark"] .liste-cat-picker-item.active { + background: #313155; + color: #89b4fa; +} + +/* modules/meals/meals.css */ +.meals-container { + padding: 10px 0 80px 0; +} +.meals-header-bar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} +.meals-title { + margin: 0; + font-size: 1.4rem; + color: var(--text-main); + display: flex; + align-items: center; + gap: 8px; +} +.meals-title-icon { + opacity: 0.5; +} + +.meals-wrapper { + background: transparent; + border: none; + box-shadow: none; + overflow: visible; +} + +.meals-week-title { + font-size: 1.1rem; + color: var(--primary); + margin: 0 0 12px 0; + text-transform: uppercase; + font-weight: 800; +} + +.meals-table-container { + background: var(--bg-panel); + border-radius: 8px; + border: 1px solid var(--border-strong); + box-shadow: var(--shadow-sm); + overflow: hidden; + margin-bottom: 32px; +} +.meals-table { + width: 100%; + table-layout: fixed; + border-collapse: collapse; +} +.meals-table th, +.meals-table td { + border: 1px solid var(--border-light); + padding: 0; + text-align: center; + vertical-align: middle; +} +.meals-table th { + background: var(--bg-subtle); + font-size: 0.65rem; + font-weight: 800; + color: var(--text-muted); + padding: 8px 2px; + text-transform: uppercase; +} +.col-sticky { + width: 42px; + background: var(--bg-subtle); + border-right: 2px solid var(--border-strong) !important; + padding: 6px 2px !important; +} +.day-label { + font-size: 0.65rem; + font-weight: 800; + color: var(--text-main); + text-transform: uppercase; + display: block; + margin-bottom: 4px; +} +.day-label.invisible { + visibility: hidden; + height: 0; + margin: 0; +} +.service-icon { + background: white; + border: 1px solid var(--border-strong); + border-radius: 6px; + font-size: 1rem; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto; + cursor: pointer; + box-shadow: var(--shadow-sm); + transition: transform 0.1s; +} +.service-icon:active { + transform: scale(0.9); +} +.row-lunch td { + border-top: 2px solid var(--border-strong) !important; +} +.row-dinner td { + border-top: 1px dashed var(--border-light); +} + +.meal-input-cell { + position: relative; + height: 100%; + background: var(--bg-panel); +} +.meal-input { + width: 100%; + height: 48px; + border: none; + background: transparent; + text-align: center; + font-size: 0.75rem; + padding: 4px; + color: var(--text-main); + font-family: inherit; + resize: none; + box-sizing: border-box; + outline: none; +} +.meal-input:focus { + background: var(--bg-soft); +} + +.context-toolbar { + position: fixed; + bottom: -80px; + left: 0; + right: 0; + height: 60px; + background: var(--bg-panel); + border-top: 1px solid var(--border-strong); + box-shadow: var(--shadow-lg); + display: flex; + justify-content: center; + align-items: center; + gap: 12px; + transition: bottom 0.2s cubic-bezier(0.16, 1, 0.3, 1); + z-index: 1000; + padding: 0 10px; +} +.context-toolbar.active { + bottom: 0; +} +.ctx-btn { + background: var(--bg-subtle); + border: 1px solid var(--border-strong); + border-radius: 8px; + padding: 10px 16px; + font-size: 0.85rem; + font-weight: 700; + color: var(--text-main); + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; +} +.ctx-btn.primary { + background: var(--primary); + color: white; + border-color: var(--primary); +} + +.pf-bottom-sheet { + display: none; + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.6); + align-items: center; + z-index: 2000; + justify-content: center; +} +.pf-bottom-sheet.open { + display: flex; + animation: fadeInBg 0.2s; +} +.pf-bs-content { + background: var(--bg-page); + width: 95%; + max-width: 600px; + border-radius: 12px; + padding: 24px; + box-sizing: border-box; + animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1); + max-height: 90vh; + display: flex; + flex-direction: column; + box-shadow: var(--shadow-lg); +} +.bs-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + flex-shrink: 0; +} +.bs-title { + margin: 0; + font-size: 1.1rem; + color: var(--text-main); + font-weight: 800; + display: flex; + align-items: center; + gap: 8px; +} +.bs-close { + background: none; + border: none; + font-size: 1.8rem; + cursor: pointer; + color: var(--text-muted); + line-height: 1; + margin-top: -5px; +} +.meals-copy-source { + font-size: 0.85rem; + color: var(--text-muted); + font-weight: 600; + margin: 0 0 16px 0; + flex-shrink: 0; +} + +.week-section-title { + background: var(--bg-subtle); + text-align: center; + padding: 6px; + font-weight: 700; + font-size: 0.85rem; + color: var(--text-muted); + border-radius: 4px; + margin: 15px 0 10px 0; + text-transform: uppercase; + border: 1px solid var(--border-light); +} +.week-section-mt { + margin-top: 20px; +} + +.copy-days-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; + overflow-y: auto; +} +.copy-day-card { + background: var(--bg-panel); + border: 1px solid var(--border-light); + border-radius: 8px; + padding: 8px 4px; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + box-shadow: var(--shadow-sm); +} +.copy-day-name { + font-size: 0.8rem; + font-weight: 700; + color: var(--text-main); + text-transform: capitalize; +} +.copy-day-actions { + display: flex; + gap: 6px; + width: 100%; + justify-content: center; +} + +.btn-target-service { + background: var(--bg-page); + border: 1px solid var(--border-strong); + border-radius: 6px; + width: 30px; + height: 30px; + font-size: 1rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: 0.1s; +} +.btn-target-service:active { + transform: scale(0.9); + background: #e2e8f0; +} + +@media (max-width: 768px) { + .pf-bs-content { + padding: 16px; + } +} +@media (max-width: 380px) { + .copy-days-grid { + grid-template-columns: repeat(3, 1fr); + } +} +@keyframes slideUp { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} +@keyframes fadeInBg { + from { + background: transparent; + } + to { + background: rgba(15, 23, 42, 0.6); + } +} + +.meal-dropzone { + border: 2px dashed var(--border-strong); + border-radius: 12px; + padding: 30px; + text-align: center; + background: var(--bg-subtle); + color: var(--text-muted); + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.meal-dropzone:hover { + border-color: var(--primary); + background: var(--bg-soft); + color: var(--primary); +} + +.meals-actions { + display: flex; + gap: 12px; +} + +.meal-dropzone { + border: 2px dashed var(--border-strong); + border-radius: 12px; + padding: 30px; + text-align: center; + background: var(--bg-subtle); + color: var(--text-muted); + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.meal-dropzone:hover { + border-color: var(--primary); + background: var(--bg-soft); + color: var(--primary); +} + +.meal-dropzone-icon { + font-size: 2.5rem; + display: block; + margin-bottom: 12px; +} +.meals-actions { + display: flex; + gap: 12px; +} + +.meal-dropzone { + border: 2px dashed var(--border-strong); + border-radius: 12px; + padding: 30px; + text-align: center; + background: var(--bg-subtle); + color: var(--text-muted); + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.meal-dropzone:hover { + border-color: var(--primary); + background: var(--bg-soft); + color: var(--primary); +} + +.meal-dropzone-icon { + font-size: 2.5rem; + display: block; + margin-bottom: 12px; +} diff --git a/modules/liste/api.php b/modules/food/includes/api/api-liste.php similarity index 94% rename from modules/liste/api.php rename to modules/food/includes/api/api-liste.php index c82fd82..e2bbb26 100644 --- a/modules/liste/api.php +++ b/modules/food/includes/api/api-liste.php @@ -1,6 +1,7 @@ false, 'error' => $e->getMessage()]); exit; }); -require_once dirname(__DIR__, 2) . '/includes/db.php'; +require_once dirname(__DIR__, 4) . '/includes/db.php'; // ── Auto-create tables ──────────────────────────────────────────────────────── $pdo->exec("CREATE TABLE IF NOT EXISTS pf_lists ( @@ -96,11 +97,9 @@ function liste_ensure_default(PDO $pdo): int { return (int)$pdo->query("SELECT id FROM pf_lists ORDER BY position, id LIMIT 1")->fetchColumn(); } -// Built-in keyword → category position mapping (position maps to seeded order above) function liste_builtin_detect(string $normalized): ?int { static $map = null; if ($map === null) $map = [ - // 1 = Fruits & Légumes 'pomme'=>1,'poire'=>1,'banane'=>1,'orange'=>1,'citron'=>1,'raisin'=>1, 'fraise'=>1,'framboise'=>1,'myrtille'=>1,'cerise'=>1,'peche'=>1,'abricot'=>1, 'mangue'=>1,'kiwi'=>1,'ananas'=>1,'melon'=>1,'pasteque'=>1,'prune'=>1, @@ -113,120 +112,96 @@ function liste_builtin_detect(string $normalized): ?int { 'gingembre'=>1,'avocat'=>1,'endive'=>1,'ciboulette'=>1,'persil'=>1, 'basilic'=>1,'menthe'=>1,'thym'=>1,'romarin'=>1,'coriandre'=>1, 'girofle'=>1,'citron vert'=>1,'lime'=>1,'pamplemousse'=>1,'litchi'=>1, - // 2 = Viande 'boeuf'=>2,'veau'=>2,'poulet'=>2,'porc'=>2,'agneau'=>2,'dinde'=>2, 'lapin'=>2,'jambon'=>2,'lardon'=>2,'saucisse'=>2,'saucisson'=>2, 'merguez'=>2,'chipolata'=>2,'steak'=>2,'escalope'=>2,'roti'=>2, 'gigot'=>2,'filet'=>2,'cuisses'=>2,'viande hachee'=>2,'andouille'=>2, 'magret'=>2,'canard'=>2,'foie'=>2,'boudin'=>2,'paupiette'=>2, 'entrecote'=>2,'cote de boeuf'=>2,'onglet'=>2,'bavette'=>2, - // 3 = Poissonnerie 'saumon'=>3,'thon'=>3,'dorade'=>3,'bar'=>3,'cabillaud'=>3,'sole'=>3, 'truite'=>3,'lieu'=>3,'maquereau'=>3,'sardine'=>3,'anchois'=>3, 'crevette'=>3,'moule'=>3,'huitre'=>3,'coquille'=>3,'poulpe'=>3, 'seiche'=>3,'langoustine'=>3,'lotte'=>3,'merlu'=>3,'colin'=>3, 'tilapia'=>3,'daurade'=>3,'aiglefin'=>3,'homard'=>3,'crabe'=>3, - // 4 = Boulangerie 'pain'=>4,'baguette'=>4,'brioche'=>4,'croissant'=>4,'pain de mie'=>4, 'ficelle'=>4,'fougasse'=>4,'ciabatta'=>4,'biscotte'=>4,'naan'=>4, 'pita'=>4,'toast'=>4,'bagel'=>4,'pain burger'=>4,'wrap'=>4, - // 5 = Frais 'lait'=>5,'creme'=>5,'yaourt'=>5,'yourt'=>5,'beurre'=>5, 'fromage'=>5,'camembert'=>5,'brie'=>5,'comte'=>5,'gruyere'=>5, 'emmental'=>5,'mozzarella'=>5,'ricotta'=>5,'mascarpone'=>5, 'feta'=>5,'chevre'=>5,'oeuf'=>5,'oeufs'=>5,'creme fraiche'=>5, 'fromage blanc'=>5,'petit suisse'=>5,'kefir'=>5, - // 6 = Surgelés 'surgele'=>6,'pizza surgelee'=>6,'frite surgelee'=>6,'poisson pane'=>6, 'legume surgele'=>6,'glace'=>6,'sorbet'=>6,'plat surgele'=>6, 'nugget'=>6,'edamame'=>6,'petits pois'=>6,'epinards'=>6, - // 7 = Boissons 'eau'=>7,'coca'=>7,'cola'=>7,'limonade'=>7,'sirop'=>7, 'biere'=>7,'vin'=>7,'champagne'=>7,'cafe'=>7,'the'=>7, 'tisane'=>7,'soda'=>7,'jus'=>7,'kombucha'=>7,'cidre'=>7, 'whisky'=>7,'rhum'=>7,'vodka'=>7,'prosecco'=>7,'rose'=>7, - // 8 = Pâtes, Riz, Féculents 'pate'=>8,'spaghetti'=>8,'tagliatelle'=>8,'penne'=>8,'fusilli'=>8, 'macaroni'=>8,'riz'=>8,'couscous'=>8,'quinoa'=>8,'lentille'=>8, 'pois chiche'=>8,'farine'=>8,'semoule'=>8,'polenta'=>8,'boulgour'=>8, 'orge'=>8,'feculent'=>8,'vermicelle'=>8,'nouille'=>8,'gnocchi'=>8, - // 9 = Épicerie salée 'chips'=>9,'crackers'=>9,'olives'=>9,'cornichon'=>9,'capres'=>9, 'tapenade'=>9,'houmous'=>9,'tzatziki'=>9,'sel'=>9,'poivre'=>9, 'epice'=>9,'cube bouillon'=>9,'bouillon'=>9,'noix de cajou'=>9, 'amande'=>9,'pistache'=>9,'noix'=>9,'cacahuete'=>9, - // 10 = Conserves 'conserve'=>10,'boite de tomate'=>10,'concentre de tomate'=>10, 'mais'=>10,'ratatouille'=>10,'cassoulet'=>10,'pate de foie'=>10, 'sardine boite'=>10,'thon boite'=>10,'maquereau boite'=>10, 'soupe boite'=>10,'haricot boite'=>10, - // 11 = Plats cuisinés 'quiche'=>11,'lasagne'=>11,'gratin'=>11,'pizza'=>11,'tartiflette'=>11, 'croque'=>11,'hachis parmentier'=>11,'paella'=>11,'moussaka'=>11, 'tarte'=>11,'flamiche'=>11,'pissaladiere'=>11, - // 12 = Sauces & Condiments 'ketchup'=>12,'mayonnaise'=>12,'moutarde'=>12,'vinaigrette'=>12, 'sauce soja'=>12,'tabasco'=>12,'huile'=>12,'vinaigre'=>12, 'sauce tomate'=>12,'pesto'=>12,'sriracha'=>12,'worcester'=>12, 'nuoc mam'=>12,'sauce'=>12,'condiment'=>12,'worcestershire'=>12, - // 13 = Petit déjeuner 'cereale'=>13,'muesli'=>13,'granola'=>13,'corn flakes'=>13, 'miel'=>13,'confiture'=>13,'nutella'=>13,'beurre de cacahuete'=>13, 'sirop d erable'=>13,'sirop erable'=>13,'chocolat en poudre'=>13, - // 14 = Biscuits & Gâteaux 'biscuit'=>14,'gateau'=>14,'cookie'=>14,'madeleine'=>14, 'financier'=>14,'quatre quarts'=>14,'brownie'=>14,'sable'=>14, 'speculoos'=>14,'oreo'=>14,'lu'=>14,'petit beurre'=>14,'galette'=>14, 'palmier'=>14,'macaron'=>14,'eclair'=>14, - // 15 = Confiserie 'chocolat'=>15,'bonbon'=>15,'reglisse'=>15,'caramel'=>15, 'nougat'=>15,'marshmallow'=>15,'guimauve'=>15,'sucette'=>15, 'chewing gum'=>15,'pastille'=>15,'calisson'=>15, - // 16 = Dessert 'yaourt dessert'=>16,'creme dessert'=>16,'mousse au chocolat'=>16, 'tiramisu'=>16,'creme brulee'=>16,'flan'=>16,'crepe'=>16, - 'madeleine'=>16,'profiterole'=>16,'eclair'=>16, - // 17 = Beauté & Hygiène + 'profiterole'=>16, 'shampoing'=>17,'gel douche'=>17,'savon'=>17,'dentifrice'=>17, 'deodorant'=>17,'crème visage'=>17,'crème corps'=>17,'rasoir'=>17, 'mousse a raser'=>17,'coton'=>17,'lingette'=>17,'maquillage'=>17, 'parfum'=>17,'brosse a dents'=>17,'fil dentaire'=>17,'serum'=>17, 'hydratant'=>17,'demaquillant'=>17, - // 18 = Bébé 'couche'=>18,'biberon'=>18,'lait infantile'=>18,'compote bebe'=>18, 'pot bebe'=>18,'puree bebe'=>18,'lingette bebe'=>18,'savon bebe'=>18, 'creme bebe'=>18,'sucette bebe'=>18, - // 19 = Entretien 'lessive'=>19,'liquide vaisselle'=>19,'nettoyant'=>19,'degraissant'=>19, 'deboucheur'=>19,'anticalcaire'=>19,'eponge'=>19,'serpillere'=>19, 'sac poubelle'=>19,'papier toilette'=>19,'sopalin'=>19,'essuie tout'=>19, 'aluminium'=>19,'film plastique'=>19,'sac congelation'=>19, 'nettoyant wc'=>19,'desinfectant'=>19,'vitre'=>19,'javel'=>19, - // 20 = Animaux 'croquette'=>20,'patee'=>20,'litiere'=>20,'os'=>20, 'friandise animale'=>20,'nourriture chat'=>20,'nourriture chien'=>20, - // 21 = Maison & Jardin 'ampoule'=>21,'pile'=>21,'bougie'=>21,'allumette'=>21, 'ruban adhesif'=>21,'colle'=>21,'vis'=>21,'terreau'=>21, 'engrais'=>21,'pot de fleur'=>21,'arrosoir'=>21,'graine'=>21, 'tournevis'=>21,'marteau'=>21,'clou'=>21,'cle'=>21, - // 22 = Pharmacie 'doliprane'=>22,'ibuprofene'=>22,'paracetamol'=>22,'aspirine'=>22, 'bandage'=>22,'pansement'=>22,'thermometre'=>22,'serum physiologique'=>22, 'vitamine'=>22,'complement'=>22,'masque'=>22,'gant medical'=>22, - 'sirop'=>22,'antihistaminique'=>22,'antidouleur'=>22, + 'antihistaminique'=>22,'antidouleur'=>22, ]; - // Check full phrase if (isset($map[$normalized])) return $map[$normalized]; - // Check individual words $words = explode(' ', $normalized); foreach ($words as $w) { if (strlen($w) >= 3 && isset($map[$w])) return $map[$w]; } - // Partial match (contains) foreach ($map as $kw => $catPos) { if (str_contains($normalized, $kw)) return $catPos; } @@ -237,12 +212,10 @@ function liste_builtin_detect(string $normalized): ?int { function liste_detect_category(PDO $pdo, string $label): ?int { $norm = liste_normalize_detect($label); - // 1. Learned rules (exact normalized label) $r = $pdo->prepare("SELECT category_id FROM pf_item_category_rules WHERE keyword=? LIMIT 1"); $r->execute([$norm]); if ($row = $r->fetch()) return (int)$row['category_id']; - // 2. Learned rules (individual words) foreach (explode(' ', $norm) as $w) { if (strlen($w) < 3) continue; $r = $pdo->prepare("SELECT category_id FROM pf_item_category_rules WHERE keyword=? LIMIT 1"); @@ -250,11 +223,9 @@ function liste_detect_category(PDO $pdo, string $label): ?int { if ($row = $r->fetch()) return (int)$row['category_id']; } - // 3. Built-in dictionary → map position to real category id $pos = liste_builtin_detect($norm); if ($pos === null) return null; - // Get category id by position order $r = $pdo->query("SELECT id FROM pf_list_categories ORDER BY position, id"); $ids = $r->fetchAll(PDO::FETCH_COLUMN); return $ids[$pos - 1] ?? null; @@ -266,7 +237,6 @@ function liste_learn(PDO $pdo, string $label, int $category_id): void { $pdo->prepare("INSERT INTO pf_item_category_rules (keyword, category_id) VALUES (?,?) ON DUPLICATE KEY UPDATE category_id=VALUES(category_id)") ->execute([$norm, $category_id]); - // Also learn individual meaningful words foreach (explode(' ', $norm) as $w) { if (strlen($w) >= 4) { $pdo->prepare("INSERT IGNORE INTO pf_item_category_rules (keyword, category_id) VALUES (?,?)") @@ -301,7 +271,6 @@ if ($action === 'set_category' && $method === 'POST') { $pdo->prepare("UPDATE pf_grocery_items SET category_id=? WHERE id=?")->execute([$category_id, $item_id]); - // Learn from manual assignment if ($category_id !== null) { $row = $pdo->prepare("SELECT label FROM pf_grocery_items WHERE id=?"); $row->execute([$item_id]); @@ -431,11 +400,11 @@ if ($action === 'clear_all' && $method === 'POST') { // ── HISTORY ─────────────────────────────────────────────────────────────────── if ($action === 'history' && $method === 'GET') { $max = 20; - $n = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type='setting' AND reference_id='liste_history_max'"); $n->execute(); + $n = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type='grocery_settings' AND reference_id='history_max'"); $n->execute(); if ($r = $n->fetch()) $max = max(1, min(50, (int)$r['content'])); $s = $pdo->query("SELECT label_display FROM pf_grocery_history ORDER BY last_used_at DESC LIMIT $max"); echo json_encode(['history' => $s->fetchAll(PDO::FETCH_COLUMN)]); exit; } http_response_code(400); -echo json_encode(['error' => 'unknown action: ' . $action]); +echo json_encode(['error' => 'unknown action: ' . $action]); \ No newline at end of file diff --git a/modules/food/includes/api/get-week.php b/modules/food/includes/api/get-week.php new file mode 100644 index 0000000..b04cd11 --- /dev/null +++ b/modules/food/includes/api/get-week.php @@ -0,0 +1,73 @@ +format('N') != 1) { + $monday->modify('last monday'); + } + + $sunday = clone $monday; + $sunday->modify('+6 days'); + + $weekData = [ + 'week_label' => tr('meal_week') . ' ' . $monday->format('W'), + 'days' => [] + ]; + + // Extraction des repas de la base (person_id = 0 pour le foyer global) + $stmt = $pdo->prepare("SELECT plan_date, service, meal_name FROM pf_meals_plan WHERE plan_date BETWEEN ? AND ? AND person_id = 0"); + $stmt->execute([$monday->format('Y-m-d'), $sunday->format('Y-m-d')]); + + $meals = []; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $meals[$row['plan_date']][$row['service']] = $row['meal_name']; + } + + $current = clone $monday; + + // Mappage vers les clés i18n existantes de HouseHub + $daysMap = [ + 1 => 'day_mon', 2 => 'day_tue', 3 => 'day_wed', + 4 => 'day_thu', 5 => 'day_fri', 6 => 'day_sat', 7 => 'day_sun' + ]; + + for ($i = 0; $i < 7; $i++) { + $dStr = $current->format('Y-m-d'); + + $dayNum = $current->format('N'); // 1 à 7 + $monthNum = $current->format('m'); // 01 à 12 + $dayDate = $current->format('d'); // 01 à 31 + + $dayName = tr($daysMap[$dayNum]); + $monthName = tr('month_' . $monthNum); + + $weekData['days'][] = [ + 'date' => $dStr, + 'day_name' => $dayName, + 'date_formatted' => $dayDate . ' ' . $monthName, + 'lunch' => $meals[$dStr]['lunch'] ?? '', + 'dinner' => $meals[$dStr]['dinner'] ?? '' + ]; + $current->modify('+1 day'); + } + + echo json_encode(['success' => true, 'data' => $weekData], JSON_UNESCAPED_UNICODE); + +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} \ No newline at end of file diff --git a/modules/food/includes/api/import-pdf.php b/modules/food/includes/api/import-pdf.php new file mode 100644 index 0000000..552387e --- /dev/null +++ b/modules/food/includes/api/import-pdf.php @@ -0,0 +1,277 @@ + false, 'error' => 'Aucun fichier reçu.']); + exit; +} + +$personId = (int)$_POST['person_id']; +if ($personId <= 0) { + echo json_encode(['success' => false, 'error' => 'Enfant non spécifié.']); + exit; +} + +// --------------------------------------------------------- +// PARSEUR PDF SPATIAL NATIF (100% PHP SANS LIBRAIRIE) +// --------------------------------------------------------- +class NativePdfParser { + public static function getMealData($filename) { + $content = file_get_contents($filename); + $texts = []; + + // 1. Décompression et Tokenization Mathématique + if (preg_match_all('/stream(.*?)endstream/is', $content, $matches)) { + foreach ($matches[1] as $stream) { + $stream = ltrim($stream, "\r\n"); + $decoded = @gzuncompress($stream); + if (!$decoded) $decoded = $stream; + + // On éclate le flux par opérateurs PDF clés (Position et Texte) + $parts = preg_split('/(Tm|Td|Tj|TJ)/', $decoded, -1, PREG_SPLIT_DELIM_CAPTURE); + $x = 0; $y = 0; + + for ($i = 0; $i < count($parts); $i++) { + $token = $parts[$i]; + if ($token === 'Tm' || $token === 'Td') { + $prev = $parts[$i-1]; + preg_match_all('/[0-9.-]+/', $prev, $nums); + if (count($nums[0]) >= 2) { + $yRaw = array_pop($nums[0]); + $xRaw = array_pop($nums[0]); + if ($token === 'Tm') { + $x = (float)$xRaw; $y = (float)$yRaw; + } else { + $x += (float)$xRaw; $y += (float)$yRaw; + } + } + } elseif ($token === 'Tj') { + $prev = $parts[$i-1]; + if (preg_match('/\((.*?)\)$/', trim($prev), $m)) { + self::addText($texts, $m[1], $x, $y); + } + } elseif ($token === 'TJ') { + $prev = $parts[$i-1]; + if (preg_match('/\[(.*?)\]$/', trim($prev), $m)) { + preg_match_all('/\((.*?)\)/', $m[1], $tjs); + $tjX = $x; + foreach ($tjs[1] as $tj) { + self::addText($texts, $tj, $tjX, $y); + $tjX += 10; // Décalage estimé pour les mots suivants du tableau + } + } + } + } + } + } + + if (empty($texts)) throw new Exception("Impossible de lire le contenu du PDF. Le fichier est peut-être crypté ou corrompu."); + + // 2. Définition des Colonnes Verticles (X) via les En-têtes de Jour + $dayLabels = ['LUNDI', 'MARDI', 'MERCREDI', 'JEUDI', 'VENDREDI']; + $colXs = []; + foreach ($texts as $t) { + $clean = strtoupper(preg_replace('/[^A-Z]/', '', $t['text'])); + if (in_array($clean, $dayLabels)) { + $colXs[$clean] = $t['x']; + } + } + + // Sécurité si les jours exacts ne sont pas détectés à cause de la police + if (count($colXs) < 5) { + $allXs = array_column($texts, 'x'); + sort($allXs); + $minX = $allXs[0] ?? 0; + $maxX = end($allXs) ?? 500; + $step = ($maxX - $minX) / 5; + $colXs = [ + 'LUNDI' => $minX + $step * 0.5, + 'MARDI' => $minX + $step * 1.5, + 'MERCREDI' => $minX + $step * 2.5, + 'JEUDI' => $minX + $step * 3.5, + 'VENDREDI' => $minX + $step * 4.5, + ]; + } + $sortedXs = array_values($colXs); + sort($sortedXs); + + // 3. Définition des Tranches Horizontales (Y) via les Titres de Rangée (Zone Extrême Gauche) + $rowHeaders = []; + foreach ($texts as $t) { + $clean = strtoupper(preg_replace('/[^A-Z]/', '', $t['text'])); + if (in_array($clean, ['HORSDEUVRE', 'DECLINAISON', 'PLATCHAUD', 'GARNITURE', 'PRODUITLAITIER', 'DESSERT', 'LESGOUTERS'])) { + // On s'assure de ne capter que la colonne tout à gauche + if ($t['x'] < ($sortedXs[0] - 20)) { + $rowHeaders[] = ['label' => $clean, 'y' => $t['y']]; + } + } + } + + // Tri de haut en bas (Y du PDF est décroissant du haut vers le bas) + usort($rowHeaders, fn($a, $b) => $b['y'] <=> $a['y']); + + $yPlatTop = null; $yPlatBot = null; + $yGarTop = null; $yGarBot = null; + + for ($i = 0; $i < count($rowHeaders); $i++) { + if ($rowHeaders[$i]['label'] === 'PLATCHAUD') { + $yPlatTop = $rowHeaders[$i]['y']; + // La limite basse est le label suivant, ou -50 unités + $yPlatBot = $rowHeaders[$i+1]['y'] ?? ($yPlatTop - 50); + } + if ($rowHeaders[$i]['label'] === 'GARNITURE') { + $yGarTop = $rowHeaders[$i]['y']; + $yGarBot = $rowHeaders[$i+1]['y'] ?? ($yGarTop - 50); + } + } + + if (!$yPlatTop || !$yGarTop) { + throw new Exception("Structure du menu non reconnue (Mots-clés 'PLAT CHAUD' et 'GARNITURE' introuvables)."); + } + + // 4. Affectation Chirurgicale par Intersection Grille (X, Y) + $days = [[], [], [], [], []]; + foreach ($texts as $t) { + $cleanAlpha = strtoupper(preg_replace('/[^A-Z]/', '', $t['text'])); + + // Rejet des labels génériques et de la fameuse ligne "Déclinaison" + if (in_array($cleanAlpha, ['DECLINAISON', 'MENUVEGETARIEN', 'PLATCHAUD', 'GARNITURE', 'PRODUITLAITIER', 'HORSDEUVRE', 'LESGOUTERS', 'DESSERT'])) continue; + + // Retrait des labels BIO/HVE et mentions superflues + $textVal = trim(preg_replace('/\b(AB|HVE|MSC|Label Rouge)\b/i', '', $t['text'])); + $textVal = str_replace(['(plat', 'complet)'], '', $textVal); + $textVal = trim(preg_replace('/[^a-zA-ZÀ-ÿ0-9\s&\-]/u', '', $textVal)); + + if (strlen($textVal) < 3) continue; + + // Filtre Vertical : Est-on dans la tranche Plat Chaud ou Garniture ? (Tolérance +/- 5 unités) + $inPlat = ($t['y'] <= $yPlatTop + 5 && $t['y'] >= $yPlatBot + 5); + $inGar = ($t['y'] <= $yGarTop + 5 && $t['y'] >= $yGarBot + 5); + + if ($inPlat || $inGar) { + // Filtre Horizontal : Quel est le jour le plus proche ? + $bestCol = 0; + $minDiff = PHP_FLOAT_MAX; + foreach ($sortedXs as $idx => $cx) { + $diff = abs($t['x'] - $cx); + if ($diff < $minDiff) { + $minDiff = $diff; + $bestCol = $idx; + } + } + $days[$bestCol][] = ['text' => $textVal, 'y' => $t['y'], 'type' => $inPlat ? 'plat' : 'gar']; + } + } + + // 5. Concaténation Finale + $finalDays = []; + for ($i = 0; $i < 5; $i++) { + $plats = array_filter($days[$i], fn($item) => $item['type'] === 'plat'); + $gars = array_filter($days[$i], fn($item) => $item['type'] === 'gar'); + + usort($plats, fn($a, $b) => $b['y'] <=> $a['y']); + usort($gars, fn($a, $b) => $b['y'] <=> $a['y']); + + $platText = self::concatLines($plats); + $garText = self::concatLines($gars); + + $full = trim("$platText + $garText", " +"); + $finalDays[] = preg_replace('/\s+/', ' ', $full); + } + + return $finalDays; + } + + private static function concatLines($items) { + $lines = []; + $currentLine = ""; + $lastY = null; + foreach ($items as $item) { + // Si le décalage Y est trop fort, on considère que c'est une nouvelle ligne + if ($lastY !== null && abs($lastY - $item['y']) > 8) { + if ($currentLine) $lines[] = trim($currentLine); + $currentLine = $item['text']; + } else { + $currentLine .= " " . $item['text']; + } + $lastY = $item['y']; + } + if ($currentLine) $lines[] = trim($currentLine); + return implode(' ', array_filter($lines, fn($l) => strlen(trim($l)) > 2)); + } + + private static function addText(&$texts, $rawText, $x, $y) { + // Décodage de l'octal \123 + $m = preg_replace_callback('/\\\\([0-7]{1,3})/', function($cb) { return chr(octdec($cb[1])); }, $rawText); + + // Détection de l'UTF-16BE (Le BOM \xFE\xFF indique que le texte est double encodé) + if (str_starts_with($m, "\xFE\xFF")) { + $m = mb_convert_encoding(substr($m, 2), 'UTF-8', 'UTF-16BE'); + } else { + // Fallback Latin-1 si pas de BOM + $m = mb_convert_encoding($m, 'UTF-8', 'ISO-8859-1'); + } + + // Suppression radicale des octets nuls responsables de l'espacement forcé (E s c a l o p e) + $m = str_replace("\0", "", $m); + $m = trim(preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\+&\'’\-]/u', '', $m)); + + if ($m !== '') { + $texts[] = ['text' => $m, 'x' => $x, 'y' => $y]; + } + } +} + +try { + $filename = $_FILES['pdf_file']['name']; + + // 1. Extraction robuste de la date depuis le nom de fichier (Menus-du-14-au-20-septembre-2026.pdf) + $mondayDate = null; + if (preg_match('/(\d{1,2})[\s-]+au[\s-]+\d{1,2}[\s-]+([a-zèéû]+)[\s-]+(\d{4})/i', $filename, $m)) { + $monthsMap = ['janvier'=>'01','fevrier'=>'02','février'=>'02','mars'=>'03','avril'=>'04','mai'=>'05','juin'=>'06','juillet'=>'07','aout'=>'08','août'=>'08','septembre'=>'09','octobre'=>'10','novembre'=>'11','decembre'=>'12','décembre'=>'12']; + $month = $monthsMap[strtolower($m[2])] ?? '09'; + $mondayDate = $m[3] . '-' . $month . '-' . str_pad($m[1], 2, '0', STR_PAD_LEFT); + } + + if (!$mondayDate) { + echo json_encode(['success' => false, 'error' => 'Veuillez nommer le fichier "Menus-du-14-au-20-septembre-2026.pdf".']); + exit; + } + + // 2. Lancement de l'Analyse Spatiale + $plats = NativePdfParser::getMealData($_FILES['pdf_file']['tmp_name']); + + if (empty($plats) || count($plats) !== 5) { + echo json_encode(['success' => false, 'error' => 'Erreur lors du découpage des 5 colonnes.']); + exit; + } + + // 3. Insertion en BDD (Lundi au Vendredi) + $pdo->beginTransaction(); + $stmt = $pdo->prepare(" + INSERT INTO pf_meals_plan (plan_date, service, person_id, meal_name) + VALUES (?, 'lunch', ?, ?) + ON DUPLICATE KEY UPDATE meal_name = VALUES(meal_name) + "); + + $currentDate = new DateTime($mondayDate); + for ($i = 0; $i < 5; $i++) { + $fullMeal = $plats[$i]; + if (!empty($fullMeal) && $fullMeal !== '+') { + $stmt->execute([$currentDate->format('Y-m-d'), $personId, $fullMeal]); + } + $currentDate->modify('+1 day'); + } + + $pdo->commit(); + echo json_encode(['success' => true]); + +} catch (Exception $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} \ No newline at end of file diff --git a/modules/food/includes/api/save-meals.php b/modules/food/includes/api/save-meals.php new file mode 100644 index 0000000..4210b85 --- /dev/null +++ b/modules/food/includes/api/save-meals.php @@ -0,0 +1,56 @@ + false, 'error' => 'Méthode non autorisée']); + exit; +} + +$action = $_POST['action'] ?? ''; + +try { + if ($action === 'save_cell') { + $date = $_POST['plan_date']; + $service = $_POST['service']; + $person_id = (int)$_POST['person_id']; + $meal_name = trim($_POST['meal_name']); + + if (empty($meal_name)) { + $stmt = $pdo->prepare("DELETE FROM pf_meals_plan WHERE plan_date=? AND service=? AND person_id=?"); + $stmt->execute([$date, $service, $person_id]); + } else { + $stmt = $pdo->prepare("INSERT INTO pf_meals_plan (plan_date, service, person_id, meal_name) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE meal_name = VALUES(meal_name)"); + $stmt->execute([$date, $service, $person_id, $meal_name]); + } + echo json_encode(['success' => true]); + exit; + } + + if ($action === 'save_bulk') { + $updates = json_decode($_POST['updates'], true); + if (is_array($updates)) { + $pdo->beginTransaction(); + $stmtDel = $pdo->prepare("DELETE FROM pf_meals_plan WHERE plan_date=? AND service=? AND person_id=?"); + $stmtIns = $pdo->prepare("INSERT INTO pf_meals_plan (plan_date, service, person_id, meal_name) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE meal_name = VALUES(meal_name)"); + + foreach ($updates as $u) { + if (empty(trim($u['meal_name']))) { + $stmtDel->execute([$u['plan_date'], $u['service'], $u['person_id']]); + } else { + $stmtIns->execute([$u['plan_date'], $u['service'], $u['person_id'], trim($u['meal_name'])]); + } + } + $pdo->commit(); + } + echo json_encode(['success' => true]); + exit; + } + +} catch (Exception $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} \ No newline at end of file diff --git a/modules/liste/assets/liste.js b/modules/food/views/liste.php similarity index 90% rename from modules/liste/assets/liste.js rename to modules/food/views/liste.php index 59640b3..39015cf 100644 --- a/modules/liste/assets/liste.js +++ b/modules/food/views/liste.php @@ -1,5 +1,53 @@ -/* Liste module JS */ -const API = '/modules/liste/api.php'; + + +
+ +
+
+ +
+ + +
+
+ + +
+ +
+ + + +
+ +
+
+
+
+ + \ No newline at end of file diff --git a/modules/food/views/meals.php b/modules/food/views/meals.php new file mode 100644 index 0000000..eb08210 --- /dev/null +++ b/modules/food/views/meals.php @@ -0,0 +1,357 @@ +query("SELECT id, name FROM pf_people WHERE role NOT IN ('helper', 'nounou', 'proche_adulte', 'proche_enfant', 'relative') AND is_active = 1 ORDER BY id ASC"); +$members = $stmt->fetchAll(PDO::FETCH_ASSOC); + +if (empty($members)) { + echo "
" . tr('meals_no_members') . "
"; + return; +} + +// Calcul des 14 jours (Lundi semaine courante -> Dimanche semaine prochaine) +$start_date = new DateTime(); +$start_date->modify('monday this week'); +$dates = []; +for ($i = 0; $i < 14; $i++) { + $d = clone $start_date; + $d->modify("+$i days"); + $dates[] = $d->format('Y-m-d'); +} + +$stmtMeals = $pdo->prepare("SELECT plan_date, service, person_id, meal_name FROM pf_meals_plan WHERE plan_date BETWEEN ? AND ?"); +$stmtMeals->execute([$dates[0], end($dates)]); +$mealsData = []; +while ($row = $stmtMeals->fetch(PDO::FETCH_ASSOC)) { + $mealsData[$row['plan_date']][$row['service']][$row['person_id']] = $row['meal_name']; +} +?> + +
+
+

+ 🍽️ + +

+
+ + +
+
+ + +
+ + +
+ +
+ $weekDates): + $startWeekStr = date('d/m', strtotime($weekDates[0])); + $endWeekStr = date('d/m', strtotime(end($weekDates))); + ?> +

( - )

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
☀️
+
+ +
+ +
🌙
+
+ +
+
+ +
+
+ + +
+
+
+

🔄

+ +
+
Source : ...
+ +
+
+ +
+
+
+ + +
+
+ +
+ +
+
+ +
+
+
+ + +
+
+ +
+
+
+ + +
+
+
+

📄

+ +
+
+ + +
+ + +
+ +
+ +
+ 📥 + + +
+
+ + +
+
+
+ + \ No newline at end of file diff --git a/modules/liste/assets/liste.css b/modules/liste/assets/liste.css deleted file mode 100644 index 66479c6..0000000 --- a/modules/liste/assets/liste.css +++ /dev/null @@ -1,575 +0,0 @@ -/* ── Module Liste ─────────────────────────────────────────────────────────── */ - -.liste-main { - max-width: 680px; - margin: 0 auto; - padding: 1rem 1rem 4rem; -} - -/* ── Tabs bar ──────────────────────────────────────────────────────────────── */ - -.liste-tabs-bar { - display: flex; - align-items: center; - gap: .4rem; - margin-bottom: .75rem; - overflow-x: auto; - padding-bottom: 2px; - scrollbar-width: none; -} -.liste-tabs-bar::-webkit-scrollbar { display: none; } - -.liste-tabs { - display: flex; - gap: .35rem; - flex-wrap: wrap; -} - -.liste-tab { - display: flex; - align-items: center; - gap: .25rem; - padding: .4rem .75rem; - border-radius: 20px; - border: 1px solid var(--border, #dee2e6); - background: var(--card-bg, #fff); - cursor: pointer; - font-size: .875rem; - white-space: nowrap; - transition: background .15s, border-color .15s; - user-select: none; -} -.liste-tab:hover { background: var(--hover-bg, #f8f9fa); } -.liste-tab.active { - background: var(--primary, #4361ee); - color: #fff; - border-color: var(--primary, #4361ee); - font-weight: 600; -} - -.liste-tab-name { pointer-events: none; } - -.liste-tab-btn { - background: none; - border: none; - padding: 0 .1rem; - cursor: pointer; - font-size: .8rem; - color: rgba(255,255,255,.75); - line-height: 1; - transition: color .15s; -} -.liste-tab-btn:hover { color: #fff; } - -.liste-tab-add { - flex-shrink: 0; - width: 2rem; - height: 2rem; - border-radius: 50%; - border: 2px dashed var(--border, #dee2e6); - background: none; - cursor: pointer; - font-size: 1.1rem; - color: var(--muted, #6c757d); - display: flex; - align-items: center; - justify-content: center; - transition: border-color .15s, color .15s; - line-height: 1; -} -.liste-tab-add:hover { - border-color: var(--primary, #4361ee); - color: var(--primary, #4361ee); -} - -/* ── New list form ─────────────────────────────────────────────────────────── */ - -.liste-new-form { - display: flex; - gap: .5rem; - align-items: center; - margin-bottom: .75rem; - flex-wrap: wrap; -} -.liste-new-form input { - flex: 1; - min-width: 160px; - padding: .45rem .75rem; - border: 1px solid var(--primary, #4361ee); - border-radius: 8px; - font-size: .9rem; - background: var(--input-bg, #fff); - color: var(--text, #212529); -} -.liste-new-form button { - padding: .45rem .9rem; - border-radius: 8px; - border: none; - cursor: pointer; - font-size: .875rem; -} -.liste-new-form #btn-confirm-new-list { - background: var(--primary, #4361ee); - color: #fff; -} -.liste-new-form #btn-cancel-new-list { - background: var(--muted-bg, #e9ecef); - color: var(--text, #212529); -} - -.hidden { display: none !important; } - -/* ── Add card ──────────────────────────────────────────────────────────────── */ - -.liste-add-card { - display: flex; - gap: .5rem; - background: var(--card-bg, #fff); - border: 1px solid var(--border, #dee2e6); - border-radius: 12px; - padding: .6rem .75rem; - margin-bottom: .75rem; - box-shadow: 0 1px 3px rgba(0,0,0,.05); -} -.liste-add-card input { - flex: 1; - border: none; - outline: none; - font-size: .95rem; - background: transparent; - color: var(--text, #212529); -} -.liste-add-card button { - background: var(--primary, #4361ee); - color: #fff; - border: none; - border-radius: 8px; - padding: .4rem .9rem; - cursor: pointer; - font-size: .875rem; - font-weight: 600; - white-space: nowrap; -} - -/* ── Toolbar ───────────────────────────────────────────────────────────────── */ - -.liste-toolbar { - display: flex; - gap: .4rem; - flex-wrap: wrap; - margin-bottom: .75rem; -} -.btn-tool { - padding: .35rem .75rem; - border-radius: 8px; - border: 1px solid var(--border, #dee2e6); - background: var(--card-bg, #fff); - cursor: pointer; - font-size: .8rem; - color: var(--text, #212529); - transition: background .15s; -} -.btn-tool:hover { background: var(--hover-bg, #f8f9fa); } -.btn-tool-danger { color: var(--danger, #e63946); border-color: var(--danger, #e63946); } -.btn-tool-danger:hover { background: rgba(230,57,70,.08); } - -/* ── Items ─────────────────────────────────────────────────────────────────── */ - -.liste-row { - display: flex; - align-items: center; - gap: .5rem; - padding: .55rem .75rem; - background: var(--card-bg, #fff); - border: 1px solid var(--border, #dee2e6); - border-radius: 10px; - margin-bottom: .4rem; - transition: opacity .2s; -} -.liste-row.in-cart { opacity: .55; } -.liste-row.in-cart .liste-label { text-decoration: line-through; } - -.liste-check-label { - display: flex; - align-items: center; - gap: .6rem; - flex: 1; - cursor: pointer; -} -.liste-check { - width: 1.1rem; - height: 1.1rem; - accent-color: var(--primary, #4361ee); - cursor: pointer; - flex-shrink: 0; -} -.liste-label { font-size: .95rem; } - -.liste-row-actions { - display: flex; - gap: .25rem; - opacity: 0; - transition: opacity .15s; -} -.liste-row:hover .liste-row-actions, -.liste-row:focus-within .liste-row-actions { opacity: 1; } - -.btn-edit-item, .btn-delete-item { - background: none; - border: none; - cursor: pointer; - padding: .2rem .3rem; - border-radius: 6px; - font-size: .9rem; - transition: background .15s; -} -.btn-edit-item:hover { background: var(--hover-bg, #f0f0f0); } -.btn-delete-item:hover { background: rgba(230,57,70,.1); } - -.liste-cart-divider { - font-size: .78rem; - color: var(--muted, #6c757d); - text-transform: uppercase; - letter-spacing: .04em; - padding: .5rem .25rem .25rem; - margin-top: .25rem; -} - -.liste-empty-items { - text-align: center; - padding: 2rem 1rem; - color: var(--muted, #6c757d); -} -.liste-empty-items span { font-size: 2rem; display: block; margin-bottom: .5rem; } - -/* ── History ───────────────────────────────────────────────────────────────── */ - -.liste-history-card { - margin-top: 1.25rem; - padding: .75rem; - background: var(--card-bg, #fff); - border: 1px solid var(--border, #dee2e6); - border-radius: 12px; -} -.liste-history-title { - font-size: .75rem; - text-transform: uppercase; - letter-spacing: .05em; - color: var(--muted, #6c757d); - margin-bottom: .5rem; -} -.liste-history-chips { display: flex; flex-wrap: wrap; gap: .35rem; } -.liste-chip { - padding: .25rem .65rem; - border-radius: 20px; - border: 1px solid var(--border, #dee2e6); - background: var(--muted-bg, #f8f9fa); - cursor: pointer; - font-size: .82rem; - color: var(--text, #212529); - transition: background .15s, border-color .15s; -} -.liste-chip:hover { - background: var(--primary-light, #eef0fd); - border-color: var(--primary, #4361ee); - color: var(--primary, #4361ee); -} - -/* ── Toast ─────────────────────────────────────────────────────────────────── */ - -.liste-toast-container { - position: fixed; - bottom: 1.5rem; - right: 1rem; - display: flex; - flex-direction: column; - gap: .4rem; - z-index: 9999; -} -.liste-toast { - padding: .55rem 1rem; - border-radius: 8px; - font-size: .875rem; - background: #333; - color: #fff; - box-shadow: 0 3px 12px rgba(0,0,0,.2); - opacity: 0; - transform: translateY(8px); - transition: opacity .2s, transform .2s; -} -.liste-toast.show { opacity: 1; transform: translateY(0); } -.liste-toast-warn { background: #f0a500; } -.liste-toast-error { background: #e63946; } - -/* ── Empty state (no lists at all) ────────────────────────────────────────── */ - -.liste-empty-state { - text-align: center; - padding: 3rem 1rem; - color: var(--muted, #6c757d); -} -.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 { - font-size: .75rem; - font-weight: 700; - color: var(--muted, #6c757d); - text-transform: uppercase; - letter-spacing: .05em; - padding: .6rem .25rem .2rem; - display: flex; - align-items: center; - gap: .4rem; -} -.liste-cat-section-header:first-child { padding-top: .1rem; } - -.liste-cat-section-count { - background: var(--muted-bg, #e9ecef); - color: var(--muted, #6c757d); - border-radius: 999px; - font-size: .7rem; - padding: .05rem .4rem; - font-weight: 600; - letter-spacing: 0; - text-transform: none; -} - -/* ── Category badge (per-item) ─────────────────────────────────────────────── */ - -.liste-cat-badge { - flex-shrink: 0; - background: none; - border: 1px solid var(--border, #dee2e6); - border-radius: 6px; - padding: .1rem .3rem; - cursor: pointer; - font-size: .85rem; - line-height: 1.4; - color: inherit; - transition: border-color .15s, background .15s; -} -.liste-cat-badge:hover { - border-color: var(--primary, #4361ee); - background: var(--primary-light, #eef0fd); -} -.liste-cat-badge-empty { - opacity: .35; - border-style: dashed; -} -.liste-row:hover .liste-cat-badge-empty { opacity: .7; } - -/* ── Category picker popover ───────────────────────────────────────────────── */ - -.liste-cat-picker { - position: fixed; - z-index: 9999; - background: var(--card-bg, #fff); - border: 1px solid var(--border, #dee2e6); - border-radius: 12px; - box-shadow: 0 8px 32px rgba(0,0,0,.15); - width: 216px; - overflow: hidden; -} - -.liste-cat-picker-title { - font-size: .72rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: .05em; - color: var(--muted, #6c757d); - padding: .6rem .75rem .35rem; - border-bottom: 1px solid var(--border, #dee2e6); -} - -.liste-cat-picker-list { - overflow-y: auto; - max-height: 300px; - padding: .3rem; -} - -.liste-cat-picker-item { - display: flex; - align-items: center; - gap: .4rem; - width: 100%; - text-align: left; - background: none; - border: none; - border-radius: 6px; - padding: .4rem .6rem; - font-size: .83rem; - color: var(--text, #212529); - cursor: pointer; - transition: background .1s; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.liste-cat-picker-item:hover { background: var(--hover-bg, #f8f9fa); } -.liste-cat-picker-item.active { - background: var(--primary-light, #eef0fd); - color: var(--primary, #4361ee); - font-weight: 600; -} - -/* ── Dark mode ─────────────────────────────────────────────────────────────── */ - -[data-theme='dark'] .liste-tab { background: #1e1e2e; border-color: #3a3a4e; color: #cdd6f4; } -[data-theme='dark'] .liste-tab:hover { background: #2a2a3e; } -[data-theme='dark'] .liste-tab.active { background: #4361ee; color: #fff; border-color: #4361ee; } -[data-theme='dark'] .liste-add-card, -[data-theme='dark'] .liste-row, -[data-theme='dark'] .liste-history-card { background: #1e1e2e; border-color: #3a3a4e; } -[data-theme='dark'] .liste-add-card input { color: #cdd6f4; } -[data-theme='dark'] .liste-chip { background: #2a2a3e; border-color: #3a3a4e; color: #cdd6f4; } -[data-theme='dark'] .liste-chip:hover { background: #313155; border-color: #4361ee; color: #89b4fa; } -[data-theme='dark'] .btn-tool { background: #1e1e2e; border-color: #3a3a4e; color: #cdd6f4; } -[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; } -[data-theme='dark'] .liste-cat-picker { background: #1e1e2e; border-color: #3a3a4e; } -[data-theme='dark'] .liste-cat-picker-title { border-color: #3a3a4e; } -[data-theme='dark'] .liste-cat-picker-item { color: #cdd6f4; } -[data-theme='dark'] .liste-cat-picker-item:hover { background: #2a2a3e; } -[data-theme='dark'] .liste-cat-picker-item.active { background: #313155; color: #89b4fa; } diff --git a/settings.php b/settings.php index dcfaaff..3a94d1a 100644 --- a/settings.php +++ b/settings.php @@ -41,7 +41,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; if ($action === 'set_modules' && $family_id) { - $all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'liste', 'calendar_ios', 'printvault', 'planka']; + $all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'food', 'calendar_ios', 'printvault', 'planka']; $enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m]))); if (empty($enabled)) { $error = tr('set_err_min_module'); @@ -404,7 +404,7 @@ require __DIR__ . '/header.php'; 'garage' => ['icon' => '🚗', 'label' => tr('menu_garage')], 'memo' => ['icon' => '📝', 'label' => tr('menu_memo')], 'todo' => ['icon' => '✅', 'label' => tr('menu_todo')], - 'liste' => ['icon' => '📝', 'label' => tr('menu_liste')], + 'food' => ['icon' => '🍳', 'label' => tr('menu_food')], 'calendar_ios' => ['icon' => '📱', 'label' => tr('menu_calendar_ios')], 'printvault' => ['icon' => '🖨️', 'label' => tr('menu_printvault')], 'planka' => ['icon' => '📋', 'label' => tr('menu_planka')], @@ -428,8 +428,8 @@ require __DIR__ . '/header.php';

📝