diff --git a/frontend/module/documents/list/DocumentListPage.tsx b/frontend/module/documents/list/DocumentListPage.tsx new file mode 100644 index 0000000..5381073 --- /dev/null +++ b/frontend/module/documents/list/DocumentListPage.tsx @@ -0,0 +1,196 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import Link from "next/link"; + +interface Document { + id: string; + title: string; + status: string; + riskScore: number | null; + uploadedAt: string; +} + +interface PaginatedResponse { + data: Document[]; + total: number; + page: number; + limit: number; +} + +const STATUS_OPTIONS = ["", "PENDING", "PROCESSING", "VERIFIED", "REJECTED"]; +const PAGE_SIZE = 10; + +function StatusBadge({ status }: { status: string }) { + const colors: Record = { + PENDING: "bg-yellow-100 text-yellow-700", + PROCESSING: "bg-blue-100 text-blue-700", + VERIFIED: "bg-green-100 text-green-700", + REJECTED: "bg-red-100 text-red-700", + }; + return ( + + {status} + + ); +} + +function SkeletonRow() { + return ( + + {Array.from({ length: 5 }).map((_, i) => ( + +
+ + ))} + + ); +} + +export default function DocumentListPage() { + const [docs, setDocs] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [statusFilter, setStatusFilter] = useState(""); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchDocs = useCallback(async () => { + setLoading(true); + setError(null); + try { + const params = new URLSearchParams({ + page: String(page), + limit: String(PAGE_SIZE), + ...(statusFilter ? { status: statusFilter } : {}), + }); + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/module/documents?${params}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("access_token")}`, + }, + } + ); + if (!res.ok) throw new Error("Failed to load documents."); + const data: PaginatedResponse = await res.json(); + setDocs(data.data); + setTotal(data.total); + } catch (err) { + setError(err instanceof Error ? err.message : "Unexpected error."); + } finally { + setLoading(false); + } + }, [page, statusFilter]); + + useEffect(() => { + fetchDocs(); + }, [fetchDocs]); + + const totalPages = Math.ceil(total / PAGE_SIZE); + + return ( +
+
+

Documents

+ +
+ + {error && ( +
+

{error}

+ +
+ )} + +
+ + + + + + + + + + + + {loading + ? Array.from({ length: 5 }).map((_, i) => ) + : docs.length === 0 + ? ( + + + + ) + : docs.map((doc) => ( + + + + + + + + ))} + +
TitleStatusRisk ScoreUpload DateActions
+ No documents found.{" "} + + Upload one + +
{doc.title} + {doc.riskScore != null ? doc.riskScore.toFixed(1) : "—"} + + {new Date(doc.uploadedAt).toLocaleDateString()} + + + View + +
+
+ + {totalPages > 1 && ( +
+ + Page {page} of {totalPages} + +
+ + +
+
+ )} +
+ ); +}