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
48 lines
No EOL
1.8 KiB
TypeScript
48 lines
No EOL
1.8 KiB
TypeScript
/**
|
|
* Pure formatting helpers shared between the PDF document and any
|
|
* preview / preview-text paths. Kept dependency-free so they can be
|
|
* imported into both client and server contexts without dragging in
|
|
* @react-pdf/renderer.
|
|
*/
|
|
|
|
/** "R 1 234.56" — the South African style used on the existing sample PDFs. */
|
|
export function formatZAR(amount: number): string {
|
|
const safe = Number.isFinite(amount) ? amount : 0;
|
|
const fixed = safe.toFixed(2);
|
|
const [whole, frac] = fixed.split(".");
|
|
// Group thousands with spaces (en-ZA locale style), keep the dot for the decimal.
|
|
const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
|
return `R ${grouped}.${frac}`;
|
|
}
|
|
|
|
/** "22 June 2026" — long form date for the Quote No / Date / Valid until block. */
|
|
export function formatDateLong(iso: string): string {
|
|
if (!iso) return "";
|
|
const d = new Date(iso);
|
|
if (isNaN(d.getTime())) return iso;
|
|
return d.toLocaleDateString("en-GB", {
|
|
day: "numeric",
|
|
month: "long",
|
|
year: "numeric",
|
|
});
|
|
}
|
|
|
|
/** Returns an ISO string for `iso + days`. Used for the "Valid until: +30 days" line. */
|
|
export function addDays(iso: string, days: number): string {
|
|
const d = new Date(iso);
|
|
d.setUTCDate(d.getUTCDate() + days);
|
|
return d.toISOString();
|
|
}
|
|
|
|
/** Sum of selling_price * quantity across an array of quote items. */
|
|
export function quoteSubtotal(items: { quantity: number; selling_price: number }[]): number {
|
|
return items.reduce((sum, it) => sum + it.quantity * it.selling_price, 0);
|
|
}
|
|
|
|
/** ZAR VAT is 15% — round to 2dp so the printed total matches the line items. */
|
|
export function vatAmount(subtotal: number): number {
|
|
return Math.round(subtotal * 0.15 * 100) / 100;
|
|
}
|
|
|
|
/** Quote validity in days — the existing sample PDF shows exactly 30. */
|
|
export const QUOTE_VALID_DAYS = 30; |