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
36impl Task {
37 pub fn new(id: String, title: String) -> Self {
38 Self {
39 item: joy_core::model::item::Item::new(
40 id,
41 title,
42 joy_core::model::item::ItemType::Task,
43 joy_core::model::item::Priority::Medium,
44 vec![joy_core::model::item::Capability::Implement],
45 ),
46 due_date: None,
47 reminder: None,
48 recurrence: None,
49 project: None,
50 source: None,
51 }
52 }
53
54 pub fn is_recurring(&self) -> bool {
56 self.recurrence.is_some()
57 }
58
59 pub fn is_dispatched(&self) -> bool {
61 self.source.is_some()
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67pub struct Project {
68 pub id: String,
69 pub title: String,
70
71 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub source: Option<String>,
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn task_new_defaults() {
82 let task = Task::new("JOT-0001".into(), "Buy milk".into());
83 assert_eq!(task.item.id, "JOT-0001");
84 assert_eq!(task.item.title, "Buy milk");
85 assert_eq!(task.item.item_type, joy_core::model::item::ItemType::Task);
86 assert!(!task.is_recurring());
87 assert!(!task.is_dispatched());
88 }
89
90 #[test]
91 fn task_serialization_roundtrip() {
92 let mut task = Task::new("JOT-0001".into(), "Weekly standup".into());
93 task.recurrence = Some("FREQ=WEEKLY;BYDAY=MO".into());
94 task.due_date = Some(NaiveDate::from_ymd_opt(2026, 3, 24).unwrap());
95 task.project = Some("JOT-P-01".into());
96
97 let yaml = serde_yaml_ng::to_string(&task).unwrap();
98 let parsed: Task = serde_yaml_ng::from_str(&yaml).unwrap();
99
100 assert_eq!(parsed.item.id, "JOT-0001");
101 assert_eq!(parsed.recurrence, Some("FREQ=WEEKLY;BYDAY=MO".into()));
102 assert!(parsed.is_recurring());
103 assert_eq!(parsed.project, Some("JOT-P-01".into()));
104 }
105
106 #[test]
107 fn dispatched_task() {
108 let mut task = Task::new("JOT-0003".into(), "Review JOY-002A".into());
109 task.source = Some("joy:acme/product:JOY-002A".into());
110 task.project = Some("JOT-P-03".into());
111
112 assert!(task.is_dispatched());
113 }
114}