Initial commit: Voiced invoice desktop app

Offline-first invoicing app for Test Vendor, 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 875a12b2e6
121 changed files with 13660 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
import { useEffect, useState } from "react";
import {
Button,
Column,
Grid,
InlineLoading,
InlineNotification,
NumberInput,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
Tag,
TextInput,
} from "@carbon/react";
import { confirm } from "@tauri-apps/plugin-dialog";
import { api } from "../lib/api";
import type { InvoiceSeries } from "../lib/types";
export default function SeriesSettings() {
const [active, setActive] = useState<InvoiceSeries | null>(null);
const [all, setAll] = useState<InvoiceSeries[]>([]);
const [loading, setLoading] = useState(true);
const [prefix, setPrefix] = useState(`INV/${new Date().getFullYear()}-`);
const [padding, setPadding] = useState(3);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const load = async () => {
try {
const [current, list] = await Promise.all([api.getActiveSeries(), api.listSeries()]);
setActive(current);
setAll(list);
if (current) {
setPrefix(current.prefix);
setPadding(current.padding);
}
setError(null);
} catch (e) {
setError(String(e));
} finally {
setLoading(false);
}
};
useEffect(() => {
void load();
}, []);
const startNew = async () => {
const ok = await confirm(
`Start a new series from ${prefix}${String(1).padStart(padding, "0")}? ` +
"The current series will be retired and the counter resets to 1.",
{ title: "Start new series", kind: "warning" },
);
if (!ok) return;
setBusy(true);
setError(null);
setNotice(null);
try {
const created = await api.startNewSeries(prefix, padding);
setNotice(`New series started. Next invoice: ${created.nextInvoiceNumber}`);
await load();
} catch (e) {
setError(String(e));
} finally {
setBusy(false);
}
};
if (loading) {
return (
<div className="voiced-page">
<InlineLoading description="Loading series" />
</div>
);
}
return (
<div className="voiced-page">
<h1 className="voiced-page__title">Invoice series</h1>
<p className="voiced-page__subtitle">
The active series drives the automatic invoice numbers. Start a new series to reset the counter.
</p>
{error ? (
<InlineNotification kind="error" lowContrast title="Something went wrong" subtitle={error} />
) : null}
{notice ? (
<InlineNotification
kind="success"
lowContrast
title="Done"
subtitle={notice}
onCloseButtonClick={() => setNotice(null)}
/>
) : null}
<Grid narrow style={{ marginBottom: "2rem" }}>
<Column sm={4} md={4} lg={6}>
<TextInput
id="seriesPrefix"
labelText="New series prefix"
helperText={`Next: ${prefix}${String(1).padStart(padding, "0")}`}
value={prefix}
onChange={(e) => setPrefix(e.target.value)}
/>
</Column>
<Column sm={4} md={4} lg={4}>
<NumberInput
id="seriesPadding"
label="Serial digits"
min={1}
max={8}
value={padding}
onChange={(_e, { value }) => setPadding(Number(value) || 3)}
/>
</Column>
<Column sm={4} md={8} lg={6} style={{ display: "flex", alignItems: "flex-end" }}>
<Button onClick={startNew} disabled={busy || !prefix.trim()}>
{busy ? "Starting…" : "Start new series"}
</Button>
</Column>
</Grid>
<TableContainer title="Series history">
<Table aria-label="Invoice series">
<TableHead>
<TableRow>
<TableHeader>Prefix</TableHeader>
<TableHeader>Next number</TableHeader>
<TableHeader>Digits</TableHeader>
<TableHeader>Status</TableHeader>
<TableHeader>Created</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{all.map((series) => (
<TableRow key={series.id}>
<TableCell>{series.prefix}</TableCell>
<TableCell>{series.nextInvoiceNumber}</TableCell>
<TableCell>{series.padding}</TableCell>
<TableCell>
{series.isActive ? <Tag type="green">Active</Tag> : <Tag type="gray">Retired</Tag>}
</TableCell>
<TableCell>{new Date(series.createdAt).toLocaleDateString("en-GB")}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</div>
);
}