1use crate::ids::ThingsId;
2use crate::wire::area::{AreaPatch, AreaProps};
3use crate::wire::checklist::{ChecklistItemPatch, ChecklistItemProps};
4use crate::wire::notes::TaskNotes;
5use crate::wire::recurrence::RecurrenceRule;
6use crate::wire::tags::{TagPatch, TagProps};
7use crate::wire::task::{TaskPatch, TaskProps, TaskStart, TaskStatus, TaskType};
8use crate::wire::wire_object::EntityType;
9use crate::wire::wire_object::Properties;
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct StateObject {
15 pub entity_type: Option<EntityType>,
16 pub properties: StateProperties,
17}
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub enum StateProperties {
21 Task(TaskStateProps),
22 ChecklistItem(ChecklistItemStateProps),
23 Area(AreaStateProps),
24 Tag(TagStateProps),
25 Other,
26}
27
28#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
29pub struct TaskStateProps {
30 pub title: String,
31 pub notes: Option<String>,
32 pub item_type: TaskType,
33 pub status: TaskStatus,
34 pub stop_date: Option<f64>,
35 pub start_location: TaskStart,
36 pub scheduled_date: Option<f64>,
37 pub today_index_reference: Option<i64>,
38 pub deadline: Option<f64>,
39 pub parent_project_ids: Vec<ThingsId>,
40 pub area_ids: Vec<ThingsId>,
41 pub action_group_ids: Vec<ThingsId>,
42 pub tag_ids: Vec<ThingsId>,
43 pub sort_index: i32,
44 pub today_sort_index: i32,
45 pub recurrence_rule: Option<RecurrenceRule>,
46 pub recurrence_template_ids: Vec<ThingsId>,
47 pub instance_creation_paused: bool,
48 pub evening_bit: i32,
49 pub leaves_tombstone: bool,
50 pub trashed: bool,
51 pub creation_date: Option<f64>,
52 pub modification_date: Option<f64>,
53}
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
56pub struct ChecklistItemStateProps {
57 pub title: String,
58 pub status: TaskStatus,
59 pub task_ids: Vec<ThingsId>,
60 pub sort_index: i32,
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
64pub struct AreaStateProps {
65 pub title: String,
66 pub tag_ids: Vec<ThingsId>,
67 pub sort_index: i32,
68}
69
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
71pub struct TagStateProps {
72 pub title: String,
73 pub shortcut: Option<String>,
74 pub sort_index: i32,
75 pub parent_ids: Vec<ThingsId>,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct Tag {
80 pub uuid: ThingsId,
81 pub title: String,
82 pub shortcut: Option<String>,
83 pub index: i32,
84 pub parent_uuid: Option<ThingsId>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct Area {
89 pub uuid: ThingsId,
90 pub title: String,
91 pub tags: Vec<ThingsId>,
92 pub index: i32,
93}
94
95#[derive(Debug, Clone, PartialEq)]
96pub struct ChecklistItem {
97 pub uuid: ThingsId,
98 pub title: String,
99 pub task_uuid: ThingsId,
100 pub status: TaskStatus,
101 pub index: i32,
102}
103
104impl ChecklistItem {
105 pub fn is_incomplete(&self) -> bool {
106 self.status == TaskStatus::Incomplete
107 }
108
109 pub fn is_completed(&self) -> bool {
110 self.status == TaskStatus::Completed
111 }
112
113 pub fn is_canceled(&self) -> bool {
114 self.status == TaskStatus::Canceled
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Default)]
119pub struct ProjectProgress {
120 pub total: i32,
121 pub done: i32,
122}
123
124#[derive(Debug, Clone, PartialEq, Default)]
125pub struct Task {
126 pub uuid: ThingsId,
127 pub title: String,
128 pub status: TaskStatus,
129 pub start: TaskStart,
130 pub item_type: TaskType,
131 pub entity: String,
132 pub notes: Option<String>,
133 pub project: Option<ThingsId>,
134 pub area: Option<ThingsId>,
135 pub action_group: Option<ThingsId>,
136 pub tags: Vec<ThingsId>,
137 pub trashed: bool,
138 pub deadline: Option<DateTime<Utc>>,
139 pub start_date: Option<DateTime<Utc>>,
140 pub stop_date: Option<DateTime<Utc>>,
141 pub creation_date: Option<DateTime<Utc>>,
142 pub modification_date: Option<DateTime<Utc>>,
143 pub index: i32,
144 pub today_index: i32,
145 pub today_index_reference: Option<i64>,
146 pub leaves_tombstone: bool,
147 pub instance_creation_paused: bool,
148 pub evening: bool,
149 pub recurrence_rule: Option<RecurrenceRule>,
150 pub recurrence_templates: Vec<ThingsId>,
151 pub checklist_items: Vec<ChecklistItem>,
152}
153
154impl Task {
155 pub fn is_incomplete(&self) -> bool {
156 self.status == TaskStatus::Incomplete
157 }
158
159 pub fn is_completed(&self) -> bool {
160 self.status == TaskStatus::Completed
161 }
162
163 pub fn is_canceled(&self) -> bool {
164 self.status == TaskStatus::Canceled
165 }
166
167 pub fn is_todo(&self) -> bool {
168 self.item_type == TaskType::Todo
169 }
170
171 pub fn is_project(&self) -> bool {
172 self.item_type == TaskType::Project
173 }
174
175 pub fn is_heading(&self) -> bool {
176 self.item_type == TaskType::Heading
177 }
178
179 pub fn in_someday(&self) -> bool {
180 self.start == TaskStart::Someday && self.start_date.is_none()
181 }
182
183 pub fn is_today(&self, today: &DateTime<Utc>) -> bool {
184 let Some(start_date) = self.start_date else {
185 return false;
186 };
187 if self.start != TaskStart::Anytime && self.start != TaskStart::Someday {
188 return false;
189 }
190 start_date <= *today
191 }
192
193 pub fn is_staged_for_today(&self, today: &DateTime<Utc>) -> bool {
194 let Some(start_date) = self.start_date else {
195 return false;
196 };
197 self.start == TaskStart::Someday && start_date <= *today
198 }
199
200 pub fn is_recurrence_template(&self) -> bool {
201 self.recurrence_rule.is_some() && self.recurrence_templates.is_empty()
202 }
203
204 pub fn is_recurrence_instance(&self) -> bool {
205 self.recurrence_rule.is_none() && !self.recurrence_templates.is_empty()
206 }
207}
208
209fn i64_to_f64_opt(value: Option<i64>) -> Option<f64> {
210 value.map(|v| v as f64)
211}
212
213fn parse_notes_from_wire(notes: &Option<TaskNotes>) -> Option<String> {
214 notes.as_ref().and_then(TaskNotes::to_plain_text)
215}
216
217impl From<TaskProps> for TaskStateProps {
218 fn from(props: TaskProps) -> Self {
219 Self {
220 title: props.title,
221 notes: parse_notes_from_wire(&props.notes),
222 item_type: props.item_type,
223 status: props.status,
224 stop_date: props.stop_date,
225 start_location: props.start_location,
226 scheduled_date: i64_to_f64_opt(props.scheduled_date),
227 today_index_reference: props.today_index_reference,
228 deadline: i64_to_f64_opt(props.deadline),
229 parent_project_ids: props.parent_project_ids,
230 area_ids: props.area_ids,
231 action_group_ids: props.action_group_ids,
232 tag_ids: props.tag_ids,
233 sort_index: props.sort_index,
234 today_sort_index: props.today_sort_index,
235 recurrence_rule: props.recurrence_rule,
236 recurrence_template_ids: props.recurrence_template_ids,
237 instance_creation_paused: props.instance_creation_paused,
238 evening_bit: props.evening_bit,
239 leaves_tombstone: props.leaves_tombstone,
240 trashed: props.trashed,
241 creation_date: props.creation_date,
242 modification_date: props.modification_date,
243 }
244 }
245}
246
247impl From<ChecklistItemProps> for ChecklistItemStateProps {
248 fn from(props: ChecklistItemProps) -> Self {
249 Self {
250 title: props.title,
251 status: props.status,
252 task_ids: props.task_ids,
253 sort_index: props.sort_index,
254 }
255 }
256}
257
258impl From<AreaProps> for AreaStateProps {
259 fn from(props: AreaProps) -> Self {
260 Self {
261 title: props.title,
262 tag_ids: props.tag_ids,
263 sort_index: props.sort_index,
264 }
265 }
266}
267
268impl From<TagProps> for TagStateProps {
269 fn from(props: TagProps) -> Self {
270 Self {
271 title: props.title,
272 shortcut: props.shortcut,
273 sort_index: props.sort_index,
274 parent_ids: props.parent_ids,
275 }
276 }
277}
278
279impl From<ChecklistItemPatch> for ChecklistItemStateProps {
280 fn from(patch: ChecklistItemPatch) -> Self {
281 let mut item = Self::default();
282 if let Some(title) = patch.title {
283 item.title = title;
284 }
285 if let Some(status) = patch.status {
286 item.status = status;
287 }
288 if let Some(task_ids) = patch.task_ids {
289 item.task_ids = task_ids;
290 }
291 if let Some(sort_index) = patch.sort_index {
292 item.sort_index = sort_index;
293 }
294 item
295 }
296}
297
298impl From<AreaPatch> for AreaStateProps {
299 fn from(patch: AreaPatch) -> Self {
300 let mut area = Self::default();
301 if let Some(title) = patch.title {
302 area.title = title;
303 }
304 if let Some(tag_ids) = patch.tag_ids {
305 area.tag_ids = tag_ids;
306 }
307 if let Some(sort_index) = patch.sort_index {
308 area.sort_index = sort_index;
309 }
310 area
311 }
312}
313
314impl From<TagPatch> for TagStateProps {
315 fn from(patch: TagPatch) -> Self {
316 let mut tag = Self::default();
317 if let Some(title) = patch.title {
318 tag.title = title;
319 }
320 if let Some(parent_ids) = patch.parent_ids {
321 tag.parent_ids = parent_ids;
322 }
323 if let Some(shortcut) = patch.shortcut {
324 tag.shortcut = shortcut;
325 }
326 if let Some(sort_index) = patch.sort_index {
327 tag.sort_index = sort_index;
328 }
329 tag
330 }
331}
332
333impl From<TaskPatch> for TaskStateProps {
334 fn from(patch: TaskPatch) -> Self {
335 let mut task = Self::default();
336 if let Some(title) = patch.title {
337 task.title = title;
338 }
339 if let Some(notes) = patch.notes {
340 task.notes = notes.to_plain_text();
341 }
342 if let Some(start_location) = patch.start_location {
343 task.start_location = start_location;
344 }
345 if let Some(scheduled_date) = patch.scheduled_date {
346 task.scheduled_date = scheduled_date.map(|v| v as f64);
347 }
348 if let Some(today_index_reference) = patch.today_index_reference {
349 task.today_index_reference = today_index_reference;
350 }
351 if let Some(parent_project_ids) = patch.parent_project_ids {
352 task.parent_project_ids = parent_project_ids;
353 }
354 if let Some(area_ids) = patch.area_ids {
355 task.area_ids = area_ids;
356 }
357 if let Some(action_group_ids) = patch.action_group_ids {
358 task.action_group_ids = action_group_ids;
359 }
360 if let Some(tag_ids) = patch.tag_ids {
361 task.tag_ids = tag_ids;
362 }
363 if let Some(evening_bit) = patch.evening_bit {
364 task.evening_bit = evening_bit;
365 }
366 if let Some(modification_date) = patch.modification_date {
367 task.modification_date = Some(modification_date);
368 }
369 if let Some(item_type) = patch.item_type {
370 task.item_type = item_type;
371 }
372 if let Some(status) = patch.status {
373 task.status = status;
374 }
375 if let Some(stop_date) = patch.stop_date {
376 task.stop_date = stop_date;
377 }
378 if let Some(deadline) = patch.deadline {
379 task.deadline = deadline;
380 }
381 if let Some(sort_index) = patch.sort_index {
382 task.sort_index = sort_index;
383 }
384 if let Some(today_sort_index) = patch.today_sort_index {
385 task.today_sort_index = today_sort_index;
386 }
387 if let Some(recurrence_rule) = patch.recurrence_rule {
388 task.recurrence_rule = recurrence_rule;
389 }
390 if let Some(recurrence_template_ids) = patch.recurrence_template_ids {
391 task.recurrence_template_ids = recurrence_template_ids;
392 }
393 if let Some(instance_creation_paused) = patch.instance_creation_paused {
394 task.instance_creation_paused = instance_creation_paused;
395 }
396 if let Some(leaves_tombstone) = patch.leaves_tombstone {
397 task.leaves_tombstone = leaves_tombstone;
398 }
399 if let Some(trashed) = patch.trashed {
400 task.trashed = trashed;
401 }
402 if let Some(creation_date) = patch.creation_date {
403 task.creation_date = creation_date;
404 }
405 task
406 }
407}
408
409impl From<Properties> for StateProperties {
410 fn from(payload: Properties) -> Self {
411 use Properties::*;
412 use StateProperties::*;
413
414 match payload {
415 TaskCreate(props) => Task(props.into()),
416 TaskUpdate(patch) => Task(patch.into()),
417 ChecklistCreate(props) => ChecklistItem(props.into()),
418 ChecklistUpdate(patch) => ChecklistItem(patch.into()),
419 AreaCreate(props) => Area(props.into()),
420 AreaUpdate(patch) => Area(patch.into()),
421 TagCreate(props) => Tag(props.into()),
422 TagUpdate(patch) => Tag(patch.into()),
423 TombstoneCreate(_) | CommandCreate(_) | Ignored(_) | Unknown(_) | Delete => Other,
424 }
425 }
426}