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.
99 lines
3.4 KiB
TypeScript
99 lines
3.4 KiB
TypeScript
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,
|
|
};
|
|
}
|