@create-lft-app/nextjs 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +549 -549
  2. package/package.json +48 -48
  3. package/template/.claude/skills/anti-patterns.md +150 -0
  4. package/template/.claude/skills/drizzle-schema.md +178 -0
  5. package/template/.claude/skills/formatting.md +56 -0
  6. package/template/.claude/skills/module-architecture.md +143 -0
  7. package/template/.claude/skills/supabase-server-actions.md +199 -0
  8. package/template/.claude/skills/ui-patterns.md +161 -0
  9. package/template/CLAUDE.md +114 -1239
  10. package/template/drizzle.config.ts +12 -12
  11. package/template/eslint.config.mjs +16 -16
  12. package/template/gitignore +36 -36
  13. package/template/next.config.ts +7 -7
  14. package/template/package.json +86 -86
  15. package/template/postcss.config.mjs +7 -7
  16. package/template/proxy.ts +12 -12
  17. package/template/public/logolft.svg +11 -11
  18. package/template/src/app/(auth)/dashboard/dashboard-content.tsx +124 -124
  19. package/template/src/app/(auth)/dashboard/page.tsx +9 -9
  20. package/template/src/app/(auth)/layout.tsx +7 -7
  21. package/template/src/app/(auth)/users/page.tsx +9 -9
  22. package/template/src/app/(auth)/users/users-content.tsx +26 -26
  23. package/template/src/app/(public)/layout.tsx +7 -7
  24. package/template/src/app/(public)/login/page.tsx +17 -17
  25. package/template/src/app/api/webhooks/route.ts +20 -20
  26. package/template/src/app/globals.css +249 -249
  27. package/template/src/app/layout.tsx +37 -37
  28. package/template/src/app/page.tsx +5 -5
  29. package/template/src/app/providers.tsx +27 -27
  30. package/template/src/components/layout/main-content.tsx +28 -28
  31. package/template/src/components/layout/sidebar-context.tsx +33 -33
  32. package/template/src/components/layout/sidebar.tsx +141 -141
  33. package/template/src/components/tables/data-table-column-header.tsx +68 -68
  34. package/template/src/components/tables/data-table-date-filter.tsx +203 -203
  35. package/template/src/components/tables/data-table-faceted-filter.tsx +185 -185
  36. package/template/src/components/tables/data-table-filters-dropdown.tsx +130 -130
  37. package/template/src/components/tables/data-table-number-filter.tsx +295 -295
  38. package/template/src/components/tables/data-table-pagination.tsx +99 -99
  39. package/template/src/components/tables/data-table-toolbar.tsx +140 -140
  40. package/template/src/components/tables/data-table-view-options.tsx +63 -63
  41. package/template/src/components/tables/data-table.tsx +148 -148
  42. package/template/src/components/tables/index.ts +9 -9
  43. package/template/src/components/ui/accordion.tsx +58 -58
  44. package/template/src/components/ui/alert-dialog.tsx +165 -165
  45. package/template/src/components/ui/alert.tsx +66 -66
  46. package/template/src/components/ui/animations/index.ts +44 -44
  47. package/template/src/components/ui/avatar.tsx +55 -55
  48. package/template/src/components/ui/badge.tsx +50 -50
  49. package/template/src/components/ui/button.tsx +118 -118
  50. package/template/src/components/ui/calendar.tsx +220 -220
  51. package/template/src/components/ui/card.tsx +113 -113
  52. package/template/src/components/ui/checkbox.tsx +38 -38
  53. package/template/src/components/ui/collapsible.tsx +33 -33
  54. package/template/src/components/ui/command.tsx +196 -196
  55. package/template/src/components/ui/dialog.tsx +156 -156
  56. package/template/src/components/ui/dropdown-menu.tsx +280 -280
  57. package/template/src/components/ui/form.tsx +171 -171
  58. package/template/src/components/ui/icons.tsx +167 -167
  59. package/template/src/components/ui/input.tsx +28 -28
  60. package/template/src/components/ui/label.tsx +25 -25
  61. package/template/src/components/ui/motion.tsx +197 -197
  62. package/template/src/components/ui/page-transition.tsx +166 -166
  63. package/template/src/components/ui/popover.tsx +59 -59
  64. package/template/src/components/ui/progress.tsx +32 -32
  65. package/template/src/components/ui/radio-group.tsx +45 -45
  66. package/template/src/components/ui/scroll-area.tsx +63 -63
  67. package/template/src/components/ui/select.tsx +208 -208
  68. package/template/src/components/ui/separator.tsx +28 -28
  69. package/template/src/components/ui/sheet.tsx +170 -170
  70. package/template/src/components/ui/sidebar.tsx +726 -726
  71. package/template/src/components/ui/skeleton.tsx +15 -15
  72. package/template/src/components/ui/slider.tsx +58 -58
  73. package/template/src/components/ui/sonner.tsx +47 -47
  74. package/template/src/components/ui/spinner.tsx +27 -27
  75. package/template/src/components/ui/submit-button.tsx +47 -47
  76. package/template/src/components/ui/switch.tsx +31 -31
  77. package/template/src/components/ui/table.tsx +120 -120
  78. package/template/src/components/ui/tabs.tsx +75 -75
  79. package/template/src/components/ui/textarea.tsx +26 -26
  80. package/template/src/components/ui/tooltip.tsx +70 -70
  81. package/template/src/config/navigation.ts +59 -59
  82. package/template/src/config/roles.ts +27 -27
  83. package/template/src/config/site.ts +12 -12
  84. package/template/src/db/index.ts +12 -12
  85. package/template/src/db/schema/index.ts +1 -1
  86. package/template/src/db/schema/users.ts +16 -16
  87. package/template/src/db/seed.ts +39 -39
  88. package/template/src/hooks/index.ts +3 -3
  89. package/template/src/hooks/use-mobile.ts +21 -21
  90. package/template/src/hooks/useDataTable.ts +82 -82
  91. package/template/src/hooks/useDebounce.ts +49 -49
  92. package/template/src/hooks/useMediaQuery.ts +36 -36
  93. package/template/src/lib/date/config.ts +36 -36
  94. package/template/src/lib/date/formatters.ts +127 -127
  95. package/template/src/lib/date/index.ts +26 -26
  96. package/template/src/lib/excel/exporter.ts +89 -89
  97. package/template/src/lib/excel/index.ts +14 -14
  98. package/template/src/lib/excel/parser.ts +96 -96
  99. package/template/src/lib/query-client.ts +35 -35
  100. package/template/src/lib/supabase/admin.ts +23 -23
  101. package/template/src/lib/supabase/client.ts +11 -11
  102. package/template/src/lib/supabase/proxy.ts +67 -67
  103. package/template/src/lib/supabase/server.ts +38 -38
  104. package/template/src/lib/supabase/types.ts +53 -53
  105. package/template/src/lib/utils.ts +6 -6
  106. package/template/src/lib/validations/common.ts +75 -75
  107. package/template/src/lib/validations/index.ts +20 -20
  108. package/template/src/modules/auth/actions/auth-actions.ts +59 -59
  109. package/template/src/modules/auth/components/login-form.tsx +68 -68
  110. package/template/src/modules/auth/hooks/useAuth.ts +38 -38
  111. package/template/src/modules/auth/hooks/useAuthMutations.ts +43 -43
  112. package/template/src/modules/auth/hooks/useAuthQueries.ts +43 -43
  113. package/template/src/modules/auth/index.ts +12 -12
  114. package/template/src/modules/auth/schemas/auth.schema.ts +32 -32
  115. package/template/src/modules/auth/stores/useAuthStore.ts +37 -37
  116. package/template/src/modules/users/actions/users-actions.ts +166 -166
  117. package/template/src/modules/users/columns.tsx +106 -106
  118. package/template/src/modules/users/components/users-list.tsx +48 -48
  119. package/template/src/modules/users/hooks/useUsers.ts +39 -39
  120. package/template/src/modules/users/hooks/useUsersMutations.ts +55 -55
  121. package/template/src/modules/users/hooks/useUsersQueries.ts +35 -35
  122. package/template/src/modules/users/index.ts +30 -30
  123. package/template/src/modules/users/schemas/users.schema.ts +51 -51
  124. package/template/src/modules/users/stores/useUsersStore.ts +60 -60
  125. package/template/src/modules/users/types/auth-user.types.ts +42 -42
  126. package/template/src/modules/users/utils/user-mapper.ts +32 -32
  127. package/template/src/stores/index.ts +1 -1
  128. package/template/src/stores/useUiStore.ts +55 -55
  129. package/template/src/types/api.ts +28 -28
  130. package/template/src/types/index.ts +2 -2
  131. package/template/src/types/table.ts +34 -34
  132. package/template/supabase/config.toml +94 -94
  133. package/template/tsconfig.json +42 -42
@@ -1,185 +1,185 @@
1
- 'use client'
2
-
3
- import * as React from 'react'
4
- import { Column } from '@tanstack/react-table'
5
- import { Check, PlusCircle } from 'lucide-react'
6
-
7
- import { cn } from '@/lib/utils'
8
- import { Badge } from '@/components/ui/badge'
9
- import { Button } from '@/components/ui/button'
10
- import {
11
- Command,
12
- CommandEmpty,
13
- CommandGroup,
14
- CommandInput,
15
- CommandItem,
16
- CommandList,
17
- CommandSeparator,
18
- } from '@/components/ui/command'
19
- import {
20
- Popover,
21
- PopoverContent,
22
- PopoverTrigger,
23
- } from '@/components/ui/popover'
24
- import { Separator } from '@/components/ui/separator'
25
-
26
- export interface FacetedFilterOption {
27
- label: string
28
- value: string
29
- icon?: React.ComponentType<{ className?: string }>
30
- }
31
-
32
- interface DataTableFacetedFilterProps<TData, TValue> {
33
- column?: Column<TData, TValue>
34
- title?: string
35
- options: FacetedFilterOption[]
36
- }
37
-
38
- export function DataTableFacetedFilter<TData, TValue>({
39
- column,
40
- title,
41
- options,
42
- }: DataTableFacetedFilterProps<TData, TValue>) {
43
- const facets = column?.getFacetedUniqueValues()
44
- const filterValue = column?.getFilterValue() as string[] | undefined
45
- const [open, setOpen] = React.useState(false)
46
- const [selectedValues, setSelectedValues] = React.useState<Set<string>>(
47
- new Set(filterValue)
48
- )
49
-
50
- // Sincronizar estado local con el filtro de la columna
51
- React.useEffect(() => {
52
- setSelectedValues(new Set(filterValue))
53
- }, [filterValue])
54
-
55
- const handleApply = () => {
56
- const filterValues = Array.from(selectedValues)
57
- column?.setFilterValue(filterValues.length ? filterValues : undefined)
58
- setOpen(false)
59
- }
60
-
61
- const handleClear = () => {
62
- setSelectedValues(new Set())
63
- column?.setFilterValue(undefined)
64
- }
65
-
66
- const handleOpenChange = (isOpen: boolean) => {
67
- if (!isOpen) {
68
- // Al cerrar sin aplicar, resetear al valor actual del filtro
69
- setSelectedValues(new Set(filterValue))
70
- }
71
- setOpen(isOpen)
72
- }
73
-
74
- return (
75
- <Popover open={open} onOpenChange={handleOpenChange}>
76
- <PopoverTrigger asChild>
77
- <Button
78
- variant="outline"
79
- size="sm"
80
- className="h-8 border-dashed"
81
- >
82
- <PlusCircle className="mr-2 h-4 w-4" />
83
- {title}
84
- {selectedValues.size > 0 && (
85
- <>
86
- <Separator orientation="vertical" className="mx-2 h-4" />
87
- <Badge
88
- variant="secondary"
89
- className="rounded-sm px-1 font-normal lg:hidden"
90
- >
91
- {selectedValues.size}
92
- </Badge>
93
- <div className="hidden space-x-1 lg:flex">
94
- {selectedValues.size > 2 ? (
95
- <Badge
96
- variant="secondary"
97
- className="rounded-sm px-1 font-normal"
98
- >
99
- {selectedValues.size} seleccionados
100
- </Badge>
101
- ) : (
102
- options
103
- .filter((option) => selectedValues.has(option.value))
104
- .map((option) => (
105
- <Badge
106
- variant="secondary"
107
- key={option.value}
108
- className="rounded-sm px-1 font-normal"
109
- >
110
- {option.label}
111
- </Badge>
112
- ))
113
- )}
114
- </div>
115
- </>
116
- )}
117
- </Button>
118
- </PopoverTrigger>
119
- <PopoverContent className="w-[200px] p-0" side="bottom" align="start" sideOffset={4}>
120
- <Command>
121
- <CommandInput placeholder={title} />
122
- <CommandList>
123
- <CommandEmpty>Sin resultados.</CommandEmpty>
124
- <CommandGroup>
125
- {options.map((option) => {
126
- const isSelected = selectedValues.has(option.value)
127
- return (
128
- <CommandItem
129
- key={option.value}
130
- onSelect={() => {
131
- const newSelected = new Set(selectedValues)
132
- if (isSelected) {
133
- newSelected.delete(option.value)
134
- } else {
135
- newSelected.add(option.value)
136
- }
137
- setSelectedValues(newSelected)
138
- }}
139
- >
140
- <div
141
- className={cn(
142
- 'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
143
- isSelected
144
- ? 'bg-primary text-primary-foreground'
145
- : 'opacity-50 [&_svg]:invisible'
146
- )}
147
- >
148
- <Check className="h-4 w-4" />
149
- </div>
150
- {option.icon && (
151
- <option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
152
- )}
153
- <span>{option.label}</span>
154
- {facets?.get(option.value) && (
155
- <span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
156
- {facets.get(option.value)}
157
- </span>
158
- )}
159
- </CommandItem>
160
- )
161
- })}
162
- </CommandGroup>
163
- </CommandList>
164
- </Command>
165
- <div className="flex gap-2 border-t border-border p-2">
166
- <Button
167
- variant="outline"
168
- size="sm"
169
- className="flex-1"
170
- onClick={handleClear}
171
- >
172
- Limpiar
173
- </Button>
174
- <Button
175
- size="sm"
176
- className="flex-1"
177
- onClick={handleApply}
178
- >
179
- Aplicar
180
- </Button>
181
- </div>
182
- </PopoverContent>
183
- </Popover>
184
- )
185
- }
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { Column } from '@tanstack/react-table'
5
+ import { Check, PlusCircle } from 'lucide-react'
6
+
7
+ import { cn } from '@/lib/utils'
8
+ import { Badge } from '@/components/ui/badge'
9
+ import { Button } from '@/components/ui/button'
10
+ import {
11
+ Command,
12
+ CommandEmpty,
13
+ CommandGroup,
14
+ CommandInput,
15
+ CommandItem,
16
+ CommandList,
17
+ CommandSeparator,
18
+ } from '@/components/ui/command'
19
+ import {
20
+ Popover,
21
+ PopoverContent,
22
+ PopoverTrigger,
23
+ } from '@/components/ui/popover'
24
+ import { Separator } from '@/components/ui/separator'
25
+
26
+ export interface FacetedFilterOption {
27
+ label: string
28
+ value: string
29
+ icon?: React.ComponentType<{ className?: string }>
30
+ }
31
+
32
+ interface DataTableFacetedFilterProps<TData, TValue> {
33
+ column?: Column<TData, TValue>
34
+ title?: string
35
+ options: FacetedFilterOption[]
36
+ }
37
+
38
+ export function DataTableFacetedFilter<TData, TValue>({
39
+ column,
40
+ title,
41
+ options,
42
+ }: DataTableFacetedFilterProps<TData, TValue>) {
43
+ const facets = column?.getFacetedUniqueValues()
44
+ const filterValue = column?.getFilterValue() as string[] | undefined
45
+ const [open, setOpen] = React.useState(false)
46
+ const [selectedValues, setSelectedValues] = React.useState<Set<string>>(
47
+ new Set(filterValue)
48
+ )
49
+
50
+ // Sincronizar estado local con el filtro de la columna
51
+ React.useEffect(() => {
52
+ setSelectedValues(new Set(filterValue))
53
+ }, [filterValue])
54
+
55
+ const handleApply = () => {
56
+ const filterValues = Array.from(selectedValues)
57
+ column?.setFilterValue(filterValues.length ? filterValues : undefined)
58
+ setOpen(false)
59
+ }
60
+
61
+ const handleClear = () => {
62
+ setSelectedValues(new Set())
63
+ column?.setFilterValue(undefined)
64
+ }
65
+
66
+ const handleOpenChange = (isOpen: boolean) => {
67
+ if (!isOpen) {
68
+ // Al cerrar sin aplicar, resetear al valor actual del filtro
69
+ setSelectedValues(new Set(filterValue))
70
+ }
71
+ setOpen(isOpen)
72
+ }
73
+
74
+ return (
75
+ <Popover open={open} onOpenChange={handleOpenChange}>
76
+ <PopoverTrigger asChild>
77
+ <Button
78
+ variant="outline"
79
+ size="sm"
80
+ className="h-8 border-dashed"
81
+ >
82
+ <PlusCircle className="mr-2 h-4 w-4" />
83
+ {title}
84
+ {selectedValues.size > 0 && (
85
+ <>
86
+ <Separator orientation="vertical" className="mx-2 h-4" />
87
+ <Badge
88
+ variant="secondary"
89
+ className="rounded-sm px-1 font-normal lg:hidden"
90
+ >
91
+ {selectedValues.size}
92
+ </Badge>
93
+ <div className="hidden space-x-1 lg:flex">
94
+ {selectedValues.size > 2 ? (
95
+ <Badge
96
+ variant="secondary"
97
+ className="rounded-sm px-1 font-normal"
98
+ >
99
+ {selectedValues.size} seleccionados
100
+ </Badge>
101
+ ) : (
102
+ options
103
+ .filter((option) => selectedValues.has(option.value))
104
+ .map((option) => (
105
+ <Badge
106
+ variant="secondary"
107
+ key={option.value}
108
+ className="rounded-sm px-1 font-normal"
109
+ >
110
+ {option.label}
111
+ </Badge>
112
+ ))
113
+ )}
114
+ </div>
115
+ </>
116
+ )}
117
+ </Button>
118
+ </PopoverTrigger>
119
+ <PopoverContent className="w-[200px] p-0" side="bottom" align="start" sideOffset={4}>
120
+ <Command>
121
+ <CommandInput placeholder={title} />
122
+ <CommandList>
123
+ <CommandEmpty>Sin resultados.</CommandEmpty>
124
+ <CommandGroup>
125
+ {options.map((option) => {
126
+ const isSelected = selectedValues.has(option.value)
127
+ return (
128
+ <CommandItem
129
+ key={option.value}
130
+ onSelect={() => {
131
+ const newSelected = new Set(selectedValues)
132
+ if (isSelected) {
133
+ newSelected.delete(option.value)
134
+ } else {
135
+ newSelected.add(option.value)
136
+ }
137
+ setSelectedValues(newSelected)
138
+ }}
139
+ >
140
+ <div
141
+ className={cn(
142
+ 'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
143
+ isSelected
144
+ ? 'bg-primary text-primary-foreground'
145
+ : 'opacity-50 [&_svg]:invisible'
146
+ )}
147
+ >
148
+ <Check className="h-4 w-4" />
149
+ </div>
150
+ {option.icon && (
151
+ <option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
152
+ )}
153
+ <span>{option.label}</span>
154
+ {facets?.get(option.value) && (
155
+ <span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
156
+ {facets.get(option.value)}
157
+ </span>
158
+ )}
159
+ </CommandItem>
160
+ )
161
+ })}
162
+ </CommandGroup>
163
+ </CommandList>
164
+ </Command>
165
+ <div className="flex gap-2 border-t border-border p-2">
166
+ <Button
167
+ variant="outline"
168
+ size="sm"
169
+ className="flex-1"
170
+ onClick={handleClear}
171
+ >
172
+ Limpiar
173
+ </Button>
174
+ <Button
175
+ size="sm"
176
+ className="flex-1"
177
+ onClick={handleApply}
178
+ >
179
+ Aplicar
180
+ </Button>
181
+ </div>
182
+ </PopoverContent>
183
+ </Popover>
184
+ )
185
+ }
@@ -1,130 +1,130 @@
1
- 'use client'
2
-
3
- import * as React from 'react'
4
- import { Table } from '@tanstack/react-table'
5
- import { Filter, X } from 'lucide-react'
6
-
7
- import { cn } from '@/lib/utils'
8
- import { Badge } from '@/components/ui/badge'
9
- import { Button } from '@/components/ui/button'
10
- import {
11
- Popover,
12
- PopoverContent,
13
- PopoverTrigger,
14
- } from '@/components/ui/popover'
15
- import { Separator } from '@/components/ui/separator'
16
- import { DataTableFacetedFilter } from './data-table-faceted-filter'
17
- import { DataTableDateFilter } from './data-table-date-filter'
18
- import { DataTableNumberFilter } from './data-table-number-filter'
19
- import type { FilterConfig } from './data-table'
20
-
21
- interface DataTableFiltersDropdownProps<TData> {
22
- table: Table<TData>
23
- filters: FilterConfig[]
24
- }
25
-
26
- export function DataTableFiltersDropdown<TData>({
27
- table,
28
- filters,
29
- }: DataTableFiltersDropdownProps<TData>) {
30
- const [open, setOpen] = React.useState(false)
31
-
32
- // Contar filtros activos (excluyendo el search)
33
- const activeFiltersCount = table.getState().columnFilters.filter(
34
- (filter) => filters.some((f) => f.columnId === filter.id)
35
- ).length
36
-
37
- const handleClearAll = () => {
38
- // Solo limpiar los filtros configurados, no el search
39
- filters.forEach((filter) => {
40
- table.getColumn(filter.columnId)?.setFilterValue(undefined)
41
- })
42
- }
43
-
44
- return (
45
- <Popover open={open} onOpenChange={setOpen}>
46
- <PopoverTrigger asChild>
47
- <Button
48
- variant="outline"
49
- size="sm"
50
- className={cn(
51
- 'h-8 border-dashed',
52
- activeFiltersCount > 0 && 'border-solid'
53
- )}
54
- >
55
- <Filter className="mr-2 h-4 w-4" />
56
- Filtros
57
- {activeFiltersCount > 0 && (
58
- <Badge
59
- variant="secondary"
60
- className="ml-2 rounded-sm px-1 font-normal"
61
- >
62
- {activeFiltersCount}
63
- </Badge>
64
- )}
65
- </Button>
66
- </PopoverTrigger>
67
- <PopoverContent className="w-[320px] p-0" align="start">
68
- <div className="flex flex-col">
69
- {/* Header */}
70
- <div className="flex items-center justify-between border-b border-border px-4 py-3">
71
- <span className="text-sm font-medium">Filtros</span>
72
- {activeFiltersCount > 0 && (
73
- <Button
74
- variant="ghost"
75
- size="sm"
76
- className="h-7 px-2 text-xs"
77
- onClick={handleClearAll}
78
- >
79
- Limpiar todo
80
- <X className="ml-1 h-3 w-3" />
81
- </Button>
82
- )}
83
- </div>
84
-
85
- {/* Filters */}
86
- <div className="flex flex-col gap-3 p-4">
87
- {filters.map((filter, index) => {
88
- const column = table.getColumn(filter.columnId)
89
- if (!column) return null
90
-
91
- return (
92
- <div key={filter.columnId} className="flex flex-col gap-2">
93
- {index > 0 && <Separator className="my-1" />}
94
- <label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
95
- {filter.title}
96
- </label>
97
-
98
- {filter.type === 'faceted' && filter.options && (
99
- <DataTableFacetedFilter
100
- column={column}
101
- title={filter.title}
102
- options={filter.options}
103
- />
104
- )}
105
-
106
- {filter.type === 'date-range' && (
107
- <DataTableDateFilter
108
- column={column}
109
- title={filter.title}
110
- />
111
- )}
112
-
113
- {filter.type === 'number-range' && (
114
- <DataTableNumberFilter
115
- column={column}
116
- title={filter.title}
117
- format={filter.format}
118
- currencySymbol={filter.currencySymbol}
119
- step={filter.step}
120
- />
121
- )}
122
- </div>
123
- )
124
- })}
125
- </div>
126
- </div>
127
- </PopoverContent>
128
- </Popover>
129
- )
130
- }
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { Table } from '@tanstack/react-table'
5
+ import { Filter, X } from 'lucide-react'
6
+
7
+ import { cn } from '@/lib/utils'
8
+ import { Badge } from '@/components/ui/badge'
9
+ import { Button } from '@/components/ui/button'
10
+ import {
11
+ Popover,
12
+ PopoverContent,
13
+ PopoverTrigger,
14
+ } from '@/components/ui/popover'
15
+ import { Separator } from '@/components/ui/separator'
16
+ import { DataTableFacetedFilter } from './data-table-faceted-filter'
17
+ import { DataTableDateFilter } from './data-table-date-filter'
18
+ import { DataTableNumberFilter } from './data-table-number-filter'
19
+ import type { FilterConfig } from './data-table'
20
+
21
+ interface DataTableFiltersDropdownProps<TData> {
22
+ table: Table<TData>
23
+ filters: FilterConfig[]
24
+ }
25
+
26
+ export function DataTableFiltersDropdown<TData>({
27
+ table,
28
+ filters,
29
+ }: DataTableFiltersDropdownProps<TData>) {
30
+ const [open, setOpen] = React.useState(false)
31
+
32
+ // Contar filtros activos (excluyendo el search)
33
+ const activeFiltersCount = table.getState().columnFilters.filter(
34
+ (filter) => filters.some((f) => f.columnId === filter.id)
35
+ ).length
36
+
37
+ const handleClearAll = () => {
38
+ // Solo limpiar los filtros configurados, no el search
39
+ filters.forEach((filter) => {
40
+ table.getColumn(filter.columnId)?.setFilterValue(undefined)
41
+ })
42
+ }
43
+
44
+ return (
45
+ <Popover open={open} onOpenChange={setOpen}>
46
+ <PopoverTrigger asChild>
47
+ <Button
48
+ variant="outline"
49
+ size="sm"
50
+ className={cn(
51
+ 'h-8 border-dashed',
52
+ activeFiltersCount > 0 && 'border-solid'
53
+ )}
54
+ >
55
+ <Filter className="mr-2 h-4 w-4" />
56
+ Filtros
57
+ {activeFiltersCount > 0 && (
58
+ <Badge
59
+ variant="secondary"
60
+ className="ml-2 rounded-sm px-1 font-normal"
61
+ >
62
+ {activeFiltersCount}
63
+ </Badge>
64
+ )}
65
+ </Button>
66
+ </PopoverTrigger>
67
+ <PopoverContent className="w-[320px] p-0" align="start">
68
+ <div className="flex flex-col">
69
+ {/* Header */}
70
+ <div className="flex items-center justify-between border-b border-border px-4 py-3">
71
+ <span className="text-sm font-medium">Filtros</span>
72
+ {activeFiltersCount > 0 && (
73
+ <Button
74
+ variant="ghost"
75
+ size="sm"
76
+ className="h-7 px-2 text-xs"
77
+ onClick={handleClearAll}
78
+ >
79
+ Limpiar todo
80
+ <X className="ml-1 h-3 w-3" />
81
+ </Button>
82
+ )}
83
+ </div>
84
+
85
+ {/* Filters */}
86
+ <div className="flex flex-col gap-3 p-4">
87
+ {filters.map((filter, index) => {
88
+ const column = table.getColumn(filter.columnId)
89
+ if (!column) return null
90
+
91
+ return (
92
+ <div key={filter.columnId} className="flex flex-col gap-2">
93
+ {index > 0 && <Separator className="my-1" />}
94
+ <label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
95
+ {filter.title}
96
+ </label>
97
+
98
+ {filter.type === 'faceted' && filter.options && (
99
+ <DataTableFacetedFilter
100
+ column={column}
101
+ title={filter.title}
102
+ options={filter.options}
103
+ />
104
+ )}
105
+
106
+ {filter.type === 'date-range' && (
107
+ <DataTableDateFilter
108
+ column={column}
109
+ title={filter.title}
110
+ />
111
+ )}
112
+
113
+ {filter.type === 'number-range' && (
114
+ <DataTableNumberFilter
115
+ column={column}
116
+ title={filter.title}
117
+ format={filter.format}
118
+ currencySymbol={filter.currencySymbol}
119
+ step={filter.step}
120
+ />
121
+ )}
122
+ </div>
123
+ )
124
+ })}
125
+ </div>
126
+ </div>
127
+ </PopoverContent>
128
+ </Popover>
129
+ )
130
+ }