@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.
- package/README.md +549 -549
- package/package.json +48 -48
- package/template/CLAUDE.md +1239 -279
- 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 -146
- package/template/src/components/tables/data-table-column-header.tsx +68 -68
- package/template/src/components/tables/data-table-date-filter.tsx +203 -0
- package/template/src/components/tables/data-table-faceted-filter.tsx +185 -0
- package/template/src/components/tables/data-table-filters-dropdown.tsx +130 -0
- package/template/src/components/tables/data-table-number-filter.tsx +295 -0
- package/template/src/components/tables/data-table-pagination.tsx +99 -99
- package/template/src/components/tables/data-table-toolbar.tsx +140 -50
- package/template/src/components/tables/data-table-view-options.tsx +63 -59
- package/template/src/components/tables/data-table.tsx +148 -128
- package/template/src/components/tables/index.ts +9 -5
- 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 -69
- package/template/src/config/roles.ts +27 -0
- 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 -34
- package/template/src/lib/date/formatters.ts +127 -120
- package/template/src/lib/date/index.ts +26 -19
- 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 -0
- 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 -94
- package/template/src/modules/users/columns.tsx +106 -86
- package/template/src/modules/users/components/users-list.tsx +48 -22
- 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 -12
- package/template/src/modules/users/schemas/users.schema.ts +51 -23
- package/template/src/modules/users/stores/useUsersStore.ts +60 -60
- package/template/src/modules/users/types/auth-user.types.ts +42 -0
- package/template/src/modules/users/utils/user-mapper.ts +32 -0
- 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
- package/template/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Column } from '@tanstack/react-table'
|
|
5
|
+
import { CalendarIcon, X } from 'lucide-react'
|
|
6
|
+
import { DateRange } from 'react-day-picker'
|
|
7
|
+
|
|
8
|
+
import { cn } from '@/lib/utils'
|
|
9
|
+
import { formatDateShort } from '@/lib/date'
|
|
10
|
+
import { Button } from '@/components/ui/button'
|
|
11
|
+
import { Calendar } from '@/components/ui/calendar'
|
|
12
|
+
import {
|
|
13
|
+
Popover,
|
|
14
|
+
PopoverContent,
|
|
15
|
+
PopoverTrigger,
|
|
16
|
+
} from '@/components/ui/popover'
|
|
17
|
+
|
|
18
|
+
interface DataTableDateFilterProps<TData> {
|
|
19
|
+
column?: Column<TData, unknown>
|
|
20
|
+
title?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function DataTableDateFilter<TData>({
|
|
24
|
+
column,
|
|
25
|
+
title = 'Fecha',
|
|
26
|
+
}: DataTableDateFilterProps<TData>) {
|
|
27
|
+
const filterValue = column?.getFilterValue() as DateRange | undefined
|
|
28
|
+
const [open, setOpen] = React.useState(false)
|
|
29
|
+
|
|
30
|
+
// Sincronizar estado local con el filtro de la columna
|
|
31
|
+
// Esto permite que "Limpiar" del toolbar resetee este filtro
|
|
32
|
+
React.useEffect(() => {
|
|
33
|
+
if (!filterValue) {
|
|
34
|
+
setDate(undefined)
|
|
35
|
+
}
|
|
36
|
+
}, [filterValue])
|
|
37
|
+
|
|
38
|
+
const [date, setDate] = React.useState<DateRange | undefined>(filterValue)
|
|
39
|
+
|
|
40
|
+
const handleSelect = (range: DateRange | undefined) => {
|
|
41
|
+
setDate(range)
|
|
42
|
+
// No aplicamos el filtro inmediatamente, esperamos al botón Aplicar
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const handleApply = () => {
|
|
46
|
+
column?.setFilterValue(date)
|
|
47
|
+
setOpen(false)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const handleClear = () => {
|
|
51
|
+
setDate(undefined)
|
|
52
|
+
column?.setFilterValue(undefined)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const handleOpenChange = (isOpen: boolean) => {
|
|
56
|
+
if (!isOpen) {
|
|
57
|
+
// Al cerrar sin aplicar, resetear al valor actual del filtro
|
|
58
|
+
setDate(filterValue)
|
|
59
|
+
}
|
|
60
|
+
setOpen(isOpen)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const hasFilter = date?.from || date?.to
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
67
|
+
<PopoverTrigger asChild>
|
|
68
|
+
<Button
|
|
69
|
+
variant="outline"
|
|
70
|
+
size="sm"
|
|
71
|
+
className={cn(
|
|
72
|
+
'h-8 border-dashed',
|
|
73
|
+
hasFilter && 'border-solid'
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
77
|
+
{hasFilter ? (
|
|
78
|
+
<span className="flex items-center gap-1">
|
|
79
|
+
{date?.from && formatDateShort(date.from)}
|
|
80
|
+
{date?.to && ` - ${formatDateShort(date.to)}`}
|
|
81
|
+
</span>
|
|
82
|
+
) : (
|
|
83
|
+
title
|
|
84
|
+
)}
|
|
85
|
+
{hasFilter && (
|
|
86
|
+
<span
|
|
87
|
+
role="button"
|
|
88
|
+
tabIndex={0}
|
|
89
|
+
onClick={(e) => {
|
|
90
|
+
e.stopPropagation()
|
|
91
|
+
handleClear()
|
|
92
|
+
}}
|
|
93
|
+
onKeyDown={(e) => {
|
|
94
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
95
|
+
e.stopPropagation()
|
|
96
|
+
handleClear()
|
|
97
|
+
}
|
|
98
|
+
}}
|
|
99
|
+
className="ml-2 rounded-full p-0.5 hover:bg-muted"
|
|
100
|
+
>
|
|
101
|
+
<X className="h-3 w-3" />
|
|
102
|
+
</span>
|
|
103
|
+
)}
|
|
104
|
+
</Button>
|
|
105
|
+
</PopoverTrigger>
|
|
106
|
+
<PopoverContent
|
|
107
|
+
className="w-auto p-0"
|
|
108
|
+
side="bottom"
|
|
109
|
+
align="start"
|
|
110
|
+
sideOffset={4}
|
|
111
|
+
>
|
|
112
|
+
<div className="flex flex-col">
|
|
113
|
+
<div className="flex items-center gap-2 border-b border-border p-3">
|
|
114
|
+
<Button
|
|
115
|
+
variant="ghost"
|
|
116
|
+
size="sm"
|
|
117
|
+
className="h-7 text-xs"
|
|
118
|
+
onClick={() => {
|
|
119
|
+
const today = new Date()
|
|
120
|
+
setDate({ from: today, to: today })
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
Hoy
|
|
124
|
+
</Button>
|
|
125
|
+
<Button
|
|
126
|
+
variant="ghost"
|
|
127
|
+
size="sm"
|
|
128
|
+
className="h-7 text-xs"
|
|
129
|
+
onClick={() => {
|
|
130
|
+
const today = new Date()
|
|
131
|
+
const weekAgo = new Date(today)
|
|
132
|
+
weekAgo.setDate(today.getDate() - 7)
|
|
133
|
+
setDate({ from: weekAgo, to: today })
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
Última semana
|
|
137
|
+
</Button>
|
|
138
|
+
<Button
|
|
139
|
+
variant="ghost"
|
|
140
|
+
size="sm"
|
|
141
|
+
className="h-7 text-xs"
|
|
142
|
+
onClick={() => {
|
|
143
|
+
const today = new Date()
|
|
144
|
+
const monthAgo = new Date(today)
|
|
145
|
+
monthAgo.setMonth(today.getMonth() - 1)
|
|
146
|
+
setDate({ from: monthAgo, to: today })
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
Último mes
|
|
150
|
+
</Button>
|
|
151
|
+
</div>
|
|
152
|
+
<Calendar
|
|
153
|
+
mode="range"
|
|
154
|
+
defaultMonth={date?.from}
|
|
155
|
+
selected={date}
|
|
156
|
+
onSelect={handleSelect}
|
|
157
|
+
numberOfMonths={2}
|
|
158
|
+
/>
|
|
159
|
+
<div className="flex gap-2 border-t border-border p-3">
|
|
160
|
+
<Button
|
|
161
|
+
variant="outline"
|
|
162
|
+
size="sm"
|
|
163
|
+
className="flex-1"
|
|
164
|
+
onClick={handleClear}
|
|
165
|
+
>
|
|
166
|
+
Limpiar
|
|
167
|
+
</Button>
|
|
168
|
+
<Button
|
|
169
|
+
size="sm"
|
|
170
|
+
className="flex-1"
|
|
171
|
+
onClick={handleApply}
|
|
172
|
+
>
|
|
173
|
+
Aplicar
|
|
174
|
+
</Button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</PopoverContent>
|
|
178
|
+
</Popover>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Filter function para usar con TanStack Table
|
|
183
|
+
export function dateRangeFilterFn<TData>(
|
|
184
|
+
row: { getValue: (id: string) => unknown },
|
|
185
|
+
columnId: string,
|
|
186
|
+
filterValue: DateRange | undefined
|
|
187
|
+
): boolean {
|
|
188
|
+
if (!filterValue?.from) return true
|
|
189
|
+
|
|
190
|
+
const cellValue = row.getValue(columnId)
|
|
191
|
+
if (!cellValue) return false
|
|
192
|
+
|
|
193
|
+
const date = new Date(cellValue as string)
|
|
194
|
+
const from = filterValue.from
|
|
195
|
+
const to = filterValue.to || from
|
|
196
|
+
|
|
197
|
+
// Reset hours for comparison
|
|
198
|
+
const dateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
|
199
|
+
const fromOnly = new Date(from.getFullYear(), from.getMonth(), from.getDate())
|
|
200
|
+
const toOnly = new Date(to.getFullYear(), to.getMonth(), to.getDate())
|
|
201
|
+
|
|
202
|
+
return dateOnly >= fromOnly && dateOnly <= toOnly
|
|
203
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|