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