Add invoice drafts, read-only issued state and keyboard shortcuts

Views stay mounted so switching tabs no longer loses a half-typed invoice.
The form autosaves a draft after 1 s, drafts can be resumed or deleted,
and after issuing the form is read-only with Export again, Duplicate and
New invoice actions. A failed export keeps the issued invoice and offers a
retry instead of issuing again. Adds Ctrl+N/S/Enter/L and a shortcut sheet;
F5 and Ctrl+R are blocked in production builds.
This commit is contained in:
2026-10-04 04:21:47 +05:30
parent 5597fb791d
commit fbed18a0bd
8 changed files with 859 additions and 62 deletions
+10 -5
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import {
Button,
Column,
@@ -20,7 +20,7 @@ import { confirm } from "@tauri-apps/plugin-dialog";
import { api } from "../lib/api";
import type { InvoiceSeries } from "../lib/types";
export default function SeriesSettings() {
export default function SeriesSettings({ active: visible }: { active: boolean }) {
const [active, setActive] = useState<InvoiceSeries | null>(null);
const [all, setAll] = useState<InvoiceSeries[]>([]);
const [loading, setLoading] = useState(true);
@@ -31,14 +31,18 @@ export default function SeriesSettings() {
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const seeded = useRef(false);
const load = async () => {
try {
const [current, list] = await Promise.all([api.getActiveSeries(), api.listSeries()]);
setActive(current);
setAll(list);
if (current) {
// 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;
}
setError(null);
} catch (e) {
@@ -48,9 +52,10 @@ export default function SeriesSettings() {
}
};
// The view stays mounted, so refetch every time it is shown again.
useEffect(() => {
void load();
}, []);
if (visible) void load();
}, [visible]);
const startNew = async () => {
const ok = await confirm(