import { useEffect, useMemo, useState } from 'react'; import { Image, KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, StyleSheet, TextInput, View, } from 'react-native'; import * as ImagePicker from 'expo-image-picker'; import DateTimePicker from '@react-native-community/datetimepicker'; import { ResizeMode, Video } from 'expo-av'; import { useRouter } from 'expo-router'; import { ThemedButton } from '@/components/themed-button'; import { ThemedText } from '@/components/themed-text'; import { ThemedView } from '@/components/themed-view'; import { ZoomImageModal } from '@/components/zoom-image-modal'; import { Colors } from '@/constants/theme'; import { useColorScheme } from '@/hooks/use-color-scheme'; import { useTranslation } from '@/localization/i18n'; import { dbPromise, initCoreTables } from '@/services/db'; const CATEGORY_PRESETS = ['seed', 'fertilizer', 'labor', 'fuel', 'equipment', 'transport', 'misc']; type FieldRow = { id: number; name: string | null; }; type CropRow = { id: number; crop_name: string | null; }; export default function NewCostScreen() { const { t } = useTranslation(); const router = useRouter(); const theme = useColorScheme() ?? 'light'; const palette = Colors[theme]; const [status, setStatus] = useState(''); const [fields, setFields] = useState([]); const [crops, setCrops] = useState([]); const [currency, setCurrency] = useState('THB'); const [fieldModalOpen, setFieldModalOpen] = useState(false); const [cropModalOpen, setCropModalOpen] = useState(false); const [selectedFieldId, setSelectedFieldId] = useState(null); const [selectedCropId, setSelectedCropId] = useState(null); const [category, setCategory] = useState(''); const [amount, setAmount] = useState(''); const [vendor, setVendor] = useState(''); const [notes, setNotes] = useState(''); const [spentDate, setSpentDate] = useState(''); const [showSpentPicker, setShowSpentPicker] = useState(false); const [mediaUris, setMediaUris] = useState([]); const [activeUri, setActiveUri] = useState(null); const [errors, setErrors] = useState<{ field?: string; amount?: string }>({}); const [zoomUri, setZoomUri] = useState(null); const [saving, setSaving] = useState(false); useEffect(() => { let isActive = true; async function loadData() { try { await initCoreTables(); const db = await dbPromise; const fieldRows = await db.getAllAsync('SELECT id, name FROM fields ORDER BY name ASC;'); const cropRows = await db.getAllAsync('SELECT id, crop_name FROM crops ORDER BY crop_name ASC;'); const profileRow = await db.getFirstAsync<{ currency: string | null }>( 'SELECT currency FROM user_profile WHERE id = 1;' ); if (!isActive) return; setFields(fieldRows); setCrops(cropRows); setCurrency(profileRow?.currency ?? 'THB'); } catch (error) { if (isActive) setStatus(`Error: ${String(error)}`); } } loadData(); return () => { isActive = false; }; }, [t]); const selectedField = useMemo( () => fields.find((item) => item.id === selectedFieldId), [fields, selectedFieldId] ); const selectedCrop = useMemo( () => crops.find((item) => item.id === selectedCropId), [crops, selectedCropId] ); const inputStyle = [ styles.input, { borderColor: palette.border, backgroundColor: palette.input, color: palette.text, }, ]; async function handleSave() { const parsedAmount = amount.trim() ? Number(amount) : null; const nextErrors: { field?: string; amount?: string } = {}; if (!selectedFieldId) nextErrors.field = t('costs.fieldRequired'); if (!parsedAmount || !Number.isFinite(parsedAmount)) nextErrors.amount = t('costs.amountInvalid'); setErrors(nextErrors); if (Object.keys(nextErrors).length > 0) return; try { setSaving(true); const db = await dbPromise; const now = new Date().toISOString(); const primaryUri = mediaUris[0] ?? normalizeMediaUri(activeUri); const result = await db.runAsync( 'INSERT INTO costs (field_id, crop_id, category, amount, currency, vendor, notes, spent_at, photo_uri, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);', selectedFieldId, selectedCropId, category.trim() || null, parsedAmount, currency, vendor.trim() || null, notes.trim() || null, spentDate || null, primaryUri ?? null, now ); const costId = Number(result.lastInsertRowId); if (costId) { const mediaToInsert = uniqueMediaUris([ ...mediaUris, ...(normalizeMediaUri(activeUri) ? [normalizeMediaUri(activeUri) as string] : []), ]); for (const uri of mediaToInsert) { await db.runAsync( 'INSERT INTO cost_media (cost_id, uri, media_type, created_at) VALUES (?, ?, ?, ?);', costId, uri, isVideoUri(uri) ? 'video' : 'image', now ); } } setStatus(t('costs.saved')); router.back(); } catch (error) { setStatus(`Error: ${String(error)}`); } finally { setSaving(false); } } return ( {t('costs.new')} {status ? {status} : null} {t('costs.field')} * setFieldModalOpen(true)} variant="secondary" /> {errors.field ? {errors.field} : null} {t('costs.crop')} setCropModalOpen(true)} variant="secondary" /> {t('costs.category')} {CATEGORY_PRESETS.map((preset) => { const label = t(`costs.category.${preset}`); const isActive = label === category || preset === category; return ( setCategory(label)}> {label} ); })} {t('costs.amount')} ({currency}) * { setAmount(value); if (errors.amount) setErrors((prev) => ({ ...prev, amount: undefined })); }} placeholder={t('costs.amountPlaceholder')} placeholderTextColor={palette.placeholder} style={inputStyle} keyboardType="decimal-pad" /> {errors.amount ? {errors.amount} : null} {t('costs.vendor')} {t('costs.date')} setShowSpentPicker(true)} style={styles.dateInput}> {spentDate || t('costs.datePlaceholder')} {showSpentPicker ? ( { setShowSpentPicker(false); if (date) setSpentDate(toDateOnly(date)); }} /> ) : null} {t('costs.notes')} {t('costs.addMedia')} {normalizeMediaUri(activeUri) ? ( isVideoUri(normalizeMediaUri(activeUri) as string) ? ( setFieldModalOpen(false)}> {t('costs.selectField')} {fields.map((item) => ( { setSelectedFieldId(item.id); setErrors((prev) => ({ ...prev, field: undefined })); setFieldModalOpen(false); }} style={styles.modalItem}> {item.name || t('costs.untitled')} ))} setCropModalOpen(false)}> {t('costs.selectCrop')} {crops.map((item) => ( { setSelectedCropId(item.id); setCropModalOpen(false); }} style={styles.modalItem}> {item.crop_name || t('costs.untitled')} ))} setZoomUri(null)} /> ); } async function handlePickMedia(onAdd: (uris: string[]) => void) { const pickerTypes = (ImagePicker as { MediaType?: { Images?: string; Videos?: string } }).MediaType ?? (ImagePicker as { MediaTypeOptions?: { Images?: string; Videos?: string } }).MediaTypeOptions ?? undefined; const mediaTypes = pickerTypes?.Images && pickerTypes?.Videos ? [pickerTypes.Images.toLowerCase(), pickerTypes.Videos.toLowerCase()] : ['images', 'videos']; const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes, quality: 1, allowsMultipleSelection: true, }); if (result.canceled) return; const uris = result.assets.map((asset) => asset.uri).filter(Boolean); if (uris.length > 0) onAdd(uris); } async function handleTakeMedia(onAdd: (uri: string | null) => void) { const pickerTypes = (ImagePicker as { MediaType?: { Images?: string; Videos?: string } }).MediaType ?? (ImagePicker as { MediaTypeOptions?: { Images?: string; Videos?: string } }).MediaTypeOptions ?? undefined; const mediaTypes = pickerTypes?.Images && pickerTypes?.Videos ? [pickerTypes.Images.toLowerCase(), pickerTypes.Videos.toLowerCase()] : ['images', 'videos']; const result = await ImagePicker.launchCameraAsync({ mediaTypes, quality: 1, }); if (result.canceled) return; onAdd(result.assets[0]?.uri ?? null); } function isVideoUri(uri: string) { const cleaned = uri.split('?')[0]?.toLowerCase() ?? ''; return ['.mp4', '.mov', '.m4v', '.webm', '.mkv', '.avi'].some((ext) => cleaned.endsWith(ext)); } function normalizeMediaUri(uri?: string | null) { if (!uri) return null; if (uri.startsWith('file://') || uri.startsWith('http://') || uri.startsWith('https://')) return uri; return `file://${uri}`; } function uniqueMediaUris(uris: string[]) { return Array.from(new Set(uris.filter(Boolean))); } function toDateOnly(date: Date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } const styles = StyleSheet.create({ container: { flex: 1 }, keyboardAvoid: { flex: 1 }, content: { padding: 16, paddingBottom: 40 }, input: { borderWidth: 1, borderRadius: 12, paddingHorizontal: 12, paddingVertical: 10, marginTop: 6, marginBottom: 12, fontSize: 16, }, multiline: { minHeight: 90, textAlignVertical: 'top' }, errorText: { color: '#C0392B', marginBottom: 8 }, requiredMark: { color: '#C0392B' }, dateInput: { borderWidth: 1, borderRadius: 12, paddingHorizontal: 12, paddingVertical: 12, marginTop: 6, marginBottom: 12, }, dateValue: { fontSize: 16 }, chipRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 8 }, chip: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 999, borderWidth: 1, borderColor: '#C9D2C4', backgroundColor: '#F8F6F0', }, chipActive: { backgroundColor: '#DDE8DA', borderColor: '#88A68F' }, chipText: { fontSize: 13 }, mediaPreview: { width: '100%', height: 200, borderRadius: 16, backgroundColor: '#F1F1F1', marginBottom: 12, }, mediaPlaceholder: { width: '100%', height: 200, borderRadius: 16, borderWidth: 1, justifyContent: 'center', alignItems: 'center', marginBottom: 12, }, mediaPlaceholderText: { opacity: 0.7 }, mediaRow: { marginBottom: 12 }, mediaThumbWrap: { marginRight: 8 }, mediaThumb: { width: 72, height: 72, borderRadius: 12 }, mediaRemove: { position: 'absolute', top: -6, right: -6, width: 22, height: 22, borderRadius: 11, backgroundColor: '#2C2C2C', justifyContent: 'center', alignItems: 'center', }, mediaRemoveText: { color: '#fff', fontSize: 14, lineHeight: 16 }, mediaButtons: { flexDirection: 'row', gap: 12, flexWrap: 'wrap', marginBottom: 16 }, actions: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8 }, modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)', justifyContent: 'center', padding: 20, }, modalCard: { borderRadius: 16, borderWidth: 1, padding: 16, maxHeight: '70%', }, modalItem: { paddingVertical: 10 }, });