use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, SqlitePool, Type}; use ts_rs::TS; use uuid::Uuid; #[derive(Debug, Clone, Type, Serialize, Deserialize, PartialEq, TS)] #[sqlx(type_name = "task_status", rename_all = "lowercase")] #[serde(rename_all = "lowercase")] #[ts(export)] pub enum TaskStatus { Todo, InProgress, InReview, Done, Cancelled, } #[derive(Debug, Clone, FromRow, Serialize, Deserialize, TS)] #[ts(export)] pub struct Task { pub id: Uuid, pub project_id: Uuid, // Foreign key to Project pub title: String, pub description: Option, pub status: TaskStatus, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[ts(export)] pub struct TaskWithAttemptStatus { pub id: Uuid, pub project_id: Uuid, pub title: String, pub description: Option, pub status: TaskStatus, pub created_at: DateTime, pub updated_at: DateTime, pub has_in_progress_attempt: bool, } #[derive(Debug, Deserialize, TS)] #[ts(export)] pub struct CreateTask { pub project_id: Uuid, pub title: String, pub description: Option, } #[derive(Debug, Deserialize, TS)] #[ts(export)] pub struct UpdateTask { pub title: Option, pub description: Option, pub status: Option, } impl Task { pub async fn find_by_project_id( pool: &SqlitePool, project_id: Uuid, ) -> Result, sqlx::Error> { sqlx::query_as!( Task, r#"SELECT id as "id!: Uuid", project_id as "project_id!: Uuid", title, description, status as "status!: TaskStatus", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime" FROM tasks WHERE project_id = $1 ORDER BY created_at DESC"#, project_id ) .fetch_all(pool) .await } pub async fn find_by_project_id_with_attempt_status( pool: &SqlitePool, project_id: Uuid, ) -> Result, sqlx::Error> { let records = sqlx::query!( r#"SELECT t.id as "id!: Uuid", t.project_id as "project_id!: Uuid", t.title, t.description, t.status as "status!: TaskStatus", t.created_at as "created_at!: DateTime", t.updated_at as "updated_at!: DateTime", CASE WHEN in_progress_attempts.task_id IS NOT NULL THEN true ELSE false END as "has_in_progress_attempt!" FROM tasks t LEFT JOIN ( SELECT DISTINCT ta.task_id FROM task_attempts ta INNER JOIN ( SELECT task_attempt_id, MAX(created_at) as latest_created_at FROM task_attempt_activities GROUP BY task_attempt_id ) latest_activity ON ta.id = latest_activity.task_attempt_id INNER JOIN task_attempt_activities taa ON ta.id = taa.task_attempt_id AND taa.created_at = latest_activity.latest_created_at WHERE taa.status = 'inprogress' ) in_progress_attempts ON t.id = in_progress_attempts.task_id WHERE t.project_id = $1 ORDER BY t.created_at DESC"#, project_id ) .fetch_all(pool) .await?; let tasks = records .into_iter() .map(|record| TaskWithAttemptStatus { id: record.id, project_id: record.project_id, title: record.title, description: record.description, status: record.status, created_at: record.created_at, updated_at: record.updated_at, has_in_progress_attempt: record.has_in_progress_attempt != 0, }) .collect(); Ok(tasks) } pub async fn find_by_id(pool: &SqlitePool, id: Uuid) -> Result, sqlx::Error> { sqlx::query_as!( Task, r#"SELECT id as "id!: Uuid", project_id as "project_id!: Uuid", title, description, status as "status!: TaskStatus", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime" FROM tasks WHERE id = $1"#, id ) .fetch_optional(pool) .await } pub async fn find_by_id_and_project_id( pool: &SqlitePool, id: Uuid, project_id: Uuid, ) -> Result, sqlx::Error> { sqlx::query_as!( Task, r#"SELECT id as "id!: Uuid", project_id as "project_id!: Uuid", title, description, status as "status!: TaskStatus", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime" FROM tasks WHERE id = $1 AND project_id = $2"#, id, project_id ) .fetch_optional(pool) .await } pub async fn create( pool: &SqlitePool, data: &CreateTask, task_id: Uuid, ) -> Result { sqlx::query_as!( Task, r#"INSERT INTO tasks (id, project_id, title, description, status) VALUES ($1, $2, $3, $4, $5) RETURNING id as "id!: Uuid", project_id as "project_id!: Uuid", title, description, status as "status!: TaskStatus", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime""#, task_id, data.project_id, data.title, data.description, TaskStatus::Todo as TaskStatus ) .fetch_one(pool) .await } pub async fn update( pool: &SqlitePool, id: Uuid, project_id: Uuid, title: String, description: Option, status: TaskStatus, ) -> Result { let status_value = status as TaskStatus; sqlx::query_as!( Task, r#"UPDATE tasks SET title = $3, description = $4, status = $5 WHERE id = $1 AND project_id = $2 RETURNING id as "id!: Uuid", project_id as "project_id!: Uuid", title, description, status as "status!: TaskStatus", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime""#, id, project_id, title, description, status_value ) .fetch_one(pool) .await } pub async fn delete(pool: &SqlitePool, id: Uuid, project_id: Uuid) -> Result { let project_id_str = project_id.to_string(); let result = sqlx::query!( "DELETE FROM tasks WHERE id = $1 AND project_id = $2", id, project_id_str ) .execute(pool) .await?; Ok(result.rows_affected()) } pub async fn exists( pool: &SqlitePool, id: Uuid, project_id: Uuid, ) -> Result { let id_str = id.to_string(); let project_id_str = project_id.to_string(); let result = sqlx::query!( "SELECT id FROM tasks WHERE id = $1 AND project_id = $2", id_str, project_id_str ) .fetch_optional(pool) .await?; Ok(result.is_some()) } }