Ver código fonte

Merge branch 'hell' of crt/BeepZone-Client-eGUI-Emo into master

crt 3 semanas atrás
pai
commit
b6d6ea01d3

+ 6 - 0
src/config.rs

@@ -82,6 +82,8 @@ pub struct KioskUiSettings {
     pub fullscreen: bool,
     pub button_height: f32,
     pub font_size: f32,
+    #[serde(default = "default_scale", alias = "component_scale")]
+    pub scale: f32,
     #[serde(default = "default_min_power")]
     pub minimum_power_level_for_full_ui: i32,
     #[serde(default)]
@@ -96,6 +98,10 @@ fn default_kiosk_title() -> String {
     "BeepZone Kiosk".to_string()
 }
 
+fn default_scale() -> f32 {
+    2.0
+}
+
 fn default_min_power() -> i32 {
     50
 }

+ 69 - 36
src/core/table_renderer.rs

@@ -141,6 +141,12 @@ pub enum SelectionModifier {
     Shift,
 }
 
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum SelectionMode {
+    Single,
+    Multi,
+}
+
 /// Callbacks for table events
 pub trait TableEventHandler<T> {
     fn on_double_click(&mut self, item: &T, row_index: usize);
@@ -153,6 +159,7 @@ pub struct TableRenderer {
     pub columns: Vec<ColumnConfig>,
     pub sort_config: SortConfig,
     pub selection: SelectionManager,
+    pub selection_mode: SelectionMode,
     pub search_query: String,
     pub search_fields: Vec<String>,
 }
@@ -169,6 +176,7 @@ impl TableRenderer {
             columns: Vec::new(),
             sort_config: SortConfig::default(),
             selection: SelectionManager::new(),
+            selection_mode: SelectionMode::Multi,
             search_query: String::new(),
             search_fields: vec![
                 // Default search fields for assets/inventory
@@ -213,6 +221,11 @@ impl TableRenderer {
         self
     }
 
+    pub fn with_selection_mode(mut self, mode: SelectionMode) -> Self {
+        self.selection_mode = mode;
+        self
+    }
+
     /// Filter and sort JSON values based on current configuration
     pub fn prepare_json_data<'a>(&self, data: &'a [Value]) -> Vec<(usize, &'a Value)> {
         let mut filtered: Vec<(usize, &Value)> = data
@@ -297,24 +310,30 @@ impl TableRenderer {
 
         table
             .header(24.0, |mut header| {
-                // Select-all checkbox header
+                // Selection header: Select All for multi, label for single
                 header.col(|ui| {
-                    let all_selected = data.len() > 0
-                        && (0..data.len()).all(|i| self.selection.selected_rows.contains(&i));
-                    let mut chk = all_selected;
-                    if ui
-                        .checkbox(&mut chk, "")
-                        .on_hover_text("Select All")
-                        .clicked()
-                    {
-                        if chk {
-                            self.selection.select_all(data.len());
-                        } else {
-                            self.selection.clear_selection();
-                        }
-                        if let Some(ref mut handler) = event_handler {
-                            handler.on_selection_changed(&self.selection.get_selected_indices());
+                    if matches!(self.selection_mode, SelectionMode::Multi) {
+                        let all_selected = data.len() > 0
+                            && (0..data.len()).all(|i| self.selection.selected_rows.contains(&i));
+                        let mut chk = all_selected;
+                        if ui
+                            .checkbox(&mut chk, "")
+                            .on_hover_text("Select All")
+                            .clicked()
+                        {
+                            if chk {
+                                self.selection.select_all(data.len());
+                            } else {
+                                self.selection.clear_selection();
+                            }
+                            if let Some(ref mut handler) = event_handler {
+                                handler.on_selection_changed(
+                                    &self.selection.get_selected_indices(),
+                                );
+                            }
                         }
+                    } else {
+                        ui.label("Selected");
                     }
                 });
 
@@ -359,19 +378,28 @@ impl TableRenderer {
                             let mut checked = self.selection.is_selected(idx);
                             let resp = ui.checkbox(&mut checked, "");
                             if resp.changed() {
-                                let mods = ui.input(|i| i.modifiers);
-                                let modifier = if mods.shift {
-                                    SelectionModifier::Shift
-                                } else if mods.command || mods.ctrl {
-                                    SelectionModifier::Ctrl
-                                } else {
-                                    SelectionModifier::None
-                                };
-
-                                if checked {
-                                    self.selection.toggle_row(idx, modifier);
+                                if matches!(self.selection_mode, SelectionMode::Multi) {
+                                    let mods = ui.input(|i| i.modifiers);
+                                    let modifier = if mods.shift {
+                                        SelectionModifier::Shift
+                                    } else if mods.command || mods.ctrl {
+                                        SelectionModifier::Ctrl
+                                    } else {
+                                        SelectionModifier::None
+                                    };
+
+                                    if checked {
+                                        self.selection.toggle_row(idx, modifier);
+                                    } else {
+                                        self.selection.selected_rows.remove(&idx);
+                                    }
                                 } else {
-                                    self.selection.selected_rows.remove(&idx);
+                                    if checked {
+                                        self.selection.clear_selection();
+                                        self.selection.selected_rows.insert(idx);
+                                    } else {
+                                        self.selection.selected_rows.remove(&idx);
+                                    }
                                 }
 
                                 if let Some(ref mut handler) = event_handler {
@@ -422,11 +450,16 @@ impl TableRenderer {
                                 self.selection.last_click_time = None;
                             } else {
                                 // Single click selection
-                                let mods = row_response.ctx.input(|i| i.modifiers);
-                                let modifier = if mods.shift {
-                                    SelectionModifier::Shift
-                                } else if mods.command || mods.ctrl {
-                                    SelectionModifier::Ctrl
+                                let modifier = if matches!(self.selection_mode, SelectionMode::Multi)
+                                {
+                                    let mods = row_response.ctx.input(|i| i.modifiers);
+                                    if mods.shift {
+                                        SelectionModifier::Shift
+                                    } else if mods.command || mods.ctrl {
+                                        SelectionModifier::Ctrl
+                                    } else {
+                                        SelectionModifier::None
+                                    }
                                 } else {
                                     SelectionModifier::None
                                 };
@@ -682,7 +715,7 @@ impl JsonCellRenderer {
                 if let Some(date_str) = json_value.as_str() {
                     let text =
                         if let Ok(date) = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
-                            date.format("%b %d, %Y").to_string()
+                            date.format("%d %b %Y").to_string()
                         } else {
                             date_str.to_string()
                         };
@@ -698,9 +731,9 @@ impl JsonCellRenderer {
                     let text = if let Ok(dt) =
                         chrono::NaiveDateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S")
                     {
-                        dt.format("%b %d, %Y %H:%M").to_string()
+                        dt.format("%d %b %Y %H:%M").to_string()
                     } else if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(datetime_str) {
-                        dt.format("%b %d, %Y %H:%M").to_string()
+                        dt.format("%d %b %Y %H:%M").to_string()
                     } else {
                         datetime_str.to_string()
                     };

+ 148 - 278
src/core/workflows/borrow_flow.rs

@@ -3,6 +3,7 @@ use chrono::{Duration, Local};
 use eframe::egui;
 use egui_phosphor::variants::regular as icons;
 use serde_json::Value;
+use crate::core::table_renderer::{TableRenderer, ColumnConfig, SelectionMode};
 
 use crate::api::ApiClient;
 use crate::models::QueryRequest;
@@ -41,6 +42,9 @@ pub struct BorrowFlow {
     pub asset_search: String,
     pub asset_loading: bool,
 
+    // Shared table renderer for assets (kiosk-friendly, consistent with inventory view)
+    pub assets_table: TableRenderer,
+
     // Step 2: Borrower Selection
     pub borrower_selection: BorrowerSelection,
     pub registered_borrowers: Vec<Value>,
@@ -48,6 +52,9 @@ pub struct BorrowFlow {
     pub borrower_search: String,
     pub borrower_loading: bool,
 
+    // Shared table renderer for borrowers
+    pub borrowers_table: TableRenderer,
+
     // New borrower registration fields
     pub new_borrower_name: String,
     pub new_borrower_class: String,
@@ -73,6 +80,26 @@ pub struct BorrowFlow {
 
 impl Default for BorrowFlow {
     fn default() -> Self {
+        let assets_table = TableRenderer::new()
+            .with_columns(vec![
+                ColumnConfig::new("Asset Tag", "asset_tag").with_width(140.0),
+                ColumnConfig::new("Name", "name").with_width(240.0),
+                ColumnConfig::new("Category", "category_name").with_width(160.0),
+            ])
+            .with_default_sort("name", true)
+            .with_selection_mode(SelectionMode::Single);
+
+        let borrowers_table = TableRenderer::new()
+            .with_columns(vec![
+                ColumnConfig::new("Name", "name").with_width(200.0),
+                ColumnConfig::new("Class", "class_name").with_width(160.0),
+                ColumnConfig::new("Role", "role").with_width(140.0),
+                ColumnConfig::new("Email", "email").with_width(240.0),
+                ColumnConfig::new("Phone", "phone_number").with_width(160.0),
+            ])
+            .with_default_sort("name", true)
+            .with_selection_mode(SelectionMode::Single);
+
         Self {
             is_open: false,
             current_step: BorrowStep::SelectAsset,
@@ -82,12 +109,14 @@ impl Default for BorrowFlow {
             selected_asset: None,
             asset_search: String::new(),
             asset_loading: false,
+            assets_table,
 
             borrower_selection: BorrowerSelection::None,
             registered_borrowers: Vec::new(),
             banned_borrowers: Vec::new(),
             borrower_search: String::new(),
             borrower_loading: false,
+            borrowers_table,
 
             new_borrower_name: String::new(),
             new_borrower_class: String::new(),
@@ -172,35 +201,7 @@ impl BorrowFlow {
             .collapsible(false)
             .open(&mut keep_open)
             .show(ctx, |ui| {
-                // Progress indicator
-                self.show_progress_bar(ui);
-
-                ui.separator();
-
-                // Show error/success messages
-                if let Some(err) = &self.error_message {
-                    ui.colored_label(egui::Color32::RED, err);
-                    ui.separator();
-                }
-                if let Some(msg) = &self.success_message {
-                    ui.colored_label(egui::Color32::GREEN, msg);
-                    ui.separator();
-                }
-
-                // Main content area
-                egui::ScrollArea::vertical()
-                    .id_salt("borrow_flow_main_scroll")
-                    .show(ui, |ui| match self.current_step {
-                        BorrowStep::SelectAsset => self.show_asset_selection(ui, api_client),
-                        BorrowStep::SelectBorrower => self.show_borrower_selection(ui, api_client),
-                        BorrowStep::SelectDuration => self.show_duration_selection(ui),
-                        BorrowStep::Confirm => self.show_confirmation(ui),
-                    });
-
-                ui.separator();
-
-                // Navigation buttons
-                self.show_navigation_buttons(ui, api_client);
+                self.show_content(ui, api_client);
             });
         if !self.is_open {
             keep_open = false;
@@ -215,6 +216,51 @@ impl BorrowFlow {
         keep_open
     }
 
+    pub fn show_content(&mut self, ui: &mut egui::Ui, api_client: &ApiClient) {
+        // Progress indicator
+        self.show_progress_bar(ui);
+
+        ui.separator();
+
+        // Show error/success messages
+        if let Some(err) = &self.error_message {
+            ui.colored_label(egui::Color32::RED, err);
+            ui.separator();
+        }
+        if let Some(msg) = &self.success_message {
+            ui.colored_label(egui::Color32::GREEN, msg);
+            ui.separator();
+        }
+
+        // Reserve space for footer; scroll main content within a fixed area
+        let footer_reserved_height: f32 = 100.0;
+        let content_height = (ui.available_height() - footer_reserved_height).max(200.0);
+
+        ui.allocate_ui_with_layout(
+            egui::vec2(ui.available_width(), content_height),
+            egui::Layout::top_down(egui::Align::Min),
+            |ui| {
+                egui::ScrollArea::vertical()
+                    .id_salt("borrow_flow_main_scroll")
+                    .max_height(content_height)
+                    .auto_shrink([false; 2])
+                    .show(ui, |ui| {
+                        match self.current_step {
+                            BorrowStep::SelectAsset => self.show_asset_selection(ui, api_client),
+                            BorrowStep::SelectBorrower => self.show_borrower_selection(ui, api_client),
+                            BorrowStep::SelectDuration => self.show_duration_selection(ui),
+                            BorrowStep::Confirm => self.show_confirmation(ui),
+                        }
+                    });
+            },
+        );
+
+        ui.separator();
+
+        // Navigation buttons
+        self.show_navigation_buttons(ui, api_client);
+    }
+
     fn show_progress_bar(&self, ui: &mut egui::Ui) {
         ui.horizontal(|ui| {
             let step_index = match self.current_step {
@@ -253,41 +299,55 @@ impl BorrowFlow {
         ui.heading("What do you want to borrow?");
         ui.add_space(10.0);
 
-        // Scan field
+        // Unified scan/search input
         ui.horizontal(|ui| {
-            ui.label("Scan or Enter Asset Tag/ID:");
+            ui.label("Scan or Search (Tag/ID):");
+            let input_id = egui::Id::new("borrow_flow_scan_search_input");
             let response = ui.add(
-                egui::TextEdit::singleline(&mut self.scan_input)
-                    .id(egui::Id::new("borrow_flow_scan_input"))
-                    .hint_text("Scan barcode or type asset tag...")
-                    .desired_width(300.0),
+                egui::TextEdit::singleline(&mut self.asset_search)
+                    .id(input_id)
+                    .hint_text("...")
+                    .desired_width(360.0),
             );
 
-            if response.changed() && !self.scan_input.is_empty() {
-                self.try_scan_asset(api_client);
-            }
-
-            if ui.button("Clear").clicked() {
-                self.scan_input.clear();
-            }
-        });
+            // Focus by default
+            response.request_focus();
+
+            // On Enter: if exactly one unique match by tag or numeric id, select and advance
+            if response.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
+                let needle = self.asset_search.trim();
+                if !needle.is_empty() {
+                    let mut matches: Vec<&Value> = Vec::new();
+                    let parsed_num = needle.parse::<i64>().ok();
+                    for asset in &self.available_assets {
+                        let tag = asset
+                            .get("asset_tag")
+                            .and_then(|v| v.as_str())
+                            .unwrap_or("");
+                        let num = asset.get("asset_numeric_id").and_then(|v| v.as_i64());
+                        if tag.eq_ignore_ascii_case(needle)
+                            || (parsed_num.is_some() && num == parsed_num)
+                        {
+                            matches.push(asset);
+                        }
+                    }
 
-        ui.add_space(10.0);
-        ui.separator();
-        ui.add_space(10.0);
+                    // Deduplicate by id just in case
+                    let mut unique: Vec<&Value> = Vec::new();
+                    for m in matches {
+                        let mid = m.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
+                        if !unique.iter().any(|u| u.get("id").and_then(|v| v.as_i64()).unwrap_or(-2) == mid) {
+                            unique.push(m);
+                        }
+                    }
 
-        // Search bar
-        ui.horizontal(|ui| {
-            ui.label("Search:");
-            if ui
-                .add(
-                    egui::TextEdit::singleline(&mut self.asset_search)
-                        .id(egui::Id::new("borrow_flow_asset_search")),
-                )
-                .changed()
-            {
-                // Filter is applied in the table rendering
+                    if unique.len() == 1 {
+                        self.selected_asset = Some(unique[0].clone());
+                        self.go_to_borrower_selection(api_client);
+                    }
+                }
             }
+
             if ui.button("Refresh").clicked() {
                 self.load_available_assets(api_client);
             }
@@ -295,10 +355,10 @@ impl BorrowFlow {
 
         ui.add_space(5.0);
 
-        // Assets table
+        // Assets table (inventory-style renderer)
         ui.label(egui::RichText::new("All Lendable Items").strong());
         ui.allocate_ui_with_layout(
-            egui::vec2(ui.available_width(), 300.0),
+            egui::vec2(ui.available_width(), 320.0),
             egui::Layout::top_down(egui::Align::Min),
             |ui| {
                 self.render_assets_table(ui);
@@ -678,8 +738,8 @@ impl BorrowFlow {
                             .add_enabled(
                                 can_submit,
                                 egui::Button::new(format!(
-                                    "{} Approve & Submit",
-                                    icons::ARROW_LEFT
+                                    "{} Process Checkout",
+                                    icons::CHECK_CIRCLE
                                 )),
                             )
                             .clicked()
@@ -719,6 +779,7 @@ impl BorrowFlow {
             columns: Some(vec![
                 "assets.id".to_string(),
                 "assets.asset_tag".to_string(),
+                "assets.asset_numeric_id".to_string(),
                 "assets.name".to_string(),
                 "assets.category_id".to_string(),
                 "assets.lending_status".to_string(),
@@ -924,230 +985,39 @@ impl BorrowFlow {
 
     // Table rendering methods
     fn render_assets_table(&mut self, ui: &mut egui::Ui) {
-        use egui_extras::{Column, TableBuilder};
+        // Sync search query from the workflow input
+        self.assets_table.set_search_query(self.asset_search.clone());
 
-        // Filter assets based on search
-        let filtered_assets: Vec<&Value> = self
-            .available_assets
-            .iter()
-            .filter(|asset| {
-                if self.asset_search.is_empty() {
-                    return true;
-                }
-                let search_lower = self.asset_search.to_lowercase();
-                let tag = asset
-                    .get("asset_tag")
-                    .and_then(|v| v.as_str())
-                    .unwrap_or("");
-                let name = asset.get("name").and_then(|v| v.as_str()).unwrap_or("");
-                let category = asset
-                    .get("category_name")
-                    .and_then(|v| v.as_str())
-                    .unwrap_or("");
-
-                tag.to_lowercase().contains(&search_lower)
-                    || name.to_lowercase().contains(&search_lower)
-                    || category.to_lowercase().contains(&search_lower)
-            })
-            .collect();
-
-        TableBuilder::new(ui)
-            .id_salt("borrow_flow_assets_table")
-            .striped(true)
-            .resizable(true)
-            .sense(egui::Sense::click())
-            .cell_layout(egui::Layout::left_to_right(egui::Align::Center))
-            .column(Column::auto().resizable(true))
-            .column(Column::remainder().resizable(true))
-            .column(Column::auto().resizable(true))
-            .column(Column::auto().resizable(true))
-            .min_scrolled_height(0.0)
-            .max_scroll_height(300.0)
-            .header(22.0, |mut header| {
-                header.col(|ui| {
-                    ui.strong("Asset Tag");
-                });
-                header.col(|ui| {
-                    ui.strong("Name");
-                });
-                header.col(|ui| {
-                    ui.strong("Category");
-                });
-                header.col(|ui| {
-                    ui.strong("Action");
-                });
-            })
-            .body(|mut body| {
-                for asset in filtered_assets {
-                    body.row(20.0, |mut row| {
-                        let asset_id = asset.get("id").and_then(|v| v.as_i64()).unwrap_or(0);
-                        let is_selected = self
-                            .selected_asset
-                            .as_ref()
-                            .and_then(|s| s.get("id").and_then(|v| v.as_i64()))
-                            .map(|id| id == asset_id)
-                            .unwrap_or(false);
+        // Prepare and render using shared renderer
+        let prepared = self.assets_table.prepare_json_data(&self.available_assets);
+        self.assets_table.render_json_table(ui, &prepared, None);
 
-                        row.col(|ui| {
-                            ui.label(
-                                asset
-                                    .get("asset_tag")
-                                    .and_then(|v| v.as_str())
-                                    .unwrap_or("N/A"),
-                            );
-                        });
-                        row.col(|ui| {
-                            ui.label(asset.get("name").and_then(|v| v.as_str()).unwrap_or("N/A"));
-                        });
-                        row.col(|ui| {
-                            ui.label(
-                                asset
-                                    .get("category_name")
-                                    .and_then(|v| v.as_str())
-                                    .unwrap_or("N/A"),
-                            );
-                        });
-                        row.col(|ui| {
-                            if is_selected {
-                                ui.colored_label(egui::Color32::GREEN, "Selected");
-                            } else {
-                                let button_id = format!("select_asset_{}", asset_id);
-                                if ui.button("Select").on_hover_text(&button_id).clicked() {
-                                    self.selected_asset = Some((*asset).clone());
-                                }
-                            }
-                        });
-                    });
-                }
-            });
+        // Map selection back to self.selected_asset (single-select semantics)
+        let sel = self.assets_table.selection.get_selected_indices();
+        if sel.is_empty() {
+            self.selected_asset = None;
+        } else {
+            let idx = sel[0].min(prepared.len().saturating_sub(1));
+            self.selected_asset = Some(prepared[idx].1.clone());
+        }
     }
 
     fn render_borrowers_table(&mut self, ui: &mut egui::Ui) {
-        use egui_extras::{Column, TableBuilder};
-
-        // Filter borrowers based on search
-        let filtered_borrowers: Vec<&Value> = self
-            .registered_borrowers
-            .iter()
-            .filter(|borrower| {
-                if self.borrower_search.is_empty() {
-                    return true;
-                }
-                let search_lower = self.borrower_search.to_lowercase();
-                let name = borrower.get("name").and_then(|v| v.as_str()).unwrap_or("");
-                let class = borrower
-                    .get("class_name")
-                    .and_then(|v| v.as_str())
-                    .unwrap_or("");
-                let role = borrower.get("role").and_then(|v| v.as_str()).unwrap_or("");
+        // Sync search query
+        self.borrowers_table.set_search_query(self.borrower_search.clone());
 
-                name.to_lowercase().contains(&search_lower)
-                    || class.to_lowercase().contains(&search_lower)
-                    || role.to_lowercase().contains(&search_lower)
-            })
-            .collect();
+        // Prepare and render
+        let prepared = self.borrowers_table.prepare_json_data(&self.registered_borrowers);
+        self.borrowers_table.render_json_table(ui, &prepared, None);
 
-        TableBuilder::new(ui)
-            .id_salt("borrow_flow_borrowers_table")
-            .striped(true)
-            .resizable(true)
-            .sense(egui::Sense::click())
-            .cell_layout(egui::Layout::left_to_right(egui::Align::Center))
-            .column(Column::auto().resizable(true))
-            .column(Column::auto().resizable(true))
-            .column(Column::auto().resizable(true))
-            .column(Column::remainder().resizable(true))
-            .column(Column::auto().resizable(true))
-            .column(Column::auto().resizable(true))
-            .min_scrolled_height(0.0)
-            .max_scroll_height(300.0)
-            .header(22.0, |mut header| {
-                header.col(|ui| {
-                    ui.strong("Name");
-                });
-                header.col(|ui| {
-                    ui.strong("Class");
-                });
-                header.col(|ui| {
-                    ui.strong("Role");
-                });
-                header.col(|ui| {
-                    ui.strong("Email");
-                });
-                header.col(|ui| {
-                    ui.strong("Phone");
-                });
-                header.col(|ui| {
-                    ui.strong("Action");
-                });
-            })
-            .body(|mut body| {
-                for borrower in filtered_borrowers {
-                    body.row(20.0, |mut row| {
-                        let borrower_id = borrower.get("id").and_then(|v| v.as_i64()).unwrap_or(0);
-                        let is_selected = match &self.borrower_selection {
-                            BorrowerSelection::Existing(b) => b
-                                .get("id")
-                                .and_then(|v| v.as_i64())
-                                .map(|id| id == borrower_id)
-                                .unwrap_or(false),
-                            _ => false,
-                        };
-
-                        row.col(|ui| {
-                            ui.label(
-                                borrower
-                                    .get("name")
-                                    .and_then(|v| v.as_str())
-                                    .unwrap_or("N/A"),
-                            );
-                        });
-                        row.col(|ui| {
-                            ui.label(
-                                borrower
-                                    .get("class_name")
-                                    .and_then(|v| v.as_str())
-                                    .unwrap_or("N/A"),
-                            );
-                        });
-                        row.col(|ui| {
-                            ui.label(
-                                borrower
-                                    .get("role")
-                                    .and_then(|v| v.as_str())
-                                    .unwrap_or("N/A"),
-                            );
-                        });
-                        row.col(|ui| {
-                            ui.label(
-                                borrower
-                                    .get("email")
-                                    .and_then(|v| v.as_str())
-                                    .unwrap_or("N/A"),
-                            );
-                        });
-                        row.col(|ui| {
-                            ui.label(
-                                borrower
-                                    .get("phone_number")
-                                    .and_then(|v| v.as_str())
-                                    .unwrap_or("N/A"),
-                            );
-                        });
-                        row.col(|ui| {
-                            if is_selected {
-                                ui.colored_label(egui::Color32::GREEN, "Selected");
-                            } else {
-                                let button_id = format!("select_borrower_{}", borrower_id);
-                                if ui.button("Select").on_hover_text(&button_id).clicked() {
-                                    self.borrower_selection =
-                                        BorrowerSelection::Existing((*borrower).clone());
-                                }
-                            }
-                        });
-                    });
-                }
-            });
+        // Map selection
+        let sel = self.borrowers_table.selection.get_selected_indices();
+        if sel.is_empty() {
+            self.borrower_selection = BorrowerSelection::None;
+        } else {
+            let idx = sel[0].min(prepared.len().saturating_sub(1));
+            self.borrower_selection = BorrowerSelection::Existing(prepared[idx].1.clone());
+        }
     }
 
     fn render_banned_borrowers_table(&self, ui: &mut egui::Ui) {

+ 151 - 184
src/core/workflows/return_flow.rs

@@ -3,6 +3,7 @@ use chrono::Local;
 use eframe::egui;
 use egui_phosphor::variants::regular as icons;
 use serde_json::Value;
+use crate::core::table_renderer::{TableRenderer, ColumnConfig, SelectionMode};
 
 use crate::api::ApiClient;
 use crate::models::QueryRequest;
@@ -24,6 +25,8 @@ pub struct ReturnFlow {
     pub selected_loan: Option<Value>,
     pub loan_search: String,
     pub loan_loading: bool,
+    // Shared table renderer for active loans
+    pub loans_table: TableRenderer,
 
     // Step 2: Notes and Issue Reporting
     pub return_notes: String,
@@ -34,6 +37,8 @@ pub struct ReturnFlow {
     pub issue_description: String,
     pub issue_severity: String,
     pub issue_priority: String,
+    // Optional: update asset status during return when reporting an issue
+    pub new_asset_status: Option<String>,
 
     // Error handling
     pub error_message: Option<String>,
@@ -43,6 +48,17 @@ pub struct ReturnFlow {
 
 impl Default for ReturnFlow {
     fn default() -> Self {
+        let loans_table = TableRenderer::new()
+            .with_columns(vec![
+                ColumnConfig::new("Asset Tag", "asset_tag").with_width(140.0),
+                ColumnConfig::new("Name", "asset_name").with_width(240.0),
+                ColumnConfig::new("Borrower", "borrower_name").with_width(200.0),
+                ColumnConfig::new("Class", "class_name").with_width(160.0),
+                ColumnConfig::new("Due Date", "due_date").with_width(140.0),
+            ])
+            .with_default_sort("due_date", true)
+            .with_selection_mode(SelectionMode::Single);
+
         Self {
             is_open: false,
             current_step: ReturnStep::SelectLoan,
@@ -52,6 +68,7 @@ impl Default for ReturnFlow {
             selected_loan: None,
             loan_search: String::new(),
             loan_loading: false,
+            loans_table,
 
             return_notes: String::new(),
 
@@ -60,6 +77,7 @@ impl Default for ReturnFlow {
             issue_description: String::new(),
             issue_severity: String::from("Medium"),
             issue_priority: String::from("Normal"),
+            new_asset_status: None,
 
             error_message: None,
             success_message: None,
@@ -108,6 +126,7 @@ impl ReturnFlow {
         self.issue_description.clear();
         self.issue_severity = String::from("Medium");
         self.issue_priority = String::from("Normal");
+        self.new_asset_status = None;
 
         self.error_message = None;
         self.success_message = None;
@@ -124,33 +143,7 @@ impl ReturnFlow {
             .collapsible(false)
             .open(&mut keep_open)
             .show(ctx, |ui| {
-                // Progress indicator
-                self.show_progress_bar(ui);
-
-                ui.separator();
-
-                // Show error/success messages
-                if let Some(err) = &self.error_message {
-                    ui.colored_label(egui::Color32::RED, err);
-                    ui.separator();
-                }
-                if let Some(msg) = &self.success_message {
-                    ui.colored_label(egui::Color32::GREEN, msg);
-                    ui.separator();
-                }
-
-                // Main content area
-                egui::ScrollArea::vertical()
-                    .id_salt("return_flow_main_scroll")
-                    .show(ui, |ui| match self.current_step {
-                        ReturnStep::SelectLoan => self.show_loan_selection(ui, api_client),
-                        ReturnStep::Confirm => self.show_confirmation(ui),
-                    });
-
-                ui.separator();
-
-                // Navigation buttons
-                self.show_navigation_buttons(ui, api_client);
+                self.show_content(ui, api_client);
             });
 
         if !keep_open {
@@ -160,6 +153,37 @@ impl ReturnFlow {
         keep_open
     }
 
+    pub fn show_content(&mut self, ui: &mut egui::Ui, api_client: &ApiClient) {
+        // Progress indicator
+        self.show_progress_bar(ui);
+
+        ui.add_space(8.0);
+
+        // Reserve space for footer buttons; scroll the main content if needed
+        let footer_reserved_height: f32 = 100.0; // keep nav visible
+        let content_height = (ui.available_height() - footer_reserved_height).max(200.0);
+
+        ui.allocate_ui_with_layout(
+            egui::vec2(ui.available_width(), content_height),
+            egui::Layout::top_down(egui::Align::Min),
+            |ui| {
+                egui::ScrollArea::vertical()
+                    .max_height(content_height)
+                    .auto_shrink([false; 2])
+                    .show(ui, |ui| {
+                        match self.current_step {
+                            ReturnStep::SelectLoan => self.show_loan_selection(ui, api_client),
+                            ReturnStep::Confirm => self.show_confirmation(ui),
+                        }
+                    });
+            },
+        );
+
+        ui.separator();
+        // Footer navigation (always visible)
+        self.show_navigation_buttons(ui, api_client);
+    }
+
     fn show_progress_bar(&self, ui: &mut egui::Ui) {
         ui.horizontal(|ui| {
             let step_index = match self.current_step {
@@ -194,41 +218,56 @@ impl ReturnFlow {
         ui.heading("Which item is being returned?");
         ui.add_space(10.0);
 
-        // Scan field
+        // Unified scan/search input
         ui.horizontal(|ui| {
-            ui.label("Scan or Enter Asset Tag/ID:");
+            ui.label("Scan or Search (Tag/Numeric ID):");
+            let input_id = egui::Id::new("return_flow_scan_search_input");
             let response = ui.add(
-                egui::TextEdit::singleline(&mut self.scan_input)
-                    .id(egui::Id::new("return_flow_scan_input"))
-                    .hint_text("Scan barcode or type asset tag...")
-                    .desired_width(300.0),
+                egui::TextEdit::singleline(&mut self.loan_search)
+                    .id(input_id)
+                    .hint_text("...")
+                    .desired_width(360.0),
             );
 
-            if response.changed() && !self.scan_input.is_empty() {
-                self.try_scan_loan(api_client);
-            }
-
-            if ui.button("Clear").clicked() {
-                self.scan_input.clear();
-            }
-        });
+            // Focus by default
+            response.request_focus();
+
+            // On Enter: if exactly one unique active loan by tag or numeric id, select and advance
+            if response.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
+                let needle = self.loan_search.trim();
+                if !needle.is_empty() {
+                    let mut matches: Vec<&Value> = Vec::new();
+                    let parsed_num = needle.parse::<i64>().ok();
+                    for loan in &self.active_loans {
+                        let tag = loan
+                            .get("asset_tag")
+                            .and_then(|v| v.as_str())
+                            .unwrap_or("");
+                        let num = loan.get("asset_numeric_id").and_then(|v| v.as_i64());
+                        if tag.eq_ignore_ascii_case(needle)
+                            || (parsed_num.is_some() && num == parsed_num)
+                        {
+                            matches.push(loan);
+                        }
+                    }
 
-        ui.add_space(10.0);
-        ui.separator();
-        ui.add_space(10.0);
+                    // Deduplicate by lending_history.id
+                    let mut unique: Vec<&Value> = Vec::new();
+                    for m in matches {
+                        let mid = m.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
+                        if !unique.iter().any(|u| u.get("id").and_then(|v| v.as_i64()).unwrap_or(-2) == mid) {
+                            unique.push(m);
+                        }
+                    }
 
-        // Search bar
-        ui.horizontal(|ui| {
-            ui.label("Search:");
-            if ui
-                .add(
-                    egui::TextEdit::singleline(&mut self.loan_search)
-                        .id(egui::Id::new("return_flow_loan_search")),
-                )
-                .changed()
-            {
-                // Filter is applied in the table rendering
+                    if unique.len() == 1 {
+                        self.selected_loan = Some(unique[0].clone());
+                        self.current_step = ReturnStep::Confirm;
+                        self.error_message = None;
+                    }
+                }
             }
+
             if ui.button("Refresh").clicked() {
                 self.load_active_loans(api_client);
             }
@@ -273,15 +312,26 @@ impl ReturnFlow {
 
                 ui.label(format!("Asset: {} - {}", asset_tag, asset_name));
                 ui.label(format!("Borrower: {}", borrower_name));
-                ui.label(format!("Due Date: {}", due_date));
+                let pretty_due = if let Ok(nd) = chrono::NaiveDate::parse_from_str(due_date, "%Y-%m-%d") {
+                    nd.format("%d %b %Y").to_string()
+                } else {
+                    due_date.to_string()
+                };
+                ui.label(format!("Due Date: {}", pretty_due));
 
-                // Check if overdue
+                // Check if overdue (use Phosphor icons instead of emoji)
                 if let Some(due_str) = loan.get("due_date").and_then(|v| v.as_str()) {
                     let today = Local::now().format("%Y-%m-%d").to_string();
                     if today.as_str() > due_str {
-                        ui.colored_label(egui::Color32::RED, "⚠ This item is OVERDUE!");
+                        ui.colored_label(
+                            egui::Color32::RED,
+                            format!("{} This item is OVERDUE!", icons::CLOCK),
+                        );
                     } else {
-                        ui.colored_label(egui::Color32::GREEN, "✓ Returned on time");
+                        ui.colored_label(
+                            egui::Color32::GREEN,
+                            format!("{} Returned on time", icons::CHECK),
+                        );
                     }
                 }
 
@@ -397,6 +447,29 @@ impl ReturnFlow {
                         .desired_width(f32::INFINITY)
                         .desired_rows(4),
                 );
+
+                ui.add_space(10.0);
+                // Optional: Update item status as part of the return
+                ui.horizontal(|ui| {
+                    ui.label("Update Item Status:");
+                    let current_status = if let Some(loan) = &self.selected_loan {
+                        loan.get("asset_status").and_then(|v| v.as_str()).unwrap_or("Unknown")
+                    } else {
+                        "Unknown"
+                    };
+                    egui::ComboBox::from_id_salt("return_flow_new_asset_status")
+                        .selected_text(self.new_asset_status.as_deref().unwrap_or("-- No change --"))
+                        .show_ui(ui, |ui| {
+                            ui.selectable_value(&mut self.new_asset_status, None, "-- No change --");
+                            ui.selectable_value(&mut self.new_asset_status, Some("Good".to_string()), "Good");
+                            ui.selectable_value(&mut self.new_asset_status, Some("Attention".to_string()), "Attention");
+                            ui.selectable_value(&mut self.new_asset_status, Some("Faulty".to_string()), "Faulty");
+                            ui.selectable_value(&mut self.new_asset_status, Some("Missing".to_string()), "Missing");
+                            ui.selectable_value(&mut self.new_asset_status, Some("Scrapped".to_string()), "Scrapped");
+                        });
+                    ui.add_space(8.0);
+                    ui.label(format!("(Current: {})", current_status));
+                });
             });
         }
     }
@@ -471,6 +544,8 @@ impl ReturnFlow {
                 "lending_history.due_date".to_string(),
                 "lending_history.notes".to_string(),
                 "assets.asset_tag".to_string(),
+                "assets.asset_numeric_id".to_string(),
+                "assets.status AS asset_status".to_string(),
                 "assets.name AS asset_name".to_string(),
                 "borrowers.name AS borrower_name".to_string(),
                 "borrowers.class_name".to_string(),
@@ -598,133 +673,21 @@ impl ReturnFlow {
 
     // Table rendering methods
     fn render_loans_table(&mut self, ui: &mut egui::Ui) {
-        use egui_extras::{Column, TableBuilder};
-
-        // Filter loans based on search
-        let filtered_loans: Vec<&Value> = self
-            .active_loans
-            .iter()
-            .filter(|loan| {
-                if self.loan_search.is_empty() {
-                    return true;
-                }
-                let search_lower = self.loan_search.to_lowercase();
-                let asset_tag = loan.get("asset_tag").and_then(|v| v.as_str()).unwrap_or("");
-                let asset_name = loan
-                    .get("asset_name")
-                    .and_then(|v| v.as_str())
-                    .unwrap_or("");
-                let borrower_name = loan
-                    .get("borrower_name")
-                    .and_then(|v| v.as_str())
-                    .unwrap_or("");
+        // Sync search query
+        self.loans_table.set_search_query(self.loan_search.clone());
 
-                asset_tag.to_lowercase().contains(&search_lower)
-                    || asset_name.to_lowercase().contains(&search_lower)
-                    || borrower_name.to_lowercase().contains(&search_lower)
-            })
-            .collect();
+        // Prepare and render
+        let prepared = self.loans_table.prepare_json_data(&self.active_loans);
+        self.loans_table.render_json_table(ui, &prepared, None);
 
-        TableBuilder::new(ui)
-            .id_salt("return_flow_loans_table")
-            .striped(true)
-            .resizable(true)
-            .cell_layout(egui::Layout::left_to_right(egui::Align::Center))
-            .column(Column::initial(100.0).resizable(true).at_least(80.0))
-            .column(Column::initial(180.0).resizable(true).at_least(120.0))
-            .column(Column::initial(150.0).resizable(true).at_least(100.0))
-            .column(Column::initial(100.0).resizable(true).at_least(80.0))
-            .column(Column::initial(100.0).resizable(true).at_least(80.0))
-            .column(Column::initial(100.0).resizable(true).at_least(80.0))
-            .max_scroll_height(350.0)
-            .header(22.0, |mut header| {
-                header.col(|ui| {
-                    ui.strong("Asset Tag");
-                });
-                header.col(|ui| {
-                    ui.strong("Name");
-                });
-                header.col(|ui| {
-                    ui.strong("Borrower");
-                });
-                header.col(|ui| {
-                    ui.strong("Class");
-                });
-                header.col(|ui| {
-                    ui.strong("Due Date");
-                });
-                header.col(|ui| {
-                    ui.strong("Action");
-                });
-            })
-            .body(|mut body| {
-                for loan in filtered_loans {
-                    body.row(20.0, |mut row| {
-                        let loan_id = loan.get("id").and_then(|v| v.as_i64()).unwrap_or(0);
-                        let is_selected = self
-                            .selected_loan
-                            .as_ref()
-                            .and_then(|s| s.get("id").and_then(|v| v.as_i64()))
-                            .map(|id| id == loan_id)
-                            .unwrap_or(false);
-
-                        // Check if overdue
-                        let due_date = loan.get("due_date").and_then(|v| v.as_str()).unwrap_or("");
-                        let today = Local::now().format("%Y-%m-%d").to_string();
-                        let is_overdue = !due_date.is_empty() && today.as_str() > due_date;
-
-                        row.col(|ui| {
-                            let tag = loan
-                                .get("asset_tag")
-                                .and_then(|v| v.as_str())
-                                .unwrap_or("N/A");
-                            if is_overdue {
-                                ui.colored_label(egui::Color32::RED, tag);
-                            } else {
-                                ui.label(tag);
-                            }
-                        });
-                        row.col(|ui| {
-                            ui.label(
-                                loan.get("asset_name")
-                                    .and_then(|v| v.as_str())
-                                    .unwrap_or("N/A"),
-                            );
-                        });
-                        row.col(|ui| {
-                            ui.label(
-                                loan.get("borrower_name")
-                                    .and_then(|v| v.as_str())
-                                    .unwrap_or("N/A"),
-                            );
-                        });
-                        row.col(|ui| {
-                            ui.label(
-                                loan.get("class_name")
-                                    .and_then(|v| v.as_str())
-                                    .unwrap_or("N/A"),
-                            );
-                        });
-                        row.col(|ui| {
-                            if is_overdue {
-                                ui.colored_label(egui::Color32::RED, format!("{} ⚠", due_date));
-                            } else {
-                                ui.label(due_date);
-                            }
-                        });
-                        row.col(|ui| {
-                            if is_selected {
-                                ui.colored_label(egui::Color32::GREEN, "Selected");
-                            } else {
-                                let button_id = format!("select_loan_{}", loan_id);
-                                if ui.button("Select").on_hover_text(&button_id).clicked() {
-                                    self.selected_loan = Some((*loan).clone());
-                                }
-                            }
-                        });
-                    });
-                }
-            });
+        // Map selection
+        let sel = self.loans_table.selection.get_selected_indices();
+        if sel.is_empty() {
+            self.selected_loan = None;
+        } else {
+            let idx = sel[0].min(prepared.len().saturating_sub(1));
+            self.selected_loan = Some(prepared[idx].1.clone());
+        }
     }
 
     // Submission method
@@ -815,6 +778,10 @@ impl ReturnFlow {
             "current_borrower_id": serde_json::Value::Null,
             "due_date": serde_json::Value::Null
         });
+        // If user chose to update physical status during issue reporting, include it
+        if let Some(new_status) = &self.new_asset_status {
+            asset_update_payload["status"] = serde_json::Value::String(new_status.clone());
+        }
         if let Some(cb) = current_borrower_id {
             asset_update_payload["previous_borrower_id"] = serde_json::Value::from(cb);
         }

+ 186 - 30
src/kioskui/app.rs

@@ -8,6 +8,7 @@ use crate::models::{UserInfo, LoginResponse};
 use crate::session::SessionManager;
 use crate::ui::app::BeepZoneApp;
 use super::login::{KioskLoginView, LoginResult};
+use super::dashboard::KioskDashboard;
 
 pub struct KioskApp {
     // Session management
@@ -19,6 +20,7 @@ pub struct KioskApp {
     
     // UI components
     login_view: KioskLoginView,
+    dashboard: KioskDashboard,
     full_ui_app: Option<BeepZoneApp>,
     
     // State
@@ -27,8 +29,13 @@ pub struct KioskApp {
     current_user: Option<UserInfo>, // The Kiosk User
     session_user: Option<UserInfo>, // The User currently logged in via Kiosk
     session_token: Option<String>,  // The Token of the User currently logged in via Kiosk
+    session_api_client: Option<ApiClient>, // API client for the session user (not kiosk user)
     error_message: Option<String>,
     show_full_ui: bool,
+    show_osk: bool,
+    osk_shift_mode: bool,
+    last_focused_id: Option<egui::Id>,
+    osk_event_queue: Vec<egui::Event>,
     last_interaction: std::time::Instant,
     startup_time: std::time::Instant,
     delayed_fullscreen_done: bool,
@@ -50,14 +57,20 @@ impl KioskApp {
             api_client: None,
             config,
             login_view,
+            dashboard: KioskDashboard::new(),
             full_ui_app: Some(full_ui_app),
             is_initialized: false,
             window_setup_done: false,
             current_user: None,
             session_user: None,
             session_token: None,
+            session_api_client: None,
             error_message: None,
             show_full_ui: false,
+            show_osk: false,
+            osk_shift_mode: false,
+            last_focused_id: None,
+            osk_event_queue: Vec::new(),
             last_interaction: std::time::Instant::now(),
             startup_time: std::time::Instant::now(),
             delayed_fullscreen_done: false,
@@ -112,6 +125,17 @@ impl KioskApp {
 
 impl eframe::App for KioskApp {
     fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
+        // Inject queued OSK events at the start of the frame
+        if !self.osk_event_queue.is_empty() {
+            let events = std::mem::take(&mut self.osk_event_queue);
+            ctx.input_mut(|i| i.events.extend(events));
+        }
+
+        // Track focus for OSK
+        if let Some(id) = ctx.memory(|m| m.focused()) {
+            self.last_focused_id = Some(id);
+        }
+
         // Ensure window state on first frame
         if !self.window_setup_done {
             if self.config.ui.fullscreen {
@@ -144,6 +168,7 @@ impl eframe::App for KioskApp {
                 // Timeout!
                 self.session_user = None;
                 self.session_token = None;
+                self.session_api_client = None;
                 self.show_full_ui = false;
                 if let Some(app) = &mut self.full_ui_app {
                     app.handle_logout(); // Ensure app state is cleared
@@ -158,12 +183,19 @@ impl eframe::App for KioskApp {
             if let Some(app) = &mut self.full_ui_app {
                 app.update(ctx, frame);
                 
+                // Check if we should return to kiosk menu (keep session)
+                if app.should_return_to_kiosk_menu {
+                    self.show_full_ui = false;
+                    app.should_return_to_kiosk_menu = false;
+                }
+
                 // Check if we should exit back to kiosk
                 if app.should_exit_to_kiosk {
                     self.show_full_ui = false;
                     app.should_exit_to_kiosk = false;
                     self.session_user = None; // Also sign out of kiosk session
                     self.session_token = None;
+                    self.session_api_client = None;
                     self.login_view.reset();
                 }
                 return;
@@ -205,44 +237,60 @@ impl eframe::App for KioskApp {
                     let session_user = self.session_user.clone();
                     if let Some(user) = session_user {
                         // Logged In View
-                        ui.centered_and_justified(|ui| {
-                            ui.vertical_centered(|ui| {
-                                ui.heading(egui::RichText::new("Yay it works").size(48.0).color(egui::Color32::GREEN));
-                                ui.add_space(20.0);
-                                ui.label(egui::RichText::new(format!("Welcome, {}", user.name)).size(32.0));
-                                ui.add_space(40.0);
-                                
-                                if ui.add_sized(egui::vec2(200.0, 80.0), egui::Button::new(egui::RichText::new("Sign Out").size(24.0))).clicked() {
-                                    self.session_user = None;
-                                    self.session_token = None;
-                                    self.login_view.reset();
+                        // Create session API client if not already created
+                        if self.session_api_client.is_none() {
+                            if let Some(token) = &self.session_token {
+                                if let Ok(mut session_client) = ApiClient::new(self.config.server_url.clone()) {
+                                    session_client.set_token(token.clone());
+                                    self.session_api_client = Some(session_client);
                                 }
+                            }
+                        }
 
-                                if user.power >= self.config.ui.minimum_power_level_for_full_ui {
-                                    ui.add_space(20.0);
-                                    if ui.add_sized(egui::vec2(200.0, 60.0), egui::Button::new(egui::RichText::new("Show Full UI").size(20.0))).clicked() {
-                                        if let Some(app) = &mut self.full_ui_app {
-                                            // Construct a LoginResponse to simulate a successful login
-                                            let login_response = LoginResponse {
-                                                success: true,
-                                                token: self.session_token.clone(),
-                                                user: Some(user.clone()),
-                                                error: None,
-                                            };
-                                            
-                                            app.handle_login_success(self.config.server_url.clone(), login_response);
-                                            self.show_full_ui = true;
-                                        }
-                                    }
-                                }
-                            });
-                        });
+                        let mut logout_requested = false;
+                        let mut show_full_ui_requested = false;
+                        
+                        // Use session_api_client for operations, fallback to kiosk client
+                        let active_client = self.session_api_client.as_ref().unwrap_or(client);
+                        
+                        self.dashboard.show(
+                            ui,
+                            active_client,
+                            &user,
+                            &self.config.ui,
+                            &mut self.show_osk,
+                            &mut logout_requested,
+                            &mut show_full_ui_requested
+                        );
+
+                        if logout_requested {
+                            self.session_user = None;
+                            self.session_token = None;
+                            self.session_api_client = None;
+                            self.login_view.reset();
+                        }
+
+                        if show_full_ui_requested {
+                            if let Some(app) = &mut self.full_ui_app {
+                                // Construct a LoginResponse to simulate a successful login
+                                let login_response = LoginResponse {
+                                    success: true,
+                                    token: self.session_token.clone(),
+                                    user: Some(user.clone()),
+                                    error: None,
+                                };
+                                
+                                app.handle_login_success(self.config.server_url.clone(), login_response);
+                                self.show_full_ui = true;
+                            }
+                        }
                     } else {
                         // Login View
                         match self.login_view.show(ui, client) {
                             LoginResult::Success(user, token) => {
                                 self.session_user = Some(user);
                                 self.session_token = Some(token);
+                                // Session API client will be created on next frame
                             }
                             LoginResult::None => {}
                         }
@@ -250,5 +298,113 @@ impl eframe::App for KioskApp {
                 }
             }
         });
+
+        // Show OSK overlay if enabled and not in full UI mode
+        if !self.show_full_ui {
+            self.show_osk_overlay(ctx);
+        }
+    }
+}
+
+impl KioskApp {
+    fn show_osk_overlay(&mut self, ctx: &egui::Context) {
+        if !self.show_osk { return; }
+
+        let height = 340.0;
+        egui::TopBottomPanel::bottom("kiosk_osk_panel")
+            .resizable(false)
+            .min_height(height)
+            .show(ctx, |ui| {
+                ui.vertical_centered(|ui| {
+                    ui.add_space(10.0);
+                    
+                    // Styling
+                    let btn_size = egui::vec2(50.0, 50.0);
+                    let spacing = 6.0;
+                    ui.style_mut().spacing.item_spacing = egui::vec2(spacing, spacing);
+
+                    // Layouts
+                    let rows_lower = [
+                        vec!["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "="],
+                        vec!["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]"],
+                        vec!["a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "'", "\\"],
+                        vec!["z", "x", "c", "v", "b", "n", "m", ",", ".", "/"],
+                    ];
+
+                    let rows_upper = [
+                        vec!["!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "+"],
+                        vec!["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "{", "}"],
+                        vec!["A", "S", "D", "F", "G", "H", "J", "K", "L", ":", "\"", "|"],
+                        vec!["Z", "X", "C", "V", "B", "N", "M", "<", ">", "?"],
+                    ];
+
+                    let rows = if self.osk_shift_mode { rows_upper } else { rows_lower };
+
+                    for row in rows {
+                        ui.horizontal(|ui| {
+                            // Center row
+                            let width = row.len() as f32 * (btn_size.x + spacing) - spacing;
+                            let margin = (ui.available_width() - 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(24.0))).clicked() {
+                                    // Queue text event for next frame
+                                    self.osk_event_queue.push(egui::Event::Text(key.to_string()));
+                                    
+                                    // Restore focus immediately
+                                    if let Some(id) = self.last_focused_id {
+                                        ctx.memory_mut(|m| m.request_focus(id));
+                                    }
+                                }
+                            }
+                        });
+                    }
+                    
+                    // Modifiers and Actions
+                    ui.horizontal(|ui| {
+                        let shift_width = 100.0;
+                        let space_width = 300.0;
+                        let back_width = 100.0;
+                        let total_width = shift_width + space_width + back_width + (spacing * 2.0);
+                        let margin = (ui.available_width() - total_width) / 2.0;
+                        if margin > 0.0 { ui.add_space(margin); }
+                        
+                        // Shift
+                        let shift_text = if self.osk_shift_mode { "SHIFT (ON)" } else { "SHIFT" };
+                        let shift_btn = egui::Button::new(egui::RichText::new(shift_text).size(20.0))
+                            .fill(if self.osk_shift_mode { egui::Color32::from_rgb(100, 100, 255) } else { ui.visuals().widgets.inactive.bg_fill });
+                        
+                        if ui.add_sized(egui::vec2(shift_width, 50.0), shift_btn).clicked() {
+                            self.osk_shift_mode = !self.osk_shift_mode;
+                            if let Some(id) = self.last_focused_id {
+                                ctx.memory_mut(|m| m.request_focus(id));
+                            }
+                        }
+
+                        // Space
+                        if ui.add_sized(egui::vec2(space_width, 50.0), egui::Button::new(egui::RichText::new("SPACE").size(20.0))).clicked() {
+                             self.osk_event_queue.push(egui::Event::Text(" ".to_string()));
+                             if let Some(id) = self.last_focused_id {
+                                ctx.memory_mut(|m| m.request_focus(id));
+                            }
+                        }
+
+                        // Backspace
+                        if ui.add_sized(egui::vec2(back_width, 50.0), egui::Button::new(egui::RichText::new("DEL").size(24.0))).clicked() {
+                            self.osk_event_queue.push(egui::Event::Key {
+                                key: egui::Key::Backspace,
+                                physical_key: None,
+                                pressed: true,
+                                repeat: false,
+                                modifiers: egui::Modifiers::default(),
+                            });
+                            if let Some(id) = self.last_focused_id {
+                                ctx.memory_mut(|m| m.request_focus(id));
+                            }
+                        }
+                    });
+                });
+            });
     }
 }

+ 211 - 0
src/kioskui/dashboard.rs

@@ -0,0 +1,211 @@
+use eframe::egui;
+use crate::api::ApiClient;
+use crate::core::workflows::borrow_flow::BorrowFlow;
+use crate::core::workflows::return_flow::ReturnFlow;
+use crate::models::UserInfo;
+use crate::config::KioskUiSettings;
+
+pub struct KioskDashboard {
+    pub borrow_flow: BorrowFlow,
+    pub return_flow: ReturnFlow,
+}
+
+impl KioskDashboard {
+    pub fn new() -> Self {
+        Self {
+            borrow_flow: BorrowFlow::default(),
+            return_flow: ReturnFlow::default(),
+        }
+    }
+
+    pub fn show(
+        &mut self,
+        ui: &mut egui::Ui,
+        api_client: &ApiClient,
+        user: &UserInfo,
+        config: &KioskUiSettings,
+        show_osk: &mut bool,
+        logout_requested: &mut bool,
+        show_full_ui_requested: &mut bool,
+    ) {
+        let avail_rect = ui.available_rect_before_wrap();
+        // Calculate row heights
+        // Structure: Row 1 (0.5h), Row 2 (1h), Row 3 (1h), Row 4 (1h)
+        // Total units = 3.5
+        let total_units = 3.5;
+        let unit_height = avail_rect.height() / total_units;
+        let header_height = unit_height * 0.5;
+        let row_height = unit_height;
+
+        // Get text scaling factor
+        let text_scale = ui.ctx().pixels_per_point();
+        let btn_font_size = 14.0 * text_scale;
+        let label_font_size = 16.0 * text_scale;
+
+        // --- Row 1: Header (Logout, OSK) ---
+        ui.allocate_ui_at_rect(
+            egui::Rect::from_min_size(avail_rect.min, egui::vec2(avail_rect.width(), header_height)),
+            |ui| {
+                ui.horizontal(|ui| {
+                    ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
+                        if ui.add_sized(egui::vec2(120.0, header_height - 20.0), egui::Button::new(egui::RichText::new("Logout").size(btn_font_size))).clicked() {
+                            *logout_requested = true;
+                        }
+                        
+                        ui.label(egui::RichText::new(format!("Logged in as: {}", user.username)).size(label_font_size));
+                    });
+                    
+                    ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
+                        if config.enable_osk {
+                            let btn_text = if *show_osk { "Hide Keyboard" } else { "Show Keyboard" };
+                            if ui.add_sized(egui::vec2(150.0, header_height - 20.0), egui::Button::new(egui::RichText::new(btn_text).size(btn_font_size))).clicked() {
+                                *show_osk = !*show_osk;
+                            }
+                        }
+                    });
+                });
+            }
+        );
+
+        // Content Area (Rows 2, 3, 4)
+        // If a workflow is open, it takes over the rest of the screen.
+        // Otherwise, show the Dashboard Buttons.
+        
+        let content_rect = egui::Rect::from_min_size(
+            avail_rect.min + egui::vec2(0.0, header_height), 
+            egui::vec2(avail_rect.width(), avail_rect.height() - header_height)
+        );
+
+        ui.allocate_ui_at_rect(content_rect, |ui| {
+            if self.borrow_flow.is_open {
+                // Apply component scale to workflows so they match kiosk UI
+                let scale = if config.scale <= 0.0 { 1.0 } else { config.scale };
+                ui.scope(|ui| {
+                    let mut style: egui::Style = ui.style().as_ref().clone();
+                    // Scale common text styles
+                    for (_ts, font) in style.text_styles.iter_mut() {
+                        font.size *= scale;
+                    }
+                    // Scale paddings and spacings
+                    style.spacing.item_spacing *= scale;
+                    style.spacing.button_padding *= scale;
+                    style.spacing.indent *= scale;
+                    style.spacing.window_margin = style.spacing.window_margin * scale;
+                    ui.set_style(style);
+
+                    self.borrow_flow.show_content(ui, api_client);
+                });
+            } else if self.return_flow.is_open {
+                // Apply component scale to workflows so they match kiosk UI
+                let scale = if config.scale <= 0.0 { 1.0 } else { config.scale };
+                ui.scope(|ui| {
+                    let mut style: egui::Style = ui.style().as_ref().clone();
+                    for (_ts, font) in style.text_styles.iter_mut() {
+                        font.size *= scale;
+                    }
+                    style.spacing.item_spacing *= scale;
+                    style.spacing.button_padding *= scale;
+                    style.spacing.indent *= scale;
+                    style.spacing.window_margin = style.spacing.window_margin * scale;
+                    ui.set_style(style);
+
+                    self.return_flow.show_content(ui, api_client);
+                });
+            } else {
+                self.show_dashboard_grid(ui, row_height, show_full_ui_requested, user, config, api_client);
+            }
+        });
+    }
+
+    fn show_dashboard_grid(
+        &mut self,
+        ui: &mut egui::Ui,
+        row_height: f32,
+        show_full_ui_requested: &mut bool,
+        user: &UserInfo,
+        config: &KioskUiSettings,
+        api_client: &ApiClient,
+    ) {
+        let width = ui.available_width();
+
+        // Get text scaling factor
+        let text_scale = ui.ctx().pixels_per_point();
+        let base_font_size = 14.0 * text_scale;
+        let heading_size = 24.0 * text_scale;
+
+        // --- Row 2: Borrowing ---
+        ui.allocate_ui(egui::vec2(width, row_height), |ui| {
+            ui.vertical(|ui| {
+                ui.with_layout(egui::Layout::top_down(egui::Align::Center), |ui| {
+                    ui.heading(egui::RichText::new("Borrowing").size(heading_size));
+                    ui.add_space(10.0);
+                });
+                
+                ui.horizontal(|ui| {
+                    let num_buttons = 3.0;
+                    let spacing = ui.spacing().item_spacing.x;
+                    let total_spacing = spacing * (num_buttons - 1.0);
+                    let btn_width = (width - total_spacing - 20.0) / num_buttons;
+                    let btn_size = egui::vec2(btn_width, row_height - 80.0);
+                    
+                    // Center buttons horizontally
+                    let total_width = btn_width * num_buttons + total_spacing;
+                    let margin = (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("Check Out").size(base_font_size))).clicked() {
+                        self.borrow_flow.open(api_client);
+                    }
+                    
+                    if ui.add_sized(btn_size, egui::Button::new(egui::RichText::new("Check In").size(base_font_size))).clicked() {
+                        self.return_flow.open(api_client);
+                    }
+                    ui.add_sized(btn_size, egui::Button::new(egui::RichText::new("Manage").size(base_font_size)));
+                });
+            });
+        });
+
+        ui.separator();
+
+        // --- Row 3: Inventory ---
+        ui.allocate_ui(egui::vec2(width, row_height), |ui| {
+            ui.vertical(|ui| {
+                ui.with_layout(egui::Layout::top_down(egui::Align::Center), |ui| {
+                    ui.heading(egui::RichText::new("Inventory").size(heading_size));
+                    ui.add_space(10.0);
+                });
+                
+                ui.horizontal(|ui| {
+                    let num_buttons = if user.power >= config.minimum_power_level_for_full_ui { 5.0 } else { 4.0 };
+                    let spacing = ui.spacing().item_spacing.x;
+                    let total_spacing = spacing * (num_buttons - 1.0);
+                    let btn_width = (width - total_spacing - 20.0) / num_buttons;
+                    let btn_size = egui::vec2(btn_width, row_height - 80.0);
+                    
+                    // Center buttons horizontally
+                    let total_width = btn_width * num_buttons + total_spacing;
+                    let margin = (width - total_width) / 2.0;
+                    if margin > 0.0 { ui.add_space(margin); }
+                    
+                    ui.add_sized(btn_size, egui::Button::new(egui::RichText::new("Add").size(base_font_size)));
+                    ui.add_sized(btn_size, egui::Button::new(egui::RichText::new("Move").size(base_font_size)));
+                    ui.add_sized(btn_size, egui::Button::new(egui::RichText::new("See").size(base_font_size)));
+                    ui.add_sized(btn_size, egui::Button::new(egui::RichText::new("Print Label").size(base_font_size)));
+
+                    if user.power >= config.minimum_power_level_for_full_ui {
+                        if ui.add_sized(btn_size, egui::Button::new(egui::RichText::new("Show Full UI").size(base_font_size))).clicked() {
+                            *show_full_ui_requested = true;
+                        }
+                    }
+                });
+            });
+        });
+
+        ui.separator();
+
+        // --- Row 4: Blank ---
+        ui.allocate_ui(egui::vec2(width, row_height), |_ui| {
+           // Blank
+        });
+    }
+}

+ 3 - 3
src/kioskui/login.rs

@@ -230,7 +230,7 @@ impl KioskLoginView {
                 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)
+                        egui::RichText::new("Badge Log 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() {
@@ -244,10 +244,10 @@ impl KioskLoginView {
     }
 
     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.label(egui::RichText::new("Scan your Badge").size(self.ui_settings.font_size));
         ui.add_space(20.0);
         
-        // Invisible input that keeps focus to capture RFID scanner input
+        // 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

+ 1 - 0
src/kioskui/mod.rs

@@ -1,2 +1,3 @@
 pub mod app;
+pub mod dashboard;
 pub mod login;

+ 9 - 1
src/ui/app.rs

@@ -50,7 +50,7 @@ pub struct BeepZoneApp {
     label_templates: LabelTemplatesView,
     ribbon_ui: Option<RibbonUI>,
 
-    // Configuration
+    // Deprecated Configuration
     #[allow(dead_code)]
     app_config: Option<AppConfig>,
 
@@ -58,6 +58,7 @@ pub struct BeepZoneApp {
     login_success: Option<(String, LoginResponse)>,
     show_about: bool,
     pub should_exit_to_kiosk: bool,
+    pub should_return_to_kiosk_menu: bool,
     
     // Kiosk integration
     pub is_kiosk_mode: bool,
@@ -201,6 +202,7 @@ impl BeepZoneApp {
             login_success: None,
             show_about: false,
             should_exit_to_kiosk: false,
+            should_return_to_kiosk_menu: false,
             is_kiosk_mode: false,
             enable_full_osk_button: false,
             show_osk: false,
@@ -409,6 +411,12 @@ impl BeepZoneApp {
                                 self.show_about = true;
                             }
 
+                            if self.is_kiosk_mode {
+                                if ui.button("Back").clicked() {
+                                    self.should_return_to_kiosk_menu = true;
+                                }
+                            }
+
                             if self.is_kiosk_mode && self.enable_full_osk_button {
                                 let btn_text = if self.show_osk { "Hide Keyboard" } else { "Keyboard" };
                                 if ui.button(btn_text).clicked() {