feat(shopping): proposition d'ajout au catalogue pour les articles libres

Quand un article tapé n'existe pas dans le catalogue et qu'aucun produit
n'est sélectionné, une case "Ajouter au catalogue" apparaît (cochée par
défaut). Si cochée, le produit est créé via POST /api/shopping/products
avant l'ajout à la liste, avec l'unité pré-remplie si saisie.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 06:55:33 +02:00
parent b92c74f641
commit a86c14b0b9
+36 -7
View File
@@ -4,7 +4,7 @@ import type { ShoppingListDetail, ShoppingList, Store, Product } from '../api/sh
import {
fetchLists, createList, fetchListDetail, deleteList,
addItem, updateItem, deleteItem, finishShopping, fetchStores, generateMagicList,
searchProducts,
searchProducts, createProduct,
} from '../api/shopping'
import Modal from '../components/Modal'
import ItemRow from '../components/shopping/ItemRow'
@@ -44,6 +44,7 @@ export default function ShoppingPage() {
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null)
const [newItemQty, setNewItemQty] = useState('')
const [newItemUnit, setNewItemUnit] = useState('')
const [addToCatalogue, setAddToCatalogue] = useState(true)
useWakeLock(currentList !== null)
@@ -109,6 +110,7 @@ export default function ShoppingPage() {
setSelectedProduct(null)
setNewItemQty('')
setNewItemUnit('')
setAddToCatalogue(true)
setShowAddItemModal(false)
}
@@ -120,13 +122,25 @@ export default function ShoppingPage() {
async function handleAddItem() {
if (!currentList) return
const hasProduct = selectedProduct !== null
const customName = hasProduct ? undefined : itemSearch.trim()
if (!hasProduct && !customName) 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: selectedProduct?.id,
custom_name: customName,
product_id: productId,
custom_name: !productId ? customName : undefined,
quantity: newItemQty || undefined,
unit: newItemUnit || undefined,
})
@@ -425,7 +439,7 @@ export default function ShoppingPage() {
color: 'var(--ink-4)', fontFamily: 'var(--font-ui)', fontSize: 13,
textAlign: 'center', padding: '12px 16px', margin: 0, ...noSelect,
}}>
{itemSearch.trim() ? `"${itemSearch}" — sera ajouté comme article libre` : 'Catalogue vide'}
{itemSearch.trim() ? `"${itemSearch}" — article libre` : 'Catalogue vide'}
</p>
)}
{filteredProducts.map((p, idx) => {
@@ -460,6 +474,21 @@ export default function ShoppingPage() {
})}
</div>
{/* Case "Ajouter au catalogue" — uniquement si article libre sans correspondance */}
{!selectedProduct && itemSearch.trim() && filteredProducts.length === 0 && (
<label style={{ display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer', userSelect: 'none', padding: '6px 2px' }}>
<input
type="checkbox"
checked={addToCatalogue}
onChange={e => setAddToCatalogue(e.target.checked)}
style={{ width: 18, height: 18, accentColor: 'var(--ok)', cursor: 'pointer', flexShrink: 0 }}
/>
<span style={{ fontFamily: 'var(--font-ui)', fontSize: 13, color: 'var(--ink-2)' }}>
Ajouter <strong style={{ color: 'var(--ink-1)' }}>{itemSearch.trim()}</strong> au catalogue
</span>
</label>
)}
{/* Quantité + Unité */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
<input