docs: cierre formal de Sprint 1.5
- OBSERVATIONS.md reseteado para Sprint 2 - STRATEGIC_DIRECTION.md con reflexiones del sprint - TECH_DEBT.md curado: items resueltos archivados - methodology/CASE_STUDIES/inq-roi-sprint-1-5.md generado Sprint 1.5 cerrado: - 11 etapas + 1 fix pre-Etapa 11 - 526 unit tests + 38 E2E al cierre - Producto con identidad InQ aplicada en web + 3 PDFs - Metodología documentada con casos reales (2 de mala clasificación de iniciativa, originaron AP.8 + refinamiento C.6) - Material para transmisión al equipo de 5 técnicos
This commit is contained in:
@@ -172,11 +172,19 @@ function _drawNarrativeBanner(
|
||||
|
||||
const tx = m + borderW + padH
|
||||
let ty = y + padV
|
||||
|
||||
// Contexto (línea 1): peso 400, gris — información de fondo
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(...PDF_COLORS.textSecondary)
|
||||
doc.text(l1, tx, ty)
|
||||
ty += l1.length * 4.2 + 3
|
||||
ty += l1.length * 4.2 + 4
|
||||
|
||||
// Resultado (línea 2): peso 700, naranja oscuro — cifras y conclusión destacadas
|
||||
// Refinamiento: contraste visual entre contexto y resultado
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(...C.orangeDark)
|
||||
doc.text(l2, tx, ty)
|
||||
|
||||
return y + boxH + 8
|
||||
@@ -184,17 +192,41 @@ function _drawNarrativeBanner(
|
||||
|
||||
// ── 5.4 KPI Hero con gradiente ────────────────────────────────────────────────
|
||||
|
||||
function _createGradientDataUrl(wMm: number, hMm: number): string | null {
|
||||
function _createGradientDataUrl(wMm: number, hMm: number, radiusMm = 0): string | null {
|
||||
if (typeof document === 'undefined') return null
|
||||
try {
|
||||
const scale = 3
|
||||
const w = Math.round(wMm * scale)
|
||||
const h = Math.round(hMm * scale)
|
||||
const r = Math.round(radiusMm * scale)
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = w
|
||||
canvas.height = h
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return null
|
||||
|
||||
// Fondo blanco opaco — las esquinas fuera del clip quedan blancas,
|
||||
// lo que se ve igual que bordes redondeados sobre página blanca sin
|
||||
// depender de la transparencia PNG en jsPDF.
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.fillRect(0, 0, w, h)
|
||||
|
||||
// Clip al rounded rect antes de pintar el gradiente
|
||||
if (r > 0) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(r, 0)
|
||||
ctx.lineTo(w - r, 0)
|
||||
ctx.arcTo(w, 0, w, r, r)
|
||||
ctx.lineTo(w, h - r)
|
||||
ctx.arcTo(w, h, w - r, h, r)
|
||||
ctx.lineTo(r, h)
|
||||
ctx.arcTo(0, h, 0, h - r, r)
|
||||
ctx.lineTo(0, r)
|
||||
ctx.arcTo(0, 0, r, 0, r)
|
||||
ctx.closePath()
|
||||
ctx.clip()
|
||||
}
|
||||
|
||||
const grad = ctx.createLinearGradient(0, 0, w, h) // 135deg approx
|
||||
grad.addColorStop(0, '#F59845')
|
||||
grad.addColorStop(1, '#ED3E8F')
|
||||
@@ -227,32 +259,28 @@ function _drawHeroCard(
|
||||
): number {
|
||||
const heroH = 46
|
||||
|
||||
// Gradiente de fondo
|
||||
const dataUrl = _createGradientDataUrl(cW, heroH)
|
||||
// Gradiente con esquinas redondeadas baked en el PNG via canvas clip
|
||||
const dataUrl = _createGradientDataUrl(cW, heroH, 4)
|
||||
if (dataUrl) {
|
||||
doc.addImage(dataUrl, 'PNG', m, y, cW, heroH)
|
||||
} else {
|
||||
_drawGradientFallback(doc, m, y, cW, heroH)
|
||||
}
|
||||
|
||||
// Esquinas redondeadas — simular con rect blanco en las esquinas (Nivel 1)
|
||||
// jsPDF no soporta clip en addImage, así que el gradiente queda sin border-radius.
|
||||
// Aceptable para esta etapa.
|
||||
|
||||
// Texto sobre el gradiente
|
||||
const heroX = m + 14
|
||||
let ty = y + 12
|
||||
let ty = y + 11
|
||||
|
||||
// Label superior
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(255, 255, 255)
|
||||
doc.text(`AHORRO PROYECTADO A ${process.analysisHorizonYears} AÑOS`, heroX, ty)
|
||||
ty += 8
|
||||
ty += 11 // Refinamiento: más espacio label→cifra (era 8mm)
|
||||
|
||||
// Cifra principal 28pt
|
||||
// Cifra principal — 26pt (Refinamiento: bajada de 28pt para mejor balance)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.setFontSize(28)
|
||||
doc.setFontSize(26)
|
||||
doc.setTextColor(...C.white)
|
||||
doc.text(formatCurrency(roi.cumulativeSavingsHorizon, currency), heroX, ty)
|
||||
ty += 9
|
||||
@@ -275,8 +303,9 @@ function _drawSecondaryGrid(
|
||||
doc: DocLike, roi: RoiResult, process: Process, currency: string,
|
||||
m: number, cW: number, y: number
|
||||
): number {
|
||||
// Refinamiento: cardH aumentado de 24→28 para acomodar tipografía uniforme (9/18/9pt)
|
||||
const cardW = (cW - 4) / 2
|
||||
const cardH = 24
|
||||
const cardH = 28
|
||||
const gapX = 4, gapY = 4
|
||||
const r = 2
|
||||
|
||||
@@ -294,7 +323,7 @@ function _drawSecondaryGrid(
|
||||
{
|
||||
label: 'AHORRO ANUAL',
|
||||
value: formatCurrency(roi.annualSavings, currency),
|
||||
delta: `× ${process.analysisHorizonYears} años`,
|
||||
delta: `x ${process.analysisHorizonYears} años`,
|
||||
col: 1, row: 0,
|
||||
},
|
||||
{
|
||||
@@ -319,23 +348,23 @@ function _drawSecondaryGrid(
|
||||
doc.setFillColor(...C.orangeLight)
|
||||
doc.roundedRect(cx, cy, cardW, cardH, r, r, 'F')
|
||||
|
||||
// Label
|
||||
// Label: 9pt uniforme en los 4 cards (Refinamiento: era 6.5pt variable)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.setFontSize(6.5)
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(...C.orangeDarker)
|
||||
doc.text(card.label, cx + 5, cy + 6)
|
||||
doc.text(card.label, cx + 5, cy + 7)
|
||||
|
||||
// Valor
|
||||
// Valor: 18pt fijo en los 4 cards (Refinamiento: era variable 9-12pt)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.setFontSize(card.value.length > 16 ? 9 : 12)
|
||||
doc.setFontSize(18)
|
||||
doc.setTextColor(...PDF_COLORS.textPrimary)
|
||||
doc.text(card.value, cx + 5, cy + 14)
|
||||
doc.text(card.value, cx + 5, cy + 17)
|
||||
|
||||
// Delta
|
||||
// Delta: 9pt uniforme en los 4 cards (Refinamiento: era 7.5pt)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.setFontSize(7.5)
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(...C.orangeDark)
|
||||
doc.text(card.delta, cx + 5, cy + 20)
|
||||
doc.text(card.delta, cx + 5, cy + 24)
|
||||
|
||||
maxY = Math.max(maxY, cy + cardH)
|
||||
}
|
||||
@@ -362,12 +391,12 @@ function _drawTop3Activities(
|
||||
.sort((a, b) => b.savings - a.savings)
|
||||
.slice(0, 3)
|
||||
|
||||
// Título
|
||||
// Título: Refinamiento — 11pt, naranja oscuro (era 8pt, gris)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.setFontSize(8)
|
||||
doc.setTextColor(...PDF_COLORS.textDisabled)
|
||||
doc.setFontSize(11)
|
||||
doc.setTextColor(...C.orangeDarker)
|
||||
doc.text('TOP 3 ACTIVIDADES POR AHORRO', m, y)
|
||||
y += 7
|
||||
y += 10
|
||||
|
||||
if (top3.length === 0) {
|
||||
doc.setFont('helvetica', 'normal')
|
||||
@@ -395,9 +424,13 @@ function _drawTop3Activities(
|
||||
doc.setTextColor(...PDF_COLORS.textPrimary)
|
||||
doc.text(name, m, y + barH - 1)
|
||||
|
||||
// Barra de fondo
|
||||
// Barra de fondo (track) con border naranja para definir el área completa
|
||||
doc.setFillColor(...C.orangeBar)
|
||||
doc.roundedRect(m + nameColW, y, barColW, barH, 1, 1, 'F')
|
||||
doc.setDrawColor(...C.heroOrange)
|
||||
doc.setLineWidth(0.3)
|
||||
doc.roundedRect(m + nameColW, y, barColW, barH, 1, 1, 'S')
|
||||
doc.setLineWidth(0.2)
|
||||
|
||||
// Barra de fill
|
||||
if (barFill > 0) {
|
||||
@@ -1011,11 +1044,19 @@ export function drawTrajectoryChart(
|
||||
|
||||
// Máximo
|
||||
doc.text(_fmtAxisVal(yMax), chartLeft - 2, chartTop + 4, { align: 'right' } as Record<string, unknown>)
|
||||
// Cero (solo si hay rango negativo)
|
||||
// Cero y mínimo: cuando la línea cero queda casi en el borde inferior (inversión
|
||||
// muy pequeña relativa al ahorro), los labels "0" y yMin se solapan con "Año 0".
|
||||
// En ese caso mostramos "0" elevado 4mm dentro del área del gráfico y omitimos
|
||||
// yMin (la zona negativa es visualmente invisible; el dato está en la anotación).
|
||||
if (yMin < 0) {
|
||||
doc.text('0', chartLeft - 2, zeroY + 1, { align: 'right' } as Record<string, unknown>)
|
||||
// Mínimo
|
||||
doc.text(_fmtAxisVal(yMin), chartLeft - 2, chartBottom, { align: 'right' } as Record<string, unknown>)
|
||||
const minSepMm = 8
|
||||
const zeroNearBottom = (chartBottom - zeroY) < minSepMm
|
||||
if (!zeroNearBottom) {
|
||||
doc.text('0', chartLeft - 2, zeroY + 1, { align: 'right' } as Record<string, unknown>)
|
||||
doc.text(_fmtAxisVal(yMin), chartLeft - 2, chartBottom, { align: 'right' } as Record<string, unknown>)
|
||||
} else {
|
||||
doc.text('0', chartLeft - 2, chartBottom - 4, { align: 'right' } as Record<string, unknown>)
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Marker de payback (línea vertical punteada, solo si hay payback en horizonte)
|
||||
|
||||
Reference in New Issue
Block a user