botserver/src/contacts/tasks_integration.rs

1299 lines
42 KiB
Rust

use axum::{response::IntoResponse, Json};
use chrono::{DateTime, Utc};
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
use crate::core::shared::schema::people::{crm_contacts as crm_contacts_table, people as people_table};
use crate::core::shared::schema::tasks::tasks as tasks_table;
use crate::shared::utils::DbPool;
#[derive(Debug, Clone)]
pub enum TasksIntegrationError {
DatabaseError(String),
ContactNotFound,
TaskNotFound,
AlreadyAssigned,
NotAssigned,
Unauthorized,
InvalidInput(String),
}
impl std::fmt::Display for TasksIntegrationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DatabaseError(e) => write!(f, "Database error: {e}"),
Self::ContactNotFound => write!(f, "Contact not found"),
Self::TaskNotFound => write!(f, "Task not found"),
Self::AlreadyAssigned => write!(f, "Contact already assigned"),
Self::NotAssigned => write!(f, "Contact not assigned"),
Self::Unauthorized => write!(f, "Unauthorized"),
Self::InvalidInput(e) => write!(f, "Invalid input: {e}"),
}
}
}
impl std::error::Error for TasksIntegrationError {}
impl IntoResponse for TasksIntegrationError {
fn into_response(self) -> axum::response::Response {
use axum::http::StatusCode;
let status = match &self {
Self::ContactNotFound | Self::TaskNotFound => StatusCode::NOT_FOUND,
Self::AlreadyAssigned | Self::NotAssigned => StatusCode::CONFLICT,
Self::Unauthorized => StatusCode::UNAUTHORIZED,
Self::InvalidInput(_) => StatusCode::BAD_REQUEST,
Self::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(serde_json::json!({ "error": self.to_string() }))).into_response()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskContact {
pub id: Uuid,
pub task_id: Uuid,
pub contact_id: Uuid,
pub role: TaskContactRole,
pub assigned_at: DateTime<Utc>,
pub assigned_by: Uuid,
pub notified: bool,
pub notified_at: Option<DateTime<Utc>>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum TaskContactRole {
#[default]
Assignee,
Reviewer,
Stakeholder,
Collaborator,
Client,
Vendor,
Consultant,
Approver,
}
impl std::fmt::Display for TaskContactRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TaskContactRole::Assignee => write!(f, "assignee"),
TaskContactRole::Reviewer => write!(f, "reviewer"),
TaskContactRole::Stakeholder => write!(f, "stakeholder"),
TaskContactRole::Collaborator => write!(f, "collaborator"),
TaskContactRole::Client => write!(f, "client"),
TaskContactRole::Vendor => write!(f, "vendor"),
TaskContactRole::Consultant => write!(f, "consultant"),
TaskContactRole::Approver => write!(f, "approver"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssignContactRequest {
pub contact_id: Uuid,
pub role: Option<TaskContactRole>,
pub send_notification: Option<bool>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BulkAssignContactsRequest {
pub assignments: Vec<ContactAssignment>,
pub send_notification: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContactAssignment {
pub contact_id: Uuid,
pub role: Option<TaskContactRole>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateTaskContactRequest {
pub role: Option<TaskContactRole>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskContactsQuery {
pub role: Option<TaskContactRole>,
pub include_contact_details: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContactTasksQuery {
pub status: Option<String>,
pub priority: Option<String>,
pub role: Option<TaskContactRole>,
pub due_before: Option<DateTime<Utc>>,
pub due_after: Option<DateTime<Utc>>,
pub project_id: Option<Uuid>,
pub limit: Option<u32>,
pub offset: Option<u32>,
pub sort_by: Option<TaskSortField>,
pub sort_order: Option<SortOrder>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum TaskSortField {
#[default]
DueDate,
Priority,
CreatedAt,
UpdatedAt,
Title,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum SortOrder {
#[default]
Asc,
Desc,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskContactWithDetails {
pub task_contact: TaskContact,
pub contact: ContactSummary,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContactSummary {
pub id: Uuid,
pub first_name: String,
pub last_name: String,
pub email: Option<String>,
pub phone: Option<String>,
pub company: Option<String>,
pub job_title: Option<String>,
pub avatar_url: Option<String>,
}
impl ContactSummary {
pub fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name).trim().to_string()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskSummary {
pub id: Uuid,
pub title: String,
pub description: Option<String>,
pub status: String,
pub priority: String,
pub due_date: Option<DateTime<Utc>>,
pub project_id: Option<Uuid>,
pub project_name: Option<String>,
pub progress: u8,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContactTaskWithDetails {
pub task_contact: TaskContact,
pub task: TaskSummary,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContactTasksResponse {
pub tasks: Vec<ContactTaskWithDetails>,
pub total_count: u32,
pub by_status: HashMap<String, u32>,
pub by_priority: HashMap<String, u32>,
pub overdue_count: u32,
pub due_today_count: u32,
pub due_this_week_count: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContactTaskStats {
pub contact_id: Uuid,
pub total_tasks: u32,
pub completed_tasks: u32,
pub in_progress_tasks: u32,
pub overdue_tasks: u32,
pub completion_rate: f32,
pub average_completion_time_days: Option<f32>,
pub tasks_by_role: HashMap<String, u32>,
pub recent_activity: Vec<TaskActivity>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskActivity {
pub id: Uuid,
pub task_id: Uuid,
pub task_title: String,
pub activity_type: TaskActivityType,
pub description: String,
pub occurred_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TaskActivityType {
Assigned,
Unassigned,
StatusChanged,
Completed,
Commented,
Updated,
DueDateChanged,
}
impl std::fmt::Display for TaskActivityType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TaskActivityType::Assigned => write!(f, "assigned"),
TaskActivityType::Unassigned => write!(f, "unassigned"),
TaskActivityType::StatusChanged => write!(f, "status_changed"),
TaskActivityType::Completed => write!(f, "completed"),
TaskActivityType::Commented => write!(f, "commented"),
TaskActivityType::Updated => write!(f, "updated"),
TaskActivityType::DueDateChanged => write!(f, "due_date_changed"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SuggestedTaskContact {
pub contact: ContactSummary,
pub reason: TaskSuggestionReason,
pub score: f32,
pub workload: ContactWorkload,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TaskSuggestionReason {
PreviouslyAssigned,
SameProject,
SimilarTasks,
TeamMember,
ExpertInArea,
LowWorkload,
ClientContact,
}
impl std::fmt::Display for TaskSuggestionReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TaskSuggestionReason::PreviouslyAssigned => write!(f, "Previously assigned to similar tasks"),
TaskSuggestionReason::SameProject => write!(f, "Assigned to same project"),
TaskSuggestionReason::SimilarTasks => write!(f, "Completed similar tasks"),
TaskSuggestionReason::TeamMember => write!(f, "Team member"),
TaskSuggestionReason::ExpertInArea => write!(f, "Expert in this area"),
TaskSuggestionReason::LowWorkload => write!(f, "Has capacity"),
TaskSuggestionReason::ClientContact => write!(f, "Client contact"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContactWorkload {
pub active_tasks: u32,
pub high_priority_tasks: u32,
pub overdue_tasks: u32,
pub due_this_week: u32,
pub workload_level: WorkloadLevel,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum WorkloadLevel {
Low,
Medium,
High,
Overloaded,
}
impl std::fmt::Display for WorkloadLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WorkloadLevel::Low => write!(f, "low"),
WorkloadLevel::Medium => write!(f, "medium"),
WorkloadLevel::High => write!(f, "high"),
WorkloadLevel::Overloaded => write!(f, "overloaded"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateTaskForContactRequest {
pub title: String,
pub description: Option<String>,
pub priority: Option<String>,
pub due_date: Option<DateTime<Utc>>,
pub project_id: Option<Uuid>,
pub tags: Option<Vec<String>>,
pub role: Option<TaskContactRole>,
pub send_notification: Option<bool>,
}
pub struct TasksIntegrationService {
db_pool: DbPool,
}
impl TasksIntegrationService {
pub fn new(pool: DbPool) -> Self {
Self { db_pool: pool }
}
pub async fn assign_contact_to_task(
&self,
organization_id: Uuid,
task_id: Uuid,
request: &AssignContactRequest,
assigned_by: Uuid,
) -> Result<TaskContact, TasksIntegrationError> {
// Verify contact exists and belongs to organization
self.verify_contact(organization_id, request.contact_id).await?;
// Verify task exists
self.verify_task(organization_id, task_id).await?;
// Check if already assigned
if self.is_contact_assigned(task_id, request.contact_id).await? {
return Err(TasksIntegrationError::AlreadyAssigned);
}
let id = Uuid::new_v4();
let now = Utc::now();
let role = request.role.clone().unwrap_or_default();
// Create assignment in database
self.create_task_contact_assignment(
id,
task_id,
request.contact_id,
&role,
assigned_by,
request.notes.as_deref(),
now,
)
.await?;
// Send notification if requested
let notified = if request.send_notification.unwrap_or(true) {
self.send_task_assignment_notification(task_id, request.contact_id)
.await
.is_ok()
} else {
false
};
// Log activity
self.log_contact_activity(
request.contact_id,
TaskActivityType::Assigned,
&format!("Assigned to task"),
task_id,
)
.await?;
Ok(TaskContact {
id,
task_id,
contact_id: request.contact_id,
role,
assigned_at: now,
assigned_by,
notified,
notified_at: if notified { Some(now) } else { None },
notes: request.notes.clone(),
})
}
pub async fn bulk_assign_contacts(
&self,
organization_id: Uuid,
task_id: Uuid,
request: &BulkAssignContactsRequest,
assigned_by: Uuid,
) -> Result<Vec<TaskContact>, TasksIntegrationError> {
let mut results = Vec::new();
for assignment in &request.assignments {
let assign_request = AssignContactRequest {
contact_id: assignment.contact_id,
role: assignment.role.clone(),
send_notification: request.send_notification,
notes: assignment.notes.clone(),
};
match self
.assign_contact_to_task(organization_id, task_id, &assign_request, assigned_by)
.await
{
Ok(task_contact) => results.push(task_contact),
Err(TasksIntegrationError::AlreadyAssigned) => continue,
Err(e) => return Err(e),
}
}
Ok(results)
}
pub async fn unassign_contact_from_task(
&self,
organization_id: Uuid,
task_id: Uuid,
contact_id: Uuid,
) -> Result<(), TasksIntegrationError> {
self.verify_contact(organization_id, contact_id).await?;
self.verify_task(organization_id, task_id).await?;
self.delete_task_contact_assignment(task_id, contact_id).await?;
self.log_contact_activity(
contact_id,
TaskActivityType::Unassigned,
"Unassigned from task",
task_id,
)
.await?;
Ok(())
}
pub async fn update_task_contact(
&self,
organization_id: Uuid,
task_id: Uuid,
contact_id: Uuid,
request: &UpdateTaskContactRequest,
) -> Result<TaskContact, TasksIntegrationError> {
self.verify_contact(organization_id, contact_id).await?;
self.verify_task(organization_id, task_id).await?;
let mut task_contact = self.get_task_contact(task_id, contact_id).await?;
if let Some(role) = &request.role {
task_contact.role = role.clone();
}
if let Some(notes) = &request.notes {
task_contact.notes = Some(notes.clone());
}
self.update_task_contact_in_db(&task_contact).await?;
Ok(task_contact)
}
pub async fn get_task_contacts(
&self,
organization_id: Uuid,
task_id: Uuid,
query: &TaskContactsQuery,
) -> Result<Vec<TaskContactWithDetails>, TasksIntegrationError> {
self.verify_task(organization_id, task_id).await?;
let contacts = self.fetch_task_contacts(task_id, query).await?;
if query.include_contact_details.unwrap_or(true) {
let mut results = Vec::new();
for task_contact in contacts {
if let Ok(contact) = self.get_contact_summary(task_contact.contact_id).await {
results.push(TaskContactWithDetails {
task_contact,
contact,
});
}
}
Ok(results)
} else {
Ok(contacts
.into_iter()
.map(|tc| TaskContactWithDetails {
contact: ContactSummary {
id: tc.contact_id,
first_name: String::new(),
last_name: String::new(),
email: None,
phone: None,
company: None,
job_title: None,
avatar_url: None,
},
task_contact: tc,
})
.collect())
}
}
pub async fn get_contact_tasks(
&self,
organization_id: Uuid,
contact_id: Uuid,
query: &ContactTasksQuery,
) -> Result<ContactTasksResponse, TasksIntegrationError> {
self.verify_contact(organization_id, contact_id).await?;
let tasks = self.fetch_contact_tasks(contact_id, query).await?;
let total_count = tasks.len() as u32;
let now = Utc::now();
let week_end = now + chrono::Duration::days(7);
let mut by_status: HashMap<String, u32> = HashMap::new();
let mut by_priority: HashMap<String, u32> = HashMap::new();
let mut overdue_count = 0;
let mut due_today_count = 0;
let mut due_this_week_count = 0;
for task in &tasks {
*by_status.entry(task.task.status.clone()).or_insert(0) += 1;
*by_priority.entry(task.task.priority.clone()).or_insert(0) += 1;
if let Some(due_date) = task.task.due_date {
if due_date < now && task.task.status != "completed" {
overdue_count += 1;
} else if due_date.date_naive() == now.date_naive() {
due_today_count += 1;
} else if due_date < week_end {
due_this_week_count += 1;
}
}
}
Ok(ContactTasksResponse {
tasks,
total_count,
by_status,
by_priority,
overdue_count,
due_today_count,
due_this_week_count,
})
}
pub async fn get_contact_task_stats(
&self,
organization_id: Uuid,
contact_id: Uuid,
) -> Result<ContactTaskStats, TasksIntegrationError> {
self.verify_contact(organization_id, contact_id).await?;
let stats = self.calculate_contact_task_stats(contact_id).await?;
Ok(stats)
}
pub async fn get_suggested_contacts(
&self,
organization_id: Uuid,
task_id: Uuid,
limit: Option<u32>,
) -> Result<Vec<SuggestedTaskContact>, TasksIntegrationError> {
self.verify_task(organization_id, task_id).await?;
let limit = limit.unwrap_or(10);
let mut suggestions: Vec<SuggestedTaskContact> = Vec::new();
// Get task details for context
let task = self.get_task_details(task_id).await?;
// Get already assigned contacts to exclude
let assigned_contacts = self.get_assigned_contact_ids(task_id).await?;
// Find contacts previously assigned to similar tasks
let previous_assignees = self
.find_similar_task_assignees(&task, &assigned_contacts, 5)
.await?;
for (contact, workload) in previous_assignees {
suggestions.push(SuggestedTaskContact {
contact,
reason: TaskSuggestionReason::PreviouslyAssigned,
score: 0.9,
workload,
});
}
// Find contacts assigned to same project
if let Some(project_id) = task.project_id {
let project_contacts = self
.find_project_contacts(project_id, &assigned_contacts, 5)
.await?;
for (contact, workload) in project_contacts {
suggestions.push(SuggestedTaskContact {
contact,
reason: TaskSuggestionReason::SameProject,
score: 0.8,
workload,
});
}
}
// Find contacts with low workload
let available_contacts = self
.find_low_workload_contacts(organization_id, &assigned_contacts, 5)
.await?;
for (contact, workload) in available_contacts {
if workload.workload_level == WorkloadLevel::Low {
suggestions.push(SuggestedTaskContact {
contact,
reason: TaskSuggestionReason::LowWorkload,
score: 0.6,
workload,
});
}
}
// Sort by score and limit
suggestions.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
suggestions.truncate(limit as usize);
Ok(suggestions)
}
pub async fn get_contact_workload(
&self,
organization_id: Uuid,
contact_id: Uuid,
) -> Result<ContactWorkload, TasksIntegrationError> {
self.verify_contact(organization_id, contact_id).await?;
let workload = self.calculate_contact_workload(contact_id).await?;
Ok(workload)
}
pub async fn create_task_for_contact(
&self,
organization_id: Uuid,
contact_id: Uuid,
request: &CreateTaskForContactRequest,
created_by: Uuid,
) -> Result<ContactTaskWithDetails, TasksIntegrationError> {
self.verify_contact(organization_id, contact_id).await?;
// Create task
let task_id = Uuid::new_v4();
let now = Utc::now();
self.create_task_in_db(
task_id,
organization_id,
&request.title,
request.description.as_deref(),
Some(created_by),
request.due_date,
)
.await?;
// Assign contact
let assign_request = AssignContactRequest {
contact_id,
role: request.role.clone(),
send_notification: request.send_notification,
notes: None,
};
let task_contact = self
.assign_contact_to_task(organization_id, task_id, &assign_request, created_by)
.await?;
let task = TaskSummary {
id: task_id,
title: request.title.clone(),
description: request.description.clone(),
status: "todo".to_string(),
priority: request.priority.clone().unwrap_or_else(|| "medium".to_string()),
due_date: request.due_date,
project_id: request.project_id,
project_name: None,
progress: 0,
created_at: now,
updated_at: now,
};
Ok(ContactTaskWithDetails { task_contact, task })
}
async fn send_task_assignment_notification(
&self,
_task_id: Uuid,
_contact_id: Uuid,
) -> Result<(), TasksIntegrationError> {
Ok(())
}
async fn log_contact_activity(
&self,
_contact_id: Uuid,
_activity_type: TaskActivityType,
_description: &str,
_task_id: Uuid,
) -> Result<(), TasksIntegrationError> {
Ok(())
}
async fn verify_contact(
&self,
_organization_id: Uuid,
_contact_id: Uuid,
) -> Result<(), TasksIntegrationError> {
// Verify contact exists and belongs to organization
Ok(())
}
async fn verify_task(
&self,
_organization_id: Uuid,
_task_id: Uuid,
) -> Result<(), TasksIntegrationError> {
// Verify task exists and belongs to organization
Ok(())
}
async fn is_contact_assigned(
&self,
_task_id: Uuid,
_contact_id: Uuid,
) -> Result<bool, TasksIntegrationError> {
// Check if contact is already assigned to task
Ok(false)
}
async fn create_task_contact_assignment(
&self,
_id: Uuid,
_task_id: Uuid,
_contact_id: Uuid,
_role: &TaskContactRole,
_assigned_by: Uuid,
_notes: Option<&str>,
_assigned_at: DateTime<Utc>,
) -> Result<(), TasksIntegrationError> {
// Insert into task_contacts table
Ok(())
}
async fn delete_task_contact_assignment(
&self,
_task_id: Uuid,
_contact_id: Uuid,
) -> Result<(), TasksIntegrationError> {
// Delete from task_contacts table
Ok(())
}
async fn get_task_contact(
&self,
task_id: Uuid,
contact_id: Uuid,
) -> Result<TaskContact, TasksIntegrationError> {
// Query task_contacts table
Ok(TaskContact {
id: Uuid::new_v4(),
task_id,
contact_id,
role: TaskContactRole::Assignee,
assigned_at: Utc::now(),
assigned_by: Uuid::new_v4(),
notified: false,
notified_at: None,
notes: None,
})
}
async fn update_task_contact_in_db(
&self,
task_contact: &TaskContact,
) -> Result<(), TasksIntegrationError> {
let pool = self.db_pool.clone();
let task_id = task_contact.task_id;
let contact_id = task_contact.contact_id;
let role = task_contact.role.to_string();
let _notes = task_contact.notes.clone();
tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|e| TasksIntegrationError::DatabaseError(e.to_string()))?;
// Get the contact's email to find the corresponding person
let contact_email: Option<String> = crm_contacts_table::table
.filter(crm_contacts_table::id.eq(contact_id))
.select(crm_contacts_table::email)
.first(&mut conn)
.map_err(|e: diesel::result::Error| TasksIntegrationError::DatabaseError(format!("Contact not found: {}", e)))?;
let contact_email = match contact_email {
Some(email) => email,
None => return Ok(()), // No email, can't link to person
};
// Find the person with this email
let person_id: Result<uuid::Uuid, _> = people_table::table
.filter(people_table::email.eq(&contact_email))
.select(people_table::id)
.first(&mut conn);
if let Ok(pid) = person_id {
// Update the task's assigned_to field if this is an assignee
if role == "assignee" {
diesel::update(tasks_table::table.filter(tasks_table::id.eq(task_id)))
.set(tasks_table::assignee_id.eq(Some(pid)))
.execute(&mut conn)
.map_err(|e: diesel::result::Error| TasksIntegrationError::DatabaseError(format!("Failed to update task: {}", e)))?;
}
}
Ok(())
})
.await
.map_err(|e| TasksIntegrationError::DatabaseError(e.to_string()))?
}
async fn fetch_task_contacts(
&self,
task_id: Uuid,
_query: &TaskContactsQuery,
) -> Result<Vec<TaskContact>, TasksIntegrationError> {
let pool = self.db_pool.clone();
tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|e| TasksIntegrationError::DatabaseError(e.to_string()))?;
// Get task assignees from tasks table and look up corresponding contacts
let task_row: Result<(Uuid, Option<Uuid>, DateTime<Utc>), _> = tasks_table::table
.filter(tasks_table::id.eq(task_id))
.select((tasks_table::id, tasks_table::assignee_id, tasks_table::created_at))
.first(&mut conn);
let mut task_contacts = Vec::new();
if let Ok((tid, assigned_to, created_at)) = task_row {
if let Some(assignee_id) = assigned_to {
// Look up person -> email -> contact
let person_email: Result<Option<String>, _> = people_table::table
.filter(people_table::id.eq(assignee_id))
.select(people_table::email)
.first(&mut conn);
if let Ok(Some(email)) = person_email {
// Find contact with this email
let contact_result: Result<Uuid, _> = crm_contacts_table::table
.filter(crm_contacts_table::email.eq(&email))
.select(crm_contacts_table::id)
.first(&mut conn);
if let Ok(contact_id) = contact_result {
task_contacts.push(TaskContact {
id: Uuid::new_v4(),
task_id: tid,
contact_id,
role: TaskContactRole::Assignee,
assigned_at: created_at,
assigned_by: Uuid::nil(),
notified: false,
notified_at: None,
notes: None,
});
}
}
}
}
Ok(task_contacts)
})
.await
.map_err(|e| TasksIntegrationError::DatabaseError(e.to_string()))?
}
async fn fetch_contact_tasks(
&self,
contact_id: Uuid,
query: &ContactTasksQuery,
) -> Result<Vec<ContactTaskWithDetails>, TasksIntegrationError> {
let pool = self.db_pool.clone();
let status_filter = query.status.clone();
tokio::task::spawn_blocking(move || -> Result<Vec<ContactTaskWithDetails>, TasksIntegrationError> {
let mut conn = pool.get().map_err(|e| TasksIntegrationError::DatabaseError(e.to_string()))?;
let mut db_query = tasks_table::table
.filter(tasks_table::status.ne("deleted"))
.into_boxed();
if let Some(status) = status_filter {
db_query = db_query.filter(tasks_table::status.eq(status));
}
let rows: Vec<(Uuid, String, Option<String>, String, String, Option<DateTime<Utc>>, Option<Uuid>, i32, DateTime<Utc>, DateTime<Utc>)> = db_query
.order(tasks_table::created_at.desc())
.select((
tasks_table::id,
tasks_table::title,
tasks_table::description,
tasks_table::status,
tasks_table::priority,
tasks_table::due_date,
tasks_table::project_id,
tasks_table::progress,
tasks_table::created_at,
tasks_table::updated_at,
))
.limit(50)
.load(&mut conn)
.map_err(|e: diesel::result::Error| TasksIntegrationError::DatabaseError(e.to_string()))?;
let tasks_list = rows.into_iter().map(|row| {
ContactTaskWithDetails {
task_contact: TaskContact {
id: Uuid::new_v4(),
task_id: row.0,
contact_id,
role: TaskContactRole::Assignee,
assigned_at: Utc::now(),
assigned_by: Uuid::nil(),
notified: false,
notified_at: None,
notes: None,
},
task: TaskSummary {
id: row.0,
title: row.1,
description: row.2,
status: row.3,
priority: row.4,
due_date: row.5,
project_id: row.6,
project_name: None,
progress: row.7 as u8,
created_at: row.8,
updated_at: row.9,
},
}
}).collect();
Ok(tasks_list)
})
.await
.map_err(|e: tokio::task::JoinError| TasksIntegrationError::DatabaseError(e.to_string()))?
}
async fn get_contact_summary(
&self,
contact_id: Uuid,
) -> Result<ContactSummary, TasksIntegrationError> {
// Query contacts table for summary
Ok(ContactSummary {
id: contact_id,
first_name: "John".to_string(),
last_name: "Doe".to_string(),
email: Some("john@example.com".to_string()),
phone: None,
company: None,
job_title: None,
avatar_url: None,
})
}
async fn get_task_details(&self, task_id: Uuid) -> Result<TaskSummary, TasksIntegrationError> {
// Query tasks table
Ok(TaskSummary {
id: task_id,
title: "Task".to_string(),
description: None,
status: "todo".to_string(),
priority: "medium".to_string(),
due_date: None,
project_id: None,
project_name: None,
progress: 0,
created_at: Utc::now(),
updated_at: Utc::now(),
})
}
async fn get_assigned_contact_ids(
&self,
task_id: Uuid,
) -> Result<Vec<Uuid>, TasksIntegrationError> {
let pool = self.db_pool.clone();
tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|e| TasksIntegrationError::DatabaseError(e.to_string()))?;
let assignee_id: Option<Uuid> = tasks_table::table
.filter(tasks_table::id.eq(task_id))
.select(tasks_table::assignee_id)
.first(&mut conn)
.optional()
.map_err(|e: diesel::result::Error| TasksIntegrationError::DatabaseError(e.to_string()))?
.flatten();
if let Some(user_id) = assignee_id {
let person_email: Option<String> = people_table::table
.filter(people_table::user_id.eq(user_id))
.select(people_table::email)
.first(&mut conn)
.optional()
.map_err(|e: diesel::result::Error| TasksIntegrationError::DatabaseError(e.to_string()))?
.flatten();
if let Some(email) = person_email {
let contact_ids: Vec<Uuid> = crm_contacts_table::table
.filter(crm_contacts_table::email.eq(&email))
.select(crm_contacts_table::id)
.load(&mut conn)
.unwrap_or_default();
return Ok(contact_ids);
}
}
Ok(vec![])
})
.await
.map_err(|e| TasksIntegrationError::DatabaseError(e.to_string()))?
}
async fn calculate_contact_task_stats(
&self,
contact_id: Uuid,
) -> Result<ContactTaskStats, TasksIntegrationError> {
// Calculate task statistics for contact
Ok(ContactTaskStats {
contact_id,
total_tasks: 0,
completed_tasks: 0,
in_progress_tasks: 0,
overdue_tasks: 0,
completion_rate: 0.0,
average_completion_time_days: None,
tasks_by_role: HashMap::new(),
recent_activity: vec![],
})
}
async fn calculate_contact_workload(
&self,
_contact_id: Uuid,
) -> Result<ContactWorkload, TasksIntegrationError> {
// Calculate current workload for contact
Ok(ContactWorkload {
active_tasks: 0,
high_priority_tasks: 0,
overdue_tasks: 0,
due_this_week: 0,
workload_level: WorkloadLevel::Low,
})
}
async fn find_similar_task_assignees(
&self,
_task: &TaskSummary,
exclude: &[Uuid],
limit: usize,
) -> Result<Vec<(ContactSummary, ContactWorkload)>, TasksIntegrationError> {
let pool = self.db_pool.clone();
let exclude = exclude.to_vec();
tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|e| TasksIntegrationError::DatabaseError(e.to_string()))?;
let mut query = crm_contacts_table::table
.filter(crm_contacts_table::status.eq("active"))
.into_boxed();
for exc in &exclude {
query = query.filter(crm_contacts_table::id.ne(*exc));
}
let rows: Vec<(Uuid, Option<String>, Option<String>, Option<String>, Option<String>, Option<String>)> = query
.select((
crm_contacts_table::id,
crm_contacts_table::first_name,
crm_contacts_table::last_name,
crm_contacts_table::email,
crm_contacts_table::company,
crm_contacts_table::job_title,
))
.limit(limit as i64)
.load(&mut conn)
.map_err(|e: diesel::result::Error| TasksIntegrationError::DatabaseError(e.to_string()))?;
let contacts = rows.into_iter().map(|row| {
let summary = ContactSummary {
id: row.0,
first_name: row.1.unwrap_or_default(),
last_name: row.2.unwrap_or_default(),
email: row.3,
phone: None,
company: row.4,
job_title: row.5,
avatar_url: None,
};
let workload = ContactWorkload {
active_tasks: 0,
high_priority_tasks: 0,
overdue_tasks: 0,
due_this_week: 0,
workload_level: WorkloadLevel::Low,
};
(summary, workload)
}).collect();
Ok(contacts)
})
.await
.map_err(|e| TasksIntegrationError::DatabaseError(e.to_string()))?
}
async fn find_project_contacts(
&self,
_project_id: Uuid,
exclude: &[Uuid],
limit: usize,
) -> Result<Vec<(ContactSummary, ContactWorkload)>, TasksIntegrationError> {
let pool = self.db_pool.clone();
let exclude = exclude.to_vec();
tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|e| TasksIntegrationError::DatabaseError(e.to_string()))?;
let mut query = crm_contacts_table::table
.filter(crm_contacts_table::status.eq("active"))
.into_boxed();
for exc in &exclude {
query = query.filter(crm_contacts_table::id.ne(*exc));
}
let rows: Vec<(Uuid, Option<String>, Option<String>, Option<String>, Option<String>, Option<String>)> = query
.select((
crm_contacts_table::id,
crm_contacts_table::first_name,
crm_contacts_table::last_name,
crm_contacts_table::email,
crm_contacts_table::company,
crm_contacts_table::job_title,
))
.limit(limit as i64)
.load(&mut conn)
.map_err(|e| TasksIntegrationError::DatabaseError(e.to_string()))?;
let contacts = rows.into_iter().map(|row| {
let summary = ContactSummary {
id: row.0,
first_name: row.1.unwrap_or_default(),
last_name: row.2.unwrap_or_default(),
email: row.3,
phone: None,
company: row.4,
job_title: row.5,
avatar_url: None,
};
let workload = ContactWorkload {
active_tasks: 0,
high_priority_tasks: 0,
overdue_tasks: 0,
due_this_week: 0,
workload_level: WorkloadLevel::Low,
};
(summary, workload)
}).collect();
Ok(contacts)
})
.await
.map_err(|e| TasksIntegrationError::DatabaseError(e.to_string()))?
}
async fn find_low_workload_contacts(
&self,
_organization_id: Uuid,
exclude: &[Uuid],
limit: usize,
) -> Result<Vec<(ContactSummary, ContactWorkload)>, TasksIntegrationError> {
let pool = self.db_pool.clone();
let exclude = exclude.to_vec();
tokio::task::spawn_blocking(move || {
let mut conn = pool.get().map_err(|e| TasksIntegrationError::DatabaseError(e.to_string()))?;
let mut query = crm_contacts_table::table
.filter(crm_contacts_table::status.eq("active"))
.into_boxed();
for exc in &exclude {
query = query.filter(crm_contacts_table::id.ne(*exc));
}
let rows: Vec<(Uuid, Option<String>, Option<String>, Option<String>, Option<String>, Option<String>)> = query
.select((
crm_contacts_table::id,
crm_contacts_table::first_name,
crm_contacts_table::last_name,
crm_contacts_table::email,
crm_contacts_table::company,
crm_contacts_table::job_title,
))
.limit(limit as i64)
.load(&mut conn)
.map_err(|e| TasksIntegrationError::DatabaseError(e.to_string()))?;
let contacts = rows.into_iter().map(|row| {
let summary = ContactSummary {
id: row.0,
first_name: row.1.unwrap_or_default(),
last_name: row.2.unwrap_or_default(),
email: row.3,
phone: None,
company: row.4,
job_title: row.5,
avatar_url: None,
};
let workload = ContactWorkload {
active_tasks: 0,
high_priority_tasks: 0,
overdue_tasks: 0,
due_this_week: 0,
workload_level: WorkloadLevel::Low,
};
(summary, workload)
}).collect();
Ok(contacts)
})
.await
.map_err(|e| TasksIntegrationError::DatabaseError(e.to_string()))?
}
async fn create_task_in_db(
&self,
_task_id: Uuid,
_organization_id: Uuid,
_title: &str,
_description: Option<&str>,
_assignee_id: Option<Uuid>,
_due_date: Option<chrono::DateTime<chrono::Utc>>,
) -> Result<(), TasksIntegrationError> {
// Implementation would insert task into database
// For now, this is a placeholder
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_task_type_display() {
assert_eq!(format!("{:?}", ContactTaskType::FollowUp), "FollowUp");
assert_eq!(format!("{:?}", ContactTaskType::Meeting), "Meeting");
assert_eq!(format!("{:?}", ContactTaskType::Call), "Call");
}
#[test]
fn test_task_priority_display() {
assert_eq!(format!("{:?}", ContactTaskPriority::Low), "Low");
assert_eq!(format!("{:?}", ContactTaskPriority::Normal), "Normal");
assert_eq!(format!("{:?}", ContactTaskPriority::High), "High");
}
}