@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,185 +1,185 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import * as React from 'react'
|
|
4
|
-
import { Column } from '@tanstack/react-table'
|
|
5
|
-
import { Check, PlusCircle } from 'lucide-react'
|
|
6
|
-
|
|
7
|
-
import { cn } from '@/lib/utils'
|
|
8
|
-
import { Badge } from '@/components/ui/badge'
|
|
9
|
-
import { Button } from '@/components/ui/button'
|
|
10
|
-
import {
|
|
11
|
-
Command,
|
|
12
|
-
CommandEmpty,
|
|
13
|
-
CommandGroup,
|
|
14
|
-
CommandInput,
|
|
15
|
-
CommandItem,
|
|
16
|
-
CommandList,
|
|
17
|
-
CommandSeparator,
|
|
18
|
-
} from '@/components/ui/command'
|
|
19
|
-
import {
|
|
20
|
-
Popover,
|
|
21
|
-
PopoverContent,
|
|
22
|
-
PopoverTrigger,
|
|
23
|
-
} from '@/components/ui/popover'
|
|
24
|
-
import { Separator } from '@/components/ui/separator'
|
|
25
|
-
|
|
26
|
-
export interface FacetedFilterOption {
|
|
27
|
-
label: string
|
|
28
|
-
value: string
|
|
29
|
-
icon?: React.ComponentType<{ className?: string }>
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface DataTableFacetedFilterProps<TData, TValue> {
|
|
33
|
-
column?: Column<TData, TValue>
|
|
34
|
-
title?: string
|
|
35
|
-
options: FacetedFilterOption[]
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function DataTableFacetedFilter<TData, TValue>({
|
|
39
|
-
column,
|
|
40
|
-
title,
|
|
41
|
-
options,
|
|
42
|
-
}: DataTableFacetedFilterProps<TData, TValue>) {
|
|
43
|
-
const facets = column?.getFacetedUniqueValues()
|
|
44
|
-
const filterValue = column?.getFilterValue() as string[] | undefined
|
|
45
|
-
const [open, setOpen] = React.useState(false)
|
|
46
|
-
const [selectedValues, setSelectedValues] = React.useState<Set<string>>(
|
|
47
|
-
new Set(filterValue)
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
// Sincronizar estado local con el filtro de la columna
|
|
51
|
-
React.useEffect(() => {
|
|
52
|
-
setSelectedValues(new Set(filterValue))
|
|
53
|
-
}, [filterValue])
|
|
54
|
-
|
|
55
|
-
const handleApply = () => {
|
|
56
|
-
const filterValues = Array.from(selectedValues)
|
|
57
|
-
column?.setFilterValue(filterValues.length ? filterValues : undefined)
|
|
58
|
-
setOpen(false)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const handleClear = () => {
|
|
62
|
-
setSelectedValues(new Set())
|
|
63
|
-
column?.setFilterValue(undefined)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const handleOpenChange = (isOpen: boolean) => {
|
|
67
|
-
if (!isOpen) {
|
|
68
|
-
// Al cerrar sin aplicar, resetear al valor actual del filtro
|
|
69
|
-
setSelectedValues(new Set(filterValue))
|
|
70
|
-
}
|
|
71
|
-
setOpen(isOpen)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return (
|
|
75
|
-
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
76
|
-
<PopoverTrigger asChild>
|
|
77
|
-
<Button
|
|
78
|
-
variant="outline"
|
|
79
|
-
size="sm"
|
|
80
|
-
className="h-8 border-dashed"
|
|
81
|
-
>
|
|
82
|
-
<PlusCircle className="mr-2 h-4 w-4" />
|
|
83
|
-
{title}
|
|
84
|
-
{selectedValues.size > 0 && (
|
|
85
|
-
<>
|
|
86
|
-
<Separator orientation="vertical" className="mx-2 h-4" />
|
|
87
|
-
<Badge
|
|
88
|
-
variant="secondary"
|
|
89
|
-
className="rounded-sm px-1 font-normal lg:hidden"
|
|
90
|
-
>
|
|
91
|
-
{selectedValues.size}
|
|
92
|
-
</Badge>
|
|
93
|
-
<div className="hidden space-x-1 lg:flex">
|
|
94
|
-
{selectedValues.size > 2 ? (
|
|
95
|
-
<Badge
|
|
96
|
-
variant="secondary"
|
|
97
|
-
className="rounded-sm px-1 font-normal"
|
|
98
|
-
>
|
|
99
|
-
{selectedValues.size} seleccionados
|
|
100
|
-
</Badge>
|
|
101
|
-
) : (
|
|
102
|
-
options
|
|
103
|
-
.filter((option) => selectedValues.has(option.value))
|
|
104
|
-
.map((option) => (
|
|
105
|
-
<Badge
|
|
106
|
-
variant="secondary"
|
|
107
|
-
key={option.value}
|
|
108
|
-
className="rounded-sm px-1 font-normal"
|
|
109
|
-
>
|
|
110
|
-
{option.label}
|
|
111
|
-
</Badge>
|
|
112
|
-
))
|
|
113
|
-
)}
|
|
114
|
-
</div>
|
|
115
|
-
</>
|
|
116
|
-
)}
|
|
117
|
-
</Button>
|
|
118
|
-
</PopoverTrigger>
|
|
119
|
-
<PopoverContent className="w-[200px] p-0" side="bottom" align="start" sideOffset={4}>
|
|
120
|
-
<Command>
|
|
121
|
-
<CommandInput placeholder={title} />
|
|
122
|
-
<CommandList>
|
|
123
|
-
<CommandEmpty>Sin resultados.</CommandEmpty>
|
|
124
|
-
<CommandGroup>
|
|
125
|
-
{options.map((option) => {
|
|
126
|
-
const isSelected = selectedValues.has(option.value)
|
|
127
|
-
return (
|
|
128
|
-
<CommandItem
|
|
129
|
-
key={option.value}
|
|
130
|
-
onSelect={() => {
|
|
131
|
-
const newSelected = new Set(selectedValues)
|
|
132
|
-
if (isSelected) {
|
|
133
|
-
newSelected.delete(option.value)
|
|
134
|
-
} else {
|
|
135
|
-
newSelected.add(option.value)
|
|
136
|
-
}
|
|
137
|
-
setSelectedValues(newSelected)
|
|
138
|
-
}}
|
|
139
|
-
>
|
|
140
|
-
<div
|
|
141
|
-
className={cn(
|
|
142
|
-
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
|
143
|
-
isSelected
|
|
144
|
-
? 'bg-primary text-primary-foreground'
|
|
145
|
-
: 'opacity-50 [&_svg]:invisible'
|
|
146
|
-
)}
|
|
147
|
-
>
|
|
148
|
-
<Check className="h-4 w-4" />
|
|
149
|
-
</div>
|
|
150
|
-
{option.icon && (
|
|
151
|
-
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
|
152
|
-
)}
|
|
153
|
-
<span>{option.label}</span>
|
|
154
|
-
{facets?.get(option.value) && (
|
|
155
|
-
<span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
|
|
156
|
-
{facets.get(option.value)}
|
|
157
|
-
</span>
|
|
158
|
-
)}
|
|
159
|
-
</CommandItem>
|
|
160
|
-
)
|
|
161
|
-
})}
|
|
162
|
-
</CommandGroup>
|
|
163
|
-
</CommandList>
|
|
164
|
-
</Command>
|
|
165
|
-
<div className="flex gap-2 border-t border-border p-2">
|
|
166
|
-
<Button
|
|
167
|
-
variant="outline"
|
|
168
|
-
size="sm"
|
|
169
|
-
className="flex-1"
|
|
170
|
-
onClick={handleClear}
|
|
171
|
-
>
|
|
172
|
-
Limpiar
|
|
173
|
-
</Button>
|
|
174
|
-
<Button
|
|
175
|
-
size="sm"
|
|
176
|
-
className="flex-1"
|
|
177
|
-
onClick={handleApply}
|
|
178
|
-
>
|
|
179
|
-
Aplicar
|
|
180
|
-
</Button>
|
|
181
|
-
</div>
|
|
182
|
-
</PopoverContent>
|
|
183
|
-
</Popover>
|
|
184
|
-
)
|
|
185
|
-
}
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Column } from '@tanstack/react-table'
|
|
5
|
+
import { Check, PlusCircle } from 'lucide-react'
|
|
6
|
+
|
|
7
|
+
import { cn } from '@/lib/utils'
|
|
8
|
+
import { Badge } from '@/components/ui/badge'
|
|
9
|
+
import { Button } from '@/components/ui/button'
|
|
10
|
+
import {
|
|
11
|
+
Command,
|
|
12
|
+
CommandEmpty,
|
|
13
|
+
CommandGroup,
|
|
14
|
+
CommandInput,
|
|
15
|
+
CommandItem,
|
|
16
|
+
CommandList,
|
|
17
|
+
CommandSeparator,
|
|
18
|
+
} from '@/components/ui/command'
|
|
19
|
+
import {
|
|
20
|
+
Popover,
|
|
21
|
+
PopoverContent,
|
|
22
|
+
PopoverTrigger,
|
|
23
|
+
} from '@/components/ui/popover'
|
|
24
|
+
import { Separator } from '@/components/ui/separator'
|
|
25
|
+
|
|
26
|
+
export interface FacetedFilterOption {
|
|
27
|
+
label: string
|
|
28
|
+
value: string
|
|
29
|
+
icon?: React.ComponentType<{ className?: string }>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface DataTableFacetedFilterProps<TData, TValue> {
|
|
33
|
+
column?: Column<TData, TValue>
|
|
34
|
+
title?: string
|
|
35
|
+
options: FacetedFilterOption[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function DataTableFacetedFilter<TData, TValue>({
|
|
39
|
+
column,
|
|
40
|
+
title,
|
|
41
|
+
options,
|
|
42
|
+
}: DataTableFacetedFilterProps<TData, TValue>) {
|
|
43
|
+
const facets = column?.getFacetedUniqueValues()
|
|
44
|
+
const filterValue = column?.getFilterValue() as string[] | undefined
|
|
45
|
+
const [open, setOpen] = React.useState(false)
|
|
46
|
+
const [selectedValues, setSelectedValues] = React.useState<Set<string>>(
|
|
47
|
+
new Set(filterValue)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
// Sincronizar estado local con el filtro de la columna
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
setSelectedValues(new Set(filterValue))
|
|
53
|
+
}, [filterValue])
|
|
54
|
+
|
|
55
|
+
const handleApply = () => {
|
|
56
|
+
const filterValues = Array.from(selectedValues)
|
|
57
|
+
column?.setFilterValue(filterValues.length ? filterValues : undefined)
|
|
58
|
+
setOpen(false)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const handleClear = () => {
|
|
62
|
+
setSelectedValues(new Set())
|
|
63
|
+
column?.setFilterValue(undefined)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const handleOpenChange = (isOpen: boolean) => {
|
|
67
|
+
if (!isOpen) {
|
|
68
|
+
// Al cerrar sin aplicar, resetear al valor actual del filtro
|
|
69
|
+
setSelectedValues(new Set(filterValue))
|
|
70
|
+
}
|
|
71
|
+
setOpen(isOpen)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
76
|
+
<PopoverTrigger asChild>
|
|
77
|
+
<Button
|
|
78
|
+
variant="outline"
|
|
79
|
+
size="sm"
|
|
80
|
+
className="h-8 border-dashed"
|
|
81
|
+
>
|
|
82
|
+
<PlusCircle className="mr-2 h-4 w-4" />
|
|
83
|
+
{title}
|
|
84
|
+
{selectedValues.size > 0 && (
|
|
85
|
+
<>
|
|
86
|
+
<Separator orientation="vertical" className="mx-2 h-4" />
|
|
87
|
+
<Badge
|
|
88
|
+
variant="secondary"
|
|
89
|
+
className="rounded-sm px-1 font-normal lg:hidden"
|
|
90
|
+
>
|
|
91
|
+
{selectedValues.size}
|
|
92
|
+
</Badge>
|
|
93
|
+
<div className="hidden space-x-1 lg:flex">
|
|
94
|
+
{selectedValues.size > 2 ? (
|
|
95
|
+
<Badge
|
|
96
|
+
variant="secondary"
|
|
97
|
+
className="rounded-sm px-1 font-normal"
|
|
98
|
+
>
|
|
99
|
+
{selectedValues.size} seleccionados
|
|
100
|
+
</Badge>
|
|
101
|
+
) : (
|
|
102
|
+
options
|
|
103
|
+
.filter((option) => selectedValues.has(option.value))
|
|
104
|
+
.map((option) => (
|
|
105
|
+
<Badge
|
|
106
|
+
variant="secondary"
|
|
107
|
+
key={option.value}
|
|
108
|
+
className="rounded-sm px-1 font-normal"
|
|
109
|
+
>
|
|
110
|
+
{option.label}
|
|
111
|
+
</Badge>
|
|
112
|
+
))
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
</>
|
|
116
|
+
)}
|
|
117
|
+
</Button>
|
|
118
|
+
</PopoverTrigger>
|
|
119
|
+
<PopoverContent className="w-[200px] p-0" side="bottom" align="start" sideOffset={4}>
|
|
120
|
+
<Command>
|
|
121
|
+
<CommandInput placeholder={title} />
|
|
122
|
+
<CommandList>
|
|
123
|
+
<CommandEmpty>Sin resultados.</CommandEmpty>
|
|
124
|
+
<CommandGroup>
|
|
125
|
+
{options.map((option) => {
|
|
126
|
+
const isSelected = selectedValues.has(option.value)
|
|
127
|
+
return (
|
|
128
|
+
<CommandItem
|
|
129
|
+
key={option.value}
|
|
130
|
+
onSelect={() => {
|
|
131
|
+
const newSelected = new Set(selectedValues)
|
|
132
|
+
if (isSelected) {
|
|
133
|
+
newSelected.delete(option.value)
|
|
134
|
+
} else {
|
|
135
|
+
newSelected.add(option.value)
|
|
136
|
+
}
|
|
137
|
+
setSelectedValues(newSelected)
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
<div
|
|
141
|
+
className={cn(
|
|
142
|
+
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
|
143
|
+
isSelected
|
|
144
|
+
? 'bg-primary text-primary-foreground'
|
|
145
|
+
: 'opacity-50 [&_svg]:invisible'
|
|
146
|
+
)}
|
|
147
|
+
>
|
|
148
|
+
<Check className="h-4 w-4" />
|
|
149
|
+
</div>
|
|
150
|
+
{option.icon && (
|
|
151
|
+
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
|
152
|
+
)}
|
|
153
|
+
<span>{option.label}</span>
|
|
154
|
+
{facets?.get(option.value) && (
|
|
155
|
+
<span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
|
|
156
|
+
{facets.get(option.value)}
|
|
157
|
+
</span>
|
|
158
|
+
)}
|
|
159
|
+
</CommandItem>
|
|
160
|
+
)
|
|
161
|
+
})}
|
|
162
|
+
</CommandGroup>
|
|
163
|
+
</CommandList>
|
|
164
|
+
</Command>
|
|
165
|
+
<div className="flex gap-2 border-t border-border p-2">
|
|
166
|
+
<Button
|
|
167
|
+
variant="outline"
|
|
168
|
+
size="sm"
|
|
169
|
+
className="flex-1"
|
|
170
|
+
onClick={handleClear}
|
|
171
|
+
>
|
|
172
|
+
Limpiar
|
|
173
|
+
</Button>
|
|
174
|
+
<Button
|
|
175
|
+
size="sm"
|
|
176
|
+
className="flex-1"
|
|
177
|
+
onClick={handleApply}
|
|
178
|
+
>
|
|
179
|
+
Aplicar
|
|
180
|
+
</Button>
|
|
181
|
+
</div>
|
|
182
|
+
</PopoverContent>
|
|
183
|
+
</Popover>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import * as React from 'react'
|
|
4
|
-
import { Table } from '@tanstack/react-table'
|
|
5
|
-
import { Filter, X } from 'lucide-react'
|
|
6
|
-
|
|
7
|
-
import { cn } from '@/lib/utils'
|
|
8
|
-
import { Badge } from '@/components/ui/badge'
|
|
9
|
-
import { Button } from '@/components/ui/button'
|
|
10
|
-
import {
|
|
11
|
-
Popover,
|
|
12
|
-
PopoverContent,
|
|
13
|
-
PopoverTrigger,
|
|
14
|
-
} from '@/components/ui/popover'
|
|
15
|
-
import { Separator } from '@/components/ui/separator'
|
|
16
|
-
import { DataTableFacetedFilter } from './data-table-faceted-filter'
|
|
17
|
-
import { DataTableDateFilter } from './data-table-date-filter'
|
|
18
|
-
import { DataTableNumberFilter } from './data-table-number-filter'
|
|
19
|
-
import type { FilterConfig } from './data-table'
|
|
20
|
-
|
|
21
|
-
interface DataTableFiltersDropdownProps<TData> {
|
|
22
|
-
table: Table<TData>
|
|
23
|
-
filters: FilterConfig[]
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function DataTableFiltersDropdown<TData>({
|
|
27
|
-
table,
|
|
28
|
-
filters,
|
|
29
|
-
}: DataTableFiltersDropdownProps<TData>) {
|
|
30
|
-
const [open, setOpen] = React.useState(false)
|
|
31
|
-
|
|
32
|
-
// Contar filtros activos (excluyendo el search)
|
|
33
|
-
const activeFiltersCount = table.getState().columnFilters.filter(
|
|
34
|
-
(filter) => filters.some((f) => f.columnId === filter.id)
|
|
35
|
-
).length
|
|
36
|
-
|
|
37
|
-
const handleClearAll = () => {
|
|
38
|
-
// Solo limpiar los filtros configurados, no el search
|
|
39
|
-
filters.forEach((filter) => {
|
|
40
|
-
table.getColumn(filter.columnId)?.setFilterValue(undefined)
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<Popover open={open} onOpenChange={setOpen}>
|
|
46
|
-
<PopoverTrigger asChild>
|
|
47
|
-
<Button
|
|
48
|
-
variant="outline"
|
|
49
|
-
size="sm"
|
|
50
|
-
className={cn(
|
|
51
|
-
'h-8 border-dashed',
|
|
52
|
-
activeFiltersCount > 0 && 'border-solid'
|
|
53
|
-
)}
|
|
54
|
-
>
|
|
55
|
-
<Filter className="mr-2 h-4 w-4" />
|
|
56
|
-
Filtros
|
|
57
|
-
{activeFiltersCount > 0 && (
|
|
58
|
-
<Badge
|
|
59
|
-
variant="secondary"
|
|
60
|
-
className="ml-2 rounded-sm px-1 font-normal"
|
|
61
|
-
>
|
|
62
|
-
{activeFiltersCount}
|
|
63
|
-
</Badge>
|
|
64
|
-
)}
|
|
65
|
-
</Button>
|
|
66
|
-
</PopoverTrigger>
|
|
67
|
-
<PopoverContent className="w-[320px] p-0" align="start">
|
|
68
|
-
<div className="flex flex-col">
|
|
69
|
-
{/* Header */}
|
|
70
|
-
<div className="flex items-center justify-between border-b border-border px-4 py-3">
|
|
71
|
-
<span className="text-sm font-medium">Filtros</span>
|
|
72
|
-
{activeFiltersCount > 0 && (
|
|
73
|
-
<Button
|
|
74
|
-
variant="ghost"
|
|
75
|
-
size="sm"
|
|
76
|
-
className="h-7 px-2 text-xs"
|
|
77
|
-
onClick={handleClearAll}
|
|
78
|
-
>
|
|
79
|
-
Limpiar todo
|
|
80
|
-
<X className="ml-1 h-3 w-3" />
|
|
81
|
-
</Button>
|
|
82
|
-
)}
|
|
83
|
-
</div>
|
|
84
|
-
|
|
85
|
-
{/* Filters */}
|
|
86
|
-
<div className="flex flex-col gap-3 p-4">
|
|
87
|
-
{filters.map((filter, index) => {
|
|
88
|
-
const column = table.getColumn(filter.columnId)
|
|
89
|
-
if (!column) return null
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<div key={filter.columnId} className="flex flex-col gap-2">
|
|
93
|
-
{index > 0 && <Separator className="my-1" />}
|
|
94
|
-
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
95
|
-
{filter.title}
|
|
96
|
-
</label>
|
|
97
|
-
|
|
98
|
-
{filter.type === 'faceted' && filter.options && (
|
|
99
|
-
<DataTableFacetedFilter
|
|
100
|
-
column={column}
|
|
101
|
-
title={filter.title}
|
|
102
|
-
options={filter.options}
|
|
103
|
-
/>
|
|
104
|
-
)}
|
|
105
|
-
|
|
106
|
-
{filter.type === 'date-range' && (
|
|
107
|
-
<DataTableDateFilter
|
|
108
|
-
column={column}
|
|
109
|
-
title={filter.title}
|
|
110
|
-
/>
|
|
111
|
-
)}
|
|
112
|
-
|
|
113
|
-
{filter.type === 'number-range' && (
|
|
114
|
-
<DataTableNumberFilter
|
|
115
|
-
column={column}
|
|
116
|
-
title={filter.title}
|
|
117
|
-
format={filter.format}
|
|
118
|
-
currencySymbol={filter.currencySymbol}
|
|
119
|
-
step={filter.step}
|
|
120
|
-
/>
|
|
121
|
-
)}
|
|
122
|
-
</div>
|
|
123
|
-
)
|
|
124
|
-
})}
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
</PopoverContent>
|
|
128
|
-
</Popover>
|
|
129
|
-
)
|
|
130
|
-
}
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Table } from '@tanstack/react-table'
|
|
5
|
+
import { Filter, X } from 'lucide-react'
|
|
6
|
+
|
|
7
|
+
import { cn } from '@/lib/utils'
|
|
8
|
+
import { Badge } from '@/components/ui/badge'
|
|
9
|
+
import { Button } from '@/components/ui/button'
|
|
10
|
+
import {
|
|
11
|
+
Popover,
|
|
12
|
+
PopoverContent,
|
|
13
|
+
PopoverTrigger,
|
|
14
|
+
} from '@/components/ui/popover'
|
|
15
|
+
import { Separator } from '@/components/ui/separator'
|
|
16
|
+
import { DataTableFacetedFilter } from './data-table-faceted-filter'
|
|
17
|
+
import { DataTableDateFilter } from './data-table-date-filter'
|
|
18
|
+
import { DataTableNumberFilter } from './data-table-number-filter'
|
|
19
|
+
import type { FilterConfig } from './data-table'
|
|
20
|
+
|
|
21
|
+
interface DataTableFiltersDropdownProps<TData> {
|
|
22
|
+
table: Table<TData>
|
|
23
|
+
filters: FilterConfig[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function DataTableFiltersDropdown<TData>({
|
|
27
|
+
table,
|
|
28
|
+
filters,
|
|
29
|
+
}: DataTableFiltersDropdownProps<TData>) {
|
|
30
|
+
const [open, setOpen] = React.useState(false)
|
|
31
|
+
|
|
32
|
+
// Contar filtros activos (excluyendo el search)
|
|
33
|
+
const activeFiltersCount = table.getState().columnFilters.filter(
|
|
34
|
+
(filter) => filters.some((f) => f.columnId === filter.id)
|
|
35
|
+
).length
|
|
36
|
+
|
|
37
|
+
const handleClearAll = () => {
|
|
38
|
+
// Solo limpiar los filtros configurados, no el search
|
|
39
|
+
filters.forEach((filter) => {
|
|
40
|
+
table.getColumn(filter.columnId)?.setFilterValue(undefined)
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
46
|
+
<PopoverTrigger asChild>
|
|
47
|
+
<Button
|
|
48
|
+
variant="outline"
|
|
49
|
+
size="sm"
|
|
50
|
+
className={cn(
|
|
51
|
+
'h-8 border-dashed',
|
|
52
|
+
activeFiltersCount > 0 && 'border-solid'
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
<Filter className="mr-2 h-4 w-4" />
|
|
56
|
+
Filtros
|
|
57
|
+
{activeFiltersCount > 0 && (
|
|
58
|
+
<Badge
|
|
59
|
+
variant="secondary"
|
|
60
|
+
className="ml-2 rounded-sm px-1 font-normal"
|
|
61
|
+
>
|
|
62
|
+
{activeFiltersCount}
|
|
63
|
+
</Badge>
|
|
64
|
+
)}
|
|
65
|
+
</Button>
|
|
66
|
+
</PopoverTrigger>
|
|
67
|
+
<PopoverContent className="w-[320px] p-0" align="start">
|
|
68
|
+
<div className="flex flex-col">
|
|
69
|
+
{/* Header */}
|
|
70
|
+
<div className="flex items-center justify-between border-b border-border px-4 py-3">
|
|
71
|
+
<span className="text-sm font-medium">Filtros</span>
|
|
72
|
+
{activeFiltersCount > 0 && (
|
|
73
|
+
<Button
|
|
74
|
+
variant="ghost"
|
|
75
|
+
size="sm"
|
|
76
|
+
className="h-7 px-2 text-xs"
|
|
77
|
+
onClick={handleClearAll}
|
|
78
|
+
>
|
|
79
|
+
Limpiar todo
|
|
80
|
+
<X className="ml-1 h-3 w-3" />
|
|
81
|
+
</Button>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Filters */}
|
|
86
|
+
<div className="flex flex-col gap-3 p-4">
|
|
87
|
+
{filters.map((filter, index) => {
|
|
88
|
+
const column = table.getColumn(filter.columnId)
|
|
89
|
+
if (!column) return null
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div key={filter.columnId} className="flex flex-col gap-2">
|
|
93
|
+
{index > 0 && <Separator className="my-1" />}
|
|
94
|
+
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
95
|
+
{filter.title}
|
|
96
|
+
</label>
|
|
97
|
+
|
|
98
|
+
{filter.type === 'faceted' && filter.options && (
|
|
99
|
+
<DataTableFacetedFilter
|
|
100
|
+
column={column}
|
|
101
|
+
title={filter.title}
|
|
102
|
+
options={filter.options}
|
|
103
|
+
/>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{filter.type === 'date-range' && (
|
|
107
|
+
<DataTableDateFilter
|
|
108
|
+
column={column}
|
|
109
|
+
title={filter.title}
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{filter.type === 'number-range' && (
|
|
114
|
+
<DataTableNumberFilter
|
|
115
|
+
column={column}
|
|
116
|
+
title={filter.title}
|
|
117
|
+
format={filter.format}
|
|
118
|
+
currencySymbol={filter.currencySymbol}
|
|
119
|
+
step={filter.step}
|
|
120
|
+
/>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
})}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</PopoverContent>
|
|
128
|
+
</Popover>
|
|
129
|
+
)
|
|
130
|
+
}
|