From 6cf89067980e86ce0d3a5ccec72ef266ab344ab2 Mon Sep 17 00:00:00 2001 From: fretuks Date: Wed, 1 Jul 2026 11:08:53 +0200 Subject: [PATCH] Allow manual prices for products --- README.md | 1 + src/screens/AlternativesScreen.js | 9 ++- src/screens/ProductDetailScreen.js | 103 ++++++++++++++++++++++++++++- 3 files changed, 109 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 53333da..24c3328 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Die App richtet sich an Privatpersonen, die im Supermarkt schnell bessere Kaufen - Kurze Begründung zur Bewertung und mögliche Alternativhinweise - Manuelle Produktsuche, falls der Barcode nicht erkannt wird - Lokale Einkaufsliste mit Menge, Preis, Abhaken und Löschen +- Manuelle Preiseingabe, wenn Open Food Facts keinen Artikelpreis liefert - Lokaler Produktcache und Offline-Modus für gespeicherte Produkte - Produktdaten per Barcode aus der Open-Food-Facts-API - Später geplant: erweiterte Profile, Budgettracking, Nachhaltigkeitsfilter und Rezeptvorschläge diff --git a/src/screens/AlternativesScreen.js b/src/screens/AlternativesScreen.js index 21fb908..a953d94 100644 --- a/src/screens/AlternativesScreen.js +++ b/src/screens/AlternativesScreen.js @@ -49,7 +49,14 @@ export default function AlternativesScreen({ product, preferences, productCache, onChoose(item.product)} + onChoose={() => { + if (typeof item.product.price === 'number') { + onChoose(item.product); + return; + } + + onOpenProduct(item.product); + }} onOpen={() => onOpenProduct(item.product)} /> ))} diff --git a/src/screens/ProductDetailScreen.js b/src/screens/ProductDetailScreen.js index c3494b3..7dff833 100644 --- a/src/screens/ProductDetailScreen.js +++ b/src/screens/ProductDetailScreen.js @@ -1,5 +1,5 @@ import { useMemo, useState } from 'react'; -import { StyleSheet, Text, View } from 'react-native'; +import { StyleSheet, Text, TextInput, View } from 'react-native'; import PrimaryButton from '../components/PrimaryButton'; import SecondaryButton from '../components/SecondaryButton'; @@ -15,10 +15,27 @@ import { formatPrice } from '../utils/formatPrice'; export default function ProductDetailScreen({ product, preferences, onBack, onAddToList, onShowAlternatives }) { 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; function addToList() { - onAddToList(product); + 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 + }); + setPriceError(''); setSavedMessage('Produkt wurde zur Einkaufsliste hinzugefügt.'); } @@ -32,7 +49,9 @@ export default function ProductDetailScreen({ product, preferences, onBack, onAd {product.brand || 'Ohne Marke'} {product.name} - {product.category} · {formatPrice(product.price, product.currency)} + + {product.category || 'Produkt'} · {formatPrice(product.price, currency)} + @@ -41,6 +60,30 @@ export default function ProductDetailScreen({ product, preferences, onBack, onAd + {!hasProductPrice ? ( + + Preis ergänzen + + Open Food Facts liefert keine Preise. Gib den Ladenpreis ein, damit die Einkaufsliste und Budgetprüfung stimmen. + + + {currency} + { + setManualPrice(value); + setPriceError(''); + }} + placeholder="z. B. 3.50" + placeholderTextColor={colors.muted} + style={styles.priceInput} + value={manualPrice} + /> + + {priceError ? {priceError} : null} + + ) : null} + Bewertungskriterien {Object.entries(rating.categoryScores).map(([category, categoryRating]) => ( @@ -62,6 +105,17 @@ export default function ProductDetailScreen({ product, preferences, onBack, onAd ); } +function parseManualPrice(value) { + const normalizedValue = value.trim().replace(',', '.'); + const parsedValue = Number(normalizedValue); + + if (!normalizedValue || !Number.isFinite(parsedValue) || parsedValue <= 0) { + return undefined; + } + + return Math.round(parsedValue * 100) / 100; +} + const styles = StyleSheet.create({ productPanel: { backgroundColor: colors.surface, @@ -102,6 +156,49 @@ const styles = StyleSheet.create({ fontSize: 16, fontWeight: '700' }, + pricePanel: { + backgroundColor: colors.surface, + borderColor: colors.border, + borderRadius: 8, + borderWidth: 1, + gap: 10, + padding: 16 + }, + priceTitle: { + color: colors.text, + fontSize: 17, + fontWeight: '800' + }, + priceCopy: { + color: colors.muted, + fontSize: 14, + lineHeight: 20 + }, + priceInputRow: { + alignItems: 'center', + flexDirection: 'row', + gap: 10 + }, + currency: { + color: colors.muted, + fontSize: 16, + fontWeight: '800' + }, + priceInput: { + borderColor: colors.border, + borderRadius: 8, + borderWidth: 1, + color: colors.text, + flex: 1, + fontSize: 16, + paddingHorizontal: 12, + paddingVertical: 10 + }, + errorText: { + color: colors.danger, + fontSize: 13, + fontWeight: '800' + }, dataPanel: { backgroundColor: colors.surface, borderColor: colors.border,