You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

949 lines
32 KiB

8 months ago
<script lang="ts">
8 months ago
import { dataConfig } from "../data/DataConfig.js";
import type { DownloadState } from "../data/DownloadTypes";
8 months ago
import PoDownload from "../cmps/PoDownload.svelte";
8 months ago
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";
import { advertenciaIconB64 } from "../data/DataIconsB64"
// ============================================================================
// TIPOS
// ============================================================================
8 months ago
interface Option {
8 months ago
nombreGrupo: string;
nombre: string;
precio?: number | null | string;
consultar: boolean;
warning: boolean;
warningMessage?: string;
8 months ago
}
interface ViewOptions {
options: Option[];
8 months ago
total: number;
8 months ago
hasWarning: boolean;
hasConsult: boolean;
}
8 months ago
interface PDFConfig {
pageWidth: number;
pageHeight: number;
margin: number;
fonts: {
title: string;
regular: string;
bold: string;
light: string;
};
}
8 months ago
8 months ago
interface TextStyles {
group: { font: string; size: number };
option: { font: string; size: number };
price: { font: string; size: number };
}
8 months ago
8 months ago
// ============================================================================
// ESTADO
// ============================================================================
8 months ago
const TIPOLOGIAS = ['P2D','P3D','P4D','B2D', 'B3D', 'B4D','A4D'];
const tiposInfo : Record<string,string> = {
'P2D' :'Vivienda de 2 Dormitorios',
'P3D' :'Vivienda de 3 Dormitorios',
'P4D' :'Vivienda de 4 Dormitorios',
'B2D' :'Planta Baja de 2 Dormitorios',
'B3D' :'Planta Baja de 3 Dormitorios',
'B4D' :'Planta Baja de 4 Dormitorios',
'A4D' :'Ático de 4 Dormitorios',
}
8 months ago
8 months ago
let downloadState: DownloadState = $state('idle');
let isVisible = $state(false);
let message = $state('');
8 months ago
8 months ago
const views = appState.getAllViews();
8 months ago
const logoProject = dataConfig.sources.logos + dataConfig.logos.project;
const logoCompany = dataConfig.sources.logos + dataConfig.logos.company;
// ============================================================================
// HANDLERS
// ============================================================================
8 months ago
8 months ago
function openSelector() {
8 months ago
isVisible = true;
8 months ago
downloadState = 'idle';
}
function handleCancel() {
isVisible = false;
downloadState = 'idle';
}
async function handleTipologiaSelected(tipologia: string) {
8 months ago
downloadState = 'creating';
try {
8 months ago
const pdfDoc = createPDFDocument();
const config = getPDFConfig(pdfDoc);
8 months ago
8 months ago
// ✅ PRECALCULAR TODAS LAS OPCIONES UNA SOLA VEZ
const allViewOptions = precalculateAllOptions(tipologia);
8 months ago
registerFonts(pdfDoc);
8 months ago
await generatePDFContent(pdfDoc, config, tipologia, allViewOptions);
8 months ago
8 months ago
downloadState = 'downloading';
pdfDoc.save(`OPENBOX_${tipologia}_${Date.now()}.pdf`);
downloadState = 'success';
8 months ago
8 months ago
setTimeout(() => {
isVisible = false;
downloadState = 'idle';
}, 2000);
8 months ago
8 months ago
} catch (err: any) {
downloadState = 'error';
message = err?.message || 'Error desconocido al generar el PDF';
console.error('PDF Generation Error:', err);
}
}
8 months ago
8 months ago
// ============================================================================
// PRECÁLCULO DE OPCIONES (una sola vez)
// ============================================================================
interface AllViewOptions {
general: Option[];
views: Map<string, ViewOptions>;
totalGeneral: number;
}
function precalculateAllOptions(tipologia: string): AllViewOptions {
const generalOptions = getGeneralOptions(tiposInfo[tipologia]);
const viewOptionsMap = new Map<string, ViewOptions>();
let totalGeneral = 0;
views.forEach((view) => {
const viewOptions = getViewOptions(view);
viewOptionsMap.set(view.id, viewOptions);
totalGeneral += viewOptions.total;
});
return {
general: generalOptions,
views: viewOptionsMap,
totalGeneral
};
}
8 months ago
// ============================================================================
// CREACIÓN DE PDF
// ============================================================================
8 months ago
8 months ago
function createPDFDocument(): jsPDF {
return new jsPDF({
orientation: 'landscape',
unit: 'mm',
format: 'A4',
});
}
8 months ago
8 months ago
function getPDFConfig(pdfDoc: jsPDF): PDFConfig {
return {
pageWidth: pdfDoc.internal.pageSize.getWidth(),
pageHeight: pdfDoc.internal.pageSize.getHeight(),
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,
}
};
}
8 months ago
8 months ago
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);
});
}
8 months ago
8 months ago
async function generatePDFContent(pdfDoc: jsPDF, config: PDFConfig, tipologia: string, allOptions: AllViewOptions) {
8 months ago
addCoverPage(pdfDoc, config, tipologia);
addPresentationPage(pdfDoc, config);
8 months ago
addViewPages(pdfDoc, config, allOptions);
addSummaryPage(pdfDoc, config, allOptions);
8 months ago
addFinalPage(pdfDoc, config);
}
8 months ago
8 months ago
// ============================================================================
// PÁGINAS DEL PDF
// ============================================================================
8 months ago
8 months ago
function addCoverPage(pdfDoc: jsPDF, config: PDFConfig, tipologia: string) {
const coverImage = dataPdfImages['pdf_openbox_ini'];
if (!coverImage) return;
8 months ago
8 months ago
const format = coverImage.includes('jpeg') ? 'JPEG' : 'PNG';
pdfDoc.addImage(coverImage, format, 0, 0, config.pageWidth, config.pageHeight, '', 'FAST');
8 months ago
8 months ago
pdfDoc.setFontSize(20);
pdfDoc.setFont(config.fonts.bold, "normal");
pdfDoc.text(tipologia.toUpperCase(), 265, 119);
8 months ago
8 months ago
pdfDoc.addPage();
}
8 months ago
8 months ago
function addPresentationPage(pdfDoc: jsPDF, config: PDFConfig) {
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');
}
8 months ago
8 months ago
// 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');
});
8 months ago
8 months ago
startY += squareSize + 14;
8 months ago
8 months ago
// Segunda fila (3 vistas)
views.slice(2, 5).forEach((view, i) => {
const image = images[getViewImage(view.id)];
if (!image) return;
const x = config.margin + i * (squareSize + spacing);
pdfDoc.addImage(image, 'JPEG', x, startY, squareSize, squareSize, '', 'FAST');
});
pdfDoc.addPage();
}
8 months ago
function addViewPages(pdfDoc: jsPDF, config: PDFConfig, allOptions: AllViewOptions) {
8 months ago
const imageRatio = 1; // 1500x1500
8 months ago
//const pageRatio = config.pageWidth / config.pageHeight;
8 months ago
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;
8 months ago
const viewOptions = allOptions.views.get(view.id);
if (viewOptions) {
addViewContent(pdfDoc, config, view, textStartX, textMaxWidth, viewOptions);
}
8 months ago
// Nueva página (excepto la última)
if (index < views.length - 1) pdfDoc.addPage();
});
}
8 months ago
function addViewContent(pdfDoc: jsPDF, config: PDFConfig, view: View, x: number, maxWidth: number, viewOptions: ViewOptions) {
8 months ago
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);
}
8 months ago
// Opciones (usar las precalculadas)
8 months ago
if (viewOptions.options.length > 0) {
cursorY = 100;
addOptionsSection(pdfDoc, config, viewOptions, x, cursorY, maxWidth);
8 months ago
}
}
8 months ago
8 months ago
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 = {
8 months ago
group: { font: config.fonts.regular, size: 11 },
8 months ago
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);
});
}
8 months ago
function addSummaryPage(pdfDoc: jsPDF, config: PDFConfig, allOptions: AllViewOptions) {
8 months ago
pdfDoc.addPage();
8 months ago
const COLS = 5;
8 months ago
const MARGIN = 10;
const COL_WIDTH = (config.pageWidth - 2 * MARGIN) / COLS;
const THUMB_SIZE = COL_WIDTH - 4;
const ICON_SIZE = 3;
8 months ago
//const ROW_SPACING = 10;
8 months ago
8 months ago
let startY = MARGIN+10;
8 months ago
8 months ago
// ===== TÍTULO DE LA PÁGINA =====
8 months ago
pdfDoc.setFontSize(16);
pdfDoc.setFont(config.fonts.bold, "normal");
8 months ago
pdfDoc.setTextColor(80, 80, 80);
pdfDoc.text('Resumen de la configuración seleccionada'.toUpperCase(), config.pageWidth / 2, startY, { align: 'center' });
8 months ago
startY += 12;
8 months ago
8 months ago
// ===== FILA 1: 5 VISTAS =====
8 months ago
const row1Y = startY-5;
8 months ago
views.slice(0, 5).forEach((view, index) => {
const x = MARGIN + index * COL_WIDTH;
8 months ago
const viewOptions = allOptions.views.get(view.id);
if (viewOptions) {
renderViewColumnWithHeader(pdfDoc, config, view, viewOptions, x, row1Y, COL_WIDTH, THUMB_SIZE, ICON_SIZE);
}
8 months ago
});
8 months ago
// ===== FILA 2: GENERALES (izq) + ESPACIO + TOTAL (der) =====
8 months ago
const row2Y = row1Y + 120;
8 months ago
8 months ago
// Columna 1: Acabados Generales (izquierda)
8 months ago
//const generalX = MARGIN;
//renderGeneralColumnWithHeader(pdfDoc, config, allOptions.general, generalX, row2Y, COL_WIDTH, THUMB_SIZE, ICON_SIZE);
renderGeneralColumnWithHeader(pdfDoc, config, allOptions.general, MARGIN, row2Y, COL_WIDTH, THUMB_SIZE, ICON_SIZE);
8 months ago
8 months ago
// Columnas 2-4: Vacías (espacio)
8 months ago
renderFirmaColumnWithHeader (pdfDoc, config, 70, row2Y, 130)
8 months ago
8 months ago
// Columna 5: Total (derecha)
const totalX = MARGIN + 4 * COL_WIDTH;
8 months ago
renderTotalColumnWithHeader(pdfDoc, config, allOptions, totalX-20, row2Y, COL_WIDTH+16);
8 months ago
}
8 months ago
8 months ago
function renderViewColumnWithHeader(
8 months ago
pdfDoc: jsPDF,
config: PDFConfig,
8 months ago
view: View,
8 months ago
viewOptions: ViewOptions,
8 months ago
x: number,
y: number,
colWidth: number,
thumbSize: number,
iconSize: number
) {
8 months ago
let cursorY = y+3;
8 months ago
8 months ago
// Header de columna
8 months ago
pdfDoc.setFillColor(46, 125, 50);
pdfDoc.rect(x+2, cursorY - 5, thumbSize, 8, 'F');
8 months ago
8 months ago
pdfDoc.setFontSize(8);
pdfDoc.setFont(config.fonts.bold, "normal");
pdfDoc.setTextColor(255, 255, 255);
const titleLines = pdfDoc.splitTextToSize(view.name.toUpperCase(), colWidth - 4);
titleLines.forEach((line: string, i: number) => {
pdfDoc.text(line, x + colWidth / 2, cursorY + i * 3, { align: 'center' });
});
8 months ago
8 months ago
cursorY += 3;
// Thumbnail de la vista
const viewImage = images[getViewImage(view.id)];
if (viewImage) {
pdfDoc.addImage(viewImage, 'JPEG', x + 2, cursorY, thumbSize, thumbSize, '', 'FAST');
}
cursorY += thumbSize + 5;
8 months ago
// ✅ USAR OPCIONES PRECALCULADAS
8 months ago
viewOptions.options.forEach((option) => {
8 months ago
cursorY = renderCompactOption(pdfDoc, config, option, x + 2, cursorY, colWidth - 4, iconSize);
});
8 months ago
8 months ago
// Total de la vista
8 months ago
if (viewOptions.total > 0) {
cursorY += 3;
pdfDoc.setDrawColor(200, 200, 200);
pdfDoc.setLineWidth(0.3);
pdfDoc.line(x + 2, cursorY, x + colWidth - 2, cursorY);
cursorY += 4;
pdfDoc.setFontSize(9);
pdfDoc.setFont(config.fonts.bold, "normal");
pdfDoc.setTextColor(46, 125, 50);
pdfDoc.text('TOTAL', x + 2, cursorY);
pdfDoc.text(formatPrice(viewOptions.total), x + colWidth - 4, cursorY, { align: 'right' });
8 months ago
pdfDoc.setLineWidth(0.2);
8 months ago
pdfDoc.setTextColor(0, 0, 0);
}
8 months ago
}
8 months ago
function renderFirmaColumnWithHeader(
pdfDoc: jsPDF,
config: PDFConfig,
x: number,
y: number,
colWidth: number,
) {
let cursorY = y;
// Header de columna
pdfDoc.setFillColor(157, 186, 113);
pdfDoc.rect(x+2, cursorY - 5, colWidth, 50, 'S');
pdfDoc.setFontSize(12);
pdfDoc.setFont(config.fonts.bold, "normal");
pdfDoc.setTextColor(80,80,80);
const titleLines = pdfDoc.splitTextToSize('FIRMA CLIENTE', colWidth - 4);
titleLines.forEach((line: string, i: number) => {
pdfDoc.text(line, x + colWidth / 2, cursorY + i * 3, { align: 'center' });
});
}
8 months ago
function renderGeneralColumnWithHeader(
8 months ago
pdfDoc: jsPDF,
config: PDFConfig,
8 months ago
generalOptions: Option[],
8 months ago
x: number,
y: number,
colWidth: number,
thumbSize: number,
iconSize: number
) {
let cursorY = y;
8 months ago
// Header de columna
8 months ago
pdfDoc.setFillColor(157, 186, 113);
pdfDoc.rect(x+2, cursorY - 5, thumbSize, 8, 'F');
8 months ago
8 months ago
pdfDoc.setFontSize(8);
pdfDoc.setFont(config.fonts.bold, "normal");
8 months ago
pdfDoc.setTextColor(80,80,80);
8 months ago
const titleLines = pdfDoc.splitTextToSize('ACABADOS GENERALES', colWidth - 4);
titleLines.forEach((line: string, i: number) => {
pdfDoc.text(line, x + colWidth / 2, cursorY + i * 3, { align: 'center' });
});
8 months ago
8 months ago
cursorY += 10;
8 months ago
// Icono o placeholder
8 months ago
/*
pdfDoc.setFillColor(157, 186, 113);
8 months ago
pdfDoc.rect(x + 2, cursorY, thumbSize, thumbSize, 'F');
pdfDoc.setFontSize(24);
8 months ago
pdfDoc.setTextColor(80, 80, 80);
8 months ago
pdfDoc.text('🏠', x + colWidth / 2, cursorY + thumbSize / 2 + 4, { align: 'center' });
cursorY += thumbSize + 5;
8 months ago
*/
// ✅ USAR OPCIONES PRECALCULADAS
8 months ago
generalOptions.forEach((option) => {
8 months ago
cursorY = renderCompactOption(pdfDoc, config, option, x + 2, cursorY, thumbSize, iconSize);
8 months ago
});
}
8 months ago
function renderTotalColumnWithHeader(
8 months ago
pdfDoc: jsPDF,
config: PDFConfig,
8 months ago
allOptions: AllViewOptions,
8 months ago
x: number,
y: number,
colWidth: number
) {
8 months ago
let cursorY = y;
// Header de columna
8 months ago
pdfDoc.setFillColor(47,101,53);
8 months ago
pdfDoc.rect(x, cursorY - 5, colWidth, 8, 'F');
8 months ago
pdfDoc.setFontSize(10);
8 months ago
pdfDoc.setFont(config.fonts.bold, "normal");
pdfDoc.setTextColor(255, 255, 255);
8 months ago
pdfDoc.text('TOTALES ESTANCIAS', x + colWidth / 2, cursorY, { align: 'center' });
8 months ago
cursorY += 15;
8 months ago
8 months ago
// ✅ USAR TOTALES PRECALCULADOS
8 months ago
const viewTotals: { view: string; total: number }[] = [];
views.forEach((view) => {
8 months ago
const vo = allOptions.views.get(view.id);
if (vo && vo.total > 0) {
8 months ago
viewTotals.push({ view: view.name.toUpperCase(), total: vo.total });
8 months ago
}
});
// Subtotales por vista
8 months ago
pdfDoc.setFontSize(10);
8 months ago
pdfDoc.setFont(config.fonts.regular, "normal");
8 months ago
pdfDoc.setTextColor(80, 80, 80);
8 months ago
viewTotals.forEach((item) => {
8 months ago
const nameLines = pdfDoc.splitTextToSize(item.view, colWidth * 0.6);
nameLines.forEach((line: string, i: number) => {
pdfDoc.text(line, x + 2, cursorY + i * 3);
});
pdfDoc.setFont(config.fonts.bold, "normal");
8 months ago
pdfDoc.text(formatPrice(item.total), x + colWidth - 4, cursorY, { align: 'right' });
8 months ago
pdfDoc.setFont(config.fonts.regular, "normal");
8 months ago
cursorY += Math.max(nameLines.length * 3, 6) + 1;
8 months ago
});
// Línea separadora
8 months ago
cursorY += 5;
/*
8 months ago
pdfDoc.setDrawColor(46, 125, 50);
pdfDoc.setLineWidth(0.5);
8 months ago
pdfDoc.line(x + 2, cursorY, x + colWidth - 2, cursorY);
8 months ago
cursorY += 6;
8 months ago
*/
8 months ago
// Total general
8 months ago
pdfDoc.setFillColor(240, 240, 240);
8 months ago
pdfDoc.rect(x + 1, cursorY - 4, colWidth - 2, 10, 'F');
pdfDoc.setFontSize(10);
8 months ago
pdfDoc.setFont(config.fonts.bold, "normal");
8 months ago
pdfDoc.setTextColor(25, 25, 25);
8 months ago
pdfDoc.text('TOTAL', x + 4, cursorY + 2);
pdfDoc.setFontSize(12);
8 months ago
pdfDoc.text(formatPrice(allOptions.totalGeneral), x + colWidth - 4, cursorY + 2, { align: 'right' });
8 months ago
8 months ago
pdfDoc.setLineWidth(0.2);
8 months ago
pdfDoc.setTextColor(0, 0, 0);
8 months ago
}
8 months ago
8 months ago
function renderCompactOption(
pdfDoc: jsPDF,
config: PDFConfig,
option: Option,
x: number,
y: number,
maxWidth: number,
iconSize: number
): number {
let cursorY = y;
8 months ago
const iconMargin = 1;
const iconSpace = (option.warning || option.consultar) ? iconSize + iconMargin : 0;
// Calcular precio para ponerlo en la línea del grupo
const precio = option.consultar ? 'consultar' : option.precio;
let priceText = '';
let priceColor = { r: 0, g: 0, b: 0 };
if (precio !== null && precio !== undefined) {
const priceDisplay = getPriceDisplay(precio);
priceText = priceDisplay.text;
priceColor = priceDisplay.color;
}
8 months ago
8 months ago
// Calcular ancho del precio
pdfDoc.setFont(config.fonts.regular, "normal");
pdfDoc.setFontSize(6);
const priceWidth = priceText ? pdfDoc.getTextWidth(priceText) + 2 : 0;
// LÍNEA 1: Grupo (izquierda) + Precio (derecha)
8 months ago
pdfDoc.setFont(config.fonts.regular, "normal");
pdfDoc.setFontSize(6);
pdfDoc.setTextColor(120, 120, 120);
8 months ago
// Calcular espacio disponible para el grupo
const availableGroupWidth = maxWidth - priceWidth;
const groupLines = pdfDoc.splitTextToSize(option.nombreGrupo.toUpperCase(), availableGroupWidth);
// Primera línea del grupo
if (groupLines.length > 0) {
pdfDoc.text(groupLines[0], x, cursorY);
// Precio a la derecha en la misma línea
if (priceText) {
pdfDoc.setTextColor(priceColor.r, priceColor.g, priceColor.b);
pdfDoc.text(priceText, x + maxWidth, cursorY, { align: 'right' });
pdfDoc.setTextColor(120, 120, 120);
}
8 months ago
cursorY += 2.5;
8 months ago
}
// Resto de líneas del grupo (si hay más)
for (let i = 1; i < groupLines.length; i++) {
pdfDoc.text(groupLines[i], x, cursorY);
cursorY += 2.5;
}
8 months ago
8 months ago
// Icono al inicio de la línea del nombre (si existe)
if ((option.warning || option.consultar) && advertenciaIconB64) {
pdfDoc.addImage(advertenciaIconB64, 'PNG', x, cursorY - iconSize + 1, iconSize, iconSize);
}
// LÍNEA 2: Nombre de la opción (bold, negro)
8 months ago
pdfDoc.setFont(config.fonts.bold, "normal");
pdfDoc.setFontSize(7);
pdfDoc.setTextColor(30, 30, 30);
8 months ago
const optionLines = pdfDoc.splitTextToSize(option.nombre, maxWidth - iconSpace);
8 months ago
optionLines.forEach((line: string) => {
8 months ago
pdfDoc.text(line, x + iconSpace, cursorY);
8 months ago
cursorY += 3;
});
cursorY += 2; // Espacio entre opciones
pdfDoc.setTextColor(0, 0, 0);
return cursorY;
}
8 months ago
function getGeneralOptions(tipologia: string): Option[] {
8 months ago
const options: Option[] = [];
8 months ago
options.push({
nombreGrupo: 'TIPO DE VIVIENDA',
nombre: tipologia,
consultar: false,
precio: null,
warning: false,
});
8 months ago
Object.entries(dataConfig.options.general).forEach(([groupId, group]) => {
const currentOptionId = appState.getCurrentOption('general', groupId);
if (!currentOptionId) return;
const optionValue = appState.getOptionValue(currentOptionId);
const optionRef = group.values.find(v => v.id === currentOptionId);
const requiresConsult = appState.requiresConsultation('general', groupId, currentOptionId);
const hasPrice = optionRef?.amount && typeof optionRef.amount === 'number' && optionRef.amount > 0;
const isConsultar = optionRef?.amount === 'consultar';
options.push({
nombreGrupo: group.name || groupId,
nombre: optionValue?.name || currentOptionId || 'No seleccionado',
precio: hasPrice ? optionRef.amount : null,
consultar: isConsultar,
warning: requiresConsult.required,
warningMessage: requiresConsult.message
});
});
return options;
}
8 months ago
function addFinalPage(pdfDoc: jsPDF, config: PDFConfig) {
pdfDoc.addPage();
const finalImage = dataPdfImages['pdf_openbox_fin'];
if (!finalImage) return;
8 months ago
8 months ago
const format = finalImage.includes('jpeg') ? 'JPEG' : 'PNG';
pdfDoc.addImage(finalImage, format, 0, 0, config.pageWidth, config.pageHeight, '', 'FAST');
}
// ============================================================================
// UTILIDADES DE TEXTO
// ============================================================================
function addMultilineText(
8 months ago
pdfDoc: jsPDF,
8 months ago
text: string | string[] | null | undefined,
x: number,
yStart: number,
maxWidth: number,
font: string,
fontSize: number,
lineHeight: number
8 months ago
): number {
8 months ago
if (!text) return yStart;
8 months ago
const safeText = Array.isArray(text) ? text.join("\n") : String(text);
8 months ago
if (safeText.trim().length === 0) return yStart;
pdfDoc.setFont(font, "normal");
pdfDoc.setFontSize(fontSize);
const paragraphs = safeText.split(/\r?\n/);
let cursorY = yStart;
8 months ago
paragraphs.forEach(paragraph => {
const trimmed = paragraph.trim();
if (!trimmed) {
8 months ago
cursorY += lineHeight;
8 months ago
return;
8 months ago
}
8 months ago
const lines = pdfDoc.splitTextToSize(trimmed, maxWidth);
lines.forEach((line: string) => {
pdfDoc.text(line, x, cursorY);
cursorY += lineHeight;
8 months ago
});
8 months ago
cursorY += lineHeight; // Espacio entre párrafos
});
8 months ago
8 months ago
return cursorY;
8 months ago
}
8 months ago
// ============================================================================
// RENDERIZADO DE OPCIONES
// ============================================================================
8 months ago
8 months ago
function renderOption(
8 months ago
pdfDoc: jsPDF,
8 months ago
option: Option,
8 months ago
x: number,
y: number,
maxWidth: number,
8 months ago
styles: TextStyles,
icon: string | null = null
8 months ago
): number {
let cursorY = y;
8 months ago
// Línea 1: Nombre del grupo
8 months ago
const cY = cursorY;
8 months ago
cursorY = renderGroupName(pdfDoc, option.nombreGrupo, x, cursorY, styles.group);
8 months ago
renderPrice(pdfDoc, option, x, cY, maxWidth, styles.price);
8 months ago
8 months ago
// Línea 2: Nombre de la opción + icono
cursorY = renderOptionName(pdfDoc, option.nombre, x, cursorY, maxWidth, styles.option, icon);
8 months ago
8 months ago
// Línea 3: Warning (si existe)
8 months ago
if (option.warningMessage) {
cursorY = renderWarning(pdfDoc, option.warningMessage, x, cursorY, maxWidth, styles.group.font);
}
8 months ago
8 months ago
return cursorY + 4; // Espacio entre opciones
}
function renderGroupName(pdfDoc: jsPDF, groupName: string, x: number, y: number, style: { font: string; size: number }): number {
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;
}
8 months ago
8 months ago
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;
8 months ago
8 months ago
pdfDoc.setFont(style.font, "normal");
pdfDoc.setFontSize(style.size);
8 months ago
pdfDoc.setTextColor(30, 30, 30);
8 months ago
const lines = pdfDoc.splitTextToSize(optionName, maxWidth);
const iconYPosition = y;
8 months ago
8 months ago
let cursorY = y;
lines.forEach((line: string) => {
8 months ago
pdfDoc.text(line, x, cursorY);
8 months ago
cursorY += style.size * 0.45;
8 months ago
});
8 months ago
// Icono alineado a la derecha
8 months ago
if (icon) {
8 months ago
const iconX = x + maxWidth - ICON_SIZE;
pdfDoc.addImage(icon, 'PNG', iconX, iconYPosition - ICON_SIZE + 1, ICON_SIZE, ICON_SIZE);
8 months ago
}
8 months ago
return cursorY;
}
8 months ago
8 months ago
function renderPrice(
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;
8 months ago
8 months ago
pdfDoc.setFont(style.font, "normal");
pdfDoc.setFontSize(style.size);
8 months ago
8 months ago
const { text, color } = getPriceDisplay(precio);
const priceWidth = pdfDoc.getTextWidth(text);
const priceX = x + maxWidth - priceWidth;
8 months ago
8 months ago
pdfDoc.setTextColor(color.r, color.g, color.b);
pdfDoc.text(text, priceX, y);
pdfDoc.setTextColor(0, 0, 0); // Reset
return y + style.size * 0.45;
}
function getPriceDisplay(precio: number | string): { text: string; color: { r: number; g: number; b: number } } {
if (typeof precio === 'number' && precio > 0) {
return {
text: '+ ' + formatPrice(precio),
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
};
8 months ago
}
8 months ago
}
8 months ago
8 months ago
function renderWarning(
pdfDoc: jsPDF,
message: string,
x: number,
y: number,
maxWidth: number,
font: string
): number {
let cursorY = y + 1;
8 months ago
8 months ago
pdfDoc.setFont(font, "normal");
8 months ago
pdfDoc.setFontSize(10);
8 months ago
pdfDoc.setTextColor(200, 100, 0);
8 months ago
8 months ago
const lines = pdfDoc.splitTextToSize(message, maxWidth - 5);
lines.forEach((line: string) => {
8 months ago
pdfDoc.text(line, x, cursorY);
cursorY += 4;
8 months ago
});
8 months ago
8 months ago
pdfDoc.setTextColor(0, 0, 0); // Reset
8 months ago
return cursorY;
}
8 months ago
// ============================================================================
// 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;
8 months ago
8 months ago
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 };
}
8 months ago
</script>
8 months ago
<!-- ============================================================================ -->
<!-- UI -->
<!-- ============================================================================ -->
8 months ago
<button
type="button"
8 months ago
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"
8 months ago
onclick={openSelector}
>
8 months ago
Descargar Selección
</button>
8 months ago
<PoDownload
{isVisible}
{message}
{downloadState}
8 months ago
tipologias={TIPOLOGIAS}
8 months ago
tiposTextos={Object.values(tiposInfo)}
8 months ago
{logoCompany}
{logoProject}
8 months ago
text="Descarga tu Selección"
onTipologiaSelected={handleTipologiaSelected}
onCancel={handleCancel}
8 months ago
/>

Powered by TurnKey Linux.