import { useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, FlatList, KeyboardAvoidingView, Platform, StyleSheet, TextInput, View, } from 'react-native'; import { Image } from 'expo-image'; import * as ImageManipulator from 'expo-image-manipulator'; import * as ImagePicker from 'expo-image-picker'; import { Link } from 'expo-router'; import { ThemedText } from '@/components/themed-text'; import { ThemedView } from '@/components/themed-view'; import { Colors, Fonts } from '@/constants/theme'; import { ThemedButton } from '@/components/themed-button'; import { useTranslation } from '@/localization/i18n'; import { dbPromise, initCoreTables } from '@/services/db'; import { useColorScheme } from '@/hooks/use-color-scheme'; type TaskRow = { id: number; name: string; description: string | null; entry_id: number | null; status: string | null; notes: string | null; meta_json: string | null; }; export default function DailyTasksScreen() { const { t } = useTranslation(); const theme = useColorScheme() ?? 'light'; const palette = Colors[theme]; const defaultTasks = [ { name: t('tasks.default.fieldCheck'), description: t('tasks.default.fieldCheckDesc'), default_time: '08:00', }, { name: t('tasks.default.scouting'), description: t('tasks.default.scoutingDesc'), default_time: '10:00', }, { name: t('tasks.default.sensors'), description: t('tasks.default.sensorsDesc'), default_time: '15:00', }, ]; const pageSize = 10; const [tasks, setTasks] = useState([]); const [status, setStatus] = useState(t('tasks.loading')); const [notesByTaskId, setNotesByTaskId] = useState>({}); const [photoByTaskId, setPhotoByTaskId] = useState>({}); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const todayKey = useMemo(() => new Date().toISOString().slice(0, 10), []); useEffect(() => { let isActive = true; async function loadTasks() { try { await initCoreTables(); const db = await dbPromise; await ensureDefaultTasks(db, defaultTasks); await fetchTasksPage(1, true, isActive); } catch (error) { if (isActive) setStatus(`Error: ${String(error)}`); } } loadTasks(); return () => { isActive = false; }; }, [t, todayKey]); async function fetchTasksPage(pageToLoad: number, replace: boolean, isActive = true) { try { const db = await dbPromise; const rows = await db.getAllAsync( `SELECT t.id, t.name, t.description, e.id as entry_id, e.status, e.notes, e.meta_json FROM daily_tasks t LEFT JOIN daily_task_entries e ON e.task_id = t.id AND substr(e.completed_at, 1, 10) = ? WHERE t.is_active = 1 ORDER BY t.id ASC LIMIT ? OFFSET ?;`, todayKey, pageSize, (pageToLoad - 1) * pageSize ); if (!isActive) return; setTasks((prev) => (replace ? rows : [...prev, ...rows])); setHasMore(rows.length === pageSize); setPage(pageToLoad); if (replace) { setStatus(rows.length === 0 ? t('tasks.empty') : ''); } setNotesByTaskId((prev) => { const next = replace ? {} : { ...prev }; rows.forEach((row) => { if (row.notes) next[row.id] = row.notes; }); return next; }); setPhotoByTaskId((prev) => { const next = replace ? {} : { ...prev }; rows.forEach((row) => { const meta = parseTaskMeta(row.meta_json); if (meta.photoUri) next[row.id] = meta.photoUri; }); return next; }); } 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 fetchTasksPage(nextPage, false); } async function handleComplete(task: TaskRow) { try { const db = await dbPromise; const now = new Date().toISOString(); const notes = (notesByTaskId[task.id] ?? '').trim(); const metaJson = serializeTaskMeta({ photoUri: photoByTaskId[task.id], }); if (task.entry_id) { await db.runAsync('DELETE FROM daily_task_entries WHERE id = ?;', task.entry_id); } else { await db.runAsync( 'INSERT INTO daily_task_entries (task_id, field_id, notes, status, completed_at, created_at, meta_json) VALUES (?, NULL, ?, ?, ?, ?, ?);', task.id, notes, 'done', now, now, metaJson ); } await fetchTasksPage(1, true); } catch (error) { setStatus(`Error: ${String(error)}`); } } async function handlePickPhoto(task: TaskRow) { const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: getImageMediaTypes(), quality: 1, }); if (result.canceled) return; const asset = result.assets[0]; console.log('[Tasks] Picked photo:', asset.uri); const normalized = await normalizeImageUri(asset.uri); console.log('[Tasks] Normalized photo:', normalized); await persistTaskPhoto(task, normalized, setPhotoByTaskId); } async function handleTakePhoto(task: TaskRow) { try { const permission = await ImagePicker.requestCameraPermissionsAsync(); if (!permission.granted) { setStatus(t('tasks.cameraDenied')); return; } const result = await ImagePicker.launchCameraAsync({ quality: 1, }); if (result.canceled) return; const asset = result.assets[0]; console.log('[Tasks] Captured photo:', asset.uri); const normalized = await normalizeImageUri(asset.uri); console.log('[Tasks] Normalized photo:', normalized); await persistTaskPhoto(task, normalized, setPhotoByTaskId); } catch (error) { setStatus(t('tasks.cameraError')); } } return ( String(item.id)} keyboardShouldPersistTaps="handled" keyboardDismissMode="on-drag" contentContainerStyle={styles.listContent} renderItem={({ item }) => { const isDone = Boolean(item.entry_id); return ( {item.name} {isDone ? t('tasks.done') : t('tasks.pending')} {item.description ? ( {item.description} ) : null} {t('tasks.photo')} {photoByTaskId[item.id] ? ( ) : null} handlePickPhoto(item)} variant="secondary" /> handleTakePhoto(item)} variant="secondary" /> setNotesByTaskId((prev) => ({ ...prev, [item.id]: value })) } placeholder={t('tasks.notePlaceholder')} style={[ styles.input, { borderColor: palette.border, backgroundColor: palette.input, color: palette.text }, ]} multiline /> handleComplete(item)} /> ); }} ItemSeparatorComponent={() => } ListHeaderComponent={ {t('tasks.title')} {t('tasks.subtitle')} {t('tasks.historyTitle')} {status ? ( {status} ) : null} } onEndReached={handleLoadMore} onEndReachedThreshold={0.4} ListFooterComponent={ {loadingMore ? : null} } /> ); } async function ensureDefaultTasks( db: Awaited, tasks: { name: string; description: string; default_time: string }[] ) { const row = await db.getFirstAsync<{ count: number }>( 'SELECT COUNT(*) as count FROM daily_tasks;' ); if (row?.count && row.count > 0) return; const now = new Date().toISOString(); for (const task of tasks) { await db.runAsync( 'INSERT INTO daily_tasks (name, description, default_time, is_active, created_at) VALUES (?, ?, ?, 1, ?);', task.name, task.description, task.default_time, now ); } } async function persistTaskPhoto( task: TaskRow, uri: string, setPhotoByTaskId: (photos: Record) => void ) { setPhotoByTaskId((prev) => ({ ...prev, [task.id]: uri })); if (!task.entry_id) return; const db = await dbPromise; const metaJson = serializeTaskMeta({ photoUri: uri }); await db.runAsync( 'UPDATE daily_task_entries SET meta_json = ? WHERE id = ?;', metaJson, task.entry_id ); } function parseTaskMeta(raw: string | null) { if (!raw) return {} as { photoUri?: string }; try { return JSON.parse(raw) as { photoUri?: string }; } catch { return {} as { photoUri?: string }; } } function serializeTaskMeta(meta: { photoUri?: string }) { if (!meta.photoUri) return null; return JSON.stringify(meta); } function getImageMediaTypes() { const mediaType = (ImagePicker as { MediaType?: { Image?: unknown; Images?: unknown } }) .MediaType; return mediaType?.Image ?? mediaType?.Images ?? ['images']; } async function normalizeImageUri(uri: string) { try { const result = await ImageManipulator.manipulateAsync( uri, [], { compress: 1, format: ImageManipulator.SaveFormat.JPEG } ); return result.uri; } catch { return uri; } } const styles = StyleSheet.create({ hero: { backgroundColor: '#E8E6DA', aspectRatio: 16 / 9, width: '100%', }, heroImage: { width: '100%', height: '100%', }, titleContainer: { gap: 8, paddingHorizontal: 16, paddingVertical: 12, }, subtitle: { opacity: 0.7, }, section: { gap: 8, marginBottom: 16, paddingHorizontal: 16, }, card: { borderRadius: 12, borderWidth: 1, borderColor: '#C6C6C6', padding: 12, marginHorizontal: 16, marginBottom: 12, gap: 8, backgroundColor: '#FFFFFF', }, cardHeader: { flexDirection: 'row', justifyContent: 'space-between', gap: 8, }, description: { opacity: 0.8, }, photoLabel: { opacity: 0.7, }, photoPreview: { height: 160, width: '100%', borderRadius: 12, }, photoRow: { flexDirection: 'row', gap: 8, }, badge: { fontSize: 12, opacity: 0.7, }, taskTitle: { color: '#2F7D4F', }, input: { borderRadius: 10, borderWidth: 1, borderColor: '#B9B9B9', paddingHorizontal: 12, paddingVertical: 10, fontSize: 15, }, buttonRow: { alignSelf: 'flex-start', }, separator: { height: 12, }, footer: { height: 24, }, keyboardAvoid: { flex: 1, }, listContent: { paddingBottom: 80, }, });