Files
Voiced/src/App.tsx
T
xavierk fbed18a0bd 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.
2026-10-04 04:21:47 +05:30

109 lines
3.0 KiB
TypeScript

import { useCallback, useEffect, useState } from "react";
import { InlineNotification, Loading, Theme } from "@carbon/react";
import { api } from "./lib/api";
import type { Settings } from "./lib/types";
import { AppShell, type AppView } from "./components/AppShell";
import Onboarding from "./views/Onboarding";
import NewInvoice from "./views/NewInvoice";
import InvoiceHistory from "./views/InvoiceHistory";
import SeriesSettings from "./views/SeriesSettings";
import AppSettings from "./views/AppSettings";
export default function App() {
const [settings, setSettings] = useState<Settings | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [view, setView] = useState<AppView>("new");
const refreshSettings = useCallback(async () => {
try {
const next = await api.getSettings();
setSettings(next);
setError(null);
} catch (e) {
setError(String(e));
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void refreshSettings();
}, [refreshSettings]);
if (loading) {
return (
<Theme theme="g10">
<div className="voiced-empty">
<Loading description="Loading Voiced" withOverlay={false} small />
</div>
</Theme>
);
}
if (error) {
return (
<Theme theme="g10">
<div className="voiced-page">
<InlineNotification
kind="error"
lowContrast
title="Could not start Voiced"
subtitle={error}
hideCloseButton
/>
</div>
</Theme>
);
}
if (!settings) return null;
const theme = settings.theme === "g100" ? "g100" : "g10";
if (!settings.onboarded) {
return (
<Theme theme={theme}>
<Onboarding
initial={settings}
onDone={(next) => {
setSettings(next);
setView("new");
}}
/>
</Theme>
);
}
const toggleTheme = async () => {
const next: Settings = { ...settings, theme: settings.theme === "g100" ? "g10" : "g100" };
setSettings(next);
try {
await api.saveSettings(next);
} catch (e) {
setError(String(e));
}
};
return (
<Theme theme={theme}>
<AppShell view={view} onNavigate={setView} theme={settings.theme} onToggleTheme={toggleTheme}>
{/* Main views stay mounted so a half-typed invoice survives switching tabs. */}
<NewInvoice
settings={settings}
onSettingsChange={refreshSettings}
active={view === "new"}
onActivate={() => setView("new")}
/>
<div hidden={view !== "invoices"}>
<InvoiceHistory settings={settings} active={view === "invoices"} />
</div>
<div hidden={view !== "series"}>
<SeriesSettings active={view === "series"} />
</div>
{view === "settings" && <AppSettings settings={settings} onSaved={refreshSettings} />}
</AppShell>
</Theme>
);
}