feat(oxibadge): add month-confirmation flow and fetch records/employees
Check, format and test / build (push) Successful in 10m0s
Check, format and test / build (push) Successful in 10m0s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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<Reader> {
|
||||
@@ -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<R701, String> {
|
||||
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<Vec<Record>, 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<Record> = 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::<Result<Vec<_>, _>>()
|
||||
.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<Record>,
|
||||
) -> Result<HashMap<u32, String>, 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");
|
||||
|
||||
Reference in New Issue
Block a user