jobber 0.1.2-alpha

Minimalistic console work time tracker
use crate::{
    commands::{Config, is_available, not_available},
    database::Result,
    entities::{Limit, format_job_id, format_opt},
    jobber::JobberGet,
    views::ApplyTableStyle,
};
use color_print::cwriteln;
use tabled::{
    Table, Tabled,
    settings::{Alignment, Color, Width, object::Columns, themes::Colorization},
};

#[derive(Tabled)]
pub(crate) struct JobView {
    #[tabled(rename = "Pos")]
    pos: String,
    #[tabled(rename = "Worker")]
    worker: String,
    #[tabled(rename = "Client")]
    client: String,
    #[tabled(rename = "Subject")]
    subject: String,
    #[tabled(rename = "Rate")]
    rate: String,
    #[tabled(rename = "Interval")]
    interval: String,
    #[tabled(rename = "Billing")]
    billing: String,
    #[tabled(rename = "Work")]
    work: String,
    #[tabled(rename = "Revenue")]
    revenue: String,
    #[tabled(rename = "Current Work")]
    current_work: String,
}

impl JobView {
    pub(crate) fn new(
        job_id: usize,
        config: &Config,
        database: &impl JobberGet,
        ansi: bool,
    ) -> Result<Self> {
        let job = database.get_job(job_id)?;
        let scheme = &job.scheme(job_id)?;
        let work = job.work(job_id)?;
        Ok(Self {
            pos: format_job_id(job_id, database, config, ansi)?,
            worker: database.get_full_worker_name(&job.worker)?,
            client: database.get_full_client_name(&job.client)?,
            subject: job.subject.clone(),
            rate: format_opt(&scheme.rate, ansi),
            interval: scheme.format_duration(
                scheme.min_work_interval.duration(),
                Limit::None,
                ansi,
            ),
            billing: format_opt(&scheme.billing_period, ansi),
            work: scheme.format_duration(work, Limit::None, ansi),
            revenue: scheme.format_revenue(work, ansi),
            current_work: if let Some(running_work) = &job.running {
                format!(
                    "since {} ({})",
                    scheme.format_date_time(running_work.start),
                    scheme.format_duration(running_work.duration(), Limit::Day, ansi)
                )
            } else {
                not_available(ansi)
            },
        })
    }
}

pub(crate) struct PosIterator<T> {
    total: usize,
    special_pos: usize,
    special_val: T,
    default_val: T,
    current: usize,
}

impl<T> PosIterator<T> {
    fn new(total: usize, special_pos: usize, special_val: T, default_val: T) -> Self {
        PosIterator {
            total,
            special_pos,
            special_val,
            default_val,
            current: 0,
        }
    }
}

impl<T: Clone> Iterator for PosIterator<T> {
    type Item = T;

    fn next(&mut self) -> Option<Self::Item> {
        if self.current >= self.total {
            return None;
        }
        let val = if self.current == self.special_pos {
            self.special_val.clone()
        } else {
            self.default_val.clone()
        };
        self.current += 1;
        Some(val)
    }
}

pub(crate) fn create_job_table(
    rows: impl IntoIterator<Item = JobView>,
    current_job: &Option<usize>,
    num_jobs: usize,
    config: &Config,
) -> Table {
    let mut table = Table::new(rows);
    table.modify(Columns::first(), Alignment::right());
    table.modify(Columns::one(4), Width::wrap(40));
    table.modify(Columns::one(4), Alignment::right());
    table.modify(Columns::one(6), Alignment::right());
    table.modify(Columns::one(5), Alignment::center());
    table.modify(Columns::one(7), Alignment::right());
    table.modify(Columns::one(8), Alignment::right());
    if let Some(current_job) = current_job {
        table.with(Colorization::rows(PosIterator::new(
            num_jobs + 1,
            current_job + 1,
            Color::BOLD,
            Color::default(),
        )));
    }
    table.style(&config.table_style);
    table
}

impl std::fmt::Display for JobView {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        cwriteln!(f, "   Job ID: <s>{}</>", self.pos,)?;
        cwriteln!(f, "   Worker: <s>{}</>", self.worker)?;
        cwriteln!(f, "   Client: <s>{}</>", self.client)?;
        if !self.subject.is_empty() {
            cwriteln!(f, "  Subject: <i>{}</>", &self.subject)?;
        }
        if !self.work.is_empty() {
            cwriteln!(f, "     Rate: {}", self.rate)?;

            cwriteln!(f, " Interval: {}", self.interval)?;
            cwriteln!(f, "  Billing: {}", self.billing)?;

            if is_available(&self.work) {
                cwriteln!(f, "     Work: {}", self.work)?;
            }
            if is_available(&self.revenue) {
                cwriteln!(f, "  Revenue: {}", self.revenue)?;
            }
        }
        Ok(())
    }
}