diff --git a/src/lib/cmps/BtDownload.svelte b/src/lib/cmps/BtDownload.svelte index a493e6b..e71f59b 100644 --- a/src/lib/cmps/BtDownload.svelte +++ b/src/lib/cmps/BtDownload.svelte @@ -282,128 +282,168 @@ function addSummaryPage(pdfDoc: jsPDF, config: PDFConfig) { pdfDoc.addPage(); - const COLS = 7; + const COLS = 5; const MARGIN = 10; const COL_WIDTH = (config.pageWidth - 2 * MARGIN) / COLS; const THUMB_SIZE = COL_WIDTH - 4; - const ROW_HEIGHT = 4; const ICON_SIZE = 3; + const ROW_SPACING = 10; let startY = MARGIN; - // Título de la página + // ===== 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' }); - startY += 10; + startY += 12; - // ===== HEADERS DE COLUMNAS ===== - const columns = [ - { title: 'ACABADOS GENERALES', isGeneral: true }, - ...views.slice(0, 5).map(v => ({ title: v.name.toUpperCase(), view: v })), - { title: 'TOTAL', isTotal: true } - ]; - - columns.forEach((col, colIndex) => { - const x = MARGIN + colIndex * COL_WIDTH; - let y = startY; - - // Fondo de header - pdfDoc.setFillColor(46, 125, 50); // Verde - pdfDoc.rect(x, y - 5, COL_WIDTH, 8, 'F'); - - // Título de columna - pdfDoc.setFontSize(8); - pdfDoc.setFont(config.fonts.bold, "normal"); - pdfDoc.setTextColor(255, 255, 255); - const titleLines = pdfDoc.splitTextToSize(col.title, COL_WIDTH - 4); - titleLines.forEach((line: string, i: number) => { - pdfDoc.text(line, x + COL_WIDTH / 2, y + i * 3, { align: 'center' }); - }); + // ===== FILA 1: 5 VISTAS ===== + const row1Y = startY; + 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); }); - startY += 10; + // ===== FILA 2: GENERALES (izq) + ESPACIO + TOTAL (der) ===== + const row2Y = row1Y + 120; // Ajustar según altura de fila 1 - // ===== CONTENIDO DE COLUMNAS ===== + // Columna 1: Acabados Generales (izquierda) + const generalX = MARGIN; + renderGeneralColumnWithHeader(pdfDoc, config, generalX, row2Y, COL_WIDTH, THUMB_SIZE, ICON_SIZE); - // COLUMNA 1: Opciones Generales - renderGeneralOptionsColumn(pdfDoc, config, MARGIN, startY, COL_WIDTH, THUMB_SIZE, ROW_HEIGHT, ICON_SIZE); + // Columnas 2-4: Vacías (espacio) - // COLUMNAS 2-6: Vistas - views.slice(0, 5).forEach((view, index) => { - const x = MARGIN + (index + 1) * COL_WIDTH; - renderViewColumn(pdfDoc, config, view, x, startY, COL_WIDTH, THUMB_SIZE, ROW_HEIGHT, ICON_SIZE); - }); - - // COLUMNA 7: Total - const totalX = MARGIN + 6 * COL_WIDTH; - renderTotalColumn(pdfDoc, config, totalX, startY, COL_WIDTH); + // Columna 5: Total (derecha) + const totalX = MARGIN + 4 * COL_WIDTH; + renderTotalColumnWithHeader(pdfDoc, config, totalX, row2Y, COL_WIDTH); } - function renderGeneralOptionsColumn( + function renderViewColumnWithHeader( pdfDoc: jsPDF, config: PDFConfig, + view: View, x: number, y: number, colWidth: number, thumbSize: number, - rowHeight: number, iconSize: number ) { let cursorY = y; - // Thumbnail placeholder (opcional) - pdfDoc.setFillColor(240, 240, 240); - pdfDoc.rect(x + 2, cursorY, thumbSize, thumbSize, 'F'); - cursorY += thumbSize + 5; + // Header de columna + pdfDoc.setFillColor(46, 125, 50); // Verde + pdfDoc.rect(x+2, cursorY - 5, colWidth, 8, 'F'); - // Obtener opciones generales - const generalOptions = getGeneralOptions(); + 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' }); + }); - generalOptions.forEach((option) => { + 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; + + // Opciones de la vista + const viewOptions = getViewOptions(view); + viewOptions.options.forEach((option) => { cursorY = renderCompactOption(pdfDoc, config, option, x + 2, cursorY, colWidth - 4, iconSize); }); + + // Total de la vista (si tiene opciones con precio) + 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.setTextColor(0, 0, 0); + } } - function renderViewColumn( + function renderGeneralColumnWithHeader( pdfDoc: jsPDF, config: PDFConfig, - view: View, x: number, y: number, colWidth: number, thumbSize: number, - rowHeight: number, iconSize: number ) { let cursorY = y; - // 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; + // Header de columna + pdfDoc.setFillColor(139, 92, 246); // Púrpura para generales + pdfDoc.rect(x, cursorY - 5, colWidth, 8, 'F'); - // Opciones de la vista - const viewOptions = getViewOptions(view); + pdfDoc.setFontSize(8); + pdfDoc.setFont(config.fonts.bold, "normal"); + pdfDoc.setTextColor(255, 255, 255); + 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' }); + }); - viewOptions.options.forEach((option) => { + cursorY += 8; + + // Icono o placeholder + pdfDoc.setFillColor(240, 240, 240); + pdfDoc.rect(x + 2, cursorY, thumbSize, thumbSize, 'F'); + + // Icono central (opcional) + pdfDoc.setFontSize(24); + pdfDoc.setTextColor(180, 180, 180); + pdfDoc.text('🏠', x + colWidth / 2, cursorY + thumbSize / 2 + 4, { align: 'center' }); + + cursorY += thumbSize + 5; + + // Opciones generales + const generalOptions = getGeneralOptions(); + generalOptions.forEach((option) => { cursorY = renderCompactOption(pdfDoc, config, option, x + 2, cursorY, colWidth - 4, iconSize); }); } - function renderTotalColumn( + function renderTotalColumnWithHeader( pdfDoc: jsPDF, config: PDFConfig, x: number, y: number, colWidth: number ) { - let cursorY = y + 60; + let cursorY = y; - // Calcular total de todas las vistas + // Header de columna + pdfDoc.setFillColor(46, 125, 50); // Verde + pdfDoc.rect(x, cursorY - 5, colWidth, 8, 'F'); + + pdfDoc.setFontSize(8); + pdfDoc.setFont(config.fonts.bold, "normal"); + pdfDoc.setTextColor(255, 255, 255); + pdfDoc.text('TOTALES', x + colWidth / 2, cursorY, { align: 'center' }); + + cursorY += 15; + + // Calcular totales let totalGeneral = 0; const viewTotals: { view: string; total: number }[] = []; @@ -411,33 +451,50 @@ const vo = getViewOptions(view); if (vo.total > 0) { totalGeneral += vo.total; - viewTotals.push({ view: view.name, total: vo.total }); + viewTotals.push({ view: view.name.toUpperCase(), total: vo.total }); } }); // Subtotales por vista - pdfDoc.setFontSize(7); + pdfDoc.setFontSize(9); pdfDoc.setFont(config.fonts.regular, "normal"); - pdfDoc.setTextColor(100, 100, 100); + pdfDoc.setTextColor(80, 80, 80); viewTotals.forEach((item) => { - pdfDoc.text(item.view, x + 2, cursorY); + // 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' }); - cursorY += 4; + pdfDoc.setFont(config.fonts.regular, "normal"); + + cursorY += Math.max(nameLines.length * 3, 4) + 1; }); // Línea separadora - cursorY += 2; - pdfDoc.setDrawColor(200, 200, 200); + cursorY += 3; + pdfDoc.setDrawColor(46, 125, 50); + pdfDoc.setLineWidth(0.5); pdfDoc.line(x + 2, cursorY, x + colWidth - 2, cursorY); - cursorY += 5; + cursorY += 6; // Total general - pdfDoc.setFontSize(11); + pdfDoc.setFillColor(46, 125, 50); + pdfDoc.rect(x + 1, cursorY - 4, colWidth - 2, 10, 'F'); + + pdfDoc.setFontSize(10); pdfDoc.setFont(config.fonts.bold, "normal"); - pdfDoc.setTextColor(46, 125, 50); - pdfDoc.text('TOTAL', x + 2, cursorY); - pdfDoc.text(formatPrice(totalGeneral), x + colWidth - 4, cursorY, { align: 'right' }); + pdfDoc.setTextColor(255, 255, 255); + pdfDoc.text('TOTAL', x + 4, cursorY + 2); + pdfDoc.setFontSize(12); + pdfDoc.text(formatPrice(totalGeneral), x + colWidth - 4, cursorY + 2, { align: 'right' }); + + pdfDoc.setLineWidth(0.2); // Reset + pdfDoc.setTextColor(0, 0, 0); } function renderCompactOption( @@ -450,48 +507,71 @@ iconSize: number ): number { let cursorY = y; + const iconMargin = 1; + const iconSpace = (option.warning || option.consultar) ? iconSize + iconMargin : 0; - // Grupo (pequeño, gris, uppercase) + // 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; + } + + // 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) pdfDoc.setFont(config.fonts.regular, "normal"); pdfDoc.setFontSize(6); pdfDoc.setTextColor(120, 120, 120); - const groupLines = pdfDoc.splitTextToSize(option.nombreGrupo.toUpperCase(), maxWidth); - groupLines.forEach((line: string) => { - pdfDoc.text(line, x, cursorY); + + // 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); + } + cursorY += 2.5; - }); + } + + // 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; + } - // Opción (pequeño, bold, negro) + icono + // 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) pdfDoc.setFont(config.fonts.bold, "normal"); pdfDoc.setFontSize(7); pdfDoc.setTextColor(30, 30, 30); - const iconYPosition = cursorY; - const optionLines = pdfDoc.splitTextToSize(option.nombre, maxWidth - (option.warning ? iconSize + 1 : 0)); + const optionLines = pdfDoc.splitTextToSize(option.nombre, maxWidth - iconSpace); + optionLines.forEach((line: string) => { - pdfDoc.text(line, x, cursorY); + pdfDoc.text(line, x + iconSpace, cursorY); cursorY += 3; }); - // Icono de warning - if (option.warning && advertenciaIconB64) { - const iconX = x + maxWidth - iconSize; - pdfDoc.addImage(advertenciaIconB64, 'PNG', iconX, iconYPosition - iconSize + 1, iconSize, iconSize); - } - - // Precio (pequeño, alineado derecha) - const precio = option.consultar ? 'consultar' : option.precio; - if (precio !== null && precio !== undefined) { - pdfDoc.setFontSize(7); - - const { text, color } = getPriceDisplay(precio); - const priceWidth = pdfDoc.getTextWidth(text); - - pdfDoc.setTextColor(color.r, color.g, color.b); - pdfDoc.text(text, x + maxWidth - priceWidth, cursorY); - cursorY += 3; - } - cursorY += 2; // Espacio entre opciones pdfDoc.setTextColor(0, 0, 0); return cursorY;