From f8f33a65c776e2f6cac9fcd308718d220840a701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Ben=C3=ADtez?= Date: Thu, 14 May 2026 20:50:12 -0300 Subject: [PATCH 1/9] Los archivos de claude code modificados --- ARCHITECTURE.md | 343 +++++++++++++++++---- CLAUDE.md | 147 ++++++--- src/features/workspace/ActivityPanel.tsx | 4 +- tests/proceso_carga_facturas.bpmn | 371 +++++++++++++++++++++++ 4 files changed, 753 insertions(+), 112 deletions(-) create mode 100644 tests/proceso_carga_facturas.bpmn 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/CLAUDE.md b/CLAUDE.md index 85c4f2a..f235e90 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ **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 +17,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 +107,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 +125,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 +151,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 +161,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 +189,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 +217,18 @@ 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 \ No newline at end of file diff --git a/src/features/workspace/ActivityPanel.tsx b/src/features/workspace/ActivityPanel.tsx index 2582373..2541472 100644 --- a/src/features/workspace/ActivityPanel.tsx +++ b/src/features/workspace/ActivityPanel.tsx @@ -122,7 +122,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 +147,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" diff --git a/tests/proceso_carga_facturas.bpmn b/tests/proceso_carga_facturas.bpmn new file mode 100644 index 0000000..5bc284b --- /dev/null +++ b/tests/proceso_carga_facturas.bpmn @@ -0,0 +1,371 @@ + + + + + + + + + + + + + + Flow_IK_1 + + + Flow_IK_1 + Flow_IK_2 + + + Flow_IK_2 + Flow_IK_3 + + + Flow_IK_3 + + + + + + + + Flow_R_1 + + + + Flow_R_1 + Flow_R_2 + + + Flow_R_2 + Flow_R_3_Si + Flow_R_3_No + + + Flow_R_3_Si + Flow_R_4 + + + Flow_R_4 + Flow_R_5 + + + Flow_R_5 + Flow_R_6_Si + Flow_R_6_No + + + Flow_R_6_Si + Flow_R_7 + + + Flow_R_7 + Flow_R_8_Si + Flow_R_8_No + + + Flow_R_8_Si + Flow_R_9_Si + Flow_R_9_No + + + Flow_R_9_No + Flow_R_10 + + SubFlow_T_1 + + + SubFlow_T_1 + SubFlow_T_2 + + + SubFlow_T_2 + SubFlow_T_3 + + + SubFlow_T_3 + + + + + + + Flow_R_9_Si + Flow_R_10 + Flow_R_11 + + + Flow_R_11 + Flow_R_12 + + + Flow_R_12 + Flow_R_13_Si + Flow_R_13_No + + + Flow_R_13_Si + Flow_R_14 + + + Flow_R_14 + Flow_R_15_Si + Flow_R_15_No + + + Flow_R_15_Si + Flow_R_16 + + + Flow_R_16 + + + Flow_R_3_No + Flow_R_6_No + Flow_R_8_No + Flow_R_13_No + Flow_R_15_No + Flow_R_17 + + + Flow_R_17 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From ce2dabfb76ac241e2f699efaefd9e07c102f4c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Ben=C3=ADtez?= Date: Thu, 14 May 2026 20:51:37 -0300 Subject: [PATCH 2/9] metodologia de costeo --- Methodology-automated-cost.md | 144 ++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 Methodology-automated-cost.md diff --git a/Methodology-automated-cost.md b/Methodology-automated-cost.md new file mode 100644 index 0000000..87d6973 --- /dev/null +++ b/Methodology-automated-cost.md @@ -0,0 +1,144 @@ +# 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 | + +--- + +## 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 From b6f31f43718bbc537a6a5063c6a7cd9a22c3fe53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Ben=C3=ADtez?= Date: Thu, 14 May 2026 20:52:36 -0300 Subject: [PATCH 3/9] renombrado el nombre --- Methodology-automated-cost.md => METHODOLOGY_AUTOMATED_COST.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Methodology-automated-cost.md => METHODOLOGY_AUTOMATED_COST.md (100%) diff --git a/Methodology-automated-cost.md b/METHODOLOGY_AUTOMATED_COST.md similarity index 100% rename from Methodology-automated-cost.md rename to METHODOLOGY_AUTOMATED_COST.md From 8b71c6ee13ff397a53bfac88a48049f2a6d8712b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Ben=C3=ADtez?= Date: Fri, 15 May 2026 22:14:53 -0300 Subject: [PATCH 4/9] =?UTF-8?q?Antes=20de=20la=20=C3=BAltima=20revisi?= =?UTF-8?q?=C3=B3n=20de=20etapa=204.=20Sprint=201.=20Previo=20al=20ajuste?= =?UTF-8?q?=20de=20look=20n=20feel=20de=20app,=20reportes,=20nombre=20y=20?= =?UTF-8?q?colores.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 262 ++++++ METHODOLOGY_AUTOMATED_COST.md | 15 + README.md | 247 +++++- TODO.md | 12 + eslint.config.js | 92 ++ src/components/ui/switch.tsx | 36 + src/domain/roi.ts | 79 ++ src/domain/schemas.ts | 6 + src/domain/simulation.ts | 15 +- src/domain/types.ts | 14 +- src/features/import/ImportPage.tsx | 6 + .../report/AutomationScenarioNote.tsx | 12 + src/features/report/ComparisonTable.tsx | 130 +++ .../report/CumulativeSavingsChart.tsx | 115 +++ src/features/report/ReportPage.tsx | 484 ++++++++--- src/features/report/RoiKpiBar.tsx | 110 +++ src/features/report/TopSavingsChart.tsx | 95 +++ src/features/report/TransparencySection.tsx | 91 ++ src/features/simulation/useSimulate.ts | 24 +- src/features/workspace/ActivityPanel.tsx | 99 ++- src/features/workspace/BpmnCanvas.tsx | 87 +- .../workspace/GlobalSettingsPanel.tsx | 105 +++ src/features/workspace/WorkspacePage.tsx | 19 +- src/lib/export/csv-export.ts | 173 +++- src/lib/export/pdf-export.ts | 190 +++-- src/lib/export/pdf-sections-roi.ts | 458 ++++++++++ src/lib/export/pdf-sections.ts | 2 +- src/lib/export/slug.ts | 5 +- src/persistence/db.ts | 23 + src/persistence/repositories.ts | 23 + src/store/simulation-store.ts | 58 +- tests/domain/roi.test.ts | 161 ++++ tests/domain/simulation.test.ts | 182 ++++ tests/e2e/export-roi.spec.ts | 280 +++++++ tests/e2e/validate-dod-etapa4.spec.ts | 411 +++++++++ tests/features/report/components.test.tsx | 12 +- tests/features/report/derivations.test.ts | 1 + tests/features/report/roi-components.test.tsx | 792 ++++++++++++++++++ .../features/workspace/ActivityPanel.test.tsx | 157 ++++ tests/features/workspace/BpmnCanvas.test.tsx | 131 +++ .../workspace/GlobalSettingsPanel.test.tsx | 132 +++ tests/integration/dexie-migration.test.ts | 145 ++++ tests/integration/gateway-validation.test.ts | 1 + .../integration/indexeddb-persistence.test.ts | 6 + tests/integration/simulation-engine.test.ts | 3 + tests/integration/sprint1-flow.test.ts | 232 +++++ tests/integration/sprint1-report-roi.test.ts | 447 ++++++++++ tests/lib/export/csv-export-roi.test.ts | 216 +++++ tests/lib/export/csv-export.test.ts | 1 + tests/lib/export/measure-sizes.test.ts | 2 + tests/lib/export/pdf-export-roi.test.ts | 206 +++++ tests/lib/export/pdf-export.test.ts | 1 + tests/setup.ts | 16 + .../simulation-store-invalidation.test.ts | 136 +++ 54 files changed, 6497 insertions(+), 261 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 eslint.config.js create mode 100644 src/components/ui/switch.tsx create mode 100644 src/domain/roi.ts create mode 100644 src/features/report/AutomationScenarioNote.tsx create mode 100644 src/features/report/ComparisonTable.tsx create mode 100644 src/features/report/CumulativeSavingsChart.tsx create mode 100644 src/features/report/RoiKpiBar.tsx create mode 100644 src/features/report/TopSavingsChart.tsx create mode 100644 src/features/report/TransparencySection.tsx create mode 100644 src/lib/export/pdf-sections-roi.ts create mode 100644 tests/domain/roi.test.ts create mode 100644 tests/e2e/export-roi.spec.ts create mode 100644 tests/e2e/validate-dod-etapa4.spec.ts create mode 100644 tests/features/report/roi-components.test.tsx create mode 100644 tests/features/workspace/ActivityPanel.test.tsx create mode 100644 tests/features/workspace/BpmnCanvas.test.tsx create mode 100644 tests/features/workspace/GlobalSettingsPanel.test.tsx create mode 100644 tests/integration/dexie-migration.test.ts create mode 100644 tests/integration/sprint1-flow.test.ts create mode 100644 tests/integration/sprint1-report-roi.test.ts create mode 100644 tests/lib/export/csv-export-roi.test.ts create mode 100644 tests/lib/export/pdf-export-roi.test.ts create mode 100644 tests/store/simulation-store-invalidation.test.ts 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/METHODOLOGY_AUTOMATED_COST.md b/METHODOLOGY_AUTOMATED_COST.md index 87d6973..6663d36 100644 --- a/METHODOLOGY_AUTOMATED_COST.md +++ b/METHODOLOGY_AUTOMATED_COST.md @@ -112,6 +112,21 @@ Antes de presentar el reporte de ROI al cliente, validar: | 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. --- 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..41b812c 100644 --- a/TODO.md +++ b/TODO.md @@ -21,6 +21,18 @@ - [ ] **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. + +- [ ] **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. + ## 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/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/domain/roi.ts b/src/domain/roi.ts new file mode 100644 index 0000000..504c5fb --- /dev/null +++ b/src/domain/roi.ts @@ -0,0 +1,79 @@ +// Cálculos de ROI de automatización — funciones puras, sin dependencias de UI ni persistencia. +// Fórmulas nominales simples (ADR-014): sin VPN, TIR ni tasa de descuento. +// Los edge cases (inversión cero, ahorro negativo, división por cero) están explicitados. + +export interface RoiInput { + costPerExecutionActual: number // costo total de una ejecución en escenario actual + costPerExecutionAutomated: number // costo total de una ejecución en escenario automatizado + annualFrequency: number // ejecuciones por año + analysisHorizonYears: number // horizonte de análisis en años + automationInvestment: number // inversión total única del proyecto de automatización +} + +export interface RoiResult { + savingsPerExecution: number // ahorro por ejecución individual (actual - automatizado) + savingsPerExecutionPercent: number // ahorro % sobre el costo actual (0 si actual = 0) + annualSavings: number // ahorro anual = ahorro/ejecución × frecuencia + paybackMonths: number // meses para recuperar inversión (Infinity si ahorro <= 0) + paybackYears: number // años para recuperar inversión + cumulativeSavingsHorizon: number // ahorro acumulado neto al final del horizonte + breaksEvenInHorizon: boolean // ¿el payback ocurre dentro del horizonte? + roiAnnualPercent: number // ROI anualizado % = (ahorroAnual / inversión) × 100 + // Infinity si inversión = 0 y ahorro > 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..396994e --- /dev/null +++ b/src/features/report/ComparisonTable.tsx @@ -0,0 +1,130 @@ +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' + return ( + + + {row.automatable && ( + + )} + + + {row.activityName} + + + {formatCurrency(row.actualCost, currency)} + + + {formatCurrency(row.automatedCost, currency)} + + + {row.savings > 0 ? '+' : ''} + {formatCurrency(row.savings, currency)} + + + {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..78ba581 --- /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: '#10b981', + }, + lineStyle: { color: '#10b981', 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: '#3b82f6', width: 2.5 }, + areaStyle: { + color: { + type: 'linear', + x: 0, y: 0, x2: 0, y2: 1, + colorStops: [ + { offset: 0, color: 'rgba(59,130,246,0.18)' }, + { offset: 1, color: 'rgba(59,130,246,0.02)' }, + ], + }, + }, + itemStyle: { color: '#3b82f6' }, + symbolSize: 6, + markLine: { + silent: true, + symbol: ['none', 'none'], + data: markLineData, + }, + }, + ], + } + }, [roi, horizonYears, currency, investment]) + + return +} 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..54151db --- /dev/null +++ b/src/features/report/RoiKpiBar.tsx @@ -0,0 +1,110 @@ +import { TrendingDown, Calendar, BarChart3, DollarSign, Percent } from 'lucide-react' +import { formatCurrency, formatPercent } from '@/lib/format' +import type { RoiResult } from '@/domain/roi' + +interface RoiKpiBarProps { + roi: RoiResult + currency: string + horizonYears: number +} + +function RoiKpiCard({ + label, value, sub, icon, iconColor, valueColor, +}: { + label: string + value: string + sub?: string + icon: React.ReactNode + iconColor: string + valueColor?: string +}) { + return ( +
+
+ {icon} +
+
+

{label}

+

{value}

+ {sub &&

{sub}

} +
+
+ ) +} + +export function RoiKpiBar({ roi, currency, horizonYears }: RoiKpiBarProps) { + const paybackStr = !isFinite(roi.paybackMonths) + ? 'No se recupera' + : roi.paybackMonths === 0 + ? 'Inmediato' + : roi.paybackMonths < 24 + ? `${roi.paybackMonths.toFixed(1)} meses` + : `${roi.paybackYears.toFixed(1)} años` + + const 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 cumulativePosColor = roi.cumulativeSavingsHorizon > 0 + ? 'text-emerald-700' + : roi.cumulativeSavingsHorizon < 0 + ? 'text-red-600' + : 'text-slate-900' + + return ( +
+ } + 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'} + /> + } + iconColor="bg-violet-50" + valueColor={cumulativePosColor} + /> + } + 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..f742757 --- /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: '#10b981', borderRadius: [0, 4, 4, 0] }, + label: { + show: true, + position: 'right', + fontSize: 10, + formatter: (p: { value: number }) => formatCurrency(p.value, currency), + color: '#10b981', + }, + }, + ], + } + }, [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..7ec1094 --- /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)} 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 2541472..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' @@ -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 */}