@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,279 +1,114 @@
1
1
  # CLAUDE.md
2
2
 
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
3
+ ## Project Overview
4
4
 
5
- ## 🚨 CRITICAL: MANDATORY ARCHITECTURAL PATTERN 🚨
5
+ Next.js application with Supabase backend, following a feature-based modular architecture with mandatory Repository Pattern.
6
6
 
7
- **⚠️ BEFORE WRITING ANY CODE, YOU MUST READ AND FOLLOW THIS PATTERN ⚠️**
7
+ ## Tech Stack
8
8
 
9
- This section is **NON-NEGOTIABLE** and MUST be followed in **100% of cases** when creating or modifying features in this codebase.
10
-
11
- ## ⚠️ The Most Common Mistake (DON'T DO THIS!)
12
-
13
- ```typescript
14
- // ❌ WRONG - NEVER IMPORT MUTATIONS OR QUERIES DIRECTLY IN COMPONENTS
15
- import { useEntityMutations } from '@/modules/[entity]/hooks/useEntityMutations'
16
- import { useEntityQueries } from '@/modules/[entity]/hooks/useEntityQueries'
17
-
18
- export function MyComponent() {
19
- const { createEntity } = useEntityMutations() // ❌ ARCHITECTURAL VIOLATION
20
- const { entities } = useEntityQueries() // ❌ ARCHITECTURAL VIOLATION
21
- }
22
-
23
- // βœ… CORRECT - ALWAYS USE THE UNIFIED HOOK
24
- import { useEntity } from '@/modules/[entity]/hooks/useEntity'
25
-
26
- export function MyComponent() {
27
- const { createEntity, entities } = useEntity() // βœ… CORRECT PATTERN
28
- }
29
- ```
30
-
31
- ## 🚨 MANDATORY Pattern: Store β†’ Queries β†’ Mutations β†’ Unified Hook β†’ Component
32
-
33
- **THIS IS THE ONLY ACCEPTABLE ARCHITECTURE FOR THIS CODEBASE.**
34
-
35
- Every module MUST follow this exact structure:
36
-
37
- ```
38
- modules/
39
- └── [entity]/
40
- β”œβ”€β”€ actions/
41
- β”‚ └── [entity]-actions.ts # Server actions with RLS
42
- β”œβ”€β”€ components/
43
- β”‚ β”œβ”€β”€ [entity]-list.tsx # Component imports ONLY unified hook
44
- β”‚ β”œβ”€β”€ [entity]-form.tsx
45
- β”‚ └── [entity]-detail.tsx
46
- β”œβ”€β”€ hooks/
47
- β”‚ β”œβ”€β”€ use[Entity]Queries.ts # TanStack Query (read operations)
48
- β”‚ β”œβ”€β”€ use[Entity]Mutations.ts # TanStack Mutations (write operations)
49
- β”‚ └── use[Entity].ts # UNIFIED HOOK (components use THIS)
50
- β”œβ”€β”€ stores/
51
- β”‚ └── use[Entity]Store.ts # Zustand store (UI state only)
52
- β”œβ”€β”€ schemas/
53
- β”‚ └── [entity].schema.ts # Zod validation schemas
54
- β”œβ”€β”€ columns.tsx # TanStack Table column definitions (if needed)
55
- └── index.ts # Barrel export
56
- ```
57
-
58
- ## πŸ“‹ Mandatory Implementation Checklist
59
-
60
- Before writing ANY code, verify this checklist:
61
-
62
- - [ ] **Step 1**: Create Drizzle schema in `db/schema/[entity].ts`
63
- - [ ] **Step 2**: Create Zod schemas in `modules/[entity]/schemas/[entity].schema.ts`
64
- - [ ] **Step 3**: Create Server actions in `modules/[entity]/actions/[entity]-actions.ts`
65
- - [ ] **Step 4**: Create Zustand store with separate state/actions selectors using `useShallow` in `modules/[entity]/stores/`
66
- - [ ] **Step 5**: Create Queries hook in `modules/[entity]/hooks/use[Entity]Queries.ts`
67
- - [ ] **Step 6**: Create Mutations hook with cache invalidation in `modules/[entity]/hooks/use[Entity]Mutations.ts`
68
- - [ ] **Step 7**: Create Unified hook that combines store + queries + mutations in `modules/[entity]/hooks/use[Entity].ts`
69
- - [ ] **Step 8**: Components ONLY import the unified hook (NEVER queries/mutations directly)
70
- - [ ] **Step 9**: Test with `pnpm typecheck && pnpm lint` before committing
71
-
72
- ## πŸ”΄ NEVER DO THIS (Anti-Patterns)
73
-
74
- ```typescript
75
- // ❌ NEVER: Import queries or mutations directly in components
76
- import { useEntityQueries } from '@/modules/[entity]/hooks/useEntityQueries'
77
- import { useEntityMutations } from '@/modules/[entity]/hooks/useEntityMutations'
78
-
79
- // ❌ NEVER: Consume entire store (causes re-renders)
80
- const store = useEntityStore()
81
-
82
- // ❌ NEVER: Manual data fetching in useEffect
83
- useEffect(() => {
84
- fetch('/api/entities').then(setData)
85
- }, [])
9
+ - **Framework**: Next.js 16 (App Router)
10
+ - **State Management**: Zustand 5
11
+ - **Data Fetching**: TanStack Query 5
12
+ - **Tables**: TanStack Table 8
13
+ - **Database**: Supabase + Drizzle ORM
14
+ - **Auth**: Supabase Auth (with Admin API for user management)
15
+ - **Validation**: Zod
16
+ - **UI**: Radix UI + Tailwind CSS
17
+ - **Forms**: React Hook Form + Zod resolver
18
+ - **Excel**: xlsx (SheetJS)
19
+ - **Dates**: dayjs
20
+ - **Package Manager**: pnpm
86
21
 
87
- // ❌ NEVER: Server actions without authenticated user context (RLS won't work)
88
- export async function deleteEntity(id: string) {
89
- const supabase = createClient() // Anonymous client - RLS will fail!
90
- await supabase.from('entities').delete().eq('id', id)
91
- }
22
+ ## Commands
23
+
24
+ ```bash
25
+ pnpm dev # Start development server
26
+ pnpm build # Production build
27
+ pnpm typecheck # Run TypeScript type checking
28
+ pnpm lint # Run ESLint
29
+ pnpm db:generate # Generate Drizzle migrations after schema changes
30
+ pnpm db:migrate # Apply pending migrations
31
+ pnpm db:push # Dev only β€” sync schema directly
32
+ pnpm db:studio # Inspect data visually
33
+ pnpm db:seed # Populate database with seed data
92
34
  ```
93
35
 
94
- ## βœ… ALWAYS DO THIS (Correct Patterns)
95
-
96
- ```typescript
97
- // βœ… ALWAYS: Use the unified hook in components
98
- import { useEntity } from '@/modules/[entity]/hooks/useEntity'
99
-
100
- // βœ… ALWAYS: Use separate selectors with useShallow
101
- import { useEntityStore } from '@/modules/[entity]/stores/useEntityStore'
102
- import { useShallow } from 'zustand/react/shallow'
103
-
104
- const state = useEntityStore(useShallow((s) => s.state))
105
- const actions = useEntityStore(useShallow((s) => s.actions))
106
-
107
- // βœ… ALWAYS: Use TanStack Query for data fetching
108
- const { data } = useQuery({ queryKey: ['entities'], queryFn: fetchEntities })
109
-
110
- // βœ… ALWAYS: Use authenticated Supabase client in server actions (RLS enforced)
111
- 'use server'
112
-
113
- import { createClient } from '@/lib/supabase/server'
114
-
115
- export async function deleteEntity(id: string) {
116
- const supabase = await createClient() // Authenticated client with user context
117
- const { data: { user } } = await supabase.auth.getUser()
36
+ > **Always run `pnpm typecheck && pnpm lint` before committing.**
118
37
 
119
- if (!user) throw new Error('Unauthorized')
120
-
121
- // RLS policies will automatically enforce authorization
122
- const { error } = await supabase.from('entities').delete().eq('id', id)
123
-
124
- if (error) throw error
125
- }
126
- ```
127
-
128
- ## πŸ“ Complete Project Structure
38
+ ## Project Structure
129
39
 
130
40
  ```
131
41
  src/
132
42
  β”œβ”€β”€ app/
133
- β”‚ β”œβ”€β”€ (auth)/ # Rutas autenticadas
43
+ β”‚ β”œβ”€β”€ (auth)/ # Authenticated routes (layout with Sidebar)
134
44
  β”‚ β”‚ β”œβ”€β”€ dashboard/
135
- β”‚ β”‚ β”‚ └── page.tsx
45
+ β”‚ β”‚ β”œβ”€β”€ users/ # Each section: layout.tsx with SecondaryMenu
46
+ β”‚ β”‚ β”œβ”€β”€ settings/
136
47
  β”‚ β”‚ └── layout.tsx
137
- β”‚ β”œβ”€β”€ (public)/ # Rutas pΓΊblicas
138
- β”‚ β”‚ β”œβ”€β”€ login/
139
- β”‚ β”‚ β”‚ └── page.tsx
140
- β”‚ β”‚ └── layout.tsx
141
- β”‚ β”œβ”€β”€ api/
142
- β”‚ β”‚ └── webhooks/
143
- β”‚ β”‚ └── route.ts
48
+ β”‚ β”œβ”€β”€ (public)/ # Public routes
49
+ β”‚ β”‚ └── login/
50
+ β”‚ β”œβ”€β”€ api/webhooks/
144
51
  β”‚ β”œβ”€β”€ layout.tsx
145
52
  β”‚ β”œβ”€β”€ page.tsx
146
53
  β”‚ └── providers.tsx
147
54
  β”‚
148
- β”œβ”€β”€ modules/ # Feature-based modules
149
- β”‚ β”œβ”€β”€ auth/
150
- β”‚ β”‚ β”œβ”€β”€ actions/
151
- β”‚ β”‚ β”‚ └── auth-actions.ts
152
- β”‚ β”‚ β”œβ”€β”€ components/
153
- β”‚ β”‚ β”‚ └── login-form.tsx
154
- β”‚ β”‚ β”œβ”€β”€ hooks/
155
- β”‚ β”‚ β”‚ β”œβ”€β”€ useAuthQueries.ts
156
- β”‚ β”‚ β”‚ β”œβ”€β”€ useAuthMutations.ts
157
- β”‚ β”‚ β”‚ └── useAuth.ts
158
- β”‚ β”‚ β”œβ”€β”€ stores/
159
- β”‚ β”‚ β”‚ └── useAuthStore.ts
160
- β”‚ β”‚ β”œβ”€β”€ schemas/
161
- β”‚ β”‚ β”‚ └── auth.schema.ts
162
- β”‚ β”‚ └── index.ts
163
- β”‚ └── users/
164
- β”‚ β”œβ”€β”€ actions/
165
- β”‚ β”‚ └── users-actions.ts
166
- β”‚ β”œβ”€β”€ components/
167
- β”‚ β”‚ β”œβ”€β”€ users-list.tsx
168
- β”‚ β”‚ β”œβ”€β”€ users-form.tsx
169
- β”‚ β”‚ └── users-detail.tsx
170
- β”‚ β”œβ”€β”€ hooks/
171
- β”‚ β”‚ β”œβ”€β”€ useUsersQueries.ts
172
- β”‚ β”‚ β”œβ”€β”€ useUsersMutations.ts
173
- β”‚ β”‚ └── useUsers.ts
174
- β”‚ β”œβ”€β”€ stores/
175
- β”‚ β”‚ └── useUsersStore.ts
176
- β”‚ β”œβ”€β”€ schemas/
177
- β”‚ β”‚ └── users.schema.ts
178
- β”‚ β”œβ”€β”€ columns.tsx
179
- β”‚ └── index.ts
55
+ β”œβ”€β”€ modules/ # Feature-based modules (see skill: module-architecture)
56
+ β”‚ β”œβ”€β”€ auth/ # Auth module (login, session)
57
+ β”‚ └── users/ # Users module (Supabase Admin API)
58
+ β”‚ # New modules follow: repository β†’ actions β†’ hooks β†’ components
180
59
  β”‚
181
60
  β”œβ”€β”€ components/
182
- β”‚ β”œβ”€β”€ ui/ # Radix custom components
183
- β”‚ β”‚ β”œβ”€β”€ button.tsx
184
- β”‚ β”‚ β”œβ”€β”€ dialog.tsx
185
- β”‚ β”‚ └── index.ts
186
- β”‚ β”œβ”€β”€ tables/ # TanStack Table components
187
- β”‚ β”‚ β”œβ”€β”€ data-table.tsx
188
- β”‚ β”‚ β”œβ”€β”€ data-table-pagination.tsx
189
- β”‚ β”‚ β”œβ”€β”€ data-table-toolbar.tsx
190
- β”‚ β”‚ β”œβ”€β”€ data-table-column-header.tsx
191
- β”‚ β”‚ └── index.ts
192
- β”‚ β”œβ”€β”€ layout/
193
- β”‚ β”‚ β”œβ”€β”€ header.tsx
194
- β”‚ β”‚ └── sidebar.tsx
61
+ β”‚ β”œβ”€β”€ ui/ # Radix custom components
62
+ β”‚ β”œβ”€β”€ tables/ # TanStack Table (data-table, pagination, filters)
63
+ β”‚ β”œβ”€β”€ layout/ # header, sidebar, secondary-menu, page-header
195
64
  β”‚ └── shared/
196
- β”‚ └── ...
197
- β”‚
198
- β”œβ”€β”€ stores/ # Global Zustand stores
199
- β”‚ β”œβ”€β”€ useUiStore.ts
200
- β”‚ └── index.ts
201
65
  β”‚
202
- β”œβ”€β”€ hooks/ # Global hooks
203
- β”‚ β”œβ”€β”€ useMediaQuery.ts
204
- β”‚ β”œβ”€β”€ useDebounce.ts
205
- β”‚ └── useDataTable.ts
66
+ β”œβ”€β”€ stores/ # Global Zustand stores
67
+ β”œβ”€β”€ hooks/ # Global hooks (useMediaQuery, useDebounce, useDataTable)
206
68
  β”‚
207
69
  β”œβ”€β”€ lib/
208
- β”‚ β”œβ”€β”€ supabase/
209
- β”‚ β”‚ β”œβ”€β”€ client.ts
210
- β”‚ β”‚ β”œβ”€β”€ server.ts
211
- β”‚ β”‚ β”œβ”€β”€ proxy.ts
212
- β”‚ β”‚ └── types.ts
213
- β”‚ β”œβ”€β”€ excel/
214
- β”‚ β”‚ β”œβ”€β”€ parser.ts
215
- β”‚ β”‚ β”œβ”€β”€ exporter.ts
216
- β”‚ β”‚ └── index.ts
217
- β”‚ β”œβ”€β”€ date/
218
- β”‚ β”‚ β”œβ”€β”€ config.ts
219
- β”‚ β”‚ β”œβ”€β”€ formatters.ts
220
- β”‚ β”‚ └── index.ts
70
+ β”‚ β”œβ”€β”€ supabase/ # client.ts, server.ts, admin.ts
71
+ β”‚ β”œβ”€β”€ excel/ # parser.ts, exporter.ts
72
+ β”‚ β”œβ”€β”€ date/ # config.ts (locale/timezone/currency), formatters.ts
221
73
  β”‚ β”œβ”€β”€ validations/
222
- β”‚ β”‚ β”œβ”€β”€ common.ts
223
- β”‚ β”‚ └── index.ts
224
74
  β”‚ β”œβ”€β”€ query-client.ts
225
75
  β”‚ └── utils.ts
226
76
  β”‚
227
- β”œβ”€β”€ db/
228
- β”‚ β”œβ”€β”€ schema/
229
- β”‚ β”‚ β”œβ”€β”€ users.ts
230
- β”‚ β”‚ └── index.ts
77
+ β”œβ”€β”€ db/ # Drizzle ORM (see skill: drizzle-schema)
78
+ β”‚ β”œβ”€β”€ schema/ # Schema per entity + barrel index.ts
231
79
  β”‚ β”œβ”€β”€ migrations/
232
- β”‚ β”œβ”€β”€ index.ts
80
+ β”‚ β”œβ”€β”€ index.ts # Exports `db` (Drizzle client)
233
81
  β”‚ └── seed.ts
234
82
  β”‚
235
83
  β”œβ”€β”€ types/
236
- β”‚ β”œβ”€β”€ api.ts
237
- β”‚ β”œβ”€β”€ table.ts
238
- β”‚ └── index.ts
239
- β”‚
240
84
  β”œβ”€β”€ config/
241
85
  β”‚ β”œβ”€β”€ site.ts
242
- β”‚ └── navigation.ts
86
+ β”‚ β”œβ”€β”€ navigation.ts # Sidebar structure with children
87
+ β”‚ └── roles.ts # Single source of truth for roles
243
88
  β”‚
244
89
  └── styles/
245
90
  └── globals.css
246
-
247
- supabase/
248
- β”œβ”€β”€ functions/
249
- β”‚ β”œβ”€β”€ process-excel/
250
- β”‚ β”‚ └── index.ts
251
- β”‚ └── send-notification/
252
- β”‚ └── index.ts
253
- β”œβ”€β”€ migrations/
254
- └── config.toml
255
-
256
- # Root files
257
- β”œβ”€β”€ .env.local
258
- β”œβ”€β”€ .env.example
259
- β”œβ”€β”€ drizzle.config.ts
260
- β”œβ”€β”€ proxy.ts
261
- β”œβ”€β”€ next.config.ts
262
- β”œβ”€β”€ package.json
263
- β”œβ”€β”€ tsconfig.json
264
- └── tailwind.config.ts
265
91
  ```
266
92
 
267
- ## πŸ› οΈ Tech Stack
268
-
269
- - **Framework**: Next.js 16 (App Router)
270
- - **State Management**: Zustand 5
271
- - **Data Fetching**: TanStack Query 5
272
- - **Tables**: TanStack Table 8
273
- - **Database**: Supabase + Drizzle ORM
274
- - **Validation**: Zod
275
- - **UI**: Radix UI + Tailwind CSS
276
- - **Forms**: React Hook Form + Zod resolver
277
- - **Excel**: xlsx (SheetJS)
278
- - **Dates**: dayjs
279
- - **Package Manager**: pnpm
93
+ ## Key Conventions
94
+
95
+ - **Mandatory architecture**: `Repository β†’ Actions β†’ Queries β†’ Mutations β†’ Unified Hook β†’ Component` (see skill: module-architecture)
96
+ - **Components NEVER import queries or mutations directly** β€” they use the unified hook
97
+ - **Server actions use `requireAuth()`** and call repositories, never `db` directly (see skill: supabase-server-actions)
98
+ - **Repository Pattern is mandatory** for entities with own tables (see skill: drizzle-schema)
99
+ - **Zustand stores = UI state only**, consumed with `useShallow` selectors
100
+ - **No modals for CRUD** β€” use full pages (`/new`, `/[id]/edit`) (see skill: ui-patterns)
101
+ - **Dates via `@/lib/date`**, numbers via `DEFAULT_LOCALE` (see skill: formatting)
102
+ - **Roles centralized** in `config/roles.ts` (see skill: supabase-server-actions)
103
+ - **See skill: anti-patterns** for common mistakes to avoid
104
+
105
+ ## Skills Reference
106
+
107
+ | Skill | What it covers |
108
+ |-------|---------------|
109
+ | `module-architecture` | Module structure, layer architecture, implementation checklist, unified hook, Zustand store |
110
+ | `anti-patterns` | All prohibited patterns with wrong/correct examples |
111
+ | `supabase-server-actions` | Server actions, auth clients, Admin API, roles, mutations integration |
112
+ | `drizzle-schema` | Schema conventions, repository pattern, commands, migration workflow |
113
+ | `formatting` | dayjs date functions, number formatting, locale/currency config |
114
+ | `ui-patterns` | Design philosophy, page architecture, navigation, SecondaryMenu, spacing |
@@ -24,11 +24,6 @@ const navItems: NavItem[] = [
24
24
  name: 'Usuarios',
25
25
  icon: 'Users',
26
26
  },
27
- {
28
- path: '/settings',
29
- name: 'ConfiguraciΓ³n',
30
- icon: 'Settings',
31
- },
32
27
  ]
33
28
 
34
29
  function MenuItem({
@@ -52,9 +47,9 @@ function MenuItem({
52
47
  'mx-[10px] px-[10px]',
53
48
  'rounded-md',
54
49
  'transition-all duration-150',
55
- !isActive && 'hover:bg-[#f7f7f7] dark:hover:bg-[#1a1a1a]',
50
+ !isActive && 'hover:bg-accent',
56
51
  isActive && [
57
- 'bg-[#f7f7f7] dark:bg-[#131313]',
52
+ 'bg-accent',
58
53
  'border border-border',
59
54
  ]
60
55
  )}
@@ -64,7 +59,7 @@ function MenuItem({
64
59
  size={20}
65
60
  className={cn(
66
61
  'transition-colors',
67
- isActive ? 'text-foreground' : 'text-[#878787]'
62
+ isActive ? 'text-foreground' : 'text-muted-foreground'
68
63
  )}
69
64
  />
70
65
  </div>
@@ -73,7 +68,7 @@ function MenuItem({
73
68
  className={cn(
74
69
  'text-sm whitespace-nowrap',
75
70
  'transition-all duration-150',
76
- isActive ? 'font-medium text-foreground' : 'text-[#878787]'
71
+ isActive ? 'font-medium text-foreground' : 'text-muted-foreground'
77
72
  )}
78
73
  >
79
74
  {item.name}
@@ -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
+ }