@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.
- package/README.md +549 -549
- package/package.json +48 -48
- package/template/.claude/skills/anti-patterns.md +150 -0
- package/template/.claude/skills/drizzle-schema.md +178 -0
- package/template/.claude/skills/formatting.md +56 -0
- package/template/.claude/skills/module-architecture.md +143 -0
- package/template/.claude/skills/supabase-server-actions.md +199 -0
- package/template/.claude/skills/ui-patterns.md +161 -0
- package/template/CLAUDE.md +114 -1239
- package/template/drizzle.config.ts +12 -12
- package/template/eslint.config.mjs +16 -16
- package/template/gitignore +36 -36
- package/template/next.config.ts +7 -7
- package/template/package.json +86 -86
- package/template/postcss.config.mjs +7 -7
- package/template/proxy.ts +12 -12
- package/template/public/logolft.svg +11 -11
- package/template/src/app/(auth)/dashboard/dashboard-content.tsx +124 -124
- package/template/src/app/(auth)/dashboard/page.tsx +9 -9
- package/template/src/app/(auth)/layout.tsx +7 -7
- package/template/src/app/(auth)/users/page.tsx +9 -9
- package/template/src/app/(auth)/users/users-content.tsx +26 -26
- package/template/src/app/(public)/layout.tsx +7 -7
- package/template/src/app/(public)/login/page.tsx +17 -17
- package/template/src/app/api/webhooks/route.ts +20 -20
- package/template/src/app/globals.css +249 -249
- package/template/src/app/layout.tsx +37 -37
- package/template/src/app/page.tsx +5 -5
- package/template/src/app/providers.tsx +27 -27
- package/template/src/components/layout/main-content.tsx +28 -28
- package/template/src/components/layout/sidebar-context.tsx +33 -33
- package/template/src/components/layout/sidebar.tsx +141 -141
- package/template/src/components/tables/data-table-column-header.tsx +68 -68
- package/template/src/components/tables/data-table-date-filter.tsx +203 -203
- package/template/src/components/tables/data-table-faceted-filter.tsx +185 -185
- package/template/src/components/tables/data-table-filters-dropdown.tsx +130 -130
- package/template/src/components/tables/data-table-number-filter.tsx +295 -295
- package/template/src/components/tables/data-table-pagination.tsx +99 -99
- package/template/src/components/tables/data-table-toolbar.tsx +140 -140
- package/template/src/components/tables/data-table-view-options.tsx +63 -63
- package/template/src/components/tables/data-table.tsx +148 -148
- package/template/src/components/tables/index.ts +9 -9
- package/template/src/components/ui/accordion.tsx +58 -58
- package/template/src/components/ui/alert-dialog.tsx +165 -165
- package/template/src/components/ui/alert.tsx +66 -66
- package/template/src/components/ui/animations/index.ts +44 -44
- package/template/src/components/ui/avatar.tsx +55 -55
- package/template/src/components/ui/badge.tsx +50 -50
- package/template/src/components/ui/button.tsx +118 -118
- package/template/src/components/ui/calendar.tsx +220 -220
- package/template/src/components/ui/card.tsx +113 -113
- package/template/src/components/ui/checkbox.tsx +38 -38
- package/template/src/components/ui/collapsible.tsx +33 -33
- package/template/src/components/ui/command.tsx +196 -196
- package/template/src/components/ui/dialog.tsx +156 -156
- package/template/src/components/ui/dropdown-menu.tsx +280 -280
- package/template/src/components/ui/form.tsx +171 -171
- package/template/src/components/ui/icons.tsx +167 -167
- package/template/src/components/ui/input.tsx +28 -28
- package/template/src/components/ui/label.tsx +25 -25
- package/template/src/components/ui/motion.tsx +197 -197
- package/template/src/components/ui/page-transition.tsx +166 -166
- package/template/src/components/ui/popover.tsx +59 -59
- package/template/src/components/ui/progress.tsx +32 -32
- package/template/src/components/ui/radio-group.tsx +45 -45
- package/template/src/components/ui/scroll-area.tsx +63 -63
- package/template/src/components/ui/select.tsx +208 -208
- package/template/src/components/ui/separator.tsx +28 -28
- package/template/src/components/ui/sheet.tsx +170 -170
- package/template/src/components/ui/sidebar.tsx +726 -726
- package/template/src/components/ui/skeleton.tsx +15 -15
- package/template/src/components/ui/slider.tsx +58 -58
- package/template/src/components/ui/sonner.tsx +47 -47
- package/template/src/components/ui/spinner.tsx +27 -27
- package/template/src/components/ui/submit-button.tsx +47 -47
- package/template/src/components/ui/switch.tsx +31 -31
- package/template/src/components/ui/table.tsx +120 -120
- package/template/src/components/ui/tabs.tsx +75 -75
- package/template/src/components/ui/textarea.tsx +26 -26
- package/template/src/components/ui/tooltip.tsx +70 -70
- package/template/src/config/navigation.ts +59 -59
- package/template/src/config/roles.ts +27 -27
- package/template/src/config/site.ts +12 -12
- package/template/src/db/index.ts +12 -12
- package/template/src/db/schema/index.ts +1 -1
- package/template/src/db/schema/users.ts +16 -16
- package/template/src/db/seed.ts +39 -39
- package/template/src/hooks/index.ts +3 -3
- package/template/src/hooks/use-mobile.ts +21 -21
- package/template/src/hooks/useDataTable.ts +82 -82
- package/template/src/hooks/useDebounce.ts +49 -49
- package/template/src/hooks/useMediaQuery.ts +36 -36
- package/template/src/lib/date/config.ts +36 -36
- package/template/src/lib/date/formatters.ts +127 -127
- package/template/src/lib/date/index.ts +26 -26
- package/template/src/lib/excel/exporter.ts +89 -89
- package/template/src/lib/excel/index.ts +14 -14
- package/template/src/lib/excel/parser.ts +96 -96
- package/template/src/lib/query-client.ts +35 -35
- package/template/src/lib/supabase/admin.ts +23 -23
- package/template/src/lib/supabase/client.ts +11 -11
- package/template/src/lib/supabase/proxy.ts +67 -67
- package/template/src/lib/supabase/server.ts +38 -38
- package/template/src/lib/supabase/types.ts +53 -53
- package/template/src/lib/utils.ts +6 -6
- package/template/src/lib/validations/common.ts +75 -75
- package/template/src/lib/validations/index.ts +20 -20
- package/template/src/modules/auth/actions/auth-actions.ts +59 -59
- package/template/src/modules/auth/components/login-form.tsx +68 -68
- package/template/src/modules/auth/hooks/useAuth.ts +38 -38
- package/template/src/modules/auth/hooks/useAuthMutations.ts +43 -43
- package/template/src/modules/auth/hooks/useAuthQueries.ts +43 -43
- package/template/src/modules/auth/index.ts +12 -12
- package/template/src/modules/auth/schemas/auth.schema.ts +32 -32
- package/template/src/modules/auth/stores/useAuthStore.ts +37 -37
- package/template/src/modules/users/actions/users-actions.ts +166 -166
- package/template/src/modules/users/columns.tsx +106 -106
- package/template/src/modules/users/components/users-list.tsx +48 -48
- package/template/src/modules/users/hooks/useUsers.ts +39 -39
- package/template/src/modules/users/hooks/useUsersMutations.ts +55 -55
- package/template/src/modules/users/hooks/useUsersQueries.ts +35 -35
- package/template/src/modules/users/index.ts +30 -30
- package/template/src/modules/users/schemas/users.schema.ts +51 -51
- package/template/src/modules/users/stores/useUsersStore.ts +60 -60
- package/template/src/modules/users/types/auth-user.types.ts +42 -42
- package/template/src/modules/users/utils/user-mapper.ts +32 -32
- package/template/src/stores/index.ts +1 -1
- package/template/src/stores/useUiStore.ts +55 -55
- package/template/src/types/api.ts +28 -28
- package/template/src/types/index.ts +2 -2
- package/template/src/types/table.ts +34 -34
- package/template/supabase/config.toml +94 -94
- 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
|
+
}
|