@create-lft-app/nextjs 3.2.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 (133) hide show
  1. package/README.md +549 -549
  2. package/package.json +48 -48
  3. package/template/.claude/skills/anti-patterns.md +150 -0
  4. package/template/.claude/skills/drizzle-schema.md +178 -0
  5. package/template/.claude/skills/formatting.md +56 -0
  6. package/template/.claude/skills/module-architecture.md +143 -0
  7. package/template/.claude/skills/supabase-server-actions.md +199 -0
  8. package/template/.claude/skills/ui-patterns.md +161 -0
  9. package/template/CLAUDE.md +114 -1239
  10. package/template/drizzle.config.ts +12 -12
  11. package/template/eslint.config.mjs +16 -16
  12. package/template/gitignore +36 -36
  13. package/template/next.config.ts +7 -7
  14. package/template/package.json +86 -86
  15. package/template/postcss.config.mjs +7 -7
  16. package/template/proxy.ts +12 -12
  17. package/template/public/logolft.svg +11 -11
  18. package/template/src/app/(auth)/dashboard/dashboard-content.tsx +124 -124
  19. package/template/src/app/(auth)/dashboard/page.tsx +9 -9
  20. package/template/src/app/(auth)/layout.tsx +7 -7
  21. package/template/src/app/(auth)/users/page.tsx +9 -9
  22. package/template/src/app/(auth)/users/users-content.tsx +26 -26
  23. package/template/src/app/(public)/layout.tsx +7 -7
  24. package/template/src/app/(public)/login/page.tsx +17 -17
  25. package/template/src/app/api/webhooks/route.ts +20 -20
  26. package/template/src/app/globals.css +249 -249
  27. package/template/src/app/layout.tsx +37 -37
  28. package/template/src/app/page.tsx +5 -5
  29. package/template/src/app/providers.tsx +27 -27
  30. package/template/src/components/layout/main-content.tsx +28 -28
  31. package/template/src/components/layout/sidebar-context.tsx +33 -33
  32. package/template/src/components/layout/sidebar.tsx +141 -141
  33. package/template/src/components/tables/data-table-column-header.tsx +68 -68
  34. package/template/src/components/tables/data-table-date-filter.tsx +203 -203
  35. package/template/src/components/tables/data-table-faceted-filter.tsx +185 -185
  36. package/template/src/components/tables/data-table-filters-dropdown.tsx +130 -130
  37. package/template/src/components/tables/data-table-number-filter.tsx +295 -295
  38. package/template/src/components/tables/data-table-pagination.tsx +99 -99
  39. package/template/src/components/tables/data-table-toolbar.tsx +140 -140
  40. package/template/src/components/tables/data-table-view-options.tsx +63 -63
  41. package/template/src/components/tables/data-table.tsx +148 -148
  42. package/template/src/components/tables/index.ts +9 -9
  43. package/template/src/components/ui/accordion.tsx +58 -58
  44. package/template/src/components/ui/alert-dialog.tsx +165 -165
  45. package/template/src/components/ui/alert.tsx +66 -66
  46. package/template/src/components/ui/animations/index.ts +44 -44
  47. package/template/src/components/ui/avatar.tsx +55 -55
  48. package/template/src/components/ui/badge.tsx +50 -50
  49. package/template/src/components/ui/button.tsx +118 -118
  50. package/template/src/components/ui/calendar.tsx +220 -220
  51. package/template/src/components/ui/card.tsx +113 -113
  52. package/template/src/components/ui/checkbox.tsx +38 -38
  53. package/template/src/components/ui/collapsible.tsx +33 -33
  54. package/template/src/components/ui/command.tsx +196 -196
  55. package/template/src/components/ui/dialog.tsx +156 -156
  56. package/template/src/components/ui/dropdown-menu.tsx +280 -280
  57. package/template/src/components/ui/form.tsx +171 -171
  58. package/template/src/components/ui/icons.tsx +167 -167
  59. package/template/src/components/ui/input.tsx +28 -28
  60. package/template/src/components/ui/label.tsx +25 -25
  61. package/template/src/components/ui/motion.tsx +197 -197
  62. package/template/src/components/ui/page-transition.tsx +166 -166
  63. package/template/src/components/ui/popover.tsx +59 -59
  64. package/template/src/components/ui/progress.tsx +32 -32
  65. package/template/src/components/ui/radio-group.tsx +45 -45
  66. package/template/src/components/ui/scroll-area.tsx +63 -63
  67. package/template/src/components/ui/select.tsx +208 -208
  68. package/template/src/components/ui/separator.tsx +28 -28
  69. package/template/src/components/ui/sheet.tsx +170 -170
  70. package/template/src/components/ui/sidebar.tsx +726 -726
  71. package/template/src/components/ui/skeleton.tsx +15 -15
  72. package/template/src/components/ui/slider.tsx +58 -58
  73. package/template/src/components/ui/sonner.tsx +47 -47
  74. package/template/src/components/ui/spinner.tsx +27 -27
  75. package/template/src/components/ui/submit-button.tsx +47 -47
  76. package/template/src/components/ui/switch.tsx +31 -31
  77. package/template/src/components/ui/table.tsx +120 -120
  78. package/template/src/components/ui/tabs.tsx +75 -75
  79. package/template/src/components/ui/textarea.tsx +26 -26
  80. package/template/src/components/ui/tooltip.tsx +70 -70
  81. package/template/src/config/navigation.ts +59 -59
  82. package/template/src/config/roles.ts +27 -27
  83. package/template/src/config/site.ts +12 -12
  84. package/template/src/db/index.ts +12 -12
  85. package/template/src/db/schema/index.ts +1 -1
  86. package/template/src/db/schema/users.ts +16 -16
  87. package/template/src/db/seed.ts +39 -39
  88. package/template/src/hooks/index.ts +3 -3
  89. package/template/src/hooks/use-mobile.ts +21 -21
  90. package/template/src/hooks/useDataTable.ts +82 -82
  91. package/template/src/hooks/useDebounce.ts +49 -49
  92. package/template/src/hooks/useMediaQuery.ts +36 -36
  93. package/template/src/lib/date/config.ts +36 -36
  94. package/template/src/lib/date/formatters.ts +127 -127
  95. package/template/src/lib/date/index.ts +26 -26
  96. package/template/src/lib/excel/exporter.ts +89 -89
  97. package/template/src/lib/excel/index.ts +14 -14
  98. package/template/src/lib/excel/parser.ts +96 -96
  99. package/template/src/lib/query-client.ts +35 -35
  100. package/template/src/lib/supabase/admin.ts +23 -23
  101. package/template/src/lib/supabase/client.ts +11 -11
  102. package/template/src/lib/supabase/proxy.ts +67 -67
  103. package/template/src/lib/supabase/server.ts +38 -38
  104. package/template/src/lib/supabase/types.ts +53 -53
  105. package/template/src/lib/utils.ts +6 -6
  106. package/template/src/lib/validations/common.ts +75 -75
  107. package/template/src/lib/validations/index.ts +20 -20
  108. package/template/src/modules/auth/actions/auth-actions.ts +59 -59
  109. package/template/src/modules/auth/components/login-form.tsx +68 -68
  110. package/template/src/modules/auth/hooks/useAuth.ts +38 -38
  111. package/template/src/modules/auth/hooks/useAuthMutations.ts +43 -43
  112. package/template/src/modules/auth/hooks/useAuthQueries.ts +43 -43
  113. package/template/src/modules/auth/index.ts +12 -12
  114. package/template/src/modules/auth/schemas/auth.schema.ts +32 -32
  115. package/template/src/modules/auth/stores/useAuthStore.ts +37 -37
  116. package/template/src/modules/users/actions/users-actions.ts +166 -166
  117. package/template/src/modules/users/columns.tsx +106 -106
  118. package/template/src/modules/users/components/users-list.tsx +48 -48
  119. package/template/src/modules/users/hooks/useUsers.ts +39 -39
  120. package/template/src/modules/users/hooks/useUsersMutations.ts +55 -55
  121. package/template/src/modules/users/hooks/useUsersQueries.ts +35 -35
  122. package/template/src/modules/users/index.ts +30 -30
  123. package/template/src/modules/users/schemas/users.schema.ts +51 -51
  124. package/template/src/modules/users/stores/useUsersStore.ts +60 -60
  125. package/template/src/modules/users/types/auth-user.types.ts +42 -42
  126. package/template/src/modules/users/utils/user-mapper.ts +32 -32
  127. package/template/src/stores/index.ts +1 -1
  128. package/template/src/stores/useUiStore.ts +55 -55
  129. package/template/src/types/api.ts +28 -28
  130. package/template/src/types/index.ts +2 -2
  131. package/template/src/types/table.ts +34 -34
  132. package/template/supabase/config.toml +94 -94
  133. package/template/tsconfig.json +42 -42
@@ -1,1239 +1,114 @@
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.
1
+ # CLAUDE.md
2
+
3
+ ## Project Overview
4
+
5
+ Next.js application with Supabase backend, following a feature-based modular architecture with mandatory Repository Pattern.
6
+
7
+ ## Tech Stack
8
+
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
21
+
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
34
+ ```
35
+
36
+ > **Always run `pnpm typecheck && pnpm lint` before committing.**
37
+
38
+ ## Project Structure
39
+
40
+ ```
41
+ src/
42
+ ├── app/
43
+ ├── (auth)/ # Authenticated routes (layout with Sidebar)
44
+ │ │ ├── dashboard/
45
+ ├── users/ # Each section: layout.tsx with SecondaryMenu
46
+ ├── settings/
47
+ └── layout.tsx
48
+ ├── (public)/ # Public routes
49
+ │ └── login/
50
+ │ ├── api/webhooks/
51
+ ├── layout.tsx
52
+ ├── page.tsx
53
+ │ └── providers.tsx
54
+
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
59
+
60
+ ├── components/
61
+ │ ├── ui/ # Radix custom components
62
+ │ ├── tables/ # TanStack Table (data-table, pagination, filters)
63
+ │ ├── layout/ # header, sidebar, secondary-menu, page-header
64
+ │ └── shared/
65
+
66
+ ├── stores/ # Global Zustand stores
67
+ ├── hooks/ # Global hooks (useMediaQuery, useDebounce, useDataTable)
68
+
69
+ ├── lib/
70
+ │ ├── supabase/ # client.ts, server.ts, admin.ts
71
+ │ ├── excel/ # parser.ts, exporter.ts
72
+ │ ├── date/ # config.ts (locale/timezone/currency), formatters.ts
73
+ │ ├── validations/
74
+ │ ├── query-client.ts
75
+ │ └── utils.ts
76
+
77
+ ├── db/ # Drizzle ORM (see skill: drizzle-schema)
78
+ │ ├── schema/ # Schema per entity + barrel index.ts
79
+ │ ├── migrations/
80
+ │ ├── index.ts # Exports `db` (Drizzle client)
81
+ │ └── seed.ts
82
+
83
+ ├── types/
84
+ ├── config/
85
+ │ ├── site.ts
86
+ │ ├── navigation.ts # Sidebar structure with children
87
+ │ └── roles.ts # Single source of truth for roles
88
+
89
+ └── styles/
90
+ └── globals.css
91
+ ```
92
+
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 |