Initial commit: Voiced invoice desktop app

Offline-first invoicing app for Test Vendor, 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:
2026-10-03 23:07:51 +05:30
co-authored by CommandCodeBot
commit 875a12b2e6
121 changed files with 13660 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
use rusqlite::{params, Connection};
use std::path::Path;
/// The full schema. Every statement is idempotent so it doubles as the migration step.
const SCHEMA: &str = r#"
CREATE TABLE IF NOT EXISTS app_settings (
id INTEGER PRIMARY KEY CHECK (id = 1),
vendor_name TEXT NOT NULL DEFAULT '',
vendor_address TEXT NOT NULL DEFAULT '',
vendor_email TEXT NOT NULL DEFAULT '',
vendor_phone TEXT NOT NULL DEFAULT '',
vendor_pan TEXT NOT NULL DEFAULT '',
vendor_gstin TEXT NOT NULL DEFAULT '',
vendor_state_code TEXT NOT NULL DEFAULT '29',
logo_path TEXT,
signature_path TEXT,
default_bank_id INTEGER,
default_tax_rate REAL NOT NULL DEFAULT 18,
default_tax_type TEXT NOT NULL DEFAULT 'cgst_sgst',
payment_terms_days INTEGER NOT NULL DEFAULT 30,
currency TEXT NOT NULL DEFAULT 'INR',
onboarded INTEGER NOT NULL DEFAULT 0,
theme TEXT NOT NULL DEFAULT 'g10'
);
CREATE TABLE IF NOT EXISTS bank_accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT NOT NULL DEFAULT '',
bank_name TEXT NOT NULL DEFAULT '',
account_name TEXT NOT NULL DEFAULT '',
account_no TEXT NOT NULL DEFAULT '',
branch TEXT NOT NULL DEFAULT '',
ifsc TEXT NOT NULL DEFAULT '',
is_default INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS invoice_series (
id INTEGER PRIMARY KEY AUTOINCREMENT,
prefix TEXT NOT NULL,
padding INTEGER NOT NULL DEFAULT 3,
next_number INTEGER NOT NULL DEFAULT 1,
is_active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS clients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
address TEXT NOT NULL DEFAULT '',
gstin TEXT NOT NULL DEFAULT '',
state_code TEXT NOT NULL DEFAULT '',
po_number TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS invoices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
number TEXT NOT NULL UNIQUE,
series_id INTEGER,
invoice_date TEXT NOT NULL,
due_date TEXT NOT NULL DEFAULT '',
client_id INTEGER,
client_name TEXT NOT NULL DEFAULT '',
client_address TEXT NOT NULL DEFAULT '',
client_gstin TEXT NOT NULL DEFAULT '',
po_number TEXT NOT NULL DEFAULT '',
place_of_supply_state_code TEXT NOT NULL DEFAULT '',
subtotal REAL NOT NULL DEFAULT 0,
discount REAL NOT NULL DEFAULT 0,
tax_type TEXT NOT NULL DEFAULT 'none',
tax_rate REAL NOT NULL DEFAULT 0,
cgst_amount REAL NOT NULL DEFAULT 0,
sgst_amount REAL NOT NULL DEFAULT 0,
igst_amount REAL NOT NULL DEFAULT 0,
total REAL NOT NULL DEFAULT 0,
amount_in_words TEXT NOT NULL DEFAULT '',
bank_account_id INTEGER,
bank_snapshot TEXT NOT NULL DEFAULT '',
signature_path TEXT,
notes TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'issued',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS invoice_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
invoice_id INTEGER NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
description TEXT NOT NULL DEFAULT '',
mode TEXT NOT NULL DEFAULT 'fixed',
rate REAL NOT NULL DEFAULT 0,
unit TEXT NOT NULL DEFAULT 'unit',
quantity REAL NOT NULL DEFAULT 1,
amount REAL NOT NULL DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_invoice_items_invoice ON invoice_items(invoice_id);
CREATE INDEX IF NOT EXISTS idx_invoices_created ON invoices(created_at DESC);
"#;
pub fn open(path: &Path) -> rusqlite::Result<Connection> {
let conn = Connection::open(path)?;
conn.pragma_update(None, "journal_mode", "WAL")?;
conn.pragma_update(None, "foreign_keys", "ON")?;
conn.execute_batch(SCHEMA)?;
seed(&conn)?;
Ok(conn)
}
/// Insert the first-run defaults the very first time the app is opened.
/// Vendor details are pre-filled from the owner's existing invoice.
fn seed(conn: &Connection) -> rusqlite::Result<()> {
let settings_count: i64 =
conn.query_row("SELECT COUNT(*) FROM app_settings", [], |r| r.get(0))?;
if settings_count == 0 {
conn.execute(
r#"INSERT INTO app_settings
(id, vendor_name, vendor_address, vendor_email, vendor_phone,
vendor_pan, vendor_gstin, vendor_state_code, default_tax_rate,
default_tax_type, payment_terms_days, currency, onboarded, theme)
VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, '29', 18, 'cgst_sgst', 30, 'INR', 0, 'g10')"#,
params![
"Test Vendor",
"1 Test Road, Bangalore - 560001",
"vendor@example.com",
"9000000000",
"ABCDE1234F",
"",
],
)?;
}
let bank_count: i64 =
conn.query_row("SELECT COUNT(*) FROM bank_accounts", [], |r| r.get(0))?;
if bank_count == 0 {
conn.execute(
r#"INSERT INTO bank_accounts
(label, bank_name, account_name, account_no, branch, ifsc, is_default)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1)"#,
params![
"Primary",
"State Bank of India",
"Test Vendor",
"000000000000",
"000001",
"TEST0000001",
],
)?;
let bank_id = conn.last_insert_rowid();
conn.execute(
"UPDATE app_settings SET default_bank_id = ?1 WHERE id = 1",
params![bank_id],
)?;
}
let series_count: i64 =
conn.query_row("SELECT COUNT(*) FROM invoice_series", [], |r| r.get(0))?;
if series_count == 0 {
let now = chrono::Utc::now().to_rfc3339();
let year = chrono::Local::now().format("%Y").to_string();
conn.execute(
r#"INSERT INTO invoice_series (prefix, padding, next_number, is_active, created_at)
VALUES (?1, 3, 1, 1, ?2)"#,
params![format!("INV/{}-", year), now],
)?;
}
Ok(())
}
/// Format an invoice number from a series definition, e.g. "INV/2026-" + 1 -> "INV/2026-001".
pub fn format_number(prefix: &str, padding: i64, number: i64) -> String {
format!("{}{:0width$}", prefix, number, width = padding.max(1) as usize)
}