diff --git a/frontend/src/pages/ShoppingPage.tsx b/frontend/src/pages/ShoppingPage.tsx index d10cf3a..1fb7d16 100644 --- a/frontend/src/pages/ShoppingPage.tsx +++ b/frontend/src/pages/ShoppingPage.tsx @@ -1,8 +1,433 @@ +// frontend/src/pages/ShoppingPage.tsx +import { useState, useEffect, useCallback } from 'react' +import type { ShoppingList, ShoppingListDetail, ShoppingItem, Store } from '../api/shopping' +import { + fetchLists, createList, fetchListDetail, updateList, deleteList, + addItem, updateItem, deleteItem, finishShopping, fetchStores, +} from '../api/shopping' +import Modal from '../components/Modal' +import ItemRow from '../components/shopping/ItemRow' +import { useWakeLock } from '../hooks/useWakeLock' + +type View = 'lists' | 'detail' | 'store' + +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 STATUS_LABELS: Record = { + draft: 'Brouillon', + active: 'En cours', + done: 'Terminée', +} + +const STATUS_COLORS: Record = { + draft: 'var(--ink-3)', + active: 'var(--ok)', + done: 'var(--accent)', +} + export default function ShoppingPage() { + const [view, setView] = useState('lists') + const [lists, setLists] = useState([]) + const [activeList, setActiveList] = useState(null) + const [stores, setStores] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [showCreateModal, setShowCreateModal] = useState(false) + const [showAddItemModal, setShowAddItemModal] = useState(false) + + const [newListName, setNewListName] = useState('') + const [newListStore, setNewListStore] = useState('') + + const [newItemName, setNewItemName] = useState('') + const [newItemQty, setNewItemQty] = useState('') + const [newItemUnit, setNewItemUnit] = useState('') + + useWakeLock(view === 'store') + + const loadLists = useCallback(async () => { + setLoading(true) + setError(null) + try { + const [listsData, storesData] = await Promise.all([fetchLists(), fetchStores()]) + setLists(listsData) + setStores(storesData) + } catch { + setError('Erreur lors du chargement') + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { void loadLists() }, [loadLists]) + + async function openList(list: ShoppingList) { + try { + const detail = await fetchListDetail(list.id) + setActiveList(detail) + setView('detail') + } catch { + setError('Erreur lors du chargement de la liste') + } + } + + async function refreshActiveList() { + if (!activeList) return + try { + setActiveList(await fetchListDetail(activeList.id)) + } catch { + setError('Erreur lors du rafraîchissement') + } + } + + async function handleCreateList() { + if (!newListName.trim()) return + try { + await createList({ + name: newListName.trim(), + store_id: newListStore || undefined, + }) + setNewListName('') + setNewListStore('') + setShowCreateModal(false) + void loadLists() + } catch { + setError('Erreur lors de la création') + } + } + + async function handleDeleteList(id: string) { + try { + await deleteList(id) + void loadLists() + } catch { + setError('Erreur lors de la suppression') + } + } + + async function handleAddItem() { + if (!activeList || !newItemName.trim()) return + try { + await addItem(activeList.id, { + custom_name: newItemName.trim(), + quantity: newItemQty || undefined, + unit: newItemUnit || undefined, + }) + setNewItemName('') + setNewItemQty('') + setNewItemUnit('') + setShowAddItemModal(false) + void refreshActiveList() + } catch { + setError("Erreur lors de l'ajout") + } + } + + async function handleCheckItem(itemId: string, checked: boolean) { + if (!activeList) return + try { + await updateItem(activeList.id, itemId, { is_checked: checked }) + void refreshActiveList() + } catch { + setError('Erreur lors du cochage') + } + } + + async function handleDeleteItem(itemId: string) { + if (!activeList) return + try { + await deleteItem(activeList.id, itemId) + void refreshActiveList() + } catch { + setError('Erreur lors de la suppression') + } + } + + async function handleFinish() { + if (!activeList) return + try { + await finishShopping(activeList.id) + setView('lists') + setActiveList(null) + void loadLists() + } catch { + setError('Erreur lors de la finalisation') + } + } + + // ── Vue mode magasin ────────────────────────────────────────────────────── + if (view === 'store' && activeList) { + const unchecked = activeList.items.filter(i => !i.is_checked) + const checked = activeList.items.filter(i => i.is_checked) + + return ( +
+
+ +
+
Mode magasin
+
+ {checked.length}/{activeList.item_count} cochés +
+
+ +
+ +
+ {unchecked.map(item => ( + void handleCheckItem(item.id, true)} + onDelete={() => void handleDeleteItem(item.id)} + storeMode + /> + ))} + {checked.length > 0 && ( + <> +
+ Cochés ({checked.length}) +
+ {checked.map(item => ( + void handleCheckItem(item.id, false)} + onDelete={() => void handleDeleteItem(item.id)} + storeMode + /> + ))} + + )} +
+
+ ) + } + + // ── Vue détail d'une liste ───────────────────────────────────────────────── + if (view === 'detail' && activeList) { + return ( +
+
+ +
+

+ {activeList.name ?? 'Liste de courses'} +

+
+ {activeList.checked_count}/{activeList.item_count} cochés +
+
+ +
+ + {error && ( +

+ {error} +

+ )} + + {activeList.items.length === 0 ? ( +

+ Aucun article — ajoutez-en avec le bouton + +

+ ) : ( +
+ {activeList.items.map(item => ( + void handleCheckItem(item.id, !item.is_checked)} + onDelete={() => void handleDeleteItem(item.id)} + /> + ))} +
+ )} + + + + {showAddItemModal && ( + setShowAddItemModal(false)}> + setNewItemName(e.target.value)} + autoFocus + onKeyDown={e => e.key === 'Enter' && void handleAddItem()} + /> +
+ setNewItemQty(e.target.value)} + /> + setNewItemUnit(e.target.value)} + /> +
+
+ + +
+
+ )} +
+ ) + } + + // ── Vue liste des listes ─────────────────────────────────────────────────── return ( -
-

Courses

-

Module en cours de développement — Phase 3

+
+
+

Courses

+
+ + {error && ( +

+ {error} +

+ )} + + {loading &&

Chargement…

} + + {!loading && lists.length === 0 && ( +

+ Aucune liste — créez-en une avec le bouton + +

+ )} + +
+ {lists.map(list => ( +
void openList(list)} + style={{ borderRadius: 10, padding: '14px 16px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 12 }} + > +
+
+ {list.name ?? 'Liste sans nom'} +
+
+ + {STATUS_LABELS[list.status]} + + + {list.checked_count}/{list.item_count} articles + +
+
+ +
+ ))} +
+ + + + {showCreateModal && ( + setShowCreateModal(false)}> + setNewListName(e.target.value)} + autoFocus + onKeyDown={e => e.key === 'Enter' && void handleCreateList()} + /> + +
+ + +
+
+ )}
) }