boat-cli 0.9.2

Basic Opinionated Activity Tracker, a command line interface inspired by bartib
use anyhow::Result;
use boat_lib::repository::activities_repository as activities;
use chrono::{Datelike, Local};
use log::info;
use rusqlite::Connection;
use std::collections::BTreeMap;

use crate::{
    cli::{self, PeriodInput, args::GroupBy},
    config::Configuration,
    models::{activity_log::PrintableActivityLog, boat_data::BoatData},
    utils::{self, date::DateTimeRenderMode},
};

pub fn list_activity_logs(
    config: &Configuration,
    conn: &Connection,
    args: &cli::FilterActivitiesArgs,
) -> Result<()> {
    let period = args
        .period
        .or(config.commands.list.period)
        .or(config.period)
        .unwrap_or(PeriodInput::Preset(cli::PresetPeriod::AllTime));
    info!("using period: {period}");

    let group_by_value = args
        .group_by
        .or(config.commands.list.group_by)
        .unwrap_or(GroupBy::None);
    info!("grouping by: {group_by_value}");

    let fields = args
        .fields
        .clone()
        .or_else(|| config.commands.list.fields.clone());

    info!("getting all activities");
    let db_acts: Vec<_> = activities::get_all(conn)?;
    let boat_data = BoatData::create_filtered_data(db_acts, period);

    info!("filtering logs by tags");
    let prt_logs = boat_data
        .get_printable_logs()
        .into_iter()
        .filter(|log| {
            if let Some(filter_tags) = &args.filter_by_tags {
                filter_tags
                    .iter()
                    .all(|tag| log.activity.tags.contains(tag))
            } else {
                true
            }
        })
        .collect::<Vec<_>>();

    info!("grouping logs based on group_by value");
    let grouped_logs = group_by(&prt_logs, group_by_value);
    if args.use_json_format {
        let json = serde_json::to_string(&grouped_logs)?;
        println!("{json}");
        return Ok(());
    }

    if grouped_logs.is_empty() {
        println!("no available data");
        return Ok(());
    }

    info!("displaying activity logs grouped by date");
    for (group, act_logs) in grouped_logs.iter() {
        let (text, tooltip) = utils::display::get_group_by_display_values(group_by_value, group)?;
        let ribbon = utils::display::format_ascii_ribbon(&text, tooltip.as_deref());
        println!("{ribbon}");
        utils::common::list_printable_items(act_logs, false, fields.as_deref())?;
    }

    Ok(())
}

trait ActivityLog {
    fn starts_at(&self) -> Option<chrono::DateTime<Local>>;
    fn ends_at(&self) -> Option<chrono::DateTime<Local>>;
}

fn group_by(
    activity_logs: &[PrintableActivityLog],
    group_by: GroupBy,
) -> BTreeMap<String, Vec<PrintableActivityLog>> {
    match group_by {
        GroupBy::None => {
            let mut map = BTreeMap::new();
            map.insert("all".to_string(), activity_logs.to_vec());
            map
        }
        GroupBy::Day => group_by_day(activity_logs),
        GroupBy::Week => group_by_week(activity_logs),
        GroupBy::Month => group_by_month(activity_logs),
        GroupBy::Year => group_by_year(activity_logs),
    }
}

fn group_by_day(
    activity_logs: &[PrintableActivityLog],
) -> BTreeMap<String, Vec<PrintableActivityLog>> {
    let mut groups: BTreeMap<_, Vec<_>> = BTreeMap::new();

    for act_log in activity_logs {
        let latest_dt = act_log.log.ends_at.unwrap_or(Local::now());
        let key = DateTimeRenderMode::DateOnly.render_date_time(latest_dt);
        groups.entry(key).or_default().push(act_log.clone());
    }

    groups
}

fn group_by_week(
    activity_logs: &[PrintableActivityLog],
) -> BTreeMap<String, Vec<PrintableActivityLog>> {
    let mut groups: BTreeMap<_, Vec<_>> = BTreeMap::new();

    for act_log in activity_logs {
        let latest_dt = act_log.log.ends_at.unwrap_or(Local::now());
        let iso_week = latest_dt.iso_week();
        let key = format!("{}-W{}", iso_week.year(), iso_week.week());
        groups.entry(key).or_default().push(act_log.clone());
    }

    groups
}

fn group_by_month(
    activity_logs: &[PrintableActivityLog],
) -> BTreeMap<String, Vec<PrintableActivityLog>> {
    let mut groups: BTreeMap<_, Vec<_>> = BTreeMap::new();

    for act_log in activity_logs {
        let latest_dt = act_log.log.ends_at.unwrap_or(Local::now());
        let key = format!("{}-{:02}", latest_dt.year(), latest_dt.month());
        groups.entry(key).or_default().push(act_log.clone());
    }

    groups
}

fn group_by_year(
    activity_logs: &[PrintableActivityLog],
) -> BTreeMap<String, Vec<PrintableActivityLog>> {
    let mut groups: BTreeMap<_, Vec<_>> = BTreeMap::new();

    for act_log in activity_logs {
        let latest_dt = act_log.log.ends_at.unwrap_or(Local::now());
        let key = format!("{}", latest_dt.year());
        groups.entry(key).or_default().push(act_log.clone());
    }

    groups
}