Skip to content

Commit f0915a9

Browse files
authored
Merge pull request #39 from umpire274/v0.8.3
V0.8.3
2 parents babdbc3 + 1b8723f commit f0915a9

File tree

24 files changed

+1041
-242
lines changed

24 files changed

+1041
-242
lines changed

CHANGELOG.md

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,107 @@
11
# Changelog
22

3+
## [0.8.3] – Unreleased
4+
5+
### ✨ New features
6+
7+
- **Import data from JSON and CSV files**
8+
- New `import` command to insert events into the database from external sources.
9+
- Supported formats:
10+
- `JSON` (flexible structures: root array, `{ days: [...] }`, `{ holidays: [...] }`)
11+
- `CSV` (with header row).
12+
- Supported options:
13+
- `--file <path>`: input file
14+
- `--format <json|csv>` (default: `json`)
15+
- `--dry-run`: simulate the import without modifying the database
16+
- `--replace`: overwrite existing conflicting data
17+
- `--source`: logical label describing the data origin
18+
19+
- **National holidays support**
20+
- Added new position `NationalHoliday`.
21+
- National holidays:
22+
- do **not** affect the user vacation balance
23+
- are compatible with automatic/preventive imports (e.g. yearly calendars).
24+
25+
- **Preventive holiday import**
26+
- Allows preparing JSON/CSV files with annual holidays and importing them in advance.
27+
- Reduces manual errors and forgotten entries.
28+
29+
---
30+
31+
### 🔧 Improvements
32+
33+
- **Enhanced data source tracking**
34+
- The `source` field of imported events now automatically includes the input format
35+
(e.g. `import (from json)`, `import (from csv)`).
36+
37+
- **`meta` field support for imported events**
38+
- Descriptive data (e.g. holiday name) is stored in the `meta` field as JSON.
39+
- Example:
40+
```json
41+
{ "name": "New Year" }
42+
```
43+
44+
- **Refactor of `Event::new()` constructor**
45+
- The constructor now explicitly supports:
46+
- `meta`
47+
- `source`
48+
- Clear separation between CLI-created events and imported events.
49+
50+
- **Database query refactor**
51+
- Split `db::queries` into topic-based submodules.
52+
- Fixed module conflict between `queries.rs` and `queries/mod.rs`.
53+
54+
- **Shared utilities**
55+
- Added helper functions in `utils::formatting` to standardize `source` generation.
56+
57+
---
58+
59+
### 🛠 Fixes
60+
61+
- Improved validation of imported records:
62+
- invalid dates
63+
- unsupported positions
64+
- malformed rows
65+
- Improved import reporting:
66+
- total rows
67+
- imported rows
68+
- skipped rows
69+
- conflicts
70+
- invalid rows
71+
72+
---
73+
74+
### 🧪 Developer notes
75+
76+
- All import logic is isolated in the `src/import` module.
77+
- Full `dry-run` support for safe testing on real databases.
78+
- Solid foundation for future integrations (ICS calendars, APIs, external sync).
79+
80+
---
381

482
## [0.8.2] — 2026-01-08
583

684
### ✨ Added
785

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.
86+
- Added a new day position **National holiday** (**N**) to represent public holidays that do not affect personal holiday
87+
allowance.
88+
- Introduced support for `--pos n` / `--pos national` in the `add` command to mark national holidays.
1089
- Added idempotent database migration to extend the `events.position` CHECK constraint with the new `N` value.
1190

1291
### 🧠 Changed
1392

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.
93+
- Updated daily and compact list views to properly display **National holiday** days with neutral time and ΔWORK values.
94+
- Improved semantic distinction between **Holiday** (personal leave) and **National holiday** (public holiday) in
95+
reports and summaries.
1696

1797
### 🐞 Fixes
1898

19-
- Ensured database migrations safely no-op when the schema is already aligned.
99+
- Ensured database migrations safely no-op when the schema is already aligned.
20100
- Fixed SQL escaping issues in migration logging statements.
21101

22102
### 🧹 Internal
23103

24-
- Extended Location enum and related parsing/formatting utilities.
104+
- Extended Location enum and related parsing/formatting utilities.
25105
- Refined migration logging for better traceability and robustness.
26106

27107
---

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.2"
3+
version = "0.8.3"
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: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,152 @@ Output path must be **absolute**.
476476

477477
---
478478

479+
## Import data (JSON / CSV)
480+
481+
Starting from **v0.8.3**, rTimelogger supports importing work sessions and holidays from external files.
482+
This feature is designed to simplify **preventive data entry**, especially for **national holidays**.
483+
484+
The import system is safe by default and provides a full **dry-run mode**.
485+
486+
---
487+
488+
### Supported formats
489+
490+
#### JSON
491+
492+
Flexible JSON structures are supported. The following formats are valid:
493+
494+
**Root object with `holidays`:**
495+
496+
```json
497+
{
498+
"year": 2026,
499+
"holidays": [
500+
{
501+
"date": "2026-01-01",
502+
"name": "New Year"
503+
},
504+
{
505+
"date": "2026-01-06",
506+
"name": "Epiphany"
507+
}
508+
]
509+
}
510+
```
511+
512+
**Root object with `days`:**
513+
514+
```json
515+
{
516+
"days": [
517+
{
518+
"date": "2026-05-01",
519+
"position": "N",
520+
"name": "Labour Day"
521+
}
522+
]
523+
}
524+
```
525+
526+
**Root array of day objects:**
527+
528+
```json
529+
[
530+
{
531+
"date": "2026-12-25",
532+
"name": "Christmas Day"
533+
}
534+
]
535+
536+
```
537+
538+
**Notes**:
539+
540+
- `position` is optional:
541+
- if omitted, it defaults to `NationalHoliday`
542+
- `name` is optional and stored in the event `meta` field
543+
544+
#### CSV
545+
546+
CSV files must include a header row.
547+
548+
Example:
549+
550+
```csv
551+
date,position,name
552+
2026-01-01,N,New Year
553+
2026-01-06,N,Epiphany
554+
2026-04-25,N,Liberation Day
555+
```
556+
557+
**Notes**:
558+
559+
- `position` must be a valid location code (`N`, `H`, `O`, `R`, `C`, `M`)
560+
- name is optional
561+
562+
### Import command
563+
564+
```bash
565+
rtimelogger import --file <path> [options]
566+
```
567+
568+
**Options**
569+
570+
- `--file <path>` : Path to the input file (required)
571+
572+
- `--format <json|csv>` : Input format (default: json)
573+
574+
- `--dry-run` : Simulate the import without modifying the database (strongly recommended)
575+
576+
- `--replace` : Replace existing events for conflicting dates (dangerous)
577+
578+
- `--source <label>` : Logical label describing the origin of imported data. The final stored value will include the
579+
format automatically (e.g. import (from json))
580+
581+
### Import behavior
582+
583+
- Only Holiday and NationalHoliday positions are accepted by default.
584+
- Dates with existing work events are skipped unless --replace is used.
585+
- Imported holidays:
586+
- do **not** affect the vacation balance
587+
- are treated as regular timeline entries
588+
589+
- Each import generates a detailed summary:
590+
- total rows
591+
- imported
592+
- skipped
593+
- conflicts
594+
- invalid rows
595+
596+
### Example (dry-run)
597+
598+
```bash
599+
rtimelogger import \
600+
--file holidays_2026.json \
601+
--format json \
602+
--dry-run
603+
```
604+
605+
### Example (apply import)
606+
607+
```bash
608+
rtimelogger import \
609+
--file holidays_2026.csv \
610+
--format csv \
611+
--source calendar
612+
```
613+
614+
### Metadata and traceability
615+
616+
- Imported events store additional information in the meta field (JSON).
617+
- The source field tracks the origin of the data:
618+
- CLI entries → cli
619+
- Imports → import (from json) / import (from csv)
620+
621+
This ensures full traceability of all events.
622+
623+
---
624+
479625
## 🗄️ Database utilities — `rtimelogger db`
480626

481627
```bash

src/cli/commands/import.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
use std::fs;
2+
3+
use crate::cli::parser::Commands;
4+
use crate::config::Config;
5+
use crate::errors::{AppError, AppResult};
6+
use crate::import::{ImportInputFormat, import_days_from_str};
7+
use crate::ui::messages::{info, success, warning};
8+
9+
use crate::utils::formatting::build_import_source;
10+
use serde::{Deserialize, Serialize};
11+
12+
#[derive(Debug, Serialize, Deserialize)]
13+
struct ImportDayJson {
14+
date: String,
15+
#[serde(default)]
16+
position: Option<String>,
17+
#[serde(default)]
18+
name: Option<String>,
19+
}
20+
21+
#[derive(Debug, Deserialize)]
22+
#[serde(untagged)]
23+
enum ImportJsonRoot {
24+
Days { days: Vec<ImportDayJson> },
25+
Holidays { holidays: Vec<ImportDayJson> },
26+
Array(Vec<ImportDayJson>),
27+
}
28+
29+
fn normalize_json_to_days(content: &str) -> AppResult<String> {
30+
let parsed: ImportJsonRoot = serde_json::from_str(content).map_err(|e| {
31+
AppError::InvalidArgs(format!(
32+
"Invalid JSON. Expected one of: {{\"days\":[...]}}, {{\"holidays\":[...]}}, or a root array [...]. Details: {}",
33+
e
34+
))
35+
})?;
36+
37+
let days: Vec<ImportDayJson> = match parsed {
38+
ImportJsonRoot::Days { days } => days,
39+
ImportJsonRoot::Holidays { holidays } => holidays,
40+
ImportJsonRoot::Array(v) => v,
41+
};
42+
43+
// Re-emit canonical shape expected by the importer: { "days": [...] }
44+
serde_json::to_string(&serde_json::json!({ "days": days }))
45+
.map_err(|e| AppError::Other(format!("Internal error while normalizing JSON: {}", e)))
46+
}
47+
48+
pub fn handle(cmd: &Commands, cfg: &Config) -> AppResult<()> {
49+
let Commands::Import {
50+
file,
51+
format,
52+
dry_run,
53+
replace,
54+
source,
55+
} = cmd
56+
else {
57+
return Ok(());
58+
};
59+
60+
let mut content = fs::read_to_string(file)?;
61+
62+
let input_format = match format.to_ascii_lowercase().as_str() {
63+
"json" => ImportInputFormat::Json,
64+
"csv" => ImportInputFormat::Csv,
65+
_ => {
66+
return Err(AppError::InvalidArgs(
67+
"Invalid --format. Use 'json' or 'csv'.".into(),
68+
));
69+
}
70+
};
71+
72+
// ✅ Normalize JSON shapes (days/holidays/array) into canonical {"days":[...]}
73+
if matches!(input_format, ImportInputFormat::Json) {
74+
content = normalize_json_to_days(&content)?;
75+
}
76+
77+
let imp_source = build_import_source(source, format);
78+
79+
let report = import_days_from_str(
80+
cfg,
81+
&content,
82+
input_format,
83+
*dry_run,
84+
*replace,
85+
imp_source.as_str(),
86+
)?;
87+
88+
info(format!(
89+
"Import summary{}:\n- File: {}\n- Format: {}\n- Source: {}\n- Total rows: {}\n- Imported: {}\n- Skipped (already present): {}\n- Conflicts: {}\n- Invalid rows: {}",
90+
if *dry_run { " (dry-run)" } else { "" },
91+
file,
92+
format,
93+
source,
94+
report.total,
95+
report.imported,
96+
report.skipped_existing,
97+
report.conflicts,
98+
report.invalid
99+
));
100+
101+
if report.conflicts > 0 && !*replace {
102+
warning(
103+
"Some dates were skipped due to existing work events. Use --replace to override (dangerous).",
104+
);
105+
}
106+
107+
if *dry_run {
108+
success("Dry-run completed. No changes were applied.");
109+
} else {
110+
success("Import completed.");
111+
}
112+
113+
Ok(())
114+
}

0 commit comments

Comments
 (0)