Skip to content

Commit 54c3d87

Browse files
committed
feat(sickleave): add marker-based Sick Leave with optional date range
- Introduce --pos s (Sick Leave) - Support optional --from/--to (range mode) - Treat Sick Leave as non-working marker day - Exclude from ΔWORK calculations - Refactor apply() and list rendering - Update recalc_pairs_for_date signature
1 parent 98068c0 commit 54c3d87

File tree

14 files changed

+444
-71
lines changed

14 files changed

+444
-71
lines changed

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,36 @@
11
# Changelog
22

3+
## [0.8.6] - 2026-02-10
4+
5+
### Added
6+
7+
- New `Sick Leave` position (`--pos s`)
8+
- Support for optional date range with `--from` and `--to` (usable only with `--pos s`)
9+
- Marker-day logic for Sick Leave (stored as sentinel event at 00:00, similar to Holiday)
10+
11+
### Changed
12+
13+
- `Sick Leave` is now treated as a non-working marker day:
14+
- No IN/OUT times displayed in `list`
15+
- No contribution to daily or monthly ΔWORK totals
16+
- No target (TGT) calculation
17+
- Validation updated:
18+
- `--from/--to` allowed only with `--pos s`
19+
- If omitted, `--pos s` applies to the provided date only
20+
- Refactored `apply()` logic for cleaner separation between:
21+
- marker days (Holiday, NationalHoliday, SickLeave)
22+
- working days
23+
- `recalc_pairs_for_date` signature updated to accept `&Connection`
24+
(allows usage inside transactions)
25+
26+
### Fixed
27+
28+
- Prevented incorrect IN/OUT display (`00:00`) for Sick Leave in `list`
29+
- Prevented unintended ΔWORK surplus calculation for Sick Leave days
30+
- Improved argument validation consistency in `add` command
31+
32+
---
33+
334
## v0.8.5 — 2026-01-13
435

536
### Added

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rtimelogger"
3-
version = "0.8.5"
3+
version = "0.8.6"
44
edition = "2024"
55
authors = ["Umpire274 <umpire274@gmail.com>"]
66
description = "A simple cross-platform CLI tool to track working hours, lunch breaks, and calculate surplus time"

src/cli/commands/add.rs

Lines changed: 96 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,31 @@ use crate::errors::{AppError, AppResult};
55
use crate::models::location::Location;
66
use crate::utils::date;
77
use crate::utils::time::parse_optional_time;
8+
use chrono::NaiveDate;
9+
10+
fn validate_sickleave_args(
11+
pos: Location,
12+
from: Option<NaiveDate>,
13+
to: Option<NaiveDate>,
14+
) -> Result<Option<(NaiveDate, NaiveDate)>, AppError> {
15+
match (from, to) {
16+
(Some(f), Some(t)) => {
17+
if pos != Location::SickLeave {
18+
return Err(AppError::InvalidArgs(
19+
"--from/--to can only be used with --pos s".into(),
20+
));
21+
}
22+
if f > t {
23+
return Err(AppError::InvalidDateRange { from: f, to: t });
24+
}
25+
Ok(Some((f, t)))
26+
}
27+
(None, None) => Ok(None),
28+
_ => Err(AppError::InvalidArgs(
29+
"Both --from and --to must be provided together.".into(),
30+
)),
31+
}
32+
}
833

934
/// Add or update a work session.
1035
pub fn handle(cmd: &Commands, cfg: &crate::config::Config) -> AppResult<()> {
@@ -18,15 +43,12 @@ pub fn handle(cmd: &Commands, cfg: &crate::config::Config) -> AppResult<()> {
1843
end,
1944
edit_pair,
2045
edit,
46+
from,
47+
to,
2148
} = cmd
2249
{
2350
//
24-
// 1. Parse date (mandatory)
25-
//
26-
let d = date::parse_date(date).ok_or_else(|| AppError::InvalidDate(date.to_string()))?;
27-
28-
//
29-
// 2. Parse position (default = Office)
51+
// 1. Parse position (default = Office)
3052
//
3153
let pos_final = match pos {
3254
Some(code) => Location::from_code(code).ok_or_else(|| {
@@ -39,25 +61,34 @@ pub fn handle(cmd: &Commands, cfg: &crate::config::Config) -> AppResult<()> {
3961
};
4062

4163
//
42-
// 3. Parse IN time (optional)
64+
// 2. Parse date (mandatory for normal ADD)
65+
// (per SickLeave puoi anche ignorarla, ma se CLI la richiede, la parse qui va bene)
66+
//
67+
let d = date::parse_date(date).map_err(|_| AppError::InvalidDate(date.to_string()))?;
68+
69+
//
70+
// 3. Parse times (optional input)
4371
//
44-
let start_parsed = parse_optional_time(start.as_ref());
72+
let start_parsed = parse_optional_time(start.as_ref())?;
4573

4674
//
4775
// 4. Parse OUT time (optional)
4876
//
49-
let end_parsed = parse_optional_time(end.as_ref());
77+
let end_parsed = parse_optional_time(end.as_ref())?;
5078

5179
//
52-
// 5. Parse lunch break (optional)
80+
// 4. Lunch break (optional)
5381
//
54-
let lunch_opt = *lunch; // lunch è Option<i32>
82+
let lunch_opt = *lunch;
5583

5684
//
57-
// 6. Open DB
85+
// 5. Open DB
5886
//
5987
let mut pool = DbPool::new(&cfg.database)?;
6088

89+
//
90+
// 6. work_gap flag
91+
//
6192
let work_gap: Option<bool> = if *work_gap {
6293
Some(true)
6394
} else if *no_work_gap {
@@ -67,21 +98,59 @@ pub fn handle(cmd: &Commands, cfg: &crate::config::Config) -> AppResult<()> {
6798
};
6899

69100
//
70-
// 7. Execute logic
71-
//
72-
AddLogic::apply(
73-
cfg,
74-
&mut pool,
75-
d,
76-
pos_final,
77-
start_parsed.unwrap(),
78-
lunch_opt,
79-
work_gap,
80-
end_parsed.unwrap(),
81-
*edit,
82-
*edit_pair,
83-
pos.clone(), // used for audit logging
84-
)?;
101+
// 7. SickLeave range validation (only if pos == SickLeave or from/to used)
102+
//
103+
let sick_range = validate_sickleave_args(pos_final, *from, *to)?;
104+
105+
match sick_range {
106+
Some((from_date, to_date)) => {
107+
let default_in = chrono::NaiveTime::from_hms_opt(9, 0, 0).unwrap();
108+
let default_out = chrono::NaiveTime::from_hms_opt(18, 0, 0).unwrap();
109+
110+
// (opzionale ma consigliato) vieta start/end nel range malattia
111+
if start_parsed.is_some() || end_parsed.is_some() {
112+
return Err(AppError::InvalidArgs(
113+
"--start/--end cannot be used with --pos s (use only --from/--to)".into(),
114+
));
115+
}
116+
117+
let s = start_parsed.unwrap_or(default_in);
118+
let e = end_parsed.unwrap_or(default_out);
119+
120+
AddLogic::apply(
121+
cfg,
122+
&mut pool,
123+
d,
124+
pos_final,
125+
Some(s),
126+
lunch_opt,
127+
work_gap,
128+
Some(e),
129+
*edit,
130+
*edit_pair,
131+
Some(from_date),
132+
Some(to_date),
133+
pos.clone(),
134+
)?;
135+
}
136+
None => {
137+
AddLogic::apply(
138+
cfg,
139+
&mut pool,
140+
d,
141+
pos_final,
142+
start_parsed,
143+
lunch_opt,
144+
work_gap,
145+
end_parsed,
146+
*edit,
147+
*edit_pair,
148+
None,
149+
None,
150+
pos.clone(),
151+
)?;
152+
}
153+
}
85154
}
86155

87156
Ok(())

src/cli/commands/del.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ pub fn handle(cmd: &Commands, cfg: &Config) -> AppResult<()> {
2828
date: date_str,
2929
} = cmd
3030
{
31-
let d = date::parse_date(date_str).ok_or_else(|| AppError::InvalidDate(date_str.into()))?;
31+
let d =
32+
date::parse_date(date_str).map_err(|_| AppError::InvalidDate(date_str.to_string()))?;
3233

3334
//
3435
// Confirmation prompt

src/cli/commands/list.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,12 @@ fn print_daily_row(
495495
let mut surplus_display = "-".to_string();
496496
let mut surplus_color = colors::GREY;
497497

498-
if day_position != Location::Holiday && day_position != Location::NationalHoliday {
498+
let is_marker_day = matches!(
499+
day_position,
500+
Location::Holiday | Location::NationalHoliday | Location::SickLeave
501+
);
502+
503+
if !is_marker_day {
499504
let first_in = timeline.pairs[0].in_event.timestamp();
500505
first_in_str = first_in.format("%H:%M").to_string();
501506

src/cli/parser.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use crate::export::ExportFormat;
2+
use crate::utils::date::parse_date;
3+
use chrono::NaiveDate;
24
use clap::{Parser, Subcommand};
35

46
/// Command-line interface definition for rTimelogger
@@ -78,10 +80,10 @@ pub enum Commands {
7880
/// Date of the event (YYYY-MM-DD)
7981
date: String,
8082

81-
/// Position (O = Office, R = Remote, H = Holiday, C = Client, M = Mixed)
83+
/// Position (O = Office, R = Remote, H = Holiday, N = National Holiday, C = Client, M = Mixed, S = Sick Leave)
8284
#[arg(
8385
long = "pos",
84-
help = "Work position: O=Office, R=Remote, H=Holiday, C=Client, M=Mixed"
86+
help = "Work position: O=Office, R=Remote, H=Holiday, N=National Holiday, C=Client, M=Mixed, S=Sick Leave"
8587
)]
8688
pos: Option<String>,
8789

@@ -124,6 +126,14 @@ pub enum Commands {
124126
help = "Edit existing pair instead of creating a new one"
125127
)]
126128
edit: bool,
129+
130+
/// Start date (YYYY-MM-DD). Only valid with --pos Malattia.
131+
#[arg(long, value_parser = parse_date)]
132+
from: Option<NaiveDate>,
133+
134+
/// End date (YYYY-MM-DD). Only valid with --pos Malattia.
135+
#[arg(long, value_parser = parse_date)]
136+
to: Option<NaiveDate>,
127137
},
128138

129139
/// Delete a work session by ID

0 commit comments

Comments
 (0)