feat: implement bulk account deletion and enhance account management with folder organization and drag-and-drop functionality
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
@@ -43,6 +44,8 @@ interface TransactionTableProps {
|
||||
formatDate: (dateStr: string) => string;
|
||||
}
|
||||
|
||||
const ROW_HEIGHT = 72; // Hauteur approximative d'une ligne
|
||||
|
||||
export function TransactionTable({
|
||||
transactions,
|
||||
accounts,
|
||||
@@ -61,7 +64,14 @@ export function TransactionTable({
|
||||
formatDate,
|
||||
}: TransactionTableProps) {
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
||||
const rowRefs = useRef<(HTMLTableRowElement | null)[]>([]);
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: transactions.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => ROW_HEIGHT,
|
||||
overscan: 10,
|
||||
});
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(index: number, transactionId: string) => {
|
||||
@@ -81,9 +91,8 @@ export function TransactionTable({
|
||||
if (newIndex !== focusedIndex) {
|
||||
setFocusedIndex(newIndex);
|
||||
onMarkReconciled(transactions[newIndex].id);
|
||||
rowRefs.current[newIndex]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
virtualizer.scrollToIndex(newIndex, {
|
||||
align: "start",
|
||||
});
|
||||
}
|
||||
} else if (e.key === "ArrowUp") {
|
||||
@@ -92,14 +101,13 @@ export function TransactionTable({
|
||||
if (newIndex !== focusedIndex) {
|
||||
setFocusedIndex(newIndex);
|
||||
onMarkReconciled(transactions[newIndex].id);
|
||||
rowRefs.current[newIndex]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
virtualizer.scrollToIndex(newIndex, {
|
||||
align: "start",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[focusedIndex, transactions, onMarkReconciled]
|
||||
[focusedIndex, transactions, onMarkReconciled, virtualizer]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -111,6 +119,7 @@ export function TransactionTable({
|
||||
useEffect(() => {
|
||||
setFocusedIndex(null);
|
||||
}, [transactions.length]);
|
||||
|
||||
const getAccount = (accountId: string) => {
|
||||
return accounts.find((a) => a.id === accountId);
|
||||
};
|
||||
@@ -124,87 +133,106 @@ export function TransactionTable({
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="p-3 text-left">
|
||||
<Checkbox
|
||||
checked={
|
||||
selectedTransactions.size === transactions.length &&
|
||||
transactions.length > 0
|
||||
}
|
||||
onCheckedChange={onToggleSelectAll}
|
||||
/>
|
||||
</th>
|
||||
<th className="p-3 text-left">
|
||||
<button
|
||||
onClick={() => onSortChange("date")}
|
||||
className="flex items-center gap-1 text-sm font-medium text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Date
|
||||
<ArrowUpDown className="w-3 h-3" />
|
||||
</button>
|
||||
</th>
|
||||
<th className="p-3 text-left">
|
||||
<button
|
||||
onClick={() => onSortChange("description")}
|
||||
className="flex items-center gap-1 text-sm font-medium text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Description
|
||||
<ArrowUpDown className="w-3 h-3" />
|
||||
</button>
|
||||
</th>
|
||||
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
||||
Compte
|
||||
</th>
|
||||
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
||||
Catégorie
|
||||
</th>
|
||||
<th className="p-3 text-right">
|
||||
<button
|
||||
onClick={() => onSortChange("amount")}
|
||||
className="flex items-center gap-1 text-sm font-medium text-muted-foreground hover:text-foreground ml-auto"
|
||||
>
|
||||
Montant
|
||||
<ArrowUpDown className="w-3 h-3" />
|
||||
</button>
|
||||
</th>
|
||||
<th className="p-3 text-center text-sm font-medium text-muted-foreground">
|
||||
Pointé
|
||||
</th>
|
||||
<th className="p-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactions.map((transaction, index) => {
|
||||
{/* Header fixe */}
|
||||
<div className="sticky top-0 z-10 bg-[var(--card)] border-b border-border">
|
||||
<div className="grid grid-cols-[auto_120px_2fr_150px_180px_140px_auto_auto] gap-0">
|
||||
<div className="p-3">
|
||||
<Checkbox
|
||||
checked={
|
||||
selectedTransactions.size === transactions.length &&
|
||||
transactions.length > 0
|
||||
}
|
||||
onCheckedChange={onToggleSelectAll}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<button
|
||||
onClick={() => onSortChange("date")}
|
||||
className="flex items-center gap-1 text-sm font-medium text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Date
|
||||
<ArrowUpDown className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<button
|
||||
onClick={() => onSortChange("description")}
|
||||
className="flex items-center gap-1 text-sm font-medium text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Description
|
||||
<ArrowUpDown className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-3 text-sm font-medium text-muted-foreground">
|
||||
Compte
|
||||
</div>
|
||||
<div className="p-3 text-sm font-medium text-muted-foreground">
|
||||
Catégorie
|
||||
</div>
|
||||
<div className="p-3 text-right">
|
||||
<button
|
||||
onClick={() => onSortChange("amount")}
|
||||
className="flex items-center gap-1 text-sm font-medium text-muted-foreground hover:text-foreground ml-auto"
|
||||
>
|
||||
Montant
|
||||
<ArrowUpDown className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-3 text-center text-sm font-medium text-muted-foreground">
|
||||
Pointé
|
||||
</div>
|
||||
<div className="p-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Body virtualisé */}
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="overflow-auto"
|
||||
style={{ height: "calc(100vh - 400px)", minHeight: "400px" }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const transaction = transactions[virtualRow.index];
|
||||
const account = getAccount(transaction.accountId);
|
||||
const isFocused = focusedIndex === index;
|
||||
const isFocused = focusedIndex === virtualRow.index;
|
||||
|
||||
return (
|
||||
<tr
|
||||
<div
|
||||
key={transaction.id}
|
||||
ref={(el) => {
|
||||
rowRefs.current[index] = el;
|
||||
data-index={virtualRow.index}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
onClick={() => handleRowClick(index, transaction.id)}
|
||||
onClick={() => handleRowClick(virtualRow.index, transaction.id)}
|
||||
className={cn(
|
||||
"border-b border-border last:border-0 hover:bg-muted/50 cursor-pointer",
|
||||
"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"
|
||||
)}
|
||||
>
|
||||
<td className="p-3">
|
||||
<div className="p-3">
|
||||
<Checkbox
|
||||
checked={selectedTransactions.has(transaction.id)}
|
||||
onCheckedChange={() =>
|
||||
onToggleSelectTransaction(transaction.id)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className="p-3 text-sm text-muted-foreground whitespace-nowrap">
|
||||
</div>
|
||||
<div className="p-3 text-sm text-muted-foreground whitespace-nowrap">
|
||||
{formatDate(transaction.date)}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<p className="font-medium text-sm">
|
||||
{transaction.description}
|
||||
</p>
|
||||
@@ -213,11 +241,11 @@ export function TransactionTable({
|
||||
{transaction.memo}
|
||||
</p>
|
||||
)}
|
||||
</td>
|
||||
<td className="p-3 text-sm text-muted-foreground">
|
||||
</div>
|
||||
<div className="p-3 text-sm text-muted-foreground">
|
||||
{account?.name || "-"}
|
||||
</td>
|
||||
<td className="p-3" onClick={(e) => e.stopPropagation()}>
|
||||
</div>
|
||||
<div className="p-3" onClick={(e) => e.stopPropagation()}>
|
||||
<CategoryCombobox
|
||||
categories={categories}
|
||||
value={transaction.categoryId}
|
||||
@@ -227,8 +255,8 @@ export function TransactionTable({
|
||||
showBadge
|
||||
align="start"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"p-3 text-right font-semibold tabular-nums",
|
||||
transaction.amount >= 0
|
||||
@@ -238,8 +266,8 @@ export function TransactionTable({
|
||||
>
|
||||
{transaction.amount >= 0 ? "+" : ""}
|
||||
{formatCurrency(transaction.amount)}
|
||||
</td>
|
||||
<td className="p-3 text-center" onClick={(e) => e.stopPropagation()}>
|
||||
</div>
|
||||
<div className="p-3 text-center" onClick={(e) => e.stopPropagation()}>
|
||||
<button
|
||||
onClick={() => onToggleReconciled(transaction.id)}
|
||||
className="p-1 hover:bg-muted rounded"
|
||||
@@ -250,8 +278,8 @@ export function TransactionTable({
|
||||
<Circle className="w-5 h-5 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
<td className="p-3" onClick={(e) => e.stopPropagation()}>
|
||||
</div>
|
||||
<div className="p-3" onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
@@ -293,12 +321,12 @@ export function TransactionTable({
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user