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>
160 lines
4.7 KiB
TypeScript
160 lines
4.7 KiB
TypeScript
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>
|
|
);
|
|
}
|