|
|
|
|
@ -2,46 +2,379 @@
|
|
|
|
|
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 {jsPDF} from "jspdf";
|
|
|
|
|
import {appState, getViewImage } from "../data/AppState.svelte";
|
|
|
|
|
import {formatPrice} from "../utils/formats";
|
|
|
|
|
import type {View} from "../data/DataConfigTypes";
|
|
|
|
|
|
|
|
|
|
const stepTimeout = 1000;
|
|
|
|
|
const logoProject = dataConfig.sources.logos + dataConfig.logos.project;
|
|
|
|
|
const logoCompany = dataConfig.sources.logos + dataConfig.logos.company;
|
|
|
|
|
console.log(logoProject, logoCompany);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let downloadState : DownloadState = 'idle';
|
|
|
|
|
let isVisible = false;
|
|
|
|
|
let message = '';
|
|
|
|
|
let downloadState: DownloadState = $state('idle');
|
|
|
|
|
let isVisible = $state(false);
|
|
|
|
|
let message = $state('');
|
|
|
|
|
|
|
|
|
|
// Función auxiliar para esperar
|
|
|
|
|
const wait = (ms : number ) => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
|
|
const views = appState.getAllViews();
|
|
|
|
|
const tipologias = ['A1', 'P1', 'P2'];
|
|
|
|
|
|
|
|
|
|
async function generatePdf () {
|
|
|
|
|
// Abrir el modal con el selector
|
|
|
|
|
function openSelector() {
|
|
|
|
|
isVisible = true;
|
|
|
|
|
downloadState = 'idle';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cancelar y cerrar
|
|
|
|
|
function handleCancel() {
|
|
|
|
|
isVisible = false;
|
|
|
|
|
downloadState = 'idle';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generar PDF con la tipología seleccionada
|
|
|
|
|
async function handleTipologiaSelected(tipologia: string) {
|
|
|
|
|
downloadState = 'creating';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await wait(3000);
|
|
|
|
|
const pdfDoc = new jsPDF({
|
|
|
|
|
orientation: 'landscape',
|
|
|
|
|
unit: 'mm',
|
|
|
|
|
format: 'A4',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const pageWidth = pdfDoc.internal.pageSize.getWidth();
|
|
|
|
|
const pageHeight = pdfDoc.internal.pageSize.getHeight();
|
|
|
|
|
const margin = 15;
|
|
|
|
|
|
|
|
|
|
// === Registrar fuentes ===
|
|
|
|
|
const tf = projectFonts['Didot'];
|
|
|
|
|
pdfDoc.addFileToVFS(tf.filename, tf.base64);
|
|
|
|
|
pdfDoc.addFont(tf.filename, tf.name, tf.style);
|
|
|
|
|
|
|
|
|
|
const pf = projectFonts['proxima_nova_regular'];
|
|
|
|
|
pdfDoc.addFileToVFS(pf.filename, pf.base64);
|
|
|
|
|
pdfDoc.addFont(pf.filename, pf.name, pf.style);
|
|
|
|
|
|
|
|
|
|
const pb = projectFonts['proxima_nova_bold'];
|
|
|
|
|
pdfDoc.addFileToVFS(pb.filename, pb.base64);
|
|
|
|
|
pdfDoc.addFont(pb.filename, pb.name, pb.style);
|
|
|
|
|
|
|
|
|
|
const pl = projectFonts['proxima_nova_light'];
|
|
|
|
|
pdfDoc.addFileToVFS(pl.filename, pl.base64);
|
|
|
|
|
pdfDoc.addFont(pl.filename, pl.name, pl.style);
|
|
|
|
|
|
|
|
|
|
// === Portada ===
|
|
|
|
|
const ini = dataPdfImages['pdf_openbox_ini'];
|
|
|
|
|
if (ini) {
|
|
|
|
|
const iniFormat = ini.includes('jpeg') ? 'JPEG' : 'PNG';
|
|
|
|
|
pdfDoc.addImage(ini, iniFormat, 0, 0, pageWidth, pageHeight, '', 'FAST');
|
|
|
|
|
|
|
|
|
|
// Tipo de vivienda (USAR LA TIPOLOGÍA SELECCIONADA)
|
|
|
|
|
pdfDoc.setFontSize(20);
|
|
|
|
|
pdfDoc.setFont(pb.name, "normal");
|
|
|
|
|
pdfDoc.text(tipologia.toUpperCase(), 265, 119);
|
|
|
|
|
|
|
|
|
|
pdfDoc.addPage();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// === Páginas de vistas ===
|
|
|
|
|
views.forEach((view, index) => {
|
|
|
|
|
const base64 = images[getViewImage(view.id)];
|
|
|
|
|
if (!base64) {
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
if (imgRatio > pageRatio) {
|
|
|
|
|
renderHeight = pageHeight - 2 * margin;
|
|
|
|
|
renderWidth = renderHeight * imgRatio;
|
|
|
|
|
} else {
|
|
|
|
|
renderHeight = pageHeight - 2 * margin;
|
|
|
|
|
renderWidth = renderHeight * imgRatio;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const x = margin;
|
|
|
|
|
const y = (pageHeight - renderHeight) / 2;
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
// color
|
|
|
|
|
pdfDoc.setFillColor(237, 237, 226);
|
|
|
|
|
pdfDoc.rect(rectX, rectY, rectWidth, rectHeight, "F");
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
pdfDoc.setFillColor(241, 242, 14);
|
|
|
|
|
pdfDoc.rect(rectX, rectY-20, rectWidth, 20, "F");
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// === TEXTOS ===
|
|
|
|
|
let textStartX = margin + renderWidth + 10;
|
|
|
|
|
let cursorY = margin + 5;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// === Título ===
|
|
|
|
|
pdfDoc.setFontSize(18);
|
|
|
|
|
pdfDoc.setFont(pf.name, "normal");
|
|
|
|
|
pdfDoc.text((view.name).toUpperCase(), textStartX, cursorY);
|
|
|
|
|
|
|
|
|
|
// === Descripción ===
|
|
|
|
|
pdfDoc.setFont(pl.name, "normal");
|
|
|
|
|
if (view.text) {
|
|
|
|
|
cursorY += 8;
|
|
|
|
|
cursorY = addTextBlock(pdfDoc,view.text, textStartX, cursorY, pageWidth - textStartX - margin, pl.name, 12, 5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// === AÑADIR INFORMACIÓN DE OPCIONES ===
|
|
|
|
|
const vo = getViewOptions(view)
|
|
|
|
|
if (vo.options.length > 0) {
|
|
|
|
|
const tx = textStartX;
|
|
|
|
|
const ty = 100;
|
|
|
|
|
const textBoxMaxWidth = rectWidth - 3;
|
|
|
|
|
// === Título ===
|
|
|
|
|
pdfDoc.setFontSize(14);
|
|
|
|
|
pdfDoc.setFont(pf.name, "normal");
|
|
|
|
|
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);
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Nueva página excepto en la última
|
|
|
|
|
if (index < views.length - 1) pdfDoc.addPage();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// === Página final ===
|
|
|
|
|
pdfDoc.addPage();
|
|
|
|
|
const fin = dataPdfImages['pdf_openbox_fin'];
|
|
|
|
|
if (fin) {
|
|
|
|
|
const finFormat = fin.includes('jpeg') ? 'JPEG' : 'PNG';
|
|
|
|
|
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
|
|
|
|
|
pdfDoc.save(`mi_OPENBOX_${tipologia}.pdf`);
|
|
|
|
|
|
|
|
|
|
downloadState = 'success';
|
|
|
|
|
|
|
|
|
|
// Cerrar modal después de 2 segundos
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
isVisible = false;
|
|
|
|
|
downloadState = 'idle';
|
|
|
|
|
}, 2000);
|
|
|
|
|
|
|
|
|
|
} catch (err : any) {
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
downloadState = 'error';
|
|
|
|
|
message = err?.message;
|
|
|
|
|
console.log(err);
|
|
|
|
|
} finally {
|
|
|
|
|
await wait(1000);
|
|
|
|
|
isVisible = false;
|
|
|
|
|
downloadState = 'idle';
|
|
|
|
|
message = err?.message || 'Error desconocido al generar el PDF';
|
|
|
|
|
console.error(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// === 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 : '+'+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
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
if (view.options.length > 0) {
|
|
|
|
|
// Obtener opciones de la vista
|
|
|
|
|
view.options.forEach((group) => {
|
|
|
|
|
const currentOptionId = appState.getCurrentOption(view.id, group.id);
|
|
|
|
|
const optionValue = currentOptionId ? appState.getOptionValue(currentOptionId) : null;
|
|
|
|
|
const optionRef = group.values.find(v => v.id === currentOptionId);
|
|
|
|
|
const requiresConsult = currentOptionId ? appState.requiresConsultation(view.id, group.id, currentOptionId) : {required: false};
|
|
|
|
|
|
|
|
|
|
const hasPrice = optionRef?.amount && typeof optionRef.amount === 'number' && optionRef.amount > 0;
|
|
|
|
|
const isConsultar = optionRef?.amount === 'consultar';
|
|
|
|
|
|
|
|
|
|
if (requiresConsult.required) {
|
|
|
|
|
hasWarning = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isConsultar) {
|
|
|
|
|
hasConsult = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<button type="button"
|
|
|
|
|
options.push({
|
|
|
|
|
nombreGrupo: group.name || group.id,
|
|
|
|
|
nombre: optionValue?.name || currentOptionId || 'No seleccionado',
|
|
|
|
|
precio: hasPrice ? optionRef.amount : null,
|
|
|
|
|
consultar: isConsultar,
|
|
|
|
|
warning: requiresConsult.required
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
options,
|
|
|
|
|
hasWarning,
|
|
|
|
|
hasConsult,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="px-3 lg:px-4 py-2 rounded-md text-base font-medium transition-colors duration-200 text-white bg-green-900 hover:bg-green-700 cursor-pointer"
|
|
|
|
|
onclick={() => generatePdf()}>
|
|
|
|
|
onclick={openSelector}
|
|
|
|
|
>
|
|
|
|
|
Descargar Selección
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
@ -49,8 +382,10 @@
|
|
|
|
|
{isVisible}
|
|
|
|
|
{message}
|
|
|
|
|
{downloadState}
|
|
|
|
|
{tipologias}
|
|
|
|
|
logoCompany={logoCompany}
|
|
|
|
|
logoProject={logoProject}
|
|
|
|
|
|
|
|
|
|
text={'testo'}
|
|
|
|
|
text="Descarga tu Selección"
|
|
|
|
|
onTipologiaSelected={handleTipologiaSelected}
|
|
|
|
|
onCancel={handleCancel}
|
|
|
|
|
/>
|