dev 8 months ago
parent f313a88000
commit f80bc158ca

@ -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<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',
}
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<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
};
}
// ============================================================================
// 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"

@ -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}
<option value={tip}>{tip}</option>
{#each tipologias as tip,i}
<option value={tip}>{tiposTextos[i]}</option>
{/each}
</select>
</div>

Loading…
Cancel
Save

Powered by TurnKey Linux.