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