293 lines
9.2 KiB
TypeScript
293 lines
9.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useMemo } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
} from "@/components/ui/command";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
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";
|
|
|
|
interface AccountFilterComboboxProps {
|
|
accounts: Account[];
|
|
folders: Folder[];
|
|
value: string[]; // ["all"] | [accountId, accountId, ...]
|
|
onChange: (value: string[]) => void;
|
|
className?: string;
|
|
}
|
|
|
|
export function AccountFilterCombobox({
|
|
accounts,
|
|
folders,
|
|
value,
|
|
onChange,
|
|
className,
|
|
}: AccountFilterComboboxProps) {
|
|
const [open, setOpen] = useState(false);
|
|
|
|
// Get root folders (folders without parent) - same as folders/page.tsx
|
|
const rootFolders = useMemo(
|
|
() => folders.filter((f) => f.parentId === null),
|
|
[folders]
|
|
);
|
|
|
|
// Get child folders for a given parent - same as FolderTreeItem
|
|
const getChildFolders = (parentId: string) =>
|
|
folders.filter((f) => f.parentId === parentId);
|
|
|
|
// Get accounts in a specific folder - same as FolderTreeItem
|
|
const getFolderAccounts = (folderId: string) =>
|
|
accounts.filter((a) => a.folderId === folderId);
|
|
|
|
// Get accounts without folder
|
|
const orphanAccounts = useMemo(
|
|
() => accounts.filter((a) => !a.folderId),
|
|
[accounts]
|
|
);
|
|
|
|
const selectedAccounts = accounts.filter((a) => value.includes(a.id));
|
|
const isAll = value.includes("all") || value.length === 0;
|
|
|
|
// Get all accounts in a folder and its descendants recursively
|
|
const getAllAccountsInFolder = (folderId: string): Account[] => {
|
|
const directAccounts = getFolderAccounts(folderId);
|
|
const childFoldersList = getChildFolders(folderId);
|
|
const childAccounts = childFoldersList.flatMap((cf) =>
|
|
getAllAccountsInFolder(cf.id)
|
|
);
|
|
return [...directAccounts, ...childAccounts];
|
|
};
|
|
|
|
const handleSelect = (newValue: string) => {
|
|
if (newValue === "all") {
|
|
onChange(["all"]);
|
|
return;
|
|
}
|
|
|
|
let newSelection: string[];
|
|
|
|
if (isAll) {
|
|
newSelection = [newValue];
|
|
} else if (value.includes(newValue)) {
|
|
newSelection = value.filter((v) => v !== newValue);
|
|
if (newSelection.length === 0) {
|
|
newSelection = ["all"];
|
|
}
|
|
} else {
|
|
newSelection = [...value, newValue];
|
|
}
|
|
|
|
onChange(newSelection);
|
|
};
|
|
|
|
const handleSelectFolder = (folderId: string) => {
|
|
const allFolderAccounts = getAllAccountsInFolder(folderId);
|
|
const allFolderAccountIds = allFolderAccounts.map((a) => a.id);
|
|
|
|
if (allFolderAccountIds.length === 0) return;
|
|
|
|
const allSelected = allFolderAccountIds.every((id) => value.includes(id));
|
|
|
|
if (allSelected) {
|
|
const newSelection = value.filter(
|
|
(v) => !allFolderAccountIds.includes(v)
|
|
);
|
|
onChange(newSelection.length > 0 ? newSelection : ["all"]);
|
|
} else {
|
|
if (isAll) {
|
|
onChange(allFolderAccountIds);
|
|
} else {
|
|
const newSelection = [...new Set([...value, ...allFolderAccountIds])];
|
|
onChange(newSelection);
|
|
}
|
|
}
|
|
};
|
|
|
|
const clearSelection = () => {
|
|
onChange(["all"]);
|
|
};
|
|
|
|
const isFolderSelected = (folderId: string) => {
|
|
const folderAccounts = getAllAccountsInFolder(folderId);
|
|
if (folderAccounts.length === 0) return false;
|
|
return folderAccounts.every((a) => value.includes(a.id));
|
|
};
|
|
|
|
const isFolderPartiallySelected = (folderId: string) => {
|
|
const folderAccounts = getAllAccountsInFolder(folderId);
|
|
if (folderAccounts.length === 0) return false;
|
|
const selectedCount = folderAccounts.filter((a) =>
|
|
value.includes(a.id)
|
|
).length;
|
|
return selectedCount > 0 && selectedCount < folderAccounts.length;
|
|
};
|
|
|
|
// Recursive render function - mirrors FolderTreeItem logic
|
|
const renderFolder = (folder: Folder, depth: number, parentPath: string) => {
|
|
const folderAccounts = getFolderAccounts(folder.id);
|
|
const childFoldersList = getChildFolders(folder.id);
|
|
const currentPath = parentPath ? `${parentPath} ${folder.name}` : folder.name;
|
|
const paddingLeft = depth * 16 + 8;
|
|
|
|
return (
|
|
<div key={folder.id}>
|
|
{/* Folder row */}
|
|
<CommandItem
|
|
value={`folder-${currentPath}`}
|
|
onSelect={() => handleSelectFolder(folder.id)}
|
|
style={{ paddingLeft: `${paddingLeft}px` }}
|
|
className="font-medium"
|
|
>
|
|
<CategoryIcon icon={folder.icon} color={folder.color} size={16} />
|
|
<span>{folder.name}</span>
|
|
<div className="ml-auto flex items-center">
|
|
{isFolderPartiallySelected(folder.id) && (
|
|
<div className="h-3 w-3 rounded-sm bg-primary/50 mr-1" />
|
|
)}
|
|
<Check
|
|
className={cn(
|
|
"h-4 w-4",
|
|
isFolderSelected(folder.id) ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
</div>
|
|
</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"
|
|
)}
|
|
/>
|
|
</CommandItem>
|
|
))}
|
|
|
|
{/* Child folders - recursive */}
|
|
{childFoldersList.map((childFolder) =>
|
|
renderFolder(childFolder, depth + 1, currentPath)
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen} modal={true}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={open}
|
|
className={cn("justify-between", className)}
|
|
>
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
{selectedAccounts.length === 1 ? (
|
|
<>
|
|
<Wallet className="h-4 w-4 text-muted-foreground" />
|
|
<span className="truncate">{selectedAccounts[0].name}</span>
|
|
</>
|
|
) : selectedAccounts.length > 1 ? (
|
|
<>
|
|
<Wallet className="h-4 w-4 text-muted-foreground" />
|
|
<span className="truncate">
|
|
{selectedAccounts.length} comptes
|
|
</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Wallet className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-muted-foreground">Tous les comptes</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
{!isAll && (
|
|
<div
|
|
role="button"
|
|
className="h-4 w-4 rounded-sm hover:bg-muted flex items-center justify-center"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
clearSelection();
|
|
}}
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</div>
|
|
)}
|
|
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
|
|
</div>
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
className="w-[300px] p-0"
|
|
align="start"
|
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
>
|
|
<Command>
|
|
<CommandInput placeholder="Rechercher..." />
|
|
<CommandList className="max-h-[300px]">
|
|
<CommandEmpty>Aucun compte trouvé.</CommandEmpty>
|
|
<CommandGroup>
|
|
<CommandItem value="all" onSelect={() => handleSelect("all")}>
|
|
<Wallet className="h-4 w-4 text-muted-foreground" />
|
|
<span>Tous les comptes</span>
|
|
<Check
|
|
className={cn(
|
|
"ml-auto h-4 w-4",
|
|
isAll ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
</CommandItem>
|
|
</CommandGroup>
|
|
|
|
<CommandGroup>
|
|
{rootFolders.map((folder) => renderFolder(folder, 0, ""))}
|
|
</CommandGroup>
|
|
|
|
{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"
|
|
)}
|
|
/>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
)}
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
}
|