gufi-cli 0.1.17 → 0.1.18
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/CLAUDE.md +175 -2076
- package/README.md +573 -0
- package/dist/commands/companies.d.ts +9 -2
- package/dist/commands/companies.js +273 -46
- package/dist/commands/config.d.ts +10 -0
- package/dist/commands/config.js +104 -0
- package/dist/commands/context.d.ts +23 -0
- package/dist/commands/context.js +408 -0
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.js +619 -0
- package/dist/commands/env.d.ts +3 -1
- package/dist/commands/env.js +28 -3
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +55 -27
- package/dist/commands/packages.d.ts +33 -2
- package/dist/commands/packages.js +249 -20
- package/dist/commands/push.d.ts +1 -0
- package/dist/commands/push.js +125 -6
- package/dist/commands/rows.d.ts +2 -0
- package/dist/commands/rows.js +35 -9
- package/dist/commands/templates.d.ts +28 -0
- package/dist/commands/templates.js +779 -0
- package/dist/index.d.ts +66 -38
- package/dist/index.js +177 -42
- package/dist/lib/api.d.ts +57 -0
- package/dist/lib/api.js +89 -2
- package/dist/lib/config.d.ts +32 -2
- package/dist/lib/config.js +107 -11
- package/dist/lib/security.d.ts +26 -0
- package/dist/lib/security.js +168 -0
- package/dist/mcp.d.ts +13 -0
- package/dist/mcp.js +1822 -0
- package/package.json +2 -1
package/CLAUDE.md
CHANGED
|
@@ -1,2107 +1,206 @@
|
|
|
1
|
-
# Gufi CLI - Documentación
|
|
1
|
+
# Gufi CLI - Documentación para Claude
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Documentación centralizada en `docs/claude/`**
|
|
4
|
+
>
|
|
5
|
+
> Este archivo es un puntero. Para documentación completa del CLI, lee:
|
|
6
|
+
> - [docs/claude/06-cli.md](../../docs/claude/06-cli.md) - Comandos del CLI
|
|
7
|
+
> - [docs/claude/00-index.md](../../docs/claude/00-index.md) - Overview de Gufi
|
|
8
|
+
> - [docs/claude/05-automations.md](../../docs/claude/05-automations.md) - Automations
|
|
9
|
+
> - [docs/claude/04-views.md](../../docs/claude/04-views.md) - Sistema de vistas
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
**Filosofía**: "Simple · Professional · Elegance" - El Apple de los ERPs
|
|
8
|
-
|
|
9
|
-
### Arquitectura General
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
┌─────────────────────────────────────────────────────────────────┐
|
|
13
|
-
│ GUFI ERP │
|
|
14
|
-
├─────────────────────────────────────────────────────────────────┤
|
|
15
|
-
│ │
|
|
16
|
-
│ Company 116 (Fitvending) Company 146 (Gufi) │
|
|
17
|
-
│ ├── Schema: company_116 ├── Schema: company_146 │
|
|
18
|
-
│ ├── Módulo: Nayax (308) ├── Módulo: Tareas (360) │
|
|
19
|
-
│ │ └── Tablas: m308_t4136 │ └── Tablas: m360_t16192 │
|
|
20
|
-
│ └── Módulo: Stock (310) └── Módulo: Incidencias (361) │
|
|
21
|
-
│ │
|
|
22
|
-
├─────────────────────────────────────────────────────────────────┤
|
|
23
|
-
│ core.modules → Definiciones JSON de módulos │
|
|
24
|
-
│ core.entities → Metadatos (permissions, automations) │
|
|
25
|
-
│ core.entities.automations → FUENTE DE VERDAD de automations │
|
|
26
|
-
│ core.automation_scripts → Código JavaScript de automations │
|
|
27
|
-
│ core.automation_meta → Índice para worker (sync auto) │
|
|
28
|
-
├─────────────────────────────────────────────────────────────────┤
|
|
29
|
-
│ marketplace.packages → Paquetes publicados │
|
|
30
|
-
│ marketplace.views → Vistas React/TypeScript │
|
|
31
|
-
└─────────────────────────────────────────────────────────────────┘
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
### Nomenclatura de Tablas
|
|
35
|
-
|
|
36
|
-
Las tablas físicas en PostgreSQL siguen el patrón:
|
|
37
|
-
```
|
|
38
|
-
m{moduleId}_t{entityId}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Ejemplo:
|
|
42
|
-
- Módulo "Tareas" tiene ID 360
|
|
43
|
-
- Entidad "asignaciones" tiene ID 16192
|
|
44
|
-
- Tabla física: `m360_t16192` en schema `company_146`
|
|
45
|
-
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
## 💜 Core Database - Relaciones Importantes
|
|
49
|
-
|
|
50
|
-
### Diagrama de Relaciones
|
|
51
|
-
|
|
52
|
-
```
|
|
53
|
-
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
54
|
-
│ SCHEMA: core │
|
|
55
|
-
├─────────────────────────────────────────────────────────────────────────────┤
|
|
56
|
-
│ │
|
|
57
|
-
│ companies (1) │
|
|
58
|
-
│ ├── id: 116 │
|
|
59
|
-
│ ├── name: "Fitvending" │
|
|
60
|
-
│ └── schema: "company_116" │
|
|
61
|
-
│ │ │
|
|
62
|
-
│ │ 1:N │
|
|
63
|
-
│ ▼ │
|
|
64
|
-
│ modules (N) users (N) │
|
|
65
|
-
│ ├── id: 308 ├── id: 16 │
|
|
66
|
-
│ ├── company_id: 116 ◄────────── ├── email: "juan@gufi.es" │
|
|
67
|
-
│ ├── name: "nayax" └── platform_role: "admin" │
|
|
68
|
-
│ └── json_definition: {...} │ │
|
|
69
|
-
│ │ │ N:M │
|
|
70
|
-
│ │ 1:N ▼ │
|
|
71
|
-
│ ▼ company_users │
|
|
72
|
-
│ entities (N) ├── user_id: 16 │
|
|
73
|
-
│ ├── id: 4136 ├── company_id: 116 │
|
|
74
|
-
│ ├── module_id: 308 ◄─────────── └── roles: ["Admin"] │
|
|
75
|
-
│ ├── company_id: 116 │
|
|
76
|
-
│ ├── name: "machines" │
|
|
77
|
-
│ ├── permissions: {"Admin": "*"} ◄── Permisos por rol │
|
|
78
|
-
│ └── automations: [{...}] ◄── JSONB array de triggers │
|
|
79
|
-
│ │ │
|
|
80
|
-
│ │ N:1 (via script_id en automations) │
|
|
81
|
-
│ ▼ │
|
|
82
|
-
│ automation_scripts (N) │
|
|
83
|
-
│ ├── id: 15 │
|
|
84
|
-
│ ├── company_id: 116 │
|
|
85
|
-
│ ├── name: "sync_machines" │
|
|
86
|
-
│ └── code: "async function..." ◄── El JavaScript │
|
|
87
|
-
│ │ │
|
|
88
|
-
│ │ (sync automático) │
|
|
89
|
-
│ ▼ │
|
|
90
|
-
│ automation_meta (índice para worker) │
|
|
91
|
-
│ automation_executions (historial) │
|
|
92
|
-
│ │
|
|
93
|
-
└─────────────────────────────────────────────────────────────────────────────┘
|
|
94
|
-
|
|
95
|
-
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
96
|
-
│ SCHEMA: company_116 │
|
|
97
|
-
├─────────────────────────────────────────────────────────────────────────────┤
|
|
98
|
-
│ │
|
|
99
|
-
│ m308_t4136 (machines) m308_t4137 (products) m308_t4140 (sales) │
|
|
100
|
-
│ ├── id ├── id ├── id │
|
|
101
|
-
│ ├── name ├── name ├── machine_id (FK) │
|
|
102
|
-
│ └── ... └── ... └── ... │
|
|
103
|
-
│ │
|
|
104
|
-
│ __audit_log__ (historial de cambios) │
|
|
105
|
-
│ │
|
|
106
|
-
└─────────────────────────────────────────────────────────────────────────────┘
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
### Tablas Core Principales
|
|
110
|
-
|
|
111
|
-
| Tabla | Descripción | Relaciones |
|
|
112
|
-
|-------|-------------|------------|
|
|
113
|
-
| `companies` | Empresas/tenants | 1 company → N modules, N users |
|
|
114
|
-
| `users` | Usuarios globales | N:M con companies via company_users |
|
|
115
|
-
| `company_users` | Membresía usuario-empresa | Contiene roles[] del usuario en esa company |
|
|
116
|
-
| `modules` | Definición JSON de módulos | 1 module → N entities |
|
|
117
|
-
| `entities` | Metadatos de tablas | Contiene permissions, automations, config |
|
|
118
|
-
| `automation_scripts` | Código JavaScript | 1 script → N entities pueden usarlo |
|
|
119
|
-
| `automation_meta` | Índice para worker | Generado auto desde entities.automations |
|
|
120
|
-
|
|
121
|
-
### Flujo de Datos
|
|
122
|
-
|
|
123
|
-
```
|
|
124
|
-
Usuario hace login
|
|
125
|
-
│
|
|
126
|
-
▼
|
|
127
|
-
JWT contiene: { user_id, company_id, roles[], platform_role }
|
|
128
|
-
│
|
|
129
|
-
▼
|
|
130
|
-
Backend: SET search_path TO company_116, core;
|
|
131
|
-
│
|
|
132
|
-
▼
|
|
133
|
-
Queries van automáticamente al schema correcto
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### IDs Importantes para CLI
|
|
137
|
-
|
|
138
|
-
| Quiero trabajar con... | Necesito el ID de... | Cómo obtenerlo |
|
|
139
|
-
|------------------------|---------------------|----------------|
|
|
140
|
-
| Datos de una tabla | entity (tabla física: m308_t4136) | `gufi schema -c 116` |
|
|
141
|
-
| Estructura de módulo | module_id | `gufi modules 116` |
|
|
142
|
-
| Código de automation | script_id | `gufi automations -c 116` |
|
|
143
|
-
| Triggers de una entity | entity_id | `gufi schema -c 116` |
|
|
144
|
-
|
|
145
|
-
---
|
|
146
|
-
|
|
147
|
-
## Estructura de un Módulo (JSON)
|
|
148
|
-
|
|
149
|
-
Un módulo es un JSON que define **toda la estructura de datos** de una funcionalidad. Gufi lee este JSON y automáticamente:
|
|
150
|
-
1. Crea las tablas en PostgreSQL
|
|
151
|
-
2. Genera la UI (formularios, listas, filtros)
|
|
152
|
-
3. Configura permisos
|
|
153
|
-
4. Vincula relaciones entre tablas
|
|
154
|
-
|
|
155
|
-
### JSON Completo de Ejemplo
|
|
156
|
-
|
|
157
|
-
```json
|
|
158
|
-
{
|
|
159
|
-
"name": "ventas",
|
|
160
|
-
"displayName": "Gestión de Ventas",
|
|
161
|
-
"icon": "ShoppingCart",
|
|
162
|
-
"submodules": [
|
|
163
|
-
{
|
|
164
|
-
"name": "maestros",
|
|
165
|
-
"label": "Datos Maestros",
|
|
166
|
-
"icon": "Database",
|
|
167
|
-
"entities": [
|
|
168
|
-
{
|
|
169
|
-
"kind": "table",
|
|
170
|
-
"name": "clientes",
|
|
171
|
-
"label": "Clientes",
|
|
172
|
-
"fields": [
|
|
173
|
-
{
|
|
174
|
-
"name": "nombre",
|
|
175
|
-
"type": "text",
|
|
176
|
-
"label": "Nombre",
|
|
177
|
-
"required": true,
|
|
178
|
-
"searchable": true
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
"name": "email",
|
|
182
|
-
"type": "email",
|
|
183
|
-
"label": "Email",
|
|
184
|
-
"unique": true
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
"name": "telefono",
|
|
188
|
-
"type": "phone",
|
|
189
|
-
"label": "Teléfono"
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
"name": "tipo",
|
|
193
|
-
"type": "select",
|
|
194
|
-
"label": "Tipo de Cliente",
|
|
195
|
-
"options": [
|
|
196
|
-
{ "value": "particular", "label": "Particular" },
|
|
197
|
-
{ "value": "empresa", "label": "Empresa" },
|
|
198
|
-
{ "value": "vip", "label": "VIP" }
|
|
199
|
-
],
|
|
200
|
-
"default": "particular"
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
"name": "limite_credito",
|
|
204
|
-
"type": "currency",
|
|
205
|
-
"label": "Límite de Crédito",
|
|
206
|
-
"currency": "EUR"
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
"name": "activo",
|
|
210
|
-
"type": "boolean",
|
|
211
|
-
"label": "Activo",
|
|
212
|
-
"default": true
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
"name": "notas",
|
|
216
|
-
"type": "textarea",
|
|
217
|
-
"label": "Notas"
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
"name": "comercial_id",
|
|
221
|
-
"type": "users",
|
|
222
|
-
"label": "Comercial Asignado"
|
|
223
|
-
}
|
|
224
|
-
],
|
|
225
|
-
"ui": {
|
|
226
|
-
"defaultSort": { "field": "nombre", "order": "ASC" },
|
|
227
|
-
"searchFields": ["nombre", "email"],
|
|
228
|
-
"listFields": ["nombre", "tipo", "comercial_id", "activo"]
|
|
229
|
-
}
|
|
230
|
-
},
|
|
231
|
-
{
|
|
232
|
-
"kind": "table",
|
|
233
|
-
"name": "productos",
|
|
234
|
-
"label": "Productos",
|
|
235
|
-
"fields": [
|
|
236
|
-
{
|
|
237
|
-
"name": "codigo",
|
|
238
|
-
"type": "text",
|
|
239
|
-
"label": "Código",
|
|
240
|
-
"required": true,
|
|
241
|
-
"unique": true
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
"name": "nombre",
|
|
245
|
-
"type": "text",
|
|
246
|
-
"label": "Nombre",
|
|
247
|
-
"required": true
|
|
248
|
-
},
|
|
249
|
-
{
|
|
250
|
-
"name": "precio",
|
|
251
|
-
"type": "currency",
|
|
252
|
-
"label": "Precio",
|
|
253
|
-
"currency": "EUR",
|
|
254
|
-
"required": true
|
|
255
|
-
},
|
|
256
|
-
{
|
|
257
|
-
"name": "stock",
|
|
258
|
-
"type": "number",
|
|
259
|
-
"label": "Stock",
|
|
260
|
-
"default": 0,
|
|
261
|
-
"min": 0
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
"name": "categoria",
|
|
265
|
-
"type": "multiselect",
|
|
266
|
-
"label": "Categorías",
|
|
267
|
-
"options": [
|
|
268
|
-
{ "value": "electronica", "label": "Electrónica" },
|
|
269
|
-
{ "value": "ropa", "label": "Ropa" },
|
|
270
|
-
{ "value": "hogar", "label": "Hogar" }
|
|
271
|
-
]
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
"name": "imagen",
|
|
275
|
-
"type": "image",
|
|
276
|
-
"label": "Imagen"
|
|
277
|
-
}
|
|
278
|
-
]
|
|
279
|
-
}
|
|
280
|
-
]
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
"name": "operaciones",
|
|
284
|
-
"label": "Operaciones",
|
|
285
|
-
"icon": "FileText",
|
|
286
|
-
"entities": [
|
|
287
|
-
{
|
|
288
|
-
"kind": "table",
|
|
289
|
-
"name": "pedidos",
|
|
290
|
-
"label": "Pedidos",
|
|
291
|
-
"fields": [
|
|
292
|
-
{
|
|
293
|
-
"name": "numero",
|
|
294
|
-
"type": "text",
|
|
295
|
-
"label": "Nº Pedido",
|
|
296
|
-
"required": true,
|
|
297
|
-
"unique": true,
|
|
298
|
-
"autoGenerate": "PED-{YYYY}{MM}-{SEQ:5}"
|
|
299
|
-
},
|
|
300
|
-
{
|
|
301
|
-
"name": "cliente_id",
|
|
302
|
-
"type": "relation",
|
|
303
|
-
"label": "Cliente",
|
|
304
|
-
"ref": "clientes",
|
|
305
|
-
"display": "nombre",
|
|
306
|
-
"required": true
|
|
307
|
-
},
|
|
308
|
-
{
|
|
309
|
-
"name": "fecha",
|
|
310
|
-
"type": "date",
|
|
311
|
-
"label": "Fecha",
|
|
312
|
-
"default": "today"
|
|
313
|
-
},
|
|
314
|
-
{
|
|
315
|
-
"name": "estado",
|
|
316
|
-
"type": "select",
|
|
317
|
-
"label": "Estado",
|
|
318
|
-
"options": [
|
|
319
|
-
{ "value": "borrador", "label": "Borrador", "color": "gray" },
|
|
320
|
-
{ "value": "confirmado", "label": "Confirmado", "color": "blue" },
|
|
321
|
-
{ "value": "enviado", "label": "Enviado", "color": "yellow" },
|
|
322
|
-
{ "value": "entregado", "label": "Entregado", "color": "green" },
|
|
323
|
-
{ "value": "cancelado", "label": "Cancelado", "color": "red" }
|
|
324
|
-
],
|
|
325
|
-
"default": "borrador"
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
"name": "total",
|
|
329
|
-
"type": "currency",
|
|
330
|
-
"label": "Total",
|
|
331
|
-
"currency": "EUR",
|
|
332
|
-
"computed": true
|
|
333
|
-
},
|
|
334
|
-
{
|
|
335
|
-
"name": "responsable_id",
|
|
336
|
-
"type": "users",
|
|
337
|
-
"label": "Responsable"
|
|
338
|
-
},
|
|
339
|
-
{
|
|
340
|
-
"name": "observaciones",
|
|
341
|
-
"type": "textarea",
|
|
342
|
-
"label": "Observaciones"
|
|
343
|
-
}
|
|
344
|
-
],
|
|
345
|
-
"automations": [
|
|
346
|
-
{
|
|
347
|
-
"trigger": "insert",
|
|
348
|
-
"function_name": "notificar_nuevo_pedido"
|
|
349
|
-
},
|
|
350
|
-
{
|
|
351
|
-
"trigger": "update",
|
|
352
|
-
"condition": "estado = 'confirmado'",
|
|
353
|
-
"function_name": "procesar_pedido"
|
|
354
|
-
},
|
|
355
|
-
{
|
|
356
|
-
"trigger": "click",
|
|
357
|
-
"function_name": "generar_factura",
|
|
358
|
-
"label": "Generar Factura",
|
|
359
|
-
"icon": "FileText",
|
|
360
|
-
"showWhen": "estado = 'entregado'"
|
|
361
|
-
}
|
|
362
|
-
]
|
|
363
|
-
},
|
|
364
|
-
{
|
|
365
|
-
"kind": "table",
|
|
366
|
-
"name": "lineas_pedido",
|
|
367
|
-
"label": "Líneas de Pedido",
|
|
368
|
-
"fields": [
|
|
369
|
-
{
|
|
370
|
-
"name": "pedido_id",
|
|
371
|
-
"type": "relation",
|
|
372
|
-
"label": "Pedido",
|
|
373
|
-
"ref": "pedidos",
|
|
374
|
-
"required": true,
|
|
375
|
-
"cascade": true
|
|
376
|
-
},
|
|
377
|
-
{
|
|
378
|
-
"name": "producto_id",
|
|
379
|
-
"type": "relation",
|
|
380
|
-
"label": "Producto",
|
|
381
|
-
"ref": "productos",
|
|
382
|
-
"display": "nombre",
|
|
383
|
-
"required": true
|
|
384
|
-
},
|
|
385
|
-
{
|
|
386
|
-
"name": "cantidad",
|
|
387
|
-
"type": "number",
|
|
388
|
-
"label": "Cantidad",
|
|
389
|
-
"required": true,
|
|
390
|
-
"min": 1,
|
|
391
|
-
"default": 1
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
"name": "precio_unitario",
|
|
395
|
-
"type": "currency",
|
|
396
|
-
"label": "Precio Unit.",
|
|
397
|
-
"currency": "EUR"
|
|
398
|
-
},
|
|
399
|
-
{
|
|
400
|
-
"name": "subtotal",
|
|
401
|
-
"type": "currency",
|
|
402
|
-
"label": "Subtotal",
|
|
403
|
-
"currency": "EUR",
|
|
404
|
-
"computed": "cantidad * precio_unitario"
|
|
405
|
-
}
|
|
406
|
-
],
|
|
407
|
-
"ui": {
|
|
408
|
-
"inline": true,
|
|
409
|
-
"parentField": "pedido_id"
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
]
|
|
413
|
-
}
|
|
414
|
-
]
|
|
415
|
-
}
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
### Tipos de Campos Disponibles
|
|
419
|
-
|
|
420
|
-
| Tipo | PostgreSQL | Descripción | Opciones |
|
|
421
|
-
|------|------------|-------------|----------|
|
|
422
|
-
| `text` | VARCHAR(255) | Texto corto | `maxLength`, `minLength`, `pattern` |
|
|
423
|
-
| `textarea` | TEXT | Texto largo | `rows`, `maxLength` |
|
|
424
|
-
| `number` | NUMERIC | Número | `min`, `max`, `decimals`, `step` |
|
|
425
|
-
| `currency` | NUMERIC(15,2) | Dinero | `currency` (EUR, USD, etc.) |
|
|
426
|
-
| `date` | DATE | Fecha | `min`, `max`, `default: "today"` |
|
|
427
|
-
| `datetime` | TIMESTAMP | Fecha y hora | `min`, `max` |
|
|
428
|
-
| `time` | TIME | Solo hora | - |
|
|
429
|
-
| `select` | VARCHAR | Selector único | `options: [{value, label, color}]` |
|
|
430
|
-
| `multiselect` | TEXT[] | Selector múltiple | `options: [{value, label}]` |
|
|
431
|
-
| `relation` | INTEGER (FK) | Relación a otra tabla | `ref`, `display`, `cascade` |
|
|
432
|
-
| `users` | INTEGER/INT[] | Usuario(s) del sistema | `multiple: true` |
|
|
433
|
-
| `boolean` | BOOLEAN | Sí/No | `default` |
|
|
434
|
-
| `email` | VARCHAR | Email validado | - |
|
|
435
|
-
| `phone` | VARCHAR | Teléfono | `format` |
|
|
436
|
-
| `url` | VARCHAR | URL | - |
|
|
437
|
-
| `image` | TEXT | URL de imagen (GCS) | `maxSize`, `allowedTypes` |
|
|
438
|
-
| `file` | TEXT | URL de archivo (GCS) | `maxSize`, `allowedTypes` |
|
|
439
|
-
| `json` | JSONB | Objeto JSON | `schema` |
|
|
440
|
-
| `color` | VARCHAR(7) | Color hex | - |
|
|
441
|
-
| `rating` | SMALLINT | Puntuación 1-5 | `max` |
|
|
442
|
-
| `percentage` | NUMERIC | Porcentaje | `min`, `max` |
|
|
443
|
-
|
|
444
|
-
### Opciones de Campo
|
|
445
|
-
|
|
446
|
-
```json
|
|
447
|
-
{
|
|
448
|
-
"name": "campo",
|
|
449
|
-
"type": "text",
|
|
450
|
-
"label": "Etiqueta visible",
|
|
451
|
-
"required": true, // Campo obligatorio
|
|
452
|
-
"unique": true, // Valor único en la tabla
|
|
453
|
-
"default": "valor", // Valor por defecto
|
|
454
|
-
"searchable": true, // Incluir en búsqueda global
|
|
455
|
-
"hidden": false, // Ocultar en UI
|
|
456
|
-
"readonly": false, // Solo lectura
|
|
457
|
-
"computed": "expr", // Calculado (no editable)
|
|
458
|
-
"autoGenerate": "pattern", // Auto-generar valor
|
|
459
|
-
"validation": { // Validaciones custom
|
|
460
|
-
"pattern": "^[A-Z]{3}",
|
|
461
|
-
"message": "Debe ser 3 letras mayúsculas"
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
### Permisos por Rol
|
|
467
|
-
|
|
468
|
-
> **⚠️ IMPORTANTE**: Los permisos ya NO se definen en el JSON del módulo.
|
|
469
|
-
> Se gestionan exclusivamente via la columna `permissions` de `core.entities`.
|
|
470
|
-
|
|
471
|
-
Al crear una entidad nueva, el Admin obtiene `"*"` (full access) automáticamente.
|
|
472
|
-
Los permisos se gestionan desde la UI de permisos o directamente en la base de datos:
|
|
473
|
-
|
|
474
|
-
```sql
|
|
475
|
-
-- Ejemplo: Configurar permisos de una entidad
|
|
476
|
-
UPDATE core.entities
|
|
477
|
-
SET permissions = '{"Admin": "*", "Manager": "crud", "User": "read"}'::jsonb
|
|
478
|
-
WHERE id = 6420;
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
| Valor | Significado |
|
|
482
|
-
|-------|-------------|
|
|
483
|
-
| `"*"` | Full access |
|
|
484
|
-
| `"crud"` | Create, Read, Update, Delete |
|
|
485
|
-
| `"read"` | Solo lectura |
|
|
486
|
-
| `""` | Sin acceso |
|
|
487
|
-
|
|
488
|
-
---
|
|
489
|
-
|
|
490
|
-
## Automations (Código JavaScript)
|
|
491
|
-
|
|
492
|
-
Las automations son **funciones JavaScript** almacenadas en la base de datos que se ejecutan en respuesta a eventos.
|
|
493
|
-
|
|
494
|
-
### 💜 Nueva Arquitectura (2025)
|
|
495
|
-
|
|
496
|
-
Las automations se almacenan en **tres lugares que trabajan juntos**:
|
|
497
|
-
|
|
498
|
-
```
|
|
499
|
-
core.entities.automations (JSONB) ← FUENTE DE VERDAD
|
|
500
|
-
│ (configuración de triggers)
|
|
501
|
-
│
|
|
502
|
-
▼ (sync automático)
|
|
503
|
-
core.automation_meta ← ÍNDICE PARA WORKER
|
|
504
|
-
│ (sincronizado automáticamente)
|
|
505
|
-
│
|
|
506
|
-
▼ (referencias)
|
|
507
|
-
core.automation_scripts ← CÓDIGO JAVASCRIPT
|
|
508
|
-
(la función que se ejecuta)
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
### Dónde se almacena cada cosa
|
|
512
|
-
|
|
513
|
-
```sql
|
|
514
|
-
-- CONFIGURACIÓN: Qué automations tiene cada entity (triggers, enabled, etc.)
|
|
515
|
-
SELECT id, name, automations FROM core.entities WHERE id = 4589;
|
|
516
|
-
|
|
517
|
-
-- CÓDIGO: El JavaScript de cada automation
|
|
518
|
-
SELECT function_name, js_code FROM core.automation_scripts WHERE company_id = 116;
|
|
519
|
-
|
|
520
|
-
-- ÍNDICE: Lo que consulta el worker (sincronizado automáticamente)
|
|
521
|
-
SELECT * FROM core.automation_meta WHERE company_id = 116;
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
### Tipos de Triggers
|
|
525
|
-
|
|
526
|
-
| Trigger | Cuándo se ejecuta |
|
|
527
|
-
|---------|-------------------|
|
|
528
|
-
| `insert` | Al crear un registro |
|
|
529
|
-
| `update` | Al modificar un registro |
|
|
530
|
-
| `delete` | Al eliminar un registro |
|
|
531
|
-
| `click` / `click` | Botón manual en la UI |
|
|
532
|
-
| `scheduled` | Cron job (ej: "0 9 * * *") |
|
|
533
|
-
|
|
534
|
-
### Formato de entities.automations
|
|
535
|
-
|
|
536
|
-
```json
|
|
537
|
-
[
|
|
538
|
-
{
|
|
539
|
-
"trigger": "insert",
|
|
540
|
-
"function_name": "procesar_pedido",
|
|
541
|
-
"script_id": 42,
|
|
542
|
-
"enabled": true
|
|
543
|
-
},
|
|
544
|
-
{
|
|
545
|
-
"trigger": "click",
|
|
546
|
-
"function_name": "generar_factura",
|
|
547
|
-
"label": "Generar Factura",
|
|
548
|
-
"enabled": true
|
|
549
|
-
}
|
|
550
|
-
]
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
### Estructura de una Automation
|
|
554
|
-
|
|
555
|
-
```javascript
|
|
556
|
-
/**
|
|
557
|
-
* Función: procesar_pedido
|
|
558
|
-
* Trigger: update cuando estado = 'confirmado'
|
|
559
|
-
*
|
|
560
|
-
* @param {Object} context - Contexto de ejecución
|
|
561
|
-
* @param {Object} api - API para interactuar con el sistema
|
|
562
|
-
* @param {Object} logger - Logger para debugging
|
|
563
|
-
*/
|
|
564
|
-
async function procesar_pedido(context, api, logger) {
|
|
565
|
-
// ═══════════════════════════════════════════════════════════
|
|
566
|
-
// CONTEXT - Información del evento
|
|
567
|
-
// ═══════════════════════════════════════════════════════════
|
|
568
|
-
const {
|
|
569
|
-
company_id, // ID de la company (ej: 116)
|
|
570
|
-
module_id, // ID del módulo (ej: 308)
|
|
571
|
-
entity_id, // ID de la entidad/tabla (ej: 4136)
|
|
572
|
-
table, // Nombre físico de tabla (ej: "m308_t4136")
|
|
573
|
-
row, // Registro actual (después del cambio)
|
|
574
|
-
old_row, // Registro anterior (solo en update)
|
|
575
|
-
input, // Input del usuario (solo en click)
|
|
576
|
-
env, // Variables de entorno de la company
|
|
577
|
-
user // Usuario que disparó el evento
|
|
578
|
-
} = context;
|
|
579
|
-
|
|
580
|
-
logger.info('Procesando pedido', { pedido_id: row.id, estado: row.estado });
|
|
581
|
-
|
|
582
|
-
// ═══════════════════════════════════════════════════════════
|
|
583
|
-
// API.QUERY - Consultas SQL
|
|
584
|
-
// ═══════════════════════════════════════════════════════════
|
|
585
|
-
|
|
586
|
-
// Obtener líneas del pedido
|
|
587
|
-
const { rows: lineas } = await api.query(
|
|
588
|
-
`SELECT lp.*, p.nombre as producto_nombre, p.stock
|
|
589
|
-
FROM ${context.schema}.m308_t4137 lp -- lineas_pedido
|
|
590
|
-
JOIN ${context.schema}.m308_t4138 p ON p.id = lp.producto_id
|
|
591
|
-
WHERE lp.pedido_id = $1`,
|
|
592
|
-
[row.id]
|
|
593
|
-
);
|
|
594
|
-
|
|
595
|
-
// Actualizar stock de productos
|
|
596
|
-
for (const linea of lineas) {
|
|
597
|
-
await api.query(
|
|
598
|
-
`UPDATE ${context.schema}.m308_t4138
|
|
599
|
-
SET stock = stock - $1
|
|
600
|
-
WHERE id = $2`,
|
|
601
|
-
[linea.cantidad, linea.producto_id]
|
|
602
|
-
);
|
|
603
|
-
|
|
604
|
-
logger.debug('Stock actualizado', {
|
|
605
|
-
producto: linea.producto_nombre,
|
|
606
|
-
cantidad: linea.cantidad
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// ═══════════════════════════════════════════════════════════
|
|
611
|
-
// API.EMAIL - Enviar emails
|
|
612
|
-
// ═══════════════════════════════════════════════════════════
|
|
613
|
-
|
|
614
|
-
// Obtener datos del cliente
|
|
615
|
-
const { rows: [cliente] } = await api.query(
|
|
616
|
-
`SELECT * FROM ${context.schema}.m308_t4135 WHERE id = $1`,
|
|
617
|
-
[row.cliente_id]
|
|
618
|
-
);
|
|
619
|
-
|
|
620
|
-
await api.email({
|
|
621
|
-
to: cliente.email,
|
|
622
|
-
subject: `Pedido ${row.numero} confirmado`,
|
|
623
|
-
body: `
|
|
624
|
-
Hola ${cliente.nombre},
|
|
625
|
-
|
|
626
|
-
Tu pedido ${row.numero} ha sido confirmado.
|
|
627
|
-
|
|
628
|
-
Total: ${row.total}€
|
|
629
|
-
|
|
630
|
-
Gracias por tu compra.
|
|
631
|
-
`,
|
|
632
|
-
// O usar HTML:
|
|
633
|
-
html: `<h1>Pedido Confirmado</h1><p>...</p>`
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
// ═══════════════════════════════════════════════════════════
|
|
637
|
-
// API.HTTP - Llamadas a APIs externas
|
|
638
|
-
// ═══════════════════════════════════════════════════════════
|
|
639
|
-
|
|
640
|
-
// Webhook a sistema externo
|
|
641
|
-
if (env.WEBHOOK_URL) {
|
|
642
|
-
const response = await api.http(env.WEBHOOK_URL, {
|
|
643
|
-
method: 'POST',
|
|
644
|
-
headers: {
|
|
645
|
-
'Content-Type': 'application/json',
|
|
646
|
-
'Authorization': `Bearer ${env.WEBHOOK_TOKEN}`
|
|
647
|
-
},
|
|
648
|
-
body: JSON.stringify({
|
|
649
|
-
event: 'pedido_confirmado',
|
|
650
|
-
pedido: row,
|
|
651
|
-
cliente: cliente
|
|
652
|
-
})
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
logger.info('Webhook enviado', { status: response.status });
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// ═══════════════════════════════════════════════════════════
|
|
659
|
-
// RETURN - Resultado de la automation
|
|
660
|
-
// ═══════════════════════════════════════════════════════════
|
|
661
|
-
|
|
662
|
-
return {
|
|
663
|
-
success: true,
|
|
664
|
-
message: `Pedido ${row.numero} procesado correctamente`,
|
|
665
|
-
data: {
|
|
666
|
-
lineas_procesadas: lineas.length,
|
|
667
|
-
email_enviado: true
|
|
668
|
-
}
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
```
|
|
672
|
-
|
|
673
|
-
### Variables de Entorno (env)
|
|
674
|
-
|
|
675
|
-
Cada company puede tener variables de entorno configuradas en `core.company_env_variables`:
|
|
676
|
-
|
|
677
|
-
```javascript
|
|
678
|
-
// Acceso en automations
|
|
679
|
-
const apiKey = env.STRIPE_API_KEY;
|
|
680
|
-
const webhookUrl = env.SLACK_WEBHOOK;
|
|
681
|
-
const emailFrom = env.EMAIL_FROM || 'noreply@gufi.com';
|
|
682
|
-
```
|
|
683
|
-
|
|
684
|
-
### Automation con Input del Usuario (Click)
|
|
685
|
-
|
|
686
|
-
```javascript
|
|
687
|
-
// Definición en el JSON del módulo:
|
|
688
|
-
{
|
|
689
|
-
"trigger": "click",
|
|
690
|
-
"function_name": "enviar_recordatorio",
|
|
691
|
-
"label": "Enviar Recordatorio",
|
|
692
|
-
"icon": "Mail",
|
|
693
|
-
"input_schema": {
|
|
694
|
-
"mensaje": {
|
|
695
|
-
"type": "textarea",
|
|
696
|
-
"label": "Mensaje personalizado",
|
|
697
|
-
"required": true
|
|
698
|
-
},
|
|
699
|
-
"urgente": {
|
|
700
|
-
"type": "boolean",
|
|
701
|
-
"label": "Marcar como urgente",
|
|
702
|
-
"default": false
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
// La función recibe el input:
|
|
708
|
-
async function enviar_recordatorio(context, api, logger) {
|
|
709
|
-
const { row, input } = context;
|
|
710
|
-
|
|
711
|
-
// input.mensaje contiene el texto del usuario
|
|
712
|
-
// input.urgente contiene true/false
|
|
713
|
-
|
|
714
|
-
await api.email({
|
|
715
|
-
to: row.cliente_email,
|
|
716
|
-
subject: input.urgente ? '🚨 URGENTE: ' + row.numero : row.numero,
|
|
717
|
-
body: input.mensaje
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
return { success: true };
|
|
721
|
-
}
|
|
722
|
-
```
|
|
723
|
-
|
|
724
|
-
---
|
|
725
|
-
|
|
726
|
-
## Views (Vistas del Marketplace)
|
|
727
|
-
|
|
728
|
-
Las Views son **componentes React/TypeScript** que los consultores escriben en el navegador y se almacenan en PostgreSQL. Se ejecutan dinámicamente en el frontend.
|
|
729
|
-
|
|
730
|
-
### Dónde se almacenan
|
|
731
|
-
|
|
732
|
-
```sql
|
|
733
|
-
SELECT id, name, code FROM marketplace.views WHERE package_id = 14;
|
|
734
|
-
```
|
|
735
|
-
|
|
736
|
-
### Estructura de Archivos de una View
|
|
737
|
-
|
|
738
|
-
Cuando descargas una view con `gufi view:pull <id>`, obtienes:
|
|
739
|
-
|
|
740
|
-
```
|
|
741
|
-
~/gufi-dev/view_<id>/
|
|
742
|
-
├── index.tsx # Entry point - exporta featureConfig y default
|
|
743
|
-
├── types.ts # Interfaces TypeScript
|
|
744
|
-
│
|
|
745
|
-
├── core/
|
|
746
|
-
│ ├── dataProvider.ts # 💜 dataSources + featureConfig (MUY IMPORTANTE)
|
|
747
|
-
│ └── permissions.ts # 💜 Sistema de permisos dinámico
|
|
748
|
-
│
|
|
749
|
-
├── metadata/
|
|
750
|
-
│ ├── inputs.ts # 💜 Inputs configurables por usuario
|
|
751
|
-
│ ├── help.es.ts # Documentación en español
|
|
752
|
-
│ └── help.en.ts # Documentación en inglés
|
|
753
|
-
│
|
|
754
|
-
├── components/
|
|
755
|
-
│ ├── MiComponente.tsx # Componentes React
|
|
756
|
-
│ └── DevPermissionSwitcher.tsx # 💜 Testing de permisos en dev
|
|
757
|
-
│
|
|
758
|
-
├── views/
|
|
759
|
-
│ └── page.tsx # Página principal
|
|
760
|
-
│
|
|
761
|
-
└── automations/ # Opcional: automations de la vista
|
|
762
|
-
└── mi_automation.js
|
|
763
|
-
```
|
|
764
|
-
|
|
765
|
-
---
|
|
766
|
-
|
|
767
|
-
### 💜 dataSources - Declarar qué Tablas Necesita la Vista
|
|
768
|
-
|
|
769
|
-
El archivo `core/dataProvider.ts` define qué tablas necesita la vista.
|
|
770
|
-
|
|
771
|
-
**Arquitectura:**
|
|
772
|
-
1. Se declaran en código (`core/dataProvider.ts`)
|
|
773
|
-
2. Se editan con CLI: `gufi pull` → editar → `gufi push`
|
|
774
|
-
3. Developer Center los muestra como **solo lectura**
|
|
775
|
-
4. Al instalar la vista, el sistema mapea nombres lógicos automáticamente
|
|
776
|
-
|
|
777
|
-
```typescript
|
|
778
|
-
// core/dataProvider.ts
|
|
779
|
-
// 💜 Mi Vista - Data Sources & Feature Configuration
|
|
780
|
-
|
|
781
|
-
/* ============================================================================
|
|
782
|
-
💜 Data Sources - Tablas que necesita esta vista
|
|
783
|
-
============================================================================ */
|
|
784
|
-
export const dataSources = [
|
|
785
|
-
{
|
|
786
|
-
key: 'tareasTable', // Identificador único
|
|
787
|
-
type: 'table', // Siempre 'table'
|
|
788
|
-
label: { es: 'Tabla de Tareas', en: 'Tasks Table' },
|
|
789
|
-
description: {
|
|
790
|
-
es: 'Tabla principal de tareas',
|
|
791
|
-
en: 'Main tasks table'
|
|
792
|
-
},
|
|
793
|
-
required: true, // true = obligatorio
|
|
794
|
-
},
|
|
795
|
-
{
|
|
796
|
-
key: 'proyectosTable',
|
|
797
|
-
type: 'table',
|
|
798
|
-
label: { es: 'Proyectos', en: 'Projects' },
|
|
799
|
-
description: {
|
|
800
|
-
es: 'Tabla de proyectos (opcional)',
|
|
801
|
-
en: 'Projects table (optional)'
|
|
802
|
-
},
|
|
803
|
-
required: false, // false = opcional
|
|
804
|
-
},
|
|
805
|
-
{
|
|
806
|
-
key: 'comentariosTable',
|
|
807
|
-
type: 'table',
|
|
808
|
-
label: { es: 'Comentarios', en: 'Comments' },
|
|
809
|
-
description: {
|
|
810
|
-
es: 'Comentarios en tareas',
|
|
811
|
-
en: 'Task comments'
|
|
812
|
-
},
|
|
813
|
-
required: false,
|
|
814
|
-
},
|
|
815
|
-
] as const;
|
|
816
|
-
|
|
817
|
-
/* ============================================================================
|
|
818
|
-
💜 Feature Config - EXPORTAR SIEMPRE
|
|
819
|
-
============================================================================ */
|
|
820
|
-
import { featureInputs } from '../metadata/inputs';
|
|
821
|
-
|
|
822
|
-
export const featureConfig = {
|
|
823
|
-
dataSources,
|
|
824
|
-
inputs: featureInputs,
|
|
825
|
-
};
|
|
826
|
-
|
|
827
|
-
// También exporta funciones de carga de datos...
|
|
828
|
-
```
|
|
829
|
-
|
|
830
|
-
**En el index.tsx:**
|
|
831
|
-
```typescript
|
|
832
|
-
// index.tsx
|
|
833
|
-
export { featureConfig } from './core/dataProvider';
|
|
834
|
-
export default function MiVista({ gufi }) { ... }
|
|
835
|
-
```
|
|
836
|
-
|
|
837
|
-
**Cómo acceder a las tablas configuradas:**
|
|
838
|
-
```typescript
|
|
839
|
-
export default function MiVista({ gufi }) {
|
|
840
|
-
const viewSpec = gufi?.context?.viewSpec || {};
|
|
841
|
-
|
|
842
|
-
// Keys de dataSources → NOMBRES LÓGICOS (nunca IDs físicos)
|
|
843
|
-
const tareasTable = viewSpec.tareasTable; // "tareas.asignaciones"
|
|
844
|
-
const proyectosTable = viewSpec.proyectosTable; // "tareas.proyectos" o undefined
|
|
845
|
-
|
|
846
|
-
// Usar para queries
|
|
847
|
-
const res = await gufi.dataProvider.getList({
|
|
848
|
-
resource: tareasTable,
|
|
849
|
-
pagination: { current: 1, pageSize: 100 },
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
```
|
|
853
|
-
|
|
854
|
-
---
|
|
855
|
-
|
|
856
|
-
### 💜 inputs (featureInputs) - Configuración del Usuario
|
|
857
|
-
|
|
858
|
-
Permite que el usuario configure opciones de la vista sin tocar código.
|
|
859
|
-
|
|
860
|
-
```typescript
|
|
861
|
-
// metadata/inputs.ts
|
|
862
|
-
// 💜 Mi Vista - Inputs Configurables
|
|
863
|
-
|
|
864
|
-
export const featureInputs = [
|
|
865
|
-
// BOOLEAN - Checkbox
|
|
866
|
-
{
|
|
867
|
-
key: 'showCompleted',
|
|
868
|
-
type: 'boolean',
|
|
869
|
-
label: { es: 'Mostrar completadas', en: 'Show completed' },
|
|
870
|
-
description: {
|
|
871
|
-
es: 'Incluir tareas completadas',
|
|
872
|
-
en: 'Include completed tasks'
|
|
873
|
-
},
|
|
874
|
-
default: false,
|
|
875
|
-
},
|
|
876
|
-
|
|
877
|
-
// SELECT - Dropdown estático
|
|
878
|
-
{
|
|
879
|
-
key: 'defaultView',
|
|
880
|
-
type: 'select',
|
|
881
|
-
label: { es: 'Vista por defecto', en: 'Default view' },
|
|
882
|
-
description: {
|
|
883
|
-
es: 'Panel inicial al abrir',
|
|
884
|
-
en: 'Initial panel on open'
|
|
885
|
-
},
|
|
886
|
-
options: [
|
|
887
|
-
{ value: 'list', label: { es: 'Lista', en: 'List' } },
|
|
888
|
-
{ value: 'kanban', label: { es: 'Kanban', en: 'Kanban' } },
|
|
889
|
-
{ value: 'calendar', label: { es: 'Calendario', en: 'Calendar' } },
|
|
890
|
-
],
|
|
891
|
-
default: 'list',
|
|
892
|
-
},
|
|
893
|
-
|
|
894
|
-
// NUMBER - Input numérico
|
|
895
|
-
{
|
|
896
|
-
key: 'lowStockThreshold',
|
|
897
|
-
type: 'number',
|
|
898
|
-
label: { es: 'Umbral stock bajo (%)', en: 'Low stock threshold (%)' },
|
|
899
|
-
description: {
|
|
900
|
-
es: 'Porcentaje para alertas',
|
|
901
|
-
en: 'Percentage for alerts'
|
|
902
|
-
},
|
|
903
|
-
placeholder: '40',
|
|
904
|
-
default: 40,
|
|
905
|
-
},
|
|
906
|
-
|
|
907
|
-
// TEXT - Input de texto
|
|
908
|
-
{
|
|
909
|
-
key: 'emailCc',
|
|
910
|
-
type: 'text',
|
|
911
|
-
label: { es: 'Email CC', en: 'CC Email' },
|
|
912
|
-
description: {
|
|
913
|
-
es: 'Email en copia',
|
|
914
|
-
en: 'CC email'
|
|
915
|
-
},
|
|
916
|
-
placeholder: 'admin@empresa.com',
|
|
917
|
-
},
|
|
918
|
-
|
|
919
|
-
// SELECT con opciones de otra tabla (dinámico)
|
|
920
|
-
{
|
|
921
|
-
key: 'defaultWarehouse',
|
|
922
|
-
type: 'select',
|
|
923
|
-
label: { es: 'Almacén por defecto', en: 'Default warehouse' },
|
|
924
|
-
description: {
|
|
925
|
-
es: 'Almacén seleccionado al abrir',
|
|
926
|
-
en: 'Warehouse on open'
|
|
927
|
-
},
|
|
928
|
-
optionsFrom: 'warehousesTable', // 💜 Carga opciones del dataSource
|
|
929
|
-
default: 'all',
|
|
930
|
-
},
|
|
931
|
-
] as const;
|
|
932
|
-
```
|
|
933
|
-
|
|
934
|
-
**Acceder a inputs en runtime:**
|
|
935
|
-
```typescript
|
|
936
|
-
export default function MiVista({ gufi }) {
|
|
937
|
-
const viewSpec = gufi?.context?.viewSpec || {};
|
|
938
|
-
|
|
939
|
-
// Los inputs están directamente en viewSpec
|
|
940
|
-
const showCompleted = viewSpec.showCompleted ?? false;
|
|
941
|
-
const defaultView = viewSpec.defaultView ?? 'list';
|
|
942
|
-
const threshold = viewSpec.lowStockThreshold ?? 40;
|
|
943
|
-
const emailCc = viewSpec.emailCc;
|
|
944
|
-
}
|
|
945
|
-
```
|
|
946
|
-
|
|
947
|
-
---
|
|
948
|
-
|
|
949
|
-
### 💜 help - Documentación para Usuarios
|
|
950
|
-
|
|
951
|
-
Crea archivos de ayuda que aparecen en el botón "?" de la vista.
|
|
952
|
-
|
|
953
|
-
```typescript
|
|
954
|
-
// metadata/help.es.ts
|
|
955
|
-
export const help = {
|
|
956
|
-
// Para Admin/Consultant
|
|
957
|
-
consultant: {
|
|
958
|
-
title: "Mi Vista",
|
|
959
|
-
description: "Descripción breve de la vista.",
|
|
960
|
-
sections: [
|
|
961
|
-
{
|
|
962
|
-
id: "overview",
|
|
963
|
-
title: "Descripción General",
|
|
964
|
-
content: `
|
|
965
|
-
**Funcionalidades:**
|
|
966
|
-
- Lista de tareas con filtros
|
|
967
|
-
- Vista Kanban por proyecto
|
|
968
|
-
- Sistema de comentarios
|
|
969
|
-
|
|
970
|
-
**Tablas requeridas:**
|
|
971
|
-
| DataSource | Descripción |
|
|
972
|
-
|------------|-------------|
|
|
973
|
-
| tareasTable | Tabla principal de tareas |
|
|
974
|
-
| proyectosTable | Proyectos (opcional) |
|
|
975
|
-
`.trim(),
|
|
976
|
-
},
|
|
977
|
-
{
|
|
978
|
-
id: "configuration",
|
|
979
|
-
title: "Configuración",
|
|
980
|
-
content: `
|
|
981
|
-
**Campos requeridos en tareas:**
|
|
982
|
-
- titulo (text)
|
|
983
|
-
- estado (select: pendiente, en_progreso, completada)
|
|
984
|
-
- asignado_a (users)
|
|
985
|
-
- proyecto_id (relation, opcional)
|
|
986
|
-
`.trim(),
|
|
987
|
-
},
|
|
988
|
-
{
|
|
989
|
-
id: "permissions",
|
|
990
|
-
title: "Permisos",
|
|
991
|
-
content: `
|
|
992
|
-
| Permiso | Efecto |
|
|
993
|
-
|---------|--------|
|
|
994
|
-
| * | Acceso completo |
|
|
995
|
-
| entity:view | Solo lectura |
|
|
996
|
-
| create_tasks | Puede crear tareas |
|
|
997
|
-
`.trim(),
|
|
998
|
-
},
|
|
999
|
-
],
|
|
1000
|
-
},
|
|
1001
|
-
|
|
1002
|
-
// Para usuarios finales
|
|
1003
|
-
user: {
|
|
1004
|
-
title: "Guía de Uso",
|
|
1005
|
-
sections: [
|
|
1006
|
-
{
|
|
1007
|
-
id: "basics",
|
|
1008
|
-
title: "Uso Básico",
|
|
1009
|
-
content: `
|
|
1010
|
-
1. **Mis Tareas**: Tareas asignadas a ti
|
|
1011
|
-
2. **Proyectos**: Vista Kanban por proyecto
|
|
1012
|
-
3. **Gestión**: Tareas que has asignado
|
|
1013
|
-
|
|
1014
|
-
Para crear tarea, pulsa el botón **+**
|
|
1015
|
-
`.trim(),
|
|
1016
|
-
},
|
|
1017
|
-
],
|
|
1018
|
-
},
|
|
1019
|
-
};
|
|
1020
|
-
```
|
|
1021
|
-
|
|
1022
|
-
---
|
|
1023
|
-
|
|
1024
|
-
### 💜 permissions - Sistema de Permisos Explícitos (Sin Wildcards)
|
|
1025
|
-
|
|
1026
|
-
Las vistas usan un sistema de **permisos explícitos** - sin wildcards, sin magia. Cada permiso es un string específico que existe o no en el array de permisos del usuario.
|
|
1027
|
-
|
|
1028
|
-
> ⚠️ **Importante**: Eliminamos el soporte de wildcards (`*`) porque causaba problemas de integridad entre el estado de la UI y los permisos reales. Siempre usa strings de permisos explícitos.
|
|
1029
|
-
|
|
1030
|
-
**1. Declarar permisos en `metadata/permissions.ts`:**
|
|
1031
|
-
```typescript
|
|
1032
|
-
// metadata/permissions.ts
|
|
1033
|
-
export const permissions = [
|
|
1034
|
-
// Permisos estáticos - toggles simples
|
|
1035
|
-
{
|
|
1036
|
-
key: 'send_orders',
|
|
1037
|
-
label: { es: 'Enviar pedidos', en: 'Send orders' },
|
|
1038
|
-
description: { es: 'Puede enviar pedidos a proveedores', en: 'Can send orders' },
|
|
1039
|
-
},
|
|
1040
|
-
{
|
|
1041
|
-
key: 'view_costs',
|
|
1042
|
-
label: { es: 'Ver costos', en: 'View costs' },
|
|
1043
|
-
description: { es: 'Puede ver precios de compra', en: 'Can see purchase prices' },
|
|
1044
|
-
},
|
|
1045
|
-
|
|
1046
|
-
// Permisos dinámicos - se resuelven en runtime
|
|
1047
|
-
{
|
|
1048
|
-
key: 'warehouse:*', // Placeholder, se resuelve a warehouse:madrid, warehouse:barcelona, etc.
|
|
1049
|
-
label: { es: 'Acceso a almacén', en: 'Warehouse access' },
|
|
1050
|
-
description: { es: 'Almacenes a los que tiene acceso', en: 'Warehouses user can access' },
|
|
1051
|
-
dynamic: true, // Marca este como dinámico
|
|
1052
|
-
},
|
|
1053
|
-
] as const;
|
|
1054
|
-
```
|
|
1055
|
-
|
|
1056
|
-
**2. Crear `core/permissions.ts` (sin wildcards):**
|
|
1057
|
-
```typescript
|
|
1058
|
-
// core/permissions.ts
|
|
1059
|
-
// SIN WILDCARDS - solo permisos explícitos
|
|
1060
|
-
|
|
1061
|
-
export interface PermissionsConfig {
|
|
1062
|
-
userPermissions: string[]; // Permisos del usuario (resueltos desde su rol)
|
|
1063
|
-
isDevMode: boolean;
|
|
1064
|
-
devPermissions: string[]; // Override en modo dev para testing
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
// Obtener permisos efectivos (dev mode override)
|
|
1068
|
-
function getEffectivePermissions(config: PermissionsConfig): string[] {
|
|
1069
|
-
const { userPermissions, isDevMode, devPermissions } = config;
|
|
1070
|
-
return (isDevMode && devPermissions.length > 0) ? devPermissions : userPermissions;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
// Check simple - ¿tiene exactamente este permiso?
|
|
1074
|
-
export function hasPermission(config: PermissionsConfig, permission: string): boolean {
|
|
1075
|
-
const perms = getEffectivePermissions(config);
|
|
1076
|
-
return perms.includes(permission);
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
// Helpers específicos de la vista
|
|
1080
|
-
export function canSendOrders(config: PermissionsConfig): boolean {
|
|
1081
|
-
return hasPermission(config, 'send_orders');
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
export function canViewCosts(config: PermissionsConfig): boolean {
|
|
1085
|
-
return hasPermission(config, 'view_costs');
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
// Obtener warehouses permitidos (null si no hay restricciones)
|
|
1089
|
-
export function getAllowedWarehouses(config: PermissionsConfig): string[] | null {
|
|
1090
|
-
const perms = getEffectivePermissions(config);
|
|
1091
|
-
|
|
1092
|
-
const warehouses: string[] = [];
|
|
1093
|
-
for (const perm of perms) {
|
|
1094
|
-
if (perm.startsWith('warehouse:')) {
|
|
1095
|
-
warehouses.push(perm.slice('warehouse:'.length));
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
return warehouses.length > 0 ? warehouses : null;
|
|
1100
|
-
}
|
|
1101
|
-
```
|
|
1102
|
-
|
|
1103
|
-
**3. Usar en el componente con inicialización correcta:**
|
|
1104
|
-
```typescript
|
|
1105
|
-
import { useState, useEffect, useRef } from 'react';
|
|
1106
|
-
import { DevPermissionSwitcher } from '../components/DevPermissionSwitcher';
|
|
1107
|
-
import { canSendOrders, getAllowedWarehouses } from '../core/permissions';
|
|
1108
|
-
|
|
1109
|
-
export default function MiVista({ gufi }) {
|
|
1110
|
-
const lang = gufi?.context?.lang || 'es';
|
|
1111
|
-
const isDevMode = gufi?.context?.isPreview || window.location.hostname === 'localhost';
|
|
1112
|
-
|
|
1113
|
-
// 💜 IMPORTANTE: Inicializar vacío, poblar cuando los datos carguen
|
|
1114
|
-
const [devPermissions, setDevPermissions] = useState<string[]>([]);
|
|
1115
|
-
const devPermissionsInitialized = useRef(false);
|
|
1116
|
-
|
|
1117
|
-
// Cargar warehouses de tu data source
|
|
1118
|
-
const [warehouses, setWarehouses] = useState([]);
|
|
1119
|
-
|
|
1120
|
-
// Inicializar permisos cuando warehouses carguen (una sola vez)
|
|
1121
|
-
useEffect(() => {
|
|
1122
|
-
if (isDevMode && !devPermissionsInitialized.current && warehouses.length > 0) {
|
|
1123
|
-
devPermissionsInitialized.current = true;
|
|
1124
|
-
|
|
1125
|
-
// Otorgar todos los permisos explícitos
|
|
1126
|
-
const allPerms = [
|
|
1127
|
-
'send_orders',
|
|
1128
|
-
'view_costs',
|
|
1129
|
-
...warehouses.map(w => `warehouse:${w.name.toLowerCase()}`),
|
|
1130
|
-
];
|
|
1131
|
-
setDevPermissions(allPerms);
|
|
1132
|
-
}
|
|
1133
|
-
}, [isDevMode, warehouses]);
|
|
1134
|
-
|
|
1135
|
-
// Construir config de permisos
|
|
1136
|
-
const permissionsConfig = {
|
|
1137
|
-
userPermissions: gufi?.context?.user?.permissions || [],
|
|
1138
|
-
isDevMode,
|
|
1139
|
-
devPermissions,
|
|
1140
|
-
};
|
|
1141
|
-
|
|
1142
|
-
// Usar helpers de permisos
|
|
1143
|
-
const showCosts = canViewCosts(permissionsConfig);
|
|
1144
|
-
const allowedWarehouses = getAllowedWarehouses(permissionsConfig);
|
|
1145
|
-
|
|
1146
|
-
return (
|
|
1147
|
-
<div>
|
|
1148
|
-
{showCosts && <CostsColumn />}
|
|
1149
|
-
|
|
1150
|
-
{/* DevPermissionSwitcher - solo en dev mode */}
|
|
1151
|
-
{isDevMode && (
|
|
1152
|
-
<DevPermissionSwitcher
|
|
1153
|
-
lang={lang}
|
|
1154
|
-
activePermissions={devPermissions}
|
|
1155
|
-
onPermissionsChange={setDevPermissions}
|
|
1156
|
-
dynamicValues={{
|
|
1157
|
-
warehouse: warehouses.map(w => w.name.toLowerCase())
|
|
1158
|
-
}}
|
|
1159
|
-
/>
|
|
1160
|
-
)}
|
|
1161
|
-
</div>
|
|
1162
|
-
);
|
|
1163
|
-
}
|
|
1164
|
-
```
|
|
1165
|
-
|
|
1166
|
-
**DevPermissionSwitcher**: Botón flotante **purple** (💜 Gufi style) que permite:
|
|
1167
|
-
- Toggle individual de permisos on/off
|
|
1168
|
-
- Botón "Todos" en el header para acceso completo
|
|
1169
|
-
- Permisos dinámicos expandibles (ej: warehouses individuales)
|
|
1170
|
-
- Integridad total: UI siempre refleja el estado real
|
|
1171
|
-
|
|
1172
|
-
### 💜 DevPermissionSwitcher AUTOMÁTICO (LivePreviewPage)
|
|
1173
|
-
|
|
1174
|
-
**¡Ya no necesitas incluir DevPermissionSwitcher en tu código!** LivePreviewPage detecta automáticamente si tu vista tiene `featureConfig.permissions` y muestra el botón DEV.
|
|
1175
|
-
|
|
1176
|
-
**Cómo funciona:**
|
|
1177
|
-
1. Declara permisos en `metadata/permissions.ts`
|
|
1178
|
-
2. Expórtalos en `featureConfig`:
|
|
1179
|
-
```typescript
|
|
1180
|
-
// core/dataProvider.ts
|
|
1181
|
-
import { permissions } from '../metadata/permissions';
|
|
1182
|
-
|
|
1183
|
-
export const featureConfig = {
|
|
1184
|
-
dataSources,
|
|
1185
|
-
inputs: featureInputs,
|
|
1186
|
-
permissions, // 💜 Solo añadir esto
|
|
1187
|
-
};
|
|
1188
|
-
```
|
|
1189
|
-
3. LivePreviewPage lo detecta y muestra el botón DEV automáticamente
|
|
1190
|
-
|
|
1191
|
-
**Usar devPermissions del contexto:**
|
|
1192
|
-
```typescript
|
|
1193
|
-
export default function MiVista({ gufi }) {
|
|
1194
|
-
// 💜 LivePreviewPage provee devPermissions automáticamente
|
|
1195
|
-
const effectiveDevPermissions = gufi?.context?.devPermissions || [];
|
|
1196
|
-
|
|
1197
|
-
const permissionsConfig = {
|
|
1198
|
-
userPermissions: gufi?.context?.userPermissions || [],
|
|
1199
|
-
isDevMode: gufi?.context?.isPreview || gufi?.context?.isDev,
|
|
1200
|
-
devPermissions: effectiveDevPermissions,
|
|
1201
|
-
};
|
|
1202
|
-
|
|
1203
|
-
// Usar helpers de permisos normalmente
|
|
1204
|
-
const canSend = hasPermission(permissionsConfig, 'send_orders');
|
|
1205
|
-
}
|
|
1206
|
-
```
|
|
1207
|
-
|
|
1208
|
-
**Cuándo incluir DevPermissionSwitcher manualmente:**
|
|
1209
|
-
- Solo si la vista se ejecuta fuera de LivePreviewPage (ej: vista nativa del frontend)
|
|
1210
|
-
- Para vistas del marketplace en Developer Center → **NO necesario**, es automático
|
|
1211
|
-
|
|
1212
|
-
**Principio clave**: Inicializar con `[]` vacío, luego usar `useEffect` para otorgar todos los permisos cuando los valores dinámicos (como warehouses) terminen de cargar. Esto garantiza integridad UI.
|
|
1213
|
-
|
|
1214
|
-
---
|
|
1215
|
-
|
|
1216
|
-
### 💜 automations en Vistas
|
|
1217
|
-
|
|
1218
|
-
Las vistas pueden incluir automations que se ejecutan al hacer click.
|
|
1219
|
-
|
|
1220
|
-
**Archivo de automation:**
|
|
1221
|
-
```javascript
|
|
1222
|
-
// automations/send_notification.js
|
|
1223
|
-
/**
|
|
1224
|
-
* 💜 Automation: send_notification
|
|
1225
|
-
* Trigger: CLICK (desde la vista)
|
|
1226
|
-
*/
|
|
1227
|
-
async function send_notification(context, api, logger) {
|
|
1228
|
-
const { input, env } = context;
|
|
1229
|
-
const { tarea_id, mensaje } = input;
|
|
1230
|
-
|
|
1231
|
-
// Obtener tarea
|
|
1232
|
-
const { rows } = await api.query(
|
|
1233
|
-
'SELECT titulo, asignado_a FROM tareas WHERE id = $1',
|
|
1234
|
-
[tarea_id]
|
|
1235
|
-
);
|
|
1236
|
-
|
|
1237
|
-
// Obtener email del asignado
|
|
1238
|
-
const { rows: users } = await api.query(
|
|
1239
|
-
'SELECT email FROM core.users WHERE id = $1',
|
|
1240
|
-
[rows[0].asignado_a]
|
|
1241
|
-
);
|
|
1242
|
-
|
|
1243
|
-
// Enviar email
|
|
1244
|
-
await api.email({
|
|
1245
|
-
to: users[0].email,
|
|
1246
|
-
subject: `Notificación: ${rows[0].titulo}`,
|
|
1247
|
-
html: `<p>${mensaje}</p>`,
|
|
1248
|
-
cc: env.NOTIFICATION_CC,
|
|
1249
|
-
});
|
|
1250
|
-
|
|
1251
|
-
logger.info('Notificación enviada', { tarea_id });
|
|
1252
|
-
return { success: true };
|
|
1253
|
-
}
|
|
1254
|
-
```
|
|
1255
|
-
|
|
1256
|
-
**Llamar automation desde React:**
|
|
1257
|
-
```typescript
|
|
1258
|
-
const handleNotify = async (tareaId: number) => {
|
|
1259
|
-
try {
|
|
1260
|
-
const result = await gufi.utils.runClickAutomation({
|
|
1261
|
-
functionName: 'send_notification',
|
|
1262
|
-
input: {
|
|
1263
|
-
tarea_id: tareaId,
|
|
1264
|
-
mensaje: 'Tu tarea ha sido actualizada',
|
|
1265
|
-
},
|
|
1266
|
-
});
|
|
1267
|
-
|
|
1268
|
-
if (result.success) {
|
|
1269
|
-
gufi.utils.toastSuccess('Notificación enviada');
|
|
1270
|
-
}
|
|
1271
|
-
} catch (err) {
|
|
1272
|
-
gufi.utils.toastError(err.message);
|
|
1273
|
-
}
|
|
1274
|
-
};
|
|
1275
|
-
```
|
|
1276
|
-
|
|
1277
|
-
---
|
|
1278
|
-
|
|
1279
|
-
### 💜 El Objeto `gufi` - Props Completas
|
|
1280
|
-
|
|
1281
|
-
Toda vista recibe `gufi` con todo lo necesario:
|
|
1282
|
-
|
|
1283
|
-
```typescript
|
|
1284
|
-
interface GufiProps {
|
|
1285
|
-
context: {
|
|
1286
|
-
user: { id, email, name, role };
|
|
1287
|
-
lang: 'es' | 'en';
|
|
1288
|
-
viewSpec: {
|
|
1289
|
-
// dataSources configurados
|
|
1290
|
-
tareasTable: string;
|
|
1291
|
-
proyectosTable?: string;
|
|
1292
|
-
// inputs configurados
|
|
1293
|
-
showCompleted: boolean;
|
|
1294
|
-
defaultView: string;
|
|
1295
|
-
lowStockThreshold: number;
|
|
1296
|
-
};
|
|
1297
|
-
companyId: number;
|
|
1298
|
-
moduleId: number;
|
|
1299
|
-
entityId: number;
|
|
1300
|
-
};
|
|
1301
|
-
|
|
1302
|
-
dataProvider: {
|
|
1303
|
-
getList({ resource, pagination, filters, sorters });
|
|
1304
|
-
getOne({ resource, id });
|
|
1305
|
-
create({ resource, variables });
|
|
1306
|
-
update({ resource, id, variables });
|
|
1307
|
-
deleteOne({ resource, id });
|
|
1308
|
-
};
|
|
1309
|
-
|
|
1310
|
-
utils: {
|
|
1311
|
-
toastSuccess(msg: string);
|
|
1312
|
-
toastError(msg: string);
|
|
1313
|
-
toastInfo(msg: string);
|
|
1314
|
-
runClickAutomation({ functionName, input });
|
|
1315
|
-
};
|
|
1316
|
-
}
|
|
1317
|
-
```
|
|
1318
|
-
|
|
1319
|
-
---
|
|
1320
|
-
|
|
1321
|
-
### 💜 seedData - Datos de Ejemplo para Demo/Testing
|
|
1322
|
-
|
|
1323
|
-
Las vistas pueden incluir datos de ejemplo que se cargan automáticamente al instalar. Esto es útil para demos, testing, y onboarding de nuevos usuarios.
|
|
1324
|
-
|
|
1325
|
-
```typescript
|
|
1326
|
-
// metadata/seedData.ts
|
|
1327
|
-
export interface SeedDataConfig {
|
|
1328
|
-
description: { es: string; en: string };
|
|
1329
|
-
data: { [dataSourceKey: string]: Array<Record<string, any>> };
|
|
1330
|
-
order: string[]; // Orden de creación (importante para referencias)
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
export const seedData: SeedDataConfig = {
|
|
1334
|
-
description: {
|
|
1335
|
-
es: 'Crea empresas de ejemplo (Singular, FitVending), proyectos y tareas de prueba',
|
|
1336
|
-
en: 'Creates sample companies, projects and test tasks',
|
|
1337
|
-
},
|
|
1338
|
-
|
|
1339
|
-
// Orden de creación - tablas con referencias van después
|
|
1340
|
-
order: ['empresasTable', 'proyectosTable', 'tareasTable'],
|
|
1341
|
-
|
|
1342
|
-
data: {
|
|
1343
|
-
// Empresas/Clientes
|
|
1344
|
-
empresasTable: [
|
|
1345
|
-
{
|
|
1346
|
-
nombre: 'Singular',
|
|
1347
|
-
contacto: 'Contacto Singular',
|
|
1348
|
-
email: 'contacto@singular.es',
|
|
1349
|
-
estado: 'activo'
|
|
1350
|
-
},
|
|
1351
|
-
{
|
|
1352
|
-
nombre: 'FitVending',
|
|
1353
|
-
contacto: 'Equipo FitVending',
|
|
1354
|
-
email: 'info@fitvending.es',
|
|
1355
|
-
estado: 'activo'
|
|
1356
|
-
},
|
|
1357
|
-
],
|
|
1358
|
-
|
|
1359
|
-
// Proyectos
|
|
1360
|
-
proyectosTable: [
|
|
1361
|
-
{
|
|
1362
|
-
nombre: 'Gufi ERP',
|
|
1363
|
-
descripcion: 'Desarrollo del ERP Gufi',
|
|
1364
|
-
color: '#8B5CF6',
|
|
1365
|
-
estado: 'activo',
|
|
1366
|
-
},
|
|
1367
|
-
],
|
|
1368
|
-
|
|
1369
|
-
// Tareas - con referencias a otras tablas
|
|
1370
|
-
tareasTable: [
|
|
1371
|
-
{
|
|
1372
|
-
titulo: 'Revisar bug crítico',
|
|
1373
|
-
descripcion: 'Los usuarios reportan problemas',
|
|
1374
|
-
asignado_a: '@currentUser', // 💜 Token especial: usuario actual
|
|
1375
|
-
asignado_por: '@currentUser',
|
|
1376
|
-
prioridad: 'urgente',
|
|
1377
|
-
estado: 'pendiente',
|
|
1378
|
-
fecha_limite: '@today', // 💜 Token especial: fecha de hoy
|
|
1379
|
-
proyecto_id: '@ref:proyectosTable.0', // 💜 Referencia: ID del primer proyecto
|
|
1380
|
-
empresa_id: '@ref:empresasTable.0', // 💜 Referencia: ID de primera empresa
|
|
1381
|
-
},
|
|
1382
|
-
{
|
|
1383
|
-
titulo: 'Preparar demo',
|
|
1384
|
-
descripcion: 'Demo del módulo de facturación',
|
|
1385
|
-
asignado_a: '@currentUser',
|
|
1386
|
-
prioridad: 'alta',
|
|
1387
|
-
fecha_limite: '@tomorrow', // 💜 Token especial: mañana
|
|
1388
|
-
empresa_id: '@ref:empresasTable.1',
|
|
1389
|
-
},
|
|
1390
|
-
],
|
|
1391
|
-
},
|
|
1392
|
-
};
|
|
1393
|
-
```
|
|
1394
|
-
|
|
1395
|
-
**Tokens Especiales:**
|
|
1396
|
-
| Token | Descripción |
|
|
1397
|
-
|-------|-------------|
|
|
1398
|
-
| `@currentUser` | ID del usuario actual (para campos users) |
|
|
1399
|
-
| `@today` | Fecha de hoy (YYYY-MM-DD) |
|
|
1400
|
-
| `@tomorrow` | Fecha de mañana |
|
|
1401
|
-
| `@nextWeek` | Fecha dentro de 7 días |
|
|
1402
|
-
| `@ref:tableKey.index` | ID del registro creado en otra tabla |
|
|
1403
|
-
|
|
1404
|
-
**Agregar a featureConfig:**
|
|
1405
|
-
```typescript
|
|
1406
|
-
// core/dataProvider.ts
|
|
1407
|
-
import { seedData } from '../metadata/seedData';
|
|
1408
|
-
|
|
1409
|
-
export const featureConfig = {
|
|
1410
|
-
dataSources,
|
|
1411
|
-
inputs: featureInputs,
|
|
1412
|
-
seedData, // 💜 Agregar aquí
|
|
1413
|
-
};
|
|
1414
|
-
```
|
|
1415
|
-
|
|
1416
|
-
**Cargar desde Developer Center:**
|
|
1417
|
-
En la página de edición de vista, el Developer Center muestra un botón "Load Sample Data" que ejecuta el seedData en la company seleccionada.
|
|
1418
|
-
|
|
1419
|
-
---
|
|
1420
|
-
|
|
1421
|
-
### 💜 Checklist para Nueva Vista
|
|
1422
|
-
|
|
1423
|
-
1. [ ] `core/dataProvider.ts` - dataSources + export featureConfig
|
|
1424
|
-
2. [ ] `metadata/inputs.ts` - featureInputs configurables
|
|
1425
|
-
3. [ ] `metadata/seedData.ts` - Datos de ejemplo (opcional pero recomendado)
|
|
1426
|
-
4. [ ] `metadata/help.es.ts` y `help.en.ts` - Documentación
|
|
1427
|
-
5. [ ] `index.tsx` - export featureConfig y default component
|
|
1428
|
-
6. [ ] Usar `gufi?.context?.viewSpec` para tablas e inputs
|
|
1429
|
-
7. [ ] Usar `gufi?.dataProvider` para CRUD
|
|
1430
|
-
8. [ ] Usar `gufi?.utils?.toast*` para notificaciones
|
|
1431
|
-
9. [ ] Si hay automation: carpeta `automations/` con .js
|
|
1432
|
-
|
|
1433
|
-
### View.tsx - Componente Principal
|
|
1434
|
-
|
|
1435
|
-
```tsx
|
|
1436
|
-
import React, { useState, useEffect } from 'react';
|
|
1437
|
-
import { useList, useUpdate, useCreate } from '@refinedev/core';
|
|
1438
|
-
|
|
1439
|
-
// ═══════════════════════════════════════════════════════════════
|
|
1440
|
-
// PROPS que recibe toda View
|
|
1441
|
-
// ═══════════════════════════════════════════════════════════════
|
|
1442
|
-
interface ViewProps {
|
|
1443
|
-
// Configuración de la vista
|
|
1444
|
-
viewSpec: {
|
|
1445
|
-
id: number;
|
|
1446
|
-
name: string;
|
|
1447
|
-
config: Record<string, any>; // Config guardada por el usuario
|
|
1448
|
-
};
|
|
1449
|
-
|
|
1450
|
-
// Contexto de la company
|
|
1451
|
-
context: {
|
|
1452
|
-
companyId: number;
|
|
1453
|
-
moduleId: number;
|
|
1454
|
-
entityId: number;
|
|
1455
|
-
};
|
|
1456
|
-
|
|
1457
|
-
// Usuario actual
|
|
1458
|
-
user: {
|
|
1459
|
-
id: number;
|
|
1460
|
-
email: string;
|
|
1461
|
-
name: string;
|
|
1462
|
-
role: string;
|
|
1463
|
-
};
|
|
1464
|
-
|
|
1465
|
-
// Notificaciones
|
|
1466
|
-
toastSuccess: (message: string) => void;
|
|
1467
|
-
toastError: (message: string) => void;
|
|
1468
|
-
toastInfo: (message: string) => void;
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
// ═══════════════════════════════════════════════════════════════
|
|
1472
|
-
// COMPONENTE PRINCIPAL
|
|
1473
|
-
// ═══════════════════════════════════════════════════════════════
|
|
1474
|
-
export default function MiVista({ viewSpec, context, user, toastSuccess, toastError }: ViewProps) {
|
|
1475
|
-
const [filtro, setFiltro] = useState('todos');
|
|
1476
|
-
|
|
1477
|
-
// ─────────────────────────────────────────────────────────────
|
|
1478
|
-
// useList - Obtener lista de registros
|
|
1479
|
-
// ─────────────────────────────────────────────────────────────
|
|
1480
|
-
const { data, isLoading, refetch } = useList({
|
|
1481
|
-
resource: 'm360_t16192', // Nombre físico de la tabla
|
|
1482
|
-
pagination: { current: 1, pageSize: 100 },
|
|
1483
|
-
filters: filtro !== 'todos' ? [
|
|
1484
|
-
{ field: 'estado', operator: 'eq', value: filtro }
|
|
1485
|
-
] : [],
|
|
1486
|
-
sorters: [
|
|
1487
|
-
{ field: 'created_at', order: 'desc' }
|
|
1488
|
-
]
|
|
1489
|
-
});
|
|
1490
|
-
|
|
1491
|
-
// ─────────────────────────────────────────────────────────────
|
|
1492
|
-
// useUpdate - Actualizar registros
|
|
1493
|
-
// ─────────────────────────────────────────────────────────────
|
|
1494
|
-
const { mutate: updateRecord } = useUpdate();
|
|
1495
|
-
|
|
1496
|
-
const handleStatusChange = (id: number, newStatus: string) => {
|
|
1497
|
-
updateRecord(
|
|
1498
|
-
{
|
|
1499
|
-
resource: 'm360_t16192',
|
|
1500
|
-
id,
|
|
1501
|
-
values: { estado: newStatus }
|
|
1502
|
-
},
|
|
1503
|
-
{
|
|
1504
|
-
onSuccess: () => {
|
|
1505
|
-
toastSuccess('Estado actualizado');
|
|
1506
|
-
refetch();
|
|
1507
|
-
},
|
|
1508
|
-
onError: (error) => {
|
|
1509
|
-
toastError('Error al actualizar: ' + error.message);
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
);
|
|
1513
|
-
};
|
|
1514
|
-
|
|
1515
|
-
// ─────────────────────────────────────────────────────────────
|
|
1516
|
-
// useCreate - Crear registros
|
|
1517
|
-
// ─────────────────────────────────────────────────────────────
|
|
1518
|
-
const { mutate: createRecord } = useCreate();
|
|
1519
|
-
|
|
1520
|
-
const handleCreate = (data: any) => {
|
|
1521
|
-
createRecord(
|
|
1522
|
-
{
|
|
1523
|
-
resource: 'm360_t16192',
|
|
1524
|
-
values: data
|
|
1525
|
-
},
|
|
1526
|
-
{
|
|
1527
|
-
onSuccess: () => {
|
|
1528
|
-
toastSuccess('Registro creado');
|
|
1529
|
-
refetch();
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
);
|
|
1533
|
-
};
|
|
1534
|
-
|
|
1535
|
-
// ─────────────────────────────────────────────────────────────
|
|
1536
|
-
// RENDER
|
|
1537
|
-
// ─────────────────────────────────────────────────────────────
|
|
1538
|
-
if (isLoading) {
|
|
1539
|
-
return (
|
|
1540
|
-
<div className="flex items-center justify-center h-64">
|
|
1541
|
-
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-violet-600" />
|
|
1542
|
-
</div>
|
|
1543
|
-
);
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
return (
|
|
1547
|
-
<div className="p-6 bg-gradient-to-br from-violet-50/40 via-white to-purple-50/40 min-h-screen">
|
|
1548
|
-
{/* Header */}
|
|
1549
|
-
<div className="flex items-center justify-between mb-6">
|
|
1550
|
-
<h1 className="text-2xl font-bold text-gray-900">
|
|
1551
|
-
{viewSpec.name}
|
|
1552
|
-
</h1>
|
|
1553
|
-
<div className="flex gap-2">
|
|
1554
|
-
<select
|
|
1555
|
-
value={filtro}
|
|
1556
|
-
onChange={(e) => setFiltro(e.target.value)}
|
|
1557
|
-
className="px-3 py-2 border rounded-lg"
|
|
1558
|
-
>
|
|
1559
|
-
<option value="todos">Todos</option>
|
|
1560
|
-
<option value="pendiente">Pendientes</option>
|
|
1561
|
-
<option value="completado">Completados</option>
|
|
1562
|
-
</select>
|
|
1563
|
-
</div>
|
|
1564
|
-
</div>
|
|
1565
|
-
|
|
1566
|
-
{/* Lista */}
|
|
1567
|
-
<div className="grid gap-4">
|
|
1568
|
-
{data?.data.map((item: any) => (
|
|
1569
|
-
<div
|
|
1570
|
-
key={item.id}
|
|
1571
|
-
className="p-4 bg-white rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow"
|
|
1572
|
-
>
|
|
1573
|
-
<div className="flex items-center justify-between">
|
|
1574
|
-
<div>
|
|
1575
|
-
<h3 className="font-medium text-gray-900">{item.titulo}</h3>
|
|
1576
|
-
<p className="text-sm text-gray-500">{item.descripcion}</p>
|
|
1577
|
-
</div>
|
|
1578
|
-
<button
|
|
1579
|
-
onClick={() => handleStatusChange(item.id, 'completado')}
|
|
1580
|
-
className="px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700"
|
|
1581
|
-
>
|
|
1582
|
-
Completar
|
|
1583
|
-
</button>
|
|
1584
|
-
</div>
|
|
1585
|
-
</div>
|
|
1586
|
-
))}
|
|
1587
|
-
</div>
|
|
1588
|
-
|
|
1589
|
-
{/* Empty state */}
|
|
1590
|
-
{data?.data.length === 0 && (
|
|
1591
|
-
<div className="text-center py-12 text-gray-500">
|
|
1592
|
-
No hay registros que mostrar
|
|
1593
|
-
</div>
|
|
1594
|
-
)}
|
|
1595
|
-
</div>
|
|
1596
|
-
);
|
|
1597
|
-
}
|
|
1598
|
-
```
|
|
1599
|
-
|
|
1600
|
-
### dataProvider.ts - Lógica de Datos Custom
|
|
1601
|
-
|
|
1602
|
-
```typescript
|
|
1603
|
-
// core/dataProvider.ts
|
|
1604
|
-
import { DataProvider } from '@refinedev/core';
|
|
1605
|
-
|
|
1606
|
-
// Puedes extender el dataProvider por defecto
|
|
1607
|
-
export const customDataProvider = (baseProvider: DataProvider): DataProvider => ({
|
|
1608
|
-
...baseProvider,
|
|
1609
|
-
|
|
1610
|
-
// Override getList para transformar datos
|
|
1611
|
-
getList: async (params) => {
|
|
1612
|
-
const result = await baseProvider.getList(params);
|
|
1613
|
-
|
|
1614
|
-
// Transformar datos antes de devolverlos
|
|
1615
|
-
return {
|
|
1616
|
-
...result,
|
|
1617
|
-
data: result.data.map(item => ({
|
|
1618
|
-
...item,
|
|
1619
|
-
// Agregar campos calculados
|
|
1620
|
-
diasPendiente: calcularDias(item.created_at),
|
|
1621
|
-
prioridadColor: getPrioridadColor(item.prioridad)
|
|
1622
|
-
}))
|
|
1623
|
-
};
|
|
1624
|
-
},
|
|
1625
|
-
|
|
1626
|
-
// Custom method para operaciones especiales
|
|
1627
|
-
custom: async ({ url, method, payload }) => {
|
|
1628
|
-
const response = await fetch(url, {
|
|
1629
|
-
method,
|
|
1630
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1631
|
-
body: JSON.stringify(payload)
|
|
1632
|
-
});
|
|
1633
|
-
return response.json();
|
|
1634
|
-
}
|
|
1635
|
-
});
|
|
1636
|
-
|
|
1637
|
-
function calcularDias(fecha: string): number {
|
|
1638
|
-
const diff = Date.now() - new Date(fecha).getTime();
|
|
1639
|
-
return Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
function getPrioridadColor(prioridad: string): string {
|
|
1643
|
-
const colores: Record<string, string> = {
|
|
1644
|
-
alta: 'red',
|
|
1645
|
-
media: 'yellow',
|
|
1646
|
-
baja: 'green'
|
|
1647
|
-
};
|
|
1648
|
-
return colores[prioridad] || 'gray';
|
|
1649
|
-
}
|
|
1650
|
-
```
|
|
1651
|
-
|
|
1652
|
-
---
|
|
1653
|
-
|
|
1654
|
-
## Packages del Marketplace
|
|
1655
|
-
|
|
1656
|
-
Un Package agrupa múltiples Views y módulos relacionados para distribuirlos.
|
|
1657
|
-
|
|
1658
|
-
### Estructura en Base de Datos
|
|
1659
|
-
|
|
1660
|
-
```sql
|
|
1661
|
-
-- Package publicado
|
|
1662
|
-
SELECT * FROM marketplace.packages WHERE id = 14;
|
|
1663
|
-
-- { id: 14, name: "Stock Management", version: "1.2.0", published_at: ... }
|
|
1664
|
-
|
|
1665
|
-
-- Vistas del package
|
|
1666
|
-
SELECT id, name FROM marketplace.views WHERE package_id = 14;
|
|
1667
|
-
-- { id: 5, name: "Stock Overview" }
|
|
1668
|
-
-- { id: 6, name: "Inventory Report" }
|
|
1669
|
-
|
|
1670
|
-
-- Módulos incluidos (snapshot)
|
|
1671
|
-
SELECT * FROM marketplace.package_modules WHERE package_id = 14;
|
|
1672
|
-
```
|
|
1673
|
-
|
|
1674
|
-
### Flujo de Desarrollo → Publicación
|
|
1675
|
-
|
|
1676
|
-
```
|
|
1677
|
-
1. DESARROLLO (en tu company)
|
|
1678
|
-
└── Creas módulo "Stock" en company 116
|
|
1679
|
-
└── Creas views en Developer Center
|
|
1680
|
-
└── Testas todo localmente
|
|
1681
|
-
|
|
1682
|
-
2. CREAR PACKAGE
|
|
1683
|
-
└── Developer Center → Packages → New Package
|
|
1684
|
-
└── Agregas módulo "Stock"
|
|
1685
|
-
└── Agregas views "Stock Overview", "Inventory Report"
|
|
1686
|
-
|
|
1687
|
-
3. PUBLICAR
|
|
1688
|
-
└── Click "Publish to Marketplace"
|
|
1689
|
-
└── Sistema crea snapshot inmutable
|
|
1690
|
-
└── Versión 1.0.0 disponible
|
|
1691
|
-
|
|
1692
|
-
4. INSTALACIÓN (otra company)
|
|
1693
|
-
└── Company 200 instala el package
|
|
1694
|
-
└── Sistema crea módulo "Stock" en company_200
|
|
1695
|
-
└── Views disponibles en su menú
|
|
1696
|
-
```
|
|
1697
|
-
|
|
1698
|
-
---
|
|
1699
|
-
|
|
1700
|
-
## Comandos del CLI
|
|
1701
|
-
|
|
1702
|
-
### 💜 Resumen Rápido para Claude
|
|
1703
|
-
|
|
1704
|
-
**El CLI gestiona companies, módulos, automations y datos. Todo es dinámico - detecta la company automáticamente.**
|
|
1705
|
-
|
|
1706
|
-
| Quiero... | Comando |
|
|
1707
|
-
|-----------|---------|
|
|
1708
|
-
| Ver companies | `gufi companies` |
|
|
1709
|
-
| Ver módulos de company | `gufi modules 146` |
|
|
1710
|
-
| Ver/editar módulo | `gufi module 360` |
|
|
1711
|
-
| Ver registros | `gufi rows m308_t4136` |
|
|
1712
|
-
| Listar scripts | `gufi automations` |
|
|
1713
|
-
| Ver/editar script | `gufi automation 15` |
|
|
1714
|
-
| **Ver índice del worker** | `gufi automations:meta` |
|
|
1715
|
-
| **Ver ejecuciones** | `gufi automations:executions` |
|
|
1716
|
-
| **Ver/editar triggers** | `gufi entity:automations 4136` |
|
|
1717
|
-
| **Ver mis packages** | `gufi packages` |
|
|
1718
|
-
| **Ver package** | `gufi package 14` |
|
|
1719
|
-
| **Crear package** | `gufi package:create "Nombre"` |
|
|
1720
|
-
| **Desarrollar view** | `gufi pull 13` → `gufi watch` → `gufi logs` |
|
|
1721
|
-
|
|
1722
|
-
**3 cosas que saber:**
|
|
1723
|
-
1. **Auto-login**: `gufi login` guarda credenciales → funciona para siempre (logout NO las borra)
|
|
1724
|
-
2. **Auto-detección**: `module`, `automation`, `rows` detectan la company automáticamente
|
|
1725
|
-
3. **Sin prefijos**: `gufi pull/push/watch/logs` (no `view:pull`, etc.)
|
|
1726
|
-
|
|
1727
|
-
### Instalación
|
|
11
|
+
## Quick Reference
|
|
1728
12
|
|
|
1729
13
|
```bash
|
|
1730
|
-
|
|
1731
|
-
gufi
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
### 💜 Autenticación Persistente (Auto-Login)
|
|
14
|
+
# Contexto y diagnóstico
|
|
15
|
+
gufi context # Genera contexto para Claude
|
|
16
|
+
gufi doctor # Diagnóstico del sistema
|
|
1735
17
|
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
"apiUrl": "https://gogufi.com",
|
|
1741
|
-
"email": "user@example.com",
|
|
1742
|
-
"password": "secreto",
|
|
1743
|
-
"token": "eyJ...",
|
|
1744
|
-
"refreshToken": "eyJ..."
|
|
1745
|
-
}
|
|
1746
|
-
```
|
|
18
|
+
# Entornos
|
|
19
|
+
gufi config # Ver entornos
|
|
20
|
+
gufi config:local # Cambiar a localhost
|
|
21
|
+
gufi config:prod # Cambiar a producción
|
|
1747
22
|
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
3. `gufi logout` → borra TODO (token + credenciales)
|
|
23
|
+
# Módulos
|
|
24
|
+
gufi modules 146 # Ver módulos
|
|
25
|
+
gufi module 360 --edit # Editar con $EDITOR
|
|
1752
26
|
|
|
1753
|
-
|
|
27
|
+
# Automations
|
|
28
|
+
gufi automations # Ver scripts
|
|
29
|
+
gufi automation 15 --edit # Editar script
|
|
30
|
+
gufi entity:automations 4136 # Ver triggers
|
|
1754
31
|
|
|
1755
|
-
|
|
32
|
+
# Vistas
|
|
33
|
+
gufi view:pull 13 # Descargar
|
|
34
|
+
gufi view:push # Subir cambios
|
|
35
|
+
gufi view:watch # Auto-sync
|
|
1756
36
|
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
# Ver companies
|
|
1761
|
-
gufi companies
|
|
1762
|
-
|
|
1763
|
-
# Ver módulos de una company
|
|
1764
|
-
gufi modules 146
|
|
1765
|
-
|
|
1766
|
-
# Ver JSON de un módulo (auto-detecta company)
|
|
1767
|
-
gufi module 360
|
|
1768
|
-
|
|
1769
|
-
# Editar módulo con tu editor
|
|
1770
|
-
gufi module 360 --edit
|
|
1771
|
-
|
|
1772
|
-
# Guardar JSON a archivo
|
|
1773
|
-
gufi module 360 --file modulo.json
|
|
1774
|
-
|
|
1775
|
-
# Actualizar módulo desde archivo
|
|
1776
|
-
gufi module:update 360 modulo.json -c 146
|
|
1777
|
-
|
|
1778
|
-
# Validar sin aplicar cambios
|
|
1779
|
-
gufi module:update 360 modulo.json -c 146 --dry-run
|
|
1780
|
-
|
|
1781
|
-
# Crear nueva company
|
|
1782
|
-
gufi company:create "Mi Empresa"
|
|
1783
|
-
```
|
|
1784
|
-
|
|
1785
|
-
### Automations (Scripts Globales)
|
|
1786
|
-
|
|
1787
|
-
```bash
|
|
1788
|
-
# Listar automation scripts
|
|
1789
|
-
gufi automations -c 116
|
|
1790
|
-
|
|
1791
|
-
# Ver código de una automation
|
|
1792
|
-
gufi automation calcular_stock -c 116
|
|
1793
|
-
|
|
1794
|
-
# Editar con tu editor
|
|
1795
|
-
gufi automation calcular_stock -c 116 --edit
|
|
1796
|
-
|
|
1797
|
-
# Guardar a archivo
|
|
1798
|
-
gufi automation calcular_stock -c 116 --file script.js
|
|
1799
|
-
|
|
1800
|
-
# Crear/actualizar automation desde archivo JS
|
|
1801
|
-
gufi automation:create calcular_stock script.js -c 116
|
|
1802
|
-
|
|
1803
|
-
# 💜 Debugging - Ver índice del worker (auto-synced desde entities.automations)
|
|
1804
|
-
gufi automations:meta -c 116
|
|
1805
|
-
|
|
1806
|
-
# 💜 Ver historial de ejecuciones
|
|
1807
|
-
gufi automations:executions -c 116
|
|
1808
|
-
gufi automations:executions -c 116 --limit 50
|
|
1809
|
-
gufi automations:executions -c 116 --script sync_nayax
|
|
37
|
+
# Datos
|
|
38
|
+
gufi rows m360_t16192 # Listar registros
|
|
39
|
+
gufi row:create table --data '{...}'
|
|
1810
40
|
```
|
|
1811
41
|
|
|
1812
|
-
|
|
42
|
+
## Output JSON (para Claude/scripts)
|
|
1813
43
|
|
|
1814
|
-
|
|
44
|
+
Todos los comandos de lectura soportan `--json` para output estructurado sin colores ni emojis:
|
|
1815
45
|
|
|
1816
46
|
```bash
|
|
1817
|
-
|
|
1818
|
-
gufi
|
|
1819
|
-
|
|
1820
|
-
#
|
|
1821
|
-
gufi
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
gufi
|
|
1825
|
-
|
|
1826
|
-
#
|
|
1827
|
-
gufi entity:automations:update 4589 automations.json
|
|
1828
|
-
```
|
|
1829
|
-
|
|
1830
|
-
**Formato del JSON de automations:**
|
|
1831
|
-
```json
|
|
1832
|
-
[
|
|
1833
|
-
{
|
|
1834
|
-
"trigger": "insert",
|
|
1835
|
-
"function_name": "notificar_nuevo",
|
|
1836
|
-
"enabled": true
|
|
1837
|
-
},
|
|
1838
|
-
{
|
|
1839
|
-
"trigger": "click",
|
|
1840
|
-
"function_name": "generar_factura",
|
|
1841
|
-
"label": "Generar Factura",
|
|
1842
|
-
"enabled": true
|
|
1843
|
-
},
|
|
1844
|
-
{
|
|
1845
|
-
"trigger": "scheduled",
|
|
1846
|
-
"function_name": "sync_diario",
|
|
1847
|
-
"interval_ms": 86400000,
|
|
1848
|
-
"enabled": true
|
|
1849
|
-
}
|
|
1850
|
-
]
|
|
1851
|
-
```
|
|
1852
|
-
|
|
1853
|
-
**Triggers disponibles:**
|
|
1854
|
-
| Trigger | Descripción |
|
|
1855
|
-
|---------|-------------|
|
|
1856
|
-
| `insert` | Al crear registro |
|
|
1857
|
-
| `update` | Al modificar registro |
|
|
1858
|
-
| `delete` | Al eliminar registro |
|
|
1859
|
-
| `click` | Botón manual en UI |
|
|
1860
|
-
| `scheduled` | Intervalo programado |
|
|
1861
|
-
|
|
1862
|
-
### Environment Variables
|
|
1863
|
-
|
|
1864
|
-
```bash
|
|
1865
|
-
# Ver variables de entorno de la company
|
|
1866
|
-
gufi env
|
|
1867
|
-
|
|
1868
|
-
# Crear/actualizar variable
|
|
1869
|
-
gufi env:set STRIPE_API_KEY sk-live-xxx
|
|
1870
|
-
|
|
1871
|
-
# Eliminar variable
|
|
1872
|
-
gufi env:delete STRIPE_API_KEY
|
|
1873
|
-
```
|
|
1874
|
-
|
|
1875
|
-
### Schema (Estructura de Tablas)
|
|
1876
|
-
|
|
1877
|
-
```bash
|
|
1878
|
-
# Ver todas las tablas de la company
|
|
1879
|
-
gufi schema
|
|
1880
|
-
|
|
1881
|
-
# Filtrar por módulo
|
|
1882
|
-
gufi schema -m 360
|
|
1883
|
-
```
|
|
1884
|
-
|
|
1885
|
-
### Row CRUD (Datos de Tablas)
|
|
1886
|
-
|
|
1887
|
-
> 💜 **Auto-detección de Company**: El CLI detecta automáticamente a qué company pertenece una tabla
|
|
1888
|
-
> analizando el nombre (ej: `m308_t4136` → módulo 308 → busca en qué company está).
|
|
1889
|
-
> Ya no necesitas usar `-c` en la mayoría de casos.
|
|
1890
|
-
|
|
1891
|
-
```bash
|
|
1892
|
-
# Listar registros de una tabla (auto-detecta company)
|
|
1893
|
-
gufi rows m360_t16192 # Últimos 20 registros
|
|
1894
|
-
gufi rows m360_t16192 -l 50 # Últimos 50 registros
|
|
1895
|
-
gufi rows m360_t16192 -f estado=activo # Filtrar por campo
|
|
1896
|
-
|
|
1897
|
-
# Ver un registro específico
|
|
1898
|
-
gufi row m360_t16192 123
|
|
1899
|
-
|
|
1900
|
-
# Crear registro
|
|
1901
|
-
gufi row:create m360_t16192 --data '{"nombre":"Test","estado":"activo"}'
|
|
1902
|
-
gufi row:create m360_t16192 --file nuevo.json
|
|
1903
|
-
|
|
1904
|
-
# Actualizar registro
|
|
1905
|
-
gufi row:update m360_t16192 123 --data '{"estado":"completado"}'
|
|
1906
|
-
gufi row:update m360_t16192 123 --file cambios.json
|
|
1907
|
-
|
|
1908
|
-
# Eliminar registro
|
|
1909
|
-
gufi row:delete m360_t16192 123
|
|
1910
|
-
|
|
1911
|
-
# Duplicar registro (copia sin id/timestamps)
|
|
1912
|
-
gufi row:duplicate m360_t16192 123
|
|
1913
|
-
|
|
1914
|
-
# Crear múltiples registros desde archivo JSON
|
|
1915
|
-
gufi rows:create m360_t16192 --file datos.json
|
|
1916
|
-
# datos.json debe ser un array: [{"nombre":"A"},{"nombre":"B"}]
|
|
1917
|
-
|
|
1918
|
-
# Si necesitas forzar una company específica, usa -c
|
|
1919
|
-
gufi rows m360_t16192 -c 146
|
|
47
|
+
gufi companies --json # Lista companies en JSON
|
|
48
|
+
gufi modules 146 --json # Lista módulos en JSON
|
|
49
|
+
gufi module 360 --json # JSON del módulo
|
|
50
|
+
gufi automations --json # Lista automations en JSON
|
|
51
|
+
gufi automation 15 --json # Código y metadata del automation
|
|
52
|
+
gufi schema --json # Estructura de tablas en JSON
|
|
53
|
+
gufi rows m360_t16192 --json # Registros en JSON
|
|
54
|
+
gufi row m360_t16192 5 --json # Registro específico en JSON
|
|
55
|
+
gufi packages --json # Lista packages en JSON
|
|
56
|
+
gufi package 14 --json # Detalles del package en JSON
|
|
1920
57
|
```
|
|
1921
58
|
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
```bash
|
|
1925
|
-
# Ver tus views del Marketplace (muestra ID de cada vista)
|
|
1926
|
-
gufi views
|
|
1927
|
-
# 📦 Gestión de Tareas (ID: 14)
|
|
1928
|
-
# └─ 13 Tasks Manager (custom)
|
|
1929
|
-
|
|
1930
|
-
# Descargar view por ID (se guarda en ~/gufi-dev/view_<id>/)
|
|
1931
|
-
gufi view:pull 13
|
|
1932
|
-
|
|
1933
|
-
# Auto-sync al guardar archivos
|
|
1934
|
-
gufi view:watch
|
|
1935
|
-
|
|
1936
|
-
# Ver console.log del LivePreview
|
|
1937
|
-
gufi view:logs
|
|
59
|
+
Usar `--json` cuando necesites parsear el output programáticamente.
|
|
1938
60
|
|
|
1939
|
-
|
|
1940
|
-
gufi view:push
|
|
61
|
+
## MCP Server (Model Context Protocol)
|
|
1941
62
|
|
|
1942
|
-
|
|
1943
|
-
gufi view:status
|
|
1944
|
-
```
|
|
63
|
+
El CLI incluye un servidor MCP que permite a Claude interactuar **directamente** con Gufi sin ejecutar comandos manualmente.
|
|
1945
64
|
|
|
1946
|
-
###
|
|
65
|
+
### Configuración (una sola vez)
|
|
1947
66
|
|
|
1948
67
|
```bash
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
68
|
+
claude mcp add --transport stdio gufi -- gufi mcp
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Tools disponibles (42 tools)
|
|
72
|
+
|
|
73
|
+
**Contexto e Info (EMPEZAR AQUÍ):**
|
|
74
|
+
- `gufi_context` - **USAR PRIMERO** - Genera contexto inteligente del proyecto
|
|
75
|
+
- `gufi_whoami` - Usuario y entorno actual
|
|
76
|
+
- `gufi_schema` - Estructura de tablas/campos
|
|
77
|
+
|
|
78
|
+
**Companies:**
|
|
79
|
+
- `gufi_companies` - Listar companies
|
|
80
|
+
- `gufi_company_create` - Crear company
|
|
81
|
+
|
|
82
|
+
**Módulos (estructura de datos):**
|
|
83
|
+
- `gufi_modules` - Listar módulos de una company
|
|
84
|
+
- `gufi_module` - Ver JSON completo de un módulo
|
|
85
|
+
- `gufi_module_update` - **Modificar módulo** (añadir campos, entidades)
|
|
86
|
+
- `gufi_module_create` - Crear módulo nuevo
|
|
87
|
+
|
|
88
|
+
**Automations (lógica de negocio):**
|
|
89
|
+
- `gufi_automations` - Listar scripts
|
|
90
|
+
- `gufi_automation` - Ver código JS de un script
|
|
91
|
+
- `gufi_automation_create` - **Crear/actualizar script**
|
|
92
|
+
- `gufi_entity_automations` - Ver triggers de una entidad
|
|
93
|
+
- `gufi_entity_automations_update` - **Configurar triggers**
|
|
94
|
+
- `gufi_automations_executions` - Ver historial de ejecuciones
|
|
95
|
+
|
|
96
|
+
**Datos (CRUD):**
|
|
97
|
+
- `gufi_rows` - Listar registros de una tabla
|
|
98
|
+
- `gufi_row` - Ver un registro
|
|
99
|
+
- `gufi_row_create` - Crear registro
|
|
100
|
+
- `gufi_row_update` - Actualizar registro
|
|
101
|
+
- `gufi_row_delete` - Eliminar registro
|
|
102
|
+
|
|
103
|
+
**Environment Variables:**
|
|
104
|
+
- `gufi_env` - Listar variables
|
|
105
|
+
- `gufi_env_set` - Crear/actualizar variable
|
|
106
|
+
- `gufi_env_delete` - Eliminar variable
|
|
107
|
+
|
|
108
|
+
**Vistas (React/TSX):**
|
|
109
|
+
- `gufi_view_files` - Ver archivos de una vista
|
|
110
|
+
- `gufi_view_file_update` - **Modificar archivo de vista**
|
|
111
|
+
- `gufi_view_files_update` - Modificar múltiples archivos
|
|
112
|
+
|
|
113
|
+
**Packages (Marketplace):**
|
|
114
|
+
- `gufi_packages` - Listar packages
|
|
115
|
+
- `gufi_package` - Ver detalles
|
|
116
|
+
- `gufi_package_create` - Crear package
|
|
117
|
+
- `gufi_package_publish` - Publicar
|
|
118
|
+
- `gufi_package_sync` - Sincronizar versión
|
|
119
|
+
- `gufi_package_add_module` / `gufi_package_remove_module`
|
|
120
|
+
- `gufi_package_add_view` / `gufi_package_remove_view`
|
|
121
|
+
|
|
122
|
+
**Package Migrations:**
|
|
123
|
+
- `gufi_package_migrations` - Listar migraciones
|
|
124
|
+
- `gufi_package_migration_create` - Crear migración SQL
|
|
125
|
+
- `gufi_package_entities` - Listar entidades para target_entity
|
|
126
|
+
|
|
127
|
+
### Ejemplos de uso con Claude
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
Usuario: "Agrega un campo 'fecha_entrega' tipo date al módulo de pedidos"
|
|
131
|
+
Claude: [usa gufi_module para leer] → [modifica JSON] → [usa gufi_module_update]
|
|
132
|
+
"Listo, agregué el campo fecha_entrega al módulo de pedidos"
|
|
133
|
+
|
|
134
|
+
Usuario: "Crea un automation que envíe email cuando un pedido pase a 'enviado'"
|
|
135
|
+
Claude: [usa gufi_automations para ver existentes]
|
|
136
|
+
[escribe código JS]
|
|
137
|
+
[usa gufi_automation_create]
|
|
138
|
+
[usa gufi_entity_automations_update para configurar trigger]
|
|
139
|
+
"Listo, creé el automation 'email_pedido_enviado' con trigger on_update"
|
|
140
|
+
|
|
141
|
+
Usuario: "¿Cuántos clientes hay en la empresa 116?"
|
|
142
|
+
Claude: [usa gufi_schema para encontrar tabla de clientes]
|
|
143
|
+
[usa gufi_rows para consultar]
|
|
144
|
+
"Hay 234 clientes en la tabla m360_t4136"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Tipos de Campo (@gufi/column-types)
|
|
148
|
+
|
|
149
|
+
Al crear o actualizar datos, usa los formatos correctos:
|
|
150
|
+
|
|
151
|
+
**Simples:**
|
|
152
|
+
- text, textarea, email, url, barcode: `"string"`
|
|
153
|
+
- number_int: `42` | number_float: `3.14` | percentage: `0.75`
|
|
154
|
+
- boolean: `true/false`
|
|
155
|
+
- date: `"2024-01-15"` | datetime: `"2024-01-15T10:30:00Z"` | time: `"14:30:00"`
|
|
156
|
+
- select: `"value"` | relation: `123`
|
|
157
|
+
|
|
158
|
+
**Arrays:**
|
|
159
|
+
- multiselect: `["value1", "value2"]`
|
|
160
|
+
- users: `[16, 23]`
|
|
161
|
+
|
|
162
|
+
**JSONB (objetos complejos):**
|
|
163
|
+
- currency: `{ "currency": "EUR", "amount": 150.00 }`
|
|
164
|
+
- phone: `{ "prefix": "+34", "number": "612345678" }`
|
|
165
|
+
- location: `{ "street": "Mayor", "number": "15", "city": "Madrid", "lat": 40.41 }`
|
|
166
|
+
- file: `[{ "url": "company_130/doc.pdf", "name": "doc.pdf" }]`
|
|
167
|
+
|
|
168
|
+
En vistas usa `gufi.CB.*` para formatear correctamente:
|
|
169
|
+
```typescript
|
|
170
|
+
gufi.CB.currency(150) // → { currency: 'EUR', amount: 150 }
|
|
171
|
+
gufi.CB.phone('612345678') // → { prefix: '+34', number: '612345678' }
|
|
172
|
+
gufi.CB.date() // → '2024-01-15' (hoy)
|
|
173
|
+
gufi.CB.multiselect('a', 'b') // → ['a', 'b']
|
|
1983
174
|
```
|
|
1984
175
|
|
|
1985
|
-
###
|
|
1986
|
-
|
|
1987
|
-
| Opción | Descripción |
|
|
1988
|
-
|--------|-------------|
|
|
1989
|
-
| `-c, --company <id>` | ID de company (solo para `modules`, `automations`, `*:create`) |
|
|
1990
|
-
| `-e, --edit` | Abrir en editor ($EDITOR) |
|
|
1991
|
-
| `-f, --file <path>` | Guardar a archivo |
|
|
1992
|
-
| `--dry-run` | Validar sin aplicar |
|
|
1993
|
-
| `-h, --help` | Ayuda del comando |
|
|
1994
|
-
|
|
1995
|
-
**Auto-detección:** `module`, `module:update`, `automation`, `entity:automations`, `rows` detectan company automáticamente del ID.
|
|
1996
|
-
|
|
1997
|
-
---
|
|
1998
|
-
|
|
1999
|
-
## Tips para Claude
|
|
2000
|
-
|
|
2001
|
-
### 💜 Flujo de Trabajo Recomendado
|
|
2002
|
-
|
|
2003
|
-
```bash
|
|
2004
|
-
# 1. Primero, oriéntate
|
|
2005
|
-
gufi companies # Ver companies disponibles
|
|
2006
|
-
gufi modules 146 # Ver módulos de una company
|
|
176
|
+
### Seguridad
|
|
2007
177
|
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
gufi row:update m308_t4136 123 --data '{"status":"done"}'
|
|
178
|
+
- Usa las credenciales del CLI (`gufi login`)
|
|
179
|
+
- Solo accede a resources con permisos del usuario
|
|
180
|
+
- Todo corre localmente (stdio), no hay servidor público
|
|
2012
181
|
|
|
2013
|
-
|
|
2014
|
-
gufi module 360 --edit # Abre JSON en tu $EDITOR
|
|
182
|
+
## Comandos Esenciales
|
|
2015
183
|
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
### Cuándo usar cada comando
|
|
2022
|
-
|
|
2023
|
-
| El usuario quiere... | Comando |
|
|
2024
|
-
|---------------------|---------|
|
|
184
|
+
| Quiero... | Comando |
|
|
185
|
+
|-----------|---------|
|
|
186
|
+
| **Contexto para Claude** | `gufi context` |
|
|
187
|
+
| **Diagnóstico** | `gufi doctor` |
|
|
2025
188
|
| Ver companies | `gufi companies` |
|
|
2026
|
-
| Ver módulos
|
|
2027
|
-
|
|
|
2028
|
-
| Ver
|
|
2029
|
-
|
|
|
2030
|
-
| Desarrollar vista | `gufi pull "Mi Vista"` → `gufi watch` → `gufi logs` |
|
|
2031
|
-
|
|
2032
|
-
### Auto-detección
|
|
2033
|
-
|
|
2034
|
-
Todo automático:
|
|
2035
|
-
- `gufi module 360` → detecta company del módulo
|
|
2036
|
-
- `gufi automation 15` → detecta company del script
|
|
2037
|
-
- `gufi rows m308_t4136` → detecta company de la tabla
|
|
2038
|
-
|
|
2039
|
-
### Automations: 4 tablas que trabajan juntas
|
|
2040
|
-
|
|
2041
|
-
```
|
|
2042
|
-
automation_scripts (id=15) ← CÓDIGO JavaScript
|
|
2043
|
-
│
|
|
2044
|
-
└── entities.automations ← CONFIGURACIÓN (qué trigger, cuándo ejecutar)
|
|
2045
|
-
│
|
|
2046
|
-
▼ (sync automático)
|
|
2047
|
-
automation_meta ← ÍNDICE para worker
|
|
2048
|
-
│
|
|
2049
|
-
▼
|
|
2050
|
-
automation_executions ← HISTORIAL de ejecuciones
|
|
2051
|
-
```
|
|
2052
|
-
|
|
2053
|
-
| Tabla | Qué guarda | CLI |
|
|
2054
|
-
|-------|------------|-----|
|
|
2055
|
-
| `automation_scripts` | Código JS | `gufi automation 15` |
|
|
2056
|
-
| `entities.automations` | Triggers (on_insert, on_click...) | `gufi entity:automations 4136` |
|
|
2057
|
-
| `automation_meta` | Índice para worker (auto-sync) | `gufi automations:meta` |
|
|
2058
|
-
| `automation_executions` | Historial de ejecuciones | `gufi automations:executions` |
|
|
2059
|
-
|
|
2060
|
-
**Ejemplo:** Un script puede usarse en varias entities:
|
|
2061
|
-
```
|
|
2062
|
-
script 15 "sync_nayax" → machines (on_click), products (scheduled), sales (on_insert)
|
|
2063
|
-
```
|
|
2064
|
-
|
|
2065
|
-
**Flujo típico:**
|
|
2066
|
-
```bash
|
|
2067
|
-
gufi automations # Ver scripts
|
|
2068
|
-
gufi automation 15 --edit # Editar código
|
|
2069
|
-
gufi entity:automations 4136 # Ver/editar triggers
|
|
2070
|
-
gufi automations:meta # Ver qué tiene el worker indexado (debugging)
|
|
2071
|
-
gufi automations:executions # Ver historial de ejecuciones
|
|
2072
|
-
```
|
|
2073
|
-
|
|
2074
|
-
### Errores comunes
|
|
2075
|
-
|
|
2076
|
-
| Error | Solución |
|
|
2077
|
-
|-------|----------|
|
|
2078
|
-
| "No estás logueado" | `gufi login` (guarda credenciales para auto-login) |
|
|
2079
|
-
| "Módulo no encontrado" | Verificar que el ID existe con `gufi modules <company>` |
|
|
2080
|
-
| "Token expirado" | Automático: refresh o auto-login con credenciales guardadas |
|
|
2081
|
-
| "JSON inválido" | Validar estructura del JSON |
|
|
2082
|
-
| "API Error 404" | Verificar nombre de tabla (formato: `m{moduleId}_t{entityId}`) |
|
|
2083
|
-
|
|
2084
|
-
### Conceptos clave
|
|
2085
|
-
|
|
2086
|
-
- **Multi-tenant**: Cada company = schema aislado en PostgreSQL
|
|
2087
|
-
- **Tablas físicas**: `m{moduleId}_t{entityId}` (ej: `m308_t4136`)
|
|
2088
|
-
- **Módulos**: JSON que define estructura → Gufi crea tablas/UI
|
|
2089
|
-
- **Automations**: JS en DB, ejecutado por worker (pg-boss)
|
|
2090
|
-
- **Views**: React/TS en DB, ejecutado en frontend dinámicamente
|
|
2091
|
-
- **Marketplace**: Sistema de distribución de packages
|
|
2092
|
-
|
|
2093
|
-
### Companies Importantes
|
|
189
|
+
| Ver módulos | `gufi modules 146` |
|
|
190
|
+
| Editar módulo | `gufi module 360 --edit` |
|
|
191
|
+
| Ver automations | `gufi automations` |
|
|
192
|
+
| Desarrollar vista | `gufi view:pull 13` → `gufi view:push` |
|
|
2094
193
|
|
|
2095
|
-
|
|
2096
|
-
|----|--------|-----|
|
|
2097
|
-
| 101 | Demo | Pruebas generales |
|
|
2098
|
-
| 116 | Fitvending | Producción - cliente real |
|
|
2099
|
-
| 146 | Gufi | Desarrollo interno |
|
|
2100
|
-
| 147 | Apuestas Pro | Cliente real |
|
|
194
|
+
## Tips
|
|
2101
195
|
|
|
2102
|
-
|
|
196
|
+
1. **`gufi context`**: Úsalo al inicio de cualquier tarea
|
|
197
|
+
2. **Auto-detección**: `module`, `automation`, `rows` detectan company automáticamente
|
|
198
|
+
3. **--edit**: Abre en `$EDITOR`
|
|
199
|
+
4. **Entornos separados**: Cada uno con sus credenciales
|
|
200
|
+
5. **Auto-login**: CLI refresca tokens automáticamente
|
|
2103
201
|
|
|
2104
|
-
##
|
|
202
|
+
## Documentación Completa
|
|
2105
203
|
|
|
2106
|
-
|
|
2107
|
-
-
|
|
204
|
+
Para documentación detallada de cada comando, arquitectura, y patrones, ve a:
|
|
205
|
+
- `docs/claude/` - Fuente de verdad para Claude
|
|
206
|
+
- `docs/guide/27-gufi-cli.md` - Guía técnica del CLI
|