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
+324
View File
@@ -0,0 +1,324 @@
import { Document, Font, Image, Page, StyleSheet, Text, View } from "@react-pdf/renderer";
import { UNIT_LABELS, type BankAccount, type InvoiceItem, type Settings, type TaxType } from "../lib/types";
import { computeLineAmount } from "../lib/invoice";
Font.register({
family: "IBM Plex Sans",
fonts: [
{ src: "/fonts/IBMPlexSans-Light.woff", fontWeight: 300 },
{ src: "/fonts/IBMPlexSans-Regular.woff", fontWeight: 400 },
{ src: "/fonts/IBMPlexSans-Italic.woff", fontWeight: 400, fontStyle: "italic" },
{ src: "/fonts/IBMPlexSans-Medium.woff", fontWeight: 500 },
{ src: "/fonts/IBMPlexSans-SemiBold.woff", fontWeight: 600 },
{ src: "/fonts/IBMPlexSans-Bold.woff", fontWeight: 700 },
],
});
Font.register({
family: "IBM Plex Mono",
fonts: [
{ src: "/fonts/IBMPlexMono-Regular.woff", fontWeight: 400 },
{ src: "/fonts/IBMPlexMono-SemiBold.woff", fontWeight: 600 },
],
});
const amountFmt = new Intl.NumberFormat("en-IN", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const money = (n: number) => `${"\u20B9"}${amountFmt.format(Number.isFinite(n) ? n : 0)}`;
const dateFmt = (iso: string) => {
if (!iso) return "\u2014";
const d = new Date(`${iso}T00:00:00`);
if (Number.isNaN(d.getTime())) return iso;
return d.toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" });
};
export interface InvoicePdfProps {
vendor: Settings;
bank: BankAccount | null;
logoDataUri: string | null;
signatureDataUri: string | null;
number: string;
invoiceDate: string;
dueDate: string;
clientName: string;
clientAddress: string;
clientGstin: string;
poNumber: string;
placeOfSupplyStateCode: string;
items: InvoiceItem[];
subtotal: number;
discount: number;
taxType: TaxType;
taxRate: number;
cgst: number;
sgst: number;
igst: number;
total: number;
amountInWords: string;
notes: string;
}
const styles = StyleSheet.create({
page: {
fontFamily: "IBM Plex Sans",
fontSize: 9.5,
color: "#161616",
paddingTop: 40,
paddingBottom: 48,
paddingHorizontal: 40,
lineHeight: 1.45,
},
header: { flexDirection: "row", justifyContent: "space-between", alignItems: "flex-start" },
vendor: { flexDirection: "row", maxWidth: "58%" },
logo: { maxHeight: 54, maxWidth: 170, marginRight: 14, objectFit: "contain" },
vendorName: { fontSize: 18, fontWeight: 600, marginBottom: 3 },
vendorLine: { color: "#393939" },
meta: { textAlign: "right", maxWidth: "40%" },
title: { fontSize: 18, fontWeight: 600, letterSpacing: 1.5, textTransform: "uppercase", marginBottom: 8 },
metaRow: { flexDirection: "row", justifyContent: "space-between", gap: 14 },
metaLabel: { color: "#525252" },
metaValue: { fontWeight: 600 },
divider: { borderBottomWidth: 1, borderBottomColor: "#e0e0e0", marginTop: 16, marginBottom: 16 },
split: { flexDirection: "row", justifyContent: "space-between", gap: 24 },
blockLabel: { fontSize: 7.5, letterSpacing: 1, textTransform: "uppercase", color: "#525252", marginBottom: 3 },
strong: { fontWeight: 600 },
block: { maxWidth: "48%" },
table: { marginTop: 22, borderTopWidth: 1.4, borderTopColor: "#161616" },
tr: { flexDirection: "row", borderBottomWidth: 0.6, borderBottomColor: "#e0e0e0", paddingVertical: 6 },
th: { fontSize: 7.5, letterSpacing: 0.8, textTransform: "uppercase", color: "#525252", paddingBottom: 4 },
colDesc: { flex: 5 },
colRate: { flex: 2, textAlign: "right" },
colQty: { flex: 2, textAlign: "right" },
colTotal: { flex: 2, textAlign: "right" },
totalsWrap: { flexDirection: "row", justifyContent: "flex-end", marginTop: 12 },
totals: { width: 250 },
totalsRow: { flexDirection: "row", justifyContent: "space-between", paddingVertical: 2.5 },
grand: {
flexDirection: "row",
justifyContent: "space-between",
borderTopWidth: 1.4,
borderTopColor: "#161616",
marginTop: 4,
paddingTop: 6,
fontSize: 13,
fontWeight: 600,
},
words: { marginTop: 12, fontStyle: "italic" },
bottom: { flexDirection: "row", justifyContent: "space-between", alignItems: "flex-end", marginTop: 34 },
signature: { textAlign: "right", minWidth: 180 },
signatureImg: { maxHeight: 58, maxWidth: 190, objectFit: "contain", marginLeft: "auto" },
signatureName: {
marginTop: 4,
paddingTop: 3,
borderTopWidth: 0.8,
borderTopColor: "#8d8d8d",
fontWeight: 600,
},
fineprint: { fontSize: 7.5, color: "#6f6f6f", textAlign: "right", marginTop: 6, maxWidth: 220 },
footer: {
position: "absolute",
bottom: 24,
left: 40,
right: 40,
flexDirection: "row",
justifyContent: "space-between",
borderTopWidth: 0.6,
borderTopColor: "#e0e0e0",
paddingTop: 6,
fontSize: 7.5,
color: "#6f6f6f",
},
});
export function InvoiceDocument(props: InvoicePdfProps) {
const {
vendor,
bank,
logoDataUri,
signatureDataUri,
number,
invoiceDate,
dueDate,
clientName,
clientAddress,
clientGstin,
poNumber,
placeOfSupplyStateCode,
items,
subtotal,
discount,
taxType,
taxRate,
cgst,
sgst,
igst,
total,
amountInWords,
notes,
} = props;
const isTaxInvoice = taxType !== "none";
const contact = [vendor.vendorEmail, vendor.vendorPhone].filter(Boolean).join(" \u00B7 ");
return (
<Document title={`${isTaxInvoice ? "Tax Invoice" : "Invoice"} ${number}`} author={vendor.vendorName}>
<Page size="A4" style={styles.page}>
<View style={styles.header}>
<View style={styles.vendor}>
{logoDataUri ? <Image style={styles.logo} src={logoDataUri} /> : null}
<View>
<Text style={styles.vendorName}>{vendor.vendorName || "Vendor"}</Text>
<Text style={styles.vendorLine}>{vendor.vendorAddress}</Text>
{contact ? <Text style={styles.vendorLine}>{contact}</Text> : null}
{vendor.vendorPan ? <Text style={styles.vendorLine}>PAN: {vendor.vendorPan}</Text> : null}
<Text style={styles.vendorLine}>GSTIN: {vendor.vendorGstin || "NA"}</Text>
</View>
</View>
<View style={styles.meta}>
<Text style={styles.title}>{isTaxInvoice ? "Tax Invoice" : "Invoice"}</Text>
<View style={styles.metaRow}>
<Text style={styles.metaLabel}>Invoice No.</Text>
<Text style={styles.metaValue}>{number}</Text>
</View>
<View style={styles.metaRow}>
<Text style={styles.metaLabel}>Date</Text>
<Text style={styles.metaValue}>{dateFmt(invoiceDate)}</Text>
</View>
{dueDate ? (
<View style={styles.metaRow}>
<Text style={styles.metaLabel}>Due Date</Text>
<Text style={styles.metaValue}>{dateFmt(dueDate)}</Text>
</View>
) : null}
</View>
</View>
<View style={styles.divider} />
<View style={styles.split}>
<View style={styles.block}>
<Text style={styles.blockLabel}>Billed To</Text>
<Text style={styles.strong}>{clientName || "\u2014"}</Text>
{clientAddress ? <Text>{clientAddress}</Text> : null}
{clientGstin ? <Text>GSTIN: {clientGstin}</Text> : null}
{placeOfSupplyStateCode ? <Text>Place of Supply: {placeOfSupplyStateCode}</Text> : null}
</View>
<View style={styles.block}>
{poNumber ? (
<>
<Text style={styles.blockLabel}>PO Number</Text>
<Text style={styles.strong}>{poNumber}</Text>
</>
) : null}
</View>
</View>
<View style={styles.table}>
<View style={[styles.tr, { borderBottomColor: "#161616" }]}>
<Text style={[styles.th, styles.colDesc]}>Description</Text>
<Text style={[styles.th, styles.colRate]}>Unit Price</Text>
<Text style={[styles.th, styles.colQty]}>Qty</Text>
<Text style={[styles.th, styles.colTotal]}>Total</Text>
</View>
{items.map((item, i) => (
<View style={styles.tr} key={item.id ?? i}>
<Text style={styles.colDesc}>{item.description || "\u2014"}</Text>
<Text style={styles.colRate}>
{item.mode === "rate" ? `${money(item.rate)} ${UNIT_LABELS[item.unit]}` : "\u2014"}
</Text>
<Text style={styles.colQty}>{item.mode === "rate" ? item.quantity : 1}</Text>
<Text style={styles.colTotal}>{money(computeLineAmount(item))}</Text>
</View>
))}
</View>
<View style={styles.totalsWrap}>
<View style={styles.totals}>
<View style={styles.totalsRow}>
<Text>Subtotal</Text>
<Text>{money(subtotal)}</Text>
</View>
{discount > 0 ? (
<View style={styles.totalsRow}>
<Text>Discount</Text>
<Text>- {money(discount)}</Text>
</View>
) : null}
{taxType === "cgst_sgst" ? (
<>
<View style={styles.totalsRow}>
<Text>CGST ({(taxRate / 2).toFixed(2)}%)</Text>
<Text>{money(cgst)}</Text>
</View>
<View style={styles.totalsRow}>
<Text>SGST ({(taxRate / 2).toFixed(2)}%)</Text>
<Text>{money(sgst)}</Text>
</View>
</>
) : null}
{taxType === "igst" ? (
<View style={styles.totalsRow}>
<Text>IGST ({taxRate.toFixed(2)}%)</Text>
<Text>{money(igst)}</Text>
</View>
) : null}
<View style={styles.grand}>
<Text>Total</Text>
<Text>{money(total)}</Text>
</View>
</View>
</View>
<Text style={styles.words}>Amount in words: {amountInWords}</Text>
{notes ? (
<View style={{ marginTop: 14 }}>
<Text style={styles.blockLabel}>Notes</Text>
<Text>{notes}</Text>
</View>
) : null}
<View style={styles.bottom}>
<View style={[styles.block, { maxWidth: "55%" }]}>
{bank ? (
<>
<Text style={styles.blockLabel}>Pay To</Text>
<Text style={styles.strong}>{bank.accountName || vendor.vendorName}</Text>
<Text>{bank.bankName}</Text>
{bank.accountNo ? <Text>Account No.: {bank.accountNo}</Text> : null}
{bank.branch ? <Text>Branch: {bank.branch}</Text> : null}
{bank.ifsc ? <Text>IFSC: {bank.ifsc}</Text> : null}
</>
) : null}
</View>
<View style={styles.signature}>
{signatureDataUri ? (
<>
<Image style={styles.signatureImg} src={signatureDataUri} />
<Text style={styles.signatureName}>{vendor.vendorName}</Text>
</>
) : (
<>
<Text style={styles.signatureName}>{vendor.vendorName}</Text>
<Text style={styles.fineprint}>
Computer-generated invoice; no signature is required.
</Text>
</>
)}
</View>
</View>
<View style={styles.footer} fixed>
<Text>{vendor.vendorName}</Text>
<Text>{"Generated with Voiced \u2014 a Bongbetic product"}</Text>
</View>
</Page>
</Document>
);
}