gufi-cli 0.1.1 → 0.1.2
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 +1390 -0
- package/dist/commands/companies.d.ts +26 -0
- package/dist/commands/companies.js +513 -0
- package/dist/commands/logs.d.ts +8 -0
- package/dist/commands/logs.js +153 -0
- package/dist/commands/push.d.ts +2 -2
- package/dist/commands/push.js +4 -3
- package/dist/index.d.ts +17 -7
- package/dist/index.js +64 -8
- package/package.json +15 -3
- package/src/commands/list.ts +0 -51
- package/src/commands/login.ts +0 -124
- package/src/commands/pull.ts +0 -113
- package/src/commands/push.ts +0 -85
- package/src/commands/watch.ts +0 -89
- package/src/index.ts +0 -73
- package/src/lib/api.ts +0 -127
- package/src/lib/config.ts +0 -93
- package/src/lib/sync.ts +0 -236
- package/tsconfig.json +0 -17
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,1390 @@
|
|
|
1
|
+
# Gufi CLI - Documentación Completa para Claude
|
|
2
|
+
|
|
3
|
+
## ¿Qué es Gufi?
|
|
4
|
+
|
|
5
|
+
Gufi es un **ERP multi-tenant** donde cada empresa (company) tiene su propia base de datos aislada. Los consultores pueden crear módulos personalizados sin programar - solo definiendo JSONs que describen la estructura de datos.
|
|
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 de cada tabla │
|
|
25
|
+
│ core.automation_scripts → Código JavaScript de automations │
|
|
26
|
+
│ core.automation_meta → Vincula scripts a triggers │
|
|
27
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
28
|
+
│ marketplace.packages → Paquetes publicados │
|
|
29
|
+
│ marketplace.views → Vistas React/TypeScript │
|
|
30
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Nomenclatura de Tablas
|
|
34
|
+
|
|
35
|
+
Las tablas físicas en PostgreSQL siguen el patrón:
|
|
36
|
+
```
|
|
37
|
+
m{moduleId}_t{entityId}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Ejemplo:
|
|
41
|
+
- Módulo "Tareas" tiene ID 360
|
|
42
|
+
- Entidad "asignaciones" tiene ID 16192
|
|
43
|
+
- Tabla física: `m360_t16192` en schema `company_146`
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Estructura de un Módulo (JSON)
|
|
48
|
+
|
|
49
|
+
Un módulo es un JSON que define **toda la estructura de datos** de una funcionalidad. Gufi lee este JSON y automáticamente:
|
|
50
|
+
1. Crea las tablas en PostgreSQL
|
|
51
|
+
2. Genera la UI (formularios, listas, filtros)
|
|
52
|
+
3. Configura permisos
|
|
53
|
+
4. Vincula relaciones entre tablas
|
|
54
|
+
|
|
55
|
+
### JSON Completo de Ejemplo
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"name": "ventas",
|
|
60
|
+
"displayName": "Gestión de Ventas",
|
|
61
|
+
"icon": "ShoppingCart",
|
|
62
|
+
"submodules": [
|
|
63
|
+
{
|
|
64
|
+
"name": "maestros",
|
|
65
|
+
"label": "Datos Maestros",
|
|
66
|
+
"icon": "Database",
|
|
67
|
+
"entities": [
|
|
68
|
+
{
|
|
69
|
+
"kind": "table",
|
|
70
|
+
"name": "clientes",
|
|
71
|
+
"label": "Clientes",
|
|
72
|
+
"fields": [
|
|
73
|
+
{
|
|
74
|
+
"name": "nombre",
|
|
75
|
+
"type": "text",
|
|
76
|
+
"label": "Nombre",
|
|
77
|
+
"required": true,
|
|
78
|
+
"searchable": true
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "email",
|
|
82
|
+
"type": "email",
|
|
83
|
+
"label": "Email",
|
|
84
|
+
"unique": true
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"name": "telefono",
|
|
88
|
+
"type": "phone",
|
|
89
|
+
"label": "Teléfono"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "tipo",
|
|
93
|
+
"type": "select",
|
|
94
|
+
"label": "Tipo de Cliente",
|
|
95
|
+
"options": [
|
|
96
|
+
{ "value": "particular", "label": "Particular" },
|
|
97
|
+
{ "value": "empresa", "label": "Empresa" },
|
|
98
|
+
{ "value": "vip", "label": "VIP" }
|
|
99
|
+
],
|
|
100
|
+
"default": "particular"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "limite_credito",
|
|
104
|
+
"type": "currency",
|
|
105
|
+
"label": "Límite de Crédito",
|
|
106
|
+
"currency": "EUR"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "activo",
|
|
110
|
+
"type": "boolean",
|
|
111
|
+
"label": "Activo",
|
|
112
|
+
"default": true
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"name": "notas",
|
|
116
|
+
"type": "textarea",
|
|
117
|
+
"label": "Notas"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"name": "comercial_id",
|
|
121
|
+
"type": "users",
|
|
122
|
+
"label": "Comercial Asignado"
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"permissions": {
|
|
126
|
+
"Admin": "*",
|
|
127
|
+
"Comercial": "crud",
|
|
128
|
+
"User": "read"
|
|
129
|
+
},
|
|
130
|
+
"ui": {
|
|
131
|
+
"defaultSort": { "field": "nombre", "order": "ASC" },
|
|
132
|
+
"searchFields": ["nombre", "email"],
|
|
133
|
+
"listFields": ["nombre", "tipo", "comercial_id", "activo"]
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"kind": "table",
|
|
138
|
+
"name": "productos",
|
|
139
|
+
"label": "Productos",
|
|
140
|
+
"fields": [
|
|
141
|
+
{
|
|
142
|
+
"name": "codigo",
|
|
143
|
+
"type": "text",
|
|
144
|
+
"label": "Código",
|
|
145
|
+
"required": true,
|
|
146
|
+
"unique": true
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"name": "nombre",
|
|
150
|
+
"type": "text",
|
|
151
|
+
"label": "Nombre",
|
|
152
|
+
"required": true
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"name": "precio",
|
|
156
|
+
"type": "currency",
|
|
157
|
+
"label": "Precio",
|
|
158
|
+
"currency": "EUR",
|
|
159
|
+
"required": true
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"name": "stock",
|
|
163
|
+
"type": "number",
|
|
164
|
+
"label": "Stock",
|
|
165
|
+
"default": 0,
|
|
166
|
+
"min": 0
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"name": "categoria",
|
|
170
|
+
"type": "multiselect",
|
|
171
|
+
"label": "Categorías",
|
|
172
|
+
"options": [
|
|
173
|
+
{ "value": "electronica", "label": "Electrónica" },
|
|
174
|
+
{ "value": "ropa", "label": "Ropa" },
|
|
175
|
+
{ "value": "hogar", "label": "Hogar" }
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"name": "imagen",
|
|
180
|
+
"type": "image",
|
|
181
|
+
"label": "Imagen"
|
|
182
|
+
}
|
|
183
|
+
],
|
|
184
|
+
"permissions": {
|
|
185
|
+
"Admin": "*",
|
|
186
|
+
"User": "read"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"name": "operaciones",
|
|
193
|
+
"label": "Operaciones",
|
|
194
|
+
"icon": "FileText",
|
|
195
|
+
"entities": [
|
|
196
|
+
{
|
|
197
|
+
"kind": "table",
|
|
198
|
+
"name": "pedidos",
|
|
199
|
+
"label": "Pedidos",
|
|
200
|
+
"fields": [
|
|
201
|
+
{
|
|
202
|
+
"name": "numero",
|
|
203
|
+
"type": "text",
|
|
204
|
+
"label": "Nº Pedido",
|
|
205
|
+
"required": true,
|
|
206
|
+
"unique": true,
|
|
207
|
+
"autoGenerate": "PED-{YYYY}{MM}-{SEQ:5}"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"name": "cliente_id",
|
|
211
|
+
"type": "relation",
|
|
212
|
+
"label": "Cliente",
|
|
213
|
+
"ref": "clientes",
|
|
214
|
+
"display": "nombre",
|
|
215
|
+
"required": true
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"name": "fecha",
|
|
219
|
+
"type": "date",
|
|
220
|
+
"label": "Fecha",
|
|
221
|
+
"default": "today"
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"name": "estado",
|
|
225
|
+
"type": "select",
|
|
226
|
+
"label": "Estado",
|
|
227
|
+
"options": [
|
|
228
|
+
{ "value": "borrador", "label": "Borrador", "color": "gray" },
|
|
229
|
+
{ "value": "confirmado", "label": "Confirmado", "color": "blue" },
|
|
230
|
+
{ "value": "enviado", "label": "Enviado", "color": "yellow" },
|
|
231
|
+
{ "value": "entregado", "label": "Entregado", "color": "green" },
|
|
232
|
+
{ "value": "cancelado", "label": "Cancelado", "color": "red" }
|
|
233
|
+
],
|
|
234
|
+
"default": "borrador"
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"name": "total",
|
|
238
|
+
"type": "currency",
|
|
239
|
+
"label": "Total",
|
|
240
|
+
"currency": "EUR",
|
|
241
|
+
"computed": true
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"name": "responsable_id",
|
|
245
|
+
"type": "users",
|
|
246
|
+
"label": "Responsable"
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"name": "observaciones",
|
|
250
|
+
"type": "textarea",
|
|
251
|
+
"label": "Observaciones"
|
|
252
|
+
}
|
|
253
|
+
],
|
|
254
|
+
"permissions": {
|
|
255
|
+
"Admin": "*",
|
|
256
|
+
"Comercial": "crud",
|
|
257
|
+
"User": "read"
|
|
258
|
+
},
|
|
259
|
+
"automations": [
|
|
260
|
+
{
|
|
261
|
+
"trigger": "on_insert",
|
|
262
|
+
"function_name": "notificar_nuevo_pedido"
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"trigger": "on_update",
|
|
266
|
+
"condition": "estado = 'confirmado'",
|
|
267
|
+
"function_name": "procesar_pedido"
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"trigger": "click",
|
|
271
|
+
"function_name": "generar_factura",
|
|
272
|
+
"label": "Generar Factura",
|
|
273
|
+
"icon": "FileText",
|
|
274
|
+
"showWhen": "estado = 'entregado'"
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"kind": "table",
|
|
280
|
+
"name": "lineas_pedido",
|
|
281
|
+
"label": "Líneas de Pedido",
|
|
282
|
+
"fields": [
|
|
283
|
+
{
|
|
284
|
+
"name": "pedido_id",
|
|
285
|
+
"type": "relation",
|
|
286
|
+
"label": "Pedido",
|
|
287
|
+
"ref": "pedidos",
|
|
288
|
+
"required": true,
|
|
289
|
+
"cascade": true
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"name": "producto_id",
|
|
293
|
+
"type": "relation",
|
|
294
|
+
"label": "Producto",
|
|
295
|
+
"ref": "productos",
|
|
296
|
+
"display": "nombre",
|
|
297
|
+
"required": true
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"name": "cantidad",
|
|
301
|
+
"type": "number",
|
|
302
|
+
"label": "Cantidad",
|
|
303
|
+
"required": true,
|
|
304
|
+
"min": 1,
|
|
305
|
+
"default": 1
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
"name": "precio_unitario",
|
|
309
|
+
"type": "currency",
|
|
310
|
+
"label": "Precio Unit.",
|
|
311
|
+
"currency": "EUR"
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"name": "subtotal",
|
|
315
|
+
"type": "currency",
|
|
316
|
+
"label": "Subtotal",
|
|
317
|
+
"currency": "EUR",
|
|
318
|
+
"computed": "cantidad * precio_unitario"
|
|
319
|
+
}
|
|
320
|
+
],
|
|
321
|
+
"permissions": {
|
|
322
|
+
"Admin": "*",
|
|
323
|
+
"Comercial": "crud"
|
|
324
|
+
},
|
|
325
|
+
"ui": {
|
|
326
|
+
"inline": true,
|
|
327
|
+
"parentField": "pedido_id"
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Tipos de Campos Disponibles
|
|
337
|
+
|
|
338
|
+
| Tipo | PostgreSQL | Descripción | Opciones |
|
|
339
|
+
|------|------------|-------------|----------|
|
|
340
|
+
| `text` | VARCHAR(255) | Texto corto | `maxLength`, `minLength`, `pattern` |
|
|
341
|
+
| `textarea` | TEXT | Texto largo | `rows`, `maxLength` |
|
|
342
|
+
| `number` | NUMERIC | Número | `min`, `max`, `decimals`, `step` |
|
|
343
|
+
| `currency` | NUMERIC(15,2) | Dinero | `currency` (EUR, USD, etc.) |
|
|
344
|
+
| `date` | DATE | Fecha | `min`, `max`, `default: "today"` |
|
|
345
|
+
| `datetime` | TIMESTAMP | Fecha y hora | `min`, `max` |
|
|
346
|
+
| `time` | TIME | Solo hora | - |
|
|
347
|
+
| `select` | VARCHAR | Selector único | `options: [{value, label, color}]` |
|
|
348
|
+
| `multiselect` | TEXT[] | Selector múltiple | `options: [{value, label}]` |
|
|
349
|
+
| `relation` | INTEGER (FK) | Relación a otra tabla | `ref`, `display`, `cascade` |
|
|
350
|
+
| `users` | INTEGER/INT[] | Usuario(s) del sistema | `multiple: true` |
|
|
351
|
+
| `boolean` | BOOLEAN | Sí/No | `default` |
|
|
352
|
+
| `email` | VARCHAR | Email validado | - |
|
|
353
|
+
| `phone` | VARCHAR | Teléfono | `format` |
|
|
354
|
+
| `url` | VARCHAR | URL | - |
|
|
355
|
+
| `image` | TEXT | URL de imagen (GCS) | `maxSize`, `allowedTypes` |
|
|
356
|
+
| `file` | TEXT | URL de archivo (GCS) | `maxSize`, `allowedTypes` |
|
|
357
|
+
| `json` | JSONB | Objeto JSON | `schema` |
|
|
358
|
+
| `color` | VARCHAR(7) | Color hex | - |
|
|
359
|
+
| `rating` | SMALLINT | Puntuación 1-5 | `max` |
|
|
360
|
+
| `percentage` | NUMERIC | Porcentaje | `min`, `max` |
|
|
361
|
+
|
|
362
|
+
### Opciones de Campo
|
|
363
|
+
|
|
364
|
+
```json
|
|
365
|
+
{
|
|
366
|
+
"name": "campo",
|
|
367
|
+
"type": "text",
|
|
368
|
+
"label": "Etiqueta visible",
|
|
369
|
+
"required": true, // Campo obligatorio
|
|
370
|
+
"unique": true, // Valor único en la tabla
|
|
371
|
+
"default": "valor", // Valor por defecto
|
|
372
|
+
"searchable": true, // Incluir en búsqueda global
|
|
373
|
+
"hidden": false, // Ocultar en UI
|
|
374
|
+
"readonly": false, // Solo lectura
|
|
375
|
+
"computed": "expr", // Calculado (no editable)
|
|
376
|
+
"autoGenerate": "pattern", // Auto-generar valor
|
|
377
|
+
"validation": { // Validaciones custom
|
|
378
|
+
"pattern": "^[A-Z]{3}",
|
|
379
|
+
"message": "Debe ser 3 letras mayúsculas"
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Permisos por Rol
|
|
385
|
+
|
|
386
|
+
```json
|
|
387
|
+
"permissions": {
|
|
388
|
+
"Admin": "*", // Todos los permisos
|
|
389
|
+
"Manager": "crud", // Create, Read, Update, Delete
|
|
390
|
+
"User": "cru", // Create, Read, Update (no delete)
|
|
391
|
+
"Viewer": "read", // Solo lectura
|
|
392
|
+
"Guest": "" // Sin acceso
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Automations (Código JavaScript)
|
|
399
|
+
|
|
400
|
+
Las automations son **funciones JavaScript** almacenadas en la base de datos que se ejecutan en respuesta a eventos.
|
|
401
|
+
|
|
402
|
+
### Dónde se almacenan
|
|
403
|
+
|
|
404
|
+
```sql
|
|
405
|
+
-- El código está en:
|
|
406
|
+
SELECT function_name, js_code FROM core.automation_scripts WHERE company_id = 116;
|
|
407
|
+
|
|
408
|
+
-- Los triggers están en:
|
|
409
|
+
SELECT * FROM core.automation_meta WHERE company_id = 116;
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Tipos de Triggers
|
|
413
|
+
|
|
414
|
+
| Trigger | Cuándo se ejecuta |
|
|
415
|
+
|---------|-------------------|
|
|
416
|
+
| `on_insert` | Al crear un registro |
|
|
417
|
+
| `on_update` | Al modificar un registro |
|
|
418
|
+
| `on_delete` | Al eliminar un registro |
|
|
419
|
+
| `click` | Botón manual en la UI |
|
|
420
|
+
| `scheduled` | Cron job (ej: "0 9 * * *") |
|
|
421
|
+
|
|
422
|
+
### Estructura de una Automation
|
|
423
|
+
|
|
424
|
+
```javascript
|
|
425
|
+
/**
|
|
426
|
+
* Función: procesar_pedido
|
|
427
|
+
* Trigger: on_update cuando estado = 'confirmado'
|
|
428
|
+
*
|
|
429
|
+
* @param {Object} context - Contexto de ejecución
|
|
430
|
+
* @param {Object} api - API para interactuar con el sistema
|
|
431
|
+
* @param {Object} logger - Logger para debugging
|
|
432
|
+
*/
|
|
433
|
+
async function procesar_pedido(context, api, logger) {
|
|
434
|
+
// ═══════════════════════════════════════════════════════════
|
|
435
|
+
// CONTEXT - Información del evento
|
|
436
|
+
// ═══════════════════════════════════════════════════════════
|
|
437
|
+
const {
|
|
438
|
+
company_id, // ID de la company (ej: 116)
|
|
439
|
+
module_id, // ID del módulo (ej: 308)
|
|
440
|
+
entity_id, // ID de la entidad/tabla (ej: 4136)
|
|
441
|
+
table, // Nombre físico de tabla (ej: "m308_t4136")
|
|
442
|
+
row, // Registro actual (después del cambio)
|
|
443
|
+
old_row, // Registro anterior (solo en on_update)
|
|
444
|
+
input, // Input del usuario (solo en click)
|
|
445
|
+
env, // Variables de entorno de la company
|
|
446
|
+
user // Usuario que disparó el evento
|
|
447
|
+
} = context;
|
|
448
|
+
|
|
449
|
+
logger.info('Procesando pedido', { pedido_id: row.id, estado: row.estado });
|
|
450
|
+
|
|
451
|
+
// ═══════════════════════════════════════════════════════════
|
|
452
|
+
// API.QUERY - Consultas SQL
|
|
453
|
+
// ═══════════════════════════════════════════════════════════
|
|
454
|
+
|
|
455
|
+
// Obtener líneas del pedido
|
|
456
|
+
const { rows: lineas } = await api.query(
|
|
457
|
+
`SELECT lp.*, p.nombre as producto_nombre, p.stock
|
|
458
|
+
FROM ${context.schema}.m308_t4137 lp -- lineas_pedido
|
|
459
|
+
JOIN ${context.schema}.m308_t4138 p ON p.id = lp.producto_id
|
|
460
|
+
WHERE lp.pedido_id = $1`,
|
|
461
|
+
[row.id]
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
// Actualizar stock de productos
|
|
465
|
+
for (const linea of lineas) {
|
|
466
|
+
await api.query(
|
|
467
|
+
`UPDATE ${context.schema}.m308_t4138
|
|
468
|
+
SET stock = stock - $1
|
|
469
|
+
WHERE id = $2`,
|
|
470
|
+
[linea.cantidad, linea.producto_id]
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
logger.debug('Stock actualizado', {
|
|
474
|
+
producto: linea.producto_nombre,
|
|
475
|
+
cantidad: linea.cantidad
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ═══════════════════════════════════════════════════════════
|
|
480
|
+
// API.EMAIL - Enviar emails
|
|
481
|
+
// ═══════════════════════════════════════════════════════════
|
|
482
|
+
|
|
483
|
+
// Obtener datos del cliente
|
|
484
|
+
const { rows: [cliente] } = await api.query(
|
|
485
|
+
`SELECT * FROM ${context.schema}.m308_t4135 WHERE id = $1`,
|
|
486
|
+
[row.cliente_id]
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
await api.email({
|
|
490
|
+
to: cliente.email,
|
|
491
|
+
subject: `Pedido ${row.numero} confirmado`,
|
|
492
|
+
body: `
|
|
493
|
+
Hola ${cliente.nombre},
|
|
494
|
+
|
|
495
|
+
Tu pedido ${row.numero} ha sido confirmado.
|
|
496
|
+
|
|
497
|
+
Total: ${row.total}€
|
|
498
|
+
|
|
499
|
+
Gracias por tu compra.
|
|
500
|
+
`,
|
|
501
|
+
// O usar HTML:
|
|
502
|
+
html: `<h1>Pedido Confirmado</h1><p>...</p>`
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// ═══════════════════════════════════════════════════════════
|
|
506
|
+
// API.HTTP - Llamadas a APIs externas
|
|
507
|
+
// ═══════════════════════════════════════════════════════════
|
|
508
|
+
|
|
509
|
+
// Webhook a sistema externo
|
|
510
|
+
if (env.WEBHOOK_URL) {
|
|
511
|
+
const response = await api.http(env.WEBHOOK_URL, {
|
|
512
|
+
method: 'POST',
|
|
513
|
+
headers: {
|
|
514
|
+
'Content-Type': 'application/json',
|
|
515
|
+
'Authorization': `Bearer ${env.WEBHOOK_TOKEN}`
|
|
516
|
+
},
|
|
517
|
+
body: JSON.stringify({
|
|
518
|
+
event: 'pedido_confirmado',
|
|
519
|
+
pedido: row,
|
|
520
|
+
cliente: cliente
|
|
521
|
+
})
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
logger.info('Webhook enviado', { status: response.status });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ═══════════════════════════════════════════════════════════
|
|
528
|
+
// RETURN - Resultado de la automation
|
|
529
|
+
// ═══════════════════════════════════════════════════════════
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
success: true,
|
|
533
|
+
message: `Pedido ${row.numero} procesado correctamente`,
|
|
534
|
+
data: {
|
|
535
|
+
lineas_procesadas: lineas.length,
|
|
536
|
+
email_enviado: true
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### Variables de Entorno (env)
|
|
543
|
+
|
|
544
|
+
Cada company puede tener variables de entorno configuradas en `core.company_env_variables`:
|
|
545
|
+
|
|
546
|
+
```javascript
|
|
547
|
+
// Acceso en automations
|
|
548
|
+
const apiKey = env.STRIPE_API_KEY;
|
|
549
|
+
const webhookUrl = env.SLACK_WEBHOOK;
|
|
550
|
+
const emailFrom = env.EMAIL_FROM || 'noreply@gufi.com';
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Automation con Input del Usuario (Click)
|
|
554
|
+
|
|
555
|
+
```javascript
|
|
556
|
+
// Definición en el JSON del módulo:
|
|
557
|
+
{
|
|
558
|
+
"trigger": "click",
|
|
559
|
+
"function_name": "enviar_recordatorio",
|
|
560
|
+
"label": "Enviar Recordatorio",
|
|
561
|
+
"icon": "Mail",
|
|
562
|
+
"input_schema": {
|
|
563
|
+
"mensaje": {
|
|
564
|
+
"type": "textarea",
|
|
565
|
+
"label": "Mensaje personalizado",
|
|
566
|
+
"required": true
|
|
567
|
+
},
|
|
568
|
+
"urgente": {
|
|
569
|
+
"type": "boolean",
|
|
570
|
+
"label": "Marcar como urgente",
|
|
571
|
+
"default": false
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// La función recibe el input:
|
|
577
|
+
async function enviar_recordatorio(context, api, logger) {
|
|
578
|
+
const { row, input } = context;
|
|
579
|
+
|
|
580
|
+
// input.mensaje contiene el texto del usuario
|
|
581
|
+
// input.urgente contiene true/false
|
|
582
|
+
|
|
583
|
+
await api.email({
|
|
584
|
+
to: row.cliente_email,
|
|
585
|
+
subject: input.urgente ? '🚨 URGENTE: ' + row.numero : row.numero,
|
|
586
|
+
body: input.mensaje
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
return { success: true };
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
## Views (Vistas del Marketplace)
|
|
596
|
+
|
|
597
|
+
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.
|
|
598
|
+
|
|
599
|
+
### Dónde se almacenan
|
|
600
|
+
|
|
601
|
+
```sql
|
|
602
|
+
SELECT id, name, code FROM marketplace.views WHERE package_id = 14;
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### Estructura de Archivos de una View
|
|
606
|
+
|
|
607
|
+
Cuando descargas una view con `gufi pull`, obtienes:
|
|
608
|
+
|
|
609
|
+
```
|
|
610
|
+
~/gufi-dev/mi-vista/
|
|
611
|
+
├── index.tsx # Entry point - exporta featureConfig y default
|
|
612
|
+
├── types.ts # Interfaces TypeScript
|
|
613
|
+
│
|
|
614
|
+
├── core/
|
|
615
|
+
│ └── dataProvider.ts # 💜 dataSources + featureConfig (MUY IMPORTANTE)
|
|
616
|
+
│
|
|
617
|
+
├── metadata/
|
|
618
|
+
│ ├── inputs.ts # 💜 Inputs configurables por usuario
|
|
619
|
+
│ ├── help.es.ts # Documentación en español
|
|
620
|
+
│ └── help.en.ts # Documentación en inglés
|
|
621
|
+
│
|
|
622
|
+
├── components/
|
|
623
|
+
│ └── MiComponente.tsx # Componentes React
|
|
624
|
+
│
|
|
625
|
+
├── views/
|
|
626
|
+
│ └── page.tsx # Página principal
|
|
627
|
+
│
|
|
628
|
+
└── automations/ # Opcional: automations de la vista
|
|
629
|
+
└── mi_automation.js
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
---
|
|
633
|
+
|
|
634
|
+
### 💜 dataSources - Declarar qué Tablas Necesita la Vista
|
|
635
|
+
|
|
636
|
+
El archivo `core/dataProvider.ts` define qué tablas necesita la vista. El Developer Center lee esto y muestra UI para que el usuario asigne sus tablas.
|
|
637
|
+
|
|
638
|
+
```typescript
|
|
639
|
+
// core/dataProvider.ts
|
|
640
|
+
// 💜 Mi Vista - Data Sources & Feature Configuration
|
|
641
|
+
|
|
642
|
+
/* ============================================================================
|
|
643
|
+
💜 Data Sources - Tablas que necesita esta vista
|
|
644
|
+
============================================================================ */
|
|
645
|
+
export const dataSources = [
|
|
646
|
+
{
|
|
647
|
+
key: 'tareasTable', // Identificador único
|
|
648
|
+
type: 'table', // Siempre 'table'
|
|
649
|
+
label: { es: 'Tabla de Tareas', en: 'Tasks Table' },
|
|
650
|
+
description: {
|
|
651
|
+
es: 'Tabla principal de tareas',
|
|
652
|
+
en: 'Main tasks table'
|
|
653
|
+
},
|
|
654
|
+
required: true, // true = obligatorio
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
key: 'proyectosTable',
|
|
658
|
+
type: 'table',
|
|
659
|
+
label: { es: 'Proyectos', en: 'Projects' },
|
|
660
|
+
description: {
|
|
661
|
+
es: 'Tabla de proyectos (opcional)',
|
|
662
|
+
en: 'Projects table (optional)'
|
|
663
|
+
},
|
|
664
|
+
required: false, // false = opcional
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
key: 'comentariosTable',
|
|
668
|
+
type: 'table',
|
|
669
|
+
label: { es: 'Comentarios', en: 'Comments' },
|
|
670
|
+
description: {
|
|
671
|
+
es: 'Comentarios en tareas',
|
|
672
|
+
en: 'Task comments'
|
|
673
|
+
},
|
|
674
|
+
required: false,
|
|
675
|
+
},
|
|
676
|
+
] as const;
|
|
677
|
+
|
|
678
|
+
/* ============================================================================
|
|
679
|
+
💜 Feature Config - EXPORTAR SIEMPRE
|
|
680
|
+
============================================================================ */
|
|
681
|
+
import { featureInputs } from '../metadata/inputs';
|
|
682
|
+
|
|
683
|
+
export const featureConfig = {
|
|
684
|
+
dataSources,
|
|
685
|
+
inputs: featureInputs,
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// También exporta funciones de carga de datos...
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
**En el index.tsx:**
|
|
692
|
+
```typescript
|
|
693
|
+
// index.tsx
|
|
694
|
+
export { featureConfig } from './core/dataProvider';
|
|
695
|
+
export default function MiVista({ gufi }) { ... }
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
**Cómo acceder a las tablas configuradas:**
|
|
699
|
+
```typescript
|
|
700
|
+
export default function MiVista({ gufi }) {
|
|
701
|
+
const viewSpec = gufi?.context?.viewSpec || {};
|
|
702
|
+
|
|
703
|
+
// Keys de dataSources → nombres de tabla físicos
|
|
704
|
+
const tareasTable = viewSpec.tareasTable; // "m360_t16194"
|
|
705
|
+
const proyectosTable = viewSpec.proyectosTable; // "m360_t16200" o undefined
|
|
706
|
+
|
|
707
|
+
// Usar para queries
|
|
708
|
+
const res = await gufi.dataProvider.getList({
|
|
709
|
+
resource: tareasTable,
|
|
710
|
+
pagination: { current: 1, pageSize: 100 },
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
---
|
|
716
|
+
|
|
717
|
+
### 💜 inputs (featureInputs) - Configuración del Usuario
|
|
718
|
+
|
|
719
|
+
Permite que el usuario configure opciones de la vista sin tocar código.
|
|
720
|
+
|
|
721
|
+
```typescript
|
|
722
|
+
// metadata/inputs.ts
|
|
723
|
+
// 💜 Mi Vista - Inputs Configurables
|
|
724
|
+
|
|
725
|
+
export const featureInputs = [
|
|
726
|
+
// BOOLEAN - Checkbox
|
|
727
|
+
{
|
|
728
|
+
key: 'showCompleted',
|
|
729
|
+
type: 'boolean',
|
|
730
|
+
label: { es: 'Mostrar completadas', en: 'Show completed' },
|
|
731
|
+
description: {
|
|
732
|
+
es: 'Incluir tareas completadas',
|
|
733
|
+
en: 'Include completed tasks'
|
|
734
|
+
},
|
|
735
|
+
default: false,
|
|
736
|
+
},
|
|
737
|
+
|
|
738
|
+
// SELECT - Dropdown estático
|
|
739
|
+
{
|
|
740
|
+
key: 'defaultView',
|
|
741
|
+
type: 'select',
|
|
742
|
+
label: { es: 'Vista por defecto', en: 'Default view' },
|
|
743
|
+
description: {
|
|
744
|
+
es: 'Panel inicial al abrir',
|
|
745
|
+
en: 'Initial panel on open'
|
|
746
|
+
},
|
|
747
|
+
options: [
|
|
748
|
+
{ value: 'list', label: { es: 'Lista', en: 'List' } },
|
|
749
|
+
{ value: 'kanban', label: { es: 'Kanban', en: 'Kanban' } },
|
|
750
|
+
{ value: 'calendar', label: { es: 'Calendario', en: 'Calendar' } },
|
|
751
|
+
],
|
|
752
|
+
default: 'list',
|
|
753
|
+
},
|
|
754
|
+
|
|
755
|
+
// NUMBER - Input numérico
|
|
756
|
+
{
|
|
757
|
+
key: 'lowStockThreshold',
|
|
758
|
+
type: 'number',
|
|
759
|
+
label: { es: 'Umbral stock bajo (%)', en: 'Low stock threshold (%)' },
|
|
760
|
+
description: {
|
|
761
|
+
es: 'Porcentaje para alertas',
|
|
762
|
+
en: 'Percentage for alerts'
|
|
763
|
+
},
|
|
764
|
+
placeholder: '40',
|
|
765
|
+
default: 40,
|
|
766
|
+
},
|
|
767
|
+
|
|
768
|
+
// TEXT - Input de texto
|
|
769
|
+
{
|
|
770
|
+
key: 'emailCc',
|
|
771
|
+
type: 'text',
|
|
772
|
+
label: { es: 'Email CC', en: 'CC Email' },
|
|
773
|
+
description: {
|
|
774
|
+
es: 'Email en copia',
|
|
775
|
+
en: 'CC email'
|
|
776
|
+
},
|
|
777
|
+
placeholder: 'admin@empresa.com',
|
|
778
|
+
},
|
|
779
|
+
|
|
780
|
+
// SELECT con opciones de otra tabla (dinámico)
|
|
781
|
+
{
|
|
782
|
+
key: 'defaultWarehouse',
|
|
783
|
+
type: 'select',
|
|
784
|
+
label: { es: 'Almacén por defecto', en: 'Default warehouse' },
|
|
785
|
+
description: {
|
|
786
|
+
es: 'Almacén seleccionado al abrir',
|
|
787
|
+
en: 'Warehouse on open'
|
|
788
|
+
},
|
|
789
|
+
optionsFrom: 'warehousesTable', // 💜 Carga opciones del dataSource
|
|
790
|
+
default: 'all',
|
|
791
|
+
},
|
|
792
|
+
] as const;
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
**Acceder a inputs en runtime:**
|
|
796
|
+
```typescript
|
|
797
|
+
export default function MiVista({ gufi }) {
|
|
798
|
+
const viewSpec = gufi?.context?.viewSpec || {};
|
|
799
|
+
|
|
800
|
+
// Los inputs están directamente en viewSpec
|
|
801
|
+
const showCompleted = viewSpec.showCompleted ?? false;
|
|
802
|
+
const defaultView = viewSpec.defaultView ?? 'list';
|
|
803
|
+
const threshold = viewSpec.lowStockThreshold ?? 40;
|
|
804
|
+
const emailCc = viewSpec.emailCc;
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
---
|
|
809
|
+
|
|
810
|
+
### 💜 help - Documentación para Usuarios
|
|
811
|
+
|
|
812
|
+
Crea archivos de ayuda que aparecen en el botón "?" de la vista.
|
|
813
|
+
|
|
814
|
+
```typescript
|
|
815
|
+
// metadata/help.es.ts
|
|
816
|
+
export const help = {
|
|
817
|
+
// Para Admin/Consultant
|
|
818
|
+
consultant: {
|
|
819
|
+
title: "Mi Vista",
|
|
820
|
+
description: "Descripción breve de la vista.",
|
|
821
|
+
sections: [
|
|
822
|
+
{
|
|
823
|
+
id: "overview",
|
|
824
|
+
title: "Descripción General",
|
|
825
|
+
content: `
|
|
826
|
+
**Funcionalidades:**
|
|
827
|
+
- Lista de tareas con filtros
|
|
828
|
+
- Vista Kanban por proyecto
|
|
829
|
+
- Sistema de comentarios
|
|
830
|
+
|
|
831
|
+
**Tablas requeridas:**
|
|
832
|
+
| DataSource | Descripción |
|
|
833
|
+
|------------|-------------|
|
|
834
|
+
| tareasTable | Tabla principal de tareas |
|
|
835
|
+
| proyectosTable | Proyectos (opcional) |
|
|
836
|
+
`.trim(),
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
id: "configuration",
|
|
840
|
+
title: "Configuración",
|
|
841
|
+
content: `
|
|
842
|
+
**Campos requeridos en tareas:**
|
|
843
|
+
- titulo (text)
|
|
844
|
+
- estado (select: pendiente, en_progreso, completada)
|
|
845
|
+
- asignado_a (users)
|
|
846
|
+
- proyecto_id (relation, opcional)
|
|
847
|
+
`.trim(),
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
id: "permissions",
|
|
851
|
+
title: "Permisos",
|
|
852
|
+
content: `
|
|
853
|
+
| Permiso | Efecto |
|
|
854
|
+
|---------|--------|
|
|
855
|
+
| * | Acceso completo |
|
|
856
|
+
| entity:view | Solo lectura |
|
|
857
|
+
| create_tasks | Puede crear tareas |
|
|
858
|
+
`.trim(),
|
|
859
|
+
},
|
|
860
|
+
],
|
|
861
|
+
},
|
|
862
|
+
|
|
863
|
+
// Para usuarios finales
|
|
864
|
+
user: {
|
|
865
|
+
title: "Guía de Uso",
|
|
866
|
+
sections: [
|
|
867
|
+
{
|
|
868
|
+
id: "basics",
|
|
869
|
+
title: "Uso Básico",
|
|
870
|
+
content: `
|
|
871
|
+
1. **Mis Tareas**: Tareas asignadas a ti
|
|
872
|
+
2. **Proyectos**: Vista Kanban por proyecto
|
|
873
|
+
3. **Gestión**: Tareas que has asignado
|
|
874
|
+
|
|
875
|
+
Para crear tarea, pulsa el botón **+**
|
|
876
|
+
`.trim(),
|
|
877
|
+
},
|
|
878
|
+
],
|
|
879
|
+
},
|
|
880
|
+
};
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
---
|
|
884
|
+
|
|
885
|
+
### 💜 automations en Vistas
|
|
886
|
+
|
|
887
|
+
Las vistas pueden incluir automations que se ejecutan al hacer click.
|
|
888
|
+
|
|
889
|
+
**Archivo de automation:**
|
|
890
|
+
```javascript
|
|
891
|
+
// automations/send_notification.js
|
|
892
|
+
/**
|
|
893
|
+
* 💜 Automation: send_notification
|
|
894
|
+
* Trigger: ON_CLICK (desde la vista)
|
|
895
|
+
*/
|
|
896
|
+
async function send_notification(context, api, logger) {
|
|
897
|
+
const { input, env } = context;
|
|
898
|
+
const { tarea_id, mensaje } = input;
|
|
899
|
+
|
|
900
|
+
// Obtener tarea
|
|
901
|
+
const { rows } = await api.query(
|
|
902
|
+
'SELECT titulo, asignado_a FROM tareas WHERE id = $1',
|
|
903
|
+
[tarea_id]
|
|
904
|
+
);
|
|
905
|
+
|
|
906
|
+
// Obtener email del asignado
|
|
907
|
+
const { rows: users } = await api.query(
|
|
908
|
+
'SELECT email FROM core.users WHERE id = $1',
|
|
909
|
+
[rows[0].asignado_a]
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
// Enviar email
|
|
913
|
+
await api.email({
|
|
914
|
+
to: users[0].email,
|
|
915
|
+
subject: `Notificación: ${rows[0].titulo}`,
|
|
916
|
+
html: `<p>${mensaje}</p>`,
|
|
917
|
+
cc: env.NOTIFICATION_CC,
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
logger.info('Notificación enviada', { tarea_id });
|
|
921
|
+
return { success: true };
|
|
922
|
+
}
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
**Llamar automation desde React:**
|
|
926
|
+
```typescript
|
|
927
|
+
const handleNotify = async (tareaId: number) => {
|
|
928
|
+
try {
|
|
929
|
+
const result = await gufi.utils.runClickAutomation({
|
|
930
|
+
functionName: 'send_notification',
|
|
931
|
+
input: {
|
|
932
|
+
tarea_id: tareaId,
|
|
933
|
+
mensaje: 'Tu tarea ha sido actualizada',
|
|
934
|
+
},
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
if (result.success) {
|
|
938
|
+
gufi.utils.toastSuccess('Notificación enviada');
|
|
939
|
+
}
|
|
940
|
+
} catch (err) {
|
|
941
|
+
gufi.utils.toastError(err.message);
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
---
|
|
947
|
+
|
|
948
|
+
### 💜 El Objeto `gufi` - Props Completas
|
|
949
|
+
|
|
950
|
+
Toda vista recibe `gufi` con todo lo necesario:
|
|
951
|
+
|
|
952
|
+
```typescript
|
|
953
|
+
interface GufiProps {
|
|
954
|
+
context: {
|
|
955
|
+
user: { id, email, name, role };
|
|
956
|
+
lang: 'es' | 'en';
|
|
957
|
+
viewSpec: {
|
|
958
|
+
// dataSources configurados
|
|
959
|
+
tareasTable: string;
|
|
960
|
+
proyectosTable?: string;
|
|
961
|
+
// inputs configurados
|
|
962
|
+
showCompleted: boolean;
|
|
963
|
+
defaultView: string;
|
|
964
|
+
lowStockThreshold: number;
|
|
965
|
+
};
|
|
966
|
+
companyId: number;
|
|
967
|
+
moduleId: number;
|
|
968
|
+
entityId: number;
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
dataProvider: {
|
|
972
|
+
getList({ resource, pagination, filters, sorters });
|
|
973
|
+
getOne({ resource, id });
|
|
974
|
+
create({ resource, variables });
|
|
975
|
+
update({ resource, id, variables });
|
|
976
|
+
deleteOne({ resource, id });
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
utils: {
|
|
980
|
+
toastSuccess(msg: string);
|
|
981
|
+
toastError(msg: string);
|
|
982
|
+
toastInfo(msg: string);
|
|
983
|
+
runClickAutomation({ functionName, input });
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
---
|
|
989
|
+
|
|
990
|
+
### 💜 Checklist para Nueva Vista
|
|
991
|
+
|
|
992
|
+
1. [ ] `core/dataProvider.ts` - dataSources + export featureConfig
|
|
993
|
+
2. [ ] `metadata/inputs.ts` - featureInputs configurables
|
|
994
|
+
3. [ ] `metadata/help.es.ts` y `help.en.ts` - Documentación
|
|
995
|
+
4. [ ] `index.tsx` - export featureConfig y default component
|
|
996
|
+
5. [ ] Usar `gufi?.context?.viewSpec` para tablas e inputs
|
|
997
|
+
6. [ ] Usar `gufi?.dataProvider` para CRUD
|
|
998
|
+
7. [ ] Usar `gufi?.utils?.toast*` para notificaciones
|
|
999
|
+
8. [ ] Si hay automation: carpeta `automations/` con .js
|
|
1000
|
+
|
|
1001
|
+
### View.tsx - Componente Principal
|
|
1002
|
+
|
|
1003
|
+
```tsx
|
|
1004
|
+
import React, { useState, useEffect } from 'react';
|
|
1005
|
+
import { useList, useUpdate, useCreate } from '@refinedev/core';
|
|
1006
|
+
|
|
1007
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1008
|
+
// PROPS que recibe toda View
|
|
1009
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1010
|
+
interface ViewProps {
|
|
1011
|
+
// Configuración de la vista
|
|
1012
|
+
viewSpec: {
|
|
1013
|
+
id: number;
|
|
1014
|
+
name: string;
|
|
1015
|
+
config: Record<string, any>; // Config guardada por el usuario
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
// Contexto de la company
|
|
1019
|
+
context: {
|
|
1020
|
+
companyId: number;
|
|
1021
|
+
moduleId: number;
|
|
1022
|
+
entityId: number;
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
// Usuario actual
|
|
1026
|
+
user: {
|
|
1027
|
+
id: number;
|
|
1028
|
+
email: string;
|
|
1029
|
+
name: string;
|
|
1030
|
+
role: string;
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
// Notificaciones
|
|
1034
|
+
toastSuccess: (message: string) => void;
|
|
1035
|
+
toastError: (message: string) => void;
|
|
1036
|
+
toastInfo: (message: string) => void;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1040
|
+
// COMPONENTE PRINCIPAL
|
|
1041
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1042
|
+
export default function MiVista({ viewSpec, context, user, toastSuccess, toastError }: ViewProps) {
|
|
1043
|
+
const [filtro, setFiltro] = useState('todos');
|
|
1044
|
+
|
|
1045
|
+
// ─────────────────────────────────────────────────────────────
|
|
1046
|
+
// useList - Obtener lista de registros
|
|
1047
|
+
// ─────────────────────────────────────────────────────────────
|
|
1048
|
+
const { data, isLoading, refetch } = useList({
|
|
1049
|
+
resource: 'm360_t16192', // Nombre físico de la tabla
|
|
1050
|
+
pagination: { current: 1, pageSize: 100 },
|
|
1051
|
+
filters: filtro !== 'todos' ? [
|
|
1052
|
+
{ field: 'estado', operator: 'eq', value: filtro }
|
|
1053
|
+
] : [],
|
|
1054
|
+
sorters: [
|
|
1055
|
+
{ field: 'created_at', order: 'desc' }
|
|
1056
|
+
]
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
// ─────────────────────────────────────────────────────────────
|
|
1060
|
+
// useUpdate - Actualizar registros
|
|
1061
|
+
// ─────────────────────────────────────────────────────────────
|
|
1062
|
+
const { mutate: updateRecord } = useUpdate();
|
|
1063
|
+
|
|
1064
|
+
const handleStatusChange = (id: number, newStatus: string) => {
|
|
1065
|
+
updateRecord(
|
|
1066
|
+
{
|
|
1067
|
+
resource: 'm360_t16192',
|
|
1068
|
+
id,
|
|
1069
|
+
values: { estado: newStatus }
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
onSuccess: () => {
|
|
1073
|
+
toastSuccess('Estado actualizado');
|
|
1074
|
+
refetch();
|
|
1075
|
+
},
|
|
1076
|
+
onError: (error) => {
|
|
1077
|
+
toastError('Error al actualizar: ' + error.message);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
);
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
// ─────────────────────────────────────────────────────────────
|
|
1084
|
+
// useCreate - Crear registros
|
|
1085
|
+
// ─────────────────────────────────────────────────────────────
|
|
1086
|
+
const { mutate: createRecord } = useCreate();
|
|
1087
|
+
|
|
1088
|
+
const handleCreate = (data: any) => {
|
|
1089
|
+
createRecord(
|
|
1090
|
+
{
|
|
1091
|
+
resource: 'm360_t16192',
|
|
1092
|
+
values: data
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
onSuccess: () => {
|
|
1096
|
+
toastSuccess('Registro creado');
|
|
1097
|
+
refetch();
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
);
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
// ─────────────────────────────────────────────────────────────
|
|
1104
|
+
// RENDER
|
|
1105
|
+
// ─────────────────────────────────────────────────────────────
|
|
1106
|
+
if (isLoading) {
|
|
1107
|
+
return (
|
|
1108
|
+
<div className="flex items-center justify-center h-64">
|
|
1109
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-violet-600" />
|
|
1110
|
+
</div>
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
return (
|
|
1115
|
+
<div className="p-6 bg-gradient-to-br from-violet-50/40 via-white to-purple-50/40 min-h-screen">
|
|
1116
|
+
{/* Header */}
|
|
1117
|
+
<div className="flex items-center justify-between mb-6">
|
|
1118
|
+
<h1 className="text-2xl font-bold text-gray-900">
|
|
1119
|
+
{viewSpec.name}
|
|
1120
|
+
</h1>
|
|
1121
|
+
<div className="flex gap-2">
|
|
1122
|
+
<select
|
|
1123
|
+
value={filtro}
|
|
1124
|
+
onChange={(e) => setFiltro(e.target.value)}
|
|
1125
|
+
className="px-3 py-2 border rounded-lg"
|
|
1126
|
+
>
|
|
1127
|
+
<option value="todos">Todos</option>
|
|
1128
|
+
<option value="pendiente">Pendientes</option>
|
|
1129
|
+
<option value="completado">Completados</option>
|
|
1130
|
+
</select>
|
|
1131
|
+
</div>
|
|
1132
|
+
</div>
|
|
1133
|
+
|
|
1134
|
+
{/* Lista */}
|
|
1135
|
+
<div className="grid gap-4">
|
|
1136
|
+
{data?.data.map((item: any) => (
|
|
1137
|
+
<div
|
|
1138
|
+
key={item.id}
|
|
1139
|
+
className="p-4 bg-white rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow"
|
|
1140
|
+
>
|
|
1141
|
+
<div className="flex items-center justify-between">
|
|
1142
|
+
<div>
|
|
1143
|
+
<h3 className="font-medium text-gray-900">{item.titulo}</h3>
|
|
1144
|
+
<p className="text-sm text-gray-500">{item.descripcion}</p>
|
|
1145
|
+
</div>
|
|
1146
|
+
<button
|
|
1147
|
+
onClick={() => handleStatusChange(item.id, 'completado')}
|
|
1148
|
+
className="px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700"
|
|
1149
|
+
>
|
|
1150
|
+
Completar
|
|
1151
|
+
</button>
|
|
1152
|
+
</div>
|
|
1153
|
+
</div>
|
|
1154
|
+
))}
|
|
1155
|
+
</div>
|
|
1156
|
+
|
|
1157
|
+
{/* Empty state */}
|
|
1158
|
+
{data?.data.length === 0 && (
|
|
1159
|
+
<div className="text-center py-12 text-gray-500">
|
|
1160
|
+
No hay registros que mostrar
|
|
1161
|
+
</div>
|
|
1162
|
+
)}
|
|
1163
|
+
</div>
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
### dataProvider.ts - Lógica de Datos Custom
|
|
1169
|
+
|
|
1170
|
+
```typescript
|
|
1171
|
+
// core/dataProvider.ts
|
|
1172
|
+
import { DataProvider } from '@refinedev/core';
|
|
1173
|
+
|
|
1174
|
+
// Puedes extender el dataProvider por defecto
|
|
1175
|
+
export const customDataProvider = (baseProvider: DataProvider): DataProvider => ({
|
|
1176
|
+
...baseProvider,
|
|
1177
|
+
|
|
1178
|
+
// Override getList para transformar datos
|
|
1179
|
+
getList: async (params) => {
|
|
1180
|
+
const result = await baseProvider.getList(params);
|
|
1181
|
+
|
|
1182
|
+
// Transformar datos antes de devolverlos
|
|
1183
|
+
return {
|
|
1184
|
+
...result,
|
|
1185
|
+
data: result.data.map(item => ({
|
|
1186
|
+
...item,
|
|
1187
|
+
// Agregar campos calculados
|
|
1188
|
+
diasPendiente: calcularDias(item.created_at),
|
|
1189
|
+
prioridadColor: getPrioridadColor(item.prioridad)
|
|
1190
|
+
}))
|
|
1191
|
+
};
|
|
1192
|
+
},
|
|
1193
|
+
|
|
1194
|
+
// Custom method para operaciones especiales
|
|
1195
|
+
custom: async ({ url, method, payload }) => {
|
|
1196
|
+
const response = await fetch(url, {
|
|
1197
|
+
method,
|
|
1198
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1199
|
+
body: JSON.stringify(payload)
|
|
1200
|
+
});
|
|
1201
|
+
return response.json();
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
function calcularDias(fecha: string): number {
|
|
1206
|
+
const diff = Date.now() - new Date(fecha).getTime();
|
|
1207
|
+
return Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function getPrioridadColor(prioridad: string): string {
|
|
1211
|
+
const colores: Record<string, string> = {
|
|
1212
|
+
alta: 'red',
|
|
1213
|
+
media: 'yellow',
|
|
1214
|
+
baja: 'green'
|
|
1215
|
+
};
|
|
1216
|
+
return colores[prioridad] || 'gray';
|
|
1217
|
+
}
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
---
|
|
1221
|
+
|
|
1222
|
+
## Packages del Marketplace
|
|
1223
|
+
|
|
1224
|
+
Un Package agrupa múltiples Views y módulos relacionados para distribuirlos.
|
|
1225
|
+
|
|
1226
|
+
### Estructura en Base de Datos
|
|
1227
|
+
|
|
1228
|
+
```sql
|
|
1229
|
+
-- Package publicado
|
|
1230
|
+
SELECT * FROM marketplace.packages WHERE id = 14;
|
|
1231
|
+
-- { id: 14, name: "Stock Management", version: "1.2.0", published_at: ... }
|
|
1232
|
+
|
|
1233
|
+
-- Vistas del package
|
|
1234
|
+
SELECT id, name FROM marketplace.views WHERE package_id = 14;
|
|
1235
|
+
-- { id: 5, name: "Stock Overview" }
|
|
1236
|
+
-- { id: 6, name: "Inventory Report" }
|
|
1237
|
+
|
|
1238
|
+
-- Módulos incluidos (snapshot)
|
|
1239
|
+
SELECT * FROM marketplace.package_modules WHERE package_id = 14;
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
### Flujo de Desarrollo → Publicación
|
|
1243
|
+
|
|
1244
|
+
```
|
|
1245
|
+
1. DESARROLLO (en tu company)
|
|
1246
|
+
└── Creas módulo "Stock" en company 116
|
|
1247
|
+
└── Creas views en Developer Center
|
|
1248
|
+
└── Testas todo localmente
|
|
1249
|
+
|
|
1250
|
+
2. CREAR PACKAGE
|
|
1251
|
+
└── Developer Center → Packages → New Package
|
|
1252
|
+
└── Agregas módulo "Stock"
|
|
1253
|
+
└── Agregas views "Stock Overview", "Inventory Report"
|
|
1254
|
+
|
|
1255
|
+
3. PUBLICAR
|
|
1256
|
+
└── Click "Publish to Marketplace"
|
|
1257
|
+
└── Sistema crea snapshot inmutable
|
|
1258
|
+
└── Versión 1.0.0 disponible
|
|
1259
|
+
|
|
1260
|
+
4. INSTALACIÓN (otra company)
|
|
1261
|
+
└── Company 200 instala el package
|
|
1262
|
+
└── Sistema crea módulo "Stock" en company_200
|
|
1263
|
+
└── Views disponibles en su menú
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
---
|
|
1267
|
+
|
|
1268
|
+
## Comandos del CLI
|
|
1269
|
+
|
|
1270
|
+
### Instalación
|
|
1271
|
+
|
|
1272
|
+
```bash
|
|
1273
|
+
npm install -g gufi-cli
|
|
1274
|
+
gufi login
|
|
1275
|
+
```
|
|
1276
|
+
|
|
1277
|
+
### Gestión de Companies y Módulos
|
|
1278
|
+
|
|
1279
|
+
```bash
|
|
1280
|
+
# Ver companies
|
|
1281
|
+
gufi companies
|
|
1282
|
+
|
|
1283
|
+
# Ver módulos de una company
|
|
1284
|
+
gufi modules 146
|
|
1285
|
+
|
|
1286
|
+
# Ver JSON de un módulo
|
|
1287
|
+
gufi module 360 -c 146
|
|
1288
|
+
|
|
1289
|
+
# Editar módulo con tu editor
|
|
1290
|
+
gufi module 360 -c 146 --edit
|
|
1291
|
+
|
|
1292
|
+
# Guardar JSON a archivo
|
|
1293
|
+
gufi module 360 -c 146 --file modulo.json
|
|
1294
|
+
|
|
1295
|
+
# Actualizar módulo desde archivo
|
|
1296
|
+
gufi module:update 360 modulo.json -c 146
|
|
1297
|
+
|
|
1298
|
+
# Validar sin aplicar cambios
|
|
1299
|
+
gufi module:update 360 modulo.json -c 146 --dry-run
|
|
1300
|
+
|
|
1301
|
+
# Crear nueva company
|
|
1302
|
+
gufi company:create "Mi Empresa"
|
|
1303
|
+
```
|
|
1304
|
+
|
|
1305
|
+
### Automations
|
|
1306
|
+
|
|
1307
|
+
```bash
|
|
1308
|
+
# Listar automation scripts
|
|
1309
|
+
gufi automations -c 116
|
|
1310
|
+
|
|
1311
|
+
# Ver código de una automation
|
|
1312
|
+
gufi automation calcular_stock -c 116
|
|
1313
|
+
|
|
1314
|
+
# Editar con tu editor
|
|
1315
|
+
gufi automation calcular_stock -c 116 --edit
|
|
1316
|
+
|
|
1317
|
+
# Guardar a archivo
|
|
1318
|
+
gufi automation calcular_stock -c 116 --file script.js
|
|
1319
|
+
```
|
|
1320
|
+
|
|
1321
|
+
### Desarrollo de Views
|
|
1322
|
+
|
|
1323
|
+
```bash
|
|
1324
|
+
# Ver tus packages y views
|
|
1325
|
+
gufi list
|
|
1326
|
+
|
|
1327
|
+
# Descargar view para editar localmente
|
|
1328
|
+
gufi pull "Stock Overview"
|
|
1329
|
+
|
|
1330
|
+
# Auto-sync al guardar archivos
|
|
1331
|
+
gufi watch
|
|
1332
|
+
|
|
1333
|
+
# Ver console.log del LivePreview
|
|
1334
|
+
gufi logs
|
|
1335
|
+
|
|
1336
|
+
# Subir cambios manualmente
|
|
1337
|
+
gufi push
|
|
1338
|
+
|
|
1339
|
+
# Ver estado de sincronización
|
|
1340
|
+
gufi status
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1343
|
+
### Opciones Globales
|
|
1344
|
+
|
|
1345
|
+
| Opción | Descripción |
|
|
1346
|
+
|--------|-------------|
|
|
1347
|
+
| `-c, --company <id>` | ID de company |
|
|
1348
|
+
| `-e, --edit` | Abrir en editor ($EDITOR) |
|
|
1349
|
+
| `-f, --file <path>` | Guardar a archivo |
|
|
1350
|
+
| `--dry-run` | Validar sin aplicar |
|
|
1351
|
+
| `-h, --help` | Ayuda del comando |
|
|
1352
|
+
|
|
1353
|
+
---
|
|
1354
|
+
|
|
1355
|
+
## Tips para Claude
|
|
1356
|
+
|
|
1357
|
+
### Cuándo usar cada comando
|
|
1358
|
+
|
|
1359
|
+
| El usuario quiere... | Comando |
|
|
1360
|
+
|---------------------|---------|
|
|
1361
|
+
| Ver qué companies tiene | `gufi companies` |
|
|
1362
|
+
| Ver estructura de una company | `gufi modules <company_id>` |
|
|
1363
|
+
| Ver/editar JSON de módulo | `gufi module <id> -c <company>` |
|
|
1364
|
+
| Ver/editar código de automation | `gufi automation <nombre> -c <company>` |
|
|
1365
|
+
| Desarrollar una vista | `gufi pull`, `gufi watch`, `gufi logs` |
|
|
1366
|
+
|
|
1367
|
+
### Errores comunes
|
|
1368
|
+
|
|
1369
|
+
| Error | Solución |
|
|
1370
|
+
|-------|----------|
|
|
1371
|
+
| "No estás logueado" | `gufi login` |
|
|
1372
|
+
| "Módulo no encontrado" | Verificar `-c <company_id>` |
|
|
1373
|
+
| "Token expirado" | `gufi login` de nuevo |
|
|
1374
|
+
| "JSON inválido" | Validar estructura del JSON |
|
|
1375
|
+
|
|
1376
|
+
### Conceptos clave
|
|
1377
|
+
|
|
1378
|
+
- **Multi-tenant**: Cada company = schema aislado en PostgreSQL
|
|
1379
|
+
- **Tablas físicas**: `m{moduleId}_t{entityId}`
|
|
1380
|
+
- **Módulos**: JSON que define estructura → Gufi crea tablas/UI
|
|
1381
|
+
- **Automations**: JS en DB, ejecutado por worker (pg-boss)
|
|
1382
|
+
- **Views**: React/TS en DB, ejecutado en frontend dinámicamente
|
|
1383
|
+
- **Marketplace**: Sistema de distribución de packages
|
|
1384
|
+
|
|
1385
|
+
---
|
|
1386
|
+
|
|
1387
|
+
## Links
|
|
1388
|
+
|
|
1389
|
+
- **Web**: https://gogufi.com
|
|
1390
|
+
- **Docs**: https://github.com/juanbp23/gogufi/blob/main/docs/guide/05-marketplace.md
|