diff --git a/src/cli/commands/timesheet.rs b/src/cli/commands/timesheet.rs index 67ab956..fa7f923 100644 --- a/src/cli/commands/timesheet.rs +++ b/src/cli/commands/timesheet.rs @@ -4,6 +4,7 @@ use std::path::Path; use chrono::Datelike; use chrono::NaiveDate; +use chrono::Utc; use walkdir::WalkDir; use crate::config::Settings; @@ -339,7 +340,7 @@ pub fn run(decimal: bool, debug: bool) -> Result<(), StreamdError> { // Load all markdown files and extract timesheets let all_shards = load_all_shards(base_folder)?; - let timesheets = extract_timesheets(&all_shards)?; + let timesheets = extract_timesheets(&all_shards, Utc::now())?; // Generate the report let report = generate_report(×heets, ×heet_config)?; diff --git a/src/timesheet/extract.rs b/src/timesheet/extract.rs index 017434f..304492e 100644 --- a/src/timesheet/extract.rs +++ b/src/timesheet/extract.rs @@ -35,7 +35,10 @@ fn shards_to_timesheet_points(shards: &[LocalizedShard]) -> Vec } /// Aggregate timesheet points for a single day into a Timesheet. -fn aggregate_timecard_day(points: &[TimesheetPoint]) -> Result, StreamdError> { +fn aggregate_timecard_day( + points: &[TimesheetPoint], + now: DateTime, +) -> Result, StreamdError> { if points.is_empty() { return Ok(None); } @@ -113,10 +116,15 @@ fn aggregate_timecard_day(points: &[TimesheetPoint]) -> Result // Check that we ended in break mode if !last_is_break { - return Err(StreamdError::TimesheetError(format!( - "Last Timecard of {} is not a break!", - card_date - ))); + if card_date == now.date_naive() { + // No closing break yet for today — artificially close at now + timecards.push(Timecard::new(last_time, now.time())); + } else { + return Err(StreamdError::TimesheetError(format!( + "Last Timecard of {} is not a break!", + card_date + ))); + } } // Only return a timesheet if there's meaningful data @@ -133,7 +141,10 @@ fn aggregate_timecard_day(points: &[TimesheetPoint]) -> Result } /// Aggregate timesheet points into timesheets, grouped by day. -fn aggregate_timecards(points: &[TimesheetPoint]) -> Result, StreamdError> { +fn aggregate_timecards( + points: &[TimesheetPoint], + now: DateTime, +) -> Result, StreamdError> { let mut timesheets = Vec::new(); // Sort points by moment to ensure proper grouping @@ -143,7 +154,7 @@ fn aggregate_timecards(points: &[TimesheetPoint]) -> Result, Stre // Group by date for (_date, group) in &sorted_points.iter().chunk_by(|p| p.moment.date_naive()) { let day_points: Vec<_> = group.cloned().collect(); - if let Some(timesheet) = aggregate_timecard_day(&day_points)? { + if let Some(timesheet) = aggregate_timecard_day(&day_points, now)? { timesheets.push(timesheet); } } @@ -152,9 +163,12 @@ fn aggregate_timecards(points: &[TimesheetPoint]) -> Result, Stre } /// Extract timesheets from localized shards. -pub fn extract_timesheets(shards: &[LocalizedShard]) -> Result, StreamdError> { +pub fn extract_timesheets( + shards: &[LocalizedShard], + now: DateTime, +) -> Result, StreamdError> { let points = shards_to_timesheet_points(shards); - aggregate_timecards(&points) + aggregate_timecards(&points, now) } #[cfg(test)] @@ -163,6 +177,11 @@ mod tests { use chrono::{NaiveTime, TimeZone}; use indexmap::IndexMap; + /// A fixed "now" in the past, so tests never match today. + fn past_now() -> DateTime { + Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap() + } + fn point(at: DateTime, point_type: TimesheetPointType) -> LocalizedShard { let mut location = IndexMap::new(); location.insert( @@ -198,7 +217,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards).unwrap(); + let result = extract_timesheets(&shards, past_now()).unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].date, day.date_naive()); @@ -251,7 +270,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards).unwrap(); + let result = extract_timesheets(&shards, past_now()).unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].timecards.len(), 3); @@ -302,7 +321,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards).unwrap(); + let result = extract_timesheets(&shards, past_now()).unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].timecards.len(), 3); @@ -336,7 +355,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards).unwrap(); + let result = extract_timesheets(&shards, past_now()).unwrap(); assert_eq!(result.len(), 2); assert_eq!(result[0].date, day1.date_naive()); @@ -359,7 +378,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards).unwrap(); + let result = extract_timesheets(&shards, past_now()).unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].special_day_type, Some(SpecialDayType::Vacation)); @@ -382,7 +401,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards).unwrap(); + let result = extract_timesheets(&shards, past_now()).unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].special_day_type, Some(SpecialDayType::Holiday)); @@ -404,7 +423,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards).unwrap(); + let result = extract_timesheets(&shards, past_now()).unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].special_day_type, Some(SpecialDayType::Undertime)); @@ -431,7 +450,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards).unwrap(); + let result = extract_timesheets(&shards, past_now()).unwrap(); assert_eq!(result.len(), 1); assert!(result[0].is_sick_leave); @@ -454,7 +473,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards).unwrap(); + let result = extract_timesheets(&shards, past_now()).unwrap(); assert_eq!(result.len(), 1); assert!(result[0].is_sick_leave); @@ -463,7 +482,7 @@ mod tests { #[test] fn test_empty_input() { - let result = extract_timesheets(&[]).unwrap(); + let result = extract_timesheets(&[], past_now()).unwrap(); assert!(result.is_empty()); } @@ -483,7 +502,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards); + let result = extract_timesheets(&shards, past_now()); assert!(result.is_err()); let err = result.unwrap_err(); @@ -511,7 +530,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards); + let result = extract_timesheets(&shards, past_now()); assert!(result.is_err()); let err = result.unwrap_err(); @@ -534,7 +553,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards).unwrap(); + let result = extract_timesheets(&shards, past_now()).unwrap(); assert!(result.is_empty()); }