use crate::client::ClickUpClient;
use crate::error::{ClickUpError, Result};
use crate::types::Task;
use serde_json::{json, Value};
#[derive(Clone)]
pub struct TaskManager {
client: ClickUpClient,
list_id: Option<String>,
}
impl TaskManager {
pub fn new(client: ClickUpClient, list_id: Option<String>) -> Self {
Self { client, list_id }
}
pub fn from_token(api_token: String, list_id: Option<String>) -> Result<Self> {
let client = ClickUpClient::new(api_token)?;
Ok(Self::new(client, list_id))
}
pub async fn create_task(&self, task: &Task) -> Result<Task> {
let list_id = if let Some(ref id) = task.list_id {
tracing::info!("🎯 Usando list_id da task: {}", id);
id.clone()
} else if let Some(ref id) = self.list_id {
tracing::info!("⚠️ list_id não encontrado na task, usando fallback: {}", id);
id.clone()
} else {
return Err(ClickUpError::ValidationError(
"list_id não encontrado na task e TaskManager não tem list_id configurado"
.to_string(),
));
};
let mut task_json = serde_json::to_value(task)?;
if let Some(obj) = task_json.as_object_mut() {
obj.remove("list_id");
obj.remove("id");
obj.remove("url");
obj.remove("date_created");
obj.remove("date_updated");
obj.remove("date_closed");
obj.remove("creator");
obj.remove("folder");
obj.remove("space");
obj.remove("project");
if let Some(priority) = obj.get("priority") {
if !priority.is_i64() {
obj.insert("priority".to_string(), serde_json::json!(3));
}
} else {
obj.insert("priority".to_string(), serde_json::json!(3));
}
}
let endpoint = format!("/list/{}/task", list_id);
let created_task: Task = self.client.post_json(&endpoint, &task_json).await?;
tracing::info!(
"✅ Task criada: {}",
created_task.id.as_ref().unwrap_or(&"?".to_string())
);
Ok(created_task)
}
pub async fn test_connection(&self) -> Result<Value> {
let user_info: Value = self.client.get_json("/user").await?;
Ok(user_info)
}
pub async fn get_list_info(&self, list_id: Option<&str>) -> Result<Value> {
let id = if let Some(id) = list_id {
id
} else if let Some(ref id) = self.list_id {
id
} else {
return Err(ClickUpError::ValidationError(
"list_id não fornecido e TaskManager não tem list_id configurado".to_string(),
));
};
let endpoint = format!("/list/{}", id);
let list_info: Value = self.client.get_json(&endpoint).await?;
Ok(list_info)
}
pub async fn get_tasks_in_list(&self, list_id: Option<&str>) -> Result<Vec<Task>> {
let id = if let Some(id) = list_id {
id
} else if let Some(ref id) = self.list_id {
id
} else {
return Err(ClickUpError::ValidationError(
"list_id não fornecido e TaskManager não tem list_id configurado".to_string(),
));
};
let endpoint = format!("/list/{}/task?archived=false", id);
let json_resp: Value = self.client.get_json(&endpoint).await?;
if let Some(tasks_array) = json_resp.get("tasks").and_then(|v| v.as_array()) {
let mut tasks = Vec::new();
for task_value in tasks_array {
match serde_json::from_value::<Task>(task_value.clone()) {
Ok(task) => tasks.push(task),
Err(e) => {
tracing::warn!("⚠️ Falha ao desserializar task: {}", e);
}
}
}
tracing::info!("✅ Listadas {} tasks da lista {}", tasks.len(), id);
Ok(tasks)
} else {
tracing::warn!("⚠️ Resposta da API sem campo 'tasks'");
Ok(Vec::new())
}
}
pub async fn find_existing_task_in_list(
&self,
list_id: Option<&str>,
title: &str,
) -> Result<Option<Task>> {
let id = if let Some(id) = list_id {
id
} else if let Some(ref id) = self.list_id {
id
} else {
return Err(ClickUpError::ValidationError(
"list_id não fornecido e TaskManager não tem list_id configurado".to_string(),
));
};
let endpoint = format!("/list/{}/task?archived=false", id);
let json_resp: Value = match self.client.get_json(&endpoint).await {
Ok(resp) => resp,
Err(ClickUpError::ApiError { status, message }) => {
if message.contains("OAUTH_027") || message.contains("Team not authorized") {
tracing::warn!(
"⚠️ Token OAuth2 sem permissão para listar tasks ({}). Assumindo que não há duplicatas.",
message
);
return Ok(None);
}
tracing::error!(
"Erro ao listar tasks na lista {}: {} - {}",
id,
status,
message
);
return Err(ClickUpError::ApiError { status, message });
}
Err(e) => {
return Err(e);
}
};
if let Some(tasks) = json_resp.get("tasks").and_then(|v| v.as_array()) {
for task_value in tasks {
if let Some(task_name) = task_value.get("name").and_then(|v| v.as_str()) {
if task_name == title {
tracing::info!("✅ Tarefa existente encontrada: '{}'", title);
let task: Task = serde_json::from_value(task_value.clone())?;
return Ok(Some(task));
}
}
}
}
tracing::debug!("ℹ️ Nenhuma tarefa encontrada com título: '{}'", title);
Ok(None)
}
pub async fn add_comment_to_task(&self, task_id: &str, comment: &str) -> Result<()> {
let endpoint = format!("/task/{}/comment", task_id);
let body = json!({
"comment_text": comment
});
let _response: Value = self.client.post_json(&endpoint, &body).await?;
tracing::debug!("✅ Comentário adicionado à task {}", task_id);
Ok(())
}
pub async fn update_task(&self, task_id: &str, task_data: &Value) -> Result<Task> {
let endpoint = format!("/task/{}", task_id);
let updated_task: Task = self.client.put_json(&endpoint, task_data).await?;
tracing::debug!("✅ Task {} atualizada", task_id);
Ok(updated_task)
}
pub async fn assign_task(&self, task_id: &str, user_ids: &[u32]) -> Result<Task> {
let endpoint = format!("/task/{}", task_id);
let body = json!({
"assignees": {
"add": user_ids
}
});
let updated_task: Task = self.client.put_json(&endpoint, &body).await?;
tracing::debug!("✅ Assignees {:?} adicionados à task {}", user_ids, task_id);
Ok(updated_task)
}
pub async fn unassign_task(&self, task_id: &str, user_ids: &[u32]) -> Result<Task> {
let endpoint = format!("/task/{}", task_id);
let body = json!({
"assignees": {
"rem": user_ids
}
});
let updated_task: Task = self.client.put_json(&endpoint, &body).await?;
tracing::debug!("✅ Assignees {:?} removidos da task {}", user_ids, task_id);
Ok(updated_task)
}
pub async fn update_assignees(
&self,
task_id: &str,
add_user_ids: &[u32],
rem_user_ids: &[u32],
) -> Result<Task> {
let endpoint = format!("/task/{}", task_id);
let body = json!({
"assignees": {
"add": add_user_ids,
"rem": rem_user_ids
}
});
let updated_task: Task = self.client.put_json(&endpoint, &body).await?;
tracing::debug!(
"✅ Assignees atualizados: +{:?} -{:?} na task {}",
add_user_ids,
rem_user_ids,
task_id
);
Ok(updated_task)
}
pub async fn update_task_status(&self, task_id: &str, status: &str) -> Result<Task> {
let endpoint = format!("/task/{}", task_id);
let body = json!({
"status": status
});
let updated_task: Task = self.client.put_json(&endpoint, &body).await?;
tracing::debug!("✅ Status da task {} atualizado para: {}", task_id, status);
Ok(updated_task)
}
pub async fn create_subtask(&self, parent_id: &str, task_data: &Value) -> Result<Task> {
let list_id = if let Some(id) = task_data.get("list_id").and_then(|v| v.as_str()) {
id.to_string()
} else if let Some(ref id) = self.list_id {
id.clone()
} else {
return Err(ClickUpError::ValidationError(
"list_id não encontrado para criar subtask".to_string(),
));
};
let mut subtask_data = task_data.clone();
if let Some(obj) = subtask_data.as_object_mut() {
obj.insert("parent".to_string(), json!(parent_id));
obj.remove("list_id"); }
let endpoint = format!("/list/{}/task", list_id);
let subtask: Task = self.client.post_json(&endpoint, &subtask_data).await?;
tracing::debug!(
"✅ Subtask criada: {} (pai: {})",
subtask.id.as_ref().unwrap_or(&"?".to_string()),
parent_id
);
Ok(subtask)
}
pub async fn set_due_date(
&self,
task_id: &str,
timestamp_ms: i64,
include_time: bool,
) -> Result<Task> {
let endpoint = format!("/task/{}", task_id);
let body = json!({
"due_date": timestamp_ms,
"due_date_time": include_time
});
let updated_task: Task = self.client.put_json(&endpoint, &body).await?;
tracing::debug!(
"✅ Due date da task {} definida: {} (include_time: {})",
task_id,
timestamp_ms,
include_time
);
Ok(updated_task)
}
pub async fn clear_due_date(&self, task_id: &str) -> Result<Task> {
let endpoint = format!("/task/{}", task_id);
let body = json!({
"due_date": null,
"due_date_time": false
});
let updated_task: Task = self.client.put_json(&endpoint, &body).await?;
tracing::debug!("✅ Due date da task {} removida", task_id);
Ok(updated_task)
}
pub async fn add_dependency(
&self,
task_id: &str,
depends_on: &str,
dependency_type: Option<&str>,
) -> Result<Value> {
let endpoint = format!("/task/{}/dependency", task_id);
let dep_type = dependency_type.unwrap_or("waiting_on");
let body = json!({
"depends_on": depends_on,
"dependency_of": dep_type
});
let response: Value = self.client.post_json(&endpoint, &body).await?;
tracing::debug!(
"✅ Dependência adicionada: task {} {} task {}",
task_id,
dep_type,
depends_on
);
Ok(response)
}
pub async fn remove_dependency(&self, task_id: &str, depends_on: &str) -> Result<Value> {
let endpoint = format!("/task/{}/dependency/{}", task_id, depends_on);
let response: Value = self.client.delete_json(&endpoint).await?;
tracing::debug!(
"✅ Dependência removida: task {} não depende mais de {}",
task_id,
depends_on
);
Ok(response)
}
pub async fn get_dependencies(&self, task_id: &str) -> Result<Vec<Value>> {
let endpoint = format!("/task/{}", task_id);
let task: Value = self.client.get_json(&endpoint).await?;
let dependencies = task
.get("dependencies")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
tracing::debug!(
"✅ Recuperadas {} dependências da task {}",
dependencies.len(),
task_id
);
Ok(dependencies)
}
}