1use chrono::{DateTime, NaiveDate, Utc};
2use serde::{Deserialize, Serialize};
3
4pub use uuid::Uuid;
5
6pub use chrono;
7pub use serde;
8pub use uuid;
9
10#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
11#[serde(rename_all = "lowercase")]
12pub enum TaskStatus {
13 Inbox,
14 Todo,
15 Doing,
16 Done,
17 Someday,
18}
19
20#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
21pub struct Task {
22 pub id: Uuid,
23 pub completed: bool,
24 pub priority: Option<char>,
25 pub description: String,
26 pub projects: Vec<String>,
27 pub contexts: Vec<String>,
28 pub status: TaskStatus,
29 pub created_at: DateTime<Utc>,
30 pub updated_at: DateTime<Utc>,
31 pub completed_at: Option<DateTime<Utc>>,
32 pub due_date: Option<NaiveDate>,
33}
34
35#[derive(Debug, Serialize, Deserialize, Clone)]
36pub struct TaskHistory {
37 pub id: i32,
38 pub task_id: Uuid,
39 pub from_status: Option<TaskStatus>,
40 pub to_status: TaskStatus,
41 pub changed_at: DateTime<Utc>,
42}
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47
48 #[test]
49 fn task_creation_with_uuid_v7() {
50 let task = Task {
51 id: Uuid::now_v7(),
52 completed: false,
53 priority: None,
54 description: "Test task".into(),
55 projects: vec![],
56 contexts: vec![],
57 status: TaskStatus::Inbox,
58 created_at: Utc::now(),
59 updated_at: Utc::now(),
60 completed_at: None,
61 due_date: None,
62 };
63 assert_eq!(task.id.get_version(), Some(uuid::Version::SortRand));
64 }
65
66 #[test]
67 fn task_serialize_deserialize_roundtrip() {
68 let task = Task {
69 id: Uuid::now_v7(),
70 completed: false,
71 priority: Some('A'),
72 description: "Buy milk +proyecto @casa".into(),
73 projects: vec!["proyecto".into()],
74 contexts: vec!["casa".into()],
75 status: TaskStatus::Todo,
76 created_at: Utc::now(),
77 updated_at: Utc::now(),
78 completed_at: None,
79 due_date: Some(NaiveDate::from_ymd_opt(2026, 7, 10).unwrap()),
80 };
81 let json = serde_json::to_string(&task).unwrap();
82 let deserialized: Task = serde_json::from_str(&json).unwrap();
83 assert_eq!(task.id, deserialized.id);
84 assert_eq!(task.description, deserialized.description);
85 assert_eq!(task.status, deserialized.status);
86 }
87
88 #[test]
89 fn task_status_serialization() {
90 let statuses = vec![
91 (TaskStatus::Inbox, "\"inbox\""),
92 (TaskStatus::Todo, "\"todo\""),
93 (TaskStatus::Doing, "\"doing\""),
94 (TaskStatus::Done, "\"done\""),
95 (TaskStatus::Someday, "\"someday\""),
96 ];
97 for (status, expected) in statuses {
98 assert_eq!(serde_json::to_string(&status).unwrap(), expected);
99 }
100 }
101
102 #[test]
103 fn task_history_creation() {
104 let history = TaskHistory {
105 id: 1,
106 task_id: Uuid::now_v7(),
107 from_status: Some(TaskStatus::Inbox),
108 to_status: TaskStatus::Doing,
109 changed_at: Utc::now(),
110 };
111 assert_eq!(history.from_status, Some(TaskStatus::Inbox));
112 assert_eq!(history.to_status, TaskStatus::Doing);
113 }
114
115 #[test]
116 fn task_default_status_is_inbox() {
117 let task = Task {
118 id: Uuid::now_v7(),
119 completed: false,
120 priority: None,
121 description: "test".into(),
122 projects: vec![],
123 contexts: vec![],
124 status: TaskStatus::Inbox,
125 created_at: Utc::now(),
126 updated_at: Utc::now(),
127 completed_at: None,
128 due_date: None,
129 };
130 assert_eq!(task.status, TaskStatus::Inbox);
131 assert!(!task.completed);
132 }
133
134 #[test]
135 fn completed_task_has_completed_at() {
136 let task = Task {
137 id: Uuid::now_v7(),
138 completed: true,
139 priority: None,
140 description: "Done task".into(),
141 projects: vec![],
142 contexts: vec![],
143 status: TaskStatus::Done,
144 created_at: Utc::now(),
145 updated_at: Utc::now(),
146 completed_at: Some(Utc::now()),
147 due_date: None,
148 };
149 assert!(task.completed);
150 assert!(task.completed_at.is_some());
151 }
152}