Add toasts, Settings tabs, Clients page and item presets (Phase E1)
- Global Carbon toast stack replaces scattered inline notices; errors persist until closed. - Settings is split into eight tabs with one Save and a remembered last tab; Numbering embeds the series settings. - New Clients page with a DataTable, shared add/edit modal, structured addresses, GST category (informational), per-client notes and payment terms. The composed address string is kept for back-compat. Deleting a client warns about invoice count; issued invoices keep their own snapshot. - Migration M7 adds the client columns and item_presets; existing clients with a GSTIN are backfilled to registered_regular. - New invoice form: client ComboBox with type-ahead and inline create, client defaults applied on selection, and an Add from preset control. - Rust tests for M7 (fresh and v6 upgrade), client validation and CRUD, presets CRUD; vitest for the toast store and new helpers. PDF output and fingerprint goldens are unchanged. The new UI has not been run in a webview yet.
This commit is contained in:
@@ -4,7 +4,6 @@ import {
|
||||
Column,
|
||||
Grid,
|
||||
InlineLoading,
|
||||
InlineNotification,
|
||||
NumberInput,
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -19,8 +18,17 @@ import {
|
||||
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 }: { active: boolean }) {
|
||||
export default function SeriesSettings({
|
||||
active: visible,
|
||||
embedded = false,
|
||||
}: {
|
||||
active: boolean;
|
||||
/** Rendered inside Settings: no page wrapper or title. */
|
||||
embedded?: boolean;
|
||||
}) {
|
||||
const toast = useToast();
|
||||
const [active, setActive] = useState<InvoiceSeries | null>(null);
|
||||
const [all, setAll] = useState<InvoiceSeries[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -28,8 +36,6 @@ export default function SeriesSettings({ active: visible }: { active: boolean })
|
||||
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 seeded = useRef(false);
|
||||
|
||||
@@ -44,9 +50,8 @@ export default function SeriesSettings({ active: visible }: { active: boolean })
|
||||
setPadding(current.padding);
|
||||
seeded.current = true;
|
||||
}
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
toast.error("Could not load series", String(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -65,47 +70,34 @@ export default function SeriesSettings({ active: visible }: { active: boolean })
|
||||
);
|
||||
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}`);
|
||||
toast.success("New series started", `Next invoice: ${created.nextInvoiceNumber}`);
|
||||
await load();
|
||||
} catch (e) {
|
||||
setError(String(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="voiced-page">
|
||||
<div className={pageClass}>
|
||||
<InlineLoading description="Loading series" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="voiced-page">
|
||||
<h1 className="voiced-page__title">Invoice series</h1>
|
||||
<p className="voiced-page__subtitle">
|
||||
<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>
|
||||
|
||||
{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
|
||||
|
||||
Reference in New Issue
Block a user