Files
Voiced/src/views/SeriesSettings.tsx
T
soubarna 5b18db3a77 fix: eslint findings
Remove dead code and unused imports, drop useless assignments, and tune
rules in eslint.config.js (underscore-prefixed unused names, intentional BOM/NBSP,
React Compiler rules from eslint-plugin-react-hooks 7 as warnings).
2026-10-05 17:29:03 +05:30

157 lines
4.8 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import {
Button,
Column,
Grid,
InlineLoading,
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";
import { useToast } from "../components/ToastProvider";
export default function SeriesSettings({
active: visible,
embedded = false,
}: {
active: boolean;
/** Rendered inside Settings: no page wrapper or title. */
embedded?: boolean;
}) {
const toast = useToast();
const [, 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 seeded = useRef(false);
const load = async () => {
try {
const [current, list] = await Promise.all([api.getActiveSeries(), api.listSeries()]);
setActive(current);
setAll(list);
// Only seed the form once, so a refetch on re-entry does not overwrite what was typed.
if (current && !seeded.current) {
setPrefix(current.prefix);
setPadding(current.padding);
seeded.current = true;
}
} catch (e) {
toast.error("Could not load series", String(e));
} finally {
setLoading(false);
}
};
// The view stays mounted, so refetch every time it is shown again.
useEffect(() => {
if (visible) void load();
}, [visible]);
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);
try {
const created = await api.startNewSeries(prefix, padding);
toast.success("New series started", `Next invoice: ${created.nextInvoiceNumber}`);
await load();
} catch (e) {
toast.error("Could not start a new series", String(e));
} finally {
setBusy(false);
}
};
const pageClass = embedded ? undefined : "voiced-page";
if (loading) {
return (
<div className={pageClass}>
<InlineLoading description="Loading series" />
</div>
);
}
return (
<div className={pageClass}>
{embedded ? null : <h1 className="voiced-page__title">Invoice series</h1>}
<p className={embedded ? "voiced-muted" : "voiced-page__subtitle"}>
The active series drives the automatic invoice numbers. Start a new series to reset the counter.
</p>
<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>
);
}