WFDASH-WEBAPP/src/lib/pdf/format.ts
SJCoetz 714e910949 feat: add branded quote PDF generation and in-app viewer
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
2026-06-23 08:30:38 +02:00

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;