This commit adds full quote PDF generation functionality with per-company letterhead branding: - Server-side PDF rendering using @react-pdf/renderer matching the sample quote layout - In-app modal PDF viewer with print, download, and new-tab controls - Letterhead service to fetch and apply company-specific branding (CNL/ISHERWOOD) - ZAR currency and date formatting utilities for South African quotes - Updated QuoteAttachment type to handle PocketBase's array-based file tokens - Fixed PocketBase attachment URL generation in quote email card - Removed deprecated `included_in_quote` field from QuoteItem and quotes service - Updated quote item creation test to validate draft item logic - Added comprehensive unit tests for PDF rendering and formatting helpers - Added dev scripts for debugging PDF and image rendering issues - Added sample generated PDF files for visual verification - Updated tsconfig to exclude development scripts directory - Added @react-pdf/renderer production dependency - Removed JEI as a valid letterhead option per requirements
65 lines
No EOL
2.4 KiB
TypeScript
65 lines
No EOL
2.4 KiB
TypeScript
/* eslint-disable */
|
|
import * as React from "react";
|
|
import { Document, Page, Image, pdf } from "@react-pdf/renderer";
|
|
import fs from "fs";
|
|
|
|
async function main() {
|
|
// Read a real JPEG from disk
|
|
const buf = fs.readFileSync("/tmp/debug-logo.bin");
|
|
console.log("Input bytes:", buf.length, "first 4:", buf.slice(0, 4).toString("hex"));
|
|
|
|
// Test 1: direct Buffer
|
|
console.log("\n=== Test 1: Buffer src ===");
|
|
const inst1 = pdf(
|
|
<Document><Page size="A4" style={{padding: 40}}>
|
|
<Image src={buf} style={{ width: 110, height: 68 }} />
|
|
</Page></Document>
|
|
);
|
|
const out1 = Buffer.from(await inst1.toString(), "binary");
|
|
console.log(" size:", out1.length, "first bytes:", out1.subarray(0, 8).toString("hex"));
|
|
checkImage(out1, "buf");
|
|
|
|
// Test 2: data URI
|
|
console.log("\n=== Test 2: data URI src ===");
|
|
const dataUri = `data:image/jpeg;base64,${buf.toString("base64")}`;
|
|
const inst2 = pdf(
|
|
<Document><Page size="A4" style={{padding: 40}}>
|
|
<Image src={dataUri} style={{ width: 110, height: 68 }} />
|
|
</Page></Document>
|
|
);
|
|
const out2 = Buffer.from(await inst2.toString(), "binary");
|
|
console.log(" size:", out2.length);
|
|
checkImage(out2, "datauri");
|
|
|
|
// Test 3: URL
|
|
console.log("\n=== Test 3: URL src ===");
|
|
const url = "https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io/api/files/pbc_1010367695/851t7163ks1f9qk/1u8lvckq51g_my2g8gttaf.jpg";
|
|
const inst3 = pdf(
|
|
<Document><Page size="A4" style={{padding: 40}}>
|
|
<Image src={url} style={{ width: 110, height: 68 }} />
|
|
</Page></Document>
|
|
);
|
|
const out3 = Buffer.from(await inst3.toString(), "binary");
|
|
console.log(" size:", out3.length);
|
|
checkImage(out3, "url");
|
|
}
|
|
|
|
function checkImage(pdfBuf: Buffer, label: string) {
|
|
// Find the JPEG stream inside the PDF
|
|
const dctIdx = pdfBuf.indexOf("/DCTDecode");
|
|
if (dctIdx < 0) {
|
|
console.log(` [${label}] no DCTDecode (JPEG) stream found`);
|
|
return;
|
|
}
|
|
// Find the stream that follows
|
|
const streamStart = pdfBuf.indexOf(Buffer.from("stream\n"), dctIdx);
|
|
if (streamStart < 0) {
|
|
console.log(` [${label}] no stream after DCTDecode`);
|
|
return;
|
|
}
|
|
const head = pdfBuf.subarray(streamStart + 7, streamStart + 20);
|
|
console.log(` [${label}] JPEG stream head: ${head.toString("hex")}`);
|
|
console.log(` [${label}] first 2 bytes valid JPEG SOI? ${head[0] === 0xff && head[1] === 0xd8}`);
|
|
}
|
|
|
|
main().catch((err) => { console.error("FAIL:", err); process.exit(1); }); |