tempo_cli/models/
project.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
6pub struct Project {
7 pub id: Option<i64>,
8 pub name: String,
9 pub path: PathBuf,
10 pub git_hash: Option<String>,
11 pub created_at: DateTime<Utc>,
12 pub updated_at: DateTime<Utc>,
13 pub is_archived: bool,
14 pub description: Option<String>,
15}
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub enum ProjectStatus {
19 Active,
20 Archived,
21 Tracking,
22 Idle,
23}
24
25impl Project {
26 pub fn new(name: String, path: PathBuf) -> Self {
27 let now = Utc::now();
28 Self {
29 id: None,
30 name,
31 path,
32 git_hash: None,
33 created_at: now,
34 updated_at: now,
35 is_archived: false,
36 description: None,
37 }
38 }
39
40 pub fn with_git_hash(mut self, git_hash: Option<String>) -> Self {
41 self.git_hash = git_hash;
42 self
43 }
44
45 pub fn with_description(mut self, description: Option<String>) -> Self {
46 self.description = description;
47 self
48 }
49
50 pub fn archive(&mut self) {
51 self.is_archived = true;
52 self.updated_at = Utc::now();
53 }
54
55 pub fn unarchive(&mut self) {
56 self.is_archived = false;
57 self.updated_at = Utc::now();
58 }
59
60 pub fn update_path(&mut self, new_path: PathBuf) {
61 self.path = new_path;
62 self.updated_at = Utc::now();
63 }
64
65 pub fn is_git_project(&self) -> bool {
66 self.path.join(".git").exists()
67 }
68
69 pub fn has_timetrack_marker(&self) -> bool {
70 self.path.join(".timetrack").exists()
71 }
72
73 pub fn get_canonical_path(&self) -> anyhow::Result<PathBuf> {
74 Ok(self.path.canonicalize()?)
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct LinkedProject {
80 pub id: Option<i64>,
81 pub name: String,
82 pub description: Option<String>,
83 pub created_at: DateTime<Utc>,
84 pub is_active: bool,
85 pub member_projects: Vec<Project>,
86}
87
88impl LinkedProject {
89 pub fn new(name: String) -> Self {
90 Self {
91 id: None,
92 name,
93 description: None,
94 created_at: Utc::now(),
95 is_active: true,
96 member_projects: Vec::new(),
97 }
98 }
99
100 pub fn add_project(&mut self, project: Project) {
101 self.member_projects.push(project);
102 }
103
104 pub fn remove_project(&mut self, project_id: i64) {
105 self.member_projects.retain(|p| p.id != Some(project_id));
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn test_project_new() {
115 let path = PathBuf::from("/tmp/test-project");
116 let project = Project::new("Test Project".to_string(), path.clone());
117
118 assert_eq!(project.name, "Test Project");
119 assert_eq!(project.path, path);
120 assert!(!project.is_archived);
121 assert!(project.git_hash.is_none());
122 }
123
124 #[test]
125 fn test_project_archive_unarchive() {
126 let mut project = Project::new("Test".to_string(), PathBuf::from("/tmp"));
127
128 assert!(!project.is_archived);
129
130 project.archive();
131 assert!(project.is_archived);
132
133 project.unarchive();
134 assert!(!project.is_archived);
135 }
136
137 #[test]
138 fn test_project_update_path() {
139 let mut project = Project::new("Test".to_string(), PathBuf::from("/tmp/old"));
140 let new_path = PathBuf::from("/tmp/new");
141
142 project.update_path(new_path.clone());
143 assert_eq!(project.path, new_path);
144 }
145
146 #[test]
147 fn test_linked_project_management() {
148 let mut linked = LinkedProject::new("Meta Project".to_string());
149 let p1 = Project::new("P1".to_string(), PathBuf::from("/p1"))
150 .with_git_hash(Some("hash1".to_string()));
151
152 linked.add_project(p1.clone());
153 assert_eq!(linked.member_projects.len(), 1);
154 assert_eq!(linked.member_projects[0].name, "P1");
155 }
156}