Skip to content
Clone
e1df092
161 lines 5.1 KB Rust
use models::issue::{stopwatch, tracked_time};
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, TransactionTrait};
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 StopwatchDto {
pub running: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Uuid>,
pub issue_id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<UserDto>,
#[serde(skip_serializing_if = "Option::is_none")]
pub started_at: Option<OffsetDateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<OffsetDateTime>,
}
#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
pub struct StopwatchPath {
pub space_name: String,
pub board_id: Uuid,
pub issue_id: String,
}
impl IssueService {
pub async fn get_stopwatch(
&self,
caller: i64,
space_name: &str,
board_id: Uuid,
issue_id: Uuid,
) -> Result<StopwatchDto, IssueError> {
lookup::require_issue_access(self, caller, space_name, board_id, issue_id).await?;
let sw = stopwatch::Entity::find()
.filter(stopwatch::Column::IssueId.eq(issue_id))
.filter(stopwatch::Column::UserId.eq(caller))
.one(self.db().read())
.await?;
let Some(sw) = sw else {
return Ok(StopwatchDto {
running: false,
id: None,
issue_id,
user: None,
started_at: None,
created_at: None,
});
};
let user_map = self.load_user_briefs(&[sw.user_id]).await?;
let user = user_map.get(&sw.user_id).cloned();
Ok(StopwatchDto {
running: true,
id: Some(sw.id),
issue_id: sw.issue_id,
user,
started_at: Some(sw.started_at),
created_at: Some(sw.created_at),
})
}
#[tracing::instrument(name = "issues.start_stopwatch", skip_all, fields(caller = %caller, space = %space_name, board_id = %board_id, issue_id = %issue_id))]
pub async fn start_stopwatch(
&self,
caller: i64,
space_name: &str,
board_id: Uuid,
issue_id: Uuid,
) -> Result<StopwatchDto, IssueError> {
lookup::require_issue_access(self, caller, space_name, board_id, issue_id).await?;
let existing = stopwatch::Entity::find()
.filter(stopwatch::Column::IssueId.eq(issue_id))
.filter(stopwatch::Column::UserId.eq(caller))
.one(self.db().read())
.await?;
if existing.is_some() {
return Err(IssueError::Conflict(
"stopwatch already running for this issue".to_string(),
));
}
let now = OffsetDateTime::now_utc();
let row = stopwatch::ActiveModel {
id: Set(Uuid::now_v7()),
issue_id: Set(issue_id),
user_id: Set(caller),
started_at: Set(now),
created_at: Set(now),
};
let inserted = row.insert(self.db().write()).await?;
let user_map = self.load_user_briefs(&[caller]).await?;
let user = user_map.get(&caller).cloned();
Ok(StopwatchDto {
running: true,
id: Some(inserted.id),
issue_id: inserted.issue_id,
user,
started_at: Some(inserted.started_at),
created_at: Some(inserted.created_at),
})
}
#[tracing::instrument(name = "issues.stop_stopwatch", skip_all, fields(caller = %caller, space = %space_name, board_id = %board_id, issue_id = %issue_id))]
pub async fn stop_stopwatch(
&self,
caller: i64,
space_name: &str,
board_id: Uuid,
issue_id: Uuid,
) -> Result<(), IssueError> {
lookup::require_issue_access(self, caller, space_name, board_id, issue_id).await?;
let sw = stopwatch::Entity::find()
.filter(stopwatch::Column::IssueId.eq(issue_id))
.filter(stopwatch::Column::UserId.eq(caller))
.one(self.db().read())
.await?
.ok_or(IssueError::NotFound)?;
let now = OffsetDateTime::now_utc();
let duration = (now - sw.started_at).whole_seconds().max(0);
self.db()
.primary()
.transaction::<_, (), IssueError>(|txn| {
Box::pin(async move {
stopwatch::Entity::delete_by_id(sw.id).exec(txn).await?;
let tt = tracked_time::ActiveModel {
id: Set(Uuid::now_v7()),
issue_id: Set(issue_id),
user_id: Set(caller),
seconds: Set(duration),
created_at: Set(now),
};
tt.insert(txn).await?;
Ok(())
})
})
.await?;
Ok(())
}
}