@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
package/template/CLAUDE.md
CHANGED
|
@@ -1,279 +1,114 @@
|
|
|
1
1
|
# CLAUDE.md
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Project Overview
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Next.js application with Supabase backend, following a feature-based modular architecture with mandatory Repository Pattern.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Tech Stack
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)/
|
|
43
|
+
β βββ (auth)/ # Authenticated routes (layout with Sidebar)
|
|
134
44
|
β β βββ dashboard/
|
|
135
|
-
β β
|
|
45
|
+
β β βββ users/ # Each section: layout.tsx with SecondaryMenu
|
|
46
|
+
β β βββ settings/
|
|
136
47
|
β β βββ layout.tsx
|
|
137
|
-
β βββ (public)/
|
|
138
|
-
β β
|
|
139
|
-
β
|
|
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/
|
|
149
|
-
β βββ auth/
|
|
150
|
-
β
|
|
151
|
-
β
|
|
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/
|
|
183
|
-
β
|
|
184
|
-
β
|
|
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
|
-
βββ
|
|
203
|
-
|
|
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
|
-
β
|
|
210
|
-
β
|
|
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
|
-
β
|
|
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
|
-
##
|
|
268
|
-
|
|
269
|
-
- **
|
|
270
|
-
- **
|
|
271
|
-
- **
|
|
272
|
-
- **
|
|
273
|
-
- **
|
|
274
|
-
- **
|
|
275
|
-
- **
|
|
276
|
-
- **
|
|
277
|
-
- **
|
|
278
|
-
|
|
279
|
-
|
|
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-
|
|
50
|
+
!isActive && 'hover:bg-accent',
|
|
56
51
|
isActive && [
|
|
57
|
-
'bg-
|
|
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-
|
|
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-
|
|
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
|
+
}
|