feat: refactor dashboard and account pages to utilize new layout components, enhancing structure and loading states
This commit is contained in:
4
components/transactions/index.ts
Normal file
4
components/transactions/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { TransactionFilters } from "./transaction-filters";
|
||||
export { TransactionBulkActions } from "./transaction-bulk-actions";
|
||||
export { TransactionTable } from "./transaction-table";
|
||||
|
||||
83
components/transactions/transaction-bulk-actions.tsx
Normal file
83
components/transactions/transaction-bulk-actions.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { CategoryIcon } from "@/components/ui/category-icon";
|
||||
import { CheckCircle2, Circle, Tags } from "lucide-react";
|
||||
import type { Category } from "@/lib/types";
|
||||
|
||||
interface TransactionBulkActionsProps {
|
||||
selectedCount: number;
|
||||
categories: Category[];
|
||||
onReconcile: (reconciled: boolean) => void;
|
||||
onSetCategory: (categoryId: string | null) => void;
|
||||
}
|
||||
|
||||
export function TransactionBulkActions({
|
||||
selectedCount,
|
||||
categories,
|
||||
onReconcile,
|
||||
onSetCategory,
|
||||
}: TransactionBulkActionsProps) {
|
||||
if (selectedCount === 0) return null;
|
||||
|
||||
return (
|
||||
<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">
|
||||
{selectedCount} sélectionnée{selectedCount > 1 ? "s" : ""}
|
||||
</span>
|
||||
<Button size="sm" variant="outline" onClick={() => onReconcile(true)}>
|
||||
<CheckCircle2 className="w-4 h-4 mr-1" />
|
||||
Pointer
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onReconcile(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={() => onSetCategory(null)}>
|
||||
Aucune catégorie
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{categories.map((cat) => (
|
||||
<DropdownMenuItem
|
||||
key={cat.id}
|
||||
onClick={() => onSetCategory(cat.id)}
|
||||
>
|
||||
<CategoryIcon
|
||||
icon={cat.icon}
|
||||
color={cat.color}
|
||||
size={14}
|
||||
className="mr-2"
|
||||
/>
|
||||
{cat.name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
100
components/transactions/transaction-filters.tsx
Normal file
100
components/transactions/transaction-filters.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Search } from "lucide-react";
|
||||
import type { Account, Category } from "@/lib/types";
|
||||
|
||||
interface TransactionFiltersProps {
|
||||
searchQuery: string;
|
||||
onSearchChange: (query: string) => void;
|
||||
selectedAccount: string;
|
||||
onAccountChange: (account: string) => void;
|
||||
selectedCategory: string;
|
||||
onCategoryChange: (category: string) => void;
|
||||
showReconciled: string;
|
||||
onReconciledChange: (value: string) => void;
|
||||
accounts: Account[];
|
||||
categories: Category[];
|
||||
}
|
||||
|
||||
export function TransactionFilters({
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
selectedAccount,
|
||||
onAccountChange,
|
||||
selectedCategory,
|
||||
onCategoryChange,
|
||||
showReconciled,
|
||||
onReconciledChange,
|
||||
accounts,
|
||||
categories,
|
||||
}: TransactionFiltersProps) {
|
||||
return (
|
||||
<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) => onSearchChange(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Select value={selectedAccount} onValueChange={onAccountChange}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Compte" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Tous les comptes</SelectItem>
|
||||
{accounts.map((account) => (
|
||||
<SelectItem key={account.id} value={account.id}>
|
||||
{account.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={selectedCategory} onValueChange={onCategoryChange}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Catégorie" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Toutes catégories</SelectItem>
|
||||
<SelectItem value="uncategorized">Non catégorisé</SelectItem>
|
||||
{categories.map((category) => (
|
||||
<SelectItem key={category.id} value={category.id}>
|
||||
{category.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={showReconciled} onValueChange={onReconciledChange}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
277
components/transactions/transaction-table.tsx
Normal file
277
components/transactions/transaction-table.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user