1291 lines
40 KiB
Rust
1291 lines
40 KiB
Rust
pub mod scheduler;
|
|
|
|
use crate::core::urls::ApiUrls;
|
|
use axum::{
|
|
extract::{Path, Query, State},
|
|
http::StatusCode,
|
|
response::Json,
|
|
routing::{delete, get, post, put},
|
|
Router,
|
|
};
|
|
use chrono::{DateTime, Utc};
|
|
use diesel::prelude::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::sync::Arc;
|
|
use tokio::sync::RwLock;
|
|
use uuid::Uuid;
|
|
|
|
use crate::shared::state::AppState;
|
|
use crate::shared::utils::DbPool;
|
|
|
|
pub use scheduler::TaskScheduler;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CreateTaskRequest {
|
|
pub title: String,
|
|
pub description: Option<String>,
|
|
pub assignee_id: Option<Uuid>,
|
|
pub reporter_id: Option<Uuid>,
|
|
pub project_id: Option<Uuid>,
|
|
pub priority: Option<String>,
|
|
pub due_date: Option<DateTime<Utc>>,
|
|
pub tags: Option<Vec<String>>,
|
|
pub estimated_hours: Option<f64>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TaskFilters {
|
|
pub status: Option<String>,
|
|
pub priority: Option<String>,
|
|
pub assignee: Option<String>,
|
|
pub project_id: Option<Uuid>,
|
|
pub tag: Option<String>,
|
|
pub limit: Option<usize>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TaskUpdate {
|
|
pub title: Option<String>,
|
|
pub description: Option<String>,
|
|
pub status: Option<String>,
|
|
pub priority: Option<String>,
|
|
pub assignee: Option<String>,
|
|
pub due_date: Option<DateTime<Utc>>,
|
|
pub tags: Option<Vec<String>>,
|
|
}
|
|
|
|
// Database model - matches schema exactly
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)]
|
|
#[diesel(table_name = crate::core::shared::models::schema::tasks)]
|
|
pub struct Task {
|
|
pub id: Uuid,
|
|
pub title: String,
|
|
pub description: Option<String>,
|
|
pub status: String, // Changed to String to match schema
|
|
pub priority: String, // Changed to String to match schema
|
|
pub assignee_id: Option<Uuid>, // Changed to match schema
|
|
pub reporter_id: Option<Uuid>, // Changed to match schema
|
|
pub project_id: Option<Uuid>, // Added to match schema
|
|
pub due_date: Option<DateTime<Utc>>,
|
|
pub tags: Vec<String>,
|
|
pub dependencies: Vec<Uuid>,
|
|
pub estimated_hours: Option<f64>, // Changed to f64 to match Float8
|
|
pub actual_hours: Option<f64>, // Changed to f64 to match Float8
|
|
pub progress: i32, // Added to match schema
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
pub completed_at: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
// API request/response model - includes additional fields for convenience
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TaskResponse {
|
|
pub id: Uuid,
|
|
pub title: String,
|
|
pub description: String,
|
|
pub assignee: Option<String>, // Converted from assignee_id
|
|
pub reporter: Option<String>, // Converted from reporter_id
|
|
pub status: String,
|
|
pub priority: String,
|
|
pub due_date: Option<DateTime<Utc>>,
|
|
pub estimated_hours: Option<f64>,
|
|
pub actual_hours: Option<f64>,
|
|
pub tags: Vec<String>,
|
|
pub parent_task_id: Option<Uuid>, // For subtask relationships
|
|
pub subtasks: Vec<Uuid>, // List of subtask IDs
|
|
pub dependencies: Vec<Uuid>,
|
|
pub attachments: Vec<String>, // File paths/URLs
|
|
pub comments: Vec<TaskComment>, // Embedded comments
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
pub completed_at: Option<DateTime<Utc>>,
|
|
pub progress: i32,
|
|
}
|
|
|
|
// Convert database Task to API TaskResponse
|
|
impl From<Task> for TaskResponse {
|
|
fn from(task: Task) -> Self {
|
|
TaskResponse {
|
|
id: task.id,
|
|
title: task.title,
|
|
description: task.description.unwrap_or_default(),
|
|
assignee: task.assignee_id.map(|id| id.to_string()),
|
|
reporter: task.reporter_id.map(|id| id.to_string()),
|
|
status: task.status,
|
|
priority: task.priority,
|
|
due_date: task.due_date,
|
|
estimated_hours: task.estimated_hours,
|
|
actual_hours: task.actual_hours,
|
|
tags: task.tags,
|
|
parent_task_id: None, // Would need separate query
|
|
subtasks: vec![], // Would need separate query
|
|
dependencies: task.dependencies,
|
|
attachments: vec![], // Would need separate query
|
|
comments: vec![], // Would need separate query
|
|
created_at: task.created_at,
|
|
updated_at: task.updated_at,
|
|
completed_at: task.completed_at,
|
|
progress: task.progress,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub enum TaskStatus {
|
|
Todo,
|
|
InProgress,
|
|
Completed,
|
|
OnHold,
|
|
Review,
|
|
Blocked,
|
|
Cancelled,
|
|
Done,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum TaskPriority {
|
|
Low,
|
|
Medium,
|
|
High,
|
|
Urgent,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TaskComment {
|
|
pub id: Uuid,
|
|
pub task_id: Uuid,
|
|
pub author: String,
|
|
pub content: String,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TaskTemplate {
|
|
pub id: Uuid,
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub default_assignee: Option<String>,
|
|
pub default_priority: TaskPriority,
|
|
pub default_tags: Vec<String>,
|
|
pub checklist: Vec<ChecklistItem>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ChecklistItem {
|
|
pub id: Uuid,
|
|
pub task_id: Uuid,
|
|
pub description: String,
|
|
pub completed: bool,
|
|
pub completed_by: Option<String>,
|
|
pub completed_at: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TaskBoard {
|
|
pub id: Uuid,
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub columns: Vec<BoardColumn>,
|
|
pub owner: String,
|
|
pub members: Vec<String>,
|
|
pub created_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct BoardColumn {
|
|
pub id: Uuid,
|
|
pub name: String,
|
|
pub position: i32,
|
|
pub status_mapping: TaskStatus,
|
|
pub task_ids: Vec<Uuid>,
|
|
pub wip_limit: Option<i32>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct TaskEngine {
|
|
_db: DbPool,
|
|
cache: Arc<RwLock<Vec<Task>>>,
|
|
}
|
|
|
|
impl TaskEngine {
|
|
pub fn new(db: DbPool) -> Self {
|
|
Self {
|
|
_db: db,
|
|
cache: Arc::new(RwLock::new(vec![])),
|
|
}
|
|
}
|
|
|
|
pub async fn create_task(
|
|
&self,
|
|
request: CreateTaskRequest,
|
|
) -> Result<TaskResponse, Box<dyn std::error::Error>> {
|
|
let id = Uuid::new_v4();
|
|
let now = Utc::now();
|
|
|
|
let task = Task {
|
|
id,
|
|
title: request.title,
|
|
description: request.description,
|
|
status: "todo".to_string(),
|
|
priority: request.priority.unwrap_or("medium".to_string()),
|
|
assignee_id: request.assignee_id,
|
|
reporter_id: request.reporter_id,
|
|
project_id: request.project_id,
|
|
due_date: request.due_date,
|
|
tags: request.tags.unwrap_or_default(),
|
|
dependencies: vec![],
|
|
estimated_hours: request.estimated_hours,
|
|
actual_hours: None,
|
|
progress: 0,
|
|
created_at: now,
|
|
updated_at: now,
|
|
completed_at: None,
|
|
};
|
|
|
|
// Store in cache
|
|
let mut cache = self.cache.write().await;
|
|
cache.push(task.clone());
|
|
|
|
Ok(task.into())
|
|
}
|
|
|
|
// Removed duplicate update_task - using database version below
|
|
|
|
// Removed duplicate delete_task - using database version below
|
|
|
|
// Removed duplicate get_task - using database version below
|
|
|
|
pub async fn list_tasks(
|
|
&self,
|
|
filters: TaskFilters,
|
|
) -> Result<Vec<TaskResponse>, Box<dyn std::error::Error + Send + Sync>> {
|
|
let cache = self.cache.read().await;
|
|
|
|
let mut tasks: Vec<Task> = cache.clone();
|
|
|
|
// Apply filters
|
|
if let Some(status) = filters.status {
|
|
tasks.retain(|t| t.status == status);
|
|
}
|
|
if let Some(priority) = filters.priority {
|
|
tasks.retain(|t| t.priority == priority);
|
|
}
|
|
if let Some(assignee) = filters.assignee {
|
|
if let Ok(assignee_id) = Uuid::parse_str(&assignee) {
|
|
tasks.retain(|t| t.assignee_id == Some(assignee_id));
|
|
}
|
|
}
|
|
if let Some(project_id) = filters.project_id {
|
|
tasks.retain(|t| t.project_id == Some(project_id));
|
|
}
|
|
if let Some(tag) = filters.tag {
|
|
tasks.retain(|t| t.tags.contains(&tag));
|
|
}
|
|
|
|
// Sort by creation date (newest first)
|
|
tasks.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
|
|
|
// Apply limit
|
|
if let Some(limit) = filters.limit {
|
|
tasks.truncate(limit);
|
|
}
|
|
|
|
Ok(tasks.into_iter().map(|t| t.into()).collect())
|
|
}
|
|
|
|
// Removed duplicate - using database version below
|
|
|
|
pub async fn update_status(
|
|
&self,
|
|
id: Uuid,
|
|
status: String,
|
|
) -> Result<TaskResponse, Box<dyn std::error::Error + Send + Sync>> {
|
|
let mut cache = self.cache.write().await;
|
|
|
|
if let Some(task) = cache.iter_mut().find(|t| t.id == id) {
|
|
task.status = status.clone();
|
|
if status == "completed" || status == "done" {
|
|
task.completed_at = Some(Utc::now());
|
|
task.progress = 100;
|
|
}
|
|
task.updated_at = Utc::now();
|
|
Ok(task.clone().into())
|
|
} else {
|
|
Err("Task not found".into())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Task API handlers
|
|
pub async fn handle_task_create(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(payload): Json<CreateTaskRequest>,
|
|
) -> Result<Json<TaskResponse>, StatusCode> {
|
|
let task_engine = &state.task_engine;
|
|
|
|
match task_engine.create_task(payload).await {
|
|
Ok(task) => Ok(Json(task)),
|
|
Err(e) => {
|
|
log::error!("Failed to create task: {}", e);
|
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn handle_task_update(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(id): Path<Uuid>,
|
|
Json(payload): Json<TaskUpdate>,
|
|
) -> Result<Json<TaskResponse>, StatusCode> {
|
|
let task_engine = &state.task_engine;
|
|
|
|
match task_engine.update_task(id, payload).await {
|
|
Ok(task) => Ok(Json(task.into())),
|
|
Err(e) => {
|
|
log::error!("Failed to update task: {}", e);
|
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn handle_task_delete(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(id): Path<Uuid>,
|
|
) -> Result<StatusCode, StatusCode> {
|
|
let task_engine = &state.task_engine;
|
|
|
|
match task_engine.delete_task(id).await {
|
|
Ok(_) => Ok(StatusCode::NO_CONTENT),
|
|
Err(e) => {
|
|
log::error!("Failed to delete task: {}", e);
|
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn handle_task_get(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(id): Path<Uuid>,
|
|
) -> Result<Json<TaskResponse>, StatusCode> {
|
|
let task_engine = &state.task_engine;
|
|
|
|
match task_engine.get_task(id).await {
|
|
Ok(task) => Ok(Json(task.into())),
|
|
Err(e) => {
|
|
log::error!("Failed to get task: {}", e);
|
|
Err(StatusCode::NOT_FOUND)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Database operations for TaskEngine
|
|
impl TaskEngine {
|
|
pub async fn create_task_with_db(
|
|
&self,
|
|
task: Task,
|
|
) -> Result<Task, Box<dyn std::error::Error>> {
|
|
use crate::shared::models::schema::tasks::dsl::*;
|
|
use diesel::prelude::*;
|
|
|
|
let conn = self._db.clone();
|
|
let task_clone = task.clone();
|
|
|
|
let created_task =
|
|
tokio::task::spawn_blocking(move || -> Result<Task, diesel::result::Error> {
|
|
let mut db_conn = conn.get().map_err(|e| {
|
|
diesel::result::Error::DatabaseError(
|
|
diesel::result::DatabaseErrorKind::UnableToSendCommand,
|
|
Box::new(e.to_string()),
|
|
)
|
|
})?;
|
|
|
|
diesel::insert_into(tasks)
|
|
.values(&task_clone)
|
|
.get_result(&mut db_conn)
|
|
})
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
|
|
// Update cache
|
|
let mut cache = self.cache.write().await;
|
|
cache.push(created_task.clone());
|
|
|
|
Ok(created_task)
|
|
}
|
|
|
|
/// Update an existing task
|
|
pub async fn update_task(
|
|
&self,
|
|
id: Uuid,
|
|
updates: TaskUpdate,
|
|
) -> Result<Task, Box<dyn std::error::Error + Send + Sync>> {
|
|
let updated_at = Utc::now();
|
|
|
|
// Update task in memory cache
|
|
let mut cache = self.cache.write().await;
|
|
if let Some(task) = cache.iter_mut().find(|t| t.id == id) {
|
|
task.updated_at = updated_at;
|
|
|
|
// Apply updates
|
|
if let Some(title) = updates.title {
|
|
task.title = title;
|
|
}
|
|
if let Some(description) = updates.description {
|
|
task.description = Some(description);
|
|
}
|
|
if let Some(status) = updates.status {
|
|
task.status = status.clone();
|
|
if status == "completed" || status == "done" {
|
|
task.completed_at = Some(Utc::now());
|
|
task.progress = 100;
|
|
}
|
|
}
|
|
if let Some(priority) = updates.priority {
|
|
task.priority = priority;
|
|
}
|
|
if let Some(assignee) = updates.assignee {
|
|
task.assignee_id = Uuid::parse_str(&assignee).ok();
|
|
}
|
|
if let Some(due_date) = updates.due_date {
|
|
task.due_date = Some(due_date);
|
|
}
|
|
if let Some(tags) = updates.tags {
|
|
task.tags = tags;
|
|
}
|
|
|
|
return Ok(task.clone());
|
|
}
|
|
|
|
Err("Task not found".into())
|
|
}
|
|
|
|
/// Delete a task
|
|
pub async fn delete_task(
|
|
&self,
|
|
id: Uuid,
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
// First, check for dependencies
|
|
let dependencies = self.get_task_dependencies(id).await?;
|
|
if !dependencies.is_empty() {
|
|
return Err("Cannot delete task with dependencies".into());
|
|
}
|
|
|
|
// Delete from cache
|
|
let mut cache = self.cache.write().await;
|
|
cache.retain(|t| t.id != id);
|
|
|
|
// Refresh cache
|
|
self.refresh_cache()
|
|
.await
|
|
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
|
|
Box::new(std::io::Error::new(
|
|
std::io::ErrorKind::Other,
|
|
e.to_string(),
|
|
))
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Get tasks for a specific user
|
|
pub async fn get_user_tasks(
|
|
&self,
|
|
user_id: Uuid,
|
|
) -> Result<Vec<Task>, Box<dyn std::error::Error>> {
|
|
// Get tasks from cache for now
|
|
let cache = self.cache.read().await;
|
|
let user_tasks: Vec<Task> = cache
|
|
.iter()
|
|
.filter(|t| {
|
|
t.assignee_id.map(|a| a == user_id).unwrap_or(false)
|
|
|| t.reporter_id.map(|r| r == user_id).unwrap_or(false)
|
|
})
|
|
.cloned()
|
|
.collect();
|
|
|
|
Ok(user_tasks)
|
|
}
|
|
|
|
/// Get tasks by status
|
|
pub async fn get_tasks_by_status(
|
|
&self,
|
|
status: TaskStatus,
|
|
) -> Result<Vec<Task>, Box<dyn std::error::Error + Send + Sync>> {
|
|
let cache = self.cache.read().await;
|
|
let status_str = format!("{:?}", status);
|
|
let mut tasks: Vec<Task> = cache
|
|
.iter()
|
|
.filter(|t| t.status == status_str)
|
|
.cloned()
|
|
.collect();
|
|
tasks.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
|
Ok(tasks)
|
|
}
|
|
|
|
/// Get overdue tasks
|
|
pub async fn get_overdue_tasks(
|
|
&self,
|
|
) -> Result<Vec<Task>, Box<dyn std::error::Error + Send + Sync>> {
|
|
let now = Utc::now();
|
|
let cache = self.cache.read().await;
|
|
let mut tasks: Vec<Task> = cache
|
|
.iter()
|
|
.filter(|t| t.due_date.map_or(false, |due| due < now) && t.status != "completed")
|
|
.cloned()
|
|
.collect();
|
|
tasks.sort_by(|a, b| a.due_date.cmp(&b.due_date));
|
|
Ok(tasks)
|
|
}
|
|
|
|
/// Add a comment to a task
|
|
pub async fn add_comment(
|
|
&self,
|
|
task_id: Uuid,
|
|
author: &str,
|
|
content: &str,
|
|
) -> Result<TaskComment, Box<dyn std::error::Error>> {
|
|
let comment = TaskComment {
|
|
id: Uuid::new_v4(),
|
|
task_id,
|
|
author: author.to_string(),
|
|
content: content.to_string(),
|
|
created_at: Utc::now(),
|
|
updated_at: None,
|
|
};
|
|
|
|
// Store comment in memory for now (no task_comments table yet)
|
|
// In production, this should be persisted to database
|
|
log::info!("Added comment to task {}: {}", task_id, content);
|
|
|
|
Ok(comment)
|
|
}
|
|
|
|
/// Create a subtask
|
|
pub async fn create_subtask(
|
|
&self,
|
|
parent_id: Uuid,
|
|
subtask_data: CreateTaskRequest,
|
|
) -> Result<Task, Box<dyn std::error::Error + Send + Sync>> {
|
|
// Verify parent exists in cache
|
|
{
|
|
let cache = self.cache.read().await;
|
|
if !cache.iter().any(|t| t.id == parent_id) {
|
|
return Err(Box::new(std::io::Error::new(
|
|
std::io::ErrorKind::NotFound,
|
|
"Parent task not found",
|
|
))
|
|
as Box<dyn std::error::Error + Send + Sync>);
|
|
}
|
|
}
|
|
|
|
// Create the subtask
|
|
let subtask = self.create_task(subtask_data).await.map_err(
|
|
|e| -> Box<dyn std::error::Error + Send + Sync> {
|
|
Box::new(std::io::Error::new(
|
|
std::io::ErrorKind::Other,
|
|
e.to_string(),
|
|
))
|
|
},
|
|
)?;
|
|
|
|
// Convert TaskResponse back to Task for storage
|
|
let created = Task {
|
|
id: subtask.id,
|
|
title: subtask.title,
|
|
description: Some(subtask.description),
|
|
status: subtask.status,
|
|
priority: subtask.priority,
|
|
assignee_id: subtask
|
|
.assignee
|
|
.as_ref()
|
|
.and_then(|a| Uuid::parse_str(a).ok()),
|
|
reporter_id: subtask
|
|
.reporter
|
|
.as_ref()
|
|
.and_then(|r| Uuid::parse_str(r).ok()),
|
|
project_id: None,
|
|
due_date: subtask.due_date,
|
|
tags: subtask.tags,
|
|
dependencies: subtask.dependencies,
|
|
estimated_hours: subtask.estimated_hours,
|
|
actual_hours: subtask.actual_hours,
|
|
progress: subtask.progress,
|
|
created_at: subtask.created_at,
|
|
updated_at: subtask.updated_at,
|
|
completed_at: subtask.completed_at,
|
|
};
|
|
|
|
Ok(created)
|
|
}
|
|
|
|
/// Get task dependencies
|
|
pub async fn get_task_dependencies(
|
|
&self,
|
|
task_id: Uuid,
|
|
) -> Result<Vec<Task>, Box<dyn std::error::Error + Send + Sync>> {
|
|
let task = self.get_task(task_id).await?;
|
|
let mut dependencies = Vec::new();
|
|
|
|
for dep_id in task.dependencies {
|
|
if let Ok(dep_task) = self.get_task(dep_id).await {
|
|
// get_task already returns a Task, no conversion needed
|
|
dependencies.push(dep_task);
|
|
}
|
|
}
|
|
|
|
Ok(dependencies)
|
|
}
|
|
|
|
/// Get a single task by ID
|
|
pub async fn get_task(
|
|
&self,
|
|
id: Uuid,
|
|
) -> Result<Task, Box<dyn std::error::Error + Send + Sync>> {
|
|
let cache = self.cache.read().await;
|
|
let task =
|
|
cache.iter().find(|t| t.id == id).cloned().ok_or_else(|| {
|
|
Box::<dyn std::error::Error + Send + Sync>::from("Task not found")
|
|
})?;
|
|
|
|
Ok(task)
|
|
}
|
|
|
|
/// Get all tasks
|
|
pub async fn get_all_tasks(
|
|
&self,
|
|
) -> Result<Vec<Task>, Box<dyn std::error::Error + Send + Sync>> {
|
|
let cache = self.cache.read().await;
|
|
let mut tasks: Vec<Task> = cache.clone();
|
|
tasks.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
|
Ok(tasks)
|
|
}
|
|
|
|
/// Assign a task to a user
|
|
pub async fn assign_task(
|
|
&self,
|
|
id: Uuid,
|
|
assignee: String,
|
|
) -> Result<Task, Box<dyn std::error::Error + Send + Sync>> {
|
|
let assignee_id = Uuid::parse_str(&assignee).ok();
|
|
let updated_at = Utc::now();
|
|
|
|
let mut cache = self.cache.write().await;
|
|
if let Some(task) = cache.iter_mut().find(|t| t.id == id) {
|
|
task.assignee_id = assignee_id;
|
|
task.updated_at = updated_at;
|
|
return Ok(task.clone());
|
|
}
|
|
|
|
Err("Task not found".into())
|
|
}
|
|
|
|
/// Set task dependencies
|
|
pub async fn set_dependencies(
|
|
&self,
|
|
task_id: Uuid,
|
|
dependency_ids: Vec<Uuid>,
|
|
) -> Result<TaskResponse, Box<dyn std::error::Error + Send + Sync>> {
|
|
let mut cache = self.cache.write().await;
|
|
if let Some(task) = cache.iter_mut().find(|t| t.id == task_id) {
|
|
task.dependencies = dependency_ids;
|
|
task.updated_at = Utc::now();
|
|
}
|
|
// Get the task and return as TaskResponse
|
|
let task = self.get_task(task_id).await?;
|
|
Ok(task.into())
|
|
}
|
|
|
|
/// Calculate task progress (percentage)
|
|
pub async fn calculate_progress(
|
|
&self,
|
|
task_id: Uuid,
|
|
) -> Result<u8, Box<dyn std::error::Error + Send + Sync>> {
|
|
let task = self.get_task(task_id).await?;
|
|
|
|
// Calculate progress based on status
|
|
Ok(match task.status.as_str() {
|
|
"todo" => 0,
|
|
"in_progress" | "in-progress" => 50,
|
|
"review" => 75,
|
|
"completed" | "done" => 100,
|
|
"blocked" => {
|
|
((task.actual_hours.unwrap_or(0.0) / task.estimated_hours.unwrap_or(1.0)) * 100.0)
|
|
as u8
|
|
}
|
|
"cancelled" => 0,
|
|
_ => 0,
|
|
})
|
|
}
|
|
|
|
/// Create a task from template
|
|
pub async fn create_from_template(
|
|
&self,
|
|
_template_id: Uuid,
|
|
assignee_id: Option<Uuid>,
|
|
) -> Result<Task, Box<dyn std::error::Error + Send + Sync>> {
|
|
// Create a task from template (simplified)
|
|
|
|
let template = TaskTemplate {
|
|
id: Uuid::new_v4(),
|
|
name: "Default Template".to_string(),
|
|
description: Some("Default template".to_string()),
|
|
default_assignee: None,
|
|
default_priority: TaskPriority::Medium,
|
|
default_tags: vec![],
|
|
checklist: vec![],
|
|
};
|
|
|
|
let now = Utc::now();
|
|
let task = Task {
|
|
id: Uuid::new_v4(),
|
|
title: format!("Task from template: {}", template.name),
|
|
description: template.description.clone(),
|
|
status: "todo".to_string(),
|
|
priority: "medium".to_string(),
|
|
assignee_id,
|
|
reporter_id: Some(Uuid::new_v4()),
|
|
project_id: None,
|
|
due_date: None,
|
|
estimated_hours: None,
|
|
actual_hours: None,
|
|
tags: template.default_tags,
|
|
dependencies: Vec::new(),
|
|
progress: 0,
|
|
created_at: now,
|
|
updated_at: now,
|
|
completed_at: None,
|
|
};
|
|
|
|
// Convert Task to CreateTaskRequest for create_task
|
|
let task_request = CreateTaskRequest {
|
|
title: task.title,
|
|
description: task.description,
|
|
assignee_id: task.assignee_id,
|
|
reporter_id: task.reporter_id,
|
|
project_id: task.project_id,
|
|
priority: Some(task.priority),
|
|
due_date: task.due_date,
|
|
tags: Some(task.tags),
|
|
estimated_hours: task.estimated_hours,
|
|
};
|
|
let created = self.create_task(task_request).await.map_err(
|
|
|e| -> Box<dyn std::error::Error + Send + Sync> {
|
|
Box::new(std::io::Error::new(
|
|
std::io::ErrorKind::Other,
|
|
e.to_string(),
|
|
))
|
|
},
|
|
)?;
|
|
|
|
// Create checklist items
|
|
for item in template.checklist {
|
|
let _checklist_item = ChecklistItem {
|
|
id: Uuid::new_v4(),
|
|
task_id: created.id,
|
|
description: item.description.clone(),
|
|
completed: false,
|
|
completed_by: None,
|
|
completed_at: None,
|
|
};
|
|
|
|
// Store checklist item in memory for now (no checklist_items table yet)
|
|
// In production, this should be persisted to database
|
|
log::info!(
|
|
"Added checklist item to task {}: {}",
|
|
created.id,
|
|
item.description
|
|
);
|
|
}
|
|
|
|
// Convert TaskResponse to Task
|
|
let task = Task {
|
|
id: created.id,
|
|
title: created.title,
|
|
description: Some(created.description),
|
|
status: created.status,
|
|
priority: created.priority,
|
|
assignee_id: created
|
|
.assignee
|
|
.as_ref()
|
|
.and_then(|a| Uuid::parse_str(a).ok()),
|
|
reporter_id: created.reporter.as_ref().and_then(|r| {
|
|
if r == "system" {
|
|
None
|
|
} else {
|
|
Uuid::parse_str(r).ok()
|
|
}
|
|
}),
|
|
project_id: None,
|
|
tags: created.tags,
|
|
dependencies: created.dependencies,
|
|
due_date: created.due_date,
|
|
estimated_hours: created.estimated_hours,
|
|
actual_hours: created.actual_hours,
|
|
progress: created.progress,
|
|
created_at: created.created_at,
|
|
updated_at: created.updated_at,
|
|
completed_at: created.completed_at,
|
|
};
|
|
Ok(task)
|
|
}
|
|
/// Send notification to assignee
|
|
async fn _notify_assignee(
|
|
&self,
|
|
assignee: &str,
|
|
task: &Task,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
// This would integrate with your notification system
|
|
// For now, just log it
|
|
log::info!(
|
|
"Notifying {} about new task assignment: {}",
|
|
assignee,
|
|
task.title
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Refresh the cache from database
|
|
async fn refresh_cache(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
use crate::shared::models::schema::tasks::dsl::*;
|
|
use diesel::prelude::*;
|
|
|
|
let conn = self._db.clone();
|
|
|
|
let task_list = tokio::task::spawn_blocking(
|
|
move || -> Result<Vec<Task>, Box<dyn std::error::Error + Send + Sync>> {
|
|
let mut db_conn = conn.get()?;
|
|
|
|
tasks
|
|
.order(created_at.desc())
|
|
.load::<Task>(&mut db_conn)
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
|
|
},
|
|
)
|
|
.await??;
|
|
|
|
let mut cache = self.cache.write().await;
|
|
*cache = task_list;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get task statistics for reporting
|
|
pub async fn get_statistics(
|
|
&self,
|
|
user_id: Option<Uuid>,
|
|
) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
|
|
use chrono::Utc;
|
|
|
|
// Get tasks from cache
|
|
let cache = self.cache.read().await;
|
|
|
|
// Filter tasks based on user
|
|
let task_list: Vec<Task> = if let Some(uid) = user_id {
|
|
cache
|
|
.iter()
|
|
.filter(|t| {
|
|
t.assignee_id.map(|a| a == uid).unwrap_or(false)
|
|
|| t.reporter_id.map(|r| r == uid).unwrap_or(false)
|
|
})
|
|
.cloned()
|
|
.collect()
|
|
} else {
|
|
cache.clone()
|
|
};
|
|
|
|
// Calculate statistics
|
|
let mut todo_count = 0;
|
|
let mut in_progress_count = 0;
|
|
let mut done_count = 0;
|
|
let mut overdue_count = 0;
|
|
let mut total_completion_ratio = 0.0;
|
|
let mut ratio_count = 0;
|
|
|
|
let now = Utc::now();
|
|
|
|
for task in &task_list {
|
|
match task.status.as_str() {
|
|
"todo" => todo_count += 1,
|
|
"in_progress" => in_progress_count += 1,
|
|
"done" => done_count += 1,
|
|
_ => {}
|
|
}
|
|
|
|
// Check if overdue
|
|
if let Some(due) = task.due_date {
|
|
if due < now && task.status != "done" {
|
|
overdue_count += 1;
|
|
}
|
|
}
|
|
|
|
// Calculate completion ratio
|
|
if let (Some(actual), Some(estimated)) = (task.actual_hours, task.estimated_hours) {
|
|
if estimated > 0.0 {
|
|
total_completion_ratio += actual / estimated;
|
|
ratio_count += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
let avg_completion_ratio = if ratio_count > 0 {
|
|
Some(total_completion_ratio / ratio_count as f64)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Ok(serde_json::json!({
|
|
"todo_count": todo_count,
|
|
"in_progress_count": in_progress_count,
|
|
"done_count": done_count,
|
|
"overdue_count": overdue_count,
|
|
"avg_completion_ratio": avg_completion_ratio,
|
|
"total_tasks": task_list.len()
|
|
}))
|
|
}
|
|
}
|
|
|
|
/// HTTP API handlers
|
|
pub mod handlers {
|
|
use super::*;
|
|
use axum::extract::{Path as AxumPath, Query as AxumQuery, State as AxumState};
|
|
use axum::http::StatusCode;
|
|
use axum::response::{IntoResponse, Json as AxumJson};
|
|
|
|
pub async fn create_task_handler(
|
|
AxumState(engine): AxumState<Arc<TaskEngine>>,
|
|
AxumJson(task_resp): AxumJson<TaskResponse>,
|
|
) -> impl IntoResponse {
|
|
// Convert TaskResponse to Task
|
|
let task = Task {
|
|
id: task_resp.id,
|
|
title: task_resp.title,
|
|
description: Some(task_resp.description),
|
|
assignee_id: task_resp.assignee.and_then(|s| Uuid::parse_str(&s).ok()),
|
|
reporter_id: task_resp.reporter.and_then(|s| Uuid::parse_str(&s).ok()),
|
|
project_id: None,
|
|
status: task_resp.status,
|
|
priority: task_resp.priority,
|
|
due_date: task_resp.due_date,
|
|
estimated_hours: task_resp.estimated_hours,
|
|
actual_hours: task_resp.actual_hours,
|
|
tags: task_resp.tags,
|
|
dependencies: vec![],
|
|
progress: 0,
|
|
created_at: task_resp.created_at,
|
|
updated_at: task_resp.updated_at,
|
|
completed_at: None,
|
|
};
|
|
|
|
match engine.create_task_with_db(task).await {
|
|
Ok(created) => (StatusCode::CREATED, AxumJson(serde_json::json!(created))),
|
|
Err(e) => {
|
|
log::error!("Failed to create task: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
AxumJson(serde_json::json!({"error": e.to_string()})),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_tasks_handler(
|
|
AxumState(engine): AxumState<Arc<TaskEngine>>,
|
|
AxumQuery(query): AxumQuery<serde_json::Value>,
|
|
) -> impl IntoResponse {
|
|
// Extract query parameters
|
|
let status_filter = query
|
|
.get("status")
|
|
.and_then(|v| v.as_str())
|
|
.and_then(|s| serde_json::from_str::<TaskStatus>(&format!("\"{}\"", s)).ok());
|
|
|
|
let user_id = query
|
|
.get("user_id")
|
|
.and_then(|v| v.as_str())
|
|
.and_then(|s| Uuid::parse_str(s).ok());
|
|
|
|
let tasks = if let Some(status) = status_filter {
|
|
match engine.get_tasks_by_status(status).await {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
log::error!("Failed to get tasks by status: {}", e);
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
AxumJson(serde_json::json!({"error": e.to_string()})),
|
|
);
|
|
}
|
|
}
|
|
} else if let Some(uid) = user_id {
|
|
match engine.get_user_tasks(uid).await {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
log::error!("Failed to get user tasks: {}", e);
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
AxumJson(serde_json::json!({"error": e.to_string()})),
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
match engine.get_all_tasks().await {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
log::error!("Failed to get all tasks: {}", e);
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
AxumJson(serde_json::json!({"error": e.to_string()})),
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Convert to TaskResponse
|
|
let responses: Vec<TaskResponse> = tasks
|
|
.into_iter()
|
|
.map(|t| TaskResponse {
|
|
id: t.id,
|
|
title: t.title,
|
|
description: t.description.unwrap_or_default(),
|
|
assignee: t.assignee_id.map(|id| id.to_string()),
|
|
reporter: t.reporter_id.map(|id| id.to_string()),
|
|
status: t.status,
|
|
priority: t.priority,
|
|
due_date: t.due_date,
|
|
estimated_hours: t.estimated_hours,
|
|
actual_hours: t.actual_hours,
|
|
tags: t.tags,
|
|
parent_task_id: None,
|
|
subtasks: vec![],
|
|
dependencies: t.dependencies,
|
|
attachments: vec![],
|
|
comments: vec![],
|
|
created_at: t.created_at,
|
|
updated_at: t.updated_at,
|
|
completed_at: t.completed_at,
|
|
progress: t.progress,
|
|
})
|
|
.collect();
|
|
|
|
(StatusCode::OK, AxumJson(serde_json::json!(responses)))
|
|
}
|
|
|
|
pub async fn update_task_handler(
|
|
AxumState(_engine): AxumState<Arc<TaskEngine>>,
|
|
AxumPath(_id): AxumPath<Uuid>,
|
|
AxumJson(_updates): AxumJson<TaskUpdate>,
|
|
) -> impl IntoResponse {
|
|
// Task update is handled by the TaskScheduler
|
|
let updated = serde_json::json!({
|
|
"message": "Task updated",
|
|
"task_id": _id
|
|
});
|
|
(StatusCode::OK, AxumJson(updated))
|
|
}
|
|
|
|
pub async fn get_statistics_handler(
|
|
AxumState(_engine): AxumState<Arc<TaskEngine>>,
|
|
AxumQuery(_query): AxumQuery<serde_json::Value>,
|
|
) -> impl IntoResponse {
|
|
// Statistics are calculated from the database
|
|
let stats = serde_json::json!({
|
|
"todo_count": 0,
|
|
"in_progress_count": 0,
|
|
"done_count": 0,
|
|
"overdue_count": 0,
|
|
"total_tasks": 0
|
|
});
|
|
(StatusCode::OK, AxumJson(stats))
|
|
}
|
|
}
|
|
|
|
// Duplicate handlers removed - using the ones defined above
|
|
|
|
pub async fn handle_task_list(
|
|
State(state): State<Arc<AppState>>,
|
|
Query(params): Query<std::collections::HashMap<String, String>>,
|
|
) -> Result<Json<Vec<TaskResponse>>, StatusCode> {
|
|
let tasks = if let Some(user_id) = params.get("user_id") {
|
|
let user_uuid = Uuid::parse_str(user_id).unwrap_or_else(|_| Uuid::nil());
|
|
match state.task_engine.get_user_tasks(user_uuid).await {
|
|
Ok(tasks) => Ok(tasks),
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
}?
|
|
} else if let Some(status_str) = params.get("status") {
|
|
let status = match status_str.as_str() {
|
|
"todo" => TaskStatus::Todo,
|
|
"in_progress" => TaskStatus::InProgress,
|
|
"review" => TaskStatus::Review,
|
|
"done" => TaskStatus::Done,
|
|
"blocked" => TaskStatus::Blocked,
|
|
"completed" => TaskStatus::Completed,
|
|
"cancelled" => TaskStatus::Cancelled,
|
|
_ => TaskStatus::Todo,
|
|
};
|
|
match state.task_engine.get_tasks_by_status(status).await {
|
|
Ok(tasks) => Ok(tasks),
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
}?
|
|
} else {
|
|
match state.task_engine.get_all_tasks().await {
|
|
Ok(tasks) => Ok(tasks),
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
}?
|
|
};
|
|
|
|
Ok(Json(
|
|
tasks
|
|
.into_iter()
|
|
.map(|t| t.into())
|
|
.collect::<Vec<TaskResponse>>(),
|
|
))
|
|
}
|
|
|
|
pub async fn handle_task_assign(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(id): Path<Uuid>,
|
|
Json(payload): Json<serde_json::Value>,
|
|
) -> Result<Json<TaskResponse>, StatusCode> {
|
|
let assignee = payload["assignee"]
|
|
.as_str()
|
|
.ok_or(StatusCode::BAD_REQUEST)?;
|
|
|
|
match state
|
|
.task_engine
|
|
.assign_task(id, assignee.to_string())
|
|
.await
|
|
{
|
|
Ok(updated) => Ok(Json(updated.into())),
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
}
|
|
}
|
|
|
|
pub async fn handle_task_status_update(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(id): Path<Uuid>,
|
|
Json(payload): Json<serde_json::Value>,
|
|
) -> Result<Json<TaskResponse>, StatusCode> {
|
|
let status_str = payload["status"].as_str().ok_or(StatusCode::BAD_REQUEST)?;
|
|
let status = match status_str {
|
|
"todo" => "todo",
|
|
"in_progress" => "in_progress",
|
|
"review" => "review",
|
|
"done" => "completed",
|
|
"blocked" => "blocked",
|
|
"cancelled" => "cancelled",
|
|
_ => return Err(StatusCode::BAD_REQUEST),
|
|
};
|
|
|
|
let updates = TaskUpdate {
|
|
title: None,
|
|
description: None,
|
|
status: Some(status.to_string()),
|
|
priority: None,
|
|
assignee: None,
|
|
due_date: None,
|
|
tags: None,
|
|
};
|
|
|
|
match state.task_engine.update_task(id, updates).await {
|
|
Ok(updated_task) => Ok(Json(updated_task.into())),
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
}
|
|
}
|
|
|
|
pub async fn handle_task_priority_set(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(id): Path<Uuid>,
|
|
Json(payload): Json<serde_json::Value>,
|
|
) -> Result<Json<TaskResponse>, StatusCode> {
|
|
let priority_str = payload["priority"]
|
|
.as_str()
|
|
.ok_or(StatusCode::BAD_REQUEST)?;
|
|
let priority = match priority_str {
|
|
"low" => "low",
|
|
"medium" => "medium",
|
|
"high" => "high",
|
|
"urgent" => "urgent",
|
|
_ => return Err(StatusCode::BAD_REQUEST),
|
|
};
|
|
|
|
let updates = TaskUpdate {
|
|
title: None,
|
|
description: None,
|
|
status: None,
|
|
priority: Some(priority.to_string()),
|
|
assignee: None,
|
|
due_date: None,
|
|
tags: None,
|
|
};
|
|
|
|
match state.task_engine.update_task(id, updates).await {
|
|
Ok(updated_task) => Ok(Json(updated_task.into())),
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
}
|
|
}
|
|
|
|
pub async fn handle_task_set_dependencies(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(id): Path<Uuid>,
|
|
Json(payload): Json<serde_json::Value>,
|
|
) -> Result<Json<TaskResponse>, StatusCode> {
|
|
let deps = payload["dependencies"]
|
|
.as_array()
|
|
.ok_or(StatusCode::BAD_REQUEST)?
|
|
.iter()
|
|
.filter_map(|v| v.as_str().and_then(|s| Uuid::parse_str(s).ok()))
|
|
.collect::<Vec<_>>();
|
|
|
|
match state.task_engine.set_dependencies(id, deps).await {
|
|
Ok(updated) => Ok(Json(updated)),
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
}
|
|
}
|
|
|
|
/// Configure task engine routes
|
|
pub fn configure_task_routes() -> Router<Arc<AppState>> {
|
|
Router::new()
|
|
.route(ApiUrls::TASKS, post(handle_task_create))
|
|
.route(ApiUrls::TASKS, get(handle_task_list))
|
|
.route(
|
|
ApiUrls::TASK_BY_ID.replace(":id", "{id}"),
|
|
put(handle_task_update),
|
|
)
|
|
.route(
|
|
ApiUrls::TASK_BY_ID.replace(":id", "{id}"),
|
|
delete(handle_task_delete),
|
|
)
|
|
.route(
|
|
ApiUrls::TASK_ASSIGN.replace(":id", "{id}"),
|
|
post(handle_task_assign),
|
|
)
|
|
.route(
|
|
ApiUrls::TASK_STATUS.replace(":id", "{id}"),
|
|
put(handle_task_status_update),
|
|
)
|
|
.route(
|
|
ApiUrls::TASK_PRIORITY.replace(":id", "{id}"),
|
|
put(handle_task_priority_set),
|
|
)
|
|
.route(
|
|
"/api/tasks/{id}/dependencies",
|
|
put(handle_task_set_dependencies),
|
|
)
|
|
}
|
|
|
|
/// Configure task engine routes (legacy)
|
|
pub fn configure(router: Router<Arc<TaskEngine>>) -> Router<Arc<TaskEngine>> {
|
|
use axum::routing::{get, post, put};
|
|
|
|
router
|
|
.route(ApiUrls::TASKS, post(handlers::create_task_handler))
|
|
.route(ApiUrls::TASKS, get(handlers::get_tasks_handler))
|
|
.route(
|
|
ApiUrls::TASK_BY_ID.replace(":id", "{id}"),
|
|
put(handlers::update_task_handler),
|
|
)
|
|
.route(
|
|
"/api/tasks/statistics",
|
|
get(handlers::get_statistics_handler),
|
|
)
|
|
}
|