This commit is contained in:
1 parent
3c615e3021
commit
4cfef07dc6
22 files changed
+2916
-778
No files matched your search
@@ -661,3 +661,14 @@ CREATE TABLE IF NOT EXISTS pf_planka_config (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
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;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
// food.php
|
||||||
|
require __DIR__ . '/includes/auth.php';
|
||||||
|
require __DIR__ . '/includes/db.php';
|
||||||
|
require_once __DIR__ . '/includes/i18n.php';
|
||||||
|
require_login('/login.php');
|
||||||
|
|
||||||
|
// Gestion de l'onglet actif
|
||||||
|
$tab = $_GET['tab'] ?? 'meals';
|
||||||
|
|
||||||
|
$pageTitle = tr('mod_food_name');
|
||||||
|
$activePage = "food";
|
||||||
|
$pageCss = "/modules/food/food.css";
|
||||||
|
|
||||||
|
require __DIR__ . '/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="pf-container">
|
||||||
|
<div class="pf-hero budget-hero">
|
||||||
|
<div class="budget-title-group">
|
||||||
|
<h1>🍳 <?= tr('mod_food_name') ?></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="budget-tabs-container">
|
||||||
|
<a href="?tab=meals" class="tab-item <?= $tab == 'meals' ? 'active' : '' ?>">
|
||||||
|
<span class="tab-icon">🍽️</span>
|
||||||
|
<span><?= tr('food_tab_meals') ?></span>
|
||||||
|
</a>
|
||||||
|
<a href="?tab=liste" class="tab-item <?= $tab == 'liste' ? 'active' : '' ?>">
|
||||||
|
<span class="tab-icon">🛒</span>
|
||||||
|
<span><?= tr('food_tab_list') ?></span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="pf-section">
|
||||||
|
<?php
|
||||||
|
$allowedTabs = ['meals', 'liste'];
|
||||||
|
if (in_array($tab, $allowedTabs)) {
|
||||||
|
$viewPath = __DIR__ . "/modules/food/views/{$tab}.php";
|
||||||
|
if (file_exists($viewPath)) {
|
||||||
|
require $viewPath;
|
||||||
|
} else {
|
||||||
|
echo "<p>Vue introuvable : " . htmlspecialchars($tab) . "</p>";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "<p>" . tr('error_occured') . "</p>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php require __DIR__ . '/footer.php'; ?>
|
||||||
+77
@@ -1808,3 +1808,80 @@ details[open].pf-accordion > .pf-accordion-summary::after {
|
|||||||
.pf-btn-pad-sm {
|
.pf-btn-pad-sm {
|
||||||
padding: 8px 16px;
|
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;
|
||||||
|
}
|
||||||
+2
-2
@@ -55,7 +55,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
|||||||
<?php if (in_array('garage', $mods)): ?><a href="/garage.php" class="pf-nav-link <?= $activePage === 'garage' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_garage') ?></a><?php endif; ?>
|
<?php if (in_array('garage', $mods)): ?><a href="/garage.php" class="pf-nav-link <?= $activePage === 'garage' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_garage') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('memo', $mods)): ?><a href="/memo.php" class="pf-nav-link <?= $activePage === 'memo' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_memo') ?></a><?php endif; ?>
|
<?php if (in_array('memo', $mods)): ?><a href="/memo.php" class="pf-nav-link <?= $activePage === 'memo' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_memo') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('todo', $mods)): ?><a href="/todo.php" class="pf-nav-link <?= $activePage === 'todo' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_todo') ?></a><?php endif; ?>
|
<?php if (in_array('todo', $mods)): ?><a href="/todo.php" class="pf-nav-link <?= $activePage === 'todo' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_todo') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('liste', $mods)): ?><a href="/liste.php" class="pf-nav-link <?= $activePage === 'liste' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_liste') ?></a><?php endif; ?>
|
<?php if (in_array('food', $mods)): ?><a href="/food.php" class="pf-nav-link <?= $activePage === 'food' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_food') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('calendar_ios', $mods)): ?><a href="/calendar-ios.php" class="pf-nav-link <?= $activePage === 'calendar-ios' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_calendar_ios') ?></a><?php endif; ?>
|
<?php if (in_array('calendar_ios', $mods)): ?><a href="/calendar-ios.php" class="pf-nav-link <?= $activePage === 'calendar-ios' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_calendar_ios') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('printvault', $mods)): ?><a href="/printvault.php" class="pf-nav-link <?= $activePage === 'printvault' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_printvault') ?></a><?php endif; ?>
|
<?php if (in_array('printvault', $mods)): ?><a href="/printvault.php" class="pf-nav-link <?= $activePage === 'printvault' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_printvault') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('planka', $mods)): ?><a href="/planka.php" class="pf-nav-link <?= $activePage === 'planka' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_planka') ?></a><?php endif; ?>
|
<?php if (in_array('planka', $mods)): ?><a href="/planka.php" class="pf-nav-link <?= $activePage === 'planka' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_planka') ?></a><?php endif; ?>
|
||||||
@@ -106,7 +106,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
|||||||
<?php if (in_array('garage', $mods)): ?><a href="/garage.php" class="pf-mobile-nav-link">🚗 <?= tr('menu_garage') ?></a><?php endif; ?>
|
<?php if (in_array('garage', $mods)): ?><a href="/garage.php" class="pf-mobile-nav-link">🚗 <?= tr('menu_garage') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('memo', $mods)): ?><a href="/memo.php" class="pf-mobile-nav-link">📝 <?= tr('menu_memo') ?></a><?php endif; ?>
|
<?php if (in_array('memo', $mods)): ?><a href="/memo.php" class="pf-mobile-nav-link">📝 <?= tr('menu_memo') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('todo', $mods)): ?><a href="/todo.php" class="pf-mobile-nav-link">✅ <?= tr('menu_todo') ?></a><?php endif; ?>
|
<?php if (in_array('todo', $mods)): ?><a href="/todo.php" class="pf-mobile-nav-link">✅ <?= tr('menu_todo') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('liste',$mods)): ?><a href="/liste.php" class="pf-mobile-nav-link">🛒 <?= tr('menu_liste') ?></a><?php endif; ?>
|
<?php if (in_array('food', $mods)): ?><a href="/food.php" class="pf-mobile-nav-link">🍳 <?= tr('menu_food') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('calendar_ios', $mods)): ?><a href="/calendar-ios.php" class="pf-mobile-nav-link">📱 <?= tr('menu_calendar_ios') ?></a><?php endif; ?>
|
<?php if (in_array('calendar_ios', $mods)): ?><a href="/calendar-ios.php" class="pf-mobile-nav-link">📱 <?= tr('menu_calendar_ios') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('printvault', $mods)): ?><a href="/printvault.php" class="pf-mobile-nav-link">🖨️ <?= tr('menu_printvault') ?></a><?php endif; ?>
|
<?php if (in_array('printvault', $mods)): ?><a href="/printvault.php" class="pf-mobile-nav-link">🖨️ <?= tr('menu_printvault') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('planka', $mods)): ?><a href="/planka.php" class="pf-mobile-nav-link">📋 <?= tr('menu_planka') ?></a><?php endif; ?>
|
<?php if (in_array('planka', $mods)): ?><a href="/planka.php" class="pf-mobile-nav-link">📋 <?= tr('menu_planka') ?></a><?php endif; ?>
|
||||||
|
|||||||
@@ -967,4 +967,45 @@ return [
|
|||||||
'groceries_settings_saved' => 'Preferències desades.',
|
'groceries_settings_saved' => 'Preferències desades.',
|
||||||
'groceries_settings_error' => 'Impossible de desar:',
|
'groceries_settings_error' => 'Impossible de desar:',
|
||||||
'groceries_err_duplicate' => 'Aquest producte ja és a la llista.',
|
'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',
|
||||||
];
|
];
|
||||||
@@ -970,4 +970,45 @@ return [
|
|||||||
'groceries_settings_saved' => 'Preferences saved.',
|
'groceries_settings_saved' => 'Preferences saved.',
|
||||||
'groceries_settings_error' => 'Could not save:',
|
'groceries_settings_error' => 'Could not save:',
|
||||||
'groceries_err_duplicate' => 'This product is already on the list.',
|
'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',
|
||||||
];
|
];
|
||||||
@@ -966,4 +966,45 @@ return [
|
|||||||
'groceries_settings_saved' => 'Préférences enregistrées.',
|
'groceries_settings_saved' => 'Préférences enregistrées.',
|
||||||
'groceries_settings_error' => 'Impossible d\'enregistrer :',
|
'groceries_settings_error' => 'Impossible d\'enregistrer :',
|
||||||
'groceries_err_duplicate' => 'Ce produit est déjà dans la liste.',
|
'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',
|
||||||
];
|
];
|
||||||
@@ -43,7 +43,7 @@ if ($_has_custom_bg): ?>
|
|||||||
<section class="pf-section">
|
<section class="pf-section">
|
||||||
<h2 style="color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.6);"><?= tr('home_modules_title') ?></h2>
|
<h2 style="color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.6);"><?= tr('home_modules_title') ?></h2>
|
||||||
|
|
||||||
<?php $mods = $_SESSION['enabled_modules'] ?? ['calendar','budget','holidays','gifts','garage','calendar_ios']; ?>
|
<?php $mods = $_SESSION['enabled_modules'] ?? ['calendar','budget','holidays','gifts','food','garage','calendar_ios']; ?>
|
||||||
<div class="pf-modules-grid">
|
<div class="pf-modules-grid">
|
||||||
|
|
||||||
<?php if (in_array('calendar', $mods)): ?>
|
<?php if (in_array('calendar', $mods)): ?>
|
||||||
@@ -91,6 +91,15 @@ if ($_has_custom_bg): ?>
|
|||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (in_array('food', $mods)): ?>
|
||||||
|
<a href="/food.php" class="pf-module-card">
|
||||||
|
<div class="pf-card-icon">🍳</div>
|
||||||
|
<h3 class="pf-card-title"><?= tr('mod_food_name') ?></h3>
|
||||||
|
<div class="pf-card-desc"><?= tr('mod_food_desc') ?></div>
|
||||||
|
<span class="pf-card-cta"><?= tr('cta_food') ?></span>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (in_array('memo', $mods)): ?>
|
<?php if (in_array('memo', $mods)): ?>
|
||||||
<a href="/memo.php" class="pf-module-card">
|
<a href="/memo.php" class="pf-module-card">
|
||||||
<div class="pf-card-icon">📝</div>
|
<div class="pf-card-icon">📝</div>
|
||||||
@@ -109,14 +118,6 @@ if ($_has_custom_bg): ?>
|
|||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (in_array('liste', $mods)): ?>
|
|
||||||
<a href="/liste.php" class="pf-module-card">
|
|
||||||
<div class="pf-card-icon">🛒</div>
|
|
||||||
<h3 class="pf-card-title"><?= tr('mod_liste_name') ?></h3>
|
|
||||||
<div class="pf-card-desc"><?= tr('mod_liste_desc') ?></div>
|
|
||||||
<span class="pf-card-cta"><?= tr('cta_cart') ?></span>
|
|
||||||
</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (in_array('calendar_ios', $mods)): ?>
|
<?php if (in_array('calendar_ios', $mods)): ?>
|
||||||
<a href="/calendar-ios.php" class="pf-module-card">
|
<a href="/calendar-ios.php" class="pf-module-card">
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
<?php
|
|
||||||
require __DIR__ . '/includes/auth.php';
|
|
||||||
require_login();
|
|
||||||
require_once __DIR__ . '/includes/i18n.php';
|
|
||||||
|
|
||||||
$pageTitle = tr('mod_liste_name') . ' — HouseHub';
|
|
||||||
$activePage = 'liste';
|
|
||||||
require __DIR__ . '/header.php';
|
|
||||||
?>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/modules/liste/assets/liste.css">
|
|
||||||
|
|
||||||
<main class="liste-main">
|
|
||||||
|
|
||||||
<!-- Tabs des listes -->
|
|
||||||
<div class="liste-tabs-bar">
|
|
||||||
<div class="liste-tabs" id="liste-tabs"></div>
|
|
||||||
<button class="liste-tab-add" id="btn-add-list" title="<?= htmlspecialchars(tr('liste_new_list')) ?>">+</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Corps de la liste active -->
|
|
||||||
<div class="liste-body" id="liste-body">
|
|
||||||
|
|
||||||
<div class="liste-add-card">
|
|
||||||
<input type="text" id="liste-input" maxlength="500"
|
|
||||||
placeholder="<?= htmlspecialchars(tr('liste_placeholder_add')) ?>" autocomplete="off">
|
|
||||||
<button id="btn-liste-add"><?= htmlspecialchars(tr('liste_btn_add')) ?></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="liste-toolbar" id="liste-toolbar">
|
|
||||||
<button class="btn-tool" id="btn-uncheck-all"><?= htmlspecialchars(tr('liste_btn_uncheck_all')) ?></button>
|
|
||||||
<button class="btn-tool btn-tool-danger" id="btn-delete-picked"><?= htmlspecialchars(tr('liste_btn_delete_picked')) ?></button>
|
|
||||||
<button class="btn-tool btn-tool-danger" id="btn-clear-all"><?= htmlspecialchars(tr('liste_btn_clear_all')) ?></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="liste-items-root"></div>
|
|
||||||
<div id="liste-history-root"></div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const LISTE_TRANSLATIONS = {
|
|
||||||
already_in: <?= json_encode(tr('liste_already_in')) ?>,
|
|
||||||
confirm_clear: <?= json_encode(tr('liste_confirm_clear')) ?>,
|
|
||||||
confirm_delete_list: <?= json_encode(tr('liste_confirm_delete_list')) ?>,
|
|
||||||
rename_list: <?= json_encode(tr('liste_rename_list')) ?>,
|
|
||||||
new_name: <?= json_encode(tr('liste_new_name')) ?>,
|
|
||||||
list_deleted: <?= json_encode(tr('liste_list_deleted')) ?>,
|
|
||||||
list_created: <?= json_encode(tr('liste_list_created')) ?>,
|
|
||||||
list_renamed: <?= json_encode(tr('liste_list_renamed')) ?>,
|
|
||||||
edit_placeholder: <?= json_encode(tr('liste_edit_placeholder')) ?>,
|
|
||||||
history_title: <?= json_encode(tr('liste_history_title')) ?>,
|
|
||||||
added: <?= json_encode(tr('liste_added')) ?>,
|
|
||||||
updated: <?= json_encode(tr('liste_updated')) ?>,
|
|
||||||
deleted: <?= json_encode(tr('liste_deleted')) ?>,
|
|
||||||
cancel: <?= json_encode(tr('cancel')) ?>,
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<script src="/modules/liste/assets/liste.js"></script>
|
|
||||||
<?php require __DIR__ . '/footer.php'; ?>
|
|
||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
require __DIR__ . '/includes/db.php';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. On s'assure que 'id' est bien défini comme clé primaire (corrige l'erreur 1075)
|
|
||||||
$pdo->exec("ALTER TABLE pf_leave_snapshots ADD PRIMARY KEY (id);");
|
|
||||||
echo "Clé primaire ajoutée avec succès sur pf_leave_snapshots.<br>";
|
|
||||||
} 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() . "<br>";
|
|
||||||
}
|
|
||||||
|
|
||||||
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. 🚀<br>";
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
echo "Erreur (Auto-increment) : " . $e->getMessage() . "<br>";
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
// migrate_meals.php
|
||||||
|
require __DIR__ . '/includes/auth.php';
|
||||||
|
require __DIR__ . '/includes/meta_db.php';
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
if (empty($_SESSION['user']['is_admin'])) {
|
||||||
|
die("Accès refusé. Droits administrateur requis.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $meta_pdo->query("SELECT db_name FROM families");
|
||||||
|
$families = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
foreach ($families as $fam) {
|
||||||
|
$dbName = $fam['db_name'];
|
||||||
|
echo "Migration de la base : $dbName ... <br>";
|
||||||
|
|
||||||
|
// --- 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.<br>";
|
||||||
|
}
|
||||||
|
echo "<br>🎉 Migration terminée avec succès.";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
die("❌ Erreur lors de la migration : " . $e->getMessage());
|
||||||
|
}
|
||||||
+626
@@ -0,0 +1,626 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
|
<title>HouseHub - Planning Repas</title>
|
||||||
|
<style>
|
||||||
|
/* --- VARIABLES --- */
|
||||||
|
:root {
|
||||||
|
--primary: #3b82f6;
|
||||||
|
--bg-page: #f8fafc;
|
||||||
|
--bg-panel: #ffffff;
|
||||||
|
--bg-subtle: #f1f5f9;
|
||||||
|
--text-main: #0f172a;
|
||||||
|
--text-muted: #64748b;
|
||||||
|
--border-light: #e2e8f0;
|
||||||
|
--border-strong: #cbd5e1;
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||||
|
--shadow-lg: 0 -4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
--alert-success-bg: #dcfce7;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family:
|
||||||
|
"Inter",
|
||||||
|
-apple-system,
|
||||||
|
sans-serif;
|
||||||
|
background: var(--bg-page);
|
||||||
|
color: var(--text-main);
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
overscroll-behavior-y: none;
|
||||||
|
padding-bottom: 70px; /* Espace pour la toolbar fixe */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- EN-TÊTE --- */
|
||||||
|
.header-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.header-bar h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.pf-btn {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
color: var(--text-main);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.pf-btn.primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- MATRICE ULTRA-COMPACTE --- */
|
||||||
|
.meals-wrapper {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.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.6rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 6px 2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-sticky {
|
||||||
|
width: 40px;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
border-right: 2px solid var(--border-strong) !important;
|
||||||
|
padding: 4px 2px !important;
|
||||||
|
}
|
||||||
|
.day-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text-main);
|
||||||
|
text-transform: uppercase;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.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: 26px;
|
||||||
|
height: 26px;
|
||||||
|
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: 44px;
|
||||||
|
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: #eff6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- BARRE CONTEXTUELLE (FIXE EN BAS) --- */
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- MODALE : COPIER VERS (DESIGN GRILLE) --- */
|
||||||
|
.pf-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(15, 23, 42, 0.6);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
.pf-modal.open {
|
||||||
|
display: flex;
|
||||||
|
animation: fadeInBg 0.2s;
|
||||||
|
}
|
||||||
|
.pf-modal-content {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
width: 95%;
|
||||||
|
max-width: 600px;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.modal-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
font-weight: 800;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.modal-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1;
|
||||||
|
margin-top: -5px;
|
||||||
|
}
|
||||||
|
.modal-source {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grille des Jours */
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.days-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
.day-card-title {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
.day-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.day-action-btn {
|
||||||
|
background: var(--bg-page);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 6px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.1s;
|
||||||
|
}
|
||||||
|
.day-action-btn:active {
|
||||||
|
transform: scale(0.9);
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 380px) {
|
||||||
|
.days-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fadeInBg {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header-bar">
|
||||||
|
<h1>
|
||||||
|
<span style="opacity: 0.5">🍽️</span>
|
||||||
|
<span data-i18n="meals_board_title">Planning Repas</span>
|
||||||
|
</h1>
|
||||||
|
<button class="pf-btn" title="Générer les courses">
|
||||||
|
🛒 <span data-i18n="btn_groceries">Courses</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BARRE CONTEXTUELLE FIXE (Évite le conflit avec le clavier mobile) -->
|
||||||
|
<div class="context-toolbar" id="contextToolbar">
|
||||||
|
<button
|
||||||
|
class="ctx-btn"
|
||||||
|
onmousedown="
|
||||||
|
event.preventDefault();
|
||||||
|
applyToAllInRow();
|
||||||
|
"
|
||||||
|
>
|
||||||
|
➡️ <span data-i18n="btn_apply_all">Tous</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ctx-btn primary"
|
||||||
|
onmousedown="
|
||||||
|
event.preventDefault();
|
||||||
|
openCopySheetFromCell();
|
||||||
|
"
|
||||||
|
>
|
||||||
|
🔄 <span data-i18n="btn_copy_to">Copier vers...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="meals-wrapper">
|
||||||
|
<table class="meals-table" id="mealsTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="col-sticky"></th>
|
||||||
|
<th>PAPA</th>
|
||||||
|
<th>MAMAN</th>
|
||||||
|
<th>E1</th>
|
||||||
|
<th>E2</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- Injecté via JS -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODALE DE COPIE (GRILLE LUNDI-DIMANCHE) -->
|
||||||
|
<div id="copyModal" class="pf-modal">
|
||||||
|
<div class="pf-modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title">
|
||||||
|
🔄 <span data-i18n="modal_copy_title">Copier vers...</span>
|
||||||
|
</h3>
|
||||||
|
<button class="modal-close" onclick="closeCopyModal()">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="copySourceLabel" class="modal-source">
|
||||||
|
Source : Plat individuel (Lundi Midi)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="week-section-title" data-i18n="week_current">
|
||||||
|
Semaine actuelle
|
||||||
|
</div>
|
||||||
|
<div class="days-grid" id="currentWeekGrid"></div>
|
||||||
|
|
||||||
|
<div class="week-section-title" data-i18n="week_next">
|
||||||
|
Semaine prochaine
|
||||||
|
</div>
|
||||||
|
<div class="days-grid" id="nextWeekGrid"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// --- I18N MOCKUP ---
|
||||||
|
window.I18N = {
|
||||||
|
meals_board_title: "Planning Repas",
|
||||||
|
btn_groceries: "Courses",
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
document
|
||||||
|
.querySelectorAll("[data-i18n]")
|
||||||
|
.forEach(
|
||||||
|
(el) => (el.innerHTML = window.I18N[el.getAttribute("data-i18n")]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const days = ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"];
|
||||||
|
const fullDays = [
|
||||||
|
"Lundi",
|
||||||
|
"Mardi",
|
||||||
|
"Mercredi",
|
||||||
|
"Jeudi",
|
||||||
|
"Vendredi",
|
||||||
|
"Samedi",
|
||||||
|
"Dimanche",
|
||||||
|
];
|
||||||
|
const membersCount = 4;
|
||||||
|
const tbody = document.querySelector("#mealsTable tbody");
|
||||||
|
|
||||||
|
let copyState = {
|
||||||
|
type: null,
|
||||||
|
dayIdx: null,
|
||||||
|
service: null,
|
||||||
|
personIdx: null,
|
||||||
|
sourceElements: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 1. GÉNÉRATION DE LA GRILLE PRINCIPALE ---
|
||||||
|
days.forEach((day, index) => {
|
||||||
|
let trLunch = document.createElement("tr");
|
||||||
|
trLunch.className = "row-lunch";
|
||||||
|
trLunch.innerHTML = `
|
||||||
|
<td class="col-sticky">
|
||||||
|
<span class="day-label">${day}</span>
|
||||||
|
<div class="service-icon" onclick="openCopySheetFromRow(${index}, 'lunch')" title="Copier la ligne">☀️</div>
|
||||||
|
</td>`;
|
||||||
|
for (let p = 0; p < membersCount; p++) {
|
||||||
|
trLunch.innerHTML += `<td class="meal-input-cell"><textarea class="meal-input js-meal-input" data-d="${index}" data-s="lunch" data-p="${p}" placeholder="-"></textarea></td>`;
|
||||||
|
}
|
||||||
|
tbody.appendChild(trLunch);
|
||||||
|
|
||||||
|
let trDinner = document.createElement("tr");
|
||||||
|
trDinner.className = "row-dinner";
|
||||||
|
trDinner.innerHTML = `
|
||||||
|
<td class="col-sticky">
|
||||||
|
<span class="day-label invisible">${day}</span>
|
||||||
|
<div class="service-icon" onclick="openCopySheetFromRow(${index}, 'dinner')" title="Copier la ligne">🌙</div>
|
||||||
|
</td>`;
|
||||||
|
for (let p = 0; p < membersCount; p++) {
|
||||||
|
trDinner.innerHTML += `<td class="meal-input-cell"><textarea class="meal-input js-meal-input" data-d="${index}" data-s="dinner" data-p="${p}" placeholder="-"></textarea></td>`;
|
||||||
|
}
|
||||||
|
tbody.appendChild(trDinner);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- 2. GÉNÉRATION DES GRILLES DE LA MODALE COPIE ---
|
||||||
|
function buildModalGrid(containerId, weekTarget) {
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
let html = "";
|
||||||
|
fullDays.forEach((dayName, idx) => {
|
||||||
|
html += `
|
||||||
|
<div class="day-card">
|
||||||
|
<div class="day-card-title">${dayName}</div>
|
||||||
|
<div class="day-card-actions">
|
||||||
|
<button type="button" class="day-action-btn" onclick="executeCopy(${idx}, 'lunch', '${weekTarget}')">☀️</button>
|
||||||
|
<button type="button" class="day-action-btn" onclick="executeCopy(${idx}, 'dinner', '${weekTarget}')">🌙</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
buildModalGrid("currentWeekGrid", "current");
|
||||||
|
buildModalGrid("nextWeekGrid", "next");
|
||||||
|
|
||||||
|
// --- 3. GESTION DU FOCUS & DE LA TOOLBAR ---
|
||||||
|
document.addEventListener("focusin", (e) => {
|
||||||
|
if (e.target.classList.contains("js-meal-input")) {
|
||||||
|
copyState = {
|
||||||
|
type: "cell",
|
||||||
|
dayIdx: e.target.getAttribute("data-d"),
|
||||||
|
service: e.target.getAttribute("data-s"),
|
||||||
|
personIdx: e.target.getAttribute("data-p"),
|
||||||
|
sourceElements: [e.target],
|
||||||
|
};
|
||||||
|
document.getElementById("contextToolbar").classList.add("active");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("focusout", (e) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!document.activeElement.classList.contains("js-meal-input")) {
|
||||||
|
document
|
||||||
|
.getElementById("contextToolbar")
|
||||||
|
.classList.remove("active");
|
||||||
|
}
|
||||||
|
}, 150);
|
||||||
|
});
|
||||||
|
|
||||||
|
function flashSuccess(input) {
|
||||||
|
input.style.transition = "background 0.3s";
|
||||||
|
input.style.background = "var(--alert-success-bg)";
|
||||||
|
setTimeout(() => (input.style.background = "transparent"), 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 4. ACTIONS DE COPIE ---
|
||||||
|
function applyToAllInRow() {
|
||||||
|
if (
|
||||||
|
copyState.type !== "cell" ||
|
||||||
|
!copyState.sourceElements[0].value.trim()
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
const val = copyState.sourceElements[0].value;
|
||||||
|
const inputs = document.querySelectorAll(
|
||||||
|
`.js-meal-input[data-d="${copyState.dayIdx}"][data-s="${copyState.service}"]`,
|
||||||
|
);
|
||||||
|
inputs.forEach((inp) => {
|
||||||
|
if (inp !== copyState.sourceElements[0]) {
|
||||||
|
inp.value = val;
|
||||||
|
flashSuccess(inp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCopySheetFromCell() {
|
||||||
|
if (
|
||||||
|
!copyState.sourceElements[0] ||
|
||||||
|
!copyState.sourceElements[0].value.trim()
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
document.getElementById("contextToolbar").classList.remove("active");
|
||||||
|
document.activeElement.blur(); // Masque le clavier mobile
|
||||||
|
|
||||||
|
const svcLabel = copyState.service === "lunch" ? "Midi" : "Soir";
|
||||||
|
document.getElementById("copySourceLabel").innerText =
|
||||||
|
`Source : ${window.I18N.source_individual} (${fullDays[copyState.dayIdx]} ${svcLabel})`;
|
||||||
|
document.getElementById("copyModal").classList.add("open");
|
||||||
|
document.body.classList.add("no-scroll");
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCopySheetFromRow(dIdx, service) {
|
||||||
|
const inputs = document.querySelectorAll(
|
||||||
|
`.js-meal-input[data-d="${dIdx}"][data-s="${service}"]`,
|
||||||
|
);
|
||||||
|
const hasContent = Array.from(inputs).some(
|
||||||
|
(inp) => inp.value.trim() !== "",
|
||||||
|
);
|
||||||
|
if (!hasContent) return;
|
||||||
|
|
||||||
|
copyState = {
|
||||||
|
type: "row",
|
||||||
|
dayIdx: dIdx,
|
||||||
|
service: service,
|
||||||
|
sourceElements: Array.from(inputs),
|
||||||
|
};
|
||||||
|
const svcLabel = service === "lunch" ? "Midi" : "Soir";
|
||||||
|
document.getElementById("copySourceLabel").innerText =
|
||||||
|
`Source : ${window.I18N.source_row} (${fullDays[dIdx]} ${svcLabel})`;
|
||||||
|
document.getElementById("copyModal").classList.add("open");
|
||||||
|
document.body.classList.add("no-scroll");
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCopyModal() {
|
||||||
|
document.getElementById("copyModal").classList.remove("open");
|
||||||
|
document.body.classList.remove("no-scroll");
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeCopy(targetDayIdx, targetService, targetWeek) {
|
||||||
|
if (targetWeek === "next") {
|
||||||
|
alert("Simulation : Copié avec succès vers la SEMAINE PROCHAINE !");
|
||||||
|
closeCopyModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copyState.type === "cell") {
|
||||||
|
const val = copyState.sourceElements[0].value;
|
||||||
|
const targetInput = document.querySelector(
|
||||||
|
`.js-meal-input[data-d="${targetDayIdx}"][data-s="${targetService}"][data-p="${copyState.personIdx}"]`,
|
||||||
|
);
|
||||||
|
if (targetInput) {
|
||||||
|
targetInput.value = val;
|
||||||
|
flashSuccess(targetInput);
|
||||||
|
}
|
||||||
|
} else if (copyState.type === "row") {
|
||||||
|
const targetInputs = document.querySelectorAll(
|
||||||
|
`.js-meal-input[data-d="${targetDayIdx}"][data-s="${targetService}"]`,
|
||||||
|
);
|
||||||
|
copyState.sourceElements.forEach((inp, idx) => {
|
||||||
|
if (inp.value.trim() !== "") {
|
||||||
|
targetInputs[idx].value = inp.value;
|
||||||
|
flashSuccess(targetInputs[idx]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
closeCopyModal();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -48,43 +48,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* --- 2. NAVIGATION TABS (Pills) --- */
|
/* --- 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 {
|
.owner-tabs {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -1486,20 +1449,7 @@ body.sum-mode-active .sum-target {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Ajouts Structure Global Budget */
|
/* 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 {
|
.btn-settings-gear {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -1582,23 +1532,7 @@ body.sum-mode-active .sum-target {
|
|||||||
max-width: 100vw !important;
|
max-width: 100vw !important;
|
||||||
box-sizing: border-box;
|
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"] {
|
.budget-view > div[style*="display:grid"] {
|
||||||
gap: 10px !important;
|
gap: 10px !important;
|
||||||
margin-bottom: 15px !important;
|
margin-bottom: 15px !important;
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
// modules/food/includes/api/api-liste.php
|
||||||
ob_start();
|
ob_start();
|
||||||
require_once dirname(__DIR__, 2) . '/includes/auth.php';
|
require_once dirname(__DIR__, 4) . '/includes/auth.php';
|
||||||
require_login();
|
require_login();
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
@@ -10,7 +11,7 @@ set_exception_handler(function (\Throwable $e) {
|
|||||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]); exit;
|
echo json_encode(['ok' => false, 'error' => $e->getMessage()]); exit;
|
||||||
});
|
});
|
||||||
|
|
||||||
require_once dirname(__DIR__, 2) . '/includes/db.php';
|
require_once dirname(__DIR__, 4) . '/includes/db.php';
|
||||||
|
|
||||||
// ── Auto-create tables ────────────────────────────────────────────────────────
|
// ── Auto-create tables ────────────────────────────────────────────────────────
|
||||||
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_lists (
|
$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();
|
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 {
|
function liste_builtin_detect(string $normalized): ?int {
|
||||||
static $map = null;
|
static $map = null;
|
||||||
if ($map === null) $map = [
|
if ($map === null) $map = [
|
||||||
// 1 = Fruits & Légumes
|
|
||||||
'pomme'=>1,'poire'=>1,'banane'=>1,'orange'=>1,'citron'=>1,'raisin'=>1,
|
'pomme'=>1,'poire'=>1,'banane'=>1,'orange'=>1,'citron'=>1,'raisin'=>1,
|
||||||
'fraise'=>1,'framboise'=>1,'myrtille'=>1,'cerise'=>1,'peche'=>1,'abricot'=>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,
|
'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,
|
'gingembre'=>1,'avocat'=>1,'endive'=>1,'ciboulette'=>1,'persil'=>1,
|
||||||
'basilic'=>1,'menthe'=>1,'thym'=>1,'romarin'=>1,'coriandre'=>1,
|
'basilic'=>1,'menthe'=>1,'thym'=>1,'romarin'=>1,'coriandre'=>1,
|
||||||
'girofle'=>1,'citron vert'=>1,'lime'=>1,'pamplemousse'=>1,'litchi'=>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,
|
'boeuf'=>2,'veau'=>2,'poulet'=>2,'porc'=>2,'agneau'=>2,'dinde'=>2,
|
||||||
'lapin'=>2,'jambon'=>2,'lardon'=>2,'saucisse'=>2,'saucisson'=>2,
|
'lapin'=>2,'jambon'=>2,'lardon'=>2,'saucisse'=>2,'saucisson'=>2,
|
||||||
'merguez'=>2,'chipolata'=>2,'steak'=>2,'escalope'=>2,'roti'=>2,
|
'merguez'=>2,'chipolata'=>2,'steak'=>2,'escalope'=>2,'roti'=>2,
|
||||||
'gigot'=>2,'filet'=>2,'cuisses'=>2,'viande hachee'=>2,'andouille'=>2,
|
'gigot'=>2,'filet'=>2,'cuisses'=>2,'viande hachee'=>2,'andouille'=>2,
|
||||||
'magret'=>2,'canard'=>2,'foie'=>2,'boudin'=>2,'paupiette'=>2,
|
'magret'=>2,'canard'=>2,'foie'=>2,'boudin'=>2,'paupiette'=>2,
|
||||||
'entrecote'=>2,'cote de boeuf'=>2,'onglet'=>2,'bavette'=>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,
|
'saumon'=>3,'thon'=>3,'dorade'=>3,'bar'=>3,'cabillaud'=>3,'sole'=>3,
|
||||||
'truite'=>3,'lieu'=>3,'maquereau'=>3,'sardine'=>3,'anchois'=>3,
|
'truite'=>3,'lieu'=>3,'maquereau'=>3,'sardine'=>3,'anchois'=>3,
|
||||||
'crevette'=>3,'moule'=>3,'huitre'=>3,'coquille'=>3,'poulpe'=>3,
|
'crevette'=>3,'moule'=>3,'huitre'=>3,'coquille'=>3,'poulpe'=>3,
|
||||||
'seiche'=>3,'langoustine'=>3,'lotte'=>3,'merlu'=>3,'colin'=>3,
|
'seiche'=>3,'langoustine'=>3,'lotte'=>3,'merlu'=>3,'colin'=>3,
|
||||||
'tilapia'=>3,'daurade'=>3,'aiglefin'=>3,'homard'=>3,'crabe'=>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,
|
'pain'=>4,'baguette'=>4,'brioche'=>4,'croissant'=>4,'pain de mie'=>4,
|
||||||
'ficelle'=>4,'fougasse'=>4,'ciabatta'=>4,'biscotte'=>4,'naan'=>4,
|
'ficelle'=>4,'fougasse'=>4,'ciabatta'=>4,'biscotte'=>4,'naan'=>4,
|
||||||
'pita'=>4,'toast'=>4,'bagel'=>4,'pain burger'=>4,'wrap'=>4,
|
'pita'=>4,'toast'=>4,'bagel'=>4,'pain burger'=>4,'wrap'=>4,
|
||||||
// 5 = Frais
|
|
||||||
'lait'=>5,'creme'=>5,'yaourt'=>5,'yourt'=>5,'beurre'=>5,
|
'lait'=>5,'creme'=>5,'yaourt'=>5,'yourt'=>5,'beurre'=>5,
|
||||||
'fromage'=>5,'camembert'=>5,'brie'=>5,'comte'=>5,'gruyere'=>5,
|
'fromage'=>5,'camembert'=>5,'brie'=>5,'comte'=>5,'gruyere'=>5,
|
||||||
'emmental'=>5,'mozzarella'=>5,'ricotta'=>5,'mascarpone'=>5,
|
'emmental'=>5,'mozzarella'=>5,'ricotta'=>5,'mascarpone'=>5,
|
||||||
'feta'=>5,'chevre'=>5,'oeuf'=>5,'oeufs'=>5,'creme fraiche'=>5,
|
'feta'=>5,'chevre'=>5,'oeuf'=>5,'oeufs'=>5,'creme fraiche'=>5,
|
||||||
'fromage blanc'=>5,'petit suisse'=>5,'kefir'=>5,
|
'fromage blanc'=>5,'petit suisse'=>5,'kefir'=>5,
|
||||||
// 6 = Surgelés
|
|
||||||
'surgele'=>6,'pizza surgelee'=>6,'frite surgelee'=>6,'poisson pane'=>6,
|
'surgele'=>6,'pizza surgelee'=>6,'frite surgelee'=>6,'poisson pane'=>6,
|
||||||
'legume surgele'=>6,'glace'=>6,'sorbet'=>6,'plat surgele'=>6,
|
'legume surgele'=>6,'glace'=>6,'sorbet'=>6,'plat surgele'=>6,
|
||||||
'nugget'=>6,'edamame'=>6,'petits pois'=>6,'epinards'=>6,
|
'nugget'=>6,'edamame'=>6,'petits pois'=>6,'epinards'=>6,
|
||||||
// 7 = Boissons
|
|
||||||
'eau'=>7,'coca'=>7,'cola'=>7,'limonade'=>7,'sirop'=>7,
|
'eau'=>7,'coca'=>7,'cola'=>7,'limonade'=>7,'sirop'=>7,
|
||||||
'biere'=>7,'vin'=>7,'champagne'=>7,'cafe'=>7,'the'=>7,
|
'biere'=>7,'vin'=>7,'champagne'=>7,'cafe'=>7,'the'=>7,
|
||||||
'tisane'=>7,'soda'=>7,'jus'=>7,'kombucha'=>7,'cidre'=>7,
|
'tisane'=>7,'soda'=>7,'jus'=>7,'kombucha'=>7,'cidre'=>7,
|
||||||
'whisky'=>7,'rhum'=>7,'vodka'=>7,'prosecco'=>7,'rose'=>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,
|
'pate'=>8,'spaghetti'=>8,'tagliatelle'=>8,'penne'=>8,'fusilli'=>8,
|
||||||
'macaroni'=>8,'riz'=>8,'couscous'=>8,'quinoa'=>8,'lentille'=>8,
|
'macaroni'=>8,'riz'=>8,'couscous'=>8,'quinoa'=>8,'lentille'=>8,
|
||||||
'pois chiche'=>8,'farine'=>8,'semoule'=>8,'polenta'=>8,'boulgour'=>8,
|
'pois chiche'=>8,'farine'=>8,'semoule'=>8,'polenta'=>8,'boulgour'=>8,
|
||||||
'orge'=>8,'feculent'=>8,'vermicelle'=>8,'nouille'=>8,'gnocchi'=>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,
|
'chips'=>9,'crackers'=>9,'olives'=>9,'cornichon'=>9,'capres'=>9,
|
||||||
'tapenade'=>9,'houmous'=>9,'tzatziki'=>9,'sel'=>9,'poivre'=>9,
|
'tapenade'=>9,'houmous'=>9,'tzatziki'=>9,'sel'=>9,'poivre'=>9,
|
||||||
'epice'=>9,'cube bouillon'=>9,'bouillon'=>9,'noix de cajou'=>9,
|
'epice'=>9,'cube bouillon'=>9,'bouillon'=>9,'noix de cajou'=>9,
|
||||||
'amande'=>9,'pistache'=>9,'noix'=>9,'cacahuete'=>9,
|
'amande'=>9,'pistache'=>9,'noix'=>9,'cacahuete'=>9,
|
||||||
// 10 = Conserves
|
|
||||||
'conserve'=>10,'boite de tomate'=>10,'concentre de tomate'=>10,
|
'conserve'=>10,'boite de tomate'=>10,'concentre de tomate'=>10,
|
||||||
'mais'=>10,'ratatouille'=>10,'cassoulet'=>10,'pate de foie'=>10,
|
'mais'=>10,'ratatouille'=>10,'cassoulet'=>10,'pate de foie'=>10,
|
||||||
'sardine boite'=>10,'thon boite'=>10,'maquereau boite'=>10,
|
'sardine boite'=>10,'thon boite'=>10,'maquereau boite'=>10,
|
||||||
'soupe boite'=>10,'haricot boite'=>10,
|
'soupe boite'=>10,'haricot boite'=>10,
|
||||||
// 11 = Plats cuisinés
|
|
||||||
'quiche'=>11,'lasagne'=>11,'gratin'=>11,'pizza'=>11,'tartiflette'=>11,
|
'quiche'=>11,'lasagne'=>11,'gratin'=>11,'pizza'=>11,'tartiflette'=>11,
|
||||||
'croque'=>11,'hachis parmentier'=>11,'paella'=>11,'moussaka'=>11,
|
'croque'=>11,'hachis parmentier'=>11,'paella'=>11,'moussaka'=>11,
|
||||||
'tarte'=>11,'flamiche'=>11,'pissaladiere'=>11,
|
'tarte'=>11,'flamiche'=>11,'pissaladiere'=>11,
|
||||||
// 12 = Sauces & Condiments
|
|
||||||
'ketchup'=>12,'mayonnaise'=>12,'moutarde'=>12,'vinaigrette'=>12,
|
'ketchup'=>12,'mayonnaise'=>12,'moutarde'=>12,'vinaigrette'=>12,
|
||||||
'sauce soja'=>12,'tabasco'=>12,'huile'=>12,'vinaigre'=>12,
|
'sauce soja'=>12,'tabasco'=>12,'huile'=>12,'vinaigre'=>12,
|
||||||
'sauce tomate'=>12,'pesto'=>12,'sriracha'=>12,'worcester'=>12,
|
'sauce tomate'=>12,'pesto'=>12,'sriracha'=>12,'worcester'=>12,
|
||||||
'nuoc mam'=>12,'sauce'=>12,'condiment'=>12,'worcestershire'=>12,
|
'nuoc mam'=>12,'sauce'=>12,'condiment'=>12,'worcestershire'=>12,
|
||||||
// 13 = Petit déjeuner
|
|
||||||
'cereale'=>13,'muesli'=>13,'granola'=>13,'corn flakes'=>13,
|
'cereale'=>13,'muesli'=>13,'granola'=>13,'corn flakes'=>13,
|
||||||
'miel'=>13,'confiture'=>13,'nutella'=>13,'beurre de cacahuete'=>13,
|
'miel'=>13,'confiture'=>13,'nutella'=>13,'beurre de cacahuete'=>13,
|
||||||
'sirop d erable'=>13,'sirop erable'=>13,'chocolat en poudre'=>13,
|
'sirop d erable'=>13,'sirop erable'=>13,'chocolat en poudre'=>13,
|
||||||
// 14 = Biscuits & Gâteaux
|
|
||||||
'biscuit'=>14,'gateau'=>14,'cookie'=>14,'madeleine'=>14,
|
'biscuit'=>14,'gateau'=>14,'cookie'=>14,'madeleine'=>14,
|
||||||
'financier'=>14,'quatre quarts'=>14,'brownie'=>14,'sable'=>14,
|
'financier'=>14,'quatre quarts'=>14,'brownie'=>14,'sable'=>14,
|
||||||
'speculoos'=>14,'oreo'=>14,'lu'=>14,'petit beurre'=>14,'galette'=>14,
|
'speculoos'=>14,'oreo'=>14,'lu'=>14,'petit beurre'=>14,'galette'=>14,
|
||||||
'palmier'=>14,'macaron'=>14,'eclair'=>14,
|
'palmier'=>14,'macaron'=>14,'eclair'=>14,
|
||||||
// 15 = Confiserie
|
|
||||||
'chocolat'=>15,'bonbon'=>15,'reglisse'=>15,'caramel'=>15,
|
'chocolat'=>15,'bonbon'=>15,'reglisse'=>15,'caramel'=>15,
|
||||||
'nougat'=>15,'marshmallow'=>15,'guimauve'=>15,'sucette'=>15,
|
'nougat'=>15,'marshmallow'=>15,'guimauve'=>15,'sucette'=>15,
|
||||||
'chewing gum'=>15,'pastille'=>15,'calisson'=>15,
|
'chewing gum'=>15,'pastille'=>15,'calisson'=>15,
|
||||||
// 16 = Dessert
|
|
||||||
'yaourt dessert'=>16,'creme dessert'=>16,'mousse au chocolat'=>16,
|
'yaourt dessert'=>16,'creme dessert'=>16,'mousse au chocolat'=>16,
|
||||||
'tiramisu'=>16,'creme brulee'=>16,'flan'=>16,'crepe'=>16,
|
'tiramisu'=>16,'creme brulee'=>16,'flan'=>16,'crepe'=>16,
|
||||||
'madeleine'=>16,'profiterole'=>16,'eclair'=>16,
|
'profiterole'=>16,
|
||||||
// 17 = Beauté & Hygiène
|
|
||||||
'shampoing'=>17,'gel douche'=>17,'savon'=>17,'dentifrice'=>17,
|
'shampoing'=>17,'gel douche'=>17,'savon'=>17,'dentifrice'=>17,
|
||||||
'deodorant'=>17,'crème visage'=>17,'crème corps'=>17,'rasoir'=>17,
|
'deodorant'=>17,'crème visage'=>17,'crème corps'=>17,'rasoir'=>17,
|
||||||
'mousse a raser'=>17,'coton'=>17,'lingette'=>17,'maquillage'=>17,
|
'mousse a raser'=>17,'coton'=>17,'lingette'=>17,'maquillage'=>17,
|
||||||
'parfum'=>17,'brosse a dents'=>17,'fil dentaire'=>17,'serum'=>17,
|
'parfum'=>17,'brosse a dents'=>17,'fil dentaire'=>17,'serum'=>17,
|
||||||
'hydratant'=>17,'demaquillant'=>17,
|
'hydratant'=>17,'demaquillant'=>17,
|
||||||
// 18 = Bébé
|
|
||||||
'couche'=>18,'biberon'=>18,'lait infantile'=>18,'compote bebe'=>18,
|
'couche'=>18,'biberon'=>18,'lait infantile'=>18,'compote bebe'=>18,
|
||||||
'pot bebe'=>18,'puree bebe'=>18,'lingette bebe'=>18,'savon bebe'=>18,
|
'pot bebe'=>18,'puree bebe'=>18,'lingette bebe'=>18,'savon bebe'=>18,
|
||||||
'creme bebe'=>18,'sucette bebe'=>18,
|
'creme bebe'=>18,'sucette bebe'=>18,
|
||||||
// 19 = Entretien
|
|
||||||
'lessive'=>19,'liquide vaisselle'=>19,'nettoyant'=>19,'degraissant'=>19,
|
'lessive'=>19,'liquide vaisselle'=>19,'nettoyant'=>19,'degraissant'=>19,
|
||||||
'deboucheur'=>19,'anticalcaire'=>19,'eponge'=>19,'serpillere'=>19,
|
'deboucheur'=>19,'anticalcaire'=>19,'eponge'=>19,'serpillere'=>19,
|
||||||
'sac poubelle'=>19,'papier toilette'=>19,'sopalin'=>19,'essuie tout'=>19,
|
'sac poubelle'=>19,'papier toilette'=>19,'sopalin'=>19,'essuie tout'=>19,
|
||||||
'aluminium'=>19,'film plastique'=>19,'sac congelation'=>19,
|
'aluminium'=>19,'film plastique'=>19,'sac congelation'=>19,
|
||||||
'nettoyant wc'=>19,'desinfectant'=>19,'vitre'=>19,'javel'=>19,
|
'nettoyant wc'=>19,'desinfectant'=>19,'vitre'=>19,'javel'=>19,
|
||||||
// 20 = Animaux
|
|
||||||
'croquette'=>20,'patee'=>20,'litiere'=>20,'os'=>20,
|
'croquette'=>20,'patee'=>20,'litiere'=>20,'os'=>20,
|
||||||
'friandise animale'=>20,'nourriture chat'=>20,'nourriture chien'=>20,
|
'friandise animale'=>20,'nourriture chat'=>20,'nourriture chien'=>20,
|
||||||
// 21 = Maison & Jardin
|
|
||||||
'ampoule'=>21,'pile'=>21,'bougie'=>21,'allumette'=>21,
|
'ampoule'=>21,'pile'=>21,'bougie'=>21,'allumette'=>21,
|
||||||
'ruban adhesif'=>21,'colle'=>21,'vis'=>21,'terreau'=>21,
|
'ruban adhesif'=>21,'colle'=>21,'vis'=>21,'terreau'=>21,
|
||||||
'engrais'=>21,'pot de fleur'=>21,'arrosoir'=>21,'graine'=>21,
|
'engrais'=>21,'pot de fleur'=>21,'arrosoir'=>21,'graine'=>21,
|
||||||
'tournevis'=>21,'marteau'=>21,'clou'=>21,'cle'=>21,
|
'tournevis'=>21,'marteau'=>21,'clou'=>21,'cle'=>21,
|
||||||
// 22 = Pharmacie
|
|
||||||
'doliprane'=>22,'ibuprofene'=>22,'paracetamol'=>22,'aspirine'=>22,
|
'doliprane'=>22,'ibuprofene'=>22,'paracetamol'=>22,'aspirine'=>22,
|
||||||
'bandage'=>22,'pansement'=>22,'thermometre'=>22,'serum physiologique'=>22,
|
'bandage'=>22,'pansement'=>22,'thermometre'=>22,'serum physiologique'=>22,
|
||||||
'vitamine'=>22,'complement'=>22,'masque'=>22,'gant medical'=>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];
|
if (isset($map[$normalized])) return $map[$normalized];
|
||||||
|
|
||||||
// Check individual words
|
|
||||||
$words = explode(' ', $normalized);
|
$words = explode(' ', $normalized);
|
||||||
foreach ($words as $w) {
|
foreach ($words as $w) {
|
||||||
if (strlen($w) >= 3 && isset($map[$w])) return $map[$w];
|
if (strlen($w) >= 3 && isset($map[$w])) return $map[$w];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Partial match (contains)
|
|
||||||
foreach ($map as $kw => $catPos) {
|
foreach ($map as $kw => $catPos) {
|
||||||
if (str_contains($normalized, $kw)) return $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 {
|
function liste_detect_category(PDO $pdo, string $label): ?int {
|
||||||
$norm = liste_normalize_detect($label);
|
$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 = $pdo->prepare("SELECT category_id FROM pf_item_category_rules WHERE keyword=? LIMIT 1");
|
||||||
$r->execute([$norm]);
|
$r->execute([$norm]);
|
||||||
if ($row = $r->fetch()) return (int)$row['category_id'];
|
if ($row = $r->fetch()) return (int)$row['category_id'];
|
||||||
|
|
||||||
// 2. Learned rules (individual words)
|
|
||||||
foreach (explode(' ', $norm) as $w) {
|
foreach (explode(' ', $norm) as $w) {
|
||||||
if (strlen($w) < 3) continue;
|
if (strlen($w) < 3) continue;
|
||||||
$r = $pdo->prepare("SELECT category_id FROM pf_item_category_rules WHERE keyword=? LIMIT 1");
|
$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'];
|
if ($row = $r->fetch()) return (int)$row['category_id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Built-in dictionary → map position to real category id
|
|
||||||
$pos = liste_builtin_detect($norm);
|
$pos = liste_builtin_detect($norm);
|
||||||
if ($pos === null) return null;
|
if ($pos === null) return null;
|
||||||
|
|
||||||
// Get category id by position order
|
|
||||||
$r = $pdo->query("SELECT id FROM pf_list_categories ORDER BY position, id");
|
$r = $pdo->query("SELECT id FROM pf_list_categories ORDER BY position, id");
|
||||||
$ids = $r->fetchAll(PDO::FETCH_COLUMN);
|
$ids = $r->fetchAll(PDO::FETCH_COLUMN);
|
||||||
return $ids[$pos - 1] ?? null;
|
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 (?,?)
|
$pdo->prepare("INSERT INTO pf_item_category_rules (keyword, category_id) VALUES (?,?)
|
||||||
ON DUPLICATE KEY UPDATE category_id=VALUES(category_id)")
|
ON DUPLICATE KEY UPDATE category_id=VALUES(category_id)")
|
||||||
->execute([$norm, $category_id]);
|
->execute([$norm, $category_id]);
|
||||||
// Also learn individual meaningful words
|
|
||||||
foreach (explode(' ', $norm) as $w) {
|
foreach (explode(' ', $norm) as $w) {
|
||||||
if (strlen($w) >= 4) {
|
if (strlen($w) >= 4) {
|
||||||
$pdo->prepare("INSERT IGNORE INTO pf_item_category_rules (keyword, category_id) VALUES (?,?)")
|
$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]);
|
$pdo->prepare("UPDATE pf_grocery_items SET category_id=? WHERE id=?")->execute([$category_id, $item_id]);
|
||||||
|
|
||||||
// Learn from manual assignment
|
|
||||||
if ($category_id !== null) {
|
if ($category_id !== null) {
|
||||||
$row = $pdo->prepare("SELECT label FROM pf_grocery_items WHERE id=?");
|
$row = $pdo->prepare("SELECT label FROM pf_grocery_items WHERE id=?");
|
||||||
$row->execute([$item_id]);
|
$row->execute([$item_id]);
|
||||||
@@ -431,7 +400,7 @@ if ($action === 'clear_all' && $method === 'POST') {
|
|||||||
// ── HISTORY ───────────────────────────────────────────────────────────────────
|
// ── HISTORY ───────────────────────────────────────────────────────────────────
|
||||||
if ($action === 'history' && $method === 'GET') {
|
if ($action === 'history' && $method === 'GET') {
|
||||||
$max = 20;
|
$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']));
|
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");
|
$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;
|
echo json_encode(['history' => $s->fetchAll(PDO::FETCH_COLUMN)]); exit;
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
// Désactive l'affichage des erreurs HTML pour garantir un retour JSON strict
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
error_reporting(0);
|
||||||
|
|
||||||
|
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||||
|
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||||
|
require dirname(__DIR__, 4) . '/includes/i18n.php';
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$dateParam = $_GET['date'] ?? date('Y-m-d');
|
||||||
|
$baseDate = new DateTime($dateParam);
|
||||||
|
|
||||||
|
// Aligner sur le lundi de la semaine
|
||||||
|
$monday = clone $baseDate;
|
||||||
|
if ($monday->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()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
<?php
|
||||||
|
// modules/meals/includes/api/import-pdf.php
|
||||||
|
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||||
|
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || empty($_FILES['pdf_file'])) {
|
||||||
|
echo json_encode(['success' => 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()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||||
|
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
echo json_encode(['success' => 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()]);
|
||||||
|
}
|
||||||
@@ -1,5 +1,53 @@
|
|||||||
/* Liste module JS */
|
<?php
|
||||||
const API = '/modules/liste/api.php';
|
// modules/food/views/liste.php
|
||||||
|
if (!isset($pdo)) { exit; }
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="liste-main">
|
||||||
|
<!-- Tabs des listes -->
|
||||||
|
<div class="liste-tabs-bar">
|
||||||
|
<div class="liste-tabs" id="liste-tabs"></div>
|
||||||
|
<button class="liste-tab-add" id="btn-add-list" title="<?= htmlspecialchars(tr('liste_new_list')) ?>">+</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Corps de la liste active -->
|
||||||
|
<div class="liste-body" id="liste-body">
|
||||||
|
<div class="liste-add-card">
|
||||||
|
<input type="text" id="liste-input" maxlength="500" placeholder="<?= htmlspecialchars(tr('liste_placeholder_add')) ?>" autocomplete="off">
|
||||||
|
<button id="btn-liste-add"><?= htmlspecialchars(tr('liste_btn_add')) ?></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="liste-toolbar" id="liste-toolbar">
|
||||||
|
<button class="btn-tool" id="btn-uncheck-all"><?= htmlspecialchars(tr('liste_btn_uncheck_all')) ?></button>
|
||||||
|
<button class="btn-tool btn-tool-danger" id="btn-delete-picked"><?= htmlspecialchars(tr('liste_btn_delete_picked')) ?></button>
|
||||||
|
<button class="btn-tool btn-tool-danger" id="btn-clear-all"><?= htmlspecialchars(tr('liste_btn_clear_all')) ?></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="liste-items-root"></div>
|
||||||
|
<div id="liste-history-root"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const LISTE_TRANSLATIONS = {
|
||||||
|
already_in: <?= json_encode(tr('liste_already_in')) ?>,
|
||||||
|
confirm_clear: <?= json_encode(tr('liste_confirm_clear')) ?>,
|
||||||
|
confirm_delete_list: <?= json_encode(tr('liste_confirm_delete_list')) ?>,
|
||||||
|
rename_list: <?= json_encode(tr('liste_rename_list')) ?>,
|
||||||
|
new_name: <?= json_encode(tr('liste_new_name')) ?>,
|
||||||
|
list_deleted: <?= json_encode(tr('liste_list_deleted')) ?>,
|
||||||
|
list_created: <?= json_encode(tr('liste_list_created')) ?>,
|
||||||
|
list_renamed: <?= json_encode(tr('liste_list_renamed')) ?>,
|
||||||
|
edit_placeholder: <?= json_encode(tr('liste_edit_placeholder')) ?>,
|
||||||
|
history_title: <?= json_encode(tr('liste_history_title')) ?>,
|
||||||
|
added: <?= json_encode(tr('liste_added')) ?>,
|
||||||
|
updated: <?= json_encode(tr('liste_updated')) ?>,
|
||||||
|
deleted: <?= json_encode(tr('liste_deleted')) ?>,
|
||||||
|
cancel: <?= json_encode(tr('cancel')) ?>,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ⚠️ NOUVEAU CHEMIN D'API
|
||||||
|
const API = '/modules/food/includes/api/api-liste.php';
|
||||||
const T = window.LISTE_TRANSLATIONS || {};
|
const T = window.LISTE_TRANSLATIONS || {};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
@@ -515,3 +563,5 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
<?php
|
||||||
|
// modules/meals/main.php
|
||||||
|
|
||||||
|
// Récupération stricte des membres du foyer (Exclusion des intervenants et proches)
|
||||||
|
$stmt = $pdo->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 "<div class='pf-alert pf-alert--warning'>" . tr('meals_no_members') . "</div>";
|
||||||
|
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'];
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="pf-container meals-container">
|
||||||
|
<div class="meals-header-bar">
|
||||||
|
<h1 class="meals-title">
|
||||||
|
<span class="meals-title-icon">🍽️</span>
|
||||||
|
<span data-i18n="meals_board_title"><?= tr('meals_board_title') ?></span>
|
||||||
|
</h1>
|
||||||
|
<div class="meals-actions">
|
||||||
|
<!-- NOUVEAU : Bouton Import PDF -->
|
||||||
|
<button type="button" class="pf-btn btn-secondary" onclick="document.getElementById('importMenuModal').classList.add('open'); document.body.classList.add('no-scroll');">
|
||||||
|
📄 <?= tr('meal_btn_import_pdf') ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Barre contextuelle (Fixe en bas) -->
|
||||||
|
<div class="context-toolbar" id="contextToolbar">
|
||||||
|
<button class="ctx-btn" onmousedown="event.preventDefault(); applyToAllInRow()">➡️ <span data-i18n="btn_apply_all"><?= tr('btn_apply_all') ?></span></button>
|
||||||
|
<button class="ctx-btn primary" onmousedown="event.preventDefault(); openCopySheetFromCell()">🔄 <span data-i18n="btn_copy_to"><?= tr('btn_copy_to') ?></span></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="meals-wrapper">
|
||||||
|
<?php
|
||||||
|
$weeks = array_chunk($dates, 7);
|
||||||
|
foreach ($weeks as $weekIndex => $weekDates):
|
||||||
|
$startWeekStr = date('d/m', strtotime($weekDates[0]));
|
||||||
|
$endWeekStr = date('d/m', strtotime(end($weekDates)));
|
||||||
|
?>
|
||||||
|
<h2 class="meals-week-title"><?= tr('meal_week') ?> (<?= $startWeekStr ?> - <?= $endWeekStr ?>)</h2>
|
||||||
|
<div class="meals-table-container">
|
||||||
|
<table class="meals-table" id="mealsTable_<?= $weekIndex ?>">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="col-sticky"></th>
|
||||||
|
<?php foreach ($members as $m): ?>
|
||||||
|
<th><?= htmlspecialchars(strtoupper($m['name'])) ?></th>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($weekDates as $dateStr):
|
||||||
|
$dayKey = strtolower(date('D', strtotime($dateStr)));
|
||||||
|
$dayName = tr('day_' . $dayKey);
|
||||||
|
$dayDate = date('d/m', strtotime($dateStr));
|
||||||
|
$fullDayName = tr('day_full_' . $dayKey) . ' ' . $dayDate;
|
||||||
|
?>
|
||||||
|
<tr class="row-lunch">
|
||||||
|
<td class="col-sticky">
|
||||||
|
<span class="day-label"><?= $dayName ?> <?= $dayDate ?></span>
|
||||||
|
<div class="service-icon" onmousedown="event.preventDefault(); openCopySheetFromRow('<?= $dateStr ?>', 'lunch', '<?= addslashes($fullDayName) ?>')" title="<?= tr('meals_copy_lunch') ?>">☀️</div>
|
||||||
|
</td>
|
||||||
|
<?php foreach ($members as $m):
|
||||||
|
$val = $mealsData[$dateStr]['lunch'][$m['id']] ?? '';
|
||||||
|
?>
|
||||||
|
<td class="meal-input-cell">
|
||||||
|
<textarea class="meal-input js-meal-input" data-d="<?= $dateStr ?>" data-s="lunch" data-p="<?= $m['id'] ?>" placeholder="-"><?= htmlspecialchars($val) ?></textarea>
|
||||||
|
</td>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr class="row-dinner">
|
||||||
|
<td class="col-sticky">
|
||||||
|
<span class="day-label invisible"><?= $dayName ?> <?= $dayDate ?></span>
|
||||||
|
<div class="service-icon" onmousedown="event.preventDefault(); openCopySheetFromRow('<?= $dateStr ?>', 'dinner', '<?= addslashes($fullDayName) ?>')" title="<?= tr('meals_copy_dinner') ?>">🌙</div>
|
||||||
|
</td>
|
||||||
|
<?php foreach ($members as $m):
|
||||||
|
$val = $mealsData[$dateStr]['dinner'][$m['id']] ?? '';
|
||||||
|
?>
|
||||||
|
<td class="meal-input-cell">
|
||||||
|
<textarea class="meal-input js-meal-input" data-d="<?= $dateStr ?>" data-s="dinner" data-p="<?= $m['id'] ?>" placeholder="-"><?= htmlspecialchars($val) ?></textarea>
|
||||||
|
</td>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom Sheet de Copie (Format Grille Ultra-compacte) -->
|
||||||
|
<div id="copyModal" class="pf-modal pf-bottom-sheet">
|
||||||
|
<div class="pf-modal-content pf-bs-content copy-modal-large">
|
||||||
|
<div class="bs-header">
|
||||||
|
<h3 class="bs-title">🔄 <span data-i18n="modal_copy_title"><?= tr('modal_copy_title') ?></span></h3>
|
||||||
|
<button class="bs-close" onclick="closeCopyModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="copySourceLabel" class="meals-copy-source">Source : ...</div>
|
||||||
|
|
||||||
|
<div class="week-section-title" data-i18n="week_current"><?= tr('week_current') ?></div>
|
||||||
|
<div class="copy-days-grid">
|
||||||
|
<?php for($i=0; $i<7; $i++): $dStr = $dates[$i]; $dName = tr('day_full_' . strtolower(date('D', strtotime($dStr)))) . ' ' . date('d/m', strtotime($dStr)); ?>
|
||||||
|
<div class="copy-day-card">
|
||||||
|
<div class="copy-day-name"><?= $dName ?></div>
|
||||||
|
<div class="copy-day-actions">
|
||||||
|
<button type="button" class="btn-target-service" onmousedown="event.preventDefault(); executeCopy('<?= $dStr ?>', 'lunch')" title="<?= tr('meal_lunch') ?>">☀️</button>
|
||||||
|
<button type="button" class="btn-target-service" onmousedown="event.preventDefault(); executeCopy('<?= $dStr ?>', 'dinner')" title="<?= tr('meal_dinner') ?>">🌙</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="week-section-title week-section-mt" data-i18n="week_next"><?= tr('week_next') ?></div>
|
||||||
|
<div class="copy-days-grid">
|
||||||
|
<?php for($i=7; $i<14; $i++): $dStr = $dates[$i]; $dName = tr('day_full_' . strtolower(date('D', strtotime($dStr)))) . ' ' . date('d/m', strtotime($dStr)); ?>
|
||||||
|
<div class="copy-day-card">
|
||||||
|
<div class="copy-day-name"><?= $dName ?></div>
|
||||||
|
<div class="copy-day-actions">
|
||||||
|
<button type="button" class="btn-target-service" onmousedown="event.preventDefault(); executeCopy('<?= $dStr ?>', 'lunch')" title="<?= tr('meal_lunch') ?>">☀️</button>
|
||||||
|
<button type="button" class="btn-target-service" onmousedown="event.preventDefault(); executeCopy('<?= $dStr ?>', 'dinner')" title="<?= tr('meal_dinner') ?>">🌙</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- NOUVEAU : Modale d'import PDF Cantine -->
|
||||||
|
<div id="importMenuModal" class="pf-modal">
|
||||||
|
<div class="pf-modal-content">
|
||||||
|
<div class="pf-modal-header" style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-light); padding-bottom: 15px; margin-bottom: 20px;">
|
||||||
|
<h3 class="pf-modal-title" style="margin: 0; border: none; padding: 0;">📄 <?= tr('meal_import_title') ?></h3>
|
||||||
|
<button type="button" class="pf-modal-close" style="background: none; border: none; font-size: 1.5rem; cursor: pointer; color: var(--text-muted);" onclick="document.getElementById('importMenuModal').classList.remove('open'); document.body.classList.remove('no-scroll');">×</button>
|
||||||
|
</div>
|
||||||
|
<form id="importMenuForm" onsubmit="importPdfMenu(event)" action="/modules/food/includes/api/import-pdf.php" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
||||||
|
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label"><?= tr('meal_import_child') ?></label>
|
||||||
|
<select name="person_id" class="pf-input" required>
|
||||||
|
<option value="" disabled selected>-- <?= tr('meal_select_child') ?> --</option>
|
||||||
|
<?php foreach ($members as $m): ?>
|
||||||
|
<option value="<?= $m['id'] ?>"><?= htmlspecialchars($m['name']) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label"><?= tr('meal_pdf_file') ?></label>
|
||||||
|
<div class="meal-dropzone" onclick="document.getElementById('canteenPdf').click()">
|
||||||
|
<span class="meal-dropzone-icon">📥</span>
|
||||||
|
<span id="pdfFileName"><?= tr('meal_drop_pdf') ?></span>
|
||||||
|
<input type="file" id="canteenPdf" name="pdf_file" accept=".pdf" style="display: none;" required onchange="document.getElementById('pdfFileName').innerText = this.files[0] ? this.files[0].name : '<?= addslashes(tr('meal_drop_pdf')) ?>';">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="pf-btn btn-secondary" onclick="document.getElementById('importMenuModal').classList.remove('open'); document.body.classList.remove('no-scroll');"><?= tr('btn_cancel') ?></button>
|
||||||
|
<button type="submit" class="pf-btn" id="btnImportPdf"><?= tr('btn_import') ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Sauvegarde automatique AJAX à chaque modification
|
||||||
|
document.querySelectorAll('.js-meal-input').forEach(input => {
|
||||||
|
input.addEventListener('change', async function() {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'save_cell');
|
||||||
|
fd.append('plan_date', this.dataset.d);
|
||||||
|
fd.append('service', this.dataset.s);
|
||||||
|
fd.append('person_id', this.dataset.p);
|
||||||
|
fd.append('meal_name', this.value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pachaFetch('/modules/food/includes/api/save-meals.php', { method: 'POST', body: fd });
|
||||||
|
flashSuccess(this);
|
||||||
|
} catch(e) {
|
||||||
|
console.error("Save error", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus Context Bar
|
||||||
|
let copyState = { type: null, sourceDate: null, service: null, personIdx: null, sourceElements: [], fullDayName: '' };
|
||||||
|
|
||||||
|
document.addEventListener('focusin', (e) => {
|
||||||
|
if(e.target.classList.contains('js-meal-input')) {
|
||||||
|
copyState = {
|
||||||
|
type: 'cell',
|
||||||
|
sourceDate: e.target.getAttribute('data-d'),
|
||||||
|
service: e.target.getAttribute('data-s'),
|
||||||
|
personIdx: e.target.getAttribute('data-p'),
|
||||||
|
sourceElements: [e.target],
|
||||||
|
fullDayName: ''
|
||||||
|
};
|
||||||
|
document.getElementById('contextToolbar').classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('focusout', (e) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if(document.activeElement && !document.activeElement.classList.contains('js-meal-input')) {
|
||||||
|
document.getElementById('contextToolbar').classList.remove('active');
|
||||||
|
}
|
||||||
|
}, 150);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actions Toolbar
|
||||||
|
window.applyToAllInRow = async function() {
|
||||||
|
if(copyState.type !== 'cell' || !copyState.sourceElements[0].value.trim()) return;
|
||||||
|
const val = copyState.sourceElements[0].value;
|
||||||
|
const inputs = document.querySelectorAll(`.js-meal-input[data-d="${copyState.sourceDate}"][data-s="${copyState.service}"]`);
|
||||||
|
|
||||||
|
let updates = [];
|
||||||
|
inputs.forEach(inp => {
|
||||||
|
if(inp !== copyState.sourceElements[0]) {
|
||||||
|
inp.value = val;
|
||||||
|
flashSuccess(inp);
|
||||||
|
updates.push({ plan_date: inp.dataset.d, service: inp.dataset.s, person_id: inp.dataset.p, meal_name: val });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updates.length > 0) {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'save_bulk');
|
||||||
|
fd.append('updates', JSON.stringify(updates));
|
||||||
|
await pachaFetch('/modules/food/includes/api/save-meals.php', { method: 'POST', body: fd });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.openCopySheetFromCell = function() {
|
||||||
|
if(!copyState.sourceElements[0] || !copyState.sourceElements[0].value.trim()) return;
|
||||||
|
if(document.activeElement) document.activeElement.blur();
|
||||||
|
|
||||||
|
const svcLabel = copyState.service === 'lunch' ? (window.I18N['meal_lunch'] || 'Midi') : (window.I18N['meal_dinner'] || 'Soir');
|
||||||
|
const dObj = new Date(copyState.sourceDate);
|
||||||
|
const dayKey = dObj.toLocaleDateString('en-US', {weekday: 'short'}).toLowerCase();
|
||||||
|
const dayName = window.I18N['day_full_' + dayKey] || dObj.toLocaleDateString();
|
||||||
|
|
||||||
|
document.getElementById('copySourceLabel').innerText = `Source : ${window.I18N['source_individual'] || 'Individuel'} (${dayName} ${svcLabel})`;
|
||||||
|
|
||||||
|
document.getElementById('copyModal').classList.add('open');
|
||||||
|
document.body.classList.add('no-scroll');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.openCopySheetFromRow = function(dateStr, service, fullDayName) {
|
||||||
|
const inputs = document.querySelectorAll(`.js-meal-input[data-d="${dateStr}"][data-s="${service}"]`);
|
||||||
|
if (!Array.from(inputs).some(inp => inp.value.trim() !== '')) return;
|
||||||
|
|
||||||
|
copyState = { type: 'row', sourceDate: dateStr, service: service, sourceElements: Array.from(inputs), fullDayName: fullDayName };
|
||||||
|
const svcLabel = service === 'lunch' ? (window.I18N['meal_lunch'] || 'Midi') : (window.I18N['meal_dinner'] || 'Soir');
|
||||||
|
document.getElementById('copySourceLabel').innerText = `Source : ${window.I18N['source_row'] || 'Repas commun'} (${fullDayName} ${svcLabel})`;
|
||||||
|
|
||||||
|
document.getElementById('copyModal').classList.add('open');
|
||||||
|
document.body.classList.add('no-scroll');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closeCopyModal = function() {
|
||||||
|
document.getElementById('copyModal').classList.remove('open');
|
||||||
|
document.body.classList.remove('no-scroll');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.executeCopy = async function(targetDate, targetService) {
|
||||||
|
let updates = [];
|
||||||
|
|
||||||
|
if(copyState.type === 'cell') {
|
||||||
|
const val = copyState.sourceElements[0].value;
|
||||||
|
const targetInput = document.querySelector(`.js-meal-input[data-d="${targetDate}"][data-s="${targetService}"][data-p="${copyState.personIdx}"]`);
|
||||||
|
if(targetInput) {
|
||||||
|
targetInput.value = val;
|
||||||
|
flashSuccess(targetInput);
|
||||||
|
updates.push({ plan_date: targetDate, service: targetService, person_id: copyState.personIdx, meal_name: val });
|
||||||
|
}
|
||||||
|
} else if (copyState.type === 'row') {
|
||||||
|
const targetInputs = document.querySelectorAll(`.js-meal-input[data-d="${targetDate}"][data-s="${targetService}"]`);
|
||||||
|
copyState.sourceElements.forEach((inp, idx) => {
|
||||||
|
if(inp.value.trim() !== '') {
|
||||||
|
targetInputs[idx].value = inp.value;
|
||||||
|
flashSuccess(targetInputs[idx]);
|
||||||
|
updates.push({ plan_date: targetDate, service: targetService, person_id: targetInputs[idx].dataset.p, meal_name: inp.value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
closeCopyModal();
|
||||||
|
|
||||||
|
if (updates.length > 0) {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'save_bulk');
|
||||||
|
fd.append('updates', JSON.stringify(updates));
|
||||||
|
await pachaFetch('/modules/meals/includes/api/save-meals.php', { method: 'POST', body: fd });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function flashSuccess(input) {
|
||||||
|
input.style.transition = 'background 0.3s';
|
||||||
|
input.style.background = 'var(--alert-success-bg)';
|
||||||
|
setTimeout(() => input.style.background = 'transparent', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOUVEAU : Fonction Javascript d'Import PDF
|
||||||
|
window.importPdfMenu = async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const btn = document.getElementById('btnImportPdf');
|
||||||
|
const oldText = btn.innerText;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerText = '⏳...';
|
||||||
|
|
||||||
|
const fd = new FormData(form);
|
||||||
|
const actionUrl = form.getAttribute('action');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await pachaFetch(actionUrl, { method: 'POST', body: fd });
|
||||||
|
if (res && res.success) {
|
||||||
|
document.getElementById('importMenuModal').classList.remove('open');
|
||||||
|
document.body.classList.remove('no-scroll');
|
||||||
|
if (window.showToast) showToast(window.I18N['meal_import_success'] || 'Menus importés !', 'success');
|
||||||
|
setTimeout(() => window.location.reload(), 1000);
|
||||||
|
} else {
|
||||||
|
if (window.showToast) showToast(res.error || window.I18N['error_occured'], 'error');
|
||||||
|
else alert(res.error);
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
if (window.showToast) showToast(window.I18N['error_occured'], 'error');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerText = oldText;
|
||||||
|
form.reset();
|
||||||
|
document.getElementById('pdfFileName').innerText = window.I18N['meal_drop_pdf'] || 'Cliquer pour ajouter le PDF';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -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; }
|
|
||||||
+4
-4
@@ -41,7 +41,7 @@ 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', '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])));
|
$enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m])));
|
||||||
if (empty($enabled)) {
|
if (empty($enabled)) {
|
||||||
$error = tr('set_err_min_module');
|
$error = tr('set_err_min_module');
|
||||||
@@ -404,7 +404,7 @@ require __DIR__ . '/header.php';
|
|||||||
'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')],
|
||||||
'liste' => ['icon' => '📝', 'label' => tr('menu_liste')],
|
'food' => ['icon' => '🍳', 'label' => tr('menu_food')],
|
||||||
'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')],
|
||||||
@@ -428,8 +428,8 @@ require __DIR__ . '/header.php';
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$enabledModsForListe = $_SESSION['enabled_modules'] ?? [];
|
$enabledModsForFood = $_SESSION['enabled_modules'] ?? [];
|
||||||
if ($family_id && in_array('liste', $enabledModsForListe, true)):
|
if ($family_id && in_array('food', $enabledModsForFood, true)):
|
||||||
?>
|
?>
|
||||||
<section class="pf-panel-card">
|
<section class="pf-panel-card">
|
||||||
<h2 class="pf-card-h2 pf-card-h2--tight">📝 <?= htmlspecialchars(tr('liste_settings_title')) ?></h2>
|
<h2 class="pf-card-h2 pf-card-h2--tight">📝 <?= htmlspecialchars(tr('liste_settings_title')) ?></h2>
|
||||||
|
|||||||
Reference in new issue
Block a user