Improve flight-finder report links and cron workflow
This commit is contained in:
@@ -83,6 +83,50 @@ function drawBulletList(doc: PDFKit.PDFDocument, items: string[] | undefined, fa
|
||||
doc.moveDown(0.2);
|
||||
}
|
||||
|
||||
type QuoteBookingLink = {
|
||||
label: string;
|
||||
text: string;
|
||||
url: string | null;
|
||||
};
|
||||
|
||||
export function describeQuoteBookingLinks(quote: FlightQuote): QuoteBookingLink[] {
|
||||
return [
|
||||
{
|
||||
label: "Search / book this fare",
|
||||
text: `Search / book this fare: ${quote.displayPriceUsd} via ${quote.source}`,
|
||||
url: quote.bookingLink
|
||||
},
|
||||
{
|
||||
label: "Direct airline booking",
|
||||
text: quote.directBookingUrl
|
||||
? `Direct airline booking: ${quote.airlineName || "Airline site"}`
|
||||
: "Direct airline booking: not captured in this run",
|
||||
url: quote.directBookingUrl || null
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function drawQuoteBookingLinks(doc: PDFKit.PDFDocument, links: QuoteBookingLink[], left: number, width: number): void {
|
||||
for (const linkItem of links) {
|
||||
const startY = doc.y;
|
||||
doc
|
||||
.fillColor("#1E2329")
|
||||
.font("Helvetica-Bold")
|
||||
.fontSize(9.5)
|
||||
.text(`${linkItem.label}:`, left, startY, { width: 130 });
|
||||
doc
|
||||
.fillColor(linkItem.url ? "#0B5EA8" : "#4F5B66")
|
||||
.font("Helvetica")
|
||||
.fontSize(9.5)
|
||||
.text(linkItem.text.replace(`${linkItem.label}: `, ""), left + 132, startY, {
|
||||
width: width - 132,
|
||||
lineGap: 1,
|
||||
...(linkItem.url ? { link: linkItem.url, underline: true } : {})
|
||||
});
|
||||
doc.moveDown(0.25);
|
||||
}
|
||||
}
|
||||
|
||||
function drawKeyValueTable(doc: PDFKit.PDFDocument, rows: Array<[string, string]>): void {
|
||||
const left = doc.page.margins.left;
|
||||
const totalWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
||||
@@ -155,26 +199,57 @@ function drawItineraryCard(
|
||||
const width = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
||||
const top = doc.y;
|
||||
const badge = option.backup ? "Backup" : "Primary";
|
||||
const quoteLines = option.quoteIds.flatMap((quoteId) => {
|
||||
const quoteBlocks = option.quoteIds.flatMap((quoteId) => {
|
||||
const quote = quotesById.get(quoteId);
|
||||
if (!quote) return [`Missing quote reference: ${quoteId}`];
|
||||
if (!quote) {
|
||||
return [
|
||||
{
|
||||
heading: `Missing quote reference: ${quoteId}`,
|
||||
details: [],
|
||||
links: []
|
||||
}
|
||||
];
|
||||
}
|
||||
const passengerLine = quote.passengerGroupIds
|
||||
.map((groupId) => passengerLabels.get(groupId) || groupId)
|
||||
.join(", ");
|
||||
const legLine = legLabels.get(quote.legId) || quote.legId;
|
||||
return [
|
||||
`${legLine}: ${quote.itinerarySummary} (${quote.displayPriceUsd})`,
|
||||
`Passengers: ${passengerLine}`,
|
||||
`Timing: ${quote.departureTimeLocal || "?"} -> ${quote.arrivalTimeLocal || "?"}; ${quote.stopsText || "stops?"}; ${quote.totalDurationText || "duration?"}`,
|
||||
...(quote.notes || []).map((note) => `Note: ${note}`)
|
||||
{
|
||||
heading: `${legLine}: ${quote.itinerarySummary} (${quote.displayPriceUsd})`,
|
||||
details: [
|
||||
`Passengers: ${passengerLine}`,
|
||||
`Timing: ${quote.departureTimeLocal || "?"} -> ${quote.arrivalTimeLocal || "?"}`,
|
||||
`Routing: ${quote.stopsText || "stops?"}; ${quote.layoverText || "layover?"}; ${quote.totalDurationText || "duration?"}`,
|
||||
...(quote.notes || []).map((note) => `Note: ${note}`)
|
||||
],
|
||||
links: describeQuoteBookingLinks(quote)
|
||||
}
|
||||
];
|
||||
});
|
||||
const bodyLines = [
|
||||
`Why it ranked here: ${option.rationale}`,
|
||||
...quoteLines
|
||||
];
|
||||
const bodyText = bodyLines.join("\n");
|
||||
const bodyHeight = doc.heightOfString(bodyText, { width: width - 20, lineGap: 2 }) + 24;
|
||||
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;
|
||||
|
||||
doc.save();
|
||||
doc.roundedRect(left, top, width, 24, 4).fill(option.backup ? "#586E75" : "#1E6B52");
|
||||
@@ -192,10 +267,37 @@ function drawItineraryCard(
|
||||
.fillColor("#1E2329")
|
||||
.font("Helvetica")
|
||||
.fontSize(10)
|
||||
.text(bodyText, left + 10, top + 34, {
|
||||
.text(`Why it ranked here: ${option.rationale}`, left + 10, top + 34, {
|
||||
width: width - 20,
|
||||
lineGap: 2
|
||||
});
|
||||
doc.y = top + 34 + rationaleHeight + 10;
|
||||
for (const block of quoteBlocks) {
|
||||
doc
|
||||
.fillColor("#10384E")
|
||||
.font("Helvetica-Bold")
|
||||
.fontSize(10)
|
||||
.text(block.heading, left + 10, doc.y, {
|
||||
width: width - 20,
|
||||
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);
|
||||
}
|
||||
doc.restore();
|
||||
doc.y = top + 24 + bodyHeight + 10;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user