@create-lft-app/nextjs 3.1.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/package.json +1 -1
- 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 +74 -239
- package/template/src/components/layout/sidebar.tsx +4 -9
- 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-toolbar.tsx +115 -25
- package/template/src/components/tables/data-table-view-options.tsx +10 -6
- package/template/src/components/tables/data-table.tsx +41 -21
- package/template/src/components/tables/index.ts +5 -1
- package/template/src/components/ui/alert-dialog.tsx +1 -1
- package/template/src/components/ui/avatar.tsx +2 -2
- package/template/src/components/ui/badge.tsx +2 -2
- package/template/src/components/ui/button.tsx +1 -1
- package/template/src/components/ui/card.tsx +1 -1
- package/template/src/components/ui/command.tsx +4 -4
- package/template/src/components/ui/dropdown-menu.tsx +4 -4
- package/template/src/components/ui/form.tsx +1 -1
- package/template/src/components/ui/icons.tsx +1 -1
- package/template/src/components/ui/popover.tsx +1 -1
- package/template/src/components/ui/progress.tsx +1 -1
- package/template/src/components/ui/select.tsx +3 -3
- package/template/src/components/ui/sonner.tsx +1 -1
- package/template/src/components/ui/spinner.tsx +1 -1
- package/template/src/components/ui/table.tsx +3 -3
- package/template/src/components/ui/tooltip.tsx +2 -2
- package/template/src/config/navigation.ts +1 -11
- package/template/src/config/roles.ts +27 -0
- package/template/src/lib/date/config.ts +4 -2
- package/template/src/lib/date/formatters.ts +7 -0
- package/template/src/lib/date/index.ts +8 -1
- package/template/src/lib/supabase/admin.ts +23 -0
- package/template/src/lib/supabase/proxy.ts +1 -1
- package/template/src/modules/users/actions/users-actions.ts +106 -34
- package/template/src/modules/users/columns.tsx +29 -9
- package/template/src/modules/users/components/users-list.tsx +27 -1
- package/template/src/modules/users/hooks/useUsersMutations.ts +3 -3
- package/template/src/modules/users/index.ts +20 -2
- package/template/src/modules/users/schemas/users.schema.ts +29 -1
- 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/tsconfig.tsbuildinfo +1 -1
|
@@ -1,50 +1,140 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import * as React from 'react'
|
|
3
4
|
import { Table } from '@tanstack/react-table'
|
|
4
|
-
import { X } from 'lucide-react'
|
|
5
|
+
import { Filter, X } from 'lucide-react'
|
|
5
6
|
|
|
6
7
|
import { Button } from '@/components/ui/button'
|
|
7
8
|
import { Input } from '@/components/ui/input'
|
|
8
|
-
import {
|
|
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'
|
|
9
14
|
|
|
10
15
|
interface DataTableToolbarProps<TData> {
|
|
11
16
|
table: Table<TData>
|
|
12
17
|
searchKey?: string
|
|
13
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'
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
export function DataTableToolbar<TData>({
|
|
17
25
|
table,
|
|
18
26
|
searchKey,
|
|
19
27
|
searchPlaceholder = 'Buscar...',
|
|
28
|
+
filters,
|
|
29
|
+
filterMode = 'dropdown',
|
|
20
30
|
}: DataTableToolbarProps<TData>) {
|
|
21
31
|
const isFiltered = table.getState().columnFilters.length > 0
|
|
32
|
+
const [showFilters, setShowFilters] = React.useState(false)
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
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}
|
|
34
56
|
/>
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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>
|
|
46
130
|
</div>
|
|
47
|
-
|
|
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
|
+
)}
|
|
48
138
|
</div>
|
|
49
139
|
)
|
|
50
140
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import * as React from 'react'
|
|
3
4
|
import { Table } from '@tanstack/react-table'
|
|
4
5
|
import { Settings2 } from 'lucide-react'
|
|
5
6
|
|
|
@@ -20,16 +21,18 @@ interface DataTableViewOptionsProps<TData> {
|
|
|
20
21
|
export function DataTableViewOptions<TData>({
|
|
21
22
|
table,
|
|
22
23
|
}: DataTableViewOptionsProps<TData>) {
|
|
24
|
+
const [open, setOpen] = React.useState(false)
|
|
25
|
+
|
|
23
26
|
return (
|
|
24
|
-
<DropdownMenu>
|
|
27
|
+
<DropdownMenu open={open} onOpenChange={setOpen}>
|
|
25
28
|
<DropdownMenuTrigger asChild>
|
|
26
29
|
<Button
|
|
27
|
-
variant="
|
|
28
|
-
size="
|
|
29
|
-
className="
|
|
30
|
+
variant="ghost"
|
|
31
|
+
size="icon"
|
|
32
|
+
className="h-8 w-8"
|
|
30
33
|
>
|
|
31
|
-
<Settings2 className="
|
|
32
|
-
Columnas
|
|
34
|
+
<Settings2 className="h-4 w-4" />
|
|
35
|
+
<span className="sr-only">Columnas</span>
|
|
33
36
|
</Button>
|
|
34
37
|
</DropdownMenuTrigger>
|
|
35
38
|
<DropdownMenuContent align="end" className="w-[150px]">
|
|
@@ -47,6 +50,7 @@ export function DataTableViewOptions<TData>({
|
|
|
47
50
|
key={column.id}
|
|
48
51
|
className="capitalize"
|
|
49
52
|
checked={column.getIsVisible()}
|
|
53
|
+
onSelect={(e) => e.preventDefault()}
|
|
50
54
|
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
|
51
55
|
>
|
|
52
56
|
{column.id}
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
VisibilityState,
|
|
9
9
|
flexRender,
|
|
10
10
|
getCoreRowModel,
|
|
11
|
+
getFacetedRowModel,
|
|
12
|
+
getFacetedUniqueValues,
|
|
11
13
|
getFilteredRowModel,
|
|
12
14
|
getPaginationRowModel,
|
|
13
15
|
getSortedRowModel,
|
|
@@ -22,15 +24,31 @@ import {
|
|
|
22
24
|
TableHeader,
|
|
23
25
|
TableRow,
|
|
24
26
|
} from '@/components/ui/table'
|
|
25
|
-
import {
|
|
27
|
+
import { DataTableToolbar } from './data-table-toolbar'
|
|
26
28
|
import { DataTablePagination } from './data-table-pagination'
|
|
27
29
|
import { DataTableViewOptions } from './data-table-view-options'
|
|
28
30
|
|
|
31
|
+
export interface FilterConfig {
|
|
32
|
+
columnId: string
|
|
33
|
+
type: 'faceted' | 'date-range' | 'number-range'
|
|
34
|
+
title: string
|
|
35
|
+
options?: { label: string; value: string; icon?: React.ComponentType<{ className?: string }> }[]
|
|
36
|
+
/** Para number-range: formato del número */
|
|
37
|
+
format?: 'currency' | 'number'
|
|
38
|
+
/** Para number-range: símbolo de moneda */
|
|
39
|
+
currencySymbol?: string
|
|
40
|
+
/** Para number-range: paso del input */
|
|
41
|
+
step?: number
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
interface DataTableProps<TData, TValue> {
|
|
30
45
|
columns: ColumnDef<TData, TValue>[]
|
|
31
46
|
data: TData[]
|
|
32
47
|
searchKey?: string
|
|
33
48
|
searchPlaceholder?: string
|
|
49
|
+
filters?: FilterConfig[]
|
|
50
|
+
/** Modo de visualización de filtros: 'dropdown' (default), 'inline', 'inline-collapsible' */
|
|
51
|
+
filterMode?: 'dropdown' | 'inline' | 'inline-collapsible'
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
export function DataTable<TData, TValue>({
|
|
@@ -38,6 +56,8 @@ export function DataTable<TData, TValue>({
|
|
|
38
56
|
data,
|
|
39
57
|
searchKey,
|
|
40
58
|
searchPlaceholder = 'Buscar...',
|
|
59
|
+
filters,
|
|
60
|
+
filterMode = 'dropdown',
|
|
41
61
|
}: DataTableProps<TData, TValue>) {
|
|
42
62
|
const [sorting, setSorting] = React.useState<SortingState>([])
|
|
43
63
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
|
|
@@ -55,6 +75,8 @@ export function DataTable<TData, TValue>({
|
|
|
55
75
|
getFilteredRowModel: getFilteredRowModel(),
|
|
56
76
|
onColumnVisibilityChange: setColumnVisibility,
|
|
57
77
|
onRowSelectionChange: setRowSelection,
|
|
78
|
+
getFacetedRowModel: getFacetedRowModel(),
|
|
79
|
+
getFacetedUniqueValues: getFacetedUniqueValues(),
|
|
58
80
|
state: {
|
|
59
81
|
sorting,
|
|
60
82
|
columnFilters,
|
|
@@ -65,33 +87,31 @@ export function DataTable<TData, TValue>({
|
|
|
65
87
|
|
|
66
88
|
return (
|
|
67
89
|
<div className="space-y-4">
|
|
68
|
-
<
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
className="max-w-sm"
|
|
77
|
-
/>
|
|
78
|
-
)}
|
|
79
|
-
<DataTableViewOptions table={table} />
|
|
80
|
-
</div>
|
|
90
|
+
<DataTableToolbar
|
|
91
|
+
table={table}
|
|
92
|
+
searchKey={searchKey}
|
|
93
|
+
searchPlaceholder={searchPlaceholder}
|
|
94
|
+
filters={filters}
|
|
95
|
+
filterMode={filterMode}
|
|
96
|
+
/>
|
|
81
97
|
<div className="rounded-md border">
|
|
82
98
|
<Table>
|
|
83
99
|
<TableHeader>
|
|
84
100
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
85
101
|
<TableRow key={headerGroup.id}>
|
|
86
|
-
{headerGroup.headers.map((header) => {
|
|
102
|
+
{headerGroup.headers.map((header, index) => {
|
|
103
|
+
const isLastHeader = index === headerGroup.headers.length - 1
|
|
87
104
|
return (
|
|
88
105
|
<TableHead key={header.id}>
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
106
|
+
<div className={isLastHeader ? 'flex items-center justify-end gap-2' : ''}>
|
|
107
|
+
{header.isPlaceholder
|
|
108
|
+
? null
|
|
109
|
+
: flexRender(
|
|
110
|
+
header.column.columnDef.header,
|
|
111
|
+
header.getContext()
|
|
112
|
+
)}
|
|
113
|
+
{isLastHeader && <DataTableViewOptions table={table} />}
|
|
114
|
+
</div>
|
|
95
115
|
</TableHead>
|
|
96
116
|
)
|
|
97
117
|
})}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
export { DataTable } from './data-table'
|
|
1
|
+
export { DataTable, type FilterConfig } from './data-table'
|
|
2
2
|
export { DataTablePagination } from './data-table-pagination'
|
|
3
3
|
export { DataTableColumnHeader } from './data-table-column-header'
|
|
4
4
|
export { DataTableToolbar } from './data-table-toolbar'
|
|
5
5
|
export { DataTableViewOptions } from './data-table-view-options'
|
|
6
|
+
export { DataTableFacetedFilter, type FacetedFilterOption } from './data-table-faceted-filter'
|
|
7
|
+
export { DataTableDateFilter, dateRangeFilterFn } from './data-table-date-filter'
|
|
8
|
+
export { DataTableNumberFilter, numberRangeFilterFn, type NumberRange } from './data-table-number-filter'
|
|
9
|
+
export { DataTableFiltersDropdown } from './data-table-filters-dropdown'
|
|
@@ -37,7 +37,7 @@ function AlertDialogOverlay({
|
|
|
37
37
|
data-slot="alert-dialog-overlay"
|
|
38
38
|
className={cn(
|
|
39
39
|
"fixed inset-0 z-50",
|
|
40
|
-
"bg-
|
|
40
|
+
"bg-background/60 dark:bg-background/80",
|
|
41
41
|
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
42
42
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
43
43
|
className
|
|
@@ -43,8 +43,8 @@ function AvatarFallback({
|
|
|
43
43
|
data-slot="avatar-fallback"
|
|
44
44
|
className={cn(
|
|
45
45
|
"flex h-full w-full items-center justify-center rounded-full",
|
|
46
|
-
"bg-
|
|
47
|
-
"text-sm font-medium text-
|
|
46
|
+
"bg-muted",
|
|
47
|
+
"text-sm font-medium text-muted-foreground",
|
|
48
48
|
className
|
|
49
49
|
)}
|
|
50
50
|
{...props}
|
|
@@ -18,9 +18,9 @@ const badgeVariants = cva(
|
|
|
18
18
|
outline:
|
|
19
19
|
"text-foreground border-border",
|
|
20
20
|
tag:
|
|
21
|
-
"rounded-none border-0 bg-
|
|
21
|
+
"rounded-none border-0 bg-muted text-muted-foreground text-[10px] px-2 py-1",
|
|
22
22
|
"tag-rounded":
|
|
23
|
-
"rounded-full border-0 bg-
|
|
23
|
+
"rounded-full border-0 bg-muted text-muted-foreground text-[12px] px-3 py-1",
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
26
|
defaultVariants: {
|
|
@@ -63,7 +63,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
63
63
|
return (
|
|
64
64
|
<div
|
|
65
65
|
data-slot="card-description"
|
|
66
|
-
className={cn("text-sm text-
|
|
66
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
67
67
|
{...props}
|
|
68
68
|
/>
|
|
69
69
|
)
|
|
@@ -71,7 +71,7 @@ function CommandInput({
|
|
|
71
71
|
data-slot="command-input-wrapper"
|
|
72
72
|
className="flex items-center border-b border-border px-3"
|
|
73
73
|
>
|
|
74
|
-
<SearchIcon className="mr-2 h-4 w-4 shrink-0 text-
|
|
74
|
+
<SearchIcon className="mr-2 h-4 w-4 shrink-0 text-muted-foreground" />
|
|
75
75
|
<CommandPrimitive.Input
|
|
76
76
|
data-slot="command-input"
|
|
77
77
|
className={cn(
|
|
@@ -156,10 +156,10 @@ function CommandItem({
|
|
|
156
156
|
"rounded-sm px-2 py-1.5",
|
|
157
157
|
"text-sm outline-none",
|
|
158
158
|
"transition-colors duration-150",
|
|
159
|
-
"data-[selected=true]:bg-
|
|
160
|
-
"data-[selected=true]:text-foreground",
|
|
159
|
+
"data-[selected=true]:bg-accent",
|
|
160
|
+
"data-[selected=true]:text-accent-foreground",
|
|
161
161
|
"data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
|
|
162
|
-
"[&_svg]:mr-2 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:text-
|
|
162
|
+
"[&_svg]:mr-2 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:text-muted-foreground",
|
|
163
163
|
className
|
|
164
164
|
)}
|
|
165
165
|
{...props}
|
|
@@ -44,7 +44,7 @@ function DropdownMenuContent({
|
|
|
44
44
|
className={cn(
|
|
45
45
|
"z-50 min-w-[8rem] overflow-hidden",
|
|
46
46
|
"rounded-md border border-border",
|
|
47
|
-
"bg-
|
|
47
|
+
"bg-popover",
|
|
48
48
|
"p-1",
|
|
49
49
|
"text-popover-foreground",
|
|
50
50
|
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
@@ -89,12 +89,12 @@ function DropdownMenuItem({
|
|
|
89
89
|
"rounded-sm px-2 py-1.5",
|
|
90
90
|
"text-sm outline-none",
|
|
91
91
|
"transition-colors duration-150",
|
|
92
|
-
"focus:bg-
|
|
93
|
-
"focus:text-foreground",
|
|
92
|
+
"focus:bg-accent",
|
|
93
|
+
"focus:text-accent-foreground",
|
|
94
94
|
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
95
95
|
"data-[inset]:pl-8",
|
|
96
96
|
"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
97
|
-
"[&_svg:not([class*='text-'])]:text-
|
|
97
|
+
"[&_svg:not([class*='text-'])]:text-muted-foreground",
|
|
98
98
|
"data-[variant=destructive]:text-destructive",
|
|
99
99
|
"data-[variant=destructive]:focus:bg-destructive/10",
|
|
100
100
|
"data-[variant=destructive]:focus:text-destructive",
|
|
@@ -32,7 +32,7 @@ function PopoverContent({
|
|
|
32
32
|
className={cn(
|
|
33
33
|
"z-50 w-72 rounded-md p-4",
|
|
34
34
|
"border border-border",
|
|
35
|
-
"bg-
|
|
35
|
+
"bg-popover",
|
|
36
36
|
"text-popover-foreground",
|
|
37
37
|
"outline-none",
|
|
38
38
|
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
@@ -52,7 +52,7 @@ function SelectTrigger({
|
|
|
52
52
|
>
|
|
53
53
|
{children}
|
|
54
54
|
<SelectPrimitive.Icon asChild>
|
|
55
|
-
<ChevronDownIcon className="h-4 w-4 text-
|
|
55
|
+
<ChevronDownIcon className="h-4 w-4 text-muted-foreground" />
|
|
56
56
|
</SelectPrimitive.Icon>
|
|
57
57
|
</SelectPrimitive.Trigger>
|
|
58
58
|
)
|
|
@@ -71,7 +71,7 @@ function SelectContent({
|
|
|
71
71
|
className={cn(
|
|
72
72
|
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden",
|
|
73
73
|
"rounded-md border border-border",
|
|
74
|
-
"bg-
|
|
74
|
+
"bg-popover",
|
|
75
75
|
"text-popover-foreground",
|
|
76
76
|
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
77
77
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
@@ -129,7 +129,7 @@ function SelectItem({
|
|
|
129
129
|
"rounded-sm py-1.5 pl-2 pr-8",
|
|
130
130
|
"text-sm outline-none",
|
|
131
131
|
"transition-colors duration-150",
|
|
132
|
-
"focus:bg-
|
|
132
|
+
"focus:bg-accent",
|
|
133
133
|
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
134
134
|
className
|
|
135
135
|
)}
|
|
@@ -29,7 +29,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|
|
29
29
|
error:
|
|
30
30
|
"group-[.toaster]:bg-destructive group-[.toaster]:text-destructive-foreground group-[.toaster]:border-destructive",
|
|
31
31
|
success:
|
|
32
|
-
"group-[.toaster]:bg-
|
|
32
|
+
"group-[.toaster]:bg-green-100 dark:group-[.toaster]:bg-green-950 group-[.toaster]:text-green-900 dark:group-[.toaster]:text-green-100 group-[.toaster]:border-green-200 dark:group-[.toaster]:border-green-800",
|
|
33
33
|
},
|
|
34
34
|
}}
|
|
35
35
|
icons={{
|
|
@@ -15,7 +15,7 @@ function Spinner({ className, size = 20, style, ...props }: SpinnerProps) {
|
|
|
15
15
|
strokeLinecap="round"
|
|
16
16
|
strokeLinejoin="round"
|
|
17
17
|
xmlns="http://www.w3.org/2000/svg"
|
|
18
|
-
className={cn("animate-spin
|
|
18
|
+
className={cn("animate-spin text-muted-foreground", className)}
|
|
19
19
|
style={{ width: size, height: size, ...style }}
|
|
20
20
|
{...props}
|
|
21
21
|
>
|
|
@@ -58,7 +58,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
|
58
58
|
data-slot="table-row"
|
|
59
59
|
className={cn(
|
|
60
60
|
"border-b border-border transition-colors",
|
|
61
|
-
"hover:bg-
|
|
61
|
+
"hover:bg-accent/50 dark:hover:bg-accent/30",
|
|
62
62
|
"data-[state=selected]:bg-accent",
|
|
63
63
|
className
|
|
64
64
|
)}
|
|
@@ -72,7 +72,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
|
72
72
|
<th
|
|
73
73
|
data-slot="table-head"
|
|
74
74
|
className={cn(
|
|
75
|
-
"h-12 px-4 text-left align-middle font-medium text-
|
|
75
|
+
"h-12 px-4 text-left align-middle font-medium text-muted-foreground",
|
|
76
76
|
"[&:has([role=checkbox])]:pr-0",
|
|
77
77
|
className
|
|
78
78
|
)}
|
|
@@ -86,7 +86,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
|
86
86
|
<td
|
|
87
87
|
data-slot="table-cell"
|
|
88
88
|
className={cn(
|
|
89
|
-
"px-4 py-
|
|
89
|
+
"px-4 py-3 align-middle",
|
|
90
90
|
"[&:has([role=checkbox])]:pr-0",
|
|
91
91
|
className
|
|
92
92
|
)}
|
|
@@ -48,8 +48,8 @@ function TooltipContent({
|
|
|
48
48
|
className={cn(
|
|
49
49
|
"z-50 overflow-hidden",
|
|
50
50
|
"border border-border/50",
|
|
51
|
-
"bg-
|
|
52
|
-
"
|
|
51
|
+
"bg-popover backdrop-blur-lg",
|
|
52
|
+
"border-border",
|
|
53
53
|
"px-3 py-1.5 text-xs",
|
|
54
54
|
"animate-in fade-in-0 zoom-in-95",
|
|
55
55
|
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LucideIcon, LayoutDashboard, Users,
|
|
1
|
+
import { LucideIcon, LayoutDashboard, Users, FileText } from 'lucide-react'
|
|
2
2
|
|
|
3
3
|
export interface NavItem {
|
|
4
4
|
title: string
|
|
@@ -45,16 +45,6 @@ export const sidebarNav: NavSection[] = [
|
|
|
45
45
|
},
|
|
46
46
|
],
|
|
47
47
|
},
|
|
48
|
-
{
|
|
49
|
-
title: 'Configuración',
|
|
50
|
-
items: [
|
|
51
|
-
{
|
|
52
|
-
title: 'Ajustes',
|
|
53
|
-
href: '/settings',
|
|
54
|
-
icon: Settings,
|
|
55
|
-
},
|
|
56
|
-
],
|
|
57
|
-
},
|
|
58
48
|
]
|
|
59
49
|
|
|
60
50
|
export const footerNav: NavItem[] = [
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuración centralizada de roles de usuario.
|
|
3
|
+
* Modificar aquí para agregar/quitar roles en toda la aplicación.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const USER_ROLES = ['admin', 'user', 'viewer'] as const
|
|
7
|
+
|
|
8
|
+
export type UserRole = (typeof USER_ROLES)[number]
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_ROLE: UserRole = 'user'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Labels para mostrar en UI (traducidos al español)
|
|
14
|
+
*/
|
|
15
|
+
export const ROLE_LABELS: Record<UserRole, string> = {
|
|
16
|
+
admin: 'Administrador',
|
|
17
|
+
user: 'Usuario',
|
|
18
|
+
viewer: 'Visualizador',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Opciones para selects/filtros de roles
|
|
23
|
+
*/
|
|
24
|
+
export const ROLE_OPTIONS = USER_ROLES.map((role) => ({
|
|
25
|
+
value: role,
|
|
26
|
+
label: ROLE_LABELS[role],
|
|
27
|
+
}))
|
|
@@ -20,8 +20,10 @@ dayjs.extend(isSameOrBefore)
|
|
|
20
20
|
// Set default locale to Spanish
|
|
21
21
|
dayjs.locale('es')
|
|
22
22
|
|
|
23
|
-
//
|
|
24
|
-
const
|
|
23
|
+
// Configuración de internacionalización
|
|
24
|
+
export const DEFAULT_LOCALE = 'es-AR'
|
|
25
|
+
export const DEFAULT_TIMEZONE = 'America/Argentina/Buenos_Aires'
|
|
26
|
+
export const DEFAULT_CURRENCY = 'ARS'
|
|
25
27
|
|
|
26
28
|
export function setDefaultTimezone(tz: string) {
|
|
27
29
|
dayjs.tz.setDefault(tz)
|
|
@@ -17,6 +17,13 @@ export function formatDateLong(date: DateInput): string {
|
|
|
17
17
|
return dayjs(date).format('D [de] MMMM [de] YYYY')
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Format date to short format (e.g., "15 ene")
|
|
22
|
+
*/
|
|
23
|
+
export function formatDateShort(date: DateInput): string {
|
|
24
|
+
return dayjs(date).format('D MMM')
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
/**
|
|
21
28
|
* Format time (e.g., "14:30")
|
|
22
29
|
*/
|