feat(shopping): pré-remplissage bottom sheet + toggle catalogue v0.4.4
- openAddSheet() pré-charge les articles non cochés de la liste courante (qty existante, PATCH si modifiée / POST si nouvel article) - Toggle "Ajouter au catalogue" sur les articles libres nouvellement saisis (coché par défaut, créé via createProduct puis lié en product_id) - Bouton "Confirmer (N)" comptant uniquement les actions réelles (new items + existing avec qty modifiée) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "homehub-frontend",
|
||||
"private": true,
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ShoppingListDetail, ShoppingList, Store, Product, ShoppingItem } f
|
||||
import {
|
||||
fetchLists, createList, fetchListDetail, deleteList,
|
||||
addItem, updateItem, deleteItem, finishShopping, fetchStores, generateMagicList,
|
||||
searchProducts,
|
||||
searchProducts, createProduct,
|
||||
} from '../api/shopping'
|
||||
import Modal from '../components/Modal'
|
||||
import BottomSheet from '../components/BottomSheet'
|
||||
@@ -67,7 +67,9 @@ export default function ShoppingPage() {
|
||||
const [editQty, setEditQty] = useState('')
|
||||
const [editUnit, setEditUnit] = useState('')
|
||||
|
||||
type Selection = { type: 'product'; product: Product; qty: number } | { type: 'custom'; name: string; qty: number }
|
||||
type Selection =
|
||||
| { type: 'product'; product: Product; qty: number; existingItemId?: string; originalQty?: number }
|
||||
| { type: 'custom'; name: string; qty: number; addToCatalogue: boolean; existingItemId?: string; originalQty?: number }
|
||||
const [itemSearch, setItemSearch] = useState('')
|
||||
const [selections, setSelections] = useState<Selection[]>([])
|
||||
const [addSaving, setAddSaving] = useState(false)
|
||||
@@ -137,6 +139,24 @@ export default function ShoppingPage() {
|
||||
setShowAddSheet(false)
|
||||
}
|
||||
|
||||
function openAddSheet() {
|
||||
setItemSearch('')
|
||||
const initial: Selection[] = []
|
||||
if (currentList) {
|
||||
for (const item of currentList.items.filter(i => !i.is_checked)) {
|
||||
const qty = Math.max(1, Math.round(parseFloat(item.quantity ?? '1') || 1))
|
||||
if (item.product_id) {
|
||||
const product = products.find(p => p.id === item.product_id)
|
||||
if (product) initial.push({ type: 'product', product, qty, existingItemId: item.id, originalQty: qty })
|
||||
} else if (item.custom_name) {
|
||||
initial.push({ type: 'custom', name: item.custom_name, qty, addToCatalogue: false, existingItemId: item.id, originalQty: qty })
|
||||
}
|
||||
}
|
||||
}
|
||||
setSelections(initial)
|
||||
setShowAddSheet(true)
|
||||
}
|
||||
|
||||
function getProductQty(id: string): number {
|
||||
return (selections.find(s => s.type === 'product' && s.product.id === id) as Extract<Selection, { type: 'product' }> | undefined)?.qty ?? 0
|
||||
}
|
||||
@@ -164,11 +184,17 @@ export default function ShoppingPage() {
|
||||
setSelections(prev => {
|
||||
const exists = prev.some(s => s.type === 'custom' && s.name === name)
|
||||
if (exists) return prev
|
||||
return [...prev, { type: 'custom' as const, name, qty: 1 }]
|
||||
return [...prev, { type: 'custom' as const, name, qty: 1, addToCatalogue: true }]
|
||||
})
|
||||
setItemSearch('')
|
||||
}
|
||||
|
||||
function toggleCatalogue(name: string) {
|
||||
setSelections(prev => prev.map(s =>
|
||||
s.type === 'custom' && s.name === name ? { ...s, addToCatalogue: !s.addToCatalogue } : s
|
||||
))
|
||||
}
|
||||
|
||||
function incrementCustom(name: string) {
|
||||
setSelections(prev => prev.map(s => s.type === 'custom' && s.name === name ? { ...s, qty: s.qty + 1 } : s))
|
||||
}
|
||||
@@ -183,21 +209,31 @@ export default function ShoppingPage() {
|
||||
}
|
||||
|
||||
async function handleConfirmAdd() {
|
||||
if (!currentList || selections.length === 0) return
|
||||
if (!currentList) return
|
||||
const toProcess = selections.filter(sel => !sel.existingItemId || sel.qty !== sel.originalQty)
|
||||
if (toProcess.length === 0) { closeAddSheet(); return }
|
||||
setAddSaving(true)
|
||||
try {
|
||||
for (const sel of selections) {
|
||||
for (const sel of toProcess) {
|
||||
if (sel.type === 'product') {
|
||||
await addItem(currentList.id, {
|
||||
product_id: sel.product.id,
|
||||
quantity: String(sel.qty),
|
||||
unit: sel.product.default_unit || undefined,
|
||||
})
|
||||
if (sel.existingItemId) {
|
||||
await updateItem(currentList.id, sel.existingItemId, { quantity: String(sel.qty) })
|
||||
} else {
|
||||
await addItem(currentList.id, {
|
||||
product_id: sel.product.id,
|
||||
quantity: String(sel.qty),
|
||||
unit: sel.product.default_unit || undefined,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
await addItem(currentList.id, {
|
||||
custom_name: sel.name,
|
||||
quantity: String(sel.qty),
|
||||
})
|
||||
if (sel.existingItemId) {
|
||||
await updateItem(currentList.id, sel.existingItemId, { quantity: String(sel.qty) })
|
||||
} else if (sel.addToCatalogue) {
|
||||
const newProduct = await createProduct({ name: sel.name })
|
||||
await addItem(currentList.id, { product_id: newProduct.id, quantity: String(sel.qty) })
|
||||
} else {
|
||||
await addItem(currentList.id, { custom_name: sel.name, quantity: String(sel.qty) })
|
||||
}
|
||||
}
|
||||
}
|
||||
closeAddSheet()
|
||||
@@ -479,7 +515,7 @@ export default function ShoppingPage() {
|
||||
{/* FAB + — masqué quand le sheet est ouvert */}
|
||||
{!showAddSheet && (
|
||||
<button
|
||||
onClick={() => setShowAddSheet(true)}
|
||||
onClick={openAddSheet}
|
||||
aria-label="Ajouter un article"
|
||||
style={{
|
||||
position: 'fixed', bottom: 72, right: 20,
|
||||
@@ -563,7 +599,19 @@ export default function ShoppingPage() {
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontFamily: 'var(--font-ui)', fontSize: 15, color: 'var(--ink-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{s.name}</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)', marginTop: 2 }}>article libre</div>
|
||||
{!s.existingItemId ? (
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 5, marginTop: 3, cursor: 'pointer' }} onClick={e => e.stopPropagation()}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={s.addToCatalogue}
|
||||
onChange={() => toggleCatalogue(s.name)}
|
||||
style={{ accentColor: 'var(--accent)', width: 14, height: 14, cursor: 'pointer', flexShrink: 0 }}
|
||||
/>
|
||||
<span style={{ fontFamily: 'var(--font-ui)', fontSize: 11, color: 'var(--ink-3)' }}>Ajouter au catalogue</span>
|
||||
</label>
|
||||
) : (
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)', marginTop: 2 }}>article libre</div>
|
||||
)}
|
||||
</div>
|
||||
<QtyControls
|
||||
qty={s.qty}
|
||||
@@ -632,22 +680,27 @@ export default function ShoppingPage() {
|
||||
</div>
|
||||
|
||||
{/* Bouton confirm sticky */}
|
||||
<div style={{ padding: '12px 16px', borderTop: '1px solid var(--bg-4)', flexShrink: 0 }}>
|
||||
<button
|
||||
onClick={() => void handleConfirmAdd()}
|
||||
disabled={selections.length === 0 || addSaving}
|
||||
style={{
|
||||
width: '100%', padding: '14px', borderRadius: 12, border: 'none',
|
||||
background: selections.length === 0 ? 'var(--bg-4)' : 'var(--accent)',
|
||||
color: selections.length === 0 ? 'var(--ink-4)' : '#1d2021',
|
||||
fontFamily: 'var(--font-ui)', fontWeight: 700, fontSize: 16,
|
||||
cursor: selections.length === 0 ? 'default' : 'pointer',
|
||||
minHeight: 52, transition: 'background 0.15s',
|
||||
}}
|
||||
>
|
||||
{addSaving ? 'Ajout en cours…' : selections.length === 0 ? 'Sélectionner des articles' : `Ajouter (${selections.length})`}
|
||||
</button>
|
||||
</div>
|
||||
{(() => {
|
||||
const actionCount = selections.filter(sel => !sel.existingItemId || sel.qty !== sel.originalQty).length
|
||||
return (
|
||||
<div style={{ padding: '12px 16px', borderTop: '1px solid var(--bg-4)', flexShrink: 0 }}>
|
||||
<button
|
||||
onClick={() => void handleConfirmAdd()}
|
||||
disabled={actionCount === 0 || addSaving}
|
||||
style={{
|
||||
width: '100%', padding: '14px', borderRadius: 12, border: 'none',
|
||||
background: actionCount === 0 ? 'var(--bg-4)' : 'var(--accent)',
|
||||
color: actionCount === 0 ? 'var(--ink-4)' : '#1d2021',
|
||||
fontFamily: 'var(--font-ui)', fontWeight: 700, fontSize: 16,
|
||||
cursor: actionCount === 0 ? 'default' : 'pointer',
|
||||
minHeight: 52, transition: 'background 0.15s',
|
||||
}}
|
||||
>
|
||||
{addSaving ? 'Enregistrement…' : actionCount === 0 ? 'Aucune modification' : `Confirmer (${actionCount})`}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</BottomSheet>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user