Aucune description

index.tsx 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. import { Image } from 'expo-image';
  2. import { FlatList, Pressable, StyleSheet, View } from 'react-native';
  3. import { useCallback, useMemo, 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. const quickActions = useMemo(
  30. () => [
  31. {
  32. key: 'fields',
  33. title: t('home.fields'),
  34. hint: t('home.fieldsHint'),
  35. icon: 'map.fill' as const,
  36. href: { pathname: '/logbook/fields', params: { from: 'home' } },
  37. },
  38. {
  39. key: 'crops',
  40. title: t('home.crops'),
  41. hint: t('home.cropsHint'),
  42. icon: 'leaf.circle.fill' as const,
  43. href: { pathname: '/logbook/crops', params: { from: 'home' } },
  44. },
  45. {
  46. key: 'observations',
  47. title: t('home.observations'),
  48. hint: t('home.observationsHint'),
  49. icon: 'eye.fill' as const,
  50. href: { pathname: '/observations', params: { from: 'home' } },
  51. },
  52. {
  53. key: 'harvests',
  54. title: t('home.harvests'),
  55. hint: t('home.harvestsHint'),
  56. icon: 'tray.full.fill' as const,
  57. href: { pathname: '/harvests', params: { from: 'home' } },
  58. },
  59. {
  60. key: 'sales',
  61. title: t('home.sales'),
  62. hint: t('home.salesHint'),
  63. icon: 'cart.fill' as const,
  64. href: { pathname: '/sales', params: { from: 'home' } },
  65. },
  66. {
  67. key: 'costs',
  68. title: t('home.costs'),
  69. hint: t('home.costsHint'),
  70. icon: 'dollarsign.circle.fill' as const,
  71. href: { pathname: '/costs', params: { from: 'home' } },
  72. },
  73. {
  74. key: 'blogs',
  75. title: t('home.blogs'),
  76. hint: t('home.blogsHint'),
  77. icon: 'doc.text.image' as const,
  78. href: '/blog',
  79. },
  80. {
  81. key: 'profile',
  82. title: t('home.profile'),
  83. hint: t('home.profileHint'),
  84. icon: 'person.crop.circle' as const,
  85. href: '/setup',
  86. },
  87. ],
  88. [t]
  89. );
  90. useFocusEffect(
  91. useCallback(() => {
  92. let isActive = true;
  93. async function loadCounts() {
  94. try {
  95. await initCoreTables();
  96. const db = await dbPromise;
  97. const fieldCount = await db.getFirstAsync<{ count: number }>(
  98. 'SELECT COUNT(*) as count FROM fields;'
  99. );
  100. const cropCount = await db.getFirstAsync<{ count: number }>(
  101. 'SELECT COUNT(*) as count FROM crops;'
  102. );
  103. const observationCount = await db.getFirstAsync<{ count: number }>(
  104. 'SELECT COUNT(*) as count FROM observations;'
  105. );
  106. const taskCount = await db.getFirstAsync<{ count: number }>(
  107. 'SELECT COUNT(*) as count FROM daily_tasks WHERE is_active = 1;'
  108. );
  109. const historyCount = await db.getFirstAsync<{ count: number }>(
  110. 'SELECT COUNT(*) as count FROM daily_task_entries;'
  111. );
  112. const harvestCount = await db.getFirstAsync<{ count: number }>(
  113. 'SELECT COUNT(*) as count FROM harvests;'
  114. );
  115. const salesCount = await db.getFirstAsync<{ count: number }>(
  116. 'SELECT COUNT(*) as count FROM sales;'
  117. );
  118. const costCount = await db.getFirstAsync<{ count: number }>(
  119. 'SELECT COUNT(*) as count FROM costs;'
  120. );
  121. if (!isActive) return;
  122. setCounts({
  123. fields: fieldCount?.count ?? 0,
  124. crops: cropCount?.count ?? 0,
  125. observations: observationCount?.count ?? 0,
  126. tasks: taskCount?.count ?? 0,
  127. history: historyCount?.count ?? 0,
  128. harvests: harvestCount?.count ?? 0,
  129. sales: salesCount?.count ?? 0,
  130. costs: costCount?.count ?? 0,
  131. });
  132. } catch {
  133. if (!isActive) return;
  134. setCounts((prev) => prev);
  135. }
  136. }
  137. loadCounts();
  138. return () => {
  139. isActive = false;
  140. };
  141. }, [])
  142. );
  143. return (
  144. <ParallaxScrollView
  145. headerBackgroundColor={{ light: '#E7E1D6', dark: '#1E241D' }}
  146. headerImage={
  147. <Image
  148. source={require('@/assets/images/home.jpg')}
  149. style={styles.heroImage}
  150. contentFit="cover"
  151. />
  152. }>
  153. <ThemedView style={styles.heroCard}>
  154. <View style={[styles.badge, { borderColor: palette.border, backgroundColor: palette.card }]}>
  155. <IconSymbol name="leaf.fill" size={16} color={iconColor} />
  156. <ThemedText style={styles.badgeText}>{t('home.badge')}</ThemedText>
  157. </View>
  158. <ThemedText type="title" style={styles.heroTitle}>
  159. {t('home.title')}
  160. </ThemedText>
  161. <ThemedText style={styles.heroSubtitle}>{t('home.subtitle')}</ThemedText>
  162. <View style={styles.heroActions}>
  163. <Link href="/logbook" asChild>
  164. <ThemedButton title={t('home.openLogbook')} />
  165. </Link>
  166. <Link href={{ pathname: '/tasks', params: { from: 'home' } }} asChild>
  167. <ThemedButton title={t('home.todayTasks')} variant="secondary" />
  168. </Link>
  169. </View>
  170. </ThemedView>
  171. <ThemedView style={styles.section}>
  172. <ThemedText type="subtitle">{t('home.quickActions')}</ThemedText>
  173. <FlatList
  174. data={quickActions}
  175. keyExtractor={(item) => item.key}
  176. horizontal
  177. showsHorizontalScrollIndicator={false}
  178. contentContainerStyle={styles.actionRow}
  179. renderItem={({ item }) => (
  180. <Link href={item.href} asChild>
  181. <Pressable
  182. style={[
  183. styles.actionCard,
  184. { borderColor: palette.border, backgroundColor: palette.card },
  185. theme === 'light' ? styles.cardShadow : null,
  186. ]}>
  187. <View style={styles.actionIconWrap}>
  188. <IconSymbol name={item.icon} size={26} color={iconColor} />
  189. </View>
  190. <ThemedText style={styles.actionTitle}>{item.title}</ThemedText>
  191. <ThemedText style={styles.actionHint}>{item.hint}</ThemedText>
  192. </Pressable>
  193. </Link>
  194. )}
  195. />
  196. </ThemedView>
  197. <ThemedView style={styles.section}>
  198. <ThemedText type="subtitle">{t('home.todayTitle')}</ThemedText>
  199. <View style={[styles.card, { borderColor: palette.border, backgroundColor: palette.surface }]}>
  200. <ThemedText style={styles.cardTitle}>{t('home.todayCardTitle')}</ThemedText>
  201. <View style={styles.countRow}>
  202. <View style={[styles.countBadge, { borderColor: palette.border, backgroundColor: palette.card }]}>
  203. <ThemedText style={styles.countText}>
  204. {t('home.count.tasks')} {counts.tasks}
  205. </ThemedText>
  206. </View>
  207. <View style={[styles.countBadge, { borderColor: palette.border, backgroundColor: palette.card }]}>
  208. <ThemedText style={styles.countText}>
  209. {t('home.count.history')} {counts.history}
  210. </ThemedText>
  211. </View>
  212. </View>
  213. <ThemedText style={styles.cardBody}>{t('home.todayCardBody')}</ThemedText>
  214. <View style={styles.cardActions}>
  215. <Link href={{ pathname: '/tasks', params: { from: 'home' } }} asChild>
  216. <ThemedButton title={t('home.openTasks')} />
  217. </Link>
  218. <Link href={{ pathname: '/task-history', params: { from: 'home' } }} asChild>
  219. <ThemedButton title={t('home.taskHistory')} variant="secondary" />
  220. </Link>
  221. </View>
  222. </View>
  223. </ThemedView>
  224. </ParallaxScrollView>
  225. );
  226. }
  227. const styles = StyleSheet.create({
  228. heroImage: {
  229. height: '100%',
  230. width: '100%',
  231. position: 'absolute',
  232. },
  233. heroCard: {
  234. gap: 12,
  235. paddingHorizontal: 4,
  236. },
  237. badge: {
  238. alignSelf: 'flex-start',
  239. flexDirection: 'row',
  240. gap: 6,
  241. paddingHorizontal: 10,
  242. paddingVertical: 6,
  243. borderRadius: 999,
  244. borderWidth: 1,
  245. },
  246. badgeText: {
  247. fontSize: 12,
  248. fontWeight: '600',
  249. },
  250. heroTitle: {
  251. fontFamily: Fonts.rounded,
  252. fontSize: 28,
  253. },
  254. heroSubtitle: {
  255. opacity: 0.8,
  256. fontSize: 15,
  257. lineHeight: 22,
  258. },
  259. heroActions: {
  260. flexDirection: 'row',
  261. flexWrap: 'wrap',
  262. gap: 12,
  263. },
  264. section: {
  265. gap: 12,
  266. },
  267. actionRow: {
  268. gap: 18,
  269. paddingRight: 4,
  270. },
  271. actionCard: {
  272. width: 180,
  273. minWidth: 180,
  274. maxWidth: 180,
  275. borderRadius: 14,
  276. borderWidth: 1,
  277. padding: 14,
  278. gap: 10,
  279. alignItems: 'center',
  280. },
  281. actionIconWrap: {
  282. width: '100%',
  283. alignItems: 'center',
  284. justifyContent: 'center',
  285. },
  286. actionTitle: {
  287. fontSize: 16,
  288. fontWeight: '600',
  289. textAlign: 'center',
  290. width: '100%',
  291. },
  292. actionHint: {
  293. fontSize: 12,
  294. opacity: 0.7,
  295. textAlign: 'center',
  296. width: '100%',
  297. },
  298. card: {
  299. borderRadius: 16,
  300. borderWidth: 1,
  301. padding: 16,
  302. gap: 10,
  303. },
  304. cardTitle: {
  305. fontSize: 18,
  306. fontWeight: '700',
  307. },
  308. cardBody: {
  309. fontSize: 14,
  310. lineHeight: 20,
  311. opacity: 0.8,
  312. },
  313. countRow: {
  314. flexDirection: 'row',
  315. flexWrap: 'wrap',
  316. gap: 8,
  317. },
  318. countBadge: {
  319. borderRadius: 999,
  320. borderWidth: 1,
  321. alignItems: 'center',
  322. justifyContent: 'center',
  323. paddingHorizontal: 8,
  324. paddingVertical: 4,
  325. },
  326. countText: {
  327. fontSize: 11,
  328. fontWeight: '600',
  329. },
  330. cardActions: {
  331. flexDirection: 'row',
  332. flexWrap: 'wrap',
  333. gap: 12,
  334. },
  335. cardShadow: {
  336. shadowColor: '#1E1A12',
  337. shadowOpacity: 0.08,
  338. shadowRadius: 10,
  339. shadowOffset: { width: 0, height: 6 },
  340. elevation: 2,
  341. },
  342. });