Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
316 changes: 147 additions & 169 deletions app/(dashboard)/transactions/page.tsx
Original file line number Diff line number Diff line change
@@ -1,182 +1,160 @@
"use client";

import { useState, useEffect, useRef } from "react";
import { Transaction, getTransactions } from "@/lib/api/transactions";
import { TransactionFilters } from "@/components/transactions/transaction-filters";
import { TransactionTable } from "@/components/transactions/transaction-table";
import { TransactionList } from "@/components/transactions/transaction-list";
import { TransactionPagination } from "@/components/transactions/pagination";
import {
exportTransactionsToCSV,
generateCSVFilename,
} from "@/app/lib/utils/csv-export";
import { TransactionEmptyState } from "@/components/transactions/empty-state";
import { TransactionPagination } from "@/components/transactions/pagination";
import { TransactionDetails } from "@/components/transactions/transaction-details";
import { exportTransactionsToCSV, generateCSVFilename } from "@/app/lib/utils/csv-export";
import { TransactionFilters } from "@/components/transactions/transaction-filters";
import { TransactionList } from "@/components/transactions/transaction-list";
import { TransactionTable } from "@/components/transactions/transaction-table";
import { Transaction, getTransactions } from "@/lib/api/transactions";
import { useEffect, useRef, useState } from "react";

const ITEMS_PER_PAGE = 10;

export default function TransactionsPage() {
const [searchQuery, setSearchQuery] = useState("");
const [debouncedSearch, setDebouncedSearch] = useState("");
const [activeFilter, setActiveFilter] = useState("All");
const [currentPage, setCurrentPage] = useState(1);
const [selectedTransaction, setSelectedTransaction] = useState<Transaction | null>(null);
const [detailsOpen, setDetailsOpen] = useState(false);
const [dateFrom, setDateFrom] = useState<string>("");
const [dateTo, setDateTo] = useState<string>("");

const [transactions, setTransactions] = useState<Transaction[]>([]);
const [totalItems, setTotalItems] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const handleSearchChange = (q: string) => {
setSearchQuery(q);
if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current);
searchTimeoutRef.current = setTimeout(() => {
setDebouncedSearch(q);
setCurrentPage(1);
}, 400);
};

const handleFilterChange = (f: string) => {
setActiveFilter(f);
setCurrentPage(1);
};

const handleDateFromChange = (date: string) => {
setDateFrom(date);
setCurrentPage(1);
};

const handleDateToChange = (date: string) => {
setDateTo(date);
setCurrentPage(1);
};

const handleClearDateRange = () => {
setDateFrom("");
setDateTo("");
setCurrentPage(1);
};

const handleExportCSV = () => {
if (transactions.length > 0) {
const filename = generateCSVFilename(dateFrom, dateTo);
exportTransactionsToCSV(transactions, filename);
const [searchQuery, setSearchQuery] = useState("");
const [debouncedSearch, setDebouncedSearch] = useState("");
const [activeFilter, setActiveFilter] = useState("All");
const [currentPage, setCurrentPage] = useState(1);
const [selectedTransaction, setSelectedTransaction] =
useState<Transaction | null>(null);
const [detailsOpen, setDetailsOpen] = useState(false);

const [transactions, setTransactions] = useState<Transaction[]>([]);
const [totalItems, setTotalItems] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const handleSearchChange = (q: string) => {
setSearchQuery(q);
if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current);
searchTimeoutRef.current = setTimeout(() => {
setDebouncedSearch(q);
setCurrentPage(1);
}, 400);
};

const handleFilterChange = (f: string) => {
setActiveFilter(f);
setCurrentPage(1);
};

const handleExportCSV = () => {

Check warning on line 48 in app/(dashboard)/transactions/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Type-check & Build

'handleExportCSV' is assigned a value but never used
if (transactions.length > 0) {
const filename = generateCSVFilename();
exportTransactionsToCSV(transactions, filename);
}
};

useEffect(() => {
let cancelled = false;
setIsLoading(true);
setError(null);

const typeParam =
activeFilter === "Withdrawal"
? "Withdraw"
: activeFilter !== "All"
? activeFilter
: undefined;

getTransactions({
page: currentPage,
limit: ITEMS_PER_PAGE,
search: debouncedSearch || undefined,
type: typeParam,
})
.then((result) => {
if (!cancelled) {
setTransactions(result.data);
setTotalItems(result.total);
}
};
})
.catch((err) => {
if (!cancelled) {
setError(
err instanceof Error ? err.message : "Failed to load transactions",
);
}
})
.finally(() => {
if (!cancelled) setIsLoading(false);
});

useEffect(() => {
let cancelled = false;

const fetchTransactions = async () => {
const typeParam =
activeFilter === 'Withdrawal'
? 'Withdraw'
: activeFilter !== 'All'
? activeFilter
: undefined;

try {
const result = await getTransactions({
page: currentPage,
limit: ITEMS_PER_PAGE,
search: debouncedSearch || undefined,
type: typeParam,
from: dateFrom || undefined,
to: dateTo || undefined,
});
if (!cancelled) {
setTransactions(result.data);
setTotalItems(result.total);
}
} catch (err) {
if (!cancelled) {
setError(
err instanceof Error ? err.message : 'Failed to load transactions'
);
}
} finally {
if (!cancelled) setIsLoading(false);
}
};

fetchTransactions();

return () => {
cancelled = true;
};
}, [currentPage, debouncedSearch, activeFilter, dateFrom, dateTo]);

const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE);

const handleTransactionClick = (tx: Transaction) => {
setSelectedTransaction(tx);
setDetailsOpen(true);
return () => {
cancelled = true;
};

return (
<div className="flex flex-col h-full space-y-4 md:space-y-6 max-w-7xl mx-auto w-full">
<div className="bg-card rounded-xl p-4 md:p-6 shadow-sm border border-border/50">
<TransactionFilters
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
activeFilter={activeFilter}
onFilterChange={handleFilterChange}
totalCount={totalItems}
dateFrom={dateFrom}
dateTo={dateTo}
onDateFromChange={handleDateFromChange}
onDateToChange={handleDateToChange}
onClearDateRange={handleClearDateRange}
onExportCSV={handleExportCSV}
/>

{isLoading ? (
<div className="flex items-center justify-center py-20">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
</div>
) : error ? (
<div className="flex flex-col items-center justify-center py-20 gap-3">
<p className="text-sm text-muted-foreground">{error}</p>
<button
onClick={() => {
setError(null);
setIsLoading(true);
}}
className="text-sm font-medium text-primary hover:underline"
>
Retry
</button>
</div>
) : transactions.length > 0 ? (
<>
<TransactionTable
transactions={transactions}
onSelectTransaction={handleTransactionClick}
/>
<TransactionList
transactions={transactions}
onSelectTransaction={handleTransactionClick}
/>
<TransactionPagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
totalItems={totalItems}
itemsPerPage={ITEMS_PER_PAGE}
/>
</>
) : (
<TransactionEmptyState />
)}
</div>

<TransactionDetails
transaction={selectedTransaction}
open={detailsOpen}
onClose={() => setDetailsOpen(false)}
}, [currentPage, debouncedSearch, activeFilter]);

const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE);

const handleTransactionClick = (tx: Transaction) => {
setSelectedTransaction(tx);
setDetailsOpen(true);
};

return (
<div className="flex flex-col h-full space-y-4 md:space-y-6 max-w-7xl mx-auto w-full">
<div className="bg-card rounded-xl p-4 md:p-6 shadow-sm border border-border/50">
<TransactionFilters
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
activeFilter={activeFilter}
onFilterChange={handleFilterChange}
totalCount={totalItems}
/>

{isLoading ? (
<div className="flex items-center justify-center py-20">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
</div>
) : error ? (
<div className="flex flex-col items-center justify-center py-20 gap-3">
<p className="text-sm text-muted-foreground">{error}</p>
<button
onClick={() => {
setError(null);
setIsLoading(true);
}}
className="text-sm font-medium text-primary hover:underline"
>
Retry
</button>
</div>
) : transactions.length > 0 ? (
<>
<TransactionTable
transactions={transactions}
onSelectTransaction={handleTransactionClick}
/>
<TransactionList
transactions={transactions}
onSelectTransaction={handleTransactionClick}
/>
<TransactionPagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
totalItems={totalItems}
itemsPerPage={ITEMS_PER_PAGE}
/>
</div>
);
</>
) : (
<TransactionEmptyState />
)}
</div>

<TransactionDetails
transaction={selectedTransaction}
open={detailsOpen}
onClose={() => setDetailsOpen(false)}
/>
</div>
);
}
Loading
Loading