|
|
|
|
@ -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;
|
|
|
|
|
|