@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.
- package/README.md +549 -0
- package/package.json +48 -48
- package/template/CLAUDE.md +1239 -279
- package/template/drizzle.config.ts +12 -12
- package/template/eslint.config.mjs +16 -16
- package/template/gitignore +36 -36
- package/template/next.config.ts +7 -7
- package/template/package.json +86 -86
- package/template/postcss.config.mjs +7 -7
- package/template/proxy.ts +12 -12
- package/template/public/logolft.svg +11 -11
- package/template/src/app/(auth)/dashboard/dashboard-content.tsx +124 -124
- package/template/src/app/(auth)/dashboard/page.tsx +9 -9
- package/template/src/app/(auth)/layout.tsx +7 -7
- package/template/src/app/(auth)/users/page.tsx +9 -9
- package/template/src/app/(auth)/users/users-content.tsx +26 -26
- package/template/src/app/(public)/layout.tsx +7 -7
- package/template/src/app/(public)/login/page.tsx +17 -17
- package/template/src/app/api/webhooks/route.ts +20 -20
- package/template/src/app/globals.css +249 -249
- package/template/src/app/layout.tsx +37 -37
- package/template/src/app/page.tsx +5 -5
- package/template/src/app/providers.tsx +27 -27
- package/template/src/components/layout/main-content.tsx +28 -28
- package/template/src/components/layout/sidebar-context.tsx +33 -33
- package/template/src/components/layout/sidebar.tsx +141 -146
- package/template/src/components/tables/data-table-column-header.tsx +68 -68
- package/template/src/components/tables/data-table-date-filter.tsx +203 -0
- package/template/src/components/tables/data-table-faceted-filter.tsx +185 -0
- package/template/src/components/tables/data-table-filters-dropdown.tsx +130 -0
- package/template/src/components/tables/data-table-number-filter.tsx +295 -0
- package/template/src/components/tables/data-table-pagination.tsx +99 -99
- package/template/src/components/tables/data-table-toolbar.tsx +140 -50
- package/template/src/components/tables/data-table-view-options.tsx +63 -59
- package/template/src/components/tables/data-table.tsx +148 -128
- package/template/src/components/tables/index.ts +9 -5
- package/template/src/components/ui/accordion.tsx +58 -58
- package/template/src/components/ui/alert-dialog.tsx +165 -165
- package/template/src/components/ui/alert.tsx +66 -66
- package/template/src/components/ui/animations/index.ts +44 -44
- package/template/src/components/ui/avatar.tsx +55 -55
- package/template/src/components/ui/badge.tsx +50 -50
- package/template/src/components/ui/button.tsx +118 -118
- package/template/src/components/ui/calendar.tsx +220 -220
- package/template/src/components/ui/card.tsx +113 -113
- package/template/src/components/ui/checkbox.tsx +38 -38
- package/template/src/components/ui/collapsible.tsx +33 -33
- package/template/src/components/ui/command.tsx +196 -196
- package/template/src/components/ui/dialog.tsx +156 -156
- package/template/src/components/ui/dropdown-menu.tsx +280 -280
- package/template/src/components/ui/form.tsx +171 -171
- package/template/src/components/ui/icons.tsx +167 -167
- package/template/src/components/ui/input.tsx +28 -28
- package/template/src/components/ui/label.tsx +25 -25
- package/template/src/components/ui/motion.tsx +197 -197
- package/template/src/components/ui/page-transition.tsx +166 -166
- package/template/src/components/ui/popover.tsx +59 -59
- package/template/src/components/ui/progress.tsx +32 -32
- package/template/src/components/ui/radio-group.tsx +45 -45
- package/template/src/components/ui/scroll-area.tsx +63 -63
- package/template/src/components/ui/select.tsx +208 -208
- package/template/src/components/ui/separator.tsx +28 -28
- package/template/src/components/ui/sheet.tsx +170 -170
- package/template/src/components/ui/sidebar.tsx +726 -726
- package/template/src/components/ui/skeleton.tsx +15 -15
- package/template/src/components/ui/slider.tsx +58 -58
- package/template/src/components/ui/sonner.tsx +47 -47
- package/template/src/components/ui/spinner.tsx +27 -27
- package/template/src/components/ui/submit-button.tsx +47 -47
- package/template/src/components/ui/switch.tsx +31 -31
- package/template/src/components/ui/table.tsx +120 -120
- package/template/src/components/ui/tabs.tsx +75 -75
- package/template/src/components/ui/textarea.tsx +26 -26
- package/template/src/components/ui/tooltip.tsx +70 -70
- package/template/src/config/navigation.ts +59 -69
- package/template/src/config/roles.ts +27 -0
- package/template/src/config/site.ts +12 -12
- package/template/src/db/index.ts +12 -12
- package/template/src/db/schema/index.ts +1 -1
- package/template/src/db/schema/users.ts +16 -16
- package/template/src/db/seed.ts +39 -39
- package/template/src/hooks/index.ts +3 -3
- package/template/src/hooks/use-mobile.ts +21 -21
- package/template/src/hooks/useDataTable.ts +82 -82
- package/template/src/hooks/useDebounce.ts +49 -49
- package/template/src/hooks/useMediaQuery.ts +36 -36
- package/template/src/lib/date/config.ts +36 -34
- package/template/src/lib/date/formatters.ts +127 -120
- package/template/src/lib/date/index.ts +26 -19
- package/template/src/lib/excel/exporter.ts +89 -89
- package/template/src/lib/excel/index.ts +14 -14
- package/template/src/lib/excel/parser.ts +96 -96
- package/template/src/lib/query-client.ts +35 -35
- package/template/src/lib/supabase/admin.ts +23 -0
- package/template/src/lib/supabase/client.ts +11 -11
- package/template/src/lib/supabase/proxy.ts +67 -67
- package/template/src/lib/supabase/server.ts +38 -38
- package/template/src/lib/supabase/types.ts +53 -53
- package/template/src/lib/utils.ts +6 -6
- package/template/src/lib/validations/common.ts +75 -75
- package/template/src/lib/validations/index.ts +20 -20
- package/template/src/modules/auth/actions/auth-actions.ts +59 -59
- package/template/src/modules/auth/components/login-form.tsx +68 -68
- package/template/src/modules/auth/hooks/useAuth.ts +38 -38
- package/template/src/modules/auth/hooks/useAuthMutations.ts +43 -43
- package/template/src/modules/auth/hooks/useAuthQueries.ts +43 -43
- package/template/src/modules/auth/index.ts +12 -12
- package/template/src/modules/auth/schemas/auth.schema.ts +32 -32
- package/template/src/modules/auth/stores/useAuthStore.ts +37 -37
- package/template/src/modules/users/actions/users-actions.ts +166 -94
- package/template/src/modules/users/columns.tsx +106 -86
- package/template/src/modules/users/components/users-list.tsx +48 -22
- package/template/src/modules/users/hooks/useUsers.ts +39 -39
- package/template/src/modules/users/hooks/useUsersMutations.ts +55 -55
- package/template/src/modules/users/hooks/useUsersQueries.ts +35 -35
- package/template/src/modules/users/index.ts +30 -12
- package/template/src/modules/users/schemas/users.schema.ts +51 -23
- package/template/src/modules/users/stores/useUsersStore.ts +60 -60
- package/template/src/modules/users/types/auth-user.types.ts +42 -0
- package/template/src/modules/users/utils/user-mapper.ts +32 -0
- package/template/src/stores/index.ts +1 -1
- package/template/src/stores/useUiStore.ts +55 -55
- package/template/src/types/api.ts +28 -28
- package/template/src/types/index.ts +2 -2
- package/template/src/types/table.ts +34 -34
- package/template/supabase/config.toml +94 -94
- package/template/tsconfig.json +42 -42
- package/template/tsconfig.tsbuildinfo +1 -1
package/template/CLAUDE.md
CHANGED
|
@@ -1,279 +1,1239 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## 🚨
|
|
6
|
-
|
|
7
|
-
**⚠️
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
## ⚠️
|
|
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
|
-
## 🚨
|
|
32
|
-
|
|
33
|
-
**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
modules/
|
|
39
|
-
└── [entity]/
|
|
40
|
-
├──
|
|
41
|
-
│ └── [entity]
|
|
42
|
-
├──
|
|
43
|
-
│
|
|
44
|
-
|
|
45
|
-
│
|
|
46
|
-
├──
|
|
47
|
-
│
|
|
48
|
-
|
|
49
|
-
│
|
|
50
|
-
├──
|
|
51
|
-
│ └── use[Entity]
|
|
52
|
-
├──
|
|
53
|
-
│ └── [
|
|
54
|
-
├──
|
|
55
|
-
└──
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
- [ ] **
|
|
65
|
-
- [ ] **
|
|
66
|
-
- [ ] **
|
|
67
|
-
- [ ] **
|
|
68
|
-
- [ ] **
|
|
69
|
-
- [ ] **
|
|
70
|
-
- [ ] **
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// ❌
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// ✅
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
│
|
|
162
|
-
│ │
|
|
163
|
-
│ └──
|
|
164
|
-
│
|
|
165
|
-
│
|
|
166
|
-
│
|
|
167
|
-
│
|
|
168
|
-
│
|
|
169
|
-
│
|
|
170
|
-
│ ├──
|
|
171
|
-
│
|
|
172
|
-
│
|
|
173
|
-
│
|
|
174
|
-
│
|
|
175
|
-
│
|
|
176
|
-
│
|
|
177
|
-
│
|
|
178
|
-
│
|
|
179
|
-
│
|
|
180
|
-
│
|
|
181
|
-
├──
|
|
182
|
-
│
|
|
183
|
-
│ │
|
|
184
|
-
│
|
|
185
|
-
│
|
|
186
|
-
│
|
|
187
|
-
│
|
|
188
|
-
|
|
189
|
-
│
|
|
190
|
-
│ │ ├──
|
|
191
|
-
│ │ └──
|
|
192
|
-
│ ├──
|
|
193
|
-
│ │
|
|
194
|
-
│ │
|
|
195
|
-
│
|
|
196
|
-
│
|
|
197
|
-
│
|
|
198
|
-
├── stores/
|
|
199
|
-
│
|
|
200
|
-
│
|
|
201
|
-
│
|
|
202
|
-
|
|
203
|
-
│
|
|
204
|
-
│
|
|
205
|
-
│
|
|
206
|
-
│
|
|
207
|
-
├──
|
|
208
|
-
│ ├──
|
|
209
|
-
│
|
|
210
|
-
│
|
|
211
|
-
│
|
|
212
|
-
│
|
|
213
|
-
│ ├──
|
|
214
|
-
│
|
|
215
|
-
│
|
|
216
|
-
│
|
|
217
|
-
│
|
|
218
|
-
│
|
|
219
|
-
│
|
|
220
|
-
│
|
|
221
|
-
│
|
|
222
|
-
│
|
|
223
|
-
│
|
|
224
|
-
│
|
|
225
|
-
│ └──
|
|
226
|
-
│
|
|
227
|
-
|
|
228
|
-
│ ├──
|
|
229
|
-
│ │
|
|
230
|
-
│
|
|
231
|
-
│ ├──
|
|
232
|
-
│
|
|
233
|
-
│
|
|
234
|
-
│
|
|
235
|
-
|
|
236
|
-
│
|
|
237
|
-
|
|
238
|
-
│
|
|
239
|
-
│
|
|
240
|
-
├──
|
|
241
|
-
│
|
|
242
|
-
│
|
|
243
|
-
│
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
├──
|
|
249
|
-
│ ├──
|
|
250
|
-
│ │ └── index.ts
|
|
251
|
-
│
|
|
252
|
-
│
|
|
253
|
-
├──
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
├──
|
|
260
|
-
├──
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
├──
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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.
|