loadImageBase64 ahora retorna { base64, pixelWidth, pixelHeight } (320×210px tras el resize) en vez de solo el string.
PdfHeaderOptions agrega logoPixelWidth/logoPixelHeight.
drawSharedHeader: logo a 15mm de ancho con alto calculado proporcionalmente desde las dimensiones reales (≈9.8mm, ratio real ~1.53:1, no el 3.56:1 hardcodeado anterior). El logo se posiciona arriba (MARGIN - 1), y processName + fecha bajan a la línea siguiente sin superponerse. clientName baja una línea adicional cuando hay logo (para no chocar con la fecha), ajustando y antes del border-bottom.
Fallback a texto "InQ ROI" intacto cuando no hay logo.
This commit is contained in:
156
sprints/sprint-6/ETAPA_1_PROMPT.md
Normal file
156
sprints/sprint-6/ETAPA_1_PROMPT.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# Etapa 1 — Logo InQuality en header de PDFs + whitespace-nowrap
|
||||||
|
|
||||||
|
**Sprint:** 6
|
||||||
|
**Fecha:** 2026-06-23
|
||||||
|
**Alcance:** 3 PDFs (Actual, Automatizado, ROI) + un fix de una línea en ComparisonTable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
El header compartido de los 3 PDFs (`src/lib/export/pdf-sections-shared.ts`, función `drawSharedHeader`) hoy dibuja el texto "InQ ROI" en naranja bold como identidad de marca. El logo PNG de InQuality ya existe en `assets/`. El objetivo es reemplazar el texto por el logo, manteniendo el layout del header sin cambios estructurales.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parte A — Logo en header de PDFs
|
||||||
|
|
||||||
|
### Assets disponibles
|
||||||
|
|
||||||
|
En `assets/` (ruta desde la raíz del proyecto):
|
||||||
|
- `MARCA FINAL 2023 AREA DE SEGURIDAD 1 COLOR.png` — logo horizontal color, sobre fondos claros — **usar este**
|
||||||
|
- `MARCA FINAL 2023 AREA DE SEGURIDAD 1 BCO.png` — logo horizontal blanco (no aplica, el header es blanco)
|
||||||
|
|
||||||
|
### Cómo cargar el PNG en jsPDF
|
||||||
|
|
||||||
|
Importar la URL del asset con Vite:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// En pdf-export.ts (o en un helper de carga)
|
||||||
|
import logoColorUrl from '../../../assets/MARCA FINAL 2023 AREA DE SEGURIDAD 1 COLOR.png?url'
|
||||||
|
```
|
||||||
|
|
||||||
|
Convertir a base64 antes de generar cualquier PDF (operación async, una vez por export):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
async function loadImageBase64(url: string): Promise<string> {
|
||||||
|
const response = await fetch(url)
|
||||||
|
const blob = await response.blob()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = () => resolve(reader.result as string)
|
||||||
|
reader.onerror = reject
|
||||||
|
reader.readAsDataURL(blob)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Llamar una vez al inicio de cada función de export (`exportPdf`, `exportRoiPdf`) y pasar el resultado como parámetro.
|
||||||
|
|
||||||
|
### Cambios en `PdfHeaderOptions`
|
||||||
|
|
||||||
|
Agregar un campo opcional:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface PdfHeaderOptions {
|
||||||
|
pageNumber: number
|
||||||
|
processName: string
|
||||||
|
sectionTitle?: string
|
||||||
|
clientName?: string
|
||||||
|
simulatedAt: number
|
||||||
|
logoBase64?: string // ← nuevo, opcional para retrocompatibilidad
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cambios en `drawSharedHeader`
|
||||||
|
|
||||||
|
Reemplazar el bloque de texto "InQ ROI" por lógica condicional:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// ANTES:
|
||||||
|
doc.setFont('helvetica', 'bold')
|
||||||
|
doc.setFontSize(13)
|
||||||
|
doc.setTextColor(...PDF_COLORS.inqOrange)
|
||||||
|
doc.text('InQ ROI', MARGIN, y)
|
||||||
|
|
||||||
|
// DESPUÉS:
|
||||||
|
if (options.logoBase64) {
|
||||||
|
// Logo PNG: ancho 32mm, alto proporcional (~9mm para este logo horizontal)
|
||||||
|
// Alinear el baseline del logo con la primera línea de texto: y - 7 (arriba del baseline)
|
||||||
|
doc.addImage(options.logoBase64, 'PNG', MARGIN, y - 7, 32, 9)
|
||||||
|
} else {
|
||||||
|
// Fallback al texto (retrocompatibilidad y tests)
|
||||||
|
doc.setFont('helvetica', 'bold')
|
||||||
|
doc.setFontSize(13)
|
||||||
|
doc.setTextColor(...PDF_COLORS.inqOrange)
|
||||||
|
doc.text('InQ ROI', MARGIN, y)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nota sobre dimensiones:** el logo horizontal de InQuality tiene una proporción aproximada de 3.5:1 (ancho:alto). A 32mm de ancho → ~9mm de alto. Ajustar si el resultado visual no queda bien — la validación visual del director confirma antes del OK formal.
|
||||||
|
|
||||||
|
### Cambios en `pdf-export.ts`
|
||||||
|
|
||||||
|
En las dos funciones de export (`exportPdf` y `exportRoiPdf`), agregar la carga del logo al inicio:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export async function exportPdf(...): Promise<void> {
|
||||||
|
const logoBase64 = await loadImageBase64(logoColorUrl).catch(() => undefined)
|
||||||
|
|
||||||
|
const headerOpts: PdfHeaderOptions = {
|
||||||
|
// ...opciones existentes...
|
||||||
|
logoBase64,
|
||||||
|
}
|
||||||
|
// resto sin cambios
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
El `.catch(() => undefined)` garantiza que si el fetch del asset falla (ej: entorno sin red), el PDF se genera igual con el fallback de texto. No bloquear el export por el logo.
|
||||||
|
|
||||||
|
### Importante — NO cambiar
|
||||||
|
|
||||||
|
- La estructura del header (posiciones de proceso, cliente, fecha, border-bottom) no cambia.
|
||||||
|
- El footer ("Powered by InQuality") no cambia.
|
||||||
|
- `drawSharedFooter` y `applyFootersToAllPages` no se tocan.
|
||||||
|
- No agregar el logo en páginas 1 de cada PDF que tengan su propio diseño de portada (verificar si `drawScenarioPage1` y `drawRoiPage1` usan o no `drawSharedHeader` — si no lo usan, el logo solo aplica a las páginas que sí lo llaman).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parte B — whitespace-nowrap en celda de Eficiencia
|
||||||
|
|
||||||
|
En `src/features/report/ComparisonTable.tsx`, dentro de `MetricCell`, el `<span>` que muestra el valor numérico de la columna "Eficiencia" se corta en dos líneas ("1.02" / "USD/min") porque la celda es angosta.
|
||||||
|
|
||||||
|
Fix: agregar `whitespace-nowrap` al className del `<span>`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Buscar el span que tiene el valor formateado de la Eficiencia y agregar whitespace-nowrap
|
||||||
|
<span className="font-mono text-slate-700 text-xs w-14 text-right whitespace-nowrap">
|
||||||
|
{format(value)}
|
||||||
|
</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
Una línea de cambio. Sin tests que actualizar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Criterios de validación
|
||||||
|
|
||||||
|
- [ ] Logo visible en el header de todas las páginas de los 3 PDFs (Actual, Automatizado, ROI)
|
||||||
|
- [ ] Logo en tamaño coherente con el header — no dominante, no microscópico
|
||||||
|
- [ ] Alineación correcta: logo a la izquierda, fecha y cliente a la derecha
|
||||||
|
- [ ] El footer "Powered by InQuality" sigue presente en todas las páginas
|
||||||
|
- [ ] Si el asset no carga (fetch falla), el PDF se genera igual con texto "InQ ROI" como fallback
|
||||||
|
- [ ] "1.02 USD/min" aparece en una sola línea en ComparisonTable
|
||||||
|
- [ ] `npm run lint` sin errores
|
||||||
|
- [ ] `npm run build` limpio
|
||||||
|
- [ ] `npm run test` → 542 tests verdes (sin regresiones — los tests de PDF existentes usan mocks y no verifican el logo)
|
||||||
|
- [ ] **Validación visual del director sobre el PDF exportado antes del OK formal** — esta es la validación principal de la etapa
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Archivos a modificar
|
||||||
|
|
||||||
|
1. `src/lib/export/pdf-sections-shared.ts` — `PdfHeaderOptions` + `drawSharedHeader`
|
||||||
|
2. `src/lib/export/pdf-export.ts` — carga del logo + pasar `logoBase64` a `headerOpts`
|
||||||
|
3. `src/features/report/ComparisonTable.tsx` — `whitespace-nowrap` en `MetricCell`
|
||||||
|
|
||||||
|
No modificar ningún otro archivo. En particular, no tocar los tests de PDF existentes — el fallback de texto garantiza que sigan pasando sin cambios.
|
||||||
@@ -51,7 +51,7 @@ function MetricCell({
|
|||||||
style={{ width: `${max > 0 ? (value / max) * 100 : 0}%` }}
|
style={{ width: `${max > 0 ? (value / max) * 100 : 0}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-mono text-slate-700 text-xs w-14 text-right">
|
<span className="font-mono text-slate-700 text-xs w-14 text-right whitespace-nowrap">
|
||||||
{format(value)}
|
{format(value)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,9 +12,46 @@ import {
|
|||||||
} from './pdf-sections-roi'
|
} from './pdf-sections-roi'
|
||||||
import { drawSharedHeader, applyFootersToAllPages } from './pdf-sections-shared'
|
import { drawSharedHeader, applyFootersToAllPages } from './pdf-sections-shared'
|
||||||
import type { Process, Simulation, Resource, Activity } from '@/domain/types'
|
import type { Process, Simulation, Resource, Activity } from '@/domain/types'
|
||||||
|
import logoColorUrl from '../../../assets/MARCA FINAL 2023 AREA DE SEGURIDAD 1 COLOR.png?url'
|
||||||
|
|
||||||
export type ReportTab = 'actual' | 'automatizado' | 'roi'
|
export type ReportTab = 'actual' | 'automatizado' | 'roi'
|
||||||
|
|
||||||
|
interface LoadedLogo {
|
||||||
|
base64: string
|
||||||
|
pixelWidth: number
|
||||||
|
pixelHeight: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convierte el logo a base64 para incrustarlo en el PDF vía doc.addImage().
|
||||||
|
// Si el fetch falla (ej. entorno sin red), el caller usa .catch(() => undefined)
|
||||||
|
// y drawSharedHeader cae al fallback de texto "InQ ROI" — el export nunca se bloquea por esto.
|
||||||
|
//
|
||||||
|
// Redimensiona antes de convertir: el asset fuente es 724×474px, pero en el PDF se
|
||||||
|
// usa a ~15mm de ancho. jsPDF embebe PNG con canal alpha como bitmap RGBA sin
|
||||||
|
// recomprimir (no reutiliza la compresión del PNG original) — sin este resize, cada
|
||||||
|
// export engordaba ~1.3MB (de ~60KB a ~1.4MB) solo por el logo. 320px de ancho es
|
||||||
|
// suficiente para nitidez en impresión a ese tamaño físico.
|
||||||
|
//
|
||||||
|
// Retorna las dimensiones en píxeles del bitmap resultante junto al base64: drawSharedHeader
|
||||||
|
// las necesita para calcular el alto proporcional real (724×474px ≈ ratio 1.53:1, no 3.56:1
|
||||||
|
// como asumía el hardcode original de 32×9mm — eso aplastaba el logo horizontalmente).
|
||||||
|
async function loadImageBase64(url: string): Promise<LoadedLogo> {
|
||||||
|
const response = await fetch(url)
|
||||||
|
const blob = await response.blob()
|
||||||
|
const bitmap = await createImageBitmap(blob)
|
||||||
|
|
||||||
|
const targetW = 320
|
||||||
|
const targetH = Math.round(targetW * (bitmap.height / bitmap.width))
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = targetW
|
||||||
|
canvas.height = targetH
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) throw new Error('No se pudo obtener contexto 2D del canvas')
|
||||||
|
ctx.drawImage(bitmap, 0, 0, targetW, targetH)
|
||||||
|
|
||||||
|
return { base64: canvas.toDataURL('image/png'), pixelWidth: targetW, pixelHeight: targetH }
|
||||||
|
}
|
||||||
|
|
||||||
export interface PdfExportParams {
|
export interface PdfExportParams {
|
||||||
process: Process
|
process: Process
|
||||||
simulation: Simulation
|
simulation: Simulation
|
||||||
@@ -35,13 +72,15 @@ export async function exportToPdf(params: PdfExportParams): Promise<void> {
|
|||||||
|
|
||||||
const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' })
|
const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' })
|
||||||
|
|
||||||
|
const logo = await loadImageBase64(logoColorUrl).catch(() => undefined)
|
||||||
|
|
||||||
if (tab === 'roi') {
|
if (tab === 'roi') {
|
||||||
await exportRoiPdf(doc, autoTable, process, simulation, activities, currency)
|
await exportRoiPdf(doc, autoTable, process, simulation, activities, currency, logo)
|
||||||
} else {
|
} else {
|
||||||
const result = tab === 'automatizado'
|
const result = tab === 'automatizado'
|
||||||
? (simulation.resultAutomated ?? simulation.result)
|
? (simulation.resultAutomated ?? simulation.result)
|
||||||
: simulation.result
|
: simulation.result
|
||||||
await exportScenarioPdf(doc, autoTable, process, simulation, result, resources, heatmapImageData, currency, tab as 'actual' | 'automatizado', activities)
|
await exportScenarioPdf(doc, autoTable, process, simulation, result, resources, heatmapImageData, currency, tab as 'actual' | 'automatizado', activities, logo)
|
||||||
}
|
}
|
||||||
|
|
||||||
const suffix = tab === 'roi' ? '_roi' : tab === 'automatizado' ? '_automatizado' : '_actual'
|
const suffix = tab === 'roi' ? '_roi' : tab === 'automatizado' ? '_automatizado' : '_actual'
|
||||||
@@ -62,7 +101,8 @@ async function exportScenarioPdf(
|
|||||||
heatmapImageData: string | null,
|
heatmapImageData: string | null,
|
||||||
currency: string,
|
currency: string,
|
||||||
tab: 'actual' | 'automatizado',
|
tab: 'actual' | 'automatizado',
|
||||||
activities: Activity[] = []
|
activities: Activity[] = [],
|
||||||
|
logo?: LoadedLogo
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const docAny = doc as any
|
const docAny = doc as any
|
||||||
const scenarioLabel = tab === 'actual' ? 'Escenario actual' : 'Escenario automatizado'
|
const scenarioLabel = tab === 'actual' ? 'Escenario actual' : 'Escenario automatizado'
|
||||||
@@ -75,7 +115,14 @@ async function exportScenarioPdf(
|
|||||||
keywords: 'bpmn, costos, simulación',
|
keywords: 'bpmn, costos, simulación',
|
||||||
})
|
})
|
||||||
|
|
||||||
const headerOpts = { processName: process.name, clientName: process.clientName || undefined, simulatedAt: simulation.executedAt }
|
const headerOpts = {
|
||||||
|
processName: process.name,
|
||||||
|
clientName: process.clientName || undefined,
|
||||||
|
simulatedAt: simulation.executedAt,
|
||||||
|
logoBase64: logo?.base64,
|
||||||
|
logoPixelWidth: logo?.pixelWidth,
|
||||||
|
logoPixelHeight: logo?.pixelHeight,
|
||||||
|
}
|
||||||
|
|
||||||
// ── Página 1 (portrait): Header denso + metadata + KPI grid (Etapa 8) ────────
|
// ── Página 1 (portrait): Header denso + metadata + KPI grid (Etapa 8) ────────
|
||||||
drawScenarioPage1(docAny, tab, process, simulation.executedAt, result, currency)
|
drawScenarioPage1(docAny, tab, process, simulation.executedAt, result, currency)
|
||||||
@@ -164,7 +211,8 @@ async function exportRoiPdf(
|
|||||||
process: Process,
|
process: Process,
|
||||||
simulation: Simulation,
|
simulation: Simulation,
|
||||||
activities: Activity[],
|
activities: Activity[],
|
||||||
currency: string
|
currency: string,
|
||||||
|
logo?: LoadedLogo
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const result = simulation.result
|
const result = simulation.result
|
||||||
const resultAutomated = simulation.resultAutomated ?? result
|
const resultAutomated = simulation.resultAutomated ?? result
|
||||||
@@ -185,7 +233,14 @@ async function exportRoiPdf(
|
|||||||
keywords: 'bpmn, roi, automatización, payback',
|
keywords: 'bpmn, roi, automatización, payback',
|
||||||
})
|
})
|
||||||
|
|
||||||
const roiHeaderOpts = { processName: process.name, clientName: process.clientName || undefined, simulatedAt: simulation.executedAt }
|
const roiHeaderOpts = {
|
||||||
|
processName: process.name,
|
||||||
|
clientName: process.clientName || undefined,
|
||||||
|
simulatedAt: simulation.executedAt,
|
||||||
|
logoBase64: logo?.base64,
|
||||||
|
logoPixelWidth: logo?.pixelWidth,
|
||||||
|
logoPixelHeight: logo?.pixelHeight,
|
||||||
|
}
|
||||||
|
|
||||||
// ── Página 1: Rediseño completo InQ (Etapa 6) ───────────────────────────────
|
// ── Página 1: Rediseño completo InQ (Etapa 6) ───────────────────────────────
|
||||||
drawRoiPage1(doc, process, roi, simulation.executedAt, result.perActivity, resultAutomated.perActivity, currency)
|
drawRoiPage1(doc, process, roi, simulation.executedAt, result.perActivity, resultAutomated.perActivity, currency)
|
||||||
|
|||||||
@@ -64,11 +64,14 @@ export interface PdfHeaderOptions {
|
|||||||
sectionTitle?: string // subtítulo de sección, ej. "Análisis de retorno de inversión"
|
sectionTitle?: string // subtítulo de sección, ej. "Análisis de retorno de inversión"
|
||||||
clientName?: string
|
clientName?: string
|
||||||
simulatedAt: number // Unix timestamp ms
|
simulatedAt: number // Unix timestamp ms
|
||||||
|
logoBase64?: string // logo PNG de InQuality, opcional — fallback a texto "InQ ROI" si no se provee
|
||||||
|
logoPixelWidth?: number // dimensiones reales del bitmap — para calcular el alto proporcional sin distorsión
|
||||||
|
logoPixelHeight?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dibuja el header InQ-branded.
|
* Dibuja el header InQ-branded.
|
||||||
* - Logo "InQ ROI" naranja a la izquierda, fecha a la derecha
|
* - Logo InQuality (o texto "InQ ROI" naranja como fallback) a la izquierda, fecha a la derecha
|
||||||
* - Nombre del proceso + cliente
|
* - Nombre del proceso + cliente
|
||||||
* - Border-bottom naranja (más grueso en página 1)
|
* - Border-bottom naranja (más grueso en página 1)
|
||||||
* Retorna la coordenada y donde empieza el contenido.
|
* Retorna la coordenada y donde empieza el contenido.
|
||||||
@@ -77,20 +80,37 @@ export function drawSharedHeader(doc: DocLike, options: PdfHeaderOptions): numbe
|
|||||||
const pageW = doc.internal.pageSize.getWidth()
|
const pageW = doc.internal.pageSize.getWidth()
|
||||||
const contentRight = pageW - MARGIN
|
const contentRight = pageW - MARGIN
|
||||||
let y = MARGIN
|
let y = MARGIN
|
||||||
|
const hasLogo = Boolean(options.logoBase64)
|
||||||
|
|
||||||
// Logo "InQ ROI" — naranja bold
|
if (hasLogo) {
|
||||||
|
// Logo compacto arriba a la izquierda, alineado con el margen. Ancho 15mm con
|
||||||
|
// alto proporcional calculado desde las dimensiones reales del bitmap — el PNG
|
||||||
|
// fuente es 724×474px (ratio ~1.53:1), no 3.56:1 como asumía el hardcode anterior
|
||||||
|
// de 32×9mm (eso aplastaba el logo horizontalmente).
|
||||||
|
const LOGO_WIDTH_MM = 15
|
||||||
|
const logoHeightMM = options.logoPixelWidth && options.logoPixelHeight
|
||||||
|
? LOGO_WIDTH_MM * (options.logoPixelHeight / options.logoPixelWidth)
|
||||||
|
: 8
|
||||||
|
doc.addImage(options.logoBase64 as string, 'PNG', MARGIN, MARGIN - 1, LOGO_WIDTH_MM, logoHeightMM)
|
||||||
|
y = MARGIN + logoHeightMM + 2
|
||||||
|
} else {
|
||||||
|
// Fallback al texto — retrocompatibilidad y tests (logo no disponible, ej. fetch falló)
|
||||||
doc.setFont('helvetica', 'bold')
|
doc.setFont('helvetica', 'bold')
|
||||||
doc.setFontSize(13)
|
doc.setFontSize(13)
|
||||||
doc.setTextColor(...PDF_COLORS.inqOrange)
|
doc.setTextColor(...PDF_COLORS.inqOrange)
|
||||||
doc.text('InQ ROI', MARGIN, y)
|
doc.text('InQ ROI', MARGIN, y)
|
||||||
|
}
|
||||||
|
|
||||||
// Fecha — derecha, slate-400
|
// Fecha — derecha, slate-400. Sin logo comparte línea con el texto "InQ ROI" (layout
|
||||||
|
// original). Con logo, el logo ya desplazó el bloque hacia abajo — la fecha cae en
|
||||||
|
// la misma línea que el nombre del proceso, debajo del logo.
|
||||||
const { date } = formatSimulationTimestamp(options.simulatedAt)
|
const { date } = formatSimulationTimestamp(options.simulatedAt)
|
||||||
doc.setFont('helvetica', 'normal')
|
doc.setFont('helvetica', 'normal')
|
||||||
doc.setFontSize(7.5)
|
doc.setFontSize(7.5)
|
||||||
doc.setTextColor(...PDF_COLORS.textDisabled)
|
doc.setTextColor(...PDF_COLORS.textDisabled)
|
||||||
doc.text(date, contentRight, y, { align: 'right' } as Record<string, unknown>)
|
doc.text(date, contentRight, y, { align: 'right' } as Record<string, unknown>)
|
||||||
y += 7
|
|
||||||
|
if (!hasLogo) y += 7
|
||||||
|
|
||||||
// Nombre del proceso — bold slate-900
|
// Nombre del proceso — bold slate-900
|
||||||
doc.setFont('helvetica', 'bold')
|
doc.setFont('helvetica', 'bold')
|
||||||
@@ -98,12 +118,16 @@ export function drawSharedHeader(doc: DocLike, options: PdfHeaderOptions): numbe
|
|||||||
doc.setTextColor(...PDF_COLORS.textPrimary)
|
doc.setTextColor(...PDF_COLORS.textPrimary)
|
||||||
doc.text(options.processName, MARGIN, y)
|
doc.text(options.processName, MARGIN, y)
|
||||||
|
|
||||||
// Cliente — derecha, slate-500
|
// Cliente — derecha, slate-500. Sin logo comparte línea con el proceso (layout
|
||||||
|
// original). Con logo, esa línea ya tiene la fecha — el cliente baja una línea para
|
||||||
|
// no superponerse, y el border-bottom se ajusta para no pisarlo.
|
||||||
if (options.clientName) {
|
if (options.clientName) {
|
||||||
|
const clientY = hasLogo ? y + 5 : y
|
||||||
doc.setFont('helvetica', 'normal')
|
doc.setFont('helvetica', 'normal')
|
||||||
doc.setFontSize(7.5)
|
doc.setFontSize(7.5)
|
||||||
doc.setTextColor(...PDF_COLORS.textTertiary)
|
doc.setTextColor(...PDF_COLORS.textTertiary)
|
||||||
doc.text(options.clientName, contentRight, y, { align: 'right' } as Record<string, unknown>)
|
doc.text(options.clientName, contentRight, clientY, { align: 'right' } as Record<string, unknown>)
|
||||||
|
if (hasLogo) y = clientY
|
||||||
}
|
}
|
||||||
y += 5
|
y += 5
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user