shifty 0.1.7

A simple cli tool to keep track of your shifts at work
Documentation
use csv::Reader;
use serde::{Deserialize, Serialize};

use crate::{
    args::Args,
    formaters::{format_money, format_time},
    parse::parse_month,
    Config,
};
use colored::Colorize;

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Shift {
    pub date: String,
    pub time: f32,
    pub premium: f32,
}

impl Shift {
    fn format(&self) -> String {
        if self.premium > 0. {
            return format!(
                "  Date: {}\n  Time: {}\n  Premium: {}%,\n",
                self.date.yellow(),
                format_time(self.time).yellow(),
                (self.premium * 100.).to_string().green()
            );
        }
        format!(
            "  Date: {}\n  Time: {},\n",
            self.date.yellow(),
            format_time(self.time).yellow()
        )
    }

    pub fn format_and_calc(&self, config: Config) -> String {
        let mut premium = 1.;
        let mut premium_str = "".to_string();
        if self.premium > 0. {
            premium_str = format!("\nPremium: {}%", (self.premium * 100.).to_string().green());
            premium = self.premium;
        }
        let income = (self.time * (config.rate * premium)) - config.cost_per_shift;
        let mut after_tax = "".to_string();
        if config.tax_percent > 0. {
            after_tax = format!(
                "\nIncome after tax: {:2}{}",
                income * config.tax_percent,
                config.currency
            );
        }
        format!(
            "Date: {}\nTime: {}{}\nIncome: {}{}{}",
            self.date.to_string().yellow(),
            format_time(self.time).yellow(),
            premium_str,
            income.to_string().green(),
            config.currency.green(),
            after_tax.green()
        )
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Shifts {
    pub shifts: Vec<Shift>,
}

impl Shifts {
    pub fn from_file(db_path: String) -> Self {
        let mut rdr = match Reader::from_path(db_path) {
            Ok(a) => a,
            Err(_) => {
                return Self { shifts: Vec::new() };
            }
        };
        let db = rdr
            .deserialize()
            .map(|v| {
                let shift: Shift = v.unwrap();
                shift
            })
            .collect::<Vec<Shift>>();
        Shifts { shifts: db }
    }

    pub fn format(&self, config: Config) -> String {
        let mut money_sum = 0.;
        let mut hours_sum: f32 = 0.;
        let shifts_formatted = self
            .shifts
            .iter()
            .map(|v| {
                hours_sum += v.time;
                let rate;
                if v.premium == 0. {
                    rate = config.rate;
                } else {
                    rate = config.rate * v.premium;
                }
                money_sum += (v.time * rate) - config.cost_per_shift as f32;
                v.format()
            })
            .collect::<Vec<String>>()
            .join("\n");
        let hours_sum_formatted = format_time(hours_sum).yellow();
        if config.tax_percent > 0.0 {
            let money_sum_after_tax = money_sum - (money_sum * config.tax_percent);
            return format!(
                "Shifts: {{\n{}}}\n\nTotal shifts:\n{}\n\nTotal hours:\n{}\n\nTotal Income:\n{}\n\nTotal Income after Tax:\n{}",
                shifts_formatted,
                self.shifts.len(),
                hours_sum_formatted,
                format_money(money_sum, &config.currency),
                format_money(money_sum_after_tax, &config.currency)
            );
        } else {
            return format!(
                "Shifts: {{\n{}}}\n\nTotal shifts:\n{}\n\nTotal hours:\n{}\n\nTotal Income:\n{}",
                shifts_formatted,
                self.shifts.len().to_string().yellow(),
                hours_sum_formatted.to_string().yellow(),
                format_money(money_sum, &config.currency),
            );
        }
    }

    pub fn stats(&self, config: Config, args: Args) -> String {
        let month_string = parse_month(args.date).unwrap();
        let date_split = month_string.split('/').collect::<Vec<&str>>();
        let (this_month, this_year) = (
            date_split[0].parse::<i32>().unwrap(),
            date_split[1].parse::<i32>().unwrap(),
        );
        let mut shift_hours_sum = 0.;
        let mut salaries = vec![Salary {
            date: None,
            hours: 0.,
            income: 0.,
        }];
        let mut month_idx = 0;
        let mut last_month = -1;
        for (idx, shift) in self.shifts.iter().enumerate() {
            let date_split = shift.date.split('/').collect::<Vec<&str>>();
            let (month, year) = (
                date_split[1].parse::<i32>().unwrap(),
                date_split[2].parse::<i32>().unwrap(),
            );
            if idx == 0 {
                last_month = month;
            }
            if year < this_year || month < this_month && year == this_year {
                if last_month != month {
                    month_idx += 1;
                    let money = shift.time * config.rate - config.cost_per_shift;
                    salaries.push(Salary {
                        date: Some(format!("{}/{}", month, year)),
                        hours: shift.time,
                        income: money,
                    });
                    last_month = month;
                } else {
                    if salaries[month_idx].date.is_none() {
                        salaries[month_idx].date = Some(format!("{}/{}", month, year));
                    }
                    salaries[month_idx].hours += shift.time;
                    salaries[month_idx].income += shift.time * config.rate - config.cost_per_shift;
                }
            }
            shift_hours_sum += shift.time;
        }

        let shift_hours_avg = shift_hours_sum / self.shifts.len() as f32;
        let shift_money_avg = shift_hours_avg * config.rate - config.cost_per_shift;
        let shift_money_after_tax_avg = shift_money_avg - (shift_money_avg * config.tax_percent);

        let avg_salary = avrage_slary(salaries.clone());
        let salaries_formatted = salaries
            .iter()
            .map(|v| format!("\n{},", v.format(config.clone())))
            .collect::<Vec<String>>();
        let mut shift_money_after_tax_avg_formatted = "".to_string();
        if config.tax_percent > 0. {
            shift_money_after_tax_avg_formatted = format!(
                "\n  Income after tax: {}",
                format_money(shift_money_after_tax_avg, &config.currency)
            );
        }

        return format!(
            "Months: {{{}\n}}\n\nAvrage Salary:\n{}\n\nAvrage shift:\n  Time: {}\n  Income: {}{}",
            salaries_formatted.join("\n"),
            avg_salary.format(config.clone()),
            format_time(shift_hours_avg).yellow(),
            format_money(shift_money_avg, &config.currency),
            shift_money_after_tax_avg_formatted,
        );
    }
}

fn avrage_slary(salaries: Vec<Salary>) -> Salary {
    let mut sum_salary = Salary {
        date: None,
        hours: 0.,
        income: 0.,
    };
    for sal in &salaries {
        sum_salary.hours += sal.hours;
        sum_salary.income += sal.income;
    }
    Salary {
        date: None,
        hours: sum_salary.hours / salaries.len() as f32,
        income: sum_salary.income / salaries.len() as f32,
    }
}

#[derive(Debug, Clone)]
struct Salary {
    date: Option<String>,
    hours: f32,
    income: f32,
}

impl Salary {
    fn format(&self, config: Config) -> String {
        let date = match &self.date {
            Some(date) => format!("  Month: {}\n", date.yellow()),
            None => "".to_string(),
        };
        let mut income_after_tax = "".to_string();
        if config.tax_percent > 0. {
            income_after_tax = format!(
                "\n  Total Income after tax: {}",
                format_money(
                    self.income - (self.income * config.tax_percent),
                    &config.currency
                )
            )
        }
        return format!(
            "{}  Total Hours: {}\n  Total Income: {}{}",
            date,
            format_time(self.hours),
            format_money(self.income, &config.currency),
            income_after_tax
        );
    }
}