feat(sprint-1-5): cierre Etapa 2 - formatPayback + fixes de marca

[
This commit is contained in:
2026-05-16 19:26:19 -03:00
parent f9a008bce5
commit eaaac88fc3
16 changed files with 1626 additions and 55 deletions

View File

@@ -1,5 +1,5 @@
import Papa from 'papaparse'
import { formatSimulationTimestamp } from '@/lib/format'
import { formatPayback, formatSimulationTimestamp } from '@/lib/format'
import { calculateRoi } from '@/domain/roi'
import { buildFileName } from './slug'
import type { Process, Simulation, Resource, Activity, ActivitySimResult } from '@/domain/types'
@@ -109,12 +109,6 @@ function generateRoiCsvContent(
automationInvestment: process.automationInvestment,
})
const paybackStr = !Number.isFinite(roi.paybackMonths)
? 'N/A'
: roi.paybackMonths === 0
? '0'
: formatDecimalForCsv(roi.paybackMonths, currency)
const roiStr = !Number.isFinite(roi.roiAnnualPercent)
? 'Infinito'
: formatDecimalForCsv(roi.roiAnnualPercent, currency)
@@ -128,7 +122,8 @@ function generateRoiCsvContent(
`# Costo total automatizado (${currency}): ${formatDecimalForCsv(resultAutomated.totalCost, currency)}`,
`# Ahorro por ejecución (${currency}): ${formatDecimalForCsv(roi.savingsPerExecution, currency)}`,
`# Ahorro anual (${currency}): ${formatDecimalForCsv(roi.annualSavings, currency)}`,
`# Payback (meses): ${paybackStr}`,
`# Payback (meses): ${formatPayback(roi.paybackMonths)}`,
`# Payback (meses, raw): ${Number.isFinite(roi.paybackMonths) ? roi.paybackMonths.toFixed(4) : roi.paybackMonths}`,
`# Ahorro acumulado a ${process.analysisHorizonYears} años (${currency}): ${formatDecimalForCsv(roi.cumulativeSavingsHorizon, currency)}`,
`# ROI anualizado (%): ${roiStr}`,
`# Frecuencia anual: ${process.annualFrequency}`,

View File

@@ -65,7 +65,7 @@ async function exportScenarioPdf(
title: `Análisis de costos - ${process.name}`,
author: process.clientName || '',
subject: 'Simulación de proceso BPMN',
creator: 'Process Cost Platform',
creator: 'InQ ROI',
keywords: 'bpmn, costos, simulación',
})
@@ -124,7 +124,7 @@ async function exportScenarioPdf(
docAny.addPage()
drawMethodologySection(docAny, process, PDF.margin)
addPageNumbers(docAny, 'Process Cost Platform', simulation.executedAt)
addPageNumbers(docAny, 'InQ ROI · Powered by InQuality', simulation.executedAt)
}
// ─── ROI — 4 páginas ──────────────────────────────────────────────────────────
@@ -153,7 +153,7 @@ async function exportRoiPdf(
title: `Análisis de ROI - ${process.name}`,
author: process.clientName || '',
subject: 'Análisis de retorno de inversión para automatización BPMN',
creator: 'Process Cost Platform',
creator: 'InQ ROI',
keywords: 'bpmn, roi, automatización, payback',
})
@@ -175,7 +175,7 @@ async function exportRoiPdf(
doc.setFont('helvetica', 'normal')
doc.setFontSize(8)
doc.setTextColor(...PDF.slate500)
doc.text(' Actividades en negrita están marcadas como automatizables', PDF.margin, y + 3)
doc.text('(*) Actividades en negrita están marcadas como automatizables', PDF.margin, y + 3)
y += 8
const tableConfig = buildComparisonTableConfig(
@@ -196,7 +196,7 @@ async function exportRoiPdf(
doc.addPage()
drawRoiMethodologySection(doc, process, PDF.margin)
addPageNumbers(doc, 'Process Cost Platform · ROI', simulation.executedAt)
addPageNumbers(doc, 'InQ ROI · Powered by InQuality', simulation.executedAt)
}
function formatNum(n: number): string {

View File

@@ -1,4 +1,4 @@
import { formatCurrency, formatPercent } from '@/lib/format'
import { formatCurrency, formatPayback, formatPercent } from '@/lib/format'
import type { ActivitySimResult, Process, Activity } from '@/domain/types'
import type { RoiResult } from '@/domain/roi'
import { PDF, type DocLike } from './pdf-sections'
@@ -78,13 +78,7 @@ export function drawRoiKpiCards(
const cardW = 52, cardH = 28, gapX = 7, gapY = 5
const col1 = PDF.margin, col2 = PDF.margin + cardW + gapX, col3 = PDF.margin + (cardW + gapX) * 2
const paybackStr = !Number.isFinite(roi.paybackMonths)
? 'No se recupera'
: roi.paybackMonths === 0
? 'Inmediato'
: roi.paybackMonths < 24
? `${roi.paybackMonths.toFixed(1)} meses`
: `${roi.paybackYears.toFixed(1)} años`
const paybackStr = formatPayback(roi.paybackMonths)
const roiStr = !Number.isFinite(roi.roiAnnualPercent)
? '∞'
@@ -94,7 +88,7 @@ export function drawRoiKpiCards(
// Fila 1: 3 cards
{ label: 'AHORRO POR EJECUCIÓN', value: formatCurrency(roi.savingsPerExecution, currency), sub: `${formatPercent(roi.savingsPerExecutionPercent)} vs. actual`, positive: roi.savingsPerExecution >= 0 },
{ label: 'AHORRO ANUAL', value: formatCurrency(roi.annualSavings, currency), sub: `× ${horizonYears} años`, positive: roi.annualSavings >= 0 },
{ label: 'PAYBACK', value: paybackStr, sub: roi.breaksEvenInHorizon ? 'Dentro del horizonte' : 'Fuera del horizonte', positive: roi.breaksEvenInHorizon },
{ label: 'PAYBACK', value: paybackStr, sub: roi.breaksEvenInHorizon ? 'Dentro del horizonte' : 'Fuera del horizonte', positive: roi.breaksEvenInHorizon },
// Fila 2: 2 cards + 1 vacía
{ label: `ACUMULADO (${horizonYears} AÑOS)`, value: formatCurrency(roi.cumulativeSavingsHorizon, currency), sub: 'neto de inversión', positive: roi.cumulativeSavingsHorizon >= 0 },
{ label: 'ROI ANUAL', value: roiStr, sub: 'retorno sobre la inversión', positive: roi.roiAnnualPercent >= 0 },
@@ -127,7 +121,7 @@ export function drawRoiKpiCards(
// Valor
const valueColor = kpi.positive ? [16, 185, 129] as [number,number,number] : [239, 68, 68] as [number,number,number]
doc.setFont('helvetica', 'bold')
doc.setFontSize(kpi.value.length > 14 ? 9 : 11)
doc.setFontSize(kpi.value.length > 22 ? 7 : kpi.value.length > 14 ? 9 : 11)
doc.setTextColor(...valueColor)
doc.text(kpi.value, x + 3, y + 16)
@@ -356,7 +350,7 @@ export function drawTransparencyBlock(
if (roiHigh) {
blocks.push({
text: doc.splitTextToSize(
`ROI inusualmente alto detectado: ${formatPercent(roi.roiAnnualPercent)}. Un ROI > ${formatPercent(ROI_HIGH_THRESHOLD)} anual suele indicar frecuencia sobreestimada, costo automatizado subestimado o inversión insuficiente. Revisá antes de presentar al cliente.`,
`ROI inusualmente alto detectado: ${formatPercent(roi.roiAnnualPercent)}. Un ROI > ${formatPercent(ROI_HIGH_THRESHOLD, 0)} anual suele indicar frecuencia sobreestimada, costo automatizado subestimado o inversión insuficiente. Revisá antes de presentar al cliente.`,
boxW - 8
),
fontSize: 8,