/** * 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_PDF, SENSACIONES_PDF, GUSTOS_PDF, DEFECTOS_PDF, } from './pdfLayout' import { dibujarCheckbox, dibujarCheckboxConLabel, dibujarCasillasTazas, dibujarCeldaTexto, 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 renderizarEncabezado(doc, xBase, yBase, muestra) renderizarTablaIntensidades(doc, xBase, yBase + 10, muestra) renderizarSeccionFraganciaAroma(doc, xBase + 48, yBase + 10, muestra) renderizarSeccionSabores(doc, xBase + 48, yBase + 32, muestra) renderizarSeccionSensacionGustos(doc, xBase, yBase + 46, muestra) renderizarSeccionTazasDefectos(doc, xBase, yBase + 58, muestra) renderizarSeccionOtrasNotas(doc, xBase, yBase + 70, muestra) renderizarSeccionSubTotal(doc, xBase, yBase + 78, muestra) } /** * 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 + 10, 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 + 2, y + 4) // Código EVC-IH01 doc.setFontSize(PDF_CONFIG.fontSize.header) doc.text('EVC-IH01', x + width - 20, y + 4) // Línea separadora después del título dibujarLineaHorizontal(doc, x, y + 5.5, x + width, PDF_CONFIG.lineWidth.thin) // MUESTRA doc.setFontSize(PDF_CONFIG.fontSize.body) doc.setFont('helvetica', 'bold') doc.text('MUESTRA:', x + 2, y + 8.5) doc.setFont('helvetica', 'normal') const nombreTruncado = truncarTexto(doc, muestra.nombre, 40, PDF_CONFIG.fontSize.body) doc.text(nombreTruncado, x + 20, y + 8.5) } /** * Renderiza la tabla de intensidades (lado izquierdo) */ function renderizarTablaIntensidades( doc: jsPDF, x: number, y: number, muestra: Muestra ): void { const colWidths = { parametro: 22, descriptiva: 12, afectiva: 12, } const rowHeight = 3.5 const tableWidth = colWidths.parametro + colWidths.descriptiva + colWidths.afectiva // Borde de la tabla dibujarRectangulo(doc, x, y, tableWidth, rowHeight * 10, PDF_CONFIG.lineWidth.normal) // Headers doc.setFontSize(PDF_CONFIG.fontSize.tiny) doc.setFont('helvetica', 'bold') // Header: Intensidad dibujarCeldaTexto(doc, x, y, colWidths.parametro, rowHeight, 'Intensidad', { fontSize: PDF_CONFIG.fontSize.tiny, fontStyle: 'bold', }) // Subheaders de intensidad doc.setFontSize(PDF_CONFIG.fontSize.tiny - 0.5) doc.text('Parámetro', x + 1, y + rowHeight + 2.5) // Header: Descriptiva dibujarCeldaTexto( doc, x + colWidths.parametro, y, colWidths.descriptiva, rowHeight * 2, '', { border: true } ) doc.setFontSize(PDF_CONFIG.fontSize.tiny) doc.text('Descriptiva', x + colWidths.parametro + 1, y + 3) doc.text('(1 al 15)', x + colWidths.parametro + 2, y + 6) // Header: Afectiva dibujarCeldaTexto( doc, x + colWidths.parametro + colWidths.descriptiva, y, colWidths.afectiva, rowHeight * 2, '', { border: true } ) doc.text('Afectiva', x + colWidths.parametro + colWidths.descriptiva + 1.5, y + 3) doc.text('(1 al 9)', x + colWidths.parametro + colWidths.descriptiva + 2.5, y + 6) // 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 + 1, currentY + 2.5) // Valor descriptiva if (intensidad.descriptiva !== null) { doc.text( String(intensidad.descriptiva), x + colWidths.parametro + colWidths.descriptiva / 2 - 1, currentY + 2.5 ) } // Valor afectiva if (intensidad.afectiva !== null) { doc.text( String(intensidad.afectiva), x + colWidths.parametro + colWidths.descriptiva + colWidths.afectiva / 2 - 1, currentY + 2.5 ) } // Línea separadora dibujarLineaHorizontal(doc, x, currentY + rowHeight, x + tableWidth, PDF_CONFIG.lineWidth.thin) currentY += rowHeight }) // Líneas verticales de la tabla dibujarLineaVertical(doc, x + colWidths.parametro, y, y + rowHeight * 10) dibujarLineaVertical( doc, x + colWidths.parametro + colWidths.descriptiva, y, y + rowHeight * 10 ) } /** * Renderiza la sección de Fragancia-Aroma */ function renderizarSeccionFraganciaAroma( doc: jsPDF, x: number, y: number, muestra: Muestra ): void { const width = 151.9 // Ancho total de la sección derecha const colWidth = width / 2 // Borde de la sección dibujarRectangulo(doc, x, y, width, 22, PDF_CONFIG.lineWidth.normal) // Header doc.setFontSize(PDF_CONFIG.fontSize.small) doc.setFont('helvetica', 'bold') doc.text('Fragancia - Aroma', x + 2, y + 3) // Línea después del header dibujarLineaHorizontal(doc, x, y + 4.5, x + width, PDF_CONFIG.lineWidth.thin) // Header columna Notas doc.text('Notas', x + colWidth + 2, y + 3) // Línea vertical divisoria dibujarLineaVertical(doc, x + colWidth, y, y + 22) // Checkboxes columna izquierda let checkY = y + 6 const checkSpacing = 1.8 // Determinar qué está seleccionado const categoriasSeleccionadas = muestra.fraganciaAromaNotas.categorias const subcategoriasSeleccionadas = muestra.fraganciaAromaNotas.subcategorias // Columna izquierda de checkboxes (primera mitad de categorías) const categoriasIzq = CATEGORIAS_PDF.columnaIzquierda.slice(0, 6) categoriasIzq.forEach((cat) => { const isChecked = categoriasSeleccionadas.includes(cat.key as any) || subcategoriasSeleccionadas.includes(cat.key) dibujarCheckboxConLabel(doc, x + 2, checkY, cat.label, isChecked, cat.indent) checkY += checkSpacing }) // Continuar en columna media checkY = y + 6 const categoriasMed = CATEGORIAS_PDF.columnaIzquierda.slice(6) categoriasMed.forEach((cat) => { const isChecked = categoriasSeleccionadas.includes(cat.key as any) || subcategoriasSeleccionadas.includes(cat.key) dibujarCheckboxConLabel(doc, x + 28, checkY, cat.label, isChecked, cat.indent) checkY += checkSpacing }) // Columna derecha de checkboxes (Notas) checkY = y + 6 const categoriasDer = CATEGORIAS_PDF.columnaDerecha.slice(0, 6) categoriasDer.forEach((cat) => { const isChecked = categoriasSeleccionadas.includes(cat.key as any) || subcategoriasSeleccionadas.includes(cat.key) dibujarCheckboxConLabel(doc, x + colWidth + 2, checkY, cat.label, isChecked, cat.indent) checkY += checkSpacing }) // Segunda columna de Notas checkY = y + 6 const categoriasDer2 = CATEGORIAS_PDF.columnaDerecha.slice(6) categoriasDer2.forEach((cat) => { const isChecked = categoriasSeleccionadas.includes(cat.key as any) || subcategoriasSeleccionadas.includes(cat.key) dibujarCheckboxConLabel(doc, x + colWidth + 35, checkY, cat.label, isChecked, cat.indent) checkY += checkSpacing }) // Nota específica if (muestra.fraganciaAromaNotas.notaEspecifica) { doc.setFontSize(PDF_CONFIG.fontSize.tiny) doc.setFont('helvetica', 'normal') const notaTruncada = truncarTexto( doc, muestra.fraganciaAromaNotas.notaEspecifica, colWidth - 10, PDF_CONFIG.fontSize.tiny ) doc.text(notaTruncada, x + 2, y + 20) } } /** * Renderiza la sección de Sabores */ function renderizarSeccionSabores( doc: jsPDF, x: number, y: number, muestra: Muestra ): void { const width = 151.9 const colWidth = width / 2 // Borde de la sección dibujarRectangulo(doc, x, y, width, 14, PDF_CONFIG.lineWidth.normal) // Header doc.setFontSize(PDF_CONFIG.fontSize.small) doc.setFont('helvetica', 'bold') doc.text('Sabores', x + 2, y + 3) // Header Notas doc.text('Notas', x + colWidth + 2, y + 3) // Línea después del header dibujarLineaHorizontal(doc, x, y + 4.5, x + width, PDF_CONFIG.lineWidth.thin) // Línea vertical divisoria dibujarLineaVertical(doc, x + colWidth, y, y + 14) // Checkboxes (mismas categorías que fragancia) let checkY = y + 6 const checkSpacing = 1.6 const categoriasSeleccionadas = muestra.saborNotas.categorias const subcategoriasSeleccionadas = muestra.saborNotas.subcategorias // Columna izquierda (primeras 4) const categoriasIzq = CATEGORIAS_PDF.columnaIzquierda.slice(0, 4) categoriasIzq.forEach((cat) => { const isChecked = categoriasSeleccionadas.includes(cat.key as any) || subcategoriasSeleccionadas.includes(cat.key) dibujarCheckboxConLabel(doc, x + 2, checkY, cat.label, isChecked, cat.indent) checkY += checkSpacing }) // Columna media checkY = y + 6 const categoriasMed = CATEGORIAS_PDF.columnaIzquierda.slice(4, 8) categoriasMed.forEach((cat) => { const isChecked = categoriasSeleccionadas.includes(cat.key as any) || subcategoriasSeleccionadas.includes(cat.key) dibujarCheckboxConLabel(doc, x + 28, checkY, cat.label, isChecked, cat.indent) checkY += checkSpacing }) // Columna derecha checkY = y + 6 const categoriasDer = CATEGORIAS_PDF.columnaDerecha.slice(0, 4) categoriasDer.forEach((cat) => { const isChecked = categoriasSeleccionadas.includes(cat.key as any) || subcategoriasSeleccionadas.includes(cat.key) dibujarCheckboxConLabel(doc, x + colWidth + 2, checkY, cat.label, isChecked, cat.indent) checkY += checkSpacing }) // Segunda columna derecha checkY = y + 6 const categoriasDer2 = CATEGORIAS_PDF.columnaDerecha.slice(4, 8) categoriasDer2.forEach((cat) => { const isChecked = categoriasSeleccionadas.includes(cat.key as any) || subcategoriasSeleccionadas.includes(cat.key) dibujarCheckboxConLabel(doc, x + colWidth + 35, checkY, cat.label, isChecked, cat.indent) checkY += checkSpacing }) // Nota específica de sabor if (muestra.saborNotas.notaEspecifica) { doc.setFontSize(PDF_CONFIG.fontSize.tiny) doc.setFont('helvetica', 'normal') const notaTruncada = truncarTexto( doc, muestra.saborNotas.notaEspecifica, colWidth - 10, PDF_CONFIG.fontSize.tiny ) doc.text(notaTruncada, x + 2, y + 12.5) } } /** * Renderiza la sección de Sensación en boca y Gustos predominantes */ function renderizarSeccionSensacionGustos( doc: jsPDF, x: number, y: number, muestra: Muestra ): void { const width = PDF_CONFIG.formWidth // Borde de la sección dibujarRectangulo(doc, x, y, width, 12, 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 + 2, y + 3) // Línea divisoria vertical dibujarLineaVertical(doc, x + sensacionWidth, y, y + 12) // Checkboxes de sensación let checkY = y + 5 const checkSpacing = 2.2 let checkX = x + 2 SENSACIONES_PDF.forEach((sens, index) => { const isChecked = muestra.sensacionEnBoca === sens.key // Ajustar posición para layout de 2 filas if (index === 3) { checkY = y + 5 checkX = x + 60 } else if (index > 0 && index < 3) { checkY += checkSpacing } else if (index > 3) { checkY += checkSpacing } // Usar label corto para ahorrar espacio const labelCorto = sens.key === 'Deja seca la boca' ? 'Deja seca boca' : sens.key === 'Áspero' ? 'Áspero' : sens.key dibujarCheckboxConLabel(doc, checkX, checkY, labelCorto, isChecked) }) // Sección Gustos predominantes doc.setFont('helvetica', 'bold') doc.text('Gustos predominantes (2)', x + sensacionWidth + 2, y + 3) // Checkboxes de gustos checkY = y + 5 checkX = x + sensacionWidth + 2 GUSTOS_PDF.forEach((gusto, index) => { const isChecked = muestra.gustosPredominantes.includes(gusto.key as any) if (index === 3) { checkY = y + 5 checkX = x + sensacionWidth + 35 } else if (index > 0 && index < 3) { checkY += checkSpacing } else if (index > 3) { checkY += checkSpacing } dibujarCheckboxConLabel(doc, checkX, checkY, 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 // Borde de la sección dibujarRectangulo(doc, x, y, width, 12, PDF_CONFIG.lineWidth.normal) // Tazas NO Uniformes dibujarCasillasTazas(doc, x + 2, y + 2, muestra.tazasNoUniformes, 'Tazas NO Uniformes:') // Tazas Defectuosas dibujarCasillasTazas(doc, x + 55, y + 2, muestra.tazasDefectuosas, 'Tazas defectuosas:') // Defectos doc.setFontSize(PDF_CONFIG.fontSize.small) doc.setFont('helvetica', 'bold') doc.text('Defecto (de beberse):', x + 110, y + 3) let checkX = x + 150 DEFECTOS_PDF.forEach((defecto) => { const isChecked = muestra.defecto === defecto.key dibujarCheckboxConLabel(doc, checkX, y + 5, defecto.label, isChecked) checkX += 18 }) } /** * Renderiza la sección de Otras Notas */ function renderizarSeccionOtrasNotas( doc: jsPDF, x: number, y: number, muestra: Muestra ): void { const width = PDF_CONFIG.formWidth // Borde de la sección dibujarRectangulo(doc, x, y, width, 8, PDF_CONFIG.lineWidth.normal) // Label doc.setFontSize(PDF_CONFIG.fontSize.small) doc.setFont('helvetica', 'bold') doc.text('OTRAS NOTAS:', x + 2, y + 4) // Valor if (muestra.otrasNotas) { doc.setFont('helvetica', 'normal') const notaTruncada = truncarTexto( doc, muestra.otrasNotas, width - 35, PDF_CONFIG.fontSize.small ) doc.text(notaTruncada, x + 30, y + 4) } // Línea para escribir dibujarLineaHorizontal(doc, x + 30, y + 5, x + width - 2, 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 // Borde de la sección dibujarRectangulo(doc, x, y, width, 7, 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 + 2, y + 4) // 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 + 28 valores.forEach((item) => { doc.setFont('helvetica', 'normal') doc.text(item.label, currentX, y + 4) currentX += doc.getTextWidth(item.label) + 1 doc.setFont('helvetica', 'bold') doc.text(item.valor, currentX, y + 4) currentX += doc.getTextWidth(item.valor) + 5 }) }