use crate::AppService; use crate::error::AppError; use chrono::Utc; use models::projects::{MemberRole, project_audit_log, project_label}; use models::system::label; use sea_orm::*; use serde::{Deserialize, Serialize}; use session::Session; use uuid::Uuid; #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct CreateLabelParams { pub name: String, pub color: String, pub description: Option, } #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct UpdateLabelParams { pub name: Option, pub color: Option, pub description: Option, } #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct LabelResponse { pub id: i64, pub project_uid: Uuid, pub name: String, pub color: String, pub description: Option, pub created_at: chrono::DateTime, } #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct LabelListResponse { pub labels: Vec, pub total: usize, } impl AppService { pub async fn project_create_label( &self, project_name: String, params: CreateLabelParams, ctx: &Session, ) -> Result { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let project = self.utils_find_project_by_name(project_name).await?; self.utils_check_project_permission( &project.id, user_uid, &[MemberRole::Admin, MemberRole::Owner], ) .await?; // Create the label in system::label table let label_model = label::ActiveModel { project: Set(project.id), name: Set(params.name.clone()), color: Set(params.color.clone()), ..Default::default() }; let created_system_label = label_model.insert(&self.db).await?; // Create the project-label relation let project_label = project_label::ActiveModel { project: Set(project.id), label: Set(created_system_label.id), relation_at: Set(Utc::now()), ..Default::default() }; let created_project_label = project_label.insert(&self.db).await?; let _ = self .project_log_activity( project.id, None, user_uid, super::activity::ActivityLogParams { event_type: "label_create".to_string(), title: format!("{} created label '{}'", user_uid, params.name), repo_id: None, content: None, event_id: None, event_sub_id: Some(created_project_label.id), metadata: Some(serde_json::json!({ "label_id": created_project_label.id, "label_name": params.name, "color": params.color, })), is_private: false, }, ) .await; let log = project_audit_log::ActiveModel { project: Set(project.id), actor: Set(user_uid), action: Set("label_create".to_string()), details: Set(Some(serde_json::json!({ "label_id": created_project_label.id, "label_name": params.name, "color": params.color, }))), created_at: Set(Utc::now()), ..Default::default() }; log.insert(&self.db).await?; Ok(LabelResponse { id: created_project_label.id, project_uid: project.id, name: params.name, color: params.color, description: params.description, created_at: created_project_label.relation_at, }) } pub async fn project_get_labels( &self, project_name: String, ) -> Result { let project = self.utils_find_project_by_name(project_name).await?; // Find all project-label relations let project_label_relations = project_label::Entity::find() .filter(project_label::Column::Project.eq(project.id)) .all(&self.db) .await?; if project_label_relations.is_empty() { return Ok(LabelListResponse { labels: vec![], total: 0, }); } // Get all label IDs let label_ids: Vec = project_label_relations.iter().map(|r| r.label).collect(); // Fetch label details let labels = label::Entity::find() .filter(label::Column::Id.is_in(label_ids)) .all(&self.db) .await?; let total = labels.len(); let labels: Vec = project_label_relations .into_iter() .filter_map(|relation| { labels .iter() .find(|l| l.id == relation.label) .map(|l| LabelResponse { id: relation.id, project_uid: relation.project, name: l.name.clone(), color: l.color.clone(), description: None, // system::label doesn't have description created_at: relation.relation_at, }) }) .collect(); Ok(LabelListResponse { labels, total }) } pub async fn project_get_label(&self, label_id: i64) -> Result { let project_label = project_label::Entity::find_by_id(label_id) .one(&self.db) .await? .ok_or(AppError::NotFound("Label not found".to_string()))?; let system_label = label::Entity::find_by_id(project_label.label) .one(&self.db) .await? .ok_or(AppError::NotFound("Label not found".to_string()))?; Ok(LabelResponse { id: project_label.id, project_uid: project_label.project, name: system_label.name, color: system_label.color, description: None, created_at: project_label.relation_at, }) } pub async fn project_update_label( &self, label_id: i64, params: UpdateLabelParams, ctx: &Session, ) -> Result { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let project_label = project_label::Entity::find_by_id(label_id) .one(&self.db) .await? .ok_or(AppError::NotFound("Label not found".to_string()))?; self.utils_check_project_permission( &project_label.project, user_uid, &[MemberRole::Admin, MemberRole::Owner], ) .await?; let system_label = label::Entity::find_by_id(project_label.label) .one(&self.db) .await? .ok_or(AppError::NotFound("Label not found".to_string()))?; let mut active_label: label::ActiveModel = system_label.into(); let updated_name = params.name.is_some(); let updated_color = params.color.is_some(); let updated_description = params.description.is_some(); if let Some(name) = params.name { active_label.name = Set(name); } if let Some(color) = params.color { active_label.color = Set(color); } let updated_system_label = active_label.update(&self.db).await?; let _ = self .project_log_activity( project_label.project, None, user_uid, super::activity::ActivityLogParams { event_type: "label_update".to_string(), title: format!("{} updated label '{}'", user_uid, updated_system_label.name), repo_id: None, content: None, event_id: None, event_sub_id: Some(label_id), metadata: Some(serde_json::json!({ "label_id": label_id, "updated_fields": { "name": updated_name, "color": updated_color, "description": updated_description, } })), is_private: false, }, ) .await; let log = project_audit_log::ActiveModel { project: Set(project_label.project), actor: Set(user_uid), action: Set("label_update".to_string()), details: Set(Some(serde_json::json!({ "label_id": label_id, "updated_fields": { "name": updated_name, "color": updated_color, "description": updated_description, } }))), created_at: Set(Utc::now()), ..Default::default() }; log.insert(&self.db).await?; Ok(LabelResponse { id: project_label.id, project_uid: project_label.project, name: updated_system_label.name, color: updated_system_label.color, description: params.description, created_at: project_label.relation_at, }) } pub async fn project_delete_label(&self, label_id: i64, ctx: &Session) -> Result<(), AppError> { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let project_label = project_label::Entity::find_by_id(label_id) .one(&self.db) .await? .ok_or(AppError::NotFound("Label not found".to_string()))?; self.utils_check_project_permission( &project_label.project, user_uid, &[MemberRole::Admin, MemberRole::Owner], ) .await?; let system_label = label::Entity::find_by_id(project_label.label) .one(&self.db) .await?; let deleted_label_name = system_label .as_ref() .map(|l| l.name.clone()) .unwrap_or_default(); let _ = self .project_log_activity( project_label.project, None, user_uid, super::activity::ActivityLogParams { event_type: "label_delete".to_string(), title: format!("{} deleted label '{}'", user_uid, deleted_label_name), repo_id: None, content: None, event_id: None, event_sub_id: Some(label_id), metadata: Some(serde_json::json!({ "label_id": label_id, "label_name": deleted_label_name, })), is_private: false, }, ) .await; let log = project_audit_log::ActiveModel { project: Set(project_label.project), actor: Set(user_uid), action: Set("label_delete".to_string()), details: Set(Some(serde_json::json!({ "label_id": label_id, "label_name": system_label.as_ref().map(|l| l.name.clone()), }))), created_at: Set(Utc::now()), ..Default::default() }; log.insert(&self.db).await?; project_label::Entity::delete_by_id(label_id) .exec(&self.db) .await?; // Also delete the system label if it exists if let Some(sl) = system_label { label::Entity::delete_by_id(sl.id).exec(&self.db).await?; } Ok(()) } }