tempo_cli/models/
goal.rs

1use chrono::{DateTime, NaiveDate, Utc};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
5pub struct Goal {
6    pub id: Option<i64>,
7    pub project_id: Option<i64>,
8    pub name: String,
9    pub description: Option<String>,
10    pub target_hours: f64,
11    pub start_date: Option<NaiveDate>,
12    pub end_date: Option<NaiveDate>,
13    pub current_progress: f64,
14    pub status: GoalStatus,
15    pub created_at: DateTime<Utc>,
16    pub updated_at: DateTime<Utc>,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20pub enum GoalStatus {
21    Active,
22    Completed,
23    Paused,
24    Cancelled,
25}
26
27impl std::fmt::Display for GoalStatus {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        match self {
30            GoalStatus::Active => write!(f, "active"),
31            GoalStatus::Completed => write!(f, "completed"),
32            GoalStatus::Paused => write!(f, "paused"),
33            GoalStatus::Cancelled => write!(f, "cancelled"),
34        }
35    }
36}
37
38impl std::str::FromStr for GoalStatus {
39    type Err = anyhow::Error;
40
41    fn from_str(s: &str) -> Result<Self, Self::Err> {
42        match s.to_lowercase().as_str() {
43            "active" => Ok(GoalStatus::Active),
44            "completed" => Ok(GoalStatus::Completed),
45            "paused" => Ok(GoalStatus::Paused),
46            "cancelled" => Ok(GoalStatus::Cancelled),
47            _ => Err(anyhow::anyhow!("Invalid goal status: {}", s)),
48        }
49    }
50}
51
52impl Goal {
53    pub fn new(name: String, target_hours: f64) -> Self {
54        let now = Utc::now();
55        Self {
56            id: None,
57            project_id: None,
58            name,
59            description: None,
60            target_hours,
61            start_date: None,
62            end_date: None,
63            current_progress: 0.0,
64            status: GoalStatus::Active,
65            created_at: now,
66            updated_at: now,
67        }
68    }
69
70    pub fn with_project(mut self, project_id: i64) -> Self {
71        self.project_id = Some(project_id);
72        self
73    }
74
75    pub fn with_description(mut self, description: String) -> Self {
76        self.description = Some(description);
77        self
78    }
79
80    pub fn with_dates(mut self, start: Option<NaiveDate>, end: Option<NaiveDate>) -> Self {
81        self.start_date = start;
82        self.end_date = end;
83        self
84    }
85
86    pub fn progress_percentage(&self) -> f64 {
87        if self.target_hours == 0.0 {
88            return 0.0;
89        }
90        (self.current_progress / self.target_hours * 100.0).min(100.0)
91    }
92
93    pub fn is_completed(&self) -> bool {
94        self.status == GoalStatus::Completed || self.current_progress >= self.target_hours
95    }
96
97    pub fn remaining_hours(&self) -> f64 {
98        (self.target_hours - self.current_progress).max(0.0)
99    }
100
101    pub fn update_progress(&mut self, hours: f64) {
102        self.current_progress += hours;
103        self.updated_at = Utc::now();
104        
105        if self.current_progress >= self.target_hours && self.status == GoalStatus::Active {
106            self.status = GoalStatus::Completed;
107        }
108    }
109
110    pub fn validate(&self) -> anyhow::Result<()> {
111        if self.name.is_empty() {
112            return Err(anyhow::anyhow!("Goal name cannot be empty"));
113        }
114        
115        if self.target_hours <= 0.0 {
116            return Err(anyhow::anyhow!("Target hours must be greater than 0"));
117        }
118        
119        if let (Some(start), Some(end)) = (self.start_date, self.end_date) {
120            if start > end {
121                return Err(anyhow::anyhow!("Start date must be before end date"));
122            }
123        }
124        
125        if self.current_progress < 0.0 {
126            return Err(anyhow::anyhow!("Current progress cannot be negative"));
127        }
128        
129        Ok(())
130    }
131}
132