#![allow(deprecated)]
use crate::{
database::{
conversions::naive_date_to_things_timestamp, query_builders::TaskUpdateBuilder, validators,
ThingsDatabase,
},
error::{Result as ThingsResult, ThingsError},
models::ThingsId,
};
use chrono::Utc;
use tracing::{info, instrument};
impl ThingsDatabase {
#[instrument(skip(self))]
pub async fn create_project(
&self,
request: crate::models::CreateProjectRequest,
) -> ThingsResult<ThingsId> {
crate::database::validate_date_range(request.start_date, request.deadline)?;
let id = ThingsId::new_things_native();
if let Some(area_uuid) = &request.area_uuid {
validators::validate_area_exists(&self.pool, area_uuid).await?;
}
let start_date_ts = request.start_date.map(naive_date_to_things_timestamp);
let deadline_ts = request.deadline.map(naive_date_to_things_timestamp);
let now = Utc::now().timestamp() as f64;
sqlx::query(
r"
INSERT INTO TMTask (
uuid, title, type, status, notes,
startDate, deadline, project, area, heading,
creationDate, userModificationDate,
trashed
) VALUES (?, ?, 1, 0, ?, ?, ?, NULL, ?, NULL, ?, ?, 0)
",
)
.bind(id.as_str())
.bind(&request.title)
.bind(request.notes.as_ref())
.bind(start_date_ts)
.bind(deadline_ts)
.bind(request.area_uuid.map(|u| u.into_string()))
.bind(now)
.bind(now)
.execute(&self.pool)
.await
.map_err(|e| ThingsError::unknown(format!("Failed to create project: {e}")))?;
if let Some(tags) = request.tags {
self.set_task_tags(&id, tags).await?;
}
info!("Created project with UUID: {}", id);
Ok(id)
}
#[instrument(skip(self))]
pub async fn update_project(
&self,
request: crate::models::UpdateProjectRequest,
) -> ThingsResult<()> {
validators::validate_project_exists(&self.pool, &request.uuid).await?;
if request.start_date.is_some() || request.deadline.is_some() {
if let Some(current_project) = self.get_project_by_uuid(&request.uuid).await? {
let final_start = request.start_date.or(current_project.start_date);
let final_deadline = request.deadline.or(current_project.deadline);
crate::database::validate_date_range(final_start, final_deadline)?;
}
}
if let Some(area_uuid) = &request.area_uuid {
validators::validate_area_exists(&self.pool, area_uuid).await?;
}
let mut builder = TaskUpdateBuilder::new();
if request.title.is_some() {
builder = builder.add_field("title");
}
if request.notes.is_some() {
builder = builder.add_field("notes");
}
if request.start_date.is_some() {
builder = builder.add_field("startDate");
}
if request.deadline.is_some() {
builder = builder.add_field("deadline");
}
if request.area_uuid.is_some() {
builder = builder.add_field("area");
}
let has_db_fields = !builder.is_empty();
if has_db_fields {
let query_str = builder.build_query_string();
let mut q = sqlx::query(&query_str);
if let Some(ref title) = request.title {
q = q.bind(title);
}
if let Some(ref notes) = request.notes {
q = q.bind(notes);
}
if let Some(start_date) = request.start_date {
q = q.bind(naive_date_to_things_timestamp(start_date));
}
if let Some(deadline) = request.deadline {
q = q.bind(naive_date_to_things_timestamp(deadline));
}
if let Some(area_uuid) = request.area_uuid {
q = q.bind(area_uuid.into_string());
}
let now = Utc::now().timestamp() as f64;
q = q.bind(now).bind(request.uuid.as_str());
q.execute(&self.pool)
.await
.map_err(|e| ThingsError::unknown(format!("Failed to update project: {e}")))?;
} else if request.tags.is_none() {
return Ok(());
}
if let Some(tags) = request.tags {
self.set_task_tags(&request.uuid, tags).await?;
}
info!("Updated project with UUID: {}", request.uuid);
Ok(())
}
#[instrument(skip(self))]
pub async fn complete_project(
&self,
id: &ThingsId,
child_handling: crate::models::ProjectChildHandling,
) -> ThingsResult<()> {
validators::validate_project_exists(&self.pool, id).await?;
let now = Utc::now().timestamp() as f64;
match child_handling {
crate::models::ProjectChildHandling::Error => {
let child_count: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM TMTask WHERE project = ? AND trashed = 0",
)
.bind(id.as_str())
.fetch_one(&self.pool)
.await
.map_err(|e| {
ThingsError::unknown(format!("Failed to check for child tasks: {e}"))
})?;
if child_count > 0 {
return Err(ThingsError::unknown(format!(
"Project {} has {} child task(s). Use cascade or orphan mode to complete.",
id, child_count
)));
}
}
crate::models::ProjectChildHandling::Cascade => {
sqlx::query(
"UPDATE TMTask SET status = 3, stopDate = ?, userModificationDate = ? WHERE project = ? AND trashed = 0",
)
.bind(now)
.bind(now)
.bind(id.as_str())
.execute(&self.pool)
.await
.map_err(|e| ThingsError::unknown(format!("Failed to complete child tasks: {e}")))?;
}
crate::models::ProjectChildHandling::Orphan => {
sqlx::query(
"UPDATE TMTask SET project = NULL, userModificationDate = ? WHERE project = ? AND trashed = 0",
)
.bind(now)
.bind(id.as_str())
.execute(&self.pool)
.await
.map_err(|e| ThingsError::unknown(format!("Failed to orphan child tasks: {e}")))?;
}
}
sqlx::query(
"UPDATE TMTask SET status = 3, stopDate = ?, userModificationDate = ? WHERE uuid = ?",
)
.bind(now)
.bind(now)
.bind(id.as_str())
.execute(&self.pool)
.await
.map_err(|e| ThingsError::unknown(format!("Failed to complete project: {e}")))?;
info!("Completed project with UUID: {}", id);
Ok(())
}
#[instrument(skip(self))]
pub async fn delete_project(
&self,
id: &ThingsId,
child_handling: crate::models::ProjectChildHandling,
) -> ThingsResult<()> {
validators::validate_project_exists(&self.pool, id).await?;
let now = Utc::now().timestamp() as f64;
match child_handling {
crate::models::ProjectChildHandling::Error => {
let child_count: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM TMTask WHERE project = ? AND trashed = 0",
)
.bind(id.as_str())
.fetch_one(&self.pool)
.await
.map_err(|e| {
ThingsError::unknown(format!("Failed to check for child tasks: {e}"))
})?;
if child_count > 0 {
return Err(ThingsError::unknown(format!(
"Project {} has {} child task(s). Use cascade or orphan mode to delete.",
id, child_count
)));
}
}
crate::models::ProjectChildHandling::Cascade => {
sqlx::query(
"UPDATE TMTask SET trashed = 1, userModificationDate = ? WHERE project = ? AND trashed = 0",
)
.bind(now)
.bind(id.as_str())
.execute(&self.pool)
.await
.map_err(|e| ThingsError::unknown(format!("Failed to delete child tasks: {e}")))?;
}
crate::models::ProjectChildHandling::Orphan => {
sqlx::query(
"UPDATE TMTask SET project = NULL, userModificationDate = ? WHERE project = ? AND trashed = 0",
)
.bind(now)
.bind(id.as_str())
.execute(&self.pool)
.await
.map_err(|e| ThingsError::unknown(format!("Failed to orphan child tasks: {e}")))?;
}
}
sqlx::query("UPDATE TMTask SET trashed = 1, userModificationDate = ? WHERE uuid = ?")
.bind(now)
.bind(id.as_str())
.execute(&self.pool)
.await
.map_err(|e| ThingsError::unknown(format!("Failed to delete project: {e}")))?;
info!("Deleted project with UUID: {}", id);
Ok(())
}
}