use crate::shared::models::UserSession; use crate::shared::state::AppState; use chrono::{DateTime, Duration, NaiveDate, Utc}; use diesel::prelude::*; use log::{error, trace}; use rhai::{Dynamic, Engine}; use std::sync::Arc; use uuid::Uuid; pub fn create_task_keyword(state: Arc, user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); let user_clone = user.clone(); engine .register_custom_syntax( &[ "CREATE_TASK", "$expr$", ",", "$expr$", ",", "$expr$", ",", "$expr$", ], false, move |context, inputs| { let title = context.eval_expression_tree(&inputs[0])?.to_string(); let assignee = context.eval_expression_tree(&inputs[1])?.to_string(); let due_date = context.eval_expression_tree(&inputs[2])?.to_string(); let project_id_input = context.eval_expression_tree(&inputs[3])?; let project_id = if project_id_input.is_unit() || project_id_input.to_string() == "null" { None } else { Some(project_id_input.to_string()) }; trace!( "CREATE_TASK: title={}, assignee={}, due_date={}, project_id={:?} for user={}", title, assignee, due_date, project_id, user_clone.user_id ); let state_for_task = Arc::clone(&state_clone); let user_for_task = user_clone.clone(); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() .build(); let send_err = if let Ok(rt) = rt { let result = rt.block_on(async move { execute_create_task( &state_for_task, &user_for_task, &title, &assignee, &due_date, project_id.as_deref(), ) .await }); tx.send(result).err() } else { tx.send(Err("Failed to build tokio runtime".to_string())) .err() }; if send_err.is_some() { error!("Failed to send CREATE_TASK result from thread"); } }); match rx.recv_timeout(std::time::Duration::from_secs(10)) { Ok(Ok(task_id)) => Ok(Dynamic::from(task_id)), Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("CREATE_TASK failed: {}", e).into(), rhai::Position::NONE, ))), Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { Err(Box::new(rhai::EvalAltResult::ErrorRuntime( "CREATE_TASK timed out".into(), rhai::Position::NONE, ))) } Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("CREATE_TASK thread failed: {}", e).into(), rhai::Position::NONE, ))), } }, ) .unwrap(); // Register ASSIGN_SMART for intelligent task assignment let state_clone2 = Arc::clone(&state); let user_clone2 = user.clone(); engine .register_custom_syntax( &["ASSIGN_SMART", "$expr$", ",", "$expr$", ",", "$expr$"], false, move |context, inputs| { let task_id = context.eval_expression_tree(&inputs[0])?.to_string(); let team_input = context.eval_expression_tree(&inputs[1])?; let load_balance = context .eval_expression_tree(&inputs[2])? .as_bool() .unwrap_or(true); let mut team = Vec::new(); if team_input.is_array() { let arr = team_input.cast::(); for item in arr.iter() { team.push(item.to_string()); } } else { team.push(team_input.to_string()); } trace!( "ASSIGN_SMART: task={}, team={:?}, load_balance={} for user={}", task_id, team, load_balance, user_clone2.user_id ); let state_for_task = Arc::clone(&state_clone2); let user_for_task = user_clone2.clone(); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() .build(); let send_err = if let Ok(rt) = rt { let result = rt.block_on(async move { smart_assign_task( &state_for_task, &user_for_task, &task_id, team, load_balance, ) .await }); tx.send(result).err() } else { tx.send(Err("Failed to build tokio runtime".to_string())) .err() }; if send_err.is_some() { error!("Failed to send ASSIGN_SMART result from thread"); } }); match rx.recv_timeout(std::time::Duration::from_secs(10)) { Ok(Ok(assignee)) => Ok(Dynamic::from(assignee)), Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("ASSIGN_SMART failed: {}", e).into(), rhai::Position::NONE, ))), Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( "ASSIGN_SMART timed out".into(), rhai::Position::NONE, ))), } }, ) .unwrap(); } async fn execute_create_task( state: &AppState, user: &UserSession, title: &str, assignee: &str, due_date: &str, project_id: Option<&str>, ) -> Result { let task_id = Uuid::new_v4().to_string(); // Parse due date let due_datetime = parse_due_date(due_date)?; // Determine actual assignee let actual_assignee = if assignee == "auto" { // Auto-assign based on workload auto_assign_task(state, project_id).await? } else { assignee.to_string() }; // Determine priority based on due date let priority = determine_priority(due_datetime); // Save task to database let mut conn = state.conn.get().map_err(|e| format!("DB error: {}", e))?; let query = diesel::sql_query( "INSERT INTO tasks (id, title, assignee, due_date, project_id, priority, status, created_by, created_at) VALUES ($1, $2, $3, $4, $5, $6, 'open', $7, $8)" ) .bind::(&task_id) .bind::(title) .bind::(&actual_assignee) .bind::, _>(&due_datetime) .bind::, _>(&project_id) .bind::(&priority); let user_id_str = user.user_id.to_string(); let now = Utc::now(); let query = query .bind::(&user_id_str) .bind::(&now); query.execute(&mut *conn).map_err(|e| { error!("Failed to create task: {}", e); format!("Failed to create task: {}", e) })?; // Send notification to assignee send_task_notification(state, &task_id, title, &actual_assignee, due_datetime).await?; trace!( "Created task '{}' assigned to {} (ID: {})", title, actual_assignee, task_id ); Ok(task_id) } async fn smart_assign_task( state: &AppState, _user: &UserSession, task_id: &str, team: Vec, load_balance: bool, ) -> Result { if !load_balance { // Simple assignment to first available team member return Ok(team[0].clone()); } // Get workload for each team member let mut conn = state.conn.get().map_err(|e| format!("DB error: {}", e))?; let mut best_assignee = team[0].clone(); let mut min_workload = i64::MAX; for member in &team { // Count open tasks for this member let query = diesel::sql_query( "SELECT COUNT(*) as task_count FROM tasks WHERE assignee = $1 AND status IN ('open', 'in_progress')", ) .bind::(member); #[derive(QueryableByName)] struct TaskCount { #[diesel(sql_type = diesel::sql_types::BigInt)] task_count: i64, } let result: Result, _> = query.load(&mut *conn); if let Ok(counts) = result { if let Some(count) = counts.first() { if count.task_count < min_workload { min_workload = count.task_count; best_assignee = member.clone(); } } } } // Update task assignment let update_query = diesel::sql_query("UPDATE tasks SET assignee = $1 WHERE id = $2") .bind::(&best_assignee) .bind::(task_id); update_query.execute(&mut *conn).map_err(|e| { error!("Failed to update task assignment: {}", e); format!("Failed to update task assignment: {}", e) })?; trace!( "Smart-assigned task {} to {} (workload: {})", task_id, best_assignee, min_workload ); Ok(best_assignee) } async fn auto_assign_task(state: &AppState, project_id: Option<&str>) -> Result { let mut conn = state.conn.get().map_err(|e| format!("DB error: {}", e))?; // Get team members for the project let team_query_str = if let Some(proj_id) = project_id { format!( "SELECT DISTINCT assignee FROM tasks WHERE project_id = '{}' AND assignee IS NOT NULL ORDER BY COUNT(*) ASC LIMIT 5", proj_id ) } else { "SELECT DISTINCT assignee FROM tasks WHERE assignee IS NOT NULL ORDER BY COUNT(*) ASC LIMIT 5" .to_string() }; let team_query = diesel::sql_query(&team_query_str); #[derive(QueryableByName)] struct TeamMember { #[diesel(sql_type = diesel::sql_types::Text)] assignee: String, } let team: Vec = team_query.load(&mut *conn).unwrap_or_default(); if team.is_empty() { return Ok("unassigned".to_string()); } // Return the team member with the least tasks Ok(team[0].assignee.clone()) } fn parse_due_date(due_date: &str) -> Result>, String> { let due_lower = due_date.to_lowercase(); if due_lower == "null" || due_lower.is_empty() { return Ok(None); } let now = Utc::now(); // Handle relative dates like "+3 days", "tomorrow", etc. if due_lower.starts_with('+') { let days_str = due_lower .trim_start_matches('+') .trim() .split_whitespace() .next() .unwrap_or("0"); if let Ok(days) = days_str.parse::() { return Ok(Some(now + Duration::days(days))); } } if due_lower == "today" { return Ok(Some( now.date_naive().and_hms_opt(17, 0, 0).unwrap().and_utc(), )); } if due_lower == "tomorrow" { return Ok(Some( (now + Duration::days(1)) .date_naive() .and_hms_opt(17, 0, 0) .unwrap() .and_utc(), )); } if due_lower.contains("next week") { return Ok(Some(now + Duration::days(7))); } if due_lower.contains("next month") { return Ok(Some(now + Duration::days(30))); } // Try parsing as a date if let Ok(date) = NaiveDate::parse_from_str(&due_date, "%Y-%m-%d") { return Ok(Some(date.and_hms_opt(17, 0, 0).unwrap().and_utc())); } // Default to 3 days from now Ok(Some(now + Duration::days(3))) } fn determine_priority(due_date: Option>) -> String { if let Some(due) = due_date { let now = Utc::now(); let days_until = (due - now).num_days(); if days_until <= 1 { "high".to_string() } else if days_until <= 7 { "medium".to_string() } else { "low".to_string() } } else { "medium".to_string() } } async fn send_task_notification( _state: &AppState, task_id: &str, title: &str, assignee: &str, due_date: Option>, ) -> Result<(), String> { // In a real implementation, this would send an actual notification trace!( "Notification sent to {} for task '{}' (ID: {})", assignee, title, task_id ); if let Some(due) = due_date { trace!("Task due: {}", due.format("%Y-%m-%d %H:%M")); } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_due_date() { assert!(parse_due_date("tomorrow").is_ok()); assert!(parse_due_date("+3 days").is_ok()); assert!(parse_due_date("2024-12-31").is_ok()); assert!(parse_due_date("null").unwrap().is_none()); } #[test] fn test_determine_priority() { let tomorrow = Some(Utc::now() + Duration::days(1)); assert_eq!(determine_priority(tomorrow), "high"); let next_week = Some(Utc::now() + Duration::days(7)); assert_eq!(determine_priority(next_week), "medium"); let next_month = Some(Utc::now() + Duration::days(30)); assert_eq!(determine_priority(next_month), "low"); } }