dev 8 months ago
parent ac8095e3a4
commit f313a88000

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

Loading…
Cancel
Save

Powered by TurnKey Linux.