@ -9,6 +9,26 @@
import { appState , getViewImage } from "../data/AppState.svelte";
import { formatPrice } from "../utils/formats";
import type { View } from "../data/DataConfigTypes";
import { advertenciaIconB64 } from "../data/DataIconsB64"
interface Option {
nombreGrupo: string,
nombre: string,
precio?: number | null | string,
consultar: boolean,
warning: boolean,
warningMessage?: string,
}
interface ViewOptions {
options: Option[];
total : number | null;
hasWarning: boolean;
hasConsult: boolean;
}
const stepTimeout = 1000;
const logoProject = dataConfig.sources.logos + dataConfig.logos.project;
@ -65,6 +85,14 @@
pdfDoc.addFileToVFS(pl.filename, pl.base64);
pdfDoc.addFont(pl.filename, pl.name, pl.style);
// Calcular tamaño de imagen
const imgPixelWidth = 1500;
const imgPixelHeight = 1500;
const pageRatio = pageWidth / pageHeight;
const imgRatio = imgPixelWidth / imgPixelHeight;
// === Portada ===
const ini = dataPdfImages['pdf_openbox_ini'];
if (ini) {
@ -79,6 +107,46 @@
pdfDoc.addPage();
}
// == Presentacion ===
// Configuración grid 3x2
const cols = 3;
const spacing = 10;
let startY = 15;
const squareSize = (pageWidth - margin * 2 - spacing * (cols - 1)) / cols;
const base64 = dataPdfImages['pdf_openbox_pre'];
if (base64) {
const preFormat = base64.includes('jpeg') ? 'JPEG' : 'PNG';
pdfDoc.addImage(base64, 'JPEG', margin, startY, squareSize, squareSize, '', 'FAST');
}
// Colocar las minuaturas de las 5 vistas en el documento
views.slice(0,2).forEach((view,i) => {
const base64 = images[getViewImage(view.id)];
if (!base64) {
return;
}
const columnIndex = i + 1; // empieza en columna 1 (segunda visualmente)
const x = margin + columnIndex * (squareSize + spacing);
pdfDoc.addImage(base64, 'JPEG', x, startY, squareSize, squareSize, '', 'FAST');
})
startY += squareSize + 14;
views.slice(2,5).forEach((view,i) => {
const vi = images[getViewImage(view.id)];
if (!vi) {
return;
}
const x = margin + i * (squareSize + spacing);
pdfDoc.addImage(vi, 'JPEG', x, startY, squareSize, squareSize, '', 'FAST');
})
pdfDoc.addPage();
// === Páginas de vistas ===
views.forEach((view, index) => {
const base64 = images[getViewImage(view.id)];
@ -86,12 +154,6 @@
return;
}
// Calcular tamaño de imagen
const imgPixelWidth = 1500;
const imgPixelHeight = 1500;
const pageRatio = pageWidth / pageHeight;
const imgRatio = imgPixelWidth / imgPixelHeight;
let renderWidth: number;
let renderHeight: number;
@ -103,8 +165,7 @@
renderWidth = renderHeight * imgRatio;
}
const x = margin;
const y = (pageHeight - renderHeight) / 2;
const x = margin; const y = (pageHeight - renderHeight) / 2;
pdfDoc.addImage(base64, 'JPEG', x, y, renderWidth, renderHeight, '', 'FAST');
@ -156,16 +217,29 @@
pdfDoc.text(('ACABADOS SELECCIONADOS').toUpperCase(), tx, ty);
let ty2 = ty + 10;
vo.options.forEach((option) => {
let icon: string | null = null;
const label = `${ option . nombreGrupo } : ${ option ? . nombre ?? '' } `;
// precio puede estar en opcion.precio o opcion.price
const priceVal = option?.precio ?? option?.precio ?? null;
ty2 = addOptionLine(pdfDoc,label, priceVal, tx, ty2, textBoxMaxWidth - 6, pf.name, 12, pf.name, 12, { r : 30 , g : 30 , b : 30 } ,icon);
ty2 = cursorY = addOptionBlock(
pdfDoc,
option.nombreGrupo, // Nombre del grupo
option.nombre, // Nombre de la opción
option.consultar ? 'consultar' : (option.precio !== undefined ? option.precio : null),
tx,
ty2,
textBoxMaxWidth,
pf.name, // Font grupo (regular)
9, // Size grupo (pequeño)
pb.name, // Font opción (bold)
11, // Size opción (grande)
pf.name, // Font precio (regular)
11, // Size precio (mediano)
(option.warning || option.consultar) ? advertenciaIconB64 : '',
6,
option.warningMessage
);
})
}
// Nueva página excepto en la última
if (index < views.length - 1 ) pdfDoc . addPage ( ) ;
});
@ -178,7 +252,6 @@
pdfDoc.addImage(fin, finFormat, 0, 0, pageWidth, pageHeight, '', 'FAST');
}
await new Promise(r => setTimeout(r, stepTimeout));
downloadState = 'downloading';
// Guardar con nombre que incluye la tipología
@ -200,7 +273,6 @@
}
// === Función auxiliar para texto multilínea ===
function addTextBlock(
pdfDoc: jsPDF,
@ -238,103 +310,12 @@
}
// === Auxiliar para línea con precio alineado a la derecha ===
function addOptionLine(
pdfDoc : jsPDF,
label: string,
price: number | null,
x: number,
y: number,
maxWidth: number,
font: string,
fontSize: number,
priceFont: string | null = null,
priceFontSize: number | null = null,
priceColor?: { r : number , g : number , b : number } ,
icon : string | null = null,
iconSize = 4,
iconMargin = 1,
) {
// Dibujar icono si se pasa
let iconWidth = 0;
if (icon) {
pdfDoc.addImage(icon, 'PNG', x, y-iconSize+1, iconSize, iconSize);
iconWidth = iconSize + iconMargin;
}
pdfDoc.setFont(font, "normal");
pdfDoc.setFontSize(fontSize);
const priceStr = price == null ? null : '+'+formatPrice(price); // (typeof price === "number" ? `+${ price . toFixed ( 2 )} €` : '+'+String(price));
const labelWidth = pdfDoc.getTextWidth(label);
const priceWidth = priceStr ? pdfDoc.getTextWidth(priceStr) : 0;
// Si label es demasiado largo, recortamos con splitTextToSize y dibujamos en varias líneas
const lineHeight = fontSize * 0.45; // controla aquí el espaciado fino
if (labelWidth + priceWidth + 6 > maxWidth) {
const availableForLabel = maxWidth - (priceWidth ? priceWidth + 6 : 0);
const labelLines = pdfDoc.splitTextToSize(label, availableForLabel);
labelLines.forEach((line: string, i:number) => {
const lineY = y + i * lineHeight;
pdfDoc.text(line, x + iconWidth, lineY);
if (i === labelLines.length - 1 && priceStr) {
if (priceColor) pdfDoc.setTextColor(priceColor.r, priceColor.g, priceColor.b);
if (priceFont && priceFontSize) {
pdfDoc.setFont(priceFont, "normal");
pdfDoc.setFontSize(priceFontSize);
}
pdfDoc.text(priceStr, x + maxWidth - priceWidth, lineY);
pdfDoc.setFont(font, "normal");
pdfDoc.setFontSize(fontSize);
pdfDoc.setTextColor(0, 0, 0);
}
});
return y + labelLines.length * lineHeight;
} else {
// etiqueta en una línea y precio alineado a la derecha si existe
pdfDoc.text(label, x+iconWidth, y);
if (priceStr) {
if (priceColor) pdfDoc.setTextColor(priceColor.r, priceColor.g, priceColor.b);
if (priceFont & & priceFontSize) pdfDoc.setFont(priceFont, "normal"), pdfDoc.setFontSize(priceFontSize);
pdfDoc.text(priceStr, x + maxWidth - priceWidth, y);
// reset font/color
pdfDoc.setFont(font, "normal");
pdfDoc.setFontSize(fontSize);
pdfDoc.setTextColor(0, 0, 0);
}
return y + 6;
}
}
interface Option {
nombreGrupo: string,
nombre: string,
precio?: number|null|string,
consultar: boolean,
warning: boolean,
}
interface ViewOptions {
options: Option[];
hasWarning: boolean;
hasConsult: boolean;
}
function getViewOptions(view: View): ViewOptions {
const options: Option[] = [];
let hasWarning = false;
let hasConsult = false;
let total = 0;
if (view.options.length > 0) {
// Obtener opciones de la vista
@ -354,18 +335,23 @@
if (isConsultar) {
hasConsult = true;
}
if (hasPrice) {
total = total + (optionRef?.amount ?? 0);
}
options.push({
nombreGrupo: group.name || group.id,
nombre: optionValue?.name || currentOptionId || 'No seleccionado',
precio: hasPrice ? optionRef.amount : null,
consultar: isConsultar,
warning: requiresConsult.required
warning: requiresConsult.required,
warningMessage: requiresConsult.required ? requiresConsult.message : undefined
});
});
}
return {
total,
options,
hasWarning,
hasConsult,
@ -373,6 +359,134 @@
}
// === Nueva función para bloques de opciones (3 líneas: grupo, opción, precio) ===
function addOptionBlock(
pdfDoc: jsPDF,
nombreGrupo: string,
nombreOpcion: string,
precio: number | null | string,
x: number,
y: number,
maxWidth: number,
groupFont: string,
groupSize: number,
optionFont: string,
optionSize: number,
priceFont: string,
priceSize: number,
icon: string | null = null,
iconSize: number = 3,
warningMessage?: string,
): number {
let cursorY = y;
// Icono (si existe)
/*
if (icon) {
pdfDoc.addImage(icon, 'PNG', x, cursorY - iconSize + 1, iconSize, iconSize);
}
*/
// LÍNEA 1: Nombre del grupo (pequeño, uppercase, gris)
pdfDoc.setFont(groupFont, "normal");
pdfDoc.setFontSize(groupSize);
pdfDoc.setTextColor(120, 120, 120);
pdfDoc.text(nombreGrupo.toUpperCase(), x, cursorY);
cursorY += groupSize * 0.5;
// LÍNEA 2: Nombre de la opción (más grande, bold, negro)
/*
pdfDoc.setFont(optionFont, "normal");
pdfDoc.setFontSize(optionSize);
pdfDoc.setTextColor(30, 30, 30);
// Dividir en múltiples líneas si es necesario
const optionLines = pdfDoc.splitTextToSize(nombreOpcion, maxWidth - (icon ? iconSize + 2 : 0));
optionLines.forEach((line: string) => {
pdfDoc.text(line, x + (icon ? iconSize + 2 : 0), cursorY);
cursorY += optionSize * 0.45;
});
*/
// LÍNEA 2: Nombre de la opción (más grande, bold, negro) + ICONO A LA DERECHA
pdfDoc.setFont(optionFont, "normal");
pdfDoc.setFontSize(optionSize);
pdfDoc.setTextColor(30, 30, 30);
// Dividir en múltiples líneas si es necesario
const optionLines = pdfDoc.splitTextToSize(nombreOpcion, maxWidth);
const iconYPosition = cursorY; // Guardar posición Y para el icono
optionLines.forEach((line: string) => {
pdfDoc.text(line, x, cursorY);
cursorY += optionSize * 0.45;
});
// Icono a la derecha, alineado con la primera línea de la opción
if (icon) {
const iconX = x + maxWidth - iconSize;
pdfDoc.addImage(icon, 'PNG', iconX, iconYPosition - iconSize + 1, iconSize, iconSize);
}
// LÍNEA 3: Precio (si existe)
// LÍNEA 3: Precio (si existe) - ALINEADO A LA DERECHA
if (precio !== null && precio !== undefined) {
pdfDoc.setFont(priceFont, "normal");
pdfDoc.setFontSize(priceSize);
let priceText: string;
let priceColor: { r : number , g : number , b : number } ;
if (typeof precio === 'number' && precio > 0) {
priceText = '+ ' + formatPrice(precio);
priceColor = { r : 46 , g : 125 , b : 50 } ; // Verde
} else if (precio === 'consultar' || typeof precio === 'string') {
priceText = 'CONSULTAR';
priceColor = { r : 255 , g : 140 , b : 0 } ; // Naranja
} else {
priceText = 'Sin coste adicional';
priceColor = { r : 120 , g : 120 , b : 120 } ; // Gris
}
// Calcular ancho del texto del precio
const priceWidth = pdfDoc.getTextWidth(priceText);
// Posición alineada a la derecha
const priceX = x + maxWidth - priceWidth;
pdfDoc.setTextColor(priceColor.r, priceColor.g, priceColor.b);
pdfDoc.text(priceText, priceX, cursorY);
cursorY += priceSize * 0.45;
}
// LÍNEA 4 (OPCIONAL): Mensaje de advertencia si existe
if (warningMessage && warningMessage.trim()) {
cursorY += 1; // Pequeño espacio antes del mensaje
pdfDoc.setFont(groupFont, "normal");
pdfDoc.setFontSize(8);
pdfDoc.setTextColor(200, 100, 0); // Color naranja/amarillo para advertencia
// Dividir el mensaje en múltiples líneas si es necesario
const warnLines = pdfDoc.splitTextToSize(warningMessage, maxWidth - 5);
warnLines.forEach((line: string) => {
pdfDoc.text('⚠️ ' + line, x, cursorY);
cursorY += 3;
});
}
// Espacio entre opciones
cursorY += 4;
// Reset
pdfDoc.setTextColor(0, 0, 0);
return cursorY;
}
< / script >
< button