WFDASH-WEBAPP/src/lib/pdf/format.ts
SJCoetz 588e9762c2 feat: add quote templates, improved email, new statuses
Add complete quote template functionality, enhanced outbound email sending, new quote statuses, and supporting infrastructure:
- Add new quote statuses (template, sending, mail_failed) with tailored UI styling for status pills
- Implement full template workflows: save real quotes as templates, create new quotes from existing templates
- Add support for custom quote dates and extended validity periods on generated PDF documents
- Build out improved email pipeline with persisted compose fields and retry functionality for failed sends
- Add attachment direction tracking to categorize inbound, outbound, and generated quote attachments
- Fix tooltip component to properly handle Radix `asChild` prop to avoid React DOM attribute errors
- Add comprehensive test coverage, schema migrations, and update sample test and production data
2026-06-24 22:57:01 +02:00

70 lines
No EOL
2.4 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;
}
/** Default validity period for new quotes — the existing sample PDF shows exactly 30. */
export const QUOTE_VALID_DAYS = 30;
/**
* Effective "Date" printed on the quote PDF. Prefers the user-editable
* `quote_date` (lets the office backdate a quote), falls back to the
* PB row's `created` timestamp.
*/
export function effectiveQuoteDate(
quote: { quote_date?: string; created?: string }
): string {
return quote.quote_date || quote.created || "";
}
/**
* Effective validity period in days. Prefers `validity_days` on the
* quote (lets the office extend to 2 months etc.), falls back to the
* package default.
*/
export function effectiveValidityDays(
quote: { validity_days?: number | null }
): number {
return quote.validity_days ?? QUOTE_VALID_DAYS;
}