Timesheet Mangement #29

Closed
opened 2026-02-01 17:46:54 +01:00 by kfickel · 4 comments
Owner

In every folder with Stream-md-files, there shall be a .streamd.toml-file.

This .stream.toml-file should contain a timesheet-section.

In the timesheet-section, there is an array of periods. Every period has a start day, an end day, and the expected working hours per week during that time.

streamd timesheet should be an alias for streamd timesheet report give the following output:
For every day intersecting with period, it should calculate the expected working hours (working days are Monday - Friday, so for an expected working hours of 38, there should be 38/5 expected working hours from Monday to Friday and 0 on the weekend. It should then also calculate the actual working hours (on sick leaves, it's always filled up the actual hours to the full day working hours artificially with the expected working hours staying the same, same for vacation etc; for overhour days actual hours should be 0 (with standard expected hours); for holidays (like easter) the expected hours should be 0.
For every month (descening) from now, print out a table of the days with their expected and actual working hours. Also, give the difference of actual and expected working hours for every month (next to the month title). In the PLAN, show a sample output.

In every folder with Stream-md-files, there shall be a .streamd.toml-file. This .stream.toml-file should contain a timesheet-section. In the timesheet-section, there is an array of `periods`. Every period has a start day, an end day, and the expected working hours per week during that time. `streamd timesheet` should be an alias for `streamd timesheet report` give the following output: For every day intersecting with period, it should calculate the expected working hours (working days are Monday - Friday, so for an expected working hours of 38, there should be 38/5 expected working hours from Monday to Friday and 0 on the weekend. It should then also calculate the actual working hours (on sick leaves, it's always filled up the actual hours to the full day working hours artificially with the expected working hours staying the same, same for vacation etc; for overhour days actual hours should be 0 (with standard expected hours); for holidays (like easter) the expected hours should be 0. For every month (descening) from now, print out a table of the days with their expected and actual working hours. Also, give the difference of actual and expected working hours for every month (next to the month title). In the PLAN, show a sample output.
Author
Owner

Implementation Plan: Timesheet Management

Overview

Implement streamd timesheet (alias for streamd timesheet report) that displays a monthly breakdown of expected vs actual working hours based on configured periods in .streamd.toml.

Configuration Format

Add to .streamd.toml:

timezone = "Europe/Berlin"  # At root level, not in [timesheet]

[timesheet]
[[timesheet.periods]]
start = "2026-01-01"
end = "2026-06-30"
hours_per_week = 38.0

[[timesheet.periods]]
start = "2026-07-01"
end = "2026-12-31"
hours_per_week = 40.0

Configuration Rules

  • Dates use ISO 8601 format (YYYY-MM-DD)
  • Periods must not overlap (validation error if they do)
  • Gaps between periods are allowed - days in gaps have 0 expected hours and are not shown
  • hours_per_week is distributed over Mon-Fri (e.g., 38h/week = 7.6h/day)

Sample Output

═══════════════════════════════════════════════════════════════════════
                          TIMESHEET REPORT
═══════════════════════════════════════════════════════════════════════

═══ March 2026 ════════════════════════════════════════ Diff: +2.3h ═══

  Date       Day   Expected   Actual   Diff    Type
  ─────────────────────────────────────────────────────────────────────
  2026-03-02 Mon      7.6h     8.2h   +0.6h
  2026-03-03 Tue      7.6h     7.6h    0.0h
  2026-03-04 Wed      7.6h     0.0h   -7.6h    [MISSING]
  2026-03-05 Thu      7.6h     9.0h   +1.4h
  2026-03-06 Fri      7.6h     7.9h   +0.3h
  2026-03-09 Mon      7.6h     7.6h    0.0h    Sick Leave
  2026-03-10 Tue      7.6h    10.2h   +2.6h    Vacation
  2026-03-11 Wed      0.0h     0.0h    0.0h    Holiday
  2026-03-12 Thu      7.6h     0.0h   -7.6h    Flex Day
  ...
  ─────────────────────────────────────────────────────────────────────
  Monthly:         152.0h   154.3h   +2.3h

═══ February 2026 ═════════════════════════════════════ Diff: -1.2h ═══
  ...

───────────────────────────────────────────────────────────────────────
                    CUMULATIVE BALANCE: +1.1h
───────────────────────────────────────────────────────────────────────

⚠ Warning: Work logged outside configured periods:
  - 2026-02-15: 3.5h worked (no period configured)

Day Type Rules

Day Type Expected Hours Actual Hours
Regular work day period.hours_per_week / 5 Sum of timecards
Weekend (Sat/Sun) 0 Sum of timecards (hidden if 0)
Sick Leave (@SickLeave) Normal expected max(expected, worked)
Vacation (@VacationDay) Normal expected expected + worked
Holiday (@Holiday) 0 Sum of timecards
Flex Day (@UndertimeDay) Normal expected 0
Day in gap (no period) 0 Sum of timecards + warning
Missing (no entries) Normal expected 0

Implementation Steps

1. Extend Configuration Model (src/config.rs or new src/timesheet/config.rs)

#[derive(Debug, Deserialize)]
pub struct TimesheetConfig {
    pub periods: Vec<Period>,
}

#[derive(Debug, Deserialize)]
pub struct Period {
    pub start: NaiveDate,
    pub end: NaiveDate,
    pub hours_per_week: f64,
}

// Add to root config:
pub struct RepositoryConfig {
    pub timezone: Option<String>,  // e.g., "Europe/Berlin"
    pub timesheet: Option<TimesheetConfig>,
    // ... existing fields
}

Add validation:

  • Ensure no periods overlap
  • Ensure start <= end for each period

2. Create Report Data Structures (src/timesheet/report.rs)

pub struct DayReport {
    pub date: NaiveDate,
    pub expected_hours: f64,
    pub actual_hours: f64,
    pub day_type: DayType,
}

pub enum DayType {
    Regular,
    SickLeave,
    Vacation,
    Holiday,
    FlexDay,
    Weekend,
    Missing,
    OutsidePeriod,
}

pub struct MonthReport {
    pub year: i32,
    pub month: u32,
    pub days: Vec<DayReport>,
    pub total_expected: f64,
    pub total_actual: f64,
}

pub struct TimesheetReport {
    pub months: Vec<MonthReport>,
    pub cumulative_balance: f64,
    pub warnings: Vec<String>,
}

3. Implement Report Generation (src/timesheet/report.rs)

  1. Load .streamd.toml from base_folder
  2. Parse timezone and periods configuration
  3. Validate periods (no overlaps)
  4. Load all markdown files and extract timesheets (existing logic)
  5. For each month that intersects with a period:
    • Generate day reports for Mon-Fri
    • Include weekends only if work was logged
    • Calculate expected hours (proportional for partial weeks at period boundaries)
    • Calculate actual hours based on day type rules
  6. Calculate monthly differences and cumulative balance
  7. Collect warnings for work logged outside periods

4. Update CLI (src/cli/commands/timesheet.rs)

  • Make timesheet the default subcommand (alias for report)
  • Remove current CSV output
  • Implement formatted table output as shown above
  • Use configurable timezone for day boundary calculations

5. Add Timezone Support

  • Add chrono-tz dependency for timezone handling
  • Convert UTC timestamps to local timezone when determining which day an entry belongs to
  • Default to system local time if no timezone configured

Files to Modify/Create

File Action
src/config.rs Add timezone field to root config
src/timesheet/config.rs NEW - TimesheetConfig, Period structs, validation
src/timesheet/report.rs NEW - Report structs and generation logic
src/timesheet/mod.rs Export new modules
src/cli/commands/timesheet.rs Replace CSV output with formatted report
Cargo.toml Add chrono-tz dependency
REQUIREMENTS.md Document timesheet feature
README.md Update with timesheet usage

Testing Strategy

  1. Unit tests for period validation (no overlaps, valid dates)
  2. Unit tests for expected hours calculation (proportional weeks)
  3. Unit tests for day type detection and hours calculation
  4. Integration tests with sample .streamd.toml and markdown files
  5. Edge cases:
    • Empty periods array
    • Work on weekends
    • Work in gaps between periods
    • Partial first/last weeks of periods

Open Questions Resolved

  • Holidays: Detected from @Holiday markers in markdown
  • No leave balance tracking
  • Flex days (@UndertimeDay) = taking accumulated overtime
  • Gaps allowed, overlaps rejected
  • ISO date format
  • Proportional partial weeks
  • Show per-month + cumulative balance
  • Row per day layout
  • Missing days show as 0 actual
  • Sick leave: max(expected, worked)
  • Vacation: expected + worked
  • Configurable timezone (root level)
  • Hide weekends unless work logged
  • Decimal hour format (7.6h)
  • Cumulative starts at zero
  • Only show months intersecting periods
  • Work in gaps shows with warning
# Implementation Plan: Timesheet Management ## Overview Implement `streamd timesheet` (alias for `streamd timesheet report`) that displays a monthly breakdown of expected vs actual working hours based on configured periods in `.streamd.toml`. ## Configuration Format Add to `.streamd.toml`: ```toml timezone = "Europe/Berlin" # At root level, not in [timesheet] [timesheet] [[timesheet.periods]] start = "2026-01-01" end = "2026-06-30" hours_per_week = 38.0 [[timesheet.periods]] start = "2026-07-01" end = "2026-12-31" hours_per_week = 40.0 ``` ### Configuration Rules - Dates use ISO 8601 format (`YYYY-MM-DD`) - Periods must not overlap (validation error if they do) - Gaps between periods are allowed - days in gaps have 0 expected hours and are not shown - `hours_per_week` is distributed over Mon-Fri (e.g., 38h/week = 7.6h/day) ## Sample Output ``` ═══════════════════════════════════════════════════════════════════════ TIMESHEET REPORT ═══════════════════════════════════════════════════════════════════════ ═══ March 2026 ════════════════════════════════════════ Diff: +2.3h ═══ Date Day Expected Actual Diff Type ───────────────────────────────────────────────────────────────────── 2026-03-02 Mon 7.6h 8.2h +0.6h 2026-03-03 Tue 7.6h 7.6h 0.0h 2026-03-04 Wed 7.6h 0.0h -7.6h [MISSING] 2026-03-05 Thu 7.6h 9.0h +1.4h 2026-03-06 Fri 7.6h 7.9h +0.3h 2026-03-09 Mon 7.6h 7.6h 0.0h Sick Leave 2026-03-10 Tue 7.6h 10.2h +2.6h Vacation 2026-03-11 Wed 0.0h 0.0h 0.0h Holiday 2026-03-12 Thu 7.6h 0.0h -7.6h Flex Day ... ───────────────────────────────────────────────────────────────────── Monthly: 152.0h 154.3h +2.3h ═══ February 2026 ═════════════════════════════════════ Diff: -1.2h ═══ ... ─────────────────────────────────────────────────────────────────────── CUMULATIVE BALANCE: +1.1h ─────────────────────────────────────────────────────────────────────── ⚠ Warning: Work logged outside configured periods: - 2026-02-15: 3.5h worked (no period configured) ``` ## Day Type Rules | Day Type | Expected Hours | Actual Hours | |----------|---------------|--------------| | Regular work day | period.hours_per_week / 5 | Sum of timecards | | Weekend (Sat/Sun) | 0 | Sum of timecards (hidden if 0) | | Sick Leave (@SickLeave) | Normal expected | max(expected, worked) | | Vacation (@VacationDay) | Normal expected | expected + worked | | Holiday (@Holiday) | 0 | Sum of timecards | | Flex Day (@UndertimeDay) | Normal expected | 0 | | Day in gap (no period) | 0 | Sum of timecards + warning | | Missing (no entries) | Normal expected | 0 | ## Implementation Steps ### 1. Extend Configuration Model (`src/config.rs` or new `src/timesheet/config.rs`) ```rust #[derive(Debug, Deserialize)] pub struct TimesheetConfig { pub periods: Vec<Period>, } #[derive(Debug, Deserialize)] pub struct Period { pub start: NaiveDate, pub end: NaiveDate, pub hours_per_week: f64, } // Add to root config: pub struct RepositoryConfig { pub timezone: Option<String>, // e.g., "Europe/Berlin" pub timesheet: Option<TimesheetConfig>, // ... existing fields } ``` Add validation: - Ensure no periods overlap - Ensure `start <= end` for each period ### 2. Create Report Data Structures (`src/timesheet/report.rs`) ```rust pub struct DayReport { pub date: NaiveDate, pub expected_hours: f64, pub actual_hours: f64, pub day_type: DayType, } pub enum DayType { Regular, SickLeave, Vacation, Holiday, FlexDay, Weekend, Missing, OutsidePeriod, } pub struct MonthReport { pub year: i32, pub month: u32, pub days: Vec<DayReport>, pub total_expected: f64, pub total_actual: f64, } pub struct TimesheetReport { pub months: Vec<MonthReport>, pub cumulative_balance: f64, pub warnings: Vec<String>, } ``` ### 3. Implement Report Generation (`src/timesheet/report.rs`) 1. Load `.streamd.toml` from base_folder 2. Parse timezone and periods configuration 3. Validate periods (no overlaps) 4. Load all markdown files and extract timesheets (existing logic) 5. For each month that intersects with a period: - Generate day reports for Mon-Fri - Include weekends only if work was logged - Calculate expected hours (proportional for partial weeks at period boundaries) - Calculate actual hours based on day type rules 6. Calculate monthly differences and cumulative balance 7. Collect warnings for work logged outside periods ### 4. Update CLI (`src/cli/commands/timesheet.rs`) - Make `timesheet` the default subcommand (alias for `report`) - Remove current CSV output - Implement formatted table output as shown above - Use configurable timezone for day boundary calculations ### 5. Add Timezone Support - Add `chrono-tz` dependency for timezone handling - Convert UTC timestamps to local timezone when determining which day an entry belongs to - Default to system local time if no timezone configured ## Files to Modify/Create | File | Action | |------|--------| | `src/config.rs` | Add timezone field to root config | | `src/timesheet/config.rs` | **NEW** - TimesheetConfig, Period structs, validation | | `src/timesheet/report.rs` | **NEW** - Report structs and generation logic | | `src/timesheet/mod.rs` | Export new modules | | `src/cli/commands/timesheet.rs` | Replace CSV output with formatted report | | `Cargo.toml` | Add `chrono-tz` dependency | | `REQUIREMENTS.md` | Document timesheet feature | | `README.md` | Update with timesheet usage | ## Testing Strategy 1. **Unit tests** for period validation (no overlaps, valid dates) 2. **Unit tests** for expected hours calculation (proportional weeks) 3. **Unit tests** for day type detection and hours calculation 4. **Integration tests** with sample `.streamd.toml` and markdown files 5. **Edge cases**: - Empty periods array - Work on weekends - Work in gaps between periods - Partial first/last weeks of periods ## Open Questions Resolved - ✅ Holidays: Detected from @Holiday markers in markdown - ✅ No leave balance tracking - ✅ Flex days (@UndertimeDay) = taking accumulated overtime - ✅ Gaps allowed, overlaps rejected - ✅ ISO date format - ✅ Proportional partial weeks - ✅ Show per-month + cumulative balance - ✅ Row per day layout - ✅ Missing days show as 0 actual - ✅ Sick leave: max(expected, worked) - ✅ Vacation: expected + worked - ✅ Configurable timezone (root level) - ✅ Hide weekends unless work logged - ✅ Decimal hour format (7.6h) - ✅ Cumulative starts at zero - ✅ Only show months intersecting periods - ✅ Work in gaps shows with warning
Author
Owner

I forgot about the warnings.
The timesheet should warn whenever

  • There a days that are empty without an explanation.
  • There are overlapping timecards
I forgot about the warnings. The timesheet should warn whenever * There a days that are empty without an explanation. * There are overlapping timecards
Author
Owner

Implementation Plan: Timesheet Management (v2)

Overview

Implement streamd timesheet (alias for streamd timesheet report) that displays a monthly breakdown of expected vs actual working hours based on configured periods in .streamd.toml.

Configuration Format

Add to .streamd.toml:

timezone = "Europe/Berlin"  # At root level, not in [timesheet]

[timesheet]
[[timesheet.periods]]
start = "2026-01-01"
end = "2026-06-30"
hours_per_week = 38.0

[[timesheet.periods]]
start = "2026-07-01"
end = "2026-12-31"
hours_per_week = 40.0

Configuration Rules

  • Dates use ISO 8601 format (YYYY-MM-DD)
  • Periods must not overlap (validation error if they do)
  • Gaps between periods are allowed - days in gaps have 0 expected hours and are not shown
  • hours_per_week is distributed over Mon-Fri (e.g., 38h/week = 7.6h/day)

Sample Output

═══════════════════════════════════════════════════════════════════════
                          TIMESHEET REPORT
═══════════════════════════════════════════════════════════════════════

═══ March 2026 ════════════════════════════════════════ Diff: +2.3h ═══

  Date       Day   Expected   Actual   Diff    Type
  ─────────────────────────────────────────────────────────────────────
  2026-03-02 Mon      7.6h     8.2h   +0.6h
  2026-03-03 Tue      7.6h     7.6h    0.0h
  2026-03-04 Wed      7.6h     0.0h   -7.6h    ⚠ Missing
  2026-03-05 Thu      7.6h     9.0h   +1.4h
  2026-03-06 Fri      7.6h     7.9h   +0.3h
  2026-03-09 Mon      7.6h     7.6h    0.0h    Sick Leave
  2026-03-10 Tue      7.6h    10.2h   +2.6h    Vacation
  2026-03-11 Wed      0.0h     0.0h    0.0h    Holiday
  2026-03-12 Thu      7.6h     0.0h   -7.6h    Flex Day
  2026-03-13 Fri      7.6h     8.5h   +0.9h    ⚠ Overlap
  ...
  ─────────────────────────────────────────────────────────────────────
  Monthly:         152.0h   154.3h   +2.3h

═══ February 2026 ═════════════════════════════════════ Diff: -1.2h ═══
  ...

───────────────────────────────────────────────────────────────────────
                    CUMULATIVE BALANCE: +1.1h
───────────────────────────────────────────────────────────────────────

⚠ Warnings:

  Missing days without explanation:
    - 2026-03-04 (Wed): No entries and no leave/holiday marker

  Overlapping timecards:
    - 2026-03-13: 09:00-12:30 overlaps with 12:00-13:00

  Work logged outside configured periods:
    - 2026-02-15: 3.5h worked (no period configured)

Day Type Rules

Day Type Expected Hours Actual Hours
Regular work day period.hours_per_week / 5 Sum of timecards
Weekend (Sat/Sun) 0 Sum of timecards (hidden if 0)
Sick Leave (@SickLeave) Normal expected max(expected, worked)
Vacation (@VacationDay) Normal expected expected + worked
Holiday (@Holiday) 0 Sum of timecards
Flex Day (@UndertimeDay) Normal expected 0
Day in gap (no period) 0 Sum of timecards + warning
Missing (no entries) Normal expected 0 + warning

Warning Types

The report generates three categories of warnings:

1. Missing Days Without Explanation

A warning is generated when a weekday (Mon-Fri) within a configured period has:

  • No timecard entries logged
  • No special day type marker (@SickLeave, @VacationDay, @Holiday, @UndertimeDay)

This helps identify forgotten entries that need to be filled in.

2. Overlapping Timecards

A warning is generated when two or more timecards on the same day have overlapping time ranges. For example:

  • 09:00-12:30 and 12:00-13:00 overlap by 30 minutes
  • 14:00-18:00 and 15:00-16:00 (nested overlap)

The warning shows both timecard ranges to help identify the conflict.

3. Work Outside Configured Periods

A warning is generated when work is logged on a day that falls outside all configured periods (in a gap or before/after all periods).

Implementation Steps

1. Extend Configuration Model (src/config.rs or new src/timesheet/config.rs)

#[derive(Debug, Deserialize)]
pub struct TimesheetConfig {
    pub periods: Vec<Period>,
}

#[derive(Debug, Deserialize)]
pub struct Period {
    pub start: NaiveDate,
    pub end: NaiveDate,
    pub hours_per_week: f64,
}

// Add to root config:
pub struct RepositoryConfig {
    pub timezone: Option<String>,  // e.g., "Europe/Berlin"
    pub timesheet: Option<TimesheetConfig>,
    // ... existing fields
}

Add validation:

  • Ensure no periods overlap
  • Ensure start <= end for each period

2. Create Report Data Structures (src/timesheet/report.rs)

pub struct DayReport {
    pub date: NaiveDate,
    pub expected_hours: f64,
    pub actual_hours: f64,
    pub day_type: DayType,
    pub warnings: Vec<DayWarning>,
}

pub enum DayType {
    Regular,
    SickLeave,
    Vacation,
    Holiday,
    FlexDay,
    Weekend,
    Missing,
    OutsidePeriod,
}

pub enum DayWarning {
    MissingWithoutExplanation,
    OverlappingTimecards { 
        first: (NaiveTime, NaiveTime), 
        second: (NaiveTime, NaiveTime) 
    },
    OutsidePeriod { hours_worked: f64 },
}

pub struct MonthReport {
    pub year: i32,
    pub month: u32,
    pub days: Vec<DayReport>,
    pub total_expected: f64,
    pub total_actual: f64,
}

pub struct TimesheetReport {
    pub months: Vec<MonthReport>,
    pub cumulative_balance: f64,
    pub warnings: Vec<ReportWarning>,
}

pub struct ReportWarning {
    pub date: NaiveDate,
    pub warning_type: DayWarning,
}

3. Implement Overlap Detection (src/timesheet/validation.rs)

/// Check if two time ranges overlap
fn timecards_overlap(a: &Timecard, b: &Timecard) -> bool {
    a.from_time < b.to_time && b.from_time < a.to_time
}

/// Find all overlapping timecard pairs for a day
pub fn find_overlapping_timecards(timecards: &[Timecard]) -> Vec<(Timecard, Timecard)> {
    let mut overlaps = Vec::new();
    for i in 0..timecards.len() {
        for j in (i + 1)..timecards.len() {
            if timecards_overlap(&timecards[i], &timecards[j]) {
                overlaps.push((timecards[i].clone(), timecards[j].clone()));
            }
        }
    }
    overlaps
}

4. Implement Report Generation (src/timesheet/report.rs)

  1. Load .streamd.toml from base_folder
  2. Parse timezone and periods configuration
  3. Validate periods (no overlaps)
  4. Load all markdown files and extract timesheets (existing logic)
  5. For each month that intersects with a period:
    • Generate day reports for Mon-Fri
    • Include weekends only if work was logged
    • Calculate expected hours (proportional for partial weeks at period boundaries)
    • Calculate actual hours based on day type rules
    • Check for overlapping timecards and add warnings
    • Check for missing days without explanation and add warnings
  6. Calculate monthly differences and cumulative balance
  7. Collect warnings for work logged outside periods

5. Update CLI (src/cli/commands/timesheet.rs)

  • Make timesheet the default subcommand (alias for report)
  • Remove current CSV output
  • Implement formatted table output as shown above
  • Display warning indicator (⚠) in the Type column for days with warnings
  • Display detailed warnings section at the end of the report
  • Use configurable timezone for day boundary calculations

6. Add Timezone Support

  • Add chrono-tz dependency for timezone handling
  • Convert UTC timestamps to local timezone when determining which day an entry belongs to
  • Default to system local time if no timezone configured

Files to Modify/Create

File Action
src/config.rs Add timezone field to root config
src/timesheet/config.rs NEW - TimesheetConfig, Period structs, validation
src/timesheet/report.rs NEW - Report structs and generation logic
src/timesheet/validation.rs NEW - Overlap detection and warning generation
src/timesheet/mod.rs Export new modules
src/cli/commands/timesheet.rs Replace CSV output with formatted report
Cargo.toml Add chrono-tz dependency
REQUIREMENTS.md Document timesheet feature
README.md Update with timesheet usage

Testing Strategy

  1. Unit tests for period validation (no overlaps, valid dates)
  2. Unit tests for expected hours calculation (proportional weeks)
  3. Unit tests for day type detection and hours calculation
  4. Unit tests for timecard overlap detection:
    • No overlap: 09:00-12:00 and 13:00-17:00
    • Partial overlap: 09:00-12:30 and 12:00-13:00
    • Full containment: 09:00-17:00 and 10:00-11:00
    • Exact match: 09:00-12:00 and 09:00-12:00
  5. Unit tests for missing day detection:
    • Day with timecards → no warning
    • Day with @SickLeave → no warning
    • Day with @VacationDay → no warning
    • Day with @Holiday → no warning
    • Day with @UndertimeDay → no warning
    • Empty day without markers → warning
  6. Integration tests with sample .streamd.toml and markdown files
  7. Edge cases:
    • Empty periods array
    • Work on weekends
    • Work in gaps between periods
    • Partial first/last weeks of periods
    • Multiple overlaps on same day

Changes from v1

  • Added warning for missing days without explanation (no timecards and no leave/holiday marker)
  • Added warning for overlapping timecards on the same day
  • Added DayWarning enum to track per-day warnings
  • Added validation.rs module for overlap detection
  • Updated sample output to show warning indicators in the table
  • Added detailed warnings section at the end of the report
  • Added test cases for overlap detection and missing day detection
# Implementation Plan: Timesheet Management (v2) ## Overview Implement `streamd timesheet` (alias for `streamd timesheet report`) that displays a monthly breakdown of expected vs actual working hours based on configured periods in `.streamd.toml`. ## Configuration Format Add to `.streamd.toml`: ```toml timezone = "Europe/Berlin" # At root level, not in [timesheet] [timesheet] [[timesheet.periods]] start = "2026-01-01" end = "2026-06-30" hours_per_week = 38.0 [[timesheet.periods]] start = "2026-07-01" end = "2026-12-31" hours_per_week = 40.0 ``` ### Configuration Rules - Dates use ISO 8601 format (`YYYY-MM-DD`) - Periods must not overlap (validation error if they do) - Gaps between periods are allowed - days in gaps have 0 expected hours and are not shown - `hours_per_week` is distributed over Mon-Fri (e.g., 38h/week = 7.6h/day) ## Sample Output ``` ═══════════════════════════════════════════════════════════════════════ TIMESHEET REPORT ═══════════════════════════════════════════════════════════════════════ ═══ March 2026 ════════════════════════════════════════ Diff: +2.3h ═══ Date Day Expected Actual Diff Type ───────────────────────────────────────────────────────────────────── 2026-03-02 Mon 7.6h 8.2h +0.6h 2026-03-03 Tue 7.6h 7.6h 0.0h 2026-03-04 Wed 7.6h 0.0h -7.6h ⚠ Missing 2026-03-05 Thu 7.6h 9.0h +1.4h 2026-03-06 Fri 7.6h 7.9h +0.3h 2026-03-09 Mon 7.6h 7.6h 0.0h Sick Leave 2026-03-10 Tue 7.6h 10.2h +2.6h Vacation 2026-03-11 Wed 0.0h 0.0h 0.0h Holiday 2026-03-12 Thu 7.6h 0.0h -7.6h Flex Day 2026-03-13 Fri 7.6h 8.5h +0.9h ⚠ Overlap ... ───────────────────────────────────────────────────────────────────── Monthly: 152.0h 154.3h +2.3h ═══ February 2026 ═════════════════════════════════════ Diff: -1.2h ═══ ... ─────────────────────────────────────────────────────────────────────── CUMULATIVE BALANCE: +1.1h ─────────────────────────────────────────────────────────────────────── ⚠ Warnings: Missing days without explanation: - 2026-03-04 (Wed): No entries and no leave/holiday marker Overlapping timecards: - 2026-03-13: 09:00-12:30 overlaps with 12:00-13:00 Work logged outside configured periods: - 2026-02-15: 3.5h worked (no period configured) ``` ## Day Type Rules | Day Type | Expected Hours | Actual Hours | |----------|---------------|--------------:| | Regular work day | period.hours_per_week / 5 | Sum of timecards | | Weekend (Sat/Sun) | 0 | Sum of timecards (hidden if 0) | | Sick Leave (@SickLeave) | Normal expected | max(expected, worked) | | Vacation (@VacationDay) | Normal expected | expected + worked | | Holiday (@Holiday) | 0 | Sum of timecards | | Flex Day (@UndertimeDay) | Normal expected | 0 | | Day in gap (no period) | 0 | Sum of timecards + warning | | Missing (no entries) | Normal expected | 0 + warning | ## Warning Types The report generates three categories of warnings: ### 1. Missing Days Without Explanation A warning is generated when a weekday (Mon-Fri) within a configured period has: - No timecard entries logged - No special day type marker (@SickLeave, @VacationDay, @Holiday, @UndertimeDay) This helps identify forgotten entries that need to be filled in. ### 2. Overlapping Timecards A warning is generated when two or more timecards on the same day have overlapping time ranges. For example: - 09:00-12:30 and 12:00-13:00 overlap by 30 minutes - 14:00-18:00 and 15:00-16:00 (nested overlap) The warning shows both timecard ranges to help identify the conflict. ### 3. Work Outside Configured Periods A warning is generated when work is logged on a day that falls outside all configured periods (in a gap or before/after all periods). ## Implementation Steps ### 1. Extend Configuration Model (`src/config.rs` or new `src/timesheet/config.rs`) ```rust #[derive(Debug, Deserialize)] pub struct TimesheetConfig { pub periods: Vec<Period>, } #[derive(Debug, Deserialize)] pub struct Period { pub start: NaiveDate, pub end: NaiveDate, pub hours_per_week: f64, } // Add to root config: pub struct RepositoryConfig { pub timezone: Option<String>, // e.g., "Europe/Berlin" pub timesheet: Option<TimesheetConfig>, // ... existing fields } ``` Add validation: - Ensure no periods overlap - Ensure `start <= end` for each period ### 2. Create Report Data Structures (`src/timesheet/report.rs`) ```rust pub struct DayReport { pub date: NaiveDate, pub expected_hours: f64, pub actual_hours: f64, pub day_type: DayType, pub warnings: Vec<DayWarning>, } pub enum DayType { Regular, SickLeave, Vacation, Holiday, FlexDay, Weekend, Missing, OutsidePeriod, } pub enum DayWarning { MissingWithoutExplanation, OverlappingTimecards { first: (NaiveTime, NaiveTime), second: (NaiveTime, NaiveTime) }, OutsidePeriod { hours_worked: f64 }, } pub struct MonthReport { pub year: i32, pub month: u32, pub days: Vec<DayReport>, pub total_expected: f64, pub total_actual: f64, } pub struct TimesheetReport { pub months: Vec<MonthReport>, pub cumulative_balance: f64, pub warnings: Vec<ReportWarning>, } pub struct ReportWarning { pub date: NaiveDate, pub warning_type: DayWarning, } ``` ### 3. Implement Overlap Detection (`src/timesheet/validation.rs`) ```rust /// Check if two time ranges overlap fn timecards_overlap(a: &Timecard, b: &Timecard) -> bool { a.from_time < b.to_time && b.from_time < a.to_time } /// Find all overlapping timecard pairs for a day pub fn find_overlapping_timecards(timecards: &[Timecard]) -> Vec<(Timecard, Timecard)> { let mut overlaps = Vec::new(); for i in 0..timecards.len() { for j in (i + 1)..timecards.len() { if timecards_overlap(&timecards[i], &timecards[j]) { overlaps.push((timecards[i].clone(), timecards[j].clone())); } } } overlaps } ``` ### 4. Implement Report Generation (`src/timesheet/report.rs`) 1. Load `.streamd.toml` from base_folder 2. Parse timezone and periods configuration 3. Validate periods (no overlaps) 4. Load all markdown files and extract timesheets (existing logic) 5. For each month that intersects with a period: - Generate day reports for Mon-Fri - Include weekends only if work was logged - Calculate expected hours (proportional for partial weeks at period boundaries) - Calculate actual hours based on day type rules - **Check for overlapping timecards and add warnings** - **Check for missing days without explanation and add warnings** 6. Calculate monthly differences and cumulative balance 7. Collect warnings for work logged outside periods ### 5. Update CLI (`src/cli/commands/timesheet.rs`) - Make `timesheet` the default subcommand (alias for `report`) - Remove current CSV output - Implement formatted table output as shown above - Display warning indicator (⚠) in the Type column for days with warnings - Display detailed warnings section at the end of the report - Use configurable timezone for day boundary calculations ### 6. Add Timezone Support - Add `chrono-tz` dependency for timezone handling - Convert UTC timestamps to local timezone when determining which day an entry belongs to - Default to system local time if no timezone configured ## Files to Modify/Create | File | Action | |------|--------| | `src/config.rs` | Add timezone field to root config | | `src/timesheet/config.rs` | **NEW** - TimesheetConfig, Period structs, validation | | `src/timesheet/report.rs` | **NEW** - Report structs and generation logic | | `src/timesheet/validation.rs` | **NEW** - Overlap detection and warning generation | | `src/timesheet/mod.rs` | Export new modules | | `src/cli/commands/timesheet.rs` | Replace CSV output with formatted report | | `Cargo.toml` | Add `chrono-tz` dependency | | `REQUIREMENTS.md` | Document timesheet feature | | `README.md` | Update with timesheet usage | ## Testing Strategy 1. **Unit tests** for period validation (no overlaps, valid dates) 2. **Unit tests** for expected hours calculation (proportional weeks) 3. **Unit tests** for day type detection and hours calculation 4. **Unit tests** for timecard overlap detection: - No overlap: 09:00-12:00 and 13:00-17:00 - Partial overlap: 09:00-12:30 and 12:00-13:00 - Full containment: 09:00-17:00 and 10:00-11:00 - Exact match: 09:00-12:00 and 09:00-12:00 5. **Unit tests** for missing day detection: - Day with timecards → no warning - Day with @SickLeave → no warning - Day with @VacationDay → no warning - Day with @Holiday → no warning - Day with @UndertimeDay → no warning - Empty day without markers → warning 6. **Integration tests** with sample `.streamd.toml` and markdown files 7. **Edge cases**: - Empty periods array - Work on weekends - Work in gaps between periods - Partial first/last weeks of periods - Multiple overlaps on same day ## Changes from v1 - ✅ Added warning for missing days without explanation (no timecards and no leave/holiday marker) - ✅ Added warning for overlapping timecards on the same day - ✅ Added `DayWarning` enum to track per-day warnings - ✅ Added `validation.rs` module for overlap detection - ✅ Updated sample output to show warning indicators in the table - ✅ Added detailed warnings section at the end of the report - ✅ Added test cases for overlap detection and missing day detection
kfickel added the
planned
label 2026-03-29 19:41:09 +02:00
Author
Owner

Implementation Complete

Branch 29_timesheet-management has been pushed with the following commits:

  1. 86433ca - feat(timesheet): add configuration model with period validation
  2. 7abf056 - feat(timesheet): add report data structures
  3. 282d83b - feat(timesheet): add overlap detection for timecards
  4. 3429f2e - feat(timesheet): add report generation logic
  5. 1119d91 - feat(timesheet): add formatted report output to CLI
  6. 14ae290 - docs: add timesheet management documentation

Implementation Summary

New files created:

  • src/timesheet/config.rs - TimesheetConfig, Period, RepositoryConfig structs
  • src/timesheet/report.rs - DayReport, DayType, DayWarning, MonthReport, TimesheetReport
  • src/timesheet/validation.rs - Overlap detection for timecards
  • src/timesheet/generator.rs - Report generation logic

Files modified:

  • Cargo.toml - Added chrono-tz dependency
  • src/timesheet/mod.rs - Export new modules
  • src/cli/commands/timesheet.rs - Replaced CSV output with formatted report
  • README.md - Added repository configuration documentation
  • REQUIREMENTS.md - Added R18a, R18b, R18c requirements

Test Coverage

  • 146 unit tests, all passing
  • Tests cover:
    • Period validation (overlaps, date ranges)
    • Expected/actual hours calculation for all day types
    • Overlap detection edge cases
    • Report generation with missing days, weekends, warnings

Stats

  • Implementation time: ~45 minutes
  • Token usage: Approximately 50,000 tokens

Please create a PR from branch 29_timesheet-management to main:
https://git.konstantinfickel.de/kfickel/streamd/compare/main...29_timesheet-management

## Implementation Complete Branch `29_timesheet-management` has been pushed with the following commits: 1. `86433ca` - feat(timesheet): add configuration model with period validation 2. `7abf056` - feat(timesheet): add report data structures 3. `282d83b` - feat(timesheet): add overlap detection for timecards 4. `3429f2e` - feat(timesheet): add report generation logic 5. `1119d91` - feat(timesheet): add formatted report output to CLI 6. `14ae290` - docs: add timesheet management documentation ### Implementation Summary **New files created:** - `src/timesheet/config.rs` - TimesheetConfig, Period, RepositoryConfig structs - `src/timesheet/report.rs` - DayReport, DayType, DayWarning, MonthReport, TimesheetReport - `src/timesheet/validation.rs` - Overlap detection for timecards - `src/timesheet/generator.rs` - Report generation logic **Files modified:** - `Cargo.toml` - Added chrono-tz dependency - `src/timesheet/mod.rs` - Export new modules - `src/cli/commands/timesheet.rs` - Replaced CSV output with formatted report - `README.md` - Added repository configuration documentation - `REQUIREMENTS.md` - Added R18a, R18b, R18c requirements ### Test Coverage - 146 unit tests, all passing - Tests cover: - Period validation (overlaps, date ranges) - Expected/actual hours calculation for all day types - Overlap detection edge cases - Report generation with missing days, weekends, warnings ### Stats - Implementation time: ~45 minutes - Token usage: Approximately 50,000 tokens Please create a PR from branch `29_timesheet-management` to `main`: https://git.konstantinfickel.de/kfickel/streamd/compare/main...29_timesheet-management
Sign in to join this conversation.
No labels
planned
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference: kfickel/streamd#29
No description provided.