Files
r701/oxibadge/src-tauri/src/lib.rs
T
nicolabelluti b4cd8e1796
Check, format and test / build (push) Successful in 10m4s
fix(oxibadge): show connection errors instead of failing silently
load_records/load_employees failures were only logged to the console
and silently reset the UI to the month picker. Errors now flow through
a shared store (src/lib/error.ts) into an ErrorBanner, reused by both
Onboarding and ConfirmMonth, with the raw message passed straight
through from Rust (no stable-code classification layer — these are
mostly plain I/O errors, not worth categorizing).

i18n stays grouped per screen as before, with error-related strings
split into their own top-level "errors" group.

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-07 13:26:01 +02:00

129 lines
4.0 KiB
Rust

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> {
preferences::load(&app)
}
#[tauri::command]
fn save_preferences(app: AppHandle, host: String, port: u16) -> Result<(), String> {
preferences::save(&app, Reader { host, port })
}
#[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 the last error
/// message as-is on failure.
async fn connect_with_retries(host: &str, port: u16) -> Result<R701, String> {
let mut last_error = String::new();
for _ in 0..3 {
match R701::connect((host, port)).await {
Ok(r701) => return Ok(r701),
Err(err) => last_error = err.to_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,
load_records,
load_employees
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}