299 lines
9.0 KiB
JavaScript
299 lines
9.0 KiB
JavaScript
import { useMemo, useState } from 'react';
|
||
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
||
|
||
import PrimaryButton from '../components/PrimaryButton';
|
||
import SecondaryButton from '../components/SecondaryButton';
|
||
import AppHeader from '../components/layout/AppHeader';
|
||
import ScreenContainer from '../components/layout/ScreenContainer';
|
||
import SectionTitle from '../components/layout/SectionTitle';
|
||
import ProductImage from '../components/product/ProductImage';
|
||
import ProductScoreBadge from '../components/product/ProductScoreBadge';
|
||
import RatingCategoryRow from '../components/product/RatingCategoryRow';
|
||
import { calculateProductRating } from '../services/ratingService';
|
||
import { colors } from '../utils/constants';
|
||
import { formatPrice } from '../utils/formatPrice';
|
||
|
||
export default function ProductDetailScreen({
|
||
product,
|
||
preferences,
|
||
onBack,
|
||
onAddToList,
|
||
onOpenList,
|
||
onSaveManualPrice,
|
||
onShowRatingCriteria,
|
||
onShowAlternatives
|
||
}) {
|
||
const [savedMessage, setSavedMessage] = useState('');
|
||
const [manualPrice, setManualPrice] = useState('');
|
||
const [priceError, setPriceError] = useState('');
|
||
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]);
|
||
const recommendation = rating.overallStatus === 'good' ? 'Passt gut zu dir' : rating.overallStatus === 'medium' ? 'Genauer prüfen' : 'Lieber meiden';
|
||
|
||
function addToList() {
|
||
if (!hasProductPrice && manualPrice.trim() && parsedManualPrice === undefined) {
|
||
setSavedMessage('');
|
||
setPriceError('Bitte gib einen gültigen Preis ein oder lasse das Feld leer.');
|
||
return;
|
||
}
|
||
|
||
if (!hasProductPrice && typeof pricedProduct.price === 'number') {
|
||
onSaveManualPrice(pricedProduct, pricedProduct.price);
|
||
}
|
||
|
||
onAddToList(pricedProduct);
|
||
setPriceError('');
|
||
setSavedMessage('Zur Einkaufsliste hinzugefügt.');
|
||
}
|
||
|
||
return (
|
||
<ScreenContainer>
|
||
<AppHeader title="Produkt im Check" subtitle="Bewertung nach deinen Einstellungen" onBack={onBack} />
|
||
|
||
<View style={styles.productPanel}>
|
||
<ProductImage imageUrl={product.imageUrl} productName={product.name} size="large" />
|
||
<View style={styles.topRow}>
|
||
<View style={styles.copy}>
|
||
<Text style={styles.brand}>{product.brand || 'Ohne Marke'}</Text>
|
||
<Text style={styles.name}>{product.name}</Text>
|
||
<Text style={styles.meta}>
|
||
{product.category || 'Produkt'} · {formatPrice(pricedProduct.price, currency)}
|
||
</Text>
|
||
<Text style={styles.source}>{sourceLabel(product.dataSource)}</Text>
|
||
</View>
|
||
<ProductScoreBadge status={rating.overallStatus} score={rating.overallScore} />
|
||
</View>
|
||
<View style={[styles.recommendation, styles[`${rating.overallStatus}Recommendation`]]}>
|
||
<Text style={[styles.recommendationTitle, styles[`${rating.overallStatus}Text`]]}>{recommendation}</Text>
|
||
<Text style={styles.recommendationCopy}>Tippe unten auf ein Kriterium, um die Bewertung nachzuvollziehen.</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{!hasProductPrice ? (
|
||
<View style={styles.pricePanel}>
|
||
<Text style={styles.priceTitle}>Ladenpreis ergänzen</Text>
|
||
<Text style={styles.priceCopy}>
|
||
Optional. Ohne Preis kannst du das Produkt speichern; die Einkaufssumme bleibt dann unvollständig.
|
||
</Text>
|
||
<View style={styles.priceInputRow}>
|
||
<Text style={styles.currency}>{currency}</Text>
|
||
<TextInput
|
||
accessibilityLabel="Ladenpreis"
|
||
keyboardType="decimal-pad"
|
||
onChangeText={(value) => {
|
||
setManualPrice(value);
|
||
setPriceError('');
|
||
setSavedMessage('');
|
||
}}
|
||
placeholder="z. B. 3.50"
|
||
placeholderTextColor={colors.muted}
|
||
style={styles.priceInput}
|
||
value={manualPrice}
|
||
/>
|
||
</View>
|
||
{priceError ? <Text style={styles.errorText}>{priceError}</Text> : null}
|
||
</View>
|
||
) : null}
|
||
|
||
{savedMessage ? (
|
||
<View style={styles.savedPanel}>
|
||
<Text style={styles.saved}>{savedMessage}</Text>
|
||
<Pressable accessibilityRole="button" onPress={onOpenList}>
|
||
<Text style={styles.savedLink}>Einkaufsliste ansehen ›</Text>
|
||
</Pressable>
|
||
</View>
|
||
) : null}
|
||
<PrimaryButton label="Zur Einkaufsliste hinzufügen" onPress={addToList} />
|
||
<SecondaryButton label="Bessere Alternativen suchen" onPress={() => onShowAlternatives(product)} />
|
||
|
||
<SectionTitle>Bewertungskriterien</SectionTitle>
|
||
{Object.entries(rating.categoryScores).map(([category, categoryRating]) => (
|
||
<RatingCategoryRow
|
||
category={category}
|
||
key={category}
|
||
onPress={() => onShowRatingCriteria(pricedProduct, category)}
|
||
rating={categoryRating}
|
||
/>
|
||
))}
|
||
|
||
<SectionTitle>Produktdaten</SectionTitle>
|
||
<View style={styles.dataPanel}>
|
||
<Text style={styles.dataText}>Barcode: {product.barcode}</Text>
|
||
<Text style={styles.dataText}>Zutaten: {(product.ingredients || []).join(', ') || 'Keine Angaben'}</Text>
|
||
<Text style={styles.dataText}>Allergene: {(product.allergens || []).join(', ') || 'Keine Angaben'}</Text>
|
||
<Text style={styles.dataText}>Nutri-Score: {product.nutrition?.nutriScore || 'Keine Angabe'}</Text>
|
||
<Text style={styles.dataText}>Eco-Score: {product.sustainability?.ecoScore || 'Keine Angabe'}</Text>
|
||
</View>
|
||
|
||
</ScreenContainer>
|
||
);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
function sourceLabel(source) {
|
||
const labels = {
|
||
cache: 'Quelle: lokaler Cache',
|
||
local: 'Quelle: lokale Beispieldaten',
|
||
'open-food-facts': 'Quelle: Open Food Facts'
|
||
};
|
||
|
||
return labels[source] || 'Quelle: unbekannt';
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
productPanel: {
|
||
backgroundColor: colors.surface,
|
||
borderColor: colors.border,
|
||
borderRadius: 20,
|
||
borderWidth: 1,
|
||
gap: 14,
|
||
padding: 18
|
||
},
|
||
topRow: {
|
||
alignItems: 'flex-start',
|
||
flexDirection: 'row',
|
||
gap: 12
|
||
},
|
||
copy: {
|
||
flex: 1
|
||
},
|
||
brand: {
|
||
color: colors.primary,
|
||
fontSize: 13,
|
||
fontWeight: '800',
|
||
textTransform: 'uppercase'
|
||
},
|
||
name: {
|
||
color: colors.text,
|
||
fontSize: 25,
|
||
fontWeight: '800',
|
||
lineHeight: 31,
|
||
marginTop: 4
|
||
},
|
||
meta: {
|
||
color: colors.muted,
|
||
fontSize: 15,
|
||
marginTop: 6
|
||
},
|
||
source: {
|
||
alignSelf: 'flex-start',
|
||
backgroundColor: '#eef3ef',
|
||
borderRadius: 8,
|
||
color: colors.primaryDark,
|
||
fontSize: 12,
|
||
fontWeight: '800',
|
||
marginTop: 10,
|
||
paddingHorizontal: 8,
|
||
paddingVertical: 4
|
||
},
|
||
recommendation: {
|
||
borderRadius: 14,
|
||
gap: 4,
|
||
padding: 14
|
||
},
|
||
goodRecommendation: { backgroundColor: '#e4f4e9' },
|
||
mediumRecommendation: { backgroundColor: '#fff3d7' },
|
||
criticalRecommendation: { backgroundColor: '#fee9e7' },
|
||
recommendationTitle: {
|
||
fontSize: 17,
|
||
fontWeight: '800'
|
||
},
|
||
goodText: { color: colors.success },
|
||
mediumText: { color: '#8a5914' },
|
||
criticalText: { color: colors.danger },
|
||
recommendationCopy: {
|
||
color: colors.text,
|
||
fontSize: 13,
|
||
lineHeight: 19
|
||
},
|
||
pricePanel: {
|
||
backgroundColor: colors.surface,
|
||
borderColor: colors.border,
|
||
borderRadius: 18,
|
||
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: 12,
|
||
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,
|
||
borderRadius: 18,
|
||
borderWidth: 1,
|
||
gap: 8,
|
||
padding: 14
|
||
},
|
||
dataText: {
|
||
color: colors.muted,
|
||
fontSize: 14,
|
||
lineHeight: 20
|
||
},
|
||
saved: {
|
||
color: colors.success,
|
||
fontSize: 14,
|
||
fontWeight: '800'
|
||
},
|
||
savedPanel: {
|
||
backgroundColor: colors.surfaceTint,
|
||
borderRadius: 14,
|
||
gap: 6,
|
||
padding: 14
|
||
},
|
||
savedLink: {
|
||
color: colors.primary,
|
||
fontSize: 14,
|
||
fontWeight: '800'
|
||
}
|
||
});
|