diff --git a/dashboard/app/dashboard/contracts/page.tsx b/dashboard/app/dashboard/contracts/page.tsx index 9d0f27d..0813fd3 100644 --- a/dashboard/app/dashboard/contracts/page.tsx +++ b/dashboard/app/dashboard/contracts/page.tsx @@ -454,27 +454,106 @@ function CopyButton({ text }: { text: string }) { function IdentityLinkGrid() { const [searchInput, setSearchInput] = useState(""); const [committedQuery, setCommittedQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState<"all" | "active" | "pending" | "revoked">("all"); + const [dateFrom, setDateFrom] = useState(""); + const [dateTo, setDateTo] = useState(""); + const [page, setPage] = useState(1); + const [sortKey, setSortKey] = useState<"linked_at" | "display_name" | "status">("linked_at"); + const [sortAsc, setSortAsc] = useState(false); const searchRef = useRef(null); const { data, loading, error, refresh } = usePolling( - () => api.identityLinks({ query: committedQuery || undefined, limit: 50 }), + () => api.identityLinks({ + query: committedQuery || undefined, + limit: 50, + offset: (page - 1) * 50, + }), 60_000, - [committedQuery] + [committedQuery, page] ); const links: IdentityLink[] = data?.links ?? []; + const total = data?.total ?? 0; + const totalPages = Math.max(1, Math.ceil(total / 50)); + + const filteredLinks = useMemo(() => { + let filtered = [...links]; + + if (statusFilter !== "all") { + filtered = filtered.filter((link) => link.status === statusFilter); + } + + if (dateFrom) { + const from = new Date(dateFrom).getTime(); + filtered = filtered.filter((link) => new Date(link.linked_at).getTime() >= from); + } + + if (dateTo) { + const to = new Date(dateTo + "T23:59:59Z").getTime(); + filtered = filtered.filter((link) => new Date(link.linked_at).getTime() <= to); + } + + filtered.sort((a, b) => { + const cmp = sortKey === "linked_at" + ? new Date(a.linked_at).getTime() - new Date(b.linked_at).getTime() + : sortKey === "display_name" + ? (a.display_name || a.user_id).localeCompare(b.display_name || b.user_id) + : a.status.localeCompare(b.status); + return sortAsc ? cmp : -cmp; + }); + + return filtered; + }, [links, statusFilter, dateFrom, dateTo, sortKey, sortAsc]); const handleSearch = (e: React.FormEvent) => { e.preventDefault(); setCommittedQuery(searchInput.trim()); + setPage(1); }; const clearSearch = () => { setSearchInput(""); setCommittedQuery(""); + setPage(1); searchRef.current?.focus(); }; + const handleExportCSV = () => { + const headers = ["User ID", "Privy DID", "Stellar Address", "Display Name", "Email", "Status", "Linked At"]; + const csvRows = [headers.join(",")]; + filteredLinks.forEach((link) => { + csvRows.push([ + link.user_id, + link.privy_did, + link.stellar_address, + link.display_name ?? "", + link.email ?? "", + link.status, + link.linked_at, + ].map((v) => `"${v}"`).join(",")); + }); + const blob = new Blob([csvRows.join("\n")], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `identity-links-${new Date().toISOString().slice(0, 10)}.csv`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + }; + + const clearFilters = () => { + setSearchInput(""); + setCommittedQuery(""); + setStatusFilter("all"); + setDateFrom(""); + setDateTo(""); + setPage(1); + }; + + const hasActiveFilters = committedQuery || statusFilter !== "all" || dateFrom || dateTo; + return (