feat(storage): extend task layer for subtasks — get_task_by_id, insert_subtask, list_subtasks, complete_subtask_and_check_parent
This commit is contained in:
@@ -286,7 +286,7 @@ pub async fn insert_task(
|
||||
|
||||
pub async fn list_tasks(pool: &SqlitePool) -> Result<Vec<TaskRow>> {
|
||||
let rows =
|
||||
sqlx::query("SELECT id, text, bucket, list_id, effort, done, done_at, created_at, source_transcript_id FROM tasks ORDER BY created_at DESC")
|
||||
sqlx::query("SELECT id, text, bucket, list_id, effort, done, done_at, created_at, source_transcript_id, parent_task_id FROM tasks WHERE parent_task_id IS NULL ORDER BY created_at DESC")
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| KonError::StorageError(format!("List tasks failed: {e}")))?;
|
||||
@@ -303,10 +303,115 @@ pub async fn list_tasks(pool: &SqlitePool) -> Result<Vec<TaskRow>> {
|
||||
done_at: r.get("done_at"),
|
||||
created_at: r.get("created_at"),
|
||||
source_transcript_id: r.get("source_transcript_id"),
|
||||
parent_task_id: r.get("parent_task_id"),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_task_by_id(pool: &SqlitePool, id: &str) -> Result<Option<TaskRow>> {
|
||||
let row = sqlx::query(
|
||||
"SELECT id, text, bucket, list_id, effort, done, done_at, created_at, \
|
||||
source_transcript_id, parent_task_id FROM tasks WHERE id = ?"
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(|e| KonError::StorageError(format!("Get task failed: {e}")))?;
|
||||
|
||||
Ok(row.map(|r| TaskRow {
|
||||
id: r.get("id"),
|
||||
text: r.get("text"),
|
||||
bucket: r.get("bucket"),
|
||||
list_id: r.get("list_id"),
|
||||
effort: r.get("effort"),
|
||||
done: r.get("done"),
|
||||
done_at: r.get("done_at"),
|
||||
created_at: r.get("created_at"),
|
||||
source_transcript_id: r.get("source_transcript_id"),
|
||||
parent_task_id: r.get("parent_task_id"),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn insert_subtask(
|
||||
pool: &SqlitePool,
|
||||
id: &str,
|
||||
text: &str,
|
||||
parent_task_id: &str,
|
||||
) -> Result<()> {
|
||||
sqlx::query(
|
||||
"INSERT INTO tasks (id, text, bucket, parent_task_id) VALUES (?, ?, 'inbox', ?)"
|
||||
)
|
||||
.bind(id)
|
||||
.bind(text)
|
||||
.bind(parent_task_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| KonError::StorageError(format!("Insert subtask failed: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_subtasks(pool: &SqlitePool, parent_id: &str) -> Result<Vec<TaskRow>> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT id, text, bucket, list_id, effort, done, done_at, created_at, \
|
||||
source_transcript_id, parent_task_id \
|
||||
FROM tasks WHERE parent_task_id = ? ORDER BY created_at ASC"
|
||||
)
|
||||
.bind(parent_id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| KonError::StorageError(format!("List subtasks failed: {e}")))?;
|
||||
|
||||
Ok(rows.into_iter().map(|r| TaskRow {
|
||||
id: r.get("id"),
|
||||
text: r.get("text"),
|
||||
bucket: r.get("bucket"),
|
||||
list_id: r.get("list_id"),
|
||||
effort: r.get("effort"),
|
||||
done: r.get("done"),
|
||||
done_at: r.get("done_at"),
|
||||
created_at: r.get("created_at"),
|
||||
source_transcript_id: r.get("source_transcript_id"),
|
||||
parent_task_id: r.get("parent_task_id"),
|
||||
}).collect())
|
||||
}
|
||||
|
||||
/// Mark a subtask done. If all siblings are now done, auto-complete the parent.
|
||||
pub async fn complete_subtask_and_check_parent(pool: &SqlitePool, subtask_id: &str) -> Result<()> {
|
||||
sqlx::query("UPDATE tasks SET done = 1, done_at = datetime('now') WHERE id = ?")
|
||||
.bind(subtask_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| KonError::StorageError(format!("Complete subtask failed: {e}")))?;
|
||||
|
||||
let parent_id: Option<String> = sqlx::query_scalar(
|
||||
"SELECT parent_task_id FROM tasks WHERE id = ?"
|
||||
)
|
||||
.bind(subtask_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| KonError::StorageError(format!("Get parent_task_id failed: {e}")))?;
|
||||
|
||||
let Some(pid) = parent_id else { return Ok(()); };
|
||||
|
||||
let pending: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM tasks WHERE parent_task_id = ? AND done = 0"
|
||||
)
|
||||
.bind(&pid)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| KonError::StorageError(format!("Count pending subtasks failed: {e}")))?;
|
||||
|
||||
if pending == 0 {
|
||||
sqlx::query("UPDATE tasks SET done = 1, done_at = datetime('now') WHERE id = ?")
|
||||
.bind(&pid)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| KonError::StorageError(format!("Auto-complete parent failed: {e}")))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn complete_task(pool: &SqlitePool, id: &str) -> Result<()> {
|
||||
sqlx::query("UPDATE tasks SET done = 1, done_at = datetime('now') WHERE id = ?")
|
||||
.bind(id)
|
||||
@@ -379,6 +484,7 @@ pub struct TaskRow {
|
||||
pub done_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub source_transcript_id: Option<String>,
|
||||
pub parent_task_id: Option<String>,
|
||||
}
|
||||
|
||||
fn transcript_row_from(r: &sqlx::sqlite::SqliteRow) -> TranscriptRow {
|
||||
@@ -552,6 +658,33 @@ mod tests {
|
||||
assert!(tasks.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subtask_crud_roundtrip() {
|
||||
let pool = test_pool().await;
|
||||
insert_task(&pool, "p1", "Write report", "inbox", None).await.unwrap();
|
||||
insert_subtask(&pool, "s1", "Open document", "p1").await.unwrap();
|
||||
insert_subtask(&pool, "s2", "Write introduction", "p1").await.unwrap();
|
||||
|
||||
// list_tasks must exclude subtasks
|
||||
let top_level = list_tasks(&pool).await.unwrap();
|
||||
assert_eq!(top_level.len(), 1);
|
||||
assert_eq!(top_level[0].id, "p1");
|
||||
|
||||
// list_subtasks returns both children
|
||||
let subs = list_subtasks(&pool, "p1").await.unwrap();
|
||||
assert_eq!(subs.len(), 2);
|
||||
|
||||
// completing s1 should NOT auto-complete parent (s2 still pending)
|
||||
complete_subtask_and_check_parent(&pool, "s1").await.unwrap();
|
||||
let parent = get_task_by_id(&pool, "p1").await.unwrap().unwrap();
|
||||
assert!(!parent.done, "parent should not be done yet");
|
||||
|
||||
// completing s2 SHOULD auto-complete parent
|
||||
complete_subtask_and_check_parent(&pool, "s2").await.unwrap();
|
||||
let parent = get_task_by_id(&pool, "p1").await.unwrap().unwrap();
|
||||
assert!(parent.done, "parent should auto-complete when all children done");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn settings_crud_roundtrip() {
|
||||
let pool = test_pool().await;
|
||||
|
||||
@@ -3,10 +3,11 @@ pub mod file_storage;
|
||||
pub mod migrations;
|
||||
|
||||
pub use database::{
|
||||
add_dictionary_entry, complete_task, count_transcripts, delete_dictionary_entry,
|
||||
delete_task, delete_transcript, get_setting, get_transcript, init, insert_task,
|
||||
insert_transcript, list_dictionary, list_recent_errors, list_tasks, list_transcripts,
|
||||
list_transcripts_paged, log_error, search_transcripts, set_setting, update_transcript,
|
||||
DictionaryEntry, ErrorLogRow, InsertTranscriptParams, TaskRow, TranscriptRow,
|
||||
add_dictionary_entry, complete_subtask_and_check_parent, complete_task, count_transcripts,
|
||||
delete_dictionary_entry, delete_task, delete_transcript, get_setting, get_task_by_id,
|
||||
get_transcript, init, insert_subtask, insert_task, insert_transcript, list_dictionary,
|
||||
list_recent_errors, list_subtasks, list_tasks, list_transcripts, list_transcripts_paged,
|
||||
log_error, search_transcripts, set_setting, update_transcript, DictionaryEntry, ErrorLogRow,
|
||||
InsertTranscriptParams, TaskRow, TranscriptRow,
|
||||
};
|
||||
pub use file_storage::{app_data_dir, crashes_dir, database_path, logs_dir, recordings_dir};
|
||||
|
||||
Reference in New Issue
Block a user