Skip to content

Commit

Permalink
add timetrack reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
bck01215 committed Jun 26, 2024
1 parent c7a731b commit af8fe4b
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 24 deletions.
16 changes: 16 additions & 0 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ When searching, the CLI will return a list for the user to choose from after que
| `--time-worked <TIME_WORKED>` | Add time in the format of 1h1m where 1 can be replaced with any number (hours must be less than 24) |
| `-s, --search <SEARCH>` | Keyword search using ElasticNow (returns all tickets in bin by default) |
| `-b, --bin <BIN>` | Override default bin for searching (defaults to user's assigned bin or override in config.toml) |
| `--no-tkt` | Uses timetracking without a ticket |
| `-h, --help` | Print help |

Usage: `elasticnow timetrack [OPTIONS] --comment <COMMENT> --time-worked <TIME_WORKED> --search <SEARCH>`
Expand All @@ -54,3 +55,18 @@ Options:
| `-h, --help` | Print help |

Usage: `elasticnow std-chg [OPTIONS]`

### Report

This gets the user's current time tracking and returns the `--top` results and total time tracking for the range. The duration flags (`--since` and `--until`) default to the current work week. If total is below 32 hours it will return red

Options:
| Flag | Description |
| --- | --- |
| `-u, --user <USER>` | Override the default user in the report |
| `--since <SINCE>` | Start date of search (defaults to 2024-06-24) |
| `--until <UNTIL>` | End date of search (defaults to 2024-06-26) |
| `-t, --top <TOP>` | Limit the number of cost centers returned in the report. Any extra fields will be grouped into other [default: 10]|
| `-h, --help` | Print help |

Usage: `elasticnow report [OPTIONS]`
53 changes: 52 additions & 1 deletion src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use chrono::{Datelike, Local};
use clap::{Command, CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Generator, Shell};
use dialoguer::{theme::ColorfulTheme, Select};
use std::io;
use std::{collections::HashMap, io};
#[derive(Parser)]
#[command(name = "elasticnow", about = "ElasticNow time tracking CLI", version)]
pub struct Args {
Expand Down Expand Up @@ -53,6 +53,10 @@ pub enum Commands {
since: Option<String>,
#[clap(long, help = format!("End date of search (defaults to {})", get_today()))]
until: Option<String>,

#[clap(short, long, default_value = "10")]
/// Limit the number of cost centers returned in the report. Any extra fields will be grouped into other
top: Option<usize>,
},

/// Create a std chg using a template
Expand Down Expand Up @@ -193,3 +197,50 @@ pub fn get_week_start() -> String {
now.day() - now.weekday().num_days_from_monday()
)
}

pub fn pretty_print_time_worked(time_worked: HashMap<String, i64>, top: usize) {
let total: i64 = time_worked.values().sum();
let human_total = seconds_to_pretty(total);
let total_str = ansi_term::Colour::Blue.bold().paint("Total:").to_string();
let top_ten = group_top_x(time_worked, top);
let mut sorted_top_ten: Vec<_> = top_ten.into_iter().collect();
sorted_top_ten.sort_by(|a, b| a.1.cmp(&b.1));
for (k, v) in sorted_top_ten {
println!(
"{}: {}",
ansi_term::Colour::Purple.italic().paint(k),
seconds_to_pretty(v)
);
}
if total < 3600 * 32 {
println!(
"{}: {}",
total_str,
ansi_term::Colour::Red.bold().paint(human_total)
);
} else {
println!(
"{}: {}",
total_str,
ansi_term::Colour::Green.bold().paint(human_total)
);
}
}

fn seconds_to_pretty(seconds: i64) -> String {
let hours = seconds / 3600;
let minutes = (seconds % 3600) / 60;
let seconds = seconds % 60;
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
}

fn group_top_x(hash_map: HashMap<String, i64>, x: usize) -> HashMap<String, i64> {
let mut sorted_hash_map = hash_map.into_iter().collect::<Vec<(String, i64)>>();
sorted_hash_map.sort_by(|a, b| b.1.cmp(&a.1));
let other_total = sorted_hash_map.iter().skip(x).map(|x| x.1).sum();
let mut ret_map: HashMap<String, i64> = sorted_hash_map.into_iter().take(x).collect();
if other_total > 0 {
ret_map.insert("Other".to_string(), other_total);
}
ret_map
}
22 changes: 20 additions & 2 deletions src/elasticnow/servicenow.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::elasticnow::servicenow_structs::{
SNResult, SysIdResult, TicketCreation, TimeWorked, UserGroupResult,
CostCenter, SNResult, SysIdResult, TicketCreation, TimeWorked, UserGroupResult,
};
use chrono::{TimeZone, Utc};
use regex::Regex;
Expand Down Expand Up @@ -198,7 +198,7 @@ impl ServiceNow {
user: &str,
) -> Result<Vec<TimeWorked>, Box<dyn Error>> {
let resp = self.get(&format!(
"{}/api/now/table/task_time_worked?sysparm_query=sys_created_by={}^u_created_forBETWEENjavascript:gs.dateGenerate('{}','start')@javascript:gs.dateGenerate('{}','end')",
"{}/api/now/table/task_time_worked?sysparm_fields=task,time_in_seconds,u_category&sysparm_exclude_reference_link=true&sysparm_query=sys_created_by={}^u_created_forBETWEENjavascript:gs.dateGenerate('{}','start')@javascript:gs.dateGenerate('{}','end')",
self.instance, user, start, end,
)).await?;

Expand All @@ -211,6 +211,24 @@ impl ServiceNow {
.result,
)
}
pub async fn get_tasks_cost_centers(
&self,
task_sys_id: &Vec<String>,
) -> Result<Vec<CostCenter>, Box<dyn Error>> {
let task_sys_ids = task_sys_id.join("^ORtask=");
let resp = self.get(
&format!("{}/api/now/table/task_cost_center?sysparm_query=task={}&sysparm_display_value=all&sysparm_exclude_reference_link=true&sysparm_fields=task,cost_center", self.instance, task_sys_ids),
).await?;

if !resp.status().is_success() {
return Err(format!("HTTP Error while querying ServiceNow: {}", resp.status()).into());
}
Ok(
debug_resp_json_deserialize::<SNResult<Vec<CostCenter>>>(resp)
.await?
.result,
)
}
}

pub fn time_add_to_epoch(time: &str) -> Result<String, Box<dyn Error>> {
Expand Down
10 changes: 8 additions & 2 deletions src/elasticnow/servicenow_structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ pub struct DisplayAndValue {
pub display_value: String,
pub value: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DisplayAndLink {
pub display_value: String,
pub link: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct LinkAndValue {
Expand Down Expand Up @@ -86,14 +91,14 @@ impl LinkAndValue {
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum TimeWorkedTask {
LinkAndValue(LinkAndValue),
DisplayAndValue(DisplayAndValue),
EmptyString(String),
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TimeWorked {
pub time_in_seconds: String,
pub task: TimeWorkedTask,
pub task: String,
#[serde(rename = "u_category")]
pub category: String,
}
Expand All @@ -112,4 +117,5 @@ impl TimeWorked {
#[derive(Debug, Serialize, Deserialize)]
pub struct CostCenter {
pub cost_center: DisplayAndValue,
pub task: DisplayAndValue,
}
60 changes: 41 additions & 19 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ use ansi_term::Colour;
use elasticnow::cli::{self, args, config};
use elasticnow::elasticnow::elasticnow::{ElasticNow, SearchResult};
use elasticnow::elasticnow::servicenow::ServiceNow;
use elasticnow::elasticnow::servicenow_structs::TimeWorkedTask;
use std::collections::HashMap;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};

#[tokio::main]
async fn main() {
tracing_subscriber::registry()
Expand Down Expand Up @@ -42,8 +41,13 @@ async fn main() {
run_setup(id, instance, sn_instance, sn_username, sn_password, bin).await;
}

Some(cli::args::Commands::Report { user, since, until }) => {
run_report(user, since, until).await;
Some(cli::args::Commands::Report {
user,
since,
until,
top,
}) => {
run_report(user, since, until, top).await;
}
_ => {
std::process::exit(1);
Expand Down Expand Up @@ -125,7 +129,12 @@ async fn run_timetrack(
std::process::exit(0);
}

async fn run_report(user: Option<String>, since: Option<String>, until: Option<String>) {
async fn run_report(
user: Option<String>,
since: Option<String>,
until: Option<String>,
top: Option<usize>,
) {
let (config, sn_client) = check_config();
let user = user.unwrap_or(config.sn_username.clone());
let since = since.unwrap_or(args::get_week_start());
Expand All @@ -134,32 +143,45 @@ async fn run_report(user: Option<String>, since: Option<String>, until: Option<S
let date_validate = args::range_format_validate(date);
if date_validate.is_err() {
tracing::error!("Invalid date format: {:?}", date_validate.err());
std::process::exit(2);
std::process::exit(1);
}
}
let tasks = sn_client.get_user_time_worked(&since, &until, &user).await;
if tasks.is_err() {
tracing::error!("Unable to get time worked: {:?}", tasks.err());
std::process::exit(2);
std::process::exit(1);
}
let mut task_cat_time: HashMap<String, i64> = HashMap::new();
let tasks = tasks.unwrap();
let mut tasks_ids: HashMap<String, i64> = HashMap::new();
for time_work in tasks {
match time_work.task {
TimeWorkedTask::LinkAndValue(link_and_value) => {
println!(
"Time worked seconds: {} {}",
time_work.time_in_seconds, link_and_value.link
);
match time_work.task.as_ref() {
"" => {
let time_in_seconds: i64 = time_work.time_in_seconds.parse().unwrap_or_default();
*task_cat_time
.entry(time_work.get_nice_name_category())
.or_insert(0) += time_in_seconds;
}
TimeWorkedTask::EmptyString(_) => {
println!(
"Time worked seconds: {} {}",
time_work.time_in_seconds,
time_work.get_nice_name_category()
);
_ => {
let time_in_seconds: i64 = time_work.time_in_seconds.parse().unwrap_or_default();
*tasks_ids.entry(time_work.task).or_insert(0) += time_in_seconds;
}
}
}
let keys = tasks_ids.keys().cloned().collect::<Vec<String>>();
let cost_centers = sn_client.get_tasks_cost_centers(&keys).await;
if cost_centers.is_err() {
tracing::error!("Unable to get cost centers: {:?}", cost_centers.err());
std::process::exit(1);
}
let cost_centers = cost_centers.unwrap();
for cost_center in cost_centers {
let time = tasks_ids.get(&cost_center.task.value).unwrap_or(&0);
*task_cat_time
.entry(cost_center.cost_center.display_value)
.or_insert(0) += time;
}
args::pretty_print_time_worked(task_cat_time, top.unwrap_or(10));
std::process::exit(0);
}
async fn run_setup(
Expand Down

0 comments on commit af8fe4b

Please sign in to comment.