feat: add project budgets and rounding override columns
This commit is contained in:
@@ -25,6 +25,9 @@ pub struct Project {
|
||||
pub hourly_rate: f64,
|
||||
pub color: String,
|
||||
pub archived: bool,
|
||||
pub budget_hours: Option<f64>,
|
||||
pub budget_amount: Option<f64>,
|
||||
pub rounding_override: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -115,7 +118,9 @@ pub fn delete_client(state: State<AppState>, id: i64) -> Result<(), String> {
|
||||
#[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 mut stmt = conn.prepare(
|
||||
"SELECT id, client_id, name, hourly_rate, color, archived, budget_hours, budget_amount, rounding_override FROM projects ORDER BY name"
|
||||
).map_err(|e| e.to_string())?;
|
||||
let projects = stmt.query_map([], |row| {
|
||||
Ok(Project {
|
||||
id: Some(row.get(0)?),
|
||||
@@ -124,6 +129,9 @@ pub fn get_projects(state: State<AppState>) -> Result<Vec<Project>, String> {
|
||||
hourly_rate: row.get(3)?,
|
||||
color: row.get(4)?,
|
||||
archived: row.get::<_, i32>(5)? != 0,
|
||||
budget_hours: row.get(6)?,
|
||||
budget_amount: row.get(7)?,
|
||||
rounding_override: row.get(8)?,
|
||||
})
|
||||
}).map_err(|e| e.to_string())?;
|
||||
projects.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
|
||||
@@ -133,8 +141,8 @@ pub fn get_projects(state: State<AppState>) -> Result<Vec<Project>, String> {
|
||||
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],
|
||||
"INSERT INTO projects (client_id, name, hourly_rate, color, archived, budget_hours, budget_amount, rounding_override) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
params![project.client_id, project.name, project.hourly_rate, project.color, project.archived as i32, project.budget_hours, project.budget_amount, project.rounding_override],
|
||||
).map_err(|e| e.to_string())?;
|
||||
Ok(conn.last_insert_rowid())
|
||||
}
|
||||
@@ -143,8 +151,8 @@ pub fn create_project(state: State<AppState>, project: Project) -> Result<i64, S
|
||||
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],
|
||||
"UPDATE projects SET client_id = ?1, name = ?2, hourly_rate = ?3, color = ?4, archived = ?5, budget_hours = ?6, budget_amount = ?7, rounding_override = ?8 WHERE id = ?9",
|
||||
params![project.client_id, project.name, project.hourly_rate, project.color, project.archived as i32, project.budget_hours, project.budget_amount, project.rounding_override, project.id],
|
||||
).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -416,7 +424,7 @@ pub fn export_data(state: State<AppState>) -> Result<serde_json::Value, String>
|
||||
};
|
||||
|
||||
let projects = {
|
||||
let mut stmt = conn.prepare("SELECT id, client_id, name, hourly_rate, color, archived FROM projects").map_err(|e| e.to_string())?;
|
||||
let mut stmt = conn.prepare("SELECT id, client_id, name, hourly_rate, color, archived, budget_hours, budget_amount, rounding_override FROM projects").map_err(|e| e.to_string())?;
|
||||
let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
|
||||
Ok(serde_json::json!({
|
||||
"id": row.get::<_, i64>(0)?,
|
||||
@@ -424,7 +432,10 @@ pub fn export_data(state: State<AppState>) -> Result<serde_json::Value, String>
|
||||
"name": row.get::<_, String>(2)?,
|
||||
"hourly_rate": row.get::<_, f64>(3)?,
|
||||
"color": row.get::<_, String>(4)?,
|
||||
"archived": row.get::<_, i32>(5)? != 0
|
||||
"archived": row.get::<_, i32>(5)? != 0,
|
||||
"budget_hours": row.get::<_, Option<f64>>(6)?,
|
||||
"budget_amount": row.get::<_, Option<f64>>(7)?,
|
||||
"rounding_override": row.get::<_, Option<i32>>(8)?
|
||||
}))
|
||||
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
|
||||
rows
|
||||
@@ -637,3 +648,32 @@ pub fn set_entry_tags(state: State<AppState>, entry_id: i64, tag_ids: Vec<i64>)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_project_budget_status(state: State<AppState>, project_id: i64) -> Result<serde_json::Value, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
let total_seconds: i64 = conn.query_row(
|
||||
"SELECT COALESCE(SUM(duration), 0) FROM time_entries WHERE project_id = ?1",
|
||||
params![project_id],
|
||||
|row| row.get(0),
|
||||
).map_err(|e| e.to_string())?;
|
||||
|
||||
let project_row: (Option<f64>, Option<f64>, f64) = conn.query_row(
|
||||
"SELECT budget_hours, budget_amount, hourly_rate FROM projects WHERE id = ?1",
|
||||
params![project_id],
|
||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
|
||||
).map_err(|e| e.to_string())?;
|
||||
|
||||
let hours_used = total_seconds as f64 / 3600.0;
|
||||
let amount_used = hours_used * project_row.2;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"hours_used": hours_used,
|
||||
"amount_used": amount_used,
|
||||
"budget_hours": project_row.0,
|
||||
"budget_amount": project_row.1,
|
||||
"percent_hours": project_row.0.map(|b| if b > 0.0 { (hours_used / b) * 100.0 } else { 0.0 }),
|
||||
"percent_amount": project_row.1.map(|b| if b > 0.0 { (amount_used / b) * 100.0 } else { 0.0 })
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -46,6 +46,24 @@ pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> {
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Migrate projects table — add budget columns (safe to re-run)
|
||||
let project_migrations = [
|
||||
"ALTER TABLE projects ADD COLUMN budget_hours REAL DEFAULT NULL",
|
||||
"ALTER TABLE projects ADD COLUMN budget_amount REAL DEFAULT NULL",
|
||||
"ALTER TABLE projects ADD COLUMN rounding_override INTEGER DEFAULT NULL",
|
||||
];
|
||||
for sql in &project_migrations {
|
||||
match conn.execute(sql, []) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if !msg.contains("duplicate column") {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
@@ -72,6 +72,7 @@ pub fn run() {
|
||||
commands::delete_tag,
|
||||
commands::get_entry_tags,
|
||||
commands::set_entry_tags,
|
||||
commands::get_project_budget_status,
|
||||
])
|
||||
.setup(|app| {
|
||||
#[cfg(desktop)]
|
||||
|
||||
113
src/stores/projects.ts
Normal file
113
src/stores/projects.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export interface Project {
|
||||
id?: number
|
||||
client_id?: number
|
||||
name: string
|
||||
hourly_rate: number
|
||||
color: string
|
||||
archived: boolean
|
||||
budget_hours?: number | null
|
||||
budget_amount?: number | null
|
||||
rounding_override?: number | null
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
id?: number
|
||||
project_id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export const useProjectsStore = defineStore('projects', () => {
|
||||
const projects = ref<Project[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetchProjects() {
|
||||
loading.value = true
|
||||
try {
|
||||
projects.value = await invoke<Project[]>('get_projects')
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch projects:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createProject(project: Project): Promise<number | null> {
|
||||
try {
|
||||
const id = await invoke<number>('create_project', { project })
|
||||
projects.value.push({ ...project, id: Number(id) })
|
||||
return Number(id)
|
||||
} catch (error) {
|
||||
console.error('Failed to create project:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProject(project: Project): Promise<boolean> {
|
||||
try {
|
||||
await invoke('update_project', { project })
|
||||
const index = projects.value.findIndex(p => p.id === project.id)
|
||||
if (index !== -1) {
|
||||
projects.value[index] = project
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to update project:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteProject(id: number): Promise<boolean> {
|
||||
try {
|
||||
await invoke('delete_project', { id })
|
||||
projects.value = projects.value.filter(p => p.id !== id)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to delete project:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTasks(projectId: number): Promise<Task[]> {
|
||||
try {
|
||||
return await invoke<Task[]>('get_tasks', { projectId })
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch tasks:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async function createTask(task: Task): Promise<number | null> {
|
||||
try {
|
||||
return await invoke<number>('create_task', { task })
|
||||
} catch (error) {
|
||||
console.error('Failed to create task:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTask(id: number): Promise<boolean> {
|
||||
try {
|
||||
await invoke('delete_task', { id })
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to delete task:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
projects,
|
||||
loading,
|
||||
fetchProjects,
|
||||
createProject,
|
||||
updateProject,
|
||||
deleteProject,
|
||||
fetchTasks,
|
||||
createTask,
|
||||
deleteTask
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user