use super::Brain;
use crate::enums::named_enum;
use crate::model::{Kind, Status, Task, covered, now};
use crate::period::{Period, Unit};
use chrono::{DateTime, Duration, Local, NaiveDate, NaiveTime};
named_enum! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Show {
#[default]
All => "all",
Time => "time",
Moves => "moves",
Comments => "comments",
New => "new",
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CalendarView {
pub period: Period,
pub tag: Option<String>,
pub query: String,
pub show: Show,
}
impl CalendarView {
pub fn new(period: Period) -> Self {
Self { period, tag: None, query: String::new(), show: Show::All }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum What {
Worked {
end: Option<DateTime<Local>>,
},
Moved {
from: Status,
to: Status,
},
Commented {
excerpt: String,
},
Created,
}
impl What {
fn shown_by(&self, show: Show) -> bool {
matches!(
(show, self),
(Show::All, _)
| (Show::Time, Self::Worked { .. })
| (Show::Moves, Self::Moved { .. })
| (Show::Comments, Self::Commented { .. })
| (Show::New, Self::Created)
)
}
}
#[derive(Debug, Clone)]
pub struct Event {
pub at: DateTime<Local>,
pub id: u32,
pub title: String,
pub kind: Kind,
pub tags: Vec<String>,
pub what: What,
pub source: usize,
pub worked: Duration,
pub comments: usize,
}
#[derive(Debug)]
pub struct Day {
pub date: NaiveDate,
pub in_period: bool,
pub tracked: Duration,
pub events: Vec<Event>,
worked: Vec<(DateTime<Local>, DateTime<Local>)>,
}
#[derive(Debug)]
pub struct Calendar {
pub days: Vec<Day>,
pub tracked: Duration,
}
impl Calendar {
pub fn events(&self) -> usize {
self.days.iter().map(|d| d.events.len()).sum()
}
pub fn merge(parts: Vec<Calendar>) -> Calendar {
let mut parts = parts.into_iter().enumerate();
let Some((_, mut merged)) = parts.next() else {
return Calendar { days: Vec::new(), tracked: Duration::zero() };
};
for (source, calendar) in parts {
for (day, other) in merged.days.iter_mut().zip(calendar.days) {
day.worked.extend(other.worked);
day.tracked = covered(day.worked.iter().copied());
day.events.extend(other.events.into_iter().map(|e| Event { source, ..e }));
day.events.sort_by_key(|e| (e.at, e.source, e.id));
}
}
merged.tracked = merged.days.iter().map(|d| d.tracked).sum();
merged
}
pub fn ids(&self) -> Vec<u32> {
self.entries().into_iter().map(|(_, id)| id).collect()
}
pub fn entries(&self) -> Vec<(usize, u32)> {
let mut entries = Vec::new();
for event in self.days.iter().flat_map(|d| &d.events) {
if !entries.contains(&(event.source, event.id)) {
entries.push((event.source, event.id));
}
}
entries
}
}
impl Brain {
pub fn calendar(&self, view: &CalendarView) -> Calendar {
let now = now();
let (first, last) = grid(view.period);
let tasks: Vec<&Task> = self
.tasks
.iter()
.filter(|t| view.tag.as_ref().is_none_or(|tag| t.tags.contains(tag)) && t.matches(&view.query))
.collect();
let days: Vec<Day> = first
.iter_days()
.take_while(|&date| date <= last)
.map(|date| {
let day = Period::containing(Unit::Day, date.and_time(NaiveTime::MIN));
let in_period = view.period.contains(day.start) || day.contains(view.period.start);
if !in_period {
return Day { date, in_period, tracked: Duration::zero(), events: Vec::new(), worked: Vec::new() };
}
let (from, to) = day.bounds();
let worked: Vec<_> = tasks.iter().flat_map(|t| &t.sessions).map(|s| s.span(from, to, now)).collect();
let tracked = covered(worked.iter().copied());
let mut events: Vec<Event> =
tasks.iter().filter_map(|task| summary(task, from, to, now, view.show)).collect();
events.sort_by_key(|e| (e.at, e.id));
Day { date, in_period, tracked, events, worked }
})
.collect();
let tracked = days.iter().map(|d| d.tracked).sum();
Calendar { days, tracked }
}
}
fn grid(period: Period) -> (NaiveDate, NaiveDate) {
let first = period.start.date();
let last = (period.end() - Duration::seconds(1)).date();
if period.unit == Unit::Month {
let monday = Period::containing(Unit::Week, period.start).start.date();
let sunday = Period::containing(Unit::Week, last.and_time(NaiveTime::MIN)).end().date() - Duration::days(1);
(monday, sunday)
} else {
(first, last)
}
}
fn summary(task: &Task, from: DateTime<Local>, to: DateTime<Local>, now: DateTime<Local>, show: Show) -> Option<Event> {
let mut all: Vec<Event> = interactions(task, from, to, now).into_iter().filter(|e| e.what.shown_by(show)).collect();
all.sort_by_key(|e| e.at);
let worked = all
.iter()
.map(|e| match e.what {
What::Worked { end } => end.unwrap_or(now) - e.at,
_ => Duration::zero(),
})
.sum();
let comments = all.iter().filter(|e| matches!(e.what, What::Commented { .. })).count();
let last = |pick: fn(&What) -> bool| all.iter().rev().find(|e| pick(&e.what));
let main = last(|w| matches!(w, What::Moved { .. }))
.or_else(|| last(|w| matches!(w, What::Created)))
.or_else(|| last(|w| matches!(w, What::Commented { .. })))
.or_else(|| all.last())?;
let at = match main.what {
What::Worked { .. } => all.iter().find(|e| matches!(e.what, What::Worked { .. })).map_or(main.at, |e| e.at),
_ => main.at,
};
Some(Event { at, worked, comments, ..main.clone() })
}
fn interactions(task: &Task, from: DateTime<Local>, to: DateTime<Local>, now: DateTime<Local>) -> Vec<Event> {
let event = |at, what| Event {
at,
id: task.id,
title: task.title.clone(),
kind: task.kind,
tags: task.tags.clone(),
what,
source: 0,
worked: Duration::zero(),
comments: 0,
};
let within = |at: DateTime<Local>| from <= at && at < to;
let mut events = Vec::new();
if within(task.created) {
events.push(event(task.created, What::Created));
}
for change in task.history.iter().filter(|c| within(c.at)) {
events.push(event(change.at, What::Moved { from: change.from, to: change.to }));
}
for session in &task.sessions {
if session.overlap(from, to, now).is_zero() {
continue;
}
let end = match session.end {
Some(end) => Some(end.min(to)),
None if now >= to => Some(to),
None => None,
};
events.push(event(session.start.max(from), What::Worked { end }));
}
for comment in task.comments.iter().filter(|c| within(c.created)) {
let excerpt = comment.body.lines().map(str::trim).find(|l| !l.is_empty()).unwrap_or_default().to_string();
events.push(event(comment.created, What::Commented { excerpt }));
}
events
}