feat: implement transaction reconciliation feature with keyboard navigation support in transaction table
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
@@ -36,6 +37,7 @@ interface TransactionTableProps {
|
||||
onToggleSelectAll: () => void;
|
||||
onToggleSelectTransaction: (id: string) => void;
|
||||
onToggleReconciled: (id: string) => void;
|
||||
onMarkReconciled: (id: string) => void;
|
||||
onSetCategory: (transactionId: string, categoryId: string | null) => void;
|
||||
formatCurrency: (amount: number) => string;
|
||||
formatDate: (dateStr: string) => string;
|
||||
@@ -52,10 +54,62 @@ export function TransactionTable({
|
||||
onToggleSelectAll,
|
||||
onToggleSelectTransaction,
|
||||
onToggleReconciled,
|
||||
onMarkReconciled,
|
||||
onSetCategory,
|
||||
formatCurrency,
|
||||
formatDate,
|
||||
}: TransactionTableProps) {
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
||||
const rowRefs = useRef<(HTMLTableRowElement | null)[]>([]);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(index: number, transactionId: string) => {
|
||||
setFocusedIndex(index);
|
||||
onMarkReconciled(transactionId);
|
||||
},
|
||||
[onMarkReconciled]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (focusedIndex === null || transactions.length === 0) return;
|
||||
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
const newIndex = Math.min(focusedIndex + 1, transactions.length - 1);
|
||||
if (newIndex !== focusedIndex) {
|
||||
setFocusedIndex(newIndex);
|
||||
onMarkReconciled(transactions[newIndex].id);
|
||||
rowRefs.current[newIndex]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
const newIndex = Math.max(focusedIndex - 1, 0);
|
||||
if (newIndex !== focusedIndex) {
|
||||
setFocusedIndex(newIndex);
|
||||
onMarkReconciled(transactions[newIndex].id);
|
||||
rowRefs.current[newIndex]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[focusedIndex, transactions, onMarkReconciled]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [handleKeyDown]);
|
||||
|
||||
// Reset focused index when transactions change
|
||||
useEffect(() => {
|
||||
setFocusedIndex(null);
|
||||
}, [transactions.length]);
|
||||
const getCategory = (categoryId: string | null) => {
|
||||
if (!categoryId) return null;
|
||||
return categories.find((c) => c.id === categoryId);
|
||||
@@ -126,14 +180,23 @@ export function TransactionTable({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactions.map((transaction) => {
|
||||
{transactions.map((transaction, index) => {
|
||||
const category = getCategory(transaction.categoryId);
|
||||
const account = getAccount(transaction.accountId);
|
||||
const isFocused = focusedIndex === index;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={transaction.id}
|
||||
className="border-b border-border last:border-0 hover:bg-muted/50"
|
||||
ref={(el) => {
|
||||
rowRefs.current[index] = el;
|
||||
}}
|
||||
onClick={() => handleRowClick(index, transaction.id)}
|
||||
className={cn(
|
||||
"border-b border-border last:border-0 hover:bg-muted/50 cursor-pointer",
|
||||
transaction.isReconciled && "bg-emerald-500/5",
|
||||
isFocused && "bg-primary/10 ring-1 ring-primary/30"
|
||||
)}
|
||||
>
|
||||
<td className="p-3">
|
||||
<Checkbox
|
||||
|
||||
Reference in New Issue
Block a user