| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- import { useEffect, useState } from 'react';
- import { ActivityIndicator, FlatList, Pressable, StyleSheet, View } from 'react-native';
- import { Image } from 'expo-image';
- import { useLocalSearchParams, useRouter } from 'expo-router';
- import { useNavigation } from '@react-navigation/native';
- import { ThemedText } from '@/components/themed-text';
- import { ThemedView } from '@/components/themed-view';
- import { IconSymbol } from '@/components/ui/icon-symbol';
- import { Colors, Fonts } from '@/constants/theme';
- import { useTranslation } from '@/localization/i18n';
- import { dbPromise, initCoreTables } from '@/services/db';
- import { useColorScheme } from '@/hooks/use-color-scheme';
- type EntryRow = {
- id: number;
- name: string;
- status: string | null;
- notes: string | null;
- completed_at: string | null;
- };
- export default function TaskHistoryScreen() {
- const { t } = useTranslation();
- const router = useRouter();
- const navigation = useNavigation();
- const params = useLocalSearchParams<{ from?: string | string[] }>();
- const theme = useColorScheme() ?? 'light';
- const palette = Colors[theme];
- const fromParam = Array.isArray(params.from) ? params.from[0] : params.from;
- const pageSize = 12;
- const [entries, setEntries] = useState<EntryRow[]>([]);
- const [status, setStatus] = useState(t('tasks.loading'));
- const [page, setPage] = useState(1);
- const [hasMore, setHasMore] = useState(true);
- const [loadingMore, setLoadingMore] = useState(false);
- useEffect(() => {
- navigation.setOptions({
- headerLeft: () => (
- <Pressable
- onPress={() => {
- if (fromParam === 'logbook') {
- router.replace('/logbook');
- return;
- }
- if (fromParam === 'home') {
- router.replace('/');
- return;
- }
- router.back();
- }}
- hitSlop={10}
- style={{ paddingHorizontal: 8 }}>
- <IconSymbol name="chevron.left" size={20} color={palette.text} />
- </Pressable>
- ),
- });
- }, [fromParam, navigation, palette.text, router]);
- useEffect(() => {
- let isActive = true;
- async function loadEntries() {
- await fetchEntriesPage(1, true, isActive);
- }
- loadEntries();
- return () => {
- isActive = false;
- };
- }, [t]);
- async function fetchEntriesPage(pageToLoad: number, replace: boolean, isActive = true) {
- try {
- await initCoreTables();
- const db = await dbPromise;
- const rows = await db.getAllAsync<EntryRow>(
- `SELECT e.id, t.name, e.status, e.notes, e.completed_at
- FROM daily_task_entries e
- JOIN daily_tasks t ON t.id = e.task_id
- ORDER BY e.completed_at DESC
- LIMIT ? OFFSET ?;`,
- pageSize,
- (pageToLoad - 1) * pageSize
- );
- if (!isActive) return;
- setEntries((prev) => (replace ? rows : [...prev, ...rows]));
- setHasMore(rows.length === pageSize);
- setPage(pageToLoad);
- if (replace) {
- setStatus(rows.length === 0 ? t('tasks.historyEmpty') : '');
- }
- } catch (error) {
- if (isActive) setStatus(`Error: ${String(error)}`);
- } finally {
- if (isActive) setLoadingMore(false);
- }
- }
- async function handleLoadMore() {
- if (loadingMore || !hasMore) return;
- setLoadingMore(true);
- const nextPage = page + 1;
- await fetchEntriesPage(nextPage, false);
- }
- return (
- <FlatList
- data={entries}
- keyExtractor={(item) => String(item.id)}
- onEndReached={handleLoadMore}
- onEndReachedThreshold={0.4}
- renderItem={({ item }) => (
- <ThemedView style={[styles.card, { backgroundColor: palette.card, borderColor: palette.border }]}>
- <ThemedText type="subtitle">{item.name}</ThemedText>
- <ThemedText style={styles.meta}>
- {item.completed_at ? formatDate(item.completed_at) : '-'}
- </ThemedText>
- {item.notes ? <ThemedText>{item.notes}</ThemedText> : null}
- </ThemedView>
- )}
- ItemSeparatorComponent={() => <View style={styles.separator} />}
- ListHeaderComponent={
- <View>
- <ThemedView style={styles.hero}>
- <Image source={require('@/assets/images/dailytask.jpg')} style={styles.heroImage} />
- </ThemedView>
- <ThemedView style={styles.titleContainer}>
- <ThemedText type="title" style={{ fontFamily: Fonts.rounded }}>
- {t('tasks.historyTitle')}
- </ThemedText>
- </ThemedView>
- {status ? (
- <ThemedView style={styles.section}>
- <ThemedText>{status}</ThemedText>
- </ThemedView>
- ) : null}
- </View>
- }
- ListFooterComponent={
- <View style={styles.footer}>
- {loadingMore ? <ActivityIndicator /> : null}
- </View>
- }
- />
- );
- }
- function formatDate(value: string) {
- try {
- return new Date(value).toLocaleString();
- } catch {
- return value;
- }
- }
- const styles = StyleSheet.create({
- hero: {
- backgroundColor: '#E8E6DA',
- aspectRatio: 16 / 9,
- width: '100%',
- },
- heroImage: {
- width: '100%',
- height: '100%',
- },
- titleContainer: {
- gap: 8,
- paddingHorizontal: 16,
- paddingVertical: 12,
- },
- section: {
- gap: 8,
- marginBottom: 16,
- paddingHorizontal: 16,
- },
- card: {
- borderRadius: 12,
- borderWidth: 1,
- borderColor: '#C6C6C6',
- padding: 12,
- marginHorizontal: 16,
- gap: 6,
- backgroundColor: '#FFFFFF',
- },
- meta: {
- opacity: 0.7,
- },
- separator: {
- height: 12,
- },
- footer: {
- height: 24,
- },
- });
|