1use std::collections::{BTreeMap, BTreeSet};
8
9use todoapp_core::{
10 Assignments, ComponentStore, Due, Duration, Estimate, Id, Schedule, Status, TaskEntityStore,
11 TimeSpent,
12};
13
14use crate::service::{Error, Services};
15
16#[derive(Debug, Clone, PartialEq)]
17pub struct Aggregate {
18 pub total: usize,
19 pub done: usize,
20 pub progress: f32,
21 pub time_spent: Duration,
22 pub estimate: Duration,
23 pub remaining: Duration,
26 pub earliest_due: Option<Due>,
27 pub assignees: BTreeSet<Id>,
28 pub status: Status,
31 pub by_status: BTreeMap<Status, usize>,
33}
34
35impl Default for Aggregate {
36 fn default() -> Self {
37 Self {
38 total: 0,
39 done: 0,
40 progress: 0.0,
41 time_spent: Duration::ZERO,
42 estimate: Duration::ZERO,
43 remaining: Duration::ZERO,
44 earliest_due: None,
45 assignees: BTreeSet::new(),
46 status: Status::Draft,
47 by_status: BTreeMap::new(),
48 }
49 }
50}
51
52impl<'a, St: ComponentStore + TaskEntityStore> Services<'a, St> {
53 pub async fn aggregate(&self, id: &Id) -> Result<Aggregate, Error> {
54 let mut agg = Aggregate::default();
55 let mut ids = self.descendants(id).await;
59 ids.insert(id.clone());
60 let mut worst: Option<Status> = None;
61 for tid in ids {
62 agg.total += 1;
63 let status = self.store.get::<Status>(&tid).await;
64 if status == Some(Status::Done) {
65 agg.done += 1;
66 }
67 let status = status.unwrap_or(Status::Draft);
68 *agg.by_status.entry(status).or_insert(0) += 1;
69 worst = Some(match worst {
70 Some(w) if w.rank() <= status.rank() => w,
71 _ => status,
72 });
73 agg.time_spent += self
74 .store
75 .get::<TimeSpent>(&tid)
76 .await
77 .map_or(Duration::ZERO, |t| t.0);
78 let estimate = self
79 .store
80 .get::<Estimate>(&tid)
81 .await
82 .map_or(Duration::ZERO, |e| e.0);
83 agg.estimate += estimate;
84 if status != Status::Done {
85 agg.remaining += estimate;
86 }
87 if let Some(Schedule(due)) = self.store.get::<Schedule>(&tid).await {
88 agg.earliest_due = Some(match agg.earliest_due.take() {
89 Some(cur) if cur <= due => cur,
90 _ => due,
91 });
92 }
93 if let Some(Assignments(asg)) = self.store.get::<Assignments>(&tid).await {
94 agg.assignees.extend(asg.into_iter().map(|a| a.actor));
95 }
96 }
97 agg.progress = if agg.total > 0 {
98 agg.done as f32 / agg.total as f32
99 } else {
100 0.0
101 };
102 agg.status = worst.unwrap_or(Status::Draft);
103 Ok(agg)
104 }
105}