1use super::Brain;
8use crate::enums::named_enum;
9use crate::model::{Kind, Status, Task, now};
10use crate::period::{Period, Unit};
11use chrono::{DateTime, Duration, Local, NaiveDate, NaiveTime};
12
13named_enum! {
14 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16 pub enum Show {
17 #[default]
19 All => "all",
20 Time => "time",
22 Moves => "moves",
24 Comments => "comments",
26 New => "new",
28 }
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct CalendarView {
34 pub period: Period,
36 pub tag: Option<String>,
38 pub query: String,
40 pub show: Show,
42}
43
44impl CalendarView {
45 pub fn new(period: Period) -> Self {
47 Self { period, tag: None, query: String::new(), show: Show::All }
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum What {
54 Worked {
56 end: Option<DateTime<Local>>,
58 },
59 Moved {
61 from: Status,
63 to: Status,
65 },
66 Commented {
68 excerpt: String,
70 },
71 Created,
73}
74
75impl What {
76 fn shown_by(&self, show: Show) -> bool {
78 matches!(
79 (show, self),
80 (Show::All, _)
81 | (Show::Time, Self::Worked { .. })
82 | (Show::Moves, Self::Moved { .. })
83 | (Show::Comments, Self::Commented { .. })
84 | (Show::New, Self::Created)
85 )
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct Event {
92 pub at: DateTime<Local>,
94 pub id: u32,
96 pub title: String,
98 pub kind: Kind,
100 pub tags: Vec<String>,
102 pub what: What,
104 pub source: usize,
106 pub worked: Duration,
108 pub comments: usize,
110}
111
112#[derive(Debug)]
114pub struct Day {
115 pub date: NaiveDate,
117 pub in_period: bool,
120 pub tracked: Duration,
122 pub events: Vec<Event>,
124}
125
126#[derive(Debug)]
128pub struct Calendar {
129 pub days: Vec<Day>,
131 pub tracked: Duration,
133}
134
135impl Calendar {
136 pub fn events(&self) -> usize {
138 self.days.iter().map(|d| d.events.len()).sum()
139 }
140
141 pub fn merge(parts: Vec<Calendar>) -> Calendar {
144 let mut parts = parts.into_iter().enumerate();
145 let Some((_, mut merged)) = parts.next() else {
146 return Calendar { days: Vec::new(), tracked: Duration::zero() };
147 };
148 for (source, calendar) in parts {
149 merged.tracked += calendar.tracked;
150 for (day, other) in merged.days.iter_mut().zip(calendar.days) {
151 day.tracked += other.tracked;
152 day.events.extend(other.events.into_iter().map(|e| Event { source, ..e }));
153 day.events.sort_by_key(|e| (e.at, e.source, e.id));
154 }
155 }
156 merged
157 }
158
159 pub fn ids(&self) -> Vec<u32> {
161 self.entries().into_iter().map(|(_, id)| id).collect()
162 }
163
164 pub fn entries(&self) -> Vec<(usize, u32)> {
166 let mut entries = Vec::new();
167 for event in self.days.iter().flat_map(|d| &d.events) {
168 if !entries.contains(&(event.source, event.id)) {
169 entries.push((event.source, event.id));
170 }
171 }
172 entries
173 }
174}
175
176impl Brain {
177 pub fn calendar(&self, view: &CalendarView) -> Calendar {
179 let now = now();
180 let (first, last) = grid(view.period);
181 let tasks: Vec<&Task> = self
182 .tasks
183 .iter()
184 .filter(|t| view.tag.as_ref().is_none_or(|tag| t.tags.contains(tag)) && t.matches(&view.query))
185 .collect();
186 let days: Vec<Day> = first
187 .iter_days()
188 .take_while(|&date| date <= last)
189 .map(|date| {
190 let day = Period::containing(Unit::Day, date.and_time(NaiveTime::MIN));
191 let in_period = view.period.contains(day.start) || day.contains(view.period.start);
192 if !in_period {
193 return Day { date, in_period, tracked: Duration::zero(), events: Vec::new() };
194 }
195 let (from, to) = day.bounds();
196 let tracked = tasks.iter().map(|t| t.time_between(from, to, now)).sum();
197 let mut events: Vec<Event> =
198 tasks.iter().filter_map(|task| summary(task, from, to, now, view.show)).collect();
199 events.sort_by_key(|e| (e.at, e.id));
200 Day { date, in_period, tracked, events }
201 })
202 .collect();
203 let tracked = days.iter().map(|d| d.tracked).sum();
204 Calendar { days, tracked }
205 }
206}
207
208fn grid(period: Period) -> (NaiveDate, NaiveDate) {
211 let first = period.start.date();
212 let last = (period.end() - Duration::seconds(1)).date();
213 if period.unit == Unit::Month {
214 let monday = Period::containing(Unit::Week, period.start).start.date();
215 let sunday = Period::containing(Unit::Week, last.and_time(NaiveTime::MIN)).end().date() - Duration::days(1);
216 (monday, sunday)
217 } else {
218 (first, last)
219 }
220}
221
222fn summary(task: &Task, from: DateTime<Local>, to: DateTime<Local>, now: DateTime<Local>, show: Show) -> Option<Event> {
226 let mut all: Vec<Event> = interactions(task, from, to, now).into_iter().filter(|e| e.what.shown_by(show)).collect();
227 all.sort_by_key(|e| e.at);
228 let worked = all
229 .iter()
230 .map(|e| match e.what {
231 What::Worked { end } => end.unwrap_or(now) - e.at,
232 _ => Duration::zero(),
233 })
234 .sum();
235 let comments = all.iter().filter(|e| matches!(e.what, What::Commented { .. })).count();
236 let last = |pick: fn(&What) -> bool| all.iter().rev().find(|e| pick(&e.what));
237 let main = last(|w| matches!(w, What::Moved { .. }))
238 .or_else(|| last(|w| matches!(w, What::Created)))
239 .or_else(|| last(|w| matches!(w, What::Commented { .. })))
240 .or_else(|| all.last())?;
241 Some(Event { worked, comments, ..main.clone() })
242}
243
244fn interactions(task: &Task, from: DateTime<Local>, to: DateTime<Local>, now: DateTime<Local>) -> Vec<Event> {
246 let event = |at, what| Event {
247 at,
248 id: task.id,
249 title: task.title.clone(),
250 kind: task.kind,
251 tags: task.tags.clone(),
252 what,
253 source: 0,
254 worked: Duration::zero(),
255 comments: 0,
256 };
257 let within = |at: DateTime<Local>| from <= at && at < to;
258 let mut events = Vec::new();
259 if within(task.created) {
260 events.push(event(task.created, What::Created));
261 }
262 for change in task.history.iter().filter(|c| within(c.at)) {
265 events.push(event(change.at, What::Moved { from: change.from, to: change.to }));
266 }
267 for session in &task.sessions {
268 if session.overlap(from, to, now).is_zero() {
269 continue;
270 }
271 let end = match session.end {
273 Some(end) => Some(end.min(to)),
274 None if now >= to => Some(to),
275 None => None,
276 };
277 events.push(event(session.start.max(from), What::Worked { end }));
278 }
279 for comment in task.comments.iter().filter(|c| within(c.created)) {
280 let excerpt = comment.body.lines().map(str::trim).find(|l| !l.is_empty()).unwrap_or_default().to_string();
281 events.push(event(comment.created, What::Commented { excerpt }));
282 }
283 events
284}