feat(sprint-1-5): cierre Etapa 2 - formatPayback + fixes de marca
[
This commit is contained in:
@@ -286,3 +286,11 @@ npx playwright test # tests E2E (Playwright headless)
|
||||
- NO usar morado CONCILIA (#572FA2, #CB1889)
|
||||
- Footer obligatorio: "Powered by InQuality"
|
||||
- No promover a Marcos como creador
|
||||
|
||||
## Verificación final antes de present_files
|
||||
|
||||
Antes de presentar cualquier PDF al usuario, verificá que:
|
||||
1. El header/footer dice "InQ ROI" (no "Process Cost Platform" ni otro)
|
||||
2. El footer incluye "Powered by InQuality"
|
||||
3. No hay caracteres rotos (símbolos sin renderizar)
|
||||
4. La marca "InQ" se escribe siempre I-n-Q
|
||||
2
TODO.md
2
TODO.md
@@ -35,6 +35,8 @@
|
||||
|
||||
- [ ] **Columna "Eficiencia de automatización" en ComparisonTable**: métrica = `(ahorro$ × probabilidad) / inversión-prorrateada`. Permite rankear actividades por "qué tan valiosa es automatizar cada una" en lugar de solo "cuánto cuestan". Útil en conversaciones de pre-venta donde el cliente quiere priorizar qué automatizar primero.
|
||||
|
||||
- [ ] **ComparisonTable — actividades no automatizables muestran "USD 0,00" y "0,0%"**: semánticamente incorrecto. El ahorro de una actividad no automatizable no es "cero", es "no aplica". Cambiar por "—" (em dash) en columnas Ahorro y % Ahorro para filas donde `isAutomatable === false`. Implementar en Etapa 3 al rediseñar ComparisonTable con paleta InQ. (detectado en Sprint 1.5 Etapa 2)
|
||||
|
||||
## Baja prioridad / Fase 2
|
||||
|
||||
- [ ] **Login y multi-tenant**: persistencia en la nube, colaboración
|
||||
|
||||
871
sprints/sprint-1-5/BRIEF.md
Normal file
871
sprints/sprint-1-5/BRIEF.md
Normal file
@@ -0,0 +1,871 @@
|
||||
# Sprint 1.5 — Brief de Rediseño Visual
|
||||
|
||||
> **Propósito:** rediseñar la identidad visual del producto (PDFs + Web App) con manual de marca InQ ROI antes del cierre de Sprint 1. No agrega nuevas features funcionales; refina presentación, identidad y polish.
|
||||
|
||||
> **Justificación:** los PDFs y la web app actuales son funcionales pero genéricos. El producto va a ser usado en pre-venta, talleres y charlas de InQuality, requiere identidad sólida desde el inicio. La iteración visual es 5–10× más barata ahora que después.
|
||||
|
||||
---
|
||||
|
||||
## 0. Contexto crítico
|
||||
|
||||
Este producto es **una herramienta de InQuality**, no un producto independiente. Se construye con identidad propia (color naranja InQ) pero parte de la familia visual de productos InQuality (junto con CONCILIA, que usa morado). Lleva la marca "Powered by InQuality" en footers.
|
||||
|
||||
**Regla NO NEGOCIABLE de marca:** la versión corta de InQuality SIEMPRE se escribe **InQ** — I mayúscula, n minúscula, Q mayúscula. Cualquier desviación (InQ, INQ, Inq, inQ) es bug.
|
||||
|
||||
---
|
||||
|
||||
## 1. Manual de marca InQ ROI
|
||||
|
||||
### 1.1 Paleta cromática
|
||||
|
||||
#### Color protagonista — Naranja InQ
|
||||
| Token CSS | Hex | Uso |
|
||||
|---|---|---|
|
||||
| `--inq-orange` | `#F59845` | Principal. Logo, KPIs hero, headers, CTAs primarios |
|
||||
| `--inq-orange-dark` | `#B45309` | Texto destacado sobre fondos claros, hover de CTAs |
|
||||
| `--inq-orange-light` | `#FEF3E2` | Fondos de KPI cards, fondos sutiles |
|
||||
| `--inq-orange-lighter` | `#FFF8ED` | Fondos de banner narrativo, hover states muy sutiles |
|
||||
|
||||
#### Gradiente hero (sólo para KPIs hero y banners principales)
|
||||
```css
|
||||
background: linear-gradient(135deg, #F59845 0%, #ED3E8F 100%);
|
||||
```
|
||||
|
||||
Donde `#ED3E8F` es el magenta InQ. Este gradiente representa la familia visual InQuality y se usa SOLO en:
|
||||
- KPI hero "Ahorro proyectado a 3 años" (PDF y web)
|
||||
- Carátulas o banners principales (si aparecen)
|
||||
- NUNCA en bordes, separadores, fondos secundarios
|
||||
|
||||
#### Colores semánticos
|
||||
| Token CSS | Hex | Uso |
|
||||
|---|---|---|
|
||||
| `--color-success` | `#16A34A` | Ahorros positivos, "óptimo" |
|
||||
| `--color-success-light` | `#F0FDF4` | Fondos de mensajes positivos |
|
||||
| `--color-warning` | `#F59E0B` | Advertencias, sección transparencia |
|
||||
| `--color-warning-bg` | `#FFFBEB` | Fondos de advertencia |
|
||||
| `--color-danger` | `#EF4444` | Costos altos, errores |
|
||||
| `--color-danger-light` | `#FEE2E2` | Fondos de heatmap (alto costo) |
|
||||
|
||||
#### Heatmap (gradient de costo) — DECISIÓN: mantener colores saturados
|
||||
```css
|
||||
/* Bajo → Medio → Alto */
|
||||
heatmap-low: #10B981; /* verde saturado */
|
||||
heatmap-mid: #F59E0B; /* ámbar saturado */
|
||||
heatmap-high: #EF4444; /* rojo saturado */
|
||||
```
|
||||
|
||||
Razón: discriminan mejor visualmente en pantallas grandes (talleres, charlas) y mantienen calibración de los E2E de análisis de píxeles. Revisar en Sprint 3-4 con feedback de uso real.
|
||||
|
||||
#### Neutrales (tipografía y bordes)
|
||||
| Token CSS | Hex | Uso |
|
||||
|---|---|---|
|
||||
| `--text-primary` | `#0F172A` | Títulos, números importantes |
|
||||
| `--text-secondary` | `#475569` | Texto del cuerpo |
|
||||
| `--text-tertiary` | `#64748B` | Labels, metadatos |
|
||||
| `--text-disabled` | `#94A3B8` | Placeholders, texto muy secundario |
|
||||
| `--border-primary` | `#E2E8F0` | Bordes generales |
|
||||
| `--border-light` | `#F1F5F9` | Separadores sutiles |
|
||||
| `--bg-app` | `#F8FAFC` | Fondo de la app |
|
||||
| `--bg-card` | `#FAFBFC` | Fondo de tarjetas secundarias |
|
||||
|
||||
### 1.2 Tipografía
|
||||
|
||||
#### Familia
|
||||
- **Primaria** (web app y PDFs): system-ui stack, con fallback a Roboto embebido en PDFs.
|
||||
```css
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
```
|
||||
- **Mono** (números en tablas, datos financieros): `'JetBrains Mono', 'SF Mono', Consolas, monospace`.
|
||||
|
||||
#### Jerarquía de tamaños (PDF)
|
||||
| Elemento | Tamaño | Peso | Tracking |
|
||||
|---|---|---|---|
|
||||
| Logo "InQ ROI" en header | 22px | 700 | -0.8px |
|
||||
| Título sección (page title) | 16px | 500 | normal |
|
||||
| KPI Hero (cifra grande) | 30px | 700 | -1px |
|
||||
| KPI secundario (cifra) | 16px | 700 | normal |
|
||||
| Body text | 11px | 400 | normal |
|
||||
| Labels en cards | 8px | 500 | 1px (uppercase) |
|
||||
| Tabla — header | 10px | 500 | normal |
|
||||
| Tabla — body | 10px | 400 | normal |
|
||||
| Footer | 9px | 400 | normal |
|
||||
|
||||
#### Jerarquía de tamaños (Web App)
|
||||
| Elemento | Tamaño |
|
||||
|---|---|
|
||||
| Logo header | 18px / 700 |
|
||||
| Page title | 16px / 500 |
|
||||
| KPI Hero | 28px / 700 |
|
||||
| KPI secundario | 16px / 700 |
|
||||
| Nav / Tabs | 12px / 400-500 |
|
||||
| Body | 12-13px / 400 |
|
||||
| Labels en panels | 10-11px / 400-500 |
|
||||
|
||||
### 1.3 Reglas de uso del logo "InQ ROI"
|
||||
|
||||
- **Tipografía:** sans-serif bold con tracking apretado (-0.5px a -0.8px)
|
||||
- **Color por defecto:** `#F59845`
|
||||
- **Color sobre gradiente o fondos oscuros:** blanco
|
||||
- **Posición en PDFs:** esquina superior derecha del header de cada página
|
||||
- **Posición en Web App:** esquina superior izquierda del topbar
|
||||
- **Mínimo aire alrededor:** 16px en todas direcciones
|
||||
- **Aclaración "Powered by InQuality":** SIEMPRE en footer, no en header
|
||||
|
||||
### 1.4 Iconografía
|
||||
|
||||
- **Sistema:** Lucide React (ya instalado)
|
||||
- **Tamaños estándar:** 12px, 14px, 16px, 20px, 24px
|
||||
- **Color por defecto:** hereda del contexto (`currentColor`)
|
||||
- **Ícono "Automatizable" (⚡):** símbolo emoji ⚡ por ahora. Cambiar a `<Bot />` de Lucide en Sprint 2-3 (anotar en TODO.md)
|
||||
- **Badge automatizable:** círculo `#F59845` con ⚡ blanco centrado, tamaño 14-22px según contexto
|
||||
|
||||
### 1.5 Accesibilidad — Contraste
|
||||
|
||||
El naranja InQ `#F59845` sobre blanco tiene contraste WCAG ~2.42:1.
|
||||
- **AA normal text (4.5:1):** FAIL
|
||||
- **AA large text (3.0:1):** FAIL (apenas por debajo)
|
||||
|
||||
Decisión: aplicar `font-weight: 700` en CTAs primarios para acercarse al cumplimiento AA large, asumiendo el costo en accesibilidad estricta como decisión consciente. Documentado en audit.
|
||||
|
||||
---
|
||||
|
||||
## 2. Sistema de diseño — Componentes clave
|
||||
|
||||
### 2.1 KPI Card (cifra grande)
|
||||
|
||||
```
|
||||
Fondo: #FEF3E2 (inq-orange-light)
|
||||
Padding: 10px 12px
|
||||
Border-radius: 6px
|
||||
Label: 8px / 500 / letter-spacing 1px / color #92400E / uppercase
|
||||
Cifra: 16px / 700 / color #0F172A
|
||||
Delta: 10px / 500 / color #B45309 (verde-orange-dark)
|
||||
```
|
||||
|
||||
### 2.2 KPI Hero (gradiente)
|
||||
|
||||
```
|
||||
Fondo: linear-gradient(135deg, #F59845 0%, #ED3E8F 100%)
|
||||
Padding: 16-18px
|
||||
Border-radius: 6-8px
|
||||
Label: 9-10px / opacity 0.9 / letter-spacing 1.2px / white
|
||||
Cifra: 28-30px / 700 / letter-spacing -1px / white
|
||||
Subtítulo: 10-11px / opacity 0.85 / white
|
||||
```
|
||||
|
||||
### 2.3 Banner narrativo (resumen ejecutivo)
|
||||
|
||||
```
|
||||
Fondo: #FFF8ED (inq-orange-lighter)
|
||||
Border-left: 3px solid #F59845
|
||||
Padding: 10-12px 14px
|
||||
Border-radius: 0 4px 4px 0
|
||||
Texto: 11px / 400 / line-height 1.55 / color #475569
|
||||
```
|
||||
|
||||
### 2.4 Sección Transparencia Metodológica
|
||||
|
||||
```
|
||||
Fondo: #FFFBEB (warning-bg)
|
||||
Border: 0.5px solid #F59E0B
|
||||
Padding: 12px 14px
|
||||
Border-radius: 6px
|
||||
Título: 11px / 500 / color #92400E (con ícono ⚠)
|
||||
Texto: 10px / 400 / line-height 1.6 / color #78350F
|
||||
```
|
||||
|
||||
### 2.5 Tabla comparativa
|
||||
|
||||
```
|
||||
Header row:
|
||||
Fondo: #FFF8ED
|
||||
Border-bottom: 1px solid #F59845
|
||||
Padding: 8px 6px
|
||||
Texto: 10px / 500 / color #92400E
|
||||
|
||||
Body row (default):
|
||||
Fondo: white
|
||||
Border-bottom: 0.5px solid #E2E8F0
|
||||
Padding: 8px 6px
|
||||
Texto: 10px / 400 / color #0F172A
|
||||
Números: font-family mono
|
||||
|
||||
Body row (no automatizable):
|
||||
Fondo: #F8FAFC
|
||||
Texto: color #64748B (más tenue)
|
||||
|
||||
Total row:
|
||||
Fondo: #FFF8ED
|
||||
Border-top: 1.5px solid #F59845
|
||||
Texto: 10px / 500 / color #0F172A
|
||||
```
|
||||
|
||||
### 2.6 Header de página PDF
|
||||
|
||||
```
|
||||
Layout: flex justify-between
|
||||
Border-bottom: 3px solid #F59845 (página 1) / 2px (otras páginas)
|
||||
Padding-bottom: 10-12px
|
||||
Margin-bottom: 14-16px
|
||||
|
||||
Izquierda:
|
||||
Logo "InQ ROI": 18-22px / 700 / #F59845
|
||||
Tagline (página 1): 9px / letter-spacing 1.5px / #94A3B8
|
||||
|
||||
Derecha:
|
||||
Label "UN PRODUCTO DE": 9px / letter-spacing 1px / #94A3B8
|
||||
"InQ": 16px / 700 / #475569
|
||||
"InQuality": 9px / #64748B
|
||||
```
|
||||
|
||||
### 2.7 Footer de página PDF
|
||||
|
||||
```
|
||||
Position: absolute bottom 14-16px
|
||||
Padding-top: 6-8px
|
||||
Border-top: 0.5px solid #E2E8F0
|
||||
Layout: flex justify-between
|
||||
Font: 9px / 400 / #94A3B8
|
||||
|
||||
Izquierda:
|
||||
"InQ ROI" en bold #F59845
|
||||
" · Powered by InQuality · "
|
||||
Nombre cliente · fecha
|
||||
|
||||
Derecha:
|
||||
"Página X de Y"
|
||||
```
|
||||
|
||||
### 2.8 Badge "Automatizable" (⚡)
|
||||
|
||||
#### En canvas BPMN (overlay)
|
||||
```
|
||||
Tamaño: 14px diameter
|
||||
Fondo: #F59845
|
||||
Border: 1.5px solid white
|
||||
Posición: top: -8, left: nodeWidth - 12 (NO usar right por compatibilidad bpmn-js v18)
|
||||
Símbolo ⚡: white, 8px, font-weight 700
|
||||
```
|
||||
|
||||
#### En tabla
|
||||
```
|
||||
Tamaño: 14px diameter inline
|
||||
Fondo: #F59845
|
||||
Color: white
|
||||
```
|
||||
|
||||
#### En leyenda
|
||||
```
|
||||
Tamaño: 14px diameter
|
||||
Mismo estilo que en tabla
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Rediseño de PDFs
|
||||
|
||||
### 3.1 Decisión arquitectónica: orientación híbrida
|
||||
|
||||
Los PDFs con diagrama BPMN tendrán **layout híbrido portrait + landscape**:
|
||||
|
||||
- **Páginas SIN diagrama:** vertical A4 (portrait)
|
||||
- **Página DEL diagrama BPMN:** horizontal A4 (landscape) — para que el diagrama tenga espacio real
|
||||
|
||||
Esto es bloqueante: jsPDF soporta página por página con orientación individual. Implementar usando `doc.addPage([width, height], 'landscape')` para la página del diagrama.
|
||||
|
||||
### 3.2 PDF de Comparación + ROI (4 páginas)
|
||||
|
||||
#### Página 1 — Resumen ejecutivo (vertical)
|
||||
|
||||
Reemplaza la carátula. Header denso que incluye toda la metadata.
|
||||
|
||||
```
|
||||
Header (con border-bottom 3px solid #F59845):
|
||||
- Logo InQ ROI a la izquierda
|
||||
- "UN PRODUCTO DE / InQ / InQuality" a la derecha
|
||||
|
||||
Strip de metadata (grid 2fr 1fr 1fr en fondo #FAFBFC):
|
||||
- Proceso: Originación de créditos personales
|
||||
- Cliente: Banco Atlántico S.A.
|
||||
- Fecha: 15 mayo 2026
|
||||
|
||||
Banner narrativo (#FFF8ED con border-left naranja):
|
||||
Resumen ejecutivo: Automatización de N actividades sobre proceso de M etapas.
|
||||
Frecuencia X/año, horizonte Y años, inversión USD Z.
|
||||
|
||||
KPI Hero (gradiente naranja → magenta):
|
||||
AHORRO PROYECTADO A 3 AÑOS
|
||||
USD 192.400 — neto de inversión
|
||||
Recuperás 2,4× la inversión en el período
|
||||
|
||||
KPIs secundarios (grid 2x2):
|
||||
- AHORRO POR EJECUCIÓN: USD 18,16 — ↓ 38%
|
||||
- AHORRO ANUAL: USD 90.800
|
||||
- PAYBACK: 10,6 meses
|
||||
- ROI ANUAL: 114%
|
||||
|
||||
Top 3 ahorros (caja con border #E2E8F0):
|
||||
Header: "[3] Top actividades con mayor ahorro"
|
||||
3 barras horizontales con %, monto, label
|
||||
|
||||
Trayectoria del ahorro (caja con border #E2E8F0):
|
||||
Gráfico SVG nativo en jsPDF con:
|
||||
- Área negativa (antes del payback) en #FEE2E2
|
||||
- Área positiva (después) en #FED7AA
|
||||
- Línea principal #F59845
|
||||
- Marker vertical en mes de payback
|
||||
- Labels eje X: 0 / Año 1 / Año 2 / Año 3
|
||||
- Valor final en esquina superior derecha
|
||||
IMPORTANTE: dibujado con jsPDF nativo, NO captura html2canvas
|
||||
(los charts ECharts en imagen quedan borrosos, lo verificamos en MVP)
|
||||
|
||||
Footer (absolute bottom):
|
||||
"InQ ROI · Powered by InQuality · Banco Atlántico · 15 may 2026"
|
||||
"Página 1 de 4"
|
||||
```
|
||||
|
||||
#### Página 2 — Diagrama BPMN (horizontal)
|
||||
|
||||
```
|
||||
Orientación: landscape A4
|
||||
|
||||
Header (border-bottom 2px solid #F59845):
|
||||
- Logo InQ ROI + título "MAPA DE CALOR · ESCENARIO ACTUAL"
|
||||
- "Originación de créditos personales"
|
||||
- Costo total a la derecha
|
||||
|
||||
Canvas del diagrama:
|
||||
- Background #FAFBFC
|
||||
- Border 0.5px #E2E8F0
|
||||
- Border-radius 6px
|
||||
- Padding 18px
|
||||
- Altura ~400px (toma casi toda la página)
|
||||
- El diagrama BPMN ocupa TODO el ancho disponible
|
||||
- Embebido como JPEG vía html2canvas
|
||||
- Badge ⚡ #F59845 sobre los nodos automatizables
|
||||
|
||||
Leyenda al pie:
|
||||
- Heatmap gradient: bajo → alto
|
||||
- Badge ⚡: marcado como automatizable
|
||||
|
||||
Footer (modificado para landscape):
|
||||
Mismo formato pero ajustado al ancho horizontal
|
||||
```
|
||||
|
||||
#### Página 3 — Comparación por actividad (vertical)
|
||||
|
||||
```
|
||||
Header:
|
||||
- Logo InQ ROI
|
||||
- "COMPARACIÓN ACTIVIDAD POR ACTIVIDAD"
|
||||
- "Detalle de costos: actual vs. automatizado"
|
||||
|
||||
Texto introductorio:
|
||||
"Las actividades marcadas con [⚡] son candidatas a automatización..."
|
||||
|
||||
Tabla autotable (jsPDF-autotable):
|
||||
Columnas: Actividad | Costo actual | Costo auto. | Ahorro | % | Prob.
|
||||
Header con fondo #FFF8ED y border-bottom #F59845
|
||||
Filas automatizables: fondo blanco
|
||||
Filas no-automatizables: fondo #F8FAFC, texto #64748B
|
||||
Total row: fondo #FFF8ED, border-top 1.5px #F59845
|
||||
|
||||
Nota:
|
||||
"Tabla muestra X de Y actividades..."
|
||||
|
||||
Footer estándar
|
||||
```
|
||||
|
||||
#### Página 4 — Análisis y metodología (vertical)
|
||||
|
||||
```
|
||||
Header:
|
||||
"CONTEXTO DEL ANÁLISIS"
|
||||
"Composición del ahorro y metodología"
|
||||
|
||||
Sección: Origen del ahorro
|
||||
Texto: "El N% del ahorro proyectado proviene de la automatización de..."
|
||||
3 barras horizontales con el desglose
|
||||
|
||||
Sección: Metodología de cálculo
|
||||
Caja gris (#F8FAFC):
|
||||
- Costo por ejecución (fórmula)
|
||||
- Ahorro anual (fórmula)
|
||||
- Payback (fórmula)
|
||||
- ROI anualizado (fórmula)
|
||||
|
||||
Sección: Supuestos del análisis
|
||||
Caja ámbar (warning):
|
||||
Texto sobre limitaciones y supuestos
|
||||
|
||||
Sección: Transparencia metodológica (CONDICIONAL)
|
||||
Solo si TransparencySection detecta casos (los 4 ya implementados):
|
||||
- Caso 1: actividades automatizables con costo cero
|
||||
- Caso 2: inversión cero
|
||||
- Caso 3: ROI > 1000%
|
||||
- Caso 4: payback < 1 mes con inversión 0
|
||||
Mismo estilo de caja ámbar
|
||||
|
||||
Sección: Próximos pasos sugeridos (opcional, hardcoded por ahora)
|
||||
3-4 puntos de validación recomendada
|
||||
|
||||
Footer estándar
|
||||
```
|
||||
|
||||
### 3.3 PDFs de Actual y Automatizado (3 páginas cada uno)
|
||||
|
||||
Los PDFs por escenario individual mantienen estructura similar pero adaptada:
|
||||
|
||||
```
|
||||
Página 1 (vertical):
|
||||
Header
|
||||
Strip de metadata
|
||||
KPIs principales (Costo total, Costo directo/Overhead, Tiempo, Actividades)
|
||||
Top 3 actividades por costo
|
||||
Footer
|
||||
|
||||
Página 2 (horizontal):
|
||||
Diagrama BPMN con heatmap embebido
|
||||
Leyenda
|
||||
|
||||
Página 3 (vertical):
|
||||
Tabla de costos por actividad
|
||||
Nota metodológica
|
||||
Footer
|
||||
```
|
||||
|
||||
### 3.4 Formato de Payback (regla global)
|
||||
|
||||
Implementar en un helper compartido entre web y PDF:
|
||||
|
||||
```typescript
|
||||
function formatPayback(months: number): string {
|
||||
if (Number.isNaN(months) || !isFinite(months)) return "No recupera en horizonte";
|
||||
if (months < 0) return "Sin payback (costo aumenta)";
|
||||
if (months === 0) return "Inmediato";
|
||||
if (months < 0.1) return "< 1 día";
|
||||
if (months < 1) return `< 1 mes (${months.toFixed(2)} meses)`;
|
||||
if (months < 12) return `${months.toFixed(1)} meses`;
|
||||
if (months < 24) {
|
||||
const years = (months / 12).toFixed(1);
|
||||
return `${months.toFixed(1)} meses (~${years} años)`;
|
||||
}
|
||||
return `${(months / 12).toFixed(1)} años`;
|
||||
}
|
||||
```
|
||||
|
||||
Tests:
|
||||
- 0 → "Inmediato"
|
||||
- 0.05 → "< 1 día"
|
||||
- 0.5 → "< 1 mes (0.50 meses)"
|
||||
- 6 → "6.0 meses"
|
||||
- 15 → "15.0 meses (~1.3 años)"
|
||||
- 30 → "2.5 años"
|
||||
- Infinity → "No recupera en horizonte"
|
||||
- -1 → "Sin payback (costo aumenta)"
|
||||
- NaN → "No recupera en horizonte"
|
||||
- 11.95 → "11.9 meses"
|
||||
- 12.0 → "12.0 meses (~1.0 años)"
|
||||
|
||||
---
|
||||
|
||||
## 4. Rediseño Web App
|
||||
|
||||
### 4.1 Topbar global
|
||||
|
||||
```
|
||||
Background: white
|
||||
Border-bottom: 1px solid #E2E8F0
|
||||
Padding: 12px 20px
|
||||
Layout: flex justify-between
|
||||
|
||||
Izquierda:
|
||||
Logo "InQ ROI" 18px / 700 / #F59845
|
||||
Spacer 24px
|
||||
Nav links:
|
||||
- Workspace (activo: fondo #FFF8ED, texto #F59845, font-weight 500)
|
||||
- Reporte
|
||||
- Recursos
|
||||
Cada link: padding 6px 12px, border-radius 4px
|
||||
|
||||
Derecha:
|
||||
Botón secundario "Exportar PDF" (white, border #E2E8F0)
|
||||
Botón primario "Simular →" (background #F59845, color white, font-weight 700)
|
||||
```
|
||||
|
||||
### 4.2 Workspace (BpmnCanvas + ActivityPanel)
|
||||
|
||||
```
|
||||
Layout: flex, height calc(100vh - topbar)
|
||||
|
||||
Canvas izquierdo (flex 1):
|
||||
Background: #FAFBFC
|
||||
Padding: 20px
|
||||
Border-right: 1px solid #E2E8F0
|
||||
|
||||
Diagrama BPMN:
|
||||
- Estilos por defecto de bpmn-js
|
||||
- Actividad seleccionada: fondo #FFF8ED, border 2px #F59845
|
||||
- Badges ⚡ en automatizables: círculo #F59845, ⚡ blanco
|
||||
- Posición badge: top: -8, left: width - 12
|
||||
|
||||
Leyenda flotante (bottom-left):
|
||||
Background white, border 0.5px #E2E8F0, radius 6px
|
||||
Padding 8px 12px
|
||||
Contenido: "N actividades automatizables" con icono
|
||||
|
||||
Panel derecho (width 280-320px):
|
||||
Background: white
|
||||
Padding: 16px 18px
|
||||
Overflow: auto
|
||||
|
||||
Header del panel:
|
||||
Badge ⚡ + nombre actividad + "Actividad seleccionada"
|
||||
Border-bottom 1px #E2E8F0
|
||||
Padding-bottom 12px
|
||||
|
||||
Sección "COSTO ACTUAL":
|
||||
Label 10px / 500 / letter-spacing 1px / #94A3B8
|
||||
Inputs: border #E2E8F0, padding 6px 10px, bg #F8FAFC
|
||||
|
||||
Sección "AUTOMATIZACIÓN" (caja destacada):
|
||||
Background: #FFF8ED
|
||||
Border: 1px solid #F59845
|
||||
Padding: 12px
|
||||
Border-radius: 6px
|
||||
|
||||
Header con título y switch:
|
||||
"AUTOMATIZACIÓN" 11px / 500 / #92400E
|
||||
Switch: 30x16px, background #F59845 cuando ON
|
||||
|
||||
Inputs:
|
||||
border #F59845, background white, color #0F172A
|
||||
|
||||
Helper de ahorro estimado (cuando aplica):
|
||||
Background: #F0FDF4
|
||||
Border-left: 2px solid #16A34A
|
||||
Padding: 8px 10px
|
||||
Texto: 10px / color #15803D
|
||||
Texto: "Ahorro estimado: USD X por ejecución (Y%)"
|
||||
```
|
||||
|
||||
### 4.3 Reporte ROI (tab Comparación + ROI)
|
||||
|
||||
```
|
||||
Page header:
|
||||
Título "Nombre del proceso"
|
||||
Subtítulo "Cliente · Simulado el ..."
|
||||
|
||||
Tabs:
|
||||
3 tabs horizontales con border-bottom-color en activo
|
||||
Activo: border-bottom 2px #F59845, texto #F59845 / 500
|
||||
Inactivo: texto #64748B
|
||||
|
||||
Contenido tab "Comparación + ROI":
|
||||
|
||||
KPI Hero:
|
||||
Mismo estilo que PDF — gradient naranja → magenta
|
||||
Margin-bottom 12px
|
||||
|
||||
KPIs secundarios (grid 4 columnas):
|
||||
Mismo estilo que PDF
|
||||
Gap 8px
|
||||
|
||||
Gráficos (grid 2 columnas):
|
||||
Trayectoria del ahorro: chart con líneas y marker
|
||||
Top 3 ahorros: barras horizontales
|
||||
|
||||
Tabla comparativa completa abajo:
|
||||
Misma estructura que PDF Pág. 3
|
||||
Sortable por columna ahorro (mantener funcionalidad existente)
|
||||
|
||||
Sección Transparencia (condicional):
|
||||
Misma estructura que PDF, banner ámbar
|
||||
```
|
||||
|
||||
### 4.4 Componentes shadcn a personalizar
|
||||
|
||||
Los componentes shadcn que ya existen necesitan tema con los tokens nuevos:
|
||||
|
||||
- `Button` variant "default" → fondo `#F59845`, hover `#B45309`, font-weight 700
|
||||
- `Button` variant "secondary" → border `#E2E8F0`, hover bg `#F8FAFC`
|
||||
- `Tabs` → active state con color naranja en lugar de azul
|
||||
- `Switch` → naranja cuando checked
|
||||
- `Badge` → variantes "automatable" con fondo naranja
|
||||
|
||||
---
|
||||
|
||||
## 5. Cambios funcionales asociados
|
||||
|
||||
### 5.1 Helper `formatPayback`
|
||||
|
||||
Crear en `src/lib/format.ts` (o donde ya estén los formatters de moneda).
|
||||
|
||||
Tests obligatorios — ver sección 3.4.
|
||||
|
||||
Aplicar en:
|
||||
- `RoiKpiBar.tsx` (web)
|
||||
- `pdf-sections-roi.ts → drawRoiKpiCards`
|
||||
- `csv-export.ts` línea de metadata
|
||||
|
||||
### 5.2 Detector de ROI inusual ya implementado en Etapa 3 de Sprint 1
|
||||
|
||||
Verificar que la sección Transparencia se renderiza correctamente en el PDF (Sprint 1 Etapa 4 tuvo PDF de 36KB que era anómalo — revisar que ahora con el rediseño tenga densidad razonable).
|
||||
|
||||
### 5.3 Overlay badge en BpmnCanvas
|
||||
|
||||
El badge ⚡ ya está implementado. Cambios visuales:
|
||||
- Fondo de blanco a `#F59845` (más prominente)
|
||||
- ⚡ blanco en lugar de ámbar
|
||||
- Posición: cambiar de `{ top: -8, right: -8 }` a `{ top: -8, left: elementWidth - 12 }` para no depender del soporte de `right` en bpmn-js v18
|
||||
|
||||
### 5.4 Carátula del PDF — eliminar
|
||||
|
||||
El PDF anterior tenía una carátula con gradient grande. Reemplazar por header denso en página 1 (ver sección 3.2 — Página 1).
|
||||
|
||||
---
|
||||
|
||||
## 6. Validación E2E
|
||||
|
||||
### 6.1 Archivos a generar
|
||||
|
||||
Para validar el rediseño, generar los siguientes archivos en `tests/e2e/__output__/`:
|
||||
|
||||
**Setup A: medium-with-gateways realista**
|
||||
- 3 actividades automatizables (`task_recv`, `task_score`, `task_analisis`)
|
||||
- Costos directos: $200/actividad
|
||||
- Costos automatizados: $20/actividad
|
||||
- Frecuencia anual: 5.000
|
||||
- Horizonte: 3 años
|
||||
- Inversión: USD 80.000
|
||||
|
||||
Generar:
|
||||
- `medium-gw_actual_v2.pdf`
|
||||
- `medium-gw_automatizado_v2.pdf`
|
||||
- `medium-gw_roi_v2.pdf`
|
||||
|
||||
**Setup B: medium-with-gateways con ROI inusual**
|
||||
- Mismas actividades automatizables
|
||||
- Costos automatizados: $1 (para forzar ROI > 1000%)
|
||||
- Frecuencia anual: 12.000
|
||||
- Inversión: USD 10.000
|
||||
|
||||
Generar:
|
||||
- `medium-gw_roi_inusual_v2.pdf` — debe mostrar Transparencia Metodológica con aviso de ROI inusual
|
||||
|
||||
**Setup C: payback en distintos rangos**
|
||||
- `payback_inmediato.pdf` — inversión = 0
|
||||
- `payback_corto.pdf` — payback de 0.5 meses
|
||||
- `payback_normal.pdf` — payback de 10 meses
|
||||
- `payback_largo.pdf` — payback de 30 meses
|
||||
|
||||
Generar:
|
||||
- `payback_formats.pdf` — uno solo con los 4 casos para verificar el formato visual
|
||||
|
||||
### 6.2 Métricas que Claude Code DEBE reportar
|
||||
|
||||
Tabla de validación:
|
||||
|
||||
```
|
||||
┌────────────────────────────┬──────────┬──────────┬───────────────────┐
|
||||
│ Archivo │ Tamaño │ Páginas │ Validación visual │
|
||||
├────────────────────────────┼──────────┼──────────┼───────────────────┤
|
||||
│ medium-gw_actual_v2.pdf │ XX KB │ 3 │ ✓ / ✗ │
|
||||
│ medium-gw_automatizado_v2 │ XX KB │ 3 │ ✓ / ✗ │
|
||||
│ medium-gw_roi_v2.pdf │ XX KB │ 4 │ ✓ / ✗ │
|
||||
│ medium-gw_roi_inusual_v2 │ XX KB │ 4 │ ✓ / ✗ │
|
||||
└────────────────────────────┴──────────┴──────────┴───────────────────┘
|
||||
```
|
||||
|
||||
**Tamaño esperado por página: 25-50 KB.** Páginas con diagrama BPMN embebido pueden llegar a 60 KB. Si alguna página queda en < 15 KB es señal de que algo no se renderizó.
|
||||
|
||||
Análisis de píxeles del heatmap embebido (igual que MVP):
|
||||
- Detectar verde, amarillo y rojo en la página del diagrama
|
||||
- Verificar que el badge ⚡ naranja `#F59845` está presente (al menos un pixel naranja sobre el diagrama)
|
||||
|
||||
Text extraction con pdf-parse para verificar palabras clave:
|
||||
- `_actual.pdf`: "Costo total esperado", "Análisis de procesos"
|
||||
- `_automatizado.pdf`: "Costo total", "actividades no automatizables"
|
||||
- `_roi.pdf`: "Análisis de retorno", "Ahorro anual", "Payback", "ROI"
|
||||
- `_roi_inusual.pdf`: TODO lo anterior + "ROI inusualmente alto detectado"
|
||||
|
||||
Verificación de marca:
|
||||
- En cada PDF, buscar texto exacto "InQ" (con I-n-Q correctos)
|
||||
- Verificar que NO aparece "INQ", "Inq", "inq" en ningún PDF
|
||||
- Verificar "Powered by InQuality" en cada footer
|
||||
|
||||
### 6.3 Validación visual obligatoria por el usuario
|
||||
|
||||
Después de generar los archivos, presentarlos vía `present_files` para que el usuario abra los PDFs en Preview y valide:
|
||||
|
||||
1. ¿Los KPIs se ven como cards prominentes con el gradiente naranja → magenta?
|
||||
2. ¿El diagrama BPMN se ve grande y legible en la página horizontal?
|
||||
3. ¿El formato de payback es claro ("10.6 meses" vs "0.0 meses")?
|
||||
4. ¿La sección Transparencia metodológica aparece cuando corresponde?
|
||||
5. ¿"InQ ROI" y "Powered by InQuality" se ven correctos en headers/footers?
|
||||
6. ¿La densidad de información es adecuada (no hay páginas semivacías)?
|
||||
|
||||
---
|
||||
|
||||
## 7. Definition of Done
|
||||
|
||||
- [ ] Manual de marca aplicado: paleta naranja InQ, tipografía consistente, "InQ" siempre I-n-Q
|
||||
- [ ] PDF de Comparación + ROI con layout híbrido (vertical + horizontal + vertical + vertical)
|
||||
- [ ] PDF de Actual y Automatizado con layout híbrido (vertical + horizontal + vertical)
|
||||
- [ ] Página 1 de cada PDF SIN carátula, con header denso que incluye metadata
|
||||
- [ ] KPI Hero con gradiente naranja → magenta en PDF y Web
|
||||
- [ ] Top 3 ahorros visible en página 1 del PDF ROI
|
||||
- [ ] Tabla comparativa con paleta naranja consistente
|
||||
- [ ] Footer real al pie de página (no a mitad)
|
||||
- [ ] Helper `formatPayback` implementado y aplicado en UI, PDF y CSV
|
||||
- [ ] Badge ⚡ en BpmnCanvas con fondo naranja sólido `#F59845`
|
||||
- [ ] Posición del badge cambiada a `{ top, left }` (no `right`) — robusto a bpmn-js v18
|
||||
- [ ] Web app con identidad: topbar con logo, CTAs naranjas, tabs con color de marca
|
||||
- [ ] Panel de Automatización en ActivityPanel con caja naranja destacada
|
||||
- [ ] 7 archivos de validación generados en `tests/e2e/__output__/`
|
||||
- [ ] Análisis de píxeles confirma badge naranja + heatmap completo
|
||||
- [ ] Verificación textual confirma "InQ" correcto en todos los PDFs
|
||||
- [ ] `present_files` ejecutado para que el usuario abra los PDFs
|
||||
- [ ] 440 tests previos siguen verdes
|
||||
- [ ] Nuevos tests de `formatPayback` (≥12 casos)
|
||||
- [ ] Build limpio, lint limpio
|
||||
- [ ] CHANGELOG.md actualizado con sección "Sprint 1.5 — Rediseño visual"
|
||||
- [ ] DESIGN_SYSTEM.md creado con paleta, tipografía y componentes (referencia para Sprint 2+)
|
||||
|
||||
---
|
||||
|
||||
## 8. Lo que NO entra en Sprint 1.5
|
||||
|
||||
- Refinamiento del ícono ⚡ a Bot de Lucide (Sprint 2-3)
|
||||
- Nueva entidad `ProcessOverheadCost` para multas/penalidades (Sprint 2-3)
|
||||
- Branding completo con logo SVG de InQuality embebido (Sprint 3-4)
|
||||
- Excel multi-hoja con identidad (Sprint 3-4)
|
||||
- Carátula opcional vía toggle de export (no se necesita por ahora — decidimos no usarla)
|
||||
- Animaciones avanzadas en web app (Sprint 4+)
|
||||
- Modo oscuro (Sprint 4+)
|
||||
|
||||
---
|
||||
|
||||
## 9. Cambios en archivos clave
|
||||
|
||||
### Nuevos
|
||||
- `src/lib/format.ts` (o extender el existente) — `formatPayback`
|
||||
- `DESIGN_SYSTEM.md` — referencia visual del sistema
|
||||
- `src/lib/export/pdf-sections-shared.ts` — header y footer reutilizables
|
||||
- `tests/lib/format-payback.test.ts` — tests del helper
|
||||
|
||||
### Modificados
|
||||
- `src/lib/export/pdf-export.ts` — soportar orientación landscape en una página
|
||||
- `src/lib/export/pdf-sections.ts` — aplicar nuevo manual de marca
|
||||
- `src/lib/export/pdf-sections-roi.ts` — rediseño completo del PDF ROI
|
||||
- `src/features/workspace/BpmnCanvas.tsx` — badge ⚡ naranja + posición
|
||||
- `src/features/workspace/ActivityPanel.tsx` — sección Automatización con caja naranja
|
||||
- `src/features/report/RoiKpiBar.tsx` — KPI Hero gradiente + secundarios paleta
|
||||
- `src/features/report/ComparisonTable.tsx` — paleta naranja
|
||||
- `src/features/report/TransparencySection.tsx` — verificar render correcto
|
||||
- `src/components/ui/*` — tokens naranja en components shadcn (button, tabs, switch)
|
||||
- `tailwind.config.ts` — extender paleta con tokens InQ
|
||||
- `index.css` — definir variables CSS
|
||||
|
||||
### En CLAUDE.md, agregar regla crítica
|
||||
```
|
||||
## Reglas de marca (NO NEGOCIABLES)
|
||||
|
||||
1. La versión corta de InQuality SIEMPRE se escribe "InQ":
|
||||
- I mayúscula
|
||||
- n minúscula
|
||||
- Q mayúscula
|
||||
- Cualquier otra forma (INQ, Inq, inq, inQ) es bug
|
||||
|
||||
2. El producto se llama "InQ ROI" en este Sprint. Es nombre de trabajo,
|
||||
se reemplazará en Sprint 3-4. No debatir el nombre ahora.
|
||||
|
||||
3. Color protagonista: naranja InQ #F59845.
|
||||
Gradiente identidad: #F59845 → #ED3E8F (naranja → magenta).
|
||||
No usar morado de CONCILIA (#572FA2, #CB1889) en este producto.
|
||||
|
||||
4. Tagline obligatorio en footers de PDF: "Powered by InQuality".
|
||||
|
||||
5. No promover a Marcos como creador del producto. Si aparece nombre del
|
||||
consultor en algún campo, es como "consultor responsable", no como
|
||||
"creador" o "founder".
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Orden de implementación (11 etapas)
|
||||
|
||||
Cada etapa es validable independientemente. Pattern: implementación → validación visual con usuario → siguiente etapa.
|
||||
|
||||
1. **Etapa 1 — Setup del sistema de diseño** ✅ COMPLETADA
|
||||
- DESIGN_SYSTEM.md, tailwind.config.ts, index.css
|
||||
- Tokens InQ + heatmap
|
||||
- Audit de azules y hardcodes
|
||||
- Tests de tokens
|
||||
|
||||
2. **Etapa 2 — Helper formatPayback + aplicación**
|
||||
- Helper en src/lib/format.ts (12 tests)
|
||||
- Aplicación en RoiKpiBar, PDF, CSV
|
||||
- Actualización del audit con WCAG y donut
|
||||
|
||||
3. **Etapa 3 — Web App: workspace + reporte**
|
||||
- Topbar con logo InQ ROI
|
||||
- ActivityPanel con sección Automatización naranja
|
||||
- Reporte con KPI Hero gradiente y secundarios paleta
|
||||
- font-weight 700 en CTAs primarios (WCAG)
|
||||
- Validación visual con usuario
|
||||
|
||||
4. **Etapa 4 — PDF: header y footer compartidos**
|
||||
- Crear `pdf-sections-shared.ts`
|
||||
- Refactorizar PDFs existentes para usar header/footer comunes
|
||||
- Aplicar paleta InQ
|
||||
|
||||
5. **Etapa 5 — PDF: layout híbrido (vertical + horizontal)**
|
||||
- Modificar `pdf-export.ts` para soportar landscape en página específica
|
||||
- Implementar página del diagrama horizontal
|
||||
- Generar primer PDF de validación
|
||||
|
||||
6. **Etapa 6 — PDF: rediseño completo de página 1 ROI**
|
||||
- Header denso sin carátula
|
||||
- KPI Hero, secundarios, top 3, gráfico de trayectoria
|
||||
- Aplicar `formatPayback`
|
||||
- Generar segundo PDF de validación
|
||||
|
||||
7. **Etapa 7 — PDF: páginas 3 y 4 ROI**
|
||||
- Tabla comparativa con paleta
|
||||
- Análisis textual con secciones
|
||||
- Sección Transparencia condicional
|
||||
- Generar tercer PDF de validación
|
||||
|
||||
8. **Etapa 8 — PDFs de Actual y Automatizado**
|
||||
- Aplicar mismo sistema
|
||||
- Verificar consistencia visual
|
||||
|
||||
9. **Etapa 9 — Badge ⚡ en BpmnCanvas**
|
||||
- Color naranja sólido
|
||||
- Posición robusta (top + left)
|
||||
- Test del overlay
|
||||
|
||||
10. **Etapa 10 — Validación E2E final**
|
||||
- Generar los 7 archivos especificados
|
||||
- Análisis de píxeles, text extraction
|
||||
- `present_files` para validación visual del usuario
|
||||
|
||||
11. **Etapa 11 — Documentación + Deploy**
|
||||
- CHANGELOG.md
|
||||
- Actualizar CLAUDE.md con reglas de marca
|
||||
- Tag de release v0.2.0-sprint1-visual
|
||||
|
||||
---
|
||||
|
||||
## Notas finales para Claude Code
|
||||
|
||||
- Este es un sprint de POLISH, no de features. Si encontrás algo que requiere cambios arquitectónicos importantes (más de 1 hora), PARÁ y consultá con el usuario antes de proceder.
|
||||
|
||||
- La validación visual final la hace el usuario abriendo los PDFs en Preview de Mac. Tu trabajo NO es solo generar archivos — es generar archivos QUE SE VEAN COMO EL MOCKUP MUESTRA. No declares la etapa terminada sin haber verificado tamaño, páginas, análisis de píxeles del heatmap, y palabras clave del texto extraído.
|
||||
|
||||
- Cada PDF generado pasa por `present_files` para que el usuario lo abra. NO te conformes con "los tests pasaron".
|
||||
|
||||
- Si en algún momento de la implementación descubrís que el mockup tiene una inconsistencia o un detalle no especificado, hacé las preguntas explícitamente antes de inventar la respuesta.
|
||||
|
||||
- El usuario ha sido riguroso pidiendo validación honesta. Mantené ese estándar: reportá lo que efectivamente verificaste, no lo que asumiste.
|
||||
220
sprints/sprint-1-5/ETAPA_1_PROMPT.md
Normal file
220
sprints/sprint-1-5/ETAPA_1_PROMPT.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# Sprint 1.5 — Etapa 1: Setup del sistema de diseño
|
||||
|
||||
> **Estado: COMPLETADA y APROBADA** (16 mayo 2026)
|
||||
>
|
||||
> Este documento es el prompt original que se ejecutó. Se preserva como referencia histórica del Sprint 1.5. La evidencia de ejecución vive en:
|
||||
> - Código del repo (tokens InQ aplicados)
|
||||
> - `sprints/sprint-1-5/etapa-1-audit.md` (audit de azules y hardcodes)
|
||||
> - Tests en `tests/design-system/tokens.test.ts`
|
||||
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Establecer la infraestructura visual del Sprint 1.5 antes de tocar cualquier componente de UI o PDF. Sin esto, los cambios de Etapas 2-9 quedarían hardcodeados con hexcodes dispersos.
|
||||
|
||||
---
|
||||
|
||||
## Plan aprobado con 4 ajustes
|
||||
|
||||
### Ajuste 1 — Heatmap: tokens SÍ, aplicación NO
|
||||
|
||||
Mantener los colores saturados actuales del heatmap (verde/ámbar/rojo). NO migrar a la paleta pastel del brief en esta etapa.
|
||||
|
||||
Razones:
|
||||
- Romper los E2E de análisis de píxeles calibrados a colores saturados sería retroceso. Esos tests fueron el mecanismo de validación más sólido en Sprint 1.
|
||||
- El producto se usa en talleres y charlas en pantallas grandes. El pastel se ve lavado a distancia.
|
||||
|
||||
Acciones:
|
||||
- En `tailwind.config.ts`, definir tokens del heatmap saturado actual:
|
||||
```typescript
|
||||
heatmap: {
|
||||
low: '#10B981',
|
||||
mid: '#F59E0B',
|
||||
high: '#EF4444',
|
||||
}
|
||||
```
|
||||
- Reemplazar los hex hardcodeados de `HeatmapCanvas` por estos tokens.
|
||||
- En `DESIGN_SYSTEM.md`, documentar la decisión.
|
||||
- En `TODO.md`: anotar revisión del heatmap para Sprint 3-4.
|
||||
|
||||
### Ajuste 2 — Audit de azules residuales y hardcodes
|
||||
|
||||
Antes de finalizar Etapa 1, ejecutar auditoría:
|
||||
|
||||
```bash
|
||||
grep -rn "blue\|sky\|indigo\|3B82F6\|2563EB\|1D4ED8" src/ \
|
||||
--include="*.tsx" --include="*.ts" --include="*.css"
|
||||
|
||||
grep -rn "primary" src/ --include="*.tsx" --include="*.ts"
|
||||
|
||||
grep -rn "#[0-9a-fA-F]\{6\}" src/ --include="*.tsx" --include="*.ts"
|
||||
```
|
||||
|
||||
Clasificar cada hallazgo en 3 categorías:
|
||||
- "Migrar a token InQ"
|
||||
- "Mantener azul funcional"
|
||||
- "Migrar a neutral"
|
||||
|
||||
Documentar en `sprints/sprint-1-5/etapa-1-audit.md` con archivo:línea, color, clasificación, acción sugerida. NO resolver en Etapa 1; solo listar.
|
||||
|
||||
### Ajuste 3 — Tests del sistema de diseño
|
||||
|
||||
Crear `tests/design-system/tokens.test.ts` con al menos:
|
||||
|
||||
```typescript
|
||||
describe('Design tokens', () => {
|
||||
it('expone tokens de marca InQ vía Tailwind config', () => {
|
||||
expect(config.theme.extend.colors.inq.orange.DEFAULT).toBe('#F59845');
|
||||
expect(config.theme.extend.colors.inq.orange.dark).toBe('#B45309');
|
||||
expect(config.theme.extend.colors.inq.orange.light).toBe('#FEF3E2');
|
||||
expect(config.theme.extend.colors.inq.orange.lighter).toBe('#FFF8ED');
|
||||
expect(config.theme.extend.colors.inq.magenta).toBe('#ED3E8F');
|
||||
});
|
||||
|
||||
it('NO incluye los colores de CONCILIA', () => {
|
||||
const allColors = JSON.stringify(config.theme.extend.colors);
|
||||
expect(allColors).not.toContain('#572FA2');
|
||||
expect(allColors).not.toContain('#CB1889');
|
||||
});
|
||||
|
||||
it('expone tokens del heatmap saturado', () => {
|
||||
expect(config.theme.extend.colors.heatmap.low).toBe('#10B981');
|
||||
expect(config.theme.extend.colors.heatmap.mid).toBe('#F59E0B');
|
||||
expect(config.theme.extend.colors.heatmap.high).toBe('#EF4444');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Adicionalmente, crear `tests/lib/format-payback.test.ts` con 7 `it.todo` que preparan Etapa 2.
|
||||
|
||||
### Ajuste 4 — Estructura anidada en Tailwind config
|
||||
|
||||
Estructura anidada (no con guiones en clases):
|
||||
|
||||
```typescript
|
||||
inq: {
|
||||
orange: {
|
||||
DEFAULT: '#F59845',
|
||||
dark: '#B45309',
|
||||
light: '#FEF3E2',
|
||||
lighter: '#FFF8ED',
|
||||
},
|
||||
magenta: '#ED3E8F',
|
||||
},
|
||||
heatmap: {
|
||||
low: '#10B981',
|
||||
mid: '#F59E0B',
|
||||
high: '#EF4444',
|
||||
}
|
||||
```
|
||||
|
||||
Permite uso natural:
|
||||
- `bg-inq-orange` (DEFAULT)
|
||||
- `bg-inq-orange-dark`
|
||||
- `bg-inq-orange-light`
|
||||
- `bg-inq-orange-lighter`
|
||||
- `bg-inq-magenta`
|
||||
- `bg-heatmap-low`
|
||||
|
||||
Colores semánticos:
|
||||
```typescript
|
||||
success: { DEFAULT: '#16A34A', light: '#F0FDF4' },
|
||||
warning: { DEFAULT: '#F59E0B', bg: '#FFFBEB' },
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist final de Etapa 1
|
||||
|
||||
```
|
||||
[x] DESIGN_SYSTEM.md creado con sección de decisión sobre heatmap
|
||||
[x] tailwind.config.ts con tokens InQ anidados + tokens heatmap saturado
|
||||
[x] src/index.css con --primary naranja y variables InQ
|
||||
[x] HeatmapCanvas migrado a tokens heatmap.* (find-and-replace simple)
|
||||
[x] CLAUDE.md con sección "Reglas de marca" al inicio (prominente)
|
||||
[x] tests/design-system/tokens.test.ts con 3 tests verdes
|
||||
[x] tests/lib/format-payback.test.ts con 7 it.todo
|
||||
[x] sprints/sprint-1-5/etapa-1-audit.md con hallazgos (sin resolver)
|
||||
[x] TODO.md actualizado con nota de revisión de heatmap para Sprint 3-4
|
||||
[x] 440 tests previos + 3 nuevos = 443 verdes
|
||||
[x] npm run build limpio
|
||||
[x] npm run dev levanta sin errores
|
||||
[x] Verificación visual con dev server: screenshots de CTAs naranjas,
|
||||
tabs activos naranjas, switches checked naranjas
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reporte de ejecución (resumen)
|
||||
|
||||
### Archivos modificados/creados
|
||||
|
||||
| Archivo | Acción | Líneas |
|
||||
|---|---|---|
|
||||
| `tailwind.config.js` | Modificado | +25 (tokens inq.*, heatmap.*, success, warning) |
|
||||
| `src/styles/globals.css` | Modificado | +22 (--primary naranja, variables --inq-*) |
|
||||
| `src/components/ui/tabs.tsx` | Modificado | 1 (text-inq-orange + font-semibold en activo) |
|
||||
| `CLAUDE.md` | Modificado | +42 (sección "Reglas de Marca" prominente) |
|
||||
| `TODO.md` | Modificado | +3 (entrada revisión heatmap Sprint 3-4) |
|
||||
| `DESIGN_SYSTEM.md` | Creado | 232 |
|
||||
| `sprints/sprint-1-5/etapa-1-audit.md` | Creado | 80 |
|
||||
| `tests/design-system/tokens.test.ts` | Creado | 37 (3 tests) |
|
||||
| `tests/lib/format-payback.test.ts` | Creado | 18 (7 it.todo) |
|
||||
| `tests/e2e/screenshots-etapa-1.spec.ts` | Creado | 60 |
|
||||
| `tests/screenshots/etapa-1/*.png` | Generados | 3 |
|
||||
|
||||
### Hallazgos del audit (9 azules + 12 hardcodes)
|
||||
|
||||
Categorizados por etapa de resolución:
|
||||
|
||||
- **Etapa 3 (charts web):** `CumulativeSavingsChart.tsx`, `TopSavingsChart.tsx`, `HeatmapLegend.tsx`, `chart-options.ts`
|
||||
- **Etapa 4-8 (PDFs):** `pdf-sections.ts`, `pdf-export.ts`, `pdf-sections-roi.ts` (`PDF.blue`)
|
||||
- **Etapa 9 (badge ⚡):** `BpmnCanvas.tsx`
|
||||
- **NO migrar (decisión arquitectónica):** `GatewayPanel.tsx` (azul = semántica funcional info banner)
|
||||
|
||||
### Validación visual confirmada
|
||||
|
||||
3 screenshots en `tests/screenshots/etapa-1/`:
|
||||
- `workspace.png` — botón Simular naranja, tab Actividad activo naranja, borde del nodo seleccionado naranja
|
||||
- `report-tabs.png` — tab Actual activo naranja, Exportar PDF naranja, KPI icons naranjas
|
||||
- `switch-on.png` — switch automatizable ON naranja, sección automatización con caja naranja, Guardar cambios naranja
|
||||
|
||||
El cambio de `--primary` de azul a naranja propagó correctamente a: Button, Switch, Tabs, Slider, Tooltip, Badge, rings de focus, bordes de selección en canvas BPMN.
|
||||
|
||||
### Descubrimientos no esperados
|
||||
|
||||
- `--primary-foreground` (210 40% 98%, blanco casi puro) funciona correctamente sobre el naranja sin modificación.
|
||||
- `--ring` también está en naranja, los focus rings de inputs y botones son coherentes con la marca.
|
||||
|
||||
### Tests finales
|
||||
|
||||
- 443 tests verdes (440 previos + 3 nuevos de tokens)
|
||||
- 7 `it.todo` listos para Etapa 2 (formatPayback)
|
||||
- Build limpio, sin warnings TS ni lint nuevos
|
||||
|
||||
---
|
||||
|
||||
## Aprobación final del usuario
|
||||
|
||||
Confirmada con observaciones que se trasladan a Etapa 2:
|
||||
|
||||
1. La fila de `GatewayPanel.tsx` en el audit se reclasifica de "Mantener azul funcional / Evaluar Sprint 2" a "Decisión arquitectónica: mantener azul informativo. No es marca, es semántica funcional."
|
||||
|
||||
2. El donut chart (`chart-options.ts:123`) tiene una decisión visual pendiente: sector "directo" en naranja InQ, sector "overhead" en slate-400 o naranja claro (no en otro naranja). Decisión final en Etapa 3.
|
||||
|
||||
3. Contraste WCAG del naranja `#F59845` sobre blanco: ratio ~2.42:1, FAIL para AA normal y AA large. Decisión propuesta para Etapa 3: aplicar `font-weight: 700` en CTAs primarios (opción D del análisis).
|
||||
|
||||
Estas 3 observaciones se incorporan en Etapa 2 como pre-tareas.
|
||||
|
||||
---
|
||||
|
||||
## Lecciones operativas capturadas
|
||||
|
||||
1. **Pattern de auditoría preventiva**: el grep de azules y hex residuales antes de mergear cambios de paleta previno que la migración a InQ quedara incompleta. Replicar este pattern en cualquier cambio de paleta futuro.
|
||||
|
||||
2. **Validación visual con screenshots Playwright**: más robusto que pedirle al usuario que capture manualmente. Los PNG quedan versionados en `tests/screenshots/etapa-*/` como evidencia de la etapa.
|
||||
|
||||
3. **Decisiones arquitectónicas vs decisiones de marca**: `GatewayPanel` enseñó que no todo color se debe alinear con la marca. El azul informativo es una convención semántica funcional que vale la pena preservar.
|
||||
|
||||
4. **El audit que clasifica por etapa de resolución** evita el "tendremos que migrar todo de golpe". Tokenizar primero, aplicar después en pequeños lotes.
|
||||
@@ -0,0 +1,213 @@
|
||||
GO — Etapa 2: Helper formatPayback + aplicación
|
||||
|
||||
Tu plan original tenía 7 it.todo listos. Esta etapa los convierte en
|
||||
tests reales, implementa el helper y lo aplica en 3 lugares.
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════
|
||||
PRE-TAREAS (actualizar artefactos de Etapa 1)
|
||||
═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
Antes de implementar formatPayback, hacé estas 3 actualizaciones rápidas:
|
||||
|
||||
1. Actualizar etapa-1-audit.md — fila de GatewayPanel.tsx:
|
||||
Cambiar la categoría de las 5 referencias blue-* de GatewayPanel de
|
||||
"Mantener azul funcional / Evaluar en Sprint 2" a:
|
||||
|
||||
"Decisión arquitectónica: mantener azul informativo. No es color
|
||||
de marca, es semántica funcional (info banner, valor inmutable,
|
||||
patrón Bootstrap/Tailwind estándar). NO migrar."
|
||||
|
||||
2. Actualizar etapa-1-audit.md — agregar fila al final de la sección
|
||||
"Hallazgos — Hex hardcodes" para el donut chart:
|
||||
|
||||
| chart-options.ts:123 | #3b82f6 → naranja | Composición directo/overhead
|
||||
| en donut. Decisión visual: el sector "directo" va en naranja InQ
|
||||
| #F59845. El sector "overhead" NO debe ser otro naranja (pierde
|
||||
| contraste) — usar slate-400 (#94A3B8) o naranja muy claro
|
||||
| (#FED7AA). Decisión final en Etapa 3 mirando el render. |
|
||||
|
||||
3. Verificar contraste WCAG de #F59845 sobre blanco. Ejecutá:
|
||||
|
||||
node -e "
|
||||
const hex = '#F59845';
|
||||
const r = parseInt(hex.slice(1,3), 16) / 255;
|
||||
const g = parseInt(hex.slice(3,5), 16) / 255;
|
||||
const b = parseInt(hex.slice(5,7), 16) / 255;
|
||||
const lin = (c) => c <= 0.03928 ? c/12.92 : Math.pow((c+0.055)/1.055, 2.4);
|
||||
const L1 = 0.2126*lin(r) + 0.7152*lin(g) + 0.0722*lin(b);
|
||||
const L2 = 1;
|
||||
const ratio = (L2 + 0.05) / (L1 + 0.05);
|
||||
console.log('Contrast ratio:', ratio.toFixed(2), ':1');
|
||||
console.log('AA normal text (4.5):', ratio >= 4.5 ? 'PASS' : 'FAIL');
|
||||
console.log('AA large text (3.0):', ratio >= 3.0 ? 'PASS' : 'FAIL');
|
||||
"
|
||||
|
||||
Anotá el resultado en etapa-1-audit.md como nueva sección
|
||||
"Accesibilidad — Contraste de marca". Si el resultado es FAIL para
|
||||
AA normal, agregá las 4 opciones de remediación al audit:
|
||||
|
||||
A) Aceptar contraste sub-WCAG en CTAs (riesgo accesibilidad)
|
||||
B) Oscurecer naranja a #D97706 para CTAs (rompe marca parcialmente)
|
||||
C) Usar texto oscuro #451A03 en CTAs sobre naranja (cambia look)
|
||||
D) Forzar font-weight: 700 en CTAs primarios (cumple AA large)
|
||||
|
||||
Marcá la opción D como propuesta a implementar en Etapa 3 cuando
|
||||
rediseñemos KPI cards y CTAs. NO modificar componentes en esta etapa.
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════
|
||||
DECISIONES YA TOMADAS (no consultar de nuevo)
|
||||
═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
Confirmadas con el usuario:
|
||||
|
||||
Payback negativo → "Sin payback (costo aumenta)"
|
||||
(caso donde costo automatizado > costo actual, escenario
|
||||
matemáticamente posible pero indica error de configuración o
|
||||
automatización inviable)
|
||||
|
||||
Texto del formato → usar caracteres compactos "<" y "~"
|
||||
(universales y legibles en KPI cards)
|
||||
|
||||
Pluralización → siempre plural incluso con decimales
|
||||
("1.0 meses", no "1.0 mes")
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════
|
||||
TAREA PRINCIPAL — IMPLEMENTAR formatPayback
|
||||
═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
1. Implementar formatPayback en src/lib/format.ts:
|
||||
|
||||
function formatPayback(months: number): string {
|
||||
if (Number.isNaN(months) || !isFinite(months)) {
|
||||
return "No recupera en horizonte";
|
||||
}
|
||||
if (months < 0) {
|
||||
return "Sin payback (costo aumenta)";
|
||||
}
|
||||
if (months === 0) {
|
||||
return "Inmediato";
|
||||
}
|
||||
if (months < 0.1) {
|
||||
return "< 1 día";
|
||||
}
|
||||
if (months < 1) {
|
||||
return `< 1 mes (${months.toFixed(2)} meses)`;
|
||||
}
|
||||
if (months < 12) {
|
||||
return `${months.toFixed(1)} meses`;
|
||||
}
|
||||
if (months < 24) {
|
||||
const years = (months / 12).toFixed(1);
|
||||
return `${months.toFixed(1)} meses (~${years} años)`;
|
||||
}
|
||||
return `${(months / 12).toFixed(1)} años`;
|
||||
}
|
||||
|
||||
2. Convertir los 7 it.todo existentes en tests reales:
|
||||
|
||||
- formatPayback(0) === "Inmediato"
|
||||
- formatPayback(0.05) === "< 1 día"
|
||||
- formatPayback(0.5) === "< 1 mes (0.50 meses)"
|
||||
- formatPayback(6) === "6.0 meses"
|
||||
- formatPayback(15) === "15.0 meses (~1.3 años)"
|
||||
- formatPayback(30) === "2.5 años"
|
||||
- formatPayback(Infinity) === "No recupera en horizonte"
|
||||
|
||||
3. Agregar 5 tests de edge cases:
|
||||
|
||||
- formatPayback(-1) === "Sin payback (costo aumenta)"
|
||||
// payback negativo: costo automatizado supera al actual
|
||||
- formatPayback(-100) === "Sin payback (costo aumenta)"
|
||||
// negativo grande: mismo comportamiento
|
||||
- formatPayback(NaN) === "No recupera en horizonte"
|
||||
// NaN cae en el primer guard
|
||||
- formatPayback(11.95) === "11.9 meses"
|
||||
// verificar que el borde con 12 no salta a "1.0 años" prematuramente
|
||||
- formatPayback(12.0) === "12.0 meses (~1.0 años)"
|
||||
// exactamente en el umbral
|
||||
|
||||
Total: 12 tests verdes en tests/lib/format-payback.test.ts
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════
|
||||
APLICACIÓN EN 3 LUGARES
|
||||
═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
a) RoiKpiBar.tsx (web app):
|
||||
|
||||
- Reemplazar el formato actual del KPI "PAYBACK" por formatPayback(months)
|
||||
- El KPI card debe mostrar el string completo
|
||||
- PROBLEMA POTENCIAL: strings largos como "15.0 meses (~1.3 años)"
|
||||
pueden desbordar la card. Soluciones:
|
||||
- Opción 1: reducir font-size del valor a 14px en el card de payback
|
||||
(solo en ese card, no en los otros KPIs)
|
||||
- Opción 2: usar 2 líneas — primera línea cifra principal, segunda
|
||||
línea texto secundario en font-size menor
|
||||
|
||||
PROBÁ ambas y mostrame screenshot de cada una en Etapa 3. Por ahora
|
||||
implementá la opción 1 (más simple, mantiene grilla consistente).
|
||||
|
||||
b) src/lib/export/pdf-sections-roi.ts → drawRoiKpiCards:
|
||||
|
||||
- Aplicar formatPayback en la KPI card del PDF
|
||||
- Verificar ancho: el card actual mide ~120px en página A4. String
|
||||
"15.0 meses (~1.3 años)" en font 16px puede no entrar.
|
||||
Si es así, reducir font-size de esa card específica a 14px.
|
||||
- Probar con paybacks de distintos rangos: 0.5, 6, 15, 30 meses
|
||||
para ver el comportamiento.
|
||||
|
||||
c) src/lib/export/csv-export.ts:
|
||||
|
||||
- En la línea de metadata "# Payback (meses): X.XX" reemplazar el
|
||||
número crudo por formatPayback formateado
|
||||
- AGREGAR una segunda línea inmediatamente después:
|
||||
"# Payback (meses, raw): X.XXXX"
|
||||
Esto preserva compatibilidad con integraciones futuras de BI/Excel
|
||||
que necesiten el valor numérico crudo.
|
||||
|
||||
d) BONUS — verificar TransparencySection:
|
||||
|
||||
El detector de ROI inusual (Etapa 3 de Sprint 1) menciona payback en
|
||||
alguno de sus 4 casos. Verificá si el texto incluye payback formateado
|
||||
o crudo. Si es crudo, aplicar formatPayback también ahí.
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════
|
||||
ENTREGABLES
|
||||
═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
[ ] etapa-1-audit.md actualizado con 3 correcciones (GatewayPanel,
|
||||
donut chart, contraste WCAG)
|
||||
[ ] formatPayback implementado en src/lib/format.ts
|
||||
[ ] 12 tests verdes en tests/lib/format-payback.test.ts
|
||||
[ ] formatPayback aplicado en RoiKpiBar.tsx
|
||||
[ ] formatPayback aplicado en pdf-sections-roi.ts (drawRoiKpiCards)
|
||||
[ ] formatPayback aplicado en csv-export.ts (con doble línea: formateado
|
||||
+ raw para compatibilidad)
|
||||
[ ] TransparencySection verificado y actualizado si aplica
|
||||
[ ] 443 tests previos + 12 nuevos = 455+ tests verdes
|
||||
[ ] npm run build limpio
|
||||
[ ] Screenshot del RoiKpiBar mostrando un payback de 10.6 meses
|
||||
(caso del PDF roi-realistic)
|
||||
[ ] PDF de muestra generado con setup realista del brief:
|
||||
- medium-with-gateways
|
||||
- 3 automatizables, $200/$20 costos
|
||||
- 5000 ejecuciones/año, 3 años, USD 80.000 inversión
|
||||
- Resultado esperado: payback ~10.6 meses (NO "0.0 meses")
|
||||
→ guardar como tests/e2e/__output__/payback-formatted-sample.pdf
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════
|
||||
REPORTE FINAL A PRESENTAR
|
||||
═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
1. Resultado del cálculo de contraste WCAG (PASS/FAIL para AA normal
|
||||
y AA large)
|
||||
2. Confirmación de las 3 actualizaciones del audit
|
||||
3. Tabla de tests con resultados (12 verdes esperados)
|
||||
4. Screenshot del RoiKpiBar con payback formateado
|
||||
5. PDF de muestra presentado con present_files para que el usuario
|
||||
abra en Preview y valide visualmente
|
||||
6. Cantidad final de tests verdes
|
||||
7. Cualquier desbordamiento de UI/PDF detectado y cómo se resolvió
|
||||
|
||||
NO arranques Etapa 3 sin mi OK explícito.
|
||||
|
||||
Go.
|
||||
@@ -21,10 +21,10 @@
|
||||
|
||||
| Archivo:línea | Clase actual | Categoría | Acción sugerida |
|
||||
|---|---|---|---|
|
||||
| `GatewayPanel.tsx:122` | `bg-blue-50 border-blue-200` | **Mantener azul funcional** | El azul en gateways AND paralelos es convención visual (flujo paralelo = azul). Evaluar en Sprint 2 si cambiar a slate-50 sin perder semántica. |
|
||||
| `GatewayPanel.tsx:124` | `text-blue-500` | **Mantener azul funcional** | Ídem — ícono Info en bloque paralelo |
|
||||
| `GatewayPanel.tsx:126-127` | `text-blue-800`, `text-blue-700` | **Mantener azul funcional** | Texto del bloque informativo de gateway AND |
|
||||
| `GatewayPanel.tsx:144` | `text-blue-600` | **Mantener azul funcional** | "100%" en gateway paralelo — valor fijo, semántica coherente |
|
||||
| `GatewayPanel.tsx:122` | `bg-blue-50 border-blue-200` | **Decisión arquitectónica: NO migrar** | Decisión arquitectónica: mantener azul informativo. No es color de marca, es semántica funcional (info banner, valor inmutable, patrón Bootstrap/Tailwind estándar). NO migrar. |
|
||||
| `GatewayPanel.tsx:124` | `text-blue-500` | **Decisión arquitectónica: NO migrar** | Decisión arquitectónica: mantener azul informativo. No es color de marca, es semántica funcional (info banner, valor inmutable, patrón Bootstrap/Tailwind estándar). NO migrar. |
|
||||
| `GatewayPanel.tsx:126-127` | `text-blue-800`, `text-blue-700` | **Decisión arquitectónica: NO migrar** | Decisión arquitectónica: mantener azul informativo. No es color de marca, es semántica funcional (info banner, valor inmutable, patrón Bootstrap/Tailwind estándar). NO migrar. |
|
||||
| `GatewayPanel.tsx:144` | `text-blue-600` | **Decisión arquitectónica: NO migrar** | Decisión arquitectónica: mantener azul informativo. No es color de marca, es semántica funcional (info banner, valor inmutable, patrón Bootstrap/Tailwind estándar). NO migrar. |
|
||||
| `ResourcesPanel.tsx:22` | `bg-blue-100 text-blue-700` (rol "role") | **Migrar a neutral** | Badge de tipo "role" no tiene razón de ser azul. Migrar a slate o usar color propio del sistema de recursos. Etapa 3. |
|
||||
|
||||
## Hallazgos — `PDF.blue` en exportación
|
||||
@@ -51,7 +51,7 @@
|
||||
| `HeatmapLegend.tsx:24` | `#10b981`, `#f59e0b`, `#ef4444` | Gradiente de la leyenda del heatmap | **Migrar a token** | Usar constantes exportadas de `colors.ts` o tokens `heatmap.*`. Etapa 3. |
|
||||
| `chart-options.ts:12` | `#3b82f6` (role) | Color del tipo de recurso "role" | **Migrar a neutral** | El azul para "roles" no es marca InQ. Evaluar paleta diferenciadora de tipos en Sprint 2. |
|
||||
| `chart-options.ts:12` | `#8b5cf6` (system) | Color del tipo "system" | **Mantener azul funcional** | Violeta para sistemas es una convención razonable. Evaluar en Sprint 2. |
|
||||
| `chart-options.ts:123` | `#3b82f6` | Costo directo en donut chart | **Migrar a InQ orange** | El donut de composición directo/overhead debería usar naranja InQ. Etapa 3. |
|
||||
| `chart-options.ts:123` | `#3b82f6` | Costo directo en donut chart | **Migrar a InQ orange** | Composición directo/overhead en donut. Decisión visual: el sector "directo" va en naranja InQ `#F59845`. El sector "overhead" NO debe ser otro naranja (pierde contraste) — usar slate-400 (`#94A3B8`) o naranja muy claro (`#FED7AA`). Decisión final en Etapa 3 mirando el render. |
|
||||
| `chart-options.ts:57,69,211,224` | `#f1f5f9`, `#e2e8f0` | Grid lines y ejes de charts | **Mantener neutral** | slate-100/200 son correctos para UI de soporte. |
|
||||
| `chart-options.ts:63,107,209` | `#475569` | Labels de ejes | **Mantener neutral** | slate-600, correcto. |
|
||||
| `chart-options.ts:78,217,237` | `#64748b`, `#94a3b8` | Labels secundarios | **Mantener neutral** | slate-500/400, correctos. |
|
||||
@@ -85,6 +85,32 @@
|
||||
| Etapa 3 (Web App) | `CumulativeSavingsChart.tsx`, `TopSavingsChart.tsx`, `HeatmapLegend.tsx`, `chart-options.ts` | Migrar hex a tokens CSS/constantes |
|
||||
| Etapa 4-8 (PDFs) | `pdf-sections.ts`, `pdf-export.ts`, `pdf-sections-roi.ts` | Reemplazar `PDF.blue` por array naranja InQ |
|
||||
| Etapa 9 (Badge ⚡) | `BpmnCanvas.tsx` | Rediseño completo del badge a naranja sólido |
|
||||
| Sprint 2 | `ResourcesPanel.tsx`, `GatewayPanel.tsx`, `chart-options.ts` | Evaluar paleta de tipos de recurso y gateways |
|
||||
| Sprint 2 | `ResourcesPanel.tsx`, `chart-options.ts` | Evaluar paleta de tipos de recurso |
|
||||
|
||||
**No se toca nada de lo anterior en Etapa 1.**
|
||||
|
||||
---
|
||||
|
||||
## Accesibilidad — Contraste de marca
|
||||
|
||||
**Color auditado**: `#F59845` (InQ orange) sobre fondo blanco `#FFFFFF`
|
||||
**Resultado (calculado 2026-05-16)**:
|
||||
|
||||
```
|
||||
Contrast ratio: 2.22 :1
|
||||
AA normal text (4.5): FAIL
|
||||
AA large text (3.0): FAIL
|
||||
```
|
||||
|
||||
**Veredicto**: el naranja InQ no alcanza el mínimo WCAG AA en ninguna categoría de texto.
|
||||
|
||||
**Opciones de remediación**:
|
||||
|
||||
| Opción | Descripción | Trade-off |
|
||||
|---|---|---|
|
||||
| A | Aceptar contraste sub-WCAG en CTAs | Riesgo accesibilidad — usuarios con baja visión |
|
||||
| B | Oscurecer naranja a `#D97706` en CTAs | Rompe marca parcialmente — perceptible en UI |
|
||||
| C | Usar texto oscuro `#451A03` en CTAs sobre naranja | Cambia el look del botón visiblemente |
|
||||
| **D** | **Forzar `font-weight: 700` en CTAs primarios** | **Cumple AA large text con texto grande/bold** |
|
||||
|
||||
**Propuesta para Etapa 3**: implementar opción D — `font-weight: 700` en todos los CTAs primarios y KPI cards. NO modificar componentes en esta etapa.
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TrendingDown, Calendar, BarChart3, DollarSign, Percent } from 'lucide-react'
|
||||
import { formatCurrency, formatPercent } from '@/lib/format'
|
||||
import { formatCurrency, formatPayback, formatPercent } from '@/lib/format'
|
||||
import type { RoiResult } from '@/domain/roi'
|
||||
|
||||
interface RoiKpiBarProps {
|
||||
@@ -9,7 +9,7 @@ interface RoiKpiBarProps {
|
||||
}
|
||||
|
||||
function RoiKpiCard({
|
||||
label, value, sub, icon, iconColor, valueColor,
|
||||
label, value, sub, icon, iconColor, valueColor, compact,
|
||||
}: {
|
||||
label: string
|
||||
value: string
|
||||
@@ -17,6 +17,7 @@ function RoiKpiCard({
|
||||
icon: React.ReactNode
|
||||
iconColor: string
|
||||
valueColor?: string
|
||||
compact?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div data-testid="roi-kpi-card" className="bg-white rounded-xl border border-slate-200 shadow-sm p-5 flex items-start gap-4">
|
||||
@@ -25,7 +26,7 @@ function RoiKpiCard({
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs font-medium text-slate-500 uppercase tracking-wide mb-1">{label}</p>
|
||||
<p className={`text-2xl font-bold font-mono leading-tight ${valueColor ?? 'text-slate-900'}`}>{value}</p>
|
||||
<p className={`${compact ? 'text-sm' : 'text-2xl'} font-bold font-mono leading-tight ${valueColor ?? 'text-slate-900'}`}>{value}</p>
|
||||
{sub && <p className="text-xs text-slate-400 mt-0.5 leading-snug">{sub}</p>}
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,13 +34,7 @@ function RoiKpiCard({
|
||||
}
|
||||
|
||||
export function RoiKpiBar({ roi, currency, horizonYears }: RoiKpiBarProps) {
|
||||
const paybackStr = !isFinite(roi.paybackMonths)
|
||||
? 'No se recupera'
|
||||
: roi.paybackMonths === 0
|
||||
? 'Inmediato'
|
||||
: roi.paybackMonths < 24
|
||||
? `${roi.paybackMonths.toFixed(1)} meses`
|
||||
: `${roi.paybackYears.toFixed(1)} años`
|
||||
const paybackStr = formatPayback(roi.paybackMonths)
|
||||
|
||||
const roiStr = !isFinite(roi.roiAnnualPercent)
|
||||
? '∞%'
|
||||
@@ -88,6 +83,7 @@ export function RoiKpiBar({ roi, currency, horizonYears }: RoiKpiBarProps) {
|
||||
icon={<Calendar className="h-5 w-5 text-primary" />}
|
||||
iconColor="bg-primary/10"
|
||||
valueColor={roi.breaksEvenInHorizon ? 'text-emerald-700' : 'text-slate-900'}
|
||||
compact
|
||||
/>
|
||||
<RoiKpiCard
|
||||
label={`Acumulado (${horizonYears} años)`}
|
||||
|
||||
@@ -73,7 +73,7 @@ export function TransparencySection({ activities, investment, roi }: Transparenc
|
||||
<span className="font-medium">
|
||||
ROI inusualmente alto detectado: {formatPercent(roi.roiAnnualPercent)}.
|
||||
</span>{' '}
|
||||
Un ROI superior al {formatPercent(ROI_HIGH_THRESHOLD)} anual suele indicar uno de estos casos:
|
||||
Un ROI superior al {formatPercent(ROI_HIGH_THRESHOLD, 0)} anual suele indicar uno de estos casos:
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-0.5 ml-1">
|
||||
<li>Frecuencia anual del proceso sobreestimada</li>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Papa from 'papaparse'
|
||||
import { formatSimulationTimestamp } from '@/lib/format'
|
||||
import { formatPayback, formatSimulationTimestamp } from '@/lib/format'
|
||||
import { calculateRoi } from '@/domain/roi'
|
||||
import { buildFileName } from './slug'
|
||||
import type { Process, Simulation, Resource, Activity, ActivitySimResult } from '@/domain/types'
|
||||
@@ -109,12 +109,6 @@ function generateRoiCsvContent(
|
||||
automationInvestment: process.automationInvestment,
|
||||
})
|
||||
|
||||
const paybackStr = !Number.isFinite(roi.paybackMonths)
|
||||
? 'N/A'
|
||||
: roi.paybackMonths === 0
|
||||
? '0'
|
||||
: formatDecimalForCsv(roi.paybackMonths, currency)
|
||||
|
||||
const roiStr = !Number.isFinite(roi.roiAnnualPercent)
|
||||
? 'Infinito'
|
||||
: formatDecimalForCsv(roi.roiAnnualPercent, currency)
|
||||
@@ -128,7 +122,8 @@ function generateRoiCsvContent(
|
||||
`# Costo total automatizado (${currency}): ${formatDecimalForCsv(resultAutomated.totalCost, currency)}`,
|
||||
`# Ahorro por ejecución (${currency}): ${formatDecimalForCsv(roi.savingsPerExecution, currency)}`,
|
||||
`# Ahorro anual (${currency}): ${formatDecimalForCsv(roi.annualSavings, currency)}`,
|
||||
`# Payback (meses): ${paybackStr}`,
|
||||
`# Payback (meses): ${formatPayback(roi.paybackMonths)}`,
|
||||
`# Payback (meses, raw): ${Number.isFinite(roi.paybackMonths) ? roi.paybackMonths.toFixed(4) : roi.paybackMonths}`,
|
||||
`# Ahorro acumulado a ${process.analysisHorizonYears} años (${currency}): ${formatDecimalForCsv(roi.cumulativeSavingsHorizon, currency)}`,
|
||||
`# ROI anualizado (%): ${roiStr}`,
|
||||
`# Frecuencia anual: ${process.annualFrequency}`,
|
||||
|
||||
@@ -65,7 +65,7 @@ async function exportScenarioPdf(
|
||||
title: `Análisis de costos - ${process.name}`,
|
||||
author: process.clientName || '',
|
||||
subject: 'Simulación de proceso BPMN',
|
||||
creator: 'Process Cost Platform',
|
||||
creator: 'InQ ROI',
|
||||
keywords: 'bpmn, costos, simulación',
|
||||
})
|
||||
|
||||
@@ -124,7 +124,7 @@ async function exportScenarioPdf(
|
||||
|
||||
docAny.addPage()
|
||||
drawMethodologySection(docAny, process, PDF.margin)
|
||||
addPageNumbers(docAny, 'Process Cost Platform', simulation.executedAt)
|
||||
addPageNumbers(docAny, 'InQ ROI · Powered by InQuality', simulation.executedAt)
|
||||
}
|
||||
|
||||
// ─── ROI — 4 páginas ──────────────────────────────────────────────────────────
|
||||
@@ -153,7 +153,7 @@ async function exportRoiPdf(
|
||||
title: `Análisis de ROI - ${process.name}`,
|
||||
author: process.clientName || '',
|
||||
subject: 'Análisis de retorno de inversión para automatización BPMN',
|
||||
creator: 'Process Cost Platform',
|
||||
creator: 'InQ ROI',
|
||||
keywords: 'bpmn, roi, automatización, payback',
|
||||
})
|
||||
|
||||
@@ -175,7 +175,7 @@ async function exportRoiPdf(
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.setFontSize(8)
|
||||
doc.setTextColor(...PDF.slate500)
|
||||
doc.text('★ Actividades en negrita están marcadas como automatizables', PDF.margin, y + 3)
|
||||
doc.text('(*) Actividades en negrita están marcadas como automatizables', PDF.margin, y + 3)
|
||||
y += 8
|
||||
|
||||
const tableConfig = buildComparisonTableConfig(
|
||||
@@ -196,7 +196,7 @@ async function exportRoiPdf(
|
||||
doc.addPage()
|
||||
drawRoiMethodologySection(doc, process, PDF.margin)
|
||||
|
||||
addPageNumbers(doc, 'Process Cost Platform · ROI', simulation.executedAt)
|
||||
addPageNumbers(doc, 'InQ ROI · Powered by InQuality', simulation.executedAt)
|
||||
}
|
||||
|
||||
function formatNum(n: number): string {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatCurrency, formatPercent } from '@/lib/format'
|
||||
import { formatCurrency, formatPayback, formatPercent } from '@/lib/format'
|
||||
import type { ActivitySimResult, Process, Activity } from '@/domain/types'
|
||||
import type { RoiResult } from '@/domain/roi'
|
||||
import { PDF, type DocLike } from './pdf-sections'
|
||||
@@ -78,13 +78,7 @@ export function drawRoiKpiCards(
|
||||
const cardW = 52, cardH = 28, gapX = 7, gapY = 5
|
||||
const col1 = PDF.margin, col2 = PDF.margin + cardW + gapX, col3 = PDF.margin + (cardW + gapX) * 2
|
||||
|
||||
const paybackStr = !Number.isFinite(roi.paybackMonths)
|
||||
? 'No se recupera'
|
||||
: roi.paybackMonths === 0
|
||||
? 'Inmediato'
|
||||
: roi.paybackMonths < 24
|
||||
? `${roi.paybackMonths.toFixed(1)} meses`
|
||||
: `${roi.paybackYears.toFixed(1)} años`
|
||||
const paybackStr = formatPayback(roi.paybackMonths)
|
||||
|
||||
const roiStr = !Number.isFinite(roi.roiAnnualPercent)
|
||||
? '∞'
|
||||
@@ -94,7 +88,7 @@ export function drawRoiKpiCards(
|
||||
// Fila 1: 3 cards
|
||||
{ label: 'AHORRO POR EJECUCIÓN', value: formatCurrency(roi.savingsPerExecution, currency), sub: `${formatPercent(roi.savingsPerExecutionPercent)} vs. actual`, positive: roi.savingsPerExecution >= 0 },
|
||||
{ label: 'AHORRO ANUAL', value: formatCurrency(roi.annualSavings, currency), sub: `× ${horizonYears} años`, positive: roi.annualSavings >= 0 },
|
||||
{ label: 'PAYBACK', value: paybackStr, sub: roi.breaksEvenInHorizon ? '✓ Dentro del horizonte' : 'Fuera del horizonte', positive: roi.breaksEvenInHorizon },
|
||||
{ label: 'PAYBACK', value: paybackStr, sub: roi.breaksEvenInHorizon ? 'Dentro del horizonte' : 'Fuera del horizonte', positive: roi.breaksEvenInHorizon },
|
||||
// Fila 2: 2 cards + 1 vacía
|
||||
{ label: `ACUMULADO (${horizonYears} AÑOS)`, value: formatCurrency(roi.cumulativeSavingsHorizon, currency), sub: 'neto de inversión', positive: roi.cumulativeSavingsHorizon >= 0 },
|
||||
{ label: 'ROI ANUAL', value: roiStr, sub: 'retorno sobre la inversión', positive: roi.roiAnnualPercent >= 0 },
|
||||
@@ -127,7 +121,7 @@ export function drawRoiKpiCards(
|
||||
// Valor
|
||||
const valueColor = kpi.positive ? [16, 185, 129] as [number,number,number] : [239, 68, 68] as [number,number,number]
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.setFontSize(kpi.value.length > 14 ? 9 : 11)
|
||||
doc.setFontSize(kpi.value.length > 22 ? 7 : kpi.value.length > 14 ? 9 : 11)
|
||||
doc.setTextColor(...valueColor)
|
||||
doc.text(kpi.value, x + 3, y + 16)
|
||||
|
||||
@@ -356,7 +350,7 @@ export function drawTransparencyBlock(
|
||||
if (roiHigh) {
|
||||
blocks.push({
|
||||
text: doc.splitTextToSize(
|
||||
`ROI inusualmente alto detectado: ${formatPercent(roi.roiAnnualPercent)}. Un ROI > ${formatPercent(ROI_HIGH_THRESHOLD)} anual suele indicar frecuencia sobreestimada, costo automatizado subestimado o inversión insuficiente. Revisá antes de presentar al cliente.`,
|
||||
`ROI inusualmente alto detectado: ${formatPercent(roi.roiAnnualPercent)}. Un ROI > ${formatPercent(ROI_HIGH_THRESHOLD, 0)} anual suele indicar frecuencia sobreestimada, costo automatizado subestimado o inversión insuficiente. Revisá antes de presentar al cliente.`,
|
||||
boxW - 8
|
||||
),
|
||||
fontSize: 8,
|
||||
|
||||
@@ -25,6 +25,32 @@ export function formatMinutes(minutes: number): string {
|
||||
return m > 0 ? `${h}h ${m}min` : `${h}h`
|
||||
}
|
||||
|
||||
export function formatPayback(months: number): string {
|
||||
if (Number.isNaN(months) || !isFinite(months)) {
|
||||
return 'No recupera en horizonte'
|
||||
}
|
||||
if (months < 0) {
|
||||
return 'Sin payback (costo aumenta)'
|
||||
}
|
||||
if (months === 0) {
|
||||
return 'Inmediato'
|
||||
}
|
||||
if (months < 0.1) {
|
||||
return '< 1 día'
|
||||
}
|
||||
if (months < 1) {
|
||||
return `< 1 mes (${months.toFixed(2)} meses)`
|
||||
}
|
||||
if (months < 12) {
|
||||
return `${months.toFixed(1)} meses`
|
||||
}
|
||||
if (months < 24) {
|
||||
const years = (months / 12).toFixed(1)
|
||||
return `${months.toFixed(1)} meses (~${years} años)`
|
||||
}
|
||||
return `${(months / 12).toFixed(1)} años`
|
||||
}
|
||||
|
||||
// Formato de timestamp de simulación: "13 de mayo de 2026 a las 09:45"
|
||||
// Locale explícito es-PY — nunca depende del locale del browser del cliente.
|
||||
export function formatSimulationTimestamp(ts: number): { date: string; time: string } {
|
||||
|
||||
174
tests/e2e/payback-etapa-2.spec.ts
Normal file
174
tests/e2e/payback-etapa-2.spec.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Etapa 2 Sprint 1.5 — Validación visual de formatPayback.
|
||||
*
|
||||
* 1. Screenshot del RoiKpiBar mostrando payback formateado (no "0.0 meses").
|
||||
* 2. Screenshot de TransparencySection con ROI > 1000%.
|
||||
* 3. PDF correcto del tab "Comparación + ROI" guardado para validación visual.
|
||||
*
|
||||
* Setup: medium-with-gateways, task_recv + task_score + task_analisis automatizables,
|
||||
* $200 costo directo / $20 costo automatizado, 5000 ejecuciones/año, $80.000 inversión.
|
||||
*
|
||||
* Root cause del PDF erróneo en run anterior: el test exportaba antes de navegar al
|
||||
* tab ROI, por lo que activeTab = 'actual' y se generaba el PDF del escenario actual.
|
||||
* Fix: navegar al tab "Comparación + ROI" ANTES de exportar.
|
||||
*/
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { resolve } from 'path'
|
||||
import { mkdirSync } from 'fs'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
||||
const BPMN_DIR = resolve(__dirname, '../../public/sample-processes')
|
||||
const OUT_DIR = resolve(__dirname, './__output__')
|
||||
|
||||
test.beforeAll(() => { mkdirSync(OUT_DIR, { recursive: true }) })
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async function importBpmn(page: import('@playwright/test').Page, filename: string) {
|
||||
await page.goto('/')
|
||||
await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, filename))
|
||||
await page.waitForURL(/\/workspace\//, { timeout: 15_000 })
|
||||
await page.waitForSelector('button:has-text("Simular")', { timeout: 10_000 })
|
||||
await page.waitForTimeout(1_000)
|
||||
}
|
||||
|
||||
async function setActivityCosts(
|
||||
page: import('@playwright/test').Page,
|
||||
elementId: string,
|
||||
directCost: number,
|
||||
automatedCost: number,
|
||||
automatedTime: number
|
||||
) {
|
||||
await page.locator(`[data-element-id="${elementId}"]`).first().click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
const toggle = page.getByRole('switch', { name: /automatizable/i })
|
||||
if (await toggle.getAttribute('aria-checked') === 'false') {
|
||||
await toggle.click()
|
||||
await page.waitForTimeout(300)
|
||||
}
|
||||
|
||||
const directCostInput = page.getByLabel(/Costo directo fijo/i)
|
||||
await directCostInput.fill(String(directCost))
|
||||
|
||||
const autoCostInput = page.getByLabel(/Costo automatizado/i)
|
||||
await autoCostInput.fill(String(automatedCost))
|
||||
|
||||
const autoTimeInput = page.getByLabel(/Tiempo automatizado/i)
|
||||
await autoTimeInput.fill(String(automatedTime))
|
||||
|
||||
const guardar = page.getByRole('button', { name: /Guardar/i })
|
||||
await guardar.click()
|
||||
await page.waitForTimeout(500)
|
||||
}
|
||||
|
||||
async function configureGlobal(
|
||||
page: import('@playwright/test').Page,
|
||||
annualFrequency: number,
|
||||
investment: number
|
||||
) {
|
||||
const globalTab = page.getByRole('tab', { name: /Global/i })
|
||||
await globalTab.click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
const freqInput = page.getByLabel(/Frecuencia anual/i)
|
||||
await freqInput.fill(String(annualFrequency))
|
||||
|
||||
const investInput = page.getByLabel(/Inversión en automatización/i)
|
||||
await investInput.fill(String(investment))
|
||||
|
||||
const guardar = page.getByRole('button', { name: /Guardar/i })
|
||||
await guardar.click()
|
||||
await page.waitForTimeout(500)
|
||||
}
|
||||
|
||||
async function setupRealisticScenario(page: import('@playwright/test').Page) {
|
||||
await importBpmn(page, 'medium-with-gateways.bpmn')
|
||||
await setActivityCosts(page, 'task_recv', 200, 20, 10)
|
||||
await setActivityCosts(page, 'task_score', 200, 20, 10)
|
||||
await setActivityCosts(page, 'task_analisis', 200, 20, 10)
|
||||
await configureGlobal(page, 5000, 80000)
|
||||
|
||||
const simBtn = page.getByRole('button', { name: 'Simular' })
|
||||
await expect(simBtn).not.toBeDisabled({ timeout: 5_000 })
|
||||
await simBtn.click()
|
||||
await page.waitForURL(/\/report\//, { timeout: 20_000 })
|
||||
await page.waitForSelector('button:has-text("Exportar PDF")', { timeout: 15_000 })
|
||||
await page.waitForTimeout(800)
|
||||
|
||||
// Navegar al tab Comparación + ROI — NECESARIO para que activeTab sea 'roi'
|
||||
const roiTab = page.getByRole('tab', { name: /Comparación/i })
|
||||
await expect(roiTab).not.toBeDisabled({ timeout: 5_000 })
|
||||
await roiTab.click()
|
||||
await page.waitForSelector('[data-testid="roi-kpi-card"]', { timeout: 10_000 })
|
||||
await page.waitForTimeout(800)
|
||||
}
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
test('screenshot RoiKpiBar — payback formateado (no "0.0 meses")', async ({ page }) => {
|
||||
await setupRealisticScenario(page)
|
||||
|
||||
// Screenshot del viewport — muestra KPI bar
|
||||
await page.screenshot({
|
||||
path: resolve(OUT_DIR, 'payback-roi-kpi-bar.png'),
|
||||
fullPage: false,
|
||||
})
|
||||
|
||||
// Verificar que el payback card NO muestra "0.0 meses"
|
||||
const cards = page.locator('[data-testid="roi-kpi-card"]')
|
||||
const texts = await cards.allTextContents()
|
||||
console.log('KPI card texts:', JSON.stringify(texts, null, 2))
|
||||
const hasOldFormat = texts.some(t => /\b0\.0 meses\b/.test(t))
|
||||
expect(hasOldFormat, 'Payback card must not show "0.0 meses" — use formatPayback').toBe(false)
|
||||
})
|
||||
|
||||
test('screenshot TransparencySection — ROI > 1000% dispara aviso', async ({ page }) => {
|
||||
await setupRealisticScenario(page)
|
||||
|
||||
// Verificar que ROI es > 1000% (condición para disparar Transparencia)
|
||||
const cards = page.locator('[data-testid="roi-kpi-card"]')
|
||||
const texts = await cards.allTextContents()
|
||||
const roiText = texts.find(t => t.toLowerCase().includes('roi anual')) ?? ''
|
||||
console.log('ROI card text:', roiText)
|
||||
|
||||
// Scroll hasta el final de la página para revelar TransparencySection
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Verificar que la sección existe en el DOM
|
||||
const transparency = page.locator('text=Transparencia metodológica').first()
|
||||
await expect(transparency).toBeVisible({ timeout: 5_000 })
|
||||
|
||||
// Screenshot full-page para capturar la sección
|
||||
await page.screenshot({
|
||||
path: resolve(OUT_DIR, 'payback-transparency-section.png'),
|
||||
fullPage: true,
|
||||
})
|
||||
|
||||
console.log('TransparencySection visible con ROI:', roiText)
|
||||
})
|
||||
|
||||
test('PDF ROI correcto — tab "Comparación + ROI" activo al exportar', async ({ page }) => {
|
||||
// Root cause del PDF erróneo anterior: exportar con activeTab = 'actual'.
|
||||
// Fix: setupRealisticScenario ya navega al tab ROI ANTES de este punto.
|
||||
await setupRealisticScenario(page)
|
||||
|
||||
// Tab ROI ya activo — el BPMN canvas no existe en este tab, no esperar por él
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent('download', { timeout: 30_000 }),
|
||||
page.getByRole('button', { name: 'Exportar PDF' }).click(),
|
||||
])
|
||||
|
||||
const outPath = resolve(OUT_DIR, 'etapa-2-payback-roi-sample.pdf')
|
||||
await download.saveAs(outPath)
|
||||
console.log('PDF ROI guardado:', outPath)
|
||||
|
||||
expect(download.suggestedFilename()).toMatch(/_roi\.pdf$|\.pdf$/)
|
||||
|
||||
// El nombre debe contener "roi" si la implementación usa activeTab en el slug
|
||||
console.log('Nombre sugerido:', download.suggestedFilename())
|
||||
})
|
||||
@@ -147,9 +147,9 @@ describe('RoiKpiBar', () => {
|
||||
expect(text).toMatch(/810/)
|
||||
})
|
||||
|
||||
it('payback Infinity → muestra "No se recupera"', () => {
|
||||
it('payback Infinity → muestra "No recupera en horizonte"', () => {
|
||||
render(<RoiKpiBar roi={roiNoSavings} currency="USD" horizonYears={3} />)
|
||||
expect(screen.getByText(/No se recupera/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/No recupera en horizonte/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('payback 0 (inversión cero) → muestra "Inmediato"', () => {
|
||||
|
||||
@@ -1,21 +1,67 @@
|
||||
/**
|
||||
* Tests de formatPayback — Sprint 1.5 Etapa 2.
|
||||
*
|
||||
* Stubs preparados. La implementación se agrega en Etapa 2.
|
||||
* Los 7 casos están definidos en SPRINT_1_5_BRIEF_REDISENIO_VISUAL.md § 3.4.
|
||||
*
|
||||
* 7 casos base + 5 edge cases = 12 tests.
|
||||
* Regla: un payback de "0.0 meses" (como mostraba el sistema con costos
|
||||
* artificialmente bajos) es confuso. formatPayback resuelve esto con
|
||||
* una escala legible para el consultor y el cliente.
|
||||
*/
|
||||
import { describe, it } from 'vitest'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { formatPayback } from '../../src/lib/format'
|
||||
|
||||
describe('formatPayback', () => {
|
||||
it.todo('0 → "Inmediato" (inversión cero con ahorro positivo)')
|
||||
it.todo('0.05 → "< 1 día" (payback de horas, prácticamente instantáneo)')
|
||||
it.todo('0.5 → "< 1 mes (0.50 meses)"')
|
||||
it.todo('6 → "6.0 meses"')
|
||||
it.todo('15 → "15.0 meses (~1.3 años)"')
|
||||
it.todo('30 → "2.5 años"')
|
||||
it.todo('Infinity → "No recupera en horizonte"')
|
||||
// Casos base
|
||||
it('0 → "Inmediato"', () => {
|
||||
expect(formatPayback(0)).toBe('Inmediato')
|
||||
})
|
||||
|
||||
it('0.05 → "< 1 día"', () => {
|
||||
expect(formatPayback(0.05)).toBe('< 1 día')
|
||||
})
|
||||
|
||||
it('0.5 → "< 1 mes (0.50 meses)"', () => {
|
||||
expect(formatPayback(0.5)).toBe('< 1 mes (0.50 meses)')
|
||||
})
|
||||
|
||||
it('6 → "6.0 meses"', () => {
|
||||
expect(formatPayback(6)).toBe('6.0 meses')
|
||||
})
|
||||
|
||||
it('15 → "15.0 meses (~1.3 años)"', () => {
|
||||
expect(formatPayback(15)).toBe('15.0 meses (~1.3 años)')
|
||||
})
|
||||
|
||||
it('30 → "2.5 años"', () => {
|
||||
expect(formatPayback(30)).toBe('2.5 años')
|
||||
})
|
||||
|
||||
it('Infinity → "No recupera en horizonte"', () => {
|
||||
expect(formatPayback(Infinity)).toBe('No recupera en horizonte')
|
||||
})
|
||||
|
||||
// Edge cases
|
||||
it('-1 → "Sin payback (costo aumenta)"', () => {
|
||||
// payback negativo: costo automatizado supera al actual
|
||||
expect(formatPayback(-1)).toBe('Sin payback (costo aumenta)')
|
||||
})
|
||||
|
||||
it('-100 → "Sin payback (costo aumenta)"', () => {
|
||||
// negativo grande: mismo comportamiento
|
||||
expect(formatPayback(-100)).toBe('Sin payback (costo aumenta)')
|
||||
})
|
||||
|
||||
it('NaN → "No recupera en horizonte"', () => {
|
||||
// NaN cae en el primer guard
|
||||
expect(formatPayback(NaN)).toBe('No recupera en horizonte')
|
||||
})
|
||||
|
||||
it('11.95 → "11.9 meses"', () => {
|
||||
// verificar que el borde con 12 no salta a "1.0 años" prematuramente
|
||||
expect(formatPayback(11.95)).toBe('11.9 meses')
|
||||
})
|
||||
|
||||
it('12.0 → "12.0 meses (~1.0 años)"', () => {
|
||||
// exactamente en el umbral
|
||||
expect(formatPayback(12.0)).toBe('12.0 meses (~1.0 años)')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user