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>
84 lines
2.7 KiB
Rust
84 lines
2.7 KiB
Rust
use crate::db::format_number;
|
|
use crate::models::InvoiceSeries;
|
|
use crate::AppState;
|
|
use rusqlite::{params, Row};
|
|
use tauri::State;
|
|
|
|
fn map_series(row: &Row) -> rusqlite::Result<InvoiceSeries> {
|
|
let prefix: String = row.get(1)?;
|
|
let padding: i64 = row.get(2)?;
|
|
let next_number: i64 = row.get(3)?;
|
|
Ok(InvoiceSeries {
|
|
id: row.get(0)?,
|
|
next_invoice_number: format_number(&prefix, padding, next_number),
|
|
prefix,
|
|
padding,
|
|
next_number,
|
|
is_active: row.get::<_, i64>(4)? != 0,
|
|
created_at: row.get(5)?,
|
|
})
|
|
}
|
|
|
|
const SERIES_SELECT: &str =
|
|
"SELECT id, prefix, padding, next_number, is_active, created_at FROM invoice_series";
|
|
|
|
#[tauri::command]
|
|
pub fn get_active_series(state: State<AppState>) -> Result<Option<InvoiceSeries>, String> {
|
|
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
|
let mut stmt = conn
|
|
.prepare(&format!(
|
|
"{} WHERE is_active = 1 ORDER BY id DESC LIMIT 1",
|
|
SERIES_SELECT
|
|
))
|
|
.map_err(|e| e.to_string())?;
|
|
let mut rows = stmt.query_map([], map_series).map_err(|e| e.to_string())?;
|
|
match rows.next() {
|
|
Some(r) => Ok(Some(r.map_err(|e| e.to_string())?)),
|
|
None => Ok(None),
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn list_series(state: State<AppState>) -> Result<Vec<InvoiceSeries>, String> {
|
|
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
|
let mut stmt = conn
|
|
.prepare(&format!("{} ORDER BY id DESC", SERIES_SELECT))
|
|
.map_err(|e| e.to_string())?;
|
|
let rows = stmt
|
|
.query_map([], map_series)
|
|
.map_err(|e| e.to_string())?
|
|
.collect::<rusqlite::Result<Vec<_>>>()
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(rows)
|
|
}
|
|
|
|
/// Deactivates the current series and starts a fresh one, resetting the counter to 1.
|
|
#[tauri::command]
|
|
pub fn start_new_series(
|
|
state: State<AppState>,
|
|
prefix: String,
|
|
padding: i64,
|
|
) -> Result<InvoiceSeries, String> {
|
|
let prefix = prefix.trim().to_string();
|
|
if prefix.is_empty() {
|
|
return Err("Series prefix cannot be empty".into());
|
|
}
|
|
let padding = padding.clamp(1, 8);
|
|
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
|
conn.execute("UPDATE invoice_series SET is_active = 0", [])
|
|
.map_err(|e| e.to_string())?;
|
|
conn.execute(
|
|
"INSERT INTO invoice_series (prefix, padding, next_number, is_active, created_at)
|
|
VALUES (?1, ?2, 1, 1, ?3)",
|
|
params![prefix, padding, chrono::Utc::now().to_rfc3339()],
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
let id = conn.last_insert_rowid();
|
|
conn.query_row(
|
|
&format!("{} WHERE id = ?1", SERIES_SELECT),
|
|
params![id],
|
|
map_series,
|
|
)
|
|
.map_err(|e| e.to_string())
|
|
}
|