|
|
|
@ -1,280 +1,297 @@
|
|
|
|
<script lang="ts">
|
|
|
|
<script lang="ts">
|
|
|
|
import {dataConfig} from "../data/DataConfig.js";
|
|
|
|
import { dataConfig } from "../data/DataConfig.js";
|
|
|
|
import type {DownloadState} from "../data/DownloadTypes";
|
|
|
|
import type { DownloadState } from "../data/DownloadTypes";
|
|
|
|
import PoDownload from "../cmps/PoDownload.svelte";
|
|
|
|
import PoDownload from "../cmps/PoDownload.svelte";
|
|
|
|
import {projectFonts} from "../data/DataFonts";
|
|
|
|
import { projectFonts } from "../data/DataFonts";
|
|
|
|
import {dataPdfImages} from "../data/DataPdfImagesB64";
|
|
|
|
import { dataPdfImages } from "../data/DataPdfImagesB64";
|
|
|
|
import {images} from "../data/DataImagesB64";
|
|
|
|
import { images } from "../data/DataImagesB64";
|
|
|
|
import {jsPDF} from "jspdf";
|
|
|
|
import { jsPDF } from "jspdf";
|
|
|
|
import {appState, getViewImage} from "../data/AppState.svelte";
|
|
|
|
import { appState, getViewImage } from "../data/AppState.svelte";
|
|
|
|
import {formatPrice} from "../utils/formats";
|
|
|
|
import { formatPrice } from "../utils/formats";
|
|
|
|
import type {View} from "../data/DataConfigTypes";
|
|
|
|
import type { View } from "../data/DataConfigTypes";
|
|
|
|
import {advertenciaIconB64} from "../data/DataIconsB64"
|
|
|
|
import { advertenciaIconB64 } from "../data/DataIconsB64"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
// TIPOS
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
interface Option {
|
|
|
|
interface Option {
|
|
|
|
nombreGrupo: string,
|
|
|
|
nombreGrupo: string;
|
|
|
|
nombre: string,
|
|
|
|
nombre: string;
|
|
|
|
precio?: number | null | string,
|
|
|
|
precio?: number | null | string;
|
|
|
|
consultar: boolean,
|
|
|
|
consultar: boolean;
|
|
|
|
warning: boolean,
|
|
|
|
warning: boolean;
|
|
|
|
warningMessage?: string,
|
|
|
|
warningMessage?: string;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface ViewOptions {
|
|
|
|
interface ViewOptions {
|
|
|
|
options: Option[];
|
|
|
|
options: Option[];
|
|
|
|
total : number | null;
|
|
|
|
total: number;
|
|
|
|
hasWarning: boolean;
|
|
|
|
hasWarning: boolean;
|
|
|
|
hasConsult: boolean;
|
|
|
|
hasConsult: boolean;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interface PDFConfig {
|
|
|
|
|
|
|
|
pageWidth: number;
|
|
|
|
|
|
|
|
pageHeight: number;
|
|
|
|
|
|
|
|
margin: number;
|
|
|
|
|
|
|
|
fonts: {
|
|
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
|
|
regular: string;
|
|
|
|
|
|
|
|
bold: string;
|
|
|
|
|
|
|
|
light: string;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interface TextStyles {
|
|
|
|
|
|
|
|
group: { font: string; size: number };
|
|
|
|
|
|
|
|
option: { font: string; size: number };
|
|
|
|
|
|
|
|
price: { font: string; size: number };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const stepTimeout = 1000;
|
|
|
|
// ============================================================================
|
|
|
|
const logoProject = dataConfig.sources.logos + dataConfig.logos.project;
|
|
|
|
// ESTADO
|
|
|
|
const logoCompany = dataConfig.sources.logos + dataConfig.logos.company;
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const TIPOLOGIAS = ['A1', 'P1', 'P2'];
|
|
|
|
|
|
|
|
const STEP_TIMEOUT = 1000;
|
|
|
|
|
|
|
|
|
|
|
|
let downloadState: DownloadState = $state('idle');
|
|
|
|
let downloadState: DownloadState = $state('idle');
|
|
|
|
let isVisible = $state(false);
|
|
|
|
let isVisible = $state(false);
|
|
|
|
let message = $state('');
|
|
|
|
let message = $state('');
|
|
|
|
|
|
|
|
|
|
|
|
const views = appState.getAllViews();
|
|
|
|
const views = appState.getAllViews();
|
|
|
|
const tipologias = ['A1', 'P1', 'P2'];
|
|
|
|
const logoProject = dataConfig.sources.logos + dataConfig.logos.project;
|
|
|
|
|
|
|
|
const logoCompany = dataConfig.sources.logos + dataConfig.logos.company;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
// HANDLERS
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
// Abrir el modal con el selector
|
|
|
|
|
|
|
|
function openSelector() {
|
|
|
|
function openSelector() {
|
|
|
|
isVisible = true;
|
|
|
|
isVisible = true;
|
|
|
|
downloadState = 'idle';
|
|
|
|
downloadState = 'idle';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Cancelar y cerrar
|
|
|
|
|
|
|
|
function handleCancel() {
|
|
|
|
function handleCancel() {
|
|
|
|
isVisible = false;
|
|
|
|
isVisible = false;
|
|
|
|
downloadState = 'idle';
|
|
|
|
downloadState = 'idle';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Generar PDF con la tipología seleccionada
|
|
|
|
|
|
|
|
async function handleTipologiaSelected(tipologia: string) {
|
|
|
|
async function handleTipologiaSelected(tipologia: string) {
|
|
|
|
downloadState = 'creating';
|
|
|
|
downloadState = 'creating';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const pdfDoc = new jsPDF({
|
|
|
|
const pdfDoc = createPDFDocument();
|
|
|
|
orientation: 'landscape',
|
|
|
|
const config = getPDFConfig(pdfDoc);
|
|
|
|
unit: 'mm',
|
|
|
|
|
|
|
|
format: 'A4',
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const pageWidth = pdfDoc.internal.pageSize.getWidth();
|
|
|
|
registerFonts(pdfDoc);
|
|
|
|
const pageHeight = pdfDoc.internal.pageSize.getHeight();
|
|
|
|
await generatePDFContent(pdfDoc, config, tipologia);
|
|
|
|
const margin = 15;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// === Registrar fuentes ===
|
|
|
|
downloadState = 'downloading';
|
|
|
|
const tf = projectFonts['Didot'];
|
|
|
|
pdfDoc.save(`OPENBOX_${tipologia}_${Date.now()}.pdf`);
|
|
|
|
pdfDoc.addFileToVFS(tf.filename, tf.base64);
|
|
|
|
downloadState = 'success';
|
|
|
|
pdfDoc.addFont(tf.filename, tf.name, tf.style);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const pf = projectFonts['proxima_nova_regular'];
|
|
|
|
setTimeout(() => {
|
|
|
|
pdfDoc.addFileToVFS(pf.filename, pf.base64);
|
|
|
|
isVisible = false;
|
|
|
|
pdfDoc.addFont(pf.filename, pf.name, pf.style);
|
|
|
|
downloadState = 'idle';
|
|
|
|
|
|
|
|
}, 2000);
|
|
|
|
|
|
|
|
|
|
|
|
const pb = projectFonts['proxima_nova_bold'];
|
|
|
|
} catch (err: any) {
|
|
|
|
pdfDoc.addFileToVFS(pb.filename, pb.base64);
|
|
|
|
downloadState = 'error';
|
|
|
|
pdfDoc.addFont(pb.filename, pb.name, pb.style);
|
|
|
|
message = err?.message || 'Error desconocido al generar el PDF';
|
|
|
|
|
|
|
|
console.error('PDF Generation Error:', err);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const pl = projectFonts['proxima_nova_light'];
|
|
|
|
// ============================================================================
|
|
|
|
pdfDoc.addFileToVFS(pl.filename, pl.base64);
|
|
|
|
// CREACIÓN DE PDF
|
|
|
|
pdfDoc.addFont(pl.filename, pl.name, pl.style);
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createPDFDocument(): jsPDF {
|
|
|
|
|
|
|
|
return new jsPDF({
|
|
|
|
|
|
|
|
orientation: 'landscape',
|
|
|
|
|
|
|
|
unit: 'mm',
|
|
|
|
|
|
|
|
format: 'A4',
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Calcular tamaño de imagen
|
|
|
|
function getPDFConfig(pdfDoc: jsPDF): PDFConfig {
|
|
|
|
const imgPixelWidth = 1500;
|
|
|
|
return {
|
|
|
|
const imgPixelHeight = 1500;
|
|
|
|
pageWidth: pdfDoc.internal.pageSize.getWidth(),
|
|
|
|
const pageRatio = pageWidth / pageHeight;
|
|
|
|
pageHeight: pdfDoc.internal.pageSize.getHeight(),
|
|
|
|
const imgRatio = imgPixelWidth / imgPixelHeight;
|
|
|
|
margin: 15,
|
|
|
|
|
|
|
|
fonts: {
|
|
|
|
|
|
|
|
title: projectFonts['Didot'].name,
|
|
|
|
|
|
|
|
regular: projectFonts['proxima_nova_regular'].name,
|
|
|
|
|
|
|
|
bold: projectFonts['proxima_nova_bold'].name,
|
|
|
|
|
|
|
|
light: projectFonts['proxima_nova_light'].name,
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function registerFonts(pdfDoc: jsPDF) {
|
|
|
|
|
|
|
|
const fontsToRegister = [
|
|
|
|
|
|
|
|
projectFonts['Didot'],
|
|
|
|
|
|
|
|
projectFonts['proxima_nova_regular'],
|
|
|
|
|
|
|
|
projectFonts['proxima_nova_bold'],
|
|
|
|
|
|
|
|
projectFonts['proxima_nova_light']
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fontsToRegister.forEach(font => {
|
|
|
|
|
|
|
|
pdfDoc.addFileToVFS(font.filename, font.base64);
|
|
|
|
|
|
|
|
pdfDoc.addFont(font.filename, font.name, font.style);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// === Portada ===
|
|
|
|
async function generatePDFContent(pdfDoc: jsPDF, config: PDFConfig, tipologia: string) {
|
|
|
|
const ini = dataPdfImages['pdf_openbox_ini'];
|
|
|
|
addCoverPage(pdfDoc, config, tipologia);
|
|
|
|
if (ini) {
|
|
|
|
addPresentationPage(pdfDoc, config);
|
|
|
|
const iniFormat = ini.includes('jpeg') ? 'JPEG' : 'PNG';
|
|
|
|
addViewPages(pdfDoc, config);
|
|
|
|
pdfDoc.addImage(ini, iniFormat, 0, 0, pageWidth, pageHeight, '', 'FAST');
|
|
|
|
addFinalPage(pdfDoc, config);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Tipo de vivienda (USAR LA TIPOLOGÍA SELECCIONADA)
|
|
|
|
// ============================================================================
|
|
|
|
pdfDoc.setFontSize(20);
|
|
|
|
// PÁGINAS DEL PDF
|
|
|
|
pdfDoc.setFont(pb.name, "normal");
|
|
|
|
// ============================================================================
|
|
|
|
pdfDoc.text(tipologia.toUpperCase(), 265, 119);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pdfDoc.addPage();
|
|
|
|
function addCoverPage(pdfDoc: jsPDF, config: PDFConfig, tipologia: string) {
|
|
|
|
}
|
|
|
|
const coverImage = dataPdfImages['pdf_openbox_ini'];
|
|
|
|
|
|
|
|
if (!coverImage) return;
|
|
|
|
|
|
|
|
|
|
|
|
// == Presentacion ===
|
|
|
|
const format = coverImage.includes('jpeg') ? 'JPEG' : 'PNG';
|
|
|
|
|
|
|
|
pdfDoc.addImage(coverImage, format, 0, 0, config.pageWidth, config.pageHeight, '', 'FAST');
|
|
|
|
|
|
|
|
|
|
|
|
// 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)];
|
|
|
|
|
|
|
|
if (!base64) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// === Página final ===
|
|
|
|
pdfDoc.setFontSize(20);
|
|
|
|
pdfDoc.addPage();
|
|
|
|
pdfDoc.setFont(config.fonts.bold, "normal");
|
|
|
|
const fin = dataPdfImages['pdf_openbox_fin'];
|
|
|
|
pdfDoc.text(tipologia.toUpperCase(), 265, 119);
|
|
|
|
if (fin) {
|
|
|
|
|
|
|
|
const finFormat = fin.includes('jpeg') ? 'JPEG' : 'PNG';
|
|
|
|
|
|
|
|
pdfDoc.addImage(fin, finFormat, 0, 0, pageWidth, pageHeight, '', 'FAST');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
downloadState = 'downloading';
|
|
|
|
pdfDoc.addPage();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Guardar con nombre que incluye la tipología
|
|
|
|
function addPresentationPage(pdfDoc: jsPDF, config: PDFConfig) {
|
|
|
|
pdfDoc.save(`mi_OPENBOX_${tipologia}.pdf`);
|
|
|
|
const cols = 3;
|
|
|
|
|
|
|
|
const spacing = 10;
|
|
|
|
|
|
|
|
const squareSize = (config.pageWidth - config.margin * 2 - spacing * (cols - 1)) / cols;
|
|
|
|
|
|
|
|
let startY = 15;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Imagen de presentación
|
|
|
|
|
|
|
|
const preImage = dataPdfImages['pdf_openbox_pre'];
|
|
|
|
|
|
|
|
if (preImage) {
|
|
|
|
|
|
|
|
const format = preImage.includes('jpeg') ? 'JPEG' : 'PNG';
|
|
|
|
|
|
|
|
pdfDoc.addImage(preImage, format, config.margin, startY, squareSize, squareSize, '', 'FAST');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
downloadState = 'success';
|
|
|
|
// Primera fila (2 vistas)
|
|
|
|
|
|
|
|
views.slice(0, 2).forEach((view, i) => {
|
|
|
|
|
|
|
|
const image = images[getViewImage(view.id)];
|
|
|
|
|
|
|
|
if (!image) return;
|
|
|
|
|
|
|
|
const x = config.margin + (i + 1) * (squareSize + spacing);
|
|
|
|
|
|
|
|
pdfDoc.addImage(image, 'JPEG', x, startY, squareSize, squareSize, '', 'FAST');
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Cerrar modal después de 2 segundos
|
|
|
|
startY += squareSize + 14;
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
|
|
isVisible = false;
|
|
|
|
|
|
|
|
downloadState = 'idle';
|
|
|
|
|
|
|
|
}, 2000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
// Segunda fila (3 vistas)
|
|
|
|
downloadState = 'error';
|
|
|
|
views.slice(2, 5).forEach((view, i) => {
|
|
|
|
message = err?.message || 'Error desconocido al generar el PDF';
|
|
|
|
const image = images[getViewImage(view.id)];
|
|
|
|
console.error(err);
|
|
|
|
if (!image) return;
|
|
|
|
|
|
|
|
const x = config.margin + i * (squareSize + spacing);
|
|
|
|
|
|
|
|
pdfDoc.addImage(image, 'JPEG', x, startY, squareSize, squareSize, '', 'FAST');
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pdfDoc.addPage();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function addViewPages(pdfDoc: jsPDF, config: PDFConfig) {
|
|
|
|
|
|
|
|
const imageRatio = 1; // 1500x1500
|
|
|
|
|
|
|
|
const pageRatio = config.pageWidth / config.pageHeight;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
views.forEach((view, index) => {
|
|
|
|
|
|
|
|
const viewImage = images[getViewImage(view.id)];
|
|
|
|
|
|
|
|
if (!viewImage) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Calcular dimensiones de imagen
|
|
|
|
|
|
|
|
const renderHeight = config.pageHeight - 2 * config.margin;
|
|
|
|
|
|
|
|
const renderWidth = renderHeight * imageRatio;
|
|
|
|
|
|
|
|
const x = config.margin;
|
|
|
|
|
|
|
|
const y = (config.pageHeight - renderHeight) / 2;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Añadir imagen
|
|
|
|
|
|
|
|
pdfDoc.addImage(viewImage, 'JPEG', x, y, renderWidth, renderHeight, '', 'FAST');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Añadir contenido de texto
|
|
|
|
|
|
|
|
const textStartX = config.margin + renderWidth + 10;
|
|
|
|
|
|
|
|
const textMaxWidth = config.pageWidth - textStartX - config.margin;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addViewContent(pdfDoc, config, view, textStartX, textMaxWidth);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Nueva página (excepto la última)
|
|
|
|
|
|
|
|
if (index < views.length - 1) pdfDoc.addPage();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function addViewContent(pdfDoc: jsPDF, config: PDFConfig, view: View, x: number, maxWidth: number) {
|
|
|
|
|
|
|
|
let cursorY = config.margin + 5;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Título de la vista
|
|
|
|
|
|
|
|
pdfDoc.setFontSize(18);
|
|
|
|
|
|
|
|
pdfDoc.setFont(config.fonts.regular, "normal");
|
|
|
|
|
|
|
|
pdfDoc.setTextColor(30, 30, 30);
|
|
|
|
|
|
|
|
pdfDoc.text(view.name.toUpperCase(), x, cursorY);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Descripción
|
|
|
|
|
|
|
|
if (view.text) {
|
|
|
|
|
|
|
|
cursorY += 8;
|
|
|
|
|
|
|
|
cursorY = addMultilineText(pdfDoc, view.text, x, cursorY, maxWidth, config.fonts.light, 12, 5);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Opciones
|
|
|
|
|
|
|
|
const viewOptions = getViewOptions(view);
|
|
|
|
|
|
|
|
if (viewOptions.options.length > 0) {
|
|
|
|
|
|
|
|
cursorY = 100;
|
|
|
|
|
|
|
|
addOptionsSection(pdfDoc, config, viewOptions, x, cursorY, maxWidth);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function addOptionsSection(pdfDoc: jsPDF, config: PDFConfig, viewOptions: ViewOptions, x: number, y: number, maxWidth: number) {
|
|
|
|
|
|
|
|
let cursorY = y;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Título de sección
|
|
|
|
|
|
|
|
pdfDoc.setFontSize(14);
|
|
|
|
|
|
|
|
pdfDoc.setFont(config.fonts.regular, "normal");
|
|
|
|
|
|
|
|
pdfDoc.setTextColor(30, 30, 30);
|
|
|
|
|
|
|
|
pdfDoc.text('ACABADOS SELECCIONADOS', x, cursorY);
|
|
|
|
|
|
|
|
cursorY += 10;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Estilos de texto
|
|
|
|
|
|
|
|
const styles: TextStyles = {
|
|
|
|
|
|
|
|
group: { font: config.fonts.regular, size: 9 },
|
|
|
|
|
|
|
|
option: { font: config.fonts.bold, size: 11 },
|
|
|
|
|
|
|
|
price: { font: config.fonts.regular, size: 11 }
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Listar opciones
|
|
|
|
|
|
|
|
viewOptions.options.forEach((option) => {
|
|
|
|
|
|
|
|
const icon = (option.warning || option.consultar) ? advertenciaIconB64 : null;
|
|
|
|
|
|
|
|
cursorY = renderOption(pdfDoc, option, x, cursorY, maxWidth, styles, icon);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function addFinalPage(pdfDoc: jsPDF, config: PDFConfig) {
|
|
|
|
|
|
|
|
pdfDoc.addPage();
|
|
|
|
|
|
|
|
const finalImage = dataPdfImages['pdf_openbox_fin'];
|
|
|
|
|
|
|
|
if (!finalImage) return;
|
|
|
|
|
|
|
|
|
|
|
|
// === Función auxiliar para texto multilínea ===
|
|
|
|
const format = finalImage.includes('jpeg') ? 'JPEG' : 'PNG';
|
|
|
|
function addTextBlock(
|
|
|
|
pdfDoc.addImage(finalImage, format, 0, 0, config.pageWidth, config.pageHeight, '', 'FAST');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
// UTILIDADES DE TEXTO
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function addMultilineText(
|
|
|
|
pdfDoc: jsPDF,
|
|
|
|
pdfDoc: jsPDF,
|
|
|
|
text: string | string[] | null | undefined,
|
|
|
|
text: string | string[] | null | undefined,
|
|
|
|
x: number,
|
|
|
|
x: number,
|
|
|
|
@ -283,9 +300,10 @@
|
|
|
|
font: string,
|
|
|
|
font: string,
|
|
|
|
fontSize: number,
|
|
|
|
fontSize: number,
|
|
|
|
lineHeight: number
|
|
|
|
lineHeight: number
|
|
|
|
) {
|
|
|
|
): number {
|
|
|
|
if (!text) return yStart;
|
|
|
|
if (!text) return yStart;
|
|
|
|
let safeText: string = Array.isArray(text) ? text.join("\n") : String(text);
|
|
|
|
|
|
|
|
|
|
|
|
const safeText = Array.isArray(text) ? text.join("\n") : String(text);
|
|
|
|
if (safeText.trim().length === 0) return yStart;
|
|
|
|
if (safeText.trim().length === 0) return yStart;
|
|
|
|
|
|
|
|
|
|
|
|
pdfDoc.setFont(font, "normal");
|
|
|
|
pdfDoc.setFont(font, "normal");
|
|
|
|
@ -294,201 +312,208 @@
|
|
|
|
const paragraphs = safeText.split(/\r?\n/);
|
|
|
|
const paragraphs = safeText.split(/\r?\n/);
|
|
|
|
let cursorY = yStart;
|
|
|
|
let cursorY = yStart;
|
|
|
|
|
|
|
|
|
|
|
|
for (let p = 0; p < paragraphs.length; p++) {
|
|
|
|
paragraphs.forEach(paragraph => {
|
|
|
|
const paragraph = paragraphs[p].trim();
|
|
|
|
const trimmed = paragraph.trim();
|
|
|
|
if (!paragraph) {
|
|
|
|
if (!trimmed) {
|
|
|
|
cursorY += lineHeight;
|
|
|
|
cursorY += lineHeight;
|
|
|
|
continue;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getViewOptions(view: View): ViewOptions {
|
|
|
|
|
|
|
|
const options: Option[] = [];
|
|
|
|
|
|
|
|
let hasWarning = false;
|
|
|
|
|
|
|
|
let hasConsult = false;
|
|
|
|
|
|
|
|
let total = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (view.options.length > 0) {
|
|
|
|
const lines = pdfDoc.splitTextToSize(trimmed, maxWidth);
|
|
|
|
// Obtener opciones de la vista
|
|
|
|
lines.forEach((line: string) => {
|
|
|
|
view.options.forEach((group) => {
|
|
|
|
pdfDoc.text(line, x, cursorY);
|
|
|
|
const currentOptionId = appState.getCurrentOption(view.id, group.id);
|
|
|
|
cursorY += lineHeight;
|
|
|
|
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;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
|
|
warningMessage: requiresConsult.required ? requiresConsult.message : undefined
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
cursorY += lineHeight; // Espacio entre párrafos
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
return cursorY;
|
|
|
|
return {
|
|
|
|
|
|
|
|
total,
|
|
|
|
|
|
|
|
options,
|
|
|
|
|
|
|
|
hasWarning,
|
|
|
|
|
|
|
|
hasConsult,
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
// RENDERIZADO DE OPCIONES
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
// === Nueva función para bloques de opciones (3 líneas: grupo, opción, precio) ===
|
|
|
|
function renderOption(
|
|
|
|
function addOptionBlock(
|
|
|
|
|
|
|
|
pdfDoc: jsPDF,
|
|
|
|
pdfDoc: jsPDF,
|
|
|
|
nombreGrupo: string,
|
|
|
|
option: Option,
|
|
|
|
nombreOpcion: string,
|
|
|
|
|
|
|
|
precio: number | null | string,
|
|
|
|
|
|
|
|
x: number,
|
|
|
|
x: number,
|
|
|
|
y: number,
|
|
|
|
y: number,
|
|
|
|
maxWidth: number,
|
|
|
|
maxWidth: number,
|
|
|
|
groupFont: string,
|
|
|
|
styles: TextStyles,
|
|
|
|
groupSize: number,
|
|
|
|
icon: string | null = null
|
|
|
|
optionFont: string,
|
|
|
|
|
|
|
|
optionSize: number,
|
|
|
|
|
|
|
|
priceFont: string,
|
|
|
|
|
|
|
|
priceSize: number,
|
|
|
|
|
|
|
|
icon: string | null = null,
|
|
|
|
|
|
|
|
iconSize: number = 3,
|
|
|
|
|
|
|
|
warningMessage?: string,
|
|
|
|
|
|
|
|
): number {
|
|
|
|
): number {
|
|
|
|
let cursorY = y;
|
|
|
|
let cursorY = y;
|
|
|
|
|
|
|
|
|
|
|
|
// Icono (si existe)
|
|
|
|
// Línea 1: Nombre del grupo
|
|
|
|
/*
|
|
|
|
cursorY = renderGroupName(pdfDoc, option.nombreGrupo, x, cursorY, styles.group);
|
|
|
|
if (icon) {
|
|
|
|
|
|
|
|
pdfDoc.addImage(icon, 'PNG', x, cursorY - iconSize + 1, iconSize, iconSize);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
// Línea 2: Nombre de la opción + icono
|
|
|
|
|
|
|
|
cursorY = renderOptionName(pdfDoc, option.nombre, x, cursorY, maxWidth, styles.option, icon);
|
|
|
|
|
|
|
|
|
|
|
|
// LÍNEA 1: Nombre del grupo (pequeño, uppercase, gris)
|
|
|
|
// Línea 3: Precio
|
|
|
|
pdfDoc.setFont(groupFont, "normal");
|
|
|
|
cursorY = renderPrice(pdfDoc, option, x, cursorY, maxWidth, styles.price);
|
|
|
|
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)
|
|
|
|
// Línea 4: Warning (si existe)
|
|
|
|
/*
|
|
|
|
if (option.warningMessage) {
|
|
|
|
pdfDoc.setFont(optionFont, "normal");
|
|
|
|
cursorY = renderWarning(pdfDoc, option.warningMessage, x, cursorY, maxWidth, styles.group.font);
|
|
|
|
pdfDoc.setFontSize(optionSize);
|
|
|
|
}
|
|
|
|
pdfDoc.setTextColor(30, 30, 30);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Dividir en múltiples líneas si es necesario
|
|
|
|
return cursorY + 4; // Espacio entre opciones
|
|
|
|
const optionLines = pdfDoc.splitTextToSize(nombreOpcion, maxWidth - (icon ? iconSize + 2 : 0));
|
|
|
|
}
|
|
|
|
optionLines.forEach((line: string) => {
|
|
|
|
|
|
|
|
pdfDoc.text(line, x + (icon ? iconSize + 2 : 0), cursorY);
|
|
|
|
function renderGroupName(pdfDoc: jsPDF, groupName: string, x: number, y: number, style: { font: string; size: number }): number {
|
|
|
|
cursorY += optionSize * 0.45;
|
|
|
|
pdfDoc.setFont(style.font, "normal");
|
|
|
|
});
|
|
|
|
pdfDoc.setFontSize(style.size);
|
|
|
|
|
|
|
|
pdfDoc.setTextColor(120, 120, 120);
|
|
|
|
|
|
|
|
pdfDoc.text(groupName.toUpperCase(), x, y);
|
|
|
|
|
|
|
|
return y + style.size * 0.5;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
function renderOptionName(
|
|
|
|
|
|
|
|
pdfDoc: jsPDF,
|
|
|
|
|
|
|
|
optionName: string,
|
|
|
|
|
|
|
|
x: number,
|
|
|
|
|
|
|
|
y: number,
|
|
|
|
|
|
|
|
maxWidth: number,
|
|
|
|
|
|
|
|
style: { font: string; size: number },
|
|
|
|
|
|
|
|
icon: string | null
|
|
|
|
|
|
|
|
): number {
|
|
|
|
|
|
|
|
const ICON_SIZE = 6;
|
|
|
|
|
|
|
|
|
|
|
|
// LÍNEA 2: Nombre de la opción (más grande, bold, negro) + ICONO A LA DERECHA
|
|
|
|
pdfDoc.setFont(style.font, "normal");
|
|
|
|
pdfDoc.setFont(optionFont, "normal");
|
|
|
|
pdfDoc.setFontSize(style.size);
|
|
|
|
pdfDoc.setFontSize(optionSize);
|
|
|
|
|
|
|
|
pdfDoc.setTextColor(30, 30, 30);
|
|
|
|
pdfDoc.setTextColor(30, 30, 30);
|
|
|
|
|
|
|
|
|
|
|
|
// Dividir en múltiples líneas si es necesario
|
|
|
|
const lines = pdfDoc.splitTextToSize(optionName, maxWidth);
|
|
|
|
const optionLines = pdfDoc.splitTextToSize(nombreOpcion, maxWidth);
|
|
|
|
const iconYPosition = y;
|
|
|
|
const iconYPosition = cursorY; // Guardar posición Y para el icono
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
optionLines.forEach((line: string) => {
|
|
|
|
let cursorY = y;
|
|
|
|
|
|
|
|
lines.forEach((line: string) => {
|
|
|
|
pdfDoc.text(line, x, cursorY);
|
|
|
|
pdfDoc.text(line, x, cursorY);
|
|
|
|
cursorY += optionSize * 0.45;
|
|
|
|
cursorY += style.size * 0.45;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Icono a la derecha, alineado con la primera línea de la opción
|
|
|
|
// Icono alineado a la derecha
|
|
|
|
if (icon) {
|
|
|
|
if (icon) {
|
|
|
|
const iconX = x + maxWidth - iconSize;
|
|
|
|
const iconX = x + maxWidth - ICON_SIZE;
|
|
|
|
pdfDoc.addImage(icon, 'PNG', iconX, iconYPosition - iconSize + 1, iconSize, iconSize);
|
|
|
|
pdfDoc.addImage(icon, 'PNG', iconX, iconYPosition - ICON_SIZE + 1, ICON_SIZE, ICON_SIZE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// LÍNEA 3: Precio (si existe)
|
|
|
|
return cursorY;
|
|
|
|
// 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
|
|
|
|
function renderPrice(
|
|
|
|
const priceWidth = pdfDoc.getTextWidth(priceText);
|
|
|
|
pdfDoc: jsPDF,
|
|
|
|
|
|
|
|
option: Option,
|
|
|
|
|
|
|
|
x: number,
|
|
|
|
|
|
|
|
y: number,
|
|
|
|
|
|
|
|
maxWidth: number,
|
|
|
|
|
|
|
|
style: { font: string; size: number }
|
|
|
|
|
|
|
|
): number {
|
|
|
|
|
|
|
|
const precio = option.consultar ? 'consultar' : option.precio;
|
|
|
|
|
|
|
|
if (precio === null || precio === undefined) return y;
|
|
|
|
|
|
|
|
|
|
|
|
// Posición alineada a la derecha
|
|
|
|
pdfDoc.setFont(style.font, "normal");
|
|
|
|
const priceX = x + maxWidth - priceWidth;
|
|
|
|
pdfDoc.setFontSize(style.size);
|
|
|
|
|
|
|
|
|
|
|
|
pdfDoc.setTextColor(priceColor.r, priceColor.g, priceColor.b);
|
|
|
|
const { text, color } = getPriceDisplay(precio);
|
|
|
|
pdfDoc.text(priceText, priceX, cursorY);
|
|
|
|
const priceWidth = pdfDoc.getTextWidth(text);
|
|
|
|
cursorY += priceSize * 0.45;
|
|
|
|
const priceX = x + maxWidth - priceWidth;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// LÍNEA 4 (OPCIONAL): Mensaje de advertencia si existe
|
|
|
|
pdfDoc.setTextColor(color.r, color.g, color.b);
|
|
|
|
if (warningMessage && warningMessage.trim()) {
|
|
|
|
pdfDoc.text(text, priceX, y);
|
|
|
|
cursorY += 1; // Pequeño espacio antes del mensaje
|
|
|
|
pdfDoc.setTextColor(0, 0, 0); // Reset
|
|
|
|
pdfDoc.setFont(groupFont, "normal");
|
|
|
|
|
|
|
|
pdfDoc.setFontSize(8);
|
|
|
|
return y + style.size * 0.45;
|
|
|
|
pdfDoc.setTextColor(200, 100, 0); // Color naranja/amarillo para advertencia
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Dividir el mensaje en múltiples líneas si es necesario
|
|
|
|
function getPriceDisplay(precio: number | string): { text: string; color: { r: number; g: number; b: number } } {
|
|
|
|
const warnLines = pdfDoc.splitTextToSize(warningMessage, maxWidth - 5);
|
|
|
|
if (typeof precio === 'number' && precio > 0) {
|
|
|
|
warnLines.forEach((line: string) => {
|
|
|
|
return {
|
|
|
|
pdfDoc.text('⚠️ ' + line, x, cursorY);
|
|
|
|
text: '+ ' + formatPrice(precio),
|
|
|
|
cursorY += 3;
|
|
|
|
color: { r: 46, g: 125, b: 50 } // Verde
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
} else if (precio === 'consultar' || typeof precio === 'string') {
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
text: 'CONSULTAR',
|
|
|
|
|
|
|
|
color: { r: 255, g: 140, b: 0 } // Naranja
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
text: 'Sin coste adicional',
|
|
|
|
|
|
|
|
color: { r: 120, g: 120, b: 120 } // Gris
|
|
|
|
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderWarning(
|
|
|
|
|
|
|
|
pdfDoc: jsPDF,
|
|
|
|
|
|
|
|
message: string,
|
|
|
|
|
|
|
|
x: number,
|
|
|
|
|
|
|
|
y: number,
|
|
|
|
|
|
|
|
maxWidth: number,
|
|
|
|
|
|
|
|
font: string
|
|
|
|
|
|
|
|
): number {
|
|
|
|
|
|
|
|
let cursorY = y + 1;
|
|
|
|
|
|
|
|
|
|
|
|
// Espacio entre opciones
|
|
|
|
pdfDoc.setFont(font, "normal");
|
|
|
|
cursorY += 4;
|
|
|
|
pdfDoc.setFontSize(8);
|
|
|
|
|
|
|
|
pdfDoc.setTextColor(200, 100, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// Reset
|
|
|
|
const lines = pdfDoc.splitTextToSize(message, maxWidth - 5);
|
|
|
|
pdfDoc.setTextColor(0, 0, 0);
|
|
|
|
lines.forEach((line: string) => {
|
|
|
|
|
|
|
|
pdfDoc.text('⚠️ ' + line, x, cursorY);
|
|
|
|
|
|
|
|
cursorY += 3;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pdfDoc.setTextColor(0, 0, 0); // Reset
|
|
|
|
return cursorY;
|
|
|
|
return cursorY;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
// EXTRACCIÓN DE DATOS
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getViewOptions(view: View): ViewOptions {
|
|
|
|
|
|
|
|
const options: Option[] = [];
|
|
|
|
|
|
|
|
let hasWarning = false;
|
|
|
|
|
|
|
|
let hasConsult = false;
|
|
|
|
|
|
|
|
let total = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
view.options.forEach((group) => {
|
|
|
|
|
|
|
|
const currentOptionId = appState.getCurrentOption(view.id, group.id);
|
|
|
|
|
|
|
|
if (!currentOptionId) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const optionValue = appState.getOptionValue(currentOptionId);
|
|
|
|
|
|
|
|
const optionRef = group.values.find(v => v.id === currentOptionId);
|
|
|
|
|
|
|
|
const requiresConsult = appState.requiresConsultation(view.id, group.id, currentOptionId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
if (hasPrice) total += (optionRef.amount as number);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
options.push({
|
|
|
|
|
|
|
|
nombreGrupo: group.name || group.id,
|
|
|
|
|
|
|
|
nombre: optionValue?.name || currentOptionId || 'No seleccionado',
|
|
|
|
|
|
|
|
precio: hasPrice ? optionRef.amount : null,
|
|
|
|
|
|
|
|
consultar: isConsultar,
|
|
|
|
|
|
|
|
warning: requiresConsult.required,
|
|
|
|
|
|
|
|
warningMessage: requiresConsult.message
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { options, total, hasWarning, hasConsult };
|
|
|
|
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ============================================================================ -->
|
|
|
|
|
|
|
|
<!-- UI -->
|
|
|
|
|
|
|
|
<!-- ============================================================================ -->
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
type="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"
|
|
|
|
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"
|
|
|
|
@ -501,9 +526,9 @@
|
|
|
|
{isVisible}
|
|
|
|
{isVisible}
|
|
|
|
{message}
|
|
|
|
{message}
|
|
|
|
{downloadState}
|
|
|
|
{downloadState}
|
|
|
|
{tipologias}
|
|
|
|
tipologias={TIPOLOGIAS}
|
|
|
|
logoCompany={logoCompany}
|
|
|
|
{logoCompany}
|
|
|
|
logoProject={logoProject}
|
|
|
|
{logoProject}
|
|
|
|
text="Descarga tu Selección"
|
|
|
|
text="Descarga tu Selección"
|
|
|
|
onTipologiaSelected={handleTipologiaSelected}
|
|
|
|
onTipologiaSelected={handleTipologiaSelected}
|
|
|
|
onCancel={handleCancel}
|
|
|
|
onCancel={handleCancel}
|
|
|
|
|