chore: clean up code by removing trailing whitespace and ensuring consistent formatting across various files = prettier

This commit is contained in:
Julien Froidefond
2025-12-01 08:37:30 +01:00
parent 757b1b84ab
commit e715779de7
98 changed files with 5453 additions and 3126 deletions

View File

@@ -32,4 +32,3 @@ export function AccountBulkActions({
</Card>
);
}

View File

@@ -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;
}

View File

@@ -142,4 +142,3 @@ export function AccountEditDialog({
</Dialog>
);
}

View File

@@ -13,4 +13,3 @@ export const accountTypeLabels = {
CREDIT_CARD: "Carte de crédit",
OTHER: "Autre",
};

View File

@@ -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";

View File

@@ -80,4 +80,3 @@ export function CategoryCard({
</div>
);
}

View File

@@ -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>
);
}

View File

@@ -43,4 +43,3 @@ export function CategorySearchBar({
</div>
);
}

View File

@@ -15,4 +15,3 @@ export const categoryColors = [
"#0891b2",
"#dc2626",
];

View File

@@ -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";

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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>();

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -115,4 +115,3 @@ export function AccountFolderDialog({
</Dialog>
);
}

View File

@@ -13,4 +13,3 @@ export const accountTypeLabels = {
CREDIT_CARD: "Carte de crédit",
OTHER: "Autre",
};

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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";

View File

@@ -1,4 +1,3 @@
export { PageLayout } from "./page-layout";
export { LoadingState } from "./loading-state";
export { PageHeader } from "./page-header";

View File

@@ -13,4 +13,3 @@ export function LoadingState() {
</div>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -15,4 +15,3 @@ export const SidebarContext = createContext<SidebarContextType>({
export function useSidebarContext() {
return useContext(SidebarContext);
}

View File

@@ -6,4 +6,3 @@ import type { ReactNode } from "react";
export function AuthSessionProvider({ children }: { children: ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}

View File

@@ -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);
}

View File

@@ -1,4 +1,3 @@
export { RuleGroupCard } from "./rule-group-card";
export { RuleCreateDialog } from "./rule-create-dialog";
export { RulesSearchBar } from "./rules-search-bar";

View File

@@ -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 &quot;{existingCategory.name}&quot;
Ce mot-clé existe déjà dans &quot;{existingCategory.name}
&quot;
</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é &quot;<strong>{keyword}</strong>&quot; sera ajouté à la
catégorie &quot;<strong>{selectedCategory?.name}</strong>&quot;
Le mot-clé &quot;<strong>{keyword}</strong>&quot; sera ajouté
à la catégorie &quot;<strong>{selectedCategory?.name}</strong>
&quot;
</span>
</div>
</div>
@@ -225,4 +227,3 @@ export function RuleCreateDialog({
</Dialog>
);
}

View File

@@ -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>
);
}

View File

@@ -75,4 +75,3 @@ export function RulesSearchBar({
</div>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -70,4 +70,3 @@ export function DataCard({
</Card>
);
}

View File

@@ -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";

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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",
}}
/>

View File

@@ -115,4 +115,3 @@ export function CategoryBarChart({
</Card>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -91,4 +91,3 @@ export function IncomeExpenseTrendChart({
</Card>
);
}

View File

@@ -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";

View File

@@ -73,4 +73,3 @@ export function MonthlyChart({ data, formatCurrency }: MonthlyChartProps) {
</Card>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -88,4 +88,3 @@ export function YearOverYearChart({
</Card>
);
}

View File

@@ -1,4 +1,3 @@
export { TransactionFilters } from "./transaction-filters";
export { TransactionBulkActions } from "./transaction-bulk-actions";
export { TransactionTable } from "./transaction-table";

View File

@@ -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>
);
}

View File

@@ -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: &quot;{searchQuery}&quot;
<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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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>
);
}