feat: add new navigation item for rules in the dashboard sidebar with associated icon

This commit is contained in:
Julien Froidefond
2025-11-29 17:18:59 +01:00
parent 292d1fb394
commit 9e576f2b0e
7 changed files with 912 additions and 0 deletions

298
app/rules/page.tsx Normal file
View File

@@ -0,0 +1,298 @@
"use client";
import { useState, useMemo, useCallback } from "react";
import { PageLayout, LoadingState, PageHeader } from "@/components/layout";
import {
RuleGroupCard,
RuleCreateDialog,
RulesSearchBar,
} from "@/components/rules";
import { useBankingData } from "@/lib/hooks";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Sparkles, RefreshCw } from "lucide-react";
import { updateCategory, autoCategorize, updateTransaction } from "@/lib/store-db";
import {
normalizeDescription,
suggestKeyword,
} from "@/components/rules/constants";
import type { Transaction, Category } from "@/lib/types";
interface TransactionGroup {
key: string;
displayName: string;
transactions: Transaction[];
totalAmount: number;
suggestedKeyword: string;
}
export default function RulesPage() {
const { data, isLoading, refresh } = useBankingData();
const [searchQuery, setSearchQuery] = useState("");
const [sortBy, setSortBy] = useState<"count" | "amount" | "name">("count");
const [filterMinCount, setFilterMinCount] = useState(2);
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
const [selectedGroup, setSelectedGroup] = useState<TransactionGroup | null>(
null
);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isAutoCategorizing, setIsAutoCategorizing] = useState(false);
// Group uncategorized transactions by normalized description
const transactionGroups = useMemo(() => {
if (!data?.transactions) return [];
const uncategorized = data.transactions.filter((t) => !t.categoryId);
const groups: Record<string, Transaction[]> = {};
uncategorized.forEach((transaction) => {
const key = normalizeDescription(transaction.description);
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(transaction);
});
// Convert to array with metadata
const groupArray: TransactionGroup[] = Object.entries(groups).map(
([key, transactions]) => {
const descriptions = transactions.map((t) => t.description);
return {
key,
displayName: transactions[0].description, // Use first transaction's description as display name
transactions,
totalAmount: transactions.reduce((sum, t) => sum + t.amount, 0),
suggestedKeyword: suggestKeyword(descriptions),
};
}
);
// Filter by search query
let filtered = groupArray;
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
filtered = groupArray.filter(
(g) =>
g.displayName.toLowerCase().includes(query) ||
g.key.includes(query) ||
g.suggestedKeyword.toLowerCase().includes(query)
);
}
// Filter by minimum count
filtered = filtered.filter((g) => g.transactions.length >= filterMinCount);
// Sort
filtered.sort((a, b) => {
switch (sortBy) {
case "count":
return b.transactions.length - a.transactions.length;
case "amount":
return Math.abs(b.totalAmount) - Math.abs(a.totalAmount);
case "name":
return a.displayName.localeCompare(b.displayName);
default:
return 0;
}
});
return filtered;
}, [data?.transactions, searchQuery, sortBy, filterMinCount]);
const uncategorizedCount = useMemo(() => {
if (!data?.transactions) return 0;
return data.transactions.filter((t) => !t.categoryId).length;
}, [data?.transactions]);
const formatCurrency = useCallback((amount: number) => {
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(amount);
}, []);
const formatDate = useCallback((dateStr: string) => {
return new Date(dateStr).toLocaleDateString("fr-FR", {
day: "2-digit",
month: "short",
year: "numeric",
});
}, []);
const toggleExpand = useCallback((key: string) => {
setExpandedGroups((prev) => {
const next = new Set(prev);
if (next.has(key)) {
next.delete(key);
} else {
next.add(key);
}
return next;
});
}, []);
const handleCreateRule = useCallback((group: TransactionGroup) => {
setSelectedGroup(group);
setIsDialogOpen(true);
}, []);
const handleSaveRule = useCallback(
async (ruleData: {
keyword: string;
categoryId: string;
applyToExisting: boolean;
transactionIds: string[];
}) => {
if (!data) return;
// 1. Add keyword to category
const category = data.categories.find((c) => c.id === ruleData.categoryId);
if (!category) {
throw new Error("Category not found");
}
// Check if keyword already exists
const keywordExists = category.keywords.some(
(k) => k.toLowerCase() === ruleData.keyword.toLowerCase()
);
if (!keywordExists) {
await updateCategory({
...category,
keywords: [...category.keywords, ruleData.keyword],
});
}
// 2. Apply to existing transactions if requested
if (ruleData.applyToExisting) {
const transactions = data.transactions.filter((t) =>
ruleData.transactionIds.includes(t.id)
);
await Promise.all(
transactions.map((t) =>
updateTransaction({ ...t, categoryId: ruleData.categoryId })
)
);
}
refresh();
},
[data, refresh]
);
const handleAutoCategorize = useCallback(async () => {
if (!data) return;
setIsAutoCategorizing(true);
try {
const uncategorized = data.transactions.filter((t) => !t.categoryId);
let categorizedCount = 0;
for (const transaction of uncategorized) {
const categoryId = autoCategorize(
transaction.description + " " + (transaction.memo || ""),
data.categories
);
if (categoryId) {
await updateTransaction({ ...transaction, categoryId });
categorizedCount++;
}
}
refresh();
alert(`${categorizedCount} transaction(s) catégorisée(s) automatiquement`);
} catch (error) {
console.error("Error auto-categorizing:", error);
alert("Erreur lors de la catégorisation automatique");
} finally {
setIsAutoCategorizing(false);
}
}, [data, refresh]);
if (isLoading || !data) {
return <LoadingState />;
}
return (
<PageLayout>
<PageHeader
title="Règles de catégorisation"
description={
<span className="flex items-center gap-2">
{transactionGroups.length} groupe
{transactionGroups.length > 1 ? "s" : ""} de transactions similaires
<Badge variant="secondary">{uncategorizedCount} non catégorisées</Badge>
</span>
}
actions={
uncategorizedCount > 0 && (
<Button
variant="outline"
onClick={handleAutoCategorize}
disabled={isAutoCategorizing}
>
{isAutoCategorizing ? (
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
) : (
<Sparkles className="h-4 w-4 mr-2" />
)}
Auto-catégoriser
</Button>
)
}
/>
<RulesSearchBar
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
sortBy={sortBy}
onSortChange={setSortBy}
filterMinCount={filterMinCount}
onFilterMinCountChange={setFilterMinCount}
/>
{transactionGroups.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-center">
<Sparkles className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{uncategorizedCount === 0
? "Toutes les transactions sont catégorisées !"
: "Aucun groupe trouvé"}
</h3>
<p className="text-sm text-muted-foreground max-w-md">
{uncategorizedCount === 0
? "Continuez à importer des transactions pour voir les suggestions de règles."
: filterMinCount > 1
? `Essayez de réduire le filtre minimum à ${filterMinCount - 1}+ transactions.`
: "Modifiez vos critères de recherche."}
</p>
</div>
) : (
<div className="space-y-3">
{transactionGroups.map((group) => (
<RuleGroupCard
key={group.key}
group={group}
isExpanded={expandedGroups.has(group.key)}
onToggleExpand={() => toggleExpand(group.key)}
onCreateRule={() => handleCreateRule(group)}
categories={data.categories}
formatCurrency={formatCurrency}
formatDate={formatDate}
/>
))}
</div>
)}
<RuleCreateDialog
open={isDialogOpen}
onOpenChange={setIsDialogOpen}
group={selectedGroup}
categories={data.categories}
onSave={handleSaveRule}
/>
</PageLayout>
);
}

View File

@@ -15,6 +15,7 @@ import {
ChevronLeft,
ChevronRight,
Settings,
Wand2,
} from "lucide-react";
const navItems = [
@@ -23,6 +24,7 @@ const navItems = [
{ href: "/folders", label: "Organisation", icon: FolderTree },
{ href: "/transactions", label: "Transactions", icon: Upload },
{ href: "/categories", label: "Catégories", icon: Tags },
{ href: "/rules", label: "Règles", icon: Wand2 },
{ href: "/statistics", label: "Statistiques", icon: BarChart3 },
];

View File

@@ -0,0 +1,95 @@
// Minimum transactions in a group to be displayed
export const MIN_GROUP_SIZE = 1;
// Common words to filter out when suggesting keywords
export const STOP_WORDS = [
"de",
"du",
"la",
"le",
"les",
"des",
"un",
"une",
"et",
"ou",
"par",
"pour",
"avec",
"sur",
"dans",
"en",
"au",
"aux",
"ce",
"cette",
"ces",
"mon",
"ma",
"mes",
"ton",
"ta",
"tes",
"son",
"sa",
"ses",
"notre",
"nos",
"votre",
"vos",
"leur",
"leurs",
];
// Function to normalize transaction descriptions for grouping
export function normalizeDescription(description: string): string {
return description
.toLowerCase()
.replace(/\d{2}\/\d{2}\/\d{4}/g, "") // Remove dates
.replace(/\d{2}-\d{2}-\d{4}/g, "") // Remove dates
.replace(/\d+[.,]\d+/g, "") // Remove amounts
.replace(/carte \*+\d+/gi, "CARTE") // Normalize card numbers
.replace(/cb\*+\d+/gi, "CB") // Normalize CB numbers
.replace(/\s+/g, " ") // Normalize spaces
.replace(/[^\w\s]/g, " ") // Remove special chars
.trim();
}
// Extract meaningful keywords from description
export function extractKeywords(description: string): string[] {
const normalized = normalizeDescription(description);
const words = normalized.split(/\s+/);
return words
.filter((word) => word.length > 2)
.filter((word) => !STOP_WORDS.includes(word.toLowerCase()))
.filter((word) => !/^\d+$/.test(word)); // Remove pure numbers
}
// Suggest a keyword based on common patterns in descriptions
export function suggestKeyword(descriptions: string[]): string {
// Find common substrings
const keywords = descriptions.flatMap(extractKeywords);
const frequency: Record<string, number> = {};
keywords.forEach((keyword) => {
frequency[keyword] = (frequency[keyword] || 0) + 1;
});
// Find the most frequent keyword that appears in most descriptions
const sorted = Object.entries(frequency)
.filter(([_, count]) => count >= Math.ceil(descriptions.length * 0.5))
.sort((a, b) => b[1] - a[1]);
if (sorted.length > 0) {
// Return the longest frequent keyword
return sorted.reduce((best, current) =>
current[0].length > best[0].length ? current : best
)[0];
}
// Fallback: first meaningful word from first description
const firstKeywords = extractKeywords(descriptions[0]);
return firstKeywords[0] || descriptions[0].slice(0, 15);
}

View File

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

View File

@@ -0,0 +1,278 @@
"use client";
import { useState, useMemo, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { CategoryIcon } from "@/components/ui/category-icon";
import { Tag, AlertCircle, CheckCircle2 } from "lucide-react";
import type { Category, Transaction } from "@/lib/types";
interface TransactionGroup {
key: string;
displayName: string;
transactions: Transaction[];
totalAmount: number;
suggestedKeyword: string;
}
interface RuleCreateDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
group: TransactionGroup | null;
categories: Category[];
onSave: (data: {
keyword: string;
categoryId: string;
applyToExisting: boolean;
transactionIds: string[];
}) => Promise<void>;
}
export function RuleCreateDialog({
open,
onOpenChange,
group,
categories,
onSave,
}: RuleCreateDialogProps) {
const [keyword, setKeyword] = useState("");
const [categoryId, setCategoryId] = useState<string>("");
const [applyToExisting, setApplyToExisting] = useState(true);
const [isLoading, setIsLoading] = useState(false);
// Reset form when group changes
useEffect(() => {
if (group) {
setKeyword(group.suggestedKeyword);
setCategoryId("");
setApplyToExisting(true);
}
}, [group]);
// Organize categories by parent
const { parentCategories, childrenByParent } = useMemo(() => {
const parents = categories.filter((c) => c.parentId === null);
const children: Record<string, Category[]> = {};
categories
.filter((c) => c.parentId !== null)
.forEach((child) => {
if (!children[child.parentId!]) {
children[child.parentId!] = [];
}
children[child.parentId!].push(child);
});
return { parentCategories: parents, childrenByParent: children };
}, [categories]);
// Check if keyword already exists in any category
const existingCategory = useMemo(() => {
if (!keyword) return null;
const lowerKeyword = keyword.toLowerCase();
return categories.find((c) =>
c.keywords.some((k) => k.toLowerCase() === lowerKeyword)
);
}, [keyword, categories]);
const selectedCategory = categories.find((c) => c.id === categoryId);
const handleSave = async () => {
if (!keyword || !categoryId || !group) return;
setIsLoading(true);
try {
await onSave({
keyword,
categoryId,
applyToExisting,
transactionIds: group.transactions.map((t) => t.id),
});
onOpenChange(false);
} catch (error) {
console.error("Failed to create rule:", error);
} finally {
setIsLoading(false);
}
};
if (!group) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Créer une règle de catégorisation</DialogTitle>
<DialogDescription>
Associez un mot-clé à une catégorie pour catégoriser automatiquement
les transactions similaires.
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* Group info */}
<div className="p-3 rounded-lg bg-muted/50 border border-border">
<div className="text-sm font-medium text-foreground mb-1">
{group.displayName}
</div>
<div className="text-xs text-muted-foreground">
{group.transactions.length} transaction
{group.transactions.length > 1 ? "s" : ""} seront catégorisées
</div>
</div>
{/* Keyword input */}
<div className="space-y-2">
<Label htmlFor="keyword">Mot-clé de détection</Label>
<div className="relative">
<Tag className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="keyword"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
placeholder="ex: spotify, carrefour, sncf..."
className="pl-10"
/>
</div>
<p className="text-xs text-muted-foreground">
Les transactions contenant ce mot-clé seront automatiquement
catégorisées.
</p>
{existingCategory && (
<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;
</span>
</div>
)}
</div>
{/* Category select */}
<div className="space-y-2">
<Label htmlFor="category">Catégorie</Label>
<Select value={categoryId} onValueChange={setCategoryId}>
<SelectTrigger>
<SelectValue placeholder="Sélectionner une catégorie" />
</SelectTrigger>
<SelectContent>
{parentCategories.map((parent) => (
<div key={parent.id}>
<SelectItem value={parent.id} className="font-medium">
<div className="flex items-center gap-2">
<CategoryIcon
icon={parent.icon}
color={parent.color}
size={16}
/>
<span>{parent.name}</span>
</div>
</SelectItem>
{childrenByParent[parent.id]?.map((child) => (
<SelectItem key={child.id} value={child.id}>
<div className="flex items-center gap-2 pl-4">
<CategoryIcon
icon={child.icon}
color={child.color}
size={16}
/>
<span>{child.name}</span>
</div>
</SelectItem>
))}
</div>
))}
</SelectContent>
</Select>
{selectedCategory && (
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs text-muted-foreground">
Mots-clés actuels:
</span>
{selectedCategory.keywords.length > 0 ? (
selectedCategory.keywords.map((k, i) => (
<Badge key={i} variant="outline" className="text-xs">
{k}
</Badge>
))
) : (
<span className="text-xs text-muted-foreground italic">
Aucun
</span>
)}
</div>
)}
</div>
{/* Apply to existing checkbox */}
<div className="flex items-start space-x-3">
<Checkbox
id="apply-existing"
checked={applyToExisting}
onCheckedChange={(checked) =>
setApplyToExisting(checked as boolean)
}
/>
<div className="space-y-1">
<Label
htmlFor="apply-existing"
className="text-sm font-medium cursor-pointer"
>
Appliquer aux transactions existantes
</Label>
<p className="text-xs text-muted-foreground">
Catégoriser immédiatement les {group.transactions.length}{" "}
transaction
{group.transactions.length > 1 ? "s" : ""} de ce groupe
</p>
</div>
</div>
{/* Preview */}
{keyword && categoryId && (
<div className="p-3 rounded-lg bg-success/10 border border-success/20">
<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;
</span>
</div>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Annuler
</Button>
<Button
onClick={handleSave}
disabled={!keyword || !categoryId || isLoading}
>
{isLoading ? "Création..." : "Créer la règle"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,157 @@
"use client";
import { ChevronDown, ChevronRight, Plus, Tag } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import type { Transaction, Category } from "@/lib/types";
interface TransactionGroup {
key: string;
displayName: string;
transactions: Transaction[];
totalAmount: number;
suggestedKeyword: string;
}
interface RuleGroupCardProps {
group: TransactionGroup;
isExpanded: boolean;
onToggleExpand: () => void;
onCreateRule: () => void;
categories: Category[];
formatCurrency: (amount: number) => string;
formatDate: (date: string) => string;
}
export function RuleGroupCard({
group,
isExpanded,
onToggleExpand,
onCreateRule,
formatCurrency,
formatDate,
}: RuleGroupCardProps) {
const avgAmount =
group.transactions.reduce((sum, t) => sum + t.amount, 0) /
group.transactions.length;
const isDebit = avgAmount < 0;
return (
<div className="border border-border rounded-lg bg-card overflow-hidden">
{/* Header */}
<div
className="flex items-center gap-3 p-4 cursor-pointer hover:bg-accent/5 transition-colors"
onClick={onToggleExpand}
>
<Button variant="ghost" size="icon" className="h-6 w-6 shrink-0">
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-foreground truncate">
{group.displayName}
</span>
<Badge variant="secondary" className="shrink-0">
{group.transactions.length} transaction
{group.transactions.length > 1 ? "s" : ""}
</Badge>
</div>
<div className="flex items-center gap-2 mt-1 text-sm text-muted-foreground">
<Tag className="h-3 w-3" />
<span className="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">
{group.suggestedKeyword}
</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<div
className={cn(
"font-semibold tabular-nums",
isDebit ? "text-destructive" : "text-success"
)}
>
{formatCurrency(group.totalAmount)}
</div>
<div className="text-xs text-muted-foreground">
Moy: {formatCurrency(avgAmount)}
</div>
</div>
<Button
size="sm"
onClick={(e) => {
e.stopPropagation();
onCreateRule();
}}
className="shrink-0"
>
<Plus className="h-4 w-4 mr-1" />
Créer règle
</Button>
</div>
</div>
{/* Expanded transactions list */}
{isExpanded && (
<div className="border-t border-border bg-muted/30">
<div className="max-h-64 overflow-y-auto">
<table className="w-full text-sm">
<thead className="bg-muted/50 sticky top-0">
<tr>
<th className="px-4 py-2 text-left font-medium text-muted-foreground">
Date
</th>
<th className="px-4 py-2 text-left font-medium text-muted-foreground">
Description
</th>
<th className="px-4 py-2 text-right font-medium text-muted-foreground">
Montant
</th>
</tr>
</thead>
<tbody>
{group.transactions.map((transaction) => (
<tr
key={transaction.id}
className="border-t border-border/50 hover:bg-muted/50"
>
<td className="px-4 py-2 text-muted-foreground whitespace-nowrap">
{formatDate(transaction.date)}
</td>
<td className="px-4 py-2 truncate max-w-md">
{transaction.description}
{transaction.memo && (
<span className="text-muted-foreground ml-2">
({transaction.memo})
</span>
)}
</td>
<td
className={cn(
"px-4 py-2 text-right tabular-nums whitespace-nowrap",
transaction.amount < 0
? "text-destructive"
: "text-success"
)}
>
{formatCurrency(transaction.amount)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,78 @@
"use client";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Search, Filter, ArrowUpDown } from "lucide-react";
interface RulesSearchBarProps {
searchQuery: string;
onSearchChange: (value: string) => void;
sortBy: "count" | "amount" | "name";
onSortChange: (value: "count" | "amount" | "name") => void;
filterMinCount: number;
onFilterMinCountChange: (value: number) => void;
}
export function RulesSearchBar({
searchQuery,
onSearchChange,
sortBy,
onSortChange,
filterMinCount,
onFilterMinCountChange,
}: RulesSearchBarProps) {
return (
<div className="flex flex-col sm:flex-row gap-3 mb-6">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Rechercher dans les descriptions..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10"
/>
</div>
<div className="flex gap-3">
<Select
value={sortBy}
onValueChange={(v) => onSortChange(v as "count" | "amount" | "name")}
>
<SelectTrigger className="w-44">
<ArrowUpDown className="h-4 w-4 mr-2" />
<SelectValue placeholder="Trier par" />
</SelectTrigger>
<SelectContent>
<SelectItem value="count">Nombre de transactions</SelectItem>
<SelectItem value="amount">Montant total</SelectItem>
<SelectItem value="name">Nom</SelectItem>
</SelectContent>
</Select>
<Select
value={filterMinCount.toString()}
onValueChange={(v) => onFilterMinCountChange(parseInt(v))}
>
<SelectTrigger className="w-36">
<Filter className="h-4 w-4 mr-2" />
<SelectValue placeholder="Minimum" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1+ transactions</SelectItem>
<SelectItem value="2">2+ transactions</SelectItem>
<SelectItem value="3">3+ transactions</SelectItem>
<SelectItem value="5">5+ transactions</SelectItem>
<SelectItem value="10">10+ transactions</SelectItem>
</SelectContent>
</Select>
</div>
</div>
);
}