use axum::{ routing::get, Router, Json, response::Json as ResponseJson, extract::{Path, Extension}, http::StatusCode, }; use sqlx::PgPool; use uuid::Uuid; use chrono::Utc; use crate::models::{ApiResponse, task::{Task, CreateTask, UpdateTask, TaskStatus}}; use crate::auth::AuthUser; pub async fn get_project_tasks( auth: AuthUser, Path(project_id): Path, Extension(pool): Extension ) -> Result>>, StatusCode> { match sqlx::query_as!( Task, r#"SELECT id, project_id, title, description, status as "status!: TaskStatus", created_at, updated_at FROM tasks WHERE project_id = $1 ORDER BY created_at DESC"#, project_id ) .fetch_all(&pool) .await { Ok(tasks) => Ok(ResponseJson(ApiResponse { success: true, data: Some(tasks), message: None, })), Err(e) => { tracing::error!("Failed to fetch tasks for project {}: {}", project_id, e); Err(StatusCode::INTERNAL_SERVER_ERROR) } } } pub async fn get_task( auth: AuthUser, Path((project_id, task_id)): Path<(Uuid, Uuid)>, Extension(pool): Extension ) -> Result>, StatusCode> { match sqlx::query_as!( Task, r#"SELECT id, project_id, title, description, status as "status!: TaskStatus", created_at, updated_at FROM tasks WHERE id = $1 AND project_id = $2"#, task_id, project_id ) .fetch_optional(&pool) .await { Ok(Some(task)) => Ok(ResponseJson(ApiResponse { success: true, data: Some(task), message: None, })), Ok(None) => Err(StatusCode::NOT_FOUND), Err(e) => { tracing::error!("Failed to fetch task {} in project {}: {}", task_id, project_id, e); Err(StatusCode::INTERNAL_SERVER_ERROR) } } } pub async fn create_task( Path(project_id): Path, auth: AuthUser, Extension(pool): Extension, Json(mut payload): Json ) -> Result>, StatusCode> { let id = Uuid::new_v4(); let now = Utc::now(); // Ensure the project_id in the payload matches the path parameter payload.project_id = project_id; // Verify project exists first let project_exists = sqlx::query!("SELECT id FROM projects WHERE id = $1", project_id) .fetch_optional(&pool) .await; match project_exists { Ok(None) => return Err(StatusCode::NOT_FOUND), Err(e) => { tracing::error!("Failed to check project existence: {}", e); return Err(StatusCode::INTERNAL_SERVER_ERROR); } Ok(Some(_)) => {} } tracing::debug!("Creating task '{}' in project {} for user {}", payload.title, project_id, auth.user_id); match sqlx::query_as!( Task, r#"INSERT INTO tasks (id, project_id, title, description, status, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, project_id, title, description, status as "status!: TaskStatus", created_at, updated_at"#, id, payload.project_id, payload.title, payload.description, TaskStatus::Todo as TaskStatus, now, now ) .fetch_one(&pool) .await { Ok(task) => Ok(ResponseJson(ApiResponse { success: true, data: Some(task), message: Some("Task created successfully".to_string()), })), Err(e) => { tracing::error!("Failed to create task: {}", e); Err(StatusCode::INTERNAL_SERVER_ERROR) } } } pub async fn update_task( Path((project_id, task_id)): Path<(Uuid, Uuid)>, Extension(pool): Extension, Json(payload): Json ) -> Result>, StatusCode> { let now = Utc::now(); // Check if task exists in the specified project let existing_task = sqlx::query_as!( Task, r#"SELECT id, project_id, title, description, status as "status!: TaskStatus", created_at, updated_at FROM tasks WHERE id = $1 AND project_id = $2"#, task_id, project_id ) .fetch_optional(&pool) .await; let existing_task = match existing_task { Ok(Some(task)) => task, Ok(None) => return Err(StatusCode::NOT_FOUND), Err(e) => { tracing::error!("Failed to check task existence: {}", e); return Err(StatusCode::INTERNAL_SERVER_ERROR); } }; // Use existing values if not provided in update let title = payload.title.unwrap_or(existing_task.title); let description = payload.description.or(existing_task.description); let status = payload.status.unwrap_or(existing_task.status); match sqlx::query_as!( Task, r#"UPDATE tasks SET title = $3, description = $4, status = $5, updated_at = $6 WHERE id = $1 AND project_id = $2 RETURNING id, project_id, title, description, status as "status!: TaskStatus", created_at, updated_at"#, task_id, project_id, title, description, status as TaskStatus, now ) .fetch_one(&pool) .await { Ok(task) => Ok(ResponseJson(ApiResponse { success: true, data: Some(task), message: Some("Task updated successfully".to_string()), })), Err(e) => { tracing::error!("Failed to update task: {}", e); Err(StatusCode::INTERNAL_SERVER_ERROR) } } } pub async fn delete_task( Path((project_id, task_id)): Path<(Uuid, Uuid)>, Extension(pool): Extension ) -> Result>, StatusCode> { match sqlx::query!( "DELETE FROM tasks WHERE id = $1 AND project_id = $2", task_id, project_id ) .execute(&pool) .await { Ok(result) => { if result.rows_affected() == 0 { Err(StatusCode::NOT_FOUND) } else { Ok(ResponseJson(ApiResponse { success: true, data: None, message: Some("Task deleted successfully".to_string()), })) } } Err(e) => { tracing::error!("Failed to delete task: {}", e); Err(StatusCode::INTERNAL_SERVER_ERROR) } } } pub fn tasks_router() -> Router { use axum::routing::{post, put, delete}; Router::new() .route("/projects/:project_id/tasks", get(get_project_tasks).post(create_task)) .route("/projects/:project_id/tasks/:task_id", get(get_task).put(update_task).delete(delete_task)) }