From d64078d96ac8d962df9069da7def93e943dc65d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Ben=C3=ADtez?= Date: Tue, 7 Jul 2026 17:55:19 -0300 Subject: [PATCH] =?UTF-8?q?Etapa=200=20aprobada=20condicionalmente.=20Qued?= =?UTF-8?q?a=20pendiente=20tu=20validaci=C3=B3n=20visual:=20abr=C3=AD=20/l?= =?UTF-8?q?ibrary=20en=20https://inq-roi.inqualityhq.com=20y=20confirm?= =?UTF-8?q?=C3=A1=20que=20el=20footer=20muestra=20InQ=20ROI=20=C2=B7=20Pow?= =?UTF-8?q?ered=20by=20InQuality=20=C2=B7=20v0.8.0=20=C2=B7=20Hecho=20desd?= =?UTF-8?q?e=20Paraguay=20=F0=9F=87=B5=F0=9F=87=BE.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 355 +++++++-------------------- package.json | 2 +- sprints/sprint-8/ETAPA_0_PROMPT.md | 37 ++- src/components/AppFooter.tsx | 4 +- src/features/library/LibraryPage.tsx | 2 + src/lib/version.ts | 1 + 6 files changed, 117 insertions(+), 284 deletions(-) create mode 100644 src/lib/version.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b1bcc3..18996cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,262 +1,97 @@ # 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 +All notable changes to InQ ROI will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.8.0] - 2026-07-07 +### Added +- CHANGELOG.md con historial de versiones +- AppFooter con versión visible en todas las páginas +- Conventional Commits adoptados como estándar de commits + +## [0.7.0] - 2026-07-07 +### Added +- Multi-tenancy: tabla `organizations`, 4 roles de plataforma (platform_admin, member, client_editor, client_viewer) +- Edge Functions `invite-user` y `revoke-user` para gestión de acceso +- Panel de administración `/admin` con gestión de Organizations y Users +- Login email/password y página `/reset-password` para flujo de invitación +- Back button contextual en workspace (muestra nombre del cliente via JOIN en getById) +- Política RLS `simulations_update` para que client_editor pueda guardar simulaciones + +### Fixed +- Race condition en AuthContext: `isProfileLoaded` como guard antes de renderizar contenido rol-dependiente +- `syncProfileFromDB` ahora lee `platform_role` y `org_id` desde DB (no inferido por dominio de email) +- `normalizeGroupLabel()`: limpia artículos iniciales de labels de grupos con datos corruptos + +## [0.6.0] - 2026-06-24 +### Added +- Logo InQuality en header de PDFs (isotipo, páginas 2+, fallback a texto) +- Panel de análisis de sensibilidad en tab ROI: sliders de volumen e inversión, recálculo en tiempo real +- Doble línea en CumulativeSavingsChart (base gris dashed + ajustada naranja con área sombreada) +- `whitespace-nowrap` en MetricCell de columna Eficiencia + +### Fixed +- `ProfileSheet.getAccessToken()` leía clave inexistente (`'supabase.auth.token'`); el guardado de perfil nunca llegaba a Supabase desde Sprint 4. Fix: calcular `sb-${projectRef}-auth-token` + +## [0.5.0] - 2026-06-23 +### Added +- Fix crítico: workspace loading — race condition auth/processId resuelto con `isProfileLoaded` +- Controles de zoom BPMN: fit-viewport con centering correcto, botones zoom-in/zoom-out/fit +- Heatmap con gradiente continuo de 4 stops + toggle percentile/min-max persistido +- "Ver procesos por recurso" en catálogo de recursos +- Campo "empresa" en perfil de usuario +- Columna "Eficiencia de automatización" en ComparisonTable (+ columna "Ahorro esperado") + +### Fixed +- Flakiness en botón "Guardar cambios": debounce + Web Locks en process-store +- Specs E2E migradas de Dexie a Supabase + +## [0.4.0] - 2026-06-18 +### Added +- Autenticación Google Workspace OAuth + Supabase +- Migración completa IndexedDB (Dexie) → PostgreSQL (Supabase) +- Row Level Security con `is_platform_admin()` y políticas granulares +- Catálogo global de recursos (sin processId) +- Identidad en UI: avatar/dropdown AppHeader, "Actualizado por" en proceso +- ResourceCatalogPage + ResourceFormSheet +- Fix Web Locks API deadlock (processLock) +- Fix `.maybeSingle()` en simulation-repo +- Fix `formatCostPerHour()` precisión dinámica +- Fix Google OAuth `prompt: 'select_account'` +- Catálogo con contexto de proceso (banner + query param fromProcess) + +## [0.3.0] - 2026-05-27 +### Added +- Biblioteca de procesos con grupos/tags/búsqueda (Dexie v4) +- Indicador de simulación desactualizada ("Hay cambios sin simular") +- Zoom BPMN fit-viewport +- Recursos TO-BE en escenario automatizado (motor + UI + reporte + PDF) +- Branding InQ ROI en todas las páginas (AppHeader + favicon + logos) +- Kit de socialización del equipo: guía de usuario, guía de pre-venta, presentación PPTX + +## [0.2.0] - 2026-05-20 +### Added +- Modelo de recursos `utilizationMinutes + units` (migración Dexie v3) +- ActivityPanel con cálculo de costo en tiempo real +- Reporte web con filas expandibles de desglose de recursos +- Edición inline de proceso/cliente +- Tabla de recursos en PDF (página condicional) + +## [0.1.5] - 2026-05-18 +### Added +- Identidad visual completa: paleta naranja InQ #F59845, gradiente → magenta #ED3E8F +- PDFs de clase profesional: 3 documentos (Actual, Automatizado, ROI) +- Badge ⚡ en canvas BPMN para actividades automatizables +- ROI con 5 KPIs +- Sistema de componentes establecido + +## [0.1.0] - 2026-05-01 +### Added +- MVP: importar BPMN 2.0, parametrizar actividades, simular costos, exportar PDF +- Motor de simulación AS-IS / TO-BE (Modelo A) +- Heatmap de costos en canvas BPMN +- Reporte web básico con tabla de actividades y gráfico de barras diff --git a/package.json b/package.json index 402ae7e..d4a46a2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "process-cost-platform", "private": true, - "version": "0.0.0", + "version": "0.8.0", "type": "module", "scripts": { "dev": "vite", diff --git a/sprints/sprint-8/ETAPA_0_PROMPT.md b/sprints/sprint-8/ETAPA_0_PROMPT.md index 79eab30..6b0e754 100644 --- a/sprints/sprint-8/ETAPA_0_PROMPT.md +++ b/sprints/sprint-8/ETAPA_0_PROMPT.md @@ -47,40 +47,34 @@ export const APP_VERSION = '0.8.0' Si Vite ya expone `import.meta.env.VITE_APP_VERSION` o similar, usar ese mecanismo. Si no, usar la constante string — es más simple y suficiente para el caso de uso. -### 3. Crear `src/components/AppFooter.tsx` +### 3. Actualizar `src/components/AppFooter.tsx` -Componente discreto, una sola línea, esquina inferior. Visible en todas las páginas. - -**Diseño:** -- Texto: `InQ ROI v0.8.0 · Powered by InQuality` -- Tipografía: `text-xs text-muted-foreground` -- Alineación: centrado o derecha, `py-4` -- Sin borde superior ni decoración adicional +El componente **ya existe** con versión hardcodeada `v0.1.0`. Solo cambiar la versión hardcodeada por la constante de `version.ts`. Mantener el resto del texto y estilos exactamente igual. ```tsx import { APP_VERSION } from '@/lib/version' export function AppFooter() { return ( -