use eframe::egui; use crate::api::ApiClient; use crate::config::{KioskFilterSettings, KioskUiSettings}; use crate::models::{QueryRequest, OrderBy, UserInfo}; use serde::Deserialize; use std::collections::HashMap; #[derive(Debug, Clone, Deserialize)] pub struct KioskUser { pub id: i32, pub username: String, pub name: String, pub role_id: i32, } #[derive(Debug, Clone, Deserialize)] pub struct KioskRole { pub id: i32, pub name: String, } pub enum LoginResult { None, Success(UserInfo, String), } pub struct KioskLoginView { users: Vec, roles: HashMap, selected_user: Option, pin_input: String, rfid_input: String, error_message: Option, is_loading: bool, filter_settings: KioskFilterSettings, ui_settings: KioskUiSettings, is_rfid_mode: bool, } impl KioskLoginView { pub fn reset(&mut self) { self.selected_user = None; self.pin_input.clear(); self.rfid_input.clear(); self.error_message = None; self.is_rfid_mode = false; } pub fn new(filter_settings: KioskFilterSettings, ui_settings: KioskUiSettings) -> Self { Self { users: Vec::new(), roles: HashMap::new(), selected_user: None, pin_input: String::new(), rfid_input: String::new(), error_message: None, is_loading: false, filter_settings, ui_settings, is_rfid_mode: false, } } pub fn refresh_users(&mut self, client: &ApiClient, current_user: Option<&UserInfo>) { self.is_loading = true; // Fetch Roles first if needed for filtering if self.filter_settings.role_whitelist_enabled || self.filter_settings.role_blacklist_enabled { let role_req = QueryRequest { action: "select".to_string(), table: "roles".to_string(), columns: Some(vec!["id".to_string(), "name".to_string()]), data: None, r#where: None, filter: None, order_by: None, limit: Some(100), offset: None, joins: None, }; if let Ok(response) = client.query(&role_req) { if response.success { if let Some(data) = response.data { if let Ok(roles) = serde_json::from_value::>(data) { self.roles = roles.into_iter().map(|r| (r.id, r.name)).collect(); } } } } } let request = QueryRequest { action: "select".to_string(), table: "users".to_string(), columns: Some(vec!["id".to_string(), "username".to_string(), "name".to_string(), "role_id".to_string()]), data: None, r#where: None, filter: None, order_by: Some(vec![OrderBy { column: "name".to_string(), direction: "ASC".to_string(), }]), limit: Some(100), offset: None, joins: None, }; match client.query(&request) { Ok(response) => { if response.success { if let Some(data) = response.data { match serde_json::from_value::>(data) { Ok(mut users) => { // Apply filters users.retain(|u| { // Hide self if self.filter_settings.hide_self { if let Some(curr) = current_user { if u.username == curr.username { return false; } } } // Username Whitelist if self.filter_settings.username_whitelist_enabled { if !self.filter_settings.username_whitelist.contains(&u.username) { return false; } } // Username Blacklist if self.filter_settings.username_blacklist_enabled { if self.filter_settings.username_blacklist.contains(&u.username) { return false; } } // Role Filtering if self.filter_settings.role_whitelist_enabled || self.filter_settings.role_blacklist_enabled { if let Some(role_name) = self.roles.get(&u.role_id) { if self.filter_settings.role_whitelist_enabled { if !self.filter_settings.role_whitelist.contains(role_name) { return false; } } if self.filter_settings.role_blacklist_enabled { if self.filter_settings.role_blacklist.contains(role_name) { return false; } } } } true }); self.users = users; self.error_message = None; } Err(e) => { self.error_message = Some(format!("Failed to parse users: {}", e)); } } } else { self.users.clear(); } } else { self.error_message = Some(format!("Failed to fetch users: {}", response.error.unwrap_or_default())); } } Err(e) => { self.error_message = Some(format!("Network error: {}", e)); } } self.is_loading = false; } pub fn show(&mut self, ui: &mut egui::Ui, client: &ApiClient) -> LoginResult { let mut result = LoginResult::None; ui.vertical_centered(|ui| { ui.add_space(50.0); ui.heading(egui::RichText::new(&self.ui_settings.title).size(32.0)); ui.add_space(30.0); if self.is_rfid_mode { result = self.show_rfid_entry(ui, client); } else if self.selected_user.is_none() { self.show_user_selection(ui); } else { result = self.show_pin_entry(ui, client); } }); result } fn show_user_selection(&mut self, ui: &mut egui::Ui) { ui.label(egui::RichText::new("Select User").size(self.ui_settings.font_size)); ui.add_space(20.0); if self.is_loading { ui.spinner(); return; } if let Some(error) = &self.error_message { ui.label(egui::RichText::new(error).color(egui::Color32::RED)); ui.add_space(10.0); } egui::ScrollArea::vertical().show(ui, |ui| { ui.vertical_centered(|ui| { for user in &self.users { let btn = egui::Button::new( egui::RichText::new(&user.name).size(self.ui_settings.font_size * 0.8) ).min_size(egui::vec2(300.0, self.ui_settings.button_height)); if ui.add(btn).clicked() { self.selected_user = Some(user.clone()); self.pin_input.clear(); self.error_message = None; } ui.add_space(10.0); } if self.ui_settings.enable_rfid { ui.add_space(20.0); let btn = egui::Button::new( egui::RichText::new("RFID Sign In").size(self.ui_settings.font_size * 0.8) ).min_size(egui::vec2(300.0, self.ui_settings.button_height)); if ui.add(btn).clicked() { self.is_rfid_mode = true; self.rfid_input.clear(); self.error_message = None; } } }); }); } fn show_rfid_entry(&mut self, ui: &mut egui::Ui, client: &ApiClient) -> LoginResult { ui.label(egui::RichText::new("Scan RFID Tag").size(self.ui_settings.font_size)); ui.add_space(20.0); // Invisible input that keeps focus to capture RFID scanner input let response = ui.add(egui::TextEdit::singleline(&mut self.rfid_input).password(true).desired_width(0.0)); // Always request focus response.request_focus(); // Check for Enter key (scanner usually sends Enter at the end) // Also check if input length > 0 to avoid empty submissions if (response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))) || (ui.input(|i| i.key_pressed(egui::Key::Enter)) && !self.rfid_input.is_empty()) { return self.attempt_rfid_login(client); } ui.add_space(20.0); ui.spinner(); ui.add_space(20.0); if ui.add_sized(egui::vec2(150.0, self.ui_settings.button_height), egui::Button::new(egui::RichText::new("Cancel").size(self.ui_settings.font_size * 0.8))).clicked() { self.is_rfid_mode = false; self.rfid_input.clear(); self.error_message = None; } if let Some(error) = &self.error_message { ui.add_space(10.0); ui.label(egui::RichText::new(error).color(egui::Color32::RED)); } LoginResult::None } fn show_pin_entry(&mut self, ui: &mut egui::Ui, client: &ApiClient) -> LoginResult { if let Some(user) = &self.selected_user { ui.label(egui::RichText::new(format!("Hello, {}", user.name)).size(self.ui_settings.font_size)); ui.add_space(20.0); ui.label(egui::RichText::new("Enter PIN:").size(self.ui_settings.font_size * 0.8)); let response = ui.add(egui::TextEdit::singleline(&mut self.pin_input).password(true).font(egui::FontId::proportional(self.ui_settings.font_size))); // Auto-focus PIN input if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { return self.attempt_login(client); } ui.add_space(20.0); // OSK if self.ui_settings.enable_osk { self.show_osk(ui); ui.add_space(20.0); } let result = ui.horizontal(|ui| { // Align buttons to bottom/center with larger size let btn_size = egui::vec2(150.0, self.ui_settings.button_height); let total_width = btn_size.x * 2.0 + 20.0; // 2 buttons + spacing let available_width = ui.available_width(); let margin = (available_width - total_width) / 2.0; if margin > 0.0 { ui.add_space(margin); } if ui.add_sized(btn_size, egui::Button::new(egui::RichText::new("Back").size(self.ui_settings.font_size * 0.8))).clicked() { self.selected_user = None; self.pin_input.clear(); self.error_message = None; } ui.add_space(20.0); if ui.add_sized(btn_size, egui::Button::new(egui::RichText::new("Login").size(self.ui_settings.font_size * 0.8))).clicked() { return self.attempt_login(client); } LoginResult::None }).inner; if let Some(error) = &self.error_message { ui.add_space(10.0); ui.label(egui::RichText::new(error).color(egui::Color32::RED)); } return result; } LoginResult::None } fn show_osk(&mut self, ui: &mut egui::Ui) { let keys = [ ["1", "2", "3"], ["4", "5", "6"], ["7", "8", "9"], ["CLR", "0", "DEL"], ]; let btn_size = egui::vec2(80.0, 80.0); // Large touch targets ui.vertical_centered(|ui| { for row in keys { ui.horizontal(|ui| { // Center the row content let row_width = btn_size.x * 3.0 + ui.style().spacing.item_spacing.x * 2.0; let available_width = ui.available_width(); let margin = (available_width - row_width) / 2.0; if margin > 0.0 { ui.add_space(margin); } for key in row { if ui.add_sized(btn_size, egui::Button::new(egui::RichText::new(key).size(32.0))).clicked() { match key { "CLR" => self.pin_input.clear(), "DEL" => { self.pin_input.pop(); }, _ => self.pin_input.push_str(key), } } } }); ui.add_space(10.0); } }); } fn attempt_login(&mut self, client: &ApiClient) -> LoginResult { if let Some(user) = &self.selected_user { match client.login_pin(&user.username, &self.pin_input) { Ok(response) => { if response.success { self.error_message = None; if let (Some(user), Some(token)) = (response.user, response.token) { return LoginResult::Success(user, token); } else { self.error_message = Some("Login successful but no user data or token returned".to_string()); } } else { self.error_message = Some(response.error.unwrap_or_else(|| "Login failed".to_string())); } } Err(e) => { self.error_message = Some(format!("Login error: {}", e)); } } } LoginResult::None } fn attempt_rfid_login(&mut self, client: &ApiClient) -> LoginResult { match client.login_token(&self.rfid_input) { Ok(response) => { if response.success { self.error_message = None; self.is_rfid_mode = false; self.rfid_input.clear(); if let (Some(user), Some(token)) = (response.user, response.token) { return LoginResult::Success(user, token); } else { self.error_message = Some("Login successful but no user data or token returned".to_string()); } } else { self.error_message = Some(response.error.unwrap_or_else(|| "Login failed".to_string())); self.rfid_input.clear(); } } Err(e) => { self.error_message = Some(format!("Login error: {}", e)); self.rfid_input.clear(); } } LoginResult::None } }