| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186 |
- import { useCallback, 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';
- import { useLocalSearchParams, useRouter } from 'expo-router';
- import { useFocusEffect, useNavigation } from '@react-navigation/native';
- 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 router = useRouter();
- const navigation = useNavigation();
- const params = useLocalSearchParams<{ from?: string | string[] }>();
- const theme = useColorScheme() ?? 'light';
- const palette = Colors[theme];
- const fromParam = Array.isArray(params.from) ? params.from[0] : params.from;
- const unitPresets = [
- { key: 'kg', value: 'kg' },
- { key: 'g', value: 'g' },
- { key: 'ton', value: 'ton' },
- { key: 'pcs', value: 'pcs' },
- ];
- const [sales, setSales] = useState<SaleRow[]>([]);
- const [currency, setCurrency] = useState('THB');
- const [fields, setFields] = useState<FieldRow[]>([]);
- const [crops, setCrops] = useState<CropRow[]>([]);
- const [harvests, setHarvests] = useState<HarvestRow[]>([]);
- const [status, setStatus] = useState(t('sales.loading'));
- const [newModalOpen, setNewModalOpen] = useState(false);
- const [editModalOpen, setEditModalOpen] = useState(false);
- const [editingId, setEditingId] = useState<number | null>(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<number | null>(null);
- const [selectedCropId, setSelectedCropId] = useState<number | null>(null);
- const [selectedHarvestId, setSelectedHarvestId] = useState<number | null>(null);
- useEffect(() => {
- navigation.setOptions({
- headerLeft: () => (
- <Pressable
- onPress={() => {
- if (fromParam === 'logbook') {
- router.replace('/logbook');
- return;
- }
- if (fromParam === 'home') {
- router.replace('/');
- return;
- }
- router.back();
- }}
- hitSlop={10}
- style={{ paddingHorizontal: 8 }}>
- <IconSymbol name="chevron.left" size={20} color={palette.text} />
- </Pressable>
- ),
- });
- }, [fromParam, navigation, palette.text, router]);
- 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<number | null>(null);
- const [editCropId, setEditCropId] = useState<number | null>(null);
- const [editHarvestId, setEditHarvestId] = useState<number | null>(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<FieldRow>(
- 'SELECT id, name FROM fields ORDER BY name ASC;'
- );
- const cropRows = await db.getAllAsync<CropRow>(
- 'SELECT id, field_id, crop_name FROM crops ORDER BY id DESC;'
- );
- const harvestRows = await db.getAllAsync<HarvestRow>(
- `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<SaleRow>(
- `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 fetchSalesPage() {
- try {
- const db = await dbPromise;
- const profileRow = await db.getFirstAsync<{ currency: string | null }>(
- 'SELECT currency FROM user_profile WHERE id = 1;'
- );
- const saleRows = await db.getAllAsync<SaleRow>(
- `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;`
- );
- setCurrency(profileRow?.currency ?? 'THB');
- setSales(saleRows);
- setStatus(saleRows.length === 0 ? t('sales.empty') : '');
- } catch (error) {
- setStatus(`Error: ${String(error)}`);
- }
- }
- useFocusEffect(
- useCallback(() => {
- fetchSalesPage();
- }, [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<SaleRow>(
- `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) {
- router.push(`/sales/${item.id}`);
- }
- 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<SaleRow>(
- `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<SaleRow>(
- `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 (
- <>
- <FlatList
- data={sales}
- keyExtractor={(item) => String(item.id)}
- renderItem={({ item }) => (
- <Pressable onPress={() => startEdit(item)}>
- <ThemedView style={[styles.card, { backgroundColor: palette.card, borderColor: palette.border }]}>
- <View style={styles.cardHeader}>
- <ThemedText type="subtitle">{item.crop_name || t('sales.untitled')}</ThemedText>
- <IconButton
- name="trash"
- onPress={() => confirmDelete(item.id)}
- accessibilityLabel={t('sales.delete')}
- variant="danger"
- />
- </View>
- <ThemedText style={styles.meta}>
- {item.field_name || t('sales.noField')}
- </ThemedText>
- {item.sold_at ? (
- <ThemedText style={styles.meta}>{formatDate(item.sold_at)}</ThemedText>
- ) : null}
- {item.quantity !== null ? (
- <ThemedText>
- {item.quantity} {item.unit || t('sales.unitPlaceholder')}
- </ThemedText>
- ) : null}
- {item.price !== null ? (
- <ThemedText>
- {t('sales.priceLabel')} {item.price} {currency}
- </ThemedText>
- ) : null}
- {item.buyer ? <ThemedText>{item.buyer}</ThemedText> : null}
- {item.notes ? <ThemedText>{item.notes}</ThemedText> : null}
- </ThemedView>
- </Pressable>
- )}
- ItemSeparatorComponent={() => <View style={styles.separator} />}
- ListHeaderComponent={
- <View>
- <ThemedView style={styles.hero}>
- <Image source={require('@/assets/images/salerecords.jpg')} style={styles.heroImage} />
- </ThemedView>
- <ThemedView style={styles.titleContainer}>
- <ThemedText type="title" style={{ fontFamily: Fonts.rounded }}>
- {t('sales.title')}
- </ThemedText>
- </ThemedView>
- {status ? (
- <ThemedView style={styles.section}>
- <ThemedText>{status}</ThemedText>
- </ThemedView>
- ) : null}
- <ThemedView style={styles.section}>
- <Pressable style={styles.newButton} onPress={() => router.push('/sales/new')}>
- <IconSymbol size={18} name="plus.circle.fill" color="#2F7D4F" />
- <ThemedText style={styles.newButtonText}>{t('sales.new')}</ThemedText>
- </Pressable>
- </ThemedView>
- </View>
- }
- ListFooterComponent={<View style={styles.footer} />}
- />
- <Modal transparent visible={fieldModalOpen} animationType="fade">
- <Pressable
- style={styles.modalOverlay}
- onPress={() => {
- setFieldModalOpen(false);
- setReopenSheetAfterSelect(false);
- }}>
- <View style={[styles.modalCard, { backgroundColor: palette.card, borderColor: palette.border }]}>
- <ThemedText type="subtitle">{t('sales.selectField')}</ThemedText>
- <FlatList
- data={fields}
- keyExtractor={(item) => String(item.id)}
- renderItem={({ item }) => (
- <Pressable
- onPress={() => {
- 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}>
- <ThemedText>{item.name || t('sales.untitled')}</ThemedText>
- </Pressable>
- )}
- />
- </View>
- </Pressable>
- </Modal>
- <Modal transparent visible={cropModalOpen} animationType="fade">
- <Pressable
- style={styles.modalOverlay}
- onPress={() => {
- setCropModalOpen(false);
- setReopenSheetAfterSelect(false);
- }}>
- <View style={[styles.modalCard, { backgroundColor: palette.card, borderColor: palette.border }]}>
- <ThemedText type="subtitle">{t('sales.selectCrop')}</ThemedText>
- <FlatList
- data={crops.filter((item) => {
- const targetField = cropModalTarget === 'edit' ? editFieldId : selectedFieldId;
- return !targetField || item.field_id === targetField;
- })}
- keyExtractor={(item) => String(item.id)}
- renderItem={({ item }) => (
- <Pressable
- onPress={() => {
- 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}>
- <ThemedText>{item.crop_name || t('sales.untitled')}</ThemedText>
- </Pressable>
- )}
- ListEmptyComponent={<ThemedText style={styles.meta}>{t('sales.noCrop')}</ThemedText>}
- />
- </View>
- </Pressable>
- </Modal>
- <Modal transparent visible={harvestModalOpen} animationType="fade">
- <Pressable
- style={styles.modalOverlay}
- onPress={() => {
- setHarvestModalOpen(false);
- setReopenSheetAfterSelect(false);
- }}>
- <View style={[styles.modalCard, { backgroundColor: palette.card, borderColor: palette.border }]}>
- <ThemedText type="subtitle">{t('sales.selectHarvest')}</ThemedText>
- <FlatList
- data={harvests.filter((item) => {
- const targetCrop = harvestModalTarget === 'edit' ? editCropId : selectedCropId;
- return !targetCrop || item.crop_id === targetCrop;
- })}
- keyExtractor={(item) => String(item.id)}
- renderItem={({ item }) => (
- <Pressable
- onPress={() => {
- 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}>
- <ThemedText>
- {item.crop_name || t('sales.untitled')} ·{' '}
- {item.harvested_at ? formatDate(item.harvested_at) : '-'}
- </ThemedText>
- </Pressable>
- )}
- ListEmptyComponent={<ThemedText style={styles.meta}>{t('sales.noHarvest')}</ThemedText>}
- />
- </View>
- </Pressable>
- </Modal>
- <Modal transparent visible={newModalOpen} animationType="slide">
- <View style={styles.sheetOverlay}>
- <Pressable style={styles.sheetBackdrop} onPress={() => setNewModalOpen(false)} />
- <KeyboardAvoidingView
- behavior={Platform.OS === 'ios' ? 'padding' : undefined}
- keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
- style={styles.keyboardAvoid}>
- <View style={[styles.sheet, { backgroundColor: palette.card, borderColor: palette.border }]}>
- <FlatList
- data={[{ key: 'new' }]}
- keyExtractor={(item) => item.key}
- contentContainerStyle={styles.sheetListContent}
- renderItem={() => (
- <View style={styles.sheetContent}>
- <ThemedText type="subtitle">{t('sales.new')}</ThemedText>
- <ThemedText>
- {t('sales.field')}
- <ThemedText style={styles.requiredMark}> *</ThemedText>
- </ThemedText>
- <ThemedButton
- title={selectedField?.name || t('sales.selectField')}
- onPress={() => {
- setReopenSheetAfterSelect(true);
- setFieldModalTarget('new');
- setNewModalOpen(false);
- setFieldModalOpen(true);
- }}
- variant="secondary"
- />
- {errors.field ? (
- <ThemedText style={styles.errorText}>{errors.field}</ThemedText>
- ) : null}
- <ThemedText>
- {t('sales.crop')}
- <ThemedText style={styles.requiredMark}> *</ThemedText>
- </ThemedText>
- <ThemedButton
- title={selectedCrop?.crop_name || t('sales.selectCrop')}
- onPress={() => {
- setReopenSheetAfterSelect(true);
- setCropModalTarget('new');
- setNewModalOpen(false);
- setCropModalOpen(true);
- }}
- variant="secondary"
- />
- {errors.crop ? (
- <ThemedText style={styles.errorText}>{errors.crop}</ThemedText>
- ) : null}
- <ThemedText>{t('sales.harvest')}</ThemedText>
- <ThemedButton
- title={
- selectedHarvest
- ? formatHarvestLabel(
- selectedHarvest.field_name,
- selectedHarvest.crop_name,
- selectedHarvest.quantity,
- selectedHarvest.unit,
- selectedHarvest.harvested_at,
- t('sales.untitled'),
- t('sales.unitPlaceholder')
- )
- : t('sales.selectHarvest')
- }
- onPress={() => {
- setReopenSheetAfterSelect(true);
- setHarvestModalTarget('new');
- setNewModalOpen(false);
- setHarvestModalOpen(true);
- }}
- variant="secondary"
- />
- <ThemedText>{t('sales.date')}</ThemedText>
- <Pressable onPress={() => setShowSoldPicker(true)} style={styles.dateInput}>
- <ThemedText style={styles.dateValue}>
- {soldDate ? formatDateLabel(soldDate) : t('sales.datePlaceholder')}
- </ThemedText>
- </Pressable>
- {showSoldPicker ? (
- <>
- {Platform.OS === 'ios' ? (
- <View style={styles.pickerRow}>
- <ThemedButton
- title={t('crops.today')}
- onPress={() => {
- setSoldDate(toDateOnly(new Date()));
- setShowSoldPicker(false);
- }}
- variant="secondary"
- />
- <ThemedButton
- title={t('crops.done')}
- onPress={() => setShowSoldPicker(false)}
- variant="secondary"
- />
- </View>
- ) : null}
- <DateTimePicker
- value={soldDate ? new Date(soldDate) : new Date()}
- mode="date"
- display={Platform.OS === 'ios' ? 'inline' : 'calendar'}
- onChange={(event, date) => {
- if (date) setSoldDate(toDateOnly(date));
- if (Platform.OS !== 'ios') setShowSoldPicker(false);
- }}
- />
- </>
- ) : null}
- <ThemedText>
- {t('sales.quantity')}
- <ThemedText style={styles.requiredMark}> *</ThemedText>
- </ThemedText>
- <TextInput
- value={quantity}
- onChangeText={(value) => {
- setQuantity(value);
- if (errors.quantity) {
- setErrors((prev) => ({ ...prev, quantity: undefined }));
- }
- }}
- placeholder={t('sales.quantityPlaceholder')}
- style={inputStyle}
- keyboardType="decimal-pad"
- />
- {errors.quantity ? (
- <ThemedText style={styles.errorText}>{errors.quantity}</ThemedText>
- ) : null}
- <ThemedText>{t('sales.unit')}</ThemedText>
- <View style={styles.unitRow}>
- {unitPresets.map((preset) => {
- const label = t(`units.${preset.key}`);
- const normalized = unit.trim().toLowerCase();
- const isActive =
- label.toLowerCase() === normalized ||
- preset.value.toLowerCase() === normalized;
- return (
- <Pressable
- key={`unit-${preset.key}`}
- onPress={() => setUnit(label)}
- style={[styles.unitChip, isActive && styles.unitChipActive]}>
- <ThemedText style={isActive ? styles.unitTextActive : styles.unitText}>
- {label}
- </ThemedText>
- </Pressable>
- );
- })}
- </View>
- <TextInput
- value={unit}
- onChangeText={setUnit}
- placeholder={t('sales.unitPlaceholder')}
- style={inputStyle}
- />
- <ThemedText>
- {t('sales.price')} ({currency})
- </ThemedText>
- <TextInput
- value={price}
- onChangeText={setPrice}
- placeholder={t('sales.pricePlaceholder')}
- style={inputStyle}
- keyboardType="decimal-pad"
- />
- <ThemedText>{t('sales.buyer')}</ThemedText>
- <TextInput
- value={buyer}
- onChangeText={setBuyer}
- placeholder={t('sales.buyerPlaceholder')}
- style={inputStyle}
- />
- <ThemedText>{t('sales.notes')}</ThemedText>
- <TextInput
- value={notes}
- onChangeText={setNotes}
- placeholder={t('sales.notesPlaceholder')}
- style={inputStyle}
- multiline
- />
- <View style={styles.modalActions}>
- <ThemedButton
- title={t('sales.cancel')}
- onPress={() => setNewModalOpen(false)}
- variant="secondary"
- />
- <ThemedButton
- title={t('sales.save')}
- onPress={async () => {
- const ok = await handleSave();
- if (ok) setNewModalOpen(false);
- }}
- />
- </View>
- <View style={styles.sheetFooter} />
- </View>
- )}
- />
- </View>
- </KeyboardAvoidingView>
- </View>
- </Modal>
- <Modal transparent visible={editModalOpen} animationType="slide">
- <View style={styles.sheetOverlay}>
- <Pressable style={styles.sheetBackdrop} onPress={cancelEdit} />
- <KeyboardAvoidingView
- behavior={Platform.OS === 'ios' ? 'padding' : undefined}
- keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
- style={styles.keyboardAvoid}>
- <View style={[styles.sheet, { backgroundColor: palette.card, borderColor: palette.border }]}>
- <FlatList
- data={[{ key: 'edit' }]}
- keyExtractor={(item) => item.key}
- contentContainerStyle={styles.sheetListContent}
- renderItem={() => (
- <View style={styles.sheetContent}>
- <ThemedText type="subtitle">{t('sales.edit')}</ThemedText>
- <ThemedText>
- {t('sales.field')}
- <ThemedText style={styles.requiredMark}> *</ThemedText>
- </ThemedText>
- <ThemedButton
- title={selectedEditField?.name || t('sales.selectField')}
- onPress={() => {
- setReopenSheetAfterSelect(true);
- setFieldModalTarget('edit');
- setEditModalOpen(false);
- setFieldModalOpen(true);
- }}
- variant="secondary"
- />
- {editErrors.field ? (
- <ThemedText style={styles.errorText}>{editErrors.field}</ThemedText>
- ) : null}
- <ThemedText>
- {t('sales.crop')}
- <ThemedText style={styles.requiredMark}> *</ThemedText>
- </ThemedText>
- <ThemedButton
- title={selectedEditCrop?.crop_name || t('sales.selectCrop')}
- onPress={() => {
- setReopenSheetAfterSelect(true);
- setCropModalTarget('edit');
- setEditModalOpen(false);
- setCropModalOpen(true);
- }}
- variant="secondary"
- />
- {editErrors.crop ? (
- <ThemedText style={styles.errorText}>{editErrors.crop}</ThemedText>
- ) : null}
- <ThemedText>{t('sales.harvest')}</ThemedText>
- <ThemedButton
- title={
- selectedEditHarvest
- ? formatHarvestLabel(
- selectedEditHarvest.field_name,
- selectedEditHarvest.crop_name,
- selectedEditHarvest.quantity,
- selectedEditHarvest.unit,
- selectedEditHarvest.harvested_at,
- t('sales.untitled'),
- t('sales.unitPlaceholder')
- )
- : t('sales.selectHarvest')
- }
- onPress={() => {
- setReopenSheetAfterSelect(true);
- setHarvestModalTarget('edit');
- setEditModalOpen(false);
- setHarvestModalOpen(true);
- }}
- variant="secondary"
- />
- <ThemedText>{t('sales.date')}</ThemedText>
- <Pressable onPress={() => setShowEditSoldPicker(true)} style={styles.dateInput}>
- <ThemedText style={styles.dateValue}>
- {editSoldDate ? formatDateLabel(editSoldDate) : t('sales.datePlaceholder')}
- </ThemedText>
- </Pressable>
- {showEditSoldPicker ? (
- <>
- {Platform.OS === 'ios' ? (
- <View style={styles.pickerRow}>
- <ThemedButton
- title={t('crops.today')}
- onPress={() => {
- setEditSoldDate(toDateOnly(new Date()));
- setShowEditSoldPicker(false);
- }}
- variant="secondary"
- />
- <ThemedButton
- title={t('crops.done')}
- onPress={() => setShowEditSoldPicker(false)}
- variant="secondary"
- />
- </View>
- ) : null}
- <DateTimePicker
- value={editSoldDate ? new Date(editSoldDate) : new Date()}
- mode="date"
- display={Platform.OS === 'ios' ? 'inline' : 'calendar'}
- onChange={(event, date) => {
- if (date) setEditSoldDate(toDateOnly(date));
- if (Platform.OS !== 'ios') setShowEditSoldPicker(false);
- }}
- />
- </>
- ) : null}
- <ThemedText>
- {t('sales.quantity')}
- <ThemedText style={styles.requiredMark}> *</ThemedText>
- </ThemedText>
- <TextInput
- value={editQuantity}
- onChangeText={(value) => {
- setEditQuantity(value);
- if (editErrors.quantity) {
- setEditErrors((prev) => ({ ...prev, quantity: undefined }));
- }
- }}
- placeholder={t('sales.quantityPlaceholder')}
- style={inputStyle}
- keyboardType="decimal-pad"
- />
- {editErrors.quantity ? (
- <ThemedText style={styles.errorText}>{editErrors.quantity}</ThemedText>
- ) : null}
- <ThemedText>{t('sales.unit')}</ThemedText>
- <View style={styles.unitRow}>
- {unitPresets.map((preset) => {
- const label = t(`units.${preset.key}`);
- const normalized = editUnit.trim().toLowerCase();
- const isActive =
- label.toLowerCase() === normalized ||
- preset.value.toLowerCase() === normalized;
- return (
- <Pressable
- key={`edit-unit-${preset.key}`}
- onPress={() => setEditUnit(label)}
- style={[styles.unitChip, isActive && styles.unitChipActive]}>
- <ThemedText style={isActive ? styles.unitTextActive : styles.unitText}>
- {label}
- </ThemedText>
- </Pressable>
- );
- })}
- </View>
- <TextInput
- value={editUnit}
- onChangeText={setEditUnit}
- placeholder={t('sales.unitPlaceholder')}
- style={inputStyle}
- />
- <ThemedText>
- {t('sales.price')} ({currency})
- </ThemedText>
- <TextInput
- value={editPrice}
- onChangeText={setEditPrice}
- placeholder={t('sales.pricePlaceholder')}
- style={inputStyle}
- keyboardType="decimal-pad"
- />
- <ThemedText>{t('sales.buyer')}</ThemedText>
- <TextInput
- value={editBuyer}
- onChangeText={setEditBuyer}
- placeholder={t('sales.buyerPlaceholder')}
- style={inputStyle}
- />
- <ThemedText>{t('sales.notes')}</ThemedText>
- <TextInput
- value={editNotes}
- onChangeText={setEditNotes}
- placeholder={t('sales.notesPlaceholder')}
- style={inputStyle}
- multiline
- />
- <View style={styles.modalActions}>
- <ThemedButton
- title={t('sales.cancel')}
- onPress={cancelEdit}
- variant="secondary"
- />
- <ThemedButton title={t('sales.update')} onPress={handleUpdate} />
- </View>
- <View style={styles.sheetFooter} />
- </View>
- )}
- />
- </View>
- </KeyboardAvoidingView>
- </View>
- </Modal>
- </>
- );
- }
- 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,
- },
- });
|