Polish flight-finder PDF layout

This commit is contained in:
2026-03-30 19:54:35 -05:00
parent 809a3955e5
commit b2e97a3036
2 changed files with 145 additions and 65 deletions

View File

@@ -48,7 +48,26 @@ function formatDateWindow(leg: FlightLegWindow): string {
return "Not provided";
}
function ensurePageSpace(doc: PDFKit.PDFDocument, requiredHeight: number): void {
const bottomLimit = doc.page.height - doc.page.margins.bottom;
if (doc.y + requiredHeight > bottomLimit) {
doc.addPage();
}
}
const WORKSPACE_REPORTS_PREFIX = "/Users/stefano/.openclaw/workspace/reports/";
export function formatArtifactsRoot(value: string): string {
const trimmed = String(value || "").trim();
if (!trimmed) return "Not provided";
if (trimmed.startsWith(WORKSPACE_REPORTS_PREFIX)) {
return `reports/${trimmed.slice(WORKSPACE_REPORTS_PREFIX.length)}`;
}
return path.basename(trimmed) || trimmed;
}
function drawSectionHeader(doc: PDFKit.PDFDocument, title: string): void {
ensurePageSpace(doc, 28);
const x = doc.page.margins.left;
const y = doc.y;
const width = doc.page.width - doc.page.margins.left - doc.page.margins.right;
@@ -68,6 +87,11 @@ function drawSectionHeader(doc: PDFKit.PDFDocument, title: string): void {
function drawBulletList(doc: PDFKit.PDFDocument, items: string[] | undefined, fallback = "Not provided."): void {
const lines = bulletLines(items, fallback);
for (const line of lines) {
const lineHeight = doc.heightOfString(line, {
width: doc.page.width - doc.page.margins.left - doc.page.margins.right - 14,
lineGap: 2
});
ensurePageSpace(doc, lineHeight + 12);
const startY = doc.y;
doc.circle(doc.page.margins.left + 4, startY + 6, 1.5).fill("#10384E");
doc
@@ -108,6 +132,14 @@ export function describeQuoteBookingLinks(quote: FlightQuote): QuoteBookingLink[
function drawQuoteBookingLinks(doc: PDFKit.PDFDocument, links: QuoteBookingLink[], left: number, width: number): void {
for (const linkItem of links) {
const lineHeight = Math.max(
doc.heightOfString(`${linkItem.label}:`, { width: 130 }),
doc.heightOfString(linkItem.text.replace(`${linkItem.label}: `, ""), {
width: width - 132,
lineGap: 1
})
);
ensurePageSpace(doc, lineHeight + 8);
const startY = doc.y;
doc
.fillColor("#1E2329")
@@ -134,10 +166,11 @@ function drawKeyValueTable(doc: PDFKit.PDFDocument, rows: Array<[string, string]
const valueWidth = totalWidth - keyWidth;
for (const [key, value] of rows) {
const startY = doc.y;
const keyHeight = doc.heightOfString(key, { width: keyWidth - 12 });
const valueHeight = doc.heightOfString(value, { width: valueWidth - 12 });
const rowHeight = Math.max(keyHeight, valueHeight) + 12;
ensurePageSpace(doc, rowHeight + 8);
const startY = doc.y;
doc.save();
doc.rect(left, startY, keyWidth, rowHeight).fill("#EEF3F7");
@@ -197,8 +230,7 @@ function drawItineraryCard(
): void {
const left = doc.page.margins.left;
const width = doc.page.width - doc.page.margins.left - doc.page.margins.right;
const top = doc.y;
const badge = option.backup ? "Backup" : "Primary";
const badge = option.backup ? "Backup option" : "Primary recommendation";
const quoteBlocks = option.quoteIds.flatMap((quoteId) => {
const quote = quotesById.get(quoteId);
if (!quote) {
@@ -227,79 +259,39 @@ function drawItineraryCard(
}
];
});
const estimateHeight = quoteBlocks.reduce((sum, block) => {
const headingHeight = doc.heightOfString(block.heading, { width: width - 20, lineGap: 2 });
const detailsHeight = block.details.reduce(
(acc, line) => acc + doc.heightOfString(line, { width: width - 44, lineGap: 2 }) + 4,
0
);
const linksHeight = block.links.reduce(
(acc, link) =>
acc +
Math.max(
doc.heightOfString(`${link.label}:`, { width: 130 }),
doc.heightOfString(link.text.replace(`${link.label}: `, ""), { width: width - 152, lineGap: 1 })
) +
4,
0
);
return sum + headingHeight + detailsHeight + linksHeight + 18;
}, 0);
const rationaleHeight = doc.heightOfString(`Why it ranked here: ${option.rationale}`, {
width: width - 20,
lineGap: 2
});
const bodyHeight = rationaleHeight + estimateHeight + 24;
ensurePageSpace(doc, 28);
const top = doc.y;
doc.save();
doc.roundedRect(left, top, width, 24, 4).fill(option.backup ? "#586E75" : "#1E6B52");
doc.roundedRect(left, top, width, 22, 4).fill(option.backup ? "#586E75" : "#1E6B52");
doc
.fillColor("white")
.font("Helvetica-Bold")
.fontSize(11)
.text(`${badge}: ${option.title} (${formatUsd(option.totalPriceUsd)})`, left + 10, top + 7, {
width: width - 20
});
doc
.roundedRect(left, top + 24, width, bodyHeight, 4)
.fillAndStroke("#F8FAFC", "#C7D0D9");
doc
.fillColor("#1E2329")
.font("Helvetica")
.fontSize(10)
.text(`Why it ranked here: ${option.rationale}`, left + 10, top + 34, {
width: width - 20,
lineGap: 2
});
doc.y = top + 34 + rationaleHeight + 10;
.fontSize(10.5)
.text(`${badge}: ${option.title}`, left + 10, top + 6.5, { width: width - 20 });
doc.restore();
doc.y = top + 28;
drawKeyValueTable(doc, [
["Recommendation total", formatUsd(option.totalPriceUsd)],
["Why it ranked here", option.rationale]
]);
for (const block of quoteBlocks) {
ensurePageSpace(doc, 22);
doc
.fillColor("#10384E")
.font("Helvetica-Bold")
.fontSize(10)
.text(block.heading, left + 10, doc.y, {
width: width - 20,
.text(block.heading, left, doc.y, {
width,
lineGap: 2
});
doc.moveDown(0.2);
for (const line of block.details) {
const startY = doc.y;
doc.circle(left + 16, startY + 6, 1.4).fill("#10384E");
doc
.fillColor("#1E2329")
.font("Helvetica")
.fontSize(9.5)
.text(line, left + 24, startY, {
width: width - 44,
lineGap: 2
});
doc.moveDown(0.2);
}
drawQuoteBookingLinks(doc, block.links, left + 10, width - 20);
doc.moveDown(0.35);
drawBulletList(doc, block.details);
drawQuoteBookingLinks(doc, block.links, left, width);
doc.moveDown(0.5);
}
doc.restore();
doc.y = top + 24 + bodyHeight + 10;
doc.moveDown(0.2);
}
export async function renderFlightReportPdf(
@@ -349,7 +341,7 @@ export async function renderFlightReportPdf(
.font("Helvetica")
.fontSize(10)
.text(
`Market: ${payload.marketCountryUsed || "Default market"} | Fresh search: ${payload.searchExecution.completedAt}`
`Market: ${payload.marketCountryUsed || "Default market"} | Fresh search completed: ${payload.searchExecution.completedAt}`
);
doc.moveDown();
@@ -363,7 +355,7 @@ export async function renderFlightReportPdf(
request.preferences.maxLayoverHours != null ? `Layover <= ${request.preferences.maxLayoverHours}h` : "Layover open"
].join(" | ")],
["Market-country mode", request.preferences.marketCountry || "Off"],
["Search artifacts", payload.searchExecution.artifactsRoot]
["Search artifacts", formatArtifactsRoot(payload.searchExecution.artifactsRoot)]
]);
drawSectionHeader(doc, "Legs And Travelers");