diff --git a/amelioration_shopping.md b/amelioration_shopping.md index c660aaf..6b72c37 100644 --- a/amelioration_shopping.md +++ b/amelioration_shopping.md @@ -12,6 +12,7 @@ - **Champs enrichis articles** : description, prix, quantité par unité, boutique par défaut, code-barres - **Champs enrichis boutiques** : URL, type (alimentaire, bricolage, jardinage…) - **Historique** via modal "Historique" pour retrouver les listes terminées +- **Sélecteur catalogue** lors de l'ajout d'un article : liste scrollable alphabétique, filtrée en temps réel, sélection en un tap (pré-remplit unité), ou saisie libre si hors catalogue - **Wake lock** activé dès qu'une liste est ouverte (écran allumé en magasin) ## En attente / Idées futures diff --git a/frontend/src/pages/ShoppingPage.tsx b/frontend/src/pages/ShoppingPage.tsx index a25dce3..a4071e3 100644 --- a/frontend/src/pages/ShoppingPage.tsx +++ b/frontend/src/pages/ShoppingPage.tsx @@ -1,9 +1,10 @@ // frontend/src/pages/ShoppingPage.tsx import { useState, useEffect, useCallback } from 'react' -import type { ShoppingListDetail, ShoppingList, Store } from '../api/shopping' +import type { ShoppingListDetail, ShoppingList, Store, Product } from '../api/shopping' import { fetchLists, createList, fetchListDetail, deleteList, addItem, updateItem, deleteItem, finishShopping, fetchStores, generateMagicList, + searchProducts, } from '../api/shopping' import Modal from '../components/Modal' import ItemRow from '../components/shopping/ItemRow' @@ -29,6 +30,7 @@ export default function ShoppingPage() { const [currentList, setCurrentList] = useState(null) const [allLists, setAllLists] = useState([]) const [stores, setStores] = useState([]) + const [products, setProducts] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [generating, setGenerating] = useState(false) @@ -38,7 +40,8 @@ export default function ShoppingPage() { const [showCatalogueModal, setShowCatalogueModal] = useState(false) const [showBoutiquesModal, setShowBoutiquesModal] = useState(false) - const [newItemName, setNewItemName] = useState('') + const [itemSearch, setItemSearch] = useState('') + const [selectedProduct, setSelectedProduct] = useState(null) const [newItemQty, setNewItemQty] = useState('') const [newItemUnit, setNewItemUnit] = useState('') @@ -48,9 +51,10 @@ export default function ShoppingPage() { setLoading(true) setError(null) try { - const [listsData, storesData] = await Promise.all([fetchLists(), fetchStores()]) + const [listsData, storesData, productsData] = await Promise.all([fetchLists(), fetchStores(), searchProducts()]) setAllLists(listsData) setStores(storesData) + setProducts([...productsData].sort((a, b) => a.name.localeCompare(b.name, 'fr'))) const current = listsData.find(l => l.status === 'draft' || l.status === 'active') if (current) { @@ -100,18 +104,33 @@ export default function ShoppingPage() { } } + function closeAddItemModal() { + setItemSearch('') + setSelectedProduct(null) + setNewItemQty('') + setNewItemUnit('') + setShowAddItemModal(false) + } + + function selectProduct(p: Product) { + setSelectedProduct(p) + setItemSearch(p.name) + setNewItemUnit(p.default_unit ?? '') + } + async function handleAddItem() { - if (!currentList || !newItemName.trim()) return + if (!currentList) return + const hasProduct = selectedProduct !== null + const customName = hasProduct ? undefined : itemSearch.trim() + if (!hasProduct && !customName) return try { await addItem(currentList.id, { - custom_name: newItemName.trim(), + product_id: selectedProduct?.id, + custom_name: customName, quantity: newItemQty || undefined, unit: newItemUnit || undefined, }) - setNewItemName('') - setNewItemQty('') - setNewItemUnit('') - setShowAddItemModal(false) + closeAddItemModal() void refreshCurrentList() } catch { setError("Erreur lors de l'ajout") @@ -180,6 +199,10 @@ export default function ShoppingPage() { const hasCurrentList = currentList !== null const pastLists = allLists.filter(l => l.status === 'done') + const filteredProducts = products.filter( + p => !itemSearch.trim() || p.name.toLowerCase().includes(itemSearch.trim().toLowerCase()) + ) + return (
{/* ── En-tête ── */} @@ -376,15 +399,68 @@ export default function ShoppingPage() { {/* ── Modals ── */} {showAddItemModal && ( - setShowAddItemModal(false)}> + + {/* Barre de recherche / nom personnalisé */} setNewItemName(e.target.value)} + style={{ + ...inputStyle, + borderColor: selectedProduct ? 'var(--ok)' : 'var(--bg-5)', + borderWidth: selectedProduct ? 2 : 1, + }} + placeholder="Rechercher ou saisir un article…" + value={itemSearch} + onChange={e => { setItemSearch(e.target.value); setSelectedProduct(null) }} autoFocus onKeyDown={e => e.key === 'Enter' && void handleAddItem()} /> + + {/* Liste scrollable alphabétique du catalogue */} +
+ {filteredProducts.length === 0 && ( +

+ {itemSearch.trim() ? `"${itemSearch}" — sera ajouté comme article libre` : 'Catalogue vide'} +

+ )} + {filteredProducts.map((p, idx) => { + const isSelected = selectedProduct?.id === p.id + return ( +
isSelected ? (setSelectedProduct(null), setItemSearch('')) : selectProduct(p)} + style={{ + padding: '10px 14px', + cursor: 'pointer', + background: isSelected ? 'var(--ok)' : 'transparent', + color: isSelected ? '#1d2021' : 'var(--ink-1)', + borderBottom: idx < filteredProducts.length - 1 ? '1px solid var(--bg-4)' : 'none', + display: 'flex', alignItems: 'center', gap: 10, + userSelect: 'none', + }} + > +
+
+ {p.name} +
+ {(p.default_unit || p.brand) && ( +
+ {[p.brand, p.default_unit].filter(Boolean).join(' · ')} +
+ )} +
+ {isSelected && } +
+ ) + })} +
+ + {/* Quantité + Unité */}
setNewItemUnit(e.target.value)} />
+