g360-cli 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,275 +1,397 @@
1
1
  /**
2
- * Template Manual de App G360 — 12 slides A4.
3
- *
2
+ * Template Manual de App G360 — formato 16:9 widescreen, estilo ventas-pulse.
3
+ *
4
4
  * Estructura:
5
- * 1. Portada
6
- * 2. ¿Qué es esta app?
7
- * 3. Requisitos / Instalacion
8
- * 4. Inicio primera pantalla
9
- * 5-N. Funcionalidades (una por feature detectada)
10
- * N+1. Flujos de trabajo (modals)
5
+ * 1. Portada
6
+ * 2. ¿Qué es? (descripcion + tarjetas usuario/tecnico)
7
+ * 3. Módulos (tabla de modulos + capturas)
8
+ * 4. Flujo de trabajo (pasos numerados + capturas)
9
+ * 5-N. Funcionalidades (una por modulo UI detectado)
10
+ * N+1. Flujos de trabajo (modales)
11
11
  * N+2. Arquitectura
12
12
  * N+3. Buenas prácticas
13
- * N+4. Resumen
13
+ * N+4. Límites conocidos (si el proyecto define appData.limits)
14
+ * N+5. Resumen
14
15
  */
15
- import PptxGenJS from 'pptxgenjs';
16
- import { createG360Theme, createCipsaTheme } from '../themes/index.js';
16
+ import path from 'path';
17
+ import { createTheme } from '../themes/index.js';
17
18
  import {
18
19
  coverSlide,
19
- screenshotSlide,
20
+ addSectionHeader,
21
+ addFooter,
22
+ card,
23
+ phoneShot,
20
24
  featureSlide,
21
25
  workflowSlide,
22
- architectureSlide,
26
+ limitsSlide,
23
27
  checklistSlide,
24
28
  kpiSlide,
25
- addFooter,
26
- addSectionHeader,
29
+ architectureSlide,
30
+ modulesTable,
27
31
  } from '../layouts/base.js';
28
32
 
33
+ const MAX_FEATURES = 6;
34
+
35
+ /** Descripciones genericas por clase UI conocida */
36
+ const FEATURE_DESC = {
37
+ KpiCard: 'Indicadores clave del negocio en tiempo real, con estados de alerta y recuento por categoría.',
38
+ Dashboard: 'Vista principal con el resumen operativo: KPIs, listados y accesos a los módulos de trabajo.',
39
+ WarehouseCard: 'Tarjeta de almacén con existencias y estado por depósito.',
40
+ SearchOverlay: 'Buscador flotante global: localiza registros de cualquier módulo sin salir de la vista.',
41
+ ExportModal: 'Exportación a Excel con formato corporativo para seguimiento y auditoría.',
42
+ SkuDetailModal: 'Detalle de SKU: existencias, movimientos y atributos relevantes.',
43
+ TrasladosModal: 'Traslados entre almacenes con validación de stock disponible.',
44
+ SinStockModal: 'Productos sin stock: prioriza reposición y alternativas.',
45
+ AppSidebar: 'Barra lateral de navegación entre módulos.',
46
+ };
47
+
48
+ function featureDesc(feature) {
49
+ return feature.desc
50
+ || FEATURE_DESC[feature.name]
51
+ || `Módulo de interfaz ubicado en src/ui/${feature.file}, integrado al flujo principal de la aplicación.`;
52
+ }
53
+
54
+ function classify(feature) {
55
+ if (feature.name.includes('Kpi') || feature.name.includes('Card')) return 'kpi';
56
+ if (feature.name.includes('Modal') || feature.name.includes('Dialog')) return 'workflow';
57
+ if (feature.name.includes('Dashboard')) return 'screenshot';
58
+ return 'feature';
59
+ }
60
+
29
61
  /**
30
- * Genera un manual PPTX completo para una app G360.
31
- *
62
+ * Genera un manual PPTX completo para una app G360 (16:9).
63
+ *
32
64
  * @param {object} appData — resultado de analyzeApp()
33
- * @param {object} options — { mode, theme, outDir }
65
+ * @param {object} options — { mode, theme, outPath }
34
66
  * @returns {Promise<string>} — ruta del archivo .pptx generado
35
67
  */
36
68
  export async function generateManualPptx(appData, options = {}) {
37
- const { mode = 'manual', theme: themeName = null } = options;
38
-
39
- // Seleccionar theme
40
- const theme = themeName === 'cipsa' || (!themeName && appData.brand === 'cipsa')
41
- ? createCipsaTheme()
42
- : createG360Theme();
43
-
69
+ const { mode = 'manual' } = options;
70
+ const themeName = options.theme || appData.brand || 'g360';
71
+ const theme = createTheme(themeName === 'cipsa' ? 'cipsa' : themeName);
44
72
  const { pptx, colors } = theme;
45
-
46
- // Configurar tamaño segun modo
47
- if (mode === 'demo') {
48
- pptx.layout = 'LAYOUT_16x9';
49
- } else {
50
- pptx.layout = 'A4';
51
- }
73
+ const S = theme.spacing;
52
74
 
53
- // ===== SLIDE 1: Portada =====
54
- let slide = pptx.addSlide();
55
- slide.background = { color: colors.bg };
75
+ const appName = appData.name || 'Mi Aplicación G360';
76
+ const features = appData.features.slice(0, MAX_FEATURES);
77
+ const hasModals = (appData.hasModals?.length || 0) > 0;
78
+ const hasLimits = Array.isArray(appData.limits) && appData.limits.length > 0;
79
+
80
+ const totalSlides = 4
81
+ + features.length
82
+ + (hasModals ? 1 : 0)
83
+ + 3
84
+ + (hasLimits ? 1 : 0);
85
+
86
+ let slideNum = 0;
87
+ const bg = { color: colors.bg };
88
+ const newSlide = () => {
89
+ const s = pptx.addSlide();
90
+ s.background = bg;
91
+ return s;
92
+ };
93
+
94
+ const screenshotFor = (feature) => {
95
+ if (feature.screenshotIndex >= 0 && appData.screenshots?.[feature.screenshotIndex]) {
96
+ return appData.screenshots[feature.screenshotIndex].path;
97
+ }
98
+ const shot = appData.screenshots?.find((s) =>
99
+ s.filename.toLowerCase().includes(feature.name.toLowerCase().slice(0, 4)),
100
+ );
101
+ return shot?.path || null;
102
+ };
103
+
104
+ // ===== 1. Portada =====
105
+ let slide = newSlide();
56
106
  coverSlide(slide, {
57
- appName: appData.name || 'Mi Aplicacion G360',
58
- description: appData.description || 'Documentacion de uso y funcionalidades',
107
+ appName,
108
+ description: appData.description || 'Documentación de uso y funcionalidades',
109
+ tagline: buildTagline(appData),
59
110
  version: appData.version,
60
- brand: appData.brand,
111
+ brand: themeName,
61
112
  }, theme);
62
- addFooter(slide, theme, 1, 12);
113
+ addFooter(slide, theme, ++slideNum, totalSlides);
63
114
 
64
- // ===== SLIDE 2: ¿Qué es? =====
65
- slide = pptx.addSlide();
66
- slide.background = { color: colors.bg };
67
- addSectionHeader(slide, 'INTRODUCCION', theme);
68
- featureSlide(slide, {
69
- title: '¿Qué es ' + (appData.name || 'esta app') + '?',
70
- description: appData.description || 'Aplicacion de escritorio construida con Flet y los estandares G360.',
71
- bullets: [
72
- 'Monitoriza datos en tiempo real desde el ERP',
73
- 'Genera reportes y analisis automaticos',
74
- 'Soporta dual theme (claro/oscuro)',
75
- 'Funciona offline con cache local',
76
- ],
115
+ // ===== 2. ¿Qué es? =====
116
+ slide = newSlide();
117
+ addSectionHeader(slide, 'INTRODUCCIÓN', theme, ++slideNum, totalSlides);
118
+ slide.addText(`¿Qué es ${appName}?`, {
119
+ x: S.marginX, y: 1.05, w: S.contentWidth, h: 0.45,
120
+ fontSize: 15, bold: true, color: colors.text, fontFace: theme.font,
121
+ });
122
+ slide.addText(appData.description || 'Aplicación construida con los estándares G360.', {
123
+ x: S.marginX, y: 1.5, w: S.contentWidth, h: 0.85,
124
+ fontSize: theme.typo.sizes.body, color: colors.text,
125
+ valign: 'top', fontFace: theme.font,
126
+ });
127
+ card(slide, {
128
+ x: S.marginX, y: 2.55, w: (S.contentWidth - S.cardGap) / 2, h: 2.0,
129
+ title: 'Para el usuario',
130
+ body: features.length > 0
131
+ ? features.slice(0, 4).map((f) => f.display || f.name)
132
+ : ['Acceso a los módulos principales de la app'],
77
133
  }, theme);
78
- addFooter(slide, theme, 2, 12);
134
+ card(slide, {
135
+ x: S.marginX + (S.contentWidth - S.cardGap) / 2 + S.cardGap, y: 2.55,
136
+ w: (S.contentWidth - S.cardGap) / 2, h: 2.0,
137
+ title: 'Bajo el capó',
138
+ body: buildTechBullets(appData, features),
139
+ }, theme);
140
+ slide.addText(buildCapabilityLine(appData), {
141
+ x: S.marginX, y: 4.85, w: S.contentWidth, h: 1.6,
142
+ fontSize: theme.typo.sizes.body, color: colors.textMuted, valign: 'top', fontFace: theme.font,
143
+ });
79
144
 
80
- // ===== SLIDE 3: Instalacion =====
81
- slide = pptx.addSlide();
82
- slide.background = { color: colors.bg };
83
- addSectionHeader(slide, 'INSTALACION', theme);
84
- const installSteps = [
85
- { title: 'Ejecutar run.bat', desc: 'El launcher instala uv, Python 3.11, dependencias y crea acceso directo.' },
86
- { title: 'O usar comando directo', desc: 'uv sync && uv run python main.py' },
87
- { title: 'Version portable', desc: 'Descomprimir zip y ejecutar launch.vbs (sin dependencias).' },
88
- ];
89
- workflowSlide(slide, {
90
- title: 'Como iniciar la aplicacion',
91
- steps: installSteps,
145
+ // ===== 3. Módulos =====
146
+ slide = newSlide();
147
+ addSectionHeader(slide, 'MÓDULOS', theme, ++slideNum, totalSlides);
148
+ modulesTable(slide, {
149
+ rows: features.map((f) => ({
150
+ name: f.display || f.name,
151
+ route: f.file,
152
+ desc: featureDesc(f),
153
+ })),
92
154
  }, theme);
93
- addFooter(slide, theme, 3, 12);
155
+ const shots = features.map(screenshotFor).filter(Boolean);
156
+ if (shots[0]) {
157
+ phoneShot(slide, {
158
+ x: S.page.width - S.marginX - 3.95, y: S.contentTopY,
159
+ w: 1.85, h: 4.0, imagePath: shots[0],
160
+ label: `Captura de ${features[0].display || features[0].name}`,
161
+ }, theme);
162
+ }
163
+ if (shots[1]) {
164
+ phoneShot(slide, {
165
+ x: S.page.width - S.marginX - 1.9, y: S.contentTopY,
166
+ w: 1.85, h: 4.0, imagePath: shots[1],
167
+ label: `Captura de ${features[1]?.display || 'módulo'}`,
168
+ }, theme);
169
+ }
170
+ slide.addText('Cada módulo se detalla en las siguientes páginas.', {
171
+ x: S.marginX, y: S.contentTopY + features.length * 1.12 + 0.1, w: S.contentWidth - 4.35, h: 0.5,
172
+ fontSize: theme.typo.sizes.caption, color: colors.textMuted, fontFace: theme.font,
173
+ });
94
174
 
95
- // ===== SLIDES 4+: Features / Screenshots =====
96
- let slideNum = 4;
97
- const totalFeatures = Math.min(appData.features.length, 6); // max 6 features en manual
98
- const totalSlides = 3 + totalFeatures + 3; // intro + features + workflows + arch + summary
99
-
100
- // Slide 4: Dashboard / inicio
101
- slide = pptx.addSlide();
102
- slide.background = { color: colors.bg };
103
- addSectionHeader(slide, 'PRIMERA VISTA', theme);
104
- const dashScreen = appData.screenshots?.find(s => s.filename.toLowerCase().includes('dash'));
105
- screenshotSlide(slide, {
106
- title: 'Dashboard Principal',
107
- description: appData.hasAutoRefresh
108
- ? 'Vista principal con KPIs en tiempo real y auto-refresh cada 15 minutos.'
109
- : 'Vista principal de la aplicacion con los indicadores clave del negocio.',
110
- screenshotPath: dashScreen?.path,
175
+ // ===== 4. Flujo de trabajo =====
176
+ slide = newSlide();
177
+ addSectionHeader(slide, 'FLUJO DE TRABAJO', theme, ++slideNum, totalSlides);
178
+ workflowSlide(slide, {
179
+ textWidth: S.contentWidth - 0.65 - 4.9,
180
+ steps: buildWorkflowSteps(appData),
111
181
  }, theme);
112
- addFooter(slide, theme, slideNum, totalSlides);
113
- slideNum++;
182
+ if (shots[2]) {
183
+ phoneShot(slide, {
184
+ x: S.page.width - S.marginX - 4.75, y: S.contentTopY + 0.15,
185
+ w: 2.25, h: 4.9, imagePath: shots[2],
186
+ label: 'Pantalla de inicio / login',
187
+ }, theme);
188
+ }
189
+ if (shots[3]) {
190
+ phoneShot(slide, {
191
+ x: S.page.width - S.marginX - 2.3, y: S.contentTopY + 0.15,
192
+ w: 2.25, h: 4.9, imagePath: shots[3],
193
+ label: 'Vista principal en uso',
194
+ }, theme);
195
+ }
114
196
 
115
- // Features individual slides
116
- for (const feature of appData.features.slice(0, totalFeatures)) {
117
- slide = pptx.addSlide();
118
- slide.background = { color: colors.bg };
119
- addSectionHeader(slide, 'FUNCIONALIDAD', theme);
120
-
121
- const shot = appData.screenshots?.find(s =>
122
- feature.name.toLowerCase().includes(s.filename.toLowerCase().slice(0, 6))
123
- );
124
-
125
- const classification = classifyFeatureForSlide(feature);
126
-
127
- if (classification === 'kpi' && feature.name.includes('Kpi')) {
197
+ // ===== 5-N. Features =====
198
+ for (const feature of features) {
199
+ slide = newSlide();
200
+ addSectionHeader(slide, 'FUNCIONALIDAD', theme, ++slideNum, totalSlides);
201
+ const kind = classify(feature);
202
+ const shotPath = screenshotFor(feature);
203
+ const textW = shotPath ? S.contentWidth - 5.1 : S.contentWidth;
204
+
205
+ if (kind === 'kpi') {
128
206
  kpiSlide(slide, {
129
207
  title: feature.display || feature.name,
130
- kpis: generateKpiPlaceholders(feature),
131
- }, theme);
132
- } else if (classification === 'workflow') {
133
- workflowSlide(slide, {
134
- title: feature.display || feature.name,
135
- steps: [`Acceder desde el menu principal`, 'Seleccionar opciones disponibles', 'Confirmar y generar resultado'],
208
+ kpis: [
209
+ { label: 'Registros', value: '—', color: colors.accent },
210
+ { label: 'Activos', value: '', color: colors.success },
211
+ { label: 'Alertas', value: '—', color: colors.warning },
212
+ { label: 'Críticos', value: '—', color: colors.danger },
213
+ ],
136
214
  }, theme);
137
215
  } else {
138
216
  featureSlide(slide, {
139
217
  title: feature.display || feature.name,
140
- description: `Module ubicado en src/ui/${feature.file}`,
141
- bullets: [
142
- 'Clase exportada desde src/ui/',
143
- 'Integracion con Dashboard principal',
144
- 'Soporta dual theme automaticamente',
145
- ],
218
+ description: featureDesc(feature),
219
+ bullets: kind === 'workflow'
220
+ ? ['Se abre desde el módulo principal', 'Interacción guiada paso a paso', 'Confirmación con resumen del resultado']
221
+ : feature.kind === 'route'
222
+ ? [
223
+ `Ruta accesible desde la navegación principal (${feature.file})`,
224
+ 'Estado sincronizado con los datos del negocio',
225
+ 'Diseño responsive y táctil',
226
+ ]
227
+ : feature.kind === 'component'
228
+ ? [
229
+ `Componente web en ${feature.file}`,
230
+ 'Integrado al flujo principal de la app',
231
+ 'Reutilizable y con estados de carga/error',
232
+ ]
233
+ : [
234
+ `Clase ${feature.name} exportada desde src/ui/${feature.file}`,
235
+ 'Integrada al Dashboard principal',
236
+ 'Respeta el tema claro/oscuro del sistema',
237
+ ],
146
238
  }, theme);
239
+ // limitar ancho del texto si hay captura
240
+ if (shotPath) {
241
+ slide.addText(`${feature.display || feature.name}`, {
242
+ x: S.marginX, y: 1.05, w: textW, h: 0.6,
243
+ fontSize: 15, bold: true, color: colors.text, fontFace: theme.font,
244
+ });
245
+ }
147
246
  }
148
-
149
- if (!shot) {
150
- // Agregar placeholder si no hay screenshot
151
- slide.addShape('rect', [1, 3.5, 8, 6], {
152
- fill: { color: colors.surface },
153
- line: { color: colors.border, width: 1, dashType: 'dash' },
154
- });
155
- slide.addText('📷 Screenshot: ' + (feature.display || feature.name), [1, 6, 8, 0.5], {
156
- fontSize: 12, color: colors.textLight, fontFace: 'Inter', align: 'center',
157
- });
247
+
248
+ if (shotPath) {
249
+ phoneShot(slide, {
250
+ x: S.page.width - S.marginX - 2.6, y: 1.25,
251
+ w: 2.55, h: 4.9, imagePath: shotPath,
252
+ label: `Captura de ${feature.display || feature.name}`,
253
+ }, theme);
158
254
  } else {
159
- screenshotSlide(slide, {
160
- title: feature.display || feature.name,
161
- description: `Módulo: src/ui/${feature.file}`,
162
- screenshotPath: shot.path,
255
+ phoneShot(slide, {
256
+ x: S.page.width - S.marginX - 2.6, y: 1.25,
257
+ w: 2.55, h: 4.9, imagePath: null,
258
+ label: `Captura de ${feature.display || feature.name} en uso`,
163
259
  }, theme);
164
- slide.clearShapes?.(); // remove placeholder
165
260
  }
166
-
167
261
  addFooter(slide, theme, slideNum, totalSlides);
168
- slideNum++;
169
262
  }
170
263
 
171
- // ===== Workflows (modals) =====
172
- if (appData.hasModals.length > 0) {
173
- slide = pptx.addSlide();
174
- slide.background = { color: colors.bg };
175
- addSectionHeader(slide, 'FLUJOS DE TRABAJO', theme);
176
-
177
- const steps = appData.hasModals.slice(0, 4).map((modal, i) => ({
178
- title: `Modal: ${modal}`,
179
- desc: 'Paso 1 → Paso 2 → Paso 3',
180
- }));
181
-
264
+ // ===== N+1. Flujos (modales) =====
265
+ if (hasModals) {
266
+ slide = newSlide();
267
+ addSectionHeader(slide, 'FLUJOS DE TRABAJO', theme, ++slideNum, totalSlides);
182
268
  workflowSlide(slide, {
183
- title: 'Modales y flujos de usuario',
184
- steps,
269
+ textWidth: S.contentWidth - 0.65,
270
+ steps: appData.hasModals.slice(0, 6).map((m) => ({
271
+ title: m.replace(/([a-z])([A-Z])/g, '$1 $2'),
272
+ desc: 'Flujo de usuario con confirmación y resultado trazable.',
273
+ })),
185
274
  }, theme);
186
275
  addFooter(slide, theme, slideNum, totalSlides);
187
- slideNum++;
188
276
  }
189
277
 
190
- // ===== Architecture =====
191
- slide = pptx.addSlide();
192
- slide.background = { color: colors.bg };
193
- addSectionHeader(slide, 'ARQUITECTURA', theme);
278
+ // ===== N+2. Arquitectura =====
279
+ slide = newSlide();
280
+ addSectionHeader(slide, 'ARQUITECTURA', theme, ++slideNum, totalSlides);
194
281
  architectureSlide(slide, {
195
- title: 'Estructura de capas',
196
282
  layers: [
197
- { name: 'UI', desc: 'Dashboard, Cards, Modals, Overlays', color: colors.accent },
198
- { name: 'Core', desc: 'Processor, Downloader, Models', color: colors.info },
199
- { name: 'Config', desc: 'Theme, Constants, Skill', color: colors.violet },
200
- { name: 'Data', desc: 'API S1, Catalogo JSON, Cache', color: colors.success },
283
+ { name: 'UI', desc: 'Dashboard, Cards, Modals, Overlays (src/ui/)', color: colors.accent },
284
+ { name: 'Core', desc: 'Lógica de negocio y procesamiento (src/core/)', color: colors.info },
285
+ { name: 'Config', desc: 'Tema, constantes y metadata (skill.json)', color: colors.violet },
286
+ { name: 'Datos', desc: 'API ERP, catálogo y cache local', color: colors.success },
201
287
  ],
202
288
  }, theme);
203
289
  addFooter(slide, theme, slideNum, totalSlides);
204
- slideNum++;
205
290
 
206
- // ===== Checklist / Buenas prácticas =====
207
- slide = pptx.addSlide();
208
- slide.background = { color: colors.bg };
209
- addSectionHeader(slide, 'BUENAS PRÁCTICAS', theme);
291
+ // ===== N+3. Buenas prácticas =====
292
+ slide = newSlide();
293
+ addSectionHeader(slide, 'BUENAS PRÁCTICAS', theme, ++slideNum, totalSlides);
210
294
  checklistSlide(slide, {
211
- title: 'Recomendaciones de uso',
212
295
  items: [
213
- { text: 'Mantener nombres de clases siguiendo PascalCase (KpiCard, Dashboard)', checked: true },
214
- { text: 'Usar theme.py para colores, nunca hardcodear #HEX', checked: true },
215
- { text: 'Registrar eventos con publish_g360_event() para comunicacion entre apps', checked: true },
216
- { text: 'Implementar shutdown() para limpieza de threads al cerrar', checked: true },
217
- { text: 'Documentar funciones con docstrings en español', checked: true },
218
- { text: 'Usar G360 Signature widget en el footer de la app', checked: true },
296
+ 'Mantener la sesión activa solo mientras se usa la app',
297
+ 'Actualizar datos antes de decidir (auto-refresh o botón de recarga)',
298
+ 'Usar la exportación a Excel para seguimiento y auditoría',
299
+ 'Reportar anomalías al administrador con el registro específico',
300
+ 'Compartir el dispositivo solo con sesión cerrada',
301
+ 'Verificar conectividad si los datos aparecen desactualizados',
219
302
  ],
220
303
  }, theme);
221
304
  addFooter(slide, theme, slideNum, totalSlides);
222
- slideNum++;
223
305
 
224
- // ===== Summary =====
225
- slide = pptx.addSlide();
226
- slide.background = { color: colors.bg };
227
- addSectionHeader(slide, 'RESUMEN', theme);
228
-
229
- const summaryItems = [
230
- `Nombre: ${appData.name || 'N/A'}`,
231
- `Version: ${appData.version || '1.0.0'}`,
232
- `Framework: ${appData.framework || 'Flet'}`,
233
- `Skills: ${appData.skill || 'N/A'}`,
234
- `Caracteristicas: ${appData.features.length} modulos UI`,
235
- appData.hasAutoRefresh ? 'Auto-refresh activado' : '',
236
- appData.hasSearch ? 'Buscador flotante implementado' : '',
237
- appData.hasExport ? 'Exportacion a Excel disponible' : '',
238
- appData.hasModals.length > 0 ? `${appData.hasModals.length} modales de trabajo` : '',
239
- ].filter(Boolean);
240
-
306
+ // ===== N+4. Límites (opcional, definidos por el proyecto) =====
307
+ if (hasLimits) {
308
+ slide = newSlide();
309
+ addSectionHeader(slide, 'LÍMITES CONOCIDOS', theme, ++slideNum, totalSlides);
310
+ limitsSlide(slide, { items: appData.limits }, theme);
311
+ addFooter(slide, theme, slideNum, totalSlides);
312
+ }
313
+
314
+ // ===== Resumen =====
315
+ slide = newSlide();
316
+ addSectionHeader(slide, 'RESUMEN', theme, ++slideNum, totalSlides);
241
317
  checklistSlide(slide, {
242
- title: 'Resumen de la aplicacion',
243
- items: summaryItems.map(t => ({ text: t, checked: true })),
318
+ startY: 1.5,
319
+ items: [
320
+ `Nombre: ${appName}`,
321
+ `Versión: ${appData.version || '1.0.0'}`,
322
+ `Framework: ${appData.framework || 'N/A'}`,
323
+ `Módulos UI: ${appData.features.length}`,
324
+ appData.hasAutoRefresh ? 'Auto-refresh de datos activado' : 'Actualización manual de datos',
325
+ appData.hasSearch ? 'Buscador global disponible' : null,
326
+ appData.hasExport ? 'Exportación a Excel disponible' : null,
327
+ hasModals ? `${appData.hasModals.length} flujos con modales de trabajo` : null,
328
+ ].filter(Boolean).map((t) => ({ text: t, checked: true })),
244
329
  }, theme);
245
- addFooter(slide, theme, slideNum, totalSlides);
330
+ slide.addText('Documentación completa: README.md · powered by G360', {
331
+ x: S.marginX, y: 6.6, w: S.contentWidth, h: 0.4,
332
+ fontSize: theme.typo.sizes.bodySmall, color: colors.textMuted,
333
+ align: 'center', fontFace: theme.font,
334
+ });
246
335
 
247
- // Save
248
- const outPath = options.outPath || path.join(process.cwd(), `${(appData.name || 'app').toLowerCase().replace(/\s+/g, '-')}-manual.pptx`);
336
+ // Save — por defecto dentro del repo de la app (cada PPTX es propio de su repo)
337
+ const slug = appName.toLowerCase().replace(/\s+/g, '-');
338
+ const defaultDir = options.targetDir || process.cwd();
339
+ const outPath = options.outPath
340
+ || path.join(defaultDir, mode === 'demo' ? `${slug}-demo.pptx` : `${slug}-manual.pptx`);
249
341
  await pptx.writeFile({ fileName: outPath });
250
342
  return outPath;
251
343
  }
252
344
 
253
- /**
254
- * Classifica feature para decidir que slide usar.
255
- */
256
- function classifyFeatureForSlide(feature) {
257
- if (feature.name.includes('Kpi')) return 'kpi';
258
- if (feature.name.includes('Modal') || feature.name.includes('Dialog')) return 'workflow';
259
- if (feature.name.includes('Dashboard')) return 'screenshot';
260
- return 'feature';
345
+ function buildTagline(appData) {
346
+ const bits = [];
347
+ if (appData.framework) bits.push(appData.framework);
348
+ if (appData.features.length) bits.push(`${appData.features.length} módulos UI`);
349
+ if (appData.hasModals?.length) bits.push(`${appData.hasModals.length} flujos de trabajo`);
350
+ if (appData.type === 'web' && appData.hasPwa) bits.push('PWA instalable');
351
+ return bits.join(' · ');
261
352
  }
262
353
 
263
- /**
264
- * Genera placeholders de KPIs basado en features detectadas.
265
- */
266
- function generateKpiPlaceholders(feature) {
354
+ function buildTechBullets(appData, features) {
355
+ const bullets = [];
356
+ if (appData.framework) bullets.push(`Framework: ${appData.framework}`);
357
+ bullets.push(`${appData.features.length} módulos UI detectados`);
358
+ if (appData.hasModals?.length) bullets.push(`${appData.hasModals.length} modales de trabajo en src/ui/modals/`);
359
+ if (appData.hasExport) bullets.push('Exportación a Excel integrada');
360
+ if (appData.hasAutoRefresh) bullets.push('Auto-refresh de datos (~15 min)');
361
+ if (appData.hasSupabase) bullets.push('Datos en tiempo real vía Supabase');
362
+ if (appData.hasPwa) bullets.push('PWA instalable con cache offline');
363
+ if (appData.hasCharts) bullets.push('Visualización de datos con gráficos');
364
+ if (appData.hasSearch) bullets.push('Búsqueda global con overlay flotante');
365
+ bullets.push('Metadata y eventos declarados en skill.json');
366
+ return bullets.slice(0, 6);
367
+ }
368
+
369
+ function buildCapabilityLine(appData) {
370
+ const caps = [];
371
+ if (appData.hasAutoRefresh) caps.push('datos en tiempo real');
372
+ if (appData.hasSearch) caps.push('búsqueda global');
373
+ if (appData.hasExport) caps.push('export a Excel');
374
+ if (appData.hasSource1 || appData.hasSupabase) caps.push('conexión a ERP/Supabase');
375
+ if (appData.hasPwa) caps.push('modo offline (PWA)');
376
+ if (appData.hasCharts) caps.push('gráficos interactivos');
377
+ return caps.length > 0
378
+ ? `Capacidades destacadas: ${caps.join(' · ')}.`
379
+ : 'Revisa los módulos en las siguientes páginas para conocer las capacidades de la app.';
380
+ }
381
+
382
+ function buildWorkflowSteps(appData) {
383
+ if (appData.templates?.length >= 4) {
384
+ return appData.templates.slice(0, 6).map((t, i) => ({
385
+ title: `${i + 1} · ${t.display}`,
386
+ desc: `Interacción "${t.name.replace(/^_on_/, '').replace(/_/g, ' ')}" registrada en src/app.py.`,
387
+ }));
388
+ }
267
389
  return [
268
- { label: 'Total', value: '—', color: '#64748B' },
269
- { label: 'Con Stock', value: '—', color: '#34D399' },
270
- { label: 'Sin Stock', value: '—', color: '#EF4444' },
271
- { label: 'Alertas', value: '—', color: '#F59E0B' },
390
+ { title: 'Ingresar', desc: 'Abre la app y autentícate si el proyecto lo requiere.' },
391
+ { title: 'Revisar el dashboard', desc: 'El resumen principal concentra KPIs y accesos a los módulos.' },
392
+ { title: 'Explorar módulos', desc: 'Cada módulo cubre una parte del flujo operativo del negocio.' },
393
+ { title: 'Operar', desc: 'Registra, edita o consulta según el módulo de trabajo.' },
394
+ { title: 'Exportar / compartir', desc: 'Genera reportes en Excel cuando el proyecto lo integre.' },
395
+ { title: 'Cerrar sesión', desc: 'Si compartes el dispositivo, cierra tu sesión al terminar.' },
272
396
  ];
273
397
  }
274
-
275
- import path from 'path';