| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947 |
- import { useEffect, useMemo, useState } from 'react';
- import {
- ActivityIndicator,
- Alert,
- FlatList,
- Image,
- KeyboardAvoidingView,
- Modal,
- Pressable,
- StyleSheet,
- TextInput,
- View,
- Platform,
- } from 'react-native';
- import DateTimePicker from '@react-native-community/datetimepicker';
- import * as ImagePicker from 'expo-image-picker';
- import { ThemedText } from '@/components/themed-text';
- import { ThemedView } from '@/components/themed-view';
- 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 { ThemedButton } from '@/components/themed-button';
- import { IconButton } from '@/components/icon-button';
- type FieldRow = {
- id: number;
- name: string | null;
- };
- type CropRow = {
- id: number;
- field_id: number | null;
- crop_name: string | null;
- variety: string | null;
- planting_date: string | null;
- expected_harvest_date: string | null;
- photo_uri: string | null;
- field_name: string | null;
- };
- export default function CropsScreen() {
- const { t } = useTranslation();
- const theme = useColorScheme() ?? 'light';
- const palette = Colors[theme];
- const pageSize = 12;
- const [crops, setCrops] = useState<CropRow[]>([]);
- const [fields, setFields] = useState<FieldRow[]>([]);
- const [status, setStatus] = useState(t('crops.loading'));
- const [selectedFieldId, setSelectedFieldId] = useState<number | null>(null);
- const [cropName, setCropName] = useState('');
- const [variety, setVariety] = useState('');
- const [plantingDate, setPlantingDate] = useState('');
- const [harvestDate, setHarvestDate] = useState('');
- const [photoUri, setPhotoUri] = useState<string | null>(null);
- const [showPlantingPicker, setShowPlantingPicker] = useState(false);
- const [showHarvestPicker, setShowHarvestPicker] = useState(false);
- const [newModalOpen, setNewModalOpen] = useState(false);
- const [editModalOpen, setEditModalOpen] = useState(false);
- const [editingId, setEditingId] = useState<number | null>(null);
- const [editFieldId, setEditFieldId] = useState<number | null>(null);
- const [editName, setEditName] = useState('');
- const [editVariety, setEditVariety] = useState('');
- const [editPlantingDate, setEditPlantingDate] = useState('');
- const [editHarvestDate, setEditHarvestDate] = useState('');
- const [editPhotoUri, setEditPhotoUri] = useState<string | null>(null);
- const [showEditPlantingPicker, setShowEditPlantingPicker] = useState(false);
- const [showEditHarvestPicker, setShowEditHarvestPicker] = useState(false);
- const [fieldModalOpen, setFieldModalOpen] = useState(false);
- const [fieldModalTarget, setFieldModalTarget] = useState<'new' | 'edit'>('new');
- const [newErrors, setNewErrors] = useState<{ field?: string; name?: string }>({});
- const [editErrors, setEditErrors] = useState<{ field?: string; name?: string }>({});
- const [page, setPage] = useState(1);
- const [hasMore, setHasMore] = useState(true);
- const [loadingMore, setLoadingMore] = useState(false);
- const selectedField = useMemo(
- () => fields.find((item) => item.id === selectedFieldId),
- [fields, selectedFieldId]
- );
- const selectedEditField = useMemo(
- () => fields.find((item) => item.id === editFieldId),
- [fields, editFieldId]
- );
- useEffect(() => {
- let isActive = true;
- async function loadData() {
- try {
- await initCoreTables();
- const db = await dbPromise;
- const fieldRows = await db.getAllAsync<FieldRow>(
- 'SELECT id, name FROM fields ORDER BY name ASC;'
- );
- if (!isActive) return;
- setFields(fieldRows);
- await fetchCropsPage(1, true, isActive);
- } catch (error) {
- if (isActive) setStatus(`Error: ${String(error)}`);
- }
- }
- loadData();
- return () => {
- isActive = false;
- };
- }, [t]);
- async function fetchCropsPage(pageToLoad: number, replace: boolean, isActive = true) {
- try {
- const db = await dbPromise;
- const cropRows = await db.getAllAsync<CropRow>(
- `SELECT c.id, c.field_id, c.crop_name, c.variety, c.planting_date, c.expected_harvest_date, c.photo_uri,
- f.name as field_name
- FROM crops c
- LEFT JOIN fields f ON f.id = c.field_id
- ORDER BY c.id DESC
- LIMIT ? OFFSET ?;`,
- pageSize,
- (pageToLoad - 1) * pageSize
- );
- if (!isActive) return;
- setCrops((prev) => (replace ? cropRows : [...prev, ...cropRows]));
- setHasMore(cropRows.length === pageSize);
- setPage(pageToLoad);
- if (replace) {
- setStatus(cropRows.length === 0 ? t('crops.empty') : '');
- }
- } catch (error) {
- if (isActive) setStatus(`Error: ${String(error)}`);
- } finally {
- if (isActive) setLoadingMore(false);
- }
- }
- async function handleLoadMore() {
- if (loadingMore || !hasMore) return;
- setLoadingMore(true);
- const nextPage = page + 1;
- await fetchCropsPage(nextPage, false);
- }
- async function handleSave() {
- const trimmedName = cropName.trim();
- const nextErrors: { field?: string; name?: string } = {};
- if (!selectedFieldId) {
- nextErrors.field = t('crops.fieldRequired');
- }
- if (!trimmedName) {
- nextErrors.name = t('crops.nameRequired');
- }
- setNewErrors(nextErrors);
- if (Object.keys(nextErrors).length > 0) {
- setStatus(nextErrors.field ?? nextErrors.name ?? t('crops.fieldRequired'));
- return false;
- }
- try {
- const db = await dbPromise;
- await db.runAsync(
- 'INSERT INTO crops (field_id, crop_name, variety, planting_date, expected_harvest_date, photo_uri) VALUES (?, ?, ?, ?, ?, ?);',
- selectedFieldId,
- trimmedName,
- variety.trim() || null,
- plantingDate.trim() || null,
- harvestDate.trim() || null,
- photoUri
- );
- setCropName('');
- setVariety('');
- setPlantingDate('');
- setHarvestDate('');
- setPhotoUri(null);
- setNewErrors({});
- await fetchCropsPage(1, true);
- setStatus(t('crops.saved'));
- return true;
- } catch (error) {
- setStatus(`Error: ${String(error)}`);
- return false;
- }
- }
- async function handleDelete(id: number) {
- try {
- const db = await dbPromise;
- await db.runAsync('DELETE FROM crops WHERE id = ?;', id);
- await fetchCropsPage(1, true);
- } catch (error) {
- setStatus(`Error: ${String(error)}`);
- }
- }
- function confirmDelete(id: number) {
- Alert.alert(
- t('crops.deleteTitle'),
- t('crops.deleteMessage'),
- [
- { text: t('crops.cancel'), style: 'cancel' },
- { text: t('crops.delete'), style: 'destructive', onPress: () => handleDelete(id) },
- ]
- );
- }
- function startEdit(crop: CropRow) {
- setEditingId(crop.id);
- setEditFieldId(crop.field_id ?? null);
- setEditName(crop.crop_name ?? '');
- setEditVariety(crop.variety ?? '');
- setEditPlantingDate(crop.planting_date ?? '');
- setEditHarvestDate(crop.expected_harvest_date ?? '');
- setEditPhotoUri(crop.photo_uri ?? null);
- setEditErrors({});
- setEditModalOpen(true);
- }
- function cancelEdit() {
- setEditingId(null);
- setEditFieldId(null);
- setEditName('');
- setEditVariety('');
- setEditPlantingDate('');
- setEditHarvestDate('');
- setEditPhotoUri(null);
- setEditErrors({});
- setEditModalOpen(false);
- }
- async function handleUpdate() {
- if (!editingId) return;
- const trimmedName = editName.trim();
- const nextErrors: { field?: string; name?: string } = {};
- if (!editFieldId) {
- nextErrors.field = t('crops.fieldRequired');
- }
- if (!trimmedName) {
- nextErrors.name = t('crops.nameRequired');
- }
- setEditErrors(nextErrors);
- if (Object.keys(nextErrors).length > 0) {
- setStatus(nextErrors.field ?? nextErrors.name ?? t('crops.fieldRequired'));
- return;
- }
- try {
- const db = await dbPromise;
- await db.runAsync(
- 'UPDATE crops SET field_id = ?, crop_name = ?, variety = ?, planting_date = ?, expected_harvest_date = ?, photo_uri = ? WHERE id = ?;',
- editFieldId,
- trimmedName,
- editVariety.trim() || null,
- editPlantingDate.trim() || null,
- editHarvestDate.trim() || null,
- editPhotoUri,
- editingId
- );
- setEditModalOpen(false);
- setEditingId(null);
- setEditErrors({});
- await fetchCropsPage(1, true);
- setStatus(t('crops.saved'));
- } catch (error) {
- setStatus(`Error: ${String(error)}`);
- }
- }
- const inputStyle = [
- styles.input,
- { borderColor: palette.border, backgroundColor: palette.input, color: palette.text },
- ];
- return (
- <>
- <FlatList
- data={crops}
- keyExtractor={(item) => String(item.id)}
- renderItem={({ item }) => (
- <Pressable onPress={() => startEdit(item)}>
- <ThemedView style={[styles.card, { backgroundColor: palette.card, borderColor: palette.border }]}>
- <ThemedText type="subtitle">{item.crop_name || t('crops.untitled')}</ThemedText>
- <ThemedText style={styles.meta}>
- {item.field_name || t('crops.noField')}
- </ThemedText>
- {item.variety ? <ThemedText>{item.variety}</ThemedText> : null}
- {item.photo_uri ? (
- <Image source={{ uri: item.photo_uri }} style={styles.photoPreview} resizeMode="cover" />
- ) : null}
- {item.planting_date ? (
- <ThemedText style={styles.meta}>
- {t('crops.plantingLabel')} {item.planting_date}
- </ThemedText>
- ) : null}
- {item.expected_harvest_date ? (
- <ThemedText style={styles.meta}>
- {t('crops.harvestLabel')} {item.expected_harvest_date}
- </ThemedText>
- ) : null}
- <View style={styles.buttonRow}>
- <IconButton
- name="trash"
- onPress={() => confirmDelete(item.id)}
- accessibilityLabel={t('crops.delete')}
- variant="danger"
- />
- </View>
- </ThemedView>
- </Pressable>
- )}
- ItemSeparatorComponent={() => <View style={styles.separator} />}
- onEndReached={handleLoadMore}
- onEndReachedThreshold={0.4}
- ListHeaderComponent={
- <View>
- <ThemedView style={styles.hero}>
- <Image source={require('@/assets/images/crops.jpg')} style={styles.heroImage} />
- </ThemedView>
- <ThemedView style={styles.titleContainer}>
- <ThemedText type="title" style={{ fontFamily: Fonts.rounded }}>
- {t('crops.title')}
- </ThemedText>
- </ThemedView>
- {status ? (
- <ThemedView style={styles.section}>
- <ThemedText>{status}</ThemedText>
- </ThemedView>
- ) : null}
- <ThemedView style={styles.section}>
- <Pressable
- style={styles.newButton}
- onPress={() => {
- setNewErrors({});
- setNewModalOpen(true);
- }}>
- <IconSymbol size={18} name="plus.circle.fill" color="#2F7D4F" />
- <ThemedText style={styles.newButtonText}>{t('crops.new')}</ThemedText>
- </Pressable>
- </ThemedView>
- </View>
- }
- ListFooterComponent={
- <View style={styles.footer}>
- {loadingMore ? <ActivityIndicator /> : null}
- </View>
- }
- />
- <Modal transparent visible={fieldModalOpen} animationType="fade">
- <Pressable
- style={styles.modalOverlay}
- onPress={() => setFieldModalOpen(false)}>
- <View style={[styles.modalCard, { backgroundColor: palette.card, borderColor: palette.border }]}>
- <ThemedText type="subtitle">{t('crops.selectField')}</ThemedText>
- <FlatList
- data={fields}
- keyExtractor={(item) => String(item.id)}
- renderItem={({ item }) => (
- <Pressable
- onPress={() => {
- if (fieldModalTarget === 'new') {
- setSelectedFieldId(item.id);
- setNewErrors((prev) => ({ ...prev, field: undefined }));
- setNewModalOpen(true);
- } else {
- setEditFieldId(item.id);
- setEditErrors((prev) => ({ ...prev, field: undefined }));
- setEditModalOpen(true);
- }
- setFieldModalOpen(false);
- }}
- style={styles.modalItem}>
- <ThemedText>{item.name || t('crops.untitled')}</ThemedText>
- </Pressable>
- )}
- />
- </View>
- </Pressable>
- </Modal>
- <Modal
- transparent
- visible={newModalOpen}
- animationType="slide"
- onRequestClose={() => setNewModalOpen(false)}>
- <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('crops.new')}</ThemedText>
- <ThemedText>
- {t('crops.field')}
- <ThemedText style={styles.requiredMark}> *</ThemedText>
- </ThemedText>
- <ThemedButton
- title={selectedField?.name || t('crops.selectField')}
- onPress={() => {
- setFieldModalTarget('new');
- setNewModalOpen(false);
- setFieldModalOpen(true);
- }}
- variant="secondary"
- />
- {newErrors.field ? (
- <ThemedText style={styles.errorText}>{newErrors.field}</ThemedText>
- ) : null}
- <ThemedText>
- {t('crops.name')}
- <ThemedText style={styles.requiredMark}> *</ThemedText>
- </ThemedText>
- <TextInput
- value={cropName}
- onChangeText={(value) => {
- setCropName(value);
- if (newErrors.name) {
- setNewErrors((prev) => ({ ...prev, name: undefined }));
- }
- }}
- placeholder={t('crops.namePlaceholder')}
- style={inputStyle}
- />
- {newErrors.name ? (
- <ThemedText style={styles.errorText}>{newErrors.name}</ThemedText>
- ) : null}
- <ThemedText>{t('crops.variety')}</ThemedText>
- <TextInput
- value={variety}
- onChangeText={setVariety}
- placeholder={t('crops.varietyPlaceholder')}
- style={inputStyle}
- />
- <ThemedText>{t('crops.photo')}</ThemedText>
- {photoUri ? (
- <Image source={{ uri: photoUri }} style={styles.photoPreview} resizeMode="cover" />
- ) : (
- <ThemedText style={styles.photoPlaceholder}>{t('crops.noPhoto')}</ThemedText>
- )}
- <View style={styles.photoRow}>
- <ThemedButton
- title={t('crops.pickPhoto')}
- onPress={() => handlePickPhoto(setPhotoUri)}
- variant="secondary"
- />
- <ThemedButton
- title={t('crops.takePhoto')}
- onPress={() =>
- handleTakePhoto(setPhotoUri, (code) =>
- setStatus(
- code === 'cameraDenied' ? t('tasks.cameraDenied') : t('tasks.cameraError')
- )
- )
- }
- variant="secondary"
- />
- </View>
- <ThemedText>{t('crops.planting')}</ThemedText>
- <Pressable onPress={() => setShowPlantingPicker(true)} style={styles.dateInput}>
- <ThemedText style={styles.dateValue}>
- {plantingDate ? formatDateLabel(plantingDate) : t('crops.plantingPlaceholder')}
- </ThemedText>
- </Pressable>
- {showPlantingPicker ? (
- <>
- {Platform.OS === 'ios' ? (
- <View style={styles.pickerRow}>
- <ThemedButton
- title={t('crops.today')}
- onPress={() => {
- setPlantingDate(toDateOnly(new Date()));
- setShowPlantingPicker(false);
- }}
- variant="secondary"
- />
- <ThemedButton
- title={t('crops.done')}
- onPress={() => setShowPlantingPicker(false)}
- variant="secondary"
- />
- </View>
- ) : null}
- <DateTimePicker
- value={plantingDate ? new Date(plantingDate) : new Date()}
- mode="date"
- display={Platform.OS === 'ios' ? 'inline' : 'calendar'}
- onChange={(event, date) => {
- if (date) setPlantingDate(toDateOnly(date));
- if (Platform.OS !== 'ios') {
- setShowPlantingPicker(false);
- }
- }}
- />
- </>
- ) : null}
- <ThemedText>{t('crops.harvest')}</ThemedText>
- <Pressable onPress={() => setShowHarvestPicker(true)} style={styles.dateInput}>
- <ThemedText style={styles.dateValue}>
- {harvestDate ? formatDateLabel(harvestDate) : t('crops.harvestPlaceholder')}
- </ThemedText>
- </Pressable>
- {showHarvestPicker ? (
- <>
- {Platform.OS === 'ios' ? (
- <View style={styles.pickerRow}>
- <ThemedButton
- title={t('crops.today')}
- onPress={() => {
- setHarvestDate(toDateOnly(new Date()));
- setShowHarvestPicker(false);
- }}
- variant="secondary"
- />
- <ThemedButton
- title={t('crops.done')}
- onPress={() => setShowHarvestPicker(false)}
- variant="secondary"
- />
- </View>
- ) : null}
- <DateTimePicker
- value={harvestDate ? new Date(harvestDate) : new Date()}
- mode="date"
- display={Platform.OS === 'ios' ? 'inline' : 'calendar'}
- onChange={(event, date) => {
- if (date) setHarvestDate(toDateOnly(date));
- if (Platform.OS !== 'ios') {
- setShowHarvestPicker(false);
- }
- }}
- />
- </>
- ) : null}
- <View style={styles.modalActions}>
- <ThemedButton
- title={t('crops.cancel')}
- onPress={() => setNewModalOpen(false)}
- variant="secondary"
- />
- <ThemedButton
- title={t('crops.save')}
- onPress={async () => {
- const ok = await handleSave();
- if (ok) setNewModalOpen(false);
- }}
- />
- </View>
- </View>
- )}
- ListFooterComponent={<View style={styles.sheetFooter} />}
- />
- </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('crops.edit')}</ThemedText>
- <ThemedText>
- {t('crops.field')}
- <ThemedText style={styles.requiredMark}> *</ThemedText>
- </ThemedText>
- <ThemedButton
- title={selectedEditField?.name || t('crops.selectField')}
- onPress={() => {
- setFieldModalTarget('edit');
- setEditModalOpen(false);
- setFieldModalOpen(true);
- }}
- variant="secondary"
- />
- {editErrors.field ? (
- <ThemedText style={styles.errorText}>{editErrors.field}</ThemedText>
- ) : null}
- <ThemedText>
- {t('crops.name')}
- <ThemedText style={styles.requiredMark}> *</ThemedText>
- </ThemedText>
- <TextInput
- value={editName}
- onChangeText={(value) => {
- setEditName(value);
- if (editErrors.name) {
- setEditErrors((prev) => ({ ...prev, name: undefined }));
- }
- }}
- placeholder={t('crops.namePlaceholder')}
- style={inputStyle}
- />
- {editErrors.name ? (
- <ThemedText style={styles.errorText}>{editErrors.name}</ThemedText>
- ) : null}
- <ThemedText>{t('crops.variety')}</ThemedText>
- <TextInput
- value={editVariety}
- onChangeText={setEditVariety}
- placeholder={t('crops.varietyPlaceholder')}
- style={inputStyle}
- />
- <ThemedText>{t('crops.photo')}</ThemedText>
- {editPhotoUri ? (
- <Image
- source={{ uri: editPhotoUri }}
- style={styles.photoPreview}
- resizeMode="cover"
- />
- ) : (
- <ThemedText style={styles.photoPlaceholder}>{t('crops.noPhoto')}</ThemedText>
- )}
- <View style={styles.photoRow}>
- <ThemedButton
- title={t('crops.pickPhoto')}
- onPress={() => handlePickPhoto(setEditPhotoUri)}
- variant="secondary"
- />
- <ThemedButton
- title={t('crops.takePhoto')}
- onPress={() =>
- handleTakePhoto(setEditPhotoUri, (code) =>
- setStatus(
- code === 'cameraDenied' ? t('tasks.cameraDenied') : t('tasks.cameraError')
- )
- )
- }
- variant="secondary"
- />
- </View>
- <ThemedText>{t('crops.planting')}</ThemedText>
- <Pressable onPress={() => setShowEditPlantingPicker(true)} style={styles.dateInput}>
- <ThemedText style={styles.dateValue}>
- {editPlantingDate
- ? formatDateLabel(editPlantingDate)
- : t('crops.plantingPlaceholder')}
- </ThemedText>
- </Pressable>
- {showEditPlantingPicker ? (
- <>
- {Platform.OS === 'ios' ? (
- <View style={styles.pickerRow}>
- <ThemedButton
- title={t('crops.today')}
- onPress={() => {
- setEditPlantingDate(toDateOnly(new Date()));
- setShowEditPlantingPicker(false);
- }}
- variant="secondary"
- />
- <ThemedButton
- title={t('crops.done')}
- onPress={() => setShowEditPlantingPicker(false)}
- variant="secondary"
- />
- </View>
- ) : null}
- <DateTimePicker
- value={editPlantingDate ? new Date(editPlantingDate) : new Date()}
- mode="date"
- display={Platform.OS === 'ios' ? 'inline' : 'calendar'}
- onChange={(event, date) => {
- if (date) setEditPlantingDate(toDateOnly(date));
- if (Platform.OS !== 'ios') {
- setShowEditPlantingPicker(false);
- }
- }}
- />
- </>
- ) : null}
- <ThemedText>{t('crops.harvest')}</ThemedText>
- <Pressable onPress={() => setShowEditHarvestPicker(true)} style={styles.dateInput}>
- <ThemedText style={styles.dateValue}>
- {editHarvestDate
- ? formatDateLabel(editHarvestDate)
- : t('crops.harvestPlaceholder')}
- </ThemedText>
- </Pressable>
- {showEditHarvestPicker ? (
- <>
- {Platform.OS === 'ios' ? (
- <View style={styles.pickerRow}>
- <ThemedButton
- title={t('crops.today')}
- onPress={() => {
- setEditHarvestDate(toDateOnly(new Date()));
- setShowEditHarvestPicker(false);
- }}
- variant="secondary"
- />
- <ThemedButton
- title={t('crops.done')}
- onPress={() => setShowEditHarvestPicker(false)}
- variant="secondary"
- />
- </View>
- ) : null}
- <DateTimePicker
- value={editHarvestDate ? new Date(editHarvestDate) : new Date()}
- mode="date"
- display={Platform.OS === 'ios' ? 'inline' : 'calendar'}
- onChange={(event, date) => {
- if (date) setEditHarvestDate(toDateOnly(date));
- if (Platform.OS !== 'ios') {
- setShowEditHarvestPicker(false);
- }
- }}
- />
- </>
- ) : null}
- <View style={styles.modalActions}>
- <ThemedButton title={t('crops.cancel')} onPress={cancelEdit} variant="secondary" />
- <ThemedButton title={t('crops.update')} onPress={handleUpdate} />
- </View>
- </View>
- )}
- ListFooterComponent={<View style={styles.sheetFooter} />}
- />
- </View>
- </KeyboardAvoidingView>
- </View>
- </Modal>
- </>
- );
- }
- function toDateOnly(date: Date) {
- return date.toISOString().slice(0, 10);
- }
- function formatDateLabel(value: string) {
- try {
- return new Date(value).toLocaleDateString();
- } catch {
- return value;
- }
- }
- async function handlePickPhoto(setter: (value: string | null) => void) {
- const result = await ImagePicker.launchImageLibraryAsync({
- mediaTypes: getImageMediaTypes(),
- quality: 1,
- });
- if (result.canceled) return;
- const asset = result.assets[0];
- setter(asset.uri);
- }
- async function handleTakePhoto(
- setter: (value: string | null) => void,
- onError?: (msg: string) => void
- ) {
- try {
- const permission = await ImagePicker.requestCameraPermissionsAsync();
- if (!permission.granted) {
- onError?.('cameraDenied');
- return;
- }
- const result = await ImagePicker.launchCameraAsync({ quality: 1 });
- if (result.canceled) return;
- const asset = result.assets[0];
- setter(asset.uri);
- } catch {
- onError?.('cameraError');
- }
- }
- function getImageMediaTypes() {
- const mediaType = (ImagePicker as { MediaType?: { Image?: unknown; Images?: unknown } })
- .MediaType;
- return mediaType?.Image ?? mediaType?.Images ?? ['images'];
- }
- 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,
- },
- card: {
- borderRadius: 12,
- borderWidth: 1,
- borderColor: '#C6C6C6',
- padding: 12,
- marginHorizontal: 16,
- gap: 6,
- },
- meta: {
- opacity: 0.7,
- },
- input: {
- borderRadius: 10,
- borderWidth: 1,
- borderColor: '#B9B9B9',
- paddingHorizontal: 12,
- paddingVertical: 10,
- fontSize: 15,
- },
- requiredMark: {
- color: '#C0392B',
- fontWeight: '700',
- },
- errorText: {
- color: '#C0392B',
- fontSize: 12,
- },
- photoPreview: {
- height: 160,
- width: '100%',
- borderRadius: 12,
- },
- photoRow: {
- flexDirection: 'row',
- gap: 8,
- },
- photoPlaceholder: {
- opacity: 0.6,
- },
- dateInput: {
- borderRadius: 10,
- borderWidth: 1,
- borderColor: '#B9B9B9',
- paddingHorizontal: 12,
- paddingVertical: 10,
- },
- dateValue: {
- fontSize: 15,
- opacity: 0.9,
- },
- pickerRow: {
- flexDirection: 'row',
- gap: 8,
- },
- buttonRow: {
- alignSelf: 'flex-start',
- },
- 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%',
- },
- modalActions: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- gap: 12,
- },
- 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',
- },
- 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',
- },
- modalItem: {
- paddingVertical: 8,
- },
- });
|