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
70 lines
No EOL
2.4 KiB
TypeScript
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;
|
|
} |