//! Time tracking session types. //! //! A `TimeSession` records a period of work on a task. Sessions are created //! when a timer is started and closed when it's stopped. At most one session //! per user can be active (ended_at IS NULL) at any time. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::id_types::{TaskId, TimeSessionId, UserId, ProjectId}; /// A single time tracking session on a task. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TimeSession { pub id: TimeSessionId, pub task_id: TaskId, pub user_id: UserId, pub started_at: DateTime, pub ended_at: Option>, pub duration_minutes: Option, pub created_at: DateTime, } impl TimeSession { /// Returns true if this session is still running (no end time). pub fn is_active(&self) -> bool { self.ended_at.is_none() } /// Returns elapsed minutes from start to now (if active) or to ended_at. pub fn elapsed_minutes(&self) -> i32 { let end = self.ended_at.unwrap_or_else(Utc::now); let diff = end.signed_duration_since(self.started_at); diff.num_minutes().max(0) as i32 } } /// Aggregated time tracking data grouped by project and date. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TimeTrackingSummary { pub project_id: Option, pub project_name: Option, pub date: String, pub total_minutes: i32, pub session_count: i32, } #[cfg(test)] mod tests { use super::*; fn make_session(started: DateTime, ended: Option>) -> TimeSession { TimeSession { id: TimeSessionId::new(), task_id: TaskId::new(), user_id: UserId::new(), started_at: started, ended_at: ended, duration_minutes: ended.map(|e| (e - started).num_minutes() as i32), created_at: started, } } #[test] fn is_active_when_no_end() { let session = make_session(Utc::now(), None); assert!(session.is_active()); } #[test] fn is_not_active_when_ended() { let start = Utc::now() - chrono::Duration::minutes(30); let end = Utc::now(); let session = make_session(start, Some(end)); assert!(!session.is_active()); } #[test] fn elapsed_minutes_for_ended_session() { let start = Utc::now() - chrono::Duration::minutes(45); let end = Utc::now(); let session = make_session(start, Some(end)); assert_eq!(session.elapsed_minutes(), 45); } #[test] fn elapsed_minutes_active_session_positive() { let start = Utc::now() - chrono::Duration::minutes(10); let session = make_session(start, None); assert!(session.elapsed_minutes() >= 9); } }