@voyantjs/products 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ export type GenerateProductPdfResult = {
3
+ pdfBytes: Uint8Array;
4
+ filename: string;
5
+ sizeBytes: number;
6
+ };
7
+ export declare function generateProductPdf(db: PostgresJsDatabase, productId: string): Promise<GenerateProductPdfResult>;
8
+ //# sourceMappingURL=generate-pdf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-pdf.d.ts","sourceRoot":"","sources":["../../src/tasks/generate-pdf.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAKjE,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,UAAU,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,kBAAkB,EACtB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,wBAAwB,CAAC,CAuHnC"}
@@ -0,0 +1,102 @@
1
+ import { asc, eq } from "drizzle-orm";
2
+ import { PDFDocument, rgb, StandardFonts } from "pdf-lib";
3
+ import { productDayServices, productDays, products } from "../schema.js";
4
+ export async function generateProductPdf(db, productId) {
5
+ // 1. Fetch product
6
+ const [product] = await db
7
+ .select()
8
+ .from(products)
9
+ .where(eq(products.id, productId))
10
+ .limit(1);
11
+ if (!product) {
12
+ throw new Error(`Product not found: ${productId}`);
13
+ }
14
+ // 2. Fetch days with services
15
+ const days = await db
16
+ .select()
17
+ .from(productDays)
18
+ .where(eq(productDays.productId, productId))
19
+ .orderBy(asc(productDays.dayNumber));
20
+ const daysWithServices = await Promise.all(days.map(async (day) => {
21
+ const services = await db
22
+ .select()
23
+ .from(productDayServices)
24
+ .where(eq(productDayServices.dayId, day.id))
25
+ .orderBy(asc(productDayServices.sortOrder));
26
+ return { ...day, services };
27
+ }));
28
+ // 3. Generate PDF
29
+ const pdfDoc = await PDFDocument.create();
30
+ const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
31
+ const boldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
32
+ const PAGE_WIDTH = 595.28;
33
+ const PAGE_HEIGHT = 841.89;
34
+ const MARGIN = 50;
35
+ const LINE_HEIGHT = 16;
36
+ let page = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
37
+ let y = PAGE_HEIGHT - MARGIN;
38
+ function ensureSpace(needed) {
39
+ if (y - needed < MARGIN) {
40
+ page = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
41
+ y = PAGE_HEIGHT - MARGIN;
42
+ }
43
+ }
44
+ function drawText(text, options) {
45
+ const f = options.font ?? font;
46
+ const size = options.size ?? 10;
47
+ const x = options.x ?? MARGIN;
48
+ ensureSpace(LINE_HEIGHT);
49
+ page.drawText(text, { x, y, size, font: f, color: rgb(0.1, 0.1, 0.1) });
50
+ y -= LINE_HEIGHT;
51
+ }
52
+ // Title
53
+ drawText(product.name, { font: boldFont, size: 20 });
54
+ y -= 8;
55
+ // Status & dates
56
+ if (product.startDate || product.endDate) {
57
+ drawText(`Dates: ${product.startDate ?? "TBD"} — ${product.endDate ?? "TBD"}`, { size: 10 });
58
+ }
59
+ if (product.pax) {
60
+ drawText(`Travelers: ${product.pax}`, { size: 10 });
61
+ }
62
+ if (product.sellAmountCents != null) {
63
+ const amount = (product.sellAmountCents / 100).toFixed(2);
64
+ drawText(`Total: ${amount} ${product.sellCurrency}`, { font: boldFont, size: 12 });
65
+ }
66
+ if (product.description) {
67
+ y -= 8;
68
+ drawText(product.description, { size: 10 });
69
+ }
70
+ y -= 16;
71
+ // Days
72
+ for (const day of daysWithServices) {
73
+ ensureSpace(LINE_HEIGHT * 3);
74
+ // Day header
75
+ const dayTitle = day.title ? `Day ${day.dayNumber}: ${day.title}` : `Day ${day.dayNumber}`;
76
+ drawText(dayTitle, { font: boldFont, size: 13 });
77
+ if (day.location) {
78
+ drawText(`Location: ${day.location}`, { size: 9 });
79
+ }
80
+ if (day.description) {
81
+ drawText(day.description, { size: 9 });
82
+ }
83
+ // Services
84
+ for (const svc of day.services) {
85
+ ensureSpace(LINE_HEIGHT * 2);
86
+ const costStr = `${(svc.costAmountCents / 100).toFixed(2)} ${svc.costCurrency}`;
87
+ const qty = svc.quantity > 1 ? ` x${svc.quantity}` : "";
88
+ drawText(` • ${svc.name} (${svc.serviceType})${qty} — ${costStr}`, { size: 9 });
89
+ if (svc.notes) {
90
+ drawText(` ${svc.notes}`, { size: 8 });
91
+ }
92
+ }
93
+ y -= 8;
94
+ }
95
+ // 4. Serialize to bytes
96
+ const pdfBytes = await pdfDoc.save();
97
+ return {
98
+ pdfBytes,
99
+ filename: `${product.name.replace(/[^a-zA-Z0-9]/g, "_")}.pdf`,
100
+ sizeBytes: pdfBytes.length,
101
+ };
102
+ }
@@ -0,0 +1,2 @@
1
+ export { generateProductPdf, type GenerateProductPdfResult } from "./generate-pdf.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tasks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,KAAK,wBAAwB,EAAE,MAAM,mBAAmB,CAAA"}
@@ -0,0 +1 @@
1
+ export { generateProductPdf } from "./generate-pdf.js";