feat: add search functionality and expand/collapse controls to categories page for improved user experience

This commit is contained in:
Julien Froidefond
2025-11-27 10:46:07 +01:00
parent 91e15ee07e
commit cc1e8c20a6
2 changed files with 84 additions and 19 deletions

View File

@@ -11,7 +11,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Plus, MoreVertical, Pencil, Trash2, RefreshCw, X, ChevronDown, ChevronRight } from "lucide-react" import { Plus, MoreVertical, Pencil, Trash2, RefreshCw, X, ChevronDown, ChevronRight, ChevronsUpDown, Search } from "lucide-react"
import { CategoryIcon } from "@/components/ui/category-icon" import { CategoryIcon } from "@/components/ui/category-icon"
import { autoCategorize, addCategory, updateCategory, deleteCategory } from "@/lib/store-db" import { autoCategorize, addCategory, updateCategory, deleteCategory } from "@/lib/store-db"
import type { Category } from "@/lib/types" import type { Category } from "@/lib/types"
@@ -35,6 +35,7 @@ export default function CategoriesPage() {
parentId: null as string | null, parentId: null as string | null,
}) })
const [newKeyword, setNewKeyword] = useState("") const [newKeyword, setNewKeyword] = useState("")
const [searchQuery, setSearchQuery] = useState("")
// Organiser les catégories par parent // Organiser les catégories par parent
const { parentCategories, childrenByParent, orphanCategories } = useMemo(() => { const { parentCategories, childrenByParent, orphanCategories } = useMemo(() => {
@@ -111,6 +112,16 @@ export default function CategoriesPage() {
setExpandedParents(newExpanded) setExpandedParents(newExpanded)
} }
const expandAll = () => {
setExpandedParents(new Set(parentCategories.map((p) => p.id)))
}
const collapseAll = () => {
setExpandedParents(new Set())
}
const allExpanded = parentCategories.length > 0 && expandedParents.size === parentCategories.length
const handleNewCategory = (parentId: string | null = null) => { const handleNewCategory = (parentId: string | null = null) => {
setEditingCategory(null) setEditingCategory(null)
setFormData({ name: "", color: "#22c55e", keywords: [], parentId }) setFormData({ name: "", color: "#22c55e", keywords: [], parentId })
@@ -282,12 +293,66 @@ export default function CategoriesPage() {
</div> </div>
</div> </div>
{/* Barre de recherche et contrôles */}
<div className="flex items-center gap-3">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Rechercher une catégorie ou un mot-clé..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery("")}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="w-4 h-4" />
</button>
)}
</div>
<Button
variant="outline"
size="sm"
onClick={allExpanded ? collapseAll : expandAll}
>
<ChevronsUpDown className="w-4 h-4 mr-2" />
{allExpanded ? "Tout replier" : "Tout déplier"}
</Button>
</div>
{/* Liste des catégories par parent */} {/* Liste des catégories par parent */}
<div className="space-y-1"> <div className="space-y-1">
{parentCategories.map((parent) => { {parentCategories
const children = childrenByParent[parent.id] || [] .filter((parent) => {
if (!searchQuery.trim()) return true
const query = searchQuery.toLowerCase()
// Afficher si le parent matche
if (parent.name.toLowerCase().includes(query)) return true
if (parent.keywords.some((k) => k.toLowerCase().includes(query))) return true
// Ou si un enfant matche
const children = childrenByParent[parent.id] || []
return children.some(
(c) =>
c.name.toLowerCase().includes(query) ||
c.keywords.some((k) => k.toLowerCase().includes(query))
)
})
.map((parent) => {
const allChildren = childrenByParent[parent.id] || []
// Filtrer les enfants aussi si recherche active
const children = searchQuery.trim()
? allChildren.filter(
(c) =>
c.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
c.keywords.some((k) => k.toLowerCase().includes(searchQuery.toLowerCase())) ||
// Garder tous les enfants si le parent matche
parent.name.toLowerCase().includes(searchQuery.toLowerCase())
)
: allChildren
const stats = getCategoryStats(parent.id, true) const stats = getCategoryStats(parent.id, true)
const isExpanded = expandedParents.has(parent.id) const isExpanded = expandedParents.has(parent.id) || (searchQuery.trim() !== "" && children.length > 0)
return ( return (
<div key={parent.id} className="border rounded-lg bg-card"> <div key={parent.id} className="border rounded-lg bg-card">

View File

@@ -18,7 +18,7 @@ export const defaultCategories: CategoryDefinition[] = [
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
{ {
slug: "alimentation", slug: "alimentation",
name: "🛒 Alimentation", name: "Alimentation",
color: "#22c55e", color: "#22c55e",
icon: "utensils", icon: "utensils",
keywords: [], keywords: [],
@@ -84,7 +84,7 @@ export const defaultCategories: CategoryDefinition[] = [
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
{ {
slug: "transport", slug: "transport",
name: "🚗 Transport", name: "Transport",
color: "#3b82f6", color: "#3b82f6",
icon: "car", icon: "car",
keywords: [], keywords: [],
@@ -201,7 +201,7 @@ export const defaultCategories: CategoryDefinition[] = [
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
{ {
slug: "logement", slug: "logement",
name: "🏠 Logement", name: "Logement",
color: "#f59e0b", color: "#f59e0b",
icon: "home", icon: "home",
keywords: [], keywords: [],
@@ -291,7 +291,7 @@ export const defaultCategories: CategoryDefinition[] = [
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
{ {
slug: "sante", slug: "sante",
name: "💊 Santé", name: "Santé",
color: "#ef4444", color: "#ef4444",
icon: "heart", icon: "heart",
keywords: [], keywords: [],
@@ -385,7 +385,7 @@ export const defaultCategories: CategoryDefinition[] = [
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
{ {
slug: "loisirs", slug: "loisirs",
name: "🎬 Loisirs", name: "Loisirs",
color: "#8b5cf6", color: "#8b5cf6",
icon: "gamepad", icon: "gamepad",
keywords: [], keywords: [],
@@ -482,7 +482,7 @@ export const defaultCategories: CategoryDefinition[] = [
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
{ {
slug: "shopping", slug: "shopping",
name: "👕 Shopping", name: "Shopping",
color: "#06b6d4", color: "#06b6d4",
icon: "shopping-bag", icon: "shopping-bag",
keywords: [], keywords: [],
@@ -548,7 +548,7 @@ export const defaultCategories: CategoryDefinition[] = [
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
{ {
slug: "abonnements", slug: "abonnements",
name: "📱 Abonnements", name: "Abonnements",
color: "#8b5cf6", color: "#8b5cf6",
icon: "repeat", icon: "repeat",
keywords: [], keywords: [],
@@ -592,7 +592,7 @@ export const defaultCategories: CategoryDefinition[] = [
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
{ {
slug: "finance", slug: "finance",
name: "💰 Finance", name: "Finance",
color: "#64748b", color: "#64748b",
icon: "landmark", icon: "landmark",
keywords: [], keywords: [],
@@ -691,7 +691,7 @@ export const defaultCategories: CategoryDefinition[] = [
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
{ {
slug: "revenus", slug: "revenus",
name: "💵 Revenus", name: "Revenus",
color: "#10b981", color: "#10b981",
icon: "wallet", icon: "wallet",
keywords: [], keywords: [],
@@ -749,7 +749,7 @@ export const defaultCategories: CategoryDefinition[] = [
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
{ {
slug: "voyage", slug: "voyage",
name: "✈️ Voyage", name: "Voyage",
color: "#a855f7", color: "#a855f7",
icon: "plane", icon: "plane",
keywords: [], keywords: [],
@@ -793,7 +793,7 @@ export const defaultCategories: CategoryDefinition[] = [
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
{ {
slug: "education", slug: "education",
name: "📚 Éducation", name: "Éducation",
color: "#0284c7", color: "#0284c7",
icon: "graduation-cap", icon: "graduation-cap",
keywords: [], keywords: [],
@@ -836,7 +836,7 @@ export const defaultCategories: CategoryDefinition[] = [
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
{ {
slug: "animaux", slug: "animaux",
name: "🐕 Animaux", name: "Animaux",
color: "#ea580c", color: "#ea580c",
icon: "paw-print", icon: "paw-print",
keywords: [ keywords: [
@@ -857,7 +857,7 @@ export const defaultCategories: CategoryDefinition[] = [
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
{ {
slug: "auto", slug: "auto",
name: "🔧 Auto & Moto", name: "Auto & Moto",
color: "#78716c", color: "#78716c",
icon: "car", icon: "car",
keywords: [], keywords: [],
@@ -899,7 +899,7 @@ export const defaultCategories: CategoryDefinition[] = [
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
{ {
slug: "dons", slug: "dons",
name: "🎁 Dons & Cadeaux", name: "Dons & Cadeaux",
color: "#f43f5e", color: "#f43f5e",
icon: "gift", icon: "gift",
keywords: [], keywords: [],
@@ -935,7 +935,7 @@ export const defaultCategories: CategoryDefinition[] = [
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
{ {
slug: "divers", slug: "divers",
name: "Divers", name: "Divers",
color: "#71717a", color: "#71717a",
icon: "more-horizontal", icon: "more-horizontal",
keywords: [], keywords: [],