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'; type FieldRow = { id: number; name: string | null; }; type CropRow = { id: number; crop_name: string | null; }; type HarvestRow = { id: number; field_id: number | null; crop_id: number | null; harvested_at: string | null; quantity: number | null; unit: string | null; field_name: string | null; crop_name: string | null; }; export default function NewSaleScreen() { 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 [harvests, setHarvests] = useState([]); const [fieldModalOpen, setFieldModalOpen] = useState(false); const [cropModalOpen, setCropModalOpen] = useState(false); const [harvestModalOpen, setHarvestModalOpen] = useState(false); const [selectedFieldId, setSelectedFieldId] = useState(null); const [selectedCropId, setSelectedCropId] = useState(null); const [selectedHarvestId, setSelectedHarvestId] = useState(null); const [saleDate, setSaleDate] = useState(''); const [showSalePicker, setShowSalePicker] = useState(false); const [quantity, setQuantity] = useState(''); const [unit, setUnit] = useState(''); const [price, setPrice] = useState(''); const [buyer, setBuyer] = useState(''); const [notes, setNotes] = useState(''); const [mediaUris, setMediaUris] = useState([]); const [activeUri, setActiveUri] = useState(null); const [errors, setErrors] = useState<{ field?: string; crop?: string; quantity?: string }>({}); const [zoomUri, setZoomUri] = useState(null); const [saving, setSaving] = useState(false); const [currency, setCurrency] = useState('THB'); 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 harvestRows = await db.getAllAsync( `SELECT h.id, h.field_id, h.crop_id, h.harvested_at, h.quantity, h.unit, f.name as field_name, c.crop_name as crop_name FROM harvests h LEFT JOIN fields f ON f.id = h.field_id LEFT JOIN crops c ON c.id = h.crop_id ORDER BY h.harvested_at DESC;` ); const profileRow = await db.getFirstAsync<{ currency: string | null }>( 'SELECT currency FROM user_profile WHERE id = 1;' ); if (!isActive) return; setFields(fieldRows); setCrops(cropRows); setHarvests(harvestRows); 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 selectedHarvest = useMemo( () => harvests.find((item) => item.id === selectedHarvestId), [harvests, selectedHarvestId] ); const inputStyle = [ styles.input, { borderColor: palette.border, backgroundColor: palette.input, color: palette.text, }, ]; const unitPresets = ['kg', 'g', 'ton', 'pcs']; async function handleSave() { const parsedQuantity = quantity.trim() ? Number(quantity) : null; const nextErrors: { field?: string; crop?: string; quantity?: string } = {}; if (!selectedFieldId) nextErrors.field = t('sales.fieldRequired'); if (!selectedCropId) nextErrors.crop = t('sales.cropRequired'); if (quantity.trim() && !Number.isFinite(parsedQuantity)) { nextErrors.quantity = t('sales.quantityInvalid'); } setErrors(nextErrors); if (Object.keys(nextErrors).length > 0) return; try { setSaving(true); const db = await dbPromise; const now = new Date().toISOString(); const result = await db.runAsync( 'INSERT INTO sales (field_id, crop_id, harvest_id, sold_at, quantity, unit, price, buyer, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);', selectedFieldId, selectedCropId, selectedHarvestId, saleDate || null, parsedQuantity, unit.trim() || null, price.trim() ? Number(price) : null, buyer.trim() || null, notes.trim() || null, now ); const saleId = Number(result.lastInsertRowId); if (saleId) { const mediaToInsert = uniqueMediaUris(mediaUris); for (const uri of mediaToInsert) { await db.runAsync( 'INSERT INTO sale_media (sale_id, uri, media_type, created_at) VALUES (?, ?, ?, ?);', saleId, uri, isVideoUri(uri) ? 'video' : 'image', now ); } } setStatus(t('sales.saved')); router.back(); } catch (error) { setStatus(`Error: ${String(error)}`); } finally { setSaving(false); } } const harvestLabel = selectedHarvest ? `${selectedHarvest.field_name || ''} ${selectedHarvest.crop_name || ''}, ${selectedHarvest.quantity ?? ''} ${selectedHarvest.unit ?? ''}, ${selectedHarvest.harvested_at ?? ''}`.trim() : t('sales.selectHarvest'); return ( {t('sales.new')} {status ? {status} : null} {t('sales.field')} setFieldModalOpen(true)} variant="secondary" /> {errors.field ? {errors.field} : null} {t('sales.crop')} setCropModalOpen(true)} variant="secondary" /> {errors.crop ? {errors.crop} : null} {t('sales.harvest')} setHarvestModalOpen(true)} variant="secondary" /> {t('sales.date')} setShowSalePicker(true)} style={styles.dateInput}> {saleDate || t('sales.datePlaceholder')} {showSalePicker ? ( { setShowSalePicker(false); if (date) setSaleDate(toDateOnly(date)); }} /> ) : null} {t('sales.quantity')} { setQuantity(value); if (errors.quantity) setErrors((prev) => ({ ...prev, quantity: undefined })); }} placeholder={t('sales.quantityPlaceholder')} placeholderTextColor={palette.placeholder} style={inputStyle} keyboardType="decimal-pad" /> {errors.quantity ? {errors.quantity} : null} {t('sales.unit')} {unitPresets.map((preset) => { const label = t(`units.${preset}`); const isActive = unit === label || unit === preset; return ( setUnit(label)}> {label} ); })} {t('sales.price')} ({currency}) {t('sales.buyer')} {t('sales.notes')} {t('sales.addMedia')} {normalizeMediaUri(activeUri) ? ( isVideoUri(normalizeMediaUri(activeUri) as string) ? ( setFieldModalOpen(false)}> {t('sales.selectField')} {fields.map((item) => ( { setSelectedFieldId(item.id); setFieldModalOpen(false); }}> {item.name || t('sales.noField')} ))} setCropModalOpen(false)}> {t('sales.selectCrop')} {crops.map((item) => ( { setSelectedCropId(item.id); setCropModalOpen(false); }}> {item.crop_name || t('sales.noCrop')} ))} setHarvestModalOpen(false)}> {t('sales.selectHarvest')} {harvests.map((item) => ( { setSelectedHarvestId(item.id); setSelectedFieldId(item.field_id ?? null); setSelectedCropId(item.crop_id ?? null); setHarvestModalOpen(false); }}> {item.field_name || ''} {item.crop_name || ''}, {item.quantity ?? ''} {item.unit ?? ''}, {item.harvested_at ?? ''} ))} setZoomUri(null)} /> ); } async function handlePickMedia(onAdd: (uris: string[]) => void) { const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: getMediaTypes(), quality: 1, allowsMultipleSelection: true, selectionLimit: 0, }); if (result.canceled) return; const uris = (result.assets ?? []).map((asset) => asset.uri).filter(Boolean) as string[]; if (uris.length === 0) return; onAdd(uris); } async function handleTakeMedia(onAdd: (uri: string | null) => void) { const permission = await ImagePicker.requestCameraPermissionsAsync(); if (!permission.granted) { return; } const result = await ImagePicker.launchCameraAsync({ mediaTypes: getMediaTypes(), quality: 1, }); if (result.canceled) return; const asset = result.assets[0]; onAdd(asset.uri); } function getMediaTypes() { const mediaType = (ImagePicker as { MediaType?: { Image?: unknown; Images?: unknown; Video?: unknown; Videos?: unknown }; }).MediaType; const imageType = mediaType?.Image ?? mediaType?.Images; const videoType = mediaType?.Video ?? mediaType?.Videos; if (imageType && videoType) { return [imageType, videoType]; } return imageType ?? videoType ?? ['images', 'videos']; } function isVideoUri(uri: string) { return /\.(mp4|mov|m4v|webm|avi|mkv)(\?.*)?$/i.test(uri); } function normalizeMediaUri(uri?: string | null) { if (typeof uri !== 'string') return null; const trimmed = uri.trim(); return trimmed ? trimmed : null; } function uniqueMediaUris(uris: string[]) { const seen = new Set(); const result: string[] = []; for (const uri of uris) { if (!uri || seen.has(uri)) continue; seen.add(uri); result.push(uri); } return result; } function toDateOnly(date: Date) { return date.toISOString().slice(0, 10); } const styles = StyleSheet.create({ container: { flex: 1, }, keyboardAvoid: { flex: 1, }, content: { padding: 16, gap: 10, paddingBottom: 40, }, input: { borderRadius: 10, borderWidth: 1, paddingHorizontal: 12, paddingVertical: 10, fontSize: 15, }, errorText: { color: '#C0392B', fontSize: 12, }, dateInput: { borderRadius: 10, borderWidth: 1, borderColor: '#B9B9B9', paddingHorizontal: 12, paddingVertical: 10, }, dateValue: { opacity: 0.7, }, mediaPreview: { width: '100%', height: 220, borderRadius: 12, backgroundColor: '#1C1C1C', }, photoRow: { flexDirection: 'row', gap: 8, }, actions: { marginTop: 12, gap: 10, }, chipRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 8, }, chip: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 999, borderWidth: 1, borderColor: '#D9D1C2', backgroundColor: '#F8F6F0', }, chipActive: { backgroundColor: '#DDE8DA', borderColor: '#88A68F', }, chipText: { fontSize: 13, }, photoPlaceholder: { opacity: 0.6, }, mediaStrip: { marginTop: 6, }, mediaChip: { width: 72, height: 72, borderRadius: 10, marginRight: 8, overflow: 'hidden', backgroundColor: '#E6E1D4', alignItems: 'center', justifyContent: 'center', }, mediaThumb: { width: '100%', height: '100%', }, videoThumb: { width: '100%', height: '100%', backgroundColor: '#1C1C1C', alignItems: 'center', justifyContent: 'center', }, videoThumbText: { color: '#FFFFFF', fontSize: 18, fontWeight: '700', }, mediaRemove: { position: 'absolute', top: 4, right: 4, width: 18, height: 18, borderRadius: 9, backgroundColor: 'rgba(0,0,0,0.6)', alignItems: 'center', justifyContent: 'center', }, mediaRemoveText: { color: '#FFFFFF', fontSize: 12, lineHeight: 14, fontWeight: '700', }, modalBackdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)', justifyContent: 'center', padding: 24, }, modalCard: { borderRadius: 14, backgroundColor: '#FFFFFF', padding: 16, gap: 10, maxHeight: '80%', }, modalList: { maxHeight: 300, }, modalItem: { paddingVertical: 10, }, });