g360-cli 1.15.8 → 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.
- package/README.md +84 -4
- package/package.json +2 -1
- package/src/assets/pptx/design-tokens.js +124 -0
- package/src/assets/pptx/layouts/base.js +441 -0
- package/src/assets/pptx/layouts/index.js +19 -0
- package/src/assets/pptx/scripts/analyze-app.js +358 -0
- package/src/assets/pptx/scripts/image-size.js +56 -0
- package/src/assets/pptx/templates/app-manual.js +397 -0
- package/src/assets/pptx/themes/base.js +8 -0
- package/src/assets/pptx/themes/cipsa.js +4 -0
- package/src/assets/pptx/themes/g360.js +4 -0
- package/src/assets/pptx/themes/index.js +87 -0
- package/src/cli.js +11 -0
- package/src/commands/pptx.js +91 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Manual de App G360 — formato 16:9 widescreen, estilo ventas-pulse.
|
|
3
|
+
*
|
|
4
|
+
* Estructura:
|
|
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
|
+
* N+2. Arquitectura
|
|
12
|
+
* N+3. Buenas prácticas
|
|
13
|
+
* N+4. Límites conocidos (si el proyecto define appData.limits)
|
|
14
|
+
* N+5. Resumen
|
|
15
|
+
*/
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { createTheme } from '../themes/index.js';
|
|
18
|
+
import {
|
|
19
|
+
coverSlide,
|
|
20
|
+
addSectionHeader,
|
|
21
|
+
addFooter,
|
|
22
|
+
card,
|
|
23
|
+
phoneShot,
|
|
24
|
+
featureSlide,
|
|
25
|
+
workflowSlide,
|
|
26
|
+
limitsSlide,
|
|
27
|
+
checklistSlide,
|
|
28
|
+
kpiSlide,
|
|
29
|
+
architectureSlide,
|
|
30
|
+
modulesTable,
|
|
31
|
+
} from '../layouts/base.js';
|
|
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
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Genera un manual PPTX completo para una app G360 (16:9).
|
|
63
|
+
*
|
|
64
|
+
* @param {object} appData — resultado de analyzeApp()
|
|
65
|
+
* @param {object} options — { mode, theme, outPath }
|
|
66
|
+
* @returns {Promise<string>} — ruta del archivo .pptx generado
|
|
67
|
+
*/
|
|
68
|
+
export async function generateManualPptx(appData, options = {}) {
|
|
69
|
+
const { mode = 'manual' } = options;
|
|
70
|
+
const themeName = options.theme || appData.brand || 'g360';
|
|
71
|
+
const theme = createTheme(themeName === 'cipsa' ? 'cipsa' : themeName);
|
|
72
|
+
const { pptx, colors } = theme;
|
|
73
|
+
const S = theme.spacing;
|
|
74
|
+
|
|
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();
|
|
106
|
+
coverSlide(slide, {
|
|
107
|
+
appName,
|
|
108
|
+
description: appData.description || 'Documentación de uso y funcionalidades',
|
|
109
|
+
tagline: buildTagline(appData),
|
|
110
|
+
version: appData.version,
|
|
111
|
+
brand: themeName,
|
|
112
|
+
}, theme);
|
|
113
|
+
addFooter(slide, theme, ++slideNum, totalSlides);
|
|
114
|
+
|
|
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'],
|
|
133
|
+
}, theme);
|
|
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
|
+
});
|
|
144
|
+
|
|
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
|
+
})),
|
|
154
|
+
}, theme);
|
|
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
|
+
});
|
|
174
|
+
|
|
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),
|
|
181
|
+
}, theme);
|
|
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
|
+
}
|
|
196
|
+
|
|
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') {
|
|
206
|
+
kpiSlide(slide, {
|
|
207
|
+
title: feature.display || feature.name,
|
|
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
|
+
],
|
|
214
|
+
}, theme);
|
|
215
|
+
} else {
|
|
216
|
+
featureSlide(slide, {
|
|
217
|
+
title: feature.display || feature.name,
|
|
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
|
+
],
|
|
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
|
+
}
|
|
246
|
+
}
|
|
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);
|
|
254
|
+
} else {
|
|
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`,
|
|
259
|
+
}, theme);
|
|
260
|
+
}
|
|
261
|
+
addFooter(slide, theme, slideNum, totalSlides);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ===== N+1. Flujos (modales) =====
|
|
265
|
+
if (hasModals) {
|
|
266
|
+
slide = newSlide();
|
|
267
|
+
addSectionHeader(slide, 'FLUJOS DE TRABAJO', theme, ++slideNum, totalSlides);
|
|
268
|
+
workflowSlide(slide, {
|
|
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
|
+
})),
|
|
274
|
+
}, theme);
|
|
275
|
+
addFooter(slide, theme, slideNum, totalSlides);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ===== N+2. Arquitectura =====
|
|
279
|
+
slide = newSlide();
|
|
280
|
+
addSectionHeader(slide, 'ARQUITECTURA', theme, ++slideNum, totalSlides);
|
|
281
|
+
architectureSlide(slide, {
|
|
282
|
+
layers: [
|
|
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 },
|
|
287
|
+
],
|
|
288
|
+
}, theme);
|
|
289
|
+
addFooter(slide, theme, slideNum, totalSlides);
|
|
290
|
+
|
|
291
|
+
// ===== N+3. Buenas prácticas =====
|
|
292
|
+
slide = newSlide();
|
|
293
|
+
addSectionHeader(slide, 'BUENAS PRÁCTICAS', theme, ++slideNum, totalSlides);
|
|
294
|
+
checklistSlide(slide, {
|
|
295
|
+
items: [
|
|
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',
|
|
302
|
+
],
|
|
303
|
+
}, theme);
|
|
304
|
+
addFooter(slide, theme, slideNum, totalSlides);
|
|
305
|
+
|
|
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);
|
|
317
|
+
checklistSlide(slide, {
|
|
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 })),
|
|
329
|
+
}, theme);
|
|
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
|
+
});
|
|
335
|
+
|
|
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`);
|
|
341
|
+
await pptx.writeFile({ fileName: outPath });
|
|
342
|
+
return outPath;
|
|
343
|
+
}
|
|
344
|
+
|
|
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(' · ');
|
|
352
|
+
}
|
|
353
|
+
|
|
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
|
+
}
|
|
389
|
+
return [
|
|
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.' },
|
|
396
|
+
];
|
|
397
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fabrica de temas G360 para pptxgenjs.
|
|
3
|
+
* Consumen design-tokens.js (fuente unica de verdad de paletas/estilos).
|
|
4
|
+
*/
|
|
5
|
+
import PptxGenJS from 'pptxgenjs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import {
|
|
10
|
+
PALETTES,
|
|
11
|
+
TYPO,
|
|
12
|
+
SPACING,
|
|
13
|
+
DEFAULT_LAYOUT,
|
|
14
|
+
resolvePalette,
|
|
15
|
+
} from '../design-tokens.js';
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
/** Layout 16:9 registrado por las fabricas de tema */
|
|
20
|
+
export function registerLayouts(pptx) {
|
|
21
|
+
pptx.defineLayout({
|
|
22
|
+
name: DEFAULT_LAYOUT,
|
|
23
|
+
width: SPACING.page.width,
|
|
24
|
+
height: SPACING.page.height,
|
|
25
|
+
});
|
|
26
|
+
pptx.layout = DEFAULT_LAYOUT;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Crea un tema completo para pptxgenjs.
|
|
31
|
+
* @param {string} name — 'g360' | 'cipsa' | 'corporate'
|
|
32
|
+
*/
|
|
33
|
+
export function createTheme(name = 'g360') {
|
|
34
|
+
const colors = resolvePalette(name);
|
|
35
|
+
const pptx = new PptxGenJS();
|
|
36
|
+
registerLayouts(pptx);
|
|
37
|
+
|
|
38
|
+
pptx.author = 'g360-cli';
|
|
39
|
+
pptx.company = name === 'cipsa' ? 'CIPSA' : 'G360';
|
|
40
|
+
pptx.subject = 'Manual de aplicacion generado por g360-cli';
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
pptx,
|
|
44
|
+
colors,
|
|
45
|
+
typo: TYPO,
|
|
46
|
+
spacing: SPACING,
|
|
47
|
+
layout: DEFAULT_LAYOUT,
|
|
48
|
+
font: TYPO.font,
|
|
49
|
+
fallbackFont: TYPO.fallback,
|
|
50
|
+
name,
|
|
51
|
+
/**
|
|
52
|
+
* Logo de marca (PNG) si existe; null si no.
|
|
53
|
+
*/
|
|
54
|
+
logo() {
|
|
55
|
+
const candidates = name === 'cipsa'
|
|
56
|
+
? [
|
|
57
|
+
path.join(process.cwd(), 'assets', 'images', 'Logo_cipsa_solid.png'),
|
|
58
|
+
path.join(__dirname, '..', 'brand', 'cipsa', 'logotypes', 'Logo_cipsa_solid.png'),
|
|
59
|
+
]
|
|
60
|
+
: [
|
|
61
|
+
path.join(process.cwd(), 'assets', 'images', 'logo-g360-dark.png'),
|
|
62
|
+
path.join(__dirname, '..', 'brand', 'g360', 'logotypes', 'logo-g360-dark.png'),
|
|
63
|
+
];
|
|
64
|
+
for (const p of candidates) {
|
|
65
|
+
if (fs.existsSync(p)) return { path: p, w: 1.5, h: 0.5 };
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Compat: tema G360 (esmeralda) */
|
|
73
|
+
export function createG360Theme() {
|
|
74
|
+
return createTheme('g360');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Compat: tema CIPSA (verde corporativo) */
|
|
78
|
+
export function createCipsaTheme() {
|
|
79
|
+
return createTheme('cipsa');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Compat: tema Corporate (azul marino) */
|
|
83
|
+
export function createCorporateTheme() {
|
|
84
|
+
return createTheme('corporate');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { PALETTES };
|
package/src/cli.js
CHANGED
|
@@ -22,6 +22,7 @@ import { ingest } from './commands/ingest.js';
|
|
|
22
22
|
import { addon } from './commands/addon.js';
|
|
23
23
|
import { docs } from './commands/docs.js';
|
|
24
24
|
import { lint } from './commands/lint.js';
|
|
25
|
+
import { pptx } from './commands/pptx.js';
|
|
25
26
|
|
|
26
27
|
// Comando config (no requiere archivo separado)
|
|
27
28
|
function configAction(options) {
|
|
@@ -212,4 +213,14 @@ program
|
|
|
212
213
|
.option('--project <path>', 'Project path', '.')
|
|
213
214
|
.action(lint);
|
|
214
215
|
|
|
216
|
+
program
|
|
217
|
+
.command('pptx')
|
|
218
|
+
.argument('[path]', 'Project path to generate presentation from', '.')
|
|
219
|
+
.option('-m, --mode <type>', 'Mode: manual, demo, onboarding', 'manual')
|
|
220
|
+
.option('-t, --theme <name>', 'Brand theme: g360, cipsa (auto-detected from skill.json)', null)
|
|
221
|
+
.option('-o, --out <file>', 'Output file path (default: {app-name}-manual.pptx)', null)
|
|
222
|
+
.option('--dry-run', 'Preview outline without generating')
|
|
223
|
+
.option('--screenshots <dir>', 'Custom screenshots directory', 'assets/screenshots')
|
|
224
|
+
.action(pptx);
|
|
225
|
+
|
|
215
226
|
program.parse();
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comando: g360 pptx [path] [opciones]
|
|
3
|
+
* Genera presentacion/manual PPTX para una app G360.
|
|
4
|
+
*/
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
|
|
11
|
+
import { analyzeApp } from '../assets/pptx/scripts/analyze-app.js';
|
|
12
|
+
import { generateManualPptx } from '../assets/pptx/templates/app-manual.js';
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
export async function pptx(targetPath, options) {
|
|
17
|
+
const {
|
|
18
|
+
mode = 'manual',
|
|
19
|
+
theme: themeName = null,
|
|
20
|
+
out,
|
|
21
|
+
dryRun = false,
|
|
22
|
+
screenshots,
|
|
23
|
+
} = options;
|
|
24
|
+
|
|
25
|
+
const targetDir = path.resolve(process.cwd(), targetPath || '.');
|
|
26
|
+
|
|
27
|
+
if (!await fs.pathExists(targetDir)) {
|
|
28
|
+
console.error(chalk.red(`❌ Directorio no encontrado: ${targetDir}`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Detectar branding desde skill.json
|
|
33
|
+
const skillPath = path.join(targetDir, 'skill.json');
|
|
34
|
+
let detectedBrand = 'g360';
|
|
35
|
+
if (await fs.pathExists(skillPath)) {
|
|
36
|
+
try {
|
|
37
|
+
const skill = await fs.readJson(skillPath);
|
|
38
|
+
detectedBrand = skill.brand || 'g360';
|
|
39
|
+
if (detectedBrand === 'cipsa' && !themeName) themeName = 'cipsa';
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(chalk.bold.cyan('\n📊 G360 App → PPTX Generator\n'));
|
|
44
|
+
console.log(chalk.gray(` Proyecto: ${targetDir}`));
|
|
45
|
+
console.log(chalk.gray(` Modo: ${mode}`));
|
|
46
|
+
console.log(chalk.gray(` Theme: ${themeName || detectedBrand}`));
|
|
47
|
+
if (out) console.log(chalk.gray(` Salida: ${out}`));
|
|
48
|
+
console.log('');
|
|
49
|
+
|
|
50
|
+
if (dryRun) {
|
|
51
|
+
console.log(chalk.yellow('📋 DRY RUN — Outline generado:\n'));
|
|
52
|
+
console.log(chalk.gray(' [1] Portada'));
|
|
53
|
+
console.log(chalk.gray(' [2] Introducción (¿Qué es?)'));
|
|
54
|
+
console.log(chalk.gray(' [3] Módulos (tabla + capturas)'));
|
|
55
|
+
console.log(chalk.gray(' [4] Flujo de trabajo'));
|
|
56
|
+
console.log(chalk.gray(' [5-N] Funcionalidades (una por módulo UI detectado)'));
|
|
57
|
+
console.log(chalk.gray(' [N+1] Flujos de trabajo (modales)'));
|
|
58
|
+
console.log(chalk.gray(' [N+2] Arquitectura'));
|
|
59
|
+
console.log(chalk.gray(' [N+3] Buenas prácticas'));
|
|
60
|
+
console.log(chalk.gray(' [N+4] Límites conocidos (si aplica)'));
|
|
61
|
+
console.log(chalk.gray(' [N+5] Resumen'));
|
|
62
|
+
console.log(chalk.gray(`\nFormato: 16:9 widescreen · Theme: ${themeName || detectedBrand}`));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const spinner = ora('Analizando aplicación...').start();
|
|
67
|
+
try {
|
|
68
|
+
const appData = await analyzeApp(targetDir);
|
|
69
|
+
spinner.text = `Generando ${mode} (${appData.features.length} módulos detectados)...`;
|
|
70
|
+
|
|
71
|
+
const pptxPath = await generateManualPptx(appData, {
|
|
72
|
+
mode,
|
|
73
|
+
theme: themeName || detectedBrand,
|
|
74
|
+
targetDir: targetDir,
|
|
75
|
+
outPath: out,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
spinner.succeed(chalk.green(`✅ PPTX generado: ${pptxPath}`));
|
|
79
|
+
console.log(chalk.gray(` Tamaño: ${Math.round((await fs.stat(pptxPath)).size / 1024)} KB`));
|
|
80
|
+
console.log(chalk.gray(` Formato: 16:9 widescreen (modo ${mode})`));
|
|
81
|
+
console.log('');
|
|
82
|
+
console.log(chalk.cyan(' Siguiente paso:'));
|
|
83
|
+
console.log(chalk.gray(' Revisar el archivo y reemplazar placeholders de screenshots'));
|
|
84
|
+
console.log(chalk.gray(` con imágenes reales de la app en ${path.join(targetDir, 'assets', 'screenshots')}/`));
|
|
85
|
+
console.log('');
|
|
86
|
+
} catch (err) {
|
|
87
|
+
spinner.fail(chalk.red(`❌ Error: ${err.message}`));
|
|
88
|
+
console.error(err.stack);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|