chore: clean up code by removing trailing whitespace and ensuring consistent formatting across various files = prettier
This commit is contained in:
@@ -32,4 +32,3 @@ export function AccountBulkActions({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,13 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { MoreVertical, Pencil, Trash2, ExternalLink, GripVertical } from "lucide-react";
|
||||
import {
|
||||
MoreVertical,
|
||||
Pencil,
|
||||
Trash2,
|
||||
ExternalLink,
|
||||
GripVertical,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import type { Account, Folder } from "@/lib/types";
|
||||
@@ -69,7 +75,13 @@ export function AccountCard({
|
||||
};
|
||||
|
||||
const cardContent = (
|
||||
<Card className={cn("relative", isSelected && "ring-2 ring-primary", isDragging && "bg-muted/80")}>
|
||||
<Card
|
||||
className={cn(
|
||||
"relative",
|
||||
isSelected && "ring-2 ring-primary",
|
||||
isDragging && "bg-muted/80",
|
||||
)}
|
||||
>
|
||||
<CardHeader className="pb-0">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
@@ -96,7 +108,9 @@ export function AccountCard({
|
||||
<Icon className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<CardTitle className="text-sm font-semibold truncate">{account.name}</CardTitle>
|
||||
<CardTitle className="text-sm font-semibold truncate">
|
||||
{account.name}
|
||||
</CardTitle>
|
||||
{!compact && (
|
||||
<>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
@@ -140,7 +154,7 @@ export function AccountCard({
|
||||
compact ? "text-lg" : "text-xl",
|
||||
"font-bold",
|
||||
!compact && "mb-1.5",
|
||||
realBalance >= 0 ? "text-emerald-600" : "text-red-600"
|
||||
realBalance >= 0 ? "text-emerald-600" : "text-red-600",
|
||||
)}
|
||||
>
|
||||
{formatCurrency(realBalance)}
|
||||
@@ -165,11 +179,12 @@ export function AccountCard({
|
||||
</Link>
|
||||
{folder && <span className="truncate ml-2">{folder.name}</span>}
|
||||
</div>
|
||||
{account.initialBalance !== undefined && account.initialBalance !== null && (
|
||||
<p className="text-xs text-muted-foreground mt-1.5">
|
||||
Solde initial: {formatCurrency(account.initialBalance)}
|
||||
</p>
|
||||
)}
|
||||
{account.initialBalance !== undefined &&
|
||||
account.initialBalance !== null && (
|
||||
<p className="text-xs text-muted-foreground mt-1.5">
|
||||
Solde initial: {formatCurrency(account.initialBalance)}
|
||||
</p>
|
||||
)}
|
||||
{account.lastImport && (
|
||||
<p className="text-xs text-muted-foreground mt-1.5">
|
||||
Dernier import:{" "}
|
||||
@@ -203,4 +218,3 @@ export function AccountCard({
|
||||
|
||||
return cardContent;
|
||||
}
|
||||
|
||||
|
||||
@@ -142,4 +142,3 @@ export function AccountEditDialog({
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,4 +13,3 @@ export const accountTypeLabels = {
|
||||
CREDIT_CARD: "Carte de crédit",
|
||||
OTHER: "Autre",
|
||||
};
|
||||
|
||||
|
||||
@@ -2,4 +2,3 @@ export { AccountCard } from "./account-card";
|
||||
export { AccountEditDialog } from "./account-edit-dialog";
|
||||
export { AccountBulkActions } from "./account-bulk-actions";
|
||||
export { accountTypeIcons, accountTypeLabels } from "./constants";
|
||||
|
||||
|
||||
@@ -80,4 +80,3 @@ export function CategoryCard({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ export function CategoryEditDialog({
|
||||
className={cn(
|
||||
"w-7 h-7 rounded-full transition-transform",
|
||||
formData.color === color &&
|
||||
"ring-2 ring-offset-2 ring-primary scale-110"
|
||||
"ring-2 ring-offset-2 ring-primary scale-110",
|
||||
)}
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
@@ -201,4 +201,3 @@ export function CategoryEditDialog({
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,4 +43,3 @@ export function CategorySearchBar({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,4 +15,3 @@ export const categoryColors = [
|
||||
"#0891b2",
|
||||
"#dc2626",
|
||||
];
|
||||
|
||||
|
||||
@@ -3,4 +3,3 @@ export { CategoryEditDialog } from "./category-edit-dialog";
|
||||
export { ParentCategoryRow } from "./parent-category-row";
|
||||
export { CategorySearchBar } from "./category-search-bar";
|
||||
export { categoryColors } from "./constants";
|
||||
|
||||
|
||||
@@ -73,7 +73,9 @@ export function ParentCategoryRow({
|
||||
size={isMobile ? 10 : 14}
|
||||
/>
|
||||
</div>
|
||||
<span className="font-medium text-xs md:text-sm truncate">{parent.name}</span>
|
||||
<span className="font-medium text-xs md:text-sm truncate">
|
||||
{parent.name}
|
||||
</span>
|
||||
{!isMobile && (
|
||||
<span className="text-xs md:text-sm text-muted-foreground shrink-0">
|
||||
{children.length} • {stats.count} opération
|
||||
@@ -102,7 +104,11 @@ export function ParentCategoryRow({
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 md:h-7 md:w-7">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 md:h-7 md:w-7"
|
||||
>
|
||||
<MoreVertical className="w-3 h-3 md:w-4 md:h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -147,4 +153,3 @@ export function ParentCategoryRow({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export function AccountsSummary({ data }: AccountsSummaryProps) {
|
||||
// Group accounts by folder
|
||||
const accountsByFolder = useMemo(() => {
|
||||
const grouped: Record<string, Account[]> = {};
|
||||
|
||||
|
||||
data.accounts.forEach((account) => {
|
||||
const folderId = account.folderId || "no-folder";
|
||||
if (!grouped[folderId]) {
|
||||
@@ -72,7 +72,12 @@ export function AccountsSummary({ data }: AccountsSummaryProps) {
|
||||
{/* Folder header */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<FolderIcon className="w-4 h-4 text-muted-foreground" />
|
||||
<h3 className={cn("font-semibold text-sm", level > 0 && "text-muted-foreground")}>
|
||||
<h3
|
||||
className={cn(
|
||||
"font-semibold text-sm",
|
||||
level > 0 && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{folder.name}
|
||||
</h3>
|
||||
{folderAccounts.length > 0 && (
|
||||
@@ -122,9 +127,7 @@ export function AccountsSummary({ data }: AccountsSummaryProps) {
|
||||
<span
|
||||
className={cn(
|
||||
"font-semibold tabular-nums",
|
||||
realBalance >= 0
|
||||
? "text-emerald-600"
|
||||
: "text-red-600",
|
||||
realBalance >= 0 ? "text-emerald-600" : "text-red-600",
|
||||
)}
|
||||
>
|
||||
{formatCurrency(realBalance)}
|
||||
@@ -218,7 +221,9 @@ export function AccountsSummary({ data }: AccountsSummaryProps) {
|
||||
<Building2 className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-sm">{account.name}</p>
|
||||
<p className="font-medium text-sm">
|
||||
{account.name}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{account.accountNumber}
|
||||
</p>
|
||||
|
||||
@@ -17,7 +17,7 @@ export function CategoryBreakdown({ data }: CategoryBreakdownProps) {
|
||||
const thisMonthStr = thisMonth.toISOString().slice(0, 7);
|
||||
|
||||
const monthExpenses = data.transactions.filter(
|
||||
(t) => t.date.startsWith(thisMonthStr) && t.amount < 0
|
||||
(t) => t.date.startsWith(thisMonthStr) && t.amount < 0,
|
||||
);
|
||||
|
||||
const categoryTotals = new Map<string, number>();
|
||||
|
||||
@@ -116,7 +116,9 @@ export function OverviewCards({ data }: OverviewCardsProps) {
|
||||
<CreditCard className="h-3 w-3 md:h-4 md:w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xl md:text-2xl font-bold">{reconciledPercent}%</div>
|
||||
<div className="text-xl md:text-2xl font-bold">
|
||||
{reconciledPercent}%
|
||||
</div>
|
||||
<p className="text-[10px] md:text-xs text-muted-foreground mt-1">
|
||||
{reconciled} / {total} opérations pointées
|
||||
</p>
|
||||
|
||||
@@ -60,7 +60,9 @@ export function RecentTransactions({ data }: RecentTransactionsProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm md:text-base">Transactions récentes</CardTitle>
|
||||
<CardTitle className="text-sm md:text-base">
|
||||
Transactions récentes
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-3 md:px-6">
|
||||
<div className="space-y-3">
|
||||
|
||||
@@ -36,7 +36,11 @@ interface SidebarContentProps {
|
||||
onNavigate?: () => void;
|
||||
}
|
||||
|
||||
function SidebarContent({ collapsed = false, onNavigate, showHeader = false }: SidebarContentProps & { showHeader?: boolean }) {
|
||||
function SidebarContent({
|
||||
collapsed = false,
|
||||
onNavigate,
|
||||
showHeader = false,
|
||||
}: SidebarContentProps & { showHeader?: boolean }) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -134,7 +138,10 @@ export function Sidebar({ open, onOpenChange }: SidebarProps) {
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent side="left" className="w-64 p-0">
|
||||
<div className="flex flex-col h-full">
|
||||
<SidebarContent showHeader onNavigate={() => onOpenChange?.(false)} />
|
||||
<SidebarContent
|
||||
showHeader
|
||||
onNavigate={() => onOpenChange?.(false)}
|
||||
/>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
@@ -115,4 +115,3 @@ export function AccountFolderDialog({
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,4 +13,3 @@ export const accountTypeLabels = {
|
||||
CREDIT_CARD: "Carte de crédit",
|
||||
OTHER: "Autre",
|
||||
};
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export function DraggableAccountItem({
|
||||
style={style}
|
||||
className={cn(
|
||||
"flex items-center gap-2 p-2 rounded-lg hover:bg-muted/50 group ml-12",
|
||||
isDragging && "bg-muted/80"
|
||||
isDragging && "bg-muted/80",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
@@ -66,14 +66,15 @@ export function DraggableAccountItem({
|
||||
{account.name}
|
||||
{account.accountNumber && (
|
||||
<span className="text-muted-foreground">
|
||||
{" "}({account.accountNumber})
|
||||
{" "}
|
||||
({account.accountNumber})
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm tabular-nums",
|
||||
realBalance >= 0 ? "text-emerald-600" : "text-red-600"
|
||||
realBalance >= 0 ? "text-emerald-600" : "text-red-600",
|
||||
)}
|
||||
>
|
||||
{formatCurrency(realBalance)}
|
||||
@@ -89,4 +90,3 @@ export function DraggableAccountItem({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export function DraggableFolderItem({
|
||||
className={cn(
|
||||
"flex items-center gap-2 p-2 rounded-lg hover:bg-muted/50 group",
|
||||
level > 0 && "ml-6",
|
||||
isDragging && "bg-muted/80"
|
||||
isDragging && "bg-muted/80",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
@@ -120,7 +120,7 @@ export function DraggableFolderItem({
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm font-semibold tabular-nums",
|
||||
folderTotal >= 0 ? "text-emerald-600" : "text-red-600"
|
||||
folderTotal >= 0 ? "text-emerald-600" : "text-red-600",
|
||||
)}
|
||||
>
|
||||
{formatCurrency(folderTotal)}
|
||||
@@ -157,4 +157,3 @@ export function DraggableFolderItem({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -96,11 +96,13 @@ export function FolderEditDialog({
|
||||
{folderColors.map(({ value }) => (
|
||||
<button
|
||||
key={value}
|
||||
onClick={() => onFormDataChange({ ...formData, color: value })}
|
||||
onClick={() =>
|
||||
onFormDataChange({ ...formData, color: value })
|
||||
}
|
||||
className={cn(
|
||||
"w-8 h-8 rounded-full transition-transform",
|
||||
formData.color === value &&
|
||||
"ring-2 ring-offset-2 ring-primary scale-110"
|
||||
"ring-2 ring-offset-2 ring-primary scale-110",
|
||||
)}
|
||||
style={{ backgroundColor: value }}
|
||||
/>
|
||||
@@ -120,4 +122,3 @@ export function FolderEditDialog({
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export function FolderTreeItem({
|
||||
const folderAccounts = accounts.filter(
|
||||
(a) =>
|
||||
a.folderId === folder.id ||
|
||||
(folder.id === "folder-root" && a.folderId === null)
|
||||
(folder.id === "folder-root" && a.folderId === null),
|
||||
);
|
||||
const childFolders = allFolders.filter((f) => f.parentId === folder.id);
|
||||
const folderTotal = folderAccounts.reduce(
|
||||
@@ -88,4 +88,3 @@ export function FolderTreeItem({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,4 +4,3 @@ export { AccountFolderDialog } from "./account-folder-dialog";
|
||||
export { DraggableFolderItem } from "./draggable-folder-item";
|
||||
export { DraggableAccountItem } from "./draggable-account-item";
|
||||
export { folderColors, accountTypeLabels } from "./constants";
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { PageLayout } from "./page-layout";
|
||||
export { LoadingState } from "./loading-state";
|
||||
export { PageHeader } from "./page-header";
|
||||
|
||||
|
||||
@@ -13,4 +13,3 @@ export function LoadingState() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,9 +37,13 @@ export function PageHeader({
|
||||
</Button>
|
||||
)}
|
||||
<div>
|
||||
<h1 className="text-lg md:text-2xl font-bold text-foreground">{title}</h1>
|
||||
<h1 className="text-lg md:text-2xl font-bold text-foreground">
|
||||
{title}
|
||||
</h1>
|
||||
{description && (
|
||||
<div className="text-xs md:text-base text-muted-foreground mt-1">{description}</div>
|
||||
<div className="text-xs md:text-base text-muted-foreground mt-1">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,4 +60,3 @@ export function PageHeader({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,14 +12,17 @@ export function PageLayout({ children }: PageLayoutProps) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={{ open: sidebarOpen, setOpen: setSidebarOpen }}>
|
||||
<SidebarContext.Provider
|
||||
value={{ open: sidebarOpen, setOpen: setSidebarOpen }}
|
||||
>
|
||||
<div className="flex h-screen bg-background overflow-hidden">
|
||||
<Sidebar open={sidebarOpen} onOpenChange={setSidebarOpen} />
|
||||
<main className="flex-1 overflow-auto overflow-x-hidden">
|
||||
<div className="p-4 md:p-6 space-y-4 md:space-y-6 max-w-full">{children}</div>
|
||||
<div className="p-4 md:p-6 space-y-4 md:space-y-6 max-w-full">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,4 +15,3 @@ export const SidebarContext = createContext<SidebarContextType>({
|
||||
export function useSidebarContext() {
|
||||
return useContext(SidebarContext);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ import type { ReactNode } from "react";
|
||||
export function AuthSessionProvider({ children }: { children: ReactNode }) {
|
||||
return <SessionProvider>{children}</SessionProvider>;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ export function suggestKeyword(descriptions: string[]): string {
|
||||
if (sorted.length > 0) {
|
||||
// Return the longest frequent keyword
|
||||
return sorted.reduce((best, current) =>
|
||||
current[0].length > best[0].length ? current : best
|
||||
current[0].length > best[0].length ? current : best,
|
||||
)[0];
|
||||
}
|
||||
|
||||
@@ -92,4 +92,3 @@ export function suggestKeyword(descriptions: string[]): string {
|
||||
const firstKeywords = extractKeywords(descriptions[0]);
|
||||
return firstKeywords[0] || descriptions[0].slice(0, 15);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { RuleGroupCard } from "./rule-group-card";
|
||||
export { RuleCreateDialog } from "./rule-create-dialog";
|
||||
export { RulesSearchBar } from "./rules-search-bar";
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ export function RuleCreateDialog({
|
||||
if (!keyword) return null;
|
||||
const lowerKeyword = keyword.toLowerCase();
|
||||
return categories.find((c) =>
|
||||
c.keywords.some((k) => k.toLowerCase() === lowerKeyword)
|
||||
c.keywords.some((k) => k.toLowerCase() === lowerKeyword),
|
||||
);
|
||||
}, [keyword, categories]);
|
||||
|
||||
@@ -136,7 +136,8 @@ export function RuleCreateDialog({
|
||||
<div className="flex items-center gap-2 text-xs text-amber-600 dark:text-amber-400">
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
<span>
|
||||
Ce mot-clé existe déjà dans "{existingCategory.name}"
|
||||
Ce mot-clé existe déjà dans "{existingCategory.name}
|
||||
"
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -202,8 +203,9 @@ export function RuleCreateDialog({
|
||||
<div className="flex items-center gap-2 text-sm text-success">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
<span>
|
||||
Le mot-clé "<strong>{keyword}</strong>" sera ajouté à la
|
||||
catégorie "<strong>{selectedCategory?.name}</strong>"
|
||||
Le mot-clé "<strong>{keyword}</strong>" sera ajouté
|
||||
à la catégorie "<strong>{selectedCategory?.name}</strong>
|
||||
"
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,4 +227,3 @@ export function RuleCreateDialog({
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,9 @@ export function RuleGroupCard({
|
||||
formatCurrency,
|
||||
formatDate,
|
||||
}: RuleGroupCardProps) {
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null);
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const avgAmount =
|
||||
@@ -59,7 +61,11 @@ export function RuleGroupCard({
|
||||
onClick={onToggleExpand}
|
||||
>
|
||||
<div className="flex items-center gap-2 md:gap-3 flex-1 min-w-0">
|
||||
<Button variant="ghost" size="icon" className="h-5 w-5 md:h-6 md:w-6 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5 md:h-6 md:w-6 shrink-0"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-3 w-3 md:h-4 md:w-4" />
|
||||
) : (
|
||||
@@ -72,7 +78,10 @@ export function RuleGroupCard({
|
||||
<span className="font-medium text-xs md:text-base text-foreground truncate">
|
||||
{group.displayName}
|
||||
</span>
|
||||
<Badge variant="secondary" className="text-[10px] md:text-xs shrink-0">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-[10px] md:text-xs shrink-0"
|
||||
>
|
||||
{group.transactions.length} 💳
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -91,7 +100,7 @@ export function RuleGroupCard({
|
||||
<div
|
||||
className={cn(
|
||||
"font-semibold tabular-nums text-sm",
|
||||
isDebit ? "text-destructive" : "text-success"
|
||||
isDebit ? "text-destructive" : "text-success",
|
||||
)}
|
||||
>
|
||||
{formatCurrency(group.totalAmount)}
|
||||
@@ -158,10 +167,7 @@ export function RuleGroupCard({
|
||||
{isMobile ? (
|
||||
<div className="max-h-64 overflow-y-auto divide-y divide-border">
|
||||
{group.transactions.map((transaction) => (
|
||||
<div
|
||||
key={transaction.id}
|
||||
className="p-3 hover:bg-muted/50"
|
||||
>
|
||||
<div key={transaction.id} className="p-3 hover:bg-muted/50">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs md:text-sm font-medium truncate">
|
||||
@@ -181,7 +187,7 @@ export function RuleGroupCard({
|
||||
"text-xs md:text-sm font-semibold tabular-nums shrink-0",
|
||||
transaction.amount < 0
|
||||
? "text-destructive"
|
||||
: "text-success"
|
||||
: "text-success",
|
||||
)}
|
||||
>
|
||||
{formatCurrency(transaction.amount)}
|
||||
@@ -228,7 +234,7 @@ export function RuleGroupCard({
|
||||
"px-4 py-2 text-right tabular-nums whitespace-nowrap",
|
||||
transaction.amount < 0
|
||||
? "text-destructive"
|
||||
: "text-success"
|
||||
: "text-success",
|
||||
)}
|
||||
>
|
||||
{formatCurrency(transaction.amount)}
|
||||
@@ -244,4 +250,3 @@ export function RuleGroupCard({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -75,4 +75,3 @@ export function RulesSearchBar({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,13 +37,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import {
|
||||
Database,
|
||||
Trash2,
|
||||
RotateCcw,
|
||||
Save,
|
||||
Clock,
|
||||
} from "lucide-react";
|
||||
import { Database, Trash2, RotateCcw, Save, Clock } from "lucide-react";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { fr } from "date-fns/locale/fr";
|
||||
import { toast } from "sonner";
|
||||
@@ -84,10 +78,17 @@ export function BackupCard() {
|
||||
|
||||
if (backupsData.success) {
|
||||
setBackups(
|
||||
backupsData.data.map((b: { id: string; filename: string; size: number; createdAt: string }) => ({
|
||||
...b,
|
||||
createdAt: new Date(b.createdAt),
|
||||
}))
|
||||
backupsData.data.map(
|
||||
(b: {
|
||||
id: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
createdAt: string;
|
||||
}) => ({
|
||||
...b,
|
||||
createdAt: new Date(b.createdAt),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,7 +117,9 @@ export function BackupCard() {
|
||||
|
||||
if (data.success) {
|
||||
if (data.data.skipped) {
|
||||
toast.info("Aucun changement détecté. La dernière sauvegarde a été mise à jour.");
|
||||
toast.info(
|
||||
"Aucun changement détecté. La dernière sauvegarde a été mise à jour.",
|
||||
);
|
||||
} else {
|
||||
toast.success("Sauvegarde créée avec succès");
|
||||
}
|
||||
@@ -160,7 +163,9 @@ export function BackupCard() {
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
toast.success("Sauvegarde restaurée avec succès. Rechargement de la page...");
|
||||
toast.success(
|
||||
"Sauvegarde restaurée avec succès. Rechargement de la page...",
|
||||
);
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
@@ -258,9 +263,9 @@ export function BackupCard() {
|
||||
<Label htmlFor="backup-frequency">Fréquence</Label>
|
||||
<Select
|
||||
value={settings.frequency}
|
||||
onValueChange={(value: "hourly" | "daily" | "weekly" | "monthly") =>
|
||||
handleSettingsChange({ frequency: value })
|
||||
}
|
||||
onValueChange={(
|
||||
value: "hourly" | "daily" | "weekly" | "monthly",
|
||||
) => handleSettingsChange({ frequency: value })}
|
||||
>
|
||||
<SelectTrigger id="backup-frequency">
|
||||
<SelectValue />
|
||||
@@ -369,17 +374,16 @@ export function BackupCard() {
|
||||
Restaurer cette sauvegarde ?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Cette action va remplacer votre base de données
|
||||
actuelle par cette sauvegarde. Une sauvegarde
|
||||
de sécurité sera créée avant la restauration.
|
||||
Cette action va remplacer votre base de
|
||||
données actuelle par cette sauvegarde. Une
|
||||
sauvegarde de sécurité sera créée avant la
|
||||
restauration.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() =>
|
||||
handleRestoreBackup(backup.id)
|
||||
}
|
||||
onClick={() => handleRestoreBackup(backup.id)}
|
||||
>
|
||||
Restaurer
|
||||
</AlertDialogAction>
|
||||
@@ -406,9 +410,7 @@ export function BackupCard() {
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() =>
|
||||
handleDeleteBackup(backup.id)
|
||||
}
|
||||
onClick={() => handleDeleteBackup(backup.id)}
|
||||
>
|
||||
Supprimer
|
||||
</AlertDialogAction>
|
||||
@@ -434,4 +436,3 @@ export function BackupCard() {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,10 @@ interface DangerZoneCardProps {
|
||||
categorizedCount: number;
|
||||
onClearCategories: () => void;
|
||||
onResetData: () => void;
|
||||
onDeduplicate: () => Promise<{ deletedCount: number; duplicatesFound: number }>;
|
||||
onDeduplicate: () => Promise<{
|
||||
deletedCount: number;
|
||||
duplicatesFound: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function DangerZoneCard({
|
||||
@@ -42,7 +45,9 @@ export function DangerZoneCard({
|
||||
try {
|
||||
const result = await onDeduplicate();
|
||||
if (result.deletedCount > 0) {
|
||||
alert(`${result.deletedCount} transaction${result.deletedCount > 1 ? "s" : ""} en double supprimée${result.deletedCount > 1 ? "s" : ""}`);
|
||||
alert(
|
||||
`${result.deletedCount} transaction${result.deletedCount > 1 ? "s" : ""} en double supprimée${result.deletedCount > 1 ? "s" : ""}`,
|
||||
);
|
||||
} else {
|
||||
alert("Aucun doublon trouvé");
|
||||
}
|
||||
@@ -88,10 +93,11 @@ export function DangerZoneCard({
|
||||
Dédoublonner les transactions ?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Cette action va rechercher et supprimer les transactions en double
|
||||
dans votre base de données. Les critères de dédoublonnage sont :
|
||||
même compte, même date, même montant et même libellé. La première
|
||||
transaction trouvée sera conservée, les autres seront supprimées.
|
||||
Cette action va rechercher et supprimer les transactions en
|
||||
double dans votre base de données. Les critères de dédoublonnage
|
||||
sont : même compte, même date, même montant et même libellé. La
|
||||
première transaction trouvée sera conservée, les autres seront
|
||||
supprimées.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
@@ -131,8 +137,8 @@ export function DangerZoneCard({
|
||||
<AlertDialogDescription>
|
||||
Cette action va retirer la catégorie de {categorizedCount}{" "}
|
||||
opération{categorizedCount > 1 ? "s" : ""}. Les catégories
|
||||
elles-mêmes ne seront pas supprimées, seulement leur
|
||||
affectation aux opérations.
|
||||
elles-mêmes ne seront pas supprimées, seulement leur affectation
|
||||
aux opérations.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
@@ -179,4 +185,3 @@ export function DangerZoneCard({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,4 +70,3 @@ export function DataCard({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,4 +3,3 @@ export { DangerZoneCard } from "./danger-zone-card";
|
||||
export { OFXInfoCard } from "./ofx-info-card";
|
||||
export { BackupCard } from "./backup-card";
|
||||
export { PasswordCard } from "./password-card";
|
||||
|
||||
|
||||
@@ -17,9 +17,7 @@ export function OFXInfoCard() {
|
||||
<FileJson className="w-5 h-5" />
|
||||
Format OFX
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Informations sur l'import de fichiers
|
||||
</CardDescription>
|
||||
<CardDescription>Informations sur l'import de fichiers</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="prose prose-sm text-muted-foreground">
|
||||
@@ -29,13 +27,12 @@ export function OFXInfoCard() {
|
||||
l'espace client de votre banque.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Lors de l'import, les transactions sont automatiquement
|
||||
catégorisées selon les mots-clés définis. Les doublons sont détectés
|
||||
et ignorés automatiquement.
|
||||
Lors de l'import, les transactions sont automatiquement catégorisées
|
||||
selon les mots-clés définis. Les doublons sont détectés et ignorés
|
||||
automatiquement.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -158,7 +158,9 @@ export function PasswordCard() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirm-password">Confirmer le mot de passe</Label>
|
||||
<Label htmlFor="confirm-password">
|
||||
Confirmer le mot de passe
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="confirm-password"
|
||||
@@ -199,4 +201,3 @@ export function PasswordCard() {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -158,7 +158,9 @@ export function BalanceLineChart({
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{
|
||||
backgroundColor: entry.color,
|
||||
transform: isHovered ? "scale(1.2)" : "scale(1)",
|
||||
transform: isHovered
|
||||
? "scale(1.2)"
|
||||
: "scale(1)",
|
||||
transition: "transform 0.15s",
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -115,4 +115,3 @@ export function CategoryBarChart({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,7 @@ import { useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CategoryIcon } from "@/components/ui/category-icon";
|
||||
import {
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from "recharts";
|
||||
import { Layers, List, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import type { Category } from "@/lib/types";
|
||||
|
||||
@@ -48,8 +42,8 @@ export function CategoryPieChart({
|
||||
const [groupByParent, setGroupByParent] = useState(true);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const hasParentData = dataByParent && dataByParent.length > 0;
|
||||
const baseData = (groupByParent && hasParentData) ? dataByParent : data;
|
||||
|
||||
const baseData = groupByParent && hasParentData ? dataByParent : data;
|
||||
|
||||
// Limit to top 8 by default, show all if expanded
|
||||
const maxItems = 8;
|
||||
const currentData = isExpanded ? baseData : baseData.slice(0, maxItems);
|
||||
@@ -64,7 +58,11 @@ export function CategoryPieChart({
|
||||
variant={groupByParent ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setGroupByParent(!groupByParent)}
|
||||
title={groupByParent ? "Afficher toutes les catégories" : "Regrouper par catégories parentes"}
|
||||
title={
|
||||
groupByParent
|
||||
? "Afficher toutes les catégories"
|
||||
: "Regrouper par catégories parentes"
|
||||
}
|
||||
className="w-full md:w-auto text-xs md:text-sm"
|
||||
>
|
||||
{groupByParent ? (
|
||||
@@ -197,4 +195,3 @@ export function CategoryPieChart({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,11 @@ export function CategoryTrendChart({
|
||||
variant={groupByParent ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setGroupByParent(!groupByParent)}
|
||||
title={groupByParent ? "Afficher toutes les catégories" : "Regrouper par catégories parentes"}
|
||||
title={
|
||||
groupByParent
|
||||
? "Afficher toutes les catégories"
|
||||
: "Regrouper par catégories parentes"
|
||||
}
|
||||
>
|
||||
{groupByParent ? (
|
||||
<>
|
||||
@@ -173,15 +177,17 @@ export function CategoryTrendChart({
|
||||
content={() => {
|
||||
// Get all category IDs from data
|
||||
const allCategoryIds = Array.from(categoryTotals.keys());
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap justify-center gap-x-4 gap-y-1 mt-2">
|
||||
{allCategoryIds.map((categoryId) => {
|
||||
const categoryInfo = getCategoryInfo(categoryId);
|
||||
const categoryName = getCategoryName(categoryId);
|
||||
if (!categoryInfo && categoryId !== "uncategorized") return null;
|
||||
|
||||
const isInDisplayCategories = displayCategories.includes(categoryId);
|
||||
if (!categoryInfo && categoryId !== "uncategorized")
|
||||
return null;
|
||||
|
||||
const isInDisplayCategories =
|
||||
displayCategories.includes(categoryId);
|
||||
const isSelected =
|
||||
selectedCategories.length === 0
|
||||
? isInDisplayCategories
|
||||
@@ -198,8 +204,8 @@ export function CategoryTrendChart({
|
||||
if (selectedCategories.includes(categoryId)) {
|
||||
setSelectedCategories(
|
||||
selectedCategories.filter(
|
||||
(id) => id !== categoryId
|
||||
)
|
||||
(id) => id !== categoryId,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setSelectedCategories([
|
||||
@@ -234,8 +240,9 @@ export function CategoryTrendChart({
|
||||
{categoriesToShow.map((categoryId, index) => {
|
||||
const categoryInfo = getCategoryInfo(categoryId);
|
||||
const categoryName = getCategoryName(categoryId);
|
||||
if (!categoryInfo && categoryId !== "uncategorized") return null;
|
||||
|
||||
if (!categoryInfo && categoryId !== "uncategorized")
|
||||
return null;
|
||||
|
||||
const isSelected =
|
||||
selectedCategories.length === 0 ||
|
||||
selectedCategories.includes(categoryId);
|
||||
@@ -245,7 +252,10 @@ export function CategoryTrendChart({
|
||||
type="monotone"
|
||||
dataKey={categoryId}
|
||||
name={categoryName}
|
||||
stroke={categoryInfo?.color || CATEGORY_COLORS[index % CATEGORY_COLORS.length]}
|
||||
stroke={
|
||||
categoryInfo?.color ||
|
||||
CATEGORY_COLORS[index % CATEGORY_COLORS.length]
|
||||
}
|
||||
strokeWidth={isSelected ? 2 : 1}
|
||||
strokeOpacity={isSelected ? 1 : 0.3}
|
||||
dot={false}
|
||||
@@ -265,4 +275,3 @@ export function CategoryTrendChart({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -91,4 +91,3 @@ export function IncomeExpenseTrendChart({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,4 +8,3 @@ export { CategoryTrendChart } from "./category-trend-chart";
|
||||
export { SavingsTrendChart } from "./savings-trend-chart";
|
||||
export { IncomeExpenseTrendChart } from "./income-expense-trend-chart";
|
||||
export { YearOverYearChart } from "./year-over-year-chart";
|
||||
|
||||
|
||||
@@ -73,4 +73,3 @@ export function MonthlyChart({ data, formatCurrency }: MonthlyChartProps) {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,13 @@ export function SavingsTrendChart({
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={data}>
|
||||
<defs>
|
||||
<linearGradient id="savingsGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<linearGradient
|
||||
id="savingsGradient"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor={isPositive ? "#22c55e" : "#ef4444"}
|
||||
@@ -113,4 +119,3 @@ export function SavingsTrendChart({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ export function StatsSummaryCards({
|
||||
<div
|
||||
className={cn(
|
||||
"text-lg md:text-2xl font-bold",
|
||||
savings >= 0 ? "text-emerald-600" : "text-red-600"
|
||||
savings >= 0 ? "text-emerald-600" : "text-red-600",
|
||||
)}
|
||||
>
|
||||
{formatCurrency(savings)}
|
||||
@@ -83,4 +83,3 @@ export function StatsSummaryCards({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,10 +29,13 @@ export function TopExpensesList({
|
||||
<div className="space-y-3 md:space-y-4">
|
||||
{expenses.map((expense, index) => {
|
||||
const category = categories.find(
|
||||
(c) => c.id === expense.categoryId
|
||||
(c) => c.id === expense.categoryId,
|
||||
);
|
||||
return (
|
||||
<div key={expense.id} className="flex items-start gap-2 md:gap-3">
|
||||
<div
|
||||
key={expense.id}
|
||||
className="flex items-start gap-2 md:gap-3"
|
||||
>
|
||||
<div className="w-6 h-6 md:w-8 md:h-8 rounded-full bg-muted flex items-center justify-center text-xs md:text-sm font-semibold shrink-0">
|
||||
{index + 1}
|
||||
</div>
|
||||
@@ -84,4 +87,3 @@ export function TopExpensesList({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -88,4 +88,3 @@ export function YearOverYearChart({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { TransactionFilters } from "./transaction-filters";
|
||||
export { TransactionBulkActions } from "./transaction-bulk-actions";
|
||||
export { TransactionTable } from "./transaction-table";
|
||||
|
||||
|
||||
@@ -20,7 +20,9 @@ export function TransactionBulkActions({
|
||||
onReconcile,
|
||||
onSetCategory,
|
||||
}: TransactionBulkActionsProps) {
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null);
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
if (selectedCount === 0) return null;
|
||||
|
||||
@@ -61,4 +63,3 @@ export function TransactionBulkActions({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,11 @@ import {
|
||||
import { CategoryFilterCombobox } from "@/components/ui/category-filter-combobox";
|
||||
import { AccountFilterCombobox } from "@/components/ui/account-filter-combobox";
|
||||
import { CategoryIcon } from "@/components/ui/category-icon";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Calendar as CalendarComponent } from "@/components/ui/calendar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Search, X, Filter, Wallet, Calendar } from "lucide-react";
|
||||
@@ -139,9 +143,15 @@ export function TransactionFilters({
|
||||
</Select>
|
||||
|
||||
{period === "custom" && (
|
||||
<Popover open={isCustomDatePickerOpen} onOpenChange={onCustomDatePickerOpenChange}>
|
||||
<Popover
|
||||
open={isCustomDatePickerOpen}
|
||||
onOpenChange={onCustomDatePickerOpenChange}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="w-full md:w-[280px] justify-start text-left font-normal">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full md:w-[280px] justify-start text-left font-normal"
|
||||
>
|
||||
<Calendar className="mr-2 h-4 w-4" />
|
||||
{customStartDate && customEndDate ? (
|
||||
<>
|
||||
@@ -151,7 +161,9 @@ export function TransactionFilters({
|
||||
) : customStartDate ? (
|
||||
format(customStartDate, "PPP", { locale: fr })
|
||||
) : (
|
||||
<span className="text-muted-foreground">Sélectionner les dates</span>
|
||||
<span className="text-muted-foreground">
|
||||
Sélectionner les dates
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -232,7 +244,9 @@ export function TransactionFilters({
|
||||
selectedCategories={selectedCategories}
|
||||
onRemoveCategory={(id) => {
|
||||
const newCategories = selectedCategories.filter((c) => c !== id);
|
||||
onCategoriesChange(newCategories.length > 0 ? newCategories : ["all"]);
|
||||
onCategoriesChange(
|
||||
newCategories.length > 0 ? newCategories : ["all"],
|
||||
);
|
||||
}}
|
||||
onClearCategories={() => onCategoriesChange(["all"])}
|
||||
showReconciled={showReconciled}
|
||||
@@ -294,12 +308,15 @@ function ActiveFilters({
|
||||
const hasReconciled = showReconciled !== "all";
|
||||
const hasPeriod = period !== "all";
|
||||
|
||||
const hasActiveFilters = hasSearch || hasAccounts || hasCategories || hasReconciled || hasPeriod;
|
||||
const hasActiveFilters =
|
||||
hasSearch || hasAccounts || hasCategories || hasReconciled || hasPeriod;
|
||||
|
||||
if (!hasActiveFilters) return null;
|
||||
|
||||
const selectedAccs = accounts.filter((a) => selectedAccounts.includes(a.id));
|
||||
const selectedCats = categories.filter((c) => selectedCategories.includes(c.id));
|
||||
const selectedCats = categories.filter((c) =>
|
||||
selectedCategories.includes(c.id),
|
||||
);
|
||||
const isUncategorized = selectedCategories.includes("uncategorized");
|
||||
|
||||
const clearAll = () => {
|
||||
@@ -313,18 +330,25 @@ function ActiveFilters({
|
||||
return (
|
||||
<div className="flex items-center gap-2 mt-3 pt-3 border-t border-border flex-wrap">
|
||||
<Filter className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
|
||||
|
||||
{hasSearch && (
|
||||
<Badge variant="secondary" className="gap-1 text-xs font-normal">
|
||||
Recherche: "{searchQuery}"
|
||||
<button onClick={onClearSearch} className="ml-1 hover:text-foreground">
|
||||
<button
|
||||
onClick={onClearSearch}
|
||||
className="ml-1 hover:text-foreground"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{selectedAccs.map((acc) => (
|
||||
<Badge key={acc.id} variant="secondary" className="gap-1 text-xs font-normal">
|
||||
<Badge
|
||||
key={acc.id}
|
||||
variant="secondary"
|
||||
className="gap-1 text-xs font-normal"
|
||||
>
|
||||
<Wallet className="h-3 w-3" />
|
||||
{acc.name}
|
||||
<button
|
||||
@@ -339,7 +363,10 @@ function ActiveFilters({
|
||||
{isUncategorized && (
|
||||
<Badge variant="secondary" className="gap-1 text-xs font-normal">
|
||||
Non catégorisé
|
||||
<button onClick={onClearCategories} className="ml-1 hover:text-foreground">
|
||||
<button
|
||||
onClick={onClearCategories}
|
||||
className="ml-1 hover:text-foreground"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
@@ -369,7 +396,10 @@ function ActiveFilters({
|
||||
{hasReconciled && (
|
||||
<Badge variant="secondary" className="gap-1 text-xs font-normal">
|
||||
{showReconciled === "reconciled" ? "Pointées" : "Non pointées"}
|
||||
<button onClick={onClearReconciled} className="ml-1 hover:text-foreground">
|
||||
<button
|
||||
onClick={onClearReconciled}
|
||||
className="ml-1 hover:text-foreground"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
@@ -381,15 +411,18 @@ function ActiveFilters({
|
||||
{period === "custom" && customStartDate && customEndDate
|
||||
? `${format(customStartDate, "d MMM", { locale: fr })} - ${format(customEndDate, "d MMM yyyy", { locale: fr })}`
|
||||
: period === "1month"
|
||||
? "1 mois"
|
||||
: period === "3months"
|
||||
? "3 mois"
|
||||
: period === "6months"
|
||||
? "6 mois"
|
||||
: period === "12months"
|
||||
? "12 mois"
|
||||
: "Période"}
|
||||
<button onClick={onClearPeriod} className="ml-1 hover:text-foreground">
|
||||
? "1 mois"
|
||||
: period === "3months"
|
||||
? "3 mois"
|
||||
: period === "6months"
|
||||
? "6 mois"
|
||||
: period === "12months"
|
||||
? "12 mois"
|
||||
: "Période"}
|
||||
<button
|
||||
onClick={onClearPeriod}
|
||||
className="ml-1 hover:text-foreground"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
@@ -404,4 +437,3 @@ function ActiveFilters({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ function DescriptionWithTooltip({ description }: { description: string }) {
|
||||
const checkTruncation = () => {
|
||||
const element = ref.current;
|
||||
if (!element) return;
|
||||
|
||||
|
||||
// Check if text is truncated by comparing scrollWidth and clientWidth
|
||||
// Add a small threshold (1px) to account for rounding issues
|
||||
const truncated = element.scrollWidth > element.clientWidth + 1;
|
||||
@@ -112,11 +112,9 @@ function DescriptionWithTooltip({ description }: { description: string }) {
|
||||
|
||||
return (
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger asChild>
|
||||
{content}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="max-w-md break-words"
|
||||
sideOffset={5}
|
||||
@@ -163,7 +161,7 @@ export function TransactionTable({
|
||||
setFocusedIndex(index);
|
||||
onMarkReconciled(transactionId);
|
||||
},
|
||||
[onMarkReconciled]
|
||||
[onMarkReconciled],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
@@ -192,7 +190,7 @@ export function TransactionTable({
|
||||
}
|
||||
}
|
||||
},
|
||||
[focusedIndex, transactions, onMarkReconciled, virtualizer]
|
||||
[focusedIndex, transactions, onMarkReconciled, virtualizer],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -205,14 +203,20 @@ export function TransactionTable({
|
||||
setFocusedIndex(null);
|
||||
}, [transactions.length]);
|
||||
|
||||
const getAccount = useCallback((accountId: string) => {
|
||||
return accounts.find((a) => a.id === accountId);
|
||||
}, [accounts]);
|
||||
const getAccount = useCallback(
|
||||
(accountId: string) => {
|
||||
return accounts.find((a) => a.id === accountId);
|
||||
},
|
||||
[accounts],
|
||||
);
|
||||
|
||||
const getCategory = useCallback((categoryId: string | null) => {
|
||||
if (!categoryId) return null;
|
||||
return categories.find((c) => c.id === categoryId);
|
||||
}, [categories]);
|
||||
const getCategory = useCallback(
|
||||
(categoryId: string | null) => {
|
||||
if (!categoryId) return null;
|
||||
return categories.find((c) => c.id === categoryId);
|
||||
},
|
||||
[categories],
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
@@ -262,7 +266,7 @@ export function TransactionTable({
|
||||
className={cn(
|
||||
"p-4 space-y-3 hover:bg-muted/50 cursor-pointer border-b border-border",
|
||||
transaction.isReconciled && "bg-emerald-500/5",
|
||||
isFocused && "bg-primary/10 ring-1 ring-primary/30"
|
||||
isFocused && "bg-primary/10 ring-1 ring-primary/30",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
@@ -290,7 +294,7 @@ export function TransactionTable({
|
||||
"font-semibold tabular-nums text-sm md:text-base shrink-0",
|
||||
transaction.amount >= 0
|
||||
? "text-emerald-600"
|
||||
: "text-red-600"
|
||||
: "text-red-600",
|
||||
)}
|
||||
>
|
||||
{transaction.amount >= 0 ? "+" : ""}
|
||||
@@ -307,7 +311,10 @@ export function TransactionTable({
|
||||
• {account.name}
|
||||
</span>
|
||||
)}
|
||||
<div onClick={(e) => e.stopPropagation()} className="flex-1">
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex-1"
|
||||
>
|
||||
<CategoryCombobox
|
||||
categories={categories}
|
||||
value={transaction.categoryId}
|
||||
@@ -319,7 +326,10 @@ export function TransactionTable({
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -344,7 +354,7 @@ export function TransactionTable({
|
||||
e.stopPropagation();
|
||||
if (
|
||||
confirm(
|
||||
`Êtes-vous sûr de vouloir supprimer cette transaction ?\n\n${transaction.description}\n${formatCurrency(transaction.amount)}`
|
||||
`Êtes-vous sûr de vouloir supprimer cette transaction ?\n\n${transaction.description}\n${formatCurrency(transaction.amount)}`,
|
||||
)
|
||||
) {
|
||||
onDelete(transaction.id);
|
||||
@@ -447,11 +457,13 @@ export function TransactionTable({
|
||||
width: "100%",
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
onClick={() => handleRowClick(virtualRow.index, transaction.id)}
|
||||
onClick={() =>
|
||||
handleRowClick(virtualRow.index, transaction.id)
|
||||
}
|
||||
className={cn(
|
||||
"grid grid-cols-[auto_120px_2fr_150px_180px_140px_auto_auto] gap-0 border-b border-border hover:bg-muted/50 cursor-pointer",
|
||||
transaction.isReconciled && "bg-emerald-500/5",
|
||||
isFocused && "bg-primary/10 ring-1 ring-primary/30"
|
||||
isFocused && "bg-primary/10 ring-1 ring-primary/30",
|
||||
)}
|
||||
>
|
||||
<div className="p-3">
|
||||
@@ -465,12 +477,17 @@ export function TransactionTable({
|
||||
<div className="p-3 text-sm text-muted-foreground whitespace-nowrap">
|
||||
{formatDate(transaction.date)}
|
||||
</div>
|
||||
<div className="p-3 min-w-0 overflow-hidden" onClick={(e) => e.stopPropagation()}>
|
||||
<div
|
||||
className="p-3 min-w-0 overflow-hidden"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<p className="font-medium text-sm truncate">
|
||||
{transaction.description}
|
||||
</p>
|
||||
{transaction.memo && (
|
||||
<DescriptionWithTooltip description={transaction.memo} />
|
||||
<DescriptionWithTooltip
|
||||
description={transaction.memo}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-3 text-sm text-muted-foreground">
|
||||
@@ -492,13 +509,16 @@ export function TransactionTable({
|
||||
"p-3 text-right font-semibold tabular-nums",
|
||||
transaction.amount >= 0
|
||||
? "text-emerald-600"
|
||||
: "text-red-600"
|
||||
: "text-red-600",
|
||||
)}
|
||||
>
|
||||
{transaction.amount >= 0 ? "+" : ""}
|
||||
{formatCurrency(transaction.amount)}
|
||||
</div>
|
||||
<div className="p-3 text-center" onClick={(e) => e.stopPropagation()}>
|
||||
<div
|
||||
className="p-3 text-center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
onClick={() => onToggleReconciled(transaction.id)}
|
||||
className="p-1 hover:bg-muted rounded"
|
||||
@@ -556,7 +576,7 @@ export function TransactionTable({
|
||||
e.stopPropagation();
|
||||
if (
|
||||
confirm(
|
||||
`Êtes-vous sûr de vouloir supprimer cette transaction ?\n\n${transaction.description}\n${formatCurrency(transaction.amount)}`
|
||||
`Êtes-vous sûr de vouloir supprimer cette transaction ?\n\n${transaction.description}\n${formatCurrency(transaction.amount)}`,
|
||||
)
|
||||
) {
|
||||
onDelete(transaction.id);
|
||||
@@ -581,4 +601,3 @@ export function TransactionTable({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,12 +43,12 @@ export function AccountFilterCombobox({
|
||||
// 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]);
|
||||
|
||||
@@ -64,7 +64,7 @@ export function AccountFilterCombobox({
|
||||
// Get root folders (folders without parent) - same as folders/page.tsx
|
||||
const rootFolders = useMemo(
|
||||
() => folders.filter((f) => f.parentId === null),
|
||||
[folders]
|
||||
[folders],
|
||||
);
|
||||
|
||||
// Get child folders for a given parent - same as FolderTreeItem
|
||||
@@ -78,7 +78,7 @@ export function AccountFilterCombobox({
|
||||
// Get accounts without folder
|
||||
const orphanAccounts = useMemo(
|
||||
() => accounts.filter((a) => !a.folderId),
|
||||
[accounts]
|
||||
[accounts],
|
||||
);
|
||||
|
||||
const selectedAccounts = accounts.filter((a) => value.includes(a.id));
|
||||
@@ -89,7 +89,7 @@ export function AccountFilterCombobox({
|
||||
const directAccounts = getFolderAccounts(folderId);
|
||||
const childFoldersList = getChildFolders(folderId);
|
||||
const childAccounts = childFoldersList.flatMap((cf) =>
|
||||
getAllAccountsInFolder(cf.id)
|
||||
getAllAccountsInFolder(cf.id),
|
||||
);
|
||||
return [...directAccounts, ...childAccounts];
|
||||
};
|
||||
@@ -126,7 +126,7 @@ export function AccountFilterCombobox({
|
||||
|
||||
if (allSelected) {
|
||||
const newSelection = value.filter(
|
||||
(v) => !allFolderAccountIds.includes(v)
|
||||
(v) => !allFolderAccountIds.includes(v),
|
||||
);
|
||||
onChange(newSelection.length > 0 ? newSelection : ["all"]);
|
||||
} else {
|
||||
@@ -153,7 +153,7 @@ export function AccountFilterCombobox({
|
||||
const folderAccounts = getAllAccountsInFolder(folderId);
|
||||
if (folderAccounts.length === 0) return false;
|
||||
const selectedCount = folderAccounts.filter((a) =>
|
||||
value.includes(a.id)
|
||||
value.includes(a.id),
|
||||
).length;
|
||||
return selectedCount > 0 && selectedCount < folderAccounts.length;
|
||||
};
|
||||
@@ -162,7 +162,9 @@ export function AccountFilterCombobox({
|
||||
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 currentPath = parentPath
|
||||
? `${parentPath} ${folder.name}`
|
||||
: folder.name;
|
||||
const paddingLeft = depth * 16 + 8;
|
||||
|
||||
return (
|
||||
@@ -183,7 +185,7 @@ export function AccountFilterCombobox({
|
||||
<Check
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
isFolderSelected(folder.id) ? "opacity-100" : "opacity-0"
|
||||
isFolderSelected(folder.id) ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -211,7 +213,7 @@ export function AccountFilterCombobox({
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4 shrink-0",
|
||||
value.includes(account.id) ? "opacity-100" : "opacity-0"
|
||||
value.includes(account.id) ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
@@ -220,7 +222,7 @@ export function AccountFilterCombobox({
|
||||
|
||||
{/* Child folders - recursive */}
|
||||
{childFoldersList.map((childFolder) =>
|
||||
renderFolder(childFolder, depth + 1, currentPath)
|
||||
renderFolder(childFolder, depth + 1, currentPath),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -239,10 +241,15 @@ export function AccountFilterCombobox({
|
||||
{selectedAccounts.length === 1 ? (
|
||||
<>
|
||||
{(() => {
|
||||
const AccountIcon = accountTypeIcons[selectedAccounts[0].type];
|
||||
return <AccountIcon className="h-4 w-4 text-muted-foreground shrink-0" />;
|
||||
const AccountIcon =
|
||||
accountTypeIcons[selectedAccounts[0].type];
|
||||
return (
|
||||
<AccountIcon className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
);
|
||||
})()}
|
||||
<span className="truncate text-left">{selectedAccounts[0].name}</span>
|
||||
<span className="truncate text-left">
|
||||
{selectedAccounts[0].name}
|
||||
</span>
|
||||
</>
|
||||
) : selectedAccounts.length > 1 ? (
|
||||
<>
|
||||
@@ -254,7 +261,9 @@ export function AccountFilterCombobox({
|
||||
) : (
|
||||
<>
|
||||
<Wallet className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="text-muted-foreground truncate text-left">Tous les comptes</span>
|
||||
<span className="text-muted-foreground truncate text-left">
|
||||
Tous les comptes
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -290,15 +299,20 @@ export function AccountFilterCombobox({
|
||||
<span>Tous les comptes</span>
|
||||
{filteredTransactions && (
|
||||
<span className="text-xs text-muted-foreground ml-1">
|
||||
({formatCurrency(
|
||||
filteredTransactions.reduce((sum, t) => sum + t.amount, 0)
|
||||
)})
|
||||
(
|
||||
{formatCurrency(
|
||||
filteredTransactions.reduce(
|
||||
(sum, t) => sum + t.amount,
|
||||
0,
|
||||
),
|
||||
)}
|
||||
)
|
||||
</span>
|
||||
)}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
isAll ? "opacity-100" : "opacity-0"
|
||||
isAll ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
@@ -321,7 +335,9 @@ export function AccountFilterCombobox({
|
||||
className="min-w-0"
|
||||
>
|
||||
<AccountIcon className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="truncate min-w-0 flex-1">{account.name}</span>
|
||||
<span className="truncate min-w-0 flex-1">
|
||||
{account.name}
|
||||
</span>
|
||||
{total !== undefined && (
|
||||
<span className="text-xs text-muted-foreground ml-1 shrink-0">
|
||||
({formatCurrency(total)})
|
||||
@@ -330,7 +346,9 @@ export function AccountFilterCombobox({
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4 shrink-0",
|
||||
value.includes(account.id) ? "opacity-100" : "opacity-0"
|
||||
value.includes(account.id)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
|
||||
@@ -115,11 +115,13 @@ export function CategoryCombobox({
|
||||
onSelect={() => handleSelect(null)}
|
||||
>
|
||||
<X className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Aucune catégorie</span>
|
||||
<span className="text-muted-foreground">
|
||||
Aucune catégorie
|
||||
</span>
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value === null ? "opacity-100" : "opacity-0"
|
||||
value === null ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
@@ -140,7 +142,7 @@ export function CategoryCombobox({
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value === parent.id ? "opacity-100" : "opacity-0"
|
||||
value === parent.id ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
@@ -160,7 +162,7 @@ export function CategoryCombobox({
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value === child.id ? "opacity-100" : "opacity-0"
|
||||
value === child.id ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
@@ -183,10 +185,7 @@ export function CategoryCombobox({
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
"justify-between",
|
||||
buttonWidth || "w-full"
|
||||
)}
|
||||
className={cn("justify-between", buttonWidth || "w-full")}
|
||||
>
|
||||
{selectedCategory ? (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -213,16 +212,13 @@ export function CategoryCombobox({
|
||||
<CommandList className="max-h-[250px]">
|
||||
<CommandEmpty>Aucune catégorie trouvée.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
value="__none__"
|
||||
onSelect={() => handleSelect(null)}
|
||||
>
|
||||
<CommandItem value="__none__" onSelect={() => handleSelect(null)}>
|
||||
<X className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Aucune catégorie</span>
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value === null ? "opacity-100" : "opacity-0"
|
||||
value === null ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
@@ -243,7 +239,7 @@ export function CategoryCombobox({
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value === parent.id ? "opacity-100" : "opacity-0"
|
||||
value === parent.id ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
@@ -263,7 +259,7 @@ export function CategoryCombobox({
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value === child.id ? "opacity-100" : "opacity-0"
|
||||
value === child.id ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
@@ -277,4 +273,3 @@ export function CategoryCombobox({
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,13 +40,13 @@ export function CategoryFilterCombobox({
|
||||
// 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]);
|
||||
|
||||
@@ -89,7 +89,7 @@ export function CategoryFilterCombobox({
|
||||
|
||||
// Category selection - toggle
|
||||
let newSelection: string[];
|
||||
|
||||
|
||||
if (isAll || isUncategorized) {
|
||||
// Start fresh with just this category
|
||||
newSelection = [newValue];
|
||||
@@ -115,7 +115,8 @@ export function CategoryFilterCombobox({
|
||||
if (isAll) return "Toutes catégories";
|
||||
if (isUncategorized) return "Non catégorisé";
|
||||
if (selectedCategories.length === 1) return selectedCategories[0].name;
|
||||
if (selectedCategories.length > 1) return `${selectedCategories.length} catégories`;
|
||||
if (selectedCategories.length > 1)
|
||||
return `${selectedCategories.length} catégories`;
|
||||
return "Catégorie";
|
||||
};
|
||||
|
||||
@@ -137,7 +138,9 @@ export function CategoryFilterCombobox({
|
||||
size={16}
|
||||
className="shrink-0"
|
||||
/>
|
||||
<span className="truncate text-left">{selectedCategories[0].name}</span>
|
||||
<span className="truncate text-left">
|
||||
{selectedCategories[0].name}
|
||||
</span>
|
||||
</>
|
||||
) : selectedCategories.length > 1 ? (
|
||||
<>
|
||||
@@ -150,7 +153,9 @@ export function CategoryFilterCombobox({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="truncate text-left">{selectedCategories.length} catégories</span>
|
||||
<span className="truncate text-left">
|
||||
{selectedCategories.length} catégories
|
||||
</span>
|
||||
</>
|
||||
) : isUncategorized ? (
|
||||
<>
|
||||
@@ -160,7 +165,9 @@ export function CategoryFilterCombobox({
|
||||
) : (
|
||||
<>
|
||||
<Tags className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="text-muted-foreground truncate text-left">{getDisplayValue()}</span>
|
||||
<span className="text-muted-foreground truncate text-left">
|
||||
{getDisplayValue()}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -191,9 +198,15 @@ export function CategoryFilterCombobox({
|
||||
<CommandList className="max-h-[300px]">
|
||||
<CommandEmpty>Aucune catégorie trouvée.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem value="all" onSelect={() => handleSelect("all")} className="min-w-0">
|
||||
<CommandItem
|
||||
value="all"
|
||||
onSelect={() => handleSelect("all")}
|
||||
className="min-w-0"
|
||||
>
|
||||
<Tags className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="truncate min-w-0 flex-1">Toutes catégories</span>
|
||||
<span className="truncate min-w-0 flex-1">
|
||||
Toutes catégories
|
||||
</span>
|
||||
{filteredTransactions && (
|
||||
<span className="text-xs text-muted-foreground ml-1 shrink-0">
|
||||
({filteredTransactions.length})
|
||||
@@ -202,7 +215,7 @@ export function CategoryFilterCombobox({
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4 shrink-0",
|
||||
isAll ? "opacity-100" : "opacity-0"
|
||||
isAll ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
@@ -221,7 +234,7 @@ export function CategoryFilterCombobox({
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4 shrink-0",
|
||||
isUncategorized ? "opacity-100" : "opacity-0"
|
||||
isUncategorized ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
@@ -240,7 +253,9 @@ export function CategoryFilterCombobox({
|
||||
size={16}
|
||||
className="shrink-0"
|
||||
/>
|
||||
<span className="font-medium truncate min-w-0 flex-1">{parent.name}</span>
|
||||
<span className="font-medium truncate min-w-0 flex-1">
|
||||
{parent.name}
|
||||
</span>
|
||||
{categoryCounts[parent.id] !== undefined && (
|
||||
<span className="text-xs text-muted-foreground ml-1 shrink-0">
|
||||
({categoryCounts[parent.id]})
|
||||
@@ -249,7 +264,7 @@ export function CategoryFilterCombobox({
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4 shrink-0",
|
||||
value.includes(parent.id) ? "opacity-100" : "opacity-0"
|
||||
value.includes(parent.id) ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
@@ -266,7 +281,9 @@ export function CategoryFilterCombobox({
|
||||
size={16}
|
||||
className="shrink-0"
|
||||
/>
|
||||
<span className="truncate min-w-0 flex-1">{child.name}</span>
|
||||
<span className="truncate min-w-0 flex-1">
|
||||
{child.name}
|
||||
</span>
|
||||
{categoryCounts[child.id] !== undefined && (
|
||||
<span className="text-xs text-muted-foreground ml-1 shrink-0">
|
||||
({categoryCounts[child.id]})
|
||||
@@ -275,7 +292,9 @@ export function CategoryFilterCombobox({
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4 shrink-0",
|
||||
value.includes(child.id) ? "opacity-100" : "opacity-0"
|
||||
value.includes(child.id)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
|
||||
@@ -20,64 +20,225 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
// Group icons by category for better organization
|
||||
const iconGroups: Record<string, string[]> = {
|
||||
"Alimentation": [
|
||||
"shopping-cart", "utensils", "croissant", "coffee", "wine", "beer",
|
||||
"pizza", "apple", "cherry", "salad", "sandwich", "ice-cream",
|
||||
"cake", "cup-soda", "milk", "egg", "fish", "beef"
|
||||
Alimentation: [
|
||||
"shopping-cart",
|
||||
"utensils",
|
||||
"croissant",
|
||||
"coffee",
|
||||
"wine",
|
||||
"beer",
|
||||
"pizza",
|
||||
"apple",
|
||||
"cherry",
|
||||
"salad",
|
||||
"sandwich",
|
||||
"ice-cream",
|
||||
"cake",
|
||||
"cup-soda",
|
||||
"milk",
|
||||
"egg",
|
||||
"fish",
|
||||
"beef",
|
||||
],
|
||||
"Transport": [
|
||||
"fuel", "train", "car", "parking", "bike", "plane", "bus",
|
||||
"ship", "sailboat", "truck", "car-front", "circle-parking",
|
||||
"train-front"
|
||||
Transport: [
|
||||
"fuel",
|
||||
"train",
|
||||
"car",
|
||||
"parking",
|
||||
"bike",
|
||||
"plane",
|
||||
"bus",
|
||||
"ship",
|
||||
"sailboat",
|
||||
"truck",
|
||||
"car-front",
|
||||
"circle-parking",
|
||||
"train-front",
|
||||
],
|
||||
"Logement": [
|
||||
"home", "zap", "droplet", "hammer", "sofa", "refrigerator",
|
||||
"washing-machine", "lamp", "lamp-desk", "armchair", "bath",
|
||||
"shower-head", "door-open", "fence", "trees", "flower",
|
||||
"leaf", "sun", "snowflake", "wind", "thermometer"
|
||||
Logement: [
|
||||
"home",
|
||||
"zap",
|
||||
"droplet",
|
||||
"hammer",
|
||||
"sofa",
|
||||
"refrigerator",
|
||||
"washing-machine",
|
||||
"lamp",
|
||||
"lamp-desk",
|
||||
"armchair",
|
||||
"bath",
|
||||
"shower-head",
|
||||
"door-open",
|
||||
"fence",
|
||||
"trees",
|
||||
"flower",
|
||||
"leaf",
|
||||
"sun",
|
||||
"snowflake",
|
||||
"wind",
|
||||
"thermometer",
|
||||
],
|
||||
"Santé": [
|
||||
"pill", "stethoscope", "hospital", "glasses", "dumbbell", "sparkles",
|
||||
"heart", "heart-pulse", "activity", "syringe", "bandage", "brain",
|
||||
"eye", "ear", "hand", "footprints", "person-standing"
|
||||
Santé: [
|
||||
"pill",
|
||||
"stethoscope",
|
||||
"hospital",
|
||||
"glasses",
|
||||
"dumbbell",
|
||||
"sparkles",
|
||||
"heart",
|
||||
"heart-pulse",
|
||||
"activity",
|
||||
"syringe",
|
||||
"bandage",
|
||||
"brain",
|
||||
"eye",
|
||||
"ear",
|
||||
"hand",
|
||||
"footprints",
|
||||
"person-standing",
|
||||
],
|
||||
"Loisirs": [
|
||||
"tv", "music", "film", "gamepad", "book", "ticket", "clapperboard",
|
||||
"headphones", "speaker", "radio", "camera", "image", "palette",
|
||||
"brush", "pen-tool", "scissors", "drama", "party-popper"
|
||||
Loisirs: [
|
||||
"tv",
|
||||
"music",
|
||||
"film",
|
||||
"gamepad",
|
||||
"book",
|
||||
"ticket",
|
||||
"clapperboard",
|
||||
"headphones",
|
||||
"speaker",
|
||||
"radio",
|
||||
"camera",
|
||||
"image",
|
||||
"palette",
|
||||
"brush",
|
||||
"pen-tool",
|
||||
"scissors",
|
||||
"drama",
|
||||
"party-popper",
|
||||
],
|
||||
"Sport": ["trophy", "medal", "target", "volleyball"],
|
||||
"Shopping": [
|
||||
"shirt", "smartphone", "package", "shopping-bag", "store", "gem",
|
||||
"watch", "sunglasses", "crown", "laptop", "monitor", "keyboard",
|
||||
"mouse", "printer", "tablet-smartphone", "headset"
|
||||
Sport: ["trophy", "medal", "target", "volleyball"],
|
||||
Shopping: [
|
||||
"shirt",
|
||||
"smartphone",
|
||||
"package",
|
||||
"shopping-bag",
|
||||
"store",
|
||||
"gem",
|
||||
"watch",
|
||||
"sunglasses",
|
||||
"crown",
|
||||
"laptop",
|
||||
"monitor",
|
||||
"keyboard",
|
||||
"mouse",
|
||||
"printer",
|
||||
"tablet-smartphone",
|
||||
"headset",
|
||||
],
|
||||
"Services": [
|
||||
"wifi", "repeat", "landmark", "shield", "receipt", "file-text",
|
||||
"mail", "phone", "message-square", "send", "globe", "cloud",
|
||||
"server", "lock", "unlock", "settings", "wrench"
|
||||
Services: [
|
||||
"wifi",
|
||||
"repeat",
|
||||
"landmark",
|
||||
"shield",
|
||||
"receipt",
|
||||
"file-text",
|
||||
"mail",
|
||||
"phone",
|
||||
"message-square",
|
||||
"send",
|
||||
"globe",
|
||||
"cloud",
|
||||
"server",
|
||||
"lock",
|
||||
"unlock",
|
||||
"settings",
|
||||
"wrench",
|
||||
],
|
||||
"Finance": [
|
||||
"piggy-bank", "banknote", "wallet", "hand-coins", "undo", "coins",
|
||||
"credit-card", "building", "building2", "trending-up", "trending-down",
|
||||
"bar-chart", "pie-chart", "line-chart", "calculator", "percent",
|
||||
"dollar-sign", "euro"
|
||||
Finance: [
|
||||
"piggy-bank",
|
||||
"banknote",
|
||||
"wallet",
|
||||
"hand-coins",
|
||||
"undo",
|
||||
"coins",
|
||||
"credit-card",
|
||||
"building",
|
||||
"building2",
|
||||
"trending-up",
|
||||
"trending-down",
|
||||
"bar-chart",
|
||||
"pie-chart",
|
||||
"line-chart",
|
||||
"calculator",
|
||||
"percent",
|
||||
"dollar-sign",
|
||||
"euro",
|
||||
],
|
||||
"Voyage": [
|
||||
"bed", "luggage", "map", "map-pin", "compass", "mountain",
|
||||
"tent", "palmtree", "umbrella", "globe2", "flag"
|
||||
Voyage: [
|
||||
"bed",
|
||||
"luggage",
|
||||
"map",
|
||||
"map-pin",
|
||||
"compass",
|
||||
"mountain",
|
||||
"tent",
|
||||
"palmtree",
|
||||
"umbrella",
|
||||
"globe2",
|
||||
"flag",
|
||||
],
|
||||
"Famille": [
|
||||
"graduation-cap", "baby", "paw-print", "users", "user", "user-plus",
|
||||
"dog", "cat", "bird", "rabbit"
|
||||
Famille: [
|
||||
"graduation-cap",
|
||||
"baby",
|
||||
"paw-print",
|
||||
"users",
|
||||
"user",
|
||||
"user-plus",
|
||||
"dog",
|
||||
"cat",
|
||||
"bird",
|
||||
"rabbit",
|
||||
],
|
||||
"Autre": [
|
||||
"heart-handshake", "gift", "cigarette", "arrow-right-left",
|
||||
"help-circle", "tag", "folder", "key", "star", "bookmark", "clock",
|
||||
"calendar", "bell", "alert-triangle", "info", "check-circle", "x-circle",
|
||||
"plus", "minus", "search", "trash", "edit", "download", "upload",
|
||||
"share", "link", "paperclip", "archive", "box", "boxes", "container",
|
||||
"briefcase", "education", "award", "lightbulb", "flame", "rocket", "atom"
|
||||
Autre: [
|
||||
"heart-handshake",
|
||||
"gift",
|
||||
"cigarette",
|
||||
"arrow-right-left",
|
||||
"help-circle",
|
||||
"tag",
|
||||
"folder",
|
||||
"key",
|
||||
"star",
|
||||
"bookmark",
|
||||
"clock",
|
||||
"calendar",
|
||||
"bell",
|
||||
"alert-triangle",
|
||||
"info",
|
||||
"check-circle",
|
||||
"x-circle",
|
||||
"plus",
|
||||
"minus",
|
||||
"search",
|
||||
"trash",
|
||||
"edit",
|
||||
"download",
|
||||
"upload",
|
||||
"share",
|
||||
"link",
|
||||
"paperclip",
|
||||
"archive",
|
||||
"box",
|
||||
"boxes",
|
||||
"container",
|
||||
"briefcase",
|
||||
"education",
|
||||
"award",
|
||||
"lightbulb",
|
||||
"flame",
|
||||
"rocket",
|
||||
"atom",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -94,21 +255,21 @@ export function IconPicker({ value, onChange, color }: IconPickerProps) {
|
||||
// Filter icons based on search
|
||||
const filteredGroups = useMemo(() => {
|
||||
if (!search.trim()) return iconGroups;
|
||||
|
||||
|
||||
const query = search.toLowerCase();
|
||||
const result: Record<string, string[]> = {};
|
||||
|
||||
|
||||
Object.entries(iconGroups).forEach(([group, icons]) => {
|
||||
const filtered = icons.filter(
|
||||
(icon) =>
|
||||
icon.toLowerCase().includes(query) ||
|
||||
group.toLowerCase().includes(query)
|
||||
group.toLowerCase().includes(query),
|
||||
);
|
||||
if (filtered.length > 0) {
|
||||
result[group] = filtered;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return result;
|
||||
}, [search]);
|
||||
|
||||
@@ -156,7 +317,7 @@ export function IconPicker({ value, onChange, color }: IconPickerProps) {
|
||||
onClick={() => handleSelect(icon)}
|
||||
className={cn(
|
||||
"flex items-center justify-center p-2 rounded-md hover:bg-accent transition-colors",
|
||||
value === icon && "bg-accent ring-2 ring-primary"
|
||||
value === icon && "bg-accent ring-2 ring-primary",
|
||||
)}
|
||||
title={icon}
|
||||
>
|
||||
@@ -172,4 +333,3 @@ export function IconPicker({ value, onChange, color }: IconPickerProps) {
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user