import React, { useState, useRef, useEffect } from 'react'; import { View, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView, ActivityIndicator, Alert, Image, Platform, } from 'react-native'; import { DatePickerModal } from '../../src/components/ui/DatePicker'; import * as ImagePicker from 'expo-image-picker'; import { CameraView, useCameraPermissions, BarcodeScanningResult } from 'expo-camera'; import { router, useLocalSearchParams } from 'expo-router'; import { MaterialIcons } from '@expo/vector-icons'; import { format } from 'date-fns'; import { de } from 'date-fns/locale'; import { createItem } from '../../src/services/items'; import { recognizeItemFromPhoto } from '../../src/services/ai'; import { lookupBarcode } from '../../src/services/barcode'; import { getStoredHouseholdId, getOrCreateDeviceId } from '../../src/services/household'; import { useHouseholdStore } from '../../src/hooks/useHousehold'; import { COLORS, CATEGORY_LABELS, BASE_CATEGORIES, STORAGE_LOCATIONS } from '../../src/constants'; import { ChipSelectInput } from '../../src/components/ui/ChipSelectInput'; import { useCustomOptions } from '../../src/hooks/useCustomOptions'; import { ItemCategory } from '../../src/types'; type Step = 'camera' | 'form'; export default function AddItemModal() { const { householdId: paramHouseholdId, deviceId: paramDeviceId } = useLocalSearchParams<{ householdId: string; deviceId: string }>(); const householdFromStore = useHouseholdStore((s) => s.household); const deviceIdFromStore = useHouseholdStore((s) => s.deviceId); const [householdId, setHouseholdId] = useState(paramHouseholdId || householdFromStore?.id || ''); const [deviceId, setDeviceId] = useState(paramDeviceId || deviceIdFromStore || ''); useEffect(() => { const resolve = async () => { if (!householdId) { const stored = await getStoredHouseholdId(); if (stored) setHouseholdId(stored); } if (!deviceId) { const stored = await getOrCreateDeviceId(); if (stored) setDeviceId(stored); } }; resolve(); }, []); const [permission, requestPermission] = useCameraPermissions(); const [step, setStep] = useState('camera'); const [mode, setMode] = useState<'photo' | 'barcode'>('photo'); const [recognizing, setRecognizing] = useState(false); const recognizingRef = useRef(false); const [photoUri, setPhotoUri] = useState(null); const [saving, setSaving] = useState(false); const cameraRef = useRef(null); // Form fields const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [category, setCategory] = useState('food'); const [quantity, setQuantity] = useState('1'); const [unit, setUnit] = useState('Stück'); const [minStock, setMinStock] = useState('0'); const [storageLocation, setStorageLocation] = useState(''); const [shoppingLocation, setShoppingLocation] = useState(''); const [price, setPrice] = useState(''); const [barcode, setBarcode] = useState(null); const [expiryDate, setExpiryDate] = useState(null); const [showDatePicker, setShowDatePicker] = useState(false); const { options: locationOptions, customOptions: customLocations, addOption: addLocation, removeOption: removeLocation } = useCustomOptions( 'houseorg_custom_locations', STORAGE_LOCATIONS ); const { options: categoryOptions, customOptions: customCategories, addOption: addCategory, removeOption: removeCategory } = useCustomOptions( 'houseorg_custom_categories', [...BASE_CATEGORIES] ); const takePhoto = async () => { if (!cameraRef.current) return; setRecognizing(true); try { const photo = await cameraRef.current.takePictureAsync({ quality: 0.8, base64: false }); if (!photo) return; setPhotoUri(photo.uri); const result = await recognizeItemFromPhoto(photo.uri); setName(result.name); setDescription(result.description); setCategory(result.category); setStep('form'); } catch { Alert.alert('KI-Erkennung fehlgeschlagen', 'Bitte manuell ausfüllen.'); setStep('form'); } finally { setRecognizing(false); } }; const onBarcodeScanned = async (result: BarcodeScanningResult) => { if (recognizingRef.current) return; recognizingRef.current = true; setRecognizing(true); setBarcode(result.data); try { const product = await lookupBarcode(result.data); if (product) { setName(product.name); setDescription(product.description); setCategory(product.category); if (product.imageUrl) setPhotoUri(product.imageUrl); } else { Alert.alert('Barcode nicht gefunden', 'Bitte manuell ausfüllen.'); } } catch (e: any) { Alert.alert('Barcode-Abfrage fehlgeschlagen', e?.message ?? 'Netzwerkfehler'); } finally { recognizingRef.current = false; setRecognizing(false); setStep('form'); } }; const pickImage = () => { Alert.alert('Foto hinzufügen', '', [ { text: 'Kamera', onPress: async () => { const result = await ImagePicker.launchCameraAsync({ quality: 0.8, allowsEditing: true, aspect: [4, 3] }); if (!result.canceled) setPhotoUri(result.assets[0].uri); }, }, { text: 'Galerie', onPress: async () => { const result = await ImagePicker.launchImageLibraryAsync({ quality: 0.8, allowsEditing: true, aspect: [4, 3] }); if (!result.canceled) setPhotoUri(result.assets[0].uri); }, }, { text: 'Abbrechen', style: 'cancel' }, ]); }; const handleSave = async () => { if (!householdId) { Alert.alert('Fehler', 'Kein Haushalt gefunden. Bitte App neu starten.'); return; } if (!deviceId) { Alert.alert('Fehler', 'Geräte-ID fehlt. Bitte App neu starten.'); return; } if (!name.trim()) { Alert.alert('Bitte gib einen Namen ein.'); return; } setSaving(true); try { console.log('Saving item to household:', householdId); await createItem(householdId, { name: name.trim(), description: description.trim(), category, quantity: parseFloat(quantity) || 0, unit, minStockThreshold: parseFloat(minStock) || 0, photoUrl: null, storageLocation: storageLocation.trim(), shoppingLocation: shoppingLocation.trim(), price: price ? parseFloat(price) : null, expiryDate, barcode, addedByDevice: deviceId, photoUri: photoUri && !photoUri.startsWith('http') ? photoUri : undefined, }); console.log('Item saved successfully'); router.back(); } catch (e: any) { console.error('createItem error:', e); Alert.alert('Speichern fehlgeschlagen', e?.message ?? String(e)); } finally { setSaving(false); } }; if (!permission) return null; if (!permission.granted) { return ( Kamera-Zugriff benötigt Zugriff erlauben ); } if (step === 'camera') { return ( {recognizing && ( {mode === 'photo' ? 'Artikel wird erkannt…' : 'Barcode wird gesucht…'} )} router.back()} style={styles.cameraTopBtn}> {Platform.OS !== 'web' && ( setMode('photo')} > Foto setMode('barcode')} > Barcode )} { setStep('form'); }} style={styles.cameraTopBtn} > Manuell {mode === 'photo' && !recognizing && ( )} {mode === 'barcode' && ( Barcode in den Rahmen halten )} ); } return ( {/* Foto-Sektion */} {photoUri ? ( <> Foto ändern ) : ( Foto hinzufügen )} CATEGORY_LABELS[cat] ?? cat} addPlaceholder="Neue Kategorie…" /> setShowDatePicker(true)} activeOpacity={0.7} > {expiryDate ? format(expiryDate, 'dd. MMMM yyyy', { locale: de }) : 'Kein MHD'} {expiryDate && ( setExpiryDate(null)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}> )} {showDatePicker && ( setShowDatePicker(false)} /> )} {saving ? ( ) : ( Artikel speichern )} ); } function Field({ label, children, style }: { label: string; children: React.ReactNode; style?: object }) { return ( {label} {children} ); } const styles = StyleSheet.create({ permissionContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16, padding: 32 }, permissionText: { fontSize: 18, fontWeight: '600', color: COLORS.text, textAlign: 'center' }, cameraContainer: { flex: 1, backgroundColor: '#000' }, overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center', gap: 16 }, overlayText: { color: COLORS.white, fontSize: 16, fontWeight: '600' }, cameraTop: { position: 'absolute', top: 48, left: 0, right: 0, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16 }, cameraTopBtn: { padding: 8 }, modeToggle: { flex: 1, flexDirection: 'row', justifyContent: 'center', gap: 8 }, modeBtn: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.2)' }, modeBtnActive: { backgroundColor: COLORS.white }, modeBtnText: { color: COLORS.white, fontWeight: '600', fontSize: 14 }, cameraBottom: { position: 'absolute', bottom: 48, left: 0, right: 0, alignItems: 'center' }, shutter: { width: 72, height: 72, borderRadius: 36, backgroundColor: 'rgba(255,255,255,0.3)', justifyContent: 'center', alignItems: 'center', borderWidth: 3, borderColor: COLORS.white }, shutterInner: { width: 56, height: 56, borderRadius: 28, backgroundColor: COLORS.white }, barcodeHint: { position: 'absolute', bottom: 80, left: 0, right: 0, alignItems: 'center' }, barcodeHintText: { color: COLORS.white, fontSize: 14, backgroundColor: 'rgba(0,0,0,0.5)', paddingHorizontal: 16, paddingVertical: 8, borderRadius: 8 }, form: { flex: 1, backgroundColor: COLORS.white }, formContent: { padding: 24, paddingBottom: 48 }, photoSection: { marginBottom: 20, borderRadius: 12, overflow: 'hidden' }, preview: { width: '100%', height: 180, borderRadius: 12, resizeMode: 'cover' }, photoOverlay: { position: 'absolute', bottom: 0, left: 0, right: 0, backgroundColor: 'rgba(0,0,0,0.45)', flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, paddingVertical: 8, }, photoOverlayText: { color: COLORS.white, fontSize: 13, fontWeight: '600' }, photoPlaceholder: { height: 120, borderRadius: 12, borderWidth: 1.5, borderColor: COLORS.border, borderStyle: 'dashed', backgroundColor: COLORS.surface, alignItems: 'center', justifyContent: 'center', gap: 8, }, photoPlaceholderText: { fontSize: 14, color: COLORS.primaryLight, fontWeight: '500' }, label: { fontSize: 13, fontWeight: '600', color: COLORS.textSecondary, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 0.3 }, input: { borderWidth: 1.5, borderColor: COLORS.border, borderRadius: 12, padding: 13, fontSize: 16, backgroundColor: COLORS.surface, color: COLORS.text }, inputMulti: { height: 80, textAlignVertical: 'top' }, dateInput: { flexDirection: 'row', alignItems: 'center', gap: 10 }, dateText: { flex: 1, fontSize: 16, color: COLORS.text }, datePlaceholder: { color: COLORS.textSecondary }, dateConfirm: { alignSelf: 'flex-end', marginTop: 8, paddingHorizontal: 16, paddingVertical: 8, backgroundColor: COLORS.primary, borderRadius: 8 }, dateConfirmText: { color: COLORS.white, fontWeight: '600', fontSize: 14 }, segmented: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 }, segBtn: { paddingHorizontal: 14, paddingVertical: 8, borderRadius: 20, borderWidth: 1.5, borderColor: COLORS.border }, segBtnActive: { backgroundColor: COLORS.primary, borderColor: COLORS.primary }, segBtnText: { fontSize: 13, color: COLORS.textSecondary, fontWeight: '500' }, segBtnTextActive: { color: COLORS.white }, row: { flexDirection: 'row', gap: 12 }, btn: { backgroundColor: COLORS.primary, borderRadius: 14, paddingVertical: 16, alignItems: 'center', marginTop: 8, shadowColor: COLORS.primary, shadowOpacity: 0.28, shadowRadius: 10, shadowOffset: { width: 0, height: 4 }, elevation: 4, }, btnDisabled: { opacity: 0.6, shadowOpacity: 0 }, btnText: { color: COLORS.white, fontSize: 16, fontWeight: '600' }, });