Brak opisu

task-history.tsx 4.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { useEffect, useState } from 'react';
  2. import { ActivityIndicator, FlatList, StyleSheet, View } from 'react-native';
  3. import { Image } from 'expo-image';
  4. import { ThemedText } from '@/components/themed-text';
  5. import { ThemedView } from '@/components/themed-view';
  6. import { Colors, Fonts } from '@/constants/theme';
  7. import { useTranslation } from '@/localization/i18n';
  8. import { dbPromise, initCoreTables } from '@/services/db';
  9. import { useColorScheme } from '@/hooks/use-color-scheme';
  10. type EntryRow = {
  11. id: number;
  12. name: string;
  13. status: string | null;
  14. notes: string | null;
  15. completed_at: string | null;
  16. };
  17. export default function TaskHistoryScreen() {
  18. const { t } = useTranslation();
  19. const theme = useColorScheme() ?? 'light';
  20. const palette = Colors[theme];
  21. const pageSize = 12;
  22. const [entries, setEntries] = useState<EntryRow[]>([]);
  23. const [status, setStatus] = useState(t('tasks.loading'));
  24. const [page, setPage] = useState(1);
  25. const [hasMore, setHasMore] = useState(true);
  26. const [loadingMore, setLoadingMore] = useState(false);
  27. useEffect(() => {
  28. let isActive = true;
  29. async function loadEntries() {
  30. await fetchEntriesPage(1, true, isActive);
  31. }
  32. loadEntries();
  33. return () => {
  34. isActive = false;
  35. };
  36. }, [t]);
  37. async function fetchEntriesPage(pageToLoad: number, replace: boolean, isActive = true) {
  38. try {
  39. await initCoreTables();
  40. const db = await dbPromise;
  41. const rows = await db.getAllAsync<EntryRow>(
  42. `SELECT e.id, t.name, e.status, e.notes, e.completed_at
  43. FROM daily_task_entries e
  44. JOIN daily_tasks t ON t.id = e.task_id
  45. ORDER BY e.completed_at DESC
  46. LIMIT ? OFFSET ?;`,
  47. pageSize,
  48. (pageToLoad - 1) * pageSize
  49. );
  50. if (!isActive) return;
  51. setEntries((prev) => (replace ? rows : [...prev, ...rows]));
  52. setHasMore(rows.length === pageSize);
  53. setPage(pageToLoad);
  54. if (replace) {
  55. setStatus(rows.length === 0 ? t('tasks.historyEmpty') : '');
  56. }
  57. } catch (error) {
  58. if (isActive) setStatus(`Error: ${String(error)}`);
  59. } finally {
  60. if (isActive) setLoadingMore(false);
  61. }
  62. }
  63. async function handleLoadMore() {
  64. if (loadingMore || !hasMore) return;
  65. setLoadingMore(true);
  66. const nextPage = page + 1;
  67. await fetchEntriesPage(nextPage, false);
  68. }
  69. return (
  70. <FlatList
  71. data={entries}
  72. keyExtractor={(item) => String(item.id)}
  73. onEndReached={handleLoadMore}
  74. onEndReachedThreshold={0.4}
  75. renderItem={({ item }) => (
  76. <ThemedView style={[styles.card, { backgroundColor: palette.card, borderColor: palette.border }]}>
  77. <ThemedText type="subtitle">{item.name}</ThemedText>
  78. <ThemedText style={styles.meta}>
  79. {item.completed_at ? formatDate(item.completed_at) : '-'}
  80. </ThemedText>
  81. {item.notes ? <ThemedText>{item.notes}</ThemedText> : null}
  82. </ThemedView>
  83. )}
  84. ItemSeparatorComponent={() => <View style={styles.separator} />}
  85. ListHeaderComponent={
  86. <View>
  87. <ThemedView style={styles.hero}>
  88. <Image source={require('@/assets/images/dailytask.jpg')} style={styles.heroImage} />
  89. </ThemedView>
  90. <ThemedView style={styles.titleContainer}>
  91. <ThemedText type="title" style={{ fontFamily: Fonts.rounded }}>
  92. {t('tasks.historyTitle')}
  93. </ThemedText>
  94. </ThemedView>
  95. {status ? (
  96. <ThemedView style={styles.section}>
  97. <ThemedText>{status}</ThemedText>
  98. </ThemedView>
  99. ) : null}
  100. </View>
  101. }
  102. ListFooterComponent={
  103. <View style={styles.footer}>
  104. {loadingMore ? <ActivityIndicator /> : null}
  105. </View>
  106. }
  107. />
  108. );
  109. }
  110. function formatDate(value: string) {
  111. try {
  112. return new Date(value).toLocaleString();
  113. } catch {
  114. return value;
  115. }
  116. }
  117. const styles = StyleSheet.create({
  118. hero: {
  119. backgroundColor: '#E8E6DA',
  120. aspectRatio: 16 / 9,
  121. width: '100%',
  122. },
  123. heroImage: {
  124. width: '100%',
  125. height: '100%',
  126. },
  127. titleContainer: {
  128. gap: 8,
  129. paddingHorizontal: 16,
  130. paddingVertical: 12,
  131. },
  132. section: {
  133. gap: 8,
  134. marginBottom: 16,
  135. paddingHorizontal: 16,
  136. },
  137. card: {
  138. borderRadius: 12,
  139. borderWidth: 1,
  140. borderColor: '#C6C6C6',
  141. padding: 12,
  142. marginHorizontal: 16,
  143. gap: 6,
  144. backgroundColor: '#FFFFFF',
  145. },
  146. meta: {
  147. opacity: 0.7,
  148. },
  149. separator: {
  150. height: 12,
  151. },
  152. footer: {
  153. height: 24,
  154. },
  155. });