Compare commits

..

15 commits

Author SHA1 Message Date
d11a35c157
refactor(timesheet): use repeat() for separator lines and sort points before grouping
All checks were successful
Continuous Integration / Build Package (push) Successful in 25s
Continuous Integration / Lint, Check & Test (push) Successful in 41s
2026-04-02 17:07:36 +02:00
b51fb511ac
fix(timesheet): display zero hours as positive instead of negative zero 2026-04-02 17:07:06 +02:00
7bee32886f
feat(timesheet): sort months ascending so newest is at bottom 2026-04-02 17:06:28 +02:00
070a47e241
docs: add timesheet management documentation
Update README.md with:
- Repository configuration section for .streamd.toml
- Timesheet periods configuration example
- Description of timesheet report features

Update REQUIREMENTS.md with:
- R18a: Timesheet report configuration format
- R18b: Day type rules for expected/actual hours calculation
- R18c: Timesheet report warning types
- Updated R20 command description
2026-04-02 16:22:28 +02:00
ca43106486
feat(timesheet): add formatted report output to CLI
Replace CSV output with formatted table display showing:
- Monthly breakdown with expected/actual hours per day
- Day types (Regular, Sick Leave, Vacation, Holiday, Flex Day, etc.)
- Warning indicators for missing days and overlapping timecards
- Monthly summaries with total expected/actual hours
- Cumulative balance across all months
- Detailed warnings section at the end

Shows helpful message when no .streamd.toml configuration is found.
2026-04-02 16:22:27 +02:00
1a716f6d0e
feat(timesheet): add report generation logic
Implement the core report generation that:
- Loads .streamd.toml configuration
- Calculates expected hours based on periods and day types
- Calculates actual hours following day type rules:
  - Sick leave: max(expected, worked)
  - Vacation: expected + worked
  - Flex day: 0
  - Holiday: 0 expected
- Generates warnings for missing days, overlapping timecards,
  and work outside configured periods
- Calculates monthly and cumulative balances
- Sorts months in descending order
2026-04-02 16:22:26 +02:00
e0ba2cddf3
feat(timesheet): add overlap detection for timecards
Add find_overlapping_timecards function to detect overlapping time
ranges on the same day. This is used to generate warnings in the
timesheet report.
2026-04-02 16:22:25 +02:00
92ca364e55
feat(timesheet): add report data structures
Add DayReport, DayType, DayWarning, MonthReport, and TimesheetReport
structs for generating timesheet reports. These support:
- Day types (Regular, SickLeave, Vacation, Holiday, FlexDay, Weekend, Missing, OutsidePeriod)
- Warnings for missing days, overlapping timecards, and outside-period work
- Monthly and cumulative calculations
2026-04-02 16:22:24 +02:00
92c8e9712a
feat(timesheet): add configuration model with period validation
Add TimesheetConfig and Period structs for configuring timesheet
periods with expected working hours per week. Include RepositoryConfig
for loading .streamd.toml with timezone and timesheet configuration.

Validation ensures:
- No overlapping periods
- Valid date ranges (start <= end)

Also adds chrono-tz dependency for timezone support.
2026-04-02 16:22:21 +02:00
38597e9fbb fix(deps): update rust crate directories to v6
All checks were successful
Continuous Integration / Lint, Check & Test (push) Successful in 3m2s
Continuous Integration / Build Package (push) Successful in 3m32s
2026-04-02 16:19:34 +02:00
55d55ffb25 fix(deps): update rust crate toml to v1
Some checks failed
Continuous Integration / Lint, Check & Test (push) Has been cancelled
Continuous Integration / Build Package (push) Has been cancelled
2026-03-31 00:08:52 +00:00
d548842fbb fix(deps): update rust crate toml to 0.9
All checks were successful
Continuous Integration / Lint, Check & Test (push) Successful in 2m18s
Continuous Integration / Build Package (push) Successful in 2m25s
2026-03-30 02:18:22 +02:00
3b2d8c7e63 fix(deps): update rust crate pulldown-cmark to 0.13
Some checks are pending
Continuous Integration / Lint, Check & Test (push) Waiting to run
Continuous Integration / Build Package (push) Waiting to run
2026-03-30 02:18:10 +02:00
a7579a7083 fix(deps): update rust crate itertools to 0.14
Some checks are pending
Continuous Integration / Lint, Check & Test (push) Waiting to run
Continuous Integration / Build Package (push) Waiting to run
2026-03-30 02:15:24 +02:00
f29840313d chore(deps): pin dependencies
All checks were successful
Continuous Integration / Lint, Check & Test (push) Successful in 2m48s
Continuous Integration / Build Package (push) Successful in 3m9s
2026-03-30 00:06:20 +00:00
5 changed files with 79 additions and 172 deletions

195
Cargo.lock generated
View file

@ -71,7 +71,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys",
] ]
[[package]] [[package]]
@ -82,7 +82,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"once_cell_polyfill", "once_cell_polyfill",
"windows-sys 0.61.2", "windows-sys",
] ]
[[package]] [[package]]
@ -254,23 +254,23 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]] [[package]]
name = "directories" name = "directories"
version = "5.0.1" version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
dependencies = [ dependencies = [
"dirs-sys", "dirs-sys",
] ]
[[package]] [[package]]
name = "dirs-sys" name = "dirs-sys"
version = "0.4.1" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users", "redox_users",
"windows-sys 0.48.0", "windows-sys",
] ]
[[package]] [[package]]
@ -292,7 +292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys",
] ]
[[package]] [[package]]
@ -429,9 +429,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.13.0" version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [ dependencies = [
"either", "either",
] ]
@ -650,9 +650,9 @@ dependencies = [
[[package]] [[package]]
name = "pulldown-cmark" name = "pulldown-cmark"
version = "0.12.2" version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"getopts", "getopts",
@ -699,13 +699,13 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
[[package]] [[package]]
name = "redox_users" name = "redox_users"
version = "0.4.6" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [ dependencies = [
"getrandom 0.2.17", "getrandom 0.2.17",
"libredox", "libredox",
"thiserror 1.0.69", "thiserror",
] ]
[[package]] [[package]]
@ -753,7 +753,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.61.2", "windows-sys",
] ]
[[package]] [[package]]
@ -822,11 +822,11 @@ dependencies = [
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.9" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98"
dependencies = [ dependencies = [
"serde", "serde_core",
] ]
[[package]] [[package]]
@ -859,7 +859,7 @@ dependencies = [
"regex", "regex",
"serde", "serde",
"tempfile", "tempfile",
"thiserror 2.0.18", "thiserror",
"toml", "toml",
"walkdir", "walkdir",
] ]
@ -912,7 +912,7 @@ dependencies = [
"getrandom 0.4.2", "getrandom 0.4.2",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.61.2", "windows-sys",
] ]
[[package]] [[package]]
@ -922,7 +922,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
dependencies = [ dependencies = [
"rustix", "rustix",
"windows-sys 0.61.2", "windows-sys",
] ]
[[package]] [[package]]
@ -935,33 +935,13 @@ dependencies = [
"unicode-width 0.2.2", "unicode-width 0.2.2",
] ]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.18" version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [ dependencies = [
"thiserror-impl 2.0.18", "thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
] ]
[[package]] [[package]]
@ -977,44 +957,42 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.23" version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde_core",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"toml_write", "toml_parser",
"toml_writer",
"winnow", "winnow",
] ]
[[package]] [[package]]
name = "toml_write" name = "toml_datetime"
version = "0.1.2" version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed"
[[package]] [[package]]
name = "unicase" name = "unicase"
@ -1177,7 +1155,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys",
] ]
[[package]] [[package]]
@ -1239,15 +1217,6 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"
@ -1257,71 +1226,11 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.15" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"

View file

@ -11,22 +11,22 @@ repository = "https://github.com/konstantinfickel/streamd"
clap = { version = "4", features = ["derive", "env"] } clap = { version = "4", features = ["derive", "env"] }
clap_complete = "4" clap_complete = "4"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
toml = "0.8" toml = "1.0"
thiserror = "2" thiserror = "2"
miette = { version = "7", features = ["fancy"] } miette = { version = "7", features = ["fancy"] }
pulldown-cmark = "0.12" pulldown-cmark = "0.13"
regex = "1" regex = "1"
once_cell = "1" once_cell = "1"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
chrono-tz = "0.9" chrono-tz = "0.9"
walkdir = "2" walkdir = "2"
indexmap = { version = "2", features = ["serde"] } indexmap = { version = "2", features = ["serde"] }
itertools = "0.13" itertools = "0.14"
directories = "5" directories = "6"
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1" pretty_assertions = "=1.4.1"
tempfile = "3" tempfile = "=3.27.0"
[[bin]] [[bin]]
name = "streamd" name = "streamd"

View file

@ -5,6 +5,9 @@ use chrono::Datelike;
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::config::Settings; use crate::config::Settings;
const SEPARATOR_WIDTH: usize = 71;
const COLUMN_SEPARATOR_WIDTH: usize = 65;
use crate::error::StreamdError; use crate::error::StreamdError;
use crate::extract::parse_markdown_file; use crate::extract::parse_markdown_file;
use crate::localize::localize_stream_file; use crate::localize::localize_stream_file;
@ -40,7 +43,7 @@ fn load_all_shards(base_folder: &Path) -> Result<Vec<LocalizedShard>, StreamdErr
/// Format hours with sign for display. /// Format hours with sign for display.
fn format_diff(hours: f64) -> String { fn format_diff(hours: f64) -> String {
if hours >= 0.0 { if hours >= 0.0 {
format!("+{:.1}h", hours) format!("+{:.1}h", hours.abs())
} else { } else {
format!("{:.1}h", hours) format!("{:.1}h", hours)
} }
@ -48,7 +51,7 @@ fn format_diff(hours: f64) -> String {
/// Format hours for display without sign. /// Format hours for display without sign.
fn format_hours(hours: f64) -> String { fn format_hours(hours: f64) -> String {
format!("{:.1}h", hours) format!("{:.1}h", hours.abs())
} }
/// Get the weekday abbreviation. /// Get the weekday abbreviation.
@ -66,13 +69,10 @@ fn weekday_abbrev(date: chrono::NaiveDate) -> &'static str {
/// Print the timesheet report header. /// Print the timesheet report header.
fn print_header() { fn print_header() {
println!( let double_line = "\u{2550}".repeat(SEPARATOR_WIDTH);
"\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}" println!("{}", double_line);
);
println!(" TIMESHEET REPORT"); println!(" TIMESHEET REPORT");
println!( println!("{}", double_line);
"\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}"
);
println!(); println!();
} }
@ -91,10 +91,9 @@ fn print_month(month: &MonthReport) {
println!(); println!();
// Column headers // Column headers
let light_line = "\u{2500}".repeat(COLUMN_SEPARATOR_WIDTH);
println!(" Date Day Expected Actual Diff Type"); println!(" Date Day Expected Actual Diff Type");
println!( println!(" {}", light_line);
" \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}"
);
// Day rows // Day rows
for day in &month.days { for day in &month.days {
@ -132,9 +131,7 @@ fn print_month(month: &MonthReport) {
} }
// Monthly totals // Monthly totals
println!( println!(" {}", light_line);
" \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}"
);
println!( println!(
" Monthly: {:>7} {:>7} {:>6}", " Monthly: {:>7} {:>7} {:>6}",
format_hours(month.total_expected()), format_hours(month.total_expected()),
@ -146,16 +143,13 @@ fn print_month(month: &MonthReport) {
/// Print the cumulative balance. /// Print the cumulative balance.
fn print_cumulative_balance(balance: f64) { fn print_cumulative_balance(balance: f64) {
println!( let light_line = "\u{2500}".repeat(SEPARATOR_WIDTH);
"\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}" println!("{}", light_line);
);
println!( println!(
" CUMULATIVE BALANCE: {}", " CUMULATIVE BALANCE: {}",
format_diff(balance) format_diff(balance)
); );
println!( println!("{}", light_line);
"\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}"
);
} }
/// Print warnings section. /// Print warnings section.

View file

@ -136,8 +136,12 @@ fn aggregate_timecard_day(points: &[TimesheetPoint]) -> Result<Option<Timesheet>
fn aggregate_timecards(points: &[TimesheetPoint]) -> Result<Vec<Timesheet>, StreamdError> { fn aggregate_timecards(points: &[TimesheetPoint]) -> Result<Vec<Timesheet>, StreamdError> {
let mut timesheets = Vec::new(); let mut timesheets = Vec::new();
// Sort points by moment to ensure proper grouping
let mut sorted_points = points.to_vec();
sorted_points.sort_by_key(|p| p.moment);
// Group by date // Group by date
for (_date, group) in &points.iter().chunk_by(|p| p.moment.date_naive()) { for (_date, group) in &sorted_points.iter().chunk_by(|p| p.moment.date_naive()) {
let day_points: Vec<_> = group.cloned().collect(); 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)? {
timesheets.push(timesheet); timesheets.push(timesheet);

View file

@ -235,11 +235,11 @@ pub fn generate_report(
} }
} }
// Sort months in descending order (most recent first) // Sort months in ascending order (oldest first, newest at bottom)
month_reports.sort_by(|a, b| { month_reports.sort_by(|a, b| {
let a_date = (a.year, a.month); let a_date = (a.year, a.month);
let b_date = (b.year, b.month); let b_date = (b.year, b.month);
b_date.cmp(&a_date) a_date.cmp(&b_date)
}); });
Ok(TimesheetReport::new() Ok(TimesheetReport::new()