Files
fintrack/components/transactions/transaction-table.tsx

278 lines
11 KiB
TypeScript

"use client";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Badge } from "@/components/ui/badge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { CategoryIcon } from "@/components/ui/category-icon";
import {
CheckCircle2,
Circle,
MoreVertical,
ArrowUpDown,
Check,
} from "lucide-react";
import { cn } from "@/lib/utils";
import type { Transaction, Account, Category } from "@/lib/types";
type SortField = "date" | "amount" | "description";
type SortOrder = "asc" | "desc";
interface TransactionTableProps {
transactions: Transaction[];
accounts: Account[];
categories: Category[];
selectedTransactions: Set<string>;
sortField: SortField;
sortOrder: SortOrder;
onSortChange: (field: SortField) => void;
onToggleSelectAll: () => void;
onToggleSelectTransaction: (id: string) => void;
onToggleReconciled: (id: string) => void;
onSetCategory: (transactionId: string, categoryId: string | null) => void;
formatCurrency: (amount: number) => string;
formatDate: (dateStr: string) => string;
}
export function TransactionTable({
transactions,
accounts,
categories,
selectedTransactions,
sortField,
sortOrder,
onSortChange,
onToggleSelectAll,
onToggleSelectTransaction,
onToggleReconciled,
onSetCategory,
formatCurrency,
formatDate,
}: TransactionTableProps) {
const getCategory = (categoryId: string | null) => {
if (!categoryId) return null;
return categories.find((c) => c.id === categoryId);
};
const getAccount = (accountId: string) => {
return accounts.find((a) => a.id === accountId);
};
return (
<Card>
<CardContent className="p-0">
{transactions.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12">
<p className="text-muted-foreground">Aucune transaction trouvée</p>
</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) => {
const category = getCategory(transaction.categoryId);
const account = getAccount(transaction.accountId);
return (
<tr
key={transaction.id}
className="border-b border-border last:border-0 hover:bg-muted/50"
>
<td 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">
{formatDate(transaction.date)}
</td>
<td className="p-3">
<p className="font-medium text-sm">
{transaction.description}
</p>
{transaction.memo && (
<p className="text-xs text-muted-foreground truncate max-w-[300px]">
{transaction.memo}
</p>
)}
</td>
<td className="p-3 text-sm text-muted-foreground">
{account?.name || "-"}
</td>
<td className="p-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-1 hover:opacity-80">
{category ? (
<Badge
variant="secondary"
className="gap-1"
style={{
backgroundColor: `${category.color}20`,
color: category.color,
}}
>
<CategoryIcon
icon={category.icon}
color={category.color}
size={12}
/>
{category.name}
</Badge>
) : (
<Badge
variant="outline"
className="text-muted-foreground"
>
Non catégorisé
</Badge>
)}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
onClick={() => onSetCategory(transaction.id, null)}
>
Aucune catégorie
</DropdownMenuItem>
<DropdownMenuSeparator />
{categories.map((cat) => (
<DropdownMenuItem
key={cat.id}
onClick={() =>
onSetCategory(transaction.id, cat.id)
}
>
<CategoryIcon
icon={cat.icon}
color={cat.color}
size={14}
className="mr-2"
/>
{cat.name}
{transaction.categoryId === cat.id && (
<Check className="w-4 h-4 ml-auto" />
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</td>
<td
className={cn(
"p-3 text-right font-semibold tabular-nums",
transaction.amount >= 0
? "text-emerald-600"
: "text-red-600"
)}
>
{transaction.amount >= 0 ? "+" : ""}
{formatCurrency(transaction.amount)}
</td>
<td className="p-3 text-center">
<button
onClick={() => onToggleReconciled(transaction.id)}
className="p-1 hover:bg-muted rounded"
>
{transaction.isReconciled ? (
<CheckCircle2 className="w-5 h-5 text-emerald-600" />
) : (
<Circle className="w-5 h-5 text-muted-foreground" />
)}
</button>
</td>
<td className="p-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
>
<MoreVertical className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => onToggleReconciled(transaction.id)}
>
{transaction.isReconciled
? "Dépointer"
: "Pointer"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
);
}