diff --git a/docker/schema_family.sql b/docker/schema_family.sql index 321070f..2b89602 100644 --- a/docker/schema_family.sql +++ b/docker/schema_family.sql @@ -345,6 +345,16 @@ CREATE TABLE IF NOT EXISTS pf_todos ( FOREIGN KEY (list_id) REFERENCES pf_todo_lists(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +-- ─── Liste de courses ───────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS pf_grocery_items ( + id INT AUTO_INCREMENT PRIMARY KEY, + label VARCHAR(500) NOT NULL, + in_cart TINYINT(1) NOT NULL DEFAULT 0, + position INT NOT NULL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + -- ─── Calendar iOS ───────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS pf_calendar_events ( id INT AUTO_INCREMENT PRIMARY KEY, diff --git a/groceries.php b/groceries.php new file mode 100644 index 0000000..2b5840b --- /dev/null +++ b/groceries.php @@ -0,0 +1,35 @@ + + + +
+ +
+
+

🛒

+

+

+
+ +
+ + +
+ +
+ + +
+ +
+
+ + + diff --git a/header.php b/header.php index 4bcc86b..561910e 100644 --- a/header.php +++ b/header.php @@ -55,6 +55,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr'; + @@ -103,6 +104,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr'; 🚗 📝 + 🛒 📱 ⚙️ Paramètres diff --git a/includes/lang/ca.php b/includes/lang/ca.php index 2e79996..1c3938d 100644 --- a/includes/lang/ca.php +++ b/includes/lang/ca.php @@ -99,6 +99,31 @@ return [ 'mod_calendar_ios_name' => 'Calendari iOS', 'mod_calendar_ios_desc' => 'Sincronitzeu els esdeveniments de HouseHub amb el calendari de l\'iPhone (iCloud CalDAV).', 'menu_todo' => 'Todo', + 'mod_groceries_name' => 'Compra', + 'mod_groceries_desc' => 'Llista compartida: afegiu productes i marqueu-los quan siguin al carretó.', + 'menu_groceries' => 'Compra', + 'groceries_placeholder_add' => 'Ex : llet, pa, tomàquets…', + 'groceries_btn_add' => 'Afegir', + 'groceries_btn_uncheck_all' => 'Tornar a marcar com a pendents', + 'groceries_btn_delete_picked' => 'Eliminar els marcats', + 'groceries_subtitle' => '{pending} pendents · {cart} al carretó', + 'groceries_added' => 'Afegit', + 'groceries_updated' => 'Actualitzat', + 'groceries_deleted' => 'Eliminat', + 'groceries_reset_next' => 'Llista llesta per a la propera compra', + 'groceries_picked_removed' => 'Articles eliminats', + 'groceries_label_empty' => 'El text no pot ser buit', + 'groceries_confirm_del_title' => 'Eliminar?', + 'groceries_confirm_del_body' => 'Voleu treure aquest producte de la llista?', + 'groceries_confirm_uncheck_title' => 'Tornar a marcar com a pendents', + 'groceries_confirm_uncheck_body' => 'Es netejaran les marques del carretó. Els productes es mantindran a la llista.', + 'groceries_confirm_clear_title' => 'Eliminar els marcats', + 'groceries_confirm_clear_body' => 'Tots els articles marcats se suprimiran de la llista.', + 'groceries_empty' => 'Encara no hi ha res. Afegiu un producte a dalt.', + 'groceries_aria_toggle' => 'Marcar com al carretó o pendent', + 'groceries_section_pending' => 'Pendents', + 'groceries_section_cart' => 'Al carretó', + 'groceries_prompt_edit' => 'Editar el producte', 'cta_open' => 'Obrir', 'cta_explore' => 'Explorar', 'cta_view_lists' => 'Desembolicar', @@ -106,6 +131,7 @@ return [ 'cta_service' => 'Revisar', 'cta_jot' => 'Apuntar', 'cta_check' => 'Marcar', + 'cta_cart' => 'Anar a comprar', 'cta_sync' => 'Sincronitzar', // ========================================== diff --git a/includes/lang/en.php b/includes/lang/en.php index d9fedc8..4aab305 100644 --- a/includes/lang/en.php +++ b/includes/lang/en.php @@ -100,6 +100,31 @@ return [ 'mod_calendar_ios_name' => 'iOS Calendar', 'mod_calendar_ios_desc' => 'Sync HouseHub events with your iPhone calendar (iCloud CalDAV).', 'menu_todo' => 'Todo', + 'mod_groceries_name' => 'Groceries', + 'mod_groceries_desc' => 'Shared list: add items and check them off when they are in your cart.', + 'menu_groceries' => 'Groceries', + 'groceries_placeholder_add' => 'e.g. milk, bread, tomatoes…', + 'groceries_btn_add' => 'Add', + 'groceries_btn_uncheck_all' => 'Reset cart checks', + 'groceries_btn_delete_picked' => 'Remove checked items', + 'groceries_subtitle' => '{pending} to pick up · {cart} in cart', + 'groceries_added' => 'Added', + 'groceries_updated' => 'Updated', + 'groceries_deleted' => 'Removed', + 'groceries_reset_next' => 'List ready for your next trip', + 'groceries_picked_removed' => 'Checked items removed', + 'groceries_label_empty' => 'Label cannot be empty', + 'groceries_confirm_del_title' => 'Delete?', + 'groceries_confirm_del_body' => 'Remove this product from the list?', + 'groceries_confirm_uncheck_title' => 'Reset cart checks', + 'groceries_confirm_uncheck_body' => 'All “in cart” checkmarks will be cleared. Items stay on the list.', + 'groceries_confirm_clear_title' => 'Remove checked items', + 'groceries_confirm_clear_body' => 'Every checked item will be deleted from the list.', + 'groceries_empty' => 'Nothing yet. Add a product above.', + 'groceries_aria_toggle' => 'Mark as in cart or still to pick up', + 'groceries_section_pending' => 'To pick up', + 'groceries_section_cart' => 'In cart', + 'groceries_prompt_edit' => 'Edit product', 'cta_open' => 'Open', 'cta_explore' => 'Explore', 'cta_view_lists' => 'Unwrap', @@ -107,6 +132,7 @@ return [ 'cta_service' => 'Service', 'cta_jot' => 'Jot down', 'cta_check' => 'Check off', + 'cta_cart' => 'Go shopping', 'cta_sync' => 'Sync', // ========================================== diff --git a/includes/lang/fr.php b/includes/lang/fr.php index 6966dc1..61aa7ca 100644 --- a/includes/lang/fr.php +++ b/includes/lang/fr.php @@ -102,13 +102,38 @@ return [ 'mod_calendar_ios_name' => 'Calendrier iOS', 'mod_calendar_ios_desc' => 'Synchronisez vos événements HouseHub avec votre calendrier iPhone (iCloud CalDAV).', 'menu_todo' => 'Todo', + 'mod_groceries_name' => 'Courses', + 'mod_groceries_desc' => 'Liste partagée : ajoutez des produits et cochez-les quand ils sont dans le caddie.', + 'menu_groceries' => 'Courses', + 'groceries_placeholder_add' => 'Ex : lait, pain, tomates…', + 'groceries_btn_add' => 'Ajouter', + 'groceries_btn_uncheck_all' => 'Tout remettre à prendre', + 'groceries_btn_delete_picked' => 'Retirer du caddie (supprimer)', + 'groceries_subtitle' => '{pending} à prendre · {cart} dans le caddie', + 'groceries_added' => 'Ajouté', + 'groceries_updated' => 'Mis à jour', + 'groceries_deleted' => 'Supprimé', + 'groceries_reset_next' => 'Liste prête pour la prochaine sortie', + 'groceries_picked_removed' => 'Articles retirés', + 'groceries_label_empty' => 'Libellé vide', + 'groceries_confirm_del_title' => 'Supprimer ?', + 'groceries_confirm_del_body' => 'Retirer ce produit de la liste ?', + 'groceries_confirm_uncheck_title' => 'Tout remettre à prendre', + 'groceries_confirm_uncheck_body' => 'Les coches « dans le caddie » seront effacées. Les produits restent dans la liste.', + 'groceries_confirm_clear_title' => 'Retirer les produits du caddie', + 'groceries_confirm_clear_body' => 'Tous les articles cochés seront supprimés de la liste.', + 'groceries_empty' => 'Rien pour le moment. Ajoutez un produit ci-dessus.', + 'groceries_aria_toggle' => 'Marquer comme dans le caddie ou à prendre', + 'groceries_section_pending' => 'À prendre', + 'groceries_section_cart' => 'Dans le caddie', + 'groceries_prompt_edit' => 'Modifier le produit', 'cta_open' => 'Ouvrir', 'cta_explore' => 'Explorer', - 'cta_view_lists' => 'Déballer', 'cta_manage' => 'Gérer', 'cta_service' => 'Réviser', 'cta_jot' => 'Griffonner', 'cta_check' => 'Cocher', + 'cta_cart' => 'Faire les courses', 'cta_sync' => 'Synchroniser', // ========================================== diff --git a/index.php b/index.php index f6acd15..6f0977c 100644 --- a/index.php +++ b/index.php @@ -109,6 +109,15 @@ if ($_has_custom_bg): ?> + + +
🛒
+

+
+ +
+ +
📱
diff --git a/modules/groceries/api.php b/modules/groceries/api.php new file mode 100644 index 0000000..10d6b4a --- /dev/null +++ b/modules/groceries/api.php @@ -0,0 +1,126 @@ + false, 'error' => $e->getMessage()]); + exit; +}); + +require_once dirname(__DIR__, 2) . '/includes/db.php'; + +$pdo->exec(" +CREATE TABLE IF NOT EXISTS pf_grocery_items ( + id INT AUTO_INCREMENT PRIMARY KEY, + label VARCHAR(500) NOT NULL, + in_cart TINYINT(1) NOT NULL DEFAULT 0, + position INT NOT NULL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 +"); + +$method = $_SERVER['REQUEST_METHOD']; +$action = $_GET['action'] ?? ''; + +function gOk($d) +{ + echo json_encode(['ok' => true, 'data' => $d], JSON_UNESCAPED_UNICODE); + exit; +} +function gErr($m, $c = 400) +{ + http_response_code($c); + echo json_encode(['ok' => false, 'error' => $m]); + exit; +} +function gBody() +{ + return json_decode(file_get_contents('php://input'), true) ?? []; +} + +if ($action === 'items') { + if ($method === 'GET') { + $rows = $pdo->query( + "SELECT id, label, in_cart, position, created_at, updated_at + FROM pf_grocery_items + ORDER BY in_cart ASC, position ASC, id ASC" + )->fetchAll(PDO::FETCH_ASSOC); + foreach ($rows as &$r) { + $r['in_cart'] = (int) $r['in_cart']; + $r['id'] = (int) $r['id']; + $r['position'] = (int) $r['position']; + } + unset($r); + gOk($rows); + } + + if ($method === 'POST') { + $d = gBody(); + $label = trim($d['label'] ?? ''); + if ($label === '') { + gErr('Libellé requis'); + } + $max = (int) $pdo->query('SELECT COALESCE(MAX(position),0) FROM pf_grocery_items')->fetchColumn(); + $pdo->prepare('INSERT INTO pf_grocery_items (label, position) VALUES (?,?)')->execute([$label, $max + 1]); + $id = (int) $pdo->lastInsertId(); + $s = $pdo->prepare('SELECT id, label, in_cart, position, created_at, updated_at FROM pf_grocery_items WHERE id=?'); + $s->execute([$id]); + $row = $s->fetch(PDO::FETCH_ASSOC); + $row['in_cart'] = (int) $row['in_cart']; + $row['id'] = (int) $row['id']; + $row['position'] = (int) $row['position']; + gOk($row); + } + + if ($method === 'PUT') { + $id = isset($_GET['id']) ? (int) $_GET['id'] : 0; + if ($id < 1) { + gErr('ID manquant'); + } + $d = gBody(); + if (array_key_exists('in_cart', $d)) { + $in = !empty($d['in_cart']) ? 1 : 0; + $pdo->prepare('UPDATE pf_grocery_items SET in_cart=?, updated_at=NOW() WHERE id=?')->execute([$in, $id]); + gOk(['in_cart' => $in]); + } + $label = trim($d['label'] ?? ''); + if ($label === '') { + gErr('Libellé requis'); + } + $pdo->prepare('UPDATE pf_grocery_items SET label=?, updated_at=NOW() WHERE id=?')->execute([$label, $id]); + gOk(['updated' => true]); + } + + if ($method === 'DELETE') { + $id = isset($_GET['id']) ? (int) $_GET['id'] : 0; + if ($id < 1) { + gErr('ID manquant'); + } + $pdo->prepare('DELETE FROM pf_grocery_items WHERE id=?')->execute([$id]); + gOk(['deleted' => true]); + } +} + +if ($action === 'uncheck_all' && $method === 'POST') { + $pdo->exec('UPDATE pf_grocery_items SET in_cart=0'); + gOk(['updated' => true]); +} + +if ($action === 'delete_picked' && $method === 'POST') { + $pdo->exec('DELETE FROM pf_grocery_items WHERE in_cart=1'); + gOk(['deleted' => true]); +} + +gErr('Action inconnue', 404); diff --git a/modules/groceries/assets/groceries.css b/modules/groceries/assets/groceries.css new file mode 100644 index 0000000..08addd6 --- /dev/null +++ b/modules/groceries/assets/groceries.css @@ -0,0 +1,174 @@ +/* HouseHub — Courses (liste de courses) */ +.groceries-layout { + max-width: 560px; + margin: 0 auto; + padding: 0 1rem 2.5rem; +} + +.groceries-hero { + padding: 1.25rem 0 1rem; +} +.groceries-hero h1 { + font-size: 1.35rem; + font-weight: 700; + margin: 0 0 0.35rem; + color: var(--text-main); +} +.groceries-hero p { + margin: 0; + font-size: 0.9rem; + color: var(--text-muted); +} + +.groceries-quick-add { + display: flex; + gap: 0.5rem; + margin-bottom: 1.25rem; +} +.groceries-quick-add input { + flex: 1; + min-width: 0; +} + +.groceries-toolbar { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.groceries-section-title { + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); + margin: 1rem 0 0.5rem; +} +.groceries-section-title:first-of-type { + margin-top: 0; +} + +.groceries-list { + display: flex; + flex-direction: column; + gap: 0.45rem; +} + +.groceries-row { + display: flex; + align-items: center; + gap: 0.65rem; + padding: 0.65rem 0.75rem; + background: var(--bg-panel); + border: 1px solid var(--border-light); + border-radius: 10px; + transition: border-color 0.12s, box-shadow 0.12s; +} +.groceries-row:hover { + border-color: #cbd5e1; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +} +.groceries-row.in-cart { + opacity: 0.62; +} +.groceries-row.in-cart .groceries-label { + text-decoration: line-through; + color: var(--text-muted); +} + +.groceries-check { + width: 2.25rem; + height: 2.25rem; + border-radius: 8px; + border: 2px solid var(--border-light); + background: var(--bg-page); + cursor: pointer; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.95rem; + color: transparent; + transition: background 0.12s, border-color 0.12s, color 0.12s; +} +.groceries-check:hover { + border-color: var(--primary); + background: #eff6ff; +} +.groceries-check.checked { + background: var(--success); + border-color: var(--success); + color: #fff; +} +.groceries-check.checked::after { + content: '✓'; +} + +.groceries-label { + flex: 1; + min-width: 0; + font-size: 0.95rem; + font-weight: 500; + color: var(--text-main); + line-height: 1.35; + word-break: break-word; +} + +.groceries-row-actions { + display: flex; + gap: 0.25rem; + flex-shrink: 0; +} + +.groceries-empty { + text-align: center; + padding: 3rem 1.5rem; + color: var(--text-muted); + font-size: 0.9rem; +} +.groceries-empty .groceries-empty-icon { + font-size: 2.75rem; + opacity: 0.35; + margin-bottom: 0.75rem; +} + +.groceries-toast-container { + position: fixed; + bottom: 1.5rem; + right: 1.5rem; + z-index: 9999; + display: flex; + flex-direction: column; + gap: 0.5rem; +} +.groceries-toast { + background: var(--bg-panel); + border: 1px solid var(--border-light); + border-radius: 10px; + padding: 0.7rem 1rem; + font-size: 0.875rem; + min-width: 200px; + color: var(--text-main); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); + border-left: 4px solid var(--primary); +} +.groceries-toast.error { + border-left-color: var(--danger); +} + +[data-theme='dark'] .groceries-row { + background: var(--bg-panel); + border-color: var(--border-light); +} +[data-theme='dark'] .groceries-row:hover { + border-color: #484f58; +} +[data-theme='dark'] .groceries-check { + border-color: #484f58; + background: var(--bg-page); +} +[data-theme='dark'] .groceries-check:hover { + background: rgba(59, 130, 246, 0.15); + border-color: var(--primary); +} diff --git a/modules/groceries/assets/groceries.js b/modules/groceries/assets/groceries.js new file mode 100644 index 0000000..a4d8ef1 --- /dev/null +++ b/modules/groceries/assets/groceries.js @@ -0,0 +1,245 @@ +// HouseHub — Liste de courses +const API = '/modules/groceries/api.php'; + +function escHtml(s) { + const d = document.createElement('div'); + d.textContent = String(s ?? ''); + return d.innerHTML; +} + +function T(key, fallback) { + if (typeof tr === 'function') { + const v = tr(key); + if (v && v !== key) return v; + } + return fallback; +} + +function escAttr(s) { + return String(s ?? '') + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/ t.remove(), 3000); +} + +async function api(action, method = 'GET', data = null, extra = '') { + const opts = { + method, + credentials: 'same-origin', + headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, + }; + if (data) { + opts.headers['Content-Type'] = 'application/json'; + opts.body = JSON.stringify(data); + } + const r = await fetch(API + '?action=' + action + extra, opts); + const text = await r.text(); + let j; + try { + j = JSON.parse(text); + } catch (e) { + console.error('Bad JSON from API:', text.slice(0, 200)); + throw new Error(T('error_occured', 'Erreur serveur')); + } + if (!j.ok) throw new Error(j.error || T('error_occured', 'Erreur')); + return j.data; +} + +async function loadItems() { + try { + const items = await api('items', 'GET'); + render(items); + updateSubtitle(items); + } catch (e) { + toast(e.message || T('error_occured', 'Erreur'), 'error'); + } +} + +function updateSubtitle(items) { + const el = document.getElementById('groceries-subtitle'); + if (!el) return; + const pending = items.filter((i) => !parseInt(i.in_cart, 10)).length; + const cart = items.filter((i) => parseInt(i.in_cart, 10)).length; + if (!items.length) { + el.textContent = ''; + return; + } + const tpl = typeof tr === 'function' ? tr('groceries_subtitle') : ''; + el.textContent = (tpl && tpl !== 'groceries_subtitle' ? tpl : '{pending} à prendre · {cart} dans le caddie') + .replace('{pending}', String(pending)) + .replace('{cart}', String(cart)); +} + +function render(items) { + const wrap = document.getElementById('groceries-lists'); + if (!items.length) { + const msg = T('groceries_empty', 'Rien pour le moment. Ajoutez un produit ci-dessus.'); + wrap.innerHTML = + '
🛒

' + escHtml(msg) + '

'; + return; + } + const pending = items.filter((i) => !parseInt(i.in_cart, 10)); + const cart = items.filter((i) => parseInt(i.in_cart, 10)); + const labPending = escHtml(T('groceries_section_pending', 'À prendre')); + const labCart = escHtml(T('groceries_section_cart', 'Dans le caddie')); + let html = ''; + if (pending.length) { + html += '
' + labPending + '
'; + pending.forEach((i) => { + html += rowHtml(i, false); + }); + html += '
'; + } + if (cart.length) { + html += '
' + labCart + '
'; + cart.forEach((i) => { + html += rowHtml(i, true); + }); + html += '
'; + } + wrap.innerHTML = html; +} + +function rowHtml(i, inCart) { + const id = i.id; + const checkClass = 'groceries-check' + (inCart ? ' checked' : ''); + const rowClass = 'groceries-row' + (inCart ? ' in-cart' : ''); + const aria = escAttr(T('groceries_aria_toggle', 'Marquer dans le caddie')); + return ( + '
' + + '' + + '
' + + escHtml(i.label) + + '
' + + '
' + + '' + + '' + + '
' + ); +} + +async function addFromInput() { + const input = document.getElementById('groceries-new'); + const label = (input.value || '').trim(); + if (!label) return; + try { + await api('items', 'POST', { label: label }); + input.value = ''; + input.focus(); + toast(T('groceries_added', 'Ajouté')); + loadItems(); + } catch (e) { + toast(e.message || T('error_occured', 'Erreur'), 'error'); + } +} + +async function toggleCart(id, next) { + try { + await api('items', 'PUT', { in_cart: !!parseInt(next, 10) }, '&id=' + id); + loadItems(); + } catch (e) { + toast(e.message || T('error_occured', 'Erreur'), 'error'); + } +} + +async function editItem(id) { + const row = document.querySelector('.groceries-row[data-id="' + id + '"]'); + const current = row ? row.querySelector('.groceries-label').textContent : ''; + const label = window.prompt(T('groceries_prompt_edit', 'Modifier le produit'), current); + if (label === null) return; + const t = label.trim(); + if (!t) { + toast(T('groceries_label_empty', 'Libellé vide'), 'error'); + return; + } + try { + await api('items', 'PUT', { label: t }, '&id=' + id); + toast(T('groceries_updated', 'Mis à jour')); + loadItems(); + } catch (e) { + toast(e.message || T('error_occured', 'Erreur'), 'error'); + } +} + +async function deleteItem(id) { + const ok = await pachaConfirm( + T('groceries_confirm_del_title', 'Supprimer ?'), + T('groceries_confirm_del_body', 'Retirer ce produit de la liste ?') + ); + if (!ok) return; + try { + await api('items', 'DELETE', null, '&id=' + id); + toast(T('groceries_deleted', 'Supprimé')); + loadItems(); + } catch (e) { + toast(e.message || T('error_occured', 'Erreur'), 'error'); + } +} + +async function uncheckAll() { + const ok = await pachaConfirm( + T('groceries_confirm_uncheck_title', 'Tout remettre à prendre'), + T('groceries_confirm_uncheck_body', '') + ); + if (!ok) return; + try { + await api('uncheck_all', 'POST', {}); + toast(T('groceries_reset_next', 'Liste prête pour la prochaine sortie')); + loadItems(); + } catch (e) { + toast(e.message || T('error_occured', 'Erreur'), 'error'); + } +} + +async function deletePicked() { + const ok = await pachaConfirm( + T('groceries_confirm_clear_title', 'Retirer les produits du caddie'), + T('groceries_confirm_clear_body', '') + ); + if (!ok) return; + try { + await api('delete_picked', 'POST', {}); + toast(T('groceries_picked_removed', 'Articles retirés')); + loadItems(); + } catch (e) { + toast(e.message || T('error_occured', 'Erreur'), 'error'); + } +} + +document.addEventListener('DOMContentLoaded', () => { + const input = document.getElementById('groceries-new'); + if (input) { + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + addFromInput(); + } + }); + } + loadItems(); +}); diff --git a/settings.php b/settings.php index 09f2c1c..2ff0a6c 100644 --- a/settings.php +++ b/settings.php @@ -36,7 +36,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; if ($action === 'set_modules' && $family_id) { - $all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'calendar_ios']; + $all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'calendar_ios', 'groceries']; $enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m]))); if (empty($enabled)) { $error = "Vous devez garder au moins un module actif."; @@ -274,6 +274,7 @@ require __DIR__ . '/header.php'; 'memo' => ['icon' => '📝', 'label' => tr('menu_memo')], 'todo' => ['icon' => '✅', 'label' => tr('menu_todo')], 'calendar_ios' => ['icon' => '📱', 'label' => tr('menu_calendar_ios')], + 'groceries' => ['icon' => '🛒', 'label' => tr('menu_groceries')], ]; ?>