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 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