Apply manual prices to budget rating
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user