feat: Agregar exportación PDF del formulario de catación EVC-IH01
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 2m51s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 2m51s
- Implementar módulo de generación PDF con jsPDF - Crear composable usePdfExport para exportar muestras y sesiones - Añadir botones de exportación PDF en header y por muestra - Replicar layout exacto del formulario físico IHCAFE - Soportar máximo 3 formularios por hoja carta
This commit is contained in:
150
nuxt4/app/composables/usePdfExport.ts
Normal file
150
nuxt4/app/composables/usePdfExport.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Composable para exportar formularios de catación a PDF
|
||||
* Genera PDFs en formato EVC-IH01 (formulario físico de IHCAFE)
|
||||
*/
|
||||
|
||||
import { jsPDF } from 'jspdf'
|
||||
import type { Muestra, SesionCatacion } from '~/types/catacion'
|
||||
import { renderizarFormulario } from '~/utils/pdf'
|
||||
|
||||
const FORMULARIOS_POR_PAGINA = 3
|
||||
|
||||
export const usePdfExport = () => {
|
||||
const exportando = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
/**
|
||||
* Exporta una muestra individual a PDF
|
||||
*/
|
||||
const exportarMuestra = async (
|
||||
muestra: Muestra,
|
||||
nombreArchivo?: string
|
||||
): Promise<void> => {
|
||||
try {
|
||||
exportando.value = true
|
||||
error.value = null
|
||||
|
||||
const doc = new jsPDF({
|
||||
orientation: 'portrait',
|
||||
unit: 'mm',
|
||||
format: 'letter',
|
||||
})
|
||||
|
||||
renderizarFormulario(doc, muestra, 0)
|
||||
|
||||
const filename =
|
||||
nombreArchivo || `catacion-${sanitizarNombre(muestra.nombre)}-${Date.now()}.pdf`
|
||||
doc.save(filename)
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Error al exportar PDF'
|
||||
console.error('Error exportando PDF:', err)
|
||||
throw err
|
||||
} finally {
|
||||
exportando.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporta toda la sesión a PDF (máximo 3 formularios por página)
|
||||
*/
|
||||
const exportarSesion = async (
|
||||
sesion: SesionCatacion,
|
||||
nombreArchivo?: string
|
||||
): Promise<void> => {
|
||||
try {
|
||||
exportando.value = true
|
||||
error.value = null
|
||||
|
||||
const doc = new jsPDF({
|
||||
orientation: 'portrait',
|
||||
unit: 'mm',
|
||||
format: 'letter',
|
||||
})
|
||||
|
||||
const muestras = sesion.muestras
|
||||
|
||||
muestras.forEach((muestra, index) => {
|
||||
const posicionEnPagina = index % FORMULARIOS_POR_PAGINA
|
||||
|
||||
// Nueva página si es necesario (excepto primera)
|
||||
if (index > 0 && posicionEnPagina === 0) {
|
||||
doc.addPage()
|
||||
}
|
||||
|
||||
renderizarFormulario(doc, muestra, posicionEnPagina as 0 | 1 | 2)
|
||||
})
|
||||
|
||||
const filename = nombreArchivo || `catacion-sesion-${sesion.fecha}.pdf`
|
||||
doc.save(filename)
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Error al exportar PDF'
|
||||
console.error('Error exportando PDF de sesión:', err)
|
||||
throw err
|
||||
} finally {
|
||||
exportando.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporta muestras seleccionadas a PDF
|
||||
*/
|
||||
const exportarMuestrasSeleccionadas = async (
|
||||
muestras: Muestra[],
|
||||
nombreArchivo?: string
|
||||
): Promise<void> => {
|
||||
try {
|
||||
exportando.value = true
|
||||
error.value = null
|
||||
|
||||
if (muestras.length === 0) {
|
||||
throw new Error('No hay muestras seleccionadas para exportar')
|
||||
}
|
||||
|
||||
const doc = new jsPDF({
|
||||
orientation: 'portrait',
|
||||
unit: 'mm',
|
||||
format: 'letter',
|
||||
})
|
||||
|
||||
muestras.forEach((muestra, index) => {
|
||||
const posicionEnPagina = index % FORMULARIOS_POR_PAGINA
|
||||
|
||||
// Nueva página si es necesario (excepto primera)
|
||||
if (index > 0 && posicionEnPagina === 0) {
|
||||
doc.addPage()
|
||||
}
|
||||
|
||||
renderizarFormulario(doc, muestra, posicionEnPagina as 0 | 1 | 2)
|
||||
})
|
||||
|
||||
const filename = nombreArchivo || `catacion-seleccion-${Date.now()}.pdf`
|
||||
doc.save(filename)
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Error al exportar PDF'
|
||||
console.error('Error exportando PDFs seleccionados:', err)
|
||||
throw err
|
||||
} finally {
|
||||
exportando.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
exportando: readonly(exportando),
|
||||
error: readonly(error),
|
||||
exportarMuestra,
|
||||
exportarSesion,
|
||||
exportarMuestrasSeleccionadas,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza un nombre para usar como nombre de archivo
|
||||
*/
|
||||
function sanitizarNombre(nombre: string): string {
|
||||
return nombre
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9áéíóúñ]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
.substring(0, 50)
|
||||
}
|
||||
@@ -51,11 +51,20 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="cata-button p-2 flex items-center justify-center hidden sm:flex"
|
||||
title="Exportar sesión"
|
||||
title="Exportar JSON"
|
||||
@click="exportar"
|
||||
>
|
||||
<UIcon name="i-lucide-download" class="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
class="cata-button p-2 flex items-center justify-center hidden sm:flex"
|
||||
title="Exportar PDF"
|
||||
:disabled="exportandoPdf"
|
||||
@click="handleExportarPdf"
|
||||
>
|
||||
<UIcon v-if="!exportandoPdf" name="i-lucide-file-text" class="w-5 h-5" />
|
||||
<span v-else class="loading-spinner-small"></span>
|
||||
</button>
|
||||
<button
|
||||
class="cata-button p-2 flex items-center justify-center hidden sm:flex"
|
||||
title="Finalizar sesión"
|
||||
@@ -87,7 +96,15 @@
|
||||
@click="exportar"
|
||||
>
|
||||
<UIcon name="i-lucide-download" class="w-4 h-4 inline mr-2" />
|
||||
Exportar Sesión
|
||||
Exportar JSON
|
||||
</button>
|
||||
<button
|
||||
class="menu-item cata-text w-full text-left px-3 py-2 hover:bg-primary/10 rounded"
|
||||
:disabled="exportandoPdf"
|
||||
@click="handleExportarPdf"
|
||||
>
|
||||
<UIcon name="i-lucide-file-text" class="w-4 h-4 inline mr-2" />
|
||||
{{ exportandoPdf ? 'Generando PDF...' : 'Exportar PDF' }}
|
||||
</button>
|
||||
<button
|
||||
class="menu-item cata-text w-full text-left px-3 py-2 hover:bg-primary/10 rounded"
|
||||
@@ -181,6 +198,16 @@
|
||||
>
|
||||
<UIcon name="i-lucide-expand" class="w-4 h-4" />
|
||||
</button>
|
||||
<!-- Botón para exportar PDF de muestra individual -->
|
||||
<button
|
||||
class="boton-pdf hidden sm:flex"
|
||||
:disabled="exportandoPdf"
|
||||
@click.stop="handleExportarMuestraPdf(item.muestra)"
|
||||
title="Exportar muestra a PDF"
|
||||
>
|
||||
<UIcon v-if="!exportandoPdf" name="i-lucide-file-text" class="w-4 h-4" />
|
||||
<span v-else class="loading-spinner-tiny"></span>
|
||||
</button>
|
||||
<CataResumenMuestra
|
||||
:muestra="item.muestra"
|
||||
:tab-activa="tabActiva"
|
||||
@@ -245,6 +272,13 @@ const {
|
||||
eliminarSesionActual,
|
||||
} = useCatacion()
|
||||
|
||||
// PDF Export
|
||||
const {
|
||||
exportando: exportandoPdf,
|
||||
exportarMuestra: exportarMuestraPdf,
|
||||
exportarSesion: exportarSesionPdf,
|
||||
} = usePdfExport()
|
||||
|
||||
const { inicializar } = useIndexedDB()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
@@ -446,7 +480,7 @@ const formatearFecha = (fecha: string): string => {
|
||||
})
|
||||
}
|
||||
|
||||
// Exportar sesión
|
||||
// Exportar sesión JSON
|
||||
const exportar = () => {
|
||||
const json = exportarSesion()
|
||||
if (!json) return
|
||||
@@ -465,6 +499,27 @@ const exportar = () => {
|
||||
mostrarMenu.value = false
|
||||
}
|
||||
|
||||
// Exportar sesión PDF
|
||||
const handleExportarPdf = async () => {
|
||||
if (!sesionActiva.value) return
|
||||
|
||||
try {
|
||||
await exportarSesionPdf(sesionActiva.value)
|
||||
mostrarMenu.value = false
|
||||
} catch (err) {
|
||||
console.error('Error al exportar PDF:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Exportar muestra individual a PDF
|
||||
const handleExportarMuestraPdf = async (muestra: Muestra) => {
|
||||
try {
|
||||
await exportarMuestraPdf(muestra)
|
||||
} catch (err) {
|
||||
console.error('Error al exportar muestra PDF:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Confirmar eliminación
|
||||
const confirmarEliminar = () => {
|
||||
const confirmar = window.confirm(
|
||||
@@ -619,6 +674,52 @@ onMounted(() => {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.boton-pdf {
|
||||
flex-shrink: 0;
|
||||
padding: 0.25rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--cata-primary);
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.boton-pdf:hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.boton-pdf:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.boton-pdf:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Loading spinner pequeño para botones */
|
||||
.loading-spinner-small {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 9999px;
|
||||
border: 2px solid color-mix(in srgb, var(--cata-primary) 20%, transparent);
|
||||
border-top-color: var(--cata-primary);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-spinner-tiny {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 9999px;
|
||||
border: 2px solid color-mix(in srgb, var(--cata-primary) 20%, transparent);
|
||||
border-top-color: var(--cata-primary);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Floating action button */
|
||||
.floating-action {
|
||||
position: fixed;
|
||||
|
||||
8
nuxt4/app/utils/pdf/index.ts
Normal file
8
nuxt4/app/utils/pdf/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Módulo de exportación PDF para catación de café
|
||||
* Exporta funciones para generar PDFs del formulario EVC-IH01
|
||||
*/
|
||||
|
||||
export { renderizarFormulario } from './pdfFormulario'
|
||||
export { PDF_CONFIG, calcularYInicio } from './pdfLayout'
|
||||
export * from './pdfHelpers'
|
||||
590
nuxt4/app/utils/pdf/pdfFormulario.ts
Normal file
590
nuxt4/app/utils/pdf/pdfFormulario.ts
Normal file
@@ -0,0 +1,590 @@
|
||||
/**
|
||||
* 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
|
||||
})
|
||||
}
|
||||
263
nuxt4/app/utils/pdf/pdfHelpers.ts
Normal file
263
nuxt4/app/utils/pdf/pdfHelpers.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* Funciones auxiliares para dibujar elementos en el PDF
|
||||
*/
|
||||
|
||||
import type { jsPDF } from 'jspdf'
|
||||
import { PDF_CONFIG } from './pdfLayout'
|
||||
|
||||
/**
|
||||
* Dibuja un checkbox (marcado o sin marcar)
|
||||
*/
|
||||
export function dibujarCheckbox(
|
||||
doc: jsPDF,
|
||||
x: number,
|
||||
y: number,
|
||||
checked: boolean = false,
|
||||
size: number = PDF_CONFIG.checkboxSize
|
||||
): void {
|
||||
doc.setDrawColor(0)
|
||||
doc.setLineWidth(PDF_CONFIG.lineWidth.normal)
|
||||
doc.rect(x, y, size, size)
|
||||
|
||||
if (checked) {
|
||||
// Dibujar X dentro del checkbox
|
||||
const padding = 0.4
|
||||
doc.line(x + padding, y + padding, x + size - padding, y + size - padding)
|
||||
doc.line(x + size - padding, y + padding, x + padding, y + size - padding)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dibuja un checkbox con label
|
||||
*/
|
||||
export function dibujarCheckboxConLabel(
|
||||
doc: jsPDF,
|
||||
x: number,
|
||||
y: number,
|
||||
label: string,
|
||||
checked: boolean = false,
|
||||
indent: boolean = false
|
||||
): number {
|
||||
const checkboxSize = PDF_CONFIG.checkboxSize
|
||||
const xOffset = indent ? 2 : 0
|
||||
|
||||
dibujarCheckbox(doc, x + xOffset, y, checked, checkboxSize)
|
||||
|
||||
doc.setFontSize(PDF_CONFIG.fontSize.tiny)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.text(label, x + xOffset + checkboxSize + 1, y + checkboxSize - 0.5)
|
||||
|
||||
// Retorna el ancho total usado
|
||||
return xOffset + checkboxSize + 1 + doc.getTextWidth(label)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dibuja las 5 casillas de tazas con números
|
||||
*/
|
||||
export function dibujarCasillasTazas(
|
||||
doc: jsPDF,
|
||||
x: number,
|
||||
y: number,
|
||||
tazasSeleccionadas: number[],
|
||||
label: string
|
||||
): void {
|
||||
const checkboxSize = PDF_CONFIG.checkboxSize
|
||||
const spacing = checkboxSize + 1.5
|
||||
|
||||
// Label
|
||||
doc.setFontSize(PDF_CONFIG.fontSize.small)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(label, x, y)
|
||||
|
||||
// Casillas 1-5
|
||||
const startX = x
|
||||
const startY = y + 2
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const checkX = startX + (i - 1) * spacing
|
||||
const isSelected = tazasSeleccionadas.includes(i)
|
||||
|
||||
dibujarCheckbox(doc, checkX, startY, isSelected, checkboxSize)
|
||||
|
||||
// Número debajo de la casilla
|
||||
doc.setFontSize(PDF_CONFIG.fontSize.tiny)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.text(String(i), checkX + checkboxSize / 2 - 0.5, startY + checkboxSize + 2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dibuja un rectángulo con texto centrado
|
||||
*/
|
||||
export function dibujarCeldaTexto(
|
||||
doc: jsPDF,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
texto: string,
|
||||
opciones: {
|
||||
fontSize?: number
|
||||
fontStyle?: 'normal' | 'bold'
|
||||
align?: 'left' | 'center' | 'right'
|
||||
border?: boolean
|
||||
backgroundColor?: string
|
||||
} = {}
|
||||
): void {
|
||||
const {
|
||||
fontSize = PDF_CONFIG.fontSize.body,
|
||||
fontStyle = 'normal',
|
||||
align = 'center',
|
||||
border = true,
|
||||
backgroundColor,
|
||||
} = opciones
|
||||
|
||||
// Fondo si se especifica
|
||||
if (backgroundColor) {
|
||||
doc.setFillColor(backgroundColor)
|
||||
doc.rect(x, y, width, height, 'F')
|
||||
}
|
||||
|
||||
// Borde
|
||||
if (border) {
|
||||
doc.setDrawColor(0)
|
||||
doc.setLineWidth(PDF_CONFIG.lineWidth.thin)
|
||||
doc.rect(x, y, width, height)
|
||||
}
|
||||
|
||||
// Texto
|
||||
doc.setFontSize(fontSize)
|
||||
doc.setFont('helvetica', fontStyle)
|
||||
|
||||
let textX = x + 1
|
||||
if (align === 'center') {
|
||||
textX = x + width / 2
|
||||
} else if (align === 'right') {
|
||||
textX = x + width - 1
|
||||
}
|
||||
|
||||
doc.text(texto, textX, y + height / 2 + fontSize / 4, {
|
||||
align,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Dibuja una línea horizontal
|
||||
*/
|
||||
export function dibujarLineaHorizontal(
|
||||
doc: jsPDF,
|
||||
x1: number,
|
||||
y: number,
|
||||
x2: number,
|
||||
grosor: number = PDF_CONFIG.lineWidth.normal
|
||||
): void {
|
||||
doc.setDrawColor(0)
|
||||
doc.setLineWidth(grosor)
|
||||
doc.line(x1, y, x2, y)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dibuja una línea vertical
|
||||
*/
|
||||
export function dibujarLineaVertical(
|
||||
doc: jsPDF,
|
||||
x: number,
|
||||
y1: number,
|
||||
y2: number,
|
||||
grosor: number = PDF_CONFIG.lineWidth.normal
|
||||
): void {
|
||||
doc.setDrawColor(0)
|
||||
doc.setLineWidth(grosor)
|
||||
doc.line(x, y1, x, y2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dibuja un campo de texto con label y línea para escribir
|
||||
*/
|
||||
export function dibujarCampoTexto(
|
||||
doc: jsPDF,
|
||||
x: number,
|
||||
y: number,
|
||||
label: string,
|
||||
valor: string | null,
|
||||
anchoLinea: number = 30
|
||||
): void {
|
||||
doc.setFontSize(PDF_CONFIG.fontSize.small)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(label, x, y)
|
||||
|
||||
const labelWidth = doc.getTextWidth(label)
|
||||
|
||||
// Línea para escribir
|
||||
dibujarLineaHorizontal(doc, x + labelWidth + 1, y + 0.5, x + labelWidth + 1 + anchoLinea)
|
||||
|
||||
// Valor si existe
|
||||
if (valor) {
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.text(valor, x + labelWidth + 2, y)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dibuja texto multilínea con word wrap
|
||||
*/
|
||||
export function dibujarTextoMultilinea(
|
||||
doc: jsPDF,
|
||||
x: number,
|
||||
y: number,
|
||||
texto: string,
|
||||
maxWidth: number,
|
||||
lineHeight: number = 3
|
||||
): number {
|
||||
doc.setFontSize(PDF_CONFIG.fontSize.small)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
|
||||
const lines = doc.splitTextToSize(texto, maxWidth)
|
||||
let currentY = y
|
||||
|
||||
for (const line of lines) {
|
||||
doc.text(line, x, currentY)
|
||||
currentY += lineHeight
|
||||
}
|
||||
|
||||
return currentY - y // Retorna la altura total usada
|
||||
}
|
||||
|
||||
/**
|
||||
* Dibuja un rectángulo con bordes
|
||||
*/
|
||||
export function dibujarRectangulo(
|
||||
doc: jsPDF,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
grosor: number = PDF_CONFIG.lineWidth.normal
|
||||
): void {
|
||||
doc.setDrawColor(0)
|
||||
doc.setLineWidth(grosor)
|
||||
doc.rect(x, y, width, height)
|
||||
}
|
||||
|
||||
/**
|
||||
* Trunca texto si excede el ancho máximo
|
||||
*/
|
||||
export function truncarTexto(
|
||||
doc: jsPDF,
|
||||
texto: string,
|
||||
maxWidth: number,
|
||||
fontSize: number
|
||||
): string {
|
||||
doc.setFontSize(fontSize)
|
||||
|
||||
if (doc.getTextWidth(texto) <= maxWidth) {
|
||||
return texto
|
||||
}
|
||||
|
||||
let truncado = texto
|
||||
while (doc.getTextWidth(truncado + '...') > maxWidth && truncado.length > 0) {
|
||||
truncado = truncado.slice(0, -1)
|
||||
}
|
||||
|
||||
return truncado + '...'
|
||||
}
|
||||
223
nuxt4/app/utils/pdf/pdfLayout.ts
Normal file
223
nuxt4/app/utils/pdf/pdfLayout.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* Constantes y configuración del layout para el formulario PDF EVC-IH01
|
||||
* Tamaño: Carta (Letter) - 8.5" x 11" (215.9mm x 279.4mm)
|
||||
* Máximo 3 formularios por página
|
||||
*/
|
||||
|
||||
export const PDF_CONFIG = {
|
||||
// Página Carta (Letter)
|
||||
pageWidth: 215.9, // mm (8.5")
|
||||
pageHeight: 279.4, // mm (11")
|
||||
|
||||
// Márgenes
|
||||
marginTop: 8,
|
||||
marginBottom: 8,
|
||||
marginLeft: 8,
|
||||
marginRight: 8,
|
||||
|
||||
// Formulario individual (3 por página)
|
||||
formHeight: 85, // ~85mm cada formulario
|
||||
formWidth: 199.9, // pageWidth - marginLeft - marginRight
|
||||
formSpacing: 2, // Espacio entre formularios
|
||||
|
||||
// Tipografía
|
||||
fontSize: {
|
||||
title: 9,
|
||||
header: 7,
|
||||
body: 6,
|
||||
small: 5,
|
||||
tiny: 4.5,
|
||||
},
|
||||
|
||||
// Colores
|
||||
colors: {
|
||||
black: '#000000',
|
||||
gray: '#666666',
|
||||
lightGray: '#CCCCCC',
|
||||
},
|
||||
|
||||
// Grosor de líneas
|
||||
lineWidth: {
|
||||
thin: 0.1,
|
||||
normal: 0.2,
|
||||
thick: 0.4,
|
||||
},
|
||||
|
||||
// Tamaño de checkbox
|
||||
checkboxSize: 2.5,
|
||||
checkboxSpacing: 0.8,
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Calcula la posición Y inicial para un formulario según su posición en la página
|
||||
* @param posicion - Posición del formulario (0, 1, o 2)
|
||||
* @returns Coordenada Y inicial en mm
|
||||
*/
|
||||
export function calcularYInicio(posicion: 0 | 1 | 2): number {
|
||||
const { marginTop, formHeight, formSpacing } = PDF_CONFIG
|
||||
return marginTop + posicion * (formHeight + formSpacing)
|
||||
}
|
||||
|
||||
/**
|
||||
* Secciones del formulario con sus posiciones relativas
|
||||
*/
|
||||
export const FORM_SECTIONS = {
|
||||
// Encabezado
|
||||
header: {
|
||||
y: 0,
|
||||
height: 10,
|
||||
},
|
||||
// Tabla de intensidades (lado izquierdo)
|
||||
intensidades: {
|
||||
x: 0,
|
||||
y: 10,
|
||||
width: 48,
|
||||
height: 32,
|
||||
},
|
||||
// Fragancia-Aroma (centro-derecha superior)
|
||||
fraganciaAroma: {
|
||||
x: 48,
|
||||
y: 10,
|
||||
width: 76,
|
||||
height: 18,
|
||||
},
|
||||
// Notas Fragancia-Aroma (derecha superior)
|
||||
notasFragancia: {
|
||||
x: 124,
|
||||
y: 10,
|
||||
width: 75.9,
|
||||
height: 18,
|
||||
},
|
||||
// Sabores (centro-derecha medio)
|
||||
sabores: {
|
||||
x: 48,
|
||||
y: 28,
|
||||
width: 76,
|
||||
height: 14,
|
||||
},
|
||||
// Notas Sabores (derecha medio)
|
||||
notasSabor: {
|
||||
x: 124,
|
||||
y: 28,
|
||||
width: 75.9,
|
||||
height: 14,
|
||||
},
|
||||
// Sensación en boca y gustos
|
||||
sensacionGustos: {
|
||||
x: 48,
|
||||
y: 42,
|
||||
width: 151.9,
|
||||
height: 12,
|
||||
},
|
||||
// Tazas y defectos
|
||||
tazasDefectos: {
|
||||
x: 0,
|
||||
y: 54,
|
||||
width: 199.9,
|
||||
height: 12,
|
||||
},
|
||||
// Otras notas
|
||||
otrasNotas: {
|
||||
x: 0,
|
||||
y: 66,
|
||||
width: 199.9,
|
||||
height: 8,
|
||||
},
|
||||
// Sub total
|
||||
subTotal: {
|
||||
x: 0,
|
||||
y: 74,
|
||||
width: 199.9,
|
||||
height: 10,
|
||||
},
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Nombres de los parámetros de intensidad para la tabla
|
||||
*/
|
||||
export const PARAMETROS_INTENSIDAD = [
|
||||
{ key: 'fragancia', label: 'Fragancia' },
|
||||
{ key: 'aroma', label: 'Aroma' },
|
||||
{ key: 'sabor', label: 'Sabor' },
|
||||
{ key: 'saborResidual', label: 'Sabor Residual' },
|
||||
{ key: 'acidez', label: 'Acidez' },
|
||||
{ key: 'dulzor', label: 'Dulzor' },
|
||||
{ key: 'sensacionBoca', label: 'Sensación Boca' },
|
||||
{ key: 'impresionGlobal', label: 'Impresión Global' },
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Tipo para categoría de nota en PDF
|
||||
*/
|
||||
export interface CategoriaPdf {
|
||||
key: string
|
||||
label: string
|
||||
indent?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorías de notas para el formulario PDF
|
||||
* Columna izquierda y derecha de checkboxes
|
||||
*/
|
||||
export const CATEGORIAS_PDF: {
|
||||
columnaIzquierda: CategoriaPdf[]
|
||||
columnaDerecha: CategoriaPdf[]
|
||||
} = {
|
||||
columnaIzquierda: [
|
||||
{ key: 'Floral', label: 'Floral' },
|
||||
{ key: 'Afrutado', label: 'Afrutado' },
|
||||
{ key: 'Bayas', label: 'Bayas', indent: true },
|
||||
{ key: 'Ácido', label: 'Ácido', indent: true },
|
||||
{ key: 'Frutas Deshidratadas', label: 'Frutas Deshid.', indent: true },
|
||||
{ key: 'Fermentado', label: 'Fermentado', indent: true },
|
||||
{ key: 'Cítricos', label: 'Cítricos' },
|
||||
{ key: 'Verde Vegetal', label: 'Verde/Vegetal' },
|
||||
{ key: 'Químico', label: 'Químico' },
|
||||
{ key: 'Humedad/Tierra', label: 'Humedad/Tierra' },
|
||||
{ key: 'Madera', label: 'Madera' },
|
||||
],
|
||||
columnaDerecha: [
|
||||
{ key: 'Tostado', label: 'Tostado' },
|
||||
{ key: 'Cereal', label: 'Cereal', indent: true },
|
||||
{ key: 'Quemado', label: 'Quemado', indent: true },
|
||||
{ key: 'Tabaco', label: 'Tabaco', indent: true },
|
||||
{ key: 'Nueces/Cacao', label: 'Nueces/Cacao' },
|
||||
{ key: 'Nueces', label: 'Nueces', indent: true },
|
||||
{ key: 'Cacao', label: 'Cacao', indent: true },
|
||||
{ key: 'Especias', label: 'Especias' },
|
||||
{ key: 'Dulce', label: 'Dulce' },
|
||||
{ key: 'Vainilla', label: 'Vainilla', indent: true },
|
||||
{ key: 'Azúcar Morena', label: 'Azúcar Morena', indent: true },
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Sensaciones en boca para el formulario PDF
|
||||
*/
|
||||
export const SENSACIONES_PDF = [
|
||||
{ key: 'Áspero', label: 'Áspero (Arenoso, Rugoso, Rasposo)' },
|
||||
{ key: 'Aceitoso', label: 'Aceitoso' },
|
||||
{ key: 'Metálico', label: 'Metálico' },
|
||||
{ key: 'Suave', label: 'Suave (Aterciopelado, Sedoso)' },
|
||||
{ key: 'Deja seca la boca', label: 'Deja seca la boca (Astringente)' },
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Gustos predominantes para el formulario PDF
|
||||
*/
|
||||
export const GUSTOS_PDF = [
|
||||
{ key: 'Salado', label: 'Salado' },
|
||||
{ key: 'Amargo', label: 'Amargo' },
|
||||
{ key: 'Ácido', label: 'Ácido' },
|
||||
{ key: 'Dulce', label: 'Dulce' },
|
||||
{ key: 'Umami', label: 'Umami' },
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Defectos para el formulario PDF
|
||||
*/
|
||||
export const DEFECTOS_PDF = [
|
||||
{ key: 'Mohoso', label: 'Mohoso' },
|
||||
{ key: 'Fenólico', label: 'Fenólico' },
|
||||
{ key: 'Papa', label: 'Papa' },
|
||||
] as const
|
||||
213
nuxt4/package-lock.json
generated
213
nuxt4/package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"@nuxt/ui": "^4.0.1",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"eslint": "^9.37.0",
|
||||
"jspdf": "^3.0.4",
|
||||
"nuxt": "^4.1.3",
|
||||
"typescript": "^5.9.3",
|
||||
"vue": "^3.5.22",
|
||||
@@ -1692,7 +1693,6 @@
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -6246,12 +6246,25 @@
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/pako": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
|
||||
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/parse-path": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz",
|
||||
"integrity": "sha512-LriObC2+KYZD3FzCrgWGv/qufdUy4eXrxcLgQMfYXgPbLIecKIsVBaQgUPmxSSLcjmYbDTQbMgr6qr6l/eb7Bg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/raf": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||
@@ -6262,7 +6275,7 @@
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
@@ -7892,6 +7905,16 @@
|
||||
"bare-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -8324,6 +8347,26 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/canvg": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
|
||||
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/raf": "^3.4.0",
|
||||
"core-js": "^3.8.3",
|
||||
"raf": "^3.4.1",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rgbcolor": "^1.0.1",
|
||||
"stackblur-canvas": "^2.0.0",
|
||||
"svg-pathdata": "^6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ccount": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
||||
@@ -8917,6 +8960,18 @@
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.47.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz",
|
||||
"integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.46.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz",
|
||||
@@ -9045,6 +9100,16 @@
|
||||
"postcss": "^8.0.9"
|
||||
}
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||
@@ -9586,6 +9651,16 @@
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz",
|
||||
"integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optional": true,
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
@@ -10880,6 +10955,23 @@
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-png": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
|
||||
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/pako": "^2.0.3",
|
||||
"iobuffer": "^5.3.2",
|
||||
"pako": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-png/node_modules/pako": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||
@@ -10923,6 +11015,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/figures": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
|
||||
@@ -12060,6 +12158,20 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
@@ -12266,6 +12378,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/iobuffer": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ioredis": {
|
||||
"version": "5.8.1",
|
||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.1.tgz",
|
||||
@@ -13329,6 +13447,23 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jspdf": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz",
|
||||
"integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"fast-png": "^6.2.0",
|
||||
"fflate": "^0.8.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"canvg": "^3.0.11",
|
||||
"core-js": "^3.6.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"html2canvas": "^1.0.0-rc.5"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -15977,6 +16112,13 @@
|
||||
"integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -16740,6 +16882,16 @@
|
||||
"integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/raf": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"performance-now": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
@@ -16924,6 +17076,13 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/regex": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz",
|
||||
@@ -17407,6 +17566,16 @@
|
||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rgbcolor": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8.15"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
|
||||
@@ -18185,6 +18354,16 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stackblur-canvas": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/standard-as-callback": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
||||
@@ -18574,6 +18753,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/svg-pathdata": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svgo": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz",
|
||||
@@ -18819,6 +19008,16 @@
|
||||
"b4a": "^1.6.4"
|
||||
}
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-inflate": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||
@@ -19888,6 +20087,16 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/vaul-vue": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vaul-vue/-/vaul-vue-0.4.1.tgz",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"@nuxt/ui": "^4.0.1",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"eslint": "^9.37.0",
|
||||
"jspdf": "^3.0.4",
|
||||
"nuxt": "^4.1.3",
|
||||
"typescript": "^5.9.3",
|
||||
"vue": "^3.5.22",
|
||||
|
||||
Reference in New Issue
Block a user