puliendo pdfGenerator

master
dev 1 year ago
parent cc561adf09
commit 819851dac7

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,7 @@
export const warningIconB64 = 'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABlUlEQVR4nOyVvWtTURjGf+feEOONix9DwEkIiB/EQcE5omCIIKg4OYqDOOomCIpbof07QjukTSDQoXOXtkNKaZZOpe3WoUnTpslb7hfNx7m594ZmKPSBAznvOe/ze/Oec7gGU9Y1IFSJqBulxE0sZpxJi5/qM8eXCsDiH/Dd+90CfkVJU1E2SZksJnXghhc6xSSnCmyH5UY7A5O5PnNbSc68doUo9B9IhdfAsjNJ3XeD7V0/u6CK1CYGyAoJmqwDT+1596XrZa6+9bdskeGZekEnyGN8i5r88M1d51vuuNAj9ryDjwuQGneA32MLwOnBH1niXmwAXf4Dd0MBcBvF31gAWeQJwteRYjuHztDom5TJ6Rb0D81gVrdmbHwJKtT0rvKrUavh6qt8BN7oXCT90BkBykuFD8PBgWsqJZJYbAJZnUPv+QKIYKx9CoLskOaxytP2A4NtsHgfZG7LqI+9kbYecMQ7YF4PEBooh57Spp8chAHa9Gj0B0ZeslTJIH2PK44UdVVkf6LcSXX1P5lTB5wHAAD//0kDVOGS5cmMAAAAAElFTkSuQmCC';

@ -132,7 +132,7 @@ export const proyecto : Proyecto = {
nombre : 'Salón',
imagen : null,
adds : null,
texto : 'Un espacio diseñado para disfrutar y recibir, con abundante luz natural que inunda cada rincón.\nSus suelos elegantes se pueden personalizar según tu gusto, eligiendo entre las opciones disponibles para combinar estilo y confort.\nLos acabados modernos y la distribución funcional hacen del salón un ambiente acogedor, ideal tanto para el día a día como para momentos de entretenimiento y reuniones familiares.',
texto : 'Un espacio diseñado para disfrutar y recibir, con abundante luz natural que inunda cada rincón.\nSus suelos elegantes se pueden personalizar según tu gusto, eligiendo entre las opciones disponibles para combinar estilo y confort.',
opciones: [
{
nombre : "Pavimento",
@ -157,6 +157,8 @@ export const proyecto : Proyecto = {
id : 'ccn',
nombre : 'Cocina',
imagen : null,
texto : 'La cocina combina funcionalidad y diseño en un entorno moderno, luminoso y ordenado. Cada elemento ha sido dispuesto para ofrecer comodidad y armonía visual, creando un lugar donde cocinar, conversar y disfrutar se integran con naturalidad.\n\n' +
'Se puede personalizar las encimeras y los acabados del mobiliario, eligiendo entre materiales y tonalidades que reflejen tu manera de entender el hogar. Cada decisión define el carácter de un espacio pensado para inspirarte cada día.',
adds : {
'led' : {
nombre : 'Tira led encimera',
@ -196,7 +198,7 @@ export const proyecto : Proyecto = {
]
},
{
nombre : 'Encimeras',
nombre : 'Encimera',
opciones: [
{
nombre : 'Enc. Blanca',
@ -248,6 +250,8 @@ export const proyecto : Proyecto = {
id : 'bnp',
nombre : 'Baño Principal',
imagen : null,
texto : 'El baño principal está concebido como un espacio de bienestar y desconexión, donde los materiales nobles y los tonos suaves crean una sensación de calma. La pureza de las líneas y el equilibrio de sus formas lo convierten en un lugar íntimo y elegante.\n\n' +
'Es posible configurar los revestimientos, acabados del mobiliario según tus preferencias, logrando una estancia que transmite tu estilo, ya sea natural, contemporáneo o clásico, sin renunciar al confort.',
adds : {
'kit' : {
nombre : 'Kit Sanitario',
@ -296,7 +300,7 @@ export const proyecto : Proyecto = {
]
},
{
nombre : "Mobiliario Baño",
nombre : "Mobiliario",
opciones: [
{
nombre : 'Mueble Blanco',
@ -317,6 +321,8 @@ export const proyecto : Proyecto = {
'bns' : {
id : 'bns',
nombre : 'Baño Secundario',
'texto': 'El baño secundario combina diseño y funcionalidad con una cuidada selección de materiales que aportan durabilidad y elegancia. Su distribución optimiza el espacio y la iluminación realza cada superficie, creando una sensación de amplitud y orden.\n\n' +
'Puedes elegir los revestimientos así como instalar ducha o bañera asi como otras opciones, adaptando su estética al conjunto de la vivienda y manteniendo la coherencia visual con el resto de los espacios.',
adds : {
'kit' : {
nombre : 'Kit Sanitario',
@ -360,7 +366,7 @@ export const proyecto : Proyecto = {
material: 'ducha',
},
{
nombre : 'Con Bañera',
nombre : 'Bañera',
value : 'diba',
material: 'banera',
precio : +1924.25
@ -369,7 +375,7 @@ export const proyecto : Proyecto = {
},
{
nombre : "Mobiliario Baño",
nombre : "Mobiliario",
opciones: [
{
nombre : 'Mueble Blanco',

@ -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] ?? {};
const textStartX = margin + renderWidth + 10;
let 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, cursorBottomY);
pdfDoc.text("Acabados seleccionados:".toUpperCase(), textStartX, cursorY);
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, cursorBottomY);
pdfDoc.text("Opciones seleccionadas:".toUpperCase(), textStartX, cursorBoxY);
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>

Loading…
Cancel
Save

Powered by TurnKey Linux.