Skip to content

Commit 55f1da4

Browse files
committed
feat(sickleave): introduce Sick Leave marker day with optional --to range
- Add --pos s (Sick Leave) - Implement marker-day storage (00:00 sentinel event) - Add optional --to range (DATE is implicit start) - Skip weekends, national holidays and existing event dates - Update list rendering to hide times and exclude ΔWORK - Refactor add/apply logic and improve CLI validation
1 parent 2293ada commit 55f1da4

File tree

4 files changed

+91
-47
lines changed

4 files changed

+91
-47
lines changed

CHANGELOG.md

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,33 @@
44

55
### Added
66

7-
- New position: `Sick Leave` (`--pos s`)
8-
- Optional Sick Leave range support via `--to` (start date is the command `DATE`)
7+
- New `Sick Leave` position (`--pos s`)
8+
- Optional range support via `--to` (start date is the command `DATE`)
9+
- Automatic skipping of:
10+
- Weekends (Saturday/Sunday)
11+
- National holidays
12+
- Dates already containing events
913

1014
### Changed
1115

12-
- Sick Leave is stored as a non-working marker day (sentinel event at `00:00`, similar to Holiday)
13-
- `list` rendering updated for Sick Leave marker days:
14-
- IN/OUT/TGT shown as `--:--`
15-
- ΔWORK does not contribute to daily/monthly totals
16-
- CLI validation updated:
16+
- Sick Leave is now stored as a non-working marker day (sentinel event at `00:00`, similar to Holiday)
17+
- `list` rendering updated:
18+
- IN/OUT/TGT displayed as `--:--`
19+
- Sick Leave days do not contribute to ΔWORK totals
20+
- CLI validation improved:
1721
- `--to` is only allowed with `--pos s`
18-
- If `--to` is omitted, Sick Leave applies to the single `DATE`
19-
- Migration updated to support `S` position in `events.position` CHECK constraint
20-
- Improved `add` handler flow to support Sick Leave marker insertion and range insertion
22+
- If `--to` is omitted, Sick Leave applies only to the specified `DATE`
23+
- `recalc_pairs_for_date` updated to accept `&Connection`
24+
- Refactored `add` flow to cleanly separate:
25+
- marker days
26+
- working days
27+
- range handling
2128

2229
### Fixed
2330

24-
- Prevented incorrect `00:00` IN time display for Sick Leave in `list`
25-
- Prevented Sick Leave days from producing surplus/deficit calculations
31+
- Prevented `00:00` IN time from being displayed for Sick Leave
32+
- Prevented incorrect surplus/deficit calculations for Sick Leave days
33+
- Improved argument validation consistency in `add`
2634

2735
---
2836

src/cli/commands/add.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,11 @@ pub fn handle(cmd: &Commands, cfg: &crate::config::Config) -> AppResult<()> {
107107
let sick_range = validate_sickleave_args(pos_final, Some(d), *to)?;
108108

109109
match sick_range {
110-
Some((from_date, to_date)) => {
110+
Some((_from_date, to_date)) => {
111111
// (opzionale ma consigliato) vieta start/end nel range malattia
112112
if start_parsed.is_some() || end_parsed.is_some() {
113113
return Err(AppError::InvalidArgs(
114-
"--start/--end cannot be used with --pos s (use only --to)".into(),
114+
"--in/--out cannot be used with --pos s (use only --to)".into(),
115115
));
116116
}
117117

@@ -126,7 +126,6 @@ pub fn handle(cmd: &Commands, cfg: &crate::config::Config) -> AppResult<()> {
126126
None,
127127
*edit,
128128
*edit_pair,
129-
Some(from_date),
130129
Some(to_date),
131130
pos.clone(),
132131
)?;
@@ -144,7 +143,6 @@ pub fn handle(cmd: &Commands, cfg: &crate::config::Config) -> AppResult<()> {
144143
*edit,
145144
*edit_pair,
146145
None,
147-
None,
148146
pos.clone(),
149147
)?;
150148
}

src/core/add.rs

Lines changed: 52 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::models::event::{Event, EventExtras};
99
use crate::models::event_type::EventType;
1010
use crate::models::location::Location;
1111
use crate::ui::messages::success;
12+
use crate::utils::date::{is_national_holiday, is_weekend};
1213
use chrono::{NaiveDate, NaiveTime, Timelike};
1314
use rusqlite::params;
1415

@@ -69,7 +70,6 @@ impl AddLogic {
6970
end: Option<NaiveTime>,
7071
edit_mode: bool,
7172
edit_pair: Option<usize>,
72-
from: Option<NaiveDate>,
7373
to: Option<NaiveDate>,
7474
pos: Option<String>,
7575
) -> AppResult<()> {
@@ -89,8 +89,8 @@ impl AddLogic {
8989
// ------------------------------------------------
9090
// Sanity: range args only allowed for SickLeave
9191
// ------------------------------------------------
92-
let range = match (from, to) {
93-
(Some(f), Some(t)) => {
92+
let range = match (date, to) {
93+
(f, Some(t)) => {
9494
if pos_final != Location::SickLeave {
9595
return Err(AppError::InvalidArgs(
9696
"--from/--to can only be used with --pos Malattia".into(),
@@ -102,12 +102,7 @@ impl AddLogic {
102102
}
103103
Some((f, t))
104104
}
105-
(None, None) => None,
106-
_ => {
107-
return Err(AppError::InvalidArgs(
108-
"Both --from and --to must be provided together.".into(),
109-
));
110-
}
105+
(_null, None) => None,
111106
};
112107

113108
// ------------------------------------------------
@@ -223,19 +218,14 @@ impl AddLogic {
223218
}
224219

225220
// Range: if omitted -> single-day (date,date)
226-
let (from_date, to_date) = match (from, to) {
227-
(Some(f), Some(t)) => {
221+
let (date, to_date) = match (date, to) {
222+
(f, Some(t)) => {
228223
if f > t {
229224
return Err(AppError::InvalidDateRange { from: f, to: t });
230225
}
231226
(f, t)
232227
}
233-
(None, None) => (date, date),
234-
_ => {
235-
return Err(AppError::InvalidArgs(
236-
"Both --from and --to must be provided together.".into(),
237-
));
238-
}
228+
(_null, None) => (date, date),
239229
};
240230

241231
// Sentinel time (00:00) like holiday
@@ -244,32 +234,58 @@ impl AddLogic {
244234

245235
let tx = pool.conn.transaction()?;
246236

247-
let mut day = from_date;
237+
let mut inserted = 0usize;
238+
let mut skipped_weekend = 0usize;
239+
let mut skipped_national = 0usize;
240+
let mut skipped_existing = 0usize;
241+
242+
let mut day = date;
248243
while day <= to_date {
249-
// Check existing events on that day (fail fast)
244+
// 1) weekend -> skip
245+
if is_weekend(day) {
246+
skipped_weekend += 1;
247+
day = day
248+
.succ_opt()
249+
.ok_or_else(|| AppError::Other("Invalid date increment.".into()))?;
250+
continue;
251+
}
252+
253+
// 2) national holiday -> skip
254+
if is_national_holiday(&tx, day)? {
255+
skipped_national += 1;
256+
day = day
257+
.succ_opt()
258+
.ok_or_else(|| AppError::Other("Invalid date increment.".into()))?;
259+
continue;
260+
}
261+
262+
// 3) already has events -> skip
250263
let day_str = day.to_string();
251264
let exists: i64 = tx.query_row(
252265
"SELECT EXISTS(SELECT 1 FROM events WHERE date = ?1 LIMIT 1)",
253266
rusqlite::params![day_str],
254267
|r| r.get(0),
255268
)?;
256269
if exists == 1 {
257-
return Err(AppError::InvalidArgs(format!(
258-
"Cannot set Sick Leave on {}: the date already has events.",
259-
day
260-
)));
270+
skipped_existing += 1;
271+
day = day
272+
.succ_opt()
273+
.ok_or_else(|| AppError::Other("Invalid date increment.".into()))?;
274+
continue;
261275
}
262276

277+
// 4) insert marker
263278
let ev = build_event_cli(
264279
day,
265-
marker_time,
280+
marker_time, // 00:00
266281
EventType::In,
267282
Location::SickLeave,
268283
extras_cli(Some(0), false),
269284
);
270285

271286
insert_event(&tx, &ev)?;
272287
recalc_pairs_for_date(&tx, &day)?;
288+
inserted += 1;
273289

274290
day = day
275291
.succ_opt()
@@ -278,17 +294,22 @@ impl AddLogic {
278294

279295
tx.commit()?;
280296

281-
if from_date == to_date {
282-
success(format!("Added SICK LEAVE on {}.\n", from_date));
297+
// output summary
298+
if to_date == date {
299+
if inserted == 1 {
300+
success(format!("Added SICK LEAVE on {}.\n", date));
301+
} else {
302+
success(format!(
303+
"No Sick Leave inserted on {} (skipped: weekend={}, national_holiday={}, existing_events={}).\n",
304+
date, skipped_weekend, skipped_national, skipped_existing
305+
));
306+
}
283307
} else {
284308
success(format!(
285-
"Added SICK LEAVE from {} to {} ({} days).\n",
286-
from_date,
287-
to_date,
288-
(to_date - from_date).num_days() + 1
309+
"SICK LEAVE range {} → {}: inserted={}, skipped (weekend={}, national_holiday={}, existing_events={}).\n",
310+
date, to_date, inserted, skipped_weekend, skipped_national, skipped_existing
289311
));
290312
}
291-
292313
return Ok(());
293314
}
294315

src/utils/date.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::core::calculator::timeline::Timeline;
2+
use crate::errors::AppResult;
23
use crate::models::location::Location;
34
use chrono::{Datelike, NaiveDate, Weekday};
45

@@ -159,3 +160,19 @@ pub fn get_day_position(timeline: &Timeline) -> Location {
159160
Location::Mixed
160161
}
161162
}
163+
164+
// helper weekend
165+
pub fn is_weekend(d: NaiveDate) -> bool {
166+
matches!(d.weekday(), Weekday::Sat | Weekday::Sun)
167+
}
168+
169+
// helper national holiday in DB (position = 'N')
170+
pub fn is_national_holiday(conn: &rusqlite::Connection, d: NaiveDate) -> AppResult<bool> {
171+
let date_str = d.to_string();
172+
let exists: i64 = conn.query_row(
173+
"SELECT EXISTS(SELECT 1 FROM events WHERE date = ?1 AND position = 'N' LIMIT 1)",
174+
rusqlite::params![date_str],
175+
|r| r.get(0),
176+
)?;
177+
Ok(exists == 1)
178+
}

0 commit comments

Comments
 (0)