1use 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#[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
28pub struct TimerState {
29 pub id: Option<u32>,
31
32 pub task_name: String,
34
35 pub description: Option<String>,
37
38 #[serde(default)]
40 pub project: Option<String>,
41
42 #[serde(default)]
44 pub customer: Option<String>,
45
46 pub start_time: OffsetDateTime,
48
49 pub end_time: Option<OffsetDateTime>,
51
52 pub date: Date,
54
55 pub status: TimerStatus,
57
58 pub paused_duration_secs: i64,
60
61 pub paused_at: Option<OffsetDateTime>,
63
64 pub created_at: OffsetDateTime,
66
67 pub updated_at: OffsetDateTime,
69
70 #[serde(default)]
73 pub source_record_id: Option<u32>,
74
75 #[serde(default)]
78 pub source_record_date: Option<Date>,
79}
80
81pub struct TimerManager {
86 storage: Storage,
87}
88
89impl TimerManager {
90 pub fn new(storage: Storage) -> Self {
93 TimerManager { storage }
94 }
95
96 pub fn start(
101 &self,
102 task_name: String,
103 description: Option<String>,
104 project: Option<String>,
105 customer: Option<String>,
106 source_record_id: Option<u32>,
107 source_record_date: Option<Date>,
108 ) -> Result<TimerState> {
109 if (self.storage.load_active_timer()?).is_some() {
111 return Err(anyhow!("A timer is already running"));
112 }
113
114 let now = OffsetDateTime::now_local()
115 .context("Failed to get local time. System clock may not be configured correctly.")?;
116 let timer = TimerState {
117 id: None,
118 task_name,
119 description,
120 project,
121 customer,
122 start_time: now,
123 end_time: None,
124 date: now.date(),
125 status: TimerStatus::Running,
126 paused_duration_secs: 0,
127 paused_at: None,
128 created_at: now,
129 updated_at: now,
130 source_record_id,
131 source_record_date,
132 };
133
134 self.storage.save_active_timer(&timer)?;
135 Ok(timer)
136 }
137
138 pub fn stop(&self) -> Result<WorkRecord> {
143 let mut timer = self
144 .storage
145 .load_active_timer()?
146 .ok_or_else(|| anyhow!("No timer is currently running"))?;
147
148 let now = OffsetDateTime::now_local()
149 .context("Failed to get local time. System clock may not be configured correctly.")?;
150
151 let target_date = timer
155 .source_record_date
156 .unwrap_or_else(|| timer.start_time.date());
157
158 timer.end_time = Some(now);
159 timer.status = TimerStatus::Stopped;
160 timer.updated_at = now;
161
162 let mut day_data = self.storage.load(&target_date)?;
164
165 if let Some(source_id) = timer.source_record_id {
168 if let Some(record) = day_data.work_records.get_mut(&source_id) {
170 let end_timepoint = TimePoint::new(now.hour(), now.minute())
172 .map_err(|e| anyhow!(e))
173 .context("Failed to create TimePoint for timer end time")?;
174 record.end = end_timepoint;
175 record.update_duration();
176 } else {
177 let mut work_record = self.to_work_record(timer.clone())?;
179 work_record.id = day_data.next_id();
181 day_data.add_record(work_record);
182 }
183 } else {
184 let mut work_record = self.to_work_record(timer.clone())?;
186 work_record.id = day_data.next_id();
188 day_data.add_record(work_record);
189 }
190
191 self.storage.save(&day_data)?;
192 self.storage.clear_active_timer()?;
193
194 let work_record = self.to_work_record(timer)?;
196 Ok(work_record)
197 }
198
199 pub fn pause(&self) -> Result<TimerState> {
204 let mut timer = self
205 .storage
206 .load_active_timer()?
207 .ok_or_else(|| anyhow!("No timer is currently running"))?;
208
209 if timer.status == TimerStatus::Paused {
210 return Err(anyhow!("Timer is already paused"));
211 }
212
213 if timer.status != TimerStatus::Running {
214 return Err(anyhow!("Can only pause a running timer"));
215 }
216
217 let now = OffsetDateTime::now_local()
218 .context("Failed to get local time. System clock may not be configured correctly.")?;
219 timer.paused_at = Some(now);
220 timer.status = TimerStatus::Paused;
221 timer.updated_at = now;
222
223 self.storage.save_active_timer(&timer)?;
224 Ok(timer)
225 }
226
227 pub fn resume(&self) -> Result<TimerState> {
232 let mut timer = self
233 .storage
234 .load_active_timer()?
235 .ok_or_else(|| anyhow!("No timer is currently running"))?;
236
237 if timer.status != TimerStatus::Paused {
238 return Err(anyhow!("Can only resume a paused timer"));
239 }
240
241 let now = OffsetDateTime::now_local()
242 .context("Failed to get local time. System clock may not be configured correctly.")?;
243
244 if let Some(paused_at) = timer.paused_at {
246 let pause_duration = (now - paused_at).whole_seconds();
247 timer.paused_duration_secs += pause_duration;
248 }
249
250 timer.paused_at = None;
251 timer.status = TimerStatus::Running;
252 timer.updated_at = now;
253
254 self.storage.save_active_timer(&timer)?;
255 Ok(timer)
256 }
257
258 pub fn status(&self) -> Result<Option<TimerState>> {
262 self.storage.load_active_timer()
263 }
264
265 pub fn get_elapsed_duration(&self, timer: &TimerState) -> StdDuration {
269 let end_point = if timer.status == TimerStatus::Paused {
270 timer.paused_at.unwrap_or_else(|| {
272 OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc())
273 })
274 } else {
275 OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc())
277 };
278
279 let elapsed = end_point - timer.start_time;
280 let paused_duration_std = StdDuration::from_secs(timer.paused_duration_secs as u64);
281
282 let elapsed_std = StdDuration::from_secs(elapsed.whole_seconds() as u64)
284 + StdDuration::from_nanos(elapsed.subsec_nanoseconds() as u64);
285
286 elapsed_std
287 .checked_sub(paused_duration_std)
288 .unwrap_or(StdDuration::ZERO)
289 }
290
291 fn to_work_record(&self, timer: TimerState) -> Result<WorkRecord> {
293 if timer.status != TimerStatus::Stopped {
294 return Err(anyhow!("Can only convert stopped timers to WorkRecord"));
295 }
296
297 let start_time = timer.start_time;
298 let end_time = timer
299 .end_time
300 .ok_or_else(|| anyhow!("Stopped timer must have end_time"))?;
301
302 let start_timepoint = TimePoint::new(start_time.hour(), start_time.minute())
304 .map_err(|e| anyhow!(e))
305 .context("Failed to create TimePoint for timer start time")?;
306
307 let end_timepoint = TimePoint::new(end_time.hour(), end_time.minute())
308 .map_err(|e| anyhow!(e))
309 .context("Failed to create TimePoint for timer end time")?;
310
311 let mut record = WorkRecord::new(
312 1, timer.task_name,
314 start_timepoint,
315 end_timepoint,
316 );
317
318 if let Some(description) = timer.description {
319 record.description = description;
320 }
321
322 if let Some(project) = timer.project {
323 record.project = project;
324 }
325
326 if let Some(customer) = timer.customer {
327 record.customer = customer;
328 }
329
330 Ok(record)
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use tempfile::TempDir;
338
339 fn create_test_storage() -> (Storage, TempDir) {
340 let temp_dir = TempDir::new().unwrap();
341 let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
342 (storage, temp_dir)
343 }
344
345 #[test]
346 fn test_timer_state_creation() {
347 let now = OffsetDateTime::now_utc();
348 let timer = TimerState {
349 id: None,
350 task_name: "Test Task".to_string(),
351 description: None,
352 project: None,
353 customer: None,
354 start_time: now,
355 end_time: None,
356 date: now.date(),
357 status: TimerStatus::Running,
358 paused_duration_secs: 0,
359 paused_at: None,
360 created_at: now,
361 updated_at: now,
362 source_record_id: None,
363 source_record_date: None,
364 };
365
366 assert_eq!(timer.task_name, "Test Task");
367 assert_eq!(timer.status, TimerStatus::Running);
368 assert_eq!(timer.paused_duration_secs, 0);
369 }
370
371 #[test]
372 fn test_timer_serialization() {
373 let now = OffsetDateTime::now_utc();
374 let timer = TimerState {
375 id: None,
376 task_name: "Test Task".to_string(),
377 description: Some("Test description".to_string()),
378 project: Some("Platform".to_string()),
379 customer: Some("ACME".to_string()),
380 start_time: now,
381 end_time: None,
382 date: now.date(),
383 status: TimerStatus::Running,
384 paused_duration_secs: 0,
385 paused_at: None,
386 created_at: now,
387 updated_at: now,
388 source_record_id: None,
389 source_record_date: None,
390 };
391
392 let json = serde_json::to_string(&timer).unwrap();
393 let deserialized: TimerState = serde_json::from_str(&json).unwrap();
394
395 assert_eq!(deserialized.task_name, timer.task_name);
396 assert_eq!(deserialized.status, timer.status);
397 }
398
399 #[test]
400 fn test_start_timer() {
401 let (storage, _temp) = create_test_storage();
402 let manager = TimerManager::new(storage);
403
404 let result = manager.start("Work".to_string(), None, None, None, None, None);
405 assert!(result.is_ok());
406
407 let timer = result.unwrap();
408 assert_eq!(timer.task_name, "Work");
409 assert_eq!(timer.status, TimerStatus::Running);
410 assert_eq!(timer.paused_duration_secs, 0);
411 }
412
413 #[test]
414 fn test_cannot_start_when_already_running() {
415 let (storage, _temp) = create_test_storage();
416 let manager = TimerManager::new(storage);
417
418 let _ = manager.start("Task 1".to_string(), None, None, None, None, None);
419 let result = manager.start("Task 2".to_string(), None, None, None, None, None);
420
421 assert!(result.is_err());
422 assert_eq!(
423 result.unwrap_err().to_string(),
424 "A timer is already running"
425 );
426 }
427
428 #[test]
429 fn test_pause_running_timer() {
430 let (storage, _temp) = create_test_storage();
431 let manager = TimerManager::new(storage);
432
433 let _ = manager.start("Work".to_string(), None, None, None, None, None);
434 let result = manager.pause();
435
436 assert!(result.is_ok());
437 let timer = result.unwrap();
438 assert_eq!(timer.status, TimerStatus::Paused);
439 assert!(timer.paused_at.is_some());
440 }
441
442 #[test]
443 fn test_cannot_pause_paused_timer() {
444 let (storage, _temp) = create_test_storage();
445 let manager = TimerManager::new(storage);
446
447 let _ = manager.start("Work".to_string(), None, None, None, None, None);
448 let _ = manager.pause();
449 let result = manager.pause();
450
451 assert!(result.is_err());
452 }
453
454 #[test]
455 fn test_pause_without_running_timer() {
456 let (storage, _temp) = create_test_storage();
457 let manager = TimerManager::new(storage);
458
459 let result = manager.pause();
460 assert!(result.is_err());
461 }
462
463 #[test]
464 fn test_resume_paused_timer() {
465 let (storage, _temp) = create_test_storage();
466 let manager = TimerManager::new(storage);
467
468 let _ = manager.start("Work".to_string(), None, None, None, None, None);
469 let _ = manager.pause();
470 let result = manager.resume();
471
472 assert!(result.is_ok());
473 let timer = result.unwrap();
474 assert_eq!(timer.status, TimerStatus::Running);
475 assert!(timer.paused_at.is_none());
476 }
477
478 #[test]
479 fn test_resume_updates_paused_duration() {
480 let (storage, _temp) = create_test_storage();
481 let manager = TimerManager::new(storage);
482
483 let _ = manager.start("Work".to_string(), None, None, None, None, None);
484 let paused1 = manager.pause().unwrap();
485 assert_eq!(paused1.paused_duration_secs, 0);
486
487 let _ = manager.resume();
489 let paused2 = manager.pause().unwrap();
490
491 assert!(paused2.paused_duration_secs >= 0);
493 }
494
495 #[test]
496 fn test_cannot_resume_running_timer() {
497 let (storage, _temp) = create_test_storage();
498 let manager = TimerManager::new(storage);
499
500 let _ = manager.start("Work".to_string(), None, None, None, None, None);
501 let result = manager.resume();
502
503 assert!(result.is_err());
504 }
505
506 #[test]
507 fn test_status_returns_none_when_no_timer() {
508 let (storage, _temp) = create_test_storage();
509 let manager = TimerManager::new(storage);
510
511 let result = manager.status().unwrap();
512 assert!(result.is_none());
513 }
514
515 #[test]
516 fn test_status_returns_running_timer() {
517 let (storage, _temp) = create_test_storage();
518 let manager = TimerManager::new(storage);
519
520 let _ = manager.start("Work".to_string(), None, None, None, None, None);
521 let result = manager.status().unwrap();
522
523 assert!(result.is_some());
524 let timer = result.unwrap();
525 assert_eq!(timer.task_name, "Work");
526 assert_eq!(timer.status, TimerStatus::Running);
527 }
528
529 #[test]
530 fn test_stop_running_timer() {
531 let (storage, _temp) = create_test_storage();
532 let manager = TimerManager::new(storage);
533
534 let _ = manager.start("Work".to_string(), None, None, None, None, None);
535 let result = manager.stop();
536
537 assert!(result.is_ok());
538 let work_record = result.unwrap();
539 assert_eq!(work_record.name, "Work");
540
541 let timer_status = manager.status().unwrap();
543 assert!(timer_status.is_none());
544 }
545
546 #[test]
547 fn test_cannot_stop_without_running_timer() {
548 let (storage, _temp) = create_test_storage();
549 let manager = TimerManager::new(storage);
550
551 let result = manager.stop();
552 assert!(result.is_err());
553 }
554
555 #[test]
556 fn test_stop_returns_work_record_with_description() {
557 let (storage, _temp) = create_test_storage();
558 let manager = TimerManager::new(storage);
559
560 let _ = manager.start(
561 "Work".to_string(),
562 Some("Important task".to_string()),
563 None,
564 None,
565 None,
566 None,
567 );
568 let work_record = manager.stop().unwrap();
569
570 assert_eq!(work_record.name, "Work");
571 assert_eq!(work_record.description, "Important task");
572 }
573
574 #[test]
575 fn test_full_timer_lifecycle() {
576 let (storage, _temp) = create_test_storage();
577 let manager = TimerManager::new(storage);
578
579 let started = manager
581 .start("Task".to_string(), None, None, None, None, None)
582 .unwrap();
583 assert_eq!(started.status, TimerStatus::Running);
584
585 let paused = manager.pause().unwrap();
587 assert_eq!(paused.status, TimerStatus::Paused);
588
589 let resumed = manager.resume().unwrap();
591 assert_eq!(resumed.status, TimerStatus::Running);
592
593 let paused_again = manager.pause().unwrap();
595 assert_eq!(paused_again.status, TimerStatus::Paused);
596
597 let resumed_again = manager.resume().unwrap();
599 assert_eq!(resumed_again.status, TimerStatus::Running);
600
601 let work_record = manager.stop().unwrap();
603 assert_eq!(work_record.name, "Task");
604
605 let status = manager.status().unwrap();
607 assert!(status.is_none());
608 }
609
610 #[test]
611 fn test_get_elapsed_duration_running() {
612 let (storage, _temp) = create_test_storage();
613 let manager = TimerManager::new(storage);
614
615 let timer = manager
616 .start("Task".to_string(), None, None, None, None, None)
617 .unwrap();
618 let elapsed = manager.get_elapsed_duration(&timer);
619
620 assert!(elapsed.as_secs() < 2);
622 }
623
624 #[test]
625 fn test_get_elapsed_duration_with_pause() {
626 let (storage, _temp) = create_test_storage();
627 let manager = TimerManager::new(storage);
628
629 let _ = manager.start("Task".to_string(), None, None, None, None, None);
630 let _ = manager.pause();
631
632 let timer = manager.status().unwrap().unwrap();
633 let elapsed = manager.get_elapsed_duration(&timer);
634
635 assert!(elapsed.as_secs() < 2);
637 }
638
639 #[test]
640 fn test_stop_updates_existing_record() {
641 use crate::models::DayData;
642 use crate::models::TimePoint;
643 use crate::models::WorkRecord;
644 use tempfile::TempDir;
645 use time::OffsetDateTime;
646
647 let temp_dir = TempDir::new().unwrap();
649 let storage_path = temp_dir.path().to_path_buf();
650
651 let now = OffsetDateTime::now_utc();
653 let today = now.date();
654 let mut day_data = DayData::new(today);
655
656 let record = WorkRecord::new(
657 1,
658 "Existing Task".to_string(),
659 TimePoint::new(9, 0).unwrap(),
660 TimePoint::new(10, 0).unwrap(),
661 );
662 day_data.add_record(record);
663
664 let storage1 = Storage::new_with_dir(storage_path.clone()).unwrap();
666 storage1.save(&day_data).unwrap();
667
668 let manager = TimerManager::new(storage1);
670 manager
671 .start(
672 "Existing Task".to_string(),
673 None,
674 None,
675 None,
676 Some(1),
677 Some(today),
678 )
679 .unwrap();
680
681 manager.stop().unwrap();
683
684 let storage2 = Storage::new_with_dir(storage_path).unwrap();
686 let updated_day_data = storage2.load(&today).unwrap();
687
688 assert_eq!(updated_day_data.work_records.len(), 1);
690
691 let updated_record = updated_day_data.work_records.get(&1).unwrap();
693 assert_eq!(updated_record.name, "Existing Task");
694 assert!(
698 updated_record.end.hour != 10 || updated_record.end.minute != 0,
699 "End time should have been updated from original 10:00, got {:02}:{:02}",
700 updated_record.end.hour,
701 updated_record.end.minute
702 );
703 }
704
705 #[test]
706 fn test_stop_returns_work_record_with_project_and_customer() {
707 let (storage, _temp) = create_test_storage();
708 let manager = TimerManager::new(storage);
709
710 let _ = manager.start(
711 "Work".to_string(),
712 None,
713 Some("Platform Revamp".to_string()),
714 Some("ACME".to_string()),
715 None,
716 None,
717 );
718 let work_record = manager.stop().unwrap();
719
720 assert_eq!(work_record.name, "Work");
721 assert_eq!(work_record.project, "Platform Revamp");
722 assert_eq!(work_record.customer, "ACME");
723 }
724}