2025-11-22 22:55:35 -03:00
|
|
|
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::utils::DbPool;
|
|
|
|
|
|
|
|
|
|
// TODO: Replace sqlx queries with Diesel queries
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct TaskUpdate {
|
|
|
|
|
pub title: Option<String>,
|
|
|
|
|
pub description: Option<String>,
|
|
|
|
|
pub status: Option<TaskStatus>,
|
|
|
|
|
pub priority: Option<TaskPriority>,
|
|
|
|
|
pub assignee: Option<String>,
|
|
|
|
|
pub due_date: Option<DateTime<Utc>>,
|
|
|
|
|
pub tags: Option<Vec<String>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct Task {
|
|
|
|
|
pub id: Uuid,
|
|
|
|
|
pub title: String,
|
|
|
|
|
pub description: Option<String>,
|
|
|
|
|
pub assignee: Option<String>,
|
|
|
|
|
pub reporter: String,
|
|
|
|
|
pub status: TaskStatus,
|
|
|
|
|
pub priority: TaskPriority,
|
|
|
|
|
pub due_date: Option<DateTime<Utc>>,
|
|
|
|
|
pub estimated_hours: Option<f32>,
|
|
|
|
|
pub actual_hours: Option<f32>,
|
|
|
|
|
pub tags: Vec<String>,
|
|
|
|
|
pub parent_task_id: Option<Uuid>,
|
|
|
|
|
pub subtasks: Vec<Uuid>,
|
|
|
|
|
pub dependencies: Vec<Uuid>,
|
|
|
|
|
pub attachments: Vec<String>,
|
|
|
|
|
pub comments: Vec<TaskComment>,
|
|
|
|
|
pub created_at: DateTime<Utc>,
|
|
|
|
|
pub updated_at: DateTime<Utc>,
|
|
|
|
|
pub completed_at: Option<DateTime<Utc>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
pub enum TaskStatus {
|
|
|
|
|
Todo,
|
|
|
|
|
InProgress,
|
|
|
|
|
Review,
|
|
|
|
|
Done,
|
|
|
|
|
Blocked,
|
|
|
|
|
Cancelled,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct TaskEngine {
|
|
|
|
|
db: Arc<DbPool>,
|
|
|
|
|
cache: Arc<RwLock<Vec<Task>>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TaskEngine {
|
|
|
|
|
pub fn new(db: Arc<DbPool>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
db,
|
|
|
|
|
cache: Arc::new(RwLock::new(Vec::new())),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a new task
|
|
|
|
|
pub async fn create_task(&self, task: Task) -> Result<Task, Box<dyn std::error::Error>> {
|
|
|
|
|
// TODO: Implement with Diesel
|
|
|
|
|
/*
|
|
|
|
|
let result = sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
INSERT INTO tasks
|
|
|
|
|
(id, title, description, assignee, reporter, status, priority,
|
|
|
|
|
due_date, estimated_hours, tags, parent_task_id, created_at, updated_at)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
|
|
|
RETURNING *
|
|
|
|
|
"#,
|
|
|
|
|
task.id,
|
|
|
|
|
task.title,
|
|
|
|
|
task.description,
|
|
|
|
|
task.assignee,
|
|
|
|
|
task.reporter,
|
|
|
|
|
serde_json::to_value(&task.status)?,
|
|
|
|
|
serde_json::to_value(&task.priority)?,
|
|
|
|
|
task.due_date,
|
|
|
|
|
task.estimated_hours,
|
|
|
|
|
&task.tags[..],
|
|
|
|
|
task.parent_task_id,
|
|
|
|
|
task.created_at,
|
|
|
|
|
task.updated_at
|
|
|
|
|
)
|
|
|
|
|
.fetch_one(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let created_task: Task = serde_json::from_value(serde_json::to_value(result)?)?;
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
let created_task = task.clone();
|
|
|
|
|
|
|
|
|
|
// 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>> {
|
|
|
|
|
let updated_at = Utc::now();
|
|
|
|
|
|
|
|
|
|
// Check if status is changing to Done
|
2025-11-26 22:54:22 -03:00
|
|
|
let completing = updates
|
|
|
|
|
.status
|
2025-11-22 22:55:35 -03:00
|
|
|
.as_ref()
|
|
|
|
|
.map(|s| matches!(s, TaskStatus::Done))
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
let completed_at = if completing { Some(Utc::now()) } else { None };
|
2025-11-22 22:55:35 -03:00
|
|
|
|
|
|
|
|
// TODO: Implement with Diesel
|
|
|
|
|
/*
|
|
|
|
|
let result = sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
UPDATE tasks
|
|
|
|
|
SET title = COALESCE($2, title),
|
|
|
|
|
description = COALESCE($3, description),
|
|
|
|
|
assignee = COALESCE($4, assignee),
|
|
|
|
|
status = COALESCE($5, status),
|
|
|
|
|
priority = COALESCE($6, priority),
|
|
|
|
|
due_date = COALESCE($7, due_date),
|
|
|
|
|
updated_at = $8,
|
|
|
|
|
completed_at = COALESCE($9, completed_at)
|
|
|
|
|
WHERE id = $1
|
|
|
|
|
RETURNING *
|
|
|
|
|
"#,
|
|
|
|
|
id,
|
|
|
|
|
updates.get("title").and_then(|v| v.as_str()),
|
|
|
|
|
updates.get("description").and_then(|v| v.as_str()),
|
|
|
|
|
updates.get("assignee").and_then(|v| v.as_str()),
|
|
|
|
|
updates.get("status").and_then(|v| serde_json::to_value(v).ok()),
|
|
|
|
|
updates.get("priority").and_then(|v| serde_json::to_value(v).ok()),
|
|
|
|
|
updates
|
|
|
|
|
.get("due_date")
|
|
|
|
|
.and_then(|v| DateTime::parse_from_rfc3339(v.as_str()?).ok())
|
|
|
|
|
.map(|dt| dt.with_timezone(&Utc)),
|
|
|
|
|
updated_at,
|
|
|
|
|
completed_at
|
|
|
|
|
)
|
|
|
|
|
.fetch_one(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let updated_task: Task = serde_json::from_value(serde_json::to_value(result)?)?;
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// Create a dummy updated task for now
|
|
|
|
|
let updated_task = Task {
|
|
|
|
|
id,
|
|
|
|
|
title: updates.title.unwrap_or_else(|| "Updated Task".to_string()),
|
|
|
|
|
description: updates.description,
|
|
|
|
|
assignee: updates.assignee,
|
|
|
|
|
reporter: "system".to_string(),
|
|
|
|
|
status: updates.status.unwrap_or(TaskStatus::Todo),
|
|
|
|
|
priority: updates.priority.unwrap_or(TaskPriority::Medium),
|
|
|
|
|
due_date: updates.due_date,
|
|
|
|
|
estimated_hours: None,
|
|
|
|
|
actual_hours: None,
|
|
|
|
|
tags: updates.tags.unwrap_or_default(),
|
|
|
|
|
parent_task_id: None,
|
|
|
|
|
subtasks: Vec::new(),
|
|
|
|
|
dependencies: Vec::new(),
|
|
|
|
|
attachments: Vec::new(),
|
|
|
|
|
comments: Vec::new(),
|
|
|
|
|
created_at: Utc::now(),
|
|
|
|
|
updated_at: Utc::now(),
|
|
|
|
|
completed_at,
|
|
|
|
|
};
|
|
|
|
|
self.refresh_cache().await?;
|
|
|
|
|
|
|
|
|
|
Ok(updated_task)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Delete a task
|
|
|
|
|
pub async fn delete_task(&self, id: Uuid) -> Result<bool, Box<dyn std::error::Error>> {
|
|
|
|
|
// First, check for dependencies
|
|
|
|
|
let dependencies = self.get_task_dependencies(id).await?;
|
|
|
|
|
if !dependencies.is_empty() {
|
|
|
|
|
return Err("Cannot delete task with dependencies".into());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: Implement with Diesel
|
|
|
|
|
/*
|
|
|
|
|
let result = sqlx::query!("DELETE FROM tasks WHERE id = $1", id)
|
|
|
|
|
.execute(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
self.refresh_cache().await?;
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get tasks for a specific user
|
|
|
|
|
pub async fn get_user_tasks(
|
|
|
|
|
&self,
|
|
|
|
|
_user_id: &str,
|
|
|
|
|
) -> Result<Vec<Task>, Box<dyn std::error::Error>> {
|
|
|
|
|
// TODO: Implement with Diesel
|
|
|
|
|
/*
|
|
|
|
|
let results = sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
SELECT * FROM tasks
|
|
|
|
|
WHERE assignee = $1 OR reporter = $1
|
|
|
|
|
ORDER BY priority DESC, due_date ASC
|
|
|
|
|
"#,
|
|
|
|
|
user_id
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
Ok(results
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|r| serde_json::from_value(serde_json::to_value(r).unwrap()).unwrap())
|
|
|
|
|
.collect())
|
|
|
|
|
*/
|
|
|
|
|
Ok(vec![])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get tasks by status
|
|
|
|
|
pub async fn get_tasks_by_status(
|
|
|
|
|
&self,
|
|
|
|
|
_status: TaskStatus,
|
|
|
|
|
) -> Result<Vec<Task>, Box<dyn std::error::Error>> {
|
|
|
|
|
// TODO: Implement with Diesel
|
|
|
|
|
/*
|
|
|
|
|
let results = sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
SELECT * FROM tasks
|
|
|
|
|
WHERE status = $1
|
|
|
|
|
ORDER BY priority DESC, created_at ASC
|
|
|
|
|
"#,
|
|
|
|
|
serde_json::to_value(&status)?
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
Ok(results
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|r| serde_json::from_value(serde_json::to_value(r).unwrap()).unwrap())
|
|
|
|
|
.collect())
|
|
|
|
|
*/
|
|
|
|
|
Ok(vec![])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get overdue tasks
|
|
|
|
|
pub async fn get_overdue_tasks(&self) -> Result<Vec<Task>, Box<dyn std::error::Error>> {
|
|
|
|
|
// TODO: Implement with Diesel
|
|
|
|
|
/*
|
|
|
|
|
let now = Utc::now();
|
|
|
|
|
let results = sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
SELECT * FROM tasks
|
|
|
|
|
WHERE due_date < $1 AND status != 'done' AND status != 'cancelled'
|
|
|
|
|
ORDER BY due_date ASC
|
|
|
|
|
"#,
|
|
|
|
|
now
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
Ok(results
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|r| serde_json::from_value(serde_json::to_value(r).unwrap()).unwrap())
|
|
|
|
|
.collect())
|
|
|
|
|
*/
|
|
|
|
|
Ok(vec![])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// TODO: Implement with Diesel
|
|
|
|
|
/*
|
|
|
|
|
sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
INSERT INTO task_comments (id, task_id, author, content, created_at)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
|
|
|
"#,
|
|
|
|
|
comment.id,
|
|
|
|
|
comment.task_id,
|
|
|
|
|
comment.author,
|
|
|
|
|
comment.content,
|
|
|
|
|
comment.created_at
|
|
|
|
|
)
|
|
|
|
|
.execute(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
Ok(comment)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a subtask
|
|
|
|
|
pub async fn create_subtask(
|
|
|
|
|
&self,
|
|
|
|
|
parent_id: Uuid,
|
|
|
|
|
subtask: Task,
|
|
|
|
|
) -> Result<Task, Box<dyn std::error::Error>> {
|
|
|
|
|
let mut subtask = subtask;
|
|
|
|
|
subtask.parent_task_id = Some(parent_id);
|
|
|
|
|
|
|
|
|
|
let created = self.create_task(subtask).await?;
|
|
|
|
|
|
|
|
|
|
// Update parent's subtasks list
|
|
|
|
|
// TODO: Implement with Diesel
|
|
|
|
|
/*
|
|
|
|
|
sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
UPDATE tasks
|
|
|
|
|
SET subtasks = array_append(subtasks, $1)
|
|
|
|
|
WHERE id = $2
|
|
|
|
|
"#,
|
|
|
|
|
created.id,
|
|
|
|
|
parent_id
|
|
|
|
|
)
|
|
|
|
|
.execute(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
Ok(created)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get task dependencies
|
|
|
|
|
pub async fn get_task_dependencies(
|
|
|
|
|
&self,
|
|
|
|
|
task_id: Uuid,
|
|
|
|
|
) -> Result<Vec<Task>, Box<dyn std::error::Error>> {
|
|
|
|
|
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 {
|
|
|
|
|
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>> {
|
|
|
|
|
// TODO: Implement with Diesel
|
|
|
|
|
/*
|
|
|
|
|
let result = sqlx::query!("SELECT * FROM tasks WHERE id = $1", id)
|
|
|
|
|
.fetch_one(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
Ok(serde_json::from_value(serde_json::to_value(result)?)?)
|
|
|
|
|
*/
|
|
|
|
|
Err("Not implemented".into())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Calculate task progress (percentage)
|
2025-11-26 22:54:22 -03:00
|
|
|
pub async fn calculate_progress(
|
|
|
|
|
&self,
|
|
|
|
|
task_id: Uuid,
|
|
|
|
|
) -> Result<f32, Box<dyn std::error::Error>> {
|
2025-11-22 22:55:35 -03:00
|
|
|
let task = self.get_task(task_id).await?;
|
|
|
|
|
|
|
|
|
|
if task.subtasks.is_empty() {
|
|
|
|
|
// No subtasks, progress based on status
|
|
|
|
|
return Ok(match task.status {
|
|
|
|
|
TaskStatus::Todo => 0.0,
|
|
|
|
|
TaskStatus::InProgress => 50.0,
|
|
|
|
|
TaskStatus::Review => 75.0,
|
|
|
|
|
TaskStatus::Done => 100.0,
|
2025-11-26 22:54:22 -03:00
|
|
|
TaskStatus::Blocked => {
|
|
|
|
|
task.actual_hours.unwrap_or(0.0) / task.estimated_hours.unwrap_or(1.0) * 100.0
|
|
|
|
|
}
|
2025-11-22 22:55:35 -03:00
|
|
|
TaskStatus::Cancelled => 0.0,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Has subtasks, calculate based on subtask completion
|
|
|
|
|
let total = task.subtasks.len() as f32;
|
|
|
|
|
let mut completed = 0.0;
|
|
|
|
|
|
|
|
|
|
for subtask_id in task.subtasks {
|
|
|
|
|
if let Ok(subtask) = self.get_task(subtask_id).await {
|
|
|
|
|
if matches!(subtask.status, TaskStatus::Done) {
|
|
|
|
|
completed += 1.0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok((completed / total) * 100.0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a task from template
|
|
|
|
|
pub async fn create_from_template(
|
|
|
|
|
&self,
|
|
|
|
|
_template_id: Uuid,
|
|
|
|
|
assignee: Option<String>,
|
|
|
|
|
) -> Result<Task, Box<dyn std::error::Error>> {
|
|
|
|
|
// TODO: Implement with Diesel
|
|
|
|
|
/*
|
|
|
|
|
let template = sqlx::query!(
|
|
|
|
|
"SELECT * FROM task_templates WHERE id = $1",
|
|
|
|
|
template_id
|
|
|
|
|
)
|
|
|
|
|
.fetch_one(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let template: TaskTemplate = serde_json::from_value(serde_json::to_value(template)?)?;
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
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 task = Task {
|
|
|
|
|
id: Uuid::new_v4(),
|
|
|
|
|
title: template.name,
|
|
|
|
|
description: template.description,
|
|
|
|
|
assignee: assignee,
|
|
|
|
|
reporter: "system".to_string(),
|
|
|
|
|
status: TaskStatus::Todo,
|
|
|
|
|
priority: template.default_priority,
|
|
|
|
|
due_date: None,
|
|
|
|
|
estimated_hours: None,
|
|
|
|
|
actual_hours: None,
|
|
|
|
|
tags: template.default_tags,
|
|
|
|
|
parent_task_id: None,
|
|
|
|
|
subtasks: Vec::new(),
|
|
|
|
|
dependencies: Vec::new(),
|
|
|
|
|
attachments: Vec::new(),
|
|
|
|
|
comments: Vec::new(),
|
|
|
|
|
created_at: Utc::now(),
|
|
|
|
|
updated_at: Utc::now(),
|
|
|
|
|
completed_at: None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let created = self.create_task(task).await?;
|
|
|
|
|
|
|
|
|
|
// Create checklist items
|
|
|
|
|
for item in template.checklist {
|
|
|
|
|
let _checklist_item = ChecklistItem {
|
|
|
|
|
id: Uuid::new_v4(),
|
|
|
|
|
task_id: created.id,
|
|
|
|
|
description: item.description,
|
|
|
|
|
completed: false,
|
|
|
|
|
completed_by: None,
|
|
|
|
|
completed_at: None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// TODO: Implement with Diesel
|
|
|
|
|
/*
|
|
|
|
|
sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
INSERT INTO task_checklists (id, task_id, description, completed)
|
|
|
|
|
VALUES ($1, $2, $3, $4)
|
|
|
|
|
"#,
|
|
|
|
|
checklist_item.id,
|
|
|
|
|
checklist_item.task_id,
|
|
|
|
|
checklist_item.description,
|
|
|
|
|
checklist_item.completed
|
|
|
|
|
)
|
|
|
|
|
.execute(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
*/
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(created)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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>> {
|
|
|
|
|
// TODO: Implement with Diesel
|
|
|
|
|
/*
|
|
|
|
|
let results = sqlx::query!("SELECT * FROM tasks ORDER BY created_at DESC")
|
|
|
|
|
.fetch_all(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let tasks: Vec<Task> = results
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|r| serde_json::from_value(serde_json::to_value(r).unwrap()).unwrap())
|
|
|
|
|
.collect();
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
let tasks: Vec<Task> = vec![];
|
|
|
|
|
|
|
|
|
|
let mut cache = self.cache.write().await;
|
|
|
|
|
*cache = tasks;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get task statistics for reporting
|
|
|
|
|
pub async fn get_statistics(
|
|
|
|
|
&self,
|
|
|
|
|
user_id: Option<&str>,
|
|
|
|
|
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
|
|
|
|
let base_query = if let Some(uid) = user_id {
|
|
|
|
|
format!("WHERE assignee = '{}' OR reporter = '{}'", uid, uid)
|
|
|
|
|
} else {
|
|
|
|
|
String::new()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// TODO: Implement with Diesel
|
|
|
|
|
/*
|
|
|
|
|
let stats = sqlx::query(&format!(
|
|
|
|
|
r#"
|
|
|
|
|
SELECT
|
|
|
|
|
COUNT(*) FILTER (WHERE status = 'todo') as todo_count,
|
|
|
|
|
COUNT(*) FILTER (WHERE status = 'in_progress') as in_progress_count,
|
|
|
|
|
COUNT(*) FILTER (WHERE status = 'done') as done_count,
|
|
|
|
|
COUNT(*) FILTER (WHERE due_date < NOW() AND status != 'done') as overdue_count,
|
|
|
|
|
AVG(actual_hours / NULLIF(estimated_hours, 0)) as avg_completion_ratio
|
|
|
|
|
FROM tasks
|
|
|
|
|
{}
|
|
|
|
|
"#,
|
|
|
|
|
base_query
|
|
|
|
|
))
|
|
|
|
|
.fetch_one(self.db.as_ref())
|
|
|
|
|
.await?;
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// Return empty stats for now
|
|
|
|
|
Ok(serde_json::json!({
|
|
|
|
|
"todo_count": 0,
|
|
|
|
|
"in_progress_count": 0,
|
|
|
|
|
"done_count": 0,
|
|
|
|
|
"overdue_count": 0,
|
|
|
|
|
"avg_completion_ratio": null
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// HTTP API handlers
|
|
|
|
|
pub mod handlers {
|
|
|
|
|
use super::*;
|
2025-11-26 22:54:22 -03:00
|
|
|
use axum::extract::{Path as AxumPath, Query as AxumQuery, State as AxumState};
|
2025-11-22 22:55:35 -03:00
|
|
|
use axum::http::StatusCode;
|
2025-11-26 22:54:22 -03:00
|
|
|
use axum::response::{IntoResponse, Json as AxumJson};
|
2025-11-22 22:55:35 -03:00
|
|
|
|
|
|
|
|
pub async fn create_task_handler<S>(
|
|
|
|
|
AxumState(_engine): AxumState<S>,
|
|
|
|
|
AxumJson(task): AxumJson<Task>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
// TODO: Implement with actual engine
|
|
|
|
|
let created = task;
|
|
|
|
|
(StatusCode::OK, AxumJson(serde_json::json!(created)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_tasks_handler<S>(
|
|
|
|
|
AxumState(_engine): AxumState<S>,
|
|
|
|
|
AxumQuery(_query): AxumQuery<serde_json::Value>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
// TODO: Implement with actual engine
|
|
|
|
|
let tasks: Vec<Task> = vec![];
|
|
|
|
|
(StatusCode::OK, AxumJson(serde_json::json!(tasks)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn update_task_handler<S>(
|
|
|
|
|
AxumState(_engine): AxumState<S>,
|
|
|
|
|
AxumPath(_id): AxumPath<Uuid>,
|
|
|
|
|
AxumJson(_updates): AxumJson<TaskUpdate>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
// TODO: Implement with actual engine
|
|
|
|
|
let updated = serde_json::json!({"message": "Task updated"});
|
|
|
|
|
(StatusCode::OK, AxumJson(updated))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_statistics_handler<S>(
|
|
|
|
|
AxumState(_engine): AxumState<S>,
|
|
|
|
|
AxumQuery(_query): AxumQuery<serde_json::Value>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
// TODO: Implement with actual engine
|
|
|
|
|
let stats = serde_json::json!({
|
|
|
|
|
"todo_count": 0,
|
|
|
|
|
"in_progress_count": 0,
|
|
|
|
|
"done_count": 0,
|
|
|
|
|
"overdue_count": 0
|
|
|
|
|
});
|
|
|
|
|
(StatusCode::OK, AxumJson(stats))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
pub async fn handle_task_create(
|
|
|
|
|
State(engine): State<Arc<TaskEngine>>,
|
|
|
|
|
Json(mut task): Json<Task>,
|
|
|
|
|
) -> Result<Json<Task>, StatusCode> {
|
|
|
|
|
task.id = Uuid::new_v4();
|
|
|
|
|
task.created_at = Utc::now();
|
|
|
|
|
task.updated_at = Utc::now();
|
|
|
|
|
|
|
|
|
|
match engine.create_task(task).await {
|
|
|
|
|
Ok(created) => Ok(Json(created)),
|
|
|
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn handle_task_update(
|
|
|
|
|
State(engine): State<Arc<TaskEngine>>,
|
|
|
|
|
Path(id): Path<Uuid>,
|
|
|
|
|
Json(updates): Json<TaskUpdate>,
|
|
|
|
|
) -> Result<Json<Task>, StatusCode> {
|
|
|
|
|
match engine.update_task(id, updates).await {
|
|
|
|
|
Ok(updated) => Ok(Json(updated)),
|
|
|
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn handle_task_delete(
|
|
|
|
|
State(engine): State<Arc<TaskEngine>>,
|
|
|
|
|
Path(id): Path<Uuid>,
|
|
|
|
|
) -> Result<StatusCode, StatusCode> {
|
|
|
|
|
match engine.delete_task(id).await {
|
|
|
|
|
Ok(true) => Ok(StatusCode::NO_CONTENT),
|
|
|
|
|
Ok(false) => Err(StatusCode::NOT_FOUND),
|
|
|
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn handle_task_list(
|
|
|
|
|
State(engine): State<Arc<TaskEngine>>,
|
|
|
|
|
Query(params): Query<std::collections::HashMap<String, String>>,
|
|
|
|
|
) -> Result<Json<Vec<Task>>, StatusCode> {
|
|
|
|
|
let tasks = if let Some(user_id) = params.get("user_id") {
|
|
|
|
|
engine.get_user_tasks(user_id).await
|
|
|
|
|
} 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,
|
|
|
|
|
"cancelled" => TaskStatus::Cancelled,
|
|
|
|
|
_ => TaskStatus::Todo,
|
|
|
|
|
};
|
|
|
|
|
engine.get_tasks_by_status(status).await
|
|
|
|
|
} else {
|
|
|
|
|
engine.get_all_tasks().await
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match tasks {
|
|
|
|
|
Ok(task_list) => Ok(Json(task_list)),
|
|
|
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn handle_task_assign(
|
|
|
|
|
State(engine): State<Arc<TaskEngine>>,
|
|
|
|
|
Path(id): Path<Uuid>,
|
|
|
|
|
Json(payload): Json<serde_json::Value>,
|
|
|
|
|
) -> Result<Json<Task>, StatusCode> {
|
|
|
|
|
let assignee = payload["assignee"]
|
|
|
|
|
.as_str()
|
|
|
|
|
.ok_or(StatusCode::BAD_REQUEST)?;
|
|
|
|
|
|
|
|
|
|
match engine.assign_task(id, assignee.to_string()).await {
|
|
|
|
|
Ok(updated) => Ok(Json(updated)),
|
|
|
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn handle_task_status_update(
|
|
|
|
|
State(engine): State<Arc<TaskEngine>>,
|
|
|
|
|
Path(id): Path<Uuid>,
|
|
|
|
|
Json(payload): Json<serde_json::Value>,
|
|
|
|
|
) -> Result<Json<Task>, StatusCode> {
|
|
|
|
|
let status_str = payload["status"].as_str().ok_or(StatusCode::BAD_REQUEST)?;
|
|
|
|
|
let status = match status_str {
|
|
|
|
|
"todo" => TaskStatus::Todo,
|
|
|
|
|
"in_progress" => TaskStatus::InProgress,
|
|
|
|
|
"review" => TaskStatus::Review,
|
|
|
|
|
"done" => TaskStatus::Done,
|
|
|
|
|
"blocked" => TaskStatus::Blocked,
|
|
|
|
|
"cancelled" => TaskStatus::Cancelled,
|
|
|
|
|
_ => return Err(StatusCode::BAD_REQUEST),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let updates = TaskUpdate {
|
|
|
|
|
title: None,
|
|
|
|
|
description: None,
|
|
|
|
|
status: Some(status),
|
|
|
|
|
priority: None,
|
|
|
|
|
assignee: None,
|
|
|
|
|
due_date: None,
|
|
|
|
|
tags: None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match engine.update_task(id, updates).await {
|
|
|
|
|
Ok(updated) => Ok(Json(updated)),
|
|
|
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn handle_task_priority_set(
|
|
|
|
|
State(engine): State<Arc<TaskEngine>>,
|
|
|
|
|
Path(id): Path<Uuid>,
|
|
|
|
|
Json(payload): Json<serde_json::Value>,
|
|
|
|
|
) -> Result<Json<Task>, StatusCode> {
|
|
|
|
|
let priority_str = payload["priority"]
|
|
|
|
|
.as_str()
|
|
|
|
|
.ok_or(StatusCode::BAD_REQUEST)?;
|
|
|
|
|
let priority = match priority_str {
|
|
|
|
|
"low" => TaskPriority::Low,
|
|
|
|
|
"medium" => TaskPriority::Medium,
|
|
|
|
|
"high" => TaskPriority::High,
|
|
|
|
|
"urgent" => TaskPriority::Urgent,
|
|
|
|
|
_ => return Err(StatusCode::BAD_REQUEST),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let updates = TaskUpdate {
|
|
|
|
|
title: None,
|
|
|
|
|
description: None,
|
|
|
|
|
status: None,
|
|
|
|
|
priority: Some(priority),
|
|
|
|
|
assignee: None,
|
|
|
|
|
due_date: None,
|
|
|
|
|
tags: None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match engine.update_task(id, updates).await {
|
|
|
|
|
Ok(updated) => Ok(Json(updated)),
|
|
|
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn handle_task_dependencies_set(
|
|
|
|
|
State(engine): State<Arc<TaskEngine>>,
|
|
|
|
|
Path(id): Path<Uuid>,
|
|
|
|
|
Json(payload): Json<serde_json::Value>,
|
|
|
|
|
) -> Result<Json<Task>, 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 engine.set_dependencies(id, deps).await {
|
|
|
|
|
Ok(updated) => Ok(Json(updated)),
|
|
|
|
|
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 22:55:35 -03:00
|
|
|
/// Configure task engine routes
|
2025-11-26 22:54:22 -03:00
|
|
|
pub fn configure_task_routes(state: Arc<TaskEngine>) -> Router {
|
|
|
|
|
Router::new()
|
|
|
|
|
.route("/api/tasks", post(handle_task_create))
|
|
|
|
|
.route("/api/tasks", get(handle_task_list))
|
|
|
|
|
.route("/api/tasks/:id", put(handle_task_update))
|
|
|
|
|
.route("/api/tasks/:id", delete(handle_task_delete))
|
|
|
|
|
.route("/api/tasks/:id/assign", post(handle_task_assign))
|
|
|
|
|
.route("/api/tasks/:id/status", put(handle_task_status_update))
|
|
|
|
|
.route("/api/tasks/:id/priority", put(handle_task_priority_set))
|
|
|
|
|
.route(
|
|
|
|
|
"/api/tasks/:id/dependencies",
|
|
|
|
|
put(handle_task_dependencies_set),
|
|
|
|
|
)
|
|
|
|
|
.with_state(state)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Configure task engine routes (legacy)
|
2025-11-22 22:55:35 -03:00
|
|
|
pub fn configure<S>(router: Router<S>) -> Router<S>
|
|
|
|
|
where
|
|
|
|
|
S: Clone + Send + Sync + 'static,
|
|
|
|
|
{
|
|
|
|
|
use axum::routing::{get, post, put};
|
|
|
|
|
|
|
|
|
|
router
|
|
|
|
|
.route("/api/tasks", post(handlers::create_task_handler::<S>))
|
|
|
|
|
.route("/api/tasks", get(handlers::get_tasks_handler::<S>))
|
|
|
|
|
.route("/api/tasks/:id", put(handlers::update_task_handler::<S>))
|
2025-11-26 22:54:22 -03:00
|
|
|
.route(
|
|
|
|
|
"/api/tasks/statistics",
|
|
|
|
|
get(handlers::get_statistics_handler::<S>),
|
|
|
|
|
)
|
2025-11-22 22:55:35 -03:00
|
|
|
}
|