feat: initialize Tauri backend with SQLite database
This commit is contained in:
34
src-tauri/Cargo.toml
Normal file
34
src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "local-time-tracker"
|
||||
version = "1.0.0"
|
||||
description = "A local time tracking app with invoicing"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "local_time_tracker_lib"
|
||||
crate-type = ["lib", "cdylib", "staticlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["tray-icon"] }
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-fs = "2"
|
||||
tauri-plugin-notification = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
directories = "5"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = "s"
|
||||
strip = true
|
||||
3
src-tauri/build.rs
Normal file
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
28
src-tauri/capabilities/default.json
Normal file
28
src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"identifier": "default",
|
||||
"description": "Default capabilities for the app",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-maximize",
|
||||
"core:window:allow-unmaximize",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-is-maximized",
|
||||
"shell:allow-open",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-save",
|
||||
"dialog:allow-message",
|
||||
"dialog:allow-ask",
|
||||
"dialog:allow-confirm",
|
||||
"fs:allow-read-text-file",
|
||||
"fs:allow-write-text-file",
|
||||
"notification:allow-is-permission-granted",
|
||||
"notification:allow-request-permission",
|
||||
"notification:allow-notify"
|
||||
]
|
||||
}
|
||||
BIN
src-tauri/icons/128x128.png
Normal file
BIN
src-tauri/icons/128x128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 306 B |
BIN
src-tauri/icons/128x128@2x.png
Normal file
BIN
src-tauri/icons/128x128@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 761 B |
BIN
src-tauri/icons/32x32.png
Normal file
BIN
src-tauri/icons/32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 B |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.icns
Normal file
Binary file not shown.
BIN
src-tauri/icons/icon.ico
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src-tauri/icons/icon.png
Normal file
BIN
src-tauri/icons/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 761 B |
355
src-tauri/src/commands.rs
Normal file
355
src-tauri/src/commands.rs
Normal file
@@ -0,0 +1,355 @@
|
||||
use crate::AppState;
|
||||
use rusqlite::params;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::State;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Client {
|
||||
pub id: Option<i64>,
|
||||
pub name: String,
|
||||
pub email: Option<String>,
|
||||
pub address: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Project {
|
||||
pub id: Option<i64>,
|
||||
pub client_id: Option<i64>,
|
||||
pub name: String,
|
||||
pub hourly_rate: f64,
|
||||
pub color: String,
|
||||
pub archived: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Task {
|
||||
pub id: Option<i64>,
|
||||
pub project_id: i64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TimeEntry {
|
||||
pub id: Option<i64>,
|
||||
pub project_id: i64,
|
||||
pub task_id: Option<i64>,
|
||||
pub description: Option<String>,
|
||||
pub start_time: String,
|
||||
pub end_time: Option<String>,
|
||||
pub duration: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Invoice {
|
||||
pub id: Option<i64>,
|
||||
pub client_id: i64,
|
||||
pub invoice_number: String,
|
||||
pub date: String,
|
||||
pub due_date: Option<String>,
|
||||
pub subtotal: f64,
|
||||
pub tax_rate: f64,
|
||||
pub tax_amount: f64,
|
||||
pub discount: f64,
|
||||
pub total: f64,
|
||||
pub notes: Option<String>,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
// Client commands
|
||||
#[tauri::command]
|
||||
pub fn get_clients(state: State<AppState>) -> Result<Vec<Client>, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
let mut stmt = conn.prepare("SELECT id, name, email, address FROM clients ORDER BY name").map_err(|e| e.to_string())?;
|
||||
let clients = stmt.query_map([], |row| {
|
||||
Ok(Client {
|
||||
id: Some(row.get(0)?),
|
||||
name: row.get(1)?,
|
||||
email: row.get(2)?,
|
||||
address: row.get(3)?,
|
||||
})
|
||||
}).map_err(|e| e.to_string())?;
|
||||
clients.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_client(state: State<AppState>, client: Client) -> Result<i64, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
conn.execute(
|
||||
"INSERT INTO clients (name, email, address) VALUES (?1, ?2, ?3)",
|
||||
params![client.name, client.email, client.address],
|
||||
).map_err(|e| e.to_string())?;
|
||||
Ok(conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_client(state: State<AppState>, client: Client) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
conn.execute(
|
||||
"UPDATE clients SET name = ?1, email = ?2, address = ?3 WHERE id = ?4",
|
||||
params![client.name, client.email, client.address, client.id],
|
||||
).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_client(state: State<AppState>, id: i64) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
conn.execute("DELETE FROM clients WHERE id = ?1", params![id]).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Project commands
|
||||
#[tauri::command]
|
||||
pub fn get_projects(state: State<AppState>) -> Result<Vec<Project>, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
let mut stmt = conn.prepare("SELECT id, client_id, name, hourly_rate, color, archived FROM projects ORDER BY name").map_err(|e| e.to_string())?;
|
||||
let projects = stmt.query_map([], |row| {
|
||||
Ok(Project {
|
||||
id: Some(row.get(0)?),
|
||||
client_id: row.get(1)?,
|
||||
name: row.get(2)?,
|
||||
hourly_rate: row.get(3)?,
|
||||
color: row.get(4)?,
|
||||
archived: row.get::<_, i32>(5)? != 0,
|
||||
})
|
||||
}).map_err(|e| e.to_string())?;
|
||||
projects.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_project(state: State<AppState>, project: Project) -> Result<i64, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
conn.execute(
|
||||
"INSERT INTO projects (client_id, name, hourly_rate, color, archived) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params![project.client_id, project.name, project.hourly_rate, project.color, project.archived as i32],
|
||||
).map_err(|e| e.to_string())?;
|
||||
Ok(conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_project(state: State<AppState>, project: Project) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
conn.execute(
|
||||
"UPDATE projects SET client_id = ?1, name = ?2, hourly_rate = ?3, color = ?4, archived = ?5 WHERE id = ?6",
|
||||
params![project.client_id, project.name, project.hourly_rate, project.color, project.archived as i32, project.id],
|
||||
).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_project(state: State<AppState>, id: i64) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
conn.execute("DELETE FROM projects WHERE id = ?1", params![id]).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Task commands
|
||||
#[tauri::command]
|
||||
pub fn get_tasks(state: State<AppState>, project_id: i64) -> Result<Vec<Task>, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
let mut stmt = conn.prepare("SELECT id, project_id, name FROM tasks WHERE project_id = ?1 ORDER BY name").map_err(|e| e.to_string())?;
|
||||
let tasks = stmt.query_map(params![project_id], |row| {
|
||||
Ok(Task {
|
||||
id: Some(row.get(0)?),
|
||||
project_id: row.get(1)?,
|
||||
name: row.get(2)?,
|
||||
})
|
||||
}).map_err(|e| e.to_string())?;
|
||||
tasks.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_task(state: State<AppState>, task: Task) -> Result<i64, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
conn.execute(
|
||||
"INSERT INTO tasks (project_id, name) VALUES (?1, ?2)",
|
||||
params![task.project_id, task.name],
|
||||
).map_err(|e| e.to_string())?;
|
||||
Ok(conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_task(state: State<AppState>, id: i64) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
conn.execute("DELETE FROM tasks WHERE id = ?1", params![id]).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Time entry commands
|
||||
#[tauri::command]
|
||||
pub fn get_time_entries(state: State<AppState>, start_date: Option<String>, end_date: Option<String>) -> Result<Vec<TimeEntry>, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
let query = match (start_date, end_date) {
|
||||
(Some(start), Some(end)) => {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, project_id, task_id, description, start_time, end_time, duration
|
||||
FROM time_entries
|
||||
WHERE date(start_time) >= date(?1) AND date(start_time) <= date(?2)
|
||||
ORDER BY start_time DESC"
|
||||
).map_err(|e| e.to_string())?;
|
||||
stmt.query_map(params![start, end], |row| {
|
||||
Ok(TimeEntry {
|
||||
id: Some(row.get(0)?),
|
||||
project_id: row.get(1)?,
|
||||
task_id: row.get(2)?,
|
||||
description: row.get(3)?,
|
||||
start_time: row.get(4)?,
|
||||
end_time: row.get(5)?,
|
||||
duration: row.get(6)?,
|
||||
})
|
||||
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?
|
||||
}
|
||||
_ => {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, project_id, task_id, description, start_time, end_time, duration
|
||||
FROM time_entries ORDER BY start_time DESC LIMIT 100"
|
||||
).map_err(|e| e.to_string())?;
|
||||
stmt.query_map([], |row| {
|
||||
Ok(TimeEntry {
|
||||
id: Some(row.get(0)?),
|
||||
project_id: row.get(1)?,
|
||||
task_id: row.get(2)?,
|
||||
description: row.get(3)?,
|
||||
start_time: row.get(4)?,
|
||||
end_time: row.get(5)?,
|
||||
duration: row.get(6)?,
|
||||
})
|
||||
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?
|
||||
}
|
||||
};
|
||||
Ok(query)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_time_entry(state: State<AppState>, entry: TimeEntry) -> Result<i64, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
conn.execute(
|
||||
"INSERT INTO time_entries (project_id, task_id, description, start_time, end_time, duration)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![entry.project_id, entry.task_id, entry.description, entry.start_time, entry.end_time, entry.duration],
|
||||
).map_err(|e| e.to_string())?;
|
||||
Ok(conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_time_entry(state: State<AppState>, entry: TimeEntry) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
conn.execute(
|
||||
"UPDATE time_entries SET project_id = ?1, task_id = ?2, description = ?3,
|
||||
start_time = ?4, end_time = ?5, duration = ?6 WHERE id = ?7",
|
||||
params![entry.project_id, entry.task_id, entry.description, entry.start_time, entry.end_time, entry.duration, entry.id],
|
||||
).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_time_entry(state: State<AppState>, id: i64) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
conn.execute("DELETE FROM time_entries WHERE id = ?1", params![id]).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Reports
|
||||
#[tauri::command]
|
||||
pub fn get_reports(state: State<AppState>, start_date: String, end_date: String) -> Result<serde_json::Value, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
// Total hours
|
||||
let total: i64 = conn.query_row(
|
||||
"SELECT COALESCE(SUM(duration), 0) FROM time_entries
|
||||
WHERE date(start_time) >= date(?1) AND date(start_time) <= date(?2)",
|
||||
params![start_date, end_date],
|
||||
|row| row.get(0),
|
||||
).map_err(|e| e.to_string())?;
|
||||
|
||||
// By project
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT p.name, p.color, SUM(t.duration) as total_duration
|
||||
FROM time_entries t
|
||||
JOIN projects p ON t.project_id = p.id
|
||||
WHERE date(t.start_time) >= date(?1) AND date(t.start_time) <= date(?2)
|
||||
GROUP BY p.id
|
||||
ORDER BY total_duration DESC"
|
||||
).map_err(|e| e.to_string())?;
|
||||
|
||||
let by_project: Vec<serde_json::Value> = stmt.query_map(params![start_date, end_date], |row| {
|
||||
Ok(serde_json::json!({
|
||||
"name": row.get::<_, String>(0)?,
|
||||
"color": row.get::<_, String>(1)?,
|
||||
"duration": row.get::<_, i64>(2)?
|
||||
}))
|
||||
}).map_err(|e| e.to_string())?
|
||||
.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"totalSeconds": total,
|
||||
"byProject": by_project
|
||||
}))
|
||||
}
|
||||
|
||||
// Invoice commands
|
||||
#[tauri::command]
|
||||
pub fn create_invoice(state: State<AppState>, invoice: Invoice) -> Result<i64, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
conn.execute(
|
||||
"INSERT INTO invoices (client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
|
||||
params![invoice.client_id, invoice.invoice_number, invoice.date, invoice.due_date,
|
||||
invoice.subtotal, invoice.tax_rate, invoice.tax_amount, invoice.discount,
|
||||
invoice.total, invoice.notes, invoice.status],
|
||||
).map_err(|e| e.to_string())?;
|
||||
Ok(conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_invoices(state: State<AppState>) -> Result<Vec<Invoice>, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status
|
||||
FROM invoices ORDER BY date DESC"
|
||||
).map_err(|e| e.to_string())?;
|
||||
let invoices = stmt.query_map([], |row| {
|
||||
Ok(Invoice {
|
||||
id: Some(row.get(0)?),
|
||||
client_id: row.get(1)?,
|
||||
invoice_number: row.get(2)?,
|
||||
date: row.get(3)?,
|
||||
due_date: row.get(4)?,
|
||||
subtotal: row.get(5)?,
|
||||
tax_rate: row.get(6)?,
|
||||
tax_amount: row.get(7)?,
|
||||
discount: row.get(8)?,
|
||||
total: row.get(9)?,
|
||||
notes: row.get(10)?,
|
||||
status: row.get(11)?,
|
||||
})
|
||||
}).map_err(|e| e.to_string())?;
|
||||
invoices.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// Settings commands
|
||||
#[tauri::command]
|
||||
pub fn get_settings(state: State<AppState>) -> Result<std::collections::HashMap<String, String>, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
let mut stmt = conn.prepare("SELECT key, value FROM settings").map_err(|e| e.to_string())?;
|
||||
let settings = stmt.query_map([], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||
}).map_err(|e| e.to_string())?;
|
||||
|
||||
let mut result = std::collections::HashMap::new();
|
||||
for setting in settings {
|
||||
let (key, value) = setting.map_err(|e| e.to_string())?;
|
||||
result.insert(key, value);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_settings(state: State<AppState>, key: String, value: String) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)",
|
||||
params![key, value],
|
||||
).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
118
src-tauri/src/database.rs
Normal file
118
src-tauri/src/database.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use rusqlite::Connection;
|
||||
|
||||
pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> {
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS clients (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT,
|
||||
address TEXT,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_id INTEGER,
|
||||
name TEXT NOT NULL,
|
||||
hourly_rate REAL DEFAULT 0,
|
||||
color TEXT DEFAULT '#F59E0B',
|
||||
archived INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (client_id) REFERENCES clients(id)
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id)
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS time_entries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER NOT NULL,
|
||||
task_id INTEGER,
|
||||
description TEXT,
|
||||
start_time TEXT NOT NULL,
|
||||
end_time TEXT,
|
||||
duration INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id),
|
||||
FOREIGN KEY (task_id) REFERENCES tasks(id)
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS invoices (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_id INTEGER NOT NULL,
|
||||
invoice_number TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
due_date TEXT,
|
||||
subtotal REAL DEFAULT 0,
|
||||
tax_rate REAL DEFAULT 0,
|
||||
tax_amount REAL DEFAULT 0,
|
||||
discount REAL DEFAULT 0,
|
||||
total REAL DEFAULT 0,
|
||||
notes TEXT,
|
||||
status TEXT DEFAULT 'draft',
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (client_id) REFERENCES clients(id)
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS invoice_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
invoice_id INTEGER NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
quantity REAL DEFAULT 1,
|
||||
rate REAL DEFAULT 0,
|
||||
amount REAL DEFAULT 0,
|
||||
time_entry_id INTEGER,
|
||||
FOREIGN KEY (invoice_id) REFERENCES invoices(id),
|
||||
FOREIGN KEY (time_entry_id) REFERENCES time_entries(id)
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Insert default settings
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO settings (key, value) VALUES ('default_hourly_rate', '50')",
|
||||
[],
|
||||
)?;
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO settings (key, value) VALUES ('idle_detection', 'true')",
|
||||
[],
|
||||
)?;
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO settings (key, value) VALUES ('idle_timeout', '5')",
|
||||
[],
|
||||
)?;
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO settings (key, value) VALUES ('reminder_interval', '30')",
|
||||
[],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
106
src-tauri/src/lib.rs
Normal file
106
src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use rusqlite::Connection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Mutex;
|
||||
use tauri::{Manager, State};
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod database;
|
||||
mod commands;
|
||||
|
||||
pub struct AppState {
|
||||
pub db: Mutex<Connection>,
|
||||
}
|
||||
|
||||
fn get_data_dir() -> PathBuf {
|
||||
let exe_path = std::env::current_exe().unwrap();
|
||||
let exe_dir = exe_path.parent().unwrap();
|
||||
let data_dir = exe_dir.join("data");
|
||||
std::fs::create_dir_all(&data_dir).ok();
|
||||
data_dir
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
env_logger::init();
|
||||
|
||||
let data_dir = get_data_dir();
|
||||
let db_path = data_dir.join("timetracker.db");
|
||||
|
||||
let conn = Connection::open(&db_path).expect("Failed to open database");
|
||||
database::init_db(&conn).expect("Failed to initialize database");
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.manage(AppState { db: Mutex::new(conn) })
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::get_clients,
|
||||
commands::create_client,
|
||||
commands::update_client,
|
||||
commands::delete_client,
|
||||
commands::get_projects,
|
||||
commands::create_project,
|
||||
commands::update_project,
|
||||
commands::delete_project,
|
||||
commands::get_tasks,
|
||||
commands::create_task,
|
||||
commands::delete_task,
|
||||
commands::get_time_entries,
|
||||
commands::create_time_entry,
|
||||
commands::update_time_entry,
|
||||
commands::delete_time_entry,
|
||||
commands::get_reports,
|
||||
commands::create_invoice,
|
||||
commands::get_invoices,
|
||||
commands::get_settings,
|
||||
commands::update_settings,
|
||||
])
|
||||
.setup(|app| {
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||
use tauri::menu::{Menu, MenuItem};
|
||||
|
||||
let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
|
||||
let show = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?;
|
||||
let menu = Menu::with_items(app, &[&show, &quit])?;
|
||||
|
||||
let _tray = TrayIconBuilder::new()
|
||||
.menu(&menu)
|
||||
.menu_on_left_click(false)
|
||||
.on_menu_event(|app, event| {
|
||||
match event.id.as_ref() {
|
||||
"quit" => {
|
||||
app.exit(0);
|
||||
}
|
||||
"show" => {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
window.show().ok();
|
||||
window.set_focus().ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.on_tray_icon_event(|tray, event| {
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Up,
|
||||
..
|
||||
} = event {
|
||||
let app = tray.app_handle();
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
window.show().ok();
|
||||
window.set_focus().ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
.build(app)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
8
src-tauri/src/main.rs
Normal file
8
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
fn main() {
|
||||
local_time_tracker_lib::run();
|
||||
}
|
||||
56
src-tauri/tauri.conf.json
Normal file
56
src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "LocalTimeTracker",
|
||||
"version": "1.0.0",
|
||||
"identifier": "com.localtimetracker.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"frontendDist": "../dist",
|
||||
"devtools": true
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "LocalTimeTracker",
|
||||
"width": 1200,
|
||||
"height": 800,
|
||||
"minWidth": 800,
|
||||
"minHeight": 600,
|
||||
"decorations": false,
|
||||
"transparent": false,
|
||||
"resizable": true,
|
||||
"center": true
|
||||
}
|
||||
],
|
||||
"trayIcon": {
|
||||
"iconPath": "icons/icon.png",
|
||||
"iconAsTemplate": true
|
||||
},
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"windows": {
|
||||
"webviewInstallMode": {
|
||||
"type": "embedBootstrapper"
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"shell": {
|
||||
"open": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user