use crate::models::{TimePoint, WorkRecord};
use crate::storage::Storage;
use anyhow::{Context, Result, anyhow};
use serde::{Deserialize, Serialize};
use std::time::Duration as StdDuration;
use time::{Date, OffsetDateTime};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TimerStatus {
Running,
Paused,
Stopped,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TimerState {
pub id: Option<u32>,
pub task_name: String,
pub description: Option<String>,
#[serde(default)]
pub project: Option<String>,
#[serde(default)]
pub customer: Option<String>,
pub start_time: OffsetDateTime,
pub end_time: Option<OffsetDateTime>,
pub date: Date,
pub status: TimerStatus,
pub paused_duration_secs: i64,
pub paused_at: Option<OffsetDateTime>,
pub created_at: OffsetDateTime,
pub updated_at: OffsetDateTime,
#[serde(default)]
pub source_record_id: Option<u32>,
#[serde(default)]
pub source_record_date: Option<Date>,
}
pub struct TimerManager {
storage: Storage,
}
impl TimerManager {
pub fn new(storage: Storage) -> Self {
TimerManager { storage }
}
pub fn start(
&self,
task_name: String,
description: Option<String>,
project: Option<String>,
customer: Option<String>,
source_record_id: Option<u32>,
source_record_date: Option<Date>,
) -> Result<TimerState> {
if (self.storage.load_active_timer()?).is_some() {
return Err(anyhow!("A timer is already running"));
}
let now = OffsetDateTime::now_local()
.context("Failed to get local time. System clock may not be configured correctly.")?;
let timer = TimerState {
id: None,
task_name,
description,
project,
customer,
start_time: now,
end_time: None,
date: now.date(),
status: TimerStatus::Running,
paused_duration_secs: 0,
paused_at: None,
created_at: now,
updated_at: now,
source_record_id,
source_record_date,
};
self.storage.save_active_timer(&timer)?;
Ok(timer)
}
pub fn stop(&self) -> Result<WorkRecord> {
let mut timer = self
.storage
.load_active_timer()?
.ok_or_else(|| anyhow!("No timer is currently running"))?;
let now = OffsetDateTime::now_local()
.context("Failed to get local time. System clock may not be configured correctly.")?;
let target_date = timer
.source_record_date
.unwrap_or_else(|| timer.start_time.date());
timer.end_time = Some(now);
timer.status = TimerStatus::Stopped;
timer.updated_at = now;
let mut day_data = self.storage.load(&target_date)?;
if let Some(source_id) = timer.source_record_id {
if let Some(record) = day_data.work_records.get_mut(&source_id) {
let end_timepoint = TimePoint::new(now.hour(), now.minute())
.map_err(|e| anyhow!(e))
.context("Failed to create TimePoint for timer end time")?;
record.end = end_timepoint;
record.update_duration();
} else {
let mut work_record = self.to_work_record(timer.clone())?;
work_record.id = day_data.next_id();
day_data.add_record(work_record);
}
} else {
let mut work_record = self.to_work_record(timer.clone())?;
work_record.id = day_data.next_id();
day_data.add_record(work_record);
}
self.storage.save(&day_data)?;
self.storage.clear_active_timer()?;
let work_record = self.to_work_record(timer)?;
Ok(work_record)
}
pub fn pause(&self) -> Result<TimerState> {
let mut timer = self
.storage
.load_active_timer()?
.ok_or_else(|| anyhow!("No timer is currently running"))?;
if timer.status == TimerStatus::Paused {
return Err(anyhow!("Timer is already paused"));
}
if timer.status != TimerStatus::Running {
return Err(anyhow!("Can only pause a running timer"));
}
let now = OffsetDateTime::now_local()
.context("Failed to get local time. System clock may not be configured correctly.")?;
timer.paused_at = Some(now);
timer.status = TimerStatus::Paused;
timer.updated_at = now;
self.storage.save_active_timer(&timer)?;
Ok(timer)
}
pub fn resume(&self) -> Result<TimerState> {
let mut timer = self
.storage
.load_active_timer()?
.ok_or_else(|| anyhow!("No timer is currently running"))?;
if timer.status != TimerStatus::Paused {
return Err(anyhow!("Can only resume a paused timer"));
}
let now = OffsetDateTime::now_local()
.context("Failed to get local time. System clock may not be configured correctly.")?;
if let Some(paused_at) = timer.paused_at {
let pause_duration = (now - paused_at).whole_seconds();
timer.paused_duration_secs += pause_duration;
}
timer.paused_at = None;
timer.status = TimerStatus::Running;
timer.updated_at = now;
self.storage.save_active_timer(&timer)?;
Ok(timer)
}
pub fn status(&self) -> Result<Option<TimerState>> {
self.storage.load_active_timer()
}
pub fn get_elapsed_duration(&self, timer: &TimerState) -> StdDuration {
let end_point = if timer.status == TimerStatus::Paused {
timer.paused_at.unwrap_or_else(|| {
OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc())
})
} else {
OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc())
};
let elapsed = end_point - timer.start_time;
let paused_duration_std = StdDuration::from_secs(timer.paused_duration_secs as u64);
let elapsed_std = StdDuration::from_secs(elapsed.whole_seconds() as u64)
+ StdDuration::from_nanos(elapsed.subsec_nanoseconds() as u64);
elapsed_std
.checked_sub(paused_duration_std)
.unwrap_or(StdDuration::ZERO)
}
fn to_work_record(&self, timer: TimerState) -> Result<WorkRecord> {
if timer.status != TimerStatus::Stopped {
return Err(anyhow!("Can only convert stopped timers to WorkRecord"));
}
let start_time = timer.start_time;
let end_time = timer
.end_time
.ok_or_else(|| anyhow!("Stopped timer must have end_time"))?;
let start_timepoint = TimePoint::new(start_time.hour(), start_time.minute())
.map_err(|e| anyhow!(e))
.context("Failed to create TimePoint for timer start time")?;
let end_timepoint = TimePoint::new(end_time.hour(), end_time.minute())
.map_err(|e| anyhow!(e))
.context("Failed to create TimePoint for timer end time")?;
let mut record = WorkRecord::new(
1, timer.task_name,
start_timepoint,
end_timepoint,
);
if let Some(description) = timer.description {
record.description = description;
}
if let Some(project) = timer.project {
record.project = project;
}
if let Some(customer) = timer.customer {
record.customer = customer;
}
Ok(record)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_storage() -> (Storage, TempDir) {
let temp_dir = TempDir::new().unwrap();
let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
(storage, temp_dir)
}
#[test]
fn test_timer_state_creation() {
let now = OffsetDateTime::now_utc();
let timer = TimerState {
id: None,
task_name: "Test Task".to_string(),
description: None,
project: None,
customer: None,
start_time: now,
end_time: None,
date: now.date(),
status: TimerStatus::Running,
paused_duration_secs: 0,
paused_at: None,
created_at: now,
updated_at: now,
source_record_id: None,
source_record_date: None,
};
assert_eq!(timer.task_name, "Test Task");
assert_eq!(timer.status, TimerStatus::Running);
assert_eq!(timer.paused_duration_secs, 0);
}
#[test]
fn test_timer_serialization() {
let now = OffsetDateTime::now_utc();
let timer = TimerState {
id: None,
task_name: "Test Task".to_string(),
description: Some("Test description".to_string()),
project: Some("Platform".to_string()),
customer: Some("ACME".to_string()),
start_time: now,
end_time: None,
date: now.date(),
status: TimerStatus::Running,
paused_duration_secs: 0,
paused_at: None,
created_at: now,
updated_at: now,
source_record_id: None,
source_record_date: None,
};
let json = serde_json::to_string(&timer).unwrap();
let deserialized: TimerState = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.task_name, timer.task_name);
assert_eq!(deserialized.status, timer.status);
}
#[test]
fn test_start_timer() {
let (storage, _temp) = create_test_storage();
let manager = TimerManager::new(storage);
let result = manager.start("Work".to_string(), None, None, None, None, None);
assert!(result.is_ok());
let timer = result.unwrap();
assert_eq!(timer.task_name, "Work");
assert_eq!(timer.status, TimerStatus::Running);
assert_eq!(timer.paused_duration_secs, 0);
}
#[test]
fn test_cannot_start_when_already_running() {
let (storage, _temp) = create_test_storage();
let manager = TimerManager::new(storage);
let _ = manager.start("Task 1".to_string(), None, None, None, None, None);
let result = manager.start("Task 2".to_string(), None, None, None, None, None);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"A timer is already running"
);
}
#[test]
fn test_pause_running_timer() {
let (storage, _temp) = create_test_storage();
let manager = TimerManager::new(storage);
let _ = manager.start("Work".to_string(), None, None, None, None, None);
let result = manager.pause();
assert!(result.is_ok());
let timer = result.unwrap();
assert_eq!(timer.status, TimerStatus::Paused);
assert!(timer.paused_at.is_some());
}
#[test]
fn test_cannot_pause_paused_timer() {
let (storage, _temp) = create_test_storage();
let manager = TimerManager::new(storage);
let _ = manager.start("Work".to_string(), None, None, None, None, None);
let _ = manager.pause();
let result = manager.pause();
assert!(result.is_err());
}
#[test]
fn test_pause_without_running_timer() {
let (storage, _temp) = create_test_storage();
let manager = TimerManager::new(storage);
let result = manager.pause();
assert!(result.is_err());
}
#[test]
fn test_resume_paused_timer() {
let (storage, _temp) = create_test_storage();
let manager = TimerManager::new(storage);
let _ = manager.start("Work".to_string(), None, None, None, None, None);
let _ = manager.pause();
let result = manager.resume();
assert!(result.is_ok());
let timer = result.unwrap();
assert_eq!(timer.status, TimerStatus::Running);
assert!(timer.paused_at.is_none());
}
#[test]
fn test_resume_updates_paused_duration() {
let (storage, _temp) = create_test_storage();
let manager = TimerManager::new(storage);
let _ = manager.start("Work".to_string(), None, None, None, None, None);
let paused1 = manager.pause().unwrap();
assert_eq!(paused1.paused_duration_secs, 0);
let _ = manager.resume();
let paused2 = manager.pause().unwrap();
assert!(paused2.paused_duration_secs >= 0);
}
#[test]
fn test_cannot_resume_running_timer() {
let (storage, _temp) = create_test_storage();
let manager = TimerManager::new(storage);
let _ = manager.start("Work".to_string(), None, None, None, None, None);
let result = manager.resume();
assert!(result.is_err());
}
#[test]
fn test_status_returns_none_when_no_timer() {
let (storage, _temp) = create_test_storage();
let manager = TimerManager::new(storage);
let result = manager.status().unwrap();
assert!(result.is_none());
}
#[test]
fn test_status_returns_running_timer() {
let (storage, _temp) = create_test_storage();
let manager = TimerManager::new(storage);
let _ = manager.start("Work".to_string(), None, None, None, None, None);
let result = manager.status().unwrap();
assert!(result.is_some());
let timer = result.unwrap();
assert_eq!(timer.task_name, "Work");
assert_eq!(timer.status, TimerStatus::Running);
}
#[test]
fn test_stop_running_timer() {
let (storage, _temp) = create_test_storage();
let manager = TimerManager::new(storage);
let _ = manager.start("Work".to_string(), None, None, None, None, None);
let result = manager.stop();
assert!(result.is_ok());
let work_record = result.unwrap();
assert_eq!(work_record.name, "Work");
let timer_status = manager.status().unwrap();
assert!(timer_status.is_none());
}
#[test]
fn test_cannot_stop_without_running_timer() {
let (storage, _temp) = create_test_storage();
let manager = TimerManager::new(storage);
let result = manager.stop();
assert!(result.is_err());
}
#[test]
fn test_stop_returns_work_record_with_description() {
let (storage, _temp) = create_test_storage();
let manager = TimerManager::new(storage);
let _ = manager.start(
"Work".to_string(),
Some("Important task".to_string()),
None,
None,
None,
None,
);
let work_record = manager.stop().unwrap();
assert_eq!(work_record.name, "Work");
assert_eq!(work_record.description, "Important task");
}
#[test]
fn test_full_timer_lifecycle() {
let (storage, _temp) = create_test_storage();
let manager = TimerManager::new(storage);
let started = manager
.start("Task".to_string(), None, None, None, None, None)
.unwrap();
assert_eq!(started.status, TimerStatus::Running);
let paused = manager.pause().unwrap();
assert_eq!(paused.status, TimerStatus::Paused);
let resumed = manager.resume().unwrap();
assert_eq!(resumed.status, TimerStatus::Running);
let paused_again = manager.pause().unwrap();
assert_eq!(paused_again.status, TimerStatus::Paused);
let resumed_again = manager.resume().unwrap();
assert_eq!(resumed_again.status, TimerStatus::Running);
let work_record = manager.stop().unwrap();
assert_eq!(work_record.name, "Task");
let status = manager.status().unwrap();
assert!(status.is_none());
}
#[test]
fn test_get_elapsed_duration_running() {
let (storage, _temp) = create_test_storage();
let manager = TimerManager::new(storage);
let timer = manager
.start("Task".to_string(), None, None, None, None, None)
.unwrap();
let elapsed = manager.get_elapsed_duration(&timer);
assert!(elapsed.as_secs() < 2);
}
#[test]
fn test_get_elapsed_duration_with_pause() {
let (storage, _temp) = create_test_storage();
let manager = TimerManager::new(storage);
let _ = manager.start("Task".to_string(), None, None, None, None, None);
let _ = manager.pause();
let timer = manager.status().unwrap().unwrap();
let elapsed = manager.get_elapsed_duration(&timer);
assert!(elapsed.as_secs() < 2);
}
#[test]
fn test_stop_updates_existing_record() {
use crate::models::DayData;
use crate::models::TimePoint;
use crate::models::WorkRecord;
use tempfile::TempDir;
use time::OffsetDateTime;
let temp_dir = TempDir::new().unwrap();
let storage_path = temp_dir.path().to_path_buf();
let now = OffsetDateTime::now_utc();
let today = now.date();
let mut day_data = DayData::new(today);
let record = WorkRecord::new(
1,
"Existing Task".to_string(),
TimePoint::new(9, 0).unwrap(),
TimePoint::new(10, 0).unwrap(),
);
day_data.add_record(record);
let storage1 = Storage::new_with_dir(storage_path.clone()).unwrap();
storage1.save(&day_data).unwrap();
let manager = TimerManager::new(storage1);
manager
.start(
"Existing Task".to_string(),
None,
None,
None,
Some(1),
Some(today),
)
.unwrap();
manager.stop().unwrap();
let storage2 = Storage::new_with_dir(storage_path).unwrap();
let updated_day_data = storage2.load(&today).unwrap();
assert_eq!(updated_day_data.work_records.len(), 1);
let updated_record = updated_day_data.work_records.get(&1).unwrap();
assert_eq!(updated_record.name, "Existing Task");
assert!(
updated_record.end.hour != 10 || updated_record.end.minute != 0,
"End time should have been updated from original 10:00, got {:02}:{:02}",
updated_record.end.hour,
updated_record.end.minute
);
}
#[test]
fn test_stop_returns_work_record_with_project_and_customer() {
let (storage, _temp) = create_test_storage();
let manager = TimerManager::new(storage);
let _ = manager.start(
"Work".to_string(),
None,
Some("Platform Revamp".to_string()),
Some("ACME".to_string()),
None,
None,
);
let work_record = manager.stop().unwrap();
assert_eq!(work_record.name, "Work");
assert_eq!(work_record.project, "Platform Revamp");
assert_eq!(work_record.customer, "ACME");
}
}