gufi-cli 0.1.1 → 0.1.3

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,1491 @@
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
+ ### 💜 seedData - Datos de Ejemplo para Demo/Testing
991
+
992
+ 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.
993
+
994
+ ```typescript
995
+ // metadata/seedData.ts
996
+ export interface SeedDataConfig {
997
+ description: { es: string; en: string };
998
+ data: { [dataSourceKey: string]: Array<Record<string, any>> };
999
+ order: string[]; // Orden de creación (importante para referencias)
1000
+ }
1001
+
1002
+ export const seedData: SeedDataConfig = {
1003
+ description: {
1004
+ es: 'Crea empresas de ejemplo (Singular, FitVending), proyectos y tareas de prueba',
1005
+ en: 'Creates sample companies, projects and test tasks',
1006
+ },
1007
+
1008
+ // Orden de creación - tablas con referencias van después
1009
+ order: ['empresasTable', 'proyectosTable', 'tareasTable'],
1010
+
1011
+ data: {
1012
+ // Empresas/Clientes
1013
+ empresasTable: [
1014
+ {
1015
+ nombre: 'Singular',
1016
+ contacto: 'Contacto Singular',
1017
+ email: 'contacto@singular.es',
1018
+ estado: 'activo'
1019
+ },
1020
+ {
1021
+ nombre: 'FitVending',
1022
+ contacto: 'Equipo FitVending',
1023
+ email: 'info@fitvending.es',
1024
+ estado: 'activo'
1025
+ },
1026
+ ],
1027
+
1028
+ // Proyectos
1029
+ proyectosTable: [
1030
+ {
1031
+ nombre: 'Gufi ERP',
1032
+ descripcion: 'Desarrollo del ERP Gufi',
1033
+ color: '#8B5CF6',
1034
+ estado: 'activo',
1035
+ },
1036
+ ],
1037
+
1038
+ // Tareas - con referencias a otras tablas
1039
+ tareasTable: [
1040
+ {
1041
+ titulo: 'Revisar bug crítico',
1042
+ descripcion: 'Los usuarios reportan problemas',
1043
+ asignado_a: '@currentUser', // 💜 Token especial: usuario actual
1044
+ asignado_por: '@currentUser',
1045
+ prioridad: 'urgente',
1046
+ estado: 'pendiente',
1047
+ fecha_limite: '@today', // 💜 Token especial: fecha de hoy
1048
+ proyecto_id: '@ref:proyectosTable.0', // 💜 Referencia: ID del primer proyecto
1049
+ empresa_id: '@ref:empresasTable.0', // 💜 Referencia: ID de primera empresa
1050
+ },
1051
+ {
1052
+ titulo: 'Preparar demo',
1053
+ descripcion: 'Demo del módulo de facturación',
1054
+ asignado_a: '@currentUser',
1055
+ prioridad: 'alta',
1056
+ fecha_limite: '@tomorrow', // 💜 Token especial: mañana
1057
+ empresa_id: '@ref:empresasTable.1',
1058
+ },
1059
+ ],
1060
+ },
1061
+ };
1062
+ ```
1063
+
1064
+ **Tokens Especiales:**
1065
+ | Token | Descripción |
1066
+ |-------|-------------|
1067
+ | `@currentUser` | ID del usuario actual (para campos users) |
1068
+ | `@today` | Fecha de hoy (YYYY-MM-DD) |
1069
+ | `@tomorrow` | Fecha de mañana |
1070
+ | `@nextWeek` | Fecha dentro de 7 días |
1071
+ | `@ref:tableKey.index` | ID del registro creado en otra tabla |
1072
+
1073
+ **Agregar a featureConfig:**
1074
+ ```typescript
1075
+ // core/dataProvider.ts
1076
+ import { seedData } from '../metadata/seedData';
1077
+
1078
+ export const featureConfig = {
1079
+ dataSources,
1080
+ inputs: featureInputs,
1081
+ seedData, // 💜 Agregar aquí
1082
+ };
1083
+ ```
1084
+
1085
+ **Cargar desde Developer Center:**
1086
+ 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.
1087
+
1088
+ ---
1089
+
1090
+ ### 💜 Checklist para Nueva Vista
1091
+
1092
+ 1. [ ] `core/dataProvider.ts` - dataSources + export featureConfig
1093
+ 2. [ ] `metadata/inputs.ts` - featureInputs configurables
1094
+ 3. [ ] `metadata/seedData.ts` - Datos de ejemplo (opcional pero recomendado)
1095
+ 4. [ ] `metadata/help.es.ts` y `help.en.ts` - Documentación
1096
+ 5. [ ] `index.tsx` - export featureConfig y default component
1097
+ 6. [ ] Usar `gufi?.context?.viewSpec` para tablas e inputs
1098
+ 7. [ ] Usar `gufi?.dataProvider` para CRUD
1099
+ 8. [ ] Usar `gufi?.utils?.toast*` para notificaciones
1100
+ 9. [ ] Si hay automation: carpeta `automations/` con .js
1101
+
1102
+ ### View.tsx - Componente Principal
1103
+
1104
+ ```tsx
1105
+ import React, { useState, useEffect } from 'react';
1106
+ import { useList, useUpdate, useCreate } from '@refinedev/core';
1107
+
1108
+ // ═══════════════════════════════════════════════════════════════
1109
+ // PROPS que recibe toda View
1110
+ // ═══════════════════════════════════════════════════════════════
1111
+ interface ViewProps {
1112
+ // Configuración de la vista
1113
+ viewSpec: {
1114
+ id: number;
1115
+ name: string;
1116
+ config: Record<string, any>; // Config guardada por el usuario
1117
+ };
1118
+
1119
+ // Contexto de la company
1120
+ context: {
1121
+ companyId: number;
1122
+ moduleId: number;
1123
+ entityId: number;
1124
+ };
1125
+
1126
+ // Usuario actual
1127
+ user: {
1128
+ id: number;
1129
+ email: string;
1130
+ name: string;
1131
+ role: string;
1132
+ };
1133
+
1134
+ // Notificaciones
1135
+ toastSuccess: (message: string) => void;
1136
+ toastError: (message: string) => void;
1137
+ toastInfo: (message: string) => void;
1138
+ }
1139
+
1140
+ // ═══════════════════════════════════════════════════════════════
1141
+ // COMPONENTE PRINCIPAL
1142
+ // ═══════════════════════════════════════════════════════════════
1143
+ export default function MiVista({ viewSpec, context, user, toastSuccess, toastError }: ViewProps) {
1144
+ const [filtro, setFiltro] = useState('todos');
1145
+
1146
+ // ─────────────────────────────────────────────────────────────
1147
+ // useList - Obtener lista de registros
1148
+ // ─────────────────────────────────────────────────────────────
1149
+ const { data, isLoading, refetch } = useList({
1150
+ resource: 'm360_t16192', // Nombre físico de la tabla
1151
+ pagination: { current: 1, pageSize: 100 },
1152
+ filters: filtro !== 'todos' ? [
1153
+ { field: 'estado', operator: 'eq', value: filtro }
1154
+ ] : [],
1155
+ sorters: [
1156
+ { field: 'created_at', order: 'desc' }
1157
+ ]
1158
+ });
1159
+
1160
+ // ─────────────────────────────────────────────────────────────
1161
+ // useUpdate - Actualizar registros
1162
+ // ─────────────────────────────────────────────────────────────
1163
+ const { mutate: updateRecord } = useUpdate();
1164
+
1165
+ const handleStatusChange = (id: number, newStatus: string) => {
1166
+ updateRecord(
1167
+ {
1168
+ resource: 'm360_t16192',
1169
+ id,
1170
+ values: { estado: newStatus }
1171
+ },
1172
+ {
1173
+ onSuccess: () => {
1174
+ toastSuccess('Estado actualizado');
1175
+ refetch();
1176
+ },
1177
+ onError: (error) => {
1178
+ toastError('Error al actualizar: ' + error.message);
1179
+ }
1180
+ }
1181
+ );
1182
+ };
1183
+
1184
+ // ─────────────────────────────────────────────────────────────
1185
+ // useCreate - Crear registros
1186
+ // ─────────────────────────────────────────────────────────────
1187
+ const { mutate: createRecord } = useCreate();
1188
+
1189
+ const handleCreate = (data: any) => {
1190
+ createRecord(
1191
+ {
1192
+ resource: 'm360_t16192',
1193
+ values: data
1194
+ },
1195
+ {
1196
+ onSuccess: () => {
1197
+ toastSuccess('Registro creado');
1198
+ refetch();
1199
+ }
1200
+ }
1201
+ );
1202
+ };
1203
+
1204
+ // ─────────────────────────────────────────────────────────────
1205
+ // RENDER
1206
+ // ─────────────────────────────────────────────────────────────
1207
+ if (isLoading) {
1208
+ return (
1209
+ <div className="flex items-center justify-center h-64">
1210
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-violet-600" />
1211
+ </div>
1212
+ );
1213
+ }
1214
+
1215
+ return (
1216
+ <div className="p-6 bg-gradient-to-br from-violet-50/40 via-white to-purple-50/40 min-h-screen">
1217
+ {/* Header */}
1218
+ <div className="flex items-center justify-between mb-6">
1219
+ <h1 className="text-2xl font-bold text-gray-900">
1220
+ {viewSpec.name}
1221
+ </h1>
1222
+ <div className="flex gap-2">
1223
+ <select
1224
+ value={filtro}
1225
+ onChange={(e) => setFiltro(e.target.value)}
1226
+ className="px-3 py-2 border rounded-lg"
1227
+ >
1228
+ <option value="todos">Todos</option>
1229
+ <option value="pendiente">Pendientes</option>
1230
+ <option value="completado">Completados</option>
1231
+ </select>
1232
+ </div>
1233
+ </div>
1234
+
1235
+ {/* Lista */}
1236
+ <div className="grid gap-4">
1237
+ {data?.data.map((item: any) => (
1238
+ <div
1239
+ key={item.id}
1240
+ className="p-4 bg-white rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow"
1241
+ >
1242
+ <div className="flex items-center justify-between">
1243
+ <div>
1244
+ <h3 className="font-medium text-gray-900">{item.titulo}</h3>
1245
+ <p className="text-sm text-gray-500">{item.descripcion}</p>
1246
+ </div>
1247
+ <button
1248
+ onClick={() => handleStatusChange(item.id, 'completado')}
1249
+ className="px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700"
1250
+ >
1251
+ Completar
1252
+ </button>
1253
+ </div>
1254
+ </div>
1255
+ ))}
1256
+ </div>
1257
+
1258
+ {/* Empty state */}
1259
+ {data?.data.length === 0 && (
1260
+ <div className="text-center py-12 text-gray-500">
1261
+ No hay registros que mostrar
1262
+ </div>
1263
+ )}
1264
+ </div>
1265
+ );
1266
+ }
1267
+ ```
1268
+
1269
+ ### dataProvider.ts - Lógica de Datos Custom
1270
+
1271
+ ```typescript
1272
+ // core/dataProvider.ts
1273
+ import { DataProvider } from '@refinedev/core';
1274
+
1275
+ // Puedes extender el dataProvider por defecto
1276
+ export const customDataProvider = (baseProvider: DataProvider): DataProvider => ({
1277
+ ...baseProvider,
1278
+
1279
+ // Override getList para transformar datos
1280
+ getList: async (params) => {
1281
+ const result = await baseProvider.getList(params);
1282
+
1283
+ // Transformar datos antes de devolverlos
1284
+ return {
1285
+ ...result,
1286
+ data: result.data.map(item => ({
1287
+ ...item,
1288
+ // Agregar campos calculados
1289
+ diasPendiente: calcularDias(item.created_at),
1290
+ prioridadColor: getPrioridadColor(item.prioridad)
1291
+ }))
1292
+ };
1293
+ },
1294
+
1295
+ // Custom method para operaciones especiales
1296
+ custom: async ({ url, method, payload }) => {
1297
+ const response = await fetch(url, {
1298
+ method,
1299
+ headers: { 'Content-Type': 'application/json' },
1300
+ body: JSON.stringify(payload)
1301
+ });
1302
+ return response.json();
1303
+ }
1304
+ });
1305
+
1306
+ function calcularDias(fecha: string): number {
1307
+ const diff = Date.now() - new Date(fecha).getTime();
1308
+ return Math.floor(diff / (1000 * 60 * 60 * 24));
1309
+ }
1310
+
1311
+ function getPrioridadColor(prioridad: string): string {
1312
+ const colores: Record<string, string> = {
1313
+ alta: 'red',
1314
+ media: 'yellow',
1315
+ baja: 'green'
1316
+ };
1317
+ return colores[prioridad] || 'gray';
1318
+ }
1319
+ ```
1320
+
1321
+ ---
1322
+
1323
+ ## Packages del Marketplace
1324
+
1325
+ Un Package agrupa múltiples Views y módulos relacionados para distribuirlos.
1326
+
1327
+ ### Estructura en Base de Datos
1328
+
1329
+ ```sql
1330
+ -- Package publicado
1331
+ SELECT * FROM marketplace.packages WHERE id = 14;
1332
+ -- { id: 14, name: "Stock Management", version: "1.2.0", published_at: ... }
1333
+
1334
+ -- Vistas del package
1335
+ SELECT id, name FROM marketplace.views WHERE package_id = 14;
1336
+ -- { id: 5, name: "Stock Overview" }
1337
+ -- { id: 6, name: "Inventory Report" }
1338
+
1339
+ -- Módulos incluidos (snapshot)
1340
+ SELECT * FROM marketplace.package_modules WHERE package_id = 14;
1341
+ ```
1342
+
1343
+ ### Flujo de Desarrollo → Publicación
1344
+
1345
+ ```
1346
+ 1. DESARROLLO (en tu company)
1347
+ └── Creas módulo "Stock" en company 116
1348
+ └── Creas views en Developer Center
1349
+ └── Testas todo localmente
1350
+
1351
+ 2. CREAR PACKAGE
1352
+ └── Developer Center → Packages → New Package
1353
+ └── Agregas módulo "Stock"
1354
+ └── Agregas views "Stock Overview", "Inventory Report"
1355
+
1356
+ 3. PUBLICAR
1357
+ └── Click "Publish to Marketplace"
1358
+ └── Sistema crea snapshot inmutable
1359
+ └── Versión 1.0.0 disponible
1360
+
1361
+ 4. INSTALACIÓN (otra company)
1362
+ └── Company 200 instala el package
1363
+ └── Sistema crea módulo "Stock" en company_200
1364
+ └── Views disponibles en su menú
1365
+ ```
1366
+
1367
+ ---
1368
+
1369
+ ## Comandos del CLI
1370
+
1371
+ ### Instalación
1372
+
1373
+ ```bash
1374
+ npm install -g gufi-cli
1375
+ gufi login
1376
+ ```
1377
+
1378
+ ### Gestión de Companies y Módulos
1379
+
1380
+ ```bash
1381
+ # Ver companies
1382
+ gufi companies
1383
+
1384
+ # Ver módulos de una company
1385
+ gufi modules 146
1386
+
1387
+ # Ver JSON de un módulo
1388
+ gufi module 360 -c 146
1389
+
1390
+ # Editar módulo con tu editor
1391
+ gufi module 360 -c 146 --edit
1392
+
1393
+ # Guardar JSON a archivo
1394
+ gufi module 360 -c 146 --file modulo.json
1395
+
1396
+ # Actualizar módulo desde archivo
1397
+ gufi module:update 360 modulo.json -c 146
1398
+
1399
+ # Validar sin aplicar cambios
1400
+ gufi module:update 360 modulo.json -c 146 --dry-run
1401
+
1402
+ # Crear nueva company
1403
+ gufi company:create "Mi Empresa"
1404
+ ```
1405
+
1406
+ ### Automations
1407
+
1408
+ ```bash
1409
+ # Listar automation scripts
1410
+ gufi automations -c 116
1411
+
1412
+ # Ver código de una automation
1413
+ gufi automation calcular_stock -c 116
1414
+
1415
+ # Editar con tu editor
1416
+ gufi automation calcular_stock -c 116 --edit
1417
+
1418
+ # Guardar a archivo
1419
+ gufi automation calcular_stock -c 116 --file script.js
1420
+ ```
1421
+
1422
+ ### Desarrollo de Views
1423
+
1424
+ ```bash
1425
+ # Ver tus packages y views
1426
+ gufi list
1427
+
1428
+ # Descargar view para editar localmente
1429
+ gufi pull "Stock Overview"
1430
+
1431
+ # Auto-sync al guardar archivos
1432
+ gufi watch
1433
+
1434
+ # Ver console.log del LivePreview
1435
+ gufi logs
1436
+
1437
+ # Subir cambios manualmente
1438
+ gufi push
1439
+
1440
+ # Ver estado de sincronización
1441
+ gufi status
1442
+ ```
1443
+
1444
+ ### Opciones Globales
1445
+
1446
+ | Opción | Descripción |
1447
+ |--------|-------------|
1448
+ | `-c, --company <id>` | ID de company |
1449
+ | `-e, --edit` | Abrir en editor ($EDITOR) |
1450
+ | `-f, --file <path>` | Guardar a archivo |
1451
+ | `--dry-run` | Validar sin aplicar |
1452
+ | `-h, --help` | Ayuda del comando |
1453
+
1454
+ ---
1455
+
1456
+ ## Tips para Claude
1457
+
1458
+ ### Cuándo usar cada comando
1459
+
1460
+ | El usuario quiere... | Comando |
1461
+ |---------------------|---------|
1462
+ | Ver qué companies tiene | `gufi companies` |
1463
+ | Ver estructura de una company | `gufi modules <company_id>` |
1464
+ | Ver/editar JSON de módulo | `gufi module <id> -c <company>` |
1465
+ | Ver/editar código de automation | `gufi automation <nombre> -c <company>` |
1466
+ | Desarrollar una vista | `gufi pull`, `gufi watch`, `gufi logs` |
1467
+
1468
+ ### Errores comunes
1469
+
1470
+ | Error | Solución |
1471
+ |-------|----------|
1472
+ | "No estás logueado" | `gufi login` |
1473
+ | "Módulo no encontrado" | Verificar `-c <company_id>` |
1474
+ | "Token expirado" | `gufi login` de nuevo |
1475
+ | "JSON inválido" | Validar estructura del JSON |
1476
+
1477
+ ### Conceptos clave
1478
+
1479
+ - **Multi-tenant**: Cada company = schema aislado en PostgreSQL
1480
+ - **Tablas físicas**: `m{moduleId}_t{entityId}`
1481
+ - **Módulos**: JSON que define estructura → Gufi crea tablas/UI
1482
+ - **Automations**: JS en DB, ejecutado por worker (pg-boss)
1483
+ - **Views**: React/TS en DB, ejecutado en frontend dinámicamente
1484
+ - **Marketplace**: Sistema de distribución de packages
1485
+
1486
+ ---
1487
+
1488
+ ## Links
1489
+
1490
+ - **Web**: https://gogufi.com
1491
+ - **Docs**: https://github.com/juanbp23/gogufi/blob/main/docs/guide/05-marketplace.md