@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,295 +1,295 @@
1
- 'use client'
2
-
3
- import * as React from 'react'
4
- import { Column } from '@tanstack/react-table'
5
- import { DollarSign, X } from 'lucide-react'
6
-
7
- import { cn } from '@/lib/utils'
8
- import { DEFAULT_LOCALE } from '@/lib/date'
9
- import { Button } from '@/components/ui/button'
10
- import { Input } from '@/components/ui/input'
11
- import {
12
- Popover,
13
- PopoverContent,
14
- PopoverTrigger,
15
- } from '@/components/ui/popover'
16
-
17
- export interface NumberRange {
18
- min?: number
19
- max?: number
20
- }
21
-
22
- interface DataTableNumberFilterProps<TData> {
23
- column?: Column<TData, unknown>
24
- title?: string
25
- /** Formato para mostrar el número (ej: 'currency', 'number') */
26
- format?: 'currency' | 'number'
27
- /** Símbolo de moneda (default: '$') */
28
- currencySymbol?: string
29
- /** Paso para los inputs (default: 1) */
30
- step?: number
31
- }
32
-
33
- export function DataTableNumberFilter<TData>({
34
- column,
35
- title = 'Monto',
36
- format = 'number',
37
- currencySymbol = '$',
38
- step = 1,
39
- }: DataTableNumberFilterProps<TData>) {
40
- const filterValue = column?.getFilterValue() as NumberRange | undefined
41
- const [open, setOpen] = React.useState(false)
42
-
43
- // Sincronizar estado local con el filtro de la columna
44
- React.useEffect(() => {
45
- if (!filterValue) {
46
- setRange({ min: undefined, max: undefined })
47
- }
48
- }, [filterValue])
49
-
50
- const [range, setRange] = React.useState<NumberRange>({
51
- min: filterValue?.min,
52
- max: filterValue?.max,
53
- })
54
-
55
- const handleMinChange = (value: string) => {
56
- const min = value === '' ? undefined : Number(value)
57
- const newRange = { ...range, min }
58
- setRange(newRange)
59
- }
60
-
61
- const handleMaxChange = (value: string) => {
62
- const max = value === '' ? undefined : Number(value)
63
- const newRange = { ...range, max }
64
- setRange(newRange)
65
- }
66
-
67
- const handleApply = () => {
68
- if (range.min === undefined && range.max === undefined) {
69
- column?.setFilterValue(undefined)
70
- } else {
71
- column?.setFilterValue(range)
72
- }
73
- setOpen(false)
74
- }
75
-
76
- const handleClear = () => {
77
- setRange({ min: undefined, max: undefined })
78
- column?.setFilterValue(undefined)
79
- }
80
-
81
- const handleOpenChange = (isOpen: boolean) => {
82
- if (!isOpen) {
83
- // Al cerrar sin aplicar, resetear al valor actual del filtro
84
- setRange({
85
- min: filterValue?.min,
86
- max: filterValue?.max,
87
- })
88
- }
89
- setOpen(isOpen)
90
- }
91
-
92
- const formatNumber = (num: number) => {
93
- if (format === 'currency') {
94
- return `${currencySymbol}${num.toLocaleString(DEFAULT_LOCALE)}`
95
- }
96
- return num.toLocaleString(DEFAULT_LOCALE)
97
- }
98
-
99
- const hasFilter = filterValue?.min !== undefined || filterValue?.max !== undefined
100
-
101
- const getDisplayText = () => {
102
- if (!hasFilter) return title
103
-
104
- const { min, max } = filterValue!
105
- if (min !== undefined && max !== undefined) {
106
- return `${formatNumber(min)} - ${formatNumber(max)}`
107
- }
108
- if (min !== undefined) {
109
- return `≥ ${formatNumber(min)}`
110
- }
111
- if (max !== undefined) {
112
- return `≤ ${formatNumber(max)}`
113
- }
114
- return title
115
- }
116
-
117
- const Icon = format === 'currency' ? DollarSign : () => <span className="text-xs font-medium">#</span>
118
-
119
- return (
120
- <Popover open={open} onOpenChange={handleOpenChange}>
121
- <PopoverTrigger asChild>
122
- <Button
123
- variant="outline"
124
- size="sm"
125
- className={cn(
126
- 'h-8 border-dashed',
127
- hasFilter && 'border-solid'
128
- )}
129
- >
130
- <Icon className="mr-2 h-4 w-4" />
131
- <span className="max-w-[150px] truncate">{getDisplayText()}</span>
132
- {hasFilter && (
133
- <span
134
- role="button"
135
- tabIndex={0}
136
- onClick={(e) => {
137
- e.stopPropagation()
138
- handleClear()
139
- }}
140
- onKeyDown={(e) => {
141
- if (e.key === 'Enter' || e.key === ' ') {
142
- e.stopPropagation()
143
- handleClear()
144
- }
145
- }}
146
- className="ml-2 rounded-full p-0.5 hover:bg-muted"
147
- >
148
- <X className="h-3 w-3" />
149
- </span>
150
- )}
151
- </Button>
152
- </PopoverTrigger>
153
- <PopoverContent className="w-[280px] p-4" side="bottom" align="start" sideOffset={4}>
154
- <div className="flex flex-col gap-4">
155
- <div className="flex flex-col gap-2">
156
- <label className="text-sm font-medium text-foreground">
157
- Rango de {title.toLowerCase()}
158
- </label>
159
- <div className="flex items-center gap-2">
160
- <div className="relative flex-1">
161
- {format === 'currency' && (
162
- <span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground">
163
- {currencySymbol}
164
- </span>
165
- )}
166
- <Input
167
- type="number"
168
- placeholder="Mín"
169
- value={range.min ?? ''}
170
- onChange={(e) => handleMinChange(e.target.value)}
171
- step={step}
172
- className={cn(
173
- 'h-9',
174
- format === 'currency' && 'pl-7'
175
- )}
176
- />
177
- </div>
178
- <span className="text-muted-foreground">-</span>
179
- <div className="relative flex-1">
180
- {format === 'currency' && (
181
- <span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground">
182
- {currencySymbol}
183
- </span>
184
- )}
185
- <Input
186
- type="number"
187
- placeholder="Máx"
188
- value={range.max ?? ''}
189
- onChange={(e) => handleMaxChange(e.target.value)}
190
- step={step}
191
- className={cn(
192
- 'h-9',
193
- format === 'currency' && 'pl-7'
194
- )}
195
- />
196
- </div>
197
- </div>
198
- </div>
199
-
200
- {/* Presets rápidos */}
201
- <div className="flex flex-wrap gap-1">
202
- <Button
203
- variant="ghost"
204
- size="sm"
205
- className="h-7 text-xs"
206
- onClick={() => {
207
- setRange({ min: 0, max: 100 })
208
- }}
209
- >
210
- 0-100
211
- </Button>
212
- <Button
213
- variant="ghost"
214
- size="sm"
215
- className="h-7 text-xs"
216
- onClick={() => {
217
- setRange({ min: 100, max: 500 })
218
- }}
219
- >
220
- 100-500
221
- </Button>
222
- <Button
223
- variant="ghost"
224
- size="sm"
225
- className="h-7 text-xs"
226
- onClick={() => {
227
- setRange({ min: 500, max: 1000 })
228
- }}
229
- >
230
- 500-1K
231
- </Button>
232
- <Button
233
- variant="ghost"
234
- size="sm"
235
- className="h-7 text-xs"
236
- onClick={() => {
237
- setRange({ min: 1000, max: undefined })
238
- }}
239
- >
240
- +1K
241
- </Button>
242
- </div>
243
-
244
- <div className="flex gap-2">
245
- <Button
246
- variant="outline"
247
- size="sm"
248
- className="flex-1"
249
- onClick={handleClear}
250
- >
251
- Limpiar
252
- </Button>
253
- <Button
254
- size="sm"
255
- className="flex-1"
256
- onClick={handleApply}
257
- >
258
- Aplicar
259
- </Button>
260
- </div>
261
- </div>
262
- </PopoverContent>
263
- </Popover>
264
- )
265
- }
266
-
267
- // Filter function para usar con TanStack Table
268
- export function numberRangeFilterFn<TData>(
269
- row: { getValue: (id: string) => unknown },
270
- columnId: string,
271
- filterValue: NumberRange | undefined
272
- ): boolean {
273
- if (!filterValue) return true
274
- if (filterValue.min === undefined && filterValue.max === undefined) return true
275
-
276
- const cellValue = row.getValue(columnId)
277
- if (cellValue === null || cellValue === undefined) return false
278
-
279
- const value = Number(cellValue)
280
- if (isNaN(value)) return false
281
-
282
- const { min, max } = filterValue
283
-
284
- if (min !== undefined && max !== undefined) {
285
- return value >= min && value <= max
286
- }
287
- if (min !== undefined) {
288
- return value >= min
289
- }
290
- if (max !== undefined) {
291
- return value <= max
292
- }
293
-
294
- return true
295
- }
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { Column } from '@tanstack/react-table'
5
+ import { DollarSign, X } from 'lucide-react'
6
+
7
+ import { cn } from '@/lib/utils'
8
+ import { DEFAULT_LOCALE } from '@/lib/date'
9
+ import { Button } from '@/components/ui/button'
10
+ import { Input } from '@/components/ui/input'
11
+ import {
12
+ Popover,
13
+ PopoverContent,
14
+ PopoverTrigger,
15
+ } from '@/components/ui/popover'
16
+
17
+ export interface NumberRange {
18
+ min?: number
19
+ max?: number
20
+ }
21
+
22
+ interface DataTableNumberFilterProps<TData> {
23
+ column?: Column<TData, unknown>
24
+ title?: string
25
+ /** Formato para mostrar el número (ej: 'currency', 'number') */
26
+ format?: 'currency' | 'number'
27
+ /** Símbolo de moneda (default: '$') */
28
+ currencySymbol?: string
29
+ /** Paso para los inputs (default: 1) */
30
+ step?: number
31
+ }
32
+
33
+ export function DataTableNumberFilter<TData>({
34
+ column,
35
+ title = 'Monto',
36
+ format = 'number',
37
+ currencySymbol = '$',
38
+ step = 1,
39
+ }: DataTableNumberFilterProps<TData>) {
40
+ const filterValue = column?.getFilterValue() as NumberRange | undefined
41
+ const [open, setOpen] = React.useState(false)
42
+
43
+ // Sincronizar estado local con el filtro de la columna
44
+ React.useEffect(() => {
45
+ if (!filterValue) {
46
+ setRange({ min: undefined, max: undefined })
47
+ }
48
+ }, [filterValue])
49
+
50
+ const [range, setRange] = React.useState<NumberRange>({
51
+ min: filterValue?.min,
52
+ max: filterValue?.max,
53
+ })
54
+
55
+ const handleMinChange = (value: string) => {
56
+ const min = value === '' ? undefined : Number(value)
57
+ const newRange = { ...range, min }
58
+ setRange(newRange)
59
+ }
60
+
61
+ const handleMaxChange = (value: string) => {
62
+ const max = value === '' ? undefined : Number(value)
63
+ const newRange = { ...range, max }
64
+ setRange(newRange)
65
+ }
66
+
67
+ const handleApply = () => {
68
+ if (range.min === undefined && range.max === undefined) {
69
+ column?.setFilterValue(undefined)
70
+ } else {
71
+ column?.setFilterValue(range)
72
+ }
73
+ setOpen(false)
74
+ }
75
+
76
+ const handleClear = () => {
77
+ setRange({ min: undefined, max: undefined })
78
+ column?.setFilterValue(undefined)
79
+ }
80
+
81
+ const handleOpenChange = (isOpen: boolean) => {
82
+ if (!isOpen) {
83
+ // Al cerrar sin aplicar, resetear al valor actual del filtro
84
+ setRange({
85
+ min: filterValue?.min,
86
+ max: filterValue?.max,
87
+ })
88
+ }
89
+ setOpen(isOpen)
90
+ }
91
+
92
+ const formatNumber = (num: number) => {
93
+ if (format === 'currency') {
94
+ return `${currencySymbol}${num.toLocaleString(DEFAULT_LOCALE)}`
95
+ }
96
+ return num.toLocaleString(DEFAULT_LOCALE)
97
+ }
98
+
99
+ const hasFilter = filterValue?.min !== undefined || filterValue?.max !== undefined
100
+
101
+ const getDisplayText = () => {
102
+ if (!hasFilter) return title
103
+
104
+ const { min, max } = filterValue!
105
+ if (min !== undefined && max !== undefined) {
106
+ return `${formatNumber(min)} - ${formatNumber(max)}`
107
+ }
108
+ if (min !== undefined) {
109
+ return `≥ ${formatNumber(min)}`
110
+ }
111
+ if (max !== undefined) {
112
+ return `≤ ${formatNumber(max)}`
113
+ }
114
+ return title
115
+ }
116
+
117
+ const Icon = format === 'currency' ? DollarSign : () => <span className="text-xs font-medium">#</span>
118
+
119
+ return (
120
+ <Popover open={open} onOpenChange={handleOpenChange}>
121
+ <PopoverTrigger asChild>
122
+ <Button
123
+ variant="outline"
124
+ size="sm"
125
+ className={cn(
126
+ 'h-8 border-dashed',
127
+ hasFilter && 'border-solid'
128
+ )}
129
+ >
130
+ <Icon className="mr-2 h-4 w-4" />
131
+ <span className="max-w-[150px] truncate">{getDisplayText()}</span>
132
+ {hasFilter && (
133
+ <span
134
+ role="button"
135
+ tabIndex={0}
136
+ onClick={(e) => {
137
+ e.stopPropagation()
138
+ handleClear()
139
+ }}
140
+ onKeyDown={(e) => {
141
+ if (e.key === 'Enter' || e.key === ' ') {
142
+ e.stopPropagation()
143
+ handleClear()
144
+ }
145
+ }}
146
+ className="ml-2 rounded-full p-0.5 hover:bg-muted"
147
+ >
148
+ <X className="h-3 w-3" />
149
+ </span>
150
+ )}
151
+ </Button>
152
+ </PopoverTrigger>
153
+ <PopoverContent className="w-[280px] p-4" side="bottom" align="start" sideOffset={4}>
154
+ <div className="flex flex-col gap-4">
155
+ <div className="flex flex-col gap-2">
156
+ <label className="text-sm font-medium text-foreground">
157
+ Rango de {title.toLowerCase()}
158
+ </label>
159
+ <div className="flex items-center gap-2">
160
+ <div className="relative flex-1">
161
+ {format === 'currency' && (
162
+ <span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground">
163
+ {currencySymbol}
164
+ </span>
165
+ )}
166
+ <Input
167
+ type="number"
168
+ placeholder="Mín"
169
+ value={range.min ?? ''}
170
+ onChange={(e) => handleMinChange(e.target.value)}
171
+ step={step}
172
+ className={cn(
173
+ 'h-9',
174
+ format === 'currency' && 'pl-7'
175
+ )}
176
+ />
177
+ </div>
178
+ <span className="text-muted-foreground">-</span>
179
+ <div className="relative flex-1">
180
+ {format === 'currency' && (
181
+ <span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground">
182
+ {currencySymbol}
183
+ </span>
184
+ )}
185
+ <Input
186
+ type="number"
187
+ placeholder="Máx"
188
+ value={range.max ?? ''}
189
+ onChange={(e) => handleMaxChange(e.target.value)}
190
+ step={step}
191
+ className={cn(
192
+ 'h-9',
193
+ format === 'currency' && 'pl-7'
194
+ )}
195
+ />
196
+ </div>
197
+ </div>
198
+ </div>
199
+
200
+ {/* Presets rápidos */}
201
+ <div className="flex flex-wrap gap-1">
202
+ <Button
203
+ variant="ghost"
204
+ size="sm"
205
+ className="h-7 text-xs"
206
+ onClick={() => {
207
+ setRange({ min: 0, max: 100 })
208
+ }}
209
+ >
210
+ 0-100
211
+ </Button>
212
+ <Button
213
+ variant="ghost"
214
+ size="sm"
215
+ className="h-7 text-xs"
216
+ onClick={() => {
217
+ setRange({ min: 100, max: 500 })
218
+ }}
219
+ >
220
+ 100-500
221
+ </Button>
222
+ <Button
223
+ variant="ghost"
224
+ size="sm"
225
+ className="h-7 text-xs"
226
+ onClick={() => {
227
+ setRange({ min: 500, max: 1000 })
228
+ }}
229
+ >
230
+ 500-1K
231
+ </Button>
232
+ <Button
233
+ variant="ghost"
234
+ size="sm"
235
+ className="h-7 text-xs"
236
+ onClick={() => {
237
+ setRange({ min: 1000, max: undefined })
238
+ }}
239
+ >
240
+ +1K
241
+ </Button>
242
+ </div>
243
+
244
+ <div className="flex gap-2">
245
+ <Button
246
+ variant="outline"
247
+ size="sm"
248
+ className="flex-1"
249
+ onClick={handleClear}
250
+ >
251
+ Limpiar
252
+ </Button>
253
+ <Button
254
+ size="sm"
255
+ className="flex-1"
256
+ onClick={handleApply}
257
+ >
258
+ Aplicar
259
+ </Button>
260
+ </div>
261
+ </div>
262
+ </PopoverContent>
263
+ </Popover>
264
+ )
265
+ }
266
+
267
+ // Filter function para usar con TanStack Table
268
+ export function numberRangeFilterFn<TData>(
269
+ row: { getValue: (id: string) => unknown },
270
+ columnId: string,
271
+ filterValue: NumberRange | undefined
272
+ ): boolean {
273
+ if (!filterValue) return true
274
+ if (filterValue.min === undefined && filterValue.max === undefined) return true
275
+
276
+ const cellValue = row.getValue(columnId)
277
+ if (cellValue === null || cellValue === undefined) return false
278
+
279
+ const value = Number(cellValue)
280
+ if (isNaN(value)) return false
281
+
282
+ const { min, max } = filterValue
283
+
284
+ if (min !== undefined && max !== undefined) {
285
+ return value >= min && value <= max
286
+ }
287
+ if (min !== undefined) {
288
+ return value >= min
289
+ }
290
+ if (max !== undefined) {
291
+ return value <= max
292
+ }
293
+
294
+ return true
295
+ }