Apply manual prices to budget rating

This commit is contained in:
2026-07-01 11:18:00 +02:00
parent 6cf8906798
commit ac7ba88422
6 changed files with 84 additions and 17 deletions
+1 -1
View File
@@ -73,7 +73,7 @@ Die App enthält jetzt einen MVP-Flow mit Bottom-Tabs:
- Produktdetail: Ampelbewertung nach Allergien, Budget, Ernährung und Nachhaltigkeit - Produktdetail: Ampelbewertung nach Allergien, Budget, Ernährung und Nachhaltigkeit
- Alternativen: bessere Produktvorschläge aus dem lokalen Beispieldatensatz - Alternativen: bessere Produktvorschläge aus dem lokalen Beispieldatensatz
- Einkaufsliste: Produkte hinzufügen, abhaken, löschen und Gesamtsumme sehen - 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: Beispiel-Barcodes für Tests über Open Food Facts:
+49 -4
View File
@@ -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 ( return (
<View style={styles.wrap}> <View style={styles.wrap}>
<Text style={styles.label}>Maximales Einkaufsbudget</Text> <Text style={styles.label}>Maximales Einkaufsbudget</Text>
@@ -15,6 +17,23 @@ export default function BudgetInput({ value, currency, onChangeValue }) {
value={String(value ?? '')} value={String(value ?? '')}
/> />
</View> </View>
<View style={styles.periodGrid}>
{budgetPeriods.map((option) => {
const isActive = period === option;
return (
<Pressable
accessibilityRole="button"
key={option}
onPress={() => onChangePeriod(option)}
style={[styles.periodChip, isActive ? styles.activePeriodChip : null]}
>
<Text style={[styles.periodText, isActive ? styles.activePeriodText : null]}>
pro {budgetPeriodLabels[option]}
</Text>
</Pressable>
);
})}
</View>
</View> </View>
); );
} }
@@ -25,7 +44,7 @@ const styles = StyleSheet.create({
borderColor: colors.border, borderColor: colors.border,
borderRadius: 8, borderRadius: 8,
borderWidth: 1, borderWidth: 1,
gap: 8, gap: 10,
padding: 14 padding: 14
}, },
label: { label: {
@@ -52,5 +71,31 @@ const styles = StyleSheet.create({
fontSize: 16, fontSize: 16,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 10 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
} }
}); });
+9 -9
View File
@@ -17,24 +17,24 @@ export default function ProductDetailScreen({ product, preferences, onBack, onAd
const [savedMessage, setSavedMessage] = useState(''); const [savedMessage, setSavedMessage] = useState('');
const [manualPrice, setManualPrice] = useState(''); const [manualPrice, setManualPrice] = useState('');
const [priceError, setPriceError] = useState(''); const [priceError, setPriceError] = useState('');
const rating = useMemo(() => calculateProductRating(product, preferences), [product, preferences]);
const hasProductPrice = typeof product.price === 'number'; const hasProductPrice = typeof product.price === 'number';
const currency = product.currency || preferences.currency; 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() { function addToList() {
const parsedManualPrice = parseManualPrice(manualPrice);
if (!hasProductPrice && parsedManualPrice === undefined) { if (!hasProductPrice && parsedManualPrice === undefined) {
setSavedMessage(''); setSavedMessage('');
setPriceError('Bitte gib einen Preis ein, bevor du das Produkt hinzufügst.'); setPriceError('Bitte gib einen Preis ein, bevor du das Produkt hinzufügst.');
return; return;
} }
onAddToList({ onAddToList(pricedProduct);
...product,
currency,
price: hasProductPrice ? product.price : parsedManualPrice
});
setPriceError(''); setPriceError('');
setSavedMessage('Produkt wurde zur Einkaufsliste hinzugefügt.'); setSavedMessage('Produkt wurde zur Einkaufsliste hinzugefügt.');
} }
@@ -50,7 +50,7 @@ export default function ProductDetailScreen({ product, preferences, onBack, onAd
<Text style={styles.brand}>{product.brand || 'Ohne Marke'}</Text> <Text style={styles.brand}>{product.brand || 'Ohne Marke'}</Text>
<Text style={styles.name}>{product.name}</Text> <Text style={styles.name}>{product.name}</Text>
<Text style={styles.meta}> <Text style={styles.meta}>
{product.category || 'Produkt'} · {formatPrice(product.price, currency)} {product.category || 'Produkt'} · {formatPrice(pricedProduct.price, currency)}
</Text> </Text>
</View> </View>
<ProductScoreBadge status={rating.overallStatus} score={rating.overallScore} /> <ProductScoreBadge status={rating.overallStatus} score={rating.overallScore} />
+2
View File
@@ -68,7 +68,9 @@ export default function ProfileScreen({ preferences, onChangePreferences }) {
<BudgetInput <BudgetInput
currency={preferences.currency} currency={preferences.currency}
onChangePeriod={(value) => updatePreference('budgetPeriod', value)}
onChangeValue={(value) => updatePreference('maxBudgetPerShoppingTrip', value)} onChangeValue={(value) => updatePreference('maxBudgetPerShoppingTrip', value)}
period={preferences.budgetPeriod || 'week'}
value={preferences.maxBudgetPerShoppingTrip} value={preferences.maxBudgetPerShoppingTrip}
/> />
+15 -3
View File
@@ -134,14 +134,26 @@ function rateBudget(product, preferences) {
} }
const share = product.price / preferences.maxBudgetPerShoppingTrip; const share = product.price / preferences.maxBudgetPerShoppingTrip;
const periodLabel = budgetPeriodLabel(preferences.budgetPeriod);
if (share <= 0.08) { 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) { 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;
} }
+8
View File
@@ -17,6 +17,7 @@ export const defaultPreferences = {
preferredDiet: 'lowSugar', preferredDiet: 'lowSugar',
preferBio: true, preferBio: true,
maxBudgetPerShoppingTrip: 45, maxBudgetPerShoppingTrip: 45,
budgetPeriod: 'week',
currency: 'CHF', currency: 'CHF',
offlineModeEnabled: false, offlineModeEnabled: false,
sustainabilityPriority: 'medium', sustainabilityPriority: 'medium',
@@ -30,3 +31,10 @@ export const dietLabels = {
lowSugar: 'Wenig Zucker', lowSugar: 'Wenig Zucker',
highProtein: 'Proteinreich' highProtein: 'Proteinreich'
}; };
export const budgetPeriodLabels = {
day: 'Tag',
week: 'Woche',
month: 'Monat',
shoppingTrip: 'Einkauf'
};