diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a77a502..35fab7e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -5,11 +5,12 @@ | Atributo | Valor | |---|---| -| Versión del documento | 0.1.0 | +| Versión del documento | 0.2.0 | | Fecha | Mayo 2026 | -| Estado | Draft — Fase 0 (MVP "Noche Cero") en construcción | +| Estado | Fase 0 completada · Sprint 1 (ROI) en construcción | | Autor | Equipo fundador + Claude (arquitectura) | -| Alcance | MVP de esta noche + Norte V1.0 (12–18 meses) | +| Alcance | MVP completado + Sprint 1 + Norte V1.0 (12–18 meses) | +| Deploy actual | https://process-cost-platform.pages.dev (v0.1.0-mvp) | --- @@ -18,14 +19,15 @@ 1. [Contexto y Propuesta de Valor](#1-contexto-y-propuesta-de-valor) 2. [Glosario y Convenciones](#2-glosario-y-convenciones) 3. [Norte Estratégico — Visión V1.0](#3-norte-estratégico--visión-v10) -4. [Fase 0 — MVP "Noche Cero"](#4-fase-0--mvp-noche-cero) -5. [Roadmap de Evolución](#5-roadmap-de-evolución) -6. [Arquitectura Técnica](#6-arquitectura-técnica) -7. [Modelo de Dominio](#7-modelo-de-dominio) -8. [Decisiones Arquitectónicas (ADRs)](#8-decisiones-arquitectónicas-adrs) -9. [Atributos de Calidad](#9-atributos-de-calidad) -10. [Riesgos y Mitigaciones](#10-riesgos-y-mitigaciones) -11. [Anexos](#11-anexos) +4. [Fase 0 — MVP "Noche Cero" (COMPLETADO)](#4-fase-0--mvp-noche-cero) +5. [Fase 1 — Sprint 1: ROI y Escenarios de Automatización](#5-fase-1--sprint-1-roi-y-escenarios-de-automatización) +6. [Roadmap de Evolución](#6-roadmap-de-evolución) +7. [Arquitectura Técnica](#7-arquitectura-técnica) +8. [Modelo de Dominio](#8-modelo-de-dominio) +9. [Decisiones Arquitectónicas (ADRs)](#9-decisiones-arquitectónicas-adrs) +10. [Atributos de Calidad](#10-atributos-de-calidad) +11. [Riesgos y Mitigaciones](#11-riesgos-y-mitigaciones) +12. [Anexos](#12-anexos) --- @@ -55,12 +57,20 @@ Las consultoras, gerentes de operaciones y profesionales de eficiencia operativa ### 1.4 Audiencia objetivo -**MVP (Fase 0)**: el propio fundador como consultor de automatización de procesos. +**MVP (Fase 0 - COMPLETADO)**: el propio fundador como consultor de automatización de procesos. + +**Sprint 1 (en curso)**: el **equipo de InQuality** como herramienta interna de consultoría y **pre-venta de proyectos de automatización con cálculo de ROI**. **V1.0**: consultores independientes y boutique de procesos, gerentes de operaciones de pymes LATAM, equipos de Project/Process Office. **V2+**: estudiantes y entusiastas de eficiencia operativa (tier educativo gratuito). +### 1.4.1 Casos de uso confirmados (Sprint 1) + +1. **Análisis de costos operativos**: el consultor de InQuality releva el proceso del cliente, lo modela en BPMN, configura costos/tiempos/recursos y entrega un reporte cuantitativo del costo operativo. + +2. **Pre-venta de automatización con ROI**: el consultor identifica actividades automatizables, define costos automatizados estimados y el costo de inversión del proyecto. El sistema calcula payback, ahorro anual y ROI %, generando un reporte ejecutivo que sirve como propuesta comercial cuantificada. + ### 1.5 Lo que NO somos - ❌ NO somos un BPMS (motor de ejecución de procesos). @@ -164,9 +174,11 @@ Las consultoras, gerentes de operaciones y profesionales de eficiencia operativa --- -## 4. Fase 0 — MVP "Noche Cero" +## 4. Fase 0 — MVP "Noche Cero" ✅ COMPLETADA -### 4.1 Hipótesis a validar +> **Estado**: ✅ Entregada el 13 de mayo de 2026. Tag `v0.1.0-mvp`. Deployada en https://process-cost-platform.pages.dev. **268 tests unit/integration + 5 E2E + 4/4 validación con gradiente real**. Bundle inicial 59.75 KB gzip. 0 vulnerabilidades. + +### 4.1 Hipótesis validada > Un consultor de procesos puede transformar un archivo `.bpmn` en un reporte visualmente deslumbrante de costos operativos en menos de 15 minutos, y usarlo al día siguiente en una reunión real con un cliente. @@ -264,43 +276,193 @@ Las consultoras, gerentes de operaciones y profesionales de eficiencia operativa --- -## 5. Roadmap de Evolución +## 5. Fase 1 — Sprint 1: ROI y Escenarios de Automatización + +> **Estado**: en construcción. +> **Audiencia primaria**: equipo de InQuality (consultora de procesos). +> **Caso de uso estrella**: pre-venta de proyectos de automatización con ROI cuantificado. + +### 5.1 Hipótesis a validar + +> Un consultor de InQuality puede modelar un proceso, identificar las actividades automatizables, configurar costos del escenario actual vs automatizado, y obtener en minutos un reporte ejecutivo de ROI (payback, ahorro anual, ROI %) que sirve como propuesta comercial cuantificada para vender un proyecto de automatización. + +### 5.2 Alcance del Sprint 1 + +| # | Capacidad | Detalle | +|---|---|---| +| S1.1 | **Volumetría del proceso** | Frecuencia anual de ejecución + horizonte de análisis (años) configurables por proceso | +| S1.2 | **Marcado de actividades automatizables** | Toggle por actividad + campos de costo y tiempo automatizados | +| S1.3 | **Inversión en automatización** | Campo único agregado a nivel proceso (sin desglose) | +| S1.4 | **Motor dual de simulación** | Mismo motor recibe `scenario: 'actual' \| 'automated'` y aplica los campos correspondientes | +| S1.5 | **Sección de ROI en reporte** | Tabs en el reporte: Actual / Automatizado / Comparación + ROI | +| S1.6 | **5 KPIs de ROI** | Ahorro por ejecución · Ahorro anual · Payback (meses) · Ahorro acumulado a N años · ROI % anualizado | +| S1.7 | **Tabla comparativa** | Actividad por actividad: costos actual vs automatizado, ahorro $ y % | +| S1.8 | **Gráficos de ROI** | Ahorro acumulado en el tiempo + Top 5 actividades con mayor ahorro | +| S1.9 | **Mecanismo de transparencia** | Detecta valores cero/inversión cero y los expone en sección visible pero discreta | +| S1.10 | **Migración Dexie v1 → v2** | Preserva datos existentes, agrega los nuevos campos con defaults | +| S1.11 | **Export PDF/CSV ampliado** | El PDF incluye páginas de ROI; el CSV de ROI exporta comparación completa | +| S1.12 | **Documento metodológico** | Anexo en el repo: cómo calcular correctamente el costo automatizado por ejecución | + +### 5.3 Decisiones arquitectónicas del Sprint 1 + +#### Modelo de escenarios — Modelo A (dos campos por actividad) + +`Activity` gana 3 campos: +- `automatable: boolean` (default false) +- `automatedCostFixed: number` (default 0) +- `automatedTimeMinutes: number` (default 0) + +`Process` gana 3 campos: +- `annualFrequency: number` (default 1000) +- `analysisHorizonYears: number` (default 3) +- `automationInvestment: number` (default 0) + +**Justificación de Modelo A**: +- Trazabilidad actividad-por-actividad sin sincronización entre diagramas +- Un solo BPMN simplifica el flujo del consultor +- Migración a Modelo B (escenarios como entidad) queda pendiente para Sprint 3-4 si InQuality lo demanda + +#### Volumetría — Multiplicador en el reporte + +El motor sigue siendo determinístico ("costo de una ejecución"). La volumetría se aplica en el reporte: +``` +costoAnual = costoUnitario × annualFrequency +costoHorizonte = costoAnual × analysisHorizonYears +``` + +No se introduce VPN, TIR ni Monte Carlo en este sprint. **Razón estratégica**: el cliente promedio de InQuality entiende payback nominal; el VPN abre discusiones sobre la tasa que distraen. + +#### Fórmulas de ROI (en `domain/roi.ts`, función pura testeable) + +```typescript +savingsPerExecution = costPerExecutionActual - costPerExecutionAutomated +savingsPerExecutionPercent = (savingsPerExecution / costPerExecutionActual) * 100 +annualSavings = savingsPerExecution × annualFrequency +paybackMonths = (automationInvestment / annualSavings) × 12 +paybackYears = paybackMonths / 12 +cumulativeSavingsHorizon = (annualSavings × horizonYears) - automationInvestment +breaksEvenInHorizon = paybackYears <= horizonYears +roiAnnualPercent = (annualSavings / automationInvestment) * 100 +``` + +**Edge cases manejados explícitamente**: +- `annualSavings <= 0`: `paybackMonths = Infinity`, `breaksEvenInHorizon = false` +- `automationInvestment === 0 && annualSavings > 0`: `paybackMonths = 0`, `roiAnnualPercent = Infinity` (display "N/A" o "∞") +- `costPerExecutionActual === 0`: `savingsPerExecutionPercent = 0` (evitar div/0) + +#### Transparencia metodológica + +Si el reporte detecta: +- Actividades marcadas como automatizables pero con `automatedCostFixed = 0` Y `automatedTimeMinutes = 0` +- `automationInvestment = 0` con payback < 1 mes +- Otros valores sospechosos + +Muestra una sección **"Transparencia metodológica"** con: +- Lista de valores no informados +- Texto introductorio explicando el motivo del listado +- Fondo gris claro, no rojo/alarmante +- Aparece en pantalla y en PDF + +**Filosofía**: convierte una posible debilidad en señal de seriedad profesional. El cliente final entiende que el consultor fue transparente sobre los supuestos. + +### 5.4 Lo que NO entra en el Sprint 1 + +❌ Múltiples escenarios (más de 2) — viene en Sprint 3-4 si se demanda +❌ Escenarios como entidad separada (Modelo B) +❌ VPN, TIR, tasa de descuento +❌ Desglose de la inversión (licencias, dev, infra) — un único campo por ahora +❌ Análisis de sensibilidad +❌ Distribuciones probabilísticas +❌ Bots como recursos con capacidad finita +❌ Multi-tenant, login, colaboración, versionado, IA, modo oscuro, i18n, PWA + +### 5.5 Definition of Done — Sprint 1 + +- [ ] Migración Dexie v1 → v2 con preservación de datos existentes +- [ ] Workspace permite marcar actividades como automatizables y configurar costos automatizados +- [ ] Workspace permite configurar volumetría e inversión a nivel proceso +- [ ] Motor de simulación acepta `scenario` y devuelve resultados correctos para ambos casos +- [ ] Reporte tiene 3 tabs (Actual / Automatizado / Comparación + ROI) +- [ ] Sección "Comparación + ROI" muestra 5 KPIs grandes correctamente formateados +- [ ] Tabla comparativa funcional con ordenamiento +- [ ] Gráfico de ahorro acumulado con marcador de payback +- [ ] Gráfico Top 5 actividades con mayor ahorro +- [ ] Sección "Transparencia metodológica" aparece cuando corresponde +- [ ] Cálculos de `domain/roi.ts` cubiertos por tests (≥ 10 casos incluyendo edge cases) +- [ ] Test E2E del flujo completo de ROI con Playwright +- [ ] PDF de ROI incluye las páginas adicionales con KPIs y gráficos +- [ ] CSV de ROI exporta comparación completa +- [ ] Documento `METHODOLOGY_AUTOMATED_COST.md` en raíz del repo +- [ ] Bundle no crece más de 20 KB gzip respecto al MVP +- [ ] Todos los tests previos (268 unit + 5 E2E) siguen verdes +- [ ] Deploy a producción exitoso +- [ ] Smoke test post-deploy verde + +--- + +## 6. Roadmap de Evolución ``` -┌────────────┬──────────────────────────────────────────────────────┐ -│ FASE │ CAPACIDADES │ -├────────────┼──────────────────────────────────────────────────────┤ -│ Fase 0 │ MVP client-side: importar, configurar, simular, │ -│ (1 noche) │ reportear, exportar PDF/CSV │ -├────────────┼──────────────────────────────────────────────────────┤ -│ Fase 1 │ + Backend mínimo + Auth + Persistencia en la nube │ -│ (1-2 sem) │ + i18n (es/en/pt-BR) + PWA installable │ -├────────────┼──────────────────────────────────────────────────────┤ -│ Fase 2 │ + Editor BPMN (no solo lectura) + Validador │ -│ (1 mes) │ + Glosario integrado + Recursos avanzados │ -├────────────┼──────────────────────────────────────────────────────┤ -│ Fase 3 │ + Estructura organizacional + Multi-tenant │ -│ (1 mes) │ + RBAC + Versionado lineal │ -├────────────┼──────────────────────────────────────────────────────┤ -│ Fase 4 │ + Colaboración en tiempo real (Yjs/CRDT) │ -│ (1-2 mes) │ + Presencia + Comentarios │ -├────────────┼──────────────────────────────────────────────────────┤ -│ Fase 5 │ + Distribuciones probabilísticas + Riesgos │ -│ (1 mes) │ + Costeo ABC completo + Gobernanza RACI │ -├────────────┼──────────────────────────────────────────────────────┤ -│ Fase 6 │ + IA: detección de cuellos de botella │ -│ (1-2 mes) │ + Elementos RPA/Agente IA en paleta │ -├────────────┼──────────────────────────────────────────────────────┤ -│ Fase 7 │ + Marketplace comunitario de plantillas │ -│ (continuo) │ + Tutoriales interactivos + Modo educativo │ -└────────────┴──────────────────────────────────────────────────────┘ +┌──────────────┬──────────────────────────────────────────────────────┐ +│ FASE │ CAPACIDADES │ +├──────────────┼──────────────────────────────────────────────────────┤ +│ Fase 0 ✅ │ MVP client-side: importar, configurar, simular, │ +│ (completa) │ reportear, exportar PDF/CSV. v0.1.0-mvp en prod. │ +├──────────────┼──────────────────────────────────────────────────────┤ +│ Sprint 1 🔄 │ ROI + escenarios de automatización: │ +│ (en curso) │ · Volumetría del proceso │ +│ │ · Marcado de actividades automatizables │ +│ │ · Inversión de automatización │ +│ │ · Motor dual: actual vs automatizado │ +│ │ · 5 KPIs de ROI + tabla comparativa + gráficos │ +│ │ · Transparencia metodológica │ +│ │ · Migración Dexie v1 → v2 │ +├──────────────┼──────────────────────────────────────────────────────┤ +│ Sprint 2 │ Productividad de carga: │ +│ │ · Grilla de edición masiva (TanStack Table) │ +│ │ · Pegado desde Excel │ +│ │ · Recursos con costo/hora + asignación múltiple │ +│ │ · Cálculo de costo esperado en vivo │ +│ │ · Edición inline de nombre proceso/cliente │ +├──────────────┼──────────────────────────────────────────────────────┤ +│ Sprint 3-4 │ Multi-escenario y reportes ejecutivos: │ +│ │ · Escenarios como entidad (modelo B) si demandado │ +│ │ · Reporte ejecutivo (PDF 1-2 páginas) separado │ +│ │ · Branding InQuality │ +│ │ · Export Excel multi-hoja │ +│ │ · Comparación de simulaciones (historial) │ +├──────────────┼──────────────────────────────────────────────────────┤ +│ Fase 2 │ Backend + colaboración básica: │ +│ (1-2 meses) │ · Backend NestJS + Postgres + Auth │ +│ │ · Multi-tenant + RBAC │ +│ │ · Versionado lineal │ +│ │ · Editor BPMN (no solo lectura) │ +│ │ · Validador + Glosario │ +│ │ · i18n (es/en/pt-BR) │ +│ │ · PWA installable │ +├──────────────┼──────────────────────────────────────────────────────┤ +│ Fase 3 │ Colaboración avanzada y modelado fino: │ +│ (1-2 meses) │ · Colaboración en tiempo real (Yjs/CRDT) │ +│ │ · Presencia + Comentarios │ +│ │ · Distribuciones probabilísticas │ +│ │ · Riesgos │ +│ │ · Costeo ABC completo │ +│ │ · Gobernanza RACI │ +├──────────────┼──────────────────────────────────────────────────────┤ +│ Fase 4 │ Inteligencia y comunidad: │ +│ (1-2 meses) │ · IA: detección de cuellos de botella │ +│ │ · Elementos RPA/Agente IA en paleta │ +│ │ · Marketplace comunitario de plantillas │ +│ │ · Tutoriales interactivos │ +│ │ · Modo educativo │ +└──────────────┴──────────────────────────────────────────────────────┘ ``` --- -## 6. Arquitectura Técnica +## 7. Arquitectura Técnica -### 6.1 Vista de alto nivel — MVP (Fase 0) +### 7.1 Vista de alto nivel — MVP (Fase 0) ``` ┌─────────────────────────────────────────────────────────────┐ @@ -340,7 +502,7 @@ Las consultoras, gerentes de operaciones y profesionales de eficiencia operativa (estático, gratis, global) ``` -### 6.2 Vista de alto nivel — V1.0 +### 7.2 Vista de alto nivel — V1.0 ``` ┌──────────────────┐ ┌──────────────────────────────┐ @@ -381,7 +543,7 @@ Las consultoras, gerentes de operaciones y profesionales de eficiencia operativa vía Docker Compose ``` -### 6.3 Stack tecnológico +### 7.3 Stack tecnológico #### 6.3.1 Stack MVP (Fase 0) @@ -423,7 +585,7 @@ Las consultoras, gerentes de operaciones y profesionales de eficiencia operativa | i18n | i18next + react-i18next | Estándar de facto | | E2E testing | Playwright | Estándar moderno | -### 6.4 Arquitectura de carpetas (MVP) +### 7.4 Arquitectura de carpetas (MVP) ``` process-cost-platform/ @@ -496,7 +658,7 @@ process-cost-platform/ └── e2e/ ← post-MVP ``` -### 6.5 Modelo conceptual del motor de simulación (MVP) +### 7.5 Modelo conceptual del motor de simulación (MVP + Sprint 1) El motor de simulación del MVP es **determinístico y agregado**. NO es discrete event simulation. Su contrato: @@ -556,7 +718,7 @@ interface SimulationResult { 5. Costo indirecto = `costoDirectoTotal × overheadPercentage`. 6. Agregar totales y rankings. -### 6.6 Integración bpmn-js + heatmap +### 7.6 Integración bpmn-js + heatmap `bpmn-js` expone: - Un `viewer` (read-only) ideal para MVP. @@ -571,7 +733,7 @@ El heatmap se implementa así: 3. Registrar handler `element.click` para drill-down. 4. Mostrar tooltip con detalle al hover. -### 6.7 Deployment del MVP +### 7.7 Deployment **Esta noche**: 1. `npm run build` → genera `dist/` estático. @@ -582,9 +744,9 @@ El heatmap se implementa así: --- -## 7. Modelo de Dominio +## 8. Modelo de Dominio -### 7.1 Entidades núcleo (MVP) +### 8.1 Entidades núcleo (estado al Sprint 1) **Distinción fundamental de capas:** - `ProcessGraph` (en memoria, reconstruido del XML cada vez): TODOS los nodos del diagrama incluidos gateways, eventos, flujos. Vive solo en el parser/motor, no se persiste. @@ -592,6 +754,8 @@ El heatmap se implementa así: - `GatewayConfig` (persistido en IndexedDB): probabilidades por flujo saliente de gateways divergentes (XOR/OR). Los gateways convergentes (merge) no tienen config — el motor suma probabilidades entrantes. - El heatmap **solo colorea nodos con Activity**. Los gateways conservan estilo BPMN neutro. +**Campos añadidos en Sprint 1** marcados con 🆕. + ``` ┌─────────────────────────────────────────────────────────────┐ │ Process │ @@ -602,6 +766,9 @@ El heatmap se implementa así: │ + bpmnXml: string (raw XML) │ │ + currency: string (ISO 4217) │ │ + overheadPercentage: number (0..1) │ +│ + annualFrequency: number 🆕 default 1000 │ +│ + analysisHorizonYears: number 🆕 default 3 │ +│ + automationInvestment: number 🆕 default 0 │ │ + createdAt, updatedAt │ └─────────────────────────────────────────────────────────────┘ │ 1 │ 1 @@ -622,8 +789,13 @@ El heatmap se implementa así: │ number │ │ probability: number │ │ + executionTimeMinutes: │ │ }> │ │ number │ │ (paralelo: todas 1.0, │ -└─────────────────────────┘ │ exclusivo: suma = 1.0) │ - │ N └──────────────────────────────┘ +│ + automatable: boolean │🆕 │ exclusivo: suma = 1.0) │ +│ + automatedCostFixed: │🆕 └──────────────────────────────┘ +│ number │ +│ + automatedTimeMinutes: │🆕 +│ number │ +└─────────────────────────┘ + │ N │ │ M ┌─────────────────────────────────────────────────────────────┐ @@ -657,13 +829,13 @@ El heatmap se implementa así: └─────────────────────────────────────────────────────────────┘ ``` -### 7.2 Modelo extendido (V1.0) +### 8.2 Modelo extendido (V1.0) Añade: `Tenant`, `User`, `Role`, `OrganizationUnit` (jerárquica recursiva), `ProcessOwnership` (RACI), `Risk`, `RiskProcessLink`, `ProcessVersion`, `Comment`, `Notification`, `Template`. --- -## 8. Decisiones Arquitectónicas (ADRs) +## 9. Decisiones Arquitectónicas (ADRs) ### ADR-001 — MVP client-side sin backend - **Contexto**: tiempo de entrega = una noche, presupuesto cero. @@ -716,11 +888,48 @@ Añade: `Tenant`, `User`, `Role`, `OrganizationUnit` (jerárquica recursiva), `P - **Decisión**: Cloudflare Pages mientras sea estático; migración a VPS al introducir backend. - **Justificación**: $0 al inicio, latencia global, sin operación. Migrar cuando haya backend. +### ADR-012 — Escenarios como dos pares de campos por actividad (Modelo A), no entidad separada +- **Contexto Sprint 1**: necesidad de modelar costo "actual" vs "automatizado" por actividad. +- **Decisión**: Modelo A — agregar `automatable`, `automatedCostFixed`, `automatedTimeMinutes` a `Activity`. El motor recibe un parámetro `scenario` y elige qué campos usar. +- **Alternativa descartada**: Modelo B (entidad `Scenario` con N configuraciones independientes) — más complejo, no necesario hasta que haya demanda real de >2 escenarios. +- **Justificación**: + - Trazabilidad actividad-a-actividad para la tabla comparativa (pieza más persuasiva del reporte). + - Un solo diagrama → consultor no mantiene 2 BPMNs sincronizados. + - Migración A→B es directa cuando llegue el momento. +- **Cuándo revisar**: Sprint 3-4 si InQuality necesita >2 escenarios o flujos distintos post-automatización. + +### ADR-013 — Volumetría aplicada en el reporte, no en el motor +- **Contexto Sprint 1**: necesidad de anualizar costos para calcular ROI. +- **Decisión**: el motor sigue siendo determinístico ("costo de una ejecución"). La volumetría (frecuencia anual + horizonte) es metadata del proceso y se aplica como multiplicador en la capa de reporte. +- **Alternativa descartada**: correr Monte Carlo o múltiples instancias en el motor. +- **Justificación**: + - El motor es determinístico → correrlo N veces da el mismo resultado × N (perder ciclos). + - Mantiene separación de concerns: motor calcula costo unitario, reporte hace análisis temporal. + - Cuando lleguen distribuciones probabilísticas (Fase 3), ahí tiene sentido Monte Carlo. + +### ADR-014 — ROI nominal sin VPN ni tasa de descuento en Sprint 1 +- **Contexto Sprint 1**: cálculo de payback y ahorro acumulado para pre-venta. +- **Decisión**: usar fórmulas nominales simples (payback = inversión / ahorro anual × 12, acumulado = ahorro × años - inversión). NO incluir VPN, TIR ni tasa de descuento. +- **Justificación**: + - El cliente promedio de InQuality entiende payback nominal. + - VPN abre discusiones sobre la tasa que distraen del mensaje principal. + - Si en el futuro un cliente sofisticado lo pide, se agrega como opción. +- **Cuándo revisar**: cuando un cliente concreto pida VPN/TIR. + +### ADR-015 — Transparencia metodológica como feature, no bug +- **Contexto Sprint 1**: riesgo de que consultores carguen valores incompletos (costos automatizados en 0, inversión en 0) generando ROIs irreales. +- **Decisión**: detectar estos casos y exponerlos en sección visible pero discreta del reporte ("Transparencia metodológica"). +- **Justificación**: + - Anticipar limitaciones es señal de seriedad profesional. + - El cliente final confía más en consultores transparentes sobre supuestos que en cifras "perfectas". + - Protege la credibilidad de InQuality y la herramienta. +- **Mecanismo**: banner amarillo informativo (no rojo de error) + lista de valores no informados. + --- -## 9. Atributos de Calidad +## 10. Atributos de Calidad -### 9.1 Performance +### 10.1 Performance | Métrica | MVP | V1.0 | |---|---|---| @@ -730,7 +939,7 @@ Añade: `Tenant`, `User`, `Role`, `OrganizationUnit` (jerárquica recursiva), `P | Render mapa de calor | < 200ms | < 200ms | | Export PDF | < 5s | < 5s | -### 9.2 Mantenibilidad +### 10.2 Mantenibilidad - **TypeScript strict** activado. - **ESLint + Prettier** configurados. @@ -738,7 +947,7 @@ Añade: `Tenant`, `User`, `Role`, `OrganizationUnit` (jerárquica recursiva), `P - **Dominio puro separado** de UI (en `/src/domain`) — testeable sin React. - **Sin lógica de negocio en componentes** — siempre en hooks o domain. -### 9.3 Seguridad (V1.0) +### 10.3 Seguridad (V1.0) - TLS obligatorio (Cloudflare en MVP, Caddy en VPS). - Auth con tokens cortos + refresh. @@ -747,7 +956,7 @@ Añade: `Tenant`, `User`, `Role`, `OrganizationUnit` (jerárquica recursiva), `P - CSP estricto en producción. - Backups cifrados (V1.0). -### 9.4 Accesibilidad +### 10.4 Accesibilidad - WCAG 2.1 AA como objetivo V1.0. - Contraste mínimo 4.5:1 en texto. @@ -756,7 +965,7 @@ Añade: `Tenant`, `User`, `Role`, `OrganizationUnit` (jerárquica recursiva), `P --- -## 10. Riesgos y Mitigaciones +## 11. Riesgos y Mitigaciones | # | Riesgo | Probabilidad | Impacto | Mitigación | |---|---|---|---|---| @@ -772,9 +981,9 @@ Añade: `Tenant`, `User`, `Role`, `OrganizationUnit` (jerárquica recursiva), `P --- -## 11. Anexos +## 12. Anexos -### 11.1 Comandos de arranque para Claude Code (MVP) +### 12.1 Comandos de arranque para Claude Code (MVP) ```bash # Inicialización @@ -798,11 +1007,11 @@ npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom npm install -D eslint prettier ``` -### 11.2 BPMN XML de prueba sugerido +### 12.2 BPMN XML de prueba sugerido Para testing inicial, usar el BPMN público de ejemplo de bpmn.io: https://github.com/bpmn-io/bpmn-js-examples -### 11.3 Recursos de referencia +### 12.3 Recursos de referencia - **bpmn-js docs**: https://bpmn.io/toolkit/bpmn-js/ - **BPMN 2.0 spec**: https://www.omg.org/spec/BPMN/2.0/ @@ -811,7 +1020,7 @@ Para testing inicial, usar el BPMN público de ejemplo de bpmn.io: https://githu - **Zustand**: https://zustand-demo.pmnd.rs/ - **Dexie.js**: https://dexie.org/ -### 11.4 Definition of Done — MVP "Noche Cero" +### 12.4 Definition of Done — MVP "Noche Cero" - [ ] El usuario puede importar un archivo `.bpmn` por drag & drop - [ ] El diagrama se renderiza correctamente en el canvas diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8b1bcc3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,262 @@ +# Changelog + +Historial de cambios del proyecto. Formato basado en [Keep a Changelog](https://keepachangelog.com/es/). +Los cambios se describen desde la perspectiva del código: qué archivos cambiaron, +por qué, y qué decisiones de diseño están implícitas — para que futuras +modificaciones profundas tengan contexto suficiente. + +--- + +## [Unreleased] — Sprint 1: ROI y Escenarios de Automatización + +### Estado +En construcción. Etapas 1–4 completas; Etapas 5–6 pendientes. + +### Etapa 1 — Cimientos del dominio (completada) + +**Nuevos campos en el modelo de dominio** (`src/domain/types.ts`): + +- `Activity` gana: `automatable: boolean`, `automatedCostFixed: number`, `automatedTimeMinutes: number` +- `Process` gana: `annualFrequency: number`, `analysisHorizonYears: number`, `automationInvestment: number` +- Nuevo tipo `Scenario = 'actual' | 'automated'` +- `SimulationInput` acepta `scenario?: Scenario` (opcional, default `'actual'` para retrocompatibilidad) +- `Simulation` gana `resultAutomated?: SimulationResult` (opcional, para retrocompatibilidad con snapshots viejos) + +**Migración Dexie v1 → v2** (`src/persistence/db.ts`): + +- `version(2).upgrade()` popula defaults en registros existentes: + - Activities: `automatable=false`, `automatedCostFixed=0`, `automatedTimeMinutes=0` + - Processes: `annualFrequency=1000`, `analysisHorizonYears=3`, `automationInvestment=0` +- Sin cambio de índices — solo nuevos campos con defaults seguros. + +**Nuevo módulo de ROI** (`src/domain/roi.ts`): + +- Función pura `calculateRoi(input: RoiInput): RoiResult` +- Fórmulas nominales simples (sin VPN ni TIR — decisión de diseño ADR-014) +- Edge cases explicitados: inversión cero, ahorro negativo, costo actual cero (evitar div/0) +- `paybackMonths = Infinity` cuando no hay ahorro, `roiAnnualPercent = Infinity` cuando inversión=0 y ahorro>0 + +**Motor de simulación dual** (`src/domain/simulation.ts`): + +- Acepta `scenario` en `SimulationInput` +- En `'automated'`: actividades con `automatable=true` usan `automatedCostFixed`/`automatedTimeMinutes` +- Las no-automatable mantienen sus costos originales en ambos escenarios +- La probabilidad de ejecución NO cambia entre escenarios — solo costos y tiempos + +**Simulation store refactorizado** (`src/store/simulation-store.ts`): + +- `result` (single) → `resultActual + resultAutomated + lastSimulatedAt` +- Nueva acción `persistResults(processId, actual, automated)`: guarda en Dexie + actualiza memoria en un solo paso atómico +- Nueva acción `invalidateResults()`: pone ambos a null +- Suscripción reactiva a `process-store`: cualquier cambio en `activities` o `currentProcess` invalida ambos resultados automáticamente +- **Dependencia intencional documentada**: `simulation-store` importa `process-store` en una dirección (sin inversa → sin ciclo) + +**useSimulate refactorizado** (`src/features/simulation/useSimulate.ts`): + +- Corre las dos simulaciones en el mismo ciclo y llama `persistResults` del store (eliminando el import directo de `simulationRepo` desde un hook de feature) +- Retorna `{ resultActual, resultAutomated }` en lugar de un único result + +**Infraestructura ESLint** (`eslint.config.js` — archivo nuevo): + +- Primer config de ESLint del proyecto (antes no existía) +- Regla `no-restricted-imports`: prohíbe importar `persistence/repositories` desde fuera de `store/` + - Overrides para `store/**`, `features/import/**`, `features/report/useReportData.ts`, `tests/**` +- Regla `react-hooks/set-state-in-effect: 'off'`: patrón de sync local state con useEffect es válido en este codebase +- **Por qué importa**: si alguien llama `activityRepo.save()` directamente desde una feature sin pasar por el store, los resultados de simulación quedan stale (la suscripción reactiva no dispara) + +**Tests nuevos** (32 tests): + +- `tests/domain/roi.test.ts`: 16 casos cubriendo happy path, inversión cero, ahorro negativo, NaN +- `tests/integration/dexie-migration.test.ts`: 6 casos de persistencia de los nuevos campos con valores default y no-default +- `tests/store/simulation-store-invalidation.test.ts`: 7 casos de la invariante de invalidación dual +- `tests/integration/sprint1-flow.test.ts`: 11 casos del flujo integrado BPMN → sim actual → sim automated → calculateRoi → KPIs +- `tests/domain/simulation.test.ts`: +4 casos de `scenario=automated` con costos en cero, sin NaN, probabilidad invariante +- `tests/integration/bpmn-collaboration.test.ts`: 3 casos para BPMNs de colaboración con múltiples pools + +**Archivos de test actualizados** (fixtures con nuevos campos): + +- `tests/integration/simulation-engine.test.ts` +- `tests/integration/gateway-validation.test.ts` +- `tests/integration/indexeddb-persistence.test.ts` +- `tests/features/report/components.test.tsx` +- `tests/features/report/derivations.test.ts` +- `tests/lib/export/measure-sizes.test.ts` +- `tests/lib/export/csv-export.test.ts` +- `tests/lib/export/pdf-export.test.ts` +- `tests/setup.ts`: mocks globales `ResizeObserver` y `PointerEvent` para jsdom (necesario desde Sprint 1 para tests con Slider de Radix) + +### Etapa 2 — UI de Configuración (completada) + +**Switch component** (`src/components/ui/switch.tsx` — nuevo): + +- Implementación Tailwind pura sin `@radix-ui/react-switch` +- `role="switch"` + `aria-checked` + `aria-label` para accesibilidad +- Transiciones CSS: `transition-colors duration-200` en el fondo, `transition-transform duration-200` en el knob +- **Decisión**: no instalar `@radix-ui/react-switch` para no agregar dependencia — el comportamiento visual es idéntico + +**ActivityPanel ampliado** (`src/features/workspace/ActivityPanel.tsx`): + +- Nueva sección "Automatización" al pie del panel, separada con `` +- Toggle Switch (`automatable`) oculta/muestra los campos con `transition-all duration-200 overflow-hidden max-h-{0|80}` + - **Limitación conocida**: el max-height trick no anima a la altura real del contenido. Ver TODO.md para mejora con `useRef + scrollHeight` +- Inputs numéricos `automatedCostFixed` y `automatedTimeMinutes` con mismo patrón que los existentes (placeholder cuando valor=0) +- Helper ámbar de transparencia metodológica: + - Aparece cuando `automatable=true && cost===0 && time===0` + - **Siempre está en el DOM** (no conditional render) — la visibilidad se controla con `opacity-0/100 + max-h-0/20 + transition-all duration-150` + - `aria-hidden` refleja el estado de visibilidad + - **Por qué importa**: con conditional render (`&&`) el helper desaparece instantáneamente al tipear — sin transición. Al mantenerlo en el DOM, el fade de 150ms se aplica correctamente + +**GlobalSettingsPanel ampliado** (`src/features/workspace/GlobalSettingsPanel.tsx`): + +- Nueva sección "Análisis de Impacto" al pie del panel +- `annualFrequency`: input numérico, min=1, sugerencias de referencia en texto helper +- `analysisHorizonYears`: Slider de 1 a 10 años con valor en texto ("3 años") +- `automationInvestment`: input numérico, default placeholder vacío cuando valor=0 +- La validación de rango de `analysisHorizonYears` (clamp 1-10) ocurre en `handleSave`, no en `onChange` + +**BpmnCanvas ampliado** (`src/features/workspace/BpmnCanvas.tsx`): + +- Nueva prop `automatableElementIds?: string[]` +- `syncAutomationOverlays()`: callback memoizado con `useCallback([automatableElementIds])` + - Limpia overlays anteriores con `overlays.remove({ type: 'automation-indicator' })` + - Re-agrega un badge por cada elementId en la lista + - Se llama después de `importXML` (en el `.then()`) y en un `useEffect` separado cuando cambia la lista +- **Posicionamiento del badge**: `{ top: -8, left: element.width - 12 }` + - Obtenido via `elementRegistry.get(elementId)` con fallback `width ?? 100` + - **No usar** `position.right` de diagram-js: con `right: -8`, la fórmula interna `left = right * -1 + width` da `left = 8 + width`, colocando el badge enteramente fuera del nodo sin overlap de esquina + - Con `left = width - 12`: badge left está 12px antes del borde derecho, su borde derecho queda 8px fuera → overlap visible en la esquina +- **Badge HTML**: círculo 20×20px, fondo `#fffbeb` (amber-50), borde `#e2e8f0`, sombra `0 1px 3px rgba(0,0,0,0.12)`, ícono Bot Lucide en `#f59e0b` como SVG inline + - `pointer-events: none` para no interferir con clicks en el nodo + - `title` nativo del browser como tooltip + - **Por qué `#fffbeb` y no `#ffffff`**: el fondo de los nodos BPMN también es blanco, el fondo ámbar le da identidad cromática propia y hace visible el círculo por contraste + +**WorkspacePage** (`src/features/workspace/WorkspacePage.tsx`): + +- Destructura `activities` del process-store +- Calcula `automatableElementIds` con `useMemo` +- Pasa `useDeferredValue(automatableElementIds)` al BpmnCanvas (evita cascada de re-renders al editar el panel) + +**AutomationScenarioNote** (`src/features/report/AutomationScenarioNote.tsx` — nuevo): + +- Componente de leyenda para el tab "Automatizado" del reporte +- Preparado y listo para ser colocado por Etapa 3 — no tiene lógica propia + +### Etapa 3 — Motor dual y reporte con tabs (completada) + +**ReportPage ampliada** (`src/features/report/ReportPage.tsx`): + +- La página ahora tiene 3 tabs: **Actual** | **Automatizado** | **Comparación + ROI** +- El contenido existente (KpiBar, heatmap, 3 gráficos, tabla de actividades) pasa intacto al tab "Actual" +- Los tabs "Automatizado" y "ROI" se deshabilitan si la simulación no tiene `resultAutomated` (snapshot viejo) — en ese caso se muestra `NeedsResimulate` que guía al usuario a re-simular +- `useMemo` para `automatableIds` y `roi` se declaran antes de los early returns (cumple reglas de hooks) +- Los refs de heatmap son independientes por tab (`heatmapRef` / `heatmapRefAuto`) — el tab "Actual" sigue siendo el fuente de imagen para el export PDF +- Export PDF/CSV sin cambios — Etapa 4 los ampliará + +**Tab "Automatizado"**: + +- Mismo layout que "Actual": KpiBar → heatmap con cross-linking → 3 gráficos → tabla +- Usa `simulation.resultAutomated` como fuente de datos +- `AutomationScenarioNote` aparece al pie del heatmap (componente preparado en Etapa 2) +- Heatmap y tabla tienen su propio estado `selectedIdAuto` — cross-linking funciona independientemente del tab "Actual" + +**Tab "Comparación + ROI"**: + +- `RoiKpiBar` (`src/features/report/RoiKpiBar.tsx` — nuevo): 5 KPI cards en grid 2×5 (lg) + - Ahorro por ejecución (con % sobre costo actual) + - Ahorro anual + - Payback (meses o años según magnitud; "Inmediato" si inversión=0; "No se recupera" si Infinity) + - Ahorro acumulado neto a N años + - ROI anual % ("∞%" si inversión=0 y ahorro>0) + - Los valores positivos se muestran en `text-emerald-700`, negativos en `text-red-600` +- `ComparisonTable` (`src/features/report/ComparisonTable.tsx` — nuevo): + - Tabla actividad por actividad: costo actual | costo automatizado | ahorro | % ahorro + - Ícono Bot ámbar en la primera columna para actividades marcadas `automatable` + - Ordenable por ahorro (↕) — default descendente + - Usa `Set` para el lookup de `automatable` en O(1) +- `CumulativeSavingsChart` (`src/features/report/CumulativeSavingsChart.tsx` — nuevo): + - Gráfico de línea + área (ECharts): X=años 0..N, Y=ahorro neto acumulado + - Línea de break-even en y=0 siempre visible + - Marker vertical dashed verde en `x=paybackYears` (solo si finite y dentro del horizonte) + - Tooltip muestra valor del año exacto con color según signo +- `TopSavingsChart` (`src/features/report/TopSavingsChart.tsx` — nuevo): + - Top 5 actividades por ahorro positivo — barras horizontales verdes + - Excluye actividades con ahorro ≤ 0 + - Altura dinámica: `max(180, n * 48)` px — no hay espacio vacío si hay menos de 5 actividades +- `TransparencySection` (`src/features/report/TransparencySection.tsx` — nuevo): + - Condicional — solo se renderiza si hay issues metodológicos detectados + - **Issue 1**: actividades con `automatable=true` pero `automatedCostFixed=0` y `automatedTimeMinutes=0` → el ahorro puede estar inflado. Lista los nombres de cada actividad + - **Issue 2**: `automationInvestment=0` → payback "inmediato" e ROI "infinito" no reflejan la realidad + - Fondo amber-50, borde amber-200 — consistente con el helper del ActivityPanel + +**Por qué ROI inline y no en `useReportData`**: +El `calculateRoi` toma `simulation.result.totalCost` y `simulation.resultAutomated.totalCost` — valores ya disponibles en el componente. Moverlo al hook agregaría un `useEffect` extra sin beneficio. Se mantiene en el componente como `useMemo`. + +**Tests nuevos** (17 tests): + +- `tests/features/workspace/ActivityPanel.test.tsx`: 6 casos — toggle, helper ámbar con `aria-hidden`, dispatch de `updateActivity` +- `tests/features/workspace/GlobalSettingsPanel.test.tsx`: 7 casos — defaults, edición de campos, dispatch de `updateProcess` +- `tests/features/workspace/BpmnCanvas.test.tsx`: 4 casos — overlay add/remove, posición `left=width-12`, HTML con `#fffbeb` y `#f59e0b` + +**Patrón de mocks para tests de componentes** (documentado para referencia futura): + +- Los mocks del store con `vi.hoisted()` devuelven referencias estables +- Sin `vi.hoisted`: cada llamada a `useProcessStore()` devuelve un nuevo objeto → el `useEffect([activities])` de ActivityPanel dispara en loop infinito → "Maximum update depth exceeded" +- `simulation-store.ts` importa `process-store.ts` al nivel de módulo: los mocks deben ser compatibles o el store no carga + +--- + +## [0.1.0-mvp] — MVP Fase 0 — 2026-05-13 + +### Contexto + +Primera entrega funcional. Construida en una sesión de trabajo. +Objetivo: demostrar en ≤15 minutos que se puede transformar un BPMN en un reporte de costos. + +### Funcionalidades + +- Importar BPMN 2.0 por drag & drop o desde procesos de ejemplo +- Renderizar diagrama en canvas read-only (bpmn-js Viewer) +- Configurar actividades: costo fijo, tiempo, recursos asignados con utilización +- CRUD de recursos (rol/persona/sistema/equipo/insumo + costo/hora) +- Configuración global: moneda, overhead %, nombre proceso, cliente +- Motor de simulación determinístico con propagación de probabilidades por gateways XOR, AND, inclusivo +- Detección de loops (back-edges via DFS): se advierte pero no bloquea +- Reporte visual: mapa de calor en dos modos (relativo/absoluto), KPIs, top actividades, composición, costo por recurso +- Export PDF: 3 páginas (header+KPIs+heatmap, análisis, tabla detalle), con jsPDF-autotable +- Export CSV: con BOM UTF-8 y metadatos del proceso +- Persistencia en IndexedDB (Dexie v1) +- Deploy estático en Cloudflare Pages + +### Decisiones de diseño relevantes para el futuro + +**Parser BPMN multi-pool** (`bpmn-parser.ts`): + +- Itera todos los `` hijos directos de `` (no solo el primero) +- Fix crítico: el parser original usaba `querySelector` que retorna solo el primero → todos los pools de una colaboración excepto el primero eran ignorados +- Pools de soporte (SAP, bandeja manual) pueden no tener StartEvent propio — la validación exige al menos uno entre todos los pools + +**Heatmap sobre bpmn-js** (`HeatmapCanvas.tsx`): + +- bpmn-js rechaza la promesa de `importXML` pero el diagrama se renderiza parcialmente → usar `eventBus.on('import.done', ...)` en lugar de `.then()` +- `element.style.fill = color` (NO `setAttribute('fill', color)`): bpmn-js aplica CSS que tiene mayor especificidad que atributos SVG +- `captureImage()` espera 500ms después de importar para que la transición CSS `fill 0.35s ease` complete antes de la captura con html2canvas + +**Validación XOR en WorkspacePage** (`WorkspacePage.tsx`): + +- `hasLoadedOnce` ref: evita que el redirect "proceso no encontrado" dispare en el render inicial donde `isLoading=false` y `currentProcess=null` (antes del primer efecto de carga) + +### Tests + +268 tests unitarios + integración al cierre de MVP. +5 tests E2E con Playwright (import → simulate → export PDF/CSV × 3 BPMNs + acentos + gradiente de color). +El test de gradiente verifica con `sharp` que el heatmap del PDF contiene píxeles verdes, ámbar y rojos — evita regresiones en la lógica de coloreo. + +--- + +## Convenciones de este changelog + +- **Decisiones de diseño**: se documenta el *por qué*, no solo el *qué* +- **Gotchas**: comportamientos contrarios a la intuición se marcan explícitamente +- **Edge cases**: si un edge case motivó un test específico, se menciona +- **Retrocompatibilidad**: cuando un campo es opcional por retrocompat, se indica +- Las entradas son para humanos que modificarán el código, no para usuarios finales diff --git a/CLAUDE.md b/CLAUDE.md index 85c4f2a..08b1d10 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,11 +5,54 @@ --- +## ⚠️ Reglas de Marca — NO NEGOCIABLES (Sprint 1.5+) + +Estas reglas aplican a TODO string que aparezca en código, PDFs, CSVs, tests y documentación. + +### 1. Ortografía de "InQ" — SIEMPRE I-n-Q + +``` +✅ InQ ← correcto +✅ InQuality ← correcto (nombre completo) +✅ InQ ROI ← nombre del producto + +❌ INQ ← bug +❌ Inq ← bug +❌ inq ← bug +❌ inQ ← bug +``` + +Cualquier desviación es un bug que debe corregirse antes de hacer commit. + +### 2. Nombre del producto + +`InQ ROI` — nombre de trabajo para Sprint 1.5. Se reemplazará en Sprint 3-4. No debatir el nombre ahora. + +### 3. Color protagonista + +``` +Primario: #F59845 (naranja InQ) +Gradiente: #F59845 → #ED3E8F (naranja → magenta InQ) +``` + +El gradiente se usa SOLO en KPI Hero y banners principales. +**NO usar morado de CONCILIA** (`#572FA2`, `#CB1889`) en este producto. + +### 4. Footer de PDFs + +Tagline obligatorio: `"Powered by InQuality"` — en todos los footers de PDF, siempre. + +### 5. Rol del consultor + +Si aparece un nombre de consultor en algún campo, es como "consultor responsable", no como "creador" ni "founder" del producto. + +--- + ## Identidad del Proyecto **Nombre de trabajo**: Process Cost Platform (nombre comercial por definir) -**Una frase**: Plataforma web para que consultores de procesos transformen un archivo BPMN 2.0 en un reporte visual deslumbrante de costos operativos en minutos. +**Una frase**: Plataforma web para que consultores de procesos cuantifiquen costos operativos y calculen ROI de automatización a partir de archivos BPMN 2.0. **Mantra**: *Los procesos de negocio son activos estratégicos de las empresas y un diferencial competitivo.* @@ -17,37 +60,83 @@ ## Estado Actual -- **Fase**: 0 — MVP "Noche Cero" +- **Fase**: 1 — Sprint 1 (ROI y escenarios de automatización) +- **Fase anterior completada**: 0 — MVP "Noche Cero" (v0.1.0-mvp, deployado en https://process-cost-platform.pages.dev) - **Documento maestro**: `ARCHITECTURE.md` (raíz del repo) -- **Fecha objetivo MVP**: una noche -- **Deploy objetivo**: Cloudflare Pages (estático, gratis) +- **Audiencia primaria del Sprint 1**: equipo de InQuality (consultora de procesos) +- **Caso de uso primario**: pre-venta de proyectos de automatización con cálculo de ROI +- **Deploy**: Cloudflare Pages (estático, gratis) --- -## Reglas No Negociables del MVP +## Reglas No Negociables — Sprint 1 -### ✅ Lo que SÍ hacemos esta noche +### ✅ Lo que SÍ entra en Sprint 1 -1. Importar archivo `.bpmn` por drag & drop -2. Renderizar el diagrama en read-only con bpmn-js -3. Editar atributos de actividades (costo, tiempo, recursos asignados) -4. CRUD de recursos (rol/persona/sistema/equipo/insumo + costo/hora) -5. Configuración global (moneda, overhead %, nombre proceso, cliente) -6. Motor de simulación determinístico agregado -7. Reporte visual con mapa de calor sobre el diagrama -8. Gráficos: KPIs, top actividades, costo por actividad, composición, costo por recurso -9. Export a PDF (presentable a cliente) y CSV -10. Persistencia en IndexedDB (Dexie) +1. **Volumetría del proceso**: frecuencia anual de ejecución + horizonte de análisis (años) +2. **Marcado de actividades automatizables** con costos automatizados (tiempo + costo fijo) +3. **Inversión en automatización** como dato del proceso (un único valor agregado) +4. **Motor de simulación dual**: corre escenarios "actual" y "automatizado" sobre el mismo BPMN +5. **Sección de ROI en el reporte**: KPIs grandes, tabla comparativa, gráficos de payback y ahorros +6. **5 KPIs de ROI**: Ahorro por ejecución, Ahorro anual, Payback (meses), Ahorro acumulado a N años, ROI % anualizado +7. **Mecanismo de transparencia**: detecta valores cero o inversión cero y los expone en el reporte +8. **Migración Dexie v1 → v2** con datos existentes preservados +9. **Export PDF/CSV ampliado** para incluir páginas/datos de ROI +10. **Documento metodológico anexo** sobre cómo calcular el costo automatizado por ejecución -### ❌ Lo que NO hacemos esta noche +### ❌ Lo que NO entra en Sprint 1 -Login · Multi-tenant · Backend · Colaboración · Edición del BPMN · Versionado · Distribuciones probabilísticas · Riesgos · Gobernanza/RACI · Jerarquía organizacional · IA · Elementos RPA/Agente custom · Tutoriales · Marketplace · i18n · PWA installable · Modo oscuro +- Múltiples escenarios (más de 2) +- Escenarios como entidad separada (modelo B) — sigue siendo modelo A: dos pares de campos por actividad +- VPN, TIR, tasa de descuento (cálculos financieros sofisticados) +- Captura desglosada del costo de automatización (licencias, desarrollo, infra) — un único campo +- Análisis de sensibilidad ("¿qué pasa si la frecuencia cae 30%?") +- Distribuciones probabilísticas +- Modelar bots como recursos con capacidad +- Multi-tenant, login, colaboración, versionado, IA, modo oscuro, i18n, PWA -**Si dudás si una feature entra: NO ENTRA.** Defendé el alcance. +**Si dudás si una feature entra: NO ENTRA.** Defendé el alcance. Anotala en `TODO.md`. --- -## Stack Obligatorio (MVP) +## Decisiones Arquitectónicas Adoptadas (Sprint 1) + +### Modelo de escenarios: Modelo A (dos campos por actividad) + +Cada actividad tiene: +- `directCostFixed` y `executionTimeMinutes` (escenario actual, ya existían) +- `automatedCostFixed` y `automatedTimeMinutes` (nuevos, escenario automatizado) +- `automatable: boolean` (default false) + +**NO se modela con dos diagramas distintos.** Un BPMN, dos pares de campos por actividad. El motor recibe un parámetro `scenario: 'actual' | 'automated'` y aplica los campos correspondientes. + +Migración a "escenarios como entidad" se evaluará en Sprint 3-4 si InQuality lo demanda. + +### Volumetría: multiplicador en el reporte, no en el motor + +El motor sigue siendo **determinístico** y calcula "una ejecución". La volumetría (frecuencia anual + horizonte) es un parámetro del proceso que el reporte usa para extrapolar: +- `costoAnual = costoUnitario × frecuenciaAnual` +- `costoHorizonte = costoAnual × horizonteAños` + +No se introduce VPN ni Monte Carlo en Sprint 1. + +### ROI: cálculos nominales simples + +- `savingsPerExecution = costActual - costAutomated` +- `annualSavings = savingsPerExecution × annualFrequency` +- `paybackMonths = (automationInvestment / annualSavings) × 12` +- `cumulativeSavingsHorizon = (annualSavings × horizonYears) - automationInvestment` +- `roiAnnualPercent = (annualSavings / automationInvestment) × 100` + +Edge cases manejados explícitamente: inversión cero, ahorro negativo, división por cero. + +### Transparencia metodológica + +Si se detectan valores cero (costo automatizado en cero, inversión en cero, etc.), el reporte muestra una **sección visible pero discreta** ("Transparencia metodológica") con la lista de valores no informados. **Es feature, no bug**: convierte una posible debilidad en señal de seriedad profesional para el cliente. + +--- + +## Stack Tecnológico (sin cambios respecto al MVP) | Capa | Tecnología | |---|---| @@ -61,16 +150,16 @@ Login · Multi-tenant · Backend · Colaboración · Edición del BPMN · Versio | Gráficos | Apache ECharts (vía echarts-for-react) | | Persistencia | IndexedDB vía Dexie | | Validación | Zod | -| Export PDF | jsPDF + html2canvas | +| Export PDF | jsPDF + jsPDF-autotable + html2canvas | | Export CSV | papaparse | -| Testing | Vitest | +| Testing | Vitest + Playwright | | Deploy | Cloudflare Pages | **No introduzcas librerías fuera de esta lista sin justificación explícita.** --- -## Arquitectura de Carpetas (obligatoria) +## Arquitectura de Carpetas ``` src/ @@ -79,22 +168,23 @@ src/ ├── router.tsx ├── domain/ ← lógica de negocio pura, sin React, testeable │ ├── types.ts -│ ├── simulation.ts ← motor de simulación +│ ├── simulation.ts ← motor de simulación (acepta scenario) +│ ├── roi.ts ← NUEVO en Sprint 1: cálculos puros de ROI │ ├── bpmn-parser.ts │ └── schemas.ts ← Zod schemas ├── store/ ← Zustand stores -├── persistence/ ← Dexie / IndexedDB +├── persistence/ ← Dexie / IndexedDB (v2 en Sprint 1) ├── features/ ← organizado por feature, no por tipo │ ├── import/ -│ ├── workspace/ -│ ├── simulation/ -│ └── report/ +│ ├── workspace/ ← ampliado en Sprint 1 (panel de automatización) +│ ├── simulation/ ← ampliado (dispara dual scenario) +│ └── report/ ← ampliado (tab Comparación + ROI) ├── components/ui/ ← shadcn copy-pastes ├── lib/ ← utilidades transversales (format, colors, pdf, csv) └── styles/ ``` -**Reglas de dependencia**: +**Reglas de dependencia (sin cambios)**: - `domain/` NO importa de React, ni de `store/`, ni de `features/`. Es puro. - `features/` puede importar de `domain/`, `store/`, `persistence/`, `lib/`, `components/`. - `store/` puede importar de `domain/` y `persistence/`. Nada más. @@ -104,7 +194,7 @@ src/ ## Principios de Código 1. **TypeScript strict** activado. Nada de `any` salvo en bordes con librerías sin tipos. -2. **Dominio puro**: el motor de simulación es una función pura testeable sin DOM. +2. **Dominio puro**: motor y cálculos de ROI son funciones puras testeables sin DOM. 3. **Componentes tontos, hooks inteligentes**: lógica en custom hooks, JSX limpio. 4. **Sin lógica de negocio en componentes**. 5. **Convenciones de naming**: @@ -114,18 +204,24 @@ src/ - Tipos: `PascalCase`, interfaces con prefijo `I` PROHIBIDO 6. **Comentarios en español**, código (variables, funciones) en inglés. 7. **Errores con mensajes accionables** para el usuario, nunca traces crudos en UI. +8. **Migraciones de schema**: SIEMPRE incrementar versión en Dexie, escribir función de upgrade y test que valide preservación de datos. --- -## Diferencial Visual (CRÍTICO) +## Diferencial Visual (sigue siendo CRÍTICO) -El 60% del valor del MVP está en **el reporte final**. Si el reporte se ve mediocre, el MVP fracasa aunque el resto funcione. Reglas: +El reporte sigue siendo el corazón. En Sprint 1 se le agregan páginas. Reglas: - Tipografía: **Inter** para UI, **JetBrains Mono** para números/datos - Paleta heatmap: verde `#10b981` → amarillo `#f59e0b` → rojo `#ef4444` +- Paleta ROI: + - Ahorro positivo: verde `#10b981` + - Ahorro marginal o neutro: gris `#64748b` + - Ahorro negativo o payback fuera de horizonte: rojo `#ef4444` + - Advertencias de transparencia: amarillo `#f59e0b` (no rojo: es informativo, no error) - Cifras de dinero siempre formateadas con separadores y símbolo de moneda +- KPIs de ROI **más grandes** que los KPIs del reporte de costos (es la sección que vende) - El export PDF debe verse igual o mejor que la pantalla -- Cada gráfico tiene título, leyenda clara y formato de moneda apropiado - Densidad de información media-alta (audiencia: consultor profesional) --- @@ -136,20 +232,23 @@ Antes de marcar una tarea como terminada: - [ ] Funciona en el happy path - [ ] Maneja el caso de datos vacíos sin crashear -- [ ] Persiste en IndexedDB cuando corresponde +- [ ] Maneja edge cases definidos (división por cero, valores negativos, valores cero) +- [ ] Persiste en IndexedDB cuando corresponde (con migración si cambió schema) - [ ] Tiene estilo consistente con el resto de la app - [ ] Si toca dominio: tiene al menos un test en Vitest +- [ ] Si toca el flujo end-to-end: tiene cobertura en Playwright --- ## Comandos del Proyecto ```bash -npm run dev # desarrollo -npm run build # producción -npm run test # tests con Vitest -npm run lint # eslint -npm run preview # previsualizar build +npm run dev # desarrollo +npm run build # producción +npm run test # tests unitarios con Vitest +npm run lint # eslint +npm run preview # previsualizar build de producción +npx playwright test # tests E2E (Playwright headless) ``` --- @@ -161,13 +260,37 @@ npm run preview # previsualizar build - Si encontrás que algo del documento de arquitectura está mal o falta, **avisame antes de improvisar**. - No agregues features no pedidas. Si algo se te ocurre, anotalo en `TODO.md`. - Cuando completes algo, **mostrame los archivos creados/modificados**, no los pegues completos en el chat salvo que te lo pida. +- **Validá programáticamente** lo que yo no puedo ver visualmente (uso Mac + VS Code + Claude Code sin browser interactivo). Para flujos críticos: Playwright + extracción de datos del PDF/CSV generado. +- Si un test revela algo raro: **NO racionalices el problema, investigá la causa raíz**. Los tests no mienten. --- -## Contexto del Fundador +## Contexto del Fundador y Equipo - Consultor de automatización de procesos basado en Paraguay -- Construye esto como herramienta consultiva propia + para vender a colegas LATAM +- Construye esto como herramienta consultiva propia + del equipo de InQuality + para vender a colegas LATAM - Usa Claude Code Pro como único "equipo de desarrollo" -- Lo va a presentar a clientes reales en días, no meses -- Valora: claridad, robustez, pulido visual, mantenibilidad \ No newline at end of file +- Sprint 1 es para uso interno de InQuality (no para mostrar a cliente todavía) +- Casos de uso confirmados: + - Análisis de costos operativos en consultoría de procesos + - **Pre-venta de proyectos de automatización con cálculo de ROI** (caso estrella del Sprint 1) +- Valora: claridad, robustez, pulido visual, mantenibilidad, validación rigurosa + +--- + +## Reglas de marca NO NEGOCIABLES + +- "InQ" siempre se escribe I-n-Q (mayúscula-minúscula-mayúscula) +- Color protagonista: naranja #F59845 +- Gradiente identidad: #F59845 → #ED3E8F +- 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 \ No newline at end of file diff --git a/DESIGN_SYSTEM.md b/DESIGN_SYSTEM.md new file mode 100644 index 0000000..fe99d63 --- /dev/null +++ b/DESIGN_SYSTEM.md @@ -0,0 +1,330 @@ +# InQ ROI — Sistema de Diseño + +> Documento de referencia visual para Sprint 2+. +> Fuente de verdad para la paleta, tipografía y componentes. +> Actualizar en cada sprint que modifique la identidad visual. + +--- + +## 1. Identidad de Marca + +### Nombre del producto +**InQ ROI** — herramienta interna de InQuality para cuantificación de ROI de automatización. + +### Regla de ortografía (NO NEGOCIABLE) +``` +✅ InQ ← I mayúscula, n minúscula, Q mayúscula +✅ InQuality ← nombre completo +❌ INQ / Inq / inq / inQ ← cualquiera es bug +``` + +### Tagline en footers PDF +`"Powered by InQuality"` — obligatorio en todos los footers de PDF. + +--- + +## 2. Paleta Cromática + +### 2.1 InQ Orange — Color protagonista + +| Token Tailwind | CSS Var | Hex | HSL | Uso | +|---|---|---|---|---| +| `bg-inq-orange` | `--inq-orange` | `#F59845` | 28° 90% 62% | Principal: logo, KPIs hero, CTAs primarios | +| `bg-inq-orange-dark` | `--inq-orange-dark` | `#B45309` | 34° 93% 37% | Texto sobre fondos claros, hover de CTAs | +| `bg-inq-orange-light` | `--inq-orange-light` | `#FEF3E2` | 36° 95% 88% | Fondos de KPI cards | +| `bg-inq-orange-lighter` | `--inq-orange-lighter` | `#FFF8ED` | 36° 100% 94% | Fondos de banner narrativo, hover sutiles | + +### 2.2 InQ Magenta — Gradiente identidad + +| Token Tailwind | CSS Var | Hex | Uso | +|---|---|---|---| +| `bg-inq-magenta` | `--inq-magenta` | `#ED3E8F` | Segundo color del gradiente identidad | + +#### Gradiente identidad +```css +background: linear-gradient(135deg, #F59845 0%, #ED3E8F 100%); +``` + +**Uso exclusivo:** KPI Hero "Ahorro proyectado" en PDF y web. NUNCA en bordes, separadores ni fondos secundarios. + +### 2.3 Semánticos + +| Token Tailwind | CSS Var | Hex | Uso | +|---|---|---|---| +| `bg-success` | `--color-success` | `#16A34A` | Ahorros positivos, payback alcanzado | +| `bg-success-light` | `--color-success-light` | `#F0FDF4` | Fondos de mensajes positivos | +| `bg-warning` | `--color-warning` | `#F59E0B` | Advertencias, sección transparencia | +| `bg-warning-bg` | `--color-warning-bg` | `#FFFBEB` | Fondo de sección transparencia | +| `bg-destructive` / `--color-danger` | — | `#EF4444` | Costos altos, valores negativos | +| `--color-danger-light` | — | `#FEE2E2` | Fondos de heatmap alto costo | + +### 2.4 Heatmap — Paleta saturada (FIJA hasta Sprint 3-4) + +| Token Tailwind | Hex | RGB | Uso | +|---|---|---|---| +| `bg-heatmap-low` | `#10B981` | (16, 185, 129) | Actividades de bajo costo | +| `bg-heatmap-mid` | `#F59E0B` | (245, 158, 11) | Actividades de costo medio | +| `bg-heatmap-high` | `#EF4444` | (239, 68, 68) | Actividades de alto costo | + +#### Decisión sobre paleta de heatmap + +**Se mantiene saturada** (verde-ámbar-rojo) en lugar de la variante pastel del brief por dos razones: + +1. **Tests E2E calibrados**: los análisis de píxeles del heatmap (`export-pdf.spec.ts`, `validate-dod-etapa4.spec.ts`) tienen umbrales calibrados para estos colores. Cambiar la paleta requiere recalibrar todos esos umbrales. + +2. **Visibilidad en pantallas grandes**: el producto se usa en talleres y charlas InQuality proyectadas en pantalla. Los colores saturados mantienen contraste a distancia. Los pasteles se "lavan" a 3+ metros. + +**Revisión programada**: Sprint 3-4 con feedback real de uso en talleres. + +**Invariante crítica**: los valores de `tailwind.config.js` `heatmap.*` DEBEN coincidir con las constantes `HEATMAP_LOW/MID/HIGH` en `src/lib/colors.ts`. Si se modifica uno, debe actualizarse el otro. + +### 2.5 Neutrales + +| Hex | Tailwind equivalente | Uso | +|---|---|---| +| `#0F172A` | `slate-900` | Títulos, números importantes | +| `#475569` | `slate-600` | Texto del cuerpo | +| `#64748B` | `slate-500` | Labels, metadatos | +| `#94A3B8` | `slate-400` | Placeholders, texto muy secundario | +| `#E2E8F0` | `slate-200` | Bordes generales | +| `#F1F5F9` | `slate-100` | Separadores sutiles | +| `#F8FAFC` | `slate-50` | Fondo de la app, canvas BPMN | + +### 2.6 Colores que NO usar + +``` +❌ #572FA2 Morado primario de CONCILIA — marca hermana, no mezclar +❌ #CB1889 Magenta de CONCILIA +❌ azul shadcn por defecto (221.2 83.2% 53.3%) — reemplazado por InQ orange +``` + +--- + +## 3. Tipografía + +### 3.1 Familias + +```css +/* Primaria — web app y body de PDFs */ +font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; + +/* Mono — números en tablas, datos financieros */ +font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace; +``` + +Tailwind: `font-sans` / `font-mono` + +### 3.2 Escala PDF + +| Elemento | Tamaño | Peso | Tracking | +|---|---|---|---| +| Logo "InQ ROI" en header | 22px | 700 | −0.8px | +| Título de sección | 16px | 500 | normal | +| KPI Hero (cifra) | 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 | + +### 3.3 Escala 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 | + +--- + +## 4. Componentes + +### 4.1 KPI Hero (gradiente naranja → magenta) + +``` +Fondo: linear-gradient(135deg, #F59845 0%, #ED3E8F 100%) +Padding: 16-18px +Border-radius: 6-8px +Label: 9-10px / uppercase / letter-spacing 1.2px / white opacity 0.9 +Cifra: 28-30px / 700 / letter-spacing −1px / white +Subtítulo: 10-11px / white opacity 0.85 +``` + +Uso: el KPI "Ahorro proyectado a N años" — el más importante del reporte ROI. + +### 4.2 KPI Card (secundario, fondo naranja claro) + +``` +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 +``` + +### 4.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 +``` + +### 4.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 +``` + +4 condiciones que activan la sección: +1. Actividades `automatable=true` con `automatedCostFixed=0` y `automatedTimeMinutes=0` +2. `automationInvestment=0` +3. `roiAnnualPercent > 1000` (finito) +4. (reservado para futuras reglas) + +### 4.5 Tabla comparativa PDF + +``` +Header row: + Fondo: #FFF8ED + Border-bottom: 1px solid #F59845 + Texto: 10px / 500 / #92400E + +Body row — automatizable: + Fondo: white + +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 / #0F172A +``` + +### 4.6 Header PDF + +``` +Layout: flex justify-between +Border-bottom: 3px solid #F59845 (pág. 1) / 2px (otras) +Padding-bottom: 10-12px +Margin-bottom: 14-16px + +Izquierda: + "InQ ROI" 18-22px / 700 / #F59845 + Tagline: 9px / letter-spacing 1.5px / #94A3B8 (solo pág. 1) + +Derecha: + "UN PRODUCTO DE" 9px / letter-spacing 1px / #94A3B8 + "InQ" 16px / 700 / #475569 + "InQuality" 9px / #64748B +``` + +### 4.7 Footer PDF + +``` +Position: absolute bottom 14-16mm +Padding-top: 6-8px +Border-top: 0.5px solid #E2E8F0 +Layout: flex justify-between +Font: 9px / 400 / #94A3B8 + +Izquierda: "InQ ROI" bold #F59845 " · Powered by InQuality · " cliente · fecha +Derecha: "Página X de Y" +``` + +### 4.8 Badge ⚡ (Automatizable) + +#### En canvas BPMN overlay +``` +Diámetro: 14px +Fondo: #F59845 (naranja sólido) +Border: 1.5px solid white +Posición: { top: -8, left: nodeWidth - 12 } +Símbolo: ⚡ blanco 8px +``` + +Nota: en Sprint 1 el badge usa fondo `#fffbeb` (ámbar-50) con ícono ámbar. La migración a naranja sólido se hace en **Etapa 9** de Sprint 1.5. + +#### En tablas y leyendas +``` +Diámetro: 14px inline +Fondo: #F59845 +Color: white +``` + +### 4.9 Layout híbrido de PDFs (portrait + landscape) + +Los PDFs combinan orientaciones por página: + +- **Páginas SIN diagrama**: A4 vertical (210×297mm) — texto, tablas, KPIs +- **Página DEL diagrama BPMN**: A4 horizontal (297×210mm) — para que el diagrama ocupe espacio real + +Implementación jsPDF: `doc.addPage([297, 210], 'landscape')` + +--- + +## 5. Tokens en código + +### Web (Tailwind) + +```tsx +// Colores de marca +
// #F59845 +
// #B45309 +
// #FEF3E2 +
// texto naranja +
// borde naranja + +// Gradiente identidad (como style prop — Tailwind no soporta gradientes dinámicos) +
+ +// Semánticos +
// #16A34A +
// #FFFBEB +``` + +### PDF (jsPDF) + +```typescript +// Constante en pdf-sections.ts (a migrar en Etapas 4-8) +const PDF_ORANGE: [number, number, number] = [245, 152, 69] // #F59845 +const PDF_ORANGE_LIGHT: [number, number, number] = [254, 243, 226] // #FEF3E2 +const PDF_MAGENTA: [number, number, number] = [237, 62, 143] // #ED3E8F + +// Gradiente en jsPDF se simula con pasos (ver pdf-sections-roi.ts) +``` + +--- + +## 6. Reglas de uso — DO / DON'T + +### DO ✅ +- Usar `bg-primary` / `text-primary` para todos los CTAs — hereda el naranja de `--primary` +- Usar gradiente identidad en exactamente 1 KPI Hero por página/vista +- Usar `font-mono` para todos los números financieros +- Escribir siempre **InQ** con I-n-Q + +### DON'T ❌ +- Usar `bg-blue-*` donde debería ir naranja InQ +- Aplicar el gradiente en más de un elemento por vista +- Usar colores de CONCILIA (`#572FA2`, `#CB1889`) +- Cambiar los colores del heatmap sin recalibrar los tests E2E + +--- + +*Actualizado: Sprint 1.5 Etapa 1 — 2026-05-16* diff --git a/METHODOLOGY_AUTOMATED_COST.md b/METHODOLOGY_AUTOMATED_COST.md new file mode 100644 index 0000000..6663d36 --- /dev/null +++ b/METHODOLOGY_AUTOMATED_COST.md @@ -0,0 +1,159 @@ +# Metodología: Costo Automatizado por Ejecución + +> **Documento de referencia para consultores que usen la plataforma para análisis de ROI de automatización.** + +--- + +## Propósito + +Cuando una actividad se marca como **automatizable** en la plataforma, el consultor debe cargar el **costo automatizado por ejecución**. Este documento define qué debe incluir ese número, qué NO debe incluir, y cómo calcularlo correctamente para evitar ROIs irreales. + +--- + +## Qué es el "costo automatizado por ejecución" + +Es el **costo recurrente** asociado a cada ejecución individual de la actividad después de automatizada. **NO incluye la inversión inicial del proyecto**, que se carga aparte como "Inversión en automatización" a nivel del proceso. + +--- + +## Componentes del costo automatizado por ejecución + +Se compone de cuatro elementos. El consultor debe estimarlos por separado y sumarlos. + +### 1. Costo del bot / licencia por ejecución + +Costo prorrateado de la licencia de la plataforma de automatización (RPA, iPaaS, agente IA) por cada ejecución individual. + +``` +costoLicencia = costoAnualLicencia / ejecucionesAnuales +``` + +**Ejemplo**: licencia anual RocketBot $5.000, ejecuta 50.000 actividades/año → `$5.000 / 50.000 = $0,10` por ejecución. + +### 2. Costo de infraestructura por ejecución + +Costo prorrateado de servidores, monitoreo, observabilidad, almacenamiento. + +``` +costoInfra = costoAnualInfra / ejecucionesAnuales +``` + +**Ejemplo**: VPS dedicado $1.200/año + monitoreo $0 → `$1.200 / 50.000 = $0,024` por ejecución. + +### 3. Costo de excepciones residuales + +Las automatizaciones nunca son 100% efectivas. Un porcentaje de las ejecuciones requieren intervención humana (fallos, casos borde, errores de validación). Se debe estimar: +- Tasa de excepciones (% de ejecuciones que requieren intervención) +- Tiempo promedio de intervención humana +- Costo/hora del recurso humano que interviene + +``` +costoExcepciones = tasaExcepciones × tiempoIntervención × costoHoraHumano +``` + +**Ejemplo**: 5% de excepciones, 8 minutos cada una, analista a $30/hora → +`0,05 × (8/60) × $30 = $0,20` por ejecución (en promedio, considerando que solo el 5% genera este costo). + +### 4. Costo de mantenimiento prorrateado + +Cambios y actualizaciones del bot/integración, ajustes ante cambios de UI/API de los sistemas integrados, debugging continuo. + +``` +costoMantenimiento = costoMantenimientoAnual / ejecucionesAnuales +``` + +**Ejemplo**: 30 horas/año de mantenimiento × $100/hora = $3.000/año → `$3.000 / 50.000 = $0,06` por ejecución. + +--- + +## Fórmula final + +``` +costoAutomatizadoPorEjecución = costoLicencia + + costoInfra + + costoExcepciones + + costoMantenimiento +``` + +**Ejemplo completo**: `$0,10 + $0,024 + $0,20 + $0,06 = $0,384 por ejecución` + +--- + +## Qué NO incluir + +- ❌ **Inversión inicial del proyecto** (desarrollo, licencias iniciales, implementación, capacitación): va en el campo "Inversión en automatización" a nivel proceso. +- ❌ **Costos no recurrentes** (consultorías puntuales, asesorías): no son por ejecución. +- ❌ **Costos del proceso de relevamiento previo**: corresponden al consultor, no a la operación. + +--- + +## Componentes de la "Inversión en automatización" (campo separado) + +Este campo agregado por proceso debe sumar: + +- Costo de **desarrollo** del bot/integración (horas-hombre × tarifa) +- **Licencias iniciales** o costos de setup únicos +- **Implementación** y configuración inicial +- **Capacitación** del equipo del cliente +- **Infraestructura inicial** (servidores, software base) +- **Consultoría externa** si aplica + +**No incluir** costos recurrentes (esos van en el costo por ejecución). + +--- + +## Validación de razonabilidad + +Antes de presentar el reporte de ROI al cliente, validar: + +| Indicador | Valor razonable | Si es muy distinto | +|---|---|---| +| Costo automatizado / costo actual | 5% - 30% | Verificar si se subestimaron las excepciones o el mantenimiento | +| Payback | 6 - 24 meses | < 3 meses: revisar inversión; > 36 meses: revisar el caso de negocio | +| Tasa de excepciones | 2% - 15% | < 1%: optimista; > 20%: el proceso quizás no es buen candidato | +| Ahorro % por ejecución | 70% - 95% | < 50%: validar si vale la pena; > 95%: revisar excepciones | +| **ROI anual** | **50% - 1.000%** | **> 1.000%: señal de inputs incorrectos — ver abajo** | + +### ⚠️ ROI superior al 1.000% anual + +La plataforma muestra una advertencia automática en la sección "Transparencia metodológica" cuando el ROI anualizado supera el 1.000% (y es un valor finito — no Infinity por inversión en cero). + +Un ROI de este nivel es matemáticamente posible pero comercialmente inverosímil. Las causas más frecuentes son: + +1. **Frecuencia anual sobreestimada**: confundir instancias del proceso con ejecuciones individuales de actividades. Verificar con datos reales del cliente. + +2. **Costo automatizado por ejecución subestimado**: olvidar el componente de excepciones, el mantenimiento o la infra. Usar la fórmula de los 4 componentes de este documento. + +3. **Inversión en automatización subestimada**: solo se incluyó el desarrollo y se olvidaron licencias, capacitación e implementación. + +**Acción recomendada**: corregir los inputs, no ignorar la advertencia. El cliente —o alguien de su equipo técnico— va a hacer las mismas cuentas y un ROI de 10.000% destruye credibilidad. + +--- + +## Manejo de valores cero + +La plataforma detecta automáticamente cuando hay valores en cero: +- Costo automatizado por ejecución = 0 en actividades marcadas como automatizables +- Inversión en automatización = 0 con payback < 1 mes + +Cuando esto ocurre, el reporte muestra una sección de **"Transparencia metodológica"** listando los valores no informados. **Esto NO es un error**: es una señal de transparencia hacia el cliente. Si el consultor no tiene aún todos los datos, el reporte refleja esa incertidumbre honestamente. + +--- + +## Recomendación de proceso para el consultor + +1. **Estimar la frecuencia anual real** del proceso (no asumir, validar con datos del cliente). +2. **Listar las actividades automatizables** y para cada una: + - Estimar costos por separado de los 4 componentes + - Sumarlos + - Cargar el total en la plataforma +3. **Estimar la inversión total** del proyecto (no por ejecución, costo único). +4. **Ejecutar la simulación** y revisar: + - ¿El ROI es razonable según la tabla de validación? + - ¿La sección de transparencia metodológica aparece? ¿Por qué? ¿Hay valores que faltan? +5. **Refinar estimaciones** hasta que el modelo sea defendible. +6. **Generar reporte** y revisarlo antes de enviarlo al cliente. + +--- + +> **Principio guía**: un ROI conservador y defendible vale más que un ROI espectacular pero frágil. El cliente recordará la promesa, no la advertencia. \ No newline at end of file diff --git a/README.md b/README.md index d7affdc..87ca16c 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,223 @@ -# Process Cost Platform — MVP Fase 0 +# Process Cost Platform -Plataforma web que transforma un archivo BPMN 2.0 en un reporte visual -de costos operativos, listo para presentar a clientes en minutos. -100% en el navegador — sin backend, sin registro, datos guardados localmente. +> Transformá un archivo BPMN 2.0 en un reporte visual de costos operativos — +> y calculá el ROI de automatizar ese proceso — en minutos, sin backend, sin registro. -## Cómo usarlo +**Hecho desde Paraguay 🇵🇾 · Herramienta interna de InQuality · Sprint 1 en desarrollo activo** -1. **Importá** tu archivo `.bpmn` arrastrándolo a la pantalla principal, - o cargá uno de los tres procesos de ejemplo incluidos. -2. **Configurá** los costos fijos y tiempos de cada actividad en el workspace. - Ajustá las probabilidades de los gateways XOR si las hay. -3. **Simulá** para generar el análisis agregado de costos. -4. **Exportá** el reporte en PDF (presentación para el cliente) o CSV (datos). +--- -## Stack +## ¿Qué hace esto? -| Capa | Tecnología | -|---|---| -| Framework | React 19 + Vite 8 | -| Routing | TanStack Router | -| Estado | Zustand 5 | -| Canvas BPMN | bpmn-js 18 (Viewer) | -| UI | shadcn/ui + Tailwind CSS | -| Gráficos | Apache ECharts | -| Persistencia | IndexedDB vía Dexie 4 | -| Export PDF | jsPDF + jspdf-autotable | -| Export CSV | papaparse | -| Testing | Vitest + Playwright | -| Deploy | Cloudflare Pages | +Un consultor de procesos recibe un diagrama BPMN del cliente. Con esta plataforma puede: -## Scripts +1. **Importar** el diagrama (.bpmn) y ver el proceso renderizado al instante. +2. **Configurar costos** actividad por actividad: costo fijo, tiempo, recursos humanos/sistemas. +3. **Simular** para obtener el costo esperado de una ejecución completa, con propagación de probabilidades a través de gateways XOR, AND e inclusivos. +4. **Ver el reporte** con mapa de calor sobre el diagrama, KPIs, gráficos, tabla de actividades. +5. **Exportar** a PDF (presentación para el cliente) o CSV (datos para Excel). + +En Sprint 1 (en curso): + +6. **Marcar actividades como automatizables** y configurar sus costos en el escenario automatizado. +7. **Calcular el ROI** del proyecto de automatización: payback, ahorro anual, acumulado a N años. +8. **Mostrar un reporte comparativo** Actual vs. Automatizado con transparencia metodológica. + +**Audiencia primaria**: consultores de InQuality que usan esto en pre-venta de proyectos de automatización. +No es un producto de consumo masivo. El diseño prioriza densidad de información y velocidad de uso. + +--- + +## Inicio rápido ```bash -npm run dev # Servidor de desarrollo (localhost:5173) -npm run build # Build de producción en dist/ -npm run preview # Preview del build en localhost:4173 -npm run test # Tests unitarios con Vitest -npx playwright test # Tests E2E con Playwright +git clone +cd simulador-web +npm install --legacy-peer-deps +npm run dev ``` -## Estado +Abre `http://localhost:5173`. Arrastrá un `.bpmn` o hacé clic en uno de los tres procesos de ejemplo. -**MVP Fase 0** — funcionalidad completa para uso consultivo básico. -Las features adicionales (multi-tenant, IA, versionado, etc.) -están documentadas en `TODO.md` para la Fase 1. +> **Sin instalación de backend.** Todo corre en el navegador. Los datos se guardan en IndexedDB +> del browser — si borrás el caché, perdés los procesos guardados. Exportá el PDF/CSV antes de limpiar. + +--- + +## Flujo de uso detallado + +### 1. Importar un proceso + +Arrastrá tu archivo `.bpmn` a la pantalla de inicio, o cargá uno de los procesos de ejemplo: +- **Proceso lineal simple** — para entender el flujo básico +- **Aprobación de crédito** — con gateways XOR y AND paralelo +- **Control de calidad con revisión** — con loop + +La plataforma soporta BPMN 2.0 con **múltiples pools** (colaboraciones): todos los pools se importan y sus actividades aparecen configurables en el workspace. + +### 2. Configurar el workspace + +El panel derecho tiene tres tabs: + +**Tab "Actividad"** (al hacer click en un nodo del diagrama): +- Costo directo fijo por ejecución +- Tiempo de ejecución en minutos +- Recursos asignados (rol/sistema/equipo con % de utilización) +- Sección "Automatización" *(Sprint 1)*: toggle + costo y tiempo del escenario automatizado + +**Tab "Recursos"**: CRUD de recursos reutilizables con costo/hora. + +**Tab "Global"**: +- Nombre del proceso y cliente +- Moneda (USD, PYG, BRL, ARS, EUR, COP, MXN, CLP) +- Overhead (% de costos indirectos sobre directos) +- Frecuencia anual de ejecución *(Sprint 1)* +- Horizonte de análisis en años *(Sprint 1)* +- Inversión total en automatización *(Sprint 1)* + +Las actividades marcadas como automatizables muestran un badge Bot ámbar (⚙️) en la esquina superior derecha del nodo en el canvas. + +### 3. Simular + +El botón "Simular" corre el motor determinístico. Si hay gateways XOR con probabilidades que no suman 100%, el botón queda deshabilitado y se muestran advertencias en la topbar. + +Desde Sprint 1, el motor corre **dos escenarios simultáneamente** (actual + automatizado) y los guarda como un único snapshot. Cualquier cambio en los datos invalida ambos resultados — hay que re-simular. + +### 4. Reportar y exportar + +El reporte muestra mapa de calor, KPIs, gráficos y tabla de actividades. En Sprint 1 se agregan tabs de comparación y ROI. + +Exportación: +- **PDF**: 3+ páginas presentables ante el cliente, con heatmap embebido como imagen JPEG. +- **CSV**: datos con BOM UTF-8 para compatibilidad con Excel regional. + +--- + +## Arquitectura técnica + +``` +src/ +├── domain/ ← Lógica de negocio pura, sin React, 100% testeable +│ ├── types.ts ← Todas las interfaces del modelo de dominio +│ ├── simulation.ts ← Motor: propaga probabilidades por el grafo BPMN +│ ├── roi.ts ← Cálculos de ROI: payback, ahorro, acumulado (Sprint 1) +│ ├── bpmn-parser.ts← Parser XML BPMN 2.0 → grafo en memoria +│ └── schemas.ts ← Validación Zod de todas las entidades +│ +├── store/ ← Estado global con Zustand +│ ├── process-store.ts ← Proceso actual, actividades, gateways, recursos +│ └── simulation-store.ts ← Resultados actual + automatizado, invalidación reactiva +│ +├── persistence/ ← IndexedDB vía Dexie (schema versión 2) +│ ├── db.ts ← Schema y migraciones +│ └── repositories.ts ← CRUD de entidades (@internal: solo llamar desde stores) +│ +├── features/ +│ ├── import/ ← Landing, drag & drop, carga de ejemplos +│ ├── workspace/ ← Canvas BPMN + panel lateral de configuración +│ ├── simulation/ ← Hook useSimulate: dispara ambos escenarios +│ └── report/ ← Reporte visual, heatmap, gráficos, export +│ +└── lib/ + ├── export/ ← pdf-export.ts, csv-export.ts (lazy-loaded al hacer clic) + ├── colors.ts ← Gradiente heatmap: verde → ámbar → rojo + └── format.ts ← Formateo de moneda, tiempo, porcentajes +``` + +### Reglas de dependencia + +``` +domain/ ← no importa nada del proyecto (puro TypeScript) +store/ ← importa solo de domain/ y persistence/ +features/ ← importa de domain/, store/, persistence/, lib/, components/ +``` + +La regla ESLint `no-restricted-imports` hace que cualquier import de +`persistence/repositories` desde fuera de `store/` falle en `npm run lint`. +La única excepción intencional es `simulation-store.ts`, que importa +`process-store.ts` para suscribirse a cambios y mantener la invariante +de invalidación dual (documentado como excepción en el archivo). + +### Motor de simulación — decisiones clave + +El motor acepta `scenario: 'actual' | 'automated'`. En `'automated'`, las +actividades con `automatable=true` usan sus campos alternativos de costo y tiempo. +La probabilidad de ejecución **no cambia entre escenarios** — solo los costos. + +Overlays en el canvas: se usa `overlays.add()` de diagram-js con posición +`{ top: -8, left: element.width - 12 }` obtenida via `elementRegistry.get()`. +**No usar** `position.right` directamente: la fórmula interna de diagram-js es +`left = right * -1 + width`, lo que con `right: -8` produce `left = width + 8` +(badge enteramente fuera del nodo, sin overlap de esquina). + +--- + +## Stack tecnológico + +| Capa | Tecnología | Nota | +|---|---|---| +| Framework | React 19 + Vite 8 (Rolldown) | | +| Routing | TanStack Router 1.x | Type-safe | +| Estado | Zustand 5 | Stores: process + simulation | +| Canvas BPMN | bpmn-js 18 / diagram-js | Viewer (read-only) | +| UI | shadcn/ui + Tailwind CSS | Components copy-pasted | +| Iconos | Lucide React | | +| Gráficos | Apache ECharts 5 | Lazy-loaded | +| Persistencia | Dexie 4 (IndexedDB) | Schema v2, migraciones automáticas | +| Validación | Zod 3 | | +| Export PDF | jsPDF + jspdf-autotable | Lazy-loaded | +| Export CSV | papaparse 5 | | +| Tests unitarios | Vitest 4 | 336+ tests | +| Tests E2E | Playwright | Con análisis de PDF y píxeles | +| Linting | ESLint 10 (flat config) | `no-restricted-imports` custom | +| Deploy | Cloudflare Pages | Estático, gratis | + +--- + +## Comandos + +```bash +# Desarrollo +npm run dev # Servidor local en localhost:5173 con HMR + +# Build y preview +npm run build # TypeScript check + Vite build → dist/ +npm run preview # Preview del build en localhost:4173 + +# Testing +npm run test # Tests unitarios e integración (Vitest) +npx playwright test # Tests E2E contra localhost:4173 (requiere build previo) +npx playwright test --config=playwright.prod.config.ts # Smoke test vs producción + +# Calidad +npm run lint # ESLint — 0 errores esperados + +# Deploy +CLOUDFLARE_ACCOUNT_ID= npx wrangler pages deploy dist \ + --project-name=process-cost-platform +``` + +--- + +## Producción + +**URL**: https://process-cost-platform.pages.dev +**Plataforma**: Cloudflare Pages (estático, sin servidor) + +Smoke test post-deploy: `npx playwright test --config=playwright.prod.config.ts` +Genera `tests/e2e/__output__/prod-smoke.pdf` como evidencia del flujo completo. + +--- + +## Documentación adicional + +| Archivo | Contenido | +|---|---| +| `ARCHITECTURE.md` | Diseño del sistema, ADRs 1-15, roadmap completo | +| `CHANGELOG.md` | Historial detallado de cambios por versión | +| `TODO.md` | Backlog de features y deuda técnica | +| `METHODOLOGY_AUTOMATED_COST.md` | Guía para calcular el costo automatizado por ejecución | + +--- + +*Los procesos de negocio son activos estratégicos de las empresas y un diferencial competitivo.* diff --git a/TODO.md b/TODO.md index acf70e3..a4fc213 100644 --- a/TODO.md +++ b/TODO.md @@ -21,6 +21,22 @@ - [ ] **Detalle de recursos por actividad** en el PDF (tabla de costos por recurso asignado) - [ ] **Importación por URL**: cargar BPMN desde enlace directo (GitHub, Confluence, etc.) +## Refinamientos UX + +- [ ] **Mejorar animación de expansión de la sección "Automatización" en ActivityPanel.** Actualmente usa max-h trick (funciona pero la aceleración no es lineal en los últimos píxeles porque el contenedor crece hasta un max fijo de 320px). Solución sofisticada: `useRef + scrollHeight` para animar a la altura real del contenido. Costo: medio. Beneficio: micro-pulido visual. + +## Deuda técnica / Decisiones arquitectónicas pendientes + +- [ ] **Dexie live queries (Modelo B de sincronización store↔db)**: Hoy la invalidación del resultado de simulación se garantiza via ESLint rule (`no-restricted-imports`) + convención: todas las mutaciones pasan por `useProcessStore`. Si en Sprint 2 la grilla de edición masiva requiere escrituras de alta frecuencia directas a IndexedDB (por performance), evaluar migración a live queries de Dexie que invaliden el store automáticamente. Documentado en la auditoría de Mayo 2026. + +- [ ] **Indicador visual en el reporte de actividades automatable** (borde punteado o ícono diferenciador): en Sprint 1 se usa solo leyenda de texto. Evaluar si el cliente necesita más claridad visual al comparar Actual vs Automatizado. + +- [ ] **Sprint 3-4: revisar paleta del heatmap** (verde/ámbar/rojo saturados vs. pasteles). Los colores saturados actuales (`#10B981`, `#F59E0B`, `#EF4444`) funcionan bien en pantallas grandes de talleres. El brief de Sprint 1.5 propone pasteles más "consultivos" (`#BBF7D0`, `#FED7AA`, `#FECACA`), pero el cambio requiere recalibrar los tests E2E de análisis de píxeles (umbrales de verde/ámbar/rojo). Hacer con feedback real de uso en talleres InQuality. + +- [ ] **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 diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..ab3b4f2 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,92 @@ +import js from '@eslint/js' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist', 'node_modules'] }, + + // ─── Base: JS + TS strict ──────────────────────────────────────────────────── + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + + // ── Regla estructural crítica ───────────────────────────────────────── + // Repositorios solo pueden llamarse desde stores. + // Cualquier import de persistence/repositories en código que no sea src/store/** + // falla el lint con un mensaje explicativo. + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['*/persistence/repositories*', '../persistence/repositories*', '@/persistence/repositories*'], + message: + 'Los repositorios solo deben llamarse desde stores (src/store/**). ' + + 'Usá el store correspondiente (useProcessStore, useSimulationStore, etc.).', + }, + ], + }, + ], + + // ── Reglas ajustadas al proyecto ───────────────────────────────────── + // any en bordes con librerías sin tipos (bpmn-js, jsPDF, autotable) está + // permitido por CLAUDE.md — se reporta como warning, no error. + '@typescript-eslint/no-explicit-any': 'warn', + + // Funciones auxiliares definidas dentro de componentes (ej: Th en ActivitiesTable, + // handlers en BpmnCanvas) son un patrón aceptado en este codebase. El riesgo de + // reset de estado no aplica porque son funciones sin estado propio. + 'react-hooks/static-components': 'off', + + // setState síncrono en useEffect con condicional deliberado (sincronización de + // estado local con props/store). Los efectos están correctamente dependenciados. + 'react-hooks/set-state-in-effect': 'off', + + // Cálculos de posición en pdf-export.ts usan y como variable acumulativa; + // el patrón y += delta → doc.addPage() → y = PDF.margin es intencional + // (separa visualmente las secciones del cálculo aunque no use el valor final). + 'no-useless-assignment': 'warn', + }, + }, + + // ─── Override: src/store/** puede importar repositories ────────────────────── + { + files: ['src/store/**/*.{ts,tsx}'], + rules: { + 'no-restricted-imports': 'off', + }, + }, + + // ─── Override: features/import puede importar repositories (operación de + // importación inicial — no hay estado de simulación previo que invalidar) ── + { + files: ['src/features/import/**/*.{ts,tsx}'], + rules: { + 'no-restricted-imports': 'off', + }, + }, + + // ─── Override: features/report usa repos en modo read-only (useReportData) ─── + { + files: ['src/features/report/useReportData.ts'], + rules: { + 'no-restricted-imports': 'off', + }, + }, + + // ─── Override: tests e2e y integración pueden importar repos directamente ──── + { + files: ['tests/**/*.{ts,tsx}'], + rules: { + 'no-restricted-imports': 'off', + }, + }, +) diff --git a/index.html b/index.html index 42beec3..f9023c9 100644 --- a/index.html +++ b/index.html @@ -1,26 +1,26 @@ - - - - - - + + + + + + - Process Cost Platform — Cuantificá tus procesos - + InQ ROI — Cuantificá tus procesos + - - - - - + + + + + - - - - - - -
- - - + + + + + + +
+ + + diff --git a/methodology-initial-commit.tar.gz b/methodology-initial-commit.tar.gz new file mode 100644 index 0000000..0861003 Binary files /dev/null and b/methodology-initial-commit.tar.gz differ diff --git a/methodology/ANTI_PATTERNS.md b/methodology/ANTI_PATTERNS.md new file mode 100644 index 0000000..1271a8d --- /dev/null +++ b/methodology/ANTI_PATTERNS.md @@ -0,0 +1,101 @@ +# Anti-patrones + +> Comportamientos que producen daño concreto y deben evitarse. Cada uno está documentado con el caso real donde apareció. +> +> A diferencia de los patrones (que son propuestos hasta validación), los anti-patrones se documentan estables porque el daño ya está observado. + +--- + +## AP.1 — Tests verdes como aprobación final + +**Comportamiento:** declarar una etapa terminada porque los tests pasaron, sin abrir el artefacto final con los ojos. + +**Daño concreto:** llegás a producción con outputs visuales rotos. Caso real (InQ ROI MVP): los tests E2E verificaban que el PDF se generaba y tenía las páginas esperadas, pero nadie abrió el PDF. El heatmap embebido estaba en blanco por un bug de timing en html2canvas. Se descubrió 2 días después cuando un consultor lo abrió. + +**Cómo detectarlo:** el reporte del agente dice "tests verdes, build limpio, PDF generado en X path" pero no incluye observaciones visuales del PDF. + +**Cómo evitarlo:** aplicar **Principio 2** (validación humana es ojo) y **Patrón C.1** (validación visual mandatoria). + +--- + +## AP.2 — Pasamanos invertido (humano como operador) + +**Comportamiento:** el humano traduce manualmente entre dos agentes o entre agente y código. Copia outputs, formatea, transporta contexto entre sesiones. + +**Daño concreto:** el humano se vuelve cuello de botella del sistema. Lo peor: deja de pensar en arquitectura/producto y se concentra en operación. Caso real (InQ ROI Sprint 1.5, Etapa 5): el director del proyecto sintió "soy pasamanos" después de varios checkpoints transportando reportes de Claude Code al arquitecto. + +**Cómo detectarlo:** preguntate si lo que estás haciendo lo podría hacer un script o un prompt mejor estructurado. Si la respuesta es sí, estás en este anti-patrón. + +**Cómo evitarlo:** aplicar **Principio 5** (humano director). Mover lo operativo a archivos (prompts pre-armados, templates) y reservar el chat para decisiones reales. + +--- + +## AP.3 — Racionalización de output anómalo + +**Comportamiento:** cuando un output del agente tiene una métrica anómala (PDF inusualmente liviano, número muy redondo, payback "0.0 meses"), el agente o el humano racionalizan ("numéricamente correcto", "es el caso esperado") en lugar de investigar la causa raíz. + +**Daño concreto:** bugs latentes que se descubren en producción. Caso real (InQ ROI Sprint 1.5, Etapa 4): el PDF de ROI pesaba 36 KB con 4 páginas (9 KB/página), comparado con 78 KB/3 páginas (26 KB/página) de los otros PDFs. El agente reportó "PDF generado". El humano paró y pidió investigar — había secciones que no se renderizaban. + +**Cómo detectarlo:** cualquier número que parezca "anómalo pero técnicamente válido". Especialmente: tamaños de archivo, payback inmediato, ROI extremo, métricas con muchos ceros. + +**Cómo evitarlo:** aplicar **Principio 3** (auditoría adversarial) y **Patrón E.3** (anti-racionalización explícita). + +--- + +## AP.4 — Decisión recurrente no registrada + +**Comportamiento:** el agente vuelve a preguntar lo mismo en cada sesión nueva, o miembros del equipo asumen distinto porque la decisión no está documentada. + +**Daño concreto:** outputs inconsistentes entre etapas, tiempo perdido re-debatiendo, deriva del producto. Caso real (InQ ROI Sprint 1.5, varias etapas): cada vez que Claude Code iba a aplicar formato de cifras, había que recordarle si usaba separador europeo o americano. Hasta que se documentó en `CLAUDE.md`. + +**Cómo detectarlo:** el agente te hace una pregunta que ya respondiste en alguna sesión anterior. + +**Cómo evitarlo:** aplicar **Principio 4** (decisiones como contratos) y **Patrón B.3** (decisiones tomadas explícitas en cada prompt). + +--- + +## AP.5 — Improvisación sin gobernanza + +**Comportamiento:** un equipo (o miembros individuales) usa agentes de IA sin método compartido. Cada uno improvisa con su herramienta preferida, sin briefs, sin validación estructurada, sin documentar decisiones. + +**Daño concreto:** +1. Deuda técnica invisible: artefactos producidos por IA que nadie puede auditar después +2. Aprendizaje no se acumula: descubrimientos individuales no se transfieren +3. Calidad inconsistente y no atribuible +4. Imposibilidad de mejorar sistemáticamente + +**Cómo detectarlo:** preguntá al equipo "¿cómo hicieron este artefacto con IA?" — si las respuestas son distintas y vagas, estás acá. + +**Cómo evitarlo:** este método entero está diseñado para evitarlo. Específicamente: empezar con **Principios 1 y 4**. + +--- + +## AP.6 — Brief grande pegado en chat + +**Comportamiento:** pegar un brief de 800+ líneas en el chat del agente como mensaje inicial. + +**Daño concreto:** +- Consume contexto innecesariamente +- No es auditable después +- Se pierde si la sesión se corta +- Cuesta tokens repetirlo + +Caso real (InQ ROI Sprint 1.5): el primer intento fue pegar el BRIEF.md completo en chat. Se reemplazó por la estructura `sprints/sprint-N/` con archivos versionados, mensaje inicial corto referenciando paths. + +**Cómo detectarlo:** si tu mensaje inicial al agente tiene más de 30 líneas, probablemente estás en este anti-patrón. + +**Cómo evitarlo:** aplicar **Patrón B.1** (prompt como artefacto del repo) y **Patrón B.2** (mensaje inicial corto). + +--- + +## Cómo agregar un anti-patrón + +Solo se agregan anti-patrones con daño concreto observado, no especulativo. Estructura obligatoria: + +1. **Nombre breve** (forma de "AP.N — Descripción") +2. **Comportamiento:** descripción de la acción problemática +3. **Daño concreto:** qué pasa cuando ocurre, con caso real citado +4. **Cómo detectarlo:** señal observable +5. **Cómo evitarlo:** principio o patrón que lo previene + +No agregar anti-patrones de "esto sería malo si pasara" — solo de cosas que **pasaron en proyectos reales** del equipo. diff --git a/methodology/CASE_STUDIES/inq-roi-sprint-1-5.md b/methodology/CASE_STUDIES/inq-roi-sprint-1-5.md new file mode 100644 index 0000000..af2e6d4 --- /dev/null +++ b/methodology/CASE_STUDIES/inq-roi-sprint-1-5.md @@ -0,0 +1,90 @@ +# Caso de estudio: InQ ROI Sprint 1.5 + +> Primer caso completo del método aplicado en producción. +> +> **Período:** Mayo 2026 +> **Producto:** InQ ROI — herramienta de cálculo de costos y ROI para procesos BPMN +> **Equipo:** 1 director técnico + Claude Code como agente de implementación +> **Duración real:** ~ [completar al cierre] +> **Sprint objetivo:** rediseño visual completo del producto con manual de marca InQ + +--- + +## Acceso al material + +Todos los artefactos del sprint están versionados en el repo del producto: + +- **Brief del sprint:** `sprints/sprint-1-5/BRIEF.md` +- **Prompts por etapa:** `sprints/sprint-1-5/ETAPA_X_PROMPT.md` +- **Reportes por etapa:** `sprints/sprint-1-5/ETAPA_X_REPORT.md` (cuando se capturen) +- **Audits:** `sprints/sprint-1-5/etapa-X-audit.md` +- **Screenshots:** `tests/screenshots/etapa-X/` +- **PDFs generados:** `tests/e2e/__output__/` + +--- + +## Estructura del sprint + +11 etapas en orden: + +| # | Etapa | Estado | Patrón principal aplicado | +|---|---|---|---| +| 1 | Setup del sistema de diseño | ✅ Cerrada | A.1, B.4 | +| 2 | Helper formatPayback + aplicación | ✅ Cerrada | C.3, E.1 | +| 3 | Rediseño web app con identidad InQ | 🔄 En curso | C.1, C.4 | +| 4 | PDF: header y footer compartidos | ⏳ Pendiente | | +| 5 | PDF: layout híbrido portrait + landscape | ⏳ Pendiente | | +| 6 | PDF: rediseño página 1 ROI | ⏳ Pendiente | | +| 7 | PDF: páginas 3 y 4 ROI | ⏳ Pendiente | | +| 8 | PDFs de Actual y Automatizado | ⏳ Pendiente | | +| 9 | Badge ⚡ en BpmnCanvas | ⏳ Pendiente | | +| 10 | Validación E2E final | ⏳ Pendiente | | +| 11 | Documentación + Deploy | ⏳ Pendiente | | + +--- + +## Lecciones documentadas + +### Patrones validados durante este caso + +[Se completa al cierre del sprint con la lista de patrones que se validaron en práctica] + +### Anti-patrones cazados durante este caso + +- **AP.1 — Tests verdes como aprobación final**: caso del PDF de heatmap embebido en blanco (origen en MVP previo al sprint) +- **AP.2 — Pasamanos invertido**: notado por el director en Etapa 5, llevó a discusión meta sobre el método +- **AP.3 — Racionalización de output anómalo**: PDF de 36 KB con 4 páginas (Etapa 4 inicial) +- **AP.6 — Brief grande pegado en chat**: corregido a estructura `sprints/sprint-N/` + +### Decisiones arquitectónicas memorables + +- Heatmap saturado en lugar de pastel (decisión consciente vs propuesta inicial) +- Layout híbrido portrait + landscape en PDFs (resolución del problema "diagramas BPMN no entran en A4 vertical") +- font-weight 700 en CTAs para remediación WCAG parcial +- Carátula del PDF eliminada — info integrada en header de página 1 + +### Decisiones de producto memorables + +- Posicionamiento como herramienta interna de InQuality (no producto independiente) +- Identidad propia (naranja) diferenciada de CONCILIA (morado) manteniendo familia visual +- Regla de marca no negociable: "InQ" siempre I-n-Q +- Detector de ROI inusual (>1000%) como feature de transparencia + +--- + +## Métricas finales del sprint + +[Completar al cierre] + +- Cantidad de tests al inicio del sprint: 440 +- Cantidad de tests al cierre: [N] +- Tamaño promedio de PDF inicial: [KB] +- Tamaño promedio de PDF final: [KB] +- Cantidad de screenshots de validación capturados: [N] +- Cantidad de iteraciones de mockup: [N] + +--- + +## Reflexión final + +[Completar al cierre del sprint con observaciones del director sobre qué funcionó, qué no, y qué cambiaríamos para el próximo proyecto] diff --git a/methodology/PATTERNS.md b/methodology/PATTERNS.md new file mode 100644 index 0000000..88d939c --- /dev/null +++ b/methodology/PATTERNS.md @@ -0,0 +1,253 @@ +# Patrones operativos + +> Aplicaciones concretas de los 5 principios. Estructurados por categoría operativa. +> +> Estado de cada patrón: **propuesto** (validado en 1 proyecto) o **estable** (validado en 2+ proyectos). +> +> A la fecha, todos los patrones aquí son propuestos. Caso de validación: InQ ROI Sprint 1.5. + +--- + +## Categoría A — Disciplina antes de codear + +### A.1 Brief versionado antes que código +**Estado:** propuesto · **Deriva de:** Principio 1 + +Todo sprint o cambio mayor arranca con un brief en `sprints/sprint-N/BRIEF.md` antes de pedirle algo a un agente. El brief define alcance, decisiones tomadas, criterios de éxito y lo que NO entra. + +**Indicador de aplicación correcta:** podés iniciar una sesión nueva con Claude Code pasándole solo el path del brief y CLAUDE.md, sin contexto adicional, y empieza a trabajar correctamente. + +**Indicador de violación:** el agente te hace preguntas que ya están respondidas en otro lugar (o peor, no las hace y asume). + +--- + +### A.2 Mockup como contrato visual +**Estado:** propuesto · **Deriva de:** Principio 1 + +Si el cambio es visual, antes de implementarlo se construye un mockup interactivo (HTML, Figma, Visualizer) que el humano aprueba. El mockup es el contrato: el agente lo replica, no lo interpreta libremente. + +**Indicador de aplicación correcta:** cuando ves el output del agente, podés compararlo lado a lado con el mockup y la diferencia es marginal. + +**Indicador de violación:** llegás a Etapa N de un rediseño visual y descubrís que el output "no tiene nada que ver" con lo que esperabas. + +--- + +### A.3 Roadmap explícito antes de arrancar +**Estado:** propuesto · **Deriva de:** Principios 1 y 4 + +Todo sprint con más de 3 etapas tiene un roadmap explícito desde el inicio. Cada etapa tiene **expectativa calibrada** de qué cambia y qué NO cambia respecto a la anterior. + +**Indicador de aplicación correcta:** después de cada checkpoint, el resultado coincide con lo que esperabas ver, no te sorprende ni para bien ni para mal. + +**Indicador de violación:** te sorprende negativamente un output intermedio porque pensabas que ya tenía X feature que en realidad está agendado 3 etapas después. + +--- + +## Categoría B — Operación con agentes + +### B.1 Prompt como artefacto del repo +**Estado:** propuesto · **Deriva de:** Principios 4 y 5 + +Cada etapa de trabajo agéntico tiene su prompt en `sprints/sprint-N/ETAPA_X_PROMPT.md`, versionado. El chat con el agente es **puente** entre el prompt y el reporte, no contenedor de la decisión. + +**Por qué:** los prompts en archivo son auditables, editables antes de ejecutar, replicables, y no se pierden al cerrar la sesión. + +**Estructura mínima de un prompt de etapa:** +1. Objetivo +2. Pre-requisitos / contexto previo +3. Decisiones ya tomadas (no consultar de nuevo) +4. Tareas concretas +5. Entregables (DoD checklist) +6. Qué reportar al final +7. Qué NO hacer en esta etapa + +--- + +### B.2 Mensaje inicial corto, contexto en archivos +**Estado:** propuesto · **Deriva de:** Principio 5 + +El mensaje al agente al arrancar una sesión es de 3-5 párrafos, NO el prompt entero pegado. Estructura: +1. "Procedé con etapa X" +2. Archivos a leer en orden +3. Confirmaciones que esperás antes de que empiece +4. Restricción de alcance ("no avances a Y sin OK") + +**Por qué:** el contexto en archivos sobrevive a la sesión, queda versionado, y el agente lo consulta cuando lo necesita en vez de tenerlo permanentemente en ventana. + +--- + +### B.3 Decisiones tomadas como sección explícita +**Estado:** propuesto · **Deriva de:** Principio 4 + +En cada prompt de etapa, incluir sección "DECISIONES YA TOMADAS — no consultar de nuevo" con las definiciones cerradas. Esto impide que el agente vuelva a preguntar lo ya resuelto. + +**Ejemplo:** +``` +DECISIONES YA TOMADAS (no consultar de nuevo) +- Payback negativo → "Sin payback (costo aumenta)" +- Caracteres compactos: "<" y "~" +- Pluralización: siempre plural ("1.0 meses") +``` + +--- + +### B.4 Reglas no negociables en CLAUDE.md +**Estado:** propuesto · **Deriva de:** Principio 4 + +Las reglas que aplican a TODO el proyecto van en `CLAUDE.md`, no solo en briefs específicos. El agente carga CLAUDE.md en cada sesión nueva por defecto. + +**Ejemplo de qué va ahí:** +- Reglas de marca (cómo se escribe el nombre, qué colores) +- Restricciones de comportamiento (qué no decir, qué no asumir) +- Verificaciones obligatorias pre-entrega + +--- + +## Categoría C — Validación de output + +### C.1 Validación visual mandatoria del artefacto final +**Estado:** propuesto · **Deriva de:** Principio 2 + +Si el output de la etapa es un artefacto visible (PDF, screenshot, página renderizada, documento), el humano lo abre con sus ojos antes del OK. Tests verdes no es suficiente. + +**Tooling sugerido:** +- PDFs/imágenes: Preview en Mac, visor PDF en Linux +- Páginas web: dev server en navegador +- Documentos: editor que renderice (no solo source) + +--- + +### C.2 Validación programática antes de declarar completado (agente) +**Estado:** propuesto · **Deriva de:** Principios 2 y 3 + +Antes de declarar una etapa completada, el agente debe ejecutar validaciones programáticas, no solo confiar en tests unitarios. Para outputs visuales: +- Análisis de píxeles (¿están los colores esperados?) +- Text extraction (¿están las palabras clave?) +- Tamaño de archivo (¿es razonable?) +- Cantidad de páginas (¿coincide con el diseño?) + +**Por qué:** un test unitario puede pasar mientras el output final está roto. La validación programática del artefacto cierra ese gap. + +--- + +### C.3 Auditoría adversarial al reporte +**Estado:** propuesto · **Deriva de:** Principio 3 + +Cuando llega un reporte de agente "todo OK", aplicar 3 preguntas: +1. ¿Los números reportados cuadran si recalculo o pido dump? +2. ¿El artefacto es el que pedí? +3. ¿Lo que no menciona podría ser relevante? + +**Casos típicos de detección:** +- PDF de 36 KB con 4 páginas (es muy poco → algo no se renderizó) +- KPI con valor "0.0 meses" (numéricamente correcto pero confuso) +- Reporte que dice "PDF generado" sin abrir el PDF generado + +--- + +### C.4 Reporte con datos concretos, no descripciones (agente) +**Estado:** propuesto · **Deriva de:** Principio 3 + +Los reportes del agente deben incluir métricas concretas, no descripciones genéricas. + +**Mal reporte:** "PDF generado correctamente, contiene todo lo esperado." + +**Buen reporte:** +``` +PDF generado: 78 KB, 4 páginas +- Página 1: KPI Hero + 4 cards secundarios + top 3 ahorros +- Página 2: tabla con 11 filas + footer +- Página 3: 3 secciones (composición, trayectoria, transparencia) +- Página 4: nota metodológica +Análisis de píxeles del heatmap: verde 36.329px, ámbar 3.889px, rojo 3.713px +Text extraction confirma palabras clave: "InQ ROI", "Powered by InQuality", "Payback" +``` + +--- + +## Categoría D — Estructura del repo + +### D.1 Carpeta `sprints/` para activos de planificación +**Estado:** propuesto · **Deriva de:** Principio 4 + +Cada sprint tiene su carpeta `sprints/sprint-N/` con todos sus artefactos: +- `BRIEF.md` +- `ETAPA_X_PROMPT.md` (uno por etapa) +- `ETAPA_X_REPORT.md` (opcional, reporte del agente capturado) +- `etapa-X-audit.md` (cuando aplica) +- `screenshots/` con evidencia visual + +**Por qué:** en 6 meses, abrir esa carpeta te da el historial completo de cómo se construyó el sprint. Sin esto, el conocimiento queda en commits + memoria, que es frágil. + +--- + +### D.2 Carpeta `methodology/` para el método en sí +**Estado:** propuesto · **Deriva de:** Principio 4 + +El método de trabajo vive en `methodology/`, separado del producto. Esto permite que el método evolucione independiente de cualquier producto específico. + +--- + +### D.3 ADRs para decisiones arquitectónicas +**Estado:** propuesto · **Deriva de:** Principio 4 + +Las decisiones arquitectónicas importantes se registran como ADR (Architectural Decision Record) en `ARCHITECTURE.md` o `docs/adr/`. Estructura mínima: +- Contexto (qué problema teníamos) +- Decisión (qué elegimos) +- Alternativas consideradas +- Consecuencias (qué implica) + +--- + +## Categoría E — Comunicación humano ↔ agente + +### E.1 Separar lo validado de lo asumido +**Estado:** propuesto · **Deriva de:** Principios 2 y 3 + +En los reportes del agente, marcar explícitamente qué fue verificado vs qué se asume funcionando. + +**Ejemplo:** +``` +Verificado: +- Tests verdes (455/455) +- PDF generado y abierto con pdf-parse +- Análisis de píxeles confirma heatmap completo + +Asumido: +- El componente se renderiza igual en Firefox (solo testeado en Chromium) +- La fuente Roboto está disponible en el sistema cliente (no validado) +``` + +--- + +### E.2 Hallazgos no esperados como sección explícita +**Estado:** propuesto · **Deriva de:** Principios 3 y 5 + +Al final de cada etapa, el agente reporta "hallazgos no esperados" — cosas que descubrió durante la implementación que merecen decisión arquitectónica, aunque no formen parte del DoD. + +**Ejemplo:** "El cambio de `--primary` propagó a focus rings de inputs (no estaba en el plan). Decisión necesaria: ¿se mantiene o se vuelve a slate?" + +--- + +### E.3 Anti-racionalización explícita +**Estado:** propuesto · **Deriva de:** Principio 3 + +Cuando un test revela algo raro, un cálculo no cuadra, o un output difiere de lo esperado: **investigar causa raíz, no minimizar**. + +Patrón típico de violación: el agente dice "el cálculo es numéricamente correcto" cuando lo que está reportando es un problema de presentación (ej. "0.0 meses" es matemáticamente correcto pero un bug de UX). + +--- + +## Cómo agregar un patrón nuevo + +1. Identificar el patrón durante un proyecto real (no inventarlo en abstracto). +2. Documentarlo con: nombre, categoría, principio del que deriva, descripción, indicador de aplicación correcta, indicador de violación. +3. Marcarlo como **propuesto** con cita del proyecto donde apareció. +4. Después de aplicarlo en un 2do proyecto distinto, promoverlo a **estable**. + +## Cómo retirar un patrón + +Si un patrón no se sostiene en práctica o tiene falsos positivos frecuentes, se retira con un PR que documente: +- Por qué no funcionó +- Qué alternativa probamos +- Si hay un patrón nuevo que lo reemplaza diff --git a/methodology/PRINCIPLES.md b/methodology/PRINCIPLES.md new file mode 100644 index 0000000..21b4cff --- /dev/null +++ b/methodology/PRINCIPLES.md @@ -0,0 +1,90 @@ +# Principios fundamentales + +> Los 5 principios que sostienen todo el método. Si tu equipo solo recuerda esto, ya tiene 80% del valor. Los patrones son aplicaciones derivadas. + +--- + +## 1. La especificación precede al código + +Antes de pedirle a un agente que genere código, **la especificación tiene que estar escrita, versionada y validada**. No hay excepciones. + +**Por qué:** un agente sin especificación genera código que parece razonable pero no resuelve el problema que vos tenés. Termina rápido, queda mal, y descubrís el desfase 3 días después cuando es caro arreglarlo. + +**Cómo se aplica:** todo sprint arranca con un brief versionado en `sprints/sprint-N/BRIEF.md`. Todo cambio importante dentro de un sprint arranca con un mockup o ADR. Todo tarea agéntica arranca con un prompt en archivo, no improvisado en chat. + +**Cuándo NO aplica:** experimentación pura sin entregable. Si estás explorando si una librería sirve, podés hacer prompts libres. Pero el output de esa experimentación nunca va directo a producción. + +--- + +## 2. La validación humana es ojo, no test + +Los tests verdes no equivalen a etapa terminada. **El humano abre el artefacto final con sus ojos antes de declarar completado.** + +**Por qué:** los agentes producen outputs que técnicamente cumplen los criterios programáticos pero pierden el contexto del producto. Tests verdes garantizan que el código no falla. Validación visual garantiza que el output es presentable. + +**Cómo se aplica:** si el output es un archivo visible (PDF, screenshot, página web, documento), el humano lo abre. Si el output es código, el humano lo lee al menos en diagonal. Si el output es comportamiento (un endpoint que responde algo), el humano lo ejecuta manualmente al menos una vez antes del merge. + +**Cuándo NO aplica:** outputs puramente internos (helpers, refactors estructurales) que se validan exclusivamente por tests. Pero estos casos son minoría. + +--- + +## 3. La auditoría es adversarial, no confirmatoria + +Cuando revisás el output de un agente, **buscá lo que está mal**, no lo que está bien. Confirmar es fácil; el agente ya viene optimizado para producir cosas que parecen correctas. Tu valor está en detectar lo que el agente racionalizó. + +**Por qué:** los agentes tienen un sesgo hacia reportar éxito. "Tests verdes, build limpio, etapa completada" suena bien pero puede ocultar que el archivo generado no es el correcto, que la métrica no cuadra con los inputs, o que un detalle crítico se omitió silenciosamente. + +**Cómo se aplica:** cuando recibís un reporte de agente, hacé tres preguntas antes de aprobar: +1. ¿Los números que reportó cuadran si los recalculo a mano o pido detalle? +2. ¿El artefacto entregado es el que pedí o uno parecido? +3. ¿Lo que no menciona el reporte podría ser relevante? + +**Cuándo NO aplica:** tareas rutinarias de bajo riesgo donde el costo de auditar supera el costo de un error. Pero estas son menos de las que parecen. + +--- + +## 4. Las decisiones se registran como contratos + +Toda decisión arquitectónica, de marca, o de proceso se documenta en el lugar correcto **explícitamente**, no en la memoria de quien decidió. Cuando aparece de nuevo, no se re-debate. + +**Por qué:** las decisiones no registradas se erosionan. Un agente vuelve a preguntar lo mismo en la siguiente sesión, un miembro del equipo asume distinto, vos mismo en 2 semanas no recordás por qué eligieron X. El costo de re-debatir es alto y los outputs quedan inconsistentes. + +**Cómo se aplica:** +- Decisiones de producto → `CLAUDE.md` (reglas no negociables) o `ARCHITECTURE.md` (ADRs) +- Decisiones de método → `methodology/PATTERNS.md` +- Decisiones de etapa → "DECISIONES YA TOMADAS" en el prompt de la siguiente etapa +- Decisiones de marca → `DESIGN_SYSTEM.md` o `BRAND_GUIDE.md` + +**Cuándo NO aplica:** decisiones reversibles sin costo. Si elegís un nombre de variable, no lo documentes formalmente. + +--- + +## 5. El humano es director, no operador + +Tu trabajo es decidir **qué** se hace, **cuándo** se valida, y **si** lo entregado es aceptable. NO es traducir entre el agente y el código. Si te encontrás ejecutando comandos que el agente podría ejecutar, parar y delegarlos. + +**Por qué:** el humano operador es el cuello de botella del sistema. Cada minuto que pasás copiando-pegando, formateando outputs, o transportando contexto entre sesiones, es un minuto que no estás pensando en producto, arquitectura, o estrategia. El humano vale por su criterio, no por su throughput. + +**Cómo se aplica:** +- Si el prompt se puede pre-armar en archivo, hacelo. No lo tipees cada vez. +- Si la validación se puede automatizar (test, lint, screenshot), automatizala. +- Si la decisión es repetitiva (formato, nombre, paleta), documentala una vez y referenciala. +- Si te volvés "pasamanos" entre dos agentes, frená y revisá el flujo. + +**Cuándo NO aplica:** las primeras 2-3 veces que aplicás el método, la operación manual es parte del aprendizaje. Después, la operación es señal de fricción que hay que eliminar. + +--- + +## Cómo se relacionan los 5 principios + +``` +1. Especificación precede al código → evita falsos arranques +2. Validación humana es ojo → evita falsos cierres +3. Auditoría adversarial → evita aprobaciones engañosas +4. Decisiones como contratos → evita re-debate y deriva +5. Humano director, no operador → evita cuello de botella +``` + +Los principios 1-2 protegen las puntas (entrada y salida del trabajo). +Los principios 3-4 protegen la mitad (cómo evaluamos y registramos). +El principio 5 protege la sostenibilidad (que el método escale). diff --git a/methodology/README.md b/methodology/README.md new file mode 100644 index 0000000..eab2202 --- /dev/null +++ b/methodology/README.md @@ -0,0 +1,54 @@ +# Método de trabajo con agentes de IA + +> Versión de trabajo. Nombre definitivo pendiente. +> +> Estado: **borrador en construcción**. Los patrones se validan en proyectos reales antes de promoverse a versión estable. + +--- + +## Qué es esto + +Es el método interno de trabajo para construir software con agentes de IA (Claude Code y otros) manteniendo gobernanza, calidad y aprendizaje acumulado. + +**Hipótesis central:** los agentes de IA permiten producir software 5-10× más rápido que un desarrollador solo, pero **sin gobernanza producen deuda invisible que cuesta más caro de la que se ahorra**. Este método es el sistema de gobernanza. + +## Qué NO es + +- No es un manual de Claude Code. No enseña a usar la herramienta. +- No es una metodología ágil reemplazante. Es complementario a Scrum/Kanban/lo que uses. +- No es opcional dentro de InQuality cuando el output va a cliente o producción. + +## Para quién + +- Desarrolladores con experiencia técnica (programadores, ingenieros, analistas) que ya entienden conceptos de software (commits, tests, refactor, deuda técnica). +- Líderes técnicos que orquestan equipos pequeños (2-10 personas) trabajando con agentes. + +## Cómo se usa este repositorio + +Lectura en orden recomendado: + +1. **PRINCIPLES.md** — los 5 principios fundamentales. Es lo único memorizado obligatorio. +2. **PATTERNS.md** — los patrones operativos derivados. Es referencia, no memoria. +3. **ANTI_PATTERNS.md** — qué evitar y por qué. Útil para detectar problemas en flagrante. +4. **TEMPLATES/** — scaffolds reutilizables para no empezar de cero. +5. **CASE_STUDIES/** — proyectos reales aplicando el método. El primero es InQ ROI Sprint 1.5. + +## Cómo contribuir + +Si descubrís un patrón nuevo o un anti-patrón: + +1. Documentalo en su archivo correspondiente con la estructura existente +2. Citá el contexto real donde lo encontraste (proyecto + sprint) +3. Pull request con revisión de al menos otra persona del equipo + +Los patrones se validan en **al menos 2 proyectos distintos** antes de promoverse de "propuesto" a "estable". + +## Estado actual de madurez + +| Sección | Estado | Próxima revisión | +|---|---|---| +| Principios | Borrador inicial — 5 derivados de InQ ROI | Después de Sprint 2 con otro proyecto | +| Patrones | 12 identificados, marcados como propuestos | Promover a estable los que se apliquen en 2do proyecto | +| Anti-patrones | 5 identificados, todos en flagrante real | Estables — son daño documentado | +| Templates | 3 disponibles (Brief, Prompt etapa, Reporte) | Iterar con uso real | +| Case studies | 1 en construcción (InQ ROI Sprint 1.5) | — | diff --git a/methodology/TEMPLATES/BRIEF_TEMPLATE.md b/methodology/TEMPLATES/BRIEF_TEMPLATE.md new file mode 100644 index 0000000..c337aa5 --- /dev/null +++ b/methodology/TEMPLATES/BRIEF_TEMPLATE.md @@ -0,0 +1,83 @@ +# Sprint N — [Nombre descriptivo] + +> **Propósito:** [una oración que describe qué cambia con este sprint] +> +> **Justificación:** [por qué ahora, qué costo evita o qué oportunidad captura] + +--- + +## 0. Contexto crítico + +[Cualquier información del producto, marca, o cliente que aplica transversalmente a todas las etapas. Reglas no negociables que el agente debe respetar.] + +--- + +## 1. Alcance del sprint + +### Sí entra +- [Feature o cambio 1] +- [Feature o cambio 2] +- ... + +### NO entra +- [Cosa que podría parecer en alcance pero NO] +- [Feature postergada con razón] +- ... + +--- + +## 2. Decisiones tomadas previamente + +| Decisión | Fuente / Justificación | +|---|---| +| [Decisión 1] | [Sprint o documento donde se decidió] | +| [Decisión 2] | [...] | + +--- + +## 3. Roadmap del sprint + +Lista ordenada de etapas con expectativa calibrada de cada una: + +| Etapa | Qué hace | Output esperado al final | +|---|---|---| +| 1 | [Descripción breve] | [Qué se ve, qué se mide] | +| 2 | [...] | [...] | +| ... | | | + +--- + +## 4. Definition of Done del sprint completo + +``` +[ ] [Criterio mensurable 1] +[ ] [Criterio mensurable 2] +[ ] [Tests verdes esperados N] +[ ] [Validación visual con usuario] +[ ] [Documentación actualizada] +[ ] [Tag de release X] +``` + +--- + +## 5. Archivos que se crean / modifican (preview) + +### Nuevos +- [Archivo + propósito] + +### Modificados +- [Archivo + qué cambia en él] + +--- + +## 6. Lo que NO entra en este sprint (postergado) + +- [Cosa importante que considerada y rechazada para este sprint, con justificación] + +--- + +## Notas finales para el agente + +- [Recordatorios específicos del sprint] +- [Cosas con las que el agente puede confundirse, aclaradas] +- [Reglas operativas que aplican durante el sprint] diff --git a/methodology/TEMPLATES/ETAPA_PROMPT_TEMPLATE.md b/methodology/TEMPLATES/ETAPA_PROMPT_TEMPLATE.md new file mode 100644 index 0000000..f6da868 --- /dev/null +++ b/methodology/TEMPLATES/ETAPA_PROMPT_TEMPLATE.md @@ -0,0 +1,105 @@ +# Sprint N — Etapa X: [Nombre descriptivo de la etapa] + +> **Objetivo:** [una oración que describe qué hace esta etapa específica] +> +> **Pre-requisitos:** [qué tiene que estar listo antes de arrancar; típicamente etapas previas] +> +> **Etapa siguiente:** [qué viene después, para que el agente NO arranque eso sin OK] + +--- + +## 1. Pre-tareas (si aplica) + +[Actualizaciones a artefactos previos antes de la implementación. Por ejemplo: actualizar un audit, completar un documento de Etapa anterior.] + +### 1.1 [Pre-tarea 1] + +[Descripción concreta de qué hacer] + +### 1.2 [Pre-tarea 2] + +[...] + +--- + +## 2. Decisiones ya tomadas (no consultar de nuevo) + +| Decisión | Justificación | +|---|---| +| [Decisión 1] | [Por qué se eligió esto y no la alternativa] | +| [Decisión 2] | [...] | + +--- + +## 3. Tarea principal + +### 3.1 [Subtarea 1] + +**Ubicación:** [archivo o módulo] + +**Descripción concreta:** [qué se implementa] + +**Snippet de referencia** (si aplica): +```typescript +// código de referencia +``` + +### 3.2 [Subtarea 2] + +[...] + +--- + +## 4. Tests obligatorios + +### 4.1 Tests unitarios + +[Lista de casos a cubrir, con inputs y outputs esperados] + +### 4.2 Tests E2E o visuales (si aplica) + +[Lo que requiere validación end-to-end] + +--- + +## 5. Aplicación en múltiples lugares (si aplica) + +[Si la tarea involucra cambios en múltiples capas, listarlas explícitamente] + +a) [Capa 1]: [qué cambia] +b) [Capa 2]: [qué cambia] +c) [Capa 3]: [qué cambia] + +--- + +## 6. Entregables (Definition of Done) + +``` +[ ] [Criterio 1 — mensurable y específico] +[ ] [Tests pasando: N esperados] +[ ] [Build limpio] +[ ] [Validación visual con artefactos concretos] +[ ] [Documentación actualizada si aplica] +``` + +--- + +## 7. Reporte final a entregar + +Cuando termines, devolveme: + +1. [Métrica concreta 1 — ej: tabla de tests con resultados] +2. [Artefacto visible 1 — ej: screenshot del componente] +3. [Artefacto visible 2 — ej: PDF presentado con present_files] +4. [Cantidad final de algo mensurable] +5. **Hallazgos no esperados** — cualquier descubrimiento que merezca decisión arquitectónica + +--- + +## 8. Reglas operativas (recordatorios) + +1. **No declares etapa terminada sin validación visual** del artefacto final. +2. **No racionalizar problemas** — si algo no cuadra, investigar causa raíz. +3. **No tocar [lo que esta etapa NO debe tocar]** — está agendado para Etapa Y. +4. **No avanzar a Etapa siguiente sin OK explícito.** +5. **Si encontrás contradicción** entre este prompt y la realidad del código (un archivo no está donde decía, una función cambió de signature, etc.): **PARÁ y consultá**, no asumas. diff --git a/methodology/TEMPLATES/REPORT_TEMPLATE.md b/methodology/TEMPLATES/REPORT_TEMPLATE.md new file mode 100644 index 0000000..2d9c73e --- /dev/null +++ b/methodology/TEMPLATES/REPORT_TEMPLATE.md @@ -0,0 +1,100 @@ +# Sprint N — Etapa X: Reporte final + +> Reporte capturado al cierre de la etapa. Útil para auditoría y para reconstruir contexto en sesiones futuras. +> +> Generado por el agente, validado por el humano antes del commit. + +--- + +## 1. Resumen de cambios + +### Archivos creados/modificados + +| Archivo | Acción | Líneas | Propósito | +|---|---|---|---| +| [path] | Creado/Modificado | +N/-M | [descripción breve] | + +--- + +## 2. Tests + +### Resultados + +- Tests previos: N (sin regresión) +- Tests nuevos: M +- Total: N+M verdes + +### Tests nuevos agregados + +| Test | Caso | Resultado | +|---|---|---| +| [nombre] | [input → output esperado] | ✅ | + +--- + +## 3. Artefactos visibles generados + +| Artefacto | Tamaño | Validación | Path | +|---|---|---|---| +| [PDF / screenshot / archivo] | XX KB | ✓ visualizado | [path] | + +--- + +## 4. Verificación programática (si aplica) + +### Análisis de píxeles / contenido / métricas + +[Resultados concretos: qué se midió y qué dio] + +### Text extraction (si aplica) + +[Palabras clave esperadas confirmadas en el output] + +--- + +## 5. Validado vs Asumido + +### Verificado +- [Lista de cosas efectivamente comprobadas] + +### Asumido (no validado en esta etapa) +- [Lista de cosas que se asumen funcionando pero no se probaron explícitamente] + +--- + +## 6. Hallazgos no esperados + +[Cualquier descubrimiento durante la implementación que merezca decisión arquitectónica, aunque no esté en el DoD] + +- **Hallazgo 1:** [descripción] + - Impacto: [cuál] + - Decisión requerida: [qué tiene que decidir el humano] + - Sugerencia del agente: [recomendación] + +--- + +## 7. Patrones aprendidos (cosecha para methodology/) + +[Si durante esta etapa surgió un patrón nuevo o se validó uno existente, registralo acá. El humano decide si pasa a `methodology/PATTERNS.md`.] + +- **Patrón candidato:** [descripción] + - Categoría: [A/B/C/D/E] + - Deriva de: [Principio N] + - Indicador de aplicación correcta: [...] + - Indicador de violación: [...] + +--- + +## 8. Definition of Done — checklist final + +``` +[x] Criterio 1 cumplido +[x] Criterio 2 cumplido +[ ] Criterio 3 — pendiente porque [razón] +``` + +--- + +## 9. Próximo paso + +[Qué viene después: etapa siguiente, decisión pendiente del humano, etc.] diff --git a/sprints/sprint-1-5/BRIEF.md b/sprints/sprint-1-5/BRIEF.md new file mode 100644 index 0000000..457ece0 --- /dev/null +++ b/sprints/sprint-1-5/BRIEF.md @@ -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 `` 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. \ No newline at end of file diff --git a/sprints/sprint-1-5/ETAPA_1_PROMPT.md b/sprints/sprint-1-5/ETAPA_1_PROMPT.md new file mode 100644 index 0000000..5852c0c --- /dev/null +++ b/sprints/sprint-1-5/ETAPA_1_PROMPT.md @@ -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. \ No newline at end of file diff --git a/sprints/sprint-1-5/ETAPA_2_PROMPT.md b/sprints/sprint-1-5/ETAPA_2_PROMPT.md new file mode 100644 index 0000000..1cc5ff9 --- /dev/null +++ b/sprints/sprint-1-5/ETAPA_2_PROMPT.md @@ -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. \ No newline at end of file diff --git a/sprints/sprint-1-5/SPRINT_1_5_BRIEF_REDISENIO_VISUAL.md b/sprints/sprint-1-5/SPRINT_1_5_BRIEF_REDISENIO_VISUAL.md new file mode 100644 index 0000000..7947e7f --- /dev/null +++ b/sprints/sprint-1-5/SPRINT_1_5_BRIEF_REDISENIO_VISUAL.md @@ -0,0 +1,852 @@ +# 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) +```css +/* Bajo → Medio → Alto */ +background: linear-gradient(to right, #BBF7D0, #FED7AA, #FECACA); +``` + +#### 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 (página 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 `` 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 + +--- + +## 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 (ver Etapa 2 - cambio de bpmn-js right) +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 (!isFinite(months)) return "No recupera en horizonte"; + 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" + +--- + +## 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) +``` + +### 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` +- `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 + +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 (ya identificamos este riesgo en checkpoint anterior) + +### 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` (≥10 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 sugerido de implementación + +Para que sea incremental y validable en cada paso: + +1. **Setup del sistema de diseño** + - Crear `DESIGN_SYSTEM.md` + - Extender `tailwind.config.ts` con tokens InQ + - Definir variables CSS en `index.css` + - Personalizar componentes shadcn (button, tabs, switch) + +2. **Helper de formato + tests** + - Implementar `formatPayback` con 10 tests + - Aplicar en `RoiKpiBar` (web) + +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 + - Validación visual con usuario + +4. **PDF: header y footer compartidos** + - Crear `pdf-sections-shared.ts` + - Refactorizar PDFs existentes para usar header/footer comunes + - Aplicar paleta InQ + +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. **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. **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. **PDFs de Actual y Automatizado** + - Aplicar mismo sistema + - Verificar consistencia visual + +9. **Badge ⚡ en BpmnCanvas** + - Color naranja sólido + - Posición robusta (top + left) + - Test del overlay + +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. **Documentación** + - 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. diff --git a/sprints/sprint-1-5/etapa-1-audit.md b/sprints/sprint-1-5/etapa-1-audit.md new file mode 100644 index 0000000..9c871cb --- /dev/null +++ b/sprints/sprint-1-5/etapa-1-audit.md @@ -0,0 +1,116 @@ +# Auditoría de colores — Etapa 1 Sprint 1.5 + +> Hallazgos del audit de azules, hardcodes y `primary` en el código fuente. +> **No se resuelven en Etapa 1.** Las acciones se ejecutan en Etapas 3 (Web App) y 4-8 (PDFs). +> +> Fecha: 2026-05-16 · Ejecutado por: Claude Code +> Comandos: `grep -rn "blue|sky|indigo|3B82F6|2563EB" src/` + `grep -rn "#[0-9a-fA-F]{6}" src/` + +--- + +## Categorías + +- **Migrar a InQ orange**: color azul que debería ser naranja de marca +- **Mantener azul funcional**: azul donde es convención semántica (info, links, paralelo AND) +- **Migrar a neutral**: azul accidental que debería ser slate/gray +- **Migrar a token**: hex hardcodeado que tiene un token equivalente en tailwind.config.js + +--- + +## Hallazgos — Azules en clases Tailwind + +| Archivo:línea | Clase actual | Categoría | Acción sugerida | +|---|---|---|---| +| `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 + +| Archivo:línea | Color actual | Categoría | Acción sugerida | +|---|---|---|---| +| `pdf-sections.ts:9` | `blue: [30, 64, 175]` (`#1E40AF`) | **Migrar a InQ orange** | Header de tabla PDF usa azul corporativo. Reemplazar por naranja InQ `[245, 152, 69]` (`#F59845`) en Etapa 4 (PDF: header y footer). | +| `pdf-sections.ts:112` | `PDF.blue` en KPI card accent | **Migrar a InQ orange** | El KPI "Costo total" usa azul como accent. Cambiar a naranja InQ en Etapa 6 (PDF: rediseño página 1). | +| `pdf-export.ts:120` | `fillColor: PDF.blue` en autotable | **Migrar a InQ orange** | Header de tabla de costos en azul. Migrar en Etapa 7 (PDF: páginas 3-4). | +| `pdf-sections-roi.ts:203` | `fillColor: PDF.blue` en autotable | **Migrar a InQ orange** | Header de tabla comparativa ROI. Migrar en Etapa 7. | + +## Hallazgos — Hex hardcodes en componentes React + +| Archivo:línea | Hex | Descripción | Categoría | Acción sugerida | +|---|---|---|---|---| +| `BpmnCanvas.tsx:24` | `#f59e0b` | Stroke del ícono Bot en SVG inline | **Migrar a InQ orange** | En Etapa 9 el badge ⚡ cambia a fondo naranja sólido `#F59845`. El stroke del SVG se rediseña en esa etapa. | +| `BpmnCanvas.tsx:40` | `#fffbeb` | Fondo del badge Bot (amber-50) | **Migrar a InQ orange** | Reemplazar por `var(--inq-orange-lighter)` `#FFF8ED` en Etapa 9. | +| `BpmnCanvas.tsx:41` | `#e2e8f0` | Borde del badge Bot | **Mantener neutral** | Borde sutil, está bien como slate-200. | +| `CumulativeSavingsChart.tsx:32,34` | `#10b981` | Color del marker de payback | **Migrar a token** | Verde éxito = `var(--color-success)` `#16A34A`. Migrar en Etapa 3. | +| `CumulativeSavingsChart.tsx:55` | `#10b981`, `#ef4444` | Colores del tooltip (positivo/negativo) | **Migrar a token** | Usar `--color-success` y `--color-danger`. Etapa 3. | +| `CumulativeSavingsChart.tsx:91,102` | `#3b82f6` | Línea y punto del chart acumulado | **Migrar a InQ orange** | La línea del gráfico de trayectoria debería ser naranja InQ `#F59845` para coherencia de marca. Etapa 3. | +| `HeatmapCanvas.tsx:118` | `#f8fafc` | Background del canvas | **Mantener neutral** | slate-50 es correcto para fondo del diagrama BPMN. | +| `TopSavingsChart.tsx:73,79` | `#10b981` | Barras y labels del chart | **Migrar a token** | Verde éxito correcto. Usar `--color-success` vía `style={{ color: 'var(--color-success)' }}`. Etapa 3. | +| `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** | 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. | +| `chart-options.ts:136` | `#1e293b` | Fill del label de total en donut | **Mantener neutral** | slate-800, correcto para texto. | + +## Hallazgos — `primary` en JSX + +| Archivo | Usos | Evaluación | +|---|---|---| +| `router.tsx`, `WorkspacePage.tsx` | `text-primary` spinner | ✅ Correcto — spinner naranja InQ ahora | +| `ResourcesPanel.tsx:101` | `border-primary/20 bg-primary/5` | ✅ Correcto — borde/fondo naranja sutil | +| `RoiKpiBar.tsx:88-89` | `text-primary`, `bg-primary/10` | ✅ Correcto — ícono Calendar naranja | +| `KpiBar.tsx:46-47` | `text-primary`, `bg-primary/10` | ✅ Correcto — ícono DollarSign naranja | +| `ActivitiesTable.tsx:22-23` | `text-primary` arrows | ✅ Correcto — flechas de ordenamiento naranjas | +| `ActivitiesTable.tsx:96` | `bg-primary/5 ring-primary/20` | ✅ Correcto — highlight de fila naranja | +| `ImportPage.tsx:173,198-199,205,210-211,252,255-258` | `text-primary`, `bg-primary/*`, `border-primary` | ✅ Correcto — AccentS de la landing page naranjas | +| `SampleProcessLoader.tsx:77,83-84,92` | `hover:border-primary/30`, `bg-primary/10`, `text-primary` | ✅ Correcto | +| `WorkspacePage.tsx:178,243` | `text-primary border-primary/30`, `bg-primary` | ✅ Correcto | +| `slider.tsx` | `bg-primary/20`, `bg-primary`, `border-primary/50` | ✅ Correcto — slider naranja | +| `tooltip.tsx` | `bg-primary text-primary-foreground` | ✅ Correcto — tooltip naranja | +| `switch.tsx` | `bg-primary` when checked | ✅ Correcto — switch naranja | +| `button.tsx` | `bg-primary text-primary-foreground` default | ✅ Correcto — botón naranja | +| `badge.tsx` | `bg-primary text-primary-foreground` | ✅ Correcto — badge naranja | + +--- + +## Resumen de acciones pendientes + +| Etapa | Archivos afectados | Acción | +|---|---|---| +| 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`, `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. diff --git a/src/components/AppFooter.tsx b/src/components/AppFooter.tsx index 958231c..261f76f 100644 --- a/src/components/AppFooter.tsx +++ b/src/components/AppFooter.tsx @@ -2,7 +2,7 @@ export function AppFooter() { return (

- Process Cost Platform · v0.1.0 · Hecho desde Paraguay 🇵🇾 + InQ ROI · Powered by InQuality · v0.1.0 · Hecho desde Paraguay 🇵🇾

) diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 62234d4..8994d57 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -8,7 +8,7 @@ const buttonVariants = cva( { variants: { variant: { - default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90 font-bold', destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx new file mode 100644 index 0000000..1112fd7 --- /dev/null +++ b/src/components/ui/switch.tsx @@ -0,0 +1,36 @@ +import { cn } from '@/lib/utils' + +interface SwitchProps { + checked: boolean + onCheckedChange: (checked: boolean) => void + id?: string + disabled?: boolean + 'aria-label'?: string +} + +export function Switch({ checked, onCheckedChange, id, disabled, 'aria-label': ariaLabel }: SwitchProps) { + return ( + + ) +} diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index c99280c..7d94059 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -26,7 +26,7 @@ const TabsTrigger = React.forwardRef< 0 +} + +export function calculateRoi(input: RoiInput): RoiResult { + const { + costPerExecutionActual, + costPerExecutionAutomated, + annualFrequency, + analysisHorizonYears, + automationInvestment, + } = input + + const savingsPerExecution = costPerExecutionActual - costPerExecutionAutomated + + // Evitar división por cero cuando el costo actual es 0 + const savingsPerExecutionPercent = + costPerExecutionActual > 0 + ? (savingsPerExecution / costPerExecutionActual) * 100 + : 0 + + const annualSavings = savingsPerExecution * annualFrequency + + // Payback: si no hay ahorro (o es negativo), nunca se recupera la inversión + let paybackMonths: number + if (annualSavings <= 0) { + paybackMonths = Infinity + } else if (automationInvestment === 0) { + // Inversión cero con ahorro positivo: recuperación instantánea + paybackMonths = 0 + } else { + paybackMonths = (automationInvestment / annualSavings) * 12 + } + + const paybackYears = paybackMonths / 12 // Infinity / 12 = Infinity, 0 / 12 = 0 + + const cumulativeSavingsHorizon = annualSavings * analysisHorizonYears - automationInvestment + + const breaksEvenInHorizon = isFinite(paybackYears) && paybackYears <= analysisHorizonYears + + // ROI anualizado: Infinity si inversión = 0 con ahorro > 0; 0 si inversión = 0 y ahorro = 0 + let roiAnnualPercent: number + if (automationInvestment === 0) { + roiAnnualPercent = annualSavings > 0 ? Infinity : 0 + } else { + roiAnnualPercent = (annualSavings / automationInvestment) * 100 + } + + return { + savingsPerExecution, + savingsPerExecutionPercent, + annualSavings, + paybackMonths, + paybackYears, + cumulativeSavingsHorizon, + breaksEvenInHorizon, + roiAnnualPercent, + } +} diff --git a/src/domain/schemas.ts b/src/domain/schemas.ts index 73da67e..a779647 100644 --- a/src/domain/schemas.ts +++ b/src/domain/schemas.ts @@ -22,6 +22,9 @@ export const ActivitySchema = z.object({ directCostFixed: z.number().min(0), executionTimeMinutes: z.number().min(0), assignedResources: z.array(ActivityResourceAssignmentSchema), + automatable: z.boolean().default(false), + automatedCostFixed: z.number().min(0).default(0), + automatedTimeMinutes: z.number().min(0).default(0), }) export const GatewayBranchSchema = z.object({ @@ -45,6 +48,9 @@ export const ProcessSchema = z.object({ bpmnXml: z.string().min(1), currency: z.string().length(3, 'Código ISO de 3 letras'), overheadPercentage: z.number().min(0).max(1), + annualFrequency: z.number().min(1).default(1000), + analysisHorizonYears: z.number().min(1).max(20).default(3), + automationInvestment: z.number().min(0).default(0), createdAt: z.number(), updatedAt: z.number(), }) diff --git a/src/domain/simulation.ts b/src/domain/simulation.ts index 4ee9bca..de4e1c0 100644 --- a/src/domain/simulation.ts +++ b/src/domain/simulation.ts @@ -181,6 +181,9 @@ export function runSimulation(input: SimulationInput): SimulationResult { const perActivity: ActivitySimResult[] = [] const resourceTotals = new Map() + // Seleccionar campos del escenario: para actividades automatable, usar costos automatizados + const scenario = input.scenario ?? 'actual' + for (const activity of activities.values()) { const execProb = execProbs.get(activity.bpmnElementId) ?? 0 const node = graph.nodes.get(activity.bpmnElementId) @@ -192,13 +195,19 @@ export function runSimulation(input: SimulationInput): SimulationResult { continue } + // En escenario automatizado, usar campos automatizados solo para actividades marcadas; + // el resto mantiene sus costos originales (regla de negocio central del Sprint 1). + const useAutomated = scenario === 'automated' && activity.automatable + const directCostFixed = useAutomated ? activity.automatedCostFixed : activity.directCostFixed + const executionTimeMinutes = useAutomated ? activity.automatedTimeMinutes : activity.executionTimeMinutes + const resourceBreakdown: ActivitySimResult['resourceCostBreakdown'] = [] let resourceCostDirect = 0 for (const assignment of activity.assignedResources) { const resource = resources.get(assignment.resourceId) if (!resource) continue - const hoursUsed = (activity.executionTimeMinutes / 60) * assignment.utilizationPercent + const hoursUsed = (executionTimeMinutes / 60) * assignment.utilizationPercent const cost = resource.costPerHour * hoursUsed * execProb resourceBreakdown.push({ resourceId: resource.id, resourceName: resource.name, cost }) resourceCostDirect += cost @@ -211,7 +220,7 @@ export function runSimulation(input: SimulationInput): SimulationResult { }) } - const fixedCostExpected = activity.directCostFixed * execProb + const fixedCostExpected = directCostFixed * execProb const totalExpectedDirectCost = fixedCostExpected + resourceCostDirect perActivity.push({ @@ -225,7 +234,7 @@ export function runSimulation(input: SimulationInput): SimulationResult { expectedExecutions: execProb, executionProbability: execProb, resourceCostBreakdown: resourceBreakdown, - executionTimeMinutes: activity.executionTimeMinutes, + executionTimeMinutes, }) } diff --git a/src/domain/types.ts b/src/domain/types.ts index 9d57366..859cb75 100644 --- a/src/domain/types.ts +++ b/src/domain/types.ts @@ -11,6 +11,9 @@ export interface Process { bpmnXml: string currency: string overheadPercentage: number + annualFrequency: number // 🆕 Sprint 1 — execuciones por año, default 1000 + analysisHorizonYears: number // 🆕 Sprint 1 — horizonte de análisis en años, default 3 + automationInvestment: number // 🆕 Sprint 1 — inversión total del proyecto, default 0 createdAt: number updatedAt: number } @@ -26,6 +29,9 @@ export interface Activity { directCostFixed: number executionTimeMinutes: number assignedResources: ActivityResourceAssignment[] + automatable: boolean // 🆕 Sprint 1 — si la actividad es candidata a automatización + automatedCostFixed: number // 🆕 Sprint 1 — costo recurrente por ejecución automatizada + automatedTimeMinutes: number // 🆕 Sprint 1 — tiempo de la actividad en escenario automatizado } export interface ActivityResourceAssignment { @@ -63,11 +69,16 @@ export interface Simulation { id: UUID processId: UUID executedAt: number - result: SimulationResult + result: SimulationResult // escenario actual + resultAutomated?: SimulationResult // 🆕 Sprint 1 — escenario automatizado (opcional por retrocompatibilidad) } // ─── Motor de simulación: Input/Output ─────────────────────────────────────── +// Escenario de simulación: actual (campos directCostFixed/executionTimeMinutes) +// o automatizado (automatedCostFixed/automatedTimeMinutes para actividades marcadas). +export type Scenario = 'actual' | 'automated' + export interface SimulationInput { processXml: string activities: Map @@ -77,6 +88,7 @@ export interface SimulationInput { currency: string overheadPercentage: number } + scenario?: Scenario // default 'actual' — opcional para retrocompatibilidad con tests existentes } export interface ActivitySimResult { diff --git a/src/features/import/ImportPage.tsx b/src/features/import/ImportPage.tsx index 9d2232c..c6d3226 100644 --- a/src/features/import/ImportPage.tsx +++ b/src/features/import/ImportPage.tsx @@ -64,6 +64,9 @@ export function ImportPage() { bpmnXml: xml, currency: 'USD', overheadPercentage: 0.2, + annualFrequency: 1000, + analysisHorizonYears: 3, + automationInvestment: 0, createdAt: now, updatedAt: now, } @@ -78,6 +81,9 @@ export function ImportPage() { directCostFixed: 0, executionTimeMinutes: 0, assignedResources: [], + automatable: false, + automatedCostFixed: 0, + automatedTimeMinutes: 0, })) const gatewayElements = extractGatewayElements(graph) diff --git a/src/features/report/AutomationScenarioNote.tsx b/src/features/report/AutomationScenarioNote.tsx new file mode 100644 index 0000000..2ef671c --- /dev/null +++ b/src/features/report/AutomationScenarioNote.tsx @@ -0,0 +1,12 @@ +// Leyenda para el tab "Automatizado" del reporte. +// Explica por qué ciertas actividades mantienen su costo original. +// Se coloca al pie del heatmap en el tab "Automatizado" — wired en Etapa 3. + +export function AutomationScenarioNote() { + return ( +

+ 💡 Las actividades no marcadas como automatizables mantienen su costo original en este + escenario. Identificalas por el ícono ámbar de automatización en el workspace. +

+ ) +} diff --git a/src/features/report/ComparisonTable.tsx b/src/features/report/ComparisonTable.tsx new file mode 100644 index 0000000..801be6f --- /dev/null +++ b/src/features/report/ComparisonTable.tsx @@ -0,0 +1,134 @@ +import { useMemo, useState } from 'react' +import { Bot, ArrowUpDown } from 'lucide-react' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { formatCurrency, formatPercent } from '@/lib/format' +import type { ActivitySimResult } from '@/domain/types' + +interface ComparisonRow { + activityId: string + activityName: string + actualCost: number + automatedCost: number + savings: number + savingsPercent: number + automatable: boolean +} + +interface ComparisonTableProps { + perActivityActual: ActivitySimResult[] + perActivityAutomated: ActivitySimResult[] + automatableIds: Set // bpmnElementIds marcados como automatizables + currency: string +} + +export function ComparisonTable({ + perActivityActual, + perActivityAutomated, + automatableIds, + currency, +}: ComparisonTableProps) { + const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc') + + const automatedByActivityId = useMemo(() => { + const m = new Map() + for (const a of perActivityAutomated) m.set(a.activityId, a) + return m + }, [perActivityAutomated]) + + const rows: ComparisonRow[] = useMemo(() => { + return perActivityActual + .map((act) => { + const automated = automatedByActivityId.get(act.activityId) + const automatedCost = automated?.expectedTotalCost ?? act.expectedTotalCost + const savings = act.expectedTotalCost - automatedCost + const savingsPercent = + act.expectedTotalCost > 0 ? (savings / act.expectedTotalCost) * 100 : 0 + return { + activityId: act.activityId, + activityName: act.activityName, + actualCost: act.expectedTotalCost, + automatedCost, + savings, + savingsPercent, + automatable: automatableIds.has(act.bpmnElementId), + } + }) + .sort((a, b) => (b.savings - a.savings) * (sortDir === 'desc' ? 1 : -1)) + }, [perActivityActual, automatedByActivityId, automatableIds, sortDir]) + + if (rows.length === 0) { + return ( +
+ Sin actividades para comparar +
+ ) + } + + return ( +
+ + + + + Actividad + Costo actual + Costo automatizado + setSortDir((d) => (d === 'desc' ? 'asc' : 'desc'))} + > + + Ahorro + + + + % ahorro + + + + {rows.map((row) => { + const savingsColor = + row.savings > 0 + ? 'text-emerald-700' + : row.savings < 0 + ? 'text-red-600' + : 'text-slate-400' + const rowClass = row.automatable ? '' : 'bg-slate-50/60' + const textClass = row.automatable ? 'text-slate-800' : 'text-slate-500' + return ( + + + {row.automatable && ( + + )} + + + {row.activityName} + + + {formatCurrency(row.actualCost, currency)} + + + {formatCurrency(row.automatedCost, currency)} + + + {row.automatable + ? (row.savings > 0 ? '+' : '') + formatCurrency(row.savings, currency) + : '—'} + + + {row.automatable + ? (row.savings > 0 ? '+' : '') + formatPercent(row.savingsPercent) + : '—'} + + + ) + })} + +
+
+ ) +} diff --git a/src/features/report/CumulativeSavingsChart.tsx b/src/features/report/CumulativeSavingsChart.tsx new file mode 100644 index 0000000..92ad3c3 --- /dev/null +++ b/src/features/report/CumulativeSavingsChart.tsx @@ -0,0 +1,115 @@ +import { useMemo } from 'react' +import ReactECharts from 'echarts-for-react' +import { formatCurrency } from '@/lib/format' +import type { RoiResult } from '@/domain/roi' + +interface CumulativeSavingsChartProps { + roi: RoiResult + horizonYears: number + currency: string + investment: number +} + +export function CumulativeSavingsChart({ + roi, + horizonYears, + currency, + investment, +}: CumulativeSavingsChartProps) { + const option = useMemo(() => { + const years = Array.from({ length: horizonYears + 1 }, (_, i) => i) + const values = years.map((y) => roi.annualSavings * y - investment) + + const markLineData: unknown[] = [] + if (isFinite(roi.paybackYears) && roi.paybackYears > 0 && roi.paybackYears <= horizonYears) { + markLineData.push({ + xAxis: roi.paybackYears, + name: 'Payback', + label: { + formatter: `Payback\n${roi.paybackYears.toFixed(1)} años`, + position: 'insideEndTop', + fontSize: 10, + color: '#16A34A', + }, + lineStyle: { color: '#16A34A', type: 'dashed', width: 2 }, + }) + } + + // Línea de referencia en y=0 para identificar el break-even visualmente + markLineData.push({ + yAxis: 0, + name: 'Break-even', + label: { formatter: '', position: 'insideStartTop' }, + lineStyle: { color: '#94a3b8', type: 'solid', width: 1 }, + }) + + return { + tooltip: { + trigger: 'axis', + formatter: (params: unknown[]) => { + const p = params[0] as { dataIndex: number; value: [number, number] } + const year = p.value[0] + const val = p.value[1] + return `
+
Año ${year}
+
Acumulado neto: ${formatCurrency(val, currency)}
+
` + }, + }, + grid: { left: 16, right: 24, top: 16, bottom: 8, containLabel: true }, + xAxis: { + type: 'value', + name: 'Año', + nameLocation: 'end', + nameTextStyle: { fontSize: 10, color: '#94a3b8' }, + min: 0, + max: horizonYears, + interval: 1, + axisLabel: { fontSize: 11, formatter: (v: number) => `Año ${v}` }, + splitLine: { lineStyle: { color: '#f1f5f9' } }, + }, + yAxis: { + type: 'value', + axisLabel: { + fontSize: 11, + formatter: (v: number) => { + if (v === 0) return '0' + const abs = Math.abs(v) + const sign = v < 0 ? '-' : '' + if (abs >= 1_000_000) return `${sign}${(abs / 1_000_000).toFixed(1)}M` + if (abs >= 1_000) return `${sign}${(abs / 1_000).toFixed(0)}k` + return String(v) + }, + }, + splitLine: { lineStyle: { color: '#f1f5f9' } }, + }, + series: [ + { + type: 'line', + data: years.map((y, i) => [y, values[i]]), + smooth: false, + lineStyle: { color: '#F59845', width: 2.5 }, + areaStyle: { + color: { + type: 'linear', + x: 0, y: 0, x2: 0, y2: 1, + colorStops: [ + { offset: 0, color: 'rgba(245,152,69,0.18)' }, + { offset: 1, color: 'rgba(245,152,69,0.02)' }, + ], + }, + }, + itemStyle: { color: '#F59845' }, + symbolSize: 6, + markLine: { + silent: true, + symbol: ['none', 'none'], + data: markLineData, + }, + }, + ], + } + }, [roi, horizonYears, currency, investment]) + + return +} diff --git a/src/features/report/HeatmapLegend.tsx b/src/features/report/HeatmapLegend.tsx index b2ead6f..dfe0a7e 100644 --- a/src/features/report/HeatmapLegend.tsx +++ b/src/features/report/HeatmapLegend.tsx @@ -1,5 +1,5 @@ import { formatCurrency, formatPercent } from '@/lib/format' -import { heatmapLegendBounds } from '@/lib/colors' +import { heatmapLegendBounds, HEATMAP_LOW_HEX, HEATMAP_MID_HEX, HEATMAP_HIGH_HEX } from '@/lib/colors' import type { HeatmapMode } from '@/lib/colors' import type { ActivitySimResult } from '@/domain/types' @@ -21,7 +21,7 @@ export function HeatmapLegend({ perActivity, mode, currency }: HeatmapLegendProp
Bajo costo
Alto costo
diff --git a/src/features/report/ReportPage.tsx b/src/features/report/ReportPage.tsx index 7dfe77f..78ce900 100644 --- a/src/features/report/ReportPage.tsx +++ b/src/features/report/ReportPage.tsx @@ -1,14 +1,17 @@ -import { useCallback, useRef, useState } from 'react' +import { useCallback, useMemo, useRef, useState } from 'react' import { useParams, useNavigate } from '@tanstack/react-router' -import { AlertTriangle, Home } from 'lucide-react' +import { AlertTriangle, Home, Info } from 'lucide-react' import { Button } from '@/components/ui/button' import { Separator } from '@/components/ui/separator' -import { TooltipProvider } from '@/components/ui/tooltip' +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' +import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' import { useToast } from '@/components/ui/use-toast' import { AppFooter } from '@/components/AppFooter' +import { calculateRoi } from '@/domain/roi' import { useReportData } from './useReportData' import { ReportHeader } from './ReportHeader' import { KpiBar } from './KpiBar' +import { RoiKpiBar } from './RoiKpiBar' import { HeatmapCanvas, type HeatmapCanvasHandle } from './HeatmapCanvas' import { HeatmapLegend } from './HeatmapLegend' import { HeatmapModeToggle } from './HeatmapModeToggle' @@ -17,48 +20,102 @@ import { TopActivitiesChart } from './TopActivitiesChart' import { CostCompositionChart } from './CostCompositionChart' import { CostByResourceChart } from './CostByResourceChart' import { ActivitiesTable } from './ActivitiesTable' +import { ComparisonTable } from './ComparisonTable' +import { CumulativeSavingsChart } from './CumulativeSavingsChart' +import { TopSavingsChart } from './TopSavingsChart' +import { TransparencySection } from './TransparencySection' +import { AutomationScenarioNote } from './AutomationScenarioNote' import { MethodologyFooter } from './MethodologyFooter' import type { HeatmapMode } from '@/lib/colors' export function ReportPage() { const { processId } = useParams({ from: '/report/$processId' }) const navigate = useNavigate() - const { process, simulation, resources, isLoading, error } = useReportData(processId) + const { process, simulation, activities, resources, isLoading, error } = useReportData(processId) const { toast } = useToast() const [heatmapMode, setHeatmapMode] = useState('relative') - const [selectedId, setSelectedId] = useState(null) - const [isExportingPdf, setIsExportingPdf] = useState(false) - const [isExportingCsv, setIsExportingCsv] = useState(false) + // Set de bpmnElementIds marcados como automatizables — debe estar antes de los early returns + const automatableIds = useMemo( + () => new Set(activities.filter((a) => a.automatable).map((a) => a.bpmnElementId)), + [activities] + ) + + // ROI — calculado solo cuando hay ambos resultados + const roi = useMemo(() => { + const result = simulation?.result + const resultAutomated = simulation?.resultAutomated + if (!result || !resultAutomated || !process) return null + return calculateRoi({ + costPerExecutionActual: result.totalCost, + costPerExecutionAutomated: resultAutomated.totalCost, + annualFrequency: process.annualFrequency, + analysisHorizonYears: process.analysisHorizonYears, + automationInvestment: process.automationInvestment, + }) + }, [simulation, process]) + + // Estado y refs para el tab "Actual" + const [selectedIdActual, setSelectedIdActual] = useState(null) const heatmapRef = useRef(null) const heatmapSectionRef = useRef(null) const tableSectionRef = useRef(null) - // Click en heatmap → resaltar fila en tabla + scroll a tabla - const handleHeatmapClick = useCallback((bpmnElementId: string) => { - setSelectedId(bpmnElementId) + // Estado y refs para el tab "Automatizado" + const [selectedIdAuto, setSelectedIdAuto] = useState(null) + const heatmapRefAuto = useRef(null) + const heatmapSectionRefAuto = useRef(null) + const tableSectionRefAuto = useRef(null) + + const [isExportingPdf, setIsExportingPdf] = useState(false) + const [isExportingCsv, setIsExportingCsv] = useState(false) + const [activeTab, setActiveTab] = useState<'actual' | 'automatizado' | 'roi'>('actual') + + // Tab "Actual" — cross-linking heatmap ↔ tabla + const handleHeatmapClickActual = useCallback((bpmnElementId: string) => { + setSelectedIdActual(bpmnElementId) setTimeout(() => { tableSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) }, 50) }, []) - // Click en fila de tabla → resaltar en canvas + scroll a heatmap - const handleTableRowClick = useCallback((bpmnElementId: string) => { - setSelectedId(bpmnElementId) + const handleTableRowClickActual = useCallback((bpmnElementId: string) => { + setSelectedIdActual(bpmnElementId) heatmapRef.current?.highlightElement(bpmnElementId) setTimeout(() => { heatmapSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) }, 50) }, []) + // Tab "Automatizado" — cross-linking heatmap ↔ tabla + const handleHeatmapClickAuto = useCallback((bpmnElementId: string) => { + setSelectedIdAuto(bpmnElementId) + setTimeout(() => { + tableSectionRefAuto.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + }, 50) + }, []) + + const handleTableRowClickAuto = useCallback((bpmnElementId: string) => { + setSelectedIdAuto(bpmnElementId) + heatmapRefAuto.current?.highlightElement(bpmnElementId) + setTimeout(() => { + heatmapSectionRefAuto.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + }, 50) + }, []) + const handleExportPdf = useCallback(async () => { if (!process || !simulation || isExportingPdf) return setIsExportingPdf(true) try { - const heatmapImageData = await heatmapRef.current?.captureImage() ?? null + let heatmapImageData: string | null = null + if (activeTab === 'actual') { + heatmapImageData = await heatmapRef.current?.captureImage() ?? null + } else if (activeTab === 'automatizado') { + heatmapImageData = await heatmapRefAuto.current?.captureImage() ?? null + } const { exportToPdf } = await import('@/lib/export/pdf-export') - await exportToPdf({ process, simulation, resources, heatmapImageData }) + await exportToPdf({ process, simulation, resources, heatmapImageData, tab: activeTab, activities }) toast({ title: 'PDF generado', description: 'El reporte se descargó correctamente.' }) } catch (err) { console.error('PDF export error:', err) @@ -66,14 +123,14 @@ export function ReportPage() { } finally { setIsExportingPdf(false) } - }, [process, simulation, resources, isExportingPdf, toast]) + }, [process, simulation, resources, isExportingPdf, toast, activeTab, activities]) const handleExportCsv = useCallback(async () => { if (!process || !simulation || isExportingCsv) return setIsExportingCsv(true) try { const { exportToCsv } = await import('@/lib/export/csv-export') - exportToCsv({ process, simulation, resources }) + exportToCsv({ process, simulation, resources, tab: activeTab, activities }) toast({ title: 'CSV generado', description: 'Los datos se descargaron correctamente.' }) } catch (err) { console.error('CSV export error:', err) @@ -81,14 +138,13 @@ export function ReportPage() { } finally { setIsExportingCsv(false) } - }, [process, simulation, resources, isExportingCsv, toast]) + }, [process, simulation, resources, isExportingCsv, toast, activeTab, activities]) - // ── Loading — skeleton en lugar de pantalla en blanco ──────────────────── + // ── Loading ─────────────────────────────────────────────────────────────── if (isLoading) { return (
- {/* Header skeleton */}
@@ -100,7 +156,6 @@ export function ReportPage() {
- {/* KPI skeleton */}
{[0, 1, 2, 3].map((i) => (
@@ -113,12 +168,10 @@ export function ReportPage() {
))}
- {/* Canvas skeleton */}
- {/* Table skeleton */}
{[0, 1, 2, 3].map((i) => ( @@ -159,14 +212,19 @@ export function ReportPage() { } const result = simulation.result - const perActivity = result.perActivity + const resultAutomated = simulation.resultAutomated ?? null + const hasAutomatedResult = resultAutomated !== null + + // Tabs de escenarios se habilitan solo cuando hay resultado automatizado Y hay actividades + // marcadas como automatizables. Sin automatizables, los tabs muestran datos idénticos al actual. + const hasAutomatedScenario = hasAutomatedResult && automatableIds.size > 0 return (
- {/* ── 1. Header ─────────────────────────────────────────────── */} + {/* ── Header (fuera de los tabs: aplica a todos los escenarios) ──── */} - {/* ── 2. KPI Bar ────────────────────────────────────────────── */} - + {/* ── Tabs ─────────────────────────────────────────────────────── */} + setActiveTab(v as typeof activeTab)}> + + Actual - {/* ── 3. Mapa de Calor ──────────────────────────────────────── */} -
-
-
-

Mapa de calor de costos

-

- Hacé click en una actividad para ver su detalle en la tabla -

-
- -
+ + + + + Automatizado + + + + {!hasAutomatedScenario && ( + + {!hasAutomatedResult + ? 'Re-simulá el proceso para generar el escenario automatizado' + : 'Marcá al menos una actividad como automatizable en el workspace para ver este escenario'} + + )} + - + + + + + Comparación + ROI + + + + {!hasAutomatedScenario && ( + + {!hasAutomatedResult + ? 'Re-simulá el proceso para generar el análisis de ROI' + : 'Marcá al menos una actividad como automatizable en el workspace para calcular el ROI'} + + )} + + - -
+ {/* ──────────────── TAB: ACTUAL ──────────────────────────────── */} + + - {/* ── 4. Warnings ───────────────────────────────────────────── */} - +
+
+
+

Mapa de calor de costos

+

+ Hacé click en una actividad para ver su detalle en la tabla +

+
+ +
- {/* ── 5. Análisis — 3 gráficos ──────────────────────────────── */} -
-

Análisis de composición

-
-
-

- Top actividades por costo -

- + +
-
-

- Composición directo / overhead -

- + + +
+

Análisis de composición

+
+
+

+ Top actividades por costo +

+ +
+
+

+ Composición directo / overhead +

+ +
+
+

+ Costo por tipo de recurso +

+ +
+
-
-

- Costo por tipo de recurso -

- +
+

+ Detalle por actividad + + ({result.perActivity.length} actividades) + +

+
+
-
-
- {/* ── 6. Tabla detalle ──────────────────────────────────────── */} -
-
-

- Detalle por actividad - ({perActivity.length} actividades) -

-
- -
+ + + + - + {/* ──────────────── TAB: AUTOMATIZADO ───────────────────────── */} + + {!hasAutomatedResult ? ( + + ) : ( + <> + - {/* ── 7. Footer metodológico ────────────────────────────────── */} - +
+
+
+

Mapa de calor — Escenario Automatizado

+

+ Hacé click en una actividad para ver su detalle en la tabla +

+
+ +
+ + + + + + +
+ + + +
+

Análisis de composición

+
+
+

+ Top actividades por costo +

+ +
+
+

+ Composición directo / overhead +

+ +
+
+

+ Costo por tipo de recurso +

+ +
+
+
+ +
+
+

+ Detalle por actividad — Automatizado + + ({resultAutomated!.perActivity.length} actividades) + +

+
+ +
+ + + + + + )} +
+ + {/* ──────────────── TAB: COMPARACIÓN + ROI ──────────────────── */} + + {!hasAutomatedResult || !roi ? ( + + ) : ( + <> + {/* KPIs de ROI */} + + + {/* Tabla de comparación actividad por actividad */} +
+

+ Comparación por actividad +

+ +
+ + {/* Gráficos: ahorro acumulado + top ahorros */} +
+
+

+ Ahorro acumulado neto ({process.analysisHorizonYears} años) +

+ +
+
+

+ Top actividades por ahorro +

+ +
+
+ + {/* Transparencia metodológica (condicional) */} +
+ +
+ + + + + + )} +
+ -
) } + +// ── Componente auxiliar: aviso cuando falta re-simular ─────────────────────── + +function NeedsResimulate({ processId }: { processId: string }) { + const navigate = useNavigate() + return ( +
+
+ +
+
+

+ Esta simulación no incluye el escenario automatizado +

+

+ Volvé al workspace y presioná "Simular" para generar ambos escenarios. +

+
+ +
+ ) +} diff --git a/src/features/report/RoiKpiBar.tsx b/src/features/report/RoiKpiBar.tsx new file mode 100644 index 0000000..4b1d64d --- /dev/null +++ b/src/features/report/RoiKpiBar.tsx @@ -0,0 +1,128 @@ +import { TrendingDown, Calendar, DollarSign, Percent } from 'lucide-react' +import { formatCurrency, formatPayback, formatPercent } from '@/lib/format' +import type { RoiResult } from '@/domain/roi' + +interface RoiKpiBarProps { + roi: RoiResult + currency: string + horizonYears: number +} + +function RoiKpiHero({ + label, value, sub, +}: { + label: string + value: string + sub?: string +}) { + return ( +
+

{label}

+

+ {value} +

+ {sub &&

{sub}

} +
+ ) +} + +function RoiKpiCard({ + label, value, sub, icon, iconColor, valueColor, compact, +}: { + label: string + value: string + sub?: string + icon: React.ReactNode + iconColor: string + valueColor?: string + compact?: boolean +}) { + return ( +
+
+ {icon} +
+
+

{label}

+

{value}

+ {sub &&

{sub}

} +
+
+ ) +} + +export function RoiKpiBar({ roi, currency, horizonYears }: RoiKpiBarProps) { + const paybackStr = formatPayback(roi.paybackMonths) + + const roiStr = !isFinite(roi.roiAnnualPercent) + ? '∞%' + : formatPercent(roi.roiAnnualPercent) + + const savingsPosColor = roi.savingsPerExecution > 0 + ? 'text-emerald-700' + : roi.savingsPerExecution < 0 + ? 'text-red-600' + : 'text-slate-900' + + const annualPosColor = roi.annualSavings > 0 + ? 'text-emerald-700' + : roi.annualSavings < 0 + ? 'text-red-600' + : 'text-slate-900' + + const cumulativeSub = roi.cumulativeSavingsHorizon > 0 + ? `neto de inversión inicial · ${horizonYears} años` + : 'sin recuperación neta en el horizonte' + + return ( +
+ {/* KPI Hero — Ahorro proyectado a N años */} + + + {/* KPIs secundarios — grid 4 columnas */} +
+ } + iconColor="bg-emerald-50" + valueColor={savingsPosColor} + /> + } + iconColor="bg-emerald-50" + valueColor={annualPosColor} + /> + } + iconColor="bg-primary/10" + valueColor={roi.breaksEvenInHorizon ? 'text-emerald-700' : 'text-slate-900'} + compact + /> + } + iconColor="bg-amber-50" + valueColor={roi.roiAnnualPercent > 0 ? 'text-emerald-700' : roi.roiAnnualPercent < 0 ? 'text-red-600' : 'text-slate-900'} + /> +
+
+ ) +} diff --git a/src/features/report/TopSavingsChart.tsx b/src/features/report/TopSavingsChart.tsx new file mode 100644 index 0000000..a17dc5c --- /dev/null +++ b/src/features/report/TopSavingsChart.tsx @@ -0,0 +1,95 @@ +import { useMemo } from 'react' +import ReactECharts from 'echarts-for-react' +import { formatCurrency } from '@/lib/format' +import type { ActivitySimResult } from '@/domain/types' + +interface TopSavingsChartProps { + perActivityActual: ActivitySimResult[] + perActivityAutomated: ActivitySimResult[] + currency: string +} + +function truncate(s: string, max: number) { + return s.length > max ? s.slice(0, max - 1) + '…' : s +} + +export function TopSavingsChart({ + perActivityActual, + perActivityAutomated, + currency, +}: TopSavingsChartProps) { + const option = useMemo(() => { + const automatedMap = new Map(perActivityAutomated.map((a) => [a.activityId, a])) + + const top5 = perActivityActual + .map((act) => { + const auto = automatedMap.get(act.activityId) + const savings = act.expectedTotalCost - (auto?.expectedTotalCost ?? act.expectedTotalCost) + return { name: act.activityName, value: savings } + }) + .filter((s) => s.value > 0) + .sort((a, b) => b.value - a.value) + .slice(0, 5) + + if (top5.length === 0) return null + + const reversed = [...top5].reverse() + + return { + tooltip: { + trigger: 'axis', + axisPointer: { type: 'shadow' }, + formatter: (params: unknown[]) => { + const p = params[0] as { name: string; value: number } + return `
${p.name}
Ahorro: ${formatCurrency(p.value, currency)}
` + }, + }, + grid: { left: 12, right: 32, top: 8, bottom: 8, containLabel: true }, + xAxis: { + type: 'value', + axisLabel: { + fontSize: 10, + formatter: (v: number) => { + if (v === 0) return '0' + if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M` + if (v >= 1_000) return `${(v / 1_000).toFixed(0)}k` + return String(v) + }, + }, + splitLine: { lineStyle: { color: '#f1f5f9' } }, + }, + yAxis: { + type: 'category', + data: reversed.map((s) => truncate(s.name, 22)), + axisLabel: { fontSize: 10, color: '#64748b' }, + axisTick: { show: false }, + axisLine: { show: false }, + }, + series: [ + { + type: 'bar', + data: reversed.map((s) => s.value), + barMaxWidth: 32, + itemStyle: { color: '#16A34A', borderRadius: [0, 4, 4, 0] }, + label: { + show: true, + position: 'right', + fontSize: 10, + formatter: (p: { value: number }) => formatCurrency(p.value, currency), + color: '#16A34A', + }, + }, + ], + } + }, [perActivityActual, perActivityAutomated, currency]) + + if (!option) { + return ( +
+ Sin actividades con ahorro positivo +
+ ) + } + + return +} diff --git a/src/features/report/TransparencySection.tsx b/src/features/report/TransparencySection.tsx new file mode 100644 index 0000000..d6c3b44 --- /dev/null +++ b/src/features/report/TransparencySection.tsx @@ -0,0 +1,91 @@ +import { AlertTriangle } from 'lucide-react' +import { formatPercent } from '@/lib/format' +import type { Activity } from '@/domain/types' +import type { RoiResult } from '@/domain/roi' + +// Umbral de ROI a partir del cual se dispara la advertencia de verosimilitud. +// Un ROI > 1000% anual es matemáticamente posible pero comercialmente inverosímil: +// indica casi siempre un input incorrecto (frecuencia sobreestimada, costo auto subestimado +// o inversión subestimada). Documentado en METHODOLOGY_AUTOMATED_COST.md. +const ROI_HIGH_THRESHOLD = 1000 + +interface TransparencySectionProps { + activities: Activity[] + investment: number + roi?: RoiResult +} + +export function TransparencySection({ activities, investment, roi }: TransparencySectionProps) { + const zeroCostAutomatable = activities.filter( + (a) => a.automatable && a.automatedCostFixed === 0 && a.automatedTimeMinutes === 0 + ) + const investmentIsZero = investment === 0 + + // Caso 4: ROI > 1000% finito (Infinity no aplica — ya lo cubre el caso de investment=0) + const roiIsUnusuallyHigh = + roi !== undefined && + Number.isFinite(roi.roiAnnualPercent) && + roi.roiAnnualPercent > ROI_HIGH_THRESHOLD + + if (zeroCostAutomatable.length === 0 && !investmentIsZero && !roiIsUnusuallyHigh) return null + + return ( +
+
+ +

Transparencia metodológica

+
+ + {zeroCostAutomatable.length > 0 && ( +
+

+ {zeroCostAutomatable.length === 1 + ? '1 actividad automatizable sin costo configurado:' + : `${zeroCostAutomatable.length} actividades automatizables sin costo configurado:`} +

+
    + {zeroCostAutomatable.map((a) => ( +
  • + {a.name || a.bpmnElementId} +
  • + ))} +
+

+ Estas actividades se incluyen en el escenario automatizado con costo cero, lo que puede + inflar artificialmente el ahorro calculado. Configurá sus costos en el workspace para + un análisis realista. +

+
+ )} + + {investmentIsZero && ( +

+ Inversión en automatización no configurada.{' '} + El payback aparece como inmediato y el ROI como infinito porque no hay inversión inicial. + Configurá la inversión total en la tab "Global" del workspace para un análisis + financiero completo. +

+ )} + + {roiIsUnusuallyHigh && roi && ( +
+

+ + ROI inusualmente alto detectado: {formatPercent(roi.roiAnnualPercent)}. + {' '} + Un ROI superior al {formatPercent(ROI_HIGH_THRESHOLD, 0)} anual suele indicar uno de estos casos: +

+
    +
  • Frecuencia anual del proceso sobreestimada
  • +
  • Costo automatizado por ejecución subestimado
  • +
  • Inversión en automatización subestimada
  • +
+

+ Revisá estos inputs antes de presentar el reporte al cliente. + Ver METHODOLOGY_AUTOMATED_COST.md para guía de estimación. +

+
+ )} +
+ ) +} diff --git a/src/features/simulation/useSimulate.ts b/src/features/simulation/useSimulate.ts index b1bc466..89d58b3 100644 --- a/src/features/simulation/useSimulate.ts +++ b/src/features/simulation/useSimulate.ts @@ -1,14 +1,12 @@ import { useCallback } from 'react' -import { v4 as uuidv4 } from 'uuid' import { useProcessStore } from '@/store/process-store' import { useSimulationStore } from '@/store/simulation-store' import { runSimulation } from '@/domain/simulation' -import { simulationRepo } from '@/persistence/repositories' import type { SimulationInput } from '@/domain/types' export function useSimulate() { const { currentProcess, activities, gateways, resources } = useProcessStore() - const { setResult, setRunning, setError } = useSimulationStore() + const { persistResults, setRunning, setError } = useSimulationStore() const simulate = useCallback(async () => { if (!currentProcess) { @@ -20,7 +18,7 @@ export function useSimulate() { setError(null) try { - const input: SimulationInput = { + const baseInput: Omit = { processXml: currentProcess.bpmnXml, activities: new Map(activities.map((a) => [a.bpmnElementId, a])), gateways: new Map(gateways.map((g) => [g.bpmnElementId, g])), @@ -31,26 +29,20 @@ export function useSimulate() { }, } - const result = runSimulation(input) + // Las dos simulaciones se computan y persisten SIEMPRE en conjunto (regla dura). + const resultActual = runSimulation({ ...baseInput, scenario: 'actual' }) + const resultAutomated = runSimulation({ ...baseInput, scenario: 'automated' }) - const simulation = { - id: uuidv4(), - processId: currentProcess.id, - executedAt: Date.now(), - result, - } - - await simulationRepo.save(simulation) - setResult(result) + await persistResults(currentProcess.id, resultActual, resultAutomated) setRunning(false) - return result + return { resultActual, resultAutomated } } catch (err) { const message = err instanceof Error ? err.message : 'Error en la simulación' setError(message) setRunning(false) return null } - }, [currentProcess, activities, gateways, resources, setResult, setRunning, setError]) + }, [currentProcess, activities, gateways, resources, persistResults, setRunning, setError]) return { simulate } } diff --git a/src/features/workspace/ActivityPanel.tsx b/src/features/workspace/ActivityPanel.tsx index 2582373..3306f30 100644 --- a/src/features/workspace/ActivityPanel.tsx +++ b/src/features/workspace/ActivityPanel.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { PlusCircle, Trash2, Info } from 'lucide-react' +import { PlusCircle, Trash2, Info, AlertTriangle } from 'lucide-react' import { Label } from '@/components/ui/label' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' @@ -7,6 +7,8 @@ import { Slider } from '@/components/ui/slider' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { ScrollArea } from '@/components/ui/scroll-area' +import { Switch } from '@/components/ui/switch' +import { Separator } from '@/components/ui/separator' import { useProcessStore } from '@/store/process-store' import { formatCurrency } from '@/lib/format' import type { Activity } from '@/domain/types' @@ -122,7 +124,7 @@ export function ActivityPanel({ selectedElementId }: ActivityPanelProps) { type="number" min={0} step={10} - value={localActivity.directCostFixed} + value={localActivity.directCostFixed === 0 ? '' : localActivity.directCostFixed} onChange={(e) => handleChange('directCostFixed', Math.max(0, parseFloat(e.target.value) || 0))} className="font-mono h-8 text-sm" placeholder="0" @@ -147,7 +149,7 @@ export function ActivityPanel({ selectedElementId }: ActivityPanelProps) { type="number" min={0} step={5} - value={localActivity.executionTimeMinutes} + value={localActivity.executionTimeMinutes === 0 ? '' : localActivity.executionTimeMinutes} onChange={(e) => handleChange('executionTimeMinutes', Math.max(0, parseFloat(e.target.value) || 0))} className="font-mono h-8 text-sm" placeholder="0" @@ -226,6 +228,101 @@ export function ActivityPanel({ selectedElementId }: ActivityPanelProps) { )}
+ {/* ── Sección: Automatización ──────────────────────────────────── */} +
+ + +
+

Automatización

+ + {/* Toggle */} +
+ + handleChange('automatable', v)} + aria-label="Marcar actividad como automatizable" + /> +
+ + {/* Campos — aparecen con transición suave cuando toggle=ON */} +
+ + {/* Costo automatizado */} +
+
+ + + + + + + Costo recurrente por ejecución automatizada. No incluye la inversión inicial del proyecto. + Ver METHODOLOGY_AUTOMATED_COST.md para la guía de cálculo. + + +
+ handleChange('automatedCostFixed', Math.max(0, parseFloat(e.target.value) || 0))} + className="font-mono h-8 text-sm" + placeholder="0" + /> +
+ + {/* Tiempo automatizado */} +
+ + handleChange('automatedTimeMinutes', Math.max(0, parseFloat(e.target.value) || 0))} + className="font-mono h-8 text-sm" + placeholder="0" + /> +
+ + {/* Helper ámbar — siempre en el DOM para que la transición funcione + al tipear un valor (no desaparece instantáneo sino con fade). + La condición controla opacity + max-h, no mount/unmount. */} + {(() => { + const showHelper = localActivity.automatable + && localActivity.automatedCostFixed === 0 + && localActivity.automatedTimeMinutes === 0 + return ( +
+
+ +

+ Esta actividad aparecerá en "Transparencia metodológica" del reporte hasta que cargues valores. +

+
+
+ ) + })()} +
+
+
+ {/* Guardar */}
+ {/* ── Sección: Análisis de Impacto ──────────────────────────────── */} +
+ +
+

Análisis de Impacto

+ + {/* Frecuencia anual */} +
+
+ + + + + + + Cantidad de veces que se ejecuta el proceso completo por año. Se usa para anualizar los costos y calcular el ahorro. + + +
+ { setLocalFrequency(Math.max(1, parseInt(e.target.value) || 1)); markDirty() }} + className="font-mono h-8 text-sm" + placeholder="1000" + /> +

+ Ejemplos: 12 (mensual) · 52 (semanal) · 250 (diario laboral) · 8760 (cada hora) +

+
+ + {/* Horizonte de análisis */} +
+
+
+ + + + + + + Años hasta los que se proyecta el ahorro acumulado. Determina el eje X del gráfico de ROI. + + +
+ + {localHorizon} {localHorizon === 1 ? 'año' : 'años'} + +
+ { setLocalHorizon(v); markDirty() }} + /> +
+ + {/* Inversión en automatización */} +
+
+ + + + + + + Costo único total del proyecto: desarrollo, licencias, implementación, capacitación. No incluir costos recurrentes. + + +
+ { setLocalInvestment(Math.max(0, parseFloat(e.target.value) || 0)); markDirty() }} + className="font-mono h-8 text-sm" + placeholder="0" + /> +
+
+
+ {/* Guardar */}