/** * FilesScreen – list of documents processed by DocuElevate. */ import React, { useCallback, useEffect, useState } from "react"; import { ActivityIndicator, FlatList, Pressable, RefreshControl, StyleSheet, Text, View, } from "react-native"; import type { FileRecord } from "../services/api"; import api from "../services/api"; function formatBytes(bytes: number | null): string { if (bytes === null || bytes === undefined) return "–"; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1024 / 1024).toFixed(1)} MB`; } function formatDate(iso: string): string { try { return new Date(iso).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric", }); } catch { return iso; } } function statusEmoji(status: string): string { const map: Record = { processed: "✅", processing: "⚙️", queued: "⏳", failed: "❌", uploaded: "⬆️", }; return map[status?.toLowerCase()] ?? "📄"; } export default function FilesScreen() { const [files, setFiles] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [error, setError] = useState(null); const fetchFiles = useCallback( async (pageNum: number, replace: boolean) => { try { const data = await api.listFiles(pageNum, 20); if (replace) { setFiles(data); } else { setFiles((prev) => [...prev, ...data]); } setHasMore(data.length === 20); setError(null); } catch (err: unknown) { setError(err instanceof Error ? err.message : "Failed to load files"); } }, [] ); useEffect(() => { (async () => { setLoading(true); await fetchFiles(1, true); setLoading(false); })(); }, [fetchFiles]); const handleRefresh = useCallback(async () => { setRefreshing(true); setPage(1); await fetchFiles(1, true); setRefreshing(false); }, [fetchFiles]); const handleLoadMore = useCallback(async () => { if (!hasMore || loading || refreshing) return; const next = page + 1; setPage(next); await fetchFiles(next, false); }, [fetchFiles, hasMore, loading, page, refreshing]); if (loading) { return ( ); } if (error) { return ( {error} Retry ); } return ( String(item.id)} contentContainerStyle={styles.listContent} renderItem={({ item }) => } refreshControl={ } onEndReached={handleLoadMore} onEndReachedThreshold={0.4} ListEmptyComponent={ 📂 No documents yet. Upload a document from the Upload tab to get started. } ListFooterComponent={ hasMore && files.length > 0 ? ( ) : null } /> ); } function FileRow({ file }: { file: FileRecord }) { return ( {statusEmoji(file.status)} {file.filename} {formatDate(file.created_at)} · {formatBytes(file.file_size)} {file.status} ); } const styles = StyleSheet.create({ list: { flex: 1, backgroundColor: "#f9fafb" }, listContent: { padding: 16 }, center: { flex: 1, alignItems: "center", justifyContent: "center", backgroundColor: "#f9fafb", padding: 24, }, errorText: { color: "#dc2626", fontSize: 15, textAlign: "center", marginBottom: 16 }, retryButton: { backgroundColor: "#1e40af", borderRadius: 8, paddingHorizontal: 24, paddingVertical: 10, }, retryText: { color: "#fff", fontWeight: "600" }, emptyState: { alignItems: "center", paddingTop: 60 }, emptyEmoji: { fontSize: 48, marginBottom: 12 }, emptyText: { fontSize: 16, color: "#374151", marginBottom: 8 }, emptyHint: { fontSize: 13, color: "#6b7280", textAlign: "center", paddingHorizontal: 32, }, }); const rowStyles = StyleSheet.create({ row: { flexDirection: "row", alignItems: "center", backgroundColor: "#fff", borderRadius: 10, padding: 14, marginBottom: 10, shadowColor: "#000", shadowOpacity: 0.04, shadowOffset: { width: 0, height: 2 }, shadowRadius: 4, elevation: 2, }, icon: { fontSize: 22, marginRight: 12 }, info: { flex: 1 }, filename: { fontSize: 14, fontWeight: "600", color: "#111827", marginBottom: 4, }, meta: { fontSize: 12, color: "#6b7280" }, status: { fontSize: 11, color: "#6b7280", fontWeight: "500", textTransform: "capitalize", }, });