feat: add search functionality and expand/collapse controls to categories page for improved user experience
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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: [],
|
||||||
|
|||||||
Reference in New Issue
Block a user