jobber 1.1.6-alpha

Minimalistic console work time tracker
use crate::{
    commands::{Cli, Config, Error, Result, current_job, from_range_months},
    entities::{DateTime, format_job_id},
    jobber::JobberWork,
    views::{CalendarView, create_calendar_table},
};
use clap::Parser;
use color_print::cformat;

/// Show work within a calendar.
#[derive(Parser, Debug)]
pub(crate) struct Calendar {
    /// Time to display
    ///
    /// Available formats:
    ///
    /// | Example     | Description     |
    /// | ------------|-----------------|
    /// | `all`       | all work time   |
    /// | `now`       | current month   |
    /// | `5/2026`    | month/year      |
    /// | `5`         | month           |
    /// | `2026`      | whole year      |
    #[clap(default_value = "all", verbatim_doc_comment)]
    start: String,
    /// Continue display until
    ///
    /// Available formats:
    ///
    /// | Example     | Description     |
    /// | ------------|-----------------|
    /// | `now`       | current month   |
    /// | `5/2026`    | month/year      |
    /// | `5`         | month           |
    /// | `2026`      | whole year      |
    #[clap(verbatim_doc_comment)]
    end: Option<String>,

    /// Show only work of this job
    ///
    /// [default: current job]
    #[clap(short, long, conflicts_with("all_jobs"))]
    job_id: Option<usize>,

    /// Show work of all jobs
    ///
    /// [default: current job only]
    #[clap(short, long, conflicts_with("job_id"))]
    all_jobs: bool,

    /// Show only deleted work and deleted jobs
    #[clap(short, long)]
    deleted: bool,
}

impl Calendar {
    pub(crate) fn run(&self, cli: &Cli) -> Result<()> {
        let config = Config::load()?;
        let database = cli.open_database()?;

        let (job_id, outro) = if self.all_jobs {
            (None, cformat!("All jobs"))
        } else {
            let job_id = if let Some(job_id) = self.job_id {
                job_id
            } else {
                current_job(&config, &database)?
            };
            (
                Some(job_id),
                cformat!(
                    "Job: {}",
                    format_job_id(job_id, &database, &config, cli.ansi())?
                ),
            )
        };
        let (start, end) = if self.start == "all" {
            let (start, end) = database.work_range(&job_id, self.deleted.into());
            if start >= end {
                return Err(Error::NoJobsFoundInRange(if let Some(end) = &self.end {
                    format!("{}..{}", self.start.clone(), end.clone())
                } else {
                    self.start.clone()
                }));
            }
            (start, end)
        } else {
            let (start, end) = from_range_months(&self.start, &self.end, DateTime::now())?;
            if start >= end {
                return Err(Error::InvalidRangeEx(if let Some(end) = &self.end {
                    format!("{}..{}", self.start.clone(), end.clone())
                } else {
                    self.start.clone()
                }));
            }
            (start, end)
        };
        let mut date = start;
        while date < end {
            let mut calendar = Vec::new();
            let mut hours_per_month = 0.0;
            let mut current_day = date.start_of_month();
            let month_end = current_day.next_month();

            let mut current_week = [None; 7];
            let mut week_start_day;

            while current_day < month_end {
                let day_of_week = current_day.day_of_week() as usize;
                current_week[day_of_week] =
                    Some(database.duration_by_day(job_id, current_day).num_minutes() as f32 / 60.0);

                week_start_day = current_day.day() as i32 - day_of_week as i32;

                if day_of_week == 6 || current_day.next_day() >= month_end {
                    let week_sum: f32 = current_week.iter().flatten().sum();
                    hours_per_month += week_sum;
                    calendar.push(CalendarView::new(
                        week_start_day,
                        current_week,
                        week_sum,
                        cli.ansi(),
                    ));
                    current_week = [None; 7];
                }

                current_day = current_day.next_day();
            }

            calendar.push(CalendarView::new_sum(
                date.start_of_month(),
                hours_per_month,
                cli.ansi(),
            ));
            if hours_per_month > 0.0 {
                database.duration_by_day(job_id, date);
                let table = create_calendar_table(calendar, &config);
                println!();
                println!(
                    "                             {:02}/{}",
                    date.month(),
                    date.year()
                );
                println!("{table}");
            } else {
                println!("                                .");
            }
            date = date.next_month();
        }

        eprintln!("{outro}");

        Ok(())
    }
}