feat(sprint-1-5): cierre Etapa 2 - formatPayback + fixes de marca
[
This commit is contained in:
@@ -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}`,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -25,6 +25,32 @@ export function formatMinutes(minutes: number): string {
|
||||
return m > 0 ? `${h}h ${m}min` : `${h}h`
|
||||
}
|
||||
|
||||
export function formatPayback(months: number): string {
|
||||
if (Number.isNaN(months) || !isFinite(months)) {
|
||||
return 'No recupera en horizonte'
|
||||
}
|
||||
if (months < 0) {
|
||||
return 'Sin payback (costo aumenta)'
|
||||
}
|
||||
if (months === 0) {
|
||||
return 'Inmediato'
|
||||
}
|
||||
if (months < 0.1) {
|
||||
return '< 1 día'
|
||||
}
|
||||
if (months < 1) {
|
||||
return `< 1 mes (${months.toFixed(2)} meses)`
|
||||
}
|
||||
if (months < 12) {
|
||||
return `${months.toFixed(1)} meses`
|
||||
}
|
||||
if (months < 24) {
|
||||
const years = (months / 12).toFixed(1)
|
||||
return `${months.toFixed(1)} meses (~${years} años)`
|
||||
}
|
||||
return `${(months / 12).toFixed(1)} años`
|
||||
}
|
||||
|
||||
// Formato de timestamp de simulación: "13 de mayo de 2026 a las 09:45"
|
||||
// Locale explícito es-PY — nunca depende del locale del browser del cliente.
|
||||
export function formatSimulationTimestamp(ts: number): { date: string; time: string } {
|
||||
|
||||
Reference in New Issue
Block a user