Files
Voiced/src/lib/previewInvoice.ts
T
xavierk 74ba04e17f Add Rust logo pipeline: trim, metrics, knockout and Branding settings
Logos (PNG, JPEG, WebP) are decoded, trimmed to their ink box, downscaled
to 1200 px and re-encoded as a content-addressed print PNG plus a white
knockout variant when the logo has alpha; aspect, ink density, mean colour
and kind (wordmark, mark, tall) are stored with the settings and frozen in
the issue snapshot. Legacy logos are derived at startup. The Arun P logo
PDF shrinks from 334 KB to 92 KB. Settings gains a Branding section with
white and dark previews and a business-name toggle (migration M5).
2026-10-04 06:31:30 +05:30

102 lines
3.6 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,
logoKnockoutPath: settings.logoKnockoutPath,
logoMeta: settings.logoMeta,
logoIncludesName: settings.logoIncludesName,
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,
};
}