Initial commit: Voiced invoice desktop app

Offline-first invoicing app for Arun P, built by Bongbetic.

- Tauri v2 (Rust) + React/TypeScript + IBM Carbon Design System UI
- SQLite (rusqlite) storage with a persistent invoice-number series
- Guided first-run setup, pre-filled vendor/bank details
- Fixed-amount or rate x quantity line items (per sec/min/hour/session/unit)
- GST as CGST+SGST or IGST, Indian amount-in-words
- Optional signature, with a computer-generated note as fallback
- Print-ready PDF export via @react-pdf/renderer
- Windows cross-compile via cargo-xwin (NSIS installer)

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
2026-10-03 23:07:51 +05:30
co-authored by CommandCodeBot
commit 3af9f3f66f
125 changed files with 13660 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
import { pdf } from "@react-pdf/renderer";
import { save } from "@tauri-apps/plugin-dialog";
import { InvoiceDocument, type InvoicePdfProps } from "../pdf/InvoiceDocument";
import { api } from "./api";
import type { BankAccount, Invoice, Settings } from "./types";
function blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = String(reader.result ?? "");
resolve(result.substring(result.indexOf(",") + 1));
};
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob);
});
}
export async function buildPdfProps(
invoice: Invoice,
settings: Settings,
bank: BankAccount | null,
): Promise<InvoicePdfProps> {
const logoDataUri = settings.logoPath ? await api.readAssetDataUri(settings.logoPath) : null;
const signatureDataUri = invoice.signaturePath
? await api.readAssetDataUri(invoice.signaturePath)
: null;
let resolvedBank = bank;
if (!resolvedBank && invoice.bankSnapshot) {
try {
resolvedBank = JSON.parse(invoice.bankSnapshot) as BankAccount;
} catch {
resolvedBank = null;
}
}
return {
vendor: settings,
bank: resolvedBank,
logoDataUri,
signatureDataUri,
number: invoice.number,
invoiceDate: invoice.invoiceDate,
dueDate: invoice.dueDate,
clientName: invoice.clientName,
clientAddress: invoice.clientAddress,
clientGstin: invoice.clientGstin,
poNumber: invoice.poNumber,
placeOfSupplyStateCode: invoice.placeOfSupplyStateCode,
items: invoice.items,
subtotal: invoice.subtotal,
discount: invoice.discount,
taxType: invoice.taxType,
taxRate: invoice.taxRate,
cgst: invoice.cgstAmount,
sgst: invoice.sgstAmount,
igst: invoice.igstAmount,
total: invoice.total,
amountInWords: invoice.amountInWords,
notes: invoice.notes,
};
}
/** Render props straight to a PDF blob. */
export async function renderPdfBlob(props: InvoicePdfProps): Promise<Blob> {
return pdf(<InvoiceDocument {...props} />).toBlob();
}
/** Render a PDF and prompt the user for a save location. */
export async function exportPdfFromProps(
props: InvoicePdfProps,
fileName: string,
): Promise<string | null> {
const blob = await renderPdfBlob(props);
const dataBase64 = await blobToBase64(blob);
const suggested = fileName.replace(/[^A-Za-z0-9._-]+/g, "-") || "invoice";
const path = await save({
defaultPath: `${suggested}.pdf`,
filters: [{ name: "PDF document", extensions: ["pdf"] }],
});
if (!path) return null;
await api.saveBinaryFile(path, dataBase64);
return path;
}
export async function exportInvoicePdf(
invoice: Invoice,
settings: Settings,
bank: BankAccount | null,
): Promise<string | null> {
const props = await buildPdfProps(invoice, settings, bank);
return exportPdfFromProps(props, invoice.number);
}