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 { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
||||
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 { autoCategorize, addCategory, updateCategory, deleteCategory } from "@/lib/store-db"
|
||||
import type { Category } from "@/lib/types"
|
||||
@@ -35,6 +35,7 @@ export default function CategoriesPage() {
|
||||
parentId: null as string | null,
|
||||
})
|
||||
const [newKeyword, setNewKeyword] = useState("")
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
|
||||
// Organiser les catégories par parent
|
||||
const { parentCategories, childrenByParent, orphanCategories } = useMemo(() => {
|
||||
@@ -111,6 +112,16 @@ export default function CategoriesPage() {
|
||||
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) => {
|
||||
setEditingCategory(null)
|
||||
setFormData({ name: "", color: "#22c55e", keywords: [], parentId })
|
||||
@@ -282,12 +293,66 @@ export default function CategoriesPage() {
|
||||
</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 */}
|
||||
<div className="space-y-1">
|
||||
{parentCategories.map((parent) => {
|
||||
{parentCategories
|
||||
.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 isExpanded = expandedParents.has(parent.id)
|
||||
const isExpanded = expandedParents.has(parent.id) || (searchQuery.trim() !== "" && children.length > 0)
|
||||
|
||||
return (
|
||||
<div key={parent.id} className="border rounded-lg bg-card">
|
||||
|
||||
@@ -18,7 +18,7 @@ export const defaultCategories: CategoryDefinition[] = [
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
{
|
||||
slug: "alimentation",
|
||||
name: "🛒 Alimentation",
|
||||
name: "Alimentation",
|
||||
color: "#22c55e",
|
||||
icon: "utensils",
|
||||
keywords: [],
|
||||
@@ -84,7 +84,7 @@ export const defaultCategories: CategoryDefinition[] = [
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
{
|
||||
slug: "transport",
|
||||
name: "🚗 Transport",
|
||||
name: "Transport",
|
||||
color: "#3b82f6",
|
||||
icon: "car",
|
||||
keywords: [],
|
||||
@@ -201,7 +201,7 @@ export const defaultCategories: CategoryDefinition[] = [
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
{
|
||||
slug: "logement",
|
||||
name: "🏠 Logement",
|
||||
name: "Logement",
|
||||
color: "#f59e0b",
|
||||
icon: "home",
|
||||
keywords: [],
|
||||
@@ -291,7 +291,7 @@ export const defaultCategories: CategoryDefinition[] = [
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
{
|
||||
slug: "sante",
|
||||
name: "💊 Santé",
|
||||
name: "Santé",
|
||||
color: "#ef4444",
|
||||
icon: "heart",
|
||||
keywords: [],
|
||||
@@ -385,7 +385,7 @@ export const defaultCategories: CategoryDefinition[] = [
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
{
|
||||
slug: "loisirs",
|
||||
name: "🎬 Loisirs",
|
||||
name: "Loisirs",
|
||||
color: "#8b5cf6",
|
||||
icon: "gamepad",
|
||||
keywords: [],
|
||||
@@ -482,7 +482,7 @@ export const defaultCategories: CategoryDefinition[] = [
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
{
|
||||
slug: "shopping",
|
||||
name: "👕 Shopping",
|
||||
name: "Shopping",
|
||||
color: "#06b6d4",
|
||||
icon: "shopping-bag",
|
||||
keywords: [],
|
||||
@@ -548,7 +548,7 @@ export const defaultCategories: CategoryDefinition[] = [
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
{
|
||||
slug: "abonnements",
|
||||
name: "📱 Abonnements",
|
||||
name: "Abonnements",
|
||||
color: "#8b5cf6",
|
||||
icon: "repeat",
|
||||
keywords: [],
|
||||
@@ -592,7 +592,7 @@ export const defaultCategories: CategoryDefinition[] = [
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
{
|
||||
slug: "finance",
|
||||
name: "💰 Finance",
|
||||
name: "Finance",
|
||||
color: "#64748b",
|
||||
icon: "landmark",
|
||||
keywords: [],
|
||||
@@ -691,7 +691,7 @@ export const defaultCategories: CategoryDefinition[] = [
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
{
|
||||
slug: "revenus",
|
||||
name: "💵 Revenus",
|
||||
name: "Revenus",
|
||||
color: "#10b981",
|
||||
icon: "wallet",
|
||||
keywords: [],
|
||||
@@ -749,7 +749,7 @@ export const defaultCategories: CategoryDefinition[] = [
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
{
|
||||
slug: "voyage",
|
||||
name: "✈️ Voyage",
|
||||
name: "Voyage",
|
||||
color: "#a855f7",
|
||||
icon: "plane",
|
||||
keywords: [],
|
||||
@@ -793,7 +793,7 @@ export const defaultCategories: CategoryDefinition[] = [
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
{
|
||||
slug: "education",
|
||||
name: "📚 Éducation",
|
||||
name: "Éducation",
|
||||
color: "#0284c7",
|
||||
icon: "graduation-cap",
|
||||
keywords: [],
|
||||
@@ -836,7 +836,7 @@ export const defaultCategories: CategoryDefinition[] = [
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
{
|
||||
slug: "animaux",
|
||||
name: "🐕 Animaux",
|
||||
name: "Animaux",
|
||||
color: "#ea580c",
|
||||
icon: "paw-print",
|
||||
keywords: [
|
||||
@@ -857,7 +857,7 @@ export const defaultCategories: CategoryDefinition[] = [
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
{
|
||||
slug: "auto",
|
||||
name: "🔧 Auto & Moto",
|
||||
name: "Auto & Moto",
|
||||
color: "#78716c",
|
||||
icon: "car",
|
||||
keywords: [],
|
||||
@@ -899,7 +899,7 @@ export const defaultCategories: CategoryDefinition[] = [
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
{
|
||||
slug: "dons",
|
||||
name: "🎁 Dons & Cadeaux",
|
||||
name: "Dons & Cadeaux",
|
||||
color: "#f43f5e",
|
||||
icon: "gift",
|
||||
keywords: [],
|
||||
@@ -935,7 +935,7 @@ export const defaultCategories: CategoryDefinition[] = [
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
{
|
||||
slug: "divers",
|
||||
name: "❓ Divers",
|
||||
name: "Divers",
|
||||
color: "#71717a",
|
||||
icon: "more-horizontal",
|
||||
keywords: [],
|
||||
|
||||
Reference in New Issue
Block a user