1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum PlanStatus {
12 Draft,
13 Active,
14 Completed,
15}
16
17impl PlanStatus {
18 #[must_use]
20 pub const fn as_str(&self) -> &'static str {
21 match self {
22 Self::Draft => "draft",
23 Self::Active => "active",
24 Self::Completed => "completed",
25 }
26 }
27
28 #[must_use]
30 pub fn from_str(s: &str) -> Self {
31 match s.to_lowercase().as_str() {
32 "active" => Self::Active,
33 "completed" => Self::Completed,
34 _ => Self::Draft,
35 }
36 }
37}
38
39impl Default for PlanStatus {
40 fn default() -> Self {
41 Self::Draft
42 }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct Plan {
54 pub id: String,
56
57 pub short_id: Option<String>,
59
60 pub project_id: String,
62
63 pub project_path: String,
65
66 pub title: String,
68
69 pub content: Option<String>,
71
72 pub status: PlanStatus,
74
75 pub success_criteria: Option<String>,
77
78 pub session_id: Option<String>,
80
81 pub created_in_session: Option<String>,
83
84 pub completed_in_session: Option<String>,
86
87 pub source_path: Option<String>,
89
90 pub source_hash: Option<String>,
92
93 pub created_at: i64,
95
96 pub updated_at: i64,
98
99 pub completed_at: Option<i64>,
101}
102
103impl Plan {
104 pub fn new(project_id: String, project_path: String, title: String) -> Self {
106 let now = chrono::Utc::now().timestamp_millis();
107 let id = format!("plan_{}", &uuid::Uuid::new_v4().to_string()[..12]);
108
109 Self {
110 id,
111 short_id: None,
112 project_id,
113 project_path,
114 title,
115 content: None,
116 status: PlanStatus::Draft,
117 success_criteria: None,
118 session_id: None,
119 created_in_session: None,
120 completed_in_session: None,
121 source_path: None,
122 source_hash: None,
123 created_at: now,
124 updated_at: now,
125 completed_at: None,
126 }
127 }
128
129 #[must_use]
131 pub fn with_content(mut self, content: &str) -> Self {
132 self.content = Some(content.to_string());
133 self
134 }
135
136 #[must_use]
138 pub fn with_status(mut self, status: PlanStatus) -> Self {
139 self.status = status;
140 self
141 }
142
143 #[must_use]
145 pub fn with_success_criteria(mut self, criteria: &str) -> Self {
146 self.success_criteria = Some(criteria.to_string());
147 self
148 }
149
150 #[must_use]
152 pub fn with_session(mut self, session_id: &str) -> Self {
153 self.session_id = Some(session_id.to_string());
154 self
155 }
156
157 #[must_use]
159 pub fn with_source(mut self, path: &str, hash: &str) -> Self {
160 self.source_path = Some(path.to_string());
161 self.source_hash = Some(hash.to_string());
162 self
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_new_plan() {
172 let plan = Plan::new(
173 "proj_123".to_string(),
174 "/home/user/myproject".to_string(),
175 "Authentication System".to_string(),
176 );
177
178 assert!(plan.id.starts_with("plan_"));
179 assert_eq!(plan.project_id, "proj_123");
180 assert_eq!(plan.title, "Authentication System");
181 assert_eq!(plan.status, PlanStatus::Draft);
182 }
183
184 #[test]
185 fn test_plan_status_parsing() {
186 assert_eq!(PlanStatus::from_str("draft"), PlanStatus::Draft);
187 assert_eq!(PlanStatus::from_str("active"), PlanStatus::Active);
188 assert_eq!(PlanStatus::from_str("completed"), PlanStatus::Completed);
189 assert_eq!(PlanStatus::from_str("unknown"), PlanStatus::Draft);
190 }
191}