Skip to main content

max / goingson

2.8 KB · 94 lines History Blame Raw
1 //! Time tracking session types.
2 //!
3 //! A `TimeSession` records a period of work on a task. Sessions are created
4 //! when a timer is started and closed when it's stopped. At most one session
5 //! per user can be active (ended_at IS NULL) at any time.
6
7 use chrono::{DateTime, Utc};
8 use serde::{Deserialize, Serialize};
9 use crate::id_types::{TaskId, TimeSessionId, UserId, ProjectId};
10
11 /// A single time tracking session on a task.
12 #[derive(Debug, Clone, Serialize, Deserialize)]
13 #[serde(rename_all = "camelCase")]
14 pub struct TimeSession {
15 pub id: TimeSessionId,
16 pub task_id: TaskId,
17 pub user_id: UserId,
18 pub started_at: DateTime<Utc>,
19 pub ended_at: Option<DateTime<Utc>>,
20 pub duration_minutes: Option<i32>,
21 pub created_at: DateTime<Utc>,
22 }
23
24 impl TimeSession {
25 /// Returns true if this session is still running (no end time).
26 pub fn is_active(&self) -> bool {
27 self.ended_at.is_none()
28 }
29
30 /// Returns elapsed minutes from start to now (if active) or to ended_at.
31 pub fn elapsed_minutes(&self) -> i32 {
32 let end = self.ended_at.unwrap_or_else(Utc::now);
33 let diff = end.signed_duration_since(self.started_at);
34 diff.num_minutes().max(0) as i32
35 }
36 }
37
38 /// Aggregated time tracking data grouped by project and date.
39 #[derive(Debug, Clone, Serialize, Deserialize)]
40 #[serde(rename_all = "camelCase")]
41 pub struct TimeTrackingSummary {
42 pub project_id: Option<ProjectId>,
43 pub project_name: Option<String>,
44 pub date: String,
45 pub total_minutes: i32,
46 pub session_count: i32,
47 }
48
49 #[cfg(test)]
50 mod tests {
51 use super::*;
52
53 fn make_session(started: DateTime<Utc>, ended: Option<DateTime<Utc>>) -> TimeSession {
54 TimeSession {
55 id: TimeSessionId::new(),
56 task_id: TaskId::new(),
57 user_id: UserId::new(),
58 started_at: started,
59 ended_at: ended,
60 duration_minutes: ended.map(|e| (e - started).num_minutes() as i32),
61 created_at: started,
62 }
63 }
64
65 #[test]
66 fn is_active_when_no_end() {
67 let session = make_session(Utc::now(), None);
68 assert!(session.is_active());
69 }
70
71 #[test]
72 fn is_not_active_when_ended() {
73 let start = Utc::now() - chrono::Duration::minutes(30);
74 let end = Utc::now();
75 let session = make_session(start, Some(end));
76 assert!(!session.is_active());
77 }
78
79 #[test]
80 fn elapsed_minutes_for_ended_session() {
81 let start = Utc::now() - chrono::Duration::minutes(45);
82 let end = Utc::now();
83 let session = make_session(start, Some(end));
84 assert_eq!(session.elapsed_minutes(), 45);
85 }
86
87 #[test]
88 fn elapsed_minutes_active_session_positive() {
89 let start = Utc::now() - chrono::Duration::minutes(10);
90 let session = make_session(start, None);
91 assert!(session.elapsed_minutes() >= 9);
92 }
93 }
94