/** * Renderizado del formulario EVC-IH01 en PDF * Replica el formato exacto del formulario físico de catación de café */ import type { jsPDF } from 'jspdf' import type { Muestra, Intensidades } from '~/types/catacion' import { calcularSumatoriaAfectiva, calcularSCAA } from '~/types/catacion' import { PDF_CONFIG, calcularYInicio, PARAMETROS_INTENSIDAD, CATEGORIAS_JERARQUICAS, SENSACIONES_PDF, GUSTOS_PDF, DEFECTOS_PDF, } from './pdfLayout' import { dibujarCheckbox, dibujarCheckboxConLabel, dibujarCasillasTazas, dibujarRectangulo, dibujarLineaHorizontal, dibujarLineaVertical, truncarTexto, } from './pdfHelpers' /** * Renderiza un formulario completo de catación */ export function renderizarFormulario( doc: jsPDF, muestra: Muestra, posicion: 0 | 1 | 2 ): void { const yBase = calcularYInicio(posicion) const xBase = PDF_CONFIG.marginLeft // Borde exterior del formulario dibujarRectangulo( doc, xBase, yBase, PDF_CONFIG.formWidth, PDF_CONFIG.formHeight, PDF_CONFIG.lineWidth.thick ) // Renderizar cada sección con posiciones ajustadas para layout compacto // Fragancia-Aroma y Sabores: 28mm cada una (layout horizontal) // SubTotal debe terminar exactamente en formHeight (130mm) renderizarEncabezado(doc, xBase, yBase, muestra) renderizarTablaIntensidades(doc, xBase, yBase + 14, muestra) renderizarSeccionFraganciaAroma(doc, xBase + 55, yBase + 14, muestra) // 28mm renderizarSeccionSabores(doc, xBase + 55, yBase + 42, muestra) // 28mm (14+28=42) renderizarSeccionSensacionGustos(doc, xBase, yBase + 70, muestra) // Después de Sabores (42+28=70) renderizarSeccionTazasDefectos(doc, xBase, yBase + 86, muestra) // 70+16=86 renderizarSeccionOtrasNotas(doc, xBase, yBase + 102, muestra) // 86+16=102, altura 20mm renderizarSeccionSubTotal(doc, xBase, yBase + 122, muestra) // 122+8=130 (exacto al borde) } /** * Renderiza el encabezado del formulario */ function renderizarEncabezado( doc: jsPDF, x: number, y: number, muestra: Muestra ): void { const width = PDF_CONFIG.formWidth // Línea separadora del encabezado dibujarLineaHorizontal(doc, x, y + 14, x + width, PDF_CONFIG.lineWidth.normal) // Título principal doc.setFontSize(PDF_CONFIG.fontSize.title) doc.setFont('helvetica', 'bold') doc.text('EVALUACION DEL VALOR DEL CAFE / DESCRIPTIVA-AFECTIVA', x + 3, y + 5) // Código EVC-IH01 doc.setFontSize(PDF_CONFIG.fontSize.header) doc.text('EVC-IH01', x + width - 22, y + 5) // Línea separadora después del título dibujarLineaHorizontal(doc, x, y + 7, x + width, PDF_CONFIG.lineWidth.thin) // MUESTRA doc.setFontSize(PDF_CONFIG.fontSize.body) doc.setFont('helvetica', 'bold') doc.text('MUESTRA:', x + 3, y + 11) doc.setFont('helvetica', 'normal') const nombreTruncado = truncarTexto(doc, muestra.nombre, 60, PDF_CONFIG.fontSize.body) doc.text(nombreTruncado, x + 25, y + 11) } /** * Renderiza la tabla de intensidades (lado izquierdo) */ function renderizarTablaIntensidades( doc: jsPDF, x: number, y: number, muestra: Muestra ): void { const colWidths = { parametro: 28, descriptiva: 13, afectiva: 13, } const rowHeight = 5 const tableWidth = colWidths.parametro + colWidths.descriptiva + colWidths.afectiva const tableHeight = rowHeight * 10 // Borde de la tabla dibujarRectangulo(doc, x, y, tableWidth, tableHeight, PDF_CONFIG.lineWidth.normal) // Headers doc.setFontSize(PDF_CONFIG.fontSize.small) doc.setFont('helvetica', 'bold') // Header: Intensidad/Parámetro doc.text('Intensidad', x + 2, y + 4) doc.setFontSize(PDF_CONFIG.fontSize.tiny) doc.text('Parámetro', x + 2, y + rowHeight + 4) // Header: Descriptiva dibujarLineaVertical(doc, x + colWidths.parametro, y, y + tableHeight) doc.setFontSize(PDF_CONFIG.fontSize.tiny) doc.text('Descriptiva', x + colWidths.parametro + 1, y + 4) doc.text('(1 al 15)', x + colWidths.parametro + 2, y + 8) // Header: Afectiva dibujarLineaVertical(doc, x + colWidths.parametro + colWidths.descriptiva, y, y + tableHeight) doc.text('Afectiva', x + colWidths.parametro + colWidths.descriptiva + 2, y + 4) doc.text('(1 al 9)', x + colWidths.parametro + colWidths.descriptiva + 3, y + 8) // Línea horizontal después de headers dibujarLineaHorizontal(doc, x, y + rowHeight * 2, x + tableWidth) // Filas de datos const intensidades = muestra.intensidades let currentY = y + rowHeight * 2 PARAMETROS_INTENSIDAD.forEach((param) => { const intensidad = intensidades[param.key as keyof Intensidades] // Parámetro doc.setFontSize(PDF_CONFIG.fontSize.tiny) doc.setFont('helvetica', 'normal') doc.text(param.label, x + 2, currentY + 3.5) // Valor descriptiva if (intensidad.descriptiva !== null) { doc.text( String(intensidad.descriptiva), x + colWidths.parametro + colWidths.descriptiva / 2 - 1, currentY + 3.5 ) } // Valor afectiva if (intensidad.afectiva !== null) { doc.text( String(intensidad.afectiva), x + colWidths.parametro + colWidths.descriptiva + colWidths.afectiva / 2 - 1, currentY + 3.5 ) } // Línea separadora dibujarLineaHorizontal(doc, x, currentY + rowHeight, x + tableWidth, PDF_CONFIG.lineWidth.thin) currentY += rowHeight }) } /** * Renderiza la sección de Fragancia-Aroma con layout horizontal para hijos */ function renderizarSeccionFraganciaAroma( doc: jsPDF, x: number, y: number, muestra: Muestra ): void { const width = 144.9 // Ancho total de la sección derecha const sectionHeight = 28 // Más compacto con layout horizontal // Borde de la sección dibujarRectangulo(doc, x, y, width, sectionHeight, PDF_CONFIG.lineWidth.normal) // Header doc.setFontSize(PDF_CONFIG.fontSize.small) doc.setFont('helvetica', 'bold') doc.text('Fragancia - Aroma', x + 3, y + 4) // Línea después del header dibujarLineaHorizontal(doc, x, y + 5.5, x + width, PDF_CONFIG.lineWidth.thin) // Determinar qué está seleccionado const categoriasSeleccionadas = muestra.fraganciaAromaNotas.categorias const subcategoriasSeleccionadas = muestra.fraganciaAromaNotas.subcategorias // Renderizar categorías con layout horizontal renderizarCategoriasHorizontal( doc, x + 2, y + 7.5, width - 4, categoriasSeleccionadas, subcategoriasSeleccionadas ) // Notas específica (sin recuadro, solo texto) doc.setFontSize(PDF_CONFIG.fontSize.tiny) doc.setFont('helvetica', 'bold') doc.text('Notas:', x + 3, y + sectionHeight - 2) if (muestra.fraganciaAromaNotas.notaEspecifica) { doc.setFont('helvetica', 'normal') const notaTruncada = truncarTexto( doc, muestra.fraganciaAromaNotas.notaEspecifica, width - 22, PDF_CONFIG.fontSize.tiny ) doc.text(notaTruncada, x + 15, y + sectionHeight - 2) } } /** * Renderiza categorías con hijos en layout horizontal * Las familias completas nunca se truncan entre líneas */ function renderizarCategoriasHorizontal( doc: jsPDF, x: number, y: number, maxWidth: number, categoriasSeleccionadas: string[], subcategoriasSeleccionadas: string[] ): void { const checkboxSize = 2.5 // Checkbox más pequeño const rowHeight = 3.8 // Altura de fila const familySpacing = 8 // Espacio entre familias const itemSpacing = 1 // Espacio entre items dentro de familia let currentX = x let currentY = y doc.setFontSize(PDF_CONFIG.fontSize.tiny) CATEGORIAS_JERARQUICAS.forEach((cat) => { // Calcular ancho total de la familia (padre + todos los hijos) const parentWidth = checkboxSize + 1 + doc.getTextWidth(cat.label) let familyWidth = parentWidth if (cat.hijos && cat.hijos.length > 0) { cat.hijos.forEach((hijo) => { familyWidth += itemSpacing + checkboxSize + 1 + doc.getTextWidth(hijo.label) }) } // Si la familia completa no cabe, ir a la siguiente línea if (currentX + familyWidth > x + maxWidth && currentX > x) { currentX = x currentY += rowHeight } // Renderizar el padre const isParentChecked = categoriasSeleccionadas.includes(cat.key as any) dibujarCheckbox(doc, currentX, currentY, isParentChecked, checkboxSize) doc.setFont('helvetica', 'bold') doc.text(cat.label, currentX + checkboxSize + 1, currentY + 2) currentX += parentWidth + itemSpacing // Renderizar hijos (si los tiene) if (cat.hijos && cat.hijos.length > 0) { doc.setFont('helvetica', 'normal') cat.hijos.forEach((hijo) => { const isChildChecked = subcategoriasSeleccionadas.includes(hijo.key) const childWidth = checkboxSize + 1 + doc.getTextWidth(hijo.label) dibujarCheckbox(doc, currentX, currentY, isChildChecked, checkboxSize) doc.text(hijo.label, currentX + checkboxSize + 1, currentY + 2) currentX += childWidth + itemSpacing }) } // Añadir espacio entre familias currentX += familySpacing }) } /** * Renderiza la sección de Sabores (mismo layout horizontal que Fragancia-Aroma) */ function renderizarSeccionSabores( doc: jsPDF, x: number, y: number, muestra: Muestra ): void { const width = 144.9 const sectionHeight = 28 // Mismo tamaño compacto que Fragancia-Aroma // Borde de la sección dibujarRectangulo(doc, x, y, width, sectionHeight, PDF_CONFIG.lineWidth.normal) // Header doc.setFontSize(PDF_CONFIG.fontSize.small) doc.setFont('helvetica', 'bold') doc.text('Sabores', x + 3, y + 4) // Línea después del header dibujarLineaHorizontal(doc, x, y + 5.5, x + width, PDF_CONFIG.lineWidth.thin) // Determinar qué está seleccionado const categoriasSeleccionadas = muestra.saborNotas.categorias const subcategoriasSeleccionadas = muestra.saborNotas.subcategorias // Renderizar categorías con layout horizontal (igual que Fragancia-Aroma) renderizarCategoriasHorizontal( doc, x + 2, y + 7.5, width - 4, categoriasSeleccionadas, subcategoriasSeleccionadas ) // Notas específica (sin recuadro, solo texto) doc.setFontSize(PDF_CONFIG.fontSize.tiny) doc.setFont('helvetica', 'bold') doc.text('Notas:', x + 3, y + sectionHeight - 2) if (muestra.saborNotas.notaEspecifica) { doc.setFont('helvetica', 'normal') const notaTruncada = truncarTexto( doc, muestra.saborNotas.notaEspecifica, width - 22, PDF_CONFIG.fontSize.tiny ) doc.text(notaTruncada, x + 15, y + sectionHeight - 2) } } /** * Renderiza la sección de Sensación en boca y Gustos predominantes * Layout horizontal en 2 filas para cada sección */ function renderizarSeccionSensacionGustos( doc: jsPDF, x: number, y: number, muestra: Muestra ): void { const width = PDF_CONFIG.formWidth const sectionHeight = 16 // Más compacto con layout horizontal // Borde de la sección dibujarRectangulo(doc, x, y, width, sectionHeight, PDF_CONFIG.lineWidth.normal) // Sección Sensación en boca const sensacionWidth = width * 0.6 doc.setFontSize(PDF_CONFIG.fontSize.small) doc.setFont('helvetica', 'bold') doc.text('Sensación en boca', x + 3, y + 4) // Línea divisoria vertical dibujarLineaVertical(doc, x + sensacionWidth, y, y + sectionHeight) // Sensaciones - Layout horizontal en 2 filas // Fila 1: Áspero, Aceitoso, Metálico // Fila 2: Suave, Deja seca la boca const sensacionesLabels = [ { key: 'Áspero', label: 'Áspero' }, { key: 'Aceitoso', label: 'Aceitoso' }, { key: 'Metálico', label: 'Metálico' }, { key: 'Suave', label: 'Suave' }, { key: 'Deja seca la boca', label: 'Astringente' }, ] const checkSpacingX = 40 // Espacio horizontal entre checkboxes const row1Y = y + 7 const row2Y = y + 12 // Fila 1: primeros 3 sensacionesLabels.slice(0, 3).forEach((sens, idx) => { const isChecked = muestra.sensacionEnBoca === sens.key dibujarCheckboxConLabel(doc, x + 3 + idx * checkSpacingX, row1Y, sens.label, isChecked) }) // Fila 2: últimos 2 sensacionesLabels.slice(3).forEach((sens, idx) => { const isChecked = muestra.sensacionEnBoca === sens.key dibujarCheckboxConLabel(doc, x + 3 + idx * checkSpacingX, row2Y, sens.label, isChecked) }) // Sección Gustos predominantes doc.setFont('helvetica', 'bold') doc.text('Gustos predominantes (2)', x + sensacionWidth + 3, y + 4) // Gustos - Layout horizontal en 2 filas // Fila 1: Salado, Amargo, Ácido // Fila 2: Dulce, Umami const gustosSpacingX = 26 // Fila 1: primeros 3 GUSTOS_PDF.slice(0, 3).forEach((gusto, idx) => { const isChecked = muestra.gustosPredominantes.includes(gusto.key as any) dibujarCheckboxConLabel(doc, x + sensacionWidth + 3 + idx * gustosSpacingX, row1Y, gusto.label, isChecked) }) // Fila 2: últimos 2 GUSTOS_PDF.slice(3).forEach((gusto, idx) => { const isChecked = muestra.gustosPredominantes.includes(gusto.key as any) dibujarCheckboxConLabel(doc, x + sensacionWidth + 3 + idx * gustosSpacingX, row2Y, gusto.label, isChecked) }) } /** * Renderiza la sección de Tazas y Defectos */ function renderizarSeccionTazasDefectos( doc: jsPDF, x: number, y: number, muestra: Muestra ): void { const width = PDF_CONFIG.formWidth const sectionHeight = 16 // Borde de la sección dibujarRectangulo(doc, x, y, width, sectionHeight, PDF_CONFIG.lineWidth.normal) // Tazas NO Uniformes dibujarCasillasTazas(doc, x + 3, y + 3, muestra.tazasNoUniformes, 'Tazas NO Uniformes:') // Tazas Defectuosas dibujarCasillasTazas(doc, x + 70, y + 3, muestra.tazasDefectuosas, 'Tazas defectuosas:') // Defectos - Header alineado con checkboxes doc.setFontSize(PDF_CONFIG.fontSize.small) doc.setFont('helvetica', 'bold') doc.text('Defecto (de haberse):', x + 140, y + 5) // Checkboxes alineados bajo el header let checkX = x + 140 const checkY = y + 10 DEFECTOS_PDF.forEach((defecto) => { const isChecked = muestra.defecto === defecto.key dibujarCheckboxConLabel(doc, checkX, checkY, defecto.label, isChecked) checkX += 20 }) } /** * Renderiza la sección de Otras Notas */ function renderizarSeccionOtrasNotas( doc: jsPDF, x: number, y: number, muestra: Muestra ): void { const width = PDF_CONFIG.formWidth const sectionHeight = 20 // Expandida para llenar espacio hasta SubTotal // Borde de la sección dibujarRectangulo(doc, x, y, width, sectionHeight, PDF_CONFIG.lineWidth.normal) // Label doc.setFontSize(PDF_CONFIG.fontSize.body) doc.setFont('helvetica', 'bold') doc.text('OTRAS NOTAS:', x + 3, y + 6) // Valor if (muestra.otrasNotas) { doc.setFont('helvetica', 'normal') const notaTruncada = truncarTexto( doc, muestra.otrasNotas, width - 45, PDF_CONFIG.fontSize.body ) doc.text(notaTruncada, x + 38, y + 6) } // Líneas para escribir (múltiples líneas para el espacio expandido) dibujarLineaHorizontal(doc, x + 38, y + 7, x + width - 3, PDF_CONFIG.lineWidth.thin) dibujarLineaHorizontal(doc, x + 3, y + 12, x + width - 3, PDF_CONFIG.lineWidth.thin) dibujarLineaHorizontal(doc, x + 3, y + 17, x + width - 3, PDF_CONFIG.lineWidth.thin) } /** * Renderiza la sección de Sub Total */ function renderizarSeccionSubTotal( doc: jsPDF, x: number, y: number, muestra: Muestra ): void { const width = PDF_CONFIG.formWidth const sectionHeight = 8 // Borde de la sección dibujarRectangulo(doc, x, y, width, sectionHeight, PDF_CONFIG.lineWidth.normal) // Cálculos const sumatoriaAfectiva = calcularSumatoriaAfectiva(muestra) const scaa = calcularSCAA(muestra) // Daño: 6pts por cada taza defectuosa (por definición también es no uniforme) const dano = muestra.tazasDefectuosas.length * 6 doc.setFontSize(PDF_CONFIG.fontSize.small) doc.setFont('helvetica', 'bold') doc.text('SUB TOTAL:', x + 3, y + 5) // Valores const valores = [ { label: 'Sumatoria Afectiva:', valor: sumatoriaAfectiva.toString() }, { label: 'Total (100%):', valor: scaa.toFixed(2) }, { label: 'Daño (6pto Taza):', valor: dano.toString() }, { label: 'Total (100%) - Daño:', valor: (scaa - dano).toFixed(2) }, ] let currentX = x + 32 valores.forEach((item) => { doc.setFont('helvetica', 'normal') doc.text(item.label, currentX, y + 5) currentX += doc.getTextWidth(item.label) + 1 doc.setFont('helvetica', 'bold') doc.text(item.valor, currentX, y + 5) currentX += doc.getTextWidth(item.valor) + 6 }) }