"chore(claude-code): configurar permisos compartidos del proyecto
- settings.json: deny patterns críticos (rm -rf root, force push, etc.) - settings.local.json en gitignore — cada dev configura sus allows Refs: methodology/PATTERNS.md B.4 (reglas no negociables en repo)
This commit is contained in:
191
src/lib/export/pdf-sections-shared.ts
Normal file
191
src/lib/export/pdf-sections-shared.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Módulo compartido de header/footer PDF — Sprint 1.5 Etapa 4.
|
||||
*
|
||||
* Provee drawSharedHeader, drawSharedFooter, applyFootersToAllPages
|
||||
* y PDF_COLORS para uso en los 3 PDFs (Actual, Automatizado, ROI).
|
||||
*
|
||||
* Diseñado para ser compatible con páginas landscape (Etapa 5):
|
||||
* usa doc.internal.pageSize.getWidth/Height() en lugar de constantes A4.
|
||||
*/
|
||||
import { formatSimulationTimestamp } from '@/lib/format'
|
||||
|
||||
// ─── Tipo DocLike ─────────────────────────────────────────────────────────────
|
||||
// Superset del DocLike anterior: agrega pageSize para compatibilidad landscape.
|
||||
// pdf-sections.ts re-exporta este tipo para mantener compatibilidad de imports.
|
||||
|
||||
export type DocLike = {
|
||||
setFont: (name: string, style: string) => void
|
||||
setFontSize: (size: number) => void
|
||||
setTextColor: (...args: number[]) => void
|
||||
setFillColor: (...args: number[]) => void
|
||||
setDrawColor: (...args: number[]) => void
|
||||
setLineWidth: (w: number) => void
|
||||
text: (text: string | string[], x: number, y: number, opts?: Record<string, unknown>) => void
|
||||
rect: (x: number, y: number, w: number, h: number, style?: string) => void
|
||||
roundedRect: (x: number, y: number, w: number, h: number, rx: number, ry: number, style?: string) => void
|
||||
line: (x1: number, y1: number, x2: number, y2: number) => void
|
||||
addImage: (img: string, format: string, x: number, y: number, w: number, h: number) => void
|
||||
addPage: () => void
|
||||
setPage: (page: number) => void
|
||||
internal: {
|
||||
getNumberOfPages: () => number
|
||||
pageSize: {
|
||||
getWidth: () => number
|
||||
getHeight: () => number
|
||||
}
|
||||
}
|
||||
splitTextToSize: (text: string, maxWidth: number) => string[]
|
||||
}
|
||||
|
||||
// ─── Paleta PDF ───────────────────────────────────────────────────────────────
|
||||
|
||||
export const PDF_COLORS = {
|
||||
inqOrange: [245, 152, 69] as [number, number, number], // #F59845
|
||||
inqOrangeDark: [180, 83, 9] as [number, number, number], // #B45309
|
||||
inqMagenta: [237, 62, 143] as [number, number, number], // #ED3E8F
|
||||
textPrimary: [15, 23, 42] as [number, number, number], // #0F172A slate-900
|
||||
textSecondary: [71, 85, 105] as [number, number, number], // #475569 slate-600
|
||||
textTertiary: [100, 116, 139] as [number, number, number], // #64748B slate-500
|
||||
textDisabled: [148, 163, 184] as [number, number, number], // #94A3B8 slate-400
|
||||
borderLight: [226, 232, 240] as [number, number, number], // #E2E8F0 slate-200
|
||||
slate50: [248, 250, 252] as [number, number, number], // #F8FAFC
|
||||
successGreen: [22, 163, 74] as [number, number, number], // #16A34A
|
||||
dangerRed: [239, 68, 68] as [number, number, number], // #EF4444
|
||||
warningAmber: [245, 158, 11] as [number, number, number], // #F59E0B
|
||||
} as const
|
||||
|
||||
const MARGIN = 20 // mm
|
||||
|
||||
// ─── Header compartido ────────────────────────────────────────────────────────
|
||||
|
||||
export interface PdfHeaderOptions {
|
||||
pageNumber: number // 1-based — determina grosor del border (3px pág 1, 2px resto)
|
||||
processName: string
|
||||
sectionTitle?: string // subtítulo de sección, ej. "Análisis de retorno de inversión"
|
||||
clientName?: string
|
||||
simulatedAt: number // Unix timestamp ms
|
||||
}
|
||||
|
||||
/**
|
||||
* Dibuja el header InQ-branded.
|
||||
* - Logo "InQ ROI" naranja a la izquierda, fecha a la derecha
|
||||
* - Nombre del proceso + cliente
|
||||
* - Border-bottom naranja (más grueso en página 1)
|
||||
* Retorna la coordenada y donde empieza el contenido.
|
||||
*/
|
||||
export function drawSharedHeader(doc: DocLike, options: PdfHeaderOptions): number {
|
||||
const pageW = doc.internal.pageSize.getWidth()
|
||||
const contentRight = pageW - MARGIN
|
||||
let y = MARGIN
|
||||
|
||||
// Logo "InQ ROI" — naranja bold
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.setFontSize(13)
|
||||
doc.setTextColor(...PDF_COLORS.inqOrange)
|
||||
doc.text('InQ ROI', MARGIN, y)
|
||||
|
||||
// Fecha — derecha, slate-400
|
||||
const { date } = formatSimulationTimestamp(options.simulatedAt)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.setFontSize(7.5)
|
||||
doc.setTextColor(...PDF_COLORS.textDisabled)
|
||||
doc.text(date, contentRight, y, { align: 'right' } as Record<string, unknown>)
|
||||
y += 7
|
||||
|
||||
// Nombre del proceso — bold slate-900
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.setFontSize(11)
|
||||
doc.setTextColor(...PDF_COLORS.textPrimary)
|
||||
doc.text(options.processName, MARGIN, y)
|
||||
|
||||
// Cliente — derecha, slate-500
|
||||
if (options.clientName) {
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.setFontSize(7.5)
|
||||
doc.setTextColor(...PDF_COLORS.textTertiary)
|
||||
doc.text(options.clientName, contentRight, y, { align: 'right' } as Record<string, unknown>)
|
||||
}
|
||||
y += 5
|
||||
|
||||
// Subtítulo de sección (opcional)
|
||||
if (options.sectionTitle) {
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.setFontSize(8.5)
|
||||
doc.setTextColor(...PDF_COLORS.textSecondary)
|
||||
doc.text(options.sectionTitle, MARGIN, y)
|
||||
y += 4.5
|
||||
}
|
||||
|
||||
// Border-bottom naranja InQ: 0.8mm (≈3pt) en página 1, 0.5mm (≈2pt) en el resto
|
||||
const borderWidth = options.pageNumber === 1 ? 0.8 : 0.5
|
||||
doc.setDrawColor(...PDF_COLORS.inqOrange)
|
||||
doc.setLineWidth(borderWidth)
|
||||
doc.line(MARGIN, y, contentRight, y)
|
||||
y += 5
|
||||
|
||||
// Reset lineWidth para no afectar el contenido siguiente
|
||||
doc.setLineWidth(0.2)
|
||||
|
||||
return y
|
||||
}
|
||||
|
||||
// ─── Footer compartido ────────────────────────────────────────────────────────
|
||||
|
||||
export interface PdfFooterOptions {
|
||||
clientName?: string
|
||||
simulatedAt: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Dibuja el footer en la posición inferior fija de la página actual.
|
||||
* - Border-top slate-200
|
||||
* - Izquierda: "InQ ROI · Powered by InQuality · [cliente] · [fecha]"
|
||||
* - Derecha: "Página X de Y"
|
||||
* No incluye versión del producto.
|
||||
*/
|
||||
export function drawSharedFooter(
|
||||
doc: DocLike,
|
||||
options: PdfFooterOptions & { pageNumber: number; totalPages: number }
|
||||
): void {
|
||||
const pageW = doc.internal.pageSize.getWidth()
|
||||
const pageH = doc.internal.pageSize.getHeight()
|
||||
const contentRight = pageW - MARGIN
|
||||
const footerLineY = pageH - 14
|
||||
const footerTextY = pageH - 9
|
||||
|
||||
// Border-top
|
||||
doc.setDrawColor(...PDF_COLORS.borderLight)
|
||||
doc.setLineWidth(0.2)
|
||||
doc.line(MARGIN, footerLineY, contentRight, footerLineY)
|
||||
|
||||
// Texto izquierda
|
||||
const { date } = formatSimulationTimestamp(options.simulatedAt)
|
||||
const parts: string[] = ['InQ ROI', 'Powered by InQuality']
|
||||
if (options.clientName) parts.push(options.clientName)
|
||||
parts.push(date)
|
||||
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.setFontSize(7)
|
||||
doc.setTextColor(...PDF_COLORS.textDisabled)
|
||||
doc.text(parts.join(' · '), MARGIN, footerTextY)
|
||||
|
||||
// Paginación derecha
|
||||
doc.text(
|
||||
`Página ${options.pageNumber} de ${options.totalPages}`,
|
||||
contentRight,
|
||||
footerTextY,
|
||||
{ align: 'right' } as Record<string, unknown>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica footer a TODAS las páginas del documento.
|
||||
* Llamar después de agregar todo el contenido.
|
||||
*/
|
||||
export function applyFootersToAllPages(doc: DocLike, options: PdfFooterOptions): void {
|
||||
const total = doc.internal.getNumberOfPages()
|
||||
for (let i = 1; i <= total; i++) {
|
||||
doc.setPage(i)
|
||||
drawSharedFooter(doc, { ...options, pageNumber: i, totalPages: total })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user