Keine Beschreibung

task-history.tsx 5.5KB

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