- Global Carbon toast stack replaces scattered inline notices; errors persist until closed. - Settings is split into eight tabs with one Save and a remembered last tab; Numbering embeds the series settings. - New Clients page with a DataTable, shared add/edit modal, structured addresses, GST category (informational), per-client notes and payment terms. The composed address string is kept for back-compat. Deleting a client warns about invoice count; issued invoices keep their own snapshot. - Migration M7 adds the client columns and item_presets; existing clients with a GSTIN are backfilled to registered_regular. - New invoice form: client ComboBox with type-ahead and inline create, client defaults applied on selection, and an Add from preset control. - Rust tests for M7 (fresh and v6 upgrade), client validation and CRUD, presets CRUD; vitest for the toast store and new helpers. PDF output and fingerprint goldens are unchanged. The new UI has not been run in a webview yet.
297 lines
8.0 KiB
TypeScript
297 lines
8.0 KiB
TypeScript
export type TaxType = "none" | "cgst_sgst" | "igst";
|
|
export type LineMode = "fixed" | "rate";
|
|
export type LineUnit = "second" | "minute" | "hour" | "session" | "unit";
|
|
|
|
export interface Settings {
|
|
vendorName: string;
|
|
vendorAddress: string;
|
|
vendorEmail: string;
|
|
vendorPhone: string;
|
|
vendorPan: string;
|
|
vendorGstin: string;
|
|
vendorStateCode: string;
|
|
logoPath: string | null;
|
|
signaturePath: string | null;
|
|
defaultBankId: number | null;
|
|
defaultTaxRate: number;
|
|
defaultTaxType: TaxType;
|
|
paymentTermsDays: number;
|
|
currency: string;
|
|
onboarded: boolean;
|
|
theme: string;
|
|
gstRegistration: GstRegistration;
|
|
defaultHsnSac: string;
|
|
signatoryName: string;
|
|
signatoryDesignation: string;
|
|
/** The user's default page setup (RenderPrefsV1 JSON); empty means the built-in defaults. */
|
|
renderPrefs: string;
|
|
/** Folder of the last export; kept by the backend (saving settings does not change it). */
|
|
lastExportDir?: string;
|
|
/** The untouched logo file the derived images came from. */
|
|
logoOriginalPath: string | null;
|
|
/** White-ink variant for dark surfaces; only for logos with transparency. */
|
|
logoKnockoutPath: string | null;
|
|
/** LogoMeta JSON from the import pipeline; empty until the logo has been through it. */
|
|
logoMeta: string;
|
|
/** Whether the logo already contains the business name. Automatic means a wordmark does. */
|
|
logoIncludesName: "auto" | "yes" | "no";
|
|
}
|
|
|
|
export interface BankAccount {
|
|
id: number | null;
|
|
label: string;
|
|
bankName: string;
|
|
accountName: string;
|
|
accountNo: string;
|
|
branch: string;
|
|
ifsc: string;
|
|
isDefault: boolean;
|
|
}
|
|
|
|
export interface InvoiceSeries {
|
|
id: number;
|
|
prefix: string;
|
|
padding: number;
|
|
nextNumber: number;
|
|
isActive: boolean;
|
|
createdAt: string;
|
|
nextInvoiceNumber: string;
|
|
}
|
|
|
|
export type GstCategory = "registered_regular" | "unregistered" | "composition" | "sez" | "overseas";
|
|
|
|
/** Informational only; the tax heads still derive from supplier state and place of supply. */
|
|
export const GST_CATEGORY_LABELS: Record<GstCategory, string> = {
|
|
registered_regular: "Registered (regular)",
|
|
unregistered: "Unregistered",
|
|
composition: "Composition",
|
|
sez: "SEZ",
|
|
overseas: "Overseas",
|
|
};
|
|
|
|
export interface Client {
|
|
id: number | null;
|
|
name: string;
|
|
/** The composed multi-line address; what invoices store and print. */
|
|
address: string;
|
|
gstin: string;
|
|
/** Doubles as the address state and the default place of supply. */
|
|
stateCode: string;
|
|
poNumber: string;
|
|
createdAt: string;
|
|
addressLine1: string;
|
|
addressLine2: string;
|
|
city: string;
|
|
pincode: string;
|
|
gstCategory: GstCategory;
|
|
/** Notes / payment terms text copied onto a new invoice when the client is picked. */
|
|
defaultNotes: string;
|
|
/** Null means the settings default. */
|
|
paymentTermsDays: number | null;
|
|
/** Read-only: how many invoices reference this client. */
|
|
invoiceCount?: number;
|
|
}
|
|
|
|
export const blankClient = (): Client => ({
|
|
id: null,
|
|
name: "",
|
|
address: "",
|
|
gstin: "",
|
|
stateCode: "",
|
|
poNumber: "",
|
|
createdAt: "",
|
|
addressLine1: "",
|
|
addressLine2: "",
|
|
city: "",
|
|
pincode: "",
|
|
gstCategory: "unregistered",
|
|
defaultNotes: "",
|
|
paymentTermsDays: null,
|
|
});
|
|
|
|
/** A saved line item. `ratePaise` is the amount for "fixed" and the per-unit rate for "rate". */
|
|
export interface ItemPreset {
|
|
id: number | null;
|
|
description: string;
|
|
hsnSac: string;
|
|
mode: LineMode;
|
|
unit: LineUnit;
|
|
ratePaise: number;
|
|
sortOrder: number;
|
|
createdAt: string;
|
|
}
|
|
|
|
export const blankPreset = (): ItemPreset => ({
|
|
id: null,
|
|
description: "",
|
|
hsnSac: "",
|
|
mode: "fixed",
|
|
unit: "unit",
|
|
ratePaise: 0,
|
|
sortOrder: 0,
|
|
createdAt: "",
|
|
});
|
|
|
|
export interface InvoiceItem {
|
|
id?: number | null;
|
|
description: string;
|
|
mode: LineMode;
|
|
rate: number;
|
|
unit: LineUnit;
|
|
quantity: number;
|
|
amount: number;
|
|
sortOrder: number;
|
|
/** Optional so older callers that build items by hand keep compiling; stored as "" when absent. */
|
|
hsnSac?: string;
|
|
}
|
|
|
|
export type GstRegistration = "unregistered" | "regular";
|
|
|
|
/** What the webview sends to issue an invoice; totals, tax and words are derived in Rust. */
|
|
export interface InvoiceInput {
|
|
invoiceDate: string;
|
|
dueDate: string;
|
|
clientId: number | null;
|
|
clientName: string;
|
|
clientAddress: string;
|
|
clientGstin: string;
|
|
poNumber: string;
|
|
placeOfSupplyStateCode: string;
|
|
discount: number;
|
|
taxType: TaxType;
|
|
taxRate: number;
|
|
reverseCharge: boolean;
|
|
bankAccountId: number | null;
|
|
signaturePath: string | null;
|
|
notes: string;
|
|
saveClient: boolean;
|
|
draftId: number | null;
|
|
items: InvoiceItem[];
|
|
}
|
|
|
|
export type InvoiceStatus = "draft" | "issued" | "cancelled";
|
|
|
|
/** An invoice as stored: all amounts are derived on the Rust side. */
|
|
export interface Invoice {
|
|
id: number;
|
|
number: string;
|
|
seriesId: number | null;
|
|
invoiceDate: string;
|
|
dueDate: string;
|
|
clientId: number | null;
|
|
clientName: string;
|
|
clientAddress: string;
|
|
clientGstin: string;
|
|
poNumber: string;
|
|
placeOfSupplyStateCode: string;
|
|
subtotal: number;
|
|
discount: number;
|
|
taxType: TaxType;
|
|
taxRate: number;
|
|
cgstAmount: number;
|
|
sgstAmount: number;
|
|
igstAmount: number;
|
|
total: number;
|
|
amountInWords: string;
|
|
bankAccountId: number | null;
|
|
bankSnapshot: string;
|
|
signaturePath: string | null;
|
|
notes: string;
|
|
status: InvoiceStatus;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
docType: "invoice" | "tax_invoice";
|
|
reverseCharge: boolean;
|
|
/** JSON of the supplier details at issue time; empty when unknown. */
|
|
vendorSnapshot: string;
|
|
snapshotOrigin: string;
|
|
cancelledAt: string | null;
|
|
cancelReason: string;
|
|
archivedPdfSha256: string | null;
|
|
/** RenderPrefsV1 JSON frozen at issue; empty when none was stored. */
|
|
renderPrefs: string;
|
|
/** Layout fingerprint of the archived PDF; null when nothing is archived. */
|
|
archivedFingerprint?: string | null;
|
|
archivedAt?: string | null;
|
|
items: InvoiceItem[];
|
|
}
|
|
|
|
export interface DraftSummary {
|
|
id: number;
|
|
updatedAt: string;
|
|
clientName: string | null;
|
|
total: number | null;
|
|
}
|
|
|
|
export interface InvoiceSummary {
|
|
id: number;
|
|
number: string;
|
|
invoiceDate: string;
|
|
dueDate: string;
|
|
clientName: string;
|
|
total: number;
|
|
status: InvoiceStatus;
|
|
createdAt: string;
|
|
}
|
|
|
|
export const EMPTY_ITEM: InvoiceItem = {
|
|
description: "",
|
|
mode: "fixed",
|
|
rate: 0,
|
|
unit: "unit",
|
|
quantity: 1,
|
|
amount: 0,
|
|
sortOrder: 0,
|
|
hsnSac: "",
|
|
};
|
|
|
|
/** GST state codes, same list as STATES in src-tauri/src/gst.rs (from India Compliance). */
|
|
export const INDIAN_STATES: { code: string; name: string }[] = [
|
|
{ code: "01", name: "Jammu and Kashmir" },
|
|
{ code: "02", name: "Himachal Pradesh" },
|
|
{ code: "03", name: "Punjab" },
|
|
{ code: "04", name: "Chandigarh" },
|
|
{ code: "05", name: "Uttarakhand" },
|
|
{ code: "06", name: "Haryana" },
|
|
{ code: "07", name: "Delhi" },
|
|
{ code: "08", name: "Rajasthan" },
|
|
{ code: "09", name: "Uttar Pradesh" },
|
|
{ code: "10", name: "Bihar" },
|
|
{ code: "11", name: "Sikkim" },
|
|
{ code: "12", name: "Sampleachal Pradesh" },
|
|
{ code: "13", name: "Nagaland" },
|
|
{ code: "14", name: "Manipur" },
|
|
{ code: "15", name: "Mizoram" },
|
|
{ code: "16", name: "Tripura" },
|
|
{ code: "17", name: "Meghalaya" },
|
|
{ code: "18", name: "Assam" },
|
|
{ code: "19", name: "West Bengal" },
|
|
{ code: "20", name: "Jharkhand" },
|
|
{ code: "21", name: "Odisha" },
|
|
{ code: "22", name: "Chhattisgarh" },
|
|
{ code: "23", name: "Madhya Pradesh" },
|
|
{ code: "24", name: "Gujarat" },
|
|
{ code: "26", name: "Dadra and Nagar Haveli and Daman and Diu" },
|
|
{ code: "27", name: "Maharashtra" },
|
|
{ code: "29", name: "Karnataka" },
|
|
{ code: "30", name: "Goa" },
|
|
{ code: "31", name: "Lakshadweep Islands" },
|
|
{ code: "32", name: "Kerala" },
|
|
{ code: "33", name: "Tamil Nadu" },
|
|
{ code: "34", name: "Puducherry" },
|
|
{ code: "35", name: "Andaman and Nicobar Islands" },
|
|
{ code: "36", name: "Telangana" },
|
|
{ code: "37", name: "Andhra Pradesh" },
|
|
{ code: "38", name: "Ladakh" },
|
|
{ code: "96", name: "Other Countries" },
|
|
{ code: "97", name: "Other Territory" },
|
|
];
|
|
|
|
export const UNIT_LABELS: Record<LineUnit, string> = {
|
|
second: "per second",
|
|
minute: "per minute",
|
|
hour: "per hour",
|
|
session: "per session",
|
|
unit: "per unit",
|
|
};
|