Skip to content

Commit d697a4e

Browse files
committed
Add National holiday support and database migration
- Added new day position: National holiday (N) - Extended Location enum and CLI parsing - Added idempotent SQLite migration to update events.position CHECK constraint - Updated list and compact views to handle national holidays - Updated CHANGELOG for v0.8.2 - Updated README with new position documentation
1 parent d8bf2be commit d697a4e

File tree

8 files changed

+221
-84
lines changed

8 files changed

+221
-84
lines changed

CHANGELOG.md

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

3+
4+
## [0.8.2] — 2026-01-08
5+
6+
### ✨ Added
7+
8+
- Added a new day position **National holiday** (**N**) to represent public holidays that do not affect personal holiday allowance.
9+
- Introduced support for `--pos n` / `--pos national` in the `add` command to mark national holidays.
10+
- Added idempotent database migration to extend the `events.position` CHECK constraint with the new `N` value.
11+
12+
### 🧠 Changed
13+
14+
- Updated daily and compact list views to properly display **National holiday** days with neutral time and ΔWORK values.
15+
- Improved semantic distinction between **Holiday** (personal leave) and **National holiday** (public holiday) in reports and summaries.
16+
17+
### 🐞 Fixes
18+
19+
- Ensured database migrations safely no-op when the schema is already aligned.
20+
- Fixed SQL escaping issues in migration logging statements.
21+
22+
### 🧹 Internal
23+
24+
- Extended Location enum and related parsing/formatting utilities.
25+
- Refined migration logging for better traceability and robustness.
26+
27+
---
28+
329
## [0.8.1] - 2026-01-07
430

531
### ✨ New

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.1"
3+
version = "0.8.2"
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"

README.md

Lines changed: 79 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -14,75 +14,22 @@ and computes **expected exit time** and **daily surplus** accurately.
1414

1515
---
1616

17-
## 🚀 What’s new in **v0.8.0**
17+
## 🚀 What’s new in **v0.8.2**
1818

19-
Version **0.8.0** is the first **stable release** based on the new **timeline engine**.
19+
### 🗄️ Database migration
2020

21-
### ✅ Timeline engine (stable)
21+
Starting from **v0.8.2**, rTimelogger automatically migrates the database schema to support the new `National holiday`
22+
position.
2223

23-
* Unlimited **IN / OUT pairs per day**
24-
* Deterministic reconstruction:
25-
**events → timeline → pairs**
26-
* Correct handling of:
24+
- The migration extends the `events.position` CHECK constraint
25+
- The migration is **idempotent** (no changes are applied if the schema is already up to date)
26+
- No manual action is required
2727

28-
* lunch breaks
29-
* working gaps
30-
* multi-position days
31-
* Legacy `work_sessions` logic fully retired
28+
### 🧭 Notes
3229

33-
---
34-
35-
### 🔗 Working gap support
36-
37-
Time **between pairs** can be explicitly controlled:
38-
39-
* `--work-gap` → gap counts as working time
40-
* `--no-work-gap` → gap does **not** count as working time
41-
42-
Features:
43-
44-
* Stored in the database
45-
* Editable retroactively
46-
* Fully reflected in:
47-
48-
* worked time
49-
* expected exit
50-
* surplus
51-
52-
Visual indicators:
53-
54-
* 🔗 working gap
55-
* ✂️ non-working gap
56-
57-
---
58-
59-
### 🧮 Accurate calculations
60-
61-
* Worked time:
62-
63-
* sum of all pairs
64-
* minus non-working gaps
65-
* plus working gaps only when marked
66-
* Expected exit:
67-
68-
* based on first IN
69-
* configured minimum working time
70-
* lunch rules and lunch window
71-
* Surplus is correct in **all multi-pair scenarios**
72-
73-
---
74-
75-
### 🧠 Consistency improvements
76-
77-
* `OUT` events inherit position from `IN` when `--pos` is omitted
78-
* Pair details show the **actual position of each pair**
79-
* Clean event listing:
80-
81-
* no duplicated dates
82-
* aligned output
83-
* Unified CLI message system:
84-
85-
* ℹ️ info · ⚠️ warning · ❌ error · ✅ success
30+
- Holiday should be used for personal leave days
31+
- National holiday should be used for public holidays defined by law or company calendar
32+
- Future versions may introduce calendar-based automation for national holidays
8633

8734
---
8835

@@ -94,6 +41,7 @@ Visual indicators:
9441
* `O` Office
9542
* `R` Remote
9643
* `C` Client / On-site
44+
* `N` National holiday
9745
* `H` Holiday
9846
* `M` Mixed
9947
* Automatic calculation of:
@@ -187,7 +135,8 @@ sudo mv rtimelogger /usr/local/bin/
187135
### 🪟 Windows
188136

189137
Download the prebuilt zip file, extract it, and move `rtimelogger.exe` to a directory in your `PATH`, e.g.,
190-
`C:\Windows\System32\` or create a dedicated folder like `C:\Program Files\rtimelogger\` and add it to your system `PATH`.
138+
`C:\Windows\System32\` or create a dedicated folder like `C:\Program Files\rtimelogger\` and add it to your system
139+
`PATH`.
191140

192141
---
193142

@@ -205,6 +154,7 @@ Example `rtimelogger.conf`:
205154
database: /home/user/.rtimelogger/rtimelogger.sqlite
206155
default_position: O
207156
min_work_duration: 8h
157+
lunch_window: 12:30-14:00
208158
min_duration_lunch_break: 30
209159
max_duration_lunch_break: 90
210160
separator_char: "-"
@@ -252,6 +202,69 @@ rtimelogger add 2025-12-15 --out 10:30 --work-gap
252202
rtimelogger add 2025-12-15 --edit --pair 2 --no-work-gap
253203
```
254204

205+
### 📌 Day positions
206+
207+
rTimelogger supports multiple day positions to describe how a working day (or non-working day) is classified.
208+
209+
**Supported positions**
210+
211+
| Code | Name | Description |
212+
|------|------------------|---------------------------------------------------------------|
213+
| `O` | Office | Regular office working day |
214+
| `R` | Remote | Remote working day |
215+
| `C` | On-site | Working day at customer site |
216+
| `M` | Mixed | Mixed working locations |
217+
| `H` | Holiday | Personal holiday (counts against personal leave allowance) |
218+
| `N` | National holiday | Public holiday (does **not** affect personal leave allowance) |
219+
220+
### ➕ Adding a national holiday
221+
222+
To mark a **public/national holiday**, use the `add` command with the national position.
223+
224+
```bash
225+
rtimelogger add 2025-12-25 --pos n
226+
```
227+
228+
or
229+
230+
```bash
231+
rtimelogger add 2025-12-25 --pos national
232+
```
233+
234+
**Behavior**
235+
236+
- No `--in`, `--out`, `--lunch`, or `--work-gap` parameters are allowed
237+
- The day is recorded as a non-working public holiday
238+
- The day does not contribute to worked time
239+
- The day does not reduce personal holiday allowance
240+
241+
### 📋 List output behavior
242+
243+
**National holiday days**
244+
245+
In both standard and compact list views:
246+
247+
- All time-related fields are displayed as `--:--`
248+
- Target end (`TGT`) is not computed
249+
- Worked delta (`ΔWORK`) is neutral (`-`)
250+
- The day is clearly labeled as **National holiday**
251+
252+
Example:
253+
254+
```text
255+
2025-12-25 (Thu) | National holiday | --:-- | --:-- | --:-- | --:-- | -
256+
```
257+
258+
### ⚖️ Holiday vs National holiday
259+
260+
| Aspect | Holiday (`H`) | National holiday (`N`) |
261+
|--------------------------|---------------|------------------------|
262+
| Working day |||
263+
| Counts as personal leave |||
264+
| Expected time |||
265+
| ΔWORK contribution |||
266+
| Requires time entries |||
267+
255268
---
256269

257270
## 📋 Listing sessions — `rtimelogger list`
@@ -417,6 +430,7 @@ The total accounts for:
417430
```bash
418431
rtimelogger list --period 2025-12 --json
419432
```
433+
420434
Outputs the data in JSON format for easy integration with other tools or scripts.
421435

422436
---

src/cli/commands/list.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ fn print_daily_row(
465465
let mut surplus_display = "-".to_string();
466466
let mut surplus_color = colors::GREY;
467467

468-
if day_position != Location::Holiday {
468+
if day_position != Location::Holiday && day_position != Location::NationalHoliday {
469469
let first_in = timeline.pairs[0].in_event.timestamp();
470470
first_in_str = first_in.format("%H:%M").to_string();
471471

@@ -652,7 +652,7 @@ fn print_daily_row_compact(
652652
let pos_label = day_position.label();
653653
let pos_color = day_position.color();
654654

655-
if day_position == Location::Holiday {
655+
if day_position == Location::Holiday || day_position == Location::NationalHoliday {
656656
println!(
657657
"{:<dw$} | {}{:<12}{}\x1b[0m | {:<21} | {:^5} | {}Δ -{}\x1b[0m",
658658
date_str,

src/core/add.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,11 @@ impl AddLogic {
164164
// ------------------------------------------------
165165
// ✅ CASE HOLIDAY: allow --pos H without --in/--out
166166
// ------------------------------------------------
167-
if pos_final == Location::Holiday {
167+
if pos_final == Location::Holiday || pos_final == Location::NationalHoliday {
168168
// Holiday è un marker di giornata: non accetto parametri temporali o lunch/work-gap
169169
if start.is_some() || end.is_some() || lunch.is_some() || work_gap.is_some() {
170170
return Err(AppError::InvalidArgs(
171-
"For --pos H (Holiday) do not specify --in, --out, --lunch or --work-gap."
172-
.into(),
171+
"For holiday days do not specify time-related arguments.".into(),
173172
));
174173
}
175174

@@ -190,15 +189,19 @@ impl AddLogic {
190189
date,
191190
holiday_time,
192191
EventType::In,
193-
Location::Holiday,
192+
pos_final,
194193
Some(0),
195194
false,
196195
);
197196

198197
insert_event(&pool.conn, &ev_holiday)?;
199198
crate::db::queries::recalc_pairs_for_date(&mut pool.conn, &date)?;
200199

201-
success(format!("Added HOLIDAY on {}.", date_str));
200+
success(match pos_final {
201+
Location::Holiday => format!("Added HOLIDAY on {}.", date),
202+
Location::NationalHoliday => format!("Added NATIONAL HOLIDAY on {}.", date),
203+
_ => unreachable!(),
204+
});
202205
return Ok(());
203206
}
204207

src/db/migrate.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,76 @@ fn align_db_schemas_to_080_version(conn: &Connection) -> Result<()> {
168168
Ok(())
169169
}
170170

171+
fn add_nation_holiday_check_to_events(conn: &Connection) -> Result<()> {
172+
if !events_table_exists(conn)? {
173+
return Ok(()); // nessuna tabella → niente da migrare
174+
}
175+
176+
// 🔎 Check preliminare
177+
if events_position_supports_national_holiday(conn)? {
178+
// Tabella già allineata → niente da fare
179+
return Ok(());
180+
}
181+
182+
let version = "20260107_1700_add_national_holiday_check_to_events_position";
183+
184+
// 1) Verifica se già applicata
185+
warning("Adding new check onto 'position' column to events table...");
186+
187+
conn.execute_batch(
188+
r#"
189+
PRAGMA foreign_keys=OFF;
190+
BEGIN;
191+
192+
ALTER TABLE events RENAME TO events_old;
193+
194+
CREATE TABLE events (
195+
id INTEGER PRIMARY KEY AUTOINCREMENT,
196+
date TEXT NOT NULL,
197+
time TEXT NOT NULL,
198+
kind TEXT NOT NULL CHECK(kind IN ('in','out')),
199+
position TEXT NOT NULL DEFAULT 'O' CHECK(position IN ('O','R','H','N','C','M')),
200+
lunch_break INTEGER NOT NULL DEFAULT 0,
201+
pair INTEGER NOT NULL DEFAULT 0,
202+
work_gap INTEGER NOT NULL DEFAULT 0,
203+
source TEXT NOT NULL DEFAULT 'cli',
204+
meta TEXT DEFAULT '',
205+
created_at TEXT NOT NULL
206+
);
207+
208+
INSERT INTO events (id, date, time, kind, position, lunch_break, source, meta, created_at)
209+
SELECT id, date, time, kind, position, lunch_break, source, meta, created_at
210+
FROM events_old;
211+
212+
DROP TABLE events_old;
213+
214+
CREATE INDEX IF NOT EXISTS idx_events_date_time ON events(date, time);
215+
CREATE INDEX IF NOT EXISTS idx_events_date_kind ON events(date, kind);
216+
217+
UPDATE sqlite_sequence
218+
SET seq = (SELECT IFNULL(MAX(id), 0) FROM events)
219+
WHERE name = 'events';
220+
221+
COMMIT;
222+
PRAGMA foreign_keys=ON;
223+
"#,
224+
)?;
225+
226+
let msg = "Added new check 'N' position to events";
227+
228+
conn.execute(
229+
r#"
230+
INSERT INTO "log" ("date", "operation", "target", "message")
231+
VALUES (datetime('now'), 'migration_applied', ?1, ?2)
232+
"#,
233+
(version, msg),
234+
)?;
235+
236+
success("new check onto 'position' column added.");
237+
238+
Ok(())
239+
}
240+
171241
fn backup_before_migration(db_path: &str) -> Result<()> {
172242
use chrono::Local;
173243
use std::fs::{self, File};
@@ -329,5 +399,24 @@ pub fn run_pending_migrations(conn: &Connection) -> Result<()> {
329399
// 6) Perform schema cleanup for 0.8.0+
330400
align_db_schemas_to_080_version(conn)?;
331401

402+
// 7) Add national holiday check to events.position
403+
add_nation_holiday_check_to_events(conn)?;
404+
332405
Ok(())
333406
}
407+
408+
fn events_position_supports_national_holiday(conn: &Connection) -> Result<bool> {
409+
let sql: String = conn.query_row(
410+
r#"
411+
SELECT sql
412+
FROM sqlite_master
413+
WHERE type = 'table'
414+
AND name = 'events'
415+
"#,
416+
[],
417+
|row| row.get(0),
418+
)?;
419+
420+
// Check semplice e affidabile
421+
Ok(sql.contains("'N'"))
422+
}

0 commit comments

Comments
 (0)