From f80bc158caba2670020df5f3e1638c52352a6bda Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 14 Feb 2026 03:38:26 +0100 Subject: [PATCH] . --- src/lib/cmps/BtDownload.svelte | 226 +++++++++++++++++++++++---------- src/lib/cmps/PoDownload.svelte | 10 +- 2 files changed, 163 insertions(+), 73 deletions(-) diff --git a/src/lib/cmps/BtDownload.svelte b/src/lib/cmps/BtDownload.svelte index e71f59b..bc7aec3 100644 --- a/src/lib/cmps/BtDownload.svelte +++ b/src/lib/cmps/BtDownload.svelte @@ -53,8 +53,17 @@ // ESTADO // ============================================================================ - const TIPOLOGIAS = ['A1', 'P1', 'P2']; - const STEP_TIMEOUT = 1000; + const TIPOLOGIAS = ['P2D','P3D','P4D','B2D', 'B3D', 'B4D','A4D']; + const tiposInfo : Record = { + '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', + } + let downloadState: DownloadState = $state('idle'); let isVisible = $state(false); @@ -85,8 +94,12 @@ const pdfDoc = createPDFDocument(); const config = getPDFConfig(pdfDoc); + // ✅ PRECALCULAR TODAS LAS OPCIONES UNA SOLA VEZ + + const allViewOptions = precalculateAllOptions(tipologia); + registerFonts(pdfDoc); - await generatePDFContent(pdfDoc, config, tipologia); + await generatePDFContent(pdfDoc, config, tipologia, allViewOptions); downloadState = 'downloading'; pdfDoc.save(`OPENBOX_${tipologia}_${Date.now()}.pdf`); @@ -104,6 +117,34 @@ } } + // ============================================================================ + // PRECÁLCULO DE OPCIONES (una sola vez) + // ============================================================================ + + interface AllViewOptions { + general: Option[]; + views: Map; + totalGeneral: number; + } + + function precalculateAllOptions(tipologia: string): AllViewOptions { + const generalOptions = getGeneralOptions(tiposInfo[tipologia]); + const viewOptionsMap = new Map(); + let totalGeneral = 0; + + views.forEach((view) => { + const viewOptions = getViewOptions(view); + viewOptionsMap.set(view.id, viewOptions); + totalGeneral += viewOptions.total; + }); + + return { + general: generalOptions, + views: viewOptionsMap, + totalGeneral + }; + } + // ============================================================================ // CREACIÓN DE PDF // ============================================================================ @@ -144,11 +185,11 @@ }); } - async function generatePDFContent(pdfDoc: jsPDF, config: PDFConfig, tipologia: string) { + async function generatePDFContent(pdfDoc: jsPDF, config: PDFConfig, tipologia: string, allOptions: AllViewOptions) { addCoverPage(pdfDoc, config, tipologia); addPresentationPage(pdfDoc, config); - addViewPages(pdfDoc, config); - addSummaryPage(pdfDoc, config); // ✅ NUEVA PÁGINA + addViewPages(pdfDoc, config, allOptions); + addSummaryPage(pdfDoc, config, allOptions); addFinalPage(pdfDoc, config); } @@ -204,9 +245,9 @@ pdfDoc.addPage(); } - function addViewPages(pdfDoc: jsPDF, config: PDFConfig) { + function addViewPages(pdfDoc: jsPDF, config: PDFConfig, allOptions: AllViewOptions) { const imageRatio = 1; // 1500x1500 - const pageRatio = config.pageWidth / config.pageHeight; + //const pageRatio = config.pageWidth / config.pageHeight; views.forEach((view, index) => { const viewImage = images[getViewImage(view.id)]; @@ -225,14 +266,17 @@ const textStartX = config.margin + renderWidth + 10; const textMaxWidth = config.pageWidth - textStartX - config.margin; - addViewContent(pdfDoc, config, view, textStartX, textMaxWidth); + const viewOptions = allOptions.views.get(view.id); + if (viewOptions) { + addViewContent(pdfDoc, config, view, textStartX, textMaxWidth, viewOptions); + } // 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) { + function addViewContent(pdfDoc: jsPDF, config: PDFConfig, view: View, x: number, maxWidth: number, viewOptions: ViewOptions) { let cursorY = config.margin + 5; // Título de la vista @@ -247,8 +291,7 @@ cursorY = addMultilineText(pdfDoc, view.text, x, cursorY, maxWidth, config.fonts.light, 12, 5); } - // Opciones - const viewOptions = getViewOptions(view); + // Opciones (usar las precalculadas) if (viewOptions.options.length > 0) { cursorY = 100; addOptionsSection(pdfDoc, config, viewOptions, x, cursorY, maxWidth); @@ -267,7 +310,7 @@ // Estilos de texto const styles: TextStyles = { - group: { font: config.fonts.regular, size: 9 }, + group: { font: config.fonts.regular, size: 11 }, option: { font: config.fonts.bold, size: 11 }, price: { font: config.fonts.regular, size: 11 } }; @@ -279,7 +322,8 @@ }); } - function addSummaryPage(pdfDoc: jsPDF, config: PDFConfig) { + + function addSummaryPage(pdfDoc: jsPDF, config: PDFConfig, allOptions: AllViewOptions) { pdfDoc.addPage(); const COLS = 5; @@ -287,53 +331,63 @@ const COL_WIDTH = (config.pageWidth - 2 * MARGIN) / COLS; const THUMB_SIZE = COL_WIDTH - 4; const ICON_SIZE = 3; - const ROW_SPACING = 10; + //const ROW_SPACING = 10; - let startY = MARGIN; + let startY = MARGIN+10; // ===== TÍTULO DE LA PÁGINA ===== pdfDoc.setFontSize(16); pdfDoc.setFont(config.fonts.bold, "normal"); - pdfDoc.setTextColor(30, 30, 30); - pdfDoc.text('RESUMEN DE ACABADOS', config.pageWidth / 2, startY, { align: 'center' }); + pdfDoc.setTextColor(80, 80, 80); + pdfDoc.text('Resumen de la configuración seleccionada'.toUpperCase(), config.pageWidth / 2, startY, { align: 'center' }); startY += 12; // ===== FILA 1: 5 VISTAS ===== - const row1Y = startY; + const row1Y = startY-5; views.slice(0, 5).forEach((view, index) => { const x = MARGIN + index * COL_WIDTH; - renderViewColumnWithHeader(pdfDoc, config, view, x, row1Y, THUMB_SIZE, THUMB_SIZE, ICON_SIZE); + const viewOptions = allOptions.views.get(view.id); + if (viewOptions) { + renderViewColumnWithHeader(pdfDoc, config, view, viewOptions, x, row1Y, COL_WIDTH, THUMB_SIZE, ICON_SIZE); + } }); // ===== FILA 2: GENERALES (izq) + ESPACIO + TOTAL (der) ===== - const row2Y = row1Y + 120; // Ajustar según altura de fila 1 + const row2Y = row1Y + 120; // Columna 1: Acabados Generales (izquierda) - const generalX = MARGIN; - renderGeneralColumnWithHeader(pdfDoc, config, generalX, row2Y, COL_WIDTH, THUMB_SIZE, ICON_SIZE); + //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); // Columnas 2-4: Vacías (espacio) + renderFirmaColumnWithHeader (pdfDoc, config, 70, row2Y, 130) + + // Columna 5: Total (derecha) const totalX = MARGIN + 4 * COL_WIDTH; - renderTotalColumnWithHeader(pdfDoc, config, totalX, row2Y, COL_WIDTH); + renderTotalColumnWithHeader(pdfDoc, config, allOptions, totalX-20, row2Y, COL_WIDTH+16); } + + function renderViewColumnWithHeader( pdfDoc: jsPDF, config: PDFConfig, view: View, + viewOptions: ViewOptions, x: number, y: number, colWidth: number, thumbSize: number, iconSize: number ) { - let cursorY = y; + let cursorY = y+3; // Header de columna - pdfDoc.setFillColor(46, 125, 50); // Verde - pdfDoc.rect(x+2, cursorY - 5, colWidth, 8, 'F'); + pdfDoc.setFillColor(46, 125, 50); + pdfDoc.rect(x+2, cursorY - 5, thumbSize, 8, 'F'); pdfDoc.setFontSize(8); pdfDoc.setFont(config.fonts.bold, "normal"); @@ -352,37 +406,61 @@ } cursorY += thumbSize + 5; - // Opciones de la vista - const viewOptions = getViewOptions(view); + // ✅ USAR OPCIONES PRECALCULADAS viewOptions.options.forEach((option) => { cursorY = renderCompactOption(pdfDoc, config, option, x + 2, cursorY, colWidth - 4, iconSize); }); - // Total de la vista (si tiene opciones con precio) + // Total de la vista if (viewOptions.total > 0) { cursorY += 3; - // Línea separadora pdfDoc.setDrawColor(200, 200, 200); pdfDoc.setLineWidth(0.3); pdfDoc.line(x + 2, cursorY, x + colWidth - 2, cursorY); cursorY += 4; - // Total 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' }); - pdfDoc.setLineWidth(0.2); // Reset + pdfDoc.setLineWidth(0.2); pdfDoc.setTextColor(0, 0, 0); } } + + 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' }); + }); + + + } + + function renderGeneralColumnWithHeader( pdfDoc: jsPDF, config: PDFConfig, + generalOptions: Option[], x: number, y: number, colWidth: number, @@ -392,40 +470,42 @@ let cursorY = y; // Header de columna - pdfDoc.setFillColor(139, 92, 246); // Púrpura para generales - pdfDoc.rect(x, cursorY - 5, colWidth, 8, 'F'); + pdfDoc.setFillColor(157, 186, 113); + pdfDoc.rect(x+2, cursorY - 5, thumbSize, 8, 'F'); pdfDoc.setFontSize(8); pdfDoc.setFont(config.fonts.bold, "normal"); - pdfDoc.setTextColor(255, 255, 255); + pdfDoc.setTextColor(80,80,80); 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' }); }); - cursorY += 8; + cursorY += 10; // Icono o placeholder - pdfDoc.setFillColor(240, 240, 240); + /* + pdfDoc.setFillColor(157, 186, 113); pdfDoc.rect(x + 2, cursorY, thumbSize, thumbSize, 'F'); - // Icono central (opcional) pdfDoc.setFontSize(24); - pdfDoc.setTextColor(180, 180, 180); + pdfDoc.setTextColor(80, 80, 80); pdfDoc.text('🏠', x + colWidth / 2, cursorY + thumbSize / 2 + 4, { align: 'center' }); cursorY += thumbSize + 5; - // Opciones generales - const generalOptions = getGeneralOptions(); + */ + + // ✅ USAR OPCIONES PRECALCULADAS generalOptions.forEach((option) => { - cursorY = renderCompactOption(pdfDoc, config, option, x + 2, cursorY, colWidth - 4, iconSize); + cursorY = renderCompactOption(pdfDoc, config, option, x + 2, cursorY, thumbSize, iconSize); }); } function renderTotalColumnWithHeader( pdfDoc: jsPDF, config: PDFConfig, + allOptions: AllViewOptions, x: number, y: number, colWidth: number @@ -433,70 +513,68 @@ let cursorY = y; // Header de columna - pdfDoc.setFillColor(46, 125, 50); // Verde + pdfDoc.setFillColor(47,101,53); pdfDoc.rect(x, cursorY - 5, colWidth, 8, 'F'); - pdfDoc.setFontSize(8); + pdfDoc.setFontSize(10); pdfDoc.setFont(config.fonts.bold, "normal"); pdfDoc.setTextColor(255, 255, 255); - pdfDoc.text('TOTALES', x + colWidth / 2, cursorY, { align: 'center' }); + pdfDoc.text('TOTALES ESTANCIAS', x + colWidth / 2, cursorY, { align: 'center' }); cursorY += 15; - // Calcular totales - let totalGeneral = 0; + // ✅ USAR TOTALES PRECALCULADOS const viewTotals: { view: string; total: number }[] = []; - views.forEach((view) => { - const vo = getViewOptions(view); - if (vo.total > 0) { - totalGeneral += vo.total; + const vo = allOptions.views.get(view.id); + if (vo && vo.total > 0) { viewTotals.push({ view: view.name.toUpperCase(), total: vo.total }); } }); // Subtotales por vista - pdfDoc.setFontSize(9); + pdfDoc.setFontSize(10); pdfDoc.setFont(config.fonts.regular, "normal"); pdfDoc.setTextColor(80, 80, 80); viewTotals.forEach((item) => { - // Nombre de vista const nameLines = pdfDoc.splitTextToSize(item.view, colWidth * 0.6); nameLines.forEach((line: string, i: number) => { pdfDoc.text(line, x + 2, cursorY + i * 3); }); - // Precio alineado a la derecha pdfDoc.setFont(config.fonts.bold, "normal"); pdfDoc.text(formatPrice(item.total), x + colWidth - 4, cursorY, { align: 'right' }); pdfDoc.setFont(config.fonts.regular, "normal"); - cursorY += Math.max(nameLines.length * 3, 4) + 1; + cursorY += Math.max(nameLines.length * 3, 6) + 1; }); // Línea separadora - cursorY += 3; + cursorY += 5; + /* pdfDoc.setDrawColor(46, 125, 50); pdfDoc.setLineWidth(0.5); pdfDoc.line(x + 2, cursorY, x + colWidth - 2, cursorY); cursorY += 6; + */ // Total general - pdfDoc.setFillColor(46, 125, 50); + pdfDoc.setFillColor(240, 240, 240); pdfDoc.rect(x + 1, cursorY - 4, colWidth - 2, 10, 'F'); pdfDoc.setFontSize(10); pdfDoc.setFont(config.fonts.bold, "normal"); - pdfDoc.setTextColor(255, 255, 255); + pdfDoc.setTextColor(25, 25, 25); pdfDoc.text('TOTAL', x + 4, cursorY + 2); pdfDoc.setFontSize(12); - pdfDoc.text(formatPrice(totalGeneral), x + colWidth - 4, cursorY + 2, { align: 'right' }); + pdfDoc.text(formatPrice(allOptions.totalGeneral), x + colWidth - 4, cursorY + 2, { align: 'right' }); - pdfDoc.setLineWidth(0.2); // Reset + pdfDoc.setLineWidth(0.2); pdfDoc.setTextColor(0, 0, 0); } + function renderCompactOption( pdfDoc: jsPDF, config: PDFConfig, @@ -577,9 +655,19 @@ return cursorY; } - function getGeneralOptions(): Option[] { + + + function getGeneralOptions(tipologia: string): Option[] { const options: Option[] = []; + options.push({ + nombreGrupo: 'TIPO DE VIVIENDA', + nombre: tipologia, + consultar: false, + precio: null, + warning: false, + }); + Object.entries(dataConfig.options.general).forEach(([groupId, group]) => { const currentOptionId = appState.getCurrentOption('general', groupId); if (!currentOptionId) return; @@ -672,15 +760,14 @@ let cursorY = y; // Línea 1: Nombre del grupo + const cY = cursorY; cursorY = renderGroupName(pdfDoc, option.nombreGrupo, x, cursorY, styles.group); + renderPrice(pdfDoc, option, x, cY, maxWidth, styles.price); // Línea 2: Nombre de la opción + icono cursorY = renderOptionName(pdfDoc, option.nombre, x, cursorY, maxWidth, styles.option, icon); - // Línea 3: Precio - cursorY = renderPrice(pdfDoc, option, x, cursorY, maxWidth, styles.price); - - // Línea 4: Warning (si existe) + // Línea 3: Warning (si existe) if (option.warningMessage) { cursorY = renderWarning(pdfDoc, option.warningMessage, x, cursorY, maxWidth, styles.group.font); } @@ -784,13 +871,13 @@ let cursorY = y + 1; pdfDoc.setFont(font, "normal"); - pdfDoc.setFontSize(8); + pdfDoc.setFontSize(10); pdfDoc.setTextColor(200, 100, 0); const lines = pdfDoc.splitTextToSize(message, maxWidth - 5); lines.forEach((line: string) => { - pdfDoc.text('⚠️ ' + line, x, cursorY); - cursorY += 3; + pdfDoc.text(line, x, cursorY); + cursorY += 4; }); pdfDoc.setTextColor(0, 0, 0); // Reset @@ -853,6 +940,7 @@ {message} {downloadState} tipologias={TIPOLOGIAS} + tiposTextos={Object.values(tiposInfo)} {logoCompany} {logoProject} text="Descarga tu Selección" diff --git a/src/lib/cmps/PoDownload.svelte b/src/lib/cmps/PoDownload.svelte index 628bc42..59351b8 100644 --- a/src/lib/cmps/PoDownload.svelte +++ b/src/lib/cmps/PoDownload.svelte @@ -10,7 +10,8 @@ logoProject, logoCompany, text, - tipologias = ['A1', 'P1', 'P2'], + tipologias, + tiposTextos, onTipologiaSelected, onCancel } = $props<{ @@ -20,7 +21,8 @@ logoProject: string; logoCompany?: string; text: string; - tipologias?: string[]; + tipologias: string[]; + tiposTextos: string[]; onTipologiaSelected: (tipologia: string) => void; onCancel: () => void; }>(); @@ -89,8 +91,8 @@ bind:value={selectedTipologia} class="w-full px-4 py-3 border-2 border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-transparent text-lg font-medium text-gray-700 bg-white" > - {#each tipologias as tip} - + {#each tipologias as tip,i} + {/each}