Sin descripción

setup.tsx 42KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349
  1. import { useEffect, useRef, useState } from 'react';
  2. import { KeyboardAvoidingView, Platform, Pressable, StyleSheet, TextInput, View } from 'react-native';
  3. import { Image } from 'expo-image';
  4. import { Asset } from 'expo-asset';
  5. import * as ImagePicker from 'expo-image-picker';
  6. import * as FileSystem from 'expo-file-system/legacy';
  7. import * as Sharing from 'expo-sharing';
  8. import ParallaxScrollView from '@/components/parallax-scroll-view';
  9. import { ThemedText } from '@/components/themed-text';
  10. import { ThemedView } from '@/components/themed-view';
  11. import { IconSymbol } from '@/components/ui/icon-symbol';
  12. import { Colors, Fonts } from '@/constants/theme';
  13. import { createTranslator, useTranslation } from '@/localization/i18n';
  14. import { dbPromise, initCoreTables } from '@/services/db';
  15. import { ThemedButton } from '@/components/themed-button';
  16. import { useColorScheme } from '@/hooks/use-color-scheme';
  17. type Profile = {
  18. name: string;
  19. farmName: string;
  20. location: string;
  21. photoUri: string;
  22. language: 'en' | 'th' | 'ja';
  23. currency: string;
  24. };
  25. const emptyProfile: Profile = {
  26. name: '',
  27. farmName: '',
  28. location: '',
  29. photoUri: '',
  30. language: 'en',
  31. currency: 'THB',
  32. };
  33. type DemoBackup = {
  34. user_profile: Array<{
  35. id: number;
  36. name: string | null;
  37. farm_name: string | null;
  38. location: string | null;
  39. photo_uri: string | null;
  40. language: string | null;
  41. currency: string | null;
  42. created_at: string | null;
  43. updated_at: string | null;
  44. }>;
  45. fields: Array<{
  46. id: number;
  47. name: string | null;
  48. area_ha: number | null;
  49. boundary_geojson: string | null;
  50. notes: string | null;
  51. photo_uri: string | null;
  52. created_at: string | null;
  53. updated_at: string | null;
  54. }>;
  55. crops: Array<{
  56. id: number;
  57. field_id: number | null;
  58. crop_name: string | null;
  59. variety: string | null;
  60. planting_date: string | null;
  61. expected_harvest_date: string | null;
  62. photo_uri: string | null;
  63. }>;
  64. observations: Array<{
  65. id: number;
  66. field_id: number | null;
  67. crop_id: number | null;
  68. obs_type: string | null;
  69. note: string | null;
  70. severity: number | null;
  71. lat: number | null;
  72. lng: number | null;
  73. observed_at: string | null;
  74. }>;
  75. images: Array<{
  76. id: number;
  77. observation_id: number | null;
  78. uri: string | null;
  79. thumbnail_uri: string | null;
  80. width: number | null;
  81. height: number | null;
  82. created_at: string | null;
  83. }>;
  84. daily_tasks: Array<{
  85. id: number;
  86. name: string | null;
  87. description: string | null;
  88. default_time: string | null;
  89. is_active: number | null;
  90. created_at: string | null;
  91. }>;
  92. daily_task_entries: Array<{
  93. id: number;
  94. task_id: number | null;
  95. field_id: number | null;
  96. notes: string | null;
  97. status: string | null;
  98. completed_at: string | null;
  99. created_at: string | null;
  100. meta_json: string | null;
  101. }>;
  102. ml_inferences: Array<{
  103. id: number;
  104. image_id: number | null;
  105. model_name: string | null;
  106. model_version: string | null;
  107. top_label: string | null;
  108. top_score: number | null;
  109. scores_json: string | null;
  110. created_at: string | null;
  111. }>;
  112. harvests: Array<{
  113. id: number;
  114. field_id: number | null;
  115. crop_id: number | null;
  116. harvested_at: string | null;
  117. quantity: number | null;
  118. unit: string | null;
  119. notes: string | null;
  120. photo_uri: string | null;
  121. created_at: string | null;
  122. }>;
  123. sales: Array<{
  124. id: number;
  125. harvest_id: number | null;
  126. field_id: number | null;
  127. crop_id: number | null;
  128. sold_at: string | null;
  129. quantity: number | null;
  130. unit: string | null;
  131. price: number | null;
  132. buyer: string | null;
  133. notes: string | null;
  134. created_at: string | null;
  135. }>;
  136. costs: Array<{
  137. id: number;
  138. field_id: number | null;
  139. crop_id: number | null;
  140. category: string | null;
  141. amount: number | null;
  142. currency: string | null;
  143. vendor: string | null;
  144. notes: string | null;
  145. spent_at: string | null;
  146. photo_uri: string | null;
  147. created_at: string | null;
  148. }>;
  149. };
  150. export default function SetupScreen() {
  151. const { language, setLanguage, t } = useTranslation();
  152. const theme = useColorScheme() ?? 'light';
  153. const palette = Colors[theme];
  154. const currencyPresets = ['THB', 'USD', 'EUR', 'JPY'];
  155. const [profile, setProfile] = useState<Profile>(emptyProfile);
  156. const [status, setStatus] = useState(t('setup.loading'));
  157. const [saveIndicator, setSaveIndicator] = useState('');
  158. const [snackbarVisible, setSnackbarVisible] = useState(false);
  159. const [snackbarMessage, setSnackbarMessage] = useState('');
  160. const demoBackupRef = useRef<DemoBackup | null>(null);
  161. const snackbarTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  162. useEffect(() => {
  163. let isActive = true;
  164. async function init() {
  165. await initCoreTables();
  166. const db = await dbPromise;
  167. const row = await db.getFirstAsync<{
  168. name: string | null;
  169. farm_name: string | null;
  170. location: string | null;
  171. photo_uri: string | null;
  172. language: string | null;
  173. currency: string | null;
  174. }>(
  175. 'SELECT name, farm_name, location, photo_uri, language, currency FROM user_profile WHERE id = 1;'
  176. );
  177. if (!isActive) return;
  178. if (row) {
  179. const storedLanguage =
  180. row.language === 'th' || row.language === 'en' || row.language === 'ja'
  181. ? row.language
  182. : language;
  183. if (storedLanguage !== language) {
  184. setLanguage(storedLanguage);
  185. }
  186. setProfile({
  187. name: row.name ?? '',
  188. farmName: row.farm_name ?? '',
  189. location: row.location ?? '',
  190. photoUri: row.photo_uri ?? '',
  191. language: storedLanguage,
  192. currency: row.currency ?? 'THB',
  193. });
  194. setStatus(t('setup.loaded'));
  195. } else {
  196. setProfile((prev) => ({ ...prev, language, currency: 'THB' }));
  197. setStatus(t('setup.none'));
  198. }
  199. }
  200. init().catch((error) => {
  201. if (isActive) setStatus(`Error: ${String(error)}`);
  202. });
  203. return () => {
  204. isActive = false;
  205. if (snackbarTimerRef.current) {
  206. clearTimeout(snackbarTimerRef.current);
  207. }
  208. };
  209. }, []);
  210. async function handleSave() {
  211. setStatus(t('setup.saving'));
  212. try {
  213. const db = await dbPromise;
  214. const now = new Date().toISOString();
  215. await db.runAsync(
  216. '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), ?), ?);',
  217. profile.name.trim(),
  218. profile.farmName.trim(),
  219. profile.location.trim(),
  220. profile.photoUri,
  221. profile.language,
  222. profile.currency.trim(),
  223. now,
  224. now
  225. );
  226. setStatus(t('setup.saved'));
  227. setSaveIndicator(t('setup.saveIndicator'));
  228. setTimeout(() => setSaveIndicator(''), 2000);
  229. } catch (error) {
  230. setStatus(`Error: ${String(error)}`);
  231. }
  232. }
  233. async function handleInsertDemoData() {
  234. try {
  235. await initCoreTables();
  236. const db = await dbPromise;
  237. const fieldCount = await db.getFirstAsync<{ count: number }>(
  238. 'SELECT COUNT(*) as count FROM fields;'
  239. );
  240. if (fieldCount?.count && fieldCount.count > 0) {
  241. showSnackbar(t('setup.demoExists'));
  242. return;
  243. }
  244. const now = new Date();
  245. const nowIso = now.toISOString();
  246. const day = 24 * 60 * 60 * 1000;
  247. const toDateOnly = (date: Date) => date.toISOString().slice(0, 10);
  248. const demoT = createTranslator(profile.language);
  249. const truncateText = (value: string, max = 160) => {
  250. if (value.length <= max) return value;
  251. return `${value.slice(0, Math.max(0, max - 3)).trimEnd()}...`;
  252. };
  253. const [fieldImageUri, cropImageUri, observationImageUri] = await loadDemoImageUris();
  254. const fieldRows = [
  255. {
  256. name: demoT('demo.field.north'),
  257. area: 2.4,
  258. notes: demoT('demo.field.northNote'),
  259. photoUri: fieldImageUri,
  260. },
  261. {
  262. name: demoT('demo.field.river'),
  263. area: 1.2,
  264. notes: demoT('demo.field.riverNote'),
  265. photoUri: fieldImageUri,
  266. },
  267. {
  268. name: demoT('demo.field.greenhouse'),
  269. area: 0.4,
  270. notes: demoT('demo.field.greenhouseNote'),
  271. photoUri: fieldImageUri,
  272. },
  273. {
  274. name: demoT('demo.field.orchard'),
  275. area: 1.8,
  276. notes: demoT('demo.field.orchardNote'),
  277. photoUri: fieldImageUri,
  278. },
  279. {
  280. name: demoT('demo.field.terrace'),
  281. area: 0.9,
  282. notes: demoT('demo.field.terraceNote'),
  283. photoUri: fieldImageUri,
  284. },
  285. ];
  286. const extraFieldCount = 100;
  287. const fieldSuffixes = [
  288. demoT('demo.field.northNote'),
  289. demoT('demo.field.riverNote'),
  290. demoT('demo.field.greenhouseNote'),
  291. demoT('demo.field.orchardNote'),
  292. demoT('demo.field.terraceNote'),
  293. demoT('demo.observation.scoutingNote'),
  294. demoT('demo.observation.irrigationNote'),
  295. demoT('demo.observation.pestNote'),
  296. demoT('demo.observation.nutrientNote'),
  297. ];
  298. for (let i = 0; i < extraFieldCount; i += 1) {
  299. const seed = fieldRows[i % fieldRows.length];
  300. fieldRows.push({
  301. name: truncateText(`${seed.name} ${fieldSuffixes[i % fieldSuffixes.length]}`),
  302. area: Number((seed.area + (i % 3) * 0.2).toFixed(2)),
  303. notes: truncateText(seed.notes),
  304. photoUri: fieldImageUri,
  305. });
  306. }
  307. const fieldIds: number[] = [];
  308. for (const field of fieldRows) {
  309. const result = await db.runAsync(
  310. 'INSERT INTO fields (name, area_ha, notes, photo_uri, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?);',
  311. field.name,
  312. field.area,
  313. field.notes,
  314. field.photoUri,
  315. nowIso,
  316. nowIso
  317. );
  318. if (result.lastInsertRowId) fieldIds.push(Number(result.lastInsertRowId));
  319. }
  320. const cropTemplates = [
  321. {
  322. name: demoT('demo.crop.tomato'),
  323. variety: demoT('demo.crop.tomatoVariety'),
  324. },
  325. {
  326. name: demoT('demo.crop.rice'),
  327. variety: demoT('demo.crop.riceVariety'),
  328. },
  329. {
  330. name: demoT('demo.crop.lettuce'),
  331. variety: demoT('demo.crop.lettuceVariety'),
  332. },
  333. {
  334. name: demoT('demo.crop.chili'),
  335. variety: demoT('demo.crop.chiliVariety'),
  336. },
  337. {
  338. name: demoT('demo.crop.cabbage'),
  339. variety: demoT('demo.crop.cabbageVariety'),
  340. },
  341. ];
  342. const cropRows = fieldIds.slice(0, cropTemplates.length).map((fieldId, index) => {
  343. const template = cropTemplates[index];
  344. return {
  345. fieldId,
  346. name: template.name,
  347. variety: template.variety,
  348. planting: toDateOnly(new Date(now.getTime() - (30 + index * 7) * day)),
  349. harvest: toDateOnly(new Date(now.getTime() + (25 + index * 5) * day)),
  350. photoUri: cropImageUri,
  351. };
  352. });
  353. const extraCropCount = 120;
  354. const cropSuffixes = [
  355. demoT('demo.observation.scoutingNote'),
  356. demoT('demo.observation.diseaseNote'),
  357. demoT('demo.observation.irrigationNote'),
  358. demoT('demo.observation.pestNote'),
  359. demoT('demo.observation.nutrientNote'),
  360. ];
  361. for (let i = 0; i < extraCropCount; i += 1) {
  362. const template = cropTemplates[i % cropTemplates.length];
  363. cropRows.push({
  364. fieldId: fieldIds[i % fieldIds.length],
  365. name: truncateText(`${template.name} ${cropSuffixes[i % cropSuffixes.length]}`),
  366. variety: truncateText(template.variety),
  367. planting: toDateOnly(new Date(now.getTime() - (10 + i * 3) * day)),
  368. harvest: toDateOnly(new Date(now.getTime() + (20 + i * 4) * day)),
  369. photoUri: cropImageUri,
  370. });
  371. }
  372. const cropIds: number[] = [];
  373. for (const crop of cropRows) {
  374. if (!crop.fieldId) continue;
  375. const result = await db.runAsync(
  376. 'INSERT INTO crops (field_id, crop_name, variety, planting_date, expected_harvest_date, photo_uri) VALUES (?, ?, ?, ?, ?, ?);',
  377. crop.fieldId,
  378. crop.name,
  379. crop.variety,
  380. crop.planting,
  381. crop.harvest,
  382. crop.photoUri
  383. );
  384. if (result.lastInsertRowId) cropIds.push(Number(result.lastInsertRowId));
  385. }
  386. const observationNotes = [
  387. demoT('demo.observation.scoutingNote'),
  388. demoT('demo.observation.diseaseNote'),
  389. demoT('demo.observation.irrigationNote'),
  390. demoT('demo.observation.pestNote'),
  391. demoT('demo.observation.nutrientNote'),
  392. ];
  393. const observationTypes = [
  394. demoT('observations.type.scouting'),
  395. demoT('observations.type.disease'),
  396. demoT('observations.type.irrigation'),
  397. demoT('observations.type.pest'),
  398. demoT('observations.type.nutrients'),
  399. ];
  400. const observationRows = [
  401. {
  402. fieldId: fieldIds[0],
  403. cropId: cropIds[0],
  404. type: demoT('observations.type.scouting'),
  405. note: demoT('demo.observation.scoutingNote'),
  406. severity: 2,
  407. observedAt: new Date(now.getTime() - 2 * day).toISOString(),
  408. },
  409. {
  410. fieldId: fieldIds[1],
  411. cropId: cropIds[1],
  412. type: demoT('observations.type.disease'),
  413. note: demoT('demo.observation.diseaseNote'),
  414. severity: 5,
  415. observedAt: new Date(now.getTime() - day).toISOString(),
  416. },
  417. {
  418. fieldId: fieldIds[2],
  419. cropId: cropIds[2],
  420. type: demoT('observations.type.irrigation'),
  421. note: demoT('demo.observation.irrigationNote'),
  422. severity: null,
  423. observedAt: nowIso,
  424. },
  425. {
  426. fieldId: fieldIds[3],
  427. cropId: cropIds[3],
  428. type: demoT('observations.type.pest'),
  429. note: demoT('demo.observation.pestNote'),
  430. severity: 3,
  431. observedAt: new Date(now.getTime() - 3 * day).toISOString(),
  432. },
  433. {
  434. fieldId: fieldIds[4],
  435. cropId: cropIds[4],
  436. type: demoT('observations.type.nutrients'),
  437. note: demoT('demo.observation.nutrientNote'),
  438. severity: 4,
  439. observedAt: new Date(now.getTime() - 4 * day).toISOString(),
  440. },
  441. ];
  442. const extraObservationCount = 160;
  443. for (let i = 0; i < extraObservationCount; i += 1) {
  444. observationRows.push({
  445. fieldId: fieldIds[i % fieldIds.length],
  446. cropId: cropIds[i % cropIds.length],
  447. type: observationTypes[i % observationTypes.length],
  448. note: observationNotes[i % observationNotes.length],
  449. severity: (i % 5) + 1,
  450. observedAt: new Date(now.getTime() - (i + 5) * day).toISOString(),
  451. });
  452. }
  453. for (const obs of observationRows) {
  454. if (!obs.fieldId) continue;
  455. const result = await db.runAsync(
  456. 'INSERT INTO observations (field_id, crop_id, obs_type, note, severity, observed_at) VALUES (?, ?, ?, ?, ?, ?);',
  457. obs.fieldId,
  458. obs.cropId ?? null,
  459. obs.type,
  460. obs.note,
  461. obs.severity,
  462. obs.observedAt
  463. );
  464. if (observationImageUri && result.lastInsertRowId) {
  465. await db.runAsync(
  466. 'INSERT INTO images (observation_id, uri, created_at) VALUES (?, ?, ?);',
  467. result.lastInsertRowId,
  468. observationImageUri,
  469. obs.observedAt
  470. );
  471. }
  472. }
  473. const taskCount = await db.getFirstAsync<{ count: number }>(
  474. 'SELECT COUNT(*) as count FROM daily_tasks;'
  475. );
  476. if (!taskCount?.count) {
  477. const defaultTasks = [
  478. { name: demoT('tasks.default.fieldCheck'), description: demoT('tasks.default.fieldCheckDesc'), time: '08:00' },
  479. { name: demoT('tasks.default.scouting'), description: demoT('tasks.default.scoutingDesc'), time: '10:00' },
  480. { name: demoT('tasks.default.sensors'), description: demoT('tasks.default.sensorsDesc'), time: '15:00' },
  481. ];
  482. for (const task of defaultTasks) {
  483. await db.runAsync(
  484. 'INSERT INTO daily_tasks (name, description, default_time, is_active, created_at) VALUES (?, ?, ?, 1, ?);',
  485. task.name,
  486. task.description,
  487. task.time,
  488. nowIso
  489. );
  490. }
  491. }
  492. const taskRows = await db.getAllAsync<{ id: number; name: string }>(
  493. 'SELECT id, name FROM daily_tasks WHERE is_active = 1 ORDER BY id ASC LIMIT 3;'
  494. );
  495. const taskNotes = [demoT('demo.task.note'), demoT('demo.task.note2'), demoT('demo.task.note3')];
  496. let entryIndex = 0;
  497. for (let dayOffset = 0; dayOffset < 120; dayOffset += 1) {
  498. for (let i = 0; i < taskRows.length; i += 1) {
  499. const entryTime = new Date(now.getTime() - (dayOffset + i) * day).toISOString();
  500. await db.runAsync(
  501. 'INSERT INTO daily_task_entries (task_id, field_id, notes, status, completed_at, created_at, meta_json) VALUES (?, ?, ?, ?, ?, ?, ?);',
  502. taskRows[i].id,
  503. fieldIds[0] ?? null,
  504. taskNotes[entryIndex % taskNotes.length],
  505. 'done',
  506. entryTime,
  507. entryTime,
  508. null
  509. );
  510. entryIndex += 1;
  511. }
  512. }
  513. const harvestRows = [
  514. {
  515. fieldId: fieldIds[0],
  516. cropId: cropIds[0],
  517. date: new Date(now.getTime() - 5 * day).toISOString(),
  518. qty: 120,
  519. unit: 'kg',
  520. notes: demoT('demo.harvest.note1'),
  521. },
  522. {
  523. fieldId: fieldIds[1],
  524. cropId: cropIds[1],
  525. date: new Date(now.getTime() - 3 * day).toISOString(),
  526. qty: 260,
  527. unit: 'kg',
  528. notes: demoT('demo.harvest.note2'),
  529. },
  530. {
  531. fieldId: fieldIds[2],
  532. cropId: cropIds[2],
  533. date: new Date(now.getTime() - 2 * day).toISOString(),
  534. qty: 40,
  535. unit: 'kg',
  536. notes: demoT('demo.harvest.note3'),
  537. },
  538. ];
  539. const extraHarvestCount = 100;
  540. for (let i = 0; i < extraHarvestCount; i += 1) {
  541. harvestRows.push({
  542. fieldId: fieldIds[i % fieldIds.length],
  543. cropId: cropIds[i % cropIds.length],
  544. date: new Date(now.getTime() - (7 + i) * day).toISOString(),
  545. qty: 30 + i * 12,
  546. unit: 'kg',
  547. notes: [demoT('demo.harvest.note1'), demoT('demo.harvest.note2'), demoT('demo.harvest.note3')][
  548. i % 3
  549. ],
  550. });
  551. }
  552. const harvestIds: number[] = [];
  553. for (const harvest of harvestRows) {
  554. if (!harvest.fieldId || !harvest.cropId) continue;
  555. const result = await db.runAsync(
  556. 'INSERT INTO harvests (field_id, crop_id, harvested_at, quantity, unit, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?);',
  557. harvest.fieldId,
  558. harvest.cropId,
  559. harvest.date,
  560. harvest.qty,
  561. harvest.unit,
  562. harvest.notes,
  563. nowIso
  564. );
  565. if (result.lastInsertRowId) harvestIds.push(Number(result.lastInsertRowId));
  566. }
  567. const saleRows = [
  568. {
  569. harvestId: harvestIds[0] ?? null,
  570. fieldId: fieldIds[0],
  571. cropId: cropIds[0],
  572. date: new Date(now.getTime() - 4 * day).toISOString(),
  573. qty: 80,
  574. unit: 'kg',
  575. price: 35,
  576. buyer: demoT('demo.sale.buyer1'),
  577. notes: demoT('demo.sale.note1'),
  578. },
  579. {
  580. harvestId: harvestIds[1] ?? null,
  581. fieldId: fieldIds[1],
  582. cropId: cropIds[1],
  583. date: new Date(now.getTime() - 2 * day).toISOString(),
  584. qty: 150,
  585. unit: 'kg',
  586. price: 28,
  587. buyer: demoT('demo.sale.buyer2'),
  588. notes: demoT('demo.sale.note2'),
  589. },
  590. {
  591. harvestId: harvestIds[2] ?? null,
  592. fieldId: fieldIds[2],
  593. cropId: cropIds[2],
  594. date: new Date(now.getTime() - day).toISOString(),
  595. qty: 25,
  596. unit: 'kg',
  597. price: 40,
  598. buyer: demoT('demo.sale.buyer3'),
  599. notes: demoT('demo.sale.note3'),
  600. },
  601. ];
  602. const extraSaleCount = Math.min(harvestIds.length, 120);
  603. for (let i = 0; i < extraSaleCount; i += 1) {
  604. saleRows.push({
  605. harvestId: harvestIds[i] ?? null,
  606. fieldId: fieldIds[i % fieldIds.length],
  607. cropId: cropIds[i % cropIds.length],
  608. date: new Date(now.getTime() - (3 + i) * day).toISOString(),
  609. qty: 20 + i * 8,
  610. unit: 'kg',
  611. price: 25 + i * 2,
  612. buyer: [demoT('demo.sale.buyer1'), demoT('demo.sale.buyer2'), demoT('demo.sale.buyer3')][i % 3],
  613. notes: [demoT('demo.sale.note1'), demoT('demo.sale.note2'), demoT('demo.sale.note3')][i % 3],
  614. });
  615. }
  616. for (const sale of saleRows) {
  617. if (!sale.fieldId || !sale.cropId) continue;
  618. await db.runAsync(
  619. 'INSERT INTO sales (harvest_id, field_id, crop_id, sold_at, quantity, unit, price, buyer, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);',
  620. sale.harvestId,
  621. sale.fieldId,
  622. sale.cropId,
  623. sale.date,
  624. sale.qty,
  625. sale.unit,
  626. sale.price,
  627. sale.buyer,
  628. sale.notes,
  629. nowIso
  630. );
  631. }
  632. const costRows = [
  633. {
  634. fieldId: fieldIds[0],
  635. cropId: cropIds[0],
  636. category: demoT('costs.category.seed'),
  637. amount: 1200,
  638. vendor: demoT('demo.cost.vendor1'),
  639. notes: demoT('demo.cost.note1'),
  640. date: new Date(now.getTime() - 6 * day).toISOString(),
  641. },
  642. {
  643. fieldId: fieldIds[1],
  644. cropId: cropIds[1],
  645. category: demoT('costs.category.fertilizer'),
  646. amount: 950,
  647. vendor: demoT('demo.cost.vendor2'),
  648. notes: demoT('demo.cost.note2'),
  649. date: new Date(now.getTime() - 4 * day).toISOString(),
  650. },
  651. {
  652. fieldId: fieldIds[2],
  653. cropId: cropIds[2],
  654. category: demoT('costs.category.labor'),
  655. amount: 600,
  656. vendor: demoT('demo.cost.vendor3'),
  657. notes: demoT('demo.cost.note3'),
  658. date: new Date(now.getTime() - 2 * day).toISOString(),
  659. },
  660. ];
  661. const extraCostCount = 140;
  662. const costCategories = [
  663. demoT('costs.category.seed'),
  664. demoT('costs.category.fertilizer'),
  665. demoT('costs.category.labor'),
  666. demoT('costs.category.fuel'),
  667. demoT('costs.category.equipment'),
  668. demoT('costs.category.transport'),
  669. demoT('costs.category.misc'),
  670. ];
  671. for (let i = 0; i < extraCostCount; i += 1) {
  672. costRows.push({
  673. fieldId: fieldIds[i % fieldIds.length],
  674. cropId: cropIds[i % cropIds.length],
  675. category: costCategories[i % costCategories.length],
  676. amount: 250 + i * 75,
  677. vendor: [demoT('demo.cost.vendor1'), demoT('demo.cost.vendor2'), demoT('demo.cost.vendor3')][i % 3],
  678. notes: [demoT('demo.cost.note1'), demoT('demo.cost.note2'), demoT('demo.cost.note3')][i % 3],
  679. date: new Date(now.getTime() - (8 + i) * day).toISOString(),
  680. });
  681. }
  682. for (const cost of costRows) {
  683. if (!cost.fieldId) continue;
  684. await db.runAsync(
  685. 'INSERT INTO costs (field_id, crop_id, category, amount, currency, vendor, notes, spent_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);',
  686. cost.fieldId,
  687. cost.cropId ?? null,
  688. cost.category,
  689. cost.amount,
  690. profile.currency || 'THB',
  691. cost.vendor,
  692. cost.notes,
  693. cost.date,
  694. nowIso
  695. );
  696. }
  697. showSnackbar(t('setup.demoInserted'));
  698. } catch (error) {
  699. showSnackbar(`${t('setup.demoError')} ${String(error)}`);
  700. }
  701. }
  702. async function handleClearDemoData() {
  703. try {
  704. await initCoreTables();
  705. const db = await dbPromise;
  706. demoBackupRef.current = await fetchDemoBackup(db);
  707. await db.execAsync(
  708. [
  709. 'DELETE FROM ml_inferences;',
  710. 'DELETE FROM images;',
  711. 'DELETE FROM observations;',
  712. 'DELETE FROM crops;',
  713. 'DELETE FROM sales;',
  714. 'DELETE FROM harvests;',
  715. 'DELETE FROM costs;',
  716. 'DELETE FROM daily_task_entries;',
  717. 'DELETE FROM daily_tasks;',
  718. 'DELETE FROM fields;',
  719. ].join('\n')
  720. );
  721. showSnackbar(t('setup.demoClearedUndo'));
  722. } catch (error) {
  723. showSnackbar(`${t('setup.demoClearError')} ${String(error)}`);
  724. }
  725. }
  726. async function handlePickPhoto() {
  727. const result = await ImagePicker.launchImageLibraryAsync({
  728. mediaTypes: getImageMediaTypes(),
  729. quality: 1,
  730. });
  731. if (result.canceled) return;
  732. const asset = result.assets[0];
  733. setProfile((prev) => ({ ...prev, photoUri: asset.uri }));
  734. }
  735. async function handleExportData() {
  736. try {
  737. await initCoreTables();
  738. const db = await dbPromise;
  739. const backup = await fetchDemoBackup(db);
  740. const payload = JSON.stringify(backup, null, 2);
  741. const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  742. const fileUri = `${FileSystem.documentDirectory}smartfarming-export-${timestamp}.json`;
  743. await FileSystem.writeAsStringAsync(fileUri, payload);
  744. const canShare = await Sharing.isAvailableAsync();
  745. if (canShare) {
  746. await Sharing.shareAsync(fileUri, {
  747. mimeType: 'application/json',
  748. dialogTitle: t('setup.exportTitle'),
  749. UTI: 'public.json',
  750. });
  751. showSnackbar(t('setup.exported'));
  752. } else {
  753. showSnackbar(`${t('setup.exported')} ${fileUri}`);
  754. }
  755. } catch (error) {
  756. showSnackbar(`${t('setup.exportError')} ${String(error)}`);
  757. }
  758. }
  759. function updateLanguage(nextLanguage: 'en' | 'th' | 'ja') {
  760. setLanguage(nextLanguage);
  761. setProfile((prev) => ({ ...prev, language: nextLanguage }));
  762. }
  763. function showSnackbar(message: string) {
  764. setSnackbarMessage(message);
  765. setSnackbarVisible(true);
  766. if (snackbarTimerRef.current) {
  767. clearTimeout(snackbarTimerRef.current);
  768. }
  769. snackbarTimerRef.current = setTimeout(() => {
  770. setSnackbarVisible(false);
  771. demoBackupRef.current = null;
  772. }, 5000);
  773. }
  774. async function handleUndoClear() {
  775. const backup = demoBackupRef.current;
  776. if (!backup) return;
  777. try {
  778. const db = await dbPromise;
  779. await restoreDemoBackup(db, backup);
  780. showSnackbar(t('setup.demoUndoDone'));
  781. } catch (error) {
  782. showSnackbar(`${t('setup.demoUndoError')} ${String(error)}`);
  783. } finally {
  784. demoBackupRef.current = null;
  785. setSnackbarVisible(false);
  786. if (snackbarTimerRef.current) {
  787. clearTimeout(snackbarTimerRef.current);
  788. }
  789. }
  790. }
  791. const inputStyle = [
  792. styles.input,
  793. { borderColor: palette.border, backgroundColor: palette.input, color: palette.text },
  794. ];
  795. return (
  796. <View style={styles.screen}>
  797. <KeyboardAvoidingView
  798. behavior={Platform.OS === 'ios' ? 'padding' : undefined}
  799. keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
  800. style={styles.keyboardAvoid}>
  801. <ParallaxScrollView
  802. headerBackgroundColor={{ light: '#E2E9DF', dark: '#262A24' }}
  803. headerImage={
  804. <Image
  805. source={require('@/assets/images/userprofile.jpg')}
  806. style={styles.headerImage}
  807. contentFit="cover"
  808. />
  809. }>
  810. <ThemedView style={styles.titleContainer}>
  811. <ThemedText type="title" style={{ fontFamily: Fonts.rounded }}>
  812. {t('setup.title')}
  813. </ThemedText>
  814. </ThemedView>
  815. <ThemedView style={styles.section}>
  816. <ThemedText type="subtitle">{t('setup.profile')}</ThemedText>
  817. {status && status !== t('setup.saved') && status !== t('setup.loaded') ? (
  818. <ThemedText>{status}</ThemedText>
  819. ) : null}
  820. </ThemedView>
  821. <ThemedView style={styles.section}>
  822. <ThemedText>{t('setup.photo')}</ThemedText>
  823. <View style={styles.photoWrap}>
  824. {profile.photoUri ? (
  825. <Image source={{ uri: profile.photoUri }} style={styles.photo} contentFit="cover" />
  826. ) : (
  827. <ThemedText>{t('setup.noPhoto')}</ThemedText>
  828. )}
  829. </View>
  830. <View style={styles.buttonRow}>
  831. <ThemedButton title={t('setup.uploadPhoto')} onPress={handlePickPhoto} />
  832. </View>
  833. </ThemedView>
  834. <ThemedView style={styles.section}>
  835. <ThemedText>{t('setup.name')}</ThemedText>
  836. <TextInput
  837. value={profile.name}
  838. onChangeText={(name) => setProfile((prev) => ({ ...prev, name }))}
  839. placeholder={t('setup.name')}
  840. style={inputStyle}
  841. />
  842. </ThemedView>
  843. <ThemedView style={styles.section}>
  844. <ThemedText>{t('setup.farmName')}</ThemedText>
  845. <TextInput
  846. value={profile.farmName}
  847. onChangeText={(farmName) => setProfile((prev) => ({ ...prev, farmName }))}
  848. placeholder={t('setup.farmName')}
  849. style={inputStyle}
  850. />
  851. </ThemedView>
  852. <ThemedView style={styles.section}>
  853. <ThemedText>{t('setup.location')}</ThemedText>
  854. <TextInput
  855. value={profile.location}
  856. onChangeText={(location) => setProfile((prev) => ({ ...prev, location }))}
  857. placeholder={t('setup.location')}
  858. style={inputStyle}
  859. />
  860. </ThemedView>
  861. <ThemedView style={styles.section}>
  862. <ThemedText>{t('setup.language')}</ThemedText>
  863. <View style={styles.languageRow}>
  864. <ThemedButton
  865. title={t('setup.lang.en')}
  866. onPress={() => updateLanguage('en')}
  867. variant={profile.language === 'en' ? 'primary' : 'secondary'}
  868. />
  869. <ThemedButton
  870. title={t('setup.lang.th')}
  871. onPress={() => updateLanguage('th')}
  872. variant={profile.language === 'th' ? 'primary' : 'secondary'}
  873. />
  874. <ThemedButton
  875. title={t('setup.lang.ja')}
  876. onPress={() => updateLanguage('ja')}
  877. variant={profile.language === 'ja' ? 'primary' : 'secondary'}
  878. />
  879. </View>
  880. </ThemedView>
  881. <ThemedView style={styles.section}>
  882. <ThemedText>{t('setup.currency')}</ThemedText>
  883. <View style={styles.currencyRow}>
  884. {currencyPresets.map((code) => {
  885. const isActive = profile.currency === code;
  886. return (
  887. <Pressable
  888. key={code}
  889. onPress={() => setProfile((prev) => ({ ...prev, currency: code }))}
  890. style={[
  891. styles.currencyChip,
  892. { borderColor: palette.border },
  893. isActive && {
  894. borderColor: palette.success,
  895. backgroundColor: palette.surface,
  896. },
  897. ]}>
  898. <ThemedText style={isActive ? styles.currencyTextActive : styles.currencyText}>
  899. {t(`setup.currency.${code.toLowerCase()}`)}
  900. </ThemedText>
  901. </Pressable>
  902. );
  903. })}
  904. </View>
  905. <TextInput
  906. value={profile.currency}
  907. onChangeText={(currency) => setProfile((prev) => ({ ...prev, currency }))}
  908. placeholder={t('setup.currencyPlaceholder')}
  909. style={inputStyle}
  910. />
  911. </ThemedView>
  912. <View style={styles.section}>
  913. <ThemedText type="subtitle">{t('setup.exportTitle')}</ThemedText>
  914. <ThemedText>{t('setup.exportHint')}</ThemedText>
  915. <View style={styles.buttonRow}>
  916. <ThemedButton title={t('setup.exportButton')} onPress={handleExportData} />
  917. </View>
  918. </View>
  919. <View style={styles.section}>
  920. <ThemedText type="subtitle">{t('setup.demoTitle')}</ThemedText>
  921. <ThemedText>{t('setup.demoHint')}</ThemedText>
  922. <View style={styles.demoRow}>
  923. <ThemedButton title={t('setup.demoButton')} onPress={handleInsertDemoData} />
  924. <ThemedButton
  925. title={t('setup.demoClearButton')}
  926. onPress={handleClearDemoData}
  927. variant="secondary"
  928. />
  929. </View>
  930. </View>
  931. <View style={styles.buttonRow}>
  932. <ThemedButton title={t('setup.save')} onPress={handleSave} />
  933. {saveIndicator ? (
  934. <ThemedText style={styles.saveIndicator}>{saveIndicator}</ThemedText>
  935. ) : null}
  936. </View>
  937. <View style={styles.keyboardSpacer} />
  938. </ParallaxScrollView>
  939. </KeyboardAvoidingView>
  940. {snackbarVisible ? (
  941. <View style={styles.snackbar}>
  942. <ThemedText style={styles.snackbarText}>{snackbarMessage}</ThemedText>
  943. <Pressable style={styles.snackbarAction} onPress={handleUndoClear}>
  944. <ThemedText style={styles.snackbarActionText}>{t('setup.demoUndo')}</ThemedText>
  945. </Pressable>
  946. </View>
  947. ) : null}
  948. </View>
  949. );
  950. }
  951. function getImageMediaTypes() {
  952. const mediaType = (ImagePicker as { MediaType?: { Image?: unknown; Images?: unknown } })
  953. .MediaType;
  954. return mediaType?.Image ?? mediaType?.Images ?? ['images'];
  955. }
  956. function buildCsvExport(backup: DemoBackup) {
  957. const header = 'table,data_json';
  958. const rows: string[] = [header];
  959. const tables: Array<{ name: string; rows: unknown[] }> = [
  960. { name: 'user_profile', rows: backup.user_profile ?? [] },
  961. { name: 'fields', rows: backup.fields },
  962. { name: 'crops', rows: backup.crops },
  963. { name: 'observations', rows: backup.observations },
  964. { name: 'images', rows: backup.images },
  965. { name: 'ml_inferences', rows: backup.ml_inferences },
  966. { name: 'daily_tasks', rows: backup.daily_tasks },
  967. { name: 'daily_task_entries', rows: backup.daily_task_entries },
  968. { name: 'harvests', rows: backup.harvests },
  969. { name: 'sales', rows: backup.sales },
  970. { name: 'costs', rows: backup.costs },
  971. ];
  972. for (const table of tables) {
  973. for (const row of table.rows) {
  974. const json = JSON.stringify(row);
  975. rows.push(`${csvEscape(table.name)},${csvEscape(json)}`);
  976. }
  977. }
  978. return rows.join('\n');
  979. }
  980. function csvEscape(value: string) {
  981. const escaped = value.replace(/"/g, '""');
  982. return `"${escaped}"`;
  983. }
  984. const styles = StyleSheet.create({
  985. screen: {
  986. flex: 1,
  987. },
  988. keyboardAvoid: {
  989. flex: 1,
  990. },
  991. keyboardSpacer: {
  992. height: 80,
  993. },
  994. headerImage: {
  995. width: '100%',
  996. height: '100%',
  997. },
  998. titleContainer: {
  999. flexDirection: 'row',
  1000. gap: 8,
  1001. },
  1002. section: {
  1003. gap: 8,
  1004. marginBottom: 16,
  1005. },
  1006. languageRow: {
  1007. flexDirection: 'row',
  1008. gap: 12,
  1009. },
  1010. currencyRow: {
  1011. flexDirection: 'row',
  1012. flexWrap: 'wrap',
  1013. gap: 8,
  1014. },
  1015. currencyChip: {
  1016. borderRadius: 999,
  1017. borderWidth: 1,
  1018. borderColor: '#C6C6C6',
  1019. paddingHorizontal: 10,
  1020. paddingVertical: 4,
  1021. },
  1022. currencyChipActive: {
  1023. borderColor: '#2F7D4F',
  1024. backgroundColor: '#E7F3EA',
  1025. },
  1026. currencyText: {
  1027. fontSize: 12,
  1028. },
  1029. currencyTextActive: {
  1030. fontSize: 12,
  1031. color: '#2F7D4F',
  1032. fontWeight: '600',
  1033. },
  1034. demoRow: {
  1035. flexDirection: 'row',
  1036. flexWrap: 'wrap',
  1037. gap: 12,
  1038. },
  1039. snackbar: {
  1040. position: 'absolute',
  1041. left: 16,
  1042. right: 16,
  1043. bottom: 24,
  1044. backgroundColor: '#2B2B23',
  1045. borderRadius: 14,
  1046. paddingHorizontal: 16,
  1047. paddingVertical: 12,
  1048. flexDirection: 'row',
  1049. alignItems: 'center',
  1050. gap: 12,
  1051. },
  1052. snackbarText: {
  1053. color: '#FFFFFF',
  1054. flex: 1,
  1055. },
  1056. snackbarAction: {
  1057. paddingHorizontal: 10,
  1058. paddingVertical: 6,
  1059. borderRadius: 999,
  1060. borderWidth: 1,
  1061. borderColor: '#FFFFFF',
  1062. },
  1063. snackbarActionText: {
  1064. color: '#FFFFFF',
  1065. fontWeight: '600',
  1066. fontSize: 12,
  1067. },
  1068. input: {
  1069. borderRadius: 10,
  1070. borderWidth: 1,
  1071. borderColor: '#B9B9B9',
  1072. paddingHorizontal: 12,
  1073. paddingVertical: 10,
  1074. fontSize: 16,
  1075. },
  1076. buttonRow: {
  1077. paddingTop: 4,
  1078. },
  1079. saveIndicator: {
  1080. marginTop: 10,
  1081. alignSelf: 'center',
  1082. backgroundColor: '#E7F3EA',
  1083. borderColor: '#2F7D4F',
  1084. borderWidth: 1,
  1085. borderRadius: 999,
  1086. paddingHorizontal: 12,
  1087. paddingVertical: 6,
  1088. fontSize: 12,
  1089. fontWeight: '600',
  1090. color: '#2F7D4F',
  1091. },
  1092. photo: {
  1093. height: 140,
  1094. width: 140,
  1095. borderRadius: 70,
  1096. },
  1097. photoWrap: {
  1098. alignItems: 'center',
  1099. },
  1100. });
  1101. async function fetchDemoBackup(db: Awaited<typeof dbPromise>): Promise<DemoBackup> {
  1102. const user_profile = await db.getAllAsync<DemoBackup['user_profile'][number]>(
  1103. 'SELECT * FROM user_profile;'
  1104. );
  1105. const fields = await db.getAllAsync<DemoBackup['fields'][number]>('SELECT * FROM fields;');
  1106. const crops = await db.getAllAsync<DemoBackup['crops'][number]>('SELECT * FROM crops;');
  1107. const observations = await db.getAllAsync<DemoBackup['observations'][number]>(
  1108. 'SELECT * FROM observations;'
  1109. );
  1110. const images = await db.getAllAsync<DemoBackup['images'][number]>('SELECT * FROM images;');
  1111. const daily_tasks = await db.getAllAsync<DemoBackup['daily_tasks'][number]>(
  1112. 'SELECT * FROM daily_tasks;'
  1113. );
  1114. const daily_task_entries = await db.getAllAsync<DemoBackup['daily_task_entries'][number]>(
  1115. 'SELECT * FROM daily_task_entries;'
  1116. );
  1117. const ml_inferences = await db.getAllAsync<DemoBackup['ml_inferences'][number]>(
  1118. 'SELECT * FROM ml_inferences;'
  1119. );
  1120. const harvests = await db.getAllAsync<DemoBackup['harvests'][number]>(
  1121. 'SELECT * FROM harvests;'
  1122. );
  1123. const sales = await db.getAllAsync<DemoBackup['sales'][number]>(
  1124. 'SELECT * FROM sales;'
  1125. );
  1126. const costs = await db.getAllAsync<DemoBackup['costs'][number]>(
  1127. 'SELECT * FROM costs;'
  1128. );
  1129. return {
  1130. user_profile,
  1131. fields,
  1132. crops,
  1133. observations,
  1134. images,
  1135. daily_tasks,
  1136. daily_task_entries,
  1137. ml_inferences,
  1138. harvests,
  1139. sales,
  1140. costs,
  1141. };
  1142. }
  1143. async function restoreDemoBackup(db: Awaited<typeof dbPromise>, backup: DemoBackup) {
  1144. for (const field of backup.fields) {
  1145. await db.runAsync(
  1146. 'INSERT INTO fields (id, name, area_ha, boundary_geojson, notes, photo_uri, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?);',
  1147. field.id,
  1148. field.name,
  1149. field.area_ha,
  1150. field.boundary_geojson,
  1151. field.notes,
  1152. field.photo_uri,
  1153. field.created_at,
  1154. field.updated_at
  1155. );
  1156. }
  1157. for (const crop of backup.crops) {
  1158. await db.runAsync(
  1159. 'INSERT INTO crops (id, field_id, crop_name, variety, planting_date, expected_harvest_date, photo_uri) VALUES (?, ?, ?, ?, ?, ?, ?);',
  1160. crop.id,
  1161. crop.field_id,
  1162. crop.crop_name,
  1163. crop.variety,
  1164. crop.planting_date,
  1165. crop.expected_harvest_date,
  1166. crop.photo_uri
  1167. );
  1168. }
  1169. for (const observation of backup.observations) {
  1170. await db.runAsync(
  1171. 'INSERT INTO observations (id, field_id, crop_id, obs_type, note, severity, lat, lng, observed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);',
  1172. observation.id,
  1173. observation.field_id,
  1174. observation.crop_id,
  1175. observation.obs_type,
  1176. observation.note,
  1177. observation.severity,
  1178. observation.lat,
  1179. observation.lng,
  1180. observation.observed_at
  1181. );
  1182. }
  1183. for (const image of backup.images) {
  1184. await db.runAsync(
  1185. 'INSERT INTO images (id, observation_id, uri, thumbnail_uri, width, height, created_at) VALUES (?, ?, ?, ?, ?, ?, ?);',
  1186. image.id,
  1187. image.observation_id,
  1188. image.uri,
  1189. image.thumbnail_uri,
  1190. image.width,
  1191. image.height,
  1192. image.created_at
  1193. );
  1194. }
  1195. for (const task of backup.daily_tasks) {
  1196. await db.runAsync(
  1197. 'INSERT INTO daily_tasks (id, name, description, default_time, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?);',
  1198. task.id,
  1199. task.name,
  1200. task.description,
  1201. task.default_time,
  1202. task.is_active,
  1203. task.created_at
  1204. );
  1205. }
  1206. for (const entry of backup.daily_task_entries) {
  1207. await db.runAsync(
  1208. 'INSERT INTO daily_task_entries (id, task_id, field_id, notes, status, completed_at, created_at, meta_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?);',
  1209. entry.id,
  1210. entry.task_id,
  1211. entry.field_id,
  1212. entry.notes,
  1213. entry.status,
  1214. entry.completed_at,
  1215. entry.created_at,
  1216. entry.meta_json
  1217. );
  1218. }
  1219. for (const inference of backup.ml_inferences) {
  1220. await db.runAsync(
  1221. 'INSERT INTO ml_inferences (id, image_id, model_name, model_version, top_label, top_score, scores_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?);',
  1222. inference.id,
  1223. inference.image_id,
  1224. inference.model_name,
  1225. inference.model_version,
  1226. inference.top_label,
  1227. inference.top_score,
  1228. inference.scores_json,
  1229. inference.created_at
  1230. );
  1231. }
  1232. for (const harvest of backup.harvests) {
  1233. await db.runAsync(
  1234. 'INSERT INTO harvests (id, field_id, crop_id, harvested_at, quantity, unit, notes, photo_uri, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);',
  1235. harvest.id,
  1236. harvest.field_id,
  1237. harvest.crop_id,
  1238. harvest.harvested_at,
  1239. harvest.quantity,
  1240. harvest.unit,
  1241. harvest.notes,
  1242. harvest.photo_uri,
  1243. harvest.created_at
  1244. );
  1245. }
  1246. for (const sale of backup.sales) {
  1247. await db.runAsync(
  1248. 'INSERT INTO sales (id, harvest_id, field_id, crop_id, sold_at, quantity, unit, price, buyer, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);',
  1249. sale.id,
  1250. sale.harvest_id,
  1251. sale.field_id,
  1252. sale.crop_id,
  1253. sale.sold_at,
  1254. sale.quantity,
  1255. sale.unit,
  1256. sale.price,
  1257. sale.buyer,
  1258. sale.notes,
  1259. sale.created_at
  1260. );
  1261. }
  1262. for (const cost of backup.costs) {
  1263. await db.runAsync(
  1264. 'INSERT INTO costs (id, field_id, crop_id, category, amount, currency, vendor, notes, spent_at, photo_uri, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);',
  1265. cost.id,
  1266. cost.field_id,
  1267. cost.crop_id,
  1268. cost.category,
  1269. cost.amount,
  1270. cost.currency,
  1271. cost.vendor,
  1272. cost.notes,
  1273. cost.spent_at,
  1274. cost.photo_uri,
  1275. cost.created_at
  1276. );
  1277. }
  1278. }
  1279. async function loadDemoImageUris() {
  1280. const modules = [
  1281. require('@/assets/images/fields.jpg'),
  1282. require('@/assets/images/crops.jpg'),
  1283. require('@/assets/images/observations.jpg'),
  1284. ];
  1285. await Asset.loadAsync(modules);
  1286. const assets = modules.map((mod) => Asset.fromModule(mod));
  1287. return assets.map((asset) => asset.localUri ?? asset.uri ?? '');
  1288. }