Replace the HTML preview with a worker-rendered PDF preview
Rendering moves into a module Web Worker behind a latest-wins RenderClient with a watchdog and a handshake-based main-thread fallback. The preview shows the real PDF through pdf.js at 2x with zoom, page indicator, audit issues popover and a focus modal. A4/Letter and margin controls are frozen into each invoice at issue and can be saved as the default (migration M3). react-pdf and pdf.js are lazy-loaded; the main chunk shrinks from 1.8 MB to 0.5 MB. The HTML twin and its styles are removed.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import type { BankAccount, Invoice, InvoiceItem, Settings, TaxType } from "./types";
|
||||
import { computeLineAmount, computeTotals } from "./invoice";
|
||||
import { amountInWords } from "./numberToWords";
|
||||
|
||||
/** The form fields the preview reads. */
|
||||
export interface PreviewForm {
|
||||
invoiceDate: string;
|
||||
dueDate: string;
|
||||
clientId: number | null;
|
||||
clientName: string;
|
||||
clientAddress: string;
|
||||
clientGstin: string;
|
||||
poNumber: string;
|
||||
placeOfSupply: string;
|
||||
discount: number;
|
||||
taxRate: number;
|
||||
reverseCharge: boolean;
|
||||
signaturePath: string | null;
|
||||
notes: string;
|
||||
items: InvoiceItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The tax treatment Rust derives from the supplier state and the place of supply
|
||||
* (src-tauri/src/gst.rs); the form shows and sends the same value.
|
||||
*/
|
||||
export function deriveTaxType(settings: Settings, placeOfSupply: string): TaxType {
|
||||
if (settings.gstRegistration === "unregistered") return "none";
|
||||
return !placeOfSupply || placeOfSupply === settings.vendorStateCode ? "cgst_sgst" : "igst";
|
||||
}
|
||||
|
||||
/** Mirrors vendor_snapshot() in src-tauri/src/commands/invoice.rs. */
|
||||
export function vendorSnapshotJson(settings: Settings): string {
|
||||
return JSON.stringify({
|
||||
vendorName: settings.vendorName,
|
||||
vendorAddress: settings.vendorAddress,
|
||||
vendorEmail: settings.vendorEmail,
|
||||
vendorPhone: settings.vendorPhone,
|
||||
vendorPan: settings.vendorPan,
|
||||
vendorGstin: settings.gstRegistration === "unregistered" ? "" : settings.vendorGstin.trim(),
|
||||
vendorStateCode: settings.vendorStateCode,
|
||||
logoPath: settings.logoPath,
|
||||
signaturePath: settings.signaturePath,
|
||||
gstRegistration: settings.gstRegistration,
|
||||
signatoryName: settings.signatoryName,
|
||||
signatoryDesignation: settings.signatoryDesignation,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A draft-status Invoice built from the live form, so the real renderer can draw it before anything
|
||||
* is issued. Totals use the same paise rounding Rust stores. `number` is the next number (a peek),
|
||||
* not yet consumed.
|
||||
*/
|
||||
export function formToPreviewInvoice(form: PreviewForm, number: string, settings: Settings, bank: BankAccount | null): Invoice {
|
||||
const taxType = deriveTaxType(settings, form.placeOfSupply);
|
||||
const registered = settings.gstRegistration !== "unregistered";
|
||||
const totals = computeTotals(form.items, form.discount, taxType, form.taxRate);
|
||||
const items = form.items.map((item, i) => ({ ...item, amount: computeLineAmount(item), sortOrder: i }));
|
||||
return {
|
||||
id: 0,
|
||||
number,
|
||||
seriesId: null,
|
||||
invoiceDate: form.invoiceDate,
|
||||
dueDate: form.dueDate,
|
||||
clientId: form.clientId,
|
||||
clientName: form.clientName,
|
||||
clientAddress: form.clientAddress,
|
||||
clientGstin: form.clientGstin,
|
||||
poNumber: form.poNumber,
|
||||
placeOfSupplyStateCode: form.placeOfSupply,
|
||||
subtotal: totals.subtotal,
|
||||
discount: totals.discount,
|
||||
taxType,
|
||||
taxRate: form.taxRate,
|
||||
cgstAmount: totals.cgst,
|
||||
sgstAmount: totals.sgst,
|
||||
igstAmount: totals.igst,
|
||||
total: totals.total,
|
||||
amountInWords: amountInWords(totals.total),
|
||||
bankAccountId: bank?.id ?? null,
|
||||
bankSnapshot: bank ? JSON.stringify(bank) : "",
|
||||
signaturePath: form.signaturePath,
|
||||
notes: form.notes,
|
||||
status: "draft",
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
docType: registered ? "tax_invoice" : "invoice",
|
||||
reverseCharge: registered && form.reverseCharge,
|
||||
vendorSnapshot: vendorSnapshotJson(settings),
|
||||
snapshotOrigin: "issued",
|
||||
cancelledAt: null,
|
||||
cancelReason: "",
|
||||
archivedPdfSha256: null,
|
||||
renderPrefs: "",
|
||||
items,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user