@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.
Files changed (49) hide show
  1. package/package.json +1 -1
  2. package/template/.claude/skills/anti-patterns.md +150 -0
  3. package/template/.claude/skills/drizzle-schema.md +178 -0
  4. package/template/.claude/skills/formatting.md +56 -0
  5. package/template/.claude/skills/module-architecture.md +143 -0
  6. package/template/.claude/skills/supabase-server-actions.md +199 -0
  7. package/template/.claude/skills/ui-patterns.md +161 -0
  8. package/template/CLAUDE.md +74 -239
  9. package/template/src/components/layout/sidebar.tsx +4 -9
  10. package/template/src/components/tables/data-table-date-filter.tsx +203 -0
  11. package/template/src/components/tables/data-table-faceted-filter.tsx +185 -0
  12. package/template/src/components/tables/data-table-filters-dropdown.tsx +130 -0
  13. package/template/src/components/tables/data-table-number-filter.tsx +295 -0
  14. package/template/src/components/tables/data-table-toolbar.tsx +115 -25
  15. package/template/src/components/tables/data-table-view-options.tsx +10 -6
  16. package/template/src/components/tables/data-table.tsx +41 -21
  17. package/template/src/components/tables/index.ts +5 -1
  18. package/template/src/components/ui/alert-dialog.tsx +1 -1
  19. package/template/src/components/ui/avatar.tsx +2 -2
  20. package/template/src/components/ui/badge.tsx +2 -2
  21. package/template/src/components/ui/button.tsx +1 -1
  22. package/template/src/components/ui/card.tsx +1 -1
  23. package/template/src/components/ui/command.tsx +4 -4
  24. package/template/src/components/ui/dropdown-menu.tsx +4 -4
  25. package/template/src/components/ui/form.tsx +1 -1
  26. package/template/src/components/ui/icons.tsx +1 -1
  27. package/template/src/components/ui/popover.tsx +1 -1
  28. package/template/src/components/ui/progress.tsx +1 -1
  29. package/template/src/components/ui/select.tsx +3 -3
  30. package/template/src/components/ui/sonner.tsx +1 -1
  31. package/template/src/components/ui/spinner.tsx +1 -1
  32. package/template/src/components/ui/table.tsx +3 -3
  33. package/template/src/components/ui/tooltip.tsx +2 -2
  34. package/template/src/config/navigation.ts +1 -11
  35. package/template/src/config/roles.ts +27 -0
  36. package/template/src/lib/date/config.ts +4 -2
  37. package/template/src/lib/date/formatters.ts +7 -0
  38. package/template/src/lib/date/index.ts +8 -1
  39. package/template/src/lib/supabase/admin.ts +23 -0
  40. package/template/src/lib/supabase/proxy.ts +1 -1
  41. package/template/src/modules/users/actions/users-actions.ts +106 -34
  42. package/template/src/modules/users/columns.tsx +29 -9
  43. package/template/src/modules/users/components/users-list.tsx +27 -1
  44. package/template/src/modules/users/hooks/useUsersMutations.ts +3 -3
  45. package/template/src/modules/users/index.ts +20 -2
  46. package/template/src/modules/users/schemas/users.schema.ts +29 -1
  47. package/template/src/modules/users/types/auth-user.types.ts +42 -0
  48. package/template/src/modules/users/utils/user-mapper.ts +32 -0
  49. 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 { DataTableViewOptions } from './data-table-view-options'
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
- return (
24
- <div className="flex items-center justify-between">
25
- <div className="flex flex-1 items-center space-x-2">
26
- {searchKey && (
27
- <Input
28
- placeholder={searchPlaceholder}
29
- value={(table.getColumn(searchKey)?.getFilterValue() as string) ?? ''}
30
- onChange={(event) =>
31
- table.getColumn(searchKey)?.setFilterValue(event.target.value)
32
- }
33
- className="h-8 w-[150px] lg:w-[250px]"
34
+ 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
- {isFiltered && (
37
- <Button
38
- variant="ghost"
39
- onClick={() => table.resetColumnFilters()}
40
- className="h-8 px-2 lg:px-3"
41
- >
42
- Limpiar
43
- <X className="ml-2 h-4 w-4" />
44
- </Button>
45
- )}
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
- <DataTableViewOptions table={table} />
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="outline"
28
- size="sm"
29
- className="ml-auto hidden h-8 lg:flex"
30
+ variant="ghost"
31
+ size="icon"
32
+ className="h-8 w-8"
30
33
  >
31
- <Settings2 className="mr-2 h-4 w-4" />
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 { Input } from '@/components/ui/input'
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
- <div className="flex items-center justify-between">
69
- {searchKey && (
70
- <Input
71
- placeholder={searchPlaceholder}
72
- value={(table.getColumn(searchKey)?.getFilterValue() as string) ?? ''}
73
- onChange={(event) =>
74
- table.getColumn(searchKey)?.setFilterValue(event.target.value)
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
- {header.isPlaceholder
90
- ? null
91
- : flexRender(
92
- header.column.columnDef.header,
93
- header.getContext()
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-[#f6f6f3]/60 dark:bg-[#0C0C0C]/80",
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-[#F2F1EF] dark:bg-[#1D1D1D]",
47
- "text-sm font-medium text-[#606060]",
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-[#F2F1EF] dark:bg-[#1D1D1D] text-[#878787] text-[10px] px-2 py-1",
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-[#F2F1EF] dark:bg-[#1D1D1D] text-[#878787] text-[12px] px-3 py-1",
23
+ "rounded-full border-0 bg-muted text-muted-foreground text-[12px] px-3 py-1",
24
24
  },
25
25
  },
26
26
  defaultVariants: {
@@ -42,7 +42,7 @@ const buttonVariants = cva(
42
42
  "hover:underline",
43
43
  ].join(" "),
44
44
  icon: [
45
- "text-[#878787]",
45
+ "text-muted-foreground",
46
46
  "hover:bg-accent hover:text-accent-foreground",
47
47
  "rounded-full",
48
48
  ].join(" "),
@@ -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-[#606060]", className)}
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-[#878787]" />
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-[#f2f2f2] dark:data-[selected=true]:bg-[#2c2c2c]",
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-[#878787]",
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-background dark:bg-[#1c1c1c]",
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-[#f2f2f2] dark:focus:bg-[#2c2c2c]",
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-[#878787]",
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",
@@ -98,7 +98,7 @@ function FormLabel({
98
98
  data-slot="form-label"
99
99
  data-error={!!error}
100
100
  className={cn(
101
- "text-xs text-[#878787] font-normal",
101
+ "text-xs text-muted-foreground font-normal",
102
102
  "data-[error=true]:text-destructive",
103
103
  className
104
104
  )}
@@ -147,7 +147,7 @@ export const Icons = {
147
147
  <SVGIcon
148
148
  size={size}
149
149
  viewBox="0 0 24 24"
150
- className={cn('animate-spin stroke-[#878787]', className)}
150
+ className={cn('animate-spin', className)}
151
151
  fill="none"
152
152
  stroke="currentColor"
153
153
  strokeWidth={1.5}
@@ -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-background dark:bg-[#1c1c1c]",
35
+ "bg-popover",
36
36
  "text-popover-foreground",
37
37
  "outline-none",
38
38
  "data-[state=open]:animate-in data-[state=closed]:animate-out",
@@ -15,7 +15,7 @@ function Progress({
15
15
  data-slot="progress"
16
16
  className={cn(
17
17
  "relative h-1.5 w-full overflow-hidden rounded-full",
18
- "bg-[#e6e6e6] dark:bg-[#2c2c2c]",
18
+ "bg-muted",
19
19
  className
20
20
  )}
21
21
  {...props}
@@ -52,7 +52,7 @@ function SelectTrigger({
52
52
  >
53
53
  {children}
54
54
  <SelectPrimitive.Icon asChild>
55
- <ChevronDownIcon className="h-4 w-4 text-[#878787]" />
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-background dark:bg-[#1c1c1c]",
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-[#f2f2f2] dark:focus:bg-[#2c2c2c]",
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-[#d3f4e5] dark:group-[.toaster]:bg-[#1a3d2e] group-[.toaster]:text-[#1a3d2e] dark:group-[.toaster]:text-[#d3f4e5] group-[.toaster]:border-[#b0e5cc] dark:group-[.toaster]:border-[#2d5a45]",
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 stroke-[#878787]", className)}
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-[#F2F1EF] dark:hover:bg-[#0f0f0f]",
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-[#666666]",
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-2 align-middle",
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-background backdrop-blur-lg",
52
- "dark:bg-[#1A1A1A]/95 dark:border-[#2C2C2C]",
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, Settings, FileText } from 'lucide-react'
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
- // Set default timezone (can be overridden)
24
- const DEFAULT_TIMEZONE = 'America/Mexico_City'
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
  */