work_tuimer/timer/
mod.rs

1//! Timer module for automatic time tracking
2//!
3//! This module provides automatic timer functionality for tracking work sessions.
4//! Timers can be started, paused, resumed, and stopped, with automatic conversion
5//! to WorkRecord upon completion.
6
7use crate::models::{TimePoint, WorkRecord};
8use crate::storage::Storage;
9use anyhow::{Context, Result, anyhow};
10use serde::{Deserialize, Serialize};
11use std::time::Duration as StdDuration;
12use time::{Date, OffsetDateTime};
13
14/// Timer status enumeration
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum TimerStatus {
18    Running,
19    Paused,
20    Stopped,
21}
22
23/// Active timer state with SQLite-ready fields
24///
25/// This struct represents an active timer session. All fields are designed
26/// to be compatible with SQLite storage for future migration (Issue #22).
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
28pub struct TimerState {
29    /// Optional ID for future SQLite primary key (currently unused)
30    pub id: Option<u32>,
31
32    /// Task name being tracked
33    pub task_name: String,
34
35    /// Optional description for the task
36    pub description: Option<String>,
37
38    /// When the timer was started (UTC)
39    pub start_time: OffsetDateTime,
40
41    /// When the timer was stopped (UTC), None if still active
42    pub end_time: Option<OffsetDateTime>,
43
44    /// Date when timer was started
45    pub date: Date,
46
47    /// Current status of the timer
48    pub status: TimerStatus,
49
50    /// Total duration in seconds when paused (cumulative)
51    pub paused_duration_secs: i64,
52
53    /// When timer was last paused (to track current pause duration)
54    pub paused_at: Option<OffsetDateTime>,
55
56    /// When this timer record was created (audit field)
57    pub created_at: OffsetDateTime,
58
59    /// When this timer record was last updated (audit field)
60    pub updated_at: OffsetDateTime,
61
62    /// ID of the source work record that this timer was started from
63    /// If present, stopping the timer will update the existing record instead of creating a new one
64    #[serde(default)]
65    pub source_record_id: Option<u32>,
66
67    /// Date of the source work record (needed when timer is started from a past/future date view)
68    /// If present, we'll update the record in this date's file instead of the timer start date
69    #[serde(default)]
70    pub source_record_date: Option<Date>,
71}
72
73/// Timer manager for controlling timer operations
74///
75/// Provides methods to start, stop, pause, and resume timers, as well as
76/// query their current status. Manages persistence through the StorageManager layer.
77pub struct TimerManager {
78    storage: Storage,
79}
80
81impl TimerManager {
82    /// Create a new timer manager with low-level Storage
83    /// For internal use - external callers should use storage::StorageManager instead
84    pub fn new(storage: Storage) -> Self {
85        TimerManager { storage }
86    }
87
88    /// Start a new timer
89    ///
90    /// # Errors
91    /// Returns an error if a timer is already running
92    pub fn start(
93        &self,
94        task_name: String,
95        description: Option<String>,
96        source_record_id: Option<u32>,
97        source_record_date: Option<Date>,
98    ) -> Result<TimerState> {
99        // Check if timer already running
100        if (self.storage.load_active_timer()?).is_some() {
101            return Err(anyhow!("A timer is already running"));
102        }
103
104        let now = OffsetDateTime::now_local()
105            .context("Failed to get local time. System clock may not be configured correctly.")?;
106        let timer = TimerState {
107            id: None,
108            task_name,
109            description,
110            start_time: now,
111            end_time: None,
112            date: now.date(),
113            status: TimerStatus::Running,
114            paused_duration_secs: 0,
115            paused_at: None,
116            created_at: now,
117            updated_at: now,
118            source_record_id,
119            source_record_date,
120        };
121
122        self.storage.save_active_timer(&timer)?;
123        Ok(timer)
124    }
125
126    /// Stop the active timer and convert it to a WorkRecord
127    ///
128    /// # Errors
129    /// Returns an error if no timer is running
130    pub fn stop(&self) -> Result<WorkRecord> {
131        let mut timer = self
132            .storage
133            .load_active_timer()?
134            .ok_or_else(|| anyhow!("No timer is currently running"))?;
135
136        let now = OffsetDateTime::now_local()
137            .context("Failed to get local time. System clock may not be configured correctly.")?;
138
139        // Determine which date's data file to load:
140        // - If timer has source_record_date, use that (record is from a specific day's view)
141        // - Otherwise use timer.start_time.date() (creating new record on timer's start date)
142        let target_date = timer
143            .source_record_date
144            .unwrap_or_else(|| timer.start_time.date());
145
146        timer.end_time = Some(now);
147        timer.status = TimerStatus::Stopped;
148        timer.updated_at = now;
149
150        // Load the day's data file
151        let mut day_data = self.storage.load(&target_date)?;
152
153        // If timer was started from an existing record, update that record's end time
154        // Otherwise, create a new work record
155        if let Some(source_id) = timer.source_record_id {
156            // Find and update the existing record
157            if let Some(record) = day_data.work_records.get_mut(&source_id) {
158                // Update the end time to now
159                let end_timepoint = TimePoint::new(now.hour(), now.minute())
160                    .map_err(|e| anyhow!(e))
161                    .context("Failed to create TimePoint for timer end time")?;
162                record.end = end_timepoint;
163                record.update_duration();
164            } else {
165                // Source record not found, create new one instead
166                let mut work_record = self.to_work_record(timer.clone())?;
167                // Assign proper ID from day_data instead of using placeholder
168                work_record.id = day_data.next_id();
169                day_data.add_record(work_record);
170            }
171        } else {
172            // No source record, create a new work record
173            let mut work_record = self.to_work_record(timer.clone())?;
174            // Assign proper ID from day_data instead of using placeholder
175            work_record.id = day_data.next_id();
176            day_data.add_record(work_record);
177        }
178
179        self.storage.save(&day_data)?;
180        self.storage.clear_active_timer()?;
181
182        // Return a work record for the stopped timer (for display purposes)
183        let work_record = self.to_work_record(timer)?;
184        Ok(work_record)
185    }
186
187    /// Pause the active timer
188    ///
189    /// # Errors
190    /// Returns an error if timer is not running
191    pub fn pause(&self) -> Result<TimerState> {
192        let mut timer = self
193            .storage
194            .load_active_timer()?
195            .ok_or_else(|| anyhow!("No timer is currently running"))?;
196
197        if timer.status == TimerStatus::Paused {
198            return Err(anyhow!("Timer is already paused"));
199        }
200
201        if timer.status != TimerStatus::Running {
202            return Err(anyhow!("Can only pause a running timer"));
203        }
204
205        let now = OffsetDateTime::now_local()
206            .context("Failed to get local time. System clock may not be configured correctly.")?;
207        timer.paused_at = Some(now);
208        timer.status = TimerStatus::Paused;
209        timer.updated_at = now;
210
211        self.storage.save_active_timer(&timer)?;
212        Ok(timer)
213    }
214
215    /// Resume a paused timer
216    ///
217    /// # Errors
218    /// Returns an error if timer is not paused
219    pub fn resume(&self) -> Result<TimerState> {
220        let mut timer = self
221            .storage
222            .load_active_timer()?
223            .ok_or_else(|| anyhow!("No timer is currently running"))?;
224
225        if timer.status != TimerStatus::Paused {
226            return Err(anyhow!("Can only resume a paused timer"));
227        }
228
229        let now = OffsetDateTime::now_local()
230            .context("Failed to get local time. System clock may not be configured correctly.")?;
231
232        // Add current pause duration to cumulative paused time
233        if let Some(paused_at) = timer.paused_at {
234            let pause_duration = (now - paused_at).whole_seconds();
235            timer.paused_duration_secs += pause_duration;
236        }
237
238        timer.paused_at = None;
239        timer.status = TimerStatus::Running;
240        timer.updated_at = now;
241
242        self.storage.save_active_timer(&timer)?;
243        Ok(timer)
244    }
245
246    /// Get the current timer status
247    ///
248    /// Returns None if no timer is running
249    pub fn status(&self) -> Result<Option<TimerState>> {
250        self.storage.load_active_timer()
251    }
252
253    /// Calculate elapsed duration of a timer
254    ///
255    /// Returns the time since start_time, minus any paused durations.
256    pub fn get_elapsed_duration(&self, timer: &TimerState) -> StdDuration {
257        let end_point = if timer.status == TimerStatus::Paused {
258            // If paused, use when it was paused
259            timer.paused_at.unwrap_or_else(|| {
260                OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc())
261            })
262        } else {
263            // If running, use now
264            OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc())
265        };
266
267        let elapsed = end_point - timer.start_time;
268        let paused_duration_std = StdDuration::from_secs(timer.paused_duration_secs as u64);
269
270        // Convert time::Duration to std::Duration for arithmetic
271        let elapsed_std = StdDuration::from_secs(elapsed.whole_seconds() as u64)
272            + StdDuration::from_nanos(elapsed.subsec_nanoseconds() as u64);
273
274        elapsed_std
275            .checked_sub(paused_duration_std)
276            .unwrap_or(StdDuration::ZERO)
277    }
278
279    /// Convert a stopped timer to a WorkRecord
280    fn to_work_record(&self, timer: TimerState) -> Result<WorkRecord> {
281        if timer.status != TimerStatus::Stopped {
282            return Err(anyhow!("Can only convert stopped timers to WorkRecord"));
283        }
284
285        let start_time = timer.start_time;
286        let end_time = timer
287            .end_time
288            .ok_or_else(|| anyhow!("Stopped timer must have end_time"))?;
289
290        // Extract just the time portion from the OffsetDateTime values
291        let start_timepoint = TimePoint::new(start_time.hour(), start_time.minute())
292            .map_err(|e| anyhow!(e))
293            .context("Failed to create TimePoint for timer start time")?;
294
295        let end_timepoint = TimePoint::new(end_time.hour(), end_time.minute())
296            .map_err(|e| anyhow!(e))
297            .context("Failed to create TimePoint for timer end time")?;
298
299        let mut record = WorkRecord::new(
300            1, // Placeholder ID, will be set by DayData
301            timer.task_name,
302            start_timepoint,
303            end_timepoint,
304        );
305
306        if let Some(description) = timer.description {
307            record.description = description;
308        }
309
310        Ok(record)
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use tempfile::TempDir;
318
319    fn create_test_storage() -> (Storage, TempDir) {
320        let temp_dir = TempDir::new().unwrap();
321        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
322        (storage, temp_dir)
323    }
324
325    #[test]
326    fn test_timer_state_creation() {
327        let now = OffsetDateTime::now_utc();
328        let timer = TimerState {
329            id: None,
330            task_name: "Test Task".to_string(),
331            description: None,
332            start_time: now,
333            end_time: None,
334            date: now.date(),
335            status: TimerStatus::Running,
336            paused_duration_secs: 0,
337            paused_at: None,
338            created_at: now,
339            updated_at: now,
340            source_record_id: None,
341            source_record_date: None,
342        };
343
344        assert_eq!(timer.task_name, "Test Task");
345        assert_eq!(timer.status, TimerStatus::Running);
346        assert_eq!(timer.paused_duration_secs, 0);
347    }
348
349    #[test]
350    fn test_timer_serialization() {
351        let now = OffsetDateTime::now_utc();
352        let timer = TimerState {
353            id: None,
354            task_name: "Test Task".to_string(),
355            description: Some("Test description".to_string()),
356            start_time: now,
357            end_time: None,
358            date: now.date(),
359            status: TimerStatus::Running,
360            paused_duration_secs: 0,
361            paused_at: None,
362            created_at: now,
363            updated_at: now,
364            source_record_id: None,
365            source_record_date: None,
366        };
367
368        let json = serde_json::to_string(&timer).unwrap();
369        let deserialized: TimerState = serde_json::from_str(&json).unwrap();
370
371        assert_eq!(deserialized.task_name, timer.task_name);
372        assert_eq!(deserialized.status, timer.status);
373    }
374
375    #[test]
376    fn test_start_timer() {
377        let (storage, _temp) = create_test_storage();
378        let manager = TimerManager::new(storage);
379
380        let result = manager.start("Work".to_string(), None, None, None);
381        assert!(result.is_ok());
382
383        let timer = result.unwrap();
384        assert_eq!(timer.task_name, "Work");
385        assert_eq!(timer.status, TimerStatus::Running);
386        assert_eq!(timer.paused_duration_secs, 0);
387    }
388
389    #[test]
390    fn test_cannot_start_when_already_running() {
391        let (storage, _temp) = create_test_storage();
392        let manager = TimerManager::new(storage);
393
394        let _ = manager.start("Task 1".to_string(), None, None, None);
395        let result = manager.start("Task 2".to_string(), None, None, None);
396
397        assert!(result.is_err());
398        assert_eq!(
399            result.unwrap_err().to_string(),
400            "A timer is already running"
401        );
402    }
403
404    #[test]
405    fn test_pause_running_timer() {
406        let (storage, _temp) = create_test_storage();
407        let manager = TimerManager::new(storage);
408
409        let _ = manager.start("Work".to_string(), None, None, None);
410        let result = manager.pause();
411
412        assert!(result.is_ok());
413        let timer = result.unwrap();
414        assert_eq!(timer.status, TimerStatus::Paused);
415        assert!(timer.paused_at.is_some());
416    }
417
418    #[test]
419    fn test_cannot_pause_paused_timer() {
420        let (storage, _temp) = create_test_storage();
421        let manager = TimerManager::new(storage);
422
423        let _ = manager.start("Work".to_string(), None, None, None);
424        let _ = manager.pause();
425        let result = manager.pause();
426
427        assert!(result.is_err());
428    }
429
430    #[test]
431    fn test_pause_without_running_timer() {
432        let (storage, _temp) = create_test_storage();
433        let manager = TimerManager::new(storage);
434
435        let result = manager.pause();
436        assert!(result.is_err());
437    }
438
439    #[test]
440    fn test_resume_paused_timer() {
441        let (storage, _temp) = create_test_storage();
442        let manager = TimerManager::new(storage);
443
444        let _ = manager.start("Work".to_string(), None, None, None);
445        let _ = manager.pause();
446        let result = manager.resume();
447
448        assert!(result.is_ok());
449        let timer = result.unwrap();
450        assert_eq!(timer.status, TimerStatus::Running);
451        assert!(timer.paused_at.is_none());
452    }
453
454    #[test]
455    fn test_resume_updates_paused_duration() {
456        let (storage, _temp) = create_test_storage();
457        let manager = TimerManager::new(storage);
458
459        let _ = manager.start("Work".to_string(), None, None, None);
460        let paused1 = manager.pause().unwrap();
461        assert_eq!(paused1.paused_duration_secs, 0);
462
463        // Simulate time passing by manually updating
464        let _ = manager.resume();
465        let paused2 = manager.pause().unwrap();
466
467        // paused_duration_secs should have increased
468        assert!(paused2.paused_duration_secs >= 0);
469    }
470
471    #[test]
472    fn test_cannot_resume_running_timer() {
473        let (storage, _temp) = create_test_storage();
474        let manager = TimerManager::new(storage);
475
476        let _ = manager.start("Work".to_string(), None, None, None);
477        let result = manager.resume();
478
479        assert!(result.is_err());
480    }
481
482    #[test]
483    fn test_status_returns_none_when_no_timer() {
484        let (storage, _temp) = create_test_storage();
485        let manager = TimerManager::new(storage);
486
487        let result = manager.status().unwrap();
488        assert!(result.is_none());
489    }
490
491    #[test]
492    fn test_status_returns_running_timer() {
493        let (storage, _temp) = create_test_storage();
494        let manager = TimerManager::new(storage);
495
496        let _ = manager.start("Work".to_string(), None, None, None);
497        let result = manager.status().unwrap();
498
499        assert!(result.is_some());
500        let timer = result.unwrap();
501        assert_eq!(timer.task_name, "Work");
502        assert_eq!(timer.status, TimerStatus::Running);
503    }
504
505    #[test]
506    fn test_stop_running_timer() {
507        let (storage, _temp) = create_test_storage();
508        let manager = TimerManager::new(storage);
509
510        let _ = manager.start("Work".to_string(), None, None, None);
511        let result = manager.stop();
512
513        assert!(result.is_ok());
514        let work_record = result.unwrap();
515        assert_eq!(work_record.name, "Work");
516
517        // Timer should be cleared
518        let timer_status = manager.status().unwrap();
519        assert!(timer_status.is_none());
520    }
521
522    #[test]
523    fn test_cannot_stop_without_running_timer() {
524        let (storage, _temp) = create_test_storage();
525        let manager = TimerManager::new(storage);
526
527        let result = manager.stop();
528        assert!(result.is_err());
529    }
530
531    #[test]
532    fn test_stop_returns_work_record_with_description() {
533        let (storage, _temp) = create_test_storage();
534        let manager = TimerManager::new(storage);
535
536        let _ = manager.start(
537            "Work".to_string(),
538            Some("Important task".to_string()),
539            None,
540            None,
541        );
542        let work_record = manager.stop().unwrap();
543
544        assert_eq!(work_record.name, "Work");
545        assert_eq!(work_record.description, "Important task");
546    }
547
548    #[test]
549    fn test_full_timer_lifecycle() {
550        let (storage, _temp) = create_test_storage();
551        let manager = TimerManager::new(storage);
552
553        // Start
554        let started = manager.start("Task".to_string(), None, None, None).unwrap();
555        assert_eq!(started.status, TimerStatus::Running);
556
557        // Pause
558        let paused = manager.pause().unwrap();
559        assert_eq!(paused.status, TimerStatus::Paused);
560
561        // Resume
562        let resumed = manager.resume().unwrap();
563        assert_eq!(resumed.status, TimerStatus::Running);
564
565        // Pause again
566        let paused_again = manager.pause().unwrap();
567        assert_eq!(paused_again.status, TimerStatus::Paused);
568
569        // Resume again
570        let resumed_again = manager.resume().unwrap();
571        assert_eq!(resumed_again.status, TimerStatus::Running);
572
573        // Stop
574        let work_record = manager.stop().unwrap();
575        assert_eq!(work_record.name, "Task");
576
577        // Verify timer is cleared
578        let status = manager.status().unwrap();
579        assert!(status.is_none());
580    }
581
582    #[test]
583    fn test_get_elapsed_duration_running() {
584        let (storage, _temp) = create_test_storage();
585        let manager = TimerManager::new(storage);
586
587        let timer = manager.start("Task".to_string(), None, None, None).unwrap();
588        let elapsed = manager.get_elapsed_duration(&timer);
589
590        // Should be close to 0 since just started
591        assert!(elapsed.as_secs() < 2);
592    }
593
594    #[test]
595    fn test_get_elapsed_duration_with_pause() {
596        let (storage, _temp) = create_test_storage();
597        let manager = TimerManager::new(storage);
598
599        let _ = manager.start("Task".to_string(), None, None, None);
600        let _ = manager.pause();
601
602        let timer = manager.status().unwrap().unwrap();
603        let elapsed = manager.get_elapsed_duration(&timer);
604
605        // Should be very small since just paused
606        assert!(elapsed.as_secs() < 2);
607    }
608
609    #[test]
610    fn test_stop_updates_existing_record() {
611        use crate::models::DayData;
612        use crate::models::TimePoint;
613        use crate::models::WorkRecord;
614        use tempfile::TempDir;
615        use time::OffsetDateTime;
616
617        // Create a temp dir and storage that we can reuse
618        let temp_dir = TempDir::new().unwrap();
619        let storage_path = temp_dir.path().to_path_buf();
620
621        // Create initial day data with one record
622        let now = OffsetDateTime::now_utc();
623        let today = now.date();
624        let mut day_data = DayData::new(today);
625
626        let record = WorkRecord::new(
627            1,
628            "Existing Task".to_string(),
629            TimePoint::new(9, 0).unwrap(),
630            TimePoint::new(10, 0).unwrap(),
631        );
632        day_data.add_record(record);
633
634        // Save using first storage instance
635        let storage1 = Storage::new_with_dir(storage_path.clone()).unwrap();
636        storage1.save(&day_data).unwrap();
637
638        // Start timer with source_record_id = 1, source_record_date = today
639        let manager = TimerManager::new(storage1);
640        manager
641            .start("Existing Task".to_string(), None, Some(1), Some(today))
642            .unwrap();
643
644        // Stop timer - should update the existing record's end time
645        manager.stop().unwrap();
646
647        // Create a new storage instance pointing to the same temp dir to verify the update
648        let storage2 = Storage::new_with_dir(storage_path).unwrap();
649        let updated_day_data = storage2.load(&today).unwrap();
650
651        // Should still have only 1 record (not 2!)
652        assert_eq!(updated_day_data.work_records.len(), 1);
653
654        // The record should have updated end time (not still 10:00)
655        let updated_record = updated_day_data.work_records.get(&1).unwrap();
656        assert_eq!(updated_record.name, "Existing Task");
657        // End time should be close to now (within a few minutes)
658        assert!(
659            updated_record.end.hour >= now.hour()
660                || (updated_record.end.hour == 0 && now.hour() == 23)
661        ); // Handle day boundary
662    }
663}