import { useEffect, useMemo, useState } from 'react'; import { Alert, FlatList, Image, KeyboardAvoidingView, Modal, Pressable, StyleSheet, TextInput, View, Platform, } from 'react-native'; import DateTimePicker from '@react-native-community/datetimepicker'; import { ThemedText } from '@/components/themed-text'; import { ThemedView } from '@/components/themed-view'; import { ThemedButton } from '@/components/themed-button'; import { IconButton } from '@/components/icon-button'; import { IconSymbol } from '@/components/ui/icon-symbol'; import { Colors, Fonts } from '@/constants/theme'; import { useTranslation } from '@/localization/i18n'; import { dbPromise, initCoreTables } from '@/services/db'; import { useColorScheme } from '@/hooks/use-color-scheme'; type FieldRow = { id: number; name: string | null; }; type CropRow = { id: number; field_id: number | null; crop_name: string | null; }; type HarvestRow = { id: number; crop_id: number | null; field_id: number | null; harvested_at: string | null; quantity: number | null; unit: string | null; crop_name: string | null; field_name: string | null; }; type SaleRow = { id: number; field_id: number | null; crop_id: number | null; harvest_id: number | null; sold_at: string | null; quantity: number | null; unit: string | null; price: number | null; buyer: string | null; notes: string | null; field_name: string | null; crop_name: string | null; }; export default function SalesScreen() { const { t } = useTranslation(); const theme = useColorScheme() ?? 'light'; const palette = Colors[theme]; const unitPresets = [ { key: 'kg', value: 'kg' }, { key: 'g', value: 'g' }, { key: 'ton', value: 'ton' }, { key: 'pcs', value: 'pcs' }, ]; const [sales, setSales] = useState([]); const [currency, setCurrency] = useState('THB'); const [fields, setFields] = useState([]); const [crops, setCrops] = useState([]); const [harvests, setHarvests] = useState([]); const [status, setStatus] = useState(t('sales.loading')); const [newModalOpen, setNewModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false); const [editingId, setEditingId] = useState(null); const [fieldModalOpen, setFieldModalOpen] = useState(false); const [cropModalOpen, setCropModalOpen] = useState(false); const [harvestModalOpen, setHarvestModalOpen] = useState(false); const [reopenSheetAfterSelect, setReopenSheetAfterSelect] = useState(false); const [fieldModalTarget, setFieldModalTarget] = useState<'new' | 'edit'>('new'); const [cropModalTarget, setCropModalTarget] = useState<'new' | 'edit'>('new'); const [harvestModalTarget, setHarvestModalTarget] = useState<'new' | 'edit'>('new'); const [selectedFieldId, setSelectedFieldId] = useState(null); const [selectedCropId, setSelectedCropId] = useState(null); const [selectedHarvestId, setSelectedHarvestId] = useState(null); const [soldDate, setSoldDate] = useState(''); const [showSoldPicker, setShowSoldPicker] = useState(false); const [quantity, setQuantity] = useState(''); const [unit, setUnit] = useState('kg'); const [price, setPrice] = useState(''); const [buyer, setBuyer] = useState(''); const [notes, setNotes] = useState(''); const [errors, setErrors] = useState<{ field?: string; crop?: string; quantity?: string }>({}); const [editFieldId, setEditFieldId] = useState(null); const [editCropId, setEditCropId] = useState(null); const [editHarvestId, setEditHarvestId] = useState(null); const [editSoldDate, setEditSoldDate] = useState(''); const [showEditSoldPicker, setShowEditSoldPicker] = useState(false); const [editQuantity, setEditQuantity] = useState(''); const [editUnit, setEditUnit] = useState('kg'); const [editPrice, setEditPrice] = useState(''); const [editBuyer, setEditBuyer] = useState(''); const [editNotes, setEditNotes] = useState(''); const [editErrors, setEditErrors] = useState<{ field?: string; crop?: string; quantity?: string }>( {} ); 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 selectedEditField = useMemo( () => fields.find((item) => item.id === editFieldId), [fields, editFieldId] ); const selectedEditCrop = useMemo( () => crops.find((item) => item.id === editCropId), [crops, editCropId] ); const selectedEditHarvest = useMemo( () => harvests.find((item) => item.id === editHarvestId), [harvests, editHarvestId] ); useEffect(() => { let isActive = true; async function loadData() { try { await initCoreTables(); const db = await dbPromise; const profileRow = await db.getFirstAsync<{ currency: string | null }>( 'SELECT currency FROM user_profile WHERE id = 1;' ); const fieldRows = await db.getAllAsync( 'SELECT id, name FROM fields ORDER BY name ASC;' ); const cropRows = await db.getAllAsync( 'SELECT id, field_id, crop_name FROM crops ORDER BY id DESC;' ); const harvestRows = await db.getAllAsync( `SELECT h.id, h.crop_id, h.field_id, h.harvested_at, h.quantity, h.unit, c.crop_name as crop_name, f.name as field_name FROM harvests h LEFT JOIN crops c ON c.id = h.crop_id LEFT JOIN fields f ON f.id = h.field_id ORDER BY h.harvested_at DESC;` ); const saleRows = await db.getAllAsync( `SELECT s.id, s.field_id, s.crop_id, s.harvest_id, s.sold_at, s.quantity, s.unit, s.price, s.buyer, s.notes, f.name as field_name, c.crop_name as crop_name FROM sales s LEFT JOIN fields f ON f.id = s.field_id LEFT JOIN crops c ON c.id = s.crop_id ORDER BY s.sold_at DESC;` ); if (!isActive) return; setCurrency(profileRow?.currency ?? 'THB'); setFields(fieldRows); setCrops(cropRows); setHarvests(harvestRows); setSales(saleRows); setStatus(saleRows.length === 0 ? t('sales.empty') : ''); } catch (error) { if (isActive) setStatus(`Error: ${String(error)}`); } } loadData(); return () => { isActive = false; }; }, [t]); async function handleSave() { const parsedQty = quantity.trim() ? Number(quantity) : null; const parsedPrice = price.trim() ? Number(price) : null; const nextErrors: { field?: string; crop?: string; quantity?: string } = {}; if (!selectedFieldId) nextErrors.field = t('sales.fieldRequired'); if (!selectedCropId) nextErrors.crop = t('sales.cropRequired'); if (!parsedQty || !Number.isFinite(parsedQty)) nextErrors.quantity = t('sales.quantityInvalid'); setErrors(nextErrors); if (Object.keys(nextErrors).length > 0) { setStatus(nextErrors.field ?? nextErrors.crop ?? nextErrors.quantity ?? t('sales.fieldRequired')); return false; } try { const db = await dbPromise; const now = new Date().toISOString(); await db.runAsync( 'INSERT INTO sales (harvest_id, field_id, crop_id, sold_at, quantity, unit, price, buyer, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);', selectedHarvestId, selectedFieldId, selectedCropId, soldDate.trim() || now, parsedQty, unit.trim() || null, parsedPrice, buyer.trim() || null, notes.trim() || null, now ); setSelectedFieldId(null); setSelectedCropId(null); setSelectedHarvestId(null); setSoldDate(''); setQuantity(''); setUnit('kg'); setPrice(''); setBuyer(''); setNotes(''); setErrors({}); setStatus(t('sales.saved')); const saleRows = await db.getAllAsync( `SELECT s.id, s.field_id, s.crop_id, s.harvest_id, s.sold_at, s.quantity, s.unit, s.price, s.buyer, s.notes, f.name as field_name, c.crop_name as crop_name FROM sales s LEFT JOIN fields f ON f.id = s.field_id LEFT JOIN crops c ON c.id = s.crop_id ORDER BY s.sold_at DESC;` ); setSales(saleRows); return true; } catch (error) { setStatus(`Error: ${String(error)}`); return false; } } function startEdit(item: SaleRow) { setEditingId(item.id); setEditFieldId(item.field_id ?? null); setEditCropId(item.crop_id ?? null); setEditHarvestId(item.harvest_id ?? null); setEditSoldDate(item.sold_at ?? ''); setEditQuantity(item.quantity !== null ? String(item.quantity) : ''); setEditUnit(item.unit ?? 'kg'); setEditPrice(item.price !== null ? String(item.price) : ''); setEditBuyer(item.buyer ?? ''); setEditNotes(item.notes ?? ''); setEditErrors({}); setEditModalOpen(true); } function cancelEdit() { setEditingId(null); setEditFieldId(null); setEditCropId(null); setEditHarvestId(null); setEditSoldDate(''); setEditQuantity(''); setEditUnit('kg'); setEditPrice(''); setEditBuyer(''); setEditNotes(''); setEditErrors({}); setEditModalOpen(false); } async function handleUpdate() { if (!editingId) return; const parsedQty = editQuantity.trim() ? Number(editQuantity) : null; const parsedPrice = editPrice.trim() ? Number(editPrice) : null; const nextErrors: { field?: string; crop?: string; quantity?: string } = {}; if (!editFieldId) nextErrors.field = t('sales.fieldRequired'); if (!editCropId) nextErrors.crop = t('sales.cropRequired'); if (!parsedQty || !Number.isFinite(parsedQty)) nextErrors.quantity = t('sales.quantityInvalid'); setEditErrors(nextErrors); if (Object.keys(nextErrors).length > 0) { setStatus(nextErrors.field ?? nextErrors.crop ?? nextErrors.quantity ?? t('sales.fieldRequired')); return; } try { const db = await dbPromise; const now = new Date().toISOString(); await db.runAsync( 'UPDATE sales SET harvest_id = ?, field_id = ?, crop_id = ?, sold_at = ?, quantity = ?, unit = ?, price = ?, buyer = ?, notes = ? WHERE id = ?;', editHarvestId, editFieldId, editCropId, editSoldDate.trim() || now, parsedQty, editUnit.trim() || null, parsedPrice, editBuyer.trim() || null, editNotes.trim() || null, editingId ); setStatus(t('sales.saved')); setEditModalOpen(false); setEditingId(null); const saleRows = await db.getAllAsync( `SELECT s.id, s.field_id, s.crop_id, s.harvest_id, s.sold_at, s.quantity, s.unit, s.price, s.buyer, s.notes, f.name as field_name, c.crop_name as crop_name FROM sales s LEFT JOIN fields f ON f.id = s.field_id LEFT JOIN crops c ON c.id = s.crop_id ORDER BY s.sold_at DESC;` ); setSales(saleRows); } catch (error) { setStatus(`Error: ${String(error)}`); } } function confirmDelete(id: number) { Alert.alert(t('sales.deleteTitle'), t('sales.deleteMessage'), [ { text: t('sales.cancel'), style: 'cancel' }, { text: t('sales.delete'), style: 'destructive', onPress: () => handleDelete(id) }, ]); } async function handleDelete(id: number) { try { const db = await dbPromise; await db.runAsync('DELETE FROM sales WHERE id = ?;', id); const saleRows = await db.getAllAsync( `SELECT s.id, s.field_id, s.crop_id, s.harvest_id, s.sold_at, s.quantity, s.unit, s.price, s.buyer, s.notes, f.name as field_name, c.crop_name as crop_name FROM sales s LEFT JOIN fields f ON f.id = s.field_id LEFT JOIN crops c ON c.id = s.crop_id ORDER BY s.sold_at DESC;` ); setSales(saleRows); setStatus(saleRows.length === 0 ? t('sales.empty') : ''); } catch (error) { setStatus(`Error: ${String(error)}`); } } const inputStyle = [ styles.input, { borderColor: palette.border, backgroundColor: palette.input, color: palette.text }, ]; return ( <> String(item.id)} renderItem={({ item }) => ( startEdit(item)}> {item.crop_name || t('sales.untitled')} confirmDelete(item.id)} accessibilityLabel={t('sales.delete')} variant="danger" /> {item.field_name || t('sales.noField')} {item.sold_at ? ( {formatDate(item.sold_at)} ) : null} {item.quantity !== null ? ( {item.quantity} {item.unit || t('sales.unitPlaceholder')} ) : null} {item.price !== null ? ( {t('sales.priceLabel')} {item.price} {currency} ) : null} {item.buyer ? {item.buyer} : null} {item.notes ? {item.notes} : null} )} ItemSeparatorComponent={() => } ListHeaderComponent={ {t('sales.title')} {status ? ( {status} ) : null} setNewModalOpen(true)}> {t('sales.new')} } ListFooterComponent={} /> { setFieldModalOpen(false); setReopenSheetAfterSelect(false); }}> {t('sales.selectField')} String(item.id)} renderItem={({ item }) => ( { if (fieldModalTarget === 'edit') { setEditFieldId(item.id); setEditErrors((prev) => ({ ...prev, field: undefined })); if (reopenSheetAfterSelect) setEditModalOpen(true); } else { setSelectedFieldId(item.id); setErrors((prev) => ({ ...prev, field: undefined })); if (reopenSheetAfterSelect) setNewModalOpen(true); } setFieldModalOpen(false); setReopenSheetAfterSelect(false); }} style={styles.modalItem}> {item.name || t('sales.untitled')} )} /> { setCropModalOpen(false); setReopenSheetAfterSelect(false); }}> {t('sales.selectCrop')} { const targetField = cropModalTarget === 'edit' ? editFieldId : selectedFieldId; return !targetField || item.field_id === targetField; })} keyExtractor={(item) => String(item.id)} renderItem={({ item }) => ( { if (cropModalTarget === 'edit') { setEditCropId(item.id); setEditErrors((prev) => ({ ...prev, crop: undefined })); if (reopenSheetAfterSelect) setEditModalOpen(true); } else { setSelectedCropId(item.id); setErrors((prev) => ({ ...prev, crop: undefined })); if (reopenSheetAfterSelect) setNewModalOpen(true); } setCropModalOpen(false); setReopenSheetAfterSelect(false); }} style={styles.modalItem}> {item.crop_name || t('sales.untitled')} )} ListEmptyComponent={{t('sales.noCrop')}} /> { setHarvestModalOpen(false); setReopenSheetAfterSelect(false); }}> {t('sales.selectHarvest')} { const targetCrop = harvestModalTarget === 'edit' ? editCropId : selectedCropId; return !targetCrop || item.crop_id === targetCrop; })} keyExtractor={(item) => String(item.id)} renderItem={({ item }) => ( { if (harvestModalTarget === 'edit') { setEditHarvestId(item.id); if (reopenSheetAfterSelect) setEditModalOpen(true); } else { setSelectedHarvestId(item.id); if (reopenSheetAfterSelect) setNewModalOpen(true); } setHarvestModalOpen(false); setReopenSheetAfterSelect(false); }} style={styles.modalItem}> {item.crop_name || t('sales.untitled')} ยท{' '} {item.harvested_at ? formatDate(item.harvested_at) : '-'} )} ListEmptyComponent={{t('sales.noHarvest')}} /> setNewModalOpen(false)} /> item.key} contentContainerStyle={styles.sheetListContent} renderItem={() => ( {t('sales.new')} {t('sales.field')} * { setReopenSheetAfterSelect(true); setFieldModalTarget('new'); setNewModalOpen(false); setFieldModalOpen(true); }} variant="secondary" /> {errors.field ? ( {errors.field} ) : null} {t('sales.crop')} * { setReopenSheetAfterSelect(true); setCropModalTarget('new'); setNewModalOpen(false); setCropModalOpen(true); }} variant="secondary" /> {errors.crop ? ( {errors.crop} ) : null} {t('sales.harvest')} { setReopenSheetAfterSelect(true); setHarvestModalTarget('new'); setNewModalOpen(false); setHarvestModalOpen(true); }} variant="secondary" /> {t('sales.date')} setShowSoldPicker(true)} style={styles.dateInput}> {soldDate ? formatDateLabel(soldDate) : t('sales.datePlaceholder')} {showSoldPicker ? ( <> {Platform.OS === 'ios' ? ( { setSoldDate(toDateOnly(new Date())); setShowSoldPicker(false); }} variant="secondary" /> setShowSoldPicker(false)} variant="secondary" /> ) : null} { if (date) setSoldDate(toDateOnly(date)); if (Platform.OS !== 'ios') setShowSoldPicker(false); }} /> ) : null} {t('sales.quantity')} * { setQuantity(value); if (errors.quantity) { setErrors((prev) => ({ ...prev, quantity: undefined })); } }} placeholder={t('sales.quantityPlaceholder')} style={inputStyle} keyboardType="decimal-pad" /> {errors.quantity ? ( {errors.quantity} ) : null} {t('sales.unit')} {unitPresets.map((preset) => { const label = t(`units.${preset.key}`); const normalized = unit.trim().toLowerCase(); const isActive = label.toLowerCase() === normalized || preset.value.toLowerCase() === normalized; return ( setUnit(label)} style={[styles.unitChip, isActive && styles.unitChipActive]}> {label} ); })} {t('sales.price')} ({currency}) {t('sales.buyer')} {t('sales.notes')} setNewModalOpen(false)} variant="secondary" /> { const ok = await handleSave(); if (ok) setNewModalOpen(false); }} /> )} /> item.key} contentContainerStyle={styles.sheetListContent} renderItem={() => ( {t('sales.edit')} {t('sales.field')} * { setReopenSheetAfterSelect(true); setFieldModalTarget('edit'); setEditModalOpen(false); setFieldModalOpen(true); }} variant="secondary" /> {editErrors.field ? ( {editErrors.field} ) : null} {t('sales.crop')} * { setReopenSheetAfterSelect(true); setCropModalTarget('edit'); setEditModalOpen(false); setCropModalOpen(true); }} variant="secondary" /> {editErrors.crop ? ( {editErrors.crop} ) : null} {t('sales.harvest')} { setReopenSheetAfterSelect(true); setHarvestModalTarget('edit'); setEditModalOpen(false); setHarvestModalOpen(true); }} variant="secondary" /> {t('sales.date')} setShowEditSoldPicker(true)} style={styles.dateInput}> {editSoldDate ? formatDateLabel(editSoldDate) : t('sales.datePlaceholder')} {showEditSoldPicker ? ( <> {Platform.OS === 'ios' ? ( { setEditSoldDate(toDateOnly(new Date())); setShowEditSoldPicker(false); }} variant="secondary" /> setShowEditSoldPicker(false)} variant="secondary" /> ) : null} { if (date) setEditSoldDate(toDateOnly(date)); if (Platform.OS !== 'ios') setShowEditSoldPicker(false); }} /> ) : null} {t('sales.quantity')} * { setEditQuantity(value); if (editErrors.quantity) { setEditErrors((prev) => ({ ...prev, quantity: undefined })); } }} placeholder={t('sales.quantityPlaceholder')} style={inputStyle} keyboardType="decimal-pad" /> {editErrors.quantity ? ( {editErrors.quantity} ) : null} {t('sales.unit')} {unitPresets.map((preset) => { const label = t(`units.${preset.key}`); const normalized = editUnit.trim().toLowerCase(); const isActive = label.toLowerCase() === normalized || preset.value.toLowerCase() === normalized; return ( setEditUnit(label)} style={[styles.unitChip, isActive && styles.unitChipActive]}> {label} ); })} {t('sales.price')} ({currency}) {t('sales.buyer')} {t('sales.notes')} )} /> ); } function formatDate(value: string) { try { return new Date(value).toLocaleString(); } catch { return value; } } function formatHarvestLabel( fieldName: string | null, cropName: string | null, quantity: number | null, unit: string | null, harvestedAt: string | null, fallback: string, unitFallback: string ) { const name = fieldName || cropName || fallback; const crop = cropName ? ` ${cropName}` : ''; const qty = quantity !== null ? `, ${quantity} ${unit || unitFallback}` : ''; const date = harvestedAt ? `, ${formatDateLabel(harvestedAt)}` : ''; return `${name}${crop}${qty}${date}`.trim(); } function formatDateLabel(value: string) { try { return new Date(value).toISOString().slice(0, 10); } catch { return value; } } function toDateOnly(date: Date) { return date.toISOString().slice(0, 10); } const styles = StyleSheet.create({ hero: { backgroundColor: '#E8E6DA', aspectRatio: 16 / 9, width: '100%', }, heroImage: { width: '100%', height: '100%', }, titleContainer: { gap: 8, paddingHorizontal: 16, paddingVertical: 12, }, section: { gap: 8, marginBottom: 16, paddingHorizontal: 16, }, newButton: { flexDirection: 'row', alignItems: 'center', gap: 8, borderRadius: 10, borderWidth: 1, borderColor: '#B9B9B9', paddingHorizontal: 12, paddingVertical: 10, alignSelf: 'flex-start', }, newButtonText: { fontSize: 15, fontWeight: '600', }, card: { borderRadius: 12, borderWidth: 1, borderColor: '#C6C6C6', padding: 12, marginHorizontal: 16, gap: 6, backgroundColor: '#FFFFFF', }, cardHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8, }, meta: { opacity: 0.7, }, separator: { height: 12, }, footer: { height: 24, }, modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.3)', justifyContent: 'center', padding: 16, }, modalCard: { borderRadius: 12, borderWidth: 1, borderColor: '#C6C6C6', padding: 16, backgroundColor: '#FFFFFF', gap: 8, maxHeight: '70%', }, modalItem: { paddingVertical: 8, }, input: { borderRadius: 10, borderWidth: 1, borderColor: '#B9B9B9', paddingHorizontal: 12, paddingVertical: 10, fontSize: 15, }, dateInput: { borderRadius: 10, borderWidth: 1, borderColor: '#B9B9B9', paddingHorizontal: 12, paddingVertical: 10, }, dateValue: { fontSize: 15, opacity: 0.9, }, pickerRow: { flexDirection: 'row', gap: 8, }, unitRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, }, unitChip: { borderRadius: 999, borderWidth: 1, borderColor: '#C6C6C6', paddingHorizontal: 10, paddingVertical: 4, }, unitChipActive: { borderColor: '#2F7D4F', backgroundColor: '#E7F3EA', }, unitText: { fontSize: 12, }, unitTextActive: { fontSize: 12, color: '#2F7D4F', fontWeight: '600', }, modalActions: { flexDirection: 'row', justifyContent: 'space-between', gap: 12, }, sheetOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.3)', justifyContent: 'flex-end', }, sheetBackdrop: { ...StyleSheet.absoluteFillObject, }, sheet: { borderTopLeftRadius: 16, borderTopRightRadius: 16, borderWidth: 1, borderColor: '#C6C6C6', padding: 16, backgroundColor: '#FFFFFF', gap: 10, maxHeight: '85%', }, sheetContent: { gap: 10, }, sheetListContent: { paddingBottom: 80, }, sheetFooter: { height: 24, }, keyboardAvoid: { width: '100%', flex: 1, justifyContent: 'flex-end', }, requiredMark: { color: '#C0392B', fontWeight: '700', }, errorText: { color: '#C0392B', fontSize: 12, }, });