1use super::Brain;
8use crate::enums::named_enum;
9use crate::model::{Kind, Status, Task, covered, 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,
123 pub events: Vec<Event>,
125 worked: Vec<(DateTime<Local>, DateTime<Local>)>,
127}
128
129#[derive(Debug)]
131pub struct Calendar {
132 pub days: Vec<Day>,
134 pub tracked: Duration,
136}
137
138impl Calendar {
139 pub fn events(&self) -> usize {
141 self.days.iter().map(|d| d.events.len()).sum()
142 }
143
144 pub fn merge(parts: Vec<Calendar>) -> Calendar {
147 let mut parts = parts.into_iter().enumerate();
148 let Some((_, mut merged)) = parts.next() else {
149 return Calendar { days: Vec::new(), tracked: Duration::zero() };
150 };
151 for (source, calendar) in parts {
152 for (day, other) in merged.days.iter_mut().zip(calendar.days) {
153 day.worked.extend(other.worked);
154 day.tracked = covered(day.worked.iter().copied());
155 day.events.extend(other.events.into_iter().map(|e| Event { source, ..e }));
156 day.events.sort_by_key(|e| (e.at, e.source, e.id));
157 }
158 }
159 merged.tracked = merged.days.iter().map(|d| d.tracked).sum();
160 merged
161 }
162
163 pub fn ids(&self) -> Vec<u32> {
165 self.entries().into_iter().map(|(_, id)| id).collect()
166 }
167
168 pub fn entries(&self) -> Vec<(usize, u32)> {
170 let mut entries = Vec::new();
171 for event in self.days.iter().flat_map(|d| &d.events) {
172 if !entries.contains(&(event.source, event.id)) {
173 entries.push((event.source, event.id));
174 }
175 }
176 entries
177 }
178}
179
180impl Brain {
181 pub fn calendar(&self, view: &CalendarView) -> Calendar {
183 let now = now();
184 let (first, last) = grid(view.period);
185 let tasks: Vec<&Task> = self
186 .tasks
187 .iter()
188 .filter(|t| view.tag.as_ref().is_none_or(|tag| t.tags.contains(tag)) && t.matches(&view.query))
189 .collect();
190 let days: Vec<Day> = first
191 .iter_days()
192 .take_while(|&date| date <= last)
193 .map(|date| {
194 let day = Period::containing(Unit::Day, date.and_time(NaiveTime::MIN));
195 let in_period = view.period.contains(day.start) || day.contains(view.period.start);
196 if !in_period {
197 return Day { date, in_period, tracked: Duration::zero(), events: Vec::new(), worked: Vec::new() };
198 }
199 let (from, to) = day.bounds();
200 let worked: Vec<_> = tasks.iter().flat_map(|t| &t.sessions).map(|s| s.span(from, to, now)).collect();
201 let tracked = covered(worked.iter().copied());
202 let mut events: Vec<Event> =
203 tasks.iter().filter_map(|task| summary(task, from, to, now, view.show)).collect();
204 events.sort_by_key(|e| (e.at, e.id));
205 Day { date, in_period, tracked, events, worked }
206 })
207 .collect();
208 let tracked = days.iter().map(|d| d.tracked).sum();
209 Calendar { days, tracked }
210 }
211}
212
213fn grid(period: Period) -> (NaiveDate, NaiveDate) {
216 let first = period.start.date();
217 let last = (period.end() - Duration::seconds(1)).date();
218 if period.unit == Unit::Month {
219 let monday = Period::containing(Unit::Week, period.start).start.date();
220 let sunday = Period::containing(Unit::Week, last.and_time(NaiveTime::MIN)).end().date() - Duration::days(1);
221 (monday, sunday)
222 } else {
223 (first, last)
224 }
225}
226
227fn summary(task: &Task, from: DateTime<Local>, to: DateTime<Local>, now: DateTime<Local>, show: Show) -> Option<Event> {
231 let mut all: Vec<Event> = interactions(task, from, to, now).into_iter().filter(|e| e.what.shown_by(show)).collect();
232 all.sort_by_key(|e| e.at);
233 let worked = all
234 .iter()
235 .map(|e| match e.what {
236 What::Worked { end } => end.unwrap_or(now) - e.at,
237 _ => Duration::zero(),
238 })
239 .sum();
240 let comments = all.iter().filter(|e| matches!(e.what, What::Commented { .. })).count();
241 let last = |pick: fn(&What) -> bool| all.iter().rev().find(|e| pick(&e.what));
242 let main = last(|w| matches!(w, What::Moved { .. }))
243 .or_else(|| last(|w| matches!(w, What::Created)))
244 .or_else(|| last(|w| matches!(w, What::Commented { .. })))
245 .or_else(|| all.last())?;
246 let at = match main.what {
248 What::Worked { .. } => all.iter().find(|e| matches!(e.what, What::Worked { .. })).map_or(main.at, |e| e.at),
249 _ => main.at,
250 };
251 Some(Event { at, worked, comments, ..main.clone() })
252}
253
254fn interactions(task: &Task, from: DateTime<Local>, to: DateTime<Local>, now: DateTime<Local>) -> Vec<Event> {
256 let event = |at, what| Event {
257 at,
258 id: task.id,
259 title: task.title.clone(),
260 kind: task.kind,
261 tags: task.tags.clone(),
262 what,
263 source: 0,
264 worked: Duration::zero(),
265 comments: 0,
266 };
267 let within = |at: DateTime<Local>| from <= at && at < to;
268 let mut events = Vec::new();
269 if within(task.created) {
270 events.push(event(task.created, What::Created));
271 }
272 for change in task.history.iter().filter(|c| within(c.at)) {
275 events.push(event(change.at, What::Moved { from: change.from, to: change.to }));
276 }
277 for session in &task.sessions {
278 if session.overlap(from, to, now).is_zero() {
279 continue;
280 }
281 let end = match session.end {
283 Some(end) => Some(end.min(to)),
284 None if now >= to => Some(to),
285 None => None,
286 };
287 events.push(event(session.start.max(from), What::Worked { end }));
288 }
289 for comment in task.comments.iter().filter(|c| within(c.created)) {
290 let excerpt = comment.body.lines().map(str::trim).find(|l| !l.is_empty()).unwrap_or_default().to_string();
291 events.push(event(comment.created, What::Commented { excerpt }));
292 }
293 events
294}