diff --git a/index.html b/index.html index 42beec3..f9023c9 100644 --- a/index.html +++ b/index.html @@ -1,26 +1,26 @@ - - - - - - + + + + + + - Process Cost Platform — Cuantificá tus procesos - + InQ ROI — Cuantificá tus procesos + - - - - - + + + + + - - - - - - -
- - - + + + + + + +
+ + + diff --git a/methodology-initial-commit.tar.gz b/methodology-initial-commit.tar.gz new file mode 100644 index 0000000..0861003 Binary files /dev/null and b/methodology-initial-commit.tar.gz differ diff --git a/src/components/AppFooter.tsx b/src/components/AppFooter.tsx index 958231c..261f76f 100644 --- a/src/components/AppFooter.tsx +++ b/src/components/AppFooter.tsx @@ -2,7 +2,7 @@ export function AppFooter() { return ( ) diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 62234d4..8994d57 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -8,7 +8,7 @@ const buttonVariants = cva( { variants: { variant: { - default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90 font-bold', destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', diff --git a/src/features/report/ComparisonTable.tsx b/src/features/report/ComparisonTable.tsx index 396994e..801be6f 100644 --- a/src/features/report/ComparisonTable.tsx +++ b/src/features/report/ComparisonTable.tsx @@ -65,16 +65,16 @@ export function ComparisonTable({ } return ( -
+
- + - - Actividad - Costo actual - Costo automatizado + + Actividad + Costo actual + Costo automatizado setSortDir((d) => (d === 'desc' ? 'asc' : 'desc'))} > @@ -82,7 +82,7 @@ export function ComparisonTable({ - % ahorro + % ahorro @@ -93,8 +93,10 @@ export function ComparisonTable({ : row.savings < 0 ? 'text-red-600' : 'text-slate-400' + const rowClass = row.automatable ? '' : 'bg-slate-50/60' + const textClass = row.automatable ? 'text-slate-800' : 'text-slate-500' return ( - + {row.automatable && ( )} - + {row.activityName} - + {formatCurrency(row.actualCost, currency)} - + {formatCurrency(row.automatedCost, currency)} - - {row.savings > 0 ? '+' : ''} - {formatCurrency(row.savings, currency)} + + {row.automatable + ? (row.savings > 0 ? '+' : '') + formatCurrency(row.savings, currency) + : '—'} - - {row.savings > 0 ? '+' : ''} - {formatPercent(row.savingsPercent)} + + {row.automatable + ? (row.savings > 0 ? '+' : '') + formatPercent(row.savingsPercent) + : '—'} ) diff --git a/src/features/report/CumulativeSavingsChart.tsx b/src/features/report/CumulativeSavingsChart.tsx index 78ba581..92ad3c3 100644 --- a/src/features/report/CumulativeSavingsChart.tsx +++ b/src/features/report/CumulativeSavingsChart.tsx @@ -29,9 +29,9 @@ export function CumulativeSavingsChart({ formatter: `Payback\n${roi.paybackYears.toFixed(1)} años`, position: 'insideEndTop', fontSize: 10, - color: '#10b981', + color: '#16A34A', }, - lineStyle: { color: '#10b981', type: 'dashed', width: 2 }, + lineStyle: { color: '#16A34A', type: 'dashed', width: 2 }, }) } @@ -52,7 +52,7 @@ export function CumulativeSavingsChart({ const val = p.value[1] return `
Año ${year}
-
Acumulado neto: ${formatCurrency(val, currency)}
+
Acumulado neto: ${formatCurrency(val, currency)}
` }, }, @@ -88,18 +88,18 @@ export function CumulativeSavingsChart({ type: 'line', data: years.map((y, i) => [y, values[i]]), smooth: false, - lineStyle: { color: '#3b82f6', width: 2.5 }, + lineStyle: { color: '#F59845', 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)' }, + { offset: 0, color: 'rgba(245,152,69,0.18)' }, + { offset: 1, color: 'rgba(245,152,69,0.02)' }, ], }, }, - itemStyle: { color: '#3b82f6' }, + itemStyle: { color: '#F59845' }, symbolSize: 6, markLine: { silent: true, diff --git a/src/features/report/HeatmapLegend.tsx b/src/features/report/HeatmapLegend.tsx index b2ead6f..dfe0a7e 100644 --- a/src/features/report/HeatmapLegend.tsx +++ b/src/features/report/HeatmapLegend.tsx @@ -1,5 +1,5 @@ import { formatCurrency, formatPercent } from '@/lib/format' -import { heatmapLegendBounds } from '@/lib/colors' +import { heatmapLegendBounds, HEATMAP_LOW_HEX, HEATMAP_MID_HEX, HEATMAP_HIGH_HEX } from '@/lib/colors' import type { HeatmapMode } from '@/lib/colors' import type { ActivitySimResult } from '@/domain/types' @@ -21,7 +21,7 @@ export function HeatmapLegend({ perActivity, mode, currency }: HeatmapLegendProp
Bajo costo
Alto costo
diff --git a/src/features/report/RoiKpiBar.tsx b/src/features/report/RoiKpiBar.tsx index 985405c..4b1d64d 100644 --- a/src/features/report/RoiKpiBar.tsx +++ b/src/features/report/RoiKpiBar.tsx @@ -1,4 +1,4 @@ -import { TrendingDown, Calendar, BarChart3, DollarSign, Percent } from 'lucide-react' +import { TrendingDown, Calendar, DollarSign, Percent } from 'lucide-react' import { formatCurrency, formatPayback, formatPercent } from '@/lib/format' import type { RoiResult } from '@/domain/roi' @@ -8,6 +8,28 @@ interface RoiKpiBarProps { horizonYears: number } +function RoiKpiHero({ + label, value, sub, +}: { + label: string + value: string + sub?: string +}) { + return ( +
+

{label}

+

+ {value} +

+ {sub &&

{sub}

} +
+ ) +} + function RoiKpiCard({ label, value, sub, icon, iconColor, valueColor, compact, }: { @@ -20,13 +42,13 @@ function RoiKpiCard({ compact?: boolean }) { return ( -
-
+
+
{icon}

{label}

-

{value}

+

{value}

{sub &&

{sub}

}
@@ -52,55 +74,55 @@ export function RoiKpiBar({ roi, currency, horizonYears }: RoiKpiBarProps) { ? 'text-red-600' : 'text-slate-900' - const cumulativePosColor = roi.cumulativeSavingsHorizon > 0 - ? 'text-emerald-700' - : roi.cumulativeSavingsHorizon < 0 - ? 'text-red-600' - : 'text-slate-900' + const cumulativeSub = roi.cumulativeSavingsHorizon > 0 + ? `neto de inversión inicial · ${horizonYears} años` + : 'sin recuperación neta en el horizonte' return ( -
- } - iconColor="bg-emerald-50" - valueColor={savingsPosColor} - /> - } - iconColor="bg-emerald-50" - valueColor={annualPosColor} - /> - } - iconColor="bg-primary/10" - valueColor={roi.breaksEvenInHorizon ? 'text-emerald-700' : 'text-slate-900'} - compact - /> - + {/* KPI Hero — Ahorro proyectado a N años */} + } - 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'} + sub={cumulativeSub} /> + + {/* KPIs secundarios — grid 4 columnas */} +
+ } + iconColor="bg-emerald-50" + valueColor={savingsPosColor} + /> + } + iconColor="bg-emerald-50" + valueColor={annualPosColor} + /> + } + iconColor="bg-primary/10" + valueColor={roi.breaksEvenInHorizon ? 'text-emerald-700' : 'text-slate-900'} + compact + /> + } + iconColor="bg-amber-50" + valueColor={roi.roiAnnualPercent > 0 ? 'text-emerald-700' : roi.roiAnnualPercent < 0 ? 'text-red-600' : 'text-slate-900'} + /> +
) } diff --git a/src/features/report/TopSavingsChart.tsx b/src/features/report/TopSavingsChart.tsx index f742757..a17dc5c 100644 --- a/src/features/report/TopSavingsChart.tsx +++ b/src/features/report/TopSavingsChart.tsx @@ -70,13 +70,13 @@ export function TopSavingsChart({ type: 'bar', data: reversed.map((s) => s.value), barMaxWidth: 32, - itemStyle: { color: '#10b981', borderRadius: [0, 4, 4, 0] }, + itemStyle: { color: '#16A34A', borderRadius: [0, 4, 4, 0] }, label: { show: true, position: 'right', fontSize: 10, formatter: (p: { value: number }) => formatCurrency(p.value, currency), - color: '#10b981', + color: '#16A34A', }, }, ], diff --git a/src/features/workspace/ResourcesPanel.tsx b/src/features/workspace/ResourcesPanel.tsx index 1b833f8..eeb191d 100644 --- a/src/features/workspace/ResourcesPanel.tsx +++ b/src/features/workspace/ResourcesPanel.tsx @@ -19,7 +19,7 @@ const RESOURCE_TYPE_LABELS: Record = { } const RESOURCE_TYPE_COLORS: Record = { - role: 'bg-blue-100 text-blue-700', + role: 'bg-slate-100 text-slate-600', person: 'bg-green-100 text-green-700', system: 'bg-purple-100 text-purple-700', equipment: 'bg-amber-100 text-amber-700', diff --git a/src/lib/chart-options.ts b/src/lib/chart-options.ts index 9dfd18b..a64003c 100644 --- a/src/lib/chart-options.ts +++ b/src/lib/chart-options.ts @@ -120,7 +120,7 @@ export function buildCostCompositionOption( labelLine: { length: 10, length2: 8 }, emphasis: { label: { fontSize: 13, fontWeight: 'bold' } }, data: [ - { value: result.totalDirectCost, name: 'Costo directo', itemStyle: { color: '#3b82f6' } }, + { value: result.totalDirectCost, name: 'Costo directo', itemStyle: { color: '#F59845' } }, ...(hasIndirect ? [{ value: result.totalIndirectCost, name: 'Costo indirecto (overhead)', itemStyle: { color: '#94a3b8' } }] : []), ], }], diff --git a/src/lib/colors.ts b/src/lib/colors.ts index e94dd0d..5acf9d9 100644 --- a/src/lib/colors.ts +++ b/src/lib/colors.ts @@ -1,8 +1,13 @@ // Paleta heatmap: verde → amarillo → rojo +// INVARIANTE: estos valores DEBEN coincidir con tailwind.config.js heatmap.* const HEATMAP_LOW = { r: 16, g: 185, b: 129 } // #10b981 const HEATMAP_MID = { r: 245, g: 158, b: 11 } // #f59e0b const HEATMAP_HIGH = { r: 239, g: 68, b: 68 } // #ef4444 +export const HEATMAP_LOW_HEX = '#10b981' +export const HEATMAP_MID_HEX = '#f59e0b' +export const HEATMAP_HIGH_HEX = '#ef4444' + function lerp(a: number, b: number, t: number): number { return Math.round(a + (b - a) * t) } diff --git a/tests/e2e/screenshots-etapa-3.spec.ts b/tests/e2e/screenshots-etapa-3.spec.ts new file mode 100644 index 0000000..d762e22 --- /dev/null +++ b/tests/e2e/screenshots-etapa-3.spec.ts @@ -0,0 +1,134 @@ +/** + * Screenshots de validación visual — Etapa 3 Sprint 1.5. + * + * Captura 8 vistas que demuestran la identidad InQ aplicada en el web app: + * 1. e3-workspace.png — topbar + CTAs naranjas bold + * 2. e3-switch-on.png — ActivityPanel switch ON (caja naranja) + * 3. e3-report-tabs.png — tab activo naranja en reporte + * 4. e3-roi-kpi-hero.png — KPI Hero con gradiente naranja→magenta + * 5. e3-comparison-table.png — tabla comparativa con header naranja + * 6. e3-cumulative-chart.png — gráfico acumulado con línea naranja + * 7. e3-donut-chart.png — donut directo/overhead (naranja/slate) + * 8. e3-transparency.png — TransparencySection con ROI > 1000% + */ +import { test } from '@playwright/test' +import { resolve } from 'path' +import { mkdirSync } from 'fs' +import { fileURLToPath } from 'url' + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const BPMN_DIR = resolve(__dirname, '../../public/sample-processes') +const OUT_DIR = resolve(__dirname, '../screenshots/etapa-3') + +test.beforeAll(() => { mkdirSync(OUT_DIR, { recursive: true }) }) + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +async function importBpmn(page: import('@playwright/test').Page, filename: string) { + await page.goto('/') + await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, filename)) + await page.waitForURL(/\/workspace\//, { timeout: 15_000 }) + await page.waitForSelector('button:has-text("Simular")', { timeout: 10_000 }) + await page.waitForTimeout(800) +} + +async function setActivityCosts( + page: import('@playwright/test').Page, + elementId: string, + directCost: number, + automatedCost: number +) { + await page.locator(`[data-element-id="${elementId}"]`).first().click() + await page.waitForTimeout(400) + const toggle = page.getByRole('switch', { name: /automatizable/i }) + if (await toggle.getAttribute('aria-checked') === 'false') await toggle.click() + await page.waitForTimeout(200) + await page.getByLabel(/Costo directo fijo/i).fill(String(directCost)) + await page.getByLabel(/Costo automatizado/i).fill(String(automatedCost)) + await page.getByLabel(/Tiempo automatizado/i).fill('10') + await page.getByRole('button', { name: /Guardar/i }).click() + await page.waitForTimeout(400) +} + +async function configureGlobal(page: import('@playwright/test').Page, freq: number, inv: number) { + await page.getByRole('tab', { name: /Global/i }).click() + await page.waitForTimeout(300) + await page.getByLabel(/Frecuencia anual/i).fill(String(freq)) + await page.getByLabel(/Inversión en automatización/i).fill(String(inv)) + await page.getByRole('button', { name: /Guardar/i }).click() + await page.waitForTimeout(400) +} + +async function goToRoiTab(page: import('@playwright/test').Page) { + const roiTab = page.getByRole('tab', { name: /Comparación/i }) + await roiTab.click() + await page.waitForSelector('[data-testid="roi-kpi-card"]', { timeout: 10_000 }) + await page.waitForTimeout(600) +} + +// ─── 1. Workspace — topbar + CTAs ───────────────────────────────────────────── + +test('e3-1: workspace — CTAs naranjas bold', async ({ page }) => { + await importBpmn(page, 'simple-linear.bpmn') + await page.waitForTimeout(500) + await page.screenshot({ path: resolve(OUT_DIR, 'e3-workspace.png'), fullPage: false }) +}) + +// ─── 2. Switch ON en ActivityPanel ──────────────────────────────────────────── + +test('e3-2: switch ON — caja automatización naranja', async ({ page }) => { + await importBpmn(page, 'simple-linear.bpmn') + await page.locator('[data-element-id="task1"]').first().click() + await page.waitForTimeout(400) + const toggle = page.getByRole('switch', { name: /automatizable/i }) + if (await toggle.getAttribute('aria-checked') === 'false') await toggle.click() + await page.waitForTimeout(300) + await page.screenshot({ path: resolve(OUT_DIR, 'e3-switch-on.png'), fullPage: false }) +}) + +// ─── 3-8. Setup realista para los screenshots del reporte ───────────────────── + +test('e3-3-to-8: reporte — 6 screenshots del tab ROI', async ({ page }) => { + await importBpmn(page, 'medium-with-gateways.bpmn') + await setActivityCosts(page, 'task_recv', 200, 20) + await setActivityCosts(page, 'task_score', 200, 20) + await setActivityCosts(page, 'task_analisis', 200, 20) + await configureGlobal(page, 5000, 80000) + + const simBtn = page.getByRole('button', { name: 'Simular' }) + await simBtn.click() + await page.waitForURL(/\/report\//, { timeout: 20_000 }) + await page.waitForSelector('button:has-text("Exportar PDF")', { timeout: 15_000 }) + await page.waitForTimeout(800) + + // 3. Tabs + await page.screenshot({ path: resolve(OUT_DIR, 'e3-report-tabs.png'), fullPage: false }) + + await goToRoiTab(page) + + // 4. KPI Hero + await page.screenshot({ path: resolve(OUT_DIR, 'e3-roi-kpi-hero.png'), fullPage: false }) + + // 5. Comparison Table (scroll down) + await page.evaluate(() => window.scrollBy(0, 420)) + await page.waitForTimeout(300) + await page.screenshot({ path: resolve(OUT_DIR, 'e3-comparison-table.png'), fullPage: false }) + + // 6. Cumulative chart (scroll to charts section) + await page.evaluate(() => window.scrollBy(0, 500)) + await page.waitForTimeout(400) + await page.screenshot({ path: resolve(OUT_DIR, 'e3-cumulative-chart.png'), fullPage: false }) + + // 7. Donut chart (actual tab) + await page.getByRole('tab', { name: 'Actual' }).click() + await page.waitForTimeout(600) + await page.evaluate(() => window.scrollTo(0, 0)) + await page.waitForTimeout(300) + await page.screenshot({ path: resolve(OUT_DIR, 'e3-donut-chart.png'), fullPage: false }) + + // 8. Transparency section — ROI > 1000% (back to ROI tab, scroll to bottom) + await goToRoiTab(page) + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)) + await page.waitForTimeout(500) + await page.screenshot({ path: resolve(OUT_DIR, 'e3-transparency.png'), fullPage: false }) +}) diff --git a/tests/features/report/__snapshots__/derivations.test.ts.snap b/tests/features/report/__snapshots__/derivations.test.ts.snap index 728e896..c81d794 100644 --- a/tests/features/report/__snapshots__/derivations.test.ts.snap +++ b/tests/features/report/__snapshots__/derivations.test.ts.snap @@ -37,7 +37,7 @@ exports[`buildCostCompositionOption — estructura del objeto ECharts > snapshot "data": [ { "itemStyle": { - "color": "#3b82f6", + "color": "#F59845", }, "name": "Costo directo", "value": 1000, diff --git a/tests/screenshots/etapa-3/e3-comparison-table.png b/tests/screenshots/etapa-3/e3-comparison-table.png new file mode 100644 index 0000000..5412d3b Binary files /dev/null and b/tests/screenshots/etapa-3/e3-comparison-table.png differ diff --git a/tests/screenshots/etapa-3/e3-cumulative-chart.png b/tests/screenshots/etapa-3/e3-cumulative-chart.png new file mode 100644 index 0000000..94df94d Binary files /dev/null and b/tests/screenshots/etapa-3/e3-cumulative-chart.png differ diff --git a/tests/screenshots/etapa-3/e3-donut-chart.png b/tests/screenshots/etapa-3/e3-donut-chart.png new file mode 100644 index 0000000..99e6f3c Binary files /dev/null and b/tests/screenshots/etapa-3/e3-donut-chart.png differ diff --git a/tests/screenshots/etapa-3/e3-report-tabs.png b/tests/screenshots/etapa-3/e3-report-tabs.png new file mode 100644 index 0000000..99e6f3c Binary files /dev/null and b/tests/screenshots/etapa-3/e3-report-tabs.png differ diff --git a/tests/screenshots/etapa-3/e3-roi-kpi-hero.png b/tests/screenshots/etapa-3/e3-roi-kpi-hero.png new file mode 100644 index 0000000..fcf01a8 Binary files /dev/null and b/tests/screenshots/etapa-3/e3-roi-kpi-hero.png differ diff --git a/tests/screenshots/etapa-3/e3-switch-on.png b/tests/screenshots/etapa-3/e3-switch-on.png new file mode 100644 index 0000000..68c59ba Binary files /dev/null and b/tests/screenshots/etapa-3/e3-switch-on.png differ diff --git a/tests/screenshots/etapa-3/e3-transparency.png b/tests/screenshots/etapa-3/e3-transparency.png new file mode 100644 index 0000000..086e215 Binary files /dev/null and b/tests/screenshots/etapa-3/e3-transparency.png differ diff --git a/tests/screenshots/etapa-3/e3-workspace.png b/tests/screenshots/etapa-3/e3-workspace.png new file mode 100644 index 0000000..9230b40 Binary files /dev/null and b/tests/screenshots/etapa-3/e3-workspace.png differ