feat: add --minutes flag to timesheet command
Adds a -m/--minutes flag to `streamd timesheet` that displays time in HH:MM format (e.g., 8:30) instead of decimal hours (e.g., 8.5h). Includes unit tests for both formatting functions.
This commit is contained in:
parent
e4ed1d839e
commit
42d9ecd3d9
3 changed files with 75 additions and 23 deletions
|
|
@ -50,7 +50,11 @@ pub enum Commands {
|
|||
},
|
||||
|
||||
/// Display extracted timesheets
|
||||
Timesheet,
|
||||
Timesheet {
|
||||
/// Display time as minutes (HH:MM) instead of decimal hours (H.Hh)
|
||||
#[arg(short, long)]
|
||||
minutes: bool,
|
||||
},
|
||||
|
||||
/// Generate shell completions
|
||||
Completions {
|
||||
|
|
|
|||
|
|
@ -41,8 +41,14 @@ fn load_all_shards(base_folder: &Path) -> Result<Vec<LocalizedShard>, StreamdErr
|
|||
}
|
||||
|
||||
/// Format hours with sign for display.
|
||||
fn format_diff(hours: f64) -> String {
|
||||
if hours >= 0.0 {
|
||||
fn format_diff(hours: f64, use_minutes: bool) -> String {
|
||||
if use_minutes {
|
||||
let total_minutes = (hours * 60.0).round() as i32;
|
||||
let h = total_minutes.abs() / 60;
|
||||
let m = total_minutes.abs() % 60;
|
||||
let sign = if hours >= 0.0 { "+" } else { "-" };
|
||||
format!("{}{}:{:02}", sign, h, m)
|
||||
} else if hours >= 0.0 {
|
||||
format!("+{:.1}h", hours.abs())
|
||||
} else {
|
||||
format!("{:.1}h", hours)
|
||||
|
|
@ -50,8 +56,15 @@ fn format_diff(hours: f64) -> String {
|
|||
}
|
||||
|
||||
/// Format hours for display without sign.
|
||||
fn format_hours(hours: f64) -> String {
|
||||
format!("{:.1}h", hours.abs())
|
||||
fn format_hours(hours: f64, use_minutes: bool) -> String {
|
||||
if use_minutes {
|
||||
let total_minutes = (hours * 60.0).round() as i32;
|
||||
let h = total_minutes.abs() / 60;
|
||||
let m = total_minutes.abs() % 60;
|
||||
format!("{}:{:02}", h, m)
|
||||
} else {
|
||||
format!("{:.1}h", hours.abs())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the weekday abbreviation.
|
||||
|
|
@ -77,8 +90,8 @@ fn print_header() {
|
|||
}
|
||||
|
||||
/// Print a month report.
|
||||
fn print_month(month: &MonthReport) {
|
||||
let diff_str = format_diff(month.diff());
|
||||
fn print_month(month: &MonthReport, use_minutes: bool) {
|
||||
let diff_str = format_diff(month.diff(), use_minutes);
|
||||
let month_title = format!("{} {}", month.month_name(), month.year);
|
||||
|
||||
// Month header with diff
|
||||
|
|
@ -99,9 +112,9 @@ fn print_month(month: &MonthReport) {
|
|||
for day in &month.days {
|
||||
let date_str = day.date.format("%Y-%m-%d").to_string();
|
||||
let weekday = weekday_abbrev(day.date);
|
||||
let expected = format_hours(day.expected_hours);
|
||||
let actual = format_hours(day.actual_hours);
|
||||
let diff = format_diff(day.diff());
|
||||
let expected = format_hours(day.expected_hours, use_minutes);
|
||||
let actual = format_hours(day.actual_hours, use_minutes);
|
||||
let diff = format_diff(day.diff(), use_minutes);
|
||||
|
||||
let type_str = match day.day_type {
|
||||
DayType::Regular => String::new(),
|
||||
|
|
@ -134,26 +147,26 @@ fn print_month(month: &MonthReport) {
|
|||
println!(" {}", light_line);
|
||||
println!(
|
||||
" Monthly: {:>7} {:>7} {:>6}",
|
||||
format_hours(month.total_expected()),
|
||||
format_hours(month.total_actual()),
|
||||
format_diff(month.diff())
|
||||
format_hours(month.total_expected(), use_minutes),
|
||||
format_hours(month.total_actual(), use_minutes),
|
||||
format_diff(month.diff(), use_minutes)
|
||||
);
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Print the cumulative balance.
|
||||
fn print_cumulative_balance(balance: f64) {
|
||||
fn print_cumulative_balance(balance: f64, use_minutes: bool) {
|
||||
let light_line = "\u{2500}".repeat(SEPARATOR_WIDTH);
|
||||
println!("{}", light_line);
|
||||
println!(
|
||||
" CUMULATIVE BALANCE: {}",
|
||||
format_diff(balance)
|
||||
format_diff(balance, use_minutes)
|
||||
);
|
||||
println!("{}", light_line);
|
||||
}
|
||||
|
||||
/// Print warnings section.
|
||||
fn print_warnings(report: &TimesheetReport) {
|
||||
fn print_warnings(report: &TimesheetReport, use_minutes: bool) {
|
||||
if !report.has_warnings() {
|
||||
return;
|
||||
}
|
||||
|
|
@ -216,9 +229,9 @@ fn print_warnings(report: &TimesheetReport) {
|
|||
for w in &outside_period_warnings {
|
||||
if let DayWarning::OutsidePeriod { hours_worked } = &w.warning {
|
||||
println!(
|
||||
" - {}: {:.1}h worked (no period configured)",
|
||||
" - {}: {} worked (no period configured)",
|
||||
w.date.format("%Y-%m-%d"),
|
||||
hours_worked
|
||||
format_hours(*hours_worked, use_minutes)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -226,7 +239,7 @@ fn print_warnings(report: &TimesheetReport) {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), StreamdError> {
|
||||
pub fn run(use_minutes: bool) -> Result<(), StreamdError> {
|
||||
let settings = Settings::load()?;
|
||||
let base_folder = Path::new(&settings.base_folder);
|
||||
|
||||
|
|
@ -266,11 +279,46 @@ pub fn run() -> Result<(), StreamdError> {
|
|||
print_header();
|
||||
|
||||
for month in &report.months {
|
||||
print_month(month);
|
||||
print_month(month, use_minutes);
|
||||
}
|
||||
|
||||
print_cumulative_balance(report.cumulative_balance);
|
||||
print_warnings(&report);
|
||||
print_cumulative_balance(report.cumulative_balance, use_minutes);
|
||||
print_warnings(&report, use_minutes);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_hours_decimal() {
|
||||
assert_eq!(format_hours(8.0, false), "8.0h");
|
||||
assert_eq!(format_hours(8.5, false), "8.5h");
|
||||
assert_eq!(format_hours(0.0, false), "0.0h");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_hours_minutes() {
|
||||
assert_eq!(format_hours(8.0, true), "8:00");
|
||||
assert_eq!(format_hours(8.5, true), "8:30");
|
||||
assert_eq!(format_hours(0.0, true), "0:00");
|
||||
assert_eq!(format_hours(1.25, true), "1:15");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_diff_decimal() {
|
||||
assert_eq!(format_diff(0.5, false), "+0.5h");
|
||||
assert_eq!(format_diff(-1.5, false), "-1.5h");
|
||||
assert_eq!(format_diff(0.0, false), "+0.0h");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_diff_minutes() {
|
||||
assert_eq!(format_diff(0.5, true), "+0:30");
|
||||
assert_eq!(format_diff(-1.5, true), "-1:30");
|
||||
assert_eq!(format_diff(0.0, true), "+0:00");
|
||||
assert_eq!(format_diff(1.25, true), "+1:15");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ fn main() -> miette::Result<()> {
|
|||
Some(TodoAction::Done { number }) => streamd::cli::commands::todo::run_done(number)?,
|
||||
},
|
||||
Some(Commands::Edit { number }) => streamd::cli::commands::edit::run(number)?,
|
||||
Some(Commands::Timesheet) => streamd::cli::commands::timesheet::run()?,
|
||||
Some(Commands::Timesheet { minutes }) => streamd::cli::commands::timesheet::run(minutes)?,
|
||||
Some(Commands::Completions { shell }) => {
|
||||
streamd::cli::commands::completions::run(shell);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue