1use chrono::{DateTime, NaiveDate, Utc};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub struct Task {
11 #[serde(flatten)]
12 pub item: joy_core::model::item::Item,
13
14 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub due_date: Option<NaiveDate>,
17
18 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub reminder: Option<DateTime<Utc>>,
21
22 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub recurrence: Option<String>,
26
27 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub project: Option<String>,
30
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub source: Option<String>,
34
35 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub closed_at: Option<DateTime<Utc>>,
40
41 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
44 pub archived: bool,
45
46 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub archived_at: Option<DateTime<Utc>>,
50}
51
52impl Task {
53 pub fn new(id: String, title: String) -> Self {
54 Self {
55 item: joy_core::model::item::Item::new(
56 id,
57 title,
58 joy_core::model::item::ItemType::Task,
59 joy_core::model::item::Priority::Medium,
60 vec![joy_core::model::item::Capability::Implement],
61 ),
62 due_date: None,
63 reminder: None,
64 recurrence: None,
65 project: None,
66 source: None,
67 closed_at: None,
68 archived: false,
69 archived_at: None,
70 }
71 }
72
73 pub fn is_recurring(&self) -> bool {
75 self.recurrence.is_some()
76 }
77
78 pub fn is_dispatched(&self) -> bool {
80 self.source.is_some()
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86pub struct Project {
87 pub id: String,
88 pub title: String,
89
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub source: Option<String>,
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn task_new_defaults() {
101 let task = Task::new("JOT-0001".into(), "Buy milk".into());
102 assert_eq!(task.item.id, "JOT-0001");
103 assert_eq!(task.item.title, "Buy milk");
104 assert_eq!(task.item.item_type, joy_core::model::item::ItemType::Task);
105 assert!(!task.is_recurring());
106 assert!(!task.is_dispatched());
107 }
108
109 #[test]
110 fn task_serialization_roundtrip() {
111 let mut task = Task::new("JOT-0001".into(), "Weekly standup".into());
112 task.recurrence = Some("FREQ=WEEKLY;BYDAY=MO".into());
113 task.due_date = Some(NaiveDate::from_ymd_opt(2026, 3, 24).unwrap());
114 task.project = Some("JOT-P-01".into());
115
116 let yaml = serde_yaml_ng::to_string(&task).unwrap();
117 let parsed: Task = serde_yaml_ng::from_str(&yaml).unwrap();
118
119 assert_eq!(parsed.item.id, "JOT-0001");
120 assert_eq!(parsed.recurrence, Some("FREQ=WEEKLY;BYDAY=MO".into()));
121 assert!(parsed.is_recurring());
122 assert_eq!(parsed.project, Some("JOT-P-01".into()));
123 }
124
125 #[test]
126 fn dispatched_task() {
127 let mut task = Task::new("JOT-0003".into(), "Review JOY-002A".into());
128 task.source = Some("joy:acme/product:JOY-002A".into());
129 task.project = Some("JOT-P-03".into());
130
131 assert!(task.is_dispatched());
132 }
133}