278 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|
|
|