Нема описа

index.tsx 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. import { Image } from 'expo-image';
  2. import { Pressable, StyleSheet, View } from 'react-native';
  3. import { useCallback, useState } from 'react';
  4. import { Link, useFocusEffect } from 'expo-router';
  5. import ParallaxScrollView from '@/components/parallax-scroll-view';
  6. import { ThemedText } from '@/components/themed-text';
  7. import { ThemedView } from '@/components/themed-view';
  8. import { ThemedButton } from '@/components/themed-button';
  9. import { IconSymbol } from '@/components/ui/icon-symbol';
  10. import { Colors, Fonts } from '@/constants/theme';
  11. import { useTranslation } from '@/localization/i18n';
  12. import { dbPromise, initCoreTables } from '@/services/db';
  13. import { useColorScheme } from '@/hooks/use-color-scheme';
  14. export default function HomeScreen() {
  15. const { t } = useTranslation();
  16. const theme = useColorScheme() ?? 'light';
  17. const palette = Colors[theme];
  18. const iconColor = palette.tint;
  19. const [counts, setCounts] = useState({
  20. fields: 0,
  21. crops: 0,
  22. observations: 0,
  23. tasks: 0,
  24. history: 0,
  25. harvests: 0,
  26. sales: 0,
  27. costs: 0,
  28. });
  29. useFocusEffect(
  30. useCallback(() => {
  31. let isActive = true;
  32. async function loadCounts() {
  33. try {
  34. await initCoreTables();
  35. const db = await dbPromise;
  36. const fieldCount = await db.getFirstAsync<{ count: number }>(
  37. 'SELECT COUNT(*) as count FROM fields;'
  38. );
  39. const cropCount = await db.getFirstAsync<{ count: number }>(
  40. 'SELECT COUNT(*) as count FROM crops;'
  41. );
  42. const observationCount = await db.getFirstAsync<{ count: number }>(
  43. 'SELECT COUNT(*) as count FROM observations;'
  44. );
  45. const taskCount = await db.getFirstAsync<{ count: number }>(
  46. 'SELECT COUNT(*) as count FROM daily_tasks WHERE is_active = 1;'
  47. );
  48. const historyCount = await db.getFirstAsync<{ count: number }>(
  49. 'SELECT COUNT(*) as count FROM daily_task_entries;'
  50. );
  51. const harvestCount = await db.getFirstAsync<{ count: number }>(
  52. 'SELECT COUNT(*) as count FROM harvests;'
  53. );
  54. const salesCount = await db.getFirstAsync<{ count: number }>(
  55. 'SELECT COUNT(*) as count FROM sales;'
  56. );
  57. const costCount = await db.getFirstAsync<{ count: number }>(
  58. 'SELECT COUNT(*) as count FROM costs;'
  59. );
  60. if (!isActive) return;
  61. setCounts({
  62. fields: fieldCount?.count ?? 0,
  63. crops: cropCount?.count ?? 0,
  64. observations: observationCount?.count ?? 0,
  65. tasks: taskCount?.count ?? 0,
  66. history: historyCount?.count ?? 0,
  67. harvests: harvestCount?.count ?? 0,
  68. sales: salesCount?.count ?? 0,
  69. costs: costCount?.count ?? 0,
  70. });
  71. } catch {
  72. if (!isActive) return;
  73. setCounts((prev) => prev);
  74. }
  75. }
  76. loadCounts();
  77. return () => {
  78. isActive = false;
  79. };
  80. }, [])
  81. );
  82. return (
  83. <ParallaxScrollView
  84. headerBackgroundColor={{ light: '#E7E1D6', dark: '#1E241D' }}
  85. headerImage={
  86. <Image
  87. source={require('@/assets/images/home.jpg')}
  88. style={styles.heroImage}
  89. contentFit="cover"
  90. />
  91. }>
  92. <ThemedView style={styles.heroCopy}>
  93. <View
  94. style={[
  95. styles.heroBadge,
  96. { borderColor: palette.border, backgroundColor: palette.surface },
  97. ]}>
  98. <IconSymbol name="leaf.fill" size={16} color={iconColor} />
  99. <ThemedText style={styles.heroBadgeText}>{t('home.badge')}</ThemedText>
  100. </View>
  101. <ThemedText type="title" style={styles.heroTitle}>
  102. {t('home.title')}
  103. </ThemedText>
  104. <ThemedText style={styles.heroSubtitle}>
  105. {t('home.subtitle')}
  106. </ThemedText>
  107. <View style={styles.heroActions}>
  108. <Link href="/logbook" asChild>
  109. <ThemedButton title={t('home.openLogbook')} />
  110. </Link>
  111. <Link href="/tasks" asChild>
  112. <ThemedButton title={t('home.todayTasks')} variant="secondary" />
  113. </Link>
  114. </View>
  115. </ThemedView>
  116. <ThemedView style={styles.section}>
  117. <ThemedText type="subtitle">{t('home.quickActions')}</ThemedText>
  118. <View style={styles.actionGrid}>
  119. <Link href="/fields" asChild>
  120. <Pressable
  121. style={[
  122. styles.actionCard,
  123. { borderColor: palette.border, backgroundColor: palette.card },
  124. theme === 'light' ? styles.cardLightShadow : null,
  125. ]}>
  126. <IconSymbol name="map.fill" size={22} color={iconColor} />
  127. <View style={styles.actionHeader}>
  128. <ThemedText style={styles.actionTitle}>{t('home.fields')}</ThemedText>
  129. <View
  130. style={[
  131. styles.countBadge,
  132. { borderColor: palette.border, backgroundColor: palette.surface },
  133. ]}>
  134. <ThemedText style={styles.countText}>{counts.fields}</ThemedText>
  135. </View>
  136. </View>
  137. <ThemedText style={styles.actionHint}>{t('home.fieldsHint')}</ThemedText>
  138. </Pressable>
  139. </Link>
  140. <Link href="/crops" asChild>
  141. <Pressable
  142. style={[
  143. styles.actionCard,
  144. { borderColor: palette.border, backgroundColor: palette.card },
  145. theme === 'light' ? styles.cardLightShadow : null,
  146. ]}>
  147. <IconSymbol name="leaf.circle.fill" size={22} color={iconColor} />
  148. <View style={styles.actionHeader}>
  149. <ThemedText style={styles.actionTitle}>{t('home.crops')}</ThemedText>
  150. <View
  151. style={[
  152. styles.countBadge,
  153. { borderColor: palette.border, backgroundColor: palette.surface },
  154. ]}>
  155. <ThemedText style={styles.countText}>{counts.crops}</ThemedText>
  156. </View>
  157. </View>
  158. <ThemedText style={styles.actionHint}>{t('home.cropsHint')}</ThemedText>
  159. </Pressable>
  160. </Link>
  161. <Link href="/observations" asChild>
  162. <Pressable
  163. style={[
  164. styles.actionCard,
  165. { borderColor: palette.border, backgroundColor: palette.card },
  166. theme === 'light' ? styles.cardLightShadow : null,
  167. ]}>
  168. <IconSymbol name="eye.fill" size={22} color={iconColor} />
  169. <View style={styles.actionHeader}>
  170. <ThemedText style={styles.actionTitle}>{t('home.observations')}</ThemedText>
  171. <View
  172. style={[
  173. styles.countBadge,
  174. { borderColor: palette.border, backgroundColor: palette.surface },
  175. ]}>
  176. <ThemedText style={styles.countText}>{counts.observations}</ThemedText>
  177. </View>
  178. </View>
  179. <ThemedText style={styles.actionHint}>{t('home.observationsHint')}</ThemedText>
  180. </Pressable>
  181. </Link>
  182. <Link href="/onnx" asChild>
  183. <Pressable
  184. style={[
  185. styles.actionCard,
  186. { borderColor: palette.border, backgroundColor: palette.card },
  187. theme === 'light' ? styles.cardLightShadow : null,
  188. ]}>
  189. <IconSymbol name="sparkles" size={22} color={iconColor} />
  190. <ThemedText style={styles.actionTitle}>{t('home.onnx')}</ThemedText>
  191. <ThemedText style={styles.actionHint}>{t('home.onnxHint')}</ThemedText>
  192. </Pressable>
  193. </Link>
  194. <Link href="/harvests" asChild>
  195. <Pressable
  196. style={[
  197. styles.actionCard,
  198. { borderColor: palette.border, backgroundColor: palette.card },
  199. theme === 'light' ? styles.cardLightShadow : null,
  200. ]}>
  201. <IconSymbol name="tray.full.fill" size={22} color={iconColor} />
  202. <View style={styles.actionHeader}>
  203. <ThemedText style={styles.actionTitle}>{t('home.harvests')}</ThemedText>
  204. <View
  205. style={[
  206. styles.countBadge,
  207. { borderColor: palette.border, backgroundColor: palette.surface },
  208. ]}>
  209. <ThemedText style={styles.countText}>{counts.harvests}</ThemedText>
  210. </View>
  211. </View>
  212. <ThemedText style={styles.actionHint}>{t('home.harvestsHint')}</ThemedText>
  213. </Pressable>
  214. </Link>
  215. <Link href="/sales" asChild>
  216. <Pressable
  217. style={[
  218. styles.actionCard,
  219. { borderColor: palette.border, backgroundColor: palette.card },
  220. theme === 'light' ? styles.cardLightShadow : null,
  221. ]}>
  222. <IconSymbol name="cart.fill" size={22} color={iconColor} />
  223. <View style={styles.actionHeader}>
  224. <ThemedText style={styles.actionTitle}>{t('home.sales')}</ThemedText>
  225. <View
  226. style={[
  227. styles.countBadge,
  228. { borderColor: palette.border, backgroundColor: palette.surface },
  229. ]}>
  230. <ThemedText style={styles.countText}>{counts.sales}</ThemedText>
  231. </View>
  232. </View>
  233. <ThemedText style={styles.actionHint}>{t('home.salesHint')}</ThemedText>
  234. </Pressable>
  235. </Link>
  236. <Link href="/costs" asChild>
  237. <Pressable
  238. style={[
  239. styles.actionCard,
  240. { borderColor: palette.border, backgroundColor: palette.card },
  241. theme === 'light' ? styles.cardLightShadow : null,
  242. ]}>
  243. <IconSymbol name="dollarsign.circle.fill" size={22} color={iconColor} />
  244. <View style={styles.actionHeader}>
  245. <ThemedText style={styles.actionTitle}>{t('home.costs')}</ThemedText>
  246. <View
  247. style={[
  248. styles.countBadge,
  249. { borderColor: palette.border, backgroundColor: palette.surface },
  250. ]}>
  251. <ThemedText style={styles.countText}>{counts.costs}</ThemedText>
  252. </View>
  253. </View>
  254. <ThemedText style={styles.actionHint}>{t('home.costsHint')}</ThemedText>
  255. </Pressable>
  256. </Link>
  257. </View>
  258. </ThemedView>
  259. <ThemedView style={styles.section}>
  260. <ThemedText type="subtitle">{t('home.todayTitle')}</ThemedText>
  261. <View
  262. style={[
  263. styles.highlightCard,
  264. { borderColor: palette.border, backgroundColor: palette.surface },
  265. ]}>
  266. <View style={styles.highlightHeader}>
  267. <ThemedText style={styles.highlightTitle}>{t('home.todayCardTitle')}</ThemedText>
  268. <View style={styles.highlightCounts}>
  269. <View
  270. style={[
  271. styles.countBadge,
  272. { borderColor: palette.border, backgroundColor: palette.card },
  273. ]}>
  274. <ThemedText style={styles.countText}>
  275. {t('home.count.tasks')} {counts.tasks}
  276. </ThemedText>
  277. </View>
  278. <View
  279. style={[
  280. styles.countBadge,
  281. { borderColor: palette.border, backgroundColor: palette.card },
  282. ]}>
  283. <ThemedText style={styles.countText}>
  284. {t('home.count.history')} {counts.history}
  285. </ThemedText>
  286. </View>
  287. </View>
  288. </View>
  289. <ThemedText style={styles.highlightBody}>
  290. {t('home.todayCardBody')}
  291. </ThemedText>
  292. <View style={styles.highlightActions}>
  293. <Link href="/tasks" asChild>
  294. <ThemedButton title={t('home.openTasks')} />
  295. </Link>
  296. <Link href="/task-history" asChild>
  297. <ThemedButton title={t('home.taskHistory')} variant="secondary" />
  298. </Link>
  299. </View>
  300. </View>
  301. </ThemedView>
  302. <ThemedView style={styles.section}>
  303. <ThemedText type="subtitle">{t('home.learnAnalyze')}</ThemedText>
  304. <View style={styles.actionGrid}>
  305. <Link href="/blog" asChild>
  306. <Pressable
  307. style={[
  308. styles.actionCardWide,
  309. { borderColor: palette.border, backgroundColor: palette.card },
  310. theme === 'light' ? styles.cardLightShadow : null,
  311. ]}>
  312. <IconSymbol name="doc.text.image" size={22} color={iconColor} />
  313. <View style={styles.actionCopy}>
  314. <ThemedText style={styles.actionTitle}>{t('home.blogs')}</ThemedText>
  315. <ThemedText style={styles.actionHint}>{t('home.blogsHint')}</ThemedText>
  316. </View>
  317. </Pressable>
  318. </Link>
  319. <Link href="/setup" asChild>
  320. <Pressable
  321. style={[
  322. styles.actionCardWide,
  323. { borderColor: palette.border, backgroundColor: palette.card },
  324. theme === 'light' ? styles.cardLightShadow : null,
  325. ]}>
  326. <IconSymbol name="person.crop.circle" size={22} color={iconColor} />
  327. <View style={styles.actionCopy}>
  328. <ThemedText style={styles.actionTitle}>{t('home.profile')}</ThemedText>
  329. <ThemedText style={styles.actionHint}>{t('home.profileHint')}</ThemedText>
  330. </View>
  331. </Pressable>
  332. </Link>
  333. </View>
  334. </ThemedView>
  335. </ParallaxScrollView>
  336. );
  337. }
  338. const styles = StyleSheet.create({
  339. heroImage: {
  340. height: '100%',
  341. width: '100%',
  342. position: 'absolute',
  343. },
  344. heroCopy: {
  345. gap: 12,
  346. paddingHorizontal: 4,
  347. },
  348. heroBadge: {
  349. alignSelf: 'flex-start',
  350. flexDirection: 'row',
  351. gap: 6,
  352. paddingHorizontal: 10,
  353. paddingVertical: 6,
  354. borderRadius: 999,
  355. borderWidth: 1,
  356. borderColor: '#D2C9B8',
  357. backgroundColor: '#F6F1E8',
  358. },
  359. heroBadgeText: {
  360. fontSize: 12,
  361. fontWeight: '600',
  362. },
  363. heroTitle: {
  364. fontFamily: Fonts.rounded,
  365. fontSize: 28,
  366. },
  367. heroSubtitle: {
  368. opacity: 0.8,
  369. fontSize: 15,
  370. lineHeight: 22,
  371. },
  372. heroActions: {
  373. flexDirection: 'row',
  374. flexWrap: 'wrap',
  375. gap: 12,
  376. },
  377. section: {
  378. gap: 12,
  379. },
  380. actionGrid: {
  381. flexDirection: 'row',
  382. flexWrap: 'wrap',
  383. columnGap: 20,
  384. rowGap: 16,
  385. },
  386. actionCard: {
  387. flexBasis: '49%',
  388. flexGrow: 1,
  389. gap: 6,
  390. borderRadius: 14,
  391. borderWidth: 1,
  392. borderColor: '#D9D1C2',
  393. padding: 12,
  394. backgroundColor: '#FBF8F1',
  395. alignItems: 'center',
  396. },
  397. actionCardWide: {
  398. flexDirection: 'row',
  399. alignItems: 'center',
  400. gap: 12,
  401. borderRadius: 14,
  402. borderWidth: 1,
  403. borderColor: '#D9D1C2',
  404. padding: 12,
  405. backgroundColor: '#FBF8F1',
  406. flexBasis: '100%',
  407. },
  408. actionCopy: {
  409. gap: 4,
  410. },
  411. actionHeader: {
  412. flexDirection: 'row',
  413. alignItems: 'center',
  414. justifyContent: 'space-between',
  415. gap: 8,
  416. },
  417. actionTitle: {
  418. fontSize: 16,
  419. fontWeight: '600',
  420. },
  421. actionHint: {
  422. fontSize: 12,
  423. opacity: 0.7,
  424. },
  425. highlightCard: {
  426. borderRadius: 16,
  427. borderWidth: 1,
  428. borderColor: '#D6CCB9',
  429. backgroundColor: '#EFE7D6',
  430. padding: 16,
  431. gap: 10,
  432. },
  433. highlightHeader: {
  434. gap: 8,
  435. },
  436. highlightCounts: {
  437. flexDirection: 'row',
  438. flexWrap: 'wrap',
  439. gap: 8,
  440. },
  441. highlightTitle: {
  442. fontSize: 18,
  443. fontWeight: '700',
  444. },
  445. highlightBody: {
  446. fontSize: 14,
  447. lineHeight: 20,
  448. opacity: 0.8,
  449. },
  450. highlightActions: {
  451. flexDirection: 'row',
  452. flexWrap: 'wrap',
  453. gap: 12,
  454. },
  455. countBadge: {
  456. minWidth: 28,
  457. borderRadius: 999,
  458. borderWidth: 1,
  459. borderColor: '#D9D1C2',
  460. backgroundColor: '#F6F1E8',
  461. alignItems: 'center',
  462. justifyContent: 'center',
  463. paddingHorizontal: 8,
  464. paddingVertical: 4,
  465. },
  466. countText: {
  467. fontSize: 11,
  468. fontWeight: '600',
  469. },
  470. cardLightShadow: {
  471. shadowColor: '#1E1A12',
  472. shadowOpacity: 0.08,
  473. shadowRadius: 10,
  474. shadowOffset: { width: 0, height: 6 },
  475. elevation: 2,
  476. },
  477. });