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
|
- 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:
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user