tempo_cli/models/
session.rs

1use chrono::{DateTime, Duration, Utc};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5pub enum SessionContext {
6    Terminal,
7    IDE,
8    Linked,
9    Manual,
10}
11
12impl std::fmt::Display for SessionContext {
13    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14        match self {
15            SessionContext::Terminal => write!(f, "terminal"),
16            SessionContext::IDE => write!(f, "ide"),
17            SessionContext::Linked => write!(f, "linked"),
18            SessionContext::Manual => write!(f, "manual"),
19        }
20    }
21}
22
23impl std::str::FromStr for SessionContext {
24    type Err = anyhow::Error;
25
26    fn from_str(s: &str) -> Result<Self, Self::Err> {
27        match s.to_lowercase().as_str() {
28            "terminal" => Ok(SessionContext::Terminal),
29            "ide" => Ok(SessionContext::IDE),
30            "linked" => Ok(SessionContext::Linked),
31            "manual" => Ok(SessionContext::Manual),
32            _ => Err(anyhow::anyhow!("Invalid session context: {}", s)),
33        }
34    }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum SessionStatus {
39    Active,
40    Paused,
41    Completed,
42}
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45pub struct Session {
46    pub id: Option<i64>,
47    pub project_id: i64,
48    pub start_time: DateTime<Utc>,
49    pub end_time: Option<DateTime<Utc>>,
50    pub context: SessionContext,
51    pub paused_duration: Duration,
52    pub notes: Option<String>,
53    pub created_at: DateTime<Utc>,
54}
55
56impl Session {
57    pub fn new(project_id: i64, context: SessionContext) -> Self {
58        let now = Utc::now();
59        Self {
60            id: None,
61            project_id,
62            start_time: now,
63            end_time: None,
64            context,
65            paused_duration: Duration::zero(),
66            notes: None,
67            created_at: now,
68        }
69    }
70
71    pub fn with_start_time(mut self, start_time: DateTime<Utc>) -> Self {
72        self.start_time = start_time;
73        self
74    }
75
76    pub fn with_notes(mut self, notes: Option<String>) -> Self {
77        self.notes = notes;
78        self
79    }
80
81    pub fn end_session(&mut self) -> anyhow::Result<()> {
82        if self.end_time.is_some() {
83            return Err(anyhow::anyhow!("Session is already ended"));
84        }
85
86        self.end_time = Some(Utc::now());
87        Ok(())
88    }
89
90    pub fn add_pause_duration(&mut self, duration: Duration) {
91        self.paused_duration = self.paused_duration + duration;
92    }
93
94    pub fn is_active(&self) -> bool {
95        self.end_time.is_none()
96    }
97
98    pub fn status(&self) -> SessionStatus {
99        if self.end_time.is_some() {
100            SessionStatus::Completed
101        } else {
102            SessionStatus::Active
103        }
104    }
105
106    pub fn total_duration(&self) -> Option<Duration> {
107        self.end_time.map(|end| end - self.start_time)
108    }
109
110    pub fn active_duration(&self) -> Option<Duration> {
111        self.total_duration()
112            .map(|total| total - self.paused_duration)
113    }
114
115    pub fn current_duration(&self) -> Duration {
116        let end_time = self.end_time.unwrap_or_else(Utc::now);
117        end_time - self.start_time
118    }
119
120    pub fn current_active_duration(&self) -> Duration {
121        self.current_duration() - self.paused_duration
122    }
123
124    pub fn validate(&self) -> anyhow::Result<()> {
125        if let Some(end_time) = self.end_time {
126            if end_time <= self.start_time {
127                return Err(anyhow::anyhow!("End time must be after start time"));
128            }
129        }
130
131        if self.paused_duration < Duration::zero() {
132            return Err(anyhow::anyhow!("Paused duration cannot be negative"));
133        }
134
135        let total = self.current_duration();
136        if self.paused_duration > total {
137            return Err(anyhow::anyhow!(
138                "Paused duration cannot exceed total duration"
139            ));
140        }
141
142        Ok(())
143    }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct SessionEdit {
148    pub id: Option<i64>,
149    pub session_id: i64,
150    pub original_start_time: DateTime<Utc>,
151    pub original_end_time: Option<DateTime<Utc>>,
152    pub new_start_time: DateTime<Utc>,
153    pub new_end_time: Option<DateTime<Utc>>,
154    pub edit_reason: Option<String>,
155    pub created_at: DateTime<Utc>,
156}
157
158impl SessionEdit {
159    pub fn new(
160        session_id: i64,
161        original_start_time: DateTime<Utc>,
162        original_end_time: Option<DateTime<Utc>>,
163        new_start_time: DateTime<Utc>,
164        new_end_time: Option<DateTime<Utc>>,
165    ) -> Self {
166        Self {
167            id: None,
168            session_id,
169            original_start_time,
170            original_end_time,
171            new_start_time,
172            new_end_time,
173            edit_reason: None,
174            created_at: Utc::now(),
175        }
176    }
177
178    pub fn with_reason(mut self, reason: Option<String>) -> Self {
179        self.edit_reason = reason;
180        self
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_session_new() {
190        let session = Session::new(1, SessionContext::Terminal);
191        assert_eq!(session.project_id, 1);
192        assert_eq!(session.context, SessionContext::Terminal);
193        assert!(session.end_time.is_none());
194        assert_eq!(session.paused_duration, Duration::zero());
195    }
196
197    #[test]
198    fn test_session_end() {
199        let mut session = Session::new(1, SessionContext::IDE);
200        assert!(session.is_active());
201
202        let result = session.end_session();
203        assert!(result.is_ok());
204        assert!(!session.is_active());
205        assert!(session.end_time.is_some());
206
207        // Cannot end twice
208        let result = session.end_session();
209        assert!(result.is_err());
210    }
211
212    #[test]
213    fn test_session_duration() {
214        let mut session = Session::new(1, SessionContext::Manual);
215        let start = Utc::now() - Duration::hours(1);
216        session.start_time = start;
217
218        // Active duration (approx 1 hour)
219        let duration = session.current_duration();
220        assert!(duration >= Duration::hours(1));
221
222        // Add pause
223        session.add_pause_duration(Duration::minutes(30));
224        let active = session.current_active_duration();
225        // Should be approx 30 mins (1h total - 30m pause)
226        assert!(active >= Duration::minutes(29) && active <= Duration::minutes(31));
227    }
228
229    #[test]
230    fn test_session_validation() {
231        let mut session = Session::new(1, SessionContext::Terminal);
232
233        // Valid case
234        assert!(session.validate().is_ok());
235
236        // Invalid: End before start
237        session.end_time = Some(session.start_time - Duration::seconds(1));
238        assert!(session.validate().is_err());
239
240        // Invalid: Pause > Total
241        session.end_time = Some(session.start_time + Duration::minutes(10));
242        session.paused_duration = Duration::minutes(20);
243        assert!(session.validate().is_err());
244    }
245}