feat: add duplicate transaction detection and display in transactions page, enhancing user experience with visual indicators for duplicates

This commit is contained in:
Julien Froidefond
2025-12-08 09:50:32 +01:00
parent cb8628ce39
commit ba4d112cb8
6 changed files with 223 additions and 55 deletions

View File

@@ -25,6 +25,7 @@ import {
Wand2,
Trash2,
Loader2,
AlertTriangle,
} from "lucide-react";
import { DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
@@ -52,6 +53,8 @@ interface TransactionTableProps {
formatCurrency: (amount: number) => string;
formatDate: (dateStr: string) => string;
updatingTransactionIds?: Set<string>;
duplicateIds?: Set<string>;
highlightDuplicates?: boolean;
}
const ROW_HEIGHT = 72; // Hauteur approximative d'une ligne
@@ -145,6 +148,8 @@ export function TransactionTable({
formatCurrency,
formatDate,
updatingTransactionIds = new Set(),
duplicateIds = new Set(),
highlightDuplicates = false,
}: TransactionTableProps) {
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
const parentRef = useRef<HTMLDivElement>(null);
@@ -164,7 +169,7 @@ export function TransactionTable({
setFocusedIndex(index);
onMarkReconciled(transactionId);
},
[onMarkReconciled],
[onMarkReconciled]
);
const handleKeyDown = useCallback(
@@ -193,7 +198,7 @@ export function TransactionTable({
}
}
},
[focusedIndex, transactions, onMarkReconciled, virtualizer],
[focusedIndex, transactions, onMarkReconciled, virtualizer]
);
useEffect(() => {
@@ -210,7 +215,7 @@ export function TransactionTable({
(accountId: string) => {
return accounts.find((a) => a.id === accountId);
},
[accounts],
[accounts]
);
const getCategory = useCallback(
@@ -218,7 +223,7 @@ export function TransactionTable({
if (!categoryId) return null;
return categories.find((c) => c.id === categoryId);
},
[categories],
[categories]
);
return (
@@ -247,6 +252,8 @@ export function TransactionTable({
const account = getAccount(transaction.accountId);
const _category = getCategory(transaction.categoryId);
const isFocused = focusedIndex === virtualRow.index;
const isDuplicate =
highlightDuplicates && duplicateIds.has(transaction.id);
return (
<div
@@ -259,6 +266,10 @@ export function TransactionTable({
left: 0,
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
...(isDuplicate && {
backgroundColor: "rgb(254 252 232)", // yellow-50
borderLeft: "4px solid rgb(234 179 8)", // yellow-500
}),
}}
onClick={() => {
// Désactiver le pointage au clic sur mobile
@@ -270,6 +281,7 @@ export function TransactionTable({
"p-4 space-y-3 hover:bg-muted/50 cursor-pointer border-b border-border",
transaction.isReconciled && "bg-emerald-500/5",
isFocused && "bg-primary/10 ring-1 ring-primary/30",
isDuplicate && "shadow-sm"
)}
>
<div className="flex items-start justify-between gap-2">
@@ -282,9 +294,23 @@ export function TransactionTable({
onClick={(e) => e.stopPropagation()}
/>
<div className="flex-1 min-w-0">
<p className="font-medium text-xs md:text-sm truncate">
{transaction.description}
</p>
<div className="flex items-center gap-2">
<p className="font-medium text-xs md:text-sm truncate">
{transaction.description}
</p>
{isDuplicate && (
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<AlertTriangle className="h-3 w-3 md:h-4 md:w-4 text-[var(--warning)] shrink-0" />
</TooltipTrigger>
<TooltipContent>
<p>
Transaction en double (même somme et date)
</p>
</TooltipContent>
</Tooltip>
)}
</div>
{transaction.memo && (
<p className="text-[10px] md:text-xs text-muted-foreground truncate mt-1">
{transaction.memo}
@@ -297,7 +323,7 @@ export function TransactionTable({
"font-semibold tabular-nums text-sm md:text-base shrink-0",
transaction.amount >= 0
? "text-emerald-600"
: "text-red-600",
: "text-red-600"
)}
>
{transaction.amount >= 0 ? "+" : ""}
@@ -332,7 +358,7 @@ export function TransactionTable({
showBadge
align="start"
disabled={updatingTransactionIds.has(
transaction.id,
transaction.id
)}
/>
</div>
@@ -365,7 +391,7 @@ export function TransactionTable({
e.stopPropagation();
if (
confirm(
`Êtes-vous sûr de vouloir supprimer cette transaction ?\n\n${transaction.description}\n${formatCurrency(transaction.amount)}`,
`Êtes-vous sûr de vouloir supprimer cette transaction ?\n\n${transaction.description}\n${formatCurrency(transaction.amount)}`
)
) {
onDelete(transaction.id);
@@ -455,6 +481,8 @@ export function TransactionTable({
const transaction = transactions[virtualRow.index];
const account = getAccount(transaction.accountId);
const isFocused = focusedIndex === virtualRow.index;
const isDuplicate =
highlightDuplicates && duplicateIds.has(transaction.id);
return (
<div
@@ -467,6 +495,10 @@ export function TransactionTable({
left: 0,
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
...(isDuplicate && {
backgroundColor: "rgb(254 252 232)", // yellow-50
borderLeft: "4px solid rgb(234 179 8)", // yellow-500
}),
}}
onClick={() =>
handleRowClick(virtualRow.index, transaction.id)
@@ -475,6 +507,7 @@ export function TransactionTable({
"grid grid-cols-[auto_120px_2fr_150px_180px_140px_auto_auto] gap-0 border-b border-border hover:bg-muted/50 cursor-pointer",
transaction.isReconciled && "bg-emerald-500/5",
isFocused && "bg-primary/10 ring-1 ring-primary/30",
isDuplicate && "shadow-sm"
)}
>
<div className="p-3">
@@ -492,9 +525,23 @@ export function TransactionTable({
className="p-3 min-w-0 overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<p className="font-medium text-sm truncate">
{transaction.description}
</p>
<div className="flex items-center gap-2">
<p className="font-medium text-sm truncate">
{transaction.description}
</p>
{isDuplicate && (
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<AlertTriangle className="h-4 w-4 text-[var(--warning)] shrink-0" />
</TooltipTrigger>
<TooltipContent>
<p>
Transaction en double (même somme et date)
</p>
</TooltipContent>
</Tooltip>
)}
</div>
{transaction.memo && (
<DescriptionWithTooltip
description={transaction.memo}
@@ -529,7 +576,7 @@ export function TransactionTable({
"p-3 text-right font-semibold tabular-nums",
transaction.amount >= 0
? "text-emerald-600"
: "text-red-600",
: "text-red-600"
)}
>
{transaction.amount >= 0 ? "+" : ""}
@@ -596,7 +643,7 @@ export function TransactionTable({
e.stopPropagation();
if (
confirm(
`Êtes-vous sûr de vouloir supprimer cette transaction ?\n\n${transaction.description}\n${formatCurrency(transaction.amount)}`,
`Êtes-vous sûr de vouloir supprimer cette transaction ?\n\n${transaction.description}\n${formatCurrency(transaction.amount)}`
)
) {
onDelete(transaction.id);