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 { 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 { 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, 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, load_records, load_employees ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }