aze 0.1.1

A time tracking tool heavenly inspired by watson
Documentation
use anyhow::anyhow;
use anyhow::Result;
use chrono::Datelike;
use chrono::Duration;
use chrono::Local;
use chrono::NaiveDate;
use chrono::NaiveDateTime;
use diesel::associations::HasTable;
use diesel::dsl::not;
use diesel::TextExpressionMethods;
use aze::cli::parse_to_datetime;
use aze::database::establish_connection;
use aze::display::Display;
use aze::models::Frame;

use crate::diesel::ExpressionMethods;
use crate::diesel::QueryDsl;
use crate::diesel::RunQueryDsl;
use colored::Colorize;

use super::MyCommand;

#[derive(clap::Args, Debug)]
pub struct LogSubcommand {
    #[clap(
        short = 'p',
        long = "project",
        help = "Logs activity only for the given project. You can add other projects by using this option several times.",
        multiple = true,
        display_order = 10
    )]
    pub projects: Vec<String>,

    #[clap(
        short = 'T',
        long = "tag",
        help = "Logs activity only for frames containing the given tag. You can add several tags by using this option multiple times.",
        multiple = true,
        display_order = 11
    )]
    pub tags: Vec<String>,

    #[clap(
        long = "ignore-project",
        help = "Logs activity for all projects but the given ones. You can ignore several projects by using this option several times.",
        multiple = true,
        display_order = 12
    )]
    pub ignored_projects: Vec<String>,

    #[clap(
        long = "ignore-tag",
        help = "Logs activity for all tags but the given ones. You can ignore several tags by using this option several times.",
        multiple = true,
        display_order = 13
    )]
    pub ignored_tags: Vec<String>,

    #[clap(
        short = 'c',
        long = "current",
        display_order = 1,
        help = "Include currently running frame in output."
    )]
    pub current: bool,

    #[clap(
        short = 'r',
        long = "reverse",
        display_order = 2,
        help = "Reverse the order of the days in output."
    )]
    pub reverse: bool,

    #[clap(help = "The date from when the log should start. Defaults to seven days ago.", display_order = 3, short = 'f', long = "from", value_parser = parse_to_datetime)]
    pub from: Option<NaiveDateTime>,
    #[clap(help = "The date at which the log should stop (inclusive). Defaults to tomorrow", display_order = 4, short = 't', long = "to", value_parser = parse_to_datetime)]
    pub to: Option<NaiveDateTime>,

    #[clap(
        short = 'y',
        long = "year",
        display_order = 5,
        group = "short_filter",
        help = "Reports activity for the current year."
    )]
    pub year: bool,

    #[clap(
        short = 'm',
        long = "month",
        display_order = 6,
        group = "short_filter",
        help = "Reports activity for the current month."
    )]
    pub month: bool,

    #[clap(
        short = 'w',
        long = "week",
        display_order = 7,
        group = "short_filter",
        help = "Reports activity for the current week."
    )]
    pub week: bool,

    #[clap(
        short = 'd',
        long = "day",
        display_order = 8,
        group = "short_filter",
        help = "Reports activity for the current day."
    )]
    pub day: bool,

    #[clap(
        short = 'a',
        long = "all",
        display_order = 9,
        group = "short_filter",
        help = "Reports all activities."
    )]
    pub all: bool,

    #[clap(
        short = 'j',
        long = "json",
        display_order = 9,
        group = "view",
        help = "Format output in JSON instead of plain text."
    )]
    pub json: bool,

    #[clap(
        short = 's',
        long = "csv",
        display_order = 9,
        group = "view",
        help = "Format output in CSV instead of plain text."
    )]
    pub csv: bool,

    #[clap(
        short = 'g',
        long = "pager",
        display_order = 9,
        group = "view",
        help = "View output through a pager."
    )]
    pub pager: bool,
}

impl LogSubcommand {
    fn parse_project(&self) -> Vec<&String> {
        let mut difference = vec![];
        for i in &self.projects {
            if self.ignored_projects.contains(&i) {
                difference.push(i);
            }
        }
        difference
    }

    fn parse_tags(&self) -> Vec<&String> {
        let mut difference = vec![];
        for i in &self.tags {
            if self.ignored_tags.contains(&i) {
                difference.push(i);
            }
        }
        difference
    }
}

impl MyCommand for LogSubcommand {
    fn run(&self, output: super::Output) -> Result<()> {
        use aze::schema::frames::dsl::*;

        if self.year || self.month || self.day || self.json || self.csv || self.all {
            return Err(anyhow!("NOT IMPLEMENTED"));
        }

        let mut conn = establish_connection();

        let collisions = self.parse_project();
        if !collisions.is_empty() {
            return Err(anyhow!("given projects can't be ignored at the same time"));
        }

        let collisions = self.parse_tags();
        if !collisions.is_empty() {
            return Err(anyhow!("given tags can't be ignored at the same time"));
        }

        let last_week = (Local::now() - Duration::weeks(1)).naive_utc();
        let filter_from = self.from.unwrap_or(last_week);

        let tomorrow = NaiveDate::succ(&Local::today().naive_local()).and_hms(23, 59, 59);
        let filter_end = self.to.unwrap_or(tomorrow);

        if filter_from > filter_end {
            return Err(anyhow!("'from' must be anterior to 'to'"));
        }

        let mut query = frames::table().into_boxed();

        query = query
            .filter(deleted.eq(false))
            .filter(start.gt(filter_from))
            .filter(project.ne_all(self.ignored_projects.to_vec()))
            .order_by(start.desc());

        if !self.projects.is_empty() {
            query = query.filter(project.eq_any(self.projects.to_vec()));
        }

        if !&self.tags.is_empty() {
            query = query.filter(not(tags.eq(tags)));
            for tag in &self.tags {
                query = query.or_filter(tags.like(format!("%{}%", tag)));
            }
        }

        for tag in &self.ignored_tags {
            query = query.filter(not(tags.like(format!("%{}%", tag))));
        }

        if filter_end > Local::now().naive_local() {
            if !self.current {
                query = query.filter(not(end.is_null()));
            }
        } else {
            query = query.filter(end.lt(filter_end));
        }

        let results = query
            .load::<Frame>(&mut conn)
            .expect("Error loading frames");

        let mut actual_day: Option<NaiveDate> = None;
        let mut list: Vec<Display> = Vec::new();

        for frame in results {
            let cloned_start = frame.start.date();
            if actual_day.is_none() || actual_day.unwrap() != frame.start.date() {
                if self.reverse {
                    list.insert(0, Display::new(cloned_start, vec![frame]));
                } else {
                    list.push(Display::new(cloned_start, vec![frame]));
                }
                actual_day = Some(cloned_start);
            } else {
                if self.reverse {
                    list.last_mut().unwrap().insert_frame(frame);
                } else {
                    list.last_mut().unwrap().add_frame(frame);
                }
            }
        }

        for mut display in list {
            let duration = display.total_duration();

            writeln!(
                output.out,
                "{} ({})",
                format!(
                    "{} {} {} {}",
                    display.date.weekday(),
                    display.date.day(),
                    display.date.month(),
                    display.date.year()
                )
                .cyan(),
                format!(
                    "{}h {}m {}s",
                    duration.num_hours(),
                    format!(
                        "{:02}",
                        duration.num_minutes() - (duration.num_hours() * 60)
                    ),
                    format!(
                        "{:02}",
                        duration.num_seconds() - (duration.num_minutes() * 60)
                    )
                )
                .green()
            )?;

            for frame in display.frames {
                let now = Local::now().naive_local();
                let frame_duration = frame.end.unwrap_or(now) - frame.start;
                writeln!(
                    output.out,
                    "\t{}\t{} to {}\t{}h {}m {}s\t{}",
                    &frame.id[..7].to_string().bright_black(),
                    frame.start.format("%H:%M").to_string().green(),
                    frame.end.unwrap_or(now).format("%H:%M").to_string().green(),
                    frame_duration.num_hours(),
                    format!(
                        "{:02}",
                        frame_duration.num_minutes() - (frame_duration.num_hours() * 60)
                    ),
                    format!(
                        "{:02}",
                        frame_duration.num_seconds() - (frame_duration.num_minutes() * 60)
                    ),
                    frame.project.purple()
                )?;
            }
        }

        Ok(())
    }
}