- Introduced a centralized `deliveryNoteLetterheadCode` function to ensure all delivery-note PDFs consistently use the Westcott Group letterhead, regardless of the subsidiary. - Updated relevant API routes and services to utilize the new function, removing previous hardcoded fallbacks. - Enhanced the `DriverStop` interface to include timestamps for better reporting capabilities. - Added new fields to track creation timestamps for driver stops, improving data accuracy for outstanding reporting.
164 lines
No EOL
6.1 KiB
TypeScript
164 lines
No EOL
6.1 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { getAdminPocketBase } from "./server/pocketbase-admin";
|
|
import type { BaseRecord } from "./types";
|
|
|
|
/**
|
|
* Letterhead — server-side read model for a row in `letterhead_settings`.
|
|
*
|
|
* Each company / brand has exactly one row. The PDF generator fetches
|
|
* this row fresh on every render so edits in the PB admin UI (logo
|
|
* swap, bank detail fix, terms tweak, accent color change) propagate
|
|
* to the next quote / DN / PO without a code deploy.
|
|
*/
|
|
export type LetterheadCode = "CNL" | "ISHERWOOD" | "WESTSCOTT GROUP OF COMPANIES";
|
|
|
|
export const LETTERHEAD_CODES: readonly LetterheadCode[] = [
|
|
"CNL",
|
|
"ISHERWOOD",
|
|
"WESTSCOTT GROUP OF COMPANIES",
|
|
] as const;
|
|
|
|
/**
|
|
* Canonical letterhead for ALL delivery-note PDFs, regardless of
|
|
* which subsidiary the customer belongs to. The subsidiary name
|
|
* still appears in the PDF meta block (so the printed document
|
|
* tells the driver which subsidiary the delivery is for) but the
|
|
* letterhead/header is always the Westcott Group one.
|
|
*
|
|
* Centralised here so the delivery-notes service and the invoicing
|
|
* API (which both render delivery-note PDFs) can't drift apart
|
|
* again. If the brand ever needs to change, this is the only
|
|
* constant to touch.
|
|
*/
|
|
export const DELIVERY_NOTE_LETTERHEAD_CODE: LetterheadCode =
|
|
"WESTSCOTT GROUP OF COMPANIES";
|
|
|
|
/**
|
|
* Helper that returns the canonical delivery-note letterhead code.
|
|
* Use this anywhere a delivery-note PDF is being rendered so the
|
|
* three-letter `?? "CNL"` fallbacks that used to live in the
|
|
* invoicing API routes stay gone.
|
|
*/
|
|
export function deliveryNoteLetterheadCode(): LetterheadCode {
|
|
return DELIVERY_NOTE_LETTERHEAD_CODE;
|
|
}
|
|
|
|
/**
|
|
* Pick the letterhead code that matches a given company name. The
|
|
* invoice / delivery-note flows don't always carry an explicit
|
|
* letterhead (the operator just hits "Send"), so we infer it from
|
|
* the order's billing company — "ISHERWOOD" or "CNL" in the name
|
|
* wins, otherwise we fall back to `null` and the caller decides
|
|
* (typically defaulting to CNL).
|
|
*/
|
|
export function letterheadForCompany(companyName: string | undefined | null): LetterheadCode | null {
|
|
const upper = (companyName ?? "").trim().toUpperCase();
|
|
if (!upper) return null;
|
|
if (upper.includes("ISHERWOOD")) return "ISHERWOOD";
|
|
if (upper.includes("CNL")) return "CNL";
|
|
return null;
|
|
}
|
|
|
|
export interface Letterhead extends BaseRecord {
|
|
display_name: string;
|
|
address: string;
|
|
phone: string;
|
|
email: string;
|
|
vat_number: string;
|
|
registration_number: string;
|
|
bank_name: string;
|
|
bank_account_number: string;
|
|
bank_branch_code: string;
|
|
terms: string;
|
|
default_markup_percent: number;
|
|
footer_text: string;
|
|
/** PB file token (filename) — combine with `letterheadLogoUrl` for a usable URL. */
|
|
logo: string;
|
|
is_active: boolean;
|
|
company: string;
|
|
/** Hex like "#738224". Set per-company so the PDF table header + accents differ. */
|
|
accent_color: string;
|
|
}
|
|
|
|
/**
|
|
* Look up the letterhead row for a given company code.
|
|
*
|
|
* Matches on the `display_name` field (e.g. "CNL Mining Supplies" matches
|
|
* the `CNL` code). Throws if no row matches — that's a config bug, not a
|
|
* user error, so it surfaces as a 500 in the API.
|
|
*/
|
|
export async function fetchLetterhead(code: LetterheadCode): Promise<Letterhead> {
|
|
if (!LETTERHEAD_CODES.includes(code)) {
|
|
throw new Error(`Unknown letterhead code: ${code}`);
|
|
}
|
|
const pb = await getAdminPocketBase();
|
|
const rows = await pb.collection("letterhead_settings").getFullList<Letterhead>({
|
|
filter: `display_name ~ "${code}"`,
|
|
});
|
|
if (rows.length === 0) {
|
|
throw new Error(
|
|
`No letterhead_settings row matching code ${code}. ` +
|
|
`Seed one in the PB admin UI (Settings → Collections → letterhead_settings).`
|
|
);
|
|
}
|
|
return rows[0];
|
|
}
|
|
|
|
/**
|
|
* Public file URL for the logo. PB's `/api/files/{collection}/{record}/{filename}`
|
|
* endpoint serves without auth, so the PDF renderer can fetch this directly
|
|
* — though we still inline as data URI in the API route to avoid extra
|
|
* round trips during render and to keep the document component pure.
|
|
*/
|
|
export function letterheadLogoUrl(row: Letterhead): string | null {
|
|
if (!row.logo) return null;
|
|
const base = process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090";
|
|
return `${base}/api/files/${row.collectionId}/${row.id}/${row.logo}`;
|
|
}
|
|
|
|
/** Default accent for a code if the row has no `accent_color` set. */
|
|
export function defaultAccentFor(code: LetterheadCode): string {
|
|
return code === "CNL" ? "#738224" : "#7ed957";
|
|
}
|
|
|
|
/**
|
|
* Load the company logo SVG from `public/letterhead/<code>.svg` and
|
|
* return it as a `data:image/svg+xml;base64,...` URI suitable for
|
|
* `<Image src={...}>` in @react-pdf.
|
|
*
|
|
* We embed SVG instead of the original raster (PNG/JPEG) logos because
|
|
* @react-pdf/renderer v4.5.1 has a confirmed byte-corruption bug that
|
|
* replaces every byte ≥ 0x80 in raster image data with `0xFD`. SVG is
|
|
* text-based, so it sidesteps that code path — @react-pdf parses the
|
|
* SVG XML and embeds vector primitives directly into the PDF stream.
|
|
*
|
|
* **Base64, not URL-encoded.** The bundled PDFKit only matches
|
|
* `^data:.+?;base64,(.*)$` when decoding inline data URIs (see
|
|
* `PDFImage.open` in `@react-pdf/pdfkit/lib/pdfkit.js`). A
|
|
* `;charset=utf-8,<urlencoded>` URI is treated as a filesystem path
|
|
* and silently dropped. Base64 it is — yes, the data URI is ~33%
|
|
* bigger than URL-encoded, but for a 1x-per-quote render that's
|
|
* a non-issue.
|
|
*
|
|
* Returns `null` if the file is missing or unreadable — that's a
|
|
* non-fatal degradation (the PDF still renders, just without a logo),
|
|
* so callers should treat it as best-effort.
|
|
*/
|
|
export async function loadLetterheadLogoDataUri(
|
|
code: LetterheadCode
|
|
): Promise<string | null> {
|
|
try {
|
|
const filePath = path.join(
|
|
process.cwd(),
|
|
"public",
|
|
"letterhead",
|
|
`${code.toLowerCase()}.svg`
|
|
);
|
|
const raw = await readFile(filePath);
|
|
return `data:image/svg+xml;base64,${raw.toString("base64")}`;
|
|
} catch (err) {
|
|
console.warn(`[letterhead] could not load logo for ${code}:`, err);
|
|
return null;
|
|
}
|
|
} |