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 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 let duration = session.current_duration();
220 assert!(duration >= Duration::hours(1));
221
222 session.add_pause_duration(Duration::minutes(30));
224 let active = session.current_active_duration();
225 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 assert!(session.validate().is_ok());
235
236 session.end_time = Some(session.start_time - Duration::seconds(1));
238 assert!(session.validate().is_err());
239
240 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}