// frontend/src/pages/ShoppingPage.tsx import { useState, useEffect, useCallback } from 'react' import { matchesSearch } from '../utils/search' import type { ShoppingListDetail, ShoppingList, Store, Product, ShoppingItem } from '../api/shopping' import { fetchLists, createList, fetchListDetail, deleteList, addItem, updateItem, deleteItem, finishShopping, fetchStores, generateMagicList, searchProducts, } from '../api/shopping' import Modal from '../components/Modal' import BottomSheet from '../components/BottomSheet' 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 [showAddSheet, setShowAddSheet] = useState(false) const [showHistoryModal, setShowHistoryModal] = useState(false) const [showCatalogueModal, setShowCatalogueModal] = useState(false) const [showBoutiquesModal, setShowBoutiquesModal] = useState(false) const [editingItem, setEditingItem] = useState(null) const [editQty, setEditQty] = useState('') const [editUnit, setEditUnit] = useState('') type Selection = { type: 'product'; product: Product } | { type: 'custom'; name: string } const [itemSearch, setItemSearch] = useState('') const [selections, setSelections] = useState([]) const [addSaving, setAddSaving] = useState(false) 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 closeAddSheet() { setItemSearch('') setSelections([]) setShowAddSheet(false) } function toggleProduct(p: Product) { setSelections(prev => { const exists = prev.some(s => s.type === 'product' && s.product.id === p.id) if (exists) return prev.filter(s => !(s.type === 'product' && s.product.id === p.id)) return [...prev, { type: 'product' as const, product: p }] }) } function addCustomItem() { const name = itemSearch.trim() if (!name) return setSelections(prev => { const exists = prev.some(s => s.type === 'custom' && s.name === name) if (exists) return prev return [...prev, { type: 'custom' as const, name }] }) setItemSearch('') } function removeSelection(key: string) { setSelections(prev => prev.filter(s => { if (s.type === 'product') return s.product.id !== key return s.name !== key })) } async function handleConfirmAdd() { if (!currentList || selections.length === 0) return setAddSaving(true) try { for (const sel of selections) { if (sel.type === 'product') { await addItem(currentList.id, { product_id: sel.product.id, unit: sel.product.default_unit || undefined, }) } else { await addItem(currentList.id, { custom_name: sel.name }) } } closeAddSheet() void refreshCurrentList() } catch { setError("Erreur lors de l'ajout") } finally { setAddSaving(false) } } 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') } } function openEditItem(item: ShoppingItem) { setEditingItem(item) setEditQty(item.quantity ?? '') setEditUnit(item.unit ?? '') } async function handleEditItem() { if (!currentList || !editingItem) return try { await updateItem(currentList.id, editingItem.id, { quantity: editQty || undefined, unit: editUnit || undefined, }) setEditingItem(null) void refreshCurrentList() } catch { setError('Erreur lors de la modification') } } 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)} onEdit={() => openEditItem(item)} storeMode /> ))} {checkedItems.length > 0 && ( <>
Cochés ({checkedItems.length})
{checkedItems.map(item => ( void handleCheckItem(item.id, false)} onDelete={() => void handleDeleteItem(item.id)} onEdit={() => openEditItem(item)} storeMode /> ))} )}
{/* FAB + — masqué quand le sheet est ouvert */} {!showAddSheet && ( )} )} {/* ── Modals ── */} {/* Modal édition quantité/unité */} {editingItem && ( setEditingItem(null)} width={320}>
setEditQty(e.target.value)} autoFocus onKeyDown={e => e.key === 'Enter' && void handleEditItem()} /> setEditUnit(e.target.value)} onKeyDown={e => e.key === 'Enter' && void handleEditItem()} />
)} {showAddSheet && ( {/* Recherche */}
setItemSearch(e.target.value)} autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} />
{/* Liste scrollable */}
{/* Articles libres sélectionnés (affichés en tête) */} {selections.filter(s => s.type === 'custom').map(s => (
removeSelection(s.name)} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 16px', cursor: 'pointer', minHeight: 56, background: 'rgba(142,192,124,0.12)', borderBottom: '1px solid var(--bg-4)', }} >
✓
{s.name}
article libre
))} {/* Produits du catalogue */} {filteredProducts.map(p => { const selected = selections.some(s => s.type === 'product' && s.product.id === p.id) return (
toggleProduct(p)} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 16px', cursor: 'pointer', minHeight: 56, background: selected ? 'rgba(142,192,124,0.12)' : 'transparent', borderBottom: '1px solid var(--bg-4)', transition: 'background 0.1s', }} >
{selected && ✓}
{p.name}
{(p.brand || p.default_unit) && (
{[p.brand, p.default_unit].filter(Boolean).join(' · ')}
)}
) })} {/* Article libre si aucun match */} {itemSearch.trim() && filteredProducts.length === 0 && (
+ Ajouter "{itemSearch.trim()}"
)} {filteredProducts.length === 0 && !itemSearch.trim() && (

Catalogue vide — utilisez le bouton Articles

)}
{/* Bouton confirm sticky */}
)} {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()} /> )}
) }