From 7d9a89c43f6844c939582036a7e6f0a860b45c3f Mon Sep 17 00:00:00 2001 From: Feyisara2108 Date: Mon, 30 Mar 2026 04:27:09 +0100 Subject: [PATCH 1/2] feat(frontend): add CSV export for wallet and loan records --- .../src/app/[locale]/loans/[loanId]/page.tsx | 93 ++++++++++++++----- frontend/src/app/utils/csv.ts | 49 ++++++++++ frontend/src/app/wallet/page.tsx | 84 ++++++++++++++--- 3 files changed, 190 insertions(+), 36 deletions(-) create mode 100644 frontend/src/app/utils/csv.ts diff --git a/frontend/src/app/[locale]/loans/[loanId]/page.tsx b/frontend/src/app/[locale]/loans/[loanId]/page.tsx index a62b078b..d8a6a021 100644 --- a/frontend/src/app/[locale]/loans/[loanId]/page.tsx +++ b/frontend/src/app/[locale]/loans/[loanId]/page.tsx @@ -2,12 +2,14 @@ import Link from "next/link"; import { useParams } from "next/navigation"; -import { ChevronRight, Clock, Wallet } from "lucide-react"; +import { ChevronRight, Clock, Download, Wallet } from "lucide-react"; import { LoanDetailSkeleton } from "../../../components/skeletons/LoanDetailSkeleton"; -import { useLoan } from "../../../hooks/useApi"; +import { useLoan, type LoanDetails } from "../../../hooks/useApi"; import { RepaymentProgress } from "../../../components/ui/RepaymentProgress"; import { LoanTimeline } from "../../../components/ui/LoanTimeline"; import { TxHashLink } from "../../../components/ui/TxHashLink"; +import { Button } from "../../../components/ui/Button"; +import { downloadCsv } from "../../../utils/csv"; function formatCurrency(value: number) { return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(value); @@ -28,6 +30,44 @@ function getDaysRemaining(deadline: string | undefined): number | null { return Math.ceil(diff / (1000 * 60 * 60 * 24)); } +function formatLoanEventType(type: string): string { + return type + .replace(/([a-z])([A-Z])/g, "$1 $2") + .replace(/_/g, " ") + .trim(); +} + +function getLoanAsset(loan: LoanDetails): string { + const loanWithAsset = loan as LoanDetails & { currency?: string; asset?: string }; + return loanWithAsset.currency ?? loanWithAsset.asset ?? "USD"; +} + +function buildLoanCsvRows(loan: LoanDetails) { + const asset = getLoanAsset(loan); + + if (loan.events.length > 0) { + return loan.events.map((event) => ({ + date: event.timestamp, + type: formatLoanEventType(event.type), + amount: event.amount, + asset, + status: loan.status, + transaction_hash: event.txHash ?? "", + })); + } + + return [ + { + date: loan.requestedAt ?? loan.approvedAt ?? new Date().toISOString(), + type: "Loan Record", + amount: loan.totalOwed, + asset, + status: loan.status, + transaction_hash: "", + }, + ]; +} + export default function LoanDetailsPage() { const params = useParams<{ loanId: string }>(); const loanId = params.loanId; @@ -63,39 +103,55 @@ export default function LoanDetailsPage() { } const latestTxHash = loan.events.find((event) => Boolean(event.txHash))?.txHash; - // Some API responses include nextPaymentDeadline in the extended loan object - const nextDeadline = (loan as unknown as { nextPaymentDeadline?: string }).nextPaymentDeadline; + const nextDeadline = (loan as LoanDetails & { nextPaymentDeadline?: string }).nextPaymentDeadline; const daysRemaining = getDaysRemaining(nextDeadline); + const handleExport = () => { + downloadCsv( + `loan-record-${loanId}-${new Date().toISOString().slice(0, 10)}.csv`, + buildLoanCsvRows(loan), + ); + }; + return (
- {/* Breadcrumb */} - {/* Header */}
-

- Borrower Portal -

-

Loan #{loanId}

-

- Track repayment timing, lender terms, and the current outstanding balance for this loan. -

+
+
+

+ Borrower Portal +

+

+ Loan #{loanId} +

+

+ Track repayment timing, lender terms, and the current outstanding balance for this loan. +

+
+ +
- {/* Loan metadata row */}
{loan.interestRate > 0 && ( @@ -125,7 +181,6 @@ export default function LoanDetailsPage() {
- {/* Main content */}

Repayment plan

@@ -163,9 +218,7 @@ export default function LoanDetailsPage() {
- {/* Sidebar */}