// frontend/src/pages/ShoppingPage.tsx import { useState, useEffect, useCallback } from 'react' import { matchesSearch } from '../utils/search' import type { ShoppingListDetail, ShoppingList, Store, Product } from '../api/shopping' import { fetchLists, createList, fetchListDetail, deleteList, addItem, updateItem, deleteItem, finishShopping, fetchStores, generateMagicList, searchProducts, createProduct, } from '../api/shopping' import Modal from '../components/Modal' import ItemRow from '../components/shopping/ItemRow' import CatalogueModal from '../components/shopping/CatalogueModal' import BoutiquesModal from '../components/shopping/BoutiquesModal' import { useWakeLock } from '../hooks/useWakeLock' const inputStyle: React.CSSProperties = { width: '100%', background: 'var(--bg-4)', border: '1px solid var(--bg-5)', borderRadius: 8, padding: '10px 12px', color: 'var(--ink-1)', fontFamily: 'var(--font-ui)', fontSize: 14, boxSizing: 'border-box', } const noSelect: React.CSSProperties = { userSelect: 'none' } 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) const [showAddItemModal, setShowAddItemModal] = useState(false) const [showHistoryModal, setShowHistoryModal] = useState(false) const [showCatalogueModal, setShowCatalogueModal] = useState(false) const [showBoutiquesModal, setShowBoutiquesModal] = useState(false) const [itemSearch, setItemSearch] = useState('') const [selectedProduct, setSelectedProduct] = useState(null) const [newItemQty, setNewItemQty] = useState('') const [newItemUnit, setNewItemUnit] = useState('') const [addToCatalogue, setAddToCatalogue] = useState(true) useWakeLock(currentList !== null) const loadData = useCallback(async () => { setLoading(true) setError(null) try { 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) { setCurrentList(await fetchListDetail(current.id)) } else { setCurrentList(null) } } catch { setError('Erreur lors du chargement') } finally { setLoading(false) } }, []) useEffect(() => { void loadData() }, [loadData]) async function refreshCurrentList() { if (!currentList) return try { setCurrentList(await fetchListDetail(currentList.id)) } catch { setError('Erreur de rafraîchissement') } } async function handleCreateManualList() { try { const detail = await createList({}) setCurrentList(detail) void loadData() } catch { setError('Erreur lors de la création') } } async function handleGenerateMagicList() { setGenerating(true) setError(null) try { const detail = await generateMagicList() setCurrentList(detail) void loadData() } catch { setError('Erreur lors de la génération') } finally { setGenerating(false) } } function closeAddItemModal() { setItemSearch('') setSelectedProduct(null) setNewItemQty('') setNewItemUnit('') setAddToCatalogue(true) setShowAddItemModal(false) } function selectProduct(p: Product) { setSelectedProduct(p) setItemSearch(p.name) setNewItemUnit(p.default_unit ?? '') } async function handleAddItem() { if (!currentList) return const customName = itemSearch.trim() if (!selectedProduct && !customName) return try { let productId = selectedProduct?.id // Article libre + case "Ajouter au catalogue" cochée → créer le produit d'abord if (!selectedProduct && customName && addToCatalogue) { const newProduct = await createProduct({ name: customName, default_unit: newItemUnit || undefined, }) productId = newProduct.id // Met à jour la liste locale des produits pour les prochains ajouts setProducts(prev => [...prev, newProduct].sort((a, b) => a.name.localeCompare(b.name, 'fr'))) } await addItem(currentList.id, { product_id: productId, custom_name: !productId ? customName : undefined, quantity: newItemQty || undefined, unit: newItemUnit || undefined, }) closeAddItemModal() void refreshCurrentList() } catch { setError("Erreur lors de l'ajout") } } async function handleCheckItem(itemId: string, checked: boolean) { if (!currentList) return try { await updateItem(currentList.id, itemId, { is_checked: checked }) void refreshCurrentList() } catch { setError('Erreur lors du cochage') } } async function handleDeleteItem(itemId: string) { if (!currentList) return try { await deleteItem(currentList.id, itemId) void refreshCurrentList() } catch { setError('Erreur lors de la suppression') } } async function handleFinish() { if (!currentList) return try { await finishShopping(currentList.id) void loadData() } catch { setError('Erreur lors de la finalisation') } } async function handleDeleteCurrentList() { if (!currentList) return if (!confirm('Supprimer la liste en cours ?')) return try { await deleteList(currentList.id) void loadData() } catch { setError('Erreur lors de la suppression') } } async function handleOpenHistoryList(list: ShoppingList) { try { const detail = await fetchListDetail(list.id) setCurrentList(detail) setShowHistoryModal(false) } catch { setError('Erreur lors du chargement') } } // Tri : non cochés alpha, cochés alpha (en bas) const sortedItems = [...(currentList?.items ?? [])].sort((a, b) => { if (a.is_checked !== b.is_checked) return a.is_checked ? 1 : -1 return a.display_name.localeCompare(b.display_name, 'fr') }) const uncheckedItems = sortedItems.filter(i => !i.is_checked) const checkedItems = sortedItems.filter(i => i.is_checked) const hasCurrentList = currentList !== null const pastLists = allLists.filter(l => l.status === 'done') const filteredProducts = products.filter(p => { const term = itemSearch.trim() return !term || matchesSearch(p.name, term) || matchesSearch(p.brand ?? '', term) }) return (
{/* ── En-tête ── */}

{hasCurrentList ? (currentList.name ?? 'Courses') : 'Courses'}

{/* ── Erreur ── */} {error && (

{error}

)} {loading && (

Chargement…

)} {/* ── Vue : pas de liste en cours ── */} {!loading && !hasCurrentList && (

Aucune liste de courses en cours

{pastLists.length > 0 && ( )}
)} {/* ── Vue : liste en cours ── */} {!loading && hasCurrentList && ( <> {/* Barre d'info + actions */}
{checkedItems.length}/{currentList.item_count} cochés {pastLists.length > 0 && ( )}
{/* Articles non cochés */} {uncheckedItems.length === 0 && checkedItems.length === 0 && (

Liste vide — ajoutez des articles avec le bouton +

)}
{uncheckedItems.map(item => ( void handleCheckItem(item.id, true)} onDelete={() => void handleDeleteItem(item.id)} storeMode /> ))} {checkedItems.length > 0 && ( <>
Cochés ({checkedItems.length})
{checkedItems.map(item => ( void handleCheckItem(item.id, false)} onDelete={() => void handleDeleteItem(item.id)} storeMode /> ))} )}
{/* FAB + */} )} {/* ── Modals ── */} {showAddItemModal && ( {/* Barre de recherche / nom personnalisé */} { 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}" — 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 && ✓}
) })}
{/* Case "Ajouter au catalogue" — uniquement si article libre sans correspondance */} {!selectedProduct && itemSearch.trim() && filteredProducts.length === 0 && ( )} {/* Quantité + Unité */}
setNewItemQty(e.target.value)} /> setNewItemUnit(e.target.value)} />
)} {showHistoryModal && ( setShowHistoryModal(false)}>
{allLists.filter(l => l.status === 'done').map(list => (
void handleOpenHistoryList(list)} className="glass interactive" style={{ borderRadius: 8, padding: '10px 14px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 10 }} >
{list.name ?? 'Liste terminée'}
{new Date(list.created_at).toLocaleDateString('fr-FR')} · {list.checked_count}/{list.item_count} articles
→
))} {allLists.filter(l => l.status === 'done').length === 0 && (

Aucune liste terminée

)}
)} {showCatalogueModal && ( setShowCatalogueModal(false)} /> )} {showBoutiquesModal && ( setShowBoutiquesModal(false)} onStoresChanged={() => void loadData()} /> )}
) }