dev 8 months ago
parent c40a0ae55f
commit b546074199

@ -2,13 +2,33 @@
import {dataConfig} from "../data/DataConfig.js";
import type {DownloadState} from "../data/DownloadTypes";
import PoDownload from "../cmps/PoDownload.svelte";
import { projectFonts } from "../data/DataFonts";
import { dataPdfImages } from "../data/DataPdfImagesB64";
import { images } from "../data/DataImagesB64";
import {projectFonts} from "../data/DataFonts";
import {dataPdfImages} from "../data/DataPdfImagesB64";
import {images} from "../data/DataImagesB64";
import {jsPDF} from "jspdf";
import {appState, getViewImage } from "../data/AppState.svelte";
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');
@ -141,7 +202,7 @@
pdfDoc.setFont(pl.name, "normal");
if (view.text) {
cursorY += 8;
cursorY = addTextBlock(pdfDoc,view.text, textStartX, cursorY, pageWidth - textStartX - margin, pl.name, 12, 5);
cursorY = addTextBlock(pdfDoc, view.text, textStartX, cursorY, pageWidth - textStartX - margin, pl.name, 12, 5);
}
// === AÑADIR INFORMACIÓN DE OPCIONES ===
@ -154,19 +215,32 @@
pdfDoc.setFontSize(14);
pdfDoc.setFont(pf.name, "normal");
pdfDoc.text(('ACABADOS SELECCIONADOS').toUpperCase(), tx, ty);
let ty2 = ty+10;
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
// 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,10 +273,9 @@
}
// === Función auxiliar para texto multilínea ===
function addTextBlock(
pdfDoc : jsPDF,
pdfDoc: jsPDF,
text: string | string[] | null | undefined,
x: number,
yStart: number,
@ -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 {
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

@ -25,7 +25,14 @@
onCancel: () => void;
}>();
let selectedTipologia = $state(tipologias[0] || 'A1');
let selectedTipologia = $state('A1');
$effect(() => {
// Si no hay nada seleccionado o lo que hay ya no existe en la lista...
if (!selectedTipologia || !tipologias.includes(selectedTipologia)) {
selectedTipologia = tipologias[0] || 'A1';
}
});
let stateText = $derived(
downloadState === 'creating' ? 'Creando documento PDF' :
@ -63,7 +70,7 @@
<img src={logoCompany} alt="logo empresa" class="w-40 block leading-none" />
{/if}
<img src={logoProject} alt="logo proyecto" class="w-92 block" />
<img src={logoProject} alt="logo proyecto" class="w-96 block" />
<h2 class="text-2xl font-semibold text-gray-600 tracking-wide leading-tight mt-1">
{text}

@ -322,7 +322,7 @@ export const dataConfig : DataConfig = {
salon: {
id: 'salon',
name: 'Salón',
text: 'Arquitectura que respira y se adapta a tu estilo. Disfruta de un salón bañado por la luz natural gracias a sus amplios ventanales, donde el diseño de vanguardia se une al confort absoluto del suelo radiante-refrescante y el control inteligente de tu hogar mediante domótica avanzada',
text: 'Arquitectura que respira y se adapta a tu estilo. Disfruta de un salón bañado por la luz natural gracias a sus amplios ventanales, donde el diseño de vanguardia se une al confort absoluto del suelo radiante-refrescante.',
help: '',
options: [],
hotspots: [
@ -593,6 +593,8 @@ export const dataConfig : DataConfig = {
id: 'enpe',
amount: textWithCost,
warn: 'Coste según tipología y personalización, se contratará con Studio56',
rules: { views: { cocina: { mobiliario: { forbid :['mnaf','mvaf','mbmb'] } } } },
},
],
},

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save

Powered by TurnKey Linux.