From 0d98b3c2108a0d22cd6bf7bd31597ba6334f7033 Mon Sep 17 00:00:00 2001 From: Raphael Faboyinde Date: Sat, 28 Mar 2026 04:51:49 +0100 Subject: [PATCH] feat: implement interactive bidirectional sorting columns --- frontend/messages/en.json | 2 +- frontend/messages/es.json | 2 +- frontend/messages/pt.json | 2 +- frontend/src/components/RecentPayments.tsx | 240 ++++++++++++++++-- frontend/tests/e2e/critical-pages-vrt.spec.ts | 3 + frontend/tests/e2e/payments-sort.spec.ts | 129 ++++++++++ 6 files changed, 353 insertions(+), 25 deletions(-) create mode 100644 frontend/tests/e2e/payments-sort.spec.ts diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 245bc0a7..a510cfce 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -169,7 +169,7 @@ "filteredSuffix": "(filters applied)", "tableStatus": "Status", "tableAmount": "Amount", - "tableDescription": "Description", + "tableRecipient": "Recipient", "tableDate": "Date", "tableLink": "Link", "view": "View", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index b607a5b3..bd34a80a 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -169,7 +169,7 @@ "filteredSuffix": "(con filtros)", "tableStatus": "Estado", "tableAmount": "Monto", - "tableDescription": "Descripcion", + "tableRecipient": "Destinatario", "tableDate": "Fecha", "tableLink": "Enlace", "view": "Ver", diff --git a/frontend/messages/pt.json b/frontend/messages/pt.json index ec8cc4ea..e567e429 100644 --- a/frontend/messages/pt.json +++ b/frontend/messages/pt.json @@ -169,7 +169,7 @@ "filteredSuffix": "(com filtros)", "tableStatus": "Status", "tableAmount": "Valor", - "tableDescription": "Descricao", + "tableRecipient": "Destinatario", "tableDate": "Data", "tableLink": "Link", "view": "Ver", diff --git a/frontend/src/components/RecentPayments.tsx b/frontend/src/components/RecentPayments.tsx index 3ee54241..525ec29a 100644 --- a/frontend/src/components/RecentPayments.tsx +++ b/frontend/src/components/RecentPayments.tsx @@ -20,6 +20,7 @@ interface Payment { id: string; amount: string; asset: string; + recipient: string; status: string; description: string | null; created_at: string; @@ -38,6 +39,9 @@ interface FilterState { dateTo: string; } +type SortColumn = "status" | "amount" | "recipient" | "created_at"; +type SortDirection = "asc" | "desc"; + const LIMIT = 100; const STATUS_OPTIONS = ["all", "pending", "confirmed", "failed", "refunded"] as const; const ASSET_OPTIONS = ["all", "XLM", "USDC"] as const; @@ -48,6 +52,8 @@ const DEFAULT_FILTERS: FilterState = { dateFrom: "", dateTo: "", }; +const DEFAULT_SORT_COLUMN: SortColumn = "created_at"; +const DEFAULT_SORT_DIRECTION: SortDirection = "desc"; function toStatusLabel( t: ReturnType, @@ -66,7 +72,36 @@ function filtersFromSearchParams(searchParams: URLSearchParams): FilterState { }; } -function buildSearchParams(filters: FilterState): URLSearchParams { +function isSortColumn(value: string | null): value is SortColumn { + return ( + value === "status" || + value === "amount" || + value === "recipient" || + value === "created_at" + ); +} + +function isSortDirection(value: string | null): value is SortDirection { + return value === "asc" || value === "desc"; +} + +function sortFromSearchParams(searchParams: URLSearchParams) { + const sortColumn = searchParams.get("sortColumn"); + const sortDirection = searchParams.get("sortDirection"); + + return { + sortColumn: isSortColumn(sortColumn) ? sortColumn : DEFAULT_SORT_COLUMN, + sortDirection: isSortDirection(sortDirection) + ? sortDirection + : DEFAULT_SORT_DIRECTION, + }; +} + +function buildSearchParams( + filters: FilterState, + sortColumn: SortColumn, + sortDirection: SortDirection, +): URLSearchParams { const params = new URLSearchParams(); if (filters.search) params.set("search", filters.search); @@ -74,10 +109,31 @@ function buildSearchParams(filters: FilterState): URLSearchParams { if (filters.asset !== "all") params.set("asset", filters.asset); if (filters.dateFrom) params.set("date_from", filters.dateFrom); if (filters.dateTo) params.set("date_to", filters.dateTo); + if (sortColumn !== DEFAULT_SORT_COLUMN) params.set("sortColumn", sortColumn); + if (sortDirection !== DEFAULT_SORT_DIRECTION) { + params.set("sortDirection", sortDirection); + } return params; } +function SortArrow({ + active, + direction, +}: { + active: boolean; + direction: SortDirection; +}) { + return ( + + ); +} + export default function RecentPayments({ showSkeleton = false, }: { @@ -97,6 +153,10 @@ export default function RecentPayments({ () => filtersFromSearchParams(searchParams), [searchParams], ); + const { sortColumn, sortDirection } = useMemo( + () => sortFromSearchParams(searchParams), + [searchParams], + ); const hasActiveFilters = filters.search || filters.status !== "all" || @@ -114,12 +174,20 @@ export default function RecentPayments({ const [flashedIds, setFlashedIds] = useState>(new Set()); const updateFilters = useCallback( - (nextFilters: FilterState) => { - const params = buildSearchParams(nextFilters); + ( + nextFilters: FilterState, + nextSortColumn: SortColumn = sortColumn, + nextSortDirection: SortDirection = sortDirection, + ) => { + const params = buildSearchParams( + nextFilters, + nextSortColumn, + nextSortDirection, + ); const query = params.toString(); router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false }); }, - [pathname, router], + [pathname, router, sortColumn, sortDirection], ); const handleFilterChange = useCallback( @@ -143,6 +211,15 @@ export default function RecentPayments({ updateFilters(DEFAULT_FILTERS); }, [updateFilters]); + const handleSort = useCallback( + (column: SortColumn) => { + const nextDirection = + sortColumn === column && sortDirection === "asc" ? "desc" : "asc"; + updateFilters(filters, column, nextDirection); + }, + [filters, sortColumn, sortDirection, updateFilters], + ); + const handleConfirmed = useCallback( (event: { id: string; @@ -190,7 +267,7 @@ export default function RecentPayments({ } const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"; - const params = buildSearchParams(filters); + const params = buildSearchParams(filters, sortColumn, sortDirection); params.set("page", page.toString()); params.set("limit", LIMIT.toString()); @@ -221,7 +298,47 @@ export default function RecentPayments({ fetchPayments(); return () => controller.abort(); - }, [apiKey, filters, t]); + }, [apiKey, filters, sortColumn, sortDirection, t]); + + const sortedPayments = useMemo(() => { + const statusOrder: Record = { + pending: 0, + confirmed: 1, + completed: 2, + failed: 3, + refunded: 4, + }; + + return [...payments].sort((left, right) => { + let result = 0; + + switch (sortColumn) { + case "amount": + result = Number(left.amount) - Number(right.amount); + break; + case "recipient": + result = left.recipient.localeCompare(right.recipient); + break; + case "status": + result = + (statusOrder[left.status] ?? Number.MAX_SAFE_INTEGER) - + (statusOrder[right.status] ?? Number.MAX_SAFE_INTEGER); + break; + case "created_at": + default: + result = + new Date(left.created_at).getTime() - + new Date(right.created_at).getTime(); + break; + } + + if (result === 0) { + result = left.id.localeCompare(right.id); + } + + return sortDirection === "asc" ? result : -result; + }); + }, [payments, sortColumn, sortDirection]); const handlePaymentClick = (paymentId: string) => { setSelectedPayment(paymentId); @@ -229,12 +346,13 @@ export default function RecentPayments({ }; const handleDownloadCSV = () => { - if (!payments.length) return; + if (!sortedPayments.length) return; - const mapped = payments.map((p) => ({ + const mapped = sortedPayments.map((p) => ({ ID: p.id, Amount: `${p.amount.toLocaleString()} ${p.asset}`, Status: p.status.charAt(0).toUpperCase() + p.status.slice(1), + Recipient: p.recipient, Description: p.description ?? "", Date: new Date(p.created_at).toLocaleString(), })); @@ -715,12 +833,12 @@ export default function RecentPayments({

- {t("showingResults", { shown: payments.length, total: totalCount })} + {t("showingResults", { shown: sortedPayments.length, total: totalCount })} {hasActiveFilters ? ` ${t("filteredSuffix")}` : ""}

({ + transactions={sortedPayments.map((payment) => ({ id: payment.id, createdAt: payment.created_at, type: "payment", @@ -742,7 +860,7 @@ export default function RecentPayments({