use crate::AppService; use crate::error::AppError; use chrono::Utc; use models::projects::{project_board, project_board_card, project_board_column}; use sea_orm::*; use serde::{Deserialize, Serialize}; use session::Session; use uuid::Uuid; #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct CreateBoardParams { pub name: String, pub description: Option, } #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct UpdateBoardParams { pub name: Option, pub description: Option, } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct BoardResponse { pub id: Uuid, pub project: Uuid, pub name: String, pub description: Option, pub created_by: Uuid, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, } impl From for BoardResponse { fn from(m: project_board::Model) -> Self { Self { id: m.id, project: m.project, name: m.name, description: m.description, created_by: m.created_by, created_at: m.created_at, updated_at: m.updated_at, } } } #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct CreateColumnParams { pub name: String, #[serde(default)] pub position: i32, pub wip_limit: Option, pub color: Option, } #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct UpdateColumnParams { pub name: Option, pub position: Option, pub wip_limit: Option, pub color: Option, } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct ColumnResponse { pub id: Uuid, pub board: Uuid, pub name: String, pub position: i32, pub wip_limit: Option, pub color: Option, } impl From for ColumnResponse { fn from(m: project_board_column::Model) -> Self { Self { id: m.id, board: m.board, name: m.name, position: m.position, wip_limit: m.wip_limit, color: m.color, } } } #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct CreateCardParams { pub column_id: Uuid, pub title: String, pub description: Option, pub issue_id: Option, pub assignee_id: Option, pub due_date: Option>, pub priority: Option, } #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct UpdateCardParams { pub title: Option, pub description: Option, pub assignee_id: Option, pub due_date: Option>, pub priority: Option, } #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct MoveCardParams { pub target_column_id: Uuid, pub position: i32, } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct CardResponse { pub id: Uuid, pub column: Uuid, pub issue_id: Option, pub project: Option, pub title: String, pub description: Option, pub position: i32, pub assignee_id: Option, pub due_date: Option>, pub priority: Option, pub created_by: Uuid, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, } impl From for CardResponse { fn from(m: project_board_card::Model) -> Self { Self { id: m.id, column: m.column, issue_id: m.issue_id, project: m.project, title: m.title, description: m.description, position: m.position, assignee_id: m.assignee_id, due_date: m.due_date, priority: m.priority, created_by: m.created_by, created_at: m.created_at, updated_at: m.updated_at, } } } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct BoardWithColumnsResponse { pub board: BoardResponse, pub columns: Vec, } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct ColumnWithCardsResponse { pub column: ColumnResponse, pub cards: Vec, } impl AppService { /// List all boards for a project. pub async fn board_list( &self, project_name: String, ctx: &Session, ) -> Result, AppError> { let _ = ctx.user().ok_or(AppError::Unauthorized)?; let project = self.utils_find_project_by_name(project_name).await?; let boards = project_board::Entity::find() .filter(project_board::Column::Project.eq(project.id)) .order_by_asc(project_board::Column::CreatedAt) .all(&self.db) .await?; Ok(boards.into_iter().map(BoardResponse::from).collect()) } pub async fn board_get( &self, project_name: String, board_id: Uuid, ctx: &Session, ) -> Result { let _ = ctx.user().ok_or(AppError::Unauthorized)?; let project = self.utils_find_project_by_name(project_name).await?; let board = project_board::Entity::find_by_id(board_id) .filter(project_board::Column::Project.eq(project.id)) .one(&self.db) .await? .ok_or_else(|| AppError::NotFound("Board not found".to_string()))?; let columns = project_board_column::Entity::find() .filter(project_board_column::Column::Board.eq(board_id)) .order_by_asc(project_board_column::Column::Position) .all(&self.db) .await?; let column_ids: Vec = columns.iter().map(|c| c.id).collect(); let cards = if column_ids.is_empty() { vec![] } else { project_board_card::Entity::find() .filter(project_board_card::Column::Column.is_in(column_ids)) .order_by_asc(project_board_card::Column::Position) .all(&self.db) .await? }; let columns_with_cards: Vec = columns .into_iter() .map(|c| { let col_cards: Vec = cards .iter() .filter(|card| card.column == c.id) .cloned() .map(CardResponse::from) .collect(); ColumnWithCardsResponse { column: ColumnResponse::from(c), cards: col_cards, } }) .collect(); Ok(BoardWithColumnsResponse { board: BoardResponse::from(board), columns: columns_with_cards, }) } /// Create a new board. pub async fn board_create( &self, project_name: String, params: CreateBoardParams, ctx: &Session, ) -> Result { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let project = self.utils_find_project_by_name(project_name).await?; let now = Utc::now(); let active = project_board::ActiveModel { id: Set(Uuid::new_v4()), project: Set(project.id), name: Set(params.name), description: Set(params.description), created_by: Set(user_uid), created_at: Set(now), updated_at: Set(now), }; let board = active.insert(&self.db).await?; Ok(BoardResponse::from(board)) } /// Update a board. pub async fn board_update( &self, project_name: String, board_id: Uuid, params: UpdateBoardParams, ctx: &Session, ) -> Result { let _ = ctx.user().ok_or(AppError::Unauthorized)?; let project = self.utils_find_project_by_name(project_name).await?; let board = project_board::Entity::find_by_id(board_id) .filter(project_board::Column::Project.eq(project.id)) .one(&self.db) .await? .ok_or_else(|| AppError::NotFound("Board not found".to_string()))?; let mut active: project_board::ActiveModel = board.into(); if let Some(v) = params.name { active.name = Set(v); } if params.description.is_some() { active.description = Set(params.description); } active.updated_at = Set(Utc::now()); let updated = active.update(&self.db).await?; Ok(BoardResponse::from(updated)) } /// Delete a board. pub async fn board_delete( &self, project_name: String, board_id: Uuid, ctx: &Session, ) -> Result<(), AppError> { let _ = ctx.user().ok_or(AppError::Unauthorized)?; let project = self.utils_find_project_by_name(project_name).await?; let deleted = project_board::Entity::delete_by_id(board_id) .filter(project_board::Column::Project.eq(project.id)) .exec(&self.db) .await?; if deleted.rows_affected == 0 { return Err(AppError::NotFound("Board not found".to_string())); } Ok(()) } /// Create a column on a board. pub async fn column_create( &self, project_name: String, board_id: Uuid, params: CreateColumnParams, ctx: &Session, ) -> Result { let _ = ctx.user().ok_or(AppError::Unauthorized)?; let project = self.utils_find_project_by_name(project_name).await?; let board = project_board::Entity::find_by_id(board_id) .filter(project_board::Column::Project.eq(project.id)) .one(&self.db) .await? .ok_or_else(|| AppError::NotFound("Board not found".to_string()))?; let active = project_board_column::ActiveModel { id: Set(Uuid::new_v4()), board: Set(board.id), name: Set(params.name), position: Set(params.position), wip_limit: Set(params.wip_limit), color: Set(params.color), }; let column = active.insert(&self.db).await?; Ok(ColumnResponse::from(column)) } /// Update a column. pub async fn column_update( &self, project_name: String, column_id: Uuid, params: UpdateColumnParams, ctx: &Session, ) -> Result { let _ = ctx.user().ok_or(AppError::Unauthorized)?; let _project = self.utils_find_project_by_name(project_name).await?; let column = project_board_column::Entity::find_by_id(column_id) .one(&self.db) .await? .ok_or_else(|| AppError::NotFound("Column not found".to_string()))?; let mut active: project_board_column::ActiveModel = column.into(); if let Some(v) = params.name { active.name = Set(v); } if let Some(v) = params.position { active.position = Set(v); } if params.wip_limit.is_some() { active.wip_limit = Set(params.wip_limit); } if params.color.is_some() { active.color = Set(params.color); } let updated = active.update(&self.db).await?; Ok(ColumnResponse::from(updated)) } /// Delete a column. pub async fn column_delete( &self, project_name: String, column_id: Uuid, ctx: &Session, ) -> Result<(), AppError> { let _ = ctx.user().ok_or(AppError::Unauthorized)?; let _project = self.utils_find_project_by_name(project_name).await?; let deleted = project_board_column::Entity::delete_by_id(column_id) .exec(&self.db) .await?; if deleted.rows_affected == 0 { return Err(AppError::NotFound("Column not found".to_string())); } Ok(()) } /// Create a card in a column. pub async fn card_create( &self, project_name: String, params: CreateCardParams, ctx: &Session, ) -> Result { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let project = self.utils_find_project_by_name(project_name).await?; let now = Utc::now(); let active = project_board_card::ActiveModel { id: Set(Uuid::new_v4()), column: Set(params.column_id), issue_id: Set(params.issue_id), project: Set(Some(project.id)), title: Set(params.title), description: Set(params.description), position: Set(0), assignee_id: Set(params.assignee_id), due_date: Set(params.due_date), priority: Set(params.priority), created_by: Set(user_uid), created_at: Set(now), updated_at: Set(now), }; let card = active.insert(&self.db).await?; Ok(CardResponse::from(card)) } /// Update a card. pub async fn card_update( &self, project_name: String, card_id: Uuid, params: UpdateCardParams, ctx: &Session, ) -> Result { let _ = ctx.user().ok_or(AppError::Unauthorized)?; let _project = self.utils_find_project_by_name(project_name).await?; let card = project_board_card::Entity::find_by_id(card_id) .one(&self.db) .await? .ok_or_else(|| AppError::NotFound("Card not found".to_string()))?; let mut active: project_board_card::ActiveModel = card.into(); if let Some(v) = params.title { active.title = Set(v); } if params.description.is_some() { active.description = Set(params.description); } if params.assignee_id.is_some() { active.assignee_id = Set(params.assignee_id); } if params.due_date.is_some() { active.due_date = Set(params.due_date); } if params.priority.is_some() { active.priority = Set(params.priority); } active.updated_at = Set(Utc::now()); let updated = active.update(&self.db).await?; Ok(CardResponse::from(updated)) } /// Move a card to a different column and/or position. pub async fn card_move( &self, project_name: String, card_id: Uuid, params: MoveCardParams, ctx: &Session, ) -> Result { let _ = ctx.user().ok_or(AppError::Unauthorized)?; let _project = self.utils_find_project_by_name(project_name).await?; let card = project_board_card::Entity::find_by_id(card_id) .one(&self.db) .await? .ok_or_else(|| AppError::NotFound("Card not found".to_string()))?; let mut active: project_board_card::ActiveModel = card.into(); active.column = Set(params.target_column_id); active.position = Set(params.position); active.updated_at = Set(Utc::now()); let updated = active.update(&self.db).await?; Ok(CardResponse::from(updated)) } /// Delete a card. pub async fn card_delete( &self, project_name: String, card_id: Uuid, ctx: &Session, ) -> Result<(), AppError> { let _ = ctx.user().ok_or(AppError::Unauthorized)?; let _project = self.utils_find_project_by_name(project_name).await?; let deleted = project_board_card::Entity::delete_by_id(card_id) .exec(&self.db) .await?; if deleted.rows_affected == 0 { return Err(AppError::NotFound("Card not found".to_string())); } Ok(()) } }