Initial commit: Voiced invoice desktop app
Offline-first invoicing app for Arun P, 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>
This commit is contained in:
+98
@@ -0,0 +1,98 @@
|
||||
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}>
|
||||
{view === "new" && <NewInvoice settings={settings} onSettingsChange={refreshSettings} />}
|
||||
{view === "invoices" && <InvoiceHistory settings={settings} />}
|
||||
{view === "series" && <SeriesSettings />}
|
||||
{view === "settings" && <AppSettings settings={settings} onSaved={refreshSettings} />}
|
||||
</AppShell>
|
||||
</Theme>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user