1use crate::RichString;
4
5use super::{filter, project::Project as ProjectTrait, task_patch::DatePatchItem};
6use chrono::{DateTime, prelude::*};
7use colored::Colorize;
8use serde::{Deserialize, Serialize};
9use std::{
10 any::Any,
11 cmp::Ordering,
12 fmt::{self, Write},
13};
14
15pub type DateTimeUtc = DateTime<Utc>;
16
17#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
18pub enum State {
19 Unknown(char),
20 #[default]
21 Uncompleted,
22 Completed,
23 InProgress,
24}
25
26impl fmt::Display for State {
27 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
28 match self {
29 State::Completed => write!(f, "✅"),
30 State::Uncompleted => write!(f, " "),
31 State::InProgress => write!(f, "⏳"),
32 State::Unknown(x) => f.write_char(*x),
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
38pub enum Priority {
39 Lowest,
40 Low,
41 #[default]
42 Normal,
43 Medium,
44 High,
45 Highest,
46}
47
48impl Priority {
49 pub fn values() -> Vec<Priority> {
50 vec![
51 Priority::Lowest,
52 Priority::Low,
53 Priority::Normal,
54 Priority::Medium,
55 Priority::High,
56 Priority::Highest,
57 ]
58 }
59}
60
61impl fmt::Display for Priority {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 write!(f, "{self:?}")
64 }
65}
66
67#[derive(Default, Debug, Clone)]
68pub struct PatchPolicy {
69 pub is_editable: bool,
70 pub is_removable: bool,
71 pub available_states: Vec<State>,
72 pub available_priorities: Vec<Priority>,
73 pub available_due_items: Vec<DatePatchItem>,
74 pub available_scheduled_items: Vec<DatePatchItem>,
75}
76
77#[allow(dead_code)]
78pub trait Task: Send + Sync {
79 fn id(&self) -> String;
80 fn name(&self) -> RichString;
81
82 fn description(&self) -> Option<RichString> {
83 None
84 }
85
86 fn priority(&self) -> Priority {
87 Priority::Normal
88 }
89
90 fn state(&self) -> State;
91 fn created_at(&self) -> Option<DateTimeUtc> {
92 None
93 }
94 fn updated_at(&self) -> Option<DateTimeUtc> {
95 None
96 }
97 fn completed_at(&self) -> Option<DateTimeUtc> {
98 None
99 }
100 fn due(&self) -> Option<DateTimeUtc> {
101 None
102 }
103 fn scheduled(&self) -> Option<DateTimeUtc> {
104 None
105 }
106
107 fn planned_date(&self) -> Option<DateTimeUtc> {
108 *planned_date(&self.scheduled(), &self.due())
109 }
110
111 fn place(&self) -> String {
112 String::new()
113 }
114
115 fn url(&self) -> String {
116 String::new()
117 }
118
119 fn labels(&self) -> Vec<String> {
120 Vec::new()
121 }
122
123 fn provider(&self) -> String;
124
125 fn project(&self) -> Option<Box<dyn ProjectTrait>>;
126
127 fn as_any(&self) -> &dyn Any;
128
129 fn clone_boxed(&self) -> Box<dyn Task>;
130
131 fn const_patch_policy(&self) -> PatchPolicy {
132 PatchPolicy {
133 is_editable: false,
134 is_removable: false,
135 available_states: vec![State::Uncompleted, State::Completed, State::InProgress],
136 available_priorities: Priority::values(),
137 available_due_items: DatePatchItem::values(),
138 available_scheduled_items: DatePatchItem::values(),
139 }
140 }
141
142 fn patch_policy(&self) -> PatchPolicy {
143 let mut pp = self.const_patch_policy();
144
145 let s = self.state();
146 pp.available_states.retain(|e| e != &s);
147 let p = self.priority();
148 pp.available_priorities.retain(|e| e != &p);
149 pp
150 }
151}
152
153pub fn datetime_to_str<Tz: TimeZone>(t: Option<DateTimeUtc>, tz: &Tz) -> String
154where
155 <Tz as TimeZone>::Offset: std::fmt::Display,
156{
157 if let Some(d) = t {
158 if d.time() == chrono::NaiveTime::default() {
159 return d.format("%Y-%m-%d").to_string();
160 }
161
162 return d.with_timezone(tz).format("%Y-%m-%d %H:%M:%S %Z").to_string();
163 }
164
165 String::from("-")
166}
167
168pub fn format(t: &dyn Task) -> String {
169 format!(
170 "- [{}] {} ({}) ({})",
171 t.state(),
172 t.name().display(),
173 format!("due: {}", datetime_to_str(t.due(), &Local::now().timezone())).blue(),
174 t.place().green()
175 )
176}
177
178pub fn due_group(due: &Option<DateTimeUtc>) -> filter::Due {
179 match due {
180 Some(d) => {
181 let now = chrono::Utc::now().date_naive();
182 match d.date_naive().cmp(&now) {
183 Ordering::Less => filter::Due::Overdue,
184 Ordering::Equal => filter::Due::Today,
185 Ordering::Greater => filter::Due::Future,
186 }
187 }
188 None => filter::Due::NoDate,
189 }
190}
191
192pub fn planned_date<'a>(scheduled: &'a Option<DateTimeUtc>, due: &'a Option<DateTimeUtc>) -> &'a Option<DateTimeUtc> {
193 if let Some(s) = scheduled
194 && (due.is_none() || s < due.as_ref().unwrap())
195 {
197 return scheduled;
198 }
199
200 due
201}
202
203#[cfg(test)]
204mod test {
205 use chrono::{NaiveDate, Utc};
206
207 use crate::task::DateTimeUtc;
208
209 use super::planned_date;
210
211 fn dt(year: u16, month: u8, day: u8) -> Option<DateTimeUtc> {
212 Some(DateTimeUtc::from_naive_utc_and_offset(
213 NaiveDate::from_ymd_opt(year as i32, month as u32, day as u32)
214 .unwrap()
215 .and_hms_opt(0, 0, 0)
216 .unwrap(),
217 Utc,
218 ))
219 }
220
221 #[test]
222 fn planned_date_test() {
223 assert_eq!(&None, planned_date(&None, &None));
224 assert_eq!(&dt(2026, 1, 1), planned_date(&None, &dt(2026, 1, 1)));
225 assert_eq!(&dt(2026, 1, 1), planned_date(&dt(2026, 1, 1), &None));
226 assert_eq!(&dt(2026, 1, 1), planned_date(&dt(2026, 1, 1), &dt(2026, 1, 2)));
227 assert_eq!(&dt(2026, 1, 1), planned_date(&dt(2026, 1, 2), &dt(2026, 1, 1)));
228 }
229}