import { useEffect, useRef, useState } from 'react'; import { KeyboardAvoidingView, Platform, Pressable, StyleSheet, TextInput, View } from 'react-native'; import { Image } from 'expo-image'; import { Asset } from 'expo-asset'; import * as ImagePicker from 'expo-image-picker'; import * as FileSystem from 'expo-file-system/legacy'; import * as Sharing from 'expo-sharing'; import ParallaxScrollView from '@/components/parallax-scroll-view'; 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 { createTranslator, useTranslation } from '@/localization/i18n'; import { dbPromise, initCoreTables } from '@/services/db'; import { ThemedButton } from '@/components/themed-button'; import { useColorScheme } from '@/hooks/use-color-scheme'; type Profile = { name: string; farmName: string; location: string; photoUri: string; language: 'en' | 'th' | 'ja'; currency: string; }; const emptyProfile: Profile = { name: '', farmName: '', location: '', photoUri: '', language: 'en', currency: 'THB', }; type DemoBackup = { user_profile: Array<{ id: number; name: string | null; farm_name: string | null; location: string | null; photo_uri: string | null; language: string | null; currency: string | null; created_at: string | null; updated_at: string | null; }>; fields: Array<{ id: number; name: string | null; area_ha: number | null; boundary_geojson: string | null; notes: string | null; photo_uri: string | null; created_at: string | null; updated_at: string | null; }>; crops: Array<{ 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; }>; observations: Array<{ id: number; field_id: number | null; crop_id: number | null; obs_type: string | null; note: string | null; severity: number | null; lat: number | null; lng: number | null; observed_at: string | null; }>; images: Array<{ id: number; observation_id: number | null; uri: string | null; thumbnail_uri: string | null; width: number | null; height: number | null; created_at: string | null; }>; daily_tasks: Array<{ id: number; name: string | null; description: string | null; default_time: string | null; is_active: number | null; created_at: string | null; }>; daily_task_entries: Array<{ id: number; task_id: number | null; field_id: number | null; notes: string | null; status: string | null; completed_at: string | null; created_at: string | null; meta_json: string | null; }>; ml_inferences: Array<{ id: number; image_id: number | null; model_name: string | null; model_version: string | null; top_label: string | null; top_score: number | null; scores_json: string | null; created_at: string | null; }>; harvests: Array<{ id: number; field_id: number | null; crop_id: number | null; harvested_at: string | null; quantity: number | null; unit: string | null; notes: string | null; photo_uri: string | null; created_at: string | null; }>; sales: Array<{ id: number; harvest_id: number | null; field_id: number | null; crop_id: number | null; sold_at: string | null; quantity: number | null; unit: string | null; price: number | null; buyer: string | null; notes: string | null; created_at: string | null; }>; costs: Array<{ id: number; field_id: number | null; crop_id: number | null; category: string | null; amount: number | null; currency: string | null; vendor: string | null; notes: string | null; spent_at: string | null; photo_uri: string | null; created_at: string | null; }>; }; export default function SetupScreen() { const { language, setLanguage, t } = useTranslation(); const theme = useColorScheme() ?? 'light'; const palette = Colors[theme]; const currencyPresets = ['THB', 'USD', 'EUR', 'JPY']; const [profile, setProfile] = useState(emptyProfile); const [status, setStatus] = useState(t('setup.loading')); const [saveIndicator, setSaveIndicator] = useState(''); const [snackbarVisible, setSnackbarVisible] = useState(false); const [snackbarMessage, setSnackbarMessage] = useState(''); const demoBackupRef = useRef(null); const snackbarTimerRef = useRef | null>(null); useEffect(() => { let isActive = true; async function init() { await initCoreTables(); const db = await dbPromise; const row = await db.getFirstAsync<{ name: string | null; farm_name: string | null; location: string | null; photo_uri: string | null; language: string | null; currency: string | null; }>( 'SELECT name, farm_name, location, photo_uri, language, currency FROM user_profile WHERE id = 1;' ); if (!isActive) return; if (row) { const storedLanguage = row.language === 'th' || row.language === 'en' || row.language === 'ja' ? row.language : language; if (storedLanguage !== language) { setLanguage(storedLanguage); } setProfile({ name: row.name ?? '', farmName: row.farm_name ?? '', location: row.location ?? '', photoUri: row.photo_uri ?? '', language: storedLanguage, currency: row.currency ?? 'THB', }); setStatus(t('setup.loaded')); } else { setProfile((prev) => ({ ...prev, language, currency: 'THB' })); setStatus(t('setup.none')); } } init().catch((error) => { if (isActive) setStatus(`Error: ${String(error)}`); }); return () => { isActive = false; if (snackbarTimerRef.current) { clearTimeout(snackbarTimerRef.current); } }; }, []); async function handleSave() { setStatus(t('setup.saving')); try { const db = await dbPromise; const now = new Date().toISOString(); await db.runAsync( 'INSERT OR REPLACE INTO user_profile (id, name, farm_name, location, photo_uri, language, currency, created_at, updated_at) VALUES (1, ?, ?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM user_profile WHERE id = 1), ?), ?);', profile.name.trim(), profile.farmName.trim(), profile.location.trim(), profile.photoUri, profile.language, profile.currency.trim(), now, now ); setStatus(t('setup.saved')); setSaveIndicator(t('setup.saveIndicator')); setTimeout(() => setSaveIndicator(''), 2000); } catch (error) { setStatus(`Error: ${String(error)}`); } } async function handleInsertDemoData() { try { await initCoreTables(); const db = await dbPromise; const fieldCount = await db.getFirstAsync<{ count: number }>( 'SELECT COUNT(*) as count FROM fields;' ); if (fieldCount?.count && fieldCount.count > 0) { showSnackbar(t('setup.demoExists')); return; } const now = new Date(); const nowIso = now.toISOString(); const day = 24 * 60 * 60 * 1000; const toDateOnly = (date: Date) => date.toISOString().slice(0, 10); const demoT = createTranslator(profile.language); const truncateText = (value: string, max = 160) => { if (value.length <= max) return value; return `${value.slice(0, Math.max(0, max - 3)).trimEnd()}...`; }; const [fieldImageUri, cropImageUri, observationImageUri] = await loadDemoImageUris(); const fieldRows = [ { name: demoT('demo.field.north'), area: 2.4, notes: demoT('demo.field.northNote'), photoUri: fieldImageUri, }, { name: demoT('demo.field.river'), area: 1.2, notes: demoT('demo.field.riverNote'), photoUri: fieldImageUri, }, { name: demoT('demo.field.greenhouse'), area: 0.4, notes: demoT('demo.field.greenhouseNote'), photoUri: fieldImageUri, }, { name: demoT('demo.field.orchard'), area: 1.8, notes: demoT('demo.field.orchardNote'), photoUri: fieldImageUri, }, { name: demoT('demo.field.terrace'), area: 0.9, notes: demoT('demo.field.terraceNote'), photoUri: fieldImageUri, }, ]; const extraFieldCount = 100; const fieldSuffixes = [ demoT('demo.field.northNote'), demoT('demo.field.riverNote'), demoT('demo.field.greenhouseNote'), demoT('demo.field.orchardNote'), demoT('demo.field.terraceNote'), demoT('demo.observation.scoutingNote'), demoT('demo.observation.irrigationNote'), demoT('demo.observation.pestNote'), demoT('demo.observation.nutrientNote'), ]; for (let i = 0; i < extraFieldCount; i += 1) { const seed = fieldRows[i % fieldRows.length]; fieldRows.push({ name: truncateText(`${seed.name} ${fieldSuffixes[i % fieldSuffixes.length]}`), area: Number((seed.area + (i % 3) * 0.2).toFixed(2)), notes: truncateText(seed.notes), photoUri: fieldImageUri, }); } const fieldIds: number[] = []; for (const field of fieldRows) { const result = await db.runAsync( 'INSERT INTO fields (name, area_ha, notes, photo_uri, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?);', field.name, field.area, field.notes, field.photoUri, nowIso, nowIso ); if (result.lastInsertRowId) fieldIds.push(Number(result.lastInsertRowId)); } const cropTemplates = [ { name: demoT('demo.crop.tomato'), variety: demoT('demo.crop.tomatoVariety'), }, { name: demoT('demo.crop.rice'), variety: demoT('demo.crop.riceVariety'), }, { name: demoT('demo.crop.lettuce'), variety: demoT('demo.crop.lettuceVariety'), }, { name: demoT('demo.crop.chili'), variety: demoT('demo.crop.chiliVariety'), }, { name: demoT('demo.crop.cabbage'), variety: demoT('demo.crop.cabbageVariety'), }, ]; const cropRows = fieldIds.slice(0, cropTemplates.length).map((fieldId, index) => { const template = cropTemplates[index]; return { fieldId, name: template.name, variety: template.variety, planting: toDateOnly(new Date(now.getTime() - (30 + index * 7) * day)), harvest: toDateOnly(new Date(now.getTime() + (25 + index * 5) * day)), photoUri: cropImageUri, }; }); const extraCropCount = 120; const cropSuffixes = [ demoT('demo.observation.scoutingNote'), demoT('demo.observation.diseaseNote'), demoT('demo.observation.irrigationNote'), demoT('demo.observation.pestNote'), demoT('demo.observation.nutrientNote'), ]; for (let i = 0; i < extraCropCount; i += 1) { const template = cropTemplates[i % cropTemplates.length]; cropRows.push({ fieldId: fieldIds[i % fieldIds.length], name: truncateText(`${template.name} ${cropSuffixes[i % cropSuffixes.length]}`), variety: truncateText(template.variety), planting: toDateOnly(new Date(now.getTime() - (10 + i * 3) * day)), harvest: toDateOnly(new Date(now.getTime() + (20 + i * 4) * day)), photoUri: cropImageUri, }); } const cropIds: number[] = []; for (const crop of cropRows) { if (!crop.fieldId) continue; const result = await db.runAsync( 'INSERT INTO crops (field_id, crop_name, variety, planting_date, expected_harvest_date, photo_uri) VALUES (?, ?, ?, ?, ?, ?);', crop.fieldId, crop.name, crop.variety, crop.planting, crop.harvest, crop.photoUri ); if (result.lastInsertRowId) cropIds.push(Number(result.lastInsertRowId)); } const observationNotes = [ demoT('demo.observation.scoutingNote'), demoT('demo.observation.diseaseNote'), demoT('demo.observation.irrigationNote'), demoT('demo.observation.pestNote'), demoT('demo.observation.nutrientNote'), ]; const observationTypes = [ demoT('observations.type.scouting'), demoT('observations.type.disease'), demoT('observations.type.irrigation'), demoT('observations.type.pest'), demoT('observations.type.nutrients'), ]; const observationRows = [ { fieldId: fieldIds[0], cropId: cropIds[0], type: demoT('observations.type.scouting'), note: demoT('demo.observation.scoutingNote'), severity: 2, observedAt: new Date(now.getTime() - 2 * day).toISOString(), }, { fieldId: fieldIds[1], cropId: cropIds[1], type: demoT('observations.type.disease'), note: demoT('demo.observation.diseaseNote'), severity: 5, observedAt: new Date(now.getTime() - day).toISOString(), }, { fieldId: fieldIds[2], cropId: cropIds[2], type: demoT('observations.type.irrigation'), note: demoT('demo.observation.irrigationNote'), severity: null, observedAt: nowIso, }, { fieldId: fieldIds[3], cropId: cropIds[3], type: demoT('observations.type.pest'), note: demoT('demo.observation.pestNote'), severity: 3, observedAt: new Date(now.getTime() - 3 * day).toISOString(), }, { fieldId: fieldIds[4], cropId: cropIds[4], type: demoT('observations.type.nutrients'), note: demoT('demo.observation.nutrientNote'), severity: 4, observedAt: new Date(now.getTime() - 4 * day).toISOString(), }, ]; const extraObservationCount = 160; for (let i = 0; i < extraObservationCount; i += 1) { observationRows.push({ fieldId: fieldIds[i % fieldIds.length], cropId: cropIds[i % cropIds.length], type: observationTypes[i % observationTypes.length], note: observationNotes[i % observationNotes.length], severity: (i % 5) + 1, observedAt: new Date(now.getTime() - (i + 5) * day).toISOString(), }); } for (const obs of observationRows) { if (!obs.fieldId) continue; const result = await db.runAsync( 'INSERT INTO observations (field_id, crop_id, obs_type, note, severity, observed_at) VALUES (?, ?, ?, ?, ?, ?);', obs.fieldId, obs.cropId ?? null, obs.type, obs.note, obs.severity, obs.observedAt ); if (observationImageUri && result.lastInsertRowId) { await db.runAsync( 'INSERT INTO images (observation_id, uri, created_at) VALUES (?, ?, ?);', result.lastInsertRowId, observationImageUri, obs.observedAt ); } } const taskCount = await db.getFirstAsync<{ count: number }>( 'SELECT COUNT(*) as count FROM daily_tasks;' ); if (!taskCount?.count) { const defaultTasks = [ { name: demoT('tasks.default.fieldCheck'), description: demoT('tasks.default.fieldCheckDesc'), time: '08:00' }, { name: demoT('tasks.default.scouting'), description: demoT('tasks.default.scoutingDesc'), time: '10:00' }, { name: demoT('tasks.default.sensors'), description: demoT('tasks.default.sensorsDesc'), time: '15:00' }, ]; for (const task of defaultTasks) { await db.runAsync( 'INSERT INTO daily_tasks (name, description, default_time, is_active, created_at) VALUES (?, ?, ?, 1, ?);', task.name, task.description, task.time, nowIso ); } } const taskRows = await db.getAllAsync<{ id: number; name: string }>( 'SELECT id, name FROM daily_tasks WHERE is_active = 1 ORDER BY id ASC LIMIT 3;' ); const taskNotes = [demoT('demo.task.note'), demoT('demo.task.note2'), demoT('demo.task.note3')]; let entryIndex = 0; for (let dayOffset = 0; dayOffset < 120; dayOffset += 1) { for (let i = 0; i < taskRows.length; i += 1) { const entryTime = new Date(now.getTime() - (dayOffset + i) * day).toISOString(); await db.runAsync( 'INSERT INTO daily_task_entries (task_id, field_id, notes, status, completed_at, created_at, meta_json) VALUES (?, ?, ?, ?, ?, ?, ?);', taskRows[i].id, fieldIds[0] ?? null, taskNotes[entryIndex % taskNotes.length], 'done', entryTime, entryTime, null ); entryIndex += 1; } } const harvestRows = [ { fieldId: fieldIds[0], cropId: cropIds[0], date: new Date(now.getTime() - 5 * day).toISOString(), qty: 120, unit: 'kg', notes: demoT('demo.harvest.note1'), }, { fieldId: fieldIds[1], cropId: cropIds[1], date: new Date(now.getTime() - 3 * day).toISOString(), qty: 260, unit: 'kg', notes: demoT('demo.harvest.note2'), }, { fieldId: fieldIds[2], cropId: cropIds[2], date: new Date(now.getTime() - 2 * day).toISOString(), qty: 40, unit: 'kg', notes: demoT('demo.harvest.note3'), }, ]; const extraHarvestCount = 100; for (let i = 0; i < extraHarvestCount; i += 1) { harvestRows.push({ fieldId: fieldIds[i % fieldIds.length], cropId: cropIds[i % cropIds.length], date: new Date(now.getTime() - (7 + i) * day).toISOString(), qty: 30 + i * 12, unit: 'kg', notes: [demoT('demo.harvest.note1'), demoT('demo.harvest.note2'), demoT('demo.harvest.note3')][ i % 3 ], }); } const harvestIds: number[] = []; for (const harvest of harvestRows) { if (!harvest.fieldId || !harvest.cropId) continue; const result = await db.runAsync( 'INSERT INTO harvests (field_id, crop_id, harvested_at, quantity, unit, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?);', harvest.fieldId, harvest.cropId, harvest.date, harvest.qty, harvest.unit, harvest.notes, nowIso ); if (result.lastInsertRowId) harvestIds.push(Number(result.lastInsertRowId)); } const saleRows = [ { harvestId: harvestIds[0] ?? null, fieldId: fieldIds[0], cropId: cropIds[0], date: new Date(now.getTime() - 4 * day).toISOString(), qty: 80, unit: 'kg', price: 35, buyer: demoT('demo.sale.buyer1'), notes: demoT('demo.sale.note1'), }, { harvestId: harvestIds[1] ?? null, fieldId: fieldIds[1], cropId: cropIds[1], date: new Date(now.getTime() - 2 * day).toISOString(), qty: 150, unit: 'kg', price: 28, buyer: demoT('demo.sale.buyer2'), notes: demoT('demo.sale.note2'), }, { harvestId: harvestIds[2] ?? null, fieldId: fieldIds[2], cropId: cropIds[2], date: new Date(now.getTime() - day).toISOString(), qty: 25, unit: 'kg', price: 40, buyer: demoT('demo.sale.buyer3'), notes: demoT('demo.sale.note3'), }, ]; const extraSaleCount = Math.min(harvestIds.length, 120); for (let i = 0; i < extraSaleCount; i += 1) { saleRows.push({ harvestId: harvestIds[i] ?? null, fieldId: fieldIds[i % fieldIds.length], cropId: cropIds[i % cropIds.length], date: new Date(now.getTime() - (3 + i) * day).toISOString(), qty: 20 + i * 8, unit: 'kg', price: 25 + i * 2, buyer: [demoT('demo.sale.buyer1'), demoT('demo.sale.buyer2'), demoT('demo.sale.buyer3')][i % 3], notes: [demoT('demo.sale.note1'), demoT('demo.sale.note2'), demoT('demo.sale.note3')][i % 3], }); } for (const sale of saleRows) { if (!sale.fieldId || !sale.cropId) continue; await db.runAsync( 'INSERT INTO sales (harvest_id, field_id, crop_id, sold_at, quantity, unit, price, buyer, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);', sale.harvestId, sale.fieldId, sale.cropId, sale.date, sale.qty, sale.unit, sale.price, sale.buyer, sale.notes, nowIso ); } const costRows = [ { fieldId: fieldIds[0], cropId: cropIds[0], category: demoT('costs.category.seed'), amount: 1200, vendor: demoT('demo.cost.vendor1'), notes: demoT('demo.cost.note1'), date: new Date(now.getTime() - 6 * day).toISOString(), }, { fieldId: fieldIds[1], cropId: cropIds[1], category: demoT('costs.category.fertilizer'), amount: 950, vendor: demoT('demo.cost.vendor2'), notes: demoT('demo.cost.note2'), date: new Date(now.getTime() - 4 * day).toISOString(), }, { fieldId: fieldIds[2], cropId: cropIds[2], category: demoT('costs.category.labor'), amount: 600, vendor: demoT('demo.cost.vendor3'), notes: demoT('demo.cost.note3'), date: new Date(now.getTime() - 2 * day).toISOString(), }, ]; const extraCostCount = 140; const costCategories = [ demoT('costs.category.seed'), demoT('costs.category.fertilizer'), demoT('costs.category.labor'), demoT('costs.category.fuel'), demoT('costs.category.equipment'), demoT('costs.category.transport'), demoT('costs.category.misc'), ]; for (let i = 0; i < extraCostCount; i += 1) { costRows.push({ fieldId: fieldIds[i % fieldIds.length], cropId: cropIds[i % cropIds.length], category: costCategories[i % costCategories.length], amount: 250 + i * 75, vendor: [demoT('demo.cost.vendor1'), demoT('demo.cost.vendor2'), demoT('demo.cost.vendor3')][i % 3], notes: [demoT('demo.cost.note1'), demoT('demo.cost.note2'), demoT('demo.cost.note3')][i % 3], date: new Date(now.getTime() - (8 + i) * day).toISOString(), }); } for (const cost of costRows) { if (!cost.fieldId) continue; await db.runAsync( 'INSERT INTO costs (field_id, crop_id, category, amount, currency, vendor, notes, spent_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);', cost.fieldId, cost.cropId ?? null, cost.category, cost.amount, profile.currency || 'THB', cost.vendor, cost.notes, cost.date, nowIso ); } showSnackbar(t('setup.demoInserted')); } catch (error) { showSnackbar(`${t('setup.demoError')} ${String(error)}`); } } async function handleClearDemoData() { try { await initCoreTables(); const db = await dbPromise; demoBackupRef.current = await fetchDemoBackup(db); await db.execAsync( [ 'DELETE FROM ml_inferences;', 'DELETE FROM images;', 'DELETE FROM observations;', 'DELETE FROM crops;', 'DELETE FROM sales;', 'DELETE FROM harvests;', 'DELETE FROM costs;', 'DELETE FROM daily_task_entries;', 'DELETE FROM daily_tasks;', 'DELETE FROM fields;', ].join('\n') ); showSnackbar(t('setup.demoClearedUndo')); } catch (error) { showSnackbar(`${t('setup.demoClearError')} ${String(error)}`); } } async function handlePickPhoto() { const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: getImageMediaTypes(), quality: 1, }); if (result.canceled) return; const asset = result.assets[0]; setProfile((prev) => ({ ...prev, photoUri: asset.uri })); } async function handleExportData() { try { await initCoreTables(); const db = await dbPromise; const backup = await fetchDemoBackup(db); const payload = JSON.stringify(backup, null, 2); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const fileUri = `${FileSystem.documentDirectory}smartfarming-export-${timestamp}.json`; await FileSystem.writeAsStringAsync(fileUri, payload); const canShare = await Sharing.isAvailableAsync(); if (canShare) { await Sharing.shareAsync(fileUri, { mimeType: 'application/json', dialogTitle: t('setup.exportTitle'), UTI: 'public.json', }); showSnackbar(t('setup.exported')); } else { showSnackbar(`${t('setup.exported')} ${fileUri}`); } } catch (error) { showSnackbar(`${t('setup.exportError')} ${String(error)}`); } } function updateLanguage(nextLanguage: 'en' | 'th' | 'ja') { setLanguage(nextLanguage); setProfile((prev) => ({ ...prev, language: nextLanguage })); } function showSnackbar(message: string) { setSnackbarMessage(message); setSnackbarVisible(true); if (snackbarTimerRef.current) { clearTimeout(snackbarTimerRef.current); } snackbarTimerRef.current = setTimeout(() => { setSnackbarVisible(false); demoBackupRef.current = null; }, 5000); } async function handleUndoClear() { const backup = demoBackupRef.current; if (!backup) return; try { const db = await dbPromise; await restoreDemoBackup(db, backup); showSnackbar(t('setup.demoUndoDone')); } catch (error) { showSnackbar(`${t('setup.demoUndoError')} ${String(error)}`); } finally { demoBackupRef.current = null; setSnackbarVisible(false); if (snackbarTimerRef.current) { clearTimeout(snackbarTimerRef.current); } } } const inputStyle = [ styles.input, { borderColor: palette.border, backgroundColor: palette.input, color: palette.text }, ]; return ( }> {t('setup.title')} {t('setup.profile')} {status && status !== t('setup.saved') && status !== t('setup.loaded') ? ( {status} ) : null} {t('setup.photo')} {profile.photoUri ? ( ) : ( {t('setup.noPhoto')} )} {t('setup.name')} setProfile((prev) => ({ ...prev, name }))} placeholder={t('setup.name')} style={inputStyle} /> {t('setup.farmName')} setProfile((prev) => ({ ...prev, farmName }))} placeholder={t('setup.farmName')} style={inputStyle} /> {t('setup.location')} setProfile((prev) => ({ ...prev, location }))} placeholder={t('setup.location')} style={inputStyle} /> {t('setup.language')} updateLanguage('en')} variant={profile.language === 'en' ? 'primary' : 'secondary'} /> updateLanguage('th')} variant={profile.language === 'th' ? 'primary' : 'secondary'} /> updateLanguage('ja')} variant={profile.language === 'ja' ? 'primary' : 'secondary'} /> {t('setup.currency')} {currencyPresets.map((code) => { const isActive = profile.currency === code; return ( setProfile((prev) => ({ ...prev, currency: code }))} style={[ styles.currencyChip, { borderColor: palette.border }, isActive && { borderColor: palette.success, backgroundColor: palette.surface, }, ]}> {t(`setup.currency.${code.toLowerCase()}`)} ); })} setProfile((prev) => ({ ...prev, currency }))} placeholder={t('setup.currencyPlaceholder')} style={inputStyle} /> {t('setup.exportTitle')} {t('setup.exportHint')} {t('setup.demoTitle')} {t('setup.demoHint')} {saveIndicator ? ( {saveIndicator} ) : null} {snackbarVisible ? ( {snackbarMessage} {t('setup.demoUndo')} ) : null} ); } function getImageMediaTypes() { const mediaType = (ImagePicker as { MediaType?: { Image?: unknown; Images?: unknown } }) .MediaType; return mediaType?.Image ?? mediaType?.Images ?? ['images']; } function buildCsvExport(backup: DemoBackup) { const header = 'table,data_json'; const rows: string[] = [header]; const tables: Array<{ name: string; rows: unknown[] }> = [ { name: 'user_profile', rows: backup.user_profile ?? [] }, { name: 'fields', rows: backup.fields }, { name: 'crops', rows: backup.crops }, { name: 'observations', rows: backup.observations }, { name: 'images', rows: backup.images }, { name: 'ml_inferences', rows: backup.ml_inferences }, { name: 'daily_tasks', rows: backup.daily_tasks }, { name: 'daily_task_entries', rows: backup.daily_task_entries }, { name: 'harvests', rows: backup.harvests }, { name: 'sales', rows: backup.sales }, { name: 'costs', rows: backup.costs }, ]; for (const table of tables) { for (const row of table.rows) { const json = JSON.stringify(row); rows.push(`${csvEscape(table.name)},${csvEscape(json)}`); } } return rows.join('\n'); } function csvEscape(value: string) { const escaped = value.replace(/"/g, '""'); return `"${escaped}"`; } const styles = StyleSheet.create({ screen: { flex: 1, }, keyboardAvoid: { flex: 1, }, keyboardSpacer: { height: 80, }, headerImage: { width: '100%', height: '100%', }, titleContainer: { flexDirection: 'row', gap: 8, }, section: { gap: 8, marginBottom: 16, }, languageRow: { flexDirection: 'row', gap: 12, }, currencyRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, }, currencyChip: { borderRadius: 999, borderWidth: 1, borderColor: '#C6C6C6', paddingHorizontal: 10, paddingVertical: 4, }, currencyChipActive: { borderColor: '#2F7D4F', backgroundColor: '#E7F3EA', }, currencyText: { fontSize: 12, }, currencyTextActive: { fontSize: 12, color: '#2F7D4F', fontWeight: '600', }, demoRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 12, }, snackbar: { position: 'absolute', left: 16, right: 16, bottom: 24, backgroundColor: '#2B2B23', borderRadius: 14, paddingHorizontal: 16, paddingVertical: 12, flexDirection: 'row', alignItems: 'center', gap: 12, }, snackbarText: { color: '#FFFFFF', flex: 1, }, snackbarAction: { paddingHorizontal: 10, paddingVertical: 6, borderRadius: 999, borderWidth: 1, borderColor: '#FFFFFF', }, snackbarActionText: { color: '#FFFFFF', fontWeight: '600', fontSize: 12, }, input: { borderRadius: 10, borderWidth: 1, borderColor: '#B9B9B9', paddingHorizontal: 12, paddingVertical: 10, fontSize: 16, }, buttonRow: { paddingTop: 4, }, saveIndicator: { marginTop: 10, alignSelf: 'center', backgroundColor: '#E7F3EA', borderColor: '#2F7D4F', borderWidth: 1, borderRadius: 999, paddingHorizontal: 12, paddingVertical: 6, fontSize: 12, fontWeight: '600', color: '#2F7D4F', }, photo: { height: 140, width: 140, borderRadius: 70, }, photoWrap: { alignItems: 'center', }, }); async function fetchDemoBackup(db: Awaited): Promise { const user_profile = await db.getAllAsync( 'SELECT * FROM user_profile;' ); const fields = await db.getAllAsync('SELECT * FROM fields;'); const crops = await db.getAllAsync('SELECT * FROM crops;'); const observations = await db.getAllAsync( 'SELECT * FROM observations;' ); const images = await db.getAllAsync('SELECT * FROM images;'); const daily_tasks = await db.getAllAsync( 'SELECT * FROM daily_tasks;' ); const daily_task_entries = await db.getAllAsync( 'SELECT * FROM daily_task_entries;' ); const ml_inferences = await db.getAllAsync( 'SELECT * FROM ml_inferences;' ); const harvests = await db.getAllAsync( 'SELECT * FROM harvests;' ); const sales = await db.getAllAsync( 'SELECT * FROM sales;' ); const costs = await db.getAllAsync( 'SELECT * FROM costs;' ); return { user_profile, fields, crops, observations, images, daily_tasks, daily_task_entries, ml_inferences, harvests, sales, costs, }; } async function restoreDemoBackup(db: Awaited, backup: DemoBackup) { for (const field of backup.fields) { await db.runAsync( 'INSERT INTO fields (id, name, area_ha, boundary_geojson, notes, photo_uri, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?);', field.id, field.name, field.area_ha, field.boundary_geojson, field.notes, field.photo_uri, field.created_at, field.updated_at ); } for (const crop of backup.crops) { await db.runAsync( 'INSERT INTO crops (id, field_id, crop_name, variety, planting_date, expected_harvest_date, photo_uri) VALUES (?, ?, ?, ?, ?, ?, ?);', crop.id, crop.field_id, crop.crop_name, crop.variety, crop.planting_date, crop.expected_harvest_date, crop.photo_uri ); } for (const observation of backup.observations) { await db.runAsync( 'INSERT INTO observations (id, field_id, crop_id, obs_type, note, severity, lat, lng, observed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);', observation.id, observation.field_id, observation.crop_id, observation.obs_type, observation.note, observation.severity, observation.lat, observation.lng, observation.observed_at ); } for (const image of backup.images) { await db.runAsync( 'INSERT INTO images (id, observation_id, uri, thumbnail_uri, width, height, created_at) VALUES (?, ?, ?, ?, ?, ?, ?);', image.id, image.observation_id, image.uri, image.thumbnail_uri, image.width, image.height, image.created_at ); } for (const task of backup.daily_tasks) { await db.runAsync( 'INSERT INTO daily_tasks (id, name, description, default_time, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?);', task.id, task.name, task.description, task.default_time, task.is_active, task.created_at ); } for (const entry of backup.daily_task_entries) { await db.runAsync( 'INSERT INTO daily_task_entries (id, task_id, field_id, notes, status, completed_at, created_at, meta_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?);', entry.id, entry.task_id, entry.field_id, entry.notes, entry.status, entry.completed_at, entry.created_at, entry.meta_json ); } for (const inference of backup.ml_inferences) { await db.runAsync( 'INSERT INTO ml_inferences (id, image_id, model_name, model_version, top_label, top_score, scores_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?);', inference.id, inference.image_id, inference.model_name, inference.model_version, inference.top_label, inference.top_score, inference.scores_json, inference.created_at ); } for (const harvest of backup.harvests) { await db.runAsync( 'INSERT INTO harvests (id, field_id, crop_id, harvested_at, quantity, unit, notes, photo_uri, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);', harvest.id, harvest.field_id, harvest.crop_id, harvest.harvested_at, harvest.quantity, harvest.unit, harvest.notes, harvest.photo_uri, harvest.created_at ); } for (const sale of backup.sales) { await db.runAsync( 'INSERT INTO sales (id, harvest_id, field_id, crop_id, sold_at, quantity, unit, price, buyer, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);', sale.id, sale.harvest_id, sale.field_id, sale.crop_id, sale.sold_at, sale.quantity, sale.unit, sale.price, sale.buyer, sale.notes, sale.created_at ); } for (const cost of backup.costs) { await db.runAsync( 'INSERT INTO costs (id, field_id, crop_id, category, amount, currency, vendor, notes, spent_at, photo_uri, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);', cost.id, cost.field_id, cost.crop_id, cost.category, cost.amount, cost.currency, cost.vendor, cost.notes, cost.spent_at, cost.photo_uri, cost.created_at ); } } async function loadDemoImageUris() { const modules = [ require('@/assets/images/fields.jpg'), require('@/assets/images/crops.jpg'), require('@/assets/images/observations.jpg'), ]; await Asset.loadAsync(modules); const assets = modules.map((mod) => Asset.fromModule(mod)); return assets.map((asset) => asset.localUri ?? asset.uri ?? ''); }