@create-lft-app/nextjs 3.0.0 → 3.2.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 (128) hide show
  1. package/README.md +549 -0
  2. package/package.json +48 -48
  3. package/template/CLAUDE.md +1239 -279
  4. package/template/drizzle.config.ts +12 -12
  5. package/template/eslint.config.mjs +16 -16
  6. package/template/gitignore +36 -36
  7. package/template/next.config.ts +7 -7
  8. package/template/package.json +86 -86
  9. package/template/postcss.config.mjs +7 -7
  10. package/template/proxy.ts +12 -12
  11. package/template/public/logolft.svg +11 -11
  12. package/template/src/app/(auth)/dashboard/dashboard-content.tsx +124 -124
  13. package/template/src/app/(auth)/dashboard/page.tsx +9 -9
  14. package/template/src/app/(auth)/layout.tsx +7 -7
  15. package/template/src/app/(auth)/users/page.tsx +9 -9
  16. package/template/src/app/(auth)/users/users-content.tsx +26 -26
  17. package/template/src/app/(public)/layout.tsx +7 -7
  18. package/template/src/app/(public)/login/page.tsx +17 -17
  19. package/template/src/app/api/webhooks/route.ts +20 -20
  20. package/template/src/app/globals.css +249 -249
  21. package/template/src/app/layout.tsx +37 -37
  22. package/template/src/app/page.tsx +5 -5
  23. package/template/src/app/providers.tsx +27 -27
  24. package/template/src/components/layout/main-content.tsx +28 -28
  25. package/template/src/components/layout/sidebar-context.tsx +33 -33
  26. package/template/src/components/layout/sidebar.tsx +141 -146
  27. package/template/src/components/tables/data-table-column-header.tsx +68 -68
  28. package/template/src/components/tables/data-table-date-filter.tsx +203 -0
  29. package/template/src/components/tables/data-table-faceted-filter.tsx +185 -0
  30. package/template/src/components/tables/data-table-filters-dropdown.tsx +130 -0
  31. package/template/src/components/tables/data-table-number-filter.tsx +295 -0
  32. package/template/src/components/tables/data-table-pagination.tsx +99 -99
  33. package/template/src/components/tables/data-table-toolbar.tsx +140 -50
  34. package/template/src/components/tables/data-table-view-options.tsx +63 -59
  35. package/template/src/components/tables/data-table.tsx +148 -128
  36. package/template/src/components/tables/index.ts +9 -5
  37. package/template/src/components/ui/accordion.tsx +58 -58
  38. package/template/src/components/ui/alert-dialog.tsx +165 -165
  39. package/template/src/components/ui/alert.tsx +66 -66
  40. package/template/src/components/ui/animations/index.ts +44 -44
  41. package/template/src/components/ui/avatar.tsx +55 -55
  42. package/template/src/components/ui/badge.tsx +50 -50
  43. package/template/src/components/ui/button.tsx +118 -118
  44. package/template/src/components/ui/calendar.tsx +220 -220
  45. package/template/src/components/ui/card.tsx +113 -113
  46. package/template/src/components/ui/checkbox.tsx +38 -38
  47. package/template/src/components/ui/collapsible.tsx +33 -33
  48. package/template/src/components/ui/command.tsx +196 -196
  49. package/template/src/components/ui/dialog.tsx +156 -156
  50. package/template/src/components/ui/dropdown-menu.tsx +280 -280
  51. package/template/src/components/ui/form.tsx +171 -171
  52. package/template/src/components/ui/icons.tsx +167 -167
  53. package/template/src/components/ui/input.tsx +28 -28
  54. package/template/src/components/ui/label.tsx +25 -25
  55. package/template/src/components/ui/motion.tsx +197 -197
  56. package/template/src/components/ui/page-transition.tsx +166 -166
  57. package/template/src/components/ui/popover.tsx +59 -59
  58. package/template/src/components/ui/progress.tsx +32 -32
  59. package/template/src/components/ui/radio-group.tsx +45 -45
  60. package/template/src/components/ui/scroll-area.tsx +63 -63
  61. package/template/src/components/ui/select.tsx +208 -208
  62. package/template/src/components/ui/separator.tsx +28 -28
  63. package/template/src/components/ui/sheet.tsx +170 -170
  64. package/template/src/components/ui/sidebar.tsx +726 -726
  65. package/template/src/components/ui/skeleton.tsx +15 -15
  66. package/template/src/components/ui/slider.tsx +58 -58
  67. package/template/src/components/ui/sonner.tsx +47 -47
  68. package/template/src/components/ui/spinner.tsx +27 -27
  69. package/template/src/components/ui/submit-button.tsx +47 -47
  70. package/template/src/components/ui/switch.tsx +31 -31
  71. package/template/src/components/ui/table.tsx +120 -120
  72. package/template/src/components/ui/tabs.tsx +75 -75
  73. package/template/src/components/ui/textarea.tsx +26 -26
  74. package/template/src/components/ui/tooltip.tsx +70 -70
  75. package/template/src/config/navigation.ts +59 -69
  76. package/template/src/config/roles.ts +27 -0
  77. package/template/src/config/site.ts +12 -12
  78. package/template/src/db/index.ts +12 -12
  79. package/template/src/db/schema/index.ts +1 -1
  80. package/template/src/db/schema/users.ts +16 -16
  81. package/template/src/db/seed.ts +39 -39
  82. package/template/src/hooks/index.ts +3 -3
  83. package/template/src/hooks/use-mobile.ts +21 -21
  84. package/template/src/hooks/useDataTable.ts +82 -82
  85. package/template/src/hooks/useDebounce.ts +49 -49
  86. package/template/src/hooks/useMediaQuery.ts +36 -36
  87. package/template/src/lib/date/config.ts +36 -34
  88. package/template/src/lib/date/formatters.ts +127 -120
  89. package/template/src/lib/date/index.ts +26 -19
  90. package/template/src/lib/excel/exporter.ts +89 -89
  91. package/template/src/lib/excel/index.ts +14 -14
  92. package/template/src/lib/excel/parser.ts +96 -96
  93. package/template/src/lib/query-client.ts +35 -35
  94. package/template/src/lib/supabase/admin.ts +23 -0
  95. package/template/src/lib/supabase/client.ts +11 -11
  96. package/template/src/lib/supabase/proxy.ts +67 -67
  97. package/template/src/lib/supabase/server.ts +38 -38
  98. package/template/src/lib/supabase/types.ts +53 -53
  99. package/template/src/lib/utils.ts +6 -6
  100. package/template/src/lib/validations/common.ts +75 -75
  101. package/template/src/lib/validations/index.ts +20 -20
  102. package/template/src/modules/auth/actions/auth-actions.ts +59 -59
  103. package/template/src/modules/auth/components/login-form.tsx +68 -68
  104. package/template/src/modules/auth/hooks/useAuth.ts +38 -38
  105. package/template/src/modules/auth/hooks/useAuthMutations.ts +43 -43
  106. package/template/src/modules/auth/hooks/useAuthQueries.ts +43 -43
  107. package/template/src/modules/auth/index.ts +12 -12
  108. package/template/src/modules/auth/schemas/auth.schema.ts +32 -32
  109. package/template/src/modules/auth/stores/useAuthStore.ts +37 -37
  110. package/template/src/modules/users/actions/users-actions.ts +166 -94
  111. package/template/src/modules/users/columns.tsx +106 -86
  112. package/template/src/modules/users/components/users-list.tsx +48 -22
  113. package/template/src/modules/users/hooks/useUsers.ts +39 -39
  114. package/template/src/modules/users/hooks/useUsersMutations.ts +55 -55
  115. package/template/src/modules/users/hooks/useUsersQueries.ts +35 -35
  116. package/template/src/modules/users/index.ts +30 -12
  117. package/template/src/modules/users/schemas/users.schema.ts +51 -23
  118. package/template/src/modules/users/stores/useUsersStore.ts +60 -60
  119. package/template/src/modules/users/types/auth-user.types.ts +42 -0
  120. package/template/src/modules/users/utils/user-mapper.ts +32 -0
  121. package/template/src/stores/index.ts +1 -1
  122. package/template/src/stores/useUiStore.ts +55 -55
  123. package/template/src/types/api.ts +28 -28
  124. package/template/src/types/index.ts +2 -2
  125. package/template/src/types/table.ts +34 -34
  126. package/template/supabase/config.toml +94 -94
  127. package/template/tsconfig.json +42 -42
  128. package/template/tsconfig.tsbuildinfo +1 -1
@@ -1,279 +1,1239 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## 🚨 CRITICAL: MANDATORY ARCHITECTURAL PATTERN 🚨
6
-
7
- **⚠️ BEFORE WRITING ANY CODE, YOU MUST READ AND FOLLOW THIS PATTERN ⚠️**
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
- }, [])
86
-
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
- }
92
- ```
93
-
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()
118
-
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
129
-
130
- ```
131
- src/
132
- ├── app/
133
- │ ├── (auth)/ # Rutas autenticadas
134
- │ │ ├── dashboard/
135
- │ │ │ └── page.tsx
136
- │ │ └── layout.tsx
137
- │ ├── (public)/ # Rutas públicas
138
- │ │ ├── login/
139
- │ │ │ └── page.tsx
140
- │ │ └── layout.tsx
141
- │ ├── api/
142
- │ │ └── webhooks/
143
- │ │ └── route.ts
144
- │ ├── layout.tsx
145
- │ ├── page.tsx
146
- │ └── providers.tsx
147
-
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
180
-
181
- ├── 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
195
- └── shared/
196
- └── ...
197
-
198
- ├── stores/ # Global Zustand stores
199
- ├── useUiStore.ts
200
- └── index.ts
201
-
202
- ├── hooks/ # Global hooks
203
- ├── useMediaQuery.ts
204
- ├── useDebounce.ts
205
- └── useDataTable.ts
206
-
207
- ├── 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
221
- ├── validations/
222
- │ ├── common.ts
223
- │ └── index.ts
224
- ├── query-client.ts
225
- │ └── utils.ts
226
-
227
- ├── db/
228
- │ ├── schema/
229
- │ │ ├── users.ts
230
- │ └── index.ts
231
- │ ├── migrations/
232
- ├── index.ts
233
- └── seed.ts
234
-
235
- ├── types/
236
- ├── api.ts
237
- ├── table.ts
238
- └── index.ts
239
-
240
- ├── config/
241
- ├── site.ts
242
- └── navigation.ts
243
-
244
- └── styles/
245
- └── 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
- ```
266
-
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
1
+ # CLAUDE.md - Instrucciones para Claude Code
2
+
3
+ **IMPORTANTE: Este archivo contiene instrucciones OBLIGATORIAS que DEBES seguir al trabajar en este repositorio.**
4
+
5
+ ## 🚨 CRÍTICO: PATRÓN ARQUITECTÓNICO OBLIGATORIO 🚨
6
+
7
+ **⚠️ ANTES DE ESCRIBIR CUALQUIER CÓDIGO, DEBES LEER Y SEGUIR ESTE PATRÓN ⚠️**
8
+
9
+ Esta sección es **NO NEGOCIABLE** y DEBES seguirla en el **100% de los casos** al crear o modificar features en este codebase.
10
+
11
+ ## ⚠️ El Error Más Común (NO HAGAS ESTO!)
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
+ ## 🚨 PATRÓN OBLIGATORIO: Store → Queries → Mutations → Unified Hook → Component
32
+
33
+ **ESTA ES LA ÚNICA ARQUITECTURA ACEPTABLE PARA ESTE CODEBASE.**
34
+
35
+ Cada módulo DEBE seguir esta estructura exacta:
36
+
37
+ ```
38
+ modules/
39
+ └── [entity]/
40
+ ├── repositories/
41
+ │ └── [entity].repository.ts # 🚨 OBLIGATORIO: Acceso a datos con Drizzle
42
+ ├── actions/
43
+ └── [entity]-actions.ts # Server actions (usa repository, NO db directo)
44
+ ├── components/
45
+ ├── [entity]-list.tsx # Component imports ONLY unified hook
46
+ ├── [entity]-form.tsx
47
+ └── [entity]-detail.tsx
48
+ ├── hooks/
49
+ ├── use[Entity]Queries.ts # TanStack Query (read operations)
50
+ ├── use[Entity]Mutations.ts # TanStack Mutations (write operations)
51
+ │ └── use[Entity].ts # UNIFIED HOOK (components use THIS)
52
+ ├── stores/
53
+ │ └── use[Entity]Store.ts # Zustand store (UI state only)
54
+ ├── schemas/
55
+ └── [entity].schema.ts # Zod validation schemas
56
+ ├── columns.tsx # TanStack Table column definitions (if needed)
57
+ └── index.ts # Barrel export
58
+ ```
59
+
60
+ ## 📋 Checklist de Implementación OBLIGATORIO
61
+
62
+ **ANTES de escribir CUALQUIER código, DEBES verificar este checklist:**
63
+
64
+ - [ ] **Paso 1**: CREA Drizzle schema en `db/schema/[entity].ts`
65
+ - [ ] **Paso 2**: CREA Repository en `modules/[entity]/repositories/[entity].repository.ts` (ver sección DRIZZLE ORM)
66
+ - [ ] **Paso 3**: CREA Zod schemas en `modules/[entity]/schemas/[entity].schema.ts`
67
+ - [ ] **Paso 4**: CREA Server actions en `modules/[entity]/actions/[entity]-actions.ts` (DEBE usar repository, NUNCA `db` directo)
68
+ - [ ] **Paso 5**: CREA Zustand store con selectores separados usando `useShallow` en `modules/[entity]/stores/`
69
+ - [ ] **Paso 6**: CREA Queries hook en `modules/[entity]/hooks/use[Entity]Queries.ts`
70
+ - [ ] **Paso 7**: CREA Mutations hook con invalidación de cache en `modules/[entity]/hooks/use[Entity]Mutations.ts`
71
+ - [ ] **Paso 8**: CREA Unified hook que combina store + queries + mutations en `modules/[entity]/hooks/use[Entity].ts`
72
+ - [ ] **Paso 9**: Los componentes SOLO importan el unified hook (NUNCA queries/mutations directamente)
73
+ - [ ] **Paso 10**: EJECUTA `pnpm type-check && pnpm lint` antes de hacer commit
74
+
75
+ ## 🔴 NUNCA HAGAS ESTO (Anti-Patterns)
76
+
77
+ **Si detectas que estás a punto de hacer algo de esta lista, DETENTE y corrige:**
78
+
79
+ ```typescript
80
+ // NUNCA: Importar queries o mutations directamente en componentes
81
+ import { useEntityQueries } from '@/modules/[entity]/hooks/useEntityQueries'
82
+ import { useEntityMutations } from '@/modules/[entity]/hooks/useEntityMutations'
83
+
84
+ // ❌ NUNCA: Consumir el store completo (causa re-renders)
85
+ const store = useEntityStore()
86
+
87
+ // ❌ NUNCA: Fetch manual de datos en useEffect
88
+ useEffect(() => {
89
+ fetch('/api/entities').then(setData)
90
+ }, [])
91
+
92
+ // ❌ NUNCA: Server actions sin contexto de usuario autenticado (RLS no funcionará)
93
+ export async function deleteEntity(id: string) {
94
+ const supabase = createClient() // Cliente anónimo - RLS fallará!
95
+ await supabase.from('entities').delete().eq('id', id)
96
+ }
97
+
98
+ // NUNCA: Usar Drizzle db directamente en Server Actions (sin Repository)
99
+ 'use server'
100
+ export async function getProducts() {
101
+ return await db.select().from(products) // VIOLACIÓN: DEBE usar repository
102
+ }
103
+ ```
104
+
105
+ ## SIEMPRE HAZ ESTO (Patrones Correctos)
106
+
107
+ **Cuando escribas código, SIEMPRE sigue estos patrones:**
108
+
109
+ ```typescript
110
+ // ✅ SIEMPRE: USA el unified hook en componentes
111
+ import { useEntity } from '@/modules/[entity]/hooks/useEntity'
112
+
113
+ // SIEMPRE: USA selectores separados con useShallow
114
+ import { useEntityStore } from '@/modules/[entity]/stores/useEntityStore'
115
+ import { useShallow } from 'zustand/react/shallow'
116
+
117
+ const state = useEntityStore(useShallow((s) => s.state))
118
+ const actions = useEntityStore(useShallow((s) => s.actions))
119
+
120
+ // ✅ SIEMPRE: USA TanStack Query para fetching de datos
121
+ const { data } = useQuery({ queryKey: ['entities'], queryFn: fetchEntities })
122
+
123
+ // ✅ SIEMPRE: USA cliente Supabase autenticado en server actions (RLS se aplica)
124
+ 'use server'
125
+
126
+ import { createClient } from '@/lib/supabase/server'
127
+
128
+ export async function deleteEntity(id: string) {
129
+ const supabase = await createClient() // Cliente autenticado con contexto de usuario
130
+ const { data: { user } } = await supabase.auth.getUser()
131
+
132
+ if (!user) throw new Error('Unauthorized')
133
+
134
+ // Las políticas RLS se aplican automáticamente
135
+ const { error } = await supabase.from('entities').delete().eq('id', id)
136
+
137
+ if (error) throw error
138
+ }
139
+
140
+ // ✅ SIEMPRE: USA Repository Pattern para acceso a datos con Drizzle
141
+ // 1. CREA el repository
142
+ // modules/products/repositories/products.repository.ts
143
+ export const productRepository = {
144
+ findAll: () => db.select().from(products),
145
+ create: (data) => db.insert(products).values(data).returning(),
146
+ }
147
+
148
+ // 2. Las Server Actions USAN el repository
149
+ 'use server'
150
+ export async function getProducts() {
151
+ await requireAuth()
152
+ return productRepository.findAll() // ✅ CORRECTO: usa repository
153
+ }
154
+ ```
155
+
156
+ ## 📁 Estructura Completa del Proyecto
157
+
158
+ ```
159
+ src/
160
+ ├── app/
161
+ ├── (auth)/ # Rutas autenticadas
162
+ │ │ ├── dashboard/
163
+ │ │ └── page.tsx
164
+ ├── users/
165
+ │ ├── layout.tsx # SecondaryMenu para Users
166
+ │ │ ├── page.tsx # Lista de usuarios
167
+ ├── new/
168
+ │ │ └── page.tsx # Crear usuario (página completa)
169
+ └── [id]/
170
+ │ │ ├── page.tsx # Detalle usuario
171
+ │ └── edit/
172
+ │ └── page.tsx # Editar usuario (página completa)
173
+ ├── settings/
174
+ │ │ ├── layout.tsx # SecondaryMenu para Settings
175
+ └── page.tsx
176
+ │ └── layout.tsx
177
+ ├── (public)/ # Rutas públicas
178
+ ├── login/
179
+ │ │ └── page.tsx
180
+ │ └── layout.tsx
181
+ ├── api/
182
+ │ └── webhooks/
183
+ │ │ └── route.ts
184
+ │ ├── layout.tsx
185
+ ├── page.tsx
186
+ └── providers.tsx
187
+
188
+ ├── modules/ # Feature-based modules
189
+ │ ├── auth/
190
+ │ │ ├── actions/
191
+ │ │ └── auth-actions.ts
192
+ ├── components/
193
+ │ │ │ └── login-form.tsx
194
+ │ │ ├── hooks/
195
+ │ │ ├── useAuthQueries.ts
196
+ │ │ ├── useAuthMutations.ts
197
+ │ │ └── useAuth.ts # 🚨 UNIFIED HOOK - componentes usan ESTE
198
+ │ │ ├── stores/
199
+ │ │ └── useAuthStore.ts
200
+ │ ├── schemas/
201
+ │ │ └── auth.schema.ts
202
+ │ │ └── index.ts
203
+
204
+ └── users/ # Ejemplo de módulo con auth.users (Supabase Admin)
205
+ ├── actions/
206
+ │ └── users-actions.ts # USA createAdminClient() para auth.users
207
+ ├── components/
208
+ ├── users-list.tsx # SOLO importa useUsers (unified hook)
209
+ │ ├── users-form.tsx
210
+ └── users-detail.tsx
211
+ ├── hooks/
212
+ ├── useUsersQueries.ts
213
+ ├── useUsersMutations.ts
214
+ └── useUsers.ts # 🚨 UNIFIED HOOK - componentes usan ESTE
215
+ ├── stores/
216
+ │ └── useUsersStore.ts
217
+ ├── schemas/
218
+ └── users.schema.ts # Importa roles desde @/config/roles
219
+ ├── utils/
220
+ │ └── user-mapper.ts # Mapper: auth.users → User type
221
+ ├── columns.tsx # TanStack Table columns
222
+ └── index.ts
223
+
224
+ # 📋 TEMPLATE para módulos con tablas propias (Repository Pattern):
225
+ # └── [entity]/
226
+ # ├── repositories/
227
+ │ # │ └── [entity].repository.ts # 🚨 OBLIGATORIO: Acceso a datos con Drizzle
228
+ # ├── actions/
229
+ # └── [entity]-actions.ts # USA repository, NUNCA db directo
230
+ # ├── components/
231
+ # ├── hooks/
232
+ # │ └── use[Entity].ts # UNIFIED HOOK
233
+ # ├── stores/
234
+ # ├── schemas/
235
+ │ # └── index.ts
236
+
237
+ ├── components/
238
+ ├── ui/ # Radix custom components
239
+ │ ├── button.tsx
240
+ │ │ ├── dialog.tsx
241
+ │ └── index.ts
242
+ ├── tables/ # TanStack Table components
243
+ │ ├── data-table.tsx
244
+ │ │ ├── data-table-pagination.tsx
245
+ │ │ ├── data-table-toolbar.tsx
246
+ │ │ ├── data-table-column-header.tsx
247
+ │ │ ├── data-table-faceted-filter.tsx
248
+ │ │ ├── data-table-date-filter.tsx
249
+ ├── data-table-number-filter.tsx # Filtro de rango numérico (documentado)
250
+ │ │ └── index.ts
251
+ ├── layout/
252
+ │ ├── header.tsx
253
+ │ │ ├── sidebar.tsx
254
+ │ │ ├── secondary-menu.tsx # Tabs horizontales para subpáginas
255
+ │ │ └── page-header.tsx # Header estándar para páginas
256
+ │ └── shared/
257
+ │ └── ...
258
+
259
+ ├── stores/ # Global Zustand stores
260
+ ├── useUiStore.ts
261
+ │ └── index.ts
262
+
263
+ ├── hooks/ # Global hooks
264
+ │ ├── useMediaQuery.ts
265
+ │ ├── useDebounce.ts
266
+ │ └── useDataTable.ts
267
+
268
+ ├── lib/
269
+ │ ├── supabase/
270
+ │ │ ├── client.ts # Cliente browser (anon key) - para UI
271
+ │ │ ├── server.ts # Cliente server (anon key + cookies) - para auth
272
+ │ │ ├── admin.ts # 🚨 Cliente admin (secret key) - para gestionar usuarios
273
+ │ │ ├── proxy.ts
274
+ │ │ └── types.ts
275
+ │ ├── excel/
276
+ │ │ ├── parser.ts
277
+ │ │ ├── exporter.ts
278
+ │ │ └── index.ts
279
+ │ ├── date/
280
+ │ │ ├── config.ts
281
+ │ │ ├── formatters.ts
282
+ │ │ └── index.ts
283
+ │ ├── validations/
284
+ │ │ ├── common.ts
285
+ │ │ └── index.ts
286
+ │ ├── query-client.ts
287
+ │ └── utils.ts
288
+
289
+ ├── db/ # Drizzle ORM
290
+ │ ├── schema/
291
+ │ │ ├── [entity].ts # Schema por entidad
292
+ │ │ └── index.ts # Barrel export - SIEMPRE exportar aquí
293
+ │ ├── migrations/ # Migraciones generadas con db:generate
294
+ │ ├── index.ts # Exporta `db` (cliente Drizzle)
295
+ │ └── seed.ts # Script de seed
296
+
297
+ ├── types/
298
+ │ ├── api.ts
299
+ │ ├── table.ts
300
+ │ └── index.ts
301
+
302
+ ├── config/
303
+ │ ├── site.ts
304
+ │ ├── navigation.ts # Estructura del sidebar con children
305
+ │ └── roles.ts # 🚨 ÚNICA fuente de verdad para roles
306
+
307
+ └── styles/
308
+ └── globals.css
309
+
310
+ supabase/
311
+ ├── functions/
312
+ │ ├── process-excel/
313
+ │ │ └── index.ts
314
+ │ └── send-notification/
315
+ │ └── index.ts
316
+ ├── migrations/
317
+ └── config.toml
318
+
319
+ # Root files
320
+ ├── .env.local
321
+ ├── .env.example # Incluye SUPABASE_SECRET_DEFAULT_KEY
322
+ ├── drizzle.config.ts
323
+ ├── proxy.ts
324
+ ├── next.config.ts
325
+ ├── package.json
326
+ ├── tsconfig.json
327
+ └── tailwind.config.ts
328
+ ```
329
+
330
+ ## 🛠️ Tech Stack
331
+
332
+ - **Framework**: Next.js 16 (App Router)
333
+ - **State Management**: Zustand 5
334
+ - **Data Fetching**: TanStack Query 5
335
+ - **Tables**: TanStack Table 8
336
+ - **Database**: Supabase + Drizzle ORM
337
+ - **Auth**: Supabase Auth (con Admin API para gestión de usuarios)
338
+ - **Validation**: Zod
339
+ - **UI**: Radix UI + Tailwind CSS
340
+ - **Forms**: React Hook Form + Zod resolver
341
+ - **Excel**: xlsx (SheetJS)
342
+ - **Dates**: dayjs
343
+ - **Package Manager**: pnpm
344
+
345
+ ---
346
+
347
+ ## 📅 FORMATEO DE FECHAS Y NÚMEROS
348
+
349
+ ### 🚨 OBLIGATORIO: USA dayjs para fechas
350
+
351
+ **NUNCA uses `new Date().toLocaleDateString()` o `toLocaleString()` para fechas. SIEMPRE usa las funciones de `@/lib/date`.**
352
+
353
+ ```typescript
354
+ // ❌ NUNCA hacer esto
355
+ const date = new Date(value)
356
+ return date.toLocaleDateString('es-ES')
357
+
358
+ // ✅ SIEMPRE hacer esto
359
+ import { formatDate, formatDateShort, formatDateTime } from '@/lib/date'
360
+ return formatDate(value) // "15/01/2024"
361
+ ```
362
+
363
+ ### Funciones disponibles en `@/lib/date`
364
+
365
+ | Función | Ejemplo de salida | Uso |
366
+ |---------|-------------------|-----|
367
+ | `formatDate(date)` | `"15/01/2024"` | Fecha estándar |
368
+ | `formatDateShort(date)` | `"15 ene"` | Filtros, chips |
369
+ | `formatDateLong(date)` | `"15 de enero de 2024"` | Headers, títulos |
370
+ | `formatTime(date)` | `"14:30"` | Solo hora |
371
+ | `formatDateTime(date)` | `"15/01/2024 14:30"` | Fecha y hora |
372
+ | `formatRelative(date)` | `"hace 2 horas"` | Tiempo relativo |
373
+
374
+ ### Formateo de Números
375
+
376
+ **USA `DEFAULT_LOCALE` de `@/lib/date` para formateo de números:**
377
+
378
+ ```typescript
379
+ // ❌ NUNCA hardcodear el locale
380
+ num.toLocaleString('es-AR')
381
+
382
+ // ✅ SIEMPRE usar la constante
383
+ import { DEFAULT_LOCALE, DEFAULT_CURRENCY } from '@/lib/date'
384
+ num.toLocaleString(DEFAULT_LOCALE)
385
+
386
+ // Para moneda
387
+ new Intl.NumberFormat(DEFAULT_LOCALE, {
388
+ style: 'currency',
389
+ currency: DEFAULT_CURRENCY,
390
+ }).format(amount)
391
+ ```
392
+
393
+ ### Configuración
394
+
395
+ La configuración de internacionalización está centralizada en `lib/date/config.ts`:
396
+
397
+ ```typescript
398
+ // lib/date/config.ts
399
+ export const DEFAULT_LOCALE = 'es-AR'
400
+ export const DEFAULT_TIMEZONE = 'America/Argentina/Buenos_Aires'
401
+ export const DEFAULT_CURRENCY = 'ARS'
402
+ ```
403
+
404
+ Cambiar estos valores afecta todo el proyecto (fechas y números).
405
+
406
+ ---
407
+
408
+ ## 🔐 SUPABASE AUTH ADMIN API
409
+
410
+ ### Arquitectura de Usuarios
411
+
412
+ Los usuarios se gestionan desde `auth.users` de Supabase (NO desde una tabla `public.users`):
413
+
414
+ ```
415
+ auth.users (Supabase Auth)
416
+ ├── id: uuid
417
+ ├── email: string
418
+ ├── app_metadata: { role: UserRole } ← ROL aquí (definido en config/roles.ts)
419
+ ├── user_metadata: { name, avatar_url } ← Datos extra aquí
420
+ ├── created_at: timestamp
421
+ └── updated_at: timestamp
422
+ ```
423
+
424
+ ### Configuración de Roles
425
+
426
+ Los roles están centralizados en `config/roles.ts`. Para agregar/modificar roles, solo editar este archivo:
427
+
428
+ ```typescript
429
+ // config/roles.ts
430
+ export const USER_ROLES = ['admin', 'user', 'viewer'] as const
431
+ export type UserRole = (typeof USER_ROLES)[number]
432
+ export const DEFAULT_ROLE: UserRole = 'user'
433
+
434
+ export const ROLE_LABELS: Record<UserRole, string> = {
435
+ admin: 'Administrador',
436
+ user: 'Usuario',
437
+ viewer: 'Visualizador',
438
+ }
439
+
440
+ export const ROLE_OPTIONS = USER_ROLES.map((role) => ({
441
+ value: role,
442
+ label: ROLE_LABELS[role],
443
+ }))
444
+ ```
445
+
446
+ **Uso en otros archivos:**
447
+ - Schemas Zod: `import { USER_ROLES, DEFAULT_ROLE } from '@/config/roles'`
448
+ - Columns/UI: `import { ROLE_LABELS, ROLE_OPTIONS } from '@/config/roles'`
449
+ - Types: `import type { UserRole } from '@/config/roles'`
450
+
451
+ ### Sistema sin Roles (Opcional)
452
+
453
+ Si el sistema NO necesita roles, puedes simplificarlos o eliminarlos completamente:
454
+
455
+ **Opción 1: Rol único (recomendado)**
456
+ ```typescript
457
+ // config/roles.ts - Simplificado
458
+ export const USER_ROLES = ['user'] as const
459
+ export type UserRole = (typeof USER_ROLES)[number]
460
+ export const DEFAULT_ROLE: UserRole = 'user'
461
+ ```
462
+
463
+ **Opción 2: Eliminar roles completamente**
464
+
465
+ 1. **Eliminar** `config/roles.ts`
466
+ 2. **En schemas Zod**: Quitar el campo `role` del schema
467
+ 3. **En columns de tabla**: Quitar la columna de rol
468
+ 4. **En formularios**: Quitar el campo de selección de rol
469
+ 5. **En Supabase**: Si usas `user_metadata.role`, simplemente no lo setees
470
+
471
+ **Archivos a modificar si eliminas roles:**
472
+ - `modules/users/schemas/users.schema.ts` → Quitar campo `role`
473
+ - `modules/users/columns.tsx` → Quitar columna de rol
474
+ - `modules/users/components/user-form.tsx` → Quitar selector de rol
475
+ - Filtros de tabla → Quitar filtro por rol
476
+
477
+ **IMPORTANTE:** Si NO necesitas roles, simplemente ignóralos. El sistema funciona sin ellos - solo no incluyas el campo `role` en tus schemas y formularios.
478
+
479
+ ### Clientes de Supabase
480
+
481
+ ```
482
+ lib/supabase/
483
+ ├── client.ts → Cliente browser (anon key) - para UI
484
+ ├── server.ts → Cliente server (anon key + cookies) - para verificar auth
485
+ └── admin.ts → Cliente admin (secret key) - para gestionar usuarios
486
+ ```
487
+
488
+ ### Cuándo usar cada cliente
489
+
490
+ | Operación | Cliente | Razón |
491
+ |-----------|---------|-------|
492
+ | Login/Logout | `server.ts` | Necesita cookies |
493
+ | Verificar sesión | `server.ts` | Necesita cookies |
494
+ | **Crear usuario** | `admin.ts` | Requiere privilegios admin |
495
+ | **Listar usuarios** | `admin.ts` | Requiere privilegios admin |
496
+ | **Actualizar usuario** | `admin.ts` | Requiere privilegios admin |
497
+ | **Eliminar usuario** | `admin.ts` | Requiere privilegios admin |
498
+ | Queries a tablas propias | `server.ts` | RLS basado en usuario |
499
+
500
+ ### Patrón para Server Actions con Admin
501
+
502
+ ```typescript
503
+ 'use server'
504
+
505
+ import { createClient } from '@/lib/supabase/server'
506
+ import { createAdminClient } from '@/lib/supabase/admin'
507
+
508
+ export async function createUser(input: CreateAuthUserInput) {
509
+ // 1. Verificar que el usuario actual está autenticado
510
+ const supabase = await createClient()
511
+ const { data: { user } } = await supabase.auth.getUser()
512
+ if (!user) throw new Error('Unauthorized')
513
+
514
+ // 2. Usar cliente admin para operaciones privilegiadas
515
+ const adminClient = createAdminClient()
516
+ const { data, error } = await adminClient.auth.admin.createUser({
517
+ email: input.email,
518
+ app_metadata: { role: input.role },
519
+ user_metadata: { name: input.name },
520
+ })
521
+
522
+ if (error) throw error
523
+ return mapAuthUserToUser(data.user)
524
+ }
525
+ ```
526
+
527
+ ### Variables de Entorno Requeridas
528
+
529
+ ```env
530
+ # .env.local
531
+ NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
532
+ NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=sb_publishable_...
533
+ SUPABASE_SECRET_DEFAULT_KEY=sb_secret_... # ← Requerido para admin
534
+ ```
535
+
536
+ ### Mapper: auth.users → User
537
+
538
+ El módulo `users` incluye un mapper para convertir la estructura de Supabase Auth al tipo `User` de la app:
539
+
540
+ ```typescript
541
+ // modules/users/utils/user-mapper.ts
542
+ export function mapAuthUserToUser(authUser: SupabaseAuthUser): User {
543
+ return {
544
+ id: authUser.id,
545
+ email: authUser.email ?? '',
546
+ name: authUser.user_metadata?.name ?? '',
547
+ role: authUser.app_metadata?.role ?? 'user',
548
+ avatar_url: authUser.user_metadata?.avatar_url ?? null,
549
+ created_at: authUser.created_at,
550
+ updated_at: authUser.updated_at,
551
+ }
552
+ }
553
+ ```
554
+
555
+ ---
556
+
557
+ ## 🗃️ DRIZZLE ORM
558
+
559
+ **Cuando trabajes con base de datos, DEBES usar Drizzle ORM siguiendo estas instrucciones:**
560
+
561
+ ### Estructura de Archivos (RESPÉTALA)
562
+
563
+ ```
564
+ src/db/
565
+ ├── index.ts # Cliente Drizzle (exporta `db`)
566
+ ├── schema/
567
+ │ ├── index.ts # Barrel export de todos los schemas
568
+ │ └── [entity].ts # Schema por entidad
569
+ ├── migrations/ # Migraciones generadas automáticamente
570
+ └── seed.ts # Script de seed
571
+ ```
572
+
573
+ ### Configuración
574
+
575
+ ```typescript
576
+ // drizzle.config.ts
577
+ import { defineConfig } from 'drizzle-kit'
578
+
579
+ export default defineConfig({
580
+ schema: './src/db/schema/index.ts',
581
+ out: './src/db/migrations',
582
+ dialect: 'postgresql',
583
+ dbCredentials: {
584
+ url: process.env.DATABASE_URL!,
585
+ },
586
+ })
587
+ ```
588
+
589
+ ### Cliente Drizzle
590
+
591
+ ```typescript
592
+ // src/db/index.ts
593
+ import { drizzle } from 'drizzle-orm/postgres-js'
594
+ import postgres from 'postgres'
595
+ import * as schema from './schema'
596
+
597
+ const connectionString = process.env.DATABASE_URL!
598
+ const client = postgres(connectionString, { prepare: false })
599
+
600
+ export const db = drizzle(client, { schema })
601
+ export type Database = typeof db
602
+ ```
603
+
604
+ ### Definir Schema por Entidad
605
+
606
+ ```typescript
607
+ // src/db/schema/users.ts
608
+ import { pgTable, uuid, varchar, text, timestamp, pgEnum } from 'drizzle-orm/pg-core'
609
+
610
+ export const userRoleEnum = pgEnum('user_role', ['admin', 'user', 'viewer'])
611
+
612
+ export const users = pgTable('users', {
613
+ id: uuid('id').primaryKey().defaultRandom(),
614
+ email: varchar('email', { length: 255 }).notNull().unique(),
615
+ name: varchar('name', { length: 255 }).notNull(),
616
+ role: userRoleEnum('role').default('user').notNull(),
617
+ avatarUrl: text('avatar_url'),
618
+ createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
619
+ updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
620
+ })
621
+
622
+ // Tipos inferidos automáticamente
623
+ export type User = typeof users.$inferSelect
624
+ export type NewUser = typeof users.$inferInsert
625
+ ```
626
+
627
+ **IMPORTANTE: SIEMPRE exporta cada schema en `src/db/schema/index.ts`:**
628
+
629
+ ```typescript
630
+ // src/db/schema/index.ts
631
+ export * from './users'
632
+ export * from './products' // Agregar nuevas entidades aquí
633
+ ```
634
+
635
+ ### Comandos de Drizzle (USA ESTOS)
636
+
637
+ | Comando | Cuándo USARLO |
638
+ |---------|---------------|
639
+ | `pnpm db:generate` | EJECUTA después de modificar schemas para generar migraciones |
640
+ | `pnpm db:migrate` | EJECUTA para aplicar migraciones pendientes |
641
+ | `pnpm db:push` | USA solo en desarrollo para sincronizar schema directamente |
642
+ | `pnpm db:studio` | USA para inspeccionar datos visualmente |
643
+ | `pnpm db:seed` | EJECUTA para poblar la base de datos con datos iniciales |
644
+
645
+ ### Workflow para Cambios de Schema (SIGUE ESTOS PASOS)
646
+
647
+ Cuando necesites modificar la base de datos, SIGUE este workflow:
648
+
649
+ 1. MODIFICA o CREA el schema en `src/db/schema/[entity].ts`
650
+ 2. EXPORTA en `src/db/schema/index.ts`
651
+ 3. EJECUTA: `pnpm db:generate`
652
+ 4. REVISA la migración generada en `src/db/migrations/`
653
+ 5. EJECUTA: `pnpm db:migrate`
654
+
655
+ ### 🚨 OBLIGATORIO: Patrón Repository por Entidad
656
+
657
+ **⚠️ CUANDO CREES UNA ENTIDAD que use tablas propias (no `auth.users`), DEBES implementar el Repository Pattern.**
658
+
659
+ El **Repository Pattern** abstrae la lógica de acceso a datos. DEBES usarlo porque:
660
+ - Separa lógica de negocio de los detalles de persistencia
661
+ - Permite crear mocks/fakes para tests sin conexión a DB
662
+ - Centraliza lógica de datos, reduce duplicación
663
+ - Oculta detalles de implementación (Drizzle, Supabase, etc.)
664
+
665
+ #### ❌ NUNCA HAGAS ESTO (Anti-Patterns)
666
+
667
+ ```typescript
668
+ // ❌ NUNCA: Queries de Drizzle directamente en Server Actions
669
+ 'use server'
670
+ export async function getProducts() {
671
+ return await db.select().from(products) // ❌ VIOLACIÓN ARQUITECTÓNICA
672
+ }
673
+
674
+ // ❌ NUNCA: Queries de Drizzle en componentes o hooks
675
+ function useProducts() {
676
+ const products = await db.select().from(products) // ❌ PROHIBIDO
677
+ }
678
+
679
+ // ❌ NUNCA: Lógica de acceso a datos dispersa en múltiples archivos
680
+ // archivo1.ts: db.select().from(products).where(...)
681
+ // archivo2.ts: db.select().from(products).where(...) // Duplicación!
682
+ ```
683
+
684
+ #### ✅ SIEMPRE HAZ ESTO (Patrón Correcto)
685
+
686
+ ```typescript
687
+ // ✅ CORRECTO: CREA un Repository que centraliza TODO el acceso a datos
688
+ // modules/products/repositories/products.repository.ts
689
+ export const productRepository = {
690
+ findAll: () => db.select().from(products),
691
+ findById: (id) => db.select().from(products).where(eq(products.id, id)),
692
+ // ... todas las queries aquí
693
+ }
694
+
695
+ // ✅ CORRECTO: Las Server Actions USAN el repository
696
+ 'use server'
697
+ export async function getProducts() {
698
+ await requireAuth()
699
+ return productRepository.findAll() // ✅ Usa repository
700
+ }
701
+ ```
702
+
703
+ #### Estructura por Capas (ENTIÉNDELA)
704
+
705
+ ```
706
+ UI/Presentation Layer
707
+ ↓ (llama hooks)
708
+ Service/Business Layer (hooks: useEntity.ts)
709
+ ↓ (llama repositories)
710
+ Data Access Layer (repositories: [entity].repository.ts)
711
+ ↓ (usa Drizzle)
712
+ Data Source (Supabase/PostgreSQL)
713
+ ```
714
+
715
+ #### Estructura de Archivos
716
+
717
+ ```
718
+ modules/[entity]/
719
+ ├── repositories/
720
+ │ └── [entity].repository.ts ← Repository (acceso a datos)
721
+ ├── actions/
722
+ │ └── [entity]-actions.ts ← Server actions (usa repository)
723
+ ├── schemas/
724
+ │ └── [entity].schema.ts ← Zod schemas (validación)
725
+ ├── hooks/
726
+ │ └── use[Entity].ts ← Hook unificado (usa actions)
727
+ └── ...
728
+
729
+ db/schema/
730
+ └── [entity].ts ← Drizzle schema (tabla)
731
+ ```
732
+
733
+ #### Implementación del Repository (COPIA ESTE PATRÓN)
734
+
735
+ **Cuando crees una nueva entidad, USA este template como base:**
736
+
737
+ ```typescript
738
+ // modules/products/repositories/products.repository.ts
739
+ import { db } from '@/db'
740
+ import { products, type Product, type NewProduct } from '@/db/schema'
741
+ import { eq } from 'drizzle-orm'
742
+
743
+ export interface ProductRepository {
744
+ findAll(): Promise<Product[]>
745
+ findById(id: string): Promise<Product | null>
746
+ create(data: NewProduct): Promise<Product>
747
+ update(id: string, data: Partial<NewProduct>): Promise<Product>
748
+ delete(id: string): Promise<void>
749
+ }
750
+
751
+ export const productRepository: ProductRepository = {
752
+ async findAll() {
753
+ return await db.select().from(products)
754
+ },
755
+
756
+ async findById(id: string) {
757
+ const [product] = await db
758
+ .select()
759
+ .from(products)
760
+ .where(eq(products.id, id))
761
+ return product ?? null
762
+ },
763
+
764
+ async create(data: NewProduct) {
765
+ const [product] = await db.insert(products).values(data).returning()
766
+ return product
767
+ },
768
+
769
+ async update(id: string, data: Partial<NewProduct>) {
770
+ const [product] = await db
771
+ .update(products)
772
+ .set({ ...data, updatedAt: new Date() })
773
+ .where(eq(products.id, id))
774
+ .returning()
775
+ return product
776
+ },
777
+
778
+ async delete(id: string) {
779
+ await db.delete(products).where(eq(products.id, id))
780
+ },
781
+ }
782
+ ```
783
+
784
+ #### Server Actions usando Repository (USA ESTE PATRÓN)
785
+
786
+ **Las Server Actions SIEMPRE deben importar y usar el repository, NUNCA `db` directo:**
787
+
788
+ ```typescript
789
+ // modules/products/actions/products-actions.ts
790
+ 'use server'
791
+
792
+ import { createClient } from '@/lib/supabase/server'
793
+ import { productRepository } from '../repositories/products.repository'
794
+ import type { NewProduct } from '@/db/schema'
795
+
796
+ // SIEMPRE verifica autenticación primero
797
+ async function requireAuth() {
798
+ const supabase = await createClient()
799
+ const { data: { user } } = await supabase.auth.getUser()
800
+ if (!user) throw new Error('Unauthorized')
801
+ return user
802
+ }
803
+
804
+ export async function getProducts() {
805
+ await requireAuth()
806
+ return productRepository.findAll() // ✅ USA repository
807
+ }
808
+
809
+ export async function getProductById(id: string) {
810
+ await requireAuth()
811
+ return productRepository.findById(id) // ✅ USA repository
812
+ }
813
+
814
+ export async function createProduct(input: NewProduct) {
815
+ await requireAuth()
816
+ return productRepository.create(input) // ✅ USA repository
817
+ }
818
+
819
+ export async function updateProduct(id: string, input: Partial<NewProduct>) {
820
+ await requireAuth()
821
+ return productRepository.update(id, input) // ✅ USA repository
822
+ }
823
+
824
+ export async function deleteProduct(id: string) {
825
+ await requireAuth()
826
+ return productRepository.delete(id) // ✅ USA repository
827
+ }
828
+ ```
829
+
830
+ #### 📋 Checklist para Nueva Entidad con Repository
831
+
832
+ **SIGUE estos pasos EN ORDEN cuando crees una nueva entidad:**
833
+
834
+ - [ ] **Paso 1**: CREA Drizzle schema en `db/schema/[entity].ts`
835
+ - [ ] **Paso 2**: EXPORTA en `db/schema/index.ts`
836
+ - [ ] **Paso 3**: CREA repository en `modules/[entity]/repositories/[entity].repository.ts`
837
+ - [ ] DEFINE interface con métodos CRUD
838
+ - [ ] IMPLEMENTA objeto que usa `db` de Drizzle
839
+ - [ ] **Paso 4**: CREA Zod schemas en `modules/[entity]/schemas/[entity].schema.ts`
840
+ - [ ] **Paso 5**: CREA Server Actions en `modules/[entity]/actions/[entity]-actions.ts`
841
+ - [ ] IMPORTA y USA el repository (NUNCA `db` directamente)
842
+ - [ ] VERIFICA autenticación con `requireAuth()`
843
+ - [ ] **Paso 6**: CREA hooks (queries, mutations, unified)
844
+ - [ ] **Paso 7**: Los componentes SOLO usan el hook unificado
845
+
846
+ **🚨 REGLA DE ORO**: Si ves `db.select()`, `db.insert()`, `db.update()`, o `db.delete()` fuera de un archivo `.repository.ts`, es una **VIOLACIÓN ARQUITECTÓNICA**. DEBES corregirlo moviendo la query al repository.
847
+
848
+ ### Seed de Datos
849
+
850
+ ```typescript
851
+ // src/db/seed.ts
852
+ import { db } from './index'
853
+ import { users } from './schema'
854
+
855
+ async function seed() {
856
+ console.log('🌱 Seeding database...')
857
+
858
+ await db.delete(users) // Limpiar datos existentes
859
+
860
+ await db.insert(users).values([
861
+ { email: 'admin@example.com', name: 'Admin', role: 'admin' },
862
+ { email: 'user@example.com', name: 'User', role: 'user' },
863
+ ])
864
+
865
+ console.log('✅ Database seeded!')
866
+ }
867
+
868
+ seed().catch(console.error).finally(() => process.exit())
869
+ ```
870
+
871
+ ### Cuándo usar Drizzle vs Supabase Admin (DECIDE CORRECTAMENTE)
872
+
873
+ **USA esta tabla para decidir qué método usar:**
874
+
875
+ | Entidad | DEBES usar | Razón |
876
+ |---------|------------|-------|
877
+ | `auth.users` | Supabase Admin API | Usuarios de autenticación |
878
+ | Tablas propias (`products`, `orders`, etc.) | Drizzle ORM + Repository | RLS + queries tipadas |
879
+
880
+ ### Variables de Entorno
881
+
882
+ ```env
883
+ # .env.local
884
+ DATABASE_URL=postgresql://user:pass@host:5432/db
885
+ ```
886
+
887
+ ---
888
+
889
+ ## 🎨 DISEÑO Y UI (Estilo Midday)
890
+
891
+ **Cuando crees UI, SIGUE estas guías de diseño:**
892
+
893
+ ### Filosofía de Diseño (APLÍCALA)
894
+
895
+ Este proyecto sigue el estilo de diseño de **Midday** (https://midday.ai). DEBES:
896
+ - **Mantener minimalismo**: Espacios amplios, tipografía clara
897
+ - **Priorizar dark mode**: Tema oscuro como experiencia principal
898
+ - **EVITAR modales para CRUD**: USA páginas completas en lugar de diálogos
899
+ - **Seguir navegación jerárquica**: Sidebar → SecondaryMenu (tabs) → Contenido
900
+ - **Limitar contenedores**: USA `max-w-[800px]` para formularios
901
+
902
+ ### 🚫 PROHIBIDO: Uso de Modales para CRUD
903
+
904
+ ```typescript
905
+ // ❌ NUNCA usar modales para crear/editar entidades
906
+ <Dialog>
907
+ <DialogTrigger>Crear Usuario</DialogTrigger>
908
+ <DialogContent>
909
+ <UserForm /> // ❌ PROHIBIDO
910
+ </DialogContent>
911
+ </Dialog>
912
+
913
+ // ✅ CORRECTO: Usar páginas completas
914
+ // Ruta: /users/new → página completa para crear
915
+ // Ruta: /users/[id]/edit → página completa para editar
916
+ ```
917
+
918
+ ### Estructura de Navegación Jerárquica
919
+
920
+ ```
921
+ Sidebar Principal (izquierda)
922
+ ├── Dashboard
923
+ ├── Users ────────────────────→ SecondaryMenu (tabs horizontales)
924
+ │ ├── /users (Lista) ├── All Users
925
+ │ ├── /users/new ├── Create New
926
+ │ └── /users/roles └── Roles & Permissions
927
+ ├── Settings ─────────────────→ SecondaryMenu
928
+ │ ├── /settings (General) ├── General
929
+ │ ├── /settings/billing ├── Billing
930
+ │ ├── /settings/members ├── Members
931
+ │ └── /settings/notifications └── Notifications
932
+ └── Account ──────────────────→ SecondaryMenu
933
+ ├── /account (Profile) ├── Profile
934
+ ├── /account/security ├── Security
935
+ └── /account/preferences └── Preferences
936
+ ```
937
+
938
+ ---
939
+
940
+ ## 📄 ARQUITECTURA DE PÁGINAS
941
+
942
+ ### Estructura de Rutas (Patrón Obligatorio)
943
+
944
+ ```
945
+ app/
946
+ ├── (auth)/ # Rutas autenticadas
947
+ │ ├── layout.tsx # Layout con Sidebar
948
+ │ ├── dashboard/
949
+ │ │ └── page.tsx
950
+ │ ├── users/
951
+ │ │ ├── layout.tsx # ⬅️ SecondaryMenu para Users
952
+ │ │ ├── page.tsx # Lista de usuarios
953
+ │ │ ├── new/
954
+ │ │ │ └── page.tsx # Crear usuario (página completa)
955
+ │ │ ├── [id]/
956
+ │ │ │ ├── page.tsx # Detalle usuario
957
+ │ │ │ └── edit/
958
+ │ │ │ └── page.tsx # Editar usuario (página completa)
959
+ │ │ └── roles/
960
+ │ │ └── page.tsx # Gestión de roles
961
+ │ ├── settings/
962
+ │ │ ├── layout.tsx # ⬅️ SecondaryMenu para Settings
963
+ │ │ ├── page.tsx # General settings
964
+ │ │ ├── billing/
965
+ │ │ │ └── page.tsx
966
+ │ │ ├── members/
967
+ │ │ │ └── page.tsx
968
+ │ │ └── notifications/
969
+ │ │ └── page.tsx
970
+ │ └── account/
971
+ │ ├── layout.tsx # ⬅️ SecondaryMenu para Account
972
+ │ ├── page.tsx # Profile
973
+ │ ├── security/
974
+ │ │ └── page.tsx
975
+ │ └── preferences/
976
+ │ └── page.tsx
977
+ └── (public)/
978
+ ├── login/
979
+ └── layout.tsx
980
+ ```
981
+
982
+ ### Layout con SecondaryMenu (PATRÓN OBLIGATORIO)
983
+
984
+ **Cuando crees una sección con subpáginas, DEBES agregar un `layout.tsx` con SecondaryMenu:**
985
+
986
+ ```typescript
987
+ // app/(auth)/settings/layout.tsx
988
+ import { SecondaryMenu } from '@/components/layout/secondary-menu'
989
+
990
+ export default function SettingsLayout({
991
+ children
992
+ }: {
993
+ children: React.ReactNode
994
+ }) {
995
+ return (
996
+ <div className="max-w-[800px]">
997
+ <SecondaryMenu
998
+ items={[
999
+ { path: '/settings', label: 'General' },
1000
+ { path: '/settings/billing', label: 'Billing' },
1001
+ { path: '/settings/members', label: 'Members' },
1002
+ { path: '/settings/notifications', label: 'Notifications' },
1003
+ ]}
1004
+ />
1005
+ <main className="mt-8">{children}</main>
1006
+ </div>
1007
+ )
1008
+ }
1009
+ ```
1010
+
1011
+ ### Componente SecondaryMenu
1012
+
1013
+ ```typescript
1014
+ // components/layout/secondary-menu.tsx
1015
+ 'use client'
1016
+
1017
+ import { cn } from '@/lib/utils'
1018
+ import Link from 'next/link'
1019
+ import { usePathname } from 'next/navigation'
1020
+
1021
+ type Item = {
1022
+ path: string
1023
+ label: string
1024
+ }
1025
+
1026
+ type Props = {
1027
+ items: Item[]
1028
+ }
1029
+
1030
+ export function SecondaryMenu({ items }: Props) {
1031
+ const pathname = usePathname()
1032
+
1033
+ return (
1034
+ <nav className="py-4">
1035
+ <ul className="flex space-x-6 text-sm overflow-auto scrollbar-hide">
1036
+ {items.map((item) => (
1037
+ <Link
1038
+ prefetch
1039
+ key={item.path}
1040
+ href={item.path}
1041
+ className={cn(
1042
+ 'text-muted-foreground hover:text-foreground transition-colors',
1043
+ pathname === item.path &&
1044
+ 'text-foreground font-medium underline underline-offset-8'
1045
+ )}
1046
+ >
1047
+ <span>{item.label}</span>
1048
+ </Link>
1049
+ ))}
1050
+ </ul>
1051
+ </nav>
1052
+ )
1053
+ }
1054
+ ```
1055
+
1056
+ ---
1057
+
1058
+ ## 🧭 CONFIGURACIÓN DE NAVEGACIÓN
1059
+
1060
+ ### Estructura del Sidebar (navigation.ts)
1061
+
1062
+ ```typescript
1063
+ // config/navigation.ts
1064
+ import { Icons } from '@/components/ui/icons'
1065
+
1066
+ export type NavItem = {
1067
+ title: string
1068
+ href: string
1069
+ icon?: keyof typeof Icons
1070
+ disabled?: boolean
1071
+ external?: boolean
1072
+ badge?: string
1073
+ children?: NavItem[] // ⬅️ Submenús
1074
+ }
1075
+
1076
+ export type NavSection = {
1077
+ title?: string
1078
+ items: NavItem[]
1079
+ }
1080
+
1081
+ export const sidebarNav: NavSection[] = [
1082
+ {
1083
+ items: [
1084
+ {
1085
+ title: 'Dashboard',
1086
+ href: '/dashboard',
1087
+ icon: 'dashboard',
1088
+ },
1089
+ {
1090
+ title: 'Users',
1091
+ href: '/users',
1092
+ icon: 'users',
1093
+ children: [
1094
+ { title: 'All Users', href: '/users' },
1095
+ { title: 'Create New', href: '/users/new' },
1096
+ { title: 'Roles', href: '/users/roles' },
1097
+ ],
1098
+ },
1099
+ ],
1100
+ },
1101
+ {
1102
+ title: 'Configuration',
1103
+ items: [
1104
+ {
1105
+ title: 'Settings',
1106
+ href: '/settings',
1107
+ icon: 'settings',
1108
+ children: [
1109
+ { title: 'General', href: '/settings' },
1110
+ { title: 'Billing', href: '/settings/billing' },
1111
+ { title: 'Members', href: '/settings/members' },
1112
+ { title: 'Notifications', href: '/settings/notifications' },
1113
+ ],
1114
+ },
1115
+ {
1116
+ title: 'Account',
1117
+ href: '/account',
1118
+ icon: 'user',
1119
+ children: [
1120
+ { title: 'Profile', href: '/account' },
1121
+ { title: 'Security', href: '/account/security' },
1122
+ { title: 'Preferences', href: '/account/preferences' },
1123
+ ],
1124
+ },
1125
+ ],
1126
+ },
1127
+ ]
1128
+ ```
1129
+
1130
+ ---
1131
+
1132
+ ## 📏 REGLAS DE UI (SÍGUELAS)
1133
+
1134
+ ### 1. Páginas Completas vs Modales (USA ESTA REFERENCIA)
1135
+
1136
+ | Acción | ❌ NO usar | ✅ DEBES usar |
1137
+ |--------|-----------|--------------|
1138
+ | Crear entidad | Dialog con form | `/entity/new` |
1139
+ | Editar entidad | Sheet con form | `/entity/[id]/edit` |
1140
+ | Ver detalle | Expandir fila | `/entity/[id]` |
1141
+ | Confirmar eliminación | - | AlertDialog (única excepción permitida) |
1142
+ | Filtros complejos | Modal | Panel colapsable o página |
1143
+
1144
+ ### 2. Anchos de Contenedor (USA ESTOS)
1145
+
1146
+ ```typescript
1147
+ // Formularios y configuración
1148
+ <div className="max-w-[800px]">
1149
+
1150
+ // Tablas y listas
1151
+ <div className="w-full">
1152
+
1153
+ // Dashboards con métricas
1154
+ <div className="max-w-[1200px]">
1155
+ ```
1156
+
1157
+ ### 3. Espaciado Consistente (USA ESTOS VALORES)
1158
+
1159
+ ```typescript
1160
+ // Entre secciones de formulario - USA space-y-12
1161
+ <div className="space-y-12">
1162
+
1163
+ // Entre campos de formulario - USA space-y-6
1164
+ <div className="space-y-6">
1165
+
1166
+ // Entre elementos pequeños - USA space-y-4
1167
+ <div className="space-y-4">
1168
+ ```
1169
+
1170
+ ### 4. Headers de Página (USA ESTE COMPONENTE)
1171
+
1172
+ ```typescript
1173
+ // components/layout/page-header.tsx - SIEMPRE usa este componente para headers
1174
+ export function PageHeader({
1175
+ title,
1176
+ description,
1177
+ actions,
1178
+ }: {
1179
+ title: string
1180
+ description?: string
1181
+ actions?: React.ReactNode
1182
+ }) {
1183
+ return (
1184
+ <div className="flex items-center justify-between mb-8">
1185
+ <div>
1186
+ <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
1187
+ {description && (
1188
+ <p className="text-muted-foreground mt-1">{description}</p>
1189
+ )}
1190
+ </div>
1191
+ {actions && <div className="flex items-center gap-2">{actions}</div>}
1192
+ </div>
1193
+ )
1194
+ }
1195
+ ```
1196
+
1197
+ ### 5. Acciones de Tabla (NAVEGA A PÁGINAS, NO ABRAS MODALES)
1198
+
1199
+ ```typescript
1200
+ // Las acciones de fila DEBEN navegar a páginas, NO abrir modales
1201
+ const actions = [
1202
+ {
1203
+ label: 'Edit',
1204
+ onClick: (row) => router.push(`/users/${row.id}/edit`), // ✅ Navega a página
1205
+ },
1206
+ {
1207
+ label: 'View',
1208
+ onClick: (row) => router.push(`/users/${row.id}`), // ✅ Navega a página
1209
+ },
1210
+ {
1211
+ label: 'Delete',
1212
+ onClick: (row) => setDeleteId(row.id), // ✅ AlertDialog es la única excepción
1213
+ },
1214
+ ]
1215
+ ```
1216
+
1217
+ ---
1218
+
1219
+ ## 🎯 CHECKLIST PARA NUEVAS PÁGINAS
1220
+
1221
+ **ANTES de crear una nueva página/sección, VERIFICA:**
1222
+
1223
+ - [ ] ¿Tiene subpáginas? → CREA `layout.tsx` con SecondaryMenu
1224
+ - [ ] ¿Es un formulario? → USA `max-w-[800px]`
1225
+ - [ ] ¿Es una lista/tabla? → USA `w-full`
1226
+ - [ ] ¿Necesita crear/editar? → CREA rutas `/new` y `/[id]/edit`
1227
+ - [ ] ¿Tiene navegación padre? → AGREGA a `navigation.ts` con children
1228
+ - [ ] ¿El sidebar necesita actualizarse? → AGREGA icono y ruta
1229
+
1230
+ ---
1231
+
1232
+ ## 🎯 CHECKLIST PARA NUEVOS MÓDULOS
1233
+
1234
+ **CUANDO crees un nuevo módulo, SIGUE AMBOS checklists:**
1235
+
1236
+ 1. **Checklist de Arquitectura** (Store → Queries → Mutations → Hook → Repository)
1237
+ 2. **Checklist de Páginas** (rutas, layouts, SecondaryMenu)
1238
+
1239
+ **RECUERDA**: El Repository Pattern es OBLIGATORIO para entidades con tablas propias.