feat: implement transaction reconciliation feature with keyboard navigation support in transaction table
This commit is contained in:
@@ -154,3 +154,4 @@ Ce projet est en développement actif. Les suggestions et améliorations sont le
|
|||||||
---
|
---
|
||||||
|
|
||||||
Développé avec ❤️ en utilisant Next.js et React
|
Développé avec ❤️ en utilisant Next.js et React
|
||||||
|
|
||||||
|
|||||||
@@ -24,3 +24,4 @@ export async function POST() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,32 @@ export default function TransactionsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const markReconciled = async (transactionId: string) => {
|
||||||
|
const transaction = data.transactions.find((t) => t.id === transactionId);
|
||||||
|
if (!transaction || transaction.isReconciled) return; // Skip if already reconciled
|
||||||
|
|
||||||
|
const updatedTransaction = {
|
||||||
|
...transaction,
|
||||||
|
isReconciled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedTransactions = data.transactions.map((t) =>
|
||||||
|
t.id === transactionId ? updatedTransaction : t
|
||||||
|
);
|
||||||
|
update({ ...data, transactions: updatedTransactions });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch("/api/banking/transactions", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(updatedTransaction),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update transaction:", error);
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const setCategory = async (
|
const setCategory = async (
|
||||||
transactionId: string,
|
transactionId: string,
|
||||||
categoryId: string | null
|
categoryId: string | null
|
||||||
@@ -299,6 +325,7 @@ export default function TransactionsPage() {
|
|||||||
onToggleSelectAll={toggleSelectAll}
|
onToggleSelectAll={toggleSelectAll}
|
||||||
onToggleSelectTransaction={toggleSelectTransaction}
|
onToggleSelectTransaction={toggleSelectTransaction}
|
||||||
onToggleReconciled={toggleReconciled}
|
onToggleReconciled={toggleReconciled}
|
||||||
|
onMarkReconciled={markReconciled}
|
||||||
onSetCategory={setCategory}
|
onSetCategory={setCategory}
|
||||||
formatCurrency={formatCurrency}
|
formatCurrency={formatCurrency}
|
||||||
formatDate={formatDate}
|
formatDate={formatDate}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState, useCallback } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
@@ -36,6 +37,7 @@ interface TransactionTableProps {
|
|||||||
onToggleSelectAll: () => void;
|
onToggleSelectAll: () => void;
|
||||||
onToggleSelectTransaction: (id: string) => void;
|
onToggleSelectTransaction: (id: string) => void;
|
||||||
onToggleReconciled: (id: string) => void;
|
onToggleReconciled: (id: string) => void;
|
||||||
|
onMarkReconciled: (id: string) => void;
|
||||||
onSetCategory: (transactionId: string, categoryId: string | null) => void;
|
onSetCategory: (transactionId: string, categoryId: string | null) => void;
|
||||||
formatCurrency: (amount: number) => string;
|
formatCurrency: (amount: number) => string;
|
||||||
formatDate: (dateStr: string) => string;
|
formatDate: (dateStr: string) => string;
|
||||||
@@ -52,10 +54,62 @@ export function TransactionTable({
|
|||||||
onToggleSelectAll,
|
onToggleSelectAll,
|
||||||
onToggleSelectTransaction,
|
onToggleSelectTransaction,
|
||||||
onToggleReconciled,
|
onToggleReconciled,
|
||||||
|
onMarkReconciled,
|
||||||
onSetCategory,
|
onSetCategory,
|
||||||
formatCurrency,
|
formatCurrency,
|
||||||
formatDate,
|
formatDate,
|
||||||
}: TransactionTableProps) {
|
}: 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) => {
|
const getCategory = (categoryId: string | null) => {
|
||||||
if (!categoryId) return null;
|
if (!categoryId) return null;
|
||||||
return categories.find((c) => c.id === categoryId);
|
return categories.find((c) => c.id === categoryId);
|
||||||
@@ -126,14 +180,23 @@ export function TransactionTable({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{transactions.map((transaction) => {
|
{transactions.map((transaction, index) => {
|
||||||
const category = getCategory(transaction.categoryId);
|
const category = getCategory(transaction.categoryId);
|
||||||
const account = getAccount(transaction.accountId);
|
const account = getAccount(transaction.accountId);
|
||||||
|
const isFocused = focusedIndex === index;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={transaction.id}
|
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">
|
<td className="p-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ console.log(" - parentId: null");
|
|||||||
console.log(" - color: #6366f1");
|
console.log(" - color: #6366f1");
|
||||||
console.log(" - icon: folder");
|
console.log(" - icon: folder");
|
||||||
console.log("3. Créez les catégories par défaut via l'interface web");
|
console.log("3. Créez les catégories par défaut via l'interface web");
|
||||||
|
|
||||||
|
|||||||
@@ -60,3 +60,4 @@ export const categoryService = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -75,3 +75,4 @@ export const folderService = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -91,3 +91,4 @@ export const transactionService = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user