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
+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 (
<View style={styles.wrap}>
<Text style={styles.label}>Maximales Einkaufsbudget</Text>
@@ -15,6 +17,23 @@ export default function BudgetInput({ value, currency, onChangeValue }) {
value={String(value ?? '')}
/>
</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>
);
}
@@ -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
}
});
+9 -9
View File
@@ -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
<Text style={styles.brand}>{product.brand || 'Ohne Marke'}</Text>
<Text style={styles.name}>{product.name}</Text>
<Text style={styles.meta}>
{product.category || 'Produkt'} · {formatPrice(product.price, currency)}
{product.category || 'Produkt'} · {formatPrice(pricedProduct.price, currency)}
</Text>
</View>
<ProductScoreBadge status={rating.overallStatus} score={rating.overallScore} />
+2
View File
@@ -68,7 +68,9 @@ export default function ProfileScreen({ preferences, onChangePreferences }) {
<BudgetInput
currency={preferences.currency}
onChangePeriod={(value) => updatePreference('budgetPeriod', value)}
onChangeValue={(value) => updatePreference('maxBudgetPerShoppingTrip', value)}
period={preferences.budgetPeriod || 'week'}
value={preferences.maxBudgetPerShoppingTrip}
/>
+15 -3
View File
@@ -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;
}
+8
View File
@@ -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'
};