feat: refactor dashboard and account pages to utilize new layout components, enhancing structure and loading states
This commit is contained in:
@@ -2,41 +2,16 @@
|
||||
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Sidebar } from "@/components/dashboard/sidebar";
|
||||
import { PageLayout, LoadingState, PageHeader } from "@/components/layout";
|
||||
import {
|
||||
TransactionFilters,
|
||||
TransactionBulkActions,
|
||||
TransactionTable,
|
||||
} from "@/components/transactions";
|
||||
import { OFXImportDialog } from "@/components/import/ofx-import-dialog";
|
||||
import { useBankingData } from "@/lib/hooks";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { OFXImportDialog } from "@/components/import/ofx-import-dialog";
|
||||
import { CategoryIcon } from "@/components/ui/category-icon";
|
||||
import {
|
||||
Search,
|
||||
CheckCircle2,
|
||||
Circle,
|
||||
MoreVertical,
|
||||
Tags,
|
||||
Upload,
|
||||
RefreshCw,
|
||||
ArrowUpDown,
|
||||
Check,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Upload } from "lucide-react";
|
||||
|
||||
type SortField = "date" | "amount" | "description";
|
||||
type SortOrder = "asc" | "desc";
|
||||
@@ -47,19 +22,19 @@ export default function TransactionsPage() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedAccount, setSelectedAccount] = useState<string>("all");
|
||||
|
||||
// Initialize account filter from URL params
|
||||
useEffect(() => {
|
||||
const accountId = searchParams.get("accountId");
|
||||
if (accountId) {
|
||||
setSelectedAccount(accountId);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>("all");
|
||||
const [showReconciled, setShowReconciled] = useState<string>("all");
|
||||
const [sortField, setSortField] = useState<SortField>("date");
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>("desc");
|
||||
const [selectedTransactions, setSelectedTransactions] = useState<Set<string>>(
|
||||
new Set(),
|
||||
new Set()
|
||||
);
|
||||
|
||||
const filteredTransactions = useMemo(() => {
|
||||
@@ -67,43 +42,38 @@ export default function TransactionsPage() {
|
||||
|
||||
let transactions = [...data.transactions];
|
||||
|
||||
// Filter by search
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
transactions = transactions.filter(
|
||||
(t) =>
|
||||
t.description.toLowerCase().includes(query) ||
|
||||
t.memo?.toLowerCase().includes(query),
|
||||
t.memo?.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by account
|
||||
if (selectedAccount !== "all") {
|
||||
transactions = transactions.filter(
|
||||
(t) => t.accountId === selectedAccount,
|
||||
(t) => t.accountId === selectedAccount
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by category
|
||||
if (selectedCategory !== "all") {
|
||||
if (selectedCategory === "uncategorized") {
|
||||
transactions = transactions.filter((t) => !t.categoryId);
|
||||
} else {
|
||||
transactions = transactions.filter(
|
||||
(t) => t.categoryId === selectedCategory,
|
||||
(t) => t.categoryId === selectedCategory
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by reconciliation status
|
||||
if (showReconciled !== "all") {
|
||||
const isReconciled = showReconciled === "reconciled";
|
||||
transactions = transactions.filter(
|
||||
(t) => t.isReconciled === isReconciled,
|
||||
(t) => t.isReconciled === isReconciled
|
||||
);
|
||||
}
|
||||
|
||||
// Sort
|
||||
transactions.sort((a, b) => {
|
||||
let comparison = 0;
|
||||
switch (sortField) {
|
||||
@@ -132,14 +102,7 @@ export default function TransactionsPage() {
|
||||
]);
|
||||
|
||||
if (isLoading || !data) {
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<Sidebar />
|
||||
<main className="flex-1 flex items-center justify-center">
|
||||
<RefreshCw className="w-8 h-8 animate-spin text-muted-foreground" />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
return <LoadingState />;
|
||||
}
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
@@ -161,15 +124,16 @@ export default function TransactionsPage() {
|
||||
const transaction = data.transactions.find((t) => t.id === transactionId);
|
||||
if (!transaction) return;
|
||||
|
||||
const updatedTransaction = { ...transaction, isReconciled: !transaction.isReconciled };
|
||||
|
||||
// Optimistic update
|
||||
const updatedTransaction = {
|
||||
...transaction,
|
||||
isReconciled: !transaction.isReconciled,
|
||||
};
|
||||
|
||||
const updatedTransactions = data.transactions.map((t) =>
|
||||
t.id === transactionId ? updatedTransaction : t,
|
||||
t.id === transactionId ? updatedTransaction : t
|
||||
);
|
||||
update({ ...data, transactions: updatedTransactions });
|
||||
|
||||
// Persist to database
|
||||
try {
|
||||
await fetch("/api/banking/transactions", {
|
||||
method: "PUT",
|
||||
@@ -178,24 +142,24 @@ export default function TransactionsPage() {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to update transaction:", error);
|
||||
// Revert on error
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
|
||||
const setCategory = async (transactionId: string, categoryId: string | null) => {
|
||||
const setCategory = async (
|
||||
transactionId: string,
|
||||
categoryId: string | null
|
||||
) => {
|
||||
const transaction = data.transactions.find((t) => t.id === transactionId);
|
||||
if (!transaction) return;
|
||||
|
||||
const updatedTransaction = { ...transaction, categoryId };
|
||||
|
||||
// Optimistic update
|
||||
|
||||
const updatedTransactions = data.transactions.map((t) =>
|
||||
t.id === transactionId ? updatedTransaction : t,
|
||||
t.id === transactionId ? updatedTransaction : t
|
||||
);
|
||||
update({ ...data, transactions: updatedTransactions });
|
||||
|
||||
// Persist to database
|
||||
try {
|
||||
await fetch("/api/banking/transactions", {
|
||||
method: "PUT",
|
||||
@@ -212,15 +176,13 @@ export default function TransactionsPage() {
|
||||
const transactionsToUpdate = data.transactions.filter((t) =>
|
||||
selectedTransactions.has(t.id)
|
||||
);
|
||||
|
||||
// Optimistic update
|
||||
|
||||
const updatedTransactions = data.transactions.map((t) =>
|
||||
selectedTransactions.has(t.id) ? { ...t, isReconciled: reconciled } : t,
|
||||
selectedTransactions.has(t.id) ? { ...t, isReconciled: reconciled } : t
|
||||
);
|
||||
update({ ...data, transactions: updatedTransactions });
|
||||
setSelectedTransactions(new Set());
|
||||
|
||||
// Persist to database
|
||||
try {
|
||||
await Promise.all(
|
||||
transactionsToUpdate.map((t) =>
|
||||
@@ -241,15 +203,13 @@ export default function TransactionsPage() {
|
||||
const transactionsToUpdate = data.transactions.filter((t) =>
|
||||
selectedTransactions.has(t.id)
|
||||
);
|
||||
|
||||
// Optimistic update
|
||||
|
||||
const updatedTransactions = data.transactions.map((t) =>
|
||||
selectedTransactions.has(t.id) ? { ...t, categoryId } : t,
|
||||
selectedTransactions.has(t.id) ? { ...t, categoryId } : t
|
||||
);
|
||||
update({ ...data, transactions: updatedTransactions });
|
||||
setSelectedTransactions(new Set());
|
||||
|
||||
// Persist to database
|
||||
try {
|
||||
await Promise.all(
|
||||
transactionsToUpdate.map((t) =>
|
||||
@@ -284,411 +244,65 @@ export default function TransactionsPage() {
|
||||
setSelectedTransactions(newSelected);
|
||||
};
|
||||
|
||||
const getCategory = (categoryId: string | null) => {
|
||||
if (!categoryId) return null;
|
||||
return data.categories.find((c) => c.id === categoryId);
|
||||
};
|
||||
|
||||
const getAccount = (accountId: string) => {
|
||||
return data.accounts.find((a) => a.id === accountId);
|
||||
const handleSortChange = (field: SortField) => {
|
||||
if (sortField === field) {
|
||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortOrder(field === "date" ? "desc" : "asc");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-background">
|
||||
<Sidebar />
|
||||
<main className="flex-1 overflow-auto">
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Transactions
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{filteredTransactions.length} transaction
|
||||
{filteredTransactions.length > 1 ? "s" : ""}
|
||||
</p>
|
||||
</div>
|
||||
<OFXImportDialog onImportComplete={refresh}>
|
||||
<Button>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
Importer OFX
|
||||
</Button>
|
||||
</OFXImportDialog>
|
||||
</div>
|
||||
<PageLayout>
|
||||
<PageHeader
|
||||
title="Transactions"
|
||||
description={`${filteredTransactions.length} transaction${filteredTransactions.length > 1 ? "s" : ""}`}
|
||||
actions={
|
||||
<OFXImportDialog onImportComplete={refresh}>
|
||||
<Button>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
Importer OFX
|
||||
</Button>
|
||||
</OFXImportDialog>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Filters */}
|
||||
<Card>
|
||||
<CardContent className="pt-4">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Rechercher..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TransactionFilters
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
selectedAccount={selectedAccount}
|
||||
onAccountChange={setSelectedAccount}
|
||||
selectedCategory={selectedCategory}
|
||||
onCategoryChange={setSelectedCategory}
|
||||
showReconciled={showReconciled}
|
||||
onReconciledChange={setShowReconciled}
|
||||
accounts={data.accounts}
|
||||
categories={data.categories}
|
||||
/>
|
||||
|
||||
<Select
|
||||
value={selectedAccount}
|
||||
onValueChange={setSelectedAccount}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Compte" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Tous les comptes</SelectItem>
|
||||
{data.accounts.map((account) => (
|
||||
<SelectItem key={account.id} value={account.id}>
|
||||
{account.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<TransactionBulkActions
|
||||
selectedCount={selectedTransactions.size}
|
||||
categories={data.categories}
|
||||
onReconcile={bulkReconcile}
|
||||
onSetCategory={bulkSetCategory}
|
||||
/>
|
||||
|
||||
<Select
|
||||
value={selectedCategory}
|
||||
onValueChange={setSelectedCategory}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Catégorie" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Toutes catégories</SelectItem>
|
||||
<SelectItem value="uncategorized">
|
||||
Non catégorisé
|
||||
</SelectItem>
|
||||
{data.categories.map((category) => (
|
||||
<SelectItem key={category.id} value={category.id}>
|
||||
{category.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={showReconciled}
|
||||
onValueChange={setShowReconciled}
|
||||
>
|
||||
<SelectTrigger className="w-[160px]">
|
||||
<SelectValue placeholder="Pointage" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Tout</SelectItem>
|
||||
<SelectItem value="reconciled">Pointées</SelectItem>
|
||||
<SelectItem value="not-reconciled">Non pointées</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Bulk actions */}
|
||||
{selectedTransactions.size > 0 && (
|
||||
<Card className="bg-primary/5 border-primary/20">
|
||||
<CardContent className="py-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm font-medium">
|
||||
{selectedTransactions.size} sélectionnée
|
||||
{selectedTransactions.size > 1 ? "s" : ""}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => bulkReconcile(true)}
|
||||
>
|
||||
<CheckCircle2 className="w-4 h-4 mr-1" />
|
||||
Pointer
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => bulkReconcile(false)}
|
||||
>
|
||||
<Circle className="w-4 h-4 mr-1" />
|
||||
Dépointer
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="outline">
|
||||
<Tags className="w-4 h-4 mr-1" />
|
||||
Catégoriser
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={() => bulkSetCategory(null)}>
|
||||
Aucune catégorie
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{data.categories.map((cat) => (
|
||||
<DropdownMenuItem
|
||||
key={cat.id}
|
||||
onClick={() => bulkSetCategory(cat.id)}
|
||||
>
|
||||
<CategoryIcon
|
||||
icon={cat.icon}
|
||||
color={cat.color}
|
||||
size={14}
|
||||
className="mr-2"
|
||||
/>
|
||||
{cat.name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Transactions list */}
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
{filteredTransactions.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 ===
|
||||
filteredTransactions.length &&
|
||||
filteredTransactions.length > 0
|
||||
}
|
||||
onCheckedChange={toggleSelectAll}
|
||||
/>
|
||||
</th>
|
||||
<th className="p-3 text-left">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (sortField === "date") {
|
||||
setSortOrder(
|
||||
sortOrder === "asc" ? "desc" : "asc",
|
||||
);
|
||||
} else {
|
||||
setSortField("date");
|
||||
setSortOrder("desc");
|
||||
}
|
||||
}}
|
||||
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={() => {
|
||||
if (sortField === "description") {
|
||||
setSortOrder(
|
||||
sortOrder === "asc" ? "desc" : "asc",
|
||||
);
|
||||
} else {
|
||||
setSortField("description");
|
||||
setSortOrder("asc");
|
||||
}
|
||||
}}
|
||||
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={() => {
|
||||
if (sortField === "amount") {
|
||||
setSortOrder(
|
||||
sortOrder === "asc" ? "desc" : "asc",
|
||||
);
|
||||
} else {
|
||||
setSortField("amount");
|
||||
setSortOrder("desc");
|
||||
}
|
||||
}}
|
||||
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>
|
||||
{filteredTransactions.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={() =>
|
||||
toggleSelectTransaction(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={() =>
|
||||
setCategory(transaction.id, null)
|
||||
}
|
||||
>
|
||||
Aucune catégorie
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{data.categories.map((cat) => (
|
||||
<DropdownMenuItem
|
||||
key={cat.id}
|
||||
onClick={() =>
|
||||
setCategory(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={() => toggleReconciled(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={() =>
|
||||
toggleReconciled(transaction.id)
|
||||
}
|
||||
>
|
||||
{transaction.isReconciled
|
||||
? "Dépointer"
|
||||
: "Pointer"}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<TransactionTable
|
||||
transactions={filteredTransactions}
|
||||
accounts={data.accounts}
|
||||
categories={data.categories}
|
||||
selectedTransactions={selectedTransactions}
|
||||
sortField={sortField}
|
||||
sortOrder={sortOrder}
|
||||
onSortChange={handleSortChange}
|
||||
onToggleSelectAll={toggleSelectAll}
|
||||
onToggleSelectTransaction={toggleSelectTransaction}
|
||||
onToggleReconciled={toggleReconciled}
|
||||
onSetCategory={setCategory}
|
||||
formatCurrency={formatCurrency}
|
||||
formatDate={formatDate}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user