diff --git a/App.js b/App.js index 4d2e9ef..1fa23ec 100644 --- a/App.js +++ b/App.js @@ -138,6 +138,7 @@ export default function App() { onStartScan={() => setActiveTab('scan')} onOpenProfile={() => setActiveTab('profile')} onToggleItem={toggleListItem} + onChangeItemQuantity={changeListItemQuantity} onUpdateItemPrice={updateListItemPrice} preferences={preferences} lastDeletedItem={lastDeletedListItem} @@ -269,6 +270,7 @@ export default function App() { ? { ...item, quantity: item.quantity + 1, + checked: false, packageQuantityGrams: item.packageQuantityGrams ?? product.packageQuantityGrams, packageQuantityLabel: item.packageQuantityLabel ?? product.packageQuantityLabel, servingQuantityGrams: item.servingQuantityGrams ?? product.servingQuantityGrams, @@ -337,6 +339,17 @@ export default function App() { })); } + function changeListItemQuantity(itemId, change) { + setShoppingList((current) => { + const items = current.items.map((item) => + item.id === itemId + ? { ...item, quantity: Math.max(1, (item.quantity || 1) + change) } + : item + ); + return { ...current, items, estimatedTotal: calculateEstimatedTotal(items) }; + }); + } + function deleteListItem(itemId) { const index = shoppingList.items.findIndex((item) => item.id === itemId); if (index < 0) return; diff --git a/src/components/list/ShoppingListItem.js b/src/components/list/ShoppingListItem.js index 2e5fa1f..acfec97 100644 --- a/src/components/list/ShoppingListItem.js +++ b/src/components/list/ShoppingListItem.js @@ -4,7 +4,7 @@ import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'; import { colors } from '../../utils/constants'; import { formatPrice } from '../../utils/formatPrice'; -export default function ShoppingListItem({ item, onToggle, onDelete, onUpdatePrice }) { +export default function ShoppingListItem({ item, onToggle, onDelete, onChangeQuantity, onUpdatePrice }) { const [priceText, setPriceText] = useState(priceToInputValue(item.estimatedPrice)); const [priceError, setPriceError] = useState(''); @@ -26,13 +26,23 @@ export default function ShoppingListItem({ item, onToggle, onDelete, onUpdatePri } return ( - + {item.checked ? '✓' : ''} {item.productName} {item.quantity} × {formatPrice(item.estimatedPrice, item.currency || 'CHF')}{item.quantity > 1 && typeof item.estimatedPrice === 'number' ? ` · ${formatPrice(item.estimatedPrice * item.quantity, item.currency || 'CHF')}` : ''} + + Menge + onChangeQuantity(-1)} style={[styles.quantityButton, item.quantity <= 1 && styles.disabledButton]}> + − + + {item.quantity} + onChangeQuantity(1)} style={styles.quantityButton}> + + + + {item.currency || 'CHF'} (b.scanCount || 0) - (a.scanCount || 0)) .slice(0, 3); const openItems = shoppingList?.items?.filter((item) => !item.checked).length || 0; + const completedItems = (shoppingList?.items?.length || 0) - openItems; return ( @@ -36,7 +37,7 @@ export default function HomeScreen({ preferences, recentProducts, shoppingList, Dein Einkauf - {shoppingList?.items?.length ? `${openItems} noch offen · ${shoppingList.items.length} insgesamt` : 'Deine Liste ist noch leer'} + {shoppingList?.items?.length ? `${completedItems} von ${shoppingList.items.length} erledigt · ${openItems} noch offen` : 'Deine Liste ist noch leer'} diff --git a/src/screens/ScanScreen.js b/src/screens/ScanScreen.js index ed06e13..bad14d0 100644 --- a/src/screens/ScanScreen.js +++ b/src/screens/ScanScreen.js @@ -54,7 +54,17 @@ export default function ScanScreen({ preferences, productCache, onScanBarcode, o setIsSearching(true); setSearchError(''); try { - const results = await searchProducts(manualSearchQuery, productCache, preferences.offlineModeEnabled); + const query = manualSearchQuery.trim(); + if (/^\d{8,14}$/.test(query)) { + const result = await onScanBarcode(query); + if (result.status !== 'found') { + setManualResults([]); + setHasSearched(false); + setSearchError(result.errorMessage || 'Zu diesem Barcode wurde kein Produkt gefunden.'); + } + return; + } + const results = await searchProducts(query, productCache, preferences.offlineModeEnabled); setManualResults(results); setHasSearched(true); } catch (error) { diff --git a/src/screens/ShoppingListScreen.js b/src/screens/ShoppingListScreen.js index 7bd0611..8d9dfd8 100644 --- a/src/screens/ShoppingListScreen.js +++ b/src/screens/ShoppingListScreen.js @@ -15,6 +15,7 @@ export default function ShoppingListScreen({ shoppingList, lastDeletedItem, onToggleItem, + onChangeItemQuantity, onDeleteItem, onUndoDelete, onUpdateItemPrice, @@ -25,7 +26,12 @@ export default function ShoppingListScreen({ const [isConfirming, setIsConfirming] = useState(false); const [wasCompleted, setWasCompleted] = useState(false); const openItems = shoppingList.items.filter((item) => !item.checked).length; + const completedItems = shoppingList.items.length - openItems; + const openTotal = shoppingList.items.reduce((sum, item) => + sum + (!item.checked && typeof item.estimatedPrice === 'number' ? item.estimatedPrice * item.quantity : 0), 0); + const progress = shoppingList.items.length ? completedItems / shoppingList.items.length : 0; const itemsWithoutPrice = shoppingList.items.filter((item) => typeof item.estimatedPrice !== 'number').length; + const openItemsWithoutPrice = shoppingList.items.filter((item) => !item.checked && typeof item.estimatedPrice !== 'number').length; const budget = preferences.maxBudgetPerShoppingTrip; const hasBudget = typeof budget === 'number' && budget > 0; const budgetShare = hasBudget ? shoppingList.estimatedTotal / budget : 0; @@ -59,6 +65,18 @@ export default function ShoppingListScreen({ {itemsWithoutPrice ? 'Bekannte Summe' : 'Voraussichtliche Summe'} {formatPrice(shoppingList.estimatedTotal, shoppingList.currency)} {itemsWithoutPrice ? {itemsWithoutPrice} {itemsWithoutPrice === 1 ? 'Produkt ohne Preis ist' : 'Produkte ohne Preis sind'} nicht eingerechnet. : null} + {shoppingList.items.length > 0 ? ( + + + {completedItems} von {shoppingList.items.length} erledigt + {Math.round(progress * 100)} % + + + + + Noch offen: {formatPrice(openTotal, shoppingList.currency)}{openItemsWithoutPrice ? ` plus ${openItemsWithoutPrice} Artikel ohne Preis` : ''} + + ) : null} {hasBudget ? Anteil am Budget pro {budgetPeriodLabels[preferences.budgetPeriod] || 'Woche'} @@ -111,15 +129,16 @@ export default function ShoppingListScreen({ ) : ( - shoppingList.items.map((item) => ( - onDeleteItem(item.id)} - onToggle={() => onToggleItem(item.id)} - onUpdatePrice={onUpdateItemPrice} - /> - )) + <> + {openItems > 0 ? Noch einkaufen · {openItems} : null} + {shoppingList.items.filter((item) => !item.checked).map((item) => ( + onDeleteItem(item.id)} onToggle={() => onToggleItem(item.id)} onChangeQuantity={(change) => onChangeItemQuantity(item.id, change)} onUpdatePrice={onUpdateItemPrice} /> + ))} + {completedItems > 0 ? Erledigt · {completedItems} : null} + {shoppingList.items.filter((item) => item.checked).map((item) => ( + onDeleteItem(item.id)} onToggle={() => onToggleItem(item.id)} onChangeQuantity={(change) => onChangeItemQuantity(item.id, change)} onUpdatePrice={onUpdateItemPrice} /> + ))} + )} {shoppingList.items.length > 0 ? ( @@ -154,6 +173,11 @@ function formatAmount(value, unit) { } const styles = StyleSheet.create({ + shoppingProgress: { marginTop: 18 }, + progressHeader: { flexDirection: 'row', justifyContent: 'space-between' }, + progressLabel: { color: colors.primaryDark, fontSize: 13, fontWeight: '800' }, + remainingCopy: { color: colors.muted, fontSize: 13, marginTop: 8 }, + groupTitle: { color: colors.text, fontSize: 16, fontWeight: '800', marginTop: 4 }, summary: { backgroundColor: colors.surface, borderColor: colors.border,