feat(shopping): stats achat produit + édition quantité article
Backend : - Migration 004 : last_purchased_at (DATE) + avg_interval_days (NUMERIC) sur shopping.products - update_item : met à jour les stats au premier cochage d'un article lié à un produit (moyenne mobile exp. 70/30) - ProductResponse expose les deux nouveaux champs Frontend : - ItemRow : long press 500ms → onEdit() (mobile) ; crayon + croix (laptop) - ShoppingPage : modal édition quantité/unité, état editingItem - api/shopping.ts : Product inclut last_purchased_at + avg_interval_days Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// 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 type { ShoppingListDetail, ShoppingList, Store, Product, ShoppingItem } from '../api/shopping'
|
||||
import {
|
||||
fetchLists, createList, fetchListDetail, deleteList,
|
||||
addItem, updateItem, deleteItem, finishShopping, fetchStores, generateMagicList,
|
||||
@@ -40,6 +40,9 @@ export default function ShoppingPage() {
|
||||
const [showHistoryModal, setShowHistoryModal] = useState(false)
|
||||
const [showCatalogueModal, setShowCatalogueModal] = useState(false)
|
||||
const [showBoutiquesModal, setShowBoutiquesModal] = useState(false)
|
||||
const [editingItem, setEditingItem] = useState<ShoppingItem | null>(null)
|
||||
const [editQty, setEditQty] = useState('')
|
||||
const [editUnit, setEditUnit] = useState('')
|
||||
|
||||
const [itemSearch, setItemSearch] = useState('')
|
||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null)
|
||||
@@ -172,6 +175,26 @@ export default function ShoppingPage() {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -367,6 +390,7 @@ export default function ShoppingPage() {
|
||||
item={item}
|
||||
onCheck={() => void handleCheckItem(item.id, true)}
|
||||
onDelete={() => void handleDeleteItem(item.id)}
|
||||
onEdit={() => openEditItem(item)}
|
||||
storeMode
|
||||
/>
|
||||
))}
|
||||
@@ -390,6 +414,7 @@ export default function ShoppingPage() {
|
||||
item={item}
|
||||
onCheck={() => void handleCheckItem(item.id, false)}
|
||||
onDelete={() => void handleDeleteItem(item.id)}
|
||||
onEdit={() => openEditItem(item)}
|
||||
storeMode
|
||||
/>
|
||||
))}
|
||||
@@ -414,6 +439,41 @@ export default function ShoppingPage() {
|
||||
|
||||
{/* ── Modals ── */}
|
||||
|
||||
{/* Modal édition quantité/unité */}
|
||||
{editingItem && (
|
||||
<Modal title={`Modifier — ${editingItem.display_name}`} onClose={() => setEditingItem(null)} width={320}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||||
<input
|
||||
style={inputStyle}
|
||||
placeholder="Quantité"
|
||||
value={editQty}
|
||||
onChange={e => setEditQty(e.target.value)}
|
||||
autoFocus
|
||||
onKeyDown={e => e.key === 'Enter' && void handleEditItem()}
|
||||
/>
|
||||
<input
|
||||
style={inputStyle}
|
||||
placeholder="Unité (kg, L…)"
|
||||
value={editUnit}
|
||||
onChange={e => setEditUnit(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && void handleEditItem()}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
onClick={() => setEditingItem(null)}
|
||||
style={{ padding: '10px 16px', borderRadius: 8, border: '1px solid var(--bg-5)', background: 'transparent', color: 'var(--ink-2)', cursor: 'pointer', fontFamily: 'var(--font-ui)', minHeight: 48 }}
|
||||
>Annuler</button>
|
||||
<button
|
||||
onClick={() => void handleEditItem()}
|
||||
style={{ padding: '10px 20px', borderRadius: 8, border: 'none', background: 'var(--accent)', color: '#1d2021', cursor: 'pointer', fontFamily: 'var(--font-ui)', fontWeight: 600, minHeight: 48 }}
|
||||
>Enregistrer</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{showAddItemModal && (
|
||||
<Modal title="Ajouter un article" onClose={closeAddItemModal} width={420}>
|
||||
{/* Barre de recherche / nom personnalisé */}
|
||||
|
||||
Reference in New Issue
Block a user