feat: implement enhanced transaction filtering capabilities with support for account and category filters, improving data visibility and user interaction
This commit is contained in:
@@ -18,7 +18,7 @@ import {
|
||||
import { CategoryIcon } from "@/components/ui/category-icon";
|
||||
import { ChevronsUpDown, Check, Wallet, X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Account, Folder } from "@/lib/types";
|
||||
import type { Account, Folder, Transaction } from "@/lib/types";
|
||||
|
||||
interface AccountFilterComboboxProps {
|
||||
accounts: Account[];
|
||||
@@ -26,6 +26,7 @@ interface AccountFilterComboboxProps {
|
||||
value: string[]; // ["all"] | [accountId, accountId, ...]
|
||||
onChange: (value: string[]) => void;
|
||||
className?: string;
|
||||
filteredTransactions?: Transaction[]; // Transactions filtered by other filters (categories, period, etc.)
|
||||
}
|
||||
|
||||
export function AccountFilterCombobox({
|
||||
@@ -34,9 +35,31 @@ export function AccountFilterCombobox({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
filteredTransactions,
|
||||
}: AccountFilterComboboxProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Calculate total amount per account based on filtered transactions
|
||||
const accountTotals = useMemo(() => {
|
||||
if (!filteredTransactions) return {};
|
||||
|
||||
const totals: Record<string, number> = {};
|
||||
filteredTransactions.forEach((t) => {
|
||||
totals[t.accountId] = (totals[t.accountId] || 0) + t.amount;
|
||||
});
|
||||
|
||||
return totals;
|
||||
}, [filteredTransactions]);
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
// Get root folders (folders without parent) - same as folders/page.tsx
|
||||
const rootFolders = useMemo(
|
||||
() => folders.filter((f) => f.parentId === null),
|
||||
@@ -166,23 +189,31 @@ export function AccountFilterCombobox({
|
||||
</CommandItem>
|
||||
|
||||
{/* Accounts in this folder */}
|
||||
{folderAccounts.map((account) => (
|
||||
<CommandItem
|
||||
key={account.id}
|
||||
value={`${currentPath} ${account.name}`}
|
||||
onSelect={() => handleSelect(account.id)}
|
||||
style={{ paddingLeft: `${paddingLeft + 16}px` }}
|
||||
>
|
||||
<Wallet className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{account.name}</span>
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value.includes(account.id) ? "opacity-100" : "opacity-0"
|
||||
{folderAccounts.map((account) => {
|
||||
const total = accountTotals[account.id];
|
||||
return (
|
||||
<CommandItem
|
||||
key={account.id}
|
||||
value={`${currentPath} ${account.name}`}
|
||||
onSelect={() => handleSelect(account.id)}
|
||||
style={{ paddingLeft: `${paddingLeft + 16}px` }}
|
||||
>
|
||||
<Wallet className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{account.name}</span>
|
||||
{total !== undefined && (
|
||||
<span className="text-xs text-muted-foreground ml-1">
|
||||
({formatCurrency(total)})
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value.includes(account.id) ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Child folders - recursive */}
|
||||
{childFoldersList.map((childFolder) =>
|
||||
@@ -251,6 +282,13 @@ export function AccountFilterCombobox({
|
||||
<CommandItem value="all" onSelect={() => handleSelect("all")}>
|
||||
<Wallet className="h-4 w-4 text-muted-foreground" />
|
||||
<span>Tous les comptes</span>
|
||||
{filteredTransactions && (
|
||||
<span className="text-xs text-muted-foreground ml-1">
|
||||
({formatCurrency(
|
||||
filteredTransactions.reduce((sum, t) => sum + t.amount, 0)
|
||||
)})
|
||||
</span>
|
||||
)}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
@@ -266,22 +304,30 @@ export function AccountFilterCombobox({
|
||||
|
||||
{orphanAccounts.length > 0 && (
|
||||
<CommandGroup heading="Sans dossier">
|
||||
{orphanAccounts.map((account) => (
|
||||
<CommandItem
|
||||
key={account.id}
|
||||
value={`sans-dossier ${account.name}`}
|
||||
onSelect={() => handleSelect(account.id)}
|
||||
>
|
||||
<Wallet className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{account.name}</span>
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value.includes(account.id) ? "opacity-100" : "opacity-0"
|
||||
{orphanAccounts.map((account) => {
|
||||
const total = accountTotals[account.id];
|
||||
return (
|
||||
<CommandItem
|
||||
key={account.id}
|
||||
value={`sans-dossier ${account.name}`}
|
||||
onSelect={() => handleSelect(account.id)}
|
||||
>
|
||||
<Wallet className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{account.name}</span>
|
||||
{total !== undefined && (
|
||||
<span className="text-xs text-muted-foreground ml-1">
|
||||
({formatCurrency(total)})
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value.includes(account.id) ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
|
||||
@@ -18,13 +18,14 @@ import {
|
||||
import { CategoryIcon } from "@/components/ui/category-icon";
|
||||
import { ChevronsUpDown, Check, Tags, CircleSlash, X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Category } from "@/lib/types";
|
||||
import type { Category, Transaction } from "@/lib/types";
|
||||
|
||||
interface CategoryFilterComboboxProps {
|
||||
categories: Category[];
|
||||
value: string[]; // ["all"] | ["uncategorized"] | [categoryId, categoryId, ...]
|
||||
onChange: (value: string[]) => void;
|
||||
className?: string;
|
||||
filteredTransactions?: Transaction[]; // Transactions filtered by other filters (accounts, period, etc.)
|
||||
}
|
||||
|
||||
export function CategoryFilterCombobox({
|
||||
@@ -32,9 +33,23 @@ export function CategoryFilterCombobox({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
filteredTransactions,
|
||||
}: CategoryFilterComboboxProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Calculate transaction counts per category based on filtered transactions
|
||||
const categoryCounts = useMemo(() => {
|
||||
if (!filteredTransactions) return {};
|
||||
|
||||
const counts: Record<string, number> = {};
|
||||
filteredTransactions.forEach((t) => {
|
||||
const catId = t.categoryId || "uncategorized";
|
||||
counts[catId] = (counts[catId] || 0) + 1;
|
||||
});
|
||||
|
||||
return counts;
|
||||
}, [filteredTransactions]);
|
||||
|
||||
// Organize categories by parent
|
||||
const { parentCategories, childrenByParent } = useMemo(() => {
|
||||
const parents = categories.filter((c) => c.parentId === null);
|
||||
@@ -178,6 +193,11 @@ export function CategoryFilterCombobox({
|
||||
<CommandItem value="all" onSelect={() => handleSelect("all")}>
|
||||
<Tags className="h-4 w-4 text-muted-foreground" />
|
||||
<span>Toutes catégories</span>
|
||||
{filteredTransactions && (
|
||||
<span className="text-xs text-muted-foreground ml-1">
|
||||
({filteredTransactions.length})
|
||||
</span>
|
||||
)}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
@@ -191,6 +211,11 @@ export function CategoryFilterCombobox({
|
||||
>
|
||||
<CircleSlash className="h-4 w-4 text-muted-foreground" />
|
||||
<span>Non catégorisé</span>
|
||||
{categoryCounts["uncategorized"] !== undefined && (
|
||||
<span className="text-xs text-muted-foreground ml-1">
|
||||
({categoryCounts["uncategorized"]})
|
||||
</span>
|
||||
)}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
@@ -212,6 +237,11 @@ export function CategoryFilterCombobox({
|
||||
size={16}
|
||||
/>
|
||||
<span className="font-medium">{parent.name}</span>
|
||||
{categoryCounts[parent.id] !== undefined && (
|
||||
<span className="text-xs text-muted-foreground ml-1">
|
||||
({categoryCounts[parent.id]})
|
||||
</span>
|
||||
)}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
@@ -232,6 +262,11 @@ export function CategoryFilterCombobox({
|
||||
size={16}
|
||||
/>
|
||||
<span>{child.name}</span>
|
||||
{categoryCounts[child.id] !== undefined && (
|
||||
<span className="text-xs text-muted-foreground ml-1">
|
||||
({categoryCounts[child.id]})
|
||||
</span>
|
||||
)}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
|
||||
Reference in New Issue
Block a user