e1df092
use models::issue::tracked_time;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
QuerySelect, Set,
};
use serde::{Deserialize, Serialize};
use space::services::spaces::UserDto;
use time::OffsetDateTime;
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
use crate::errors::IssueError;
use crate::services::service::IssueService;
use crate::share::lookup;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct TrackedTimeDto {
pub id: Uuid,
pub issue_id: Uuid,
pub user: Option<UserDto>,
pub seconds: i64,
pub created_at: OffsetDateTime,
}
pub type TrackedTimePage = export::pagination::Page<TrackedTimeDto>;
#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
pub struct ListTrackedTimeQuery {
pub user_id: Option<i64>,
pub limit: Option<u64>,
pub offset: Option<u64>,
}
#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
pub struct TrackedTimePath {
pub space_name: String,
pub board_id: Uuid,
pub issue_id: String,
}
#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
pub struct TrackedTimeIdPath {
pub space_name: String,
pub board_id: Uuid,
pub issue_id: String,
pub time_id: Uuid,
}
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct CreateTrackedTimeCmd {
pub seconds: i64,
}
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct UpdateTrackedTimeCmd {
pub seconds: i64,
}
impl IssueService {
#[tracing::instrument(name = "issues.create_tracked_time", skip_all, fields(caller = %caller, space = %space_name, board_id = %board_id, issue_id = %issue_id))]
pub async fn create_tracked_time(
&self,
caller: i64,
space_name: &str,
board_id: Uuid,
issue_id: Uuid,
cmd: CreateTrackedTimeCmd,
) -> Result<TrackedTimeDto, IssueError> {
if cmd.seconds <= 0 {
return Err(IssueError::BadRequest(
"seconds must be positive".to_string(),
));
}
lookup::require_issue_access(self, caller, space_name, board_id, issue_id).await?;
let row = tracked_time::ActiveModel {
id: Set(Uuid::now_v7()),
issue_id: Set(issue_id),
user_id: Set(caller),
seconds: Set(cmd.seconds),
created_at: Set(OffsetDateTime::now_utc()),
}
.insert(self.db().write())
.await?;
let user_map = self.load_user_briefs(&[row.user_id]).await?;
Ok(TrackedTimeDto {
id: row.id,
issue_id: row.issue_id,
user: user_map.get(&row.user_id).cloned(),
seconds: row.seconds,
created_at: row.created_at,
})
}
#[tracing::instrument(name = "issues.update_tracked_time", skip_all, fields(caller = %caller, space = %space_name, board_id = %board_id, issue_id = %issue_id, time_id = %time_id))]
pub async fn update_tracked_time(
&self,
caller: i64,
space_name: &str,
board_id: Uuid,
issue_id: Uuid,
time_id: Uuid,
cmd: UpdateTrackedTimeCmd,
) -> Result<TrackedTimeDto, IssueError> {
if cmd.seconds <= 0 {
return Err(IssueError::BadRequest(
"seconds must be positive".to_string(),
));
}
lookup::require_issue_access(self, caller, space_name, board_id, issue_id).await?;
let row = tracked_time::Entity::find_by_id(time_id)
.filter(tracked_time::Column::IssueId.eq(issue_id))
.one(self.db().write())
.await?
.ok_or(IssueError::NotFound)?;
if row.user_id != caller {
return Err(IssueError::Forbidden);
}
let mut am: tracked_time::ActiveModel = row.into();
am.seconds = Set(cmd.seconds);
let updated = am.update(self.db().write()).await?;
let user_map = self.load_user_briefs(&[updated.user_id]).await?;
Ok(TrackedTimeDto {
id: updated.id,
issue_id: updated.issue_id,
user: user_map.get(&updated.user_id).cloned(),
seconds: updated.seconds,
created_at: updated.created_at,
})
}
#[tracing::instrument(name = "issues.delete_tracked_time", skip_all, fields(caller = %caller, space = %space_name, board_id = %board_id, issue_id = %issue_id, time_id = %time_id))]
pub async fn delete_tracked_time(
&self,
caller: i64,
space_name: &str,
board_id: Uuid,
issue_id: Uuid,
time_id: Uuid,
) -> Result<(), IssueError> {
lookup::require_issue_access(self, caller, space_name, board_id, issue_id).await?;
let row = tracked_time::Entity::find_by_id(time_id)
.filter(tracked_time::Column::IssueId.eq(issue_id))
.one(self.db().read())
.await?
.ok_or(IssueError::NotFound)?;
if row.user_id != caller {
return Err(IssueError::Forbidden);
}
tracked_time::Entity::delete_by_id(time_id)
.exec(self.db().write())
.await?;
Ok(())
}
pub async fn list_tracked_time(
&self,
caller: i64,
space_name: &str,
board_id: Uuid,
issue_id: Uuid,
q: ListTrackedTimeQuery,
) -> Result<TrackedTimePage, IssueError> {
lookup::require_issue_access(self, caller, space_name, board_id, issue_id).await?;
let (limit, offset) = self.paginate(q.limit, q.offset);
let mut select =
tracked_time::Entity::find().filter(tracked_time::Column::IssueId.eq(issue_id));
if let Some(uid) = q.user_id {
select = select.filter(tracked_time::Column::UserId.eq(uid));
}
select = select.order_by_desc(tracked_time::Column::CreatedAt);
let total = select.clone().count(self.db().read()).await? as i64;
let rows = select
.limit(limit)
.offset(offset)
.all(self.db().read())
.await?;
let user_ids: Vec<i64> = rows.iter().map(|t| t.user_id).collect();
let user_map = self.load_user_briefs(&user_ids).await?;
let items = rows
.into_iter()
.map(|t| {
let user = user_map.get(&t.user_id).cloned();
TrackedTimeDto {
id: t.id,
issue_id: t.issue_id,
user,
seconds: t.seconds,
created_at: t.created_at,
}
})
.collect();
Ok(TrackedTimePage {
items,
total,
limit,
offset,
})
}
}