feat(oxibadge): add settings screen for shifts, rounding and language
Check, format and test / build (push) Successful in 10m22s
Check, format and test / build (push) Successful in 10m22s
Add a settings screen (gear button on the month-confirmation screen) to configure the expected shift times, the rounding step, the interface language, and the time clock address — replacing the old onboarding-only connection flow. Settings persist to the same preferences.toml as the reader address, under a single load_settings/ save_settings command pair. The review screen's "fix shifts" action now redistributes stacked punches by nearest configured shift time instead of blind position, and a stacked cell manually cleared down to one entry no longer leaves a dangling empty input. Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
mod preferences;
|
||||
|
||||
use chrono::{Local, TimeZone};
|
||||
use preferences::Reader;
|
||||
use preferences::Settings;
|
||||
use r701::{Record, R701};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -9,13 +9,13 @@ use tauri::AppHandle;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
#[tauri::command]
|
||||
fn load_preferences(app: AppHandle) -> Option<Reader> {
|
||||
fn load_settings(app: AppHandle) -> Settings {
|
||||
preferences::load(&app)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn save_preferences(app: AppHandle, host: String, port: u16) -> Result<(), String> {
|
||||
preferences::save(&app, Reader { host, port })
|
||||
fn save_settings(app: AppHandle, settings: Settings) -> Result<(), String> {
|
||||
preferences::save(&app, &settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -46,8 +46,11 @@ async fn connect_with_retries(host: &str, port: u16) -> Result<R701, String> {
|
||||
/// 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 settings = preferences::load(&app);
|
||||
if settings.reader.host.is_empty() {
|
||||
return Err("no reader configured".to_string());
|
||||
}
|
||||
let r701 = connect_with_retries(&settings.reader.host, settings.reader.port).await?;
|
||||
|
||||
let invalid = || "invalid year/month".to_string();
|
||||
let start = Local
|
||||
@@ -85,8 +88,11 @@ 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 settings = preferences::load(&app);
|
||||
if settings.reader.host.is_empty() {
|
||||
return Err("no reader configured".to_string());
|
||||
}
|
||||
let r701 = connect_with_retries(&settings.reader.host, settings.reader.port).await?;
|
||||
|
||||
let mut employees = HashMap::new();
|
||||
for record in &records {
|
||||
@@ -145,8 +151,8 @@ fn export_timesheet(payload: ExportPayload) {
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
load_preferences,
|
||||
save_preferences,
|
||||
load_settings,
|
||||
save_settings,
|
||||
test_connection,
|
||||
load_records,
|
||||
load_employees,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -7,25 +8,65 @@ pub struct Reader {
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
struct Preferences {
|
||||
#[serde(default)]
|
||||
reader: Vec<Reader>,
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Shifts {
|
||||
pub in1: String,
|
||||
pub out1: String,
|
||||
pub in2: String,
|
||||
pub out2: String,
|
||||
pub round_step_min: u32,
|
||||
}
|
||||
|
||||
pub fn load(app: &AppHandle) -> Option<Reader> {
|
||||
let path = app.path().app_data_dir().ok()?.join("preferences.toml");
|
||||
let contents = std::fs::read_to_string(path).ok()?;
|
||||
let preferences: Preferences = toml::from_str(&contents).ok()?;
|
||||
preferences.reader.into_iter().next()
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub reader: Reader,
|
||||
pub shifts: Shifts,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub lang: Option<String>,
|
||||
}
|
||||
|
||||
pub fn save(app: &AppHandle, reader: Reader) -> Result<(), String> {
|
||||
let dir = app.path().app_data_dir().map_err(|err| err.to_string())?;
|
||||
std::fs::create_dir_all(&dir).map_err(|err| err.to_string())?;
|
||||
let preferences = Preferences {
|
||||
reader: vec![reader],
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
reader: Reader {
|
||||
host: String::new(),
|
||||
port: 5005,
|
||||
},
|
||||
shifts: Shifts {
|
||||
in1: "07:30".to_string(),
|
||||
out1: "12:00".to_string(),
|
||||
in2: "13:30".to_string(),
|
||||
out2: "17:00".to_string(),
|
||||
round_step_min: 10,
|
||||
},
|
||||
lang: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn path(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
Ok(app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|err| err.to_string())?
|
||||
.join("preferences.toml"))
|
||||
}
|
||||
|
||||
pub fn load(app: &AppHandle) -> Settings {
|
||||
let Ok(path) = path(app) else {
|
||||
return Settings::default();
|
||||
};
|
||||
let contents = toml::to_string_pretty(&preferences).map_err(|err| err.to_string())?;
|
||||
std::fs::write(dir.join("preferences.toml"), contents).map_err(|err| err.to_string())
|
||||
std::fs::read_to_string(path)
|
||||
.ok()
|
||||
.and_then(|contents| toml::from_str(&contents).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn save(app: &AppHandle, settings: &Settings) -> Result<(), String> {
|
||||
let dest = path(app)?;
|
||||
if let Some(dir) = dest.parent() {
|
||||
std::fs::create_dir_all(dir).map_err(|err| err.to_string())?;
|
||||
}
|
||||
let contents = toml::to_string_pretty(settings).map_err(|err| err.to_string())?;
|
||||
std::fs::write(dest, contents).map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user