diff --git a/README.md b/README.md index 24c3328..621ab9c 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Die App enthält jetzt einen MVP-Flow mit Bottom-Tabs: - Produktdetail: Ampelbewertung nach Allergien, Budget, Ernährung und Nachhaltigkeit - Alternativen: bessere Produktvorschläge aus dem lokalen Beispieldatensatz - Einkaufsliste: Produkte hinzufügen, abhaken, löschen und Gesamtsumme sehen -- Profil: Präferenzen, Allergien, Budget, Bio-Vorliebe und Offline-Modus verwalten +- Profil: Präferenzen, Allergien, Budget mit Zeitraum, Bio-Vorliebe und Offline-Modus verwalten Beispiel-Barcodes für Tests über Open Food Facts: diff --git a/src/components/form/BudgetInput.js b/src/components/form/BudgetInput.js index 379e062..be80bd6 100644 --- a/src/components/form/BudgetInput.js +++ b/src/components/form/BudgetInput.js @@ -1,8 +1,10 @@ -import { StyleSheet, Text, TextInput, View } from 'react-native'; +import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'; -import { colors } from '../../utils/constants'; +import { budgetPeriodLabels, colors } from '../../utils/constants'; -export default function BudgetInput({ value, currency, onChangeValue }) { +const budgetPeriods = ['day', 'week', 'month', 'shoppingTrip']; + +export default function BudgetInput({ value, currency, period, onChangePeriod, onChangeValue }) { return ( Maximales Einkaufsbudget @@ -15,6 +17,23 @@ export default function BudgetInput({ value, currency, onChangeValue }) { value={String(value ?? '')} /> + + {budgetPeriods.map((option) => { + const isActive = period === option; + return ( + onChangePeriod(option)} + style={[styles.periodChip, isActive ? styles.activePeriodChip : null]} + > + + pro {budgetPeriodLabels[option]} + + + ); + })} + ); } @@ -25,7 +44,7 @@ const styles = StyleSheet.create({ borderColor: colors.border, borderRadius: 8, borderWidth: 1, - gap: 8, + gap: 10, padding: 14 }, label: { @@ -52,5 +71,31 @@ const styles = StyleSheet.create({ fontSize: 16, paddingHorizontal: 12, paddingVertical: 10 + }, + periodGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 8 + }, + periodChip: { + alignItems: 'center', + borderColor: colors.border, + borderRadius: 8, + borderWidth: 1, + flexGrow: 1, + paddingHorizontal: 10, + paddingVertical: 9 + }, + activePeriodChip: { + backgroundColor: colors.primary, + borderColor: colors.primary + }, + periodText: { + color: colors.text, + fontSize: 13, + fontWeight: '800' + }, + activePeriodText: { + color: colors.surface } }); diff --git a/src/screens/ProductDetailScreen.js b/src/screens/ProductDetailScreen.js index 7dff833..62bff3b 100644 --- a/src/screens/ProductDetailScreen.js +++ b/src/screens/ProductDetailScreen.js @@ -17,24 +17,24 @@ export default function ProductDetailScreen({ product, preferences, onBack, onAd const [savedMessage, setSavedMessage] = useState(''); const [manualPrice, setManualPrice] = useState(''); const [priceError, setPriceError] = useState(''); - const rating = useMemo(() => calculateProductRating(product, preferences), [product, preferences]); const hasProductPrice = typeof product.price === 'number'; const currency = product.currency || preferences.currency; + const parsedManualPrice = parseManualPrice(manualPrice); + const pricedProduct = useMemo(() => ({ + ...product, + currency, + price: hasProductPrice ? product.price : parsedManualPrice + }), [currency, hasProductPrice, parsedManualPrice, product]); + const rating = useMemo(() => calculateProductRating(pricedProduct, preferences), [pricedProduct, preferences]); function addToList() { - const parsedManualPrice = parseManualPrice(manualPrice); - if (!hasProductPrice && parsedManualPrice === undefined) { setSavedMessage(''); setPriceError('Bitte gib einen Preis ein, bevor du das Produkt hinzufügst.'); return; } - onAddToList({ - ...product, - currency, - price: hasProductPrice ? product.price : parsedManualPrice - }); + onAddToList(pricedProduct); setPriceError(''); setSavedMessage('Produkt wurde zur Einkaufsliste hinzugefügt.'); } @@ -50,7 +50,7 @@ export default function ProductDetailScreen({ product, preferences, onBack, onAd {product.brand || 'Ohne Marke'} {product.name} - {product.category || 'Produkt'} · {formatPrice(product.price, currency)} + {product.category || 'Produkt'} · {formatPrice(pricedProduct.price, currency)} diff --git a/src/screens/ProfileScreen.js b/src/screens/ProfileScreen.js index 107fd7b..42bb8fa 100644 --- a/src/screens/ProfileScreen.js +++ b/src/screens/ProfileScreen.js @@ -68,7 +68,9 @@ export default function ProfileScreen({ preferences, onChangePreferences }) { updatePreference('budgetPeriod', value)} onChangeValue={(value) => updatePreference('maxBudgetPerShoppingTrip', value)} + period={preferences.budgetPeriod || 'week'} value={preferences.maxBudgetPerShoppingTrip} /> diff --git a/src/services/ratingService.js b/src/services/ratingService.js index 118f27e..399b373 100644 --- a/src/services/ratingService.js +++ b/src/services/ratingService.js @@ -134,14 +134,26 @@ function rateBudget(product, preferences) { } const share = product.price / preferences.maxBudgetPerShoppingTrip; + const periodLabel = budgetPeriodLabel(preferences.budgetPeriod); if (share <= 0.08) { - return { status: 'good', message: 'Preis passt gut zu deinem Einkaufsbudget.' }; + return { status: 'good', message: `Preis passt gut zu deinem Budget pro ${periodLabel}.` }; } if (share <= 0.18) { - return { status: 'medium', message: 'Preis ist spürbar, aber noch im Rahmen.' }; + return { status: 'medium', message: `Preis ist für dein Budget pro ${periodLabel} spürbar, aber noch im Rahmen.` }; } - return { status: 'critical', message: 'Preis belastet dein Einkaufsbudget stark.' }; + return { status: 'critical', message: `Preis belastet dein Budget pro ${periodLabel} stark.` }; +} + +function budgetPeriodLabel(period) { + const labels = { + day: 'Tag', + week: 'Woche', + month: 'Monat', + shoppingTrip: 'Einkauf' + }; + + return labels[period] || labels.week; } diff --git a/src/utils/constants.js b/src/utils/constants.js index 8795a3d..ecb4de3 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -17,6 +17,7 @@ export const defaultPreferences = { preferredDiet: 'lowSugar', preferBio: true, maxBudgetPerShoppingTrip: 45, + budgetPeriod: 'week', currency: 'CHF', offlineModeEnabled: false, sustainabilityPriority: 'medium', @@ -30,3 +31,10 @@ export const dietLabels = { lowSugar: 'Wenig Zucker', highProtein: 'Proteinreich' }; + +export const budgetPeriodLabels = { + day: 'Tag', + week: 'Woche', + month: 'Monat', + shoppingTrip: 'Einkauf' +};