Refactor profile screen layout and user preference handling
Android Release Build / build-android (push) Successful in 13m44s
Android Release Build / build-android (push) Successful in 13m44s
This commit is contained in:
@@ -138,6 +138,7 @@ export default function App() {
|
|||||||
onStartScan={() => setActiveTab('scan')}
|
onStartScan={() => setActiveTab('scan')}
|
||||||
onOpenProfile={() => setActiveTab('profile')}
|
onOpenProfile={() => setActiveTab('profile')}
|
||||||
onToggleItem={toggleListItem}
|
onToggleItem={toggleListItem}
|
||||||
|
onChangeItemQuantity={changeListItemQuantity}
|
||||||
onUpdateItemPrice={updateListItemPrice}
|
onUpdateItemPrice={updateListItemPrice}
|
||||||
preferences={preferences}
|
preferences={preferences}
|
||||||
lastDeletedItem={lastDeletedListItem}
|
lastDeletedItem={lastDeletedListItem}
|
||||||
@@ -269,6 +270,7 @@ export default function App() {
|
|||||||
? {
|
? {
|
||||||
...item,
|
...item,
|
||||||
quantity: item.quantity + 1,
|
quantity: item.quantity + 1,
|
||||||
|
checked: false,
|
||||||
packageQuantityGrams: item.packageQuantityGrams ?? product.packageQuantityGrams,
|
packageQuantityGrams: item.packageQuantityGrams ?? product.packageQuantityGrams,
|
||||||
packageQuantityLabel: item.packageQuantityLabel ?? product.packageQuantityLabel,
|
packageQuantityLabel: item.packageQuantityLabel ?? product.packageQuantityLabel,
|
||||||
servingQuantityGrams: item.servingQuantityGrams ?? product.servingQuantityGrams,
|
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) {
|
function deleteListItem(itemId) {
|
||||||
const index = shoppingList.items.findIndex((item) => item.id === itemId);
|
const index = shoppingList.items.findIndex((item) => item.id === itemId);
|
||||||
if (index < 0) return;
|
if (index < 0) return;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
|||||||
import { colors } from '../../utils/constants';
|
import { colors } from '../../utils/constants';
|
||||||
import { formatPrice } from '../../utils/formatPrice';
|
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 [priceText, setPriceText] = useState(priceToInputValue(item.estimatedPrice));
|
||||||
const [priceError, setPriceError] = useState('');
|
const [priceError, setPriceError] = useState('');
|
||||||
|
|
||||||
@@ -26,13 +26,23 @@ export default function ShoppingListItem({ item, onToggle, onDelete, onUpdatePri
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.row}>
|
<View style={[styles.row, item.checked && styles.completedRow]}>
|
||||||
<Pressable accessibilityLabel={`${item.productName} ${item.checked ? 'als offen markieren' : 'abhaken'}`} accessibilityRole="checkbox" accessibilityState={{ checked: item.checked }} onPress={onToggle} style={[styles.checkbox, item.checked ? styles.checked : null]}>
|
<Pressable accessibilityLabel={`${item.productName} ${item.checked ? 'als offen markieren' : 'abhaken'}`} accessibilityRole="checkbox" accessibilityState={{ checked: item.checked }} onPress={onToggle} style={[styles.checkbox, item.checked ? styles.checked : null]}>
|
||||||
<Text style={styles.checkText}>{item.checked ? '✓' : ''}</Text>
|
<Text style={styles.checkText}>{item.checked ? '✓' : ''}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<View style={styles.copy}>
|
<View style={styles.copy}>
|
||||||
<Text style={[styles.name, item.checked ? styles.done : null]}>{item.productName}</Text>
|
<Text style={[styles.name, item.checked ? styles.done : null]}>{item.productName}</Text>
|
||||||
<Text style={styles.meta}>{item.quantity} × {formatPrice(item.estimatedPrice, item.currency || 'CHF')}{item.quantity > 1 && typeof item.estimatedPrice === 'number' ? ` · ${formatPrice(item.estimatedPrice * item.quantity, item.currency || 'CHF')}` : ''}</Text>
|
<Text style={styles.meta}>{item.quantity} × {formatPrice(item.estimatedPrice, item.currency || 'CHF')}{item.quantity > 1 && typeof item.estimatedPrice === 'number' ? ` · ${formatPrice(item.estimatedPrice * item.quantity, item.currency || 'CHF')}` : ''}</Text>
|
||||||
|
<View style={styles.quantityRow}>
|
||||||
|
<Text style={styles.quantityLabel}>Menge</Text>
|
||||||
|
<Pressable accessibilityLabel={`Menge von ${item.productName} verringern`} accessibilityRole="button" accessibilityState={{ disabled: item.quantity <= 1 }} disabled={item.quantity <= 1} onPress={() => onChangeQuantity(-1)} style={[styles.quantityButton, item.quantity <= 1 && styles.disabledButton]}>
|
||||||
|
<Text style={styles.quantityButtonText}>−</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Text style={styles.quantityValue}>{item.quantity}</Text>
|
||||||
|
<Pressable accessibilityLabel={`Menge von ${item.productName} erhöhen`} accessibilityRole="button" onPress={() => onChangeQuantity(1)} style={styles.quantityButton}>
|
||||||
|
<Text style={styles.quantityButtonText}>+</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
<View style={styles.priceRow}>
|
<View style={styles.priceRow}>
|
||||||
<Text style={styles.currency}>{item.currency || 'CHF'}</Text>
|
<Text style={styles.currency}>{item.currency || 'CHF'}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -75,6 +85,13 @@ function parsePrice(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
completedRow: { backgroundColor: '#f2f7f4' },
|
||||||
|
quantityRow: { alignItems: 'center', flexDirection: 'row', gap: 8, marginTop: 10 },
|
||||||
|
quantityLabel: { color: colors.muted, fontSize: 13, fontWeight: '700', marginRight: 4 },
|
||||||
|
quantityButton: { alignItems: 'center', backgroundColor: colors.surfaceTint, borderRadius: 9, height: 36, justifyContent: 'center', width: 36 },
|
||||||
|
disabledButton: { opacity: 0.4 },
|
||||||
|
quantityButtonText: { color: colors.primaryDark, fontSize: 21, fontWeight: '700' },
|
||||||
|
quantityValue: { color: colors.text, fontSize: 15, fontWeight: '800', minWidth: 22, textAlign: 'center' },
|
||||||
row: {
|
row: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: colors.surface,
|
backgroundColor: colors.surface,
|
||||||
@@ -102,7 +119,8 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '800'
|
fontWeight: '800'
|
||||||
},
|
},
|
||||||
copy: {
|
copy: {
|
||||||
flex: 1
|
flex: 1,
|
||||||
|
minWidth: 0
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default function HomeScreen({ preferences, recentProducts, shoppingList,
|
|||||||
.sort((a, b) => (b.scanCount || 0) - (a.scanCount || 0))
|
.sort((a, b) => (b.scanCount || 0) - (a.scanCount || 0))
|
||||||
.slice(0, 3);
|
.slice(0, 3);
|
||||||
const openItems = shoppingList?.items?.filter((item) => !item.checked).length || 0;
|
const openItems = shoppingList?.items?.filter((item) => !item.checked).length || 0;
|
||||||
|
const completedItems = (shoppingList?.items?.length || 0) - openItems;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenContainer>
|
<ScreenContainer>
|
||||||
@@ -36,7 +37,7 @@ export default function HomeScreen({ preferences, recentProducts, shoppingList,
|
|||||||
<View style={styles.listShortcutText}>
|
<View style={styles.listShortcutText}>
|
||||||
<Text style={styles.listShortcutTitle}>Dein Einkauf</Text>
|
<Text style={styles.listShortcutTitle}>Dein Einkauf</Text>
|
||||||
<Text style={styles.listShortcutCopy}>
|
<Text style={styles.listShortcutCopy}>
|
||||||
{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'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.listShortcutTotal}>
|
<Text style={styles.listShortcutTotal}>
|
||||||
|
|||||||
@@ -54,7 +54,17 @@ export default function ScanScreen({ preferences, productCache, onScanBarcode, o
|
|||||||
setIsSearching(true);
|
setIsSearching(true);
|
||||||
setSearchError('');
|
setSearchError('');
|
||||||
try {
|
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);
|
setManualResults(results);
|
||||||
setHasSearched(true);
|
setHasSearched(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default function ShoppingListScreen({
|
|||||||
shoppingList,
|
shoppingList,
|
||||||
lastDeletedItem,
|
lastDeletedItem,
|
||||||
onToggleItem,
|
onToggleItem,
|
||||||
|
onChangeItemQuantity,
|
||||||
onDeleteItem,
|
onDeleteItem,
|
||||||
onUndoDelete,
|
onUndoDelete,
|
||||||
onUpdateItemPrice,
|
onUpdateItemPrice,
|
||||||
@@ -25,7 +26,12 @@ export default function ShoppingListScreen({
|
|||||||
const [isConfirming, setIsConfirming] = useState(false);
|
const [isConfirming, setIsConfirming] = useState(false);
|
||||||
const [wasCompleted, setWasCompleted] = useState(false);
|
const [wasCompleted, setWasCompleted] = useState(false);
|
||||||
const openItems = shoppingList.items.filter((item) => !item.checked).length;
|
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 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 budget = preferences.maxBudgetPerShoppingTrip;
|
||||||
const hasBudget = typeof budget === 'number' && budget > 0;
|
const hasBudget = typeof budget === 'number' && budget > 0;
|
||||||
const budgetShare = hasBudget ? shoppingList.estimatedTotal / budget : 0;
|
const budgetShare = hasBudget ? shoppingList.estimatedTotal / budget : 0;
|
||||||
@@ -59,6 +65,18 @@ export default function ShoppingListScreen({
|
|||||||
<Text style={styles.totalLabel}>{itemsWithoutPrice ? 'Bekannte Summe' : 'Voraussichtliche Summe'}</Text>
|
<Text style={styles.totalLabel}>{itemsWithoutPrice ? 'Bekannte Summe' : 'Voraussichtliche Summe'}</Text>
|
||||||
<Text style={styles.totalValue}>{formatPrice(shoppingList.estimatedTotal, shoppingList.currency)}</Text>
|
<Text style={styles.totalValue}>{formatPrice(shoppingList.estimatedTotal, shoppingList.currency)}</Text>
|
||||||
{itemsWithoutPrice ? <Text style={styles.summaryCopy}>{itemsWithoutPrice} {itemsWithoutPrice === 1 ? 'Produkt ohne Preis ist' : 'Produkte ohne Preis sind'} nicht eingerechnet.</Text> : null}
|
{itemsWithoutPrice ? <Text style={styles.summaryCopy}>{itemsWithoutPrice} {itemsWithoutPrice === 1 ? 'Produkt ohne Preis ist' : 'Produkte ohne Preis sind'} nicht eingerechnet.</Text> : null}
|
||||||
|
{shoppingList.items.length > 0 ? (
|
||||||
|
<View style={styles.shoppingProgress}>
|
||||||
|
<View style={styles.progressHeader}>
|
||||||
|
<Text style={styles.progressLabel}>{completedItems} von {shoppingList.items.length} erledigt</Text>
|
||||||
|
<Text style={styles.progressLabel}>{Math.round(progress * 100)} %</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.progressTrack}>
|
||||||
|
<View style={[styles.progressFill, styles.goodFill, { width: `${progress * 100}%` }]} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.remainingCopy}>Noch offen: {formatPrice(openTotal, shoppingList.currency)}{openItemsWithoutPrice ? ` plus ${openItemsWithoutPrice} Artikel ohne Preis` : ''}</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
{hasBudget ? <View style={styles.budgetBox}>
|
{hasBudget ? <View style={styles.budgetBox}>
|
||||||
<View style={styles.budgetHeader}>
|
<View style={styles.budgetHeader}>
|
||||||
<Text style={styles.budgetLabel}>Anteil am Budget pro {budgetPeriodLabels[preferences.budgetPeriod] || 'Woche'}</Text>
|
<Text style={styles.budgetLabel}>Anteil am Budget pro {budgetPeriodLabels[preferences.budgetPeriod] || 'Woche'}</Text>
|
||||||
@@ -111,15 +129,16 @@ export default function ShoppingListScreen({
|
|||||||
<SecondaryButton label="Produkt scannen" onPress={onStartScan} />
|
<SecondaryButton label="Produkt scannen" onPress={onStartScan} />
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
shoppingList.items.map((item) => (
|
<>
|
||||||
<ShoppingListItem
|
{openItems > 0 ? <Text style={styles.groupTitle}>Noch einkaufen · {openItems}</Text> : null}
|
||||||
item={item}
|
{shoppingList.items.filter((item) => !item.checked).map((item) => (
|
||||||
key={item.id}
|
<ShoppingListItem item={item} key={item.id} onDelete={() => onDeleteItem(item.id)} onToggle={() => onToggleItem(item.id)} onChangeQuantity={(change) => onChangeItemQuantity(item.id, change)} onUpdatePrice={onUpdateItemPrice} />
|
||||||
onDelete={() => onDeleteItem(item.id)}
|
))}
|
||||||
onToggle={() => onToggleItem(item.id)}
|
{completedItems > 0 ? <Text style={styles.groupTitle}>Erledigt · {completedItems}</Text> : null}
|
||||||
onUpdatePrice={onUpdateItemPrice}
|
{shoppingList.items.filter((item) => item.checked).map((item) => (
|
||||||
/>
|
<ShoppingListItem item={item} key={item.id} onDelete={() => onDeleteItem(item.id)} onToggle={() => onToggleItem(item.id)} onChangeQuantity={(change) => onChangeItemQuantity(item.id, change)} onUpdatePrice={onUpdateItemPrice} />
|
||||||
))
|
))}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shoppingList.items.length > 0 ? (
|
{shoppingList.items.length > 0 ? (
|
||||||
@@ -154,6 +173,11 @@ function formatAmount(value, unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
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: {
|
summary: {
|
||||||
backgroundColor: colors.surface,
|
backgroundColor: colors.surface,
|
||||||
borderColor: colors.border,
|
borderColor: colors.border,
|
||||||
|
|||||||
Reference in New Issue
Block a user