tododo 0.1.2

A minimal terminal todo manager built with Rust and Ratatui
Documentation
use chrono::{FixedOffset, Local};
use sea_orm::{
    ActiveModelTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder,
    sea_query::Expr,
};
use uuid::Uuid;

use crate::domain::priority::Priority;
use crate::domain::TodoStatus;
use crate::entity::todo_item::{self, Entity as TodoItem, Model};
use crate::error::AppError;
use chrono::DateTime;

fn local_now() -> chrono::DateTime<FixedOffset> {
    Local::now().fixed_offset()
}

pub struct TodoRepository {
    db: DatabaseConnection,
}

impl TodoRepository {
    pub fn new(db: DatabaseConnection) -> Self {
        Self { db }
    }

    pub async fn find_active(&self) -> Result<Vec<Model>, AppError> {
        TodoItem::find()
            .filter(Expr::col(todo_item::Column::DeletedAt).is_null())
            .order_by_desc(todo_item::Column::Status)
            .order_by_asc(todo_item::Column::Priority)
            .order_by_desc(todo_item::Column::CreatedAt)
            .all(&self.db)
            .await
            .map_err(AppError::Db)
    }

    pub async fn find_active_by_time(&self) -> Result<Vec<Model>, AppError> {
        TodoItem::find()
            .filter(Expr::col(todo_item::Column::DeletedAt).is_null())
            .order_by_desc(todo_item::Column::CreatedAt)
            .all(&self.db)
            .await
            .map_err(AppError::Db)
    }

    pub async fn find_by_id(&self, id: &str) -> Result<Option<Model>, AppError> {
        TodoItem::find_by_id(id)
            .filter(Expr::col(todo_item::Column::DeletedAt).is_null())
            .one(&self.db)
            .await
            .map_err(AppError::Db)
    }

    pub async fn create(&self, title: &str, note: &str) -> Result<Model, AppError> {
        let now = local_now();
        let id = Uuid::new_v4().to_string();

        let active = todo_item::ActiveModel {
            id: sea_orm::Set(id.clone()),
            title: sea_orm::Set(title.to_string()),
            note: sea_orm::Set(note.to_string()),
            status: sea_orm::Set(TodoStatus::PENDING.to_string()),
            priority: sea_orm::Set(Priority::NONE.into()),
            created_at: sea_orm::Set(now),
            updated_at: sea_orm::Set(now),
            completed_at: sea_orm::Set(None),
            deleted_at: sea_orm::Set(None),
        };

        TodoItem::insert(active).exec(&self.db).await.map_err(AppError::Db)?;
        
        self.find_by_id(&id)
            .await?
            .ok_or_else(|| AppError::NotFound(id))
    }

    pub async fn toggle_status(&self, id: &str) -> Result<Model, AppError> {
        let item = self
            .find_by_id(id)
            .await?
            .ok_or_else(|| AppError::NotFound(id.to_string()))?;

        let now = local_now();
        let (new_status, completed_at) = if item.status == TodoStatus::PENDING {
            (TodoStatus::COMPLETED.to_string(), Some(now))
        } else {
            (TodoStatus::PENDING.to_string(), None)
        };

        let mut active: todo_item::ActiveModel = item.into();
        active.status = sea_orm::Set(new_status);
        active.updated_at = sea_orm::Set(now);
        active.completed_at = sea_orm::Set(completed_at);

        active.update(&self.db).await.map_err(AppError::Db)
    }

    pub async fn update_todo(&self, id: &str, title: &str, note: &str) -> Result<Model, AppError> {
        let item = self
            .find_by_id(id)
            .await?
            .ok_or_else(|| AppError::NotFound(id.to_string()))?;

        let now = local_now();
        let mut active: todo_item::ActiveModel = item.into();
        active.title = sea_orm::Set(title.to_string());
        active.note = sea_orm::Set(note.to_string());
        active.updated_at = sea_orm::Set(now);

        active.update(&self.db).await.map_err(AppError::Db)
    }

    pub async fn set_priority(&self, id: &str, priority: i32) -> Result<Model, AppError> {
        let item = self
            .find_by_id(id)
            .await?
            .ok_or_else(|| AppError::NotFound(id.to_string()))?;

        let now = local_now();
        let mut active: todo_item::ActiveModel = item.into();
        active.priority = sea_orm::Set(priority);
        active.updated_at = sea_orm::Set(now);

        active.update(&self.db).await.map_err(AppError::Db)
    }

    pub async fn find_completed_since(
        &self,
        since: DateTime<FixedOffset>,
    ) -> Result<Vec<Model>, AppError> {
        TodoItem::find()
            .filter(Expr::col(todo_item::Column::DeletedAt).is_null())
            .filter(Expr::col(todo_item::Column::CompletedAt).is_not_null())
            .filter(Expr::col(todo_item::Column::CompletedAt).gte(since))
            .order_by_desc(todo_item::Column::CompletedAt)
            .all(&self.db)
            .await
            .map_err(AppError::Db)
    }

    pub async fn soft_delete(&self, id: &str) -> Result<(), AppError> {
        let item = self
            .find_by_id(id)
            .await?
            .ok_or_else(|| AppError::NotFound(id.to_string()))?;

        let now = local_now();
        let mut active: todo_item::ActiveModel = item.into();
        active.deleted_at = sea_orm::Set(Some(now));

        active.update(&self.db).await.map_err(AppError::Db)?;
        Ok(())
    }
}