@create-lft-app/nextjs 3.1.0 → 3.2.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 (128) hide show
  1. package/README.md +549 -549
  2. package/package.json +48 -48
  3. package/template/CLAUDE.md +1239 -279
  4. package/template/drizzle.config.ts +12 -12
  5. package/template/eslint.config.mjs +16 -16
  6. package/template/gitignore +36 -36
  7. package/template/next.config.ts +7 -7
  8. package/template/package.json +86 -86
  9. package/template/postcss.config.mjs +7 -7
  10. package/template/proxy.ts +12 -12
  11. package/template/public/logolft.svg +11 -11
  12. package/template/src/app/(auth)/dashboard/dashboard-content.tsx +124 -124
  13. package/template/src/app/(auth)/dashboard/page.tsx +9 -9
  14. package/template/src/app/(auth)/layout.tsx +7 -7
  15. package/template/src/app/(auth)/users/page.tsx +9 -9
  16. package/template/src/app/(auth)/users/users-content.tsx +26 -26
  17. package/template/src/app/(public)/layout.tsx +7 -7
  18. package/template/src/app/(public)/login/page.tsx +17 -17
  19. package/template/src/app/api/webhooks/route.ts +20 -20
  20. package/template/src/app/globals.css +249 -249
  21. package/template/src/app/layout.tsx +37 -37
  22. package/template/src/app/page.tsx +5 -5
  23. package/template/src/app/providers.tsx +27 -27
  24. package/template/src/components/layout/main-content.tsx +28 -28
  25. package/template/src/components/layout/sidebar-context.tsx +33 -33
  26. package/template/src/components/layout/sidebar.tsx +141 -146
  27. package/template/src/components/tables/data-table-column-header.tsx +68 -68
  28. package/template/src/components/tables/data-table-date-filter.tsx +203 -0
  29. package/template/src/components/tables/data-table-faceted-filter.tsx +185 -0
  30. package/template/src/components/tables/data-table-filters-dropdown.tsx +130 -0
  31. package/template/src/components/tables/data-table-number-filter.tsx +295 -0
  32. package/template/src/components/tables/data-table-pagination.tsx +99 -99
  33. package/template/src/components/tables/data-table-toolbar.tsx +140 -50
  34. package/template/src/components/tables/data-table-view-options.tsx +63 -59
  35. package/template/src/components/tables/data-table.tsx +148 -128
  36. package/template/src/components/tables/index.ts +9 -5
  37. package/template/src/components/ui/accordion.tsx +58 -58
  38. package/template/src/components/ui/alert-dialog.tsx +165 -165
  39. package/template/src/components/ui/alert.tsx +66 -66
  40. package/template/src/components/ui/animations/index.ts +44 -44
  41. package/template/src/components/ui/avatar.tsx +55 -55
  42. package/template/src/components/ui/badge.tsx +50 -50
  43. package/template/src/components/ui/button.tsx +118 -118
  44. package/template/src/components/ui/calendar.tsx +220 -220
  45. package/template/src/components/ui/card.tsx +113 -113
  46. package/template/src/components/ui/checkbox.tsx +38 -38
  47. package/template/src/components/ui/collapsible.tsx +33 -33
  48. package/template/src/components/ui/command.tsx +196 -196
  49. package/template/src/components/ui/dialog.tsx +156 -156
  50. package/template/src/components/ui/dropdown-menu.tsx +280 -280
  51. package/template/src/components/ui/form.tsx +171 -171
  52. package/template/src/components/ui/icons.tsx +167 -167
  53. package/template/src/components/ui/input.tsx +28 -28
  54. package/template/src/components/ui/label.tsx +25 -25
  55. package/template/src/components/ui/motion.tsx +197 -197
  56. package/template/src/components/ui/page-transition.tsx +166 -166
  57. package/template/src/components/ui/popover.tsx +59 -59
  58. package/template/src/components/ui/progress.tsx +32 -32
  59. package/template/src/components/ui/radio-group.tsx +45 -45
  60. package/template/src/components/ui/scroll-area.tsx +63 -63
  61. package/template/src/components/ui/select.tsx +208 -208
  62. package/template/src/components/ui/separator.tsx +28 -28
  63. package/template/src/components/ui/sheet.tsx +170 -170
  64. package/template/src/components/ui/sidebar.tsx +726 -726
  65. package/template/src/components/ui/skeleton.tsx +15 -15
  66. package/template/src/components/ui/slider.tsx +58 -58
  67. package/template/src/components/ui/sonner.tsx +47 -47
  68. package/template/src/components/ui/spinner.tsx +27 -27
  69. package/template/src/components/ui/submit-button.tsx +47 -47
  70. package/template/src/components/ui/switch.tsx +31 -31
  71. package/template/src/components/ui/table.tsx +120 -120
  72. package/template/src/components/ui/tabs.tsx +75 -75
  73. package/template/src/components/ui/textarea.tsx +26 -26
  74. package/template/src/components/ui/tooltip.tsx +70 -70
  75. package/template/src/config/navigation.ts +59 -69
  76. package/template/src/config/roles.ts +27 -0
  77. package/template/src/config/site.ts +12 -12
  78. package/template/src/db/index.ts +12 -12
  79. package/template/src/db/schema/index.ts +1 -1
  80. package/template/src/db/schema/users.ts +16 -16
  81. package/template/src/db/seed.ts +39 -39
  82. package/template/src/hooks/index.ts +3 -3
  83. package/template/src/hooks/use-mobile.ts +21 -21
  84. package/template/src/hooks/useDataTable.ts +82 -82
  85. package/template/src/hooks/useDebounce.ts +49 -49
  86. package/template/src/hooks/useMediaQuery.ts +36 -36
  87. package/template/src/lib/date/config.ts +36 -34
  88. package/template/src/lib/date/formatters.ts +127 -120
  89. package/template/src/lib/date/index.ts +26 -19
  90. package/template/src/lib/excel/exporter.ts +89 -89
  91. package/template/src/lib/excel/index.ts +14 -14
  92. package/template/src/lib/excel/parser.ts +96 -96
  93. package/template/src/lib/query-client.ts +35 -35
  94. package/template/src/lib/supabase/admin.ts +23 -0
  95. package/template/src/lib/supabase/client.ts +11 -11
  96. package/template/src/lib/supabase/proxy.ts +67 -67
  97. package/template/src/lib/supabase/server.ts +38 -38
  98. package/template/src/lib/supabase/types.ts +53 -53
  99. package/template/src/lib/utils.ts +6 -6
  100. package/template/src/lib/validations/common.ts +75 -75
  101. package/template/src/lib/validations/index.ts +20 -20
  102. package/template/src/modules/auth/actions/auth-actions.ts +59 -59
  103. package/template/src/modules/auth/components/login-form.tsx +68 -68
  104. package/template/src/modules/auth/hooks/useAuth.ts +38 -38
  105. package/template/src/modules/auth/hooks/useAuthMutations.ts +43 -43
  106. package/template/src/modules/auth/hooks/useAuthQueries.ts +43 -43
  107. package/template/src/modules/auth/index.ts +12 -12
  108. package/template/src/modules/auth/schemas/auth.schema.ts +32 -32
  109. package/template/src/modules/auth/stores/useAuthStore.ts +37 -37
  110. package/template/src/modules/users/actions/users-actions.ts +166 -94
  111. package/template/src/modules/users/columns.tsx +106 -86
  112. package/template/src/modules/users/components/users-list.tsx +48 -22
  113. package/template/src/modules/users/hooks/useUsers.ts +39 -39
  114. package/template/src/modules/users/hooks/useUsersMutations.ts +55 -55
  115. package/template/src/modules/users/hooks/useUsersQueries.ts +35 -35
  116. package/template/src/modules/users/index.ts +30 -12
  117. package/template/src/modules/users/schemas/users.schema.ts +51 -23
  118. package/template/src/modules/users/stores/useUsersStore.ts +60 -60
  119. package/template/src/modules/users/types/auth-user.types.ts +42 -0
  120. package/template/src/modules/users/utils/user-mapper.ts +32 -0
  121. package/template/src/stores/index.ts +1 -1
  122. package/template/src/stores/useUiStore.ts +55 -55
  123. package/template/src/types/api.ts +28 -28
  124. package/template/src/types/index.ts +2 -2
  125. package/template/src/types/table.ts +34 -34
  126. package/template/supabase/config.toml +94 -94
  127. package/template/tsconfig.json +42 -42
  128. package/template/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +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,99 +1,99 @@
1
- 'use client'
2
-
3
- import { Table } from '@tanstack/react-table'
4
- import {
5
- ChevronLeft,
6
- ChevronRight,
7
- ChevronsLeft,
8
- ChevronsRight,
9
- } from 'lucide-react'
10
-
11
- import { Button } from '@/components/ui/button'
12
- import {
13
- Select,
14
- SelectContent,
15
- SelectItem,
16
- SelectTrigger,
17
- SelectValue,
18
- } from '@/components/ui/select'
19
-
20
- interface DataTablePaginationProps<TData> {
21
- table: Table<TData>
22
- }
23
-
24
- export function DataTablePagination<TData>({
25
- table,
26
- }: DataTablePaginationProps<TData>) {
27
- return (
28
- <div className="flex items-center justify-between px-2">
29
- <div className="flex-1 text-sm text-muted-foreground">
30
- {table.getFilteredSelectedRowModel().rows.length} de{' '}
31
- {table.getFilteredRowModel().rows.length} fila(s) seleccionada(s).
32
- </div>
33
- <div className="flex items-center space-x-6 lg:space-x-8">
34
- <div className="flex items-center space-x-2">
35
- <p className="text-sm font-medium">Filas por página</p>
36
- <Select
37
- value={`${table.getState().pagination.pageSize}`}
38
- onValueChange={(value) => {
39
- table.setPageSize(Number(value))
40
- }}
41
- >
42
- <SelectTrigger className="h-8 w-[70px]">
43
- <SelectValue placeholder={table.getState().pagination.pageSize} />
44
- </SelectTrigger>
45
- <SelectContent side="top">
46
- {[10, 20, 30, 40, 50].map((pageSize) => (
47
- <SelectItem key={pageSize} value={`${pageSize}`}>
48
- {pageSize}
49
- </SelectItem>
50
- ))}
51
- </SelectContent>
52
- </Select>
53
- </div>
54
- <div className="flex w-[100px] items-center justify-center text-sm font-medium">
55
- Página {table.getState().pagination.pageIndex + 1} de{' '}
56
- {table.getPageCount()}
57
- </div>
58
- <div className="flex items-center space-x-2">
59
- <Button
60
- variant="outline"
61
- className="hidden h-8 w-8 p-0 lg:flex"
62
- onClick={() => table.setPageIndex(0)}
63
- disabled={!table.getCanPreviousPage()}
64
- >
65
- <span className="sr-only">Ir a primera página</span>
66
- <ChevronsLeft className="h-4 w-4" />
67
- </Button>
68
- <Button
69
- variant="outline"
70
- className="h-8 w-8 p-0"
71
- onClick={() => table.previousPage()}
72
- disabled={!table.getCanPreviousPage()}
73
- >
74
- <span className="sr-only">Ir a página anterior</span>
75
- <ChevronLeft className="h-4 w-4" />
76
- </Button>
77
- <Button
78
- variant="outline"
79
- className="h-8 w-8 p-0"
80
- onClick={() => table.nextPage()}
81
- disabled={!table.getCanNextPage()}
82
- >
83
- <span className="sr-only">Ir a página siguiente</span>
84
- <ChevronRight className="h-4 w-4" />
85
- </Button>
86
- <Button
87
- variant="outline"
88
- className="hidden h-8 w-8 p-0 lg:flex"
89
- onClick={() => table.setPageIndex(table.getPageCount() - 1)}
90
- disabled={!table.getCanNextPage()}
91
- >
92
- <span className="sr-only">Ir a última página</span>
93
- <ChevronsRight className="h-4 w-4" />
94
- </Button>
95
- </div>
96
- </div>
97
- </div>
98
- )
99
- }
1
+ 'use client'
2
+
3
+ import { Table } from '@tanstack/react-table'
4
+ import {
5
+ ChevronLeft,
6
+ ChevronRight,
7
+ ChevronsLeft,
8
+ ChevronsRight,
9
+ } from 'lucide-react'
10
+
11
+ import { Button } from '@/components/ui/button'
12
+ import {
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from '@/components/ui/select'
19
+
20
+ interface DataTablePaginationProps<TData> {
21
+ table: Table<TData>
22
+ }
23
+
24
+ export function DataTablePagination<TData>({
25
+ table,
26
+ }: DataTablePaginationProps<TData>) {
27
+ return (
28
+ <div className="flex items-center justify-between px-2">
29
+ <div className="flex-1 text-sm text-muted-foreground">
30
+ {table.getFilteredSelectedRowModel().rows.length} de{' '}
31
+ {table.getFilteredRowModel().rows.length} fila(s) seleccionada(s).
32
+ </div>
33
+ <div className="flex items-center space-x-6 lg:space-x-8">
34
+ <div className="flex items-center space-x-2">
35
+ <p className="text-sm font-medium">Filas por página</p>
36
+ <Select
37
+ value={`${table.getState().pagination.pageSize}`}
38
+ onValueChange={(value) => {
39
+ table.setPageSize(Number(value))
40
+ }}
41
+ >
42
+ <SelectTrigger className="h-8 w-[70px]">
43
+ <SelectValue placeholder={table.getState().pagination.pageSize} />
44
+ </SelectTrigger>
45
+ <SelectContent side="top">
46
+ {[10, 20, 30, 40, 50].map((pageSize) => (
47
+ <SelectItem key={pageSize} value={`${pageSize}`}>
48
+ {pageSize}
49
+ </SelectItem>
50
+ ))}
51
+ </SelectContent>
52
+ </Select>
53
+ </div>
54
+ <div className="flex w-[100px] items-center justify-center text-sm font-medium">
55
+ Página {table.getState().pagination.pageIndex + 1} de{' '}
56
+ {table.getPageCount()}
57
+ </div>
58
+ <div className="flex items-center space-x-2">
59
+ <Button
60
+ variant="outline"
61
+ className="hidden h-8 w-8 p-0 lg:flex"
62
+ onClick={() => table.setPageIndex(0)}
63
+ disabled={!table.getCanPreviousPage()}
64
+ >
65
+ <span className="sr-only">Ir a primera página</span>
66
+ <ChevronsLeft className="h-4 w-4" />
67
+ </Button>
68
+ <Button
69
+ variant="outline"
70
+ className="h-8 w-8 p-0"
71
+ onClick={() => table.previousPage()}
72
+ disabled={!table.getCanPreviousPage()}
73
+ >
74
+ <span className="sr-only">Ir a página anterior</span>
75
+ <ChevronLeft className="h-4 w-4" />
76
+ </Button>
77
+ <Button
78
+ variant="outline"
79
+ className="h-8 w-8 p-0"
80
+ onClick={() => table.nextPage()}
81
+ disabled={!table.getCanNextPage()}
82
+ >
83
+ <span className="sr-only">Ir a página siguiente</span>
84
+ <ChevronRight className="h-4 w-4" />
85
+ </Button>
86
+ <Button
87
+ variant="outline"
88
+ className="hidden h-8 w-8 p-0 lg:flex"
89
+ onClick={() => table.setPageIndex(table.getPageCount() - 1)}
90
+ disabled={!table.getCanNextPage()}
91
+ >
92
+ <span className="sr-only">Ir a última página</span>
93
+ <ChevronsRight className="h-4 w-4" />
94
+ </Button>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ )
99
+ }
@@ -1,50 +1,140 @@
1
- 'use client'
2
-
3
- import { Table } from '@tanstack/react-table'
4
- import { X } from 'lucide-react'
5
-
6
- import { Button } from '@/components/ui/button'
7
- import { Input } from '@/components/ui/input'
8
- import { DataTableViewOptions } from './data-table-view-options'
9
-
10
- interface DataTableToolbarProps<TData> {
11
- table: Table<TData>
12
- searchKey?: string
13
- searchPlaceholder?: string
14
- }
15
-
16
- export function DataTableToolbar<TData>({
17
- table,
18
- searchKey,
19
- searchPlaceholder = 'Buscar...',
20
- }: DataTableToolbarProps<TData>) {
21
- const isFiltered = table.getState().columnFilters.length > 0
22
-
23
- return (
24
- <div className="flex items-center justify-between">
25
- <div className="flex flex-1 items-center space-x-2">
26
- {searchKey && (
27
- <Input
28
- placeholder={searchPlaceholder}
29
- value={(table.getColumn(searchKey)?.getFilterValue() as string) ?? ''}
30
- onChange={(event) =>
31
- table.getColumn(searchKey)?.setFilterValue(event.target.value)
32
- }
33
- className="h-8 w-[150px] lg:w-[250px]"
34
- />
35
- )}
36
- {isFiltered && (
37
- <Button
38
- variant="ghost"
39
- onClick={() => table.resetColumnFilters()}
40
- className="h-8 px-2 lg:px-3"
41
- >
42
- Limpiar
43
- <X className="ml-2 h-4 w-4" />
44
- </Button>
45
- )}
46
- </div>
47
- <DataTableViewOptions table={table} />
48
- </div>
49
- )
50
- }
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 { Button } from '@/components/ui/button'
8
+ import { Input } from '@/components/ui/input'
9
+ import { DataTableFacetedFilter } from './data-table-faceted-filter'
10
+ import { DataTableDateFilter } from './data-table-date-filter'
11
+ import { DataTableNumberFilter } from './data-table-number-filter'
12
+ import { DataTableFiltersDropdown } from './data-table-filters-dropdown'
13
+ import type { FilterConfig } from './data-table'
14
+
15
+ interface DataTableToolbarProps<TData> {
16
+ table: Table<TData>
17
+ searchKey?: string
18
+ searchPlaceholder?: string
19
+ filters?: FilterConfig[]
20
+ /** Modo de visualización: 'dropdown' agrupa filtros, 'inline' los muestra en línea, 'inline-collapsible' los muestra en línea pero ocultables */
21
+ filterMode?: 'dropdown' | 'inline' | 'inline-collapsible'
22
+ }
23
+
24
+ export function DataTableToolbar<TData>({
25
+ table,
26
+ searchKey,
27
+ searchPlaceholder = 'Buscar...',
28
+ filters,
29
+ filterMode = 'dropdown',
30
+ }: DataTableToolbarProps<TData>) {
31
+ const isFiltered = table.getState().columnFilters.length > 0
32
+ const [showFilters, setShowFilters] = React.useState(false)
33
+
34
+ const renderFilters = () => {
35
+ return filters?.map((filter) => {
36
+ const column = table.getColumn(filter.columnId)
37
+ if (!column) return null
38
+
39
+ if (filter.type === 'faceted' && filter.options) {
40
+ return (
41
+ <DataTableFacetedFilter
42
+ key={filter.columnId}
43
+ column={column}
44
+ title={filter.title}
45
+ options={filter.options}
46
+ />
47
+ )
48
+ }
49
+
50
+ if (filter.type === 'date-range') {
51
+ return (
52
+ <DataTableDateFilter
53
+ key={filter.columnId}
54
+ column={column}
55
+ title={filter.title}
56
+ />
57
+ )
58
+ }
59
+
60
+ if (filter.type === 'number-range') {
61
+ return (
62
+ <DataTableNumberFilter
63
+ key={filter.columnId}
64
+ column={column}
65
+ title={filter.title}
66
+ format={filter.format}
67
+ currencySymbol={filter.currencySymbol}
68
+ step={filter.step}
69
+ />
70
+ )
71
+ }
72
+
73
+ return null
74
+ })
75
+ }
76
+
77
+ return (
78
+ <div className="flex flex-col gap-2">
79
+ <div className="flex items-center justify-between">
80
+ <div className="flex flex-1 flex-wrap items-center gap-2">
81
+ {searchKey && (
82
+ <Input
83
+ placeholder={searchPlaceholder}
84
+ value={(table.getColumn(searchKey)?.getFilterValue() as string) ?? ''}
85
+ onChange={(event) =>
86
+ table.getColumn(searchKey)?.setFilterValue(event.target.value)
87
+ }
88
+ className="h-8 w-[150px] lg:w-[250px]"
89
+ />
90
+ )}
91
+
92
+ {/* Modo dropdown: un solo botón que agrupa filtros */}
93
+ {filterMode === 'dropdown' && filters && filters.length > 0 && (
94
+ <DataTableFiltersDropdown table={table} filters={filters} />
95
+ )}
96
+
97
+ {/* Modo inline: filtros visibles individualmente */}
98
+ {filterMode === 'inline' && renderFilters()}
99
+
100
+ {/* Modo inline-collapsible: botón para mostrar/ocultar filtros */}
101
+ {filterMode === 'inline-collapsible' && filters && filters.length > 0 && (
102
+ <Button
103
+ variant="outline"
104
+ size="sm"
105
+ className="h-8 border-dashed"
106
+ onClick={() => setShowFilters(!showFilters)}
107
+ >
108
+ <Filter className="mr-2 h-4 w-4" />
109
+ Filtros
110
+ {isFiltered && (
111
+ <span className="ml-2 rounded-full bg-primary px-1.5 py-0.5 text-xs text-primary-foreground">
112
+ {table.getState().columnFilters.length}
113
+ </span>
114
+ )}
115
+ </Button>
116
+ )}
117
+
118
+ {/* Botón limpiar para modos inline */}
119
+ {(filterMode === 'inline' || (filterMode === 'inline-collapsible' && showFilters)) && isFiltered && (
120
+ <Button
121
+ variant="ghost"
122
+ onClick={() => table.resetColumnFilters()}
123
+ className="h-8 px-2 lg:px-3"
124
+ >
125
+ Limpiar
126
+ <X className="ml-2 h-4 w-4" />
127
+ </Button>
128
+ )}
129
+ </div>
130
+ </div>
131
+
132
+ {/* Filtros inline colapsables - fila separada */}
133
+ {filterMode === 'inline-collapsible' && showFilters && filters && filters.length > 0 && (
134
+ <div className="flex flex-wrap items-center gap-2">
135
+ {renderFilters()}
136
+ </div>
137
+ )}
138
+ </div>
139
+ )
140
+ }