@ -7,6 +7,10 @@
import { estanciasImagen , getSelecciones , imagesNames } from "../stores/configurador.svelte.js";
import { proyecto } from "../../data/data_proyecto";
import { projectFonts } from "../../data/data_fonts";
import { formatearPrecio } from "../utils/formaters.js";
import { warningIconB64 } from "../../data/data_icons_b64";
import * as JSPdf from "jspdf";
let pdfEstado : PdfEstado = 'idle';
@ -16,6 +20,8 @@
const stepTimeout = 1000;
const textNotify = 'Consulte si las opciones seleccionadas estan disponibles para la tipología de su vivienda.';
const textError = 'El pavimento es diferente al seleccionado en el salón, que es el que determina el pavimento de la vivienda.'
/*
async function generatePdf() {
@ -316,6 +322,7 @@
const textLightFont = pl.name;
const textBoldFont = pb.name;
// === Portada ===
const ini = pdfImages['pdf_quaddro_ini'];
if (ini) {
@ -324,46 +331,13 @@
pdfDoc.addPage();
}
// === Función auxiliar para texto multilínea ===
function addTextBlock(
text: string | string[] | null | undefined,
x: number,
yStart: number,
maxWidth: number,
font: string,
fontSize: number,
lineHeight: number
) {
if (!text) return yStart;
let safeText: string = Array.isArray(text) ? text.join("\n") : String(text);
if (safeText.trim().length === 0) return yStart;
pdfDoc.setFont(font, "normal");
pdfDoc.setFontSize(fontSize);
const paragraphs = safeText.split(/\r?\n/);
let cursorY = yStart;
for (let p = 0; p < paragraphs.length ; p ++) {
const paragraph = paragraphs[p].trim();
if (!paragraph) {
cursorY += lineHeight;
continue;
}
const lines = pdfDoc.splitTextToSize(paragraph, maxWidth);
for (let i = 0; i < lines.length ; i ++) {
pdfDoc.text(lines[i], x, cursorY + i * lineHeight);
}
cursorY += lines.length * lineHeight + lineHeight;
}
return cursorY;
}
// === Estancias ===
estancias.forEach((entrada, index) => {
const base64 = renderImages[entrada.imagen];
if (!base64) {
console.log('no existe la imagen ', entrada.imagen);
return;
}
@ -389,6 +363,17 @@
pdfDoc.addImage(base64, 'JPEG', x, y, renderWidth, renderHeight, '', 'FAST');
// Posiciones del rectángulo
const rectX = renderWidth + margin + 10;
const rectY = margin + 90;
const rectWidth = pageWidth - rectX - margin;
const rectHeight = pageHeight - rectY - margin;
// Dibujar fondo gris claro
pdfDoc.setFillColor(246, 246, 246);
pdfDoc.rect(rectX, rectY, rectWidth, rectHeight, "F");
//----------------------------------------
// TEXTO
//----------------------------------------
@ -396,9 +381,10 @@
const estancia = proyecto.estancias[estanciaId];
const seleccion = selecciones?.[estanciaId] ?? {} ;
cons t textStartX = margin + renderWidth + 10;
le t textStartX = margin + renderWidth + 10;
let cursorY = margin + 5;
// === Título ===
pdfDoc.setFontSize(18);
pdfDoc.setFont(titleFont, "normal");
@ -408,64 +394,77 @@
pdfDoc.setFont(textLightFont, "normal");
if (estancia.texto) {
cursorY += 8;
cursorY = addTextBlock(estancia.texto, textStartX, cursorY, pageWidth - textStartX - margin, textLightFont, 12, 5);
cursorY = addTextBlock(pdfDoc, estancia.texto, textStartX, cursorY, pageWidth - textStartX - margin, textLightFont, 12, 5);
}
// === Opciones y Add-ons al margen inferior ===
const opciones = seleccion.opciones ?? [];
const adds = seleccion.adds ?? [];
const lineHeightOpc = 5;
const lineHeightAdd = 5;
let totalOpcHeight = 0;
opciones.forEach((grupo: Record< string , any > ) => {
const nombreGrupo = Object.keys(grupo)[0];
const opcion = grupo[nombreGrupo];
const lines = pdfDoc.splitTextToSize(`${ nombreGrupo } : ${ opcion . nombre } `, pageWidth - textStartX - margin - 5);
totalOpcHeight += lines.length * lineHeightOpc + 2;
});
// === Opciones y Add-ons después del texto ===
textStartX += 5;
cursorY = 115; // separación fija después de la descripción
let totalAddsHeight = 0;
adds.forEach((add: Record< string , any > ) => {
const nombreAdd = Object.keys(add)[0];
const lines = pdfDoc.splitTextToSize(nombreAdd, pageWidth - textStartX - margin - 5);
totalAddsHeight += lines.length * lineHeightAdd + 2;
});
let cursorBoxY = cursorY;
let textBoxX = textStartX;
const textBoxMaxWidth = rectWidth - 3;
let cursorBottomY = pageHeight - margin - totalOpcHeight - totalAddsHeight - 10;
const opciones = seleccion.opciones ?? [];
const adds = seleccion.adds ?? [];
const lineHeightOpc = 3;
const lineHeightAdd = 3;
let notify = false;
// === Opciones seleccionadas ===
if (opciones.length > 0) {
pdfDoc.setFont(textFont, "bold");
pdfDoc.setFont(textBoldFont, "normal ");
pdfDoc.setFontSize(11);
pdfDoc.text("Opciones seleccionadas:", textStartX, cursorBottom Y);
pdfDoc.text("Acabados seleccionados:".toUpperCase(), textStartX, cursor Y);
pdfDoc.setFont(textFont, "normal");
pdfDoc.setFontSize(10);
cursorBottomY += 5;
opciones.forEach((grupo: Record< string , any > ) => {
pdfDoc.setFontSize(11);
cursorBoxY += 7;
for (const grupo of opciones) {
const nombreGrupo = Object.keys(grupo)[0];
const opcion = grupo[nombreGrupo];
cursorBottomY = addTextBlock(`${ nombreGrupo } : ${ opcion . nombre } `, textStartX + 5, cursorBottomY, pageWidth - textStartX - margin - 5, textFont, 10, lineHeightOpc);
});
const label = `${ nombreGrupo } : ${ opcion ? . nombre ?? '' } `;
// precio puede estar en opcion.precio o opcion.price
const priceVal = opcion?.precio ?? opcion?.price ?? null;
cursorBoxY = addOptionLine(pdfDoc,label, priceVal, textBoxX, cursorBoxY, textBoxMaxWidth - 6, textFont, 10, textFont, 10, { r : 30 , g : 30 , b : 30 } );
}
}
// === Add-ons seleccionados ===
if (adds.length > 0) {
cursorBottomY += 5;
pdfDoc.setFont(textFont, "bold ");
cursorBoxY += 10; // pequeña separación entre opciones y add-ons
pdfDoc.setFont(textBoldFont, "normal ");
pdfDoc.setFontSize(11);
pdfDoc.text("Add-ons seleccionados:", textStartX, cursorBottom Y);
pdfDoc.text("Opciones seleccionadas:".toUpperCase(), textStartX, cursorBox Y);
pdfDoc.setFont(textFont, "normal");
pdfDoc.setFontSize(10);
cursorBottomY += 5;
adds.forEach((add: Record< string , any > ) => {
const nombreAdd = Object.keys(add)[0];
cursorBottomY = addTextBlock(nombreAdd, textStartX + 5, cursorBottomY, pageWidth - textStartX - margin - 5, textFont, 10, lineHeightAdd);
});
pdfDoc.setFontSize(11);
cursorBoxY += 7;
for (const add of adds) {
const info = add[Object.keys(add)[0]];
const priceVal = info?.precio ?? info?.price ?? null;
let label = info.nombre;
let icon : string | null = info.filtro ? warningIconB64 : null;
if (info.filtro) {
notify = true;
}
cursorBoxY = addOptionLine(pdfDoc,label, priceVal, textBoxX, cursorBoxY, textBoxMaxWidth - 6, textFont, 10, textFont, 10, { r : 30 , g : 30 , b : 30 } ,icon);
}
}
if (notify) {
addOptionLine(pdfDoc,textNotify,null,15,200,1000,textFont,9,null,null,undefined,warningIconB64);
}
// Nueva página excepto en la última
if (index < estancias.length - 1 ) pdfDoc . addPage ( ) ;
});
@ -496,6 +495,123 @@
}
// === Función auxiliar para texto multilínea ===
function addTextBlock(
pdfDoc : jsPDF,
text: string | string[] | null | undefined,
x: number,
yStart: number,
maxWidth: number,
font: string,
fontSize: number,
lineHeight: number
) {
if (!text) return yStart;
let safeText: string = Array.isArray(text) ? text.join("\n") : String(text);
if (safeText.trim().length === 0) return yStart;
pdfDoc.setFont(font, "normal");
pdfDoc.setFontSize(fontSize);
const paragraphs = safeText.split(/\r?\n/);
let cursorY = yStart;
for (let p = 0; p < paragraphs.length ; p ++) {
const paragraph = paragraphs[p].trim();
if (!paragraph) {
cursorY += lineHeight;
continue;
}
const lines = pdfDoc.splitTextToSize(paragraph, maxWidth);
for (let i = 0; i < lines.length ; i ++) {
pdfDoc.text(lines[i], x, cursorY + i * lineHeight);
}
cursorY += lines.length * lineHeight + lineHeight;
}
return cursorY;
}
// === 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 : '+'+formatearPrecio(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
if (labelWidth + priceWidth + 6 > maxWidth) {
// dividir label en líneas que dejen espacio para precio en la última línea
const availableForLabel = maxWidth - (priceWidth ? priceWidth + 6 : 0);
const labelLines = pdfDoc.splitTextToSize(label, availableForLabel);
for (let i = 0; i < labelLines.length ; i ++) {
const line = labelLines[i];
const lineY = y + i * (fontSize * 0.6 + 2);
pdfDoc.text(line, x, lineY);
// si es la última línea y hay precio, pintarlo a la derecha
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);
// reset font/color
pdfDoc.setFont(font, "normal");
pdfDoc.setFontSize(fontSize);
pdfDoc.setTextColor(0, 0, 0);
}
}
return y + labelLines.length * (fontSize * 0.6 + 2);
} 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;
}
}
< / script >