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
+132
View File
@@ -0,0 +1,132 @@
import { useEffect, useState } from "react";
import {
Button,
InlineLoading,
InlineNotification,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@carbon/react";
import { Download, TrashCan } from "@carbon/icons-react";
import { confirm } from "@tauri-apps/plugin-dialog";
import { api } from "../lib/api";
import { formatAmount, formatDate } from "../lib/format";
import { exportInvoicePdf } from "../lib/pdf";
import type { InvoiceSummary, Settings } from "../lib/types";
export default function InvoiceHistory({ settings }: { settings: Settings }) {
const [rows, setRows] = useState<InvoiceSummary[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [busyId, setBusyId] = useState<number | null>(null);
const load = async () => {
try {
setRows(await api.listInvoices());
setError(null);
} catch (e) {
setError(String(e));
} finally {
setLoading(false);
}
};
useEffect(() => {
void load();
}, []);
const onExport = async (id: number) => {
setBusyId(id);
setError(null);
try {
const invoice = await api.getInvoice(id);
await exportInvoicePdf(invoice, settings, null);
} catch (e) {
setError(String(e));
} finally {
setBusyId(null);
}
};
const onDelete = async (row: InvoiceSummary) => {
const ok = await confirm(`Delete invoice ${row.number}? This cannot be undone.`, {
title: "Delete invoice",
kind: "warning",
});
if (!ok) return;
try {
await api.deleteInvoice(row.id);
await load();
} catch (e) {
setError(String(e));
}
};
return (
<div className="voiced-page">
<h1 className="voiced-page__title">Invoices</h1>
<p className="voiced-page__subtitle">Every invoice you have issued, newest first.</p>
{error ? (
<InlineNotification kind="error" lowContrast title="Something went wrong" subtitle={error} />
) : null}
{loading ? (
<InlineLoading description="Loading invoices" />
) : rows.length === 0 ? (
<div className="voiced-empty">No invoices yet. Create one from “New invoice”.</div>
) : (
<TableContainer>
<Table aria-label="Invoices">
<TableHead>
<TableRow>
<TableHeader>Number</TableHeader>
<TableHeader>Date</TableHeader>
<TableHeader>Client</TableHeader>
<TableHeader>Total</TableHeader>
<TableHeader>Status</TableHeader>
<TableHeader>Actions</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<TableRow key={row.id}>
<TableCell>{row.number}</TableCell>
<TableCell>{formatDate(row.invoiceDate)}</TableCell>
<TableCell>{row.clientName || "—"}</TableCell>
<TableCell>₹{formatAmount(row.total)}</TableCell>
<TableCell>{row.status}</TableCell>
<TableCell>
<div className="voiced-header-action">
<Button
kind="ghost"
size="sm"
renderIcon={Download}
onClick={() => onExport(row.id)}
disabled={busyId === row.id}
>
{busyId === row.id ? "Exporting…" : "PDF"}
</Button>
<Button
kind="ghost"
size="sm"
renderIcon={TrashCan}
onClick={() => onDelete(row)}
>
Delete
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</div>
);
}