From 28186c774552642fbac344872b48c8396e62ffc0 Mon Sep 17 00:00:00 2001 From: Nicola Belluti Date: Wed, 7 Oct 2026 12:47:36 +0200 Subject: [PATCH] feat(oxibadge): add month-confirmation flow and fetch records/employees Adds the screen after onboarding: pick a month/year (ConfirmMonth), show loading screens while the device is queried, then download that month's records and resolve each distinct employee's name, printing both to stdout (debug pretty-print) before landing on the placeholder "done" screen. - r701: RecordIterator's boxed stream is now Send, required for it to cross into a Tauri command's future. - oxibadge: load_records/load_employees commands, sharing the existing 3-retry connect_with_retries helper. load_records uses skip_while/ take_while on the device's newest-first stream to grab only the target month without walking its whole history, then a single collect + in-place Vec::reverse() back to chronological order. - Shared LoadingScreen/Intro components extracted so future screens reuse the same look without duplicating markup/CSS. Assisted-by: Claude Sonnet 5 --- Cargo.lock | 2 + oxibadge/src-tauri/Cargo.toml | 4 +- oxibadge/src-tauri/src/lib.rs | 99 +++++- oxibadge/src/app.css | 10 + .../src/lib/components/ConfirmMonth.svelte | 316 ++++++++++++++++++ oxibadge/src/lib/components/Intro.svelte | 26 ++ .../src/lib/components/LoadingScreen.svelte | 84 +++++ oxibadge/src/lib/components/Onboarding.svelte | 32 +- oxibadge/src/lib/components/Screen.svelte | 1 + oxibadge/src/lib/locales/en.json | 19 ++ oxibadge/src/lib/locales/it.json | 19 ++ oxibadge/src/lib/month.ts | 9 + oxibadge/src/routes/+page.svelte | 39 ++- r701/src/record_iterator.rs | 2 +- 14 files changed, 621 insertions(+), 41 deletions(-) create mode 100644 oxibadge/src/lib/components/ConfirmMonth.svelte create mode 100644 oxibadge/src/lib/components/Intro.svelte create mode 100644 oxibadge/src/lib/components/LoadingScreen.svelte create mode 100644 oxibadge/src/lib/month.ts diff --git a/Cargo.lock b/Cargo.lock index b62e0aa..852a9f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2113,10 +2113,12 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" name = "oxibadge" version = "0.1.0" dependencies = [ + "chrono", "r701", "serde", "tauri", "tauri-build", + "tokio-stream", "toml 0.9.12+spec-1.1.0", ] diff --git a/oxibadge/src-tauri/Cargo.toml b/oxibadge/src-tauri/Cargo.toml index 366779c..71c98d5 100644 --- a/oxibadge/src-tauri/Cargo.toml +++ b/oxibadge/src-tauri/Cargo.toml @@ -19,4 +19,6 @@ tauri-build = { version = "2", features = [] } tauri = { version = "2", features = [] } serde = { version = "1", features = ["derive"] } toml = "0.9" -r701 = { path = "../../r701" } +r701 = { path = "../../r701", features = ["serde"] } +chrono = { version = "0.4.38", default-features = false, features = ["clock"] } +tokio-stream = { version = "0.1.19", default-features = false } diff --git a/oxibadge/src-tauri/src/lib.rs b/oxibadge/src-tauri/src/lib.rs index 807e1f2..bda3d17 100644 --- a/oxibadge/src-tauri/src/lib.rs +++ b/oxibadge/src-tauri/src/lib.rs @@ -1,7 +1,11 @@ mod preferences; +use chrono::{Local, TimeZone}; use preferences::Reader; +use r701::{Record, R701}; +use std::collections::HashMap; use tauri::AppHandle; +use tokio_stream::StreamExt; #[tauri::command] fn load_preferences(app: AppHandle) -> Option { @@ -13,14 +17,19 @@ fn save_preferences(app: AppHandle, host: String, port: u16) -> Result<(), Strin preferences::save(&app, Reader { host, port }) } -/// Connects to the device up to 3 times, returning a stable error code (not -/// a user-facing message) on failure so the frontend can translate it. #[tauri::command] async fn test_connection(host: String, port: u16) -> Result<(), String> { + connect_with_retries(&host, port).await.map(|_| ()) +} + +/// Connects to the device, trying up to 3 times, returning a stable error +/// code (not a user-facing message) on failure so the frontend can +/// translate it. +async fn connect_with_retries(host: &str, port: u16) -> Result { let mut last_error = String::new(); for _ in 0..3 { - let err = match r701::R701::connect((host.as_str(), port)).await { - Ok(_) => return Ok(()), + let err = match R701::connect((host, port)).await { + Ok(r701) => return Ok(r701), Err(err) => err, }; last_error = match err.kind() { @@ -38,13 +47,93 @@ async fn test_connection(host: String, port: u16) -> Result<(), String> { Err(last_error) } +/// Fetches every attendance record for `year`/`month` (1-12) from the +/// configured reader. +/// +/// The device streams records newest first, so the target month's records +/// are skipped over (`skip_while`) until found, then taken (`take_while`) +/// until the stream moves past them into older months — without walking +/// the device's entire history. The result is reversed back to +/// chronological (oldest first) order before being returned. +#[tauri::command] +async fn load_records(app: AppHandle, year: i32, month: u32) -> Result, String> { + let reader = preferences::load(&app).ok_or_else(|| "no reader configured".to_string())?; + let r701 = connect_with_retries(&reader.host, reader.port).await?; + + let invalid = || "invalid year/month".to_string(); + let start = Local + .with_ymd_and_hms(year, month, 1, 0, 0, 0) + .single() + .ok_or_else(invalid)?; + let (next_year, next_month) = if month == 12 { + (year + 1, 1) + } else { + (year, month + 1) + }; + let end = Local + .with_ymd_and_hms(next_year, next_month, 1, 0, 0, 0) + .single() + .ok_or_else(invalid)?; + + let mut records: Vec = r701 + .into_record_iter() + .await + .map_err(|err| err.to_string())? + .skip_while(|record| matches!(record, Ok(record) if record.datetime >= end)) + .take_while(|record| !matches!(record, Ok(record) if record.datetime < start)) + .collect::, _>>() + .await + .map_err(|err| err.to_string())?; + + records.reverse(); + + dbg!(&records); + + Ok(records) +} + +/// Resolves the name of every distinct employee in `records`, printing both +/// the records and the resulting employee map to stdout before returning +/// the map. +#[tauri::command] +async fn load_employees( + app: AppHandle, + records: Vec, +) -> Result, String> { + let reader = preferences::load(&app).ok_or_else(|| "no reader configured".to_string())?; + let r701 = connect_with_retries(&reader.host, reader.port).await?; + + let mut employees = HashMap::new(); + for record in &records { + if employees.contains_key(&record.employee_id) { + continue; + } + let name = r701 + .get_name(record.employee_id) + .await + .map_err(|err| err.to_string())? + .map(|name| { + name.unwrap_or_else(|error| String::from_utf8_lossy(error.as_bytes()).into_owned()) + }); + if let Some(name) = name { + employees.insert(record.employee_id, name); + } + } + + dbg!(&employees); + + Ok(employees) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![ load_preferences, save_preferences, - test_connection + test_connection, + load_records, + load_employees ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/oxibadge/src/app.css b/oxibadge/src/app.css index dc60fde..8ca253b 100644 --- a/oxibadge/src/app.css +++ b/oxibadge/src/app.css @@ -49,3 +49,13 @@ svg { stroke-linejoin: round; flex: none; } + +h1 { + margin: 0; + font-family: var(--ox-font-display); + font-weight: 700; + font-size: 48px; + line-height: 1.05; + letter-spacing: -0.03em; + color: var(--ox-text-strong); +} diff --git a/oxibadge/src/lib/components/ConfirmMonth.svelte b/oxibadge/src/lib/components/ConfirmMonth.svelte new file mode 100644 index 0000000..f756b3f --- /dev/null +++ b/oxibadge/src/lib/components/ConfirmMonth.svelte @@ -0,0 +1,316 @@ + + + + +{#if open} + +{/if} + + + + +
+
+ + + {#if open === 'month'} +
+ {#each monthShort as label, i} + + {/each} +
+ {/if} +
+ + + +
+ + + {#if open === 'year'} +
+ {#each years as y (y)} + + {/each} +
+ {/if} +
+
+ +