Files
cataRio/nuxt4/app/utils/pdf/pdfFormulario.ts
josedario87 ce460a17c2
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m12s
fix: Ajustes al layout del PDF de catación
- Cambiar a 2 formularios por hoja para más espacio vertical
- Corregir distribución de familias de notas (Floral, Afrutado, Verde/Vegetal, Otra, Tostado, etc.)
- Aumentar margen vertical entre checkboxes
- Corregir "beberse" → "haberse" en sección de defectos
- Alinear checkboxes de defectos bajo el header
- Quitar recuadro de Notas y mostrar solo "Notas: valor"
2025-11-24 17:37:47 -06:00

493 lines
14 KiB
TypeScript

/**
* 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,
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 formulario de 130mm
renderizarEncabezado(doc, xBase, yBase, muestra)
renderizarTablaIntensidades(doc, xBase, yBase + 14, muestra)
renderizarSeccionFraganciaAroma(doc, xBase + 55, yBase + 14, muestra)
renderizarSeccionSabores(doc, xBase + 55, yBase + 55, muestra)
renderizarSeccionSensacionGustos(doc, xBase, yBase + 76, muestra)
renderizarSeccionTazasDefectos(doc, xBase, yBase + 96, muestra)
renderizarSeccionOtrasNotas(doc, xBase, yBase + 112, muestra)
renderizarSeccionSubTotal(doc, xBase, yBase + 122, 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 + 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
*/
function renderizarSeccionFraganciaAroma(
doc: jsPDF,
x: number,
y: number,
muestra: Muestra
): void {
const width = 144.9 // Ancho total de la sección derecha
const sectionHeight = 41
// Borde de la sección
dibujarRectangulo(doc, x, y, width, sectionHeight, PDF_CONFIG.lineWidth.normal)
// Header
doc.setFontSize(PDF_CONFIG.fontSize.body)
doc.setFont('helvetica', 'bold')
doc.text('Fragancia - Aroma', x + 3, y + 5)
// Línea después del header
dibujarLineaHorizontal(doc, x, y + 7, x + width, PDF_CONFIG.lineWidth.thin)
// Determinar qué está seleccionado
const categoriasSeleccionadas = muestra.fraganciaAromaNotas.categorias
const subcategoriasSeleccionadas = muestra.fraganciaAromaNotas.subcategorias
// Espaciado entre checkboxes (mayor margen vertical)
const checkSpacing = 3.2
const startY = y + 10
// Columna izquierda de checkboxes
let checkY = startY
CATEGORIAS_PDF.columnaIzquierda.forEach((cat) => {
const isChecked =
categoriasSeleccionadas.includes(cat.key as any) ||
subcategoriasSeleccionadas.includes(cat.key)
dibujarCheckboxConLabel(doc, x + 3, checkY, cat.label, isChecked, cat.indent)
checkY += checkSpacing
})
// Columna derecha de checkboxes
checkY = startY
CATEGORIAS_PDF.columnaDerecha.forEach((cat) => {
const isChecked =
categoriasSeleccionadas.includes(cat.key as any) ||
subcategoriasSeleccionadas.includes(cat.key)
dibujarCheckboxConLabel(doc, x + 75, checkY, cat.label, isChecked, cat.indent)
checkY += checkSpacing
})
// Notas específica (sin recuadro, solo texto)
doc.setFontSize(PDF_CONFIG.fontSize.small)
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 - 25,
PDF_CONFIG.fontSize.small
)
doc.text(notaTruncada, x + 18, y + sectionHeight - 2)
}
}
/**
* Renderiza la sección de Sabores
*/
function renderizarSeccionSabores(
doc: jsPDF,
x: number,
y: number,
muestra: Muestra
): void {
const width = 144.9
const sectionHeight = 21
// Borde de la sección
dibujarRectangulo(doc, x, y, width, sectionHeight, PDF_CONFIG.lineWidth.normal)
// Header
doc.setFontSize(PDF_CONFIG.fontSize.body)
doc.setFont('helvetica', 'bold')
doc.text('Sabores', x + 3, y + 5)
// Línea después del header
dibujarLineaHorizontal(doc, x, y + 7, x + width, PDF_CONFIG.lineWidth.thin)
const categoriasSeleccionadas = muestra.saborNotas.categorias
const subcategoriasSeleccionadas = muestra.saborNotas.subcategorias
// Checkboxes en una sola fila (solo categorías principales sin subcategorías)
const checkSpacing = 3
const startY = y + 11
// Mostrar solo categorías principales
const categoriasPrincipales = [
{ key: 'Floral', label: 'Floral' },
{ key: 'Afrutado', label: 'Afrutado' },
{ key: 'Verde Vegetal', label: 'Verde/Vegetal' },
{ key: 'Otro', label: 'Otra' },
{ key: 'Tostado', label: 'Tostado' },
{ key: 'Nueces/Cacao', label: 'Nueces/Cacao' },
{ key: 'Especias', label: 'Especias' },
{ key: 'Dulce', label: 'Dulce' },
]
let checkX = x + 3
categoriasPrincipales.forEach((cat) => {
const isChecked =
categoriasSeleccionadas.includes(cat.key as any) ||
subcategoriasSeleccionadas.includes(cat.key)
dibujarCheckboxConLabel(doc, checkX, startY, cat.label, isChecked)
checkX += 18
})
// Notas específica de sabor (sin recuadro)
doc.setFontSize(PDF_CONFIG.fontSize.small)
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 - 25,
PDF_CONFIG.fontSize.small
)
doc.text(notaTruncada, x + 18, y + sectionHeight - 2)
}
}
/**
* 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
const sectionHeight = 20
// 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.body)
doc.setFont('helvetica', 'bold')
doc.text('Sensación en boca', x + 3, y + 5)
// Línea divisoria vertical
dibujarLineaVertical(doc, x + sensacionWidth, y, y + sectionHeight)
// Checkboxes de sensación con más espacio vertical
const checkSpacing = 3.5
let checkY = y + 9
SENSACIONES_PDF.forEach((sens) => {
const isChecked = muestra.sensacionEnBoca === sens.key
// Usar label corto para ahorrar espacio
const labelCorto =
sens.key === 'Deja seca la boca'
? 'Deja seca la boca (Astringente)'
: sens.key === 'Áspero'
? 'Áspero (Arenoso, Rugoso)'
: sens.key === 'Suave'
? 'Suave (Aterciopelado)'
: sens.key
dibujarCheckboxConLabel(doc, x + 3, checkY, labelCorto, isChecked)
checkY += checkSpacing
})
// Sección Gustos predominantes
doc.setFont('helvetica', 'bold')
doc.text('Gustos predominantes (2)', x + sensacionWidth + 3, y + 5)
// Checkboxes de gustos con más espacio vertical
checkY = y + 9
GUSTOS_PDF.forEach((gusto) => {
const isChecked = muestra.gustosPredominantes.includes(gusto.key as any)
dibujarCheckboxConLabel(doc, x + sensacionWidth + 3, checkY, gusto.label, isChecked)
checkY += checkSpacing
})
}
/**
* 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 = 10
// 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ínea para escribir
dibujarLineaHorizontal(doc, x + 38, y + 7, 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
})
}