UMTS at Teleco hai 1 mes
pai
achega
6163482b66
Modificáronse 24 ficheiros con 2119 adicións e 759 borrados
  1. 1 0
      Cargo.toml
  2. 1 8
      docs/todo.md
  3. 26 0
      kiosk.toml
  4. 14 12
      src/api.rs
  5. 56 0
      src/config.rs
  6. 232 0
      src/kioskui/app.rs
  7. 421 0
      src/kioskui/login.rs
  8. 2 0
      src/kioskui/mod.rs
  9. 62 6
      src/main.rs
  10. 9 4
      src/models.rs
  11. 2 0
      src/session.rs
  12. 269 30
      src/ui/app.rs
  13. 9 3
      src/ui/audits.rs
  14. 11 4
      src/ui/borrowing.rs
  15. 74 61
      src/ui/categories.rs
  16. 176 144
      src/ui/inventory.rs
  17. 59 39
      src/ui/issues.rs
  18. 66 48
      src/ui/label_templates.rs
  19. 11 5
      src/ui/login.rs
  20. 68 50
      src/ui/printers.rs
  21. 367 191
      src/ui/ribbon.rs
  22. 17 10
      src/ui/suppliers.rs
  23. 51 44
      src/ui/templates.rs
  24. 115 100
      src/ui/zones.rs

+ 1 - 0
Cargo.toml

@@ -22,6 +22,7 @@ tokio = { version = "1.40", features = ["full"] }
 # jayson derulo
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
+toml = "0.8"
 
 # basics
 chrono = { version = "0.4", features = ["serde"] }

+ 1 - 8
docs/todo.md

@@ -1,18 +1,11 @@
 # Important :
 
-## Part 2 (Probably needs some changes to the database) : 
-- Add an Asset Tag Generation Field to each item for automatic updates during audits because im stupid
-- Item Relationship System (items that belong to another item, item must be in same zone though for that)
-- Item Replacement system
-    - Give the ability to replace an item with another one in some way ?
-        - Say I have a keyboard in storage and want to replace on from a computer like that should be possible
-
 
 
 
 ## Part 3 :
 - make it actuall support backends RBAC lol
-
+-- kinda does now i guess ?
 
 ## Part 4 :
 - Properly implementing issue view (currently a "design" concept was slapped there by an LLM)

+ 26 - 0
kiosk.toml

@@ -0,0 +1,26 @@
+[kiosk]
+server_url = "http://localhost:5777"
+username = "kiosk"
+password = "kiosk"
+
+[kiosk.filter]
+hide_self = true
+role_whitelist_enabled = false
+role_whitelist = []
+role_blacklist_enabled = true
+role_blacklist = ["Kiosk", "User"]
+username_whitelist_enabled = false
+username_whitelist = []
+username_blacklist_enabled = false
+username_blacklist = []
+
+[kiosk.ui]
+title = "BeepZone  Kiosk"
+enable_osk = true
+enable_rfid = true
+fullscreen = true
+button_height = 80.0
+font_size = 24.0
+minimum_power_level_for_full_ui = 50
+timeout_seconds = 240
+enable_full_osk_button = true

+ 14 - 12
src/api.rs

@@ -31,6 +31,10 @@ impl ApiClient {
         })
     }
 
+    pub fn get_token(&self) -> Option<String> {
+        self.token.clone()
+    }
+
     fn flag_timeout_signal(&self) {
         self.db_timeout_flag.store(true, Ordering::SeqCst);
     }
@@ -128,8 +132,7 @@ impl ApiClient {
     }
 
     /// Login with PIN
-    #[allow(dead_code)]
-    pub fn login_pin(&self, username: &str, pin: &str) -> Result<ApiResponse<LoginResponse>> {
+    pub fn login_pin(&self, username: &str, pin: &str) -> Result<LoginResponse> {
         let url = format!("{}/auth/login", self.base_url);
         let body = LoginRequest {
             method: "pin".to_string(),
@@ -144,7 +147,7 @@ impl ApiClient {
             "Failed to send PIN login request",
         )?;
 
-        let result: ApiResponse<LoginResponse> =
+        let result: LoginResponse =
             response.json().context("Failed to parse login response")?;
         self.observe_response_error(&result.error);
 
@@ -152,8 +155,7 @@ impl ApiClient {
     }
 
     /// Login with token/RFID string
-    #[allow(dead_code)]
-    pub fn login_token(&self, login_string: &str) -> Result<ApiResponse<LoginResponse>> {
+    pub fn login_token(&self, login_string: &str) -> Result<LoginResponse> {
         let url = format!("{}/auth/login", self.base_url);
         let body = LoginRequest {
             method: "token".to_string(),
@@ -168,7 +170,7 @@ impl ApiClient {
             "Failed to send token login request",
         )?;
 
-        let result: ApiResponse<LoginResponse> =
+        let result: LoginResponse =
             response.json().context("Failed to parse login response")?;
         self.observe_response_error(&result.error);
 
@@ -257,14 +259,14 @@ impl ApiClient {
 
     /// Get current user's permissions
     #[allow(dead_code)]
-    pub fn get_permissions(&self) -> Result<ApiResponse<PermissionsResponse>> {
+    pub fn get_permissions(&self) -> Result<PermissionsResponse> {
         let url = format!("{}/permissions", self.base_url);
         let response = self.send_request(
             self.make_authorized_request(reqwest::Method::GET, &url)?,
             "Failed to get permissions",
         )?;
 
-            let result: ApiResponse<PermissionsResponse> = response.json()?;
+        let result: PermissionsResponse = response.json()?;
         self.observe_response_error(&result.error);
         Ok(result)
     }
@@ -274,7 +276,7 @@ impl ApiClient {
     pub fn get_preferences(
         &self,
         user_id: Option<i32>,
-    ) -> Result<ApiResponse<PreferencesResponse>> {
+    ) -> Result<PreferencesResponse> {
         let url = format!("{}/preferences", self.base_url);
         let body = PreferencesRequest {
             action: "get".to_string(),
@@ -288,7 +290,7 @@ impl ApiClient {
             "Failed to get preferences",
         )?;
 
-            let result: ApiResponse<PreferencesResponse> = response.json()?;
+        let result: PreferencesResponse = response.json()?;
         self.observe_response_error(&result.error);
         Ok(result)
     }
@@ -299,7 +301,7 @@ impl ApiClient {
         &self,
         values: serde_json::Value,
         user_id: Option<i32>,
-    ) -> Result<ApiResponse<PreferencesResponse>> {
+    ) -> Result<PreferencesResponse> {
         let url = format!("{}/preferences", self.base_url);
         let body = PreferencesRequest {
             action: "set".to_string(),
@@ -313,7 +315,7 @@ impl ApiClient {
             "Failed to set preferences",
         )?;
 
-            let result: ApiResponse<PreferencesResponse> = response.json()?;
+            let result: PreferencesResponse = response.json()?;
             self.observe_response_error(&result.error);
             Ok(result)
     }

+ 56 - 0
src/config.rs

@@ -41,3 +41,59 @@ pub struct WebPreferences {
 pub struct MobilePreferences {
     pub scan_mode: String,
 }
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct KioskConfig {
+    pub kiosk: KioskSettings,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct KioskSettings {
+    pub server_url: String,
+    pub username: String,
+    pub password: String,
+    #[serde(default)]
+    pub filter: KioskFilterSettings,
+    #[serde(default)]
+    pub ui: KioskUiSettings,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct KioskFilterSettings {
+    pub hide_self: bool,
+    pub role_whitelist_enabled: bool,
+    pub role_whitelist: Vec<String>,
+    pub role_blacklist_enabled: bool,
+    pub role_blacklist: Vec<String>,
+    pub username_whitelist_enabled: bool,
+    pub username_whitelist: Vec<String>,
+    pub username_blacklist_enabled: bool,
+    pub username_blacklist: Vec<String>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct KioskUiSettings {
+    #[serde(default = "default_kiosk_title")]
+    pub title: String,
+    pub enable_osk: bool,
+    #[serde(default)]
+    pub enable_rfid: bool,
+    #[serde(default)]
+    pub fullscreen: bool,
+    pub button_height: f32,
+    pub font_size: f32,
+    #[serde(default = "default_min_power")]
+    pub minimum_power_level_for_full_ui: i32,
+    #[serde(default)]
+    pub timeout_seconds: Option<u64>,
+    #[serde(default)]
+    pub enable_full_osk_button: bool,
+}
+
+fn default_kiosk_title() -> String {
+    "BeepZone Kiosk".to_string()
+}
+
+fn default_min_power() -> i32 {
+    50
+}

+ 232 - 0
src/kioskui/app.rs

@@ -0,0 +1,232 @@
+use eframe::egui;
+use std::sync::Arc;
+use tokio::sync::Mutex;
+
+use crate::api::ApiClient;
+use crate::config::KioskSettings;
+use crate::models::{UserInfo, LoginResponse};
+use crate::session::SessionManager;
+use crate::ui::app::BeepZoneApp;
+use super::login::{KioskLoginView, LoginResult};
+
+pub struct KioskApp {
+    // Session management
+    session_manager: Arc<Mutex<SessionManager>>,
+    api_client: Option<ApiClient>,
+    
+    // Kiosk state
+    config: KioskSettings,
+    
+    // UI components
+    login_view: KioskLoginView,
+    full_ui_app: Option<BeepZoneApp>,
+    
+    // State
+    is_initialized: bool,
+    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
+    error_message: Option<String>,
+    show_full_ui: bool,
+    last_interaction: std::time::Instant,
+}
+
+impl KioskApp {
+    pub fn new(
+        cc: &eframe::CreationContext<'_>,
+        session_manager: Arc<Mutex<SessionManager>>,
+        config: KioskSettings,
+    ) -> Self {
+        let login_view = KioskLoginView::new(config.filter.clone(), config.ui.clone());
+        let mut full_ui_app = BeepZoneApp::new(cc, session_manager.clone());
+        full_ui_app.is_kiosk_mode = true;
+        full_ui_app.enable_full_osk_button = config.ui.enable_full_osk_button;
+        
+        Self {
+            session_manager,
+            api_client: None,
+            config,
+            login_view,
+            full_ui_app: Some(full_ui_app),
+            is_initialized: false,
+            current_user: None,
+            session_user: None,
+            session_token: None,
+            error_message: None,
+            show_full_ui: false,
+            last_interaction: std::time::Instant::now(),
+        }
+    }
+
+    fn initialize_session(&mut self) {
+        if self.is_initialized {
+            return;
+        }
+
+        log::info!("Initializing Kiosk session for user: {}", self.config.username);
+        
+        // Create API client
+        let mut client = match ApiClient::new(self.config.server_url.clone()) {
+            Ok(c) => c,
+            Err(e) => {
+                self.error_message = Some(format!("Failed to connect to server: {}", e));
+                return;
+            }
+        };
+
+        // Attempt login
+        match client.login_password(&self.config.username, &self.config.password) {
+            Ok(response) => {
+                if response.success {
+                    if let (Some(token), Some(user)) = (response.token, response.user) {
+                        log::info!("Kiosk login successful");
+                        client.set_token(token);
+                        self.api_client = Some(client);
+                        self.current_user = Some(user);
+                        self.is_initialized = true;
+                        self.error_message = None;
+                        
+                        // Initialize login view with client
+                        if let Some(client) = &self.api_client {
+                            self.login_view.refresh_users(client, self.current_user.as_ref());
+                        }
+                    } else {
+                        self.error_message = Some("Login successful but missing token or user data".to_string());
+                    }
+                } else {
+                    self.error_message = Some("Login failed: Invalid credentials".to_string());
+                }
+            }
+            Err(e) => {
+                self.error_message = Some(format!("Login error: {}", e));
+            }
+        }
+    }
+}
+
+impl eframe::App for KioskApp {
+    fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
+        // Check for interaction (clicks or key presses, ignore mouse moves to prevent drift issues)
+        let has_interaction = ctx.input(|i| {
+            i.pointer.any_pressed() || 
+            i.events.iter().any(|e| matches!(e, egui::Event::Key{..} | egui::Event::Text(_)))
+        });
+
+        if has_interaction {
+            self.last_interaction = std::time::Instant::now();
+        }
+
+        // Check timeout
+        if let Some(timeout) = self.config.ui.timeout_seconds {
+            if self.session_user.is_some() && self.last_interaction.elapsed().as_secs() > timeout {
+                // Timeout!
+                self.session_user = None;
+                self.session_token = None;
+                self.show_full_ui = false;
+                if let Some(app) = &mut self.full_ui_app {
+                    app.handle_logout(); // Ensure app state is cleared
+                    app.should_exit_to_kiosk = false; // We handled it
+                }
+                self.login_view.reset();
+            }
+        }
+
+        // Handle Full UI Mode
+        if self.show_full_ui {
+            if let Some(app) = &mut self.full_ui_app {
+                app.update(ctx, frame);
+                
+                // 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.login_view.reset();
+                }
+                return;
+            }
+        }
+
+        // Initialize on first frame
+        if !self.is_initialized && self.error_message.is_none() {
+            self.initialize_session();
+        }
+
+        // Full screen container
+        egui::CentralPanel::default().show(ctx, |ui| {
+            // Clone error message to avoid borrow checker issues
+            let error_msg = self.error_message.clone();
+            
+            if let Some(error) = error_msg {
+                // Error state
+                ui.centered_and_justified(|ui| {
+                    ui.vertical_centered(|ui| {
+                        ui.heading(egui::RichText::new("Kiosk Initialization Failed").color(egui::Color32::RED));
+                        ui.add_space(10.0);
+                        ui.label(error);
+                        ui.add_space(20.0);
+                        if ui.button("Retry").clicked() {
+                            self.error_message = None;
+                            self.is_initialized = false;
+                        }
+                    });
+                });
+            } else if !self.is_initialized {
+                // Loading state
+                ui.centered_and_justified(|ui| {
+                    ui.spinner();
+                });
+            } else {
+                // Main Kiosk UI
+                if let Some(client) = &self.api_client {
+                    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();
+                                }
+
+                                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;
+                                        }
+                                    }
+                                }
+                            });
+                        });
+                    } else {
+                        // Login View
+                        match self.login_view.show(ui, client) {
+                            LoginResult::Success(user, token) => {
+                                self.session_user = Some(user);
+                                self.session_token = Some(token);
+                            }
+                            LoginResult::None => {}
+                        }
+                    }
+                }
+            }
+        });
+    }
+}

+ 421 - 0
src/kioskui/login.rs

@@ -0,0 +1,421 @@
+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<KioskUser>,
+    roles: HashMap<i32, String>,
+    selected_user: Option<KioskUser>,
+    pin_input: String,
+    rfid_input: String,
+    error_message: Option<String>,
+    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::<Vec<KioskRole>>(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::<Vec<KioskUser>>(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
+    }
+}

+ 2 - 0
src/kioskui/mod.rs

@@ -0,0 +1,2 @@
+pub mod app;
+pub mod login;

+ 62 - 6
src/main.rs

@@ -1,3 +1,5 @@
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
+
 use eframe::egui;
 use std::sync::Arc;
 use tokio::sync::Mutex;
@@ -5,12 +7,14 @@ use tokio::sync::Mutex;
 mod api;
 mod config;
 mod core;
+mod kioskui;
 mod models;
 mod session;
 mod ui;
 
 use session::SessionManager;
 use ui::app::BeepZoneApp;
+use kioskui::app::KioskApp;
 
 fn main() -> eframe::Result<()> {
     // Initialize logging
@@ -20,12 +24,51 @@ fn main() -> eframe::Result<()> {
 
     log::info!("Starting BeepZone Inventory Management System");
 
+    // Parse command line arguments
+    let args: Vec<String> = std::env::args().collect();
+    let mut kiosk_mode = false;
+    let mut kiosk_config = None;
+
+    if args.len() >= 3 && args[1] == "--kiosk" {
+        kiosk_mode = true;
+        let config_path = &args[2];
+        
+        match std::fs::read_to_string(config_path) {
+            Ok(content) => {
+                match toml::from_str::<config::KioskConfig>(&content) {
+                    Ok(config) => {
+                        log::info!("Starting in Kiosk mode connecting to {}", config.kiosk.server_url);
+                        kiosk_config = Some(config.kiosk);
+                    }
+                    Err(e) => {
+                        log::error!("Failed to parse kiosk config: {}", e);
+                        kiosk_mode = false;
+                    }
+                }
+            }
+            Err(e) => {
+                log::error!("Failed to read kiosk config file: {}", e);
+                kiosk_mode = false;
+            }
+        }
+    }
+
     // Configure egui options
+    let mut viewport_builder = egui::ViewportBuilder::default()
+        .with_inner_size([1280.0, 800.0])
+        .with_min_inner_size([800.0, 600.0])
+        .with_icon(load_icon());
+
+    if kiosk_mode {
+        if let Some(config) = &kiosk_config {
+            if config.ui.fullscreen {
+                viewport_builder = viewport_builder.with_fullscreen(true);
+            }
+        }
+    }
+
     let options = eframe::NativeOptions {
-        viewport: egui::ViewportBuilder::default()
-            .with_inner_size([1280.0, 800.0])
-            .with_min_inner_size([800.0, 600.0])
-            .with_icon(load_icon()),
+        viewport: viewport_builder,
         persist_window: true,
         ..Default::default()
     };
@@ -35,14 +78,27 @@ fn main() -> eframe::Result<()> {
 
     // Run the application
     eframe::run_native(
-        "BeepZone Inventory System",
+        if kiosk_mode { "BeepZone Kiosk" } else { "BeepZone Inventory System" },
         options,
         Box::new(move |cc| {
             // Configure fonts and style
             configure_fonts(&cc.egui_ctx);
             configure_style(&cc.egui_ctx);
 
-            Ok(Box::new(BeepZoneApp::new(cc, session_manager)))
+            if kiosk_mode {
+                if let Some(config) = kiosk_config {
+                    Ok(Box::new(KioskApp::new(
+                        cc,
+                        session_manager,
+                        config,
+                    )))
+                } else {
+                    // Should not happen due to logic above, but fallback
+                    panic!("Kiosk config missing");
+                }
+            } else {
+                Ok(Box::new(BeepZoneApp::new(cc, session_manager)))
+            }
         }),
     )
 }

+ 9 - 4
src/models.rs

@@ -41,14 +41,16 @@ pub struct LoginRequest {
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct LoginResponse {
     pub success: bool,
-    pub token: String,
-    pub user: UserInfo,
+    pub token: Option<String>,
+    pub user: Option<UserInfo>,
+    pub error: Option<String>,
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct UserInfo {
     pub id: i32,
     pub username: String,
+    pub name: String,
     pub role: String,
     pub power: i32,
 }
@@ -68,10 +70,12 @@ pub struct SessionStatus {
 #[allow(dead_code)]
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct PermissionsResponse {
+    pub success: bool,
     pub user: UserInfo,
     pub user_settings_access: String,
     pub permissions: serde_json::Value, // Flexible permissions structure
     pub security_clearance: Option<String>,
+    pub error: Option<String>,
 }
 
 // ============================================================================
@@ -91,8 +95,9 @@ pub struct PreferencesRequest {
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct PreferencesResponse {
-    pub user_id: i32,
-    pub preferences: serde_json::Value,
+    pub success: bool,
+    pub preferences: Option<serde_json::Value>,
+    pub error: Option<String>,
 }
 
 // ============================================================================

+ 2 - 0
src/session.rs

@@ -15,6 +15,8 @@ pub struct SessionData {
     pub remember_username: bool,
     pub saved_username: Option<String>,
     #[serde(default)]
+    pub permissions: Option<serde_json::Value>,
+    #[serde(default)]
     pub default_printer_id: Option<i64>,
     /// Remember last-used printer (may differ from default_printer_id if user overrides per-print)
     #[serde(default)]

+ 269 - 30
src/ui/app.rs

@@ -30,6 +30,7 @@ pub struct BeepZoneApp {
     current_view: AppView,
     previous_view: Option<AppView>,
     current_user: Option<UserInfo>,
+    current_permissions: Option<serde_json::Value>,
 
     // Per-view filter state storage
     view_filter_states: HashMap<AppView, crate::core::components::filter_builder::FilterGroup>,
@@ -56,6 +57,15 @@ pub struct BeepZoneApp {
     // State
     login_success: Option<(String, LoginResponse)>,
     show_about: bool,
+    pub should_exit_to_kiosk: bool,
+    
+    // Kiosk integration
+    pub is_kiosk_mode: bool,
+    pub enable_full_osk_button: bool,
+    pub show_osk: bool,
+    pub osk_shift_mode: bool,
+    last_focused_id: Option<egui::Id>,
+    osk_event_queue: Vec<egui::Event>,
 
     // Status bar state
     server_status: ServerStatus,
@@ -111,7 +121,7 @@ impl BeepZoneApp {
         let login_screen = LoginScreen::new(&session_manager_blocking);
 
         // Try to restore session on startup
-        let (api_client, current_view, current_user) =
+        let (api_client, current_view, current_user, current_permissions) =
             if let Some(session) = session_manager_blocking.get_session() {
                 log::info!("Found saved session, attempting to restore...");
 
@@ -127,27 +137,37 @@ impl BeepZoneApp {
                                     "Session restored successfully for user: {}",
                                     session.user.username
                                 );
-                                (Some(client), AppView::Dashboard, Some(session.user.clone()))
+                                (
+                                    Some(client),
+                                    AppView::Dashboard,
+                                    Some(session.user.clone()),
+                                    session.permissions.clone(),
+                                )
                             }
                             Ok(false) => {
                                 log::warn!("Saved session check returned invalid");
-                                (None, AppView::Login, None)
+                                (None, AppView::Login, None, None)
                             }
                             Err(e) => {
                                 log::warn!("Saved session validity check error: {}", e);
                                 // Be forgiving on startup: keep client and let periodic checks refine
-                                (Some(client), AppView::Dashboard, Some(session.user.clone()))
+                                (
+                                    Some(client),
+                                    AppView::Dashboard,
+                                    Some(session.user.clone()),
+                                    session.permissions.clone(),
+                                )
                             }
                         }
                     }
                     Err(e) => {
                         log::error!("Failed to create API client: {}", e);
-                        (None, AppView::Login, None)
+                        (None, AppView::Login, None, None)
                     }
                 }
             } else {
                 log::info!("No saved session found");
-                (None, AppView::Login, None)
+                (None, AppView::Login, None, None)
             };
 
         drop(session_manager_blocking);
@@ -163,6 +183,7 @@ impl BeepZoneApp {
             previous_view: None,
             view_filter_states: HashMap::new(),
             current_user,
+            current_permissions,
             login_screen,
             dashboard: DashboardView::new(),
             inventory: InventoryView::new(),
@@ -179,6 +200,13 @@ impl BeepZoneApp {
             app_config,
             login_success: None,
             show_about: false,
+            should_exit_to_kiosk: false,
+            is_kiosk_mode: false,
+            enable_full_osk_button: false,
+            show_osk: false,
+            osk_shift_mode: false,
+            last_focused_id: None,
+            osk_event_queue: Vec::new(),
             server_status: ServerStatus::Unknown,
             last_health_check: std::time::Instant::now(),
             health_check_in_progress: false,
@@ -198,9 +226,26 @@ impl BeepZoneApp {
         app
     }
 
-    fn handle_login_success(&mut self, server_url: String, response: LoginResponse) {
+    pub fn handle_login_success(&mut self, server_url: String, response: LoginResponse) {
+        // Ensure we have token and user
+        let token = match response.token {
+            Some(t) => t,
+            None => {
+                log::error!("Login successful but no token returned");
+                return;
+            }
+        };
+        
+        let user = match response.user {
+            Some(u) => u,
+            None => {
+                log::error!("Login successful but no user returned");
+                return;
+            }
+        };
+
         // Capture username for logging before moving fields out of response
-        let username = response.user.username.clone();
+        let username = user.username.clone();
         log::info!("Login successful for user: {}", username);
 
         // Create API client with token
@@ -212,19 +257,39 @@ impl BeepZoneApp {
                 return;
             }
         };
-        api_client.set_token(response.token.clone());
+        api_client.set_token(token.clone());
 
-        self.api_client = Some(api_client);
-        self.current_user = Some(response.user.clone());
+        self.api_client = Some(api_client.clone());
+        self.current_user = Some(user.clone());
+
+        // Fetch permissions
+        let permissions = match api_client.get_permissions() {
+            Ok(resp) => {
+                if resp.success {
+                    log::info!("Permissions fetched successfully: {:?}", resp.permissions);
+                    Some(resp.permissions)
+                } else {
+                    log::warn!("Failed to fetch permissions: {:?}", resp.error);
+                    None
+                }
+            }
+            Err(e) => {
+                log::warn!("Error fetching permissions: {}", e);
+                None
+            }
+        };
+        
+        self.current_permissions = permissions.clone();
 
         // Save session (blocking is fine here, it's just writing a small JSON file)
         let session_data = SessionData {
             server_url,
-            token: response.token,
-            user: response.user,
+            token,
+            user,
             remember_server: true,
             remember_username: true,
             saved_username: self.current_user.as_ref().map(|u| u.username.clone()),
+            permissions,
             default_printer_id: None,
             last_printer_id: None,
         };
@@ -246,6 +311,23 @@ impl BeepZoneApp {
     }
 
     fn handle_reauth_success(&mut self, server_url: String, response: LoginResponse) {
+        // Ensure we have token and user
+        let token = match response.token {
+            Some(t) => t,
+            None => {
+                log::error!("Reauth successful but no token returned");
+                return;
+            }
+        };
+        
+        let user = match response.user {
+            Some(u) => u,
+            None => {
+                log::error!("Reauth successful but no user returned");
+                return;
+            }
+        };
+
         // Preserve current view but refresh token and user
         let mut new_client = match ApiClient::new(server_url.clone()) {
             Ok(client) => client,
@@ -255,20 +337,39 @@ impl BeepZoneApp {
                 return;
             }
         };
-        new_client.set_token(response.token.clone());
+        new_client.set_token(token.clone());
 
         // Replace client and user
-        self.api_client = Some(new_client);
-        self.current_user = Some(response.user.clone());
+        self.api_client = Some(new_client.clone());
+        self.current_user = Some(user.clone());
+
+        // Fetch permissions
+        let permissions = match new_client.get_permissions() {
+            Ok(resp) => {
+                if resp.success {
+                    Some(resp.permissions)
+                } else {
+                    log::warn!("Failed to fetch permissions: {:?}", resp.error);
+                    None
+                }
+            }
+            Err(e) => {
+                log::warn!("Error fetching permissions: {}", e);
+                None
+            }
+        };
+        
+        self.current_permissions = permissions.clone();
 
         // Save updated session
         let session_data = SessionData {
             server_url,
-            token: response.token,
-            user: response.user,
+            token,
+            user,
             remember_server: true,
             remember_username: true,
             saved_username: self.current_user.as_ref().map(|u| u.username.clone()),
+            permissions,
             default_printer_id: None,
             last_printer_id: None,
         };
@@ -308,6 +409,13 @@ impl BeepZoneApp {
                                 self.show_about = 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() {
+                                    self.show_osk = !self.show_osk;
+                                }
+                            }
+
                             if ui.button("Bye").clicked() {
                                 self.handle_logout();
                             }
@@ -368,7 +476,7 @@ impl BeepZoneApp {
                                 ui.set_width(content_width);
                                 ui.scope(|ui| {
                                     ui.spacing_mut().item_spacing = egui::vec2(8.0, 8.0);
-                                    action_triggered = ribbon_ui.show(ctx, ui);
+                                    action_triggered = ribbon_ui.show(ctx, ui, self.current_permissions.as_ref());
                                 });
 
                                 // Update current view based on active ribbon tab
@@ -449,7 +557,7 @@ impl BeepZoneApp {
         action_triggered
     }
 
-    fn handle_logout(&mut self) {
+    pub fn handle_logout(&mut self) {
         log::info!("Taking myself out");
 
         // Logout from API
@@ -472,7 +580,13 @@ impl BeepZoneApp {
         self.api_client = None;
         self.current_user = None;
         self.current_view = AppView::Login;
+        self.should_exit_to_kiosk = true;
         self.server_status = ServerStatus::Unknown;
+
+        // Reset ribbon state to default
+        if let Some(ribbon) = &mut self.ribbon_ui {
+            ribbon.active_tab = "Dashboard".to_string();
+        }
     }
 
     /// Force an immediate health check (used when timeout errors detected)
@@ -885,6 +999,20 @@ impl BeepZoneApp {
 
 impl eframe::App for BeepZoneApp {
     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);
+        }
+
+        // Show OSK first so it reserves space at the bottom (preventing click-through and overlap)
+        self.show_osk_overlay(ctx);
+
         // Detect view changes and save/restore filter state
         if let Some(prev_view) = self.previous_view {
             if prev_view != self.current_view {
@@ -976,7 +1104,7 @@ impl eframe::App for BeepZoneApp {
             }
 
             self.show_top_bar(ctx, block_interaction);
-            let ribbon_action = if block_interaction {
+            let ribbon_action = if block_interaction || self.current_view == AppView::Login {
                 None
             } else {
                 self.show_ribbon(ctx)
@@ -1014,11 +1142,12 @@ impl eframe::App for BeepZoneApp {
                         self.api_client.as_ref(),
                         self.ribbon_ui.as_mut(),
                         &self.session_manager,
+                        self.current_permissions.as_ref(),
                     );
                 }
                 AppView::Categories => {
                     self.categories
-                        .show(ui, self.api_client.as_ref(), self.ribbon_ui.as_mut());
+                        .show(ui, self.api_client.as_ref(), self.ribbon_ui.as_mut(), self.current_permissions.as_ref());
                 }
                 AppView::Zones => {
                     if let Some(ribbon) = self.ribbon_ui.as_mut() {
@@ -1030,7 +1159,7 @@ impl eframe::App for BeepZoneApp {
                                 .insert("zones_filter_changed".to_string(), true);
                         }
 
-                        self.zones.show(ui, self.api_client.as_ref(), ribbon);
+                        self.zones.show(ui, self.api_client.as_ref(), ribbon, self.current_permissions.as_ref());
 
                         // Handle zone navigation request to inventory
                         if let Some(zone_code) = self.zones.switch_to_inventory_with_zone.take() {
@@ -1077,7 +1206,7 @@ impl eframe::App for BeepZoneApp {
                         }
 
                         self.borrowing
-                            .show(ctx, ui, self.api_client.as_ref(), ribbon);
+                            .show(ctx, ui, self.api_client.as_ref(), ribbon, self.current_permissions.as_ref());
 
                         // Handle borrower navigation request to inventory
                         if let Some(borrower_id) =
@@ -1120,7 +1249,7 @@ impl eframe::App for BeepZoneApp {
                 }
                 AppView::Audits => {
                     let user_id = self.current_user.as_ref().map(|u| u.id);
-                    self.audits.show(ctx, ui, self.api_client.as_ref(), user_id);
+                    self.audits.show(ctx, ui, self.api_client.as_ref(), user_id, self.current_permissions.as_ref());
                 }
                 AppView::Templates => {
                     if let Some(ribbon) = self.ribbon_ui.as_mut() {
@@ -1134,12 +1263,12 @@ impl eframe::App for BeepZoneApp {
 
                         let flags = self
                             .templates
-                            .show(ui, self.api_client.as_ref(), Some(ribbon));
+                            .show(ui, self.api_client.as_ref(), Some(ribbon), self.current_permissions.as_ref());
                         for flag in flags {
                             ribbon.checkboxes.insert(flag, false);
                         }
                     } else {
-                        self.templates.show(ui, self.api_client.as_ref(), None);
+                        self.templates.show(ui, self.api_client.as_ref(), None, self.current_permissions.as_ref());
                     }
                 }
                 AppView::Suppliers => {
@@ -1148,16 +1277,17 @@ impl eframe::App for BeepZoneApp {
                             ui,
                             self.api_client.as_ref(),
                             Some(&mut *ribbon_ui),
+                            self.current_permissions.as_ref(),
                         );
                         for flag in flags {
                             ribbon_ui.checkboxes.insert(flag, false);
                         }
                     } else {
-                        let _ = self.suppliers.show(ui, self.api_client.as_ref(), None);
+                        let _ = self.suppliers.show(ui, self.api_client.as_ref(), None, self.current_permissions.as_ref());
                     }
                 }
                 AppView::IssueTracker => {
-                    self.issues.show(ui, self.api_client.as_ref());
+                    self.issues.show(ui, self.api_client.as_ref(), self.current_permissions.as_ref());
                 }
                 AppView::Printers => {
                     // Render printers dropdown in ribbon if we're on printers tab
@@ -1172,6 +1302,7 @@ impl eframe::App for BeepZoneApp {
                         self.api_client.as_ref(),
                         self.ribbon_ui.as_mut(),
                         &self.session_manager,
+                        self.current_permissions.as_ref(),
                     );
                 }
                 AppView::LabelTemplates => {
@@ -1179,9 +1310,12 @@ impl eframe::App for BeepZoneApp {
                         ui,
                         self.api_client.as_ref(),
                         self.ribbon_ui.as_mut(),
+                        self.current_permissions.as_ref(),
                     );
                 }
-                AppView::Login => unreachable!(),
+                AppView::Login => {
+                    // Do nothing, we are transitioning to logout
+                }
             });
             } else {
                 self.show_reconnect_overlay(ctx);
@@ -1271,3 +1405,108 @@ impl eframe::App for BeepZoneApp {
         }
     }
 }
+
+impl BeepZoneApp {
+    fn show_osk_overlay(&mut self, ctx: &egui::Context) {
+        if !self.show_osk { return; }
+
+        let height = 340.0;
+        egui::TopBottomPanel::bottom("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("BACK").size(20.0))).clicked() {
+                             self.osk_event_queue.push(egui::Event::Key {
+                                 key: egui::Key::Backspace,
+                                 pressed: true,
+                                 modifiers: egui::Modifiers::NONE,
+                                 repeat: false,
+                                 physical_key: None,
+                             });
+                             if let Some(id) = self.last_focused_id {
+                                ctx.memory_mut(|m| m.request_focus(id));
+                            }
+                        }
+                    });
+                    ui.add_space(10.0);
+                });
+            });
+    }
+}
+

+ 9 - 3
src/ui/audits.rs

@@ -143,6 +143,7 @@ impl AuditsView {
         ui: &mut egui::Ui,
         api: &ApiClient,
         current_user_id: Option<i32>,
+        permissions: Option<&serde_json::Value>,
     ) {
         egui::Frame::group(ui.style())
             .fill(ui.style().visuals.extreme_bg_color)
@@ -271,12 +272,15 @@ impl AuditsView {
                         self.last_popup_rect = None;
                     }
 
-                    let disable_new = self.workflow.is_active();
+                    let has_perm = crate::ui::ribbon::RibbonUI::check_permission(permissions, "create_audit");
+                    let disable_new = self.workflow.is_active() || !has_perm;
+                    
                     let start_zone_clicked_button = ui
                         .add_enabled(
                             !disable_new,
                             egui::Button::new("Start Zone Audit").min_size(egui::vec2(btn_w, 0.0)),
                         )
+                        .on_disabled_hover_text(if !has_perm { "Permission denied" } else { "Audit in progress" })
                         .clicked();
 
                     let start_spot_clicked = ui
@@ -284,12 +288,13 @@ impl AuditsView {
                             !disable_new,
                             egui::Button::new("Start Spot Check").min_size(egui::vec2(btn_w, 0.0)),
                         )
+                        .on_disabled_hover_text(if !has_perm { "Permission denied" } else { "Audit in progress" })
                         .clicked();
 
                     let start_zone_pressed_enter =
                         (text_resp.lost_focus() || text_resp.has_focus()) 
                         && ui.input(|i| i.key_pressed(egui::Key::Enter));
-                    let start_zone_clicked = start_zone_clicked_button || start_zone_pressed_enter;
+                    let start_zone_clicked = (start_zone_clicked_button || start_zone_pressed_enter) && has_perm;
 
                     if start_zone_clicked {
                         // ui.memory_mut(|m| m.close_popup(ui.make_persistent_id("zone_autocomplete")));
@@ -400,6 +405,7 @@ impl AuditsView {
         ui: &mut egui::Ui,
         api_client: Option<&ApiClient>,
         current_user_id: Option<i32>,
+        permissions: Option<&serde_json::Value>,
     ) {
         // Auto-load if not loaded
         if !self.init_loaded {
@@ -440,7 +446,7 @@ impl AuditsView {
                 ui.separator();
 
                 if let Some(api) = api_client {
-                    self.render_launch_controls(ui, api, current_user_id);
+                    self.render_launch_controls(ui, api, current_user_id, permissions);
                 }
 
                 if !self.init_loaded {

+ 11 - 4
src/ui/borrowing.rs

@@ -265,6 +265,7 @@ impl BorrowingView {
         ui: &mut egui::Ui,
         api_client: Option<&ApiClient>,
         ribbon: &mut crate::ui::ribbon::RibbonUI,
+        permissions: Option<&serde_json::Value>,
     ) {
         ui.horizontal(|ui| {
             ui.heading("Borrowing");
@@ -309,7 +310,9 @@ impl BorrowingView {
                 .copied()
                 .unwrap_or(false)
             {
-                self.borrow_flow.open(api);
+                if crate::ui::ribbon::RibbonUI::check_permission(permissions, "checkout_asset") {
+                    self.borrow_flow.open(api);
+                }
             }
 
             if ribbon
@@ -318,7 +321,9 @@ impl BorrowingView {
                 .copied()
                 .unwrap_or(false)
             {
-                self.return_flow.open(api);
+                if crate::ui::ribbon::RibbonUI::check_permission(permissions, "return_asset") {
+                    self.return_flow.open(api);
+                }
             }
 
             if ribbon
@@ -327,8 +332,10 @@ impl BorrowingView {
                 .copied()
                 .unwrap_or(false)
             {
-                self.show_register_dialog = true;
-                self.register_error = None;
+                if crate::ui::ribbon::RibbonUI::check_permission(permissions, "create_borrower") {
+                    self.show_register_dialog = true;
+                    self.register_error = None;
+                }
             }
 
             if ribbon

+ 74 - 61
src/ui/categories.rs

@@ -408,6 +408,7 @@ impl CategoriesView {
         ui: &mut egui::Ui,
         api_client: Option<&ApiClient>,
         ribbon: Option<&mut RibbonUI>,
+        permissions: Option<&serde_json::Value>,
     ) -> Vec<String> {
         let mut flags_to_clear = Vec::new();
 
@@ -415,29 +416,35 @@ impl CategoriesView {
         if let Some(item) = ui.ctx().data_mut(|d| {
             d.remove_temp::<serde_json::Value>(egui::Id::new("cat_double_click_edit"))
         }) {
-            self.open_editor_with(&item);
-            ui.ctx().request_repaint();
+            if RibbonUI::check_permission(permissions, "edit_category") {
+                self.open_editor_with(&item);
+                ui.ctx().request_repaint();
+            }
         }
 
         if let Some(item) = ui.ctx().data_mut(|d| {
             d.remove_temp::<serde_json::Value>(egui::Id::new("cat_context_menu_edit"))
         }) {
-            self.open_editor_with(&item);
-            ui.ctx().request_repaint();
+            if RibbonUI::check_permission(permissions, "edit_category") {
+                self.open_editor_with(&item);
+                ui.ctx().request_repaint();
+            }
         }
 
         if let Some(item) = ui.ctx().data_mut(|d| {
             d.remove_temp::<serde_json::Value>(egui::Id::new("cat_context_menu_delete"))
         }) {
-            let name = item
-                .get("category_name")
-                .and_then(|v| v.as_str())
-                .unwrap_or("Unknown")
-                .to_string();
-            let id = item.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
-            self.pending_delete_ids = vec![id]; // Changed to vector
-            self.delete_dialog.open(name, id.to_string());
-            ui.ctx().request_repaint();
+            if RibbonUI::check_permission(permissions, "delete_category") {
+                let name = item
+                    .get("category_name")
+                    .and_then(|v| v.as_str())
+                    .unwrap_or("Unknown")
+                    .to_string();
+                let id = item.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
+                self.pending_delete_ids = vec![id]; // Changed to vector
+                self.delete_dialog.open(name, id.to_string());
+                ui.ctx().request_repaint();
+            }
         }
 
         // Auto-load on first show, but only try once unless user explicitly requests retry
@@ -477,9 +484,11 @@ impl CategoriesView {
                 .copied()
                 .unwrap_or(false)
             {
-                // Create a new add dialog with current category options
-                self.add_dialog = self.create_add_dialog_with_options();
-                self.add_dialog.open(&serde_json::json!({})); // Open with empty data
+                if RibbonUI::check_permission(permissions, "create_category") {
+                    // Create a new add dialog with current category options
+                    self.add_dialog = self.create_add_dialog_with_options();
+                    self.add_dialog.open(&serde_json::json!({})); // Open with empty data
+                }
                 flags_to_clear.push("categories_add".to_string());
             }
 
@@ -489,25 +498,27 @@ impl CategoriesView {
                 .copied()
                 .unwrap_or(false)
             {
-                // Get selected category IDs
-                let selected_ids = self.get_selected_ids();
-
-                if !selected_ids.is_empty() {
-                    // For edit, only edit the first selected category (bulk edit of categories is complex)
-                    if let Some(&first_id) = selected_ids.first() {
-                        // Clone the category to avoid borrowing issues
-                        let category = self
-                            .categories
-                            .iter()
-                            .find(|c| c.get("id").and_then(|v| v.as_i64()) == Some(first_id))
-                            .cloned();
-
-                        if let Some(cat) = category {
-                            self.open_editor_with(&cat);
+                if RibbonUI::check_permission(permissions, "edit_category") {
+                    // Get selected category IDs
+                    let selected_ids = self.get_selected_ids();
+
+                    if !selected_ids.is_empty() {
+                        // For edit, only edit the first selected category (bulk edit of categories is complex)
+                        if let Some(&first_id) = selected_ids.first() {
+                            // Clone the category to avoid borrowing issues
+                            let category = self
+                                .categories
+                                .iter()
+                                .find(|c| c.get("id").and_then(|v| v.as_i64()) == Some(first_id))
+                                .cloned();
+
+                            if let Some(cat) = category {
+                                self.open_editor_with(&cat);
+                            }
                         }
+                    } else {
+                        log::warn!("Edit requested but no categories selected");
                     }
-                } else {
-                    log::warn!("Edit requested but no categories selected");
                 }
                 flags_to_clear.push("categories_edit".to_string());
             }
@@ -518,36 +529,38 @@ impl CategoriesView {
                 .copied()
                 .unwrap_or(false)
             {
-                // Get selected category IDs for bulk delete
-                let selected_ids = self.get_selected_ids();
-
-                if !selected_ids.is_empty() {
-                    self.pending_delete_ids = selected_ids.clone();
-                    let count = selected_ids.len();
-
-                    // Show dialog with appropriate message for single or multiple deletes
-                    let message =
-                        if count == 1 {
-                            // Get the category name for single delete
-                            if let Some(category) = self.categories.iter().find(|c| {
-                                c.get("id").and_then(|v| v.as_i64()) == Some(selected_ids[0])
-                            }) {
-                                category
-                                    .get("category_name")
-                                    .and_then(|v| v.as_str())
-                                    .unwrap_or("Unknown")
-                                    .to_string()
+                if RibbonUI::check_permission(permissions, "delete_category") {
+                    // Get selected category IDs for bulk delete
+                    let selected_ids = self.get_selected_ids();
+
+                    if !selected_ids.is_empty() {
+                        self.pending_delete_ids = selected_ids.clone();
+                        let count = selected_ids.len();
+
+                        // Show dialog with appropriate message for single or multiple deletes
+                        let message =
+                            if count == 1 {
+                                // Get the category name for single delete
+                                if let Some(category) = self.categories.iter().find(|c| {
+                                    c.get("id").and_then(|v| v.as_i64()) == Some(selected_ids[0])
+                                }) {
+                                    category
+                                        .get("category_name")
+                                        .and_then(|v| v.as_str())
+                                        .unwrap_or("Unknown")
+                                        .to_string()
+                                } else {
+                                    "Unknown".to_string()
+                                }
                             } else {
-                                "Unknown".to_string()
-                            }
-                        } else {
-                            format!("{} categories", count)
-                        };
+                                format!("{} categories", count)
+                            };
 
-                    self.delete_dialog
-                        .open(message, format!("IDs: {:?}", selected_ids));
-                } else {
-                    log::warn!("Delete requested but no categories selected");
+                        self.delete_dialog
+                            .open(message, format!("IDs: {:?}", selected_ids));
+                    } else {
+                        log::warn!("Delete requested but no categories selected");
+                    }
                 }
                 flags_to_clear.push("categories_delete".to_string());
             }

+ 176 - 144
src/ui/inventory.rs

@@ -468,6 +468,7 @@ impl InventoryView {
         ui: &mut egui::Ui,
         api_client: Option<&ApiClient>,
         session_manager: &std::sync::Arc<tokio::sync::Mutex<crate::session::SessionManager>>,
+        permissions: Option<&serde_json::Value>,
     ) {
         // We need to work around Rust's borrowing rules here
         // First, get the data we need
@@ -478,6 +479,7 @@ impl InventoryView {
         let mut deferred_actions: Vec<DeferredAction> = Vec::new();
         let mut temp_handler = TempInventoryEventHandler {
             deferred_actions: &mut deferred_actions,
+            permissions,
         };
 
         // Render table with the temporary event handler
@@ -783,6 +785,7 @@ impl InventoryView {
         api_client: Option<&ApiClient>,
         ribbon_ui: Option<&mut crate::ui::ribbon::RibbonUI>,
         session_manager: &std::sync::Arc<tokio::sync::Mutex<crate::session::SessionManager>>,
+        permissions: Option<&serde_json::Value>,
     ) {
         // Handle initial load if needed - but ONLY if we've never loaded before
         // Don't auto-load if assets are empty due to filters returning 0 results
@@ -821,6 +824,7 @@ impl InventoryView {
             Some(100),
             ribbon_ui.as_ref().map(|r| &**r),
             session_manager,
+            permissions,
         );
 
         // Clear the flags after processing
@@ -838,10 +842,11 @@ impl InventoryView {
         limit: Option<u32>,
         ribbon_ui: Option<&crate::ui::ribbon::RibbonUI>,
         session_manager: &std::sync::Arc<tokio::sync::Mutex<crate::session::SessionManager>>,
+        permissions: Option<&serde_json::Value>,
     ) -> Vec<String> {
         // Handle ribbon actions first
         let flags_to_clear = if let Some(ribbon) = ribbon_ui {
-            self.handle_ribbon_actions(ribbon, api_client, session_manager)
+            self.handle_ribbon_actions(ribbon, api_client, session_manager, permissions)
         } else {
             Vec::new()
         };
@@ -879,8 +884,10 @@ impl InventoryView {
                     }
                 }
 
-                if ui.button("➕ Add Asset").clicked() {
-                    self.prepare_add_asset_editor(client);
+                if crate::ui::ribbon::RibbonUI::check_permission(permissions, "create_asset") {
+                    if ui.button("➕ Add Asset").clicked() {
+                        self.prepare_add_asset_editor(client);
+                    }
                 }
 
                 // Show selection count but no buttons (use right-click instead)
@@ -1086,7 +1093,7 @@ impl InventoryView {
         }
 
         // Render table with event handling
-        self.render_table_with_events(ui, api_client, session_manager);
+        self.render_table_with_events(ui, api_client, session_manager, permissions);
 
         // Handle dialogs
         self.handle_dialogs(ui, api_client, limit, session_manager);
@@ -1497,6 +1504,7 @@ impl InventoryView {
         ribbon: &crate::ui::ribbon::RibbonUI,
         api_client: Option<&ApiClient>,
         session_manager: &std::sync::Arc<tokio::sync::Mutex<crate::session::SessionManager>>,
+        permissions: Option<&serde_json::Value>,
     ) -> Vec<String> {
         let mut flags_to_clear = Vec::new();
 
@@ -1574,20 +1582,22 @@ impl InventoryView {
             .get("inventory_action_add")
             .unwrap_or(&false)
         {
-            if let Some(client) = api_client {
-                self.prepare_add_asset_editor(client);
-            } else {
-                let mut preset = serde_json::Map::new();
-                preset.insert(
-                    "asset_type".to_string(),
-                    serde_json::Value::String("N".to_string()),
-                );
-                preset.insert(
-                    "status".to_string(),
-                    serde_json::Value::String("Good".to_string()),
-                );
-                self.add_dialog.title = "Add Asset".to_string();
-                self.add_dialog.open_new(Some(&preset));
+            if crate::ui::ribbon::RibbonUI::check_permission(permissions, "create_asset") {
+                if let Some(client) = api_client {
+                    self.prepare_add_asset_editor(client);
+                } else {
+                    let mut preset = serde_json::Map::new();
+                    preset.insert(
+                        "asset_type".to_string(),
+                        serde_json::Value::String("N".to_string()),
+                    );
+                    preset.insert(
+                        "status".to_string(),
+                        serde_json::Value::String("Good".to_string()),
+                    );
+                    self.add_dialog.title = "Add Asset".to_string();
+                    self.add_dialog.open_new(Some(&preset));
+                }
             }
         }
 
@@ -1596,27 +1606,29 @@ impl InventoryView {
             .get("inventory_action_delete")
             .unwrap_or(&false)
         {
-            if !selected_ids.is_empty() {
-                self.pending_delete_ids = selected_ids.clone();
-                if selected_ids.len() == 1 {
-                    if let Some(asset) = self.find_asset_by_id(selected_ids[0]) {
-                        let name = asset
-                            .get("name")
-                            .and_then(|v| v.as_str())
-                            .unwrap_or("Unknown")
-                            .to_string();
-                        self.delete_dialog.open(name, selected_ids[0].to_string());
+            if crate::ui::ribbon::RibbonUI::check_permission(permissions, "delete_asset") {
+                if !selected_ids.is_empty() {
+                    self.pending_delete_ids = selected_ids.clone();
+                    if selected_ids.len() == 1 {
+                        if let Some(asset) = self.find_asset_by_id(selected_ids[0]) {
+                            let name = asset
+                                .get("name")
+                                .and_then(|v| v.as_str())
+                                .unwrap_or("Unknown")
+                                .to_string();
+                            self.delete_dialog.open(name, selected_ids[0].to_string());
+                        }
+                    } else {
+                        self.delete_dialog.title = "Delete Assets".to_string();
+                        self.delete_dialog.message = format!(
+                            "Are you sure you want to delete {} selected assets?",
+                            selected_ids.len()
+                        );
+                        self.delete_dialog.open(
+                            format!("Multiple items ({} selected)", selected_ids.len()),
+                            "multiple".to_string(),
+                        );
                     }
-                } else {
-                    self.delete_dialog.title = "Delete Assets".to_string();
-                    self.delete_dialog.message = format!(
-                        "Are you sure you want to delete {} selected assets?",
-                        selected_ids.len()
-                    );
-                    self.delete_dialog.open(
-                        format!("Multiple items ({} selected)", selected_ids.len()),
-                        "multiple".to_string(),
-                    );
                 }
             }
         }
@@ -1626,18 +1638,20 @@ impl InventoryView {
             .get("inventory_action_edit_easy")
             .unwrap_or(&false)
         {
-            if !selected_ids.is_empty() {
-                self.pending_edit_ids = selected_ids.clone();
-                self.is_bulk_edit = selected_ids.len() > 1;
-                if let Some(client) = api_client {
-                    self.prepare_easy_edit_fields(client);
-                }
-                if selected_ids.len() == 1 {
-                    if let Some(asset) = self.find_asset_by_id(selected_ids[0]) {
-                        self.edit_dialog.open(&asset);
+            if crate::ui::ribbon::RibbonUI::check_permission(permissions, "edit_asset") {
+                if !selected_ids.is_empty() {
+                    self.pending_edit_ids = selected_ids.clone();
+                    self.is_bulk_edit = selected_ids.len() > 1;
+                    if let Some(client) = api_client {
+                        self.prepare_easy_edit_fields(client);
+                    }
+                    if selected_ids.len() == 1 {
+                        if let Some(asset) = self.find_asset_by_id(selected_ids[0]) {
+                            self.edit_dialog.open(&asset);
+                        }
+                    } else {
+                        self.edit_dialog.open_new(None);
                     }
-                } else {
-                    self.edit_dialog.open_new(None);
                 }
             }
         }
@@ -1647,18 +1661,20 @@ impl InventoryView {
             .get("inventory_action_edit_adv")
             .unwrap_or(&false)
         {
-            if !selected_ids.is_empty() {
-                self.pending_edit_ids = selected_ids.clone();
-                self.is_bulk_edit = selected_ids.len() > 1;
-                if let Some(client) = api_client {
-                    self.prepare_advanced_edit_fields(client);
-                }
-                if selected_ids.len() == 1 {
-                    if let Some(asset) = self.find_asset_by_id(selected_ids[0]) {
-                        self.advanced_edit_dialog.open(&asset);
+            if crate::ui::ribbon::RibbonUI::check_permission(permissions, "edit_asset") {
+                if !selected_ids.is_empty() {
+                    self.pending_edit_ids = selected_ids.clone();
+                    self.is_bulk_edit = selected_ids.len() > 1;
+                    if let Some(client) = api_client {
+                        self.prepare_advanced_edit_fields(client);
+                    }
+                    if selected_ids.len() == 1 {
+                        if let Some(asset) = self.find_asset_by_id(selected_ids[0]) {
+                            self.advanced_edit_dialog.open(&asset);
+                        }
+                    } else {
+                        self.advanced_edit_dialog.open_new(None);
                     }
-                } else {
-                    self.advanced_edit_dialog.open_new(None);
                 }
             }
         }
@@ -1849,66 +1865,76 @@ enum DeferredAction {
 // Temporary event handler that collects actions for later processing
 struct TempInventoryEventHandler<'a> {
     deferred_actions: &'a mut Vec<DeferredAction>,
+    permissions: Option<&'a serde_json::Value>,
 }
 
 impl<'a> TableEventHandler<Value> for TempInventoryEventHandler<'a> {
     fn on_double_click(&mut self, item: &Value, _row_index: usize) {
-        log::info!("Double-click detected on asset: {:?}", item.get("name"));
-        self.deferred_actions
-            .push(DeferredAction::DoubleClick(item.clone()));
+        // Check edit permission for double-click action
+        if crate::ui::ribbon::RibbonUI::check_permission(self.permissions, "edit_asset") {
+            log::info!("Double-click detected on asset: {:?}", item.get("name"));
+            self.deferred_actions
+                .push(DeferredAction::DoubleClick(item.clone()));
+        }
     }
 
     fn on_context_menu(&mut self, ui: &mut egui::Ui, item: &Value, _row_index: usize) {
-        if ui
-            .button(format!("{} Edit", egui_phosphor::regular::PENCIL))
-            .clicked()
-        {
-            log::info!(
-                "Context menu edit clicked for asset: {:?}",
-                item.get("name")
-            );
-            self.deferred_actions
-                .push(DeferredAction::ContextEdit(item.clone()));
-            ui.close();
+        if crate::ui::ribbon::RibbonUI::check_permission(self.permissions, "edit_asset") {
+            if ui
+                .button(format!("{} Edit", egui_phosphor::regular::PENCIL))
+                .clicked()
+            {
+                log::info!(
+                    "Context menu edit clicked for asset: {:?}",
+                    item.get("name")
+                );
+                self.deferred_actions
+                    .push(DeferredAction::ContextEdit(item.clone()));
+                ui.close();
+            }
         }
 
-        if ui
-            .button(format!("{} Clone Asset", egui_phosphor::regular::COPY))
-            .clicked()
-        {
-            log::info!(
-                "Context menu clone clicked for asset: {:?}",
-                item.get("name")
-            );
-            self.deferred_actions
-                .push(DeferredAction::ContextClone(item.clone()));
-            ui.close();
+        if crate::ui::ribbon::RibbonUI::check_permission(self.permissions, "create_asset") {
+            if ui
+                .button(format!("{} Clone Asset", egui_phosphor::regular::COPY))
+                .clicked()
+            {
+                log::info!(
+                    "Context menu clone clicked for asset: {:?}",
+                    item.get("name")
+                );
+                self.deferred_actions
+                    .push(DeferredAction::ContextClone(item.clone()));
+                ui.close();
+            }
         }
 
-        if ui
-            .button(format!("{} Advanced Edit", egui_phosphor::regular::GEAR))
-            .clicked()
-        {
-            log::info!(
-                "Context menu advanced edit clicked for asset: {:?}",
-                item.get("name")
-            );
-            self.deferred_actions
-                .push(DeferredAction::ContextAdvancedEdit(item.clone()));
-            ui.close();
-        }
+        if crate::ui::ribbon::RibbonUI::check_permission(self.permissions, "edit_asset") {
+            if ui
+                .button(format!("{} Advanced Edit", egui_phosphor::regular::GEAR))
+                .clicked()
+            {
+                log::info!(
+                    "Context menu advanced edit clicked for asset: {:?}",
+                    item.get("name")
+                );
+                self.deferred_actions
+                    .push(DeferredAction::ContextAdvancedEdit(item.clone()));
+                ui.close();
+            }
 
-        if ui
-            .button(format!("{} Replace Item", egui_phosphor::regular::ARROWS_LEFT_RIGHT))
-            .clicked()
-        {
-            log::info!(
-                "Context menu replace clicked for asset: {:?}",
-                item.get("name")
-            );
-            self.deferred_actions
-                .push(DeferredAction::ContextReplace(item.clone()));
-            ui.close();
+            if ui
+                .button(format!("{} Replace Item", egui_phosphor::regular::ARROWS_LEFT_RIGHT))
+                .clicked()
+            {
+                log::info!(
+                    "Context menu replace clicked for asset: {:?}",
+                    item.get("name")
+                );
+                self.deferred_actions
+                    .push(DeferredAction::ContextReplace(item.clone()));
+                ui.close();
+            }
         }
 
         if ui
@@ -1976,52 +2002,58 @@ impl<'a> TableEventHandler<Value> for TempInventoryEventHandler<'a> {
 
         if lendable && !status.is_empty() {
             if status == "Available" {
-                if ui
-                    .button(format!("{} Lend Item", egui_phosphor::regular::ARROW_LEFT))
-                    .clicked()
-                {
-                    log::info!(
-                        "Context menu lend clicked for asset: {:?}",
-                        item.get("name")
-                    );
-                    self.deferred_actions
-                        .push(DeferredAction::ContextLend(item.clone()));
-                    ui.close();
+                if crate::ui::ribbon::RibbonUI::check_permission(self.permissions, "checkout_asset") {
+                    if ui
+                        .button(format!("{} Lend Item", egui_phosphor::regular::ARROW_LEFT))
+                        .clicked()
+                    {
+                        log::info!(
+                            "Context menu lend clicked for asset: {:?}",
+                            item.get("name")
+                        );
+                        self.deferred_actions
+                            .push(DeferredAction::ContextLend(item.clone()));
+                        ui.close();
+                    }
                 }
             } else if matches!(
                 status,
                 "Borrowed" | "Overdue" | "Stolen" | "Illegally Handed Out" | "Deployed"
             ) {
-                if ui
-                    .button(format!(
-                        "{} Return Item",
-                        egui_phosphor::regular::ARROW_RIGHT
-                    ))
-                    .clicked()
-                {
-                    log::info!(
-                        "Context menu return clicked for asset: {:?}",
-                        item.get("name")
-                    );
-                    self.deferred_actions
-                        .push(DeferredAction::ContextReturn(item.clone()));
-                    ui.close();
+                if crate::ui::ribbon::RibbonUI::check_permission(self.permissions, "return_asset") {
+                    if ui
+                        .button(format!(
+                            "{} Return Item",
+                            egui_phosphor::regular::ARROW_RIGHT
+                        ))
+                        .clicked()
+                    {
+                        log::info!(
+                            "Context menu return clicked for asset: {:?}",
+                            item.get("name")
+                        );
+                        self.deferred_actions
+                            .push(DeferredAction::ContextReturn(item.clone()));
+                        ui.close();
+                    }
                 }
             }
             ui.separator();
         }
 
-        if ui
-            .button(format!("{} Delete", egui_phosphor::regular::TRASH))
-            .clicked()
-        {
-            log::info!(
-                "Context menu delete clicked for asset: {:?}",
-                item.get("name")
-            );
-            self.deferred_actions
-                .push(DeferredAction::ContextDelete(item.clone()));
-            ui.close();
+        if crate::ui::ribbon::RibbonUI::check_permission(self.permissions, "delete_asset") {
+            if ui
+                .button(format!("{} Delete", egui_phosphor::regular::TRASH))
+                .clicked()
+            {
+                log::info!(
+                    "Context menu delete clicked for asset: {:?}",
+                    item.get("name")
+                );
+                self.deferred_actions
+                    .push(DeferredAction::ContextDelete(item.clone()));
+                ui.close();
+            }
         }
     }
 

+ 59 - 39
src/ui/issues.rs

@@ -240,7 +240,12 @@ impl IssuesView {
         self.recompute_summary();
     }
 
-    pub fn show(&mut self, ui: &mut egui::Ui, api_client: Option<&ApiClient>) {
+    pub fn show(
+        &mut self,
+        ui: &mut egui::Ui,
+        api_client: Option<&ApiClient>,
+        permissions: Option<&serde_json::Value>,
+    ) {
         ui.horizontal(|ui| {
             ui.heading("Issues");
             if self.is_loading {
@@ -287,7 +292,7 @@ impl IssuesView {
 
         let visible_columns: Vec<ColumnConfig> =
             self.columns.iter().filter(|c| c.visible).cloned().collect();
-        self.render_table(ui, &visible_columns);
+        self.render_table(ui, &visible_columns, permissions);
 
         // Process selection/dialog events
         let ctx = ui.ctx();
@@ -302,27 +307,33 @@ impl IssuesView {
         if let Some(item) = ctx.data_mut(|d| {
             d.remove_temp::<serde_json::Value>(egui::Id::new("iss_double_click_edit"))
         }) {
-            self.open_editor_with(&item);
-            ctx.request_repaint();
+            if crate::ui::ribbon::RibbonUI::check_permission(permissions, "edit_issue") {
+                self.open_editor_with(&item);
+                ctx.request_repaint();
+            }
         }
         if let Some(item) = ctx.data_mut(|d| {
             d.remove_temp::<serde_json::Value>(egui::Id::new("iss_context_menu_edit"))
         }) {
-            self.open_editor_with(&item);
-            ctx.request_repaint();
+            if crate::ui::ribbon::RibbonUI::check_permission(permissions, "edit_issue") {
+                self.open_editor_with(&item);
+                ctx.request_repaint();
+            }
         }
         if let Some(item) = ctx.data_mut(|d| {
             d.remove_temp::<serde_json::Value>(egui::Id::new("iss_context_menu_delete"))
         }) {
-            let title = item
-                .get("title")
-                .and_then(|v| v.as_str())
-                .unwrap_or("Unknown")
-                .to_string();
-            let id = item.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
-            self.delete_current_id = Some(id);
-            self.delete_dialog.open(title, id.to_string());
-            ctx.request_repaint();
+            if crate::ui::ribbon::RibbonUI::check_permission(permissions, "delete_issue") {
+                let title = item
+                    .get("title")
+                    .and_then(|v| v.as_str())
+                    .unwrap_or("Unknown")
+                    .to_string();
+                let id = item.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
+                self.delete_current_id = Some(id);
+                self.delete_dialog.open(title, id.to_string());
+                ctx.request_repaint();
+            }
         }
 
         if let Some(confirmed) = self.delete_dialog.show_dialog(ctx) {
@@ -493,7 +504,12 @@ impl IssuesView {
             });
     }
 
-    fn render_table(&mut self, ui: &mut egui::Ui, visible_columns: &Vec<ColumnConfig>) {
+    fn render_table(
+        &mut self,
+        ui: &mut egui::Ui,
+        visible_columns: &Vec<ColumnConfig>,
+        permissions: Option<&serde_json::Value>,
+    ) {
         use egui_extras::{Column, TableBuilder};
         let mut table = TableBuilder::new(ui)
             .striped(true)
@@ -638,30 +654,34 @@ impl IssuesView {
                             }
                         }
                         row_resp.context_menu(|ui| {
-                            if ui
-                                .button(format!("{} Edit", egui_phosphor::regular::PENCIL))
-                                .clicked()
-                            {
-                                ui.ctx().data_mut(|d| {
-                                    d.insert_temp(
-                                        egui::Id::new("iss_context_menu_edit"),
-                                        r_clone.clone(),
-                                    )
-                                });
-                                ui.close();
+                            if crate::ui::ribbon::RibbonUI::check_permission(permissions, "edit_issue") {
+                                if ui
+                                    .button(format!("{} Edit", egui_phosphor::regular::PENCIL))
+                                    .clicked()
+                                {
+                                    ui.ctx().data_mut(|d| {
+                                        d.insert_temp(
+                                            egui::Id::new("iss_context_menu_edit"),
+                                            r_clone.clone(),
+                                        )
+                                    });
+                                    ui.close();
+                                }
+                                ui.separator();
                             }
-                            ui.separator();
-                            if ui
-                                .button(format!("{} Delete", egui_phosphor::regular::TRASH))
-                                .clicked()
-                            {
-                                ui.ctx().data_mut(|d| {
-                                    d.insert_temp(
-                                        egui::Id::new("iss_context_menu_delete"),
-                                        r_clone.clone(),
-                                    )
-                                });
-                                ui.close();
+                            if crate::ui::ribbon::RibbonUI::check_permission(permissions, "delete_issue") {
+                                if ui
+                                    .button(format!("{} Delete", egui_phosphor::regular::TRASH))
+                                    .clicked()
+                                {
+                                    ui.ctx().data_mut(|d| {
+                                        d.insert_temp(
+                                            egui::Id::new("iss_context_menu_delete"),
+                                            r_clone.clone(),
+                                        )
+                                    });
+                                    ui.close();
+                                }
                             }
                         });
                     });

+ 66 - 48
src/ui/label_templates.rs

@@ -162,6 +162,7 @@ impl LabelTemplatesView {
         ui: &mut egui::Ui,
         api_client: Option<&ApiClient>,
         ribbon_ui: Option<&mut RibbonUI>,
+        permissions: Option<&serde_json::Value>,
     ) {
         self.ensure_loaded(api_client);
 
@@ -183,8 +184,9 @@ impl LabelTemplatesView {
                 .copied()
                 .unwrap_or(false)
             {
-                // Provide helpful default layout JSON template matching database schema
-                let layout_json = r##"{
+                if RibbonUI::check_permission(permissions, "create_label_template") {
+                    // Provide helpful default layout JSON template matching database schema
+                    let layout_json = r##"{
   "version": "1.0",
   "background": "#FFFFFF",
   "elements": [
@@ -214,10 +216,11 @@ impl LabelTemplatesView {
     }
   ]
 }"##;
-                let default_data = serde_json::json!({
-                    "layout_json": layout_json
-                });
-                self.add_dialog.open(&default_data);
+                    let default_data = serde_json::json!({
+                        "layout_json": layout_json
+                    });
+                    self.add_dialog.open(&default_data);
+                }
             }
             if ribbon
                 .checkboxes
@@ -254,7 +257,7 @@ impl LabelTemplatesView {
         }
 
         // Render table with event handling
-        self.render_table_with_events(ui, api_client);
+        self.render_table_with_events(ui, api_client, permissions);
 
         // Handle dialogs
         self.handle_dialogs(ui, api_client);
@@ -263,7 +266,12 @@ impl LabelTemplatesView {
         self.process_deferred_actions(ui, api_client);
     }
 
-    fn render_table_with_events(&mut self, ui: &mut egui::Ui, api_client: Option<&ApiClient>) {
+    fn render_table_with_events(
+        &mut self,
+        ui: &mut egui::Ui,
+        api_client: Option<&ApiClient>,
+        permissions: Option<&serde_json::Value>,
+    ) {
         let templates_clone = self.templates.clone();
         let prepared_data = self.table_renderer.prepare_json_data(&templates_clone);
 
@@ -271,6 +279,7 @@ impl LabelTemplatesView {
         let mut temp_handler = TempTemplatesEventHandler {
             api_client,
             deferred_actions: &mut deferred_actions,
+            permissions,
         };
 
         self.table_renderer
@@ -544,60 +553,69 @@ struct TempTemplatesEventHandler<'a> {
     #[allow(dead_code)]
     api_client: Option<&'a ApiClient>,
     deferred_actions: &'a mut Vec<DeferredAction>,
+    permissions: Option<&'a serde_json::Value>,
 }
 
 impl<'a> TableEventHandler<Value> for TempTemplatesEventHandler<'a> {
     fn on_double_click(&mut self, item: &Value, _row_index: usize) {
-        log::info!(
-            "Double-click detected on template: {:?}",
-            item.get("template_name")
-        );
-        self.deferred_actions
-            .push(DeferredAction::DoubleClick(item.clone()));
-    }
-
-    fn on_context_menu(&mut self, ui: &mut egui::Ui, item: &Value, _row_index: usize) {
-        if ui
-            .button(format!("{} Edit Template", egui_phosphor::regular::PENCIL))
-            .clicked()
-        {
+        if RibbonUI::check_permission(self.permissions, "edit_label_template") {
             log::info!(
-                "Context menu edit clicked for template: {:?}",
+                "Double-click detected on template: {:?}",
                 item.get("template_name")
             );
             self.deferred_actions
-                .push(DeferredAction::ContextEdit(item.clone()));
-            ui.close();
+                .push(DeferredAction::DoubleClick(item.clone()));
         }
+    }
 
-        ui.separator();
+    fn on_context_menu(&mut self, ui: &mut egui::Ui, item: &Value, _row_index: usize) {
+        if RibbonUI::check_permission(self.permissions, "edit_label_template") {
+            if ui
+                .button(format!("{} Edit Template", egui_phosphor::regular::PENCIL))
+                .clicked()
+            {
+                log::info!(
+                    "Context menu edit clicked for template: {:?}",
+                    item.get("template_name")
+                );
+                self.deferred_actions
+                    .push(DeferredAction::ContextEdit(item.clone()));
+                ui.close();
+            }
 
-        if ui
-            .button(format!("{} Clone Template", egui_phosphor::regular::COPY))
-            .clicked()
-        {
-            log::info!(
-                "Context menu clone clicked for template: {:?}",
-                item.get("template_name")
-            );
-            self.deferred_actions
-                .push(DeferredAction::ContextClone(item.clone()));
-            ui.close();
+            ui.separator();
         }
 
-        ui.separator();
+        if RibbonUI::check_permission(self.permissions, "create_label_template") {
+            if ui
+                .button(format!("{} Clone Template", egui_phosphor::regular::COPY))
+                .clicked()
+            {
+                log::info!(
+                    "Context menu clone clicked for template: {:?}",
+                    item.get("template_name")
+                );
+                self.deferred_actions
+                    .push(DeferredAction::ContextClone(item.clone()));
+                ui.close();
+            }
 
-        if ui
-            .button(format!("{} Delete Template", egui_phosphor::regular::TRASH))
-            .clicked()
-        {
-            log::info!(
-                "Context menu delete clicked for template: {:?}",
-                item.get("template_name")
-            );
-            self.deferred_actions
-                .push(DeferredAction::ContextDelete(item.clone()));
-            ui.close();
+            ui.separator();
+        }
+
+        if RibbonUI::check_permission(self.permissions, "delete_label_template") {
+            if ui
+                .button(format!("{} Delete Template", egui_phosphor::regular::TRASH))
+                .clicked()
+            {
+                log::info!(
+                    "Context menu delete clicked for template: {:?}",
+                    item.get("template_name")
+                );
+                self.deferred_actions
+                    .push(DeferredAction::ContextDelete(item.clone()));
+                ui.close();
+            }
         }
     }
 

+ 11 - 5
src/ui/login.rs

@@ -238,11 +238,17 @@ impl LoginScreen {
                                 response.success
                             );
                             if response.success {
-                                log::info!(
-                                    "Login successfulf for user: {}",
-                                    response.user.username
-                                );
-                                Ok((server_url, response))
+                                if let Some(user) = &response.user {
+                                    log::info!(
+                                        "Login successfulf for user: {}",
+                                        user.username
+                                    );
+                                    Ok((server_url, response))
+                                } else {
+                                    let error = "Login successful but no user data returned".to_string();
+                                    log::error!("{}", error);
+                                    Err(error)
+                                }
                             } else {
                                 let error = "Login failed gay credentials".to_string();
                                 log::error!("{}", error);

+ 68 - 50
src/ui/printers.rs

@@ -405,6 +405,7 @@ impl PrintersView {
         api_client: Option<&ApiClient>,
         ribbon_ui: Option<&mut RibbonUI>,
         session_manager: &std::sync::Arc<tokio::sync::Mutex<crate::session::SessionManager>>,
+        permissions: Option<&serde_json::Value>,
     ) {
         self.ensure_loaded(api_client);
 
@@ -426,14 +427,16 @@ impl PrintersView {
                 .copied()
                 .unwrap_or(false)
             {
-                // Provide default values - printer_settings will get plugin-specific template
-                let default_data = serde_json::json!({
-                    "printer_settings": "{}",
-                    "log": true,
-                    "can_be_used_for_reports": false,
-                    "min_powerlevel_to_use": "0"
-                });
-                self.add_dialog.open(&default_data);
+                if RibbonUI::check_permission(permissions, "create_printer") {
+                    // Provide default values - printer_settings will get plugin-specific template
+                    let default_data = serde_json::json!({
+                        "printer_settings": "{}",
+                        "log": true,
+                        "can_be_used_for_reports": false,
+                        "min_powerlevel_to_use": "0"
+                    });
+                    self.add_dialog.open(&default_data);
+                }
             }
             if ribbon
                 .checkboxes
@@ -509,7 +512,7 @@ impl PrintersView {
         }
 
         // Render table with event handling
-        self.render_table_with_events(ui, api_client);
+        self.render_table_with_events(ui, api_client, permissions);
 
         // Handle dialogs
         self.handle_dialogs(ui, api_client);
@@ -548,7 +551,12 @@ impl PrintersView {
             .insert("_printers_list".to_string(), printers_json);
     }
 
-    fn render_table_with_events(&mut self, ui: &mut egui::Ui, api_client: Option<&ApiClient>) {
+    fn render_table_with_events(
+        &mut self,
+        ui: &mut egui::Ui,
+        api_client: Option<&ApiClient>,
+        permissions: Option<&serde_json::Value>,
+    ) {
         let printers_clone = self.printers.clone();
         let prepared_data = self.table_renderer.prepare_json_data(&printers_clone);
 
@@ -556,6 +564,7 @@ impl PrintersView {
         let mut temp_handler = TempPrintersEventHandler {
             api_client,
             deferred_actions: &mut deferred_actions,
+            permissions,
         };
 
         self.table_renderer
@@ -880,60 +889,69 @@ struct TempPrintersEventHandler<'a> {
     #[allow(dead_code)]
     api_client: Option<&'a ApiClient>,
     deferred_actions: &'a mut Vec<DeferredAction>,
+    permissions: Option<&'a serde_json::Value>,
 }
 
 impl<'a> TableEventHandler<Value> for TempPrintersEventHandler<'a> {
     fn on_double_click(&mut self, item: &Value, _row_index: usize) {
-        log::info!(
-            "Double-click detected on printer: {:?}",
-            item.get("printer_name")
-        );
-        self.deferred_actions
-            .push(DeferredAction::DoubleClick(item.clone()));
-    }
-
-    fn on_context_menu(&mut self, ui: &mut egui::Ui, item: &Value, _row_index: usize) {
-        if ui
-            .button(format!("{} Edit Printer", egui_phosphor::regular::PENCIL))
-            .clicked()
-        {
+        if RibbonUI::check_permission(self.permissions, "edit_printer") {
             log::info!(
-                "Context menu edit clicked for printer: {:?}",
+                "Double-click detected on printer: {:?}",
                 item.get("printer_name")
             );
             self.deferred_actions
-                .push(DeferredAction::ContextEdit(item.clone()));
-            ui.close();
+                .push(DeferredAction::DoubleClick(item.clone()));
         }
+    }
 
-        ui.separator();
+    fn on_context_menu(&mut self, ui: &mut egui::Ui, item: &Value, _row_index: usize) {
+        if RibbonUI::check_permission(self.permissions, "edit_printer") {
+            if ui
+                .button(format!("{} Edit Printer", egui_phosphor::regular::PENCIL))
+                .clicked()
+            {
+                log::info!(
+                    "Context menu edit clicked for printer: {:?}",
+                    item.get("printer_name")
+                );
+                self.deferred_actions
+                    .push(DeferredAction::ContextEdit(item.clone()));
+                ui.close();
+            }
 
-        if ui
-            .button(format!("{} Clone Printer", egui_phosphor::regular::COPY))
-            .clicked()
-        {
-            log::info!(
-                "Context menu clone clicked for printer: {:?}",
-                item.get("printer_name")
-            );
-            self.deferred_actions
-                .push(DeferredAction::ContextClone(item.clone()));
-            ui.close();
+            ui.separator();
         }
 
-        ui.separator();
+        if RibbonUI::check_permission(self.permissions, "create_printer") {
+            if ui
+                .button(format!("{} Clone Printer", egui_phosphor::regular::COPY))
+                .clicked()
+            {
+                log::info!(
+                    "Context menu clone clicked for printer: {:?}",
+                    item.get("printer_name")
+                );
+                self.deferred_actions
+                    .push(DeferredAction::ContextClone(item.clone()));
+                ui.close();
+            }
 
-        if ui
-            .button(format!("{} Delete Printer", egui_phosphor::regular::TRASH))
-            .clicked()
-        {
-            log::info!(
-                "Context menu delete clicked for printer: {:?}",
-                item.get("printer_name")
-            );
-            self.deferred_actions
-                .push(DeferredAction::ContextDelete(item.clone()));
-            ui.close();
+            ui.separator();
+        }
+
+        if RibbonUI::check_permission(self.permissions, "delete_printer") {
+            if ui
+                .button(format!("{} Delete Printer", egui_phosphor::regular::TRASH))
+                .clicked()
+            {
+                log::info!(
+                    "Context menu delete clicked for printer: {:?}",
+                    item.get("printer_name")
+                );
+                self.deferred_actions
+                    .push(DeferredAction::ContextDelete(item.clone()));
+                ui.close();
+            }
         }
     }
 

+ 367 - 191
src/ui/ribbon.rs

@@ -37,7 +37,102 @@ impl RibbonUI {
         Some(self.active_tab.clone())
     }
 
-    pub fn show(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) -> Option<String> {
+    pub fn check_permission(permissions: Option<&serde_json::Value>, key: &str) -> bool {
+        if let Some(perms) = permissions {
+            // If permissions is a boolean map (legacy/simple), handle it
+            if let Some(val) = perms.get(key) {
+                if let Some(b) = val.as_bool() {
+                    return b;
+                }
+            }
+
+            // If permissions is a table map (RBAC), map key to table permission
+            // Helper to check table permission
+            let check_table = |table: &str, require_write: bool| -> bool {
+                if let Some(p_val) = perms.get(table) {
+                    if let Some(p_str) = p_val.as_str() {
+                        if require_write {
+                            return p_str == "w" || p_str == "rw";
+                        } else {
+                            return p_str == "r" || p_str == "rw";
+                        }
+                    }
+                }
+                false
+            };
+
+            match key {
+                // Views
+                "view_dashboard" => true, // Everyone can see dashboard
+                "view_inventory" => check_table("assets", false),
+                "view_categories" => check_table("categories", false),
+                "view_zones" => check_table("zones", false),
+                "view_borrowing" => check_table("assets", false), // Needs assets read
+                "view_audits" => check_table("audit_tasks", false) || check_table("physical_audits", false),
+                "view_suppliers" => check_table("suppliers", false),
+                "view_issues" => check_table("issue_tracker", false),
+                "view_printers" => check_table("printer_settings", false),
+                "view_label_templates" => check_table("label_templates", false),
+                "view_item_templates" => check_table("templates", false),
+
+                // Inventory Actions
+                "create_asset" => check_table("assets", true),
+                "edit_asset" => check_table("assets", true),
+                "delete_asset" => check_table("assets", true),
+                
+                // Borrowing Actions
+                "checkout_asset" => check_table("assets", true), // Modifies asset status
+                "return_asset" => check_table("assets", true),
+                "create_borrower" => check_table("borrowers", true),
+                "edit_borrower" => check_table("borrowers", true),
+                "delete_borrower" => check_table("borrowers", true),
+
+                // Audit Actions
+                "create_audit" => check_table("physical_audits", true),
+                
+                // Supplier Actions
+                "create_supplier" => check_table("suppliers", true),
+                "edit_supplier" => check_table("suppliers", true),
+                "delete_supplier" => check_table("suppliers", true),
+                
+                // Issue Actions
+                "report_issue" => check_table("issue_tracker", true),
+                "view_issue" => check_table("issue_tracker", false),
+                "edit_issue" => check_table("issue_tracker", true),
+                "delete_issue" => check_table("issue_tracker", true),
+                "resolve_issue" => check_table("issue_tracker", true),
+
+                // Category Actions
+                "create_category" => check_table("categories", true),
+                "edit_category" => check_table("categories", true),
+                "delete_category" => check_table("categories", true),
+
+                // Zone Actions
+                "create_zone" => check_table("zones", true),
+                "edit_zone" => check_table("zones", true),
+                "delete_zone" => check_table("zones", true),
+                
+                // Printer Actions
+                "create_printer" => check_table("printer_settings", true),
+                
+                // Label Template Actions
+                "create_label_template" => check_table("label_templates", true),
+
+                // Template Actions
+                "create_template" => check_table("templates", true),
+                "edit_template" => check_table("templates", true),
+                "delete_template" => check_table("templates", true),
+
+                // Fallback
+                _ => false,
+            }
+        } else {
+            // If no permissions provided (legacy/dev), allow everything
+            true
+        }
+    }
+
+    pub fn show(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui, permissions: Option<&serde_json::Value>) -> Option<String> {
         // Clear one-shot trigger flags from previous frame so clicks only fire once
         // NOTE: inventory_filter_changed and templates_filter_changed are NOT cleared here - they're cleared by their views after processing
         for key in [
@@ -64,27 +159,30 @@ impl RibbonUI {
         ui.vertical(|ui| {
             // Tab headers row
             ui.horizontal(|ui| {
-                let tabs = vec![
-                    "Dashboard",
-                    "Inventory",
-                    "Categories",
-                    "Zones",
-                    "Borrowing",
-                    "Audits",
-                    "Suppliers",
-                    "Issues",
-                    "Printers",
-                    "Label Templates",
-                    "Item Templates",
+                let all_tabs = vec![
+                    ("Dashboard", "view_dashboard"),
+                    ("Inventory", "view_inventory"),
+                    ("Categories", "view_categories"),
+                    ("Zones", "view_zones"),
+                    ("Borrowing", "view_borrowing"),
+                    ("Audits", "view_audits"),
+                    ("Suppliers", "view_suppliers"),
+                    ("Issues", "view_issues"),
+                    ("Printers", "view_printers"),
+                    ("Label Templates", "view_label_templates"),
+                    ("Item Templates", "view_item_templates"),
                 ];
-                for tab in &tabs {
-                    if ui.selectable_label(self.active_tab == *tab, *tab).clicked() {
-                        self.active_tab = tab.to_string();
-                        // Update filter columns based on the active tab
-                        match *tab {
-                            "Zones" => self.filter_builder.set_columns_for_context("zones"),
-                            "Inventory" => self.filter_builder.set_columns_for_context("assets"),
-                            _ => {}
+                
+                for (tab, perm) in &all_tabs {
+                    if Self::check_permission(permissions, perm) {
+                        if ui.selectable_label(self.active_tab == *tab, *tab).clicked() {
+                            self.active_tab = tab.to_string();
+                            // Update filter columns based on the active tab
+                            match *tab {
+                                "Zones" => self.filter_builder.set_columns_for_context("zones"),
+                                "Inventory" => self.filter_builder.set_columns_for_context("assets"),
+                                _ => {}
+                            }
                         }
                     }
                 }
@@ -100,17 +198,17 @@ impl RibbonUI {
                 |ui| {
                     egui::ScrollArea::horizontal().show(ui, |ui| {
                         ui.horizontal(|ui| match self.active_tab.as_str() {
-                            "Dashboard" => self.show_dashboard_tab(ui, ribbon_height),
-                            "Inventory" => self.show_inventory_tab(ui, ribbon_height),
-                            "Categories" => self.show_categories_tab(ui, ribbon_height),
-                            "Zones" => self.show_zones_tab(ui, ribbon_height),
-                            "Borrowing" => self.show_borrowing_tab(ui, ribbon_height),
-                            "Audits" => self.show_audits_tab(ui, ribbon_height),
-                            "Item Templates" => self.show_templates_tab(ui, ribbon_height),
-                            "Suppliers" => self.show_suppliers_tab(ui, ribbon_height),
-                            "Issues" => self.show_issues_tab(ui, ribbon_height),
-                            "Printers" => self.show_printers_tab(ui, ribbon_height),
-                            "Label Templates" => self.show_label_templates_tab(ui, ribbon_height),
+                            "Dashboard" => self.show_dashboard_tab(ui, ribbon_height, permissions),
+                            "Inventory" => self.show_inventory_tab(ui, ribbon_height, permissions),
+                            "Categories" => self.show_categories_tab(ui, ribbon_height, permissions),
+                            "Zones" => self.show_zones_tab(ui, ribbon_height, permissions),
+                            "Borrowing" => self.show_borrowing_tab(ui, ribbon_height, permissions),
+                            "Audits" => self.show_audits_tab(ui, ribbon_height, permissions),
+                            "Item Templates" => self.show_templates_tab(ui, ribbon_height, permissions),
+                            "Suppliers" => self.show_suppliers_tab(ui, ribbon_height, permissions),
+                            "Issues" => self.show_issues_tab(ui, ribbon_height, permissions),
+                            "Printers" => self.show_printers_tab(ui, ribbon_height, permissions),
+                            "Label Templates" => self.show_label_templates_tab(ui, ribbon_height, permissions),
                             _ => {}
                         });
                     });
@@ -172,7 +270,7 @@ impl RibbonUI {
             });
     }
 
-    fn show_dashboard_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32) {
+    fn show_dashboard_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32, _permissions: Option<&serde_json::Value>) {
         ui.group(|ui| {
             ui.set_height(ribbon_height);
             ui.vertical(|ui| {
@@ -207,7 +305,7 @@ impl RibbonUI {
         });
     }
 
-    fn show_inventory_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32) {
+    fn show_inventory_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32, permissions: Option<&serde_json::Value>) {
         ui.group(|ui| {
             ui.set_height(ribbon_height);
             ui.vertical(|ui| {
@@ -242,42 +340,54 @@ impl RibbonUI {
                                 let idx = col * rows + row;
                                 let w = col_widths[col];
                                 let button = egui::Button::new(labels[idx]);
-                                let clicked = ui.add_sized([w, row_height], button).clicked();
-                                if clicked {
-                                    // If user holds Alt while clicking Edit, trigger Advanced Edit instead of Easy
-                                    let alt_held = ui.input(|i| i.modifiers.alt);
-                                    match idx {
-                                        0 => {
-                                            self.checkboxes
-                                                .insert("inventory_action_add".to_string(), true);
-                                        }
-                                        1 => {
-                                            if alt_held {
+                                
+                                let enabled = match idx {
+                                    0 => Self::check_permission(permissions, "create_asset"),
+                                    1 => Self::check_permission(permissions, "edit_asset"),
+                                    2 => Self::check_permission(permissions, "delete_asset"),
+                                    _ => true,
+                                };
+
+                                if enabled {
+                                    let clicked = ui.add_sized([w, row_height], button).clicked();
+                                    if clicked {
+                                        // If user holds Alt while clicking Edit, trigger Advanced Edit instead of Easy
+                                        let alt_held = ui.input(|i| i.modifiers.alt);
+                                        match idx {
+                                            0 => {
+                                                self.checkboxes
+                                                    .insert("inventory_action_add".to_string(), true);
+                                            }
+                                            1 => {
+                                                if alt_held {
+                                                    self.checkboxes.insert(
+                                                        "inventory_action_edit_adv".to_string(),
+                                                        true,
+                                                    );
+                                                } else {
+                                                    self.checkboxes.insert(
+                                                        "inventory_action_edit_easy".to_string(),
+                                                        true,
+                                                    );
+                                                }
+                                            }
+                                            2 => {
                                                 self.checkboxes.insert(
-                                                    "inventory_action_edit_adv".to_string(),
+                                                    "inventory_action_delete".to_string(),
                                                     true,
                                                 );
-                                            } else {
+                                            }
+                                            3 => {
                                                 self.checkboxes.insert(
-                                                    "inventory_action_edit_easy".to_string(),
+                                                    "inventory_action_print_label".to_string(),
                                                     true,
                                                 );
                                             }
+                                            _ => {}
                                         }
-                                        2 => {
-                                            self.checkboxes.insert(
-                                                "inventory_action_delete".to_string(),
-                                                true,
-                                            );
-                                        }
-                                        3 => {
-                                            self.checkboxes.insert(
-                                                "inventory_action_print_label".to_string(),
-                                                true,
-                                            );
-                                        }
-                                        _ => {}
                                     }
+                                } else {
+                                    ui.allocate_space(egui::vec2(w, row_height));
                                 }
                             }
                             ui.end_row();
@@ -341,51 +451,53 @@ impl RibbonUI {
                         let w2 = 8.0 * "Add from...".len() as f32 + pad_x * 2.0;
                         let w = w1.max(w2).max(130.0);
 
-                        if ui
-                            .add_sized([w, 24.0], egui::Button::new("Inventarize Room"))
-                            .clicked()
-                        {
-                            self.checkboxes
-                                .insert("inventory_quick_inventarize_room".to_string(), true);
-                        }
+                        if Self::check_permission(permissions, "create_asset") {
+                            if ui
+                                .add_sized([w, 24.0], egui::Button::new("Inventarize Room"))
+                                .clicked()
+                            {
+                                self.checkboxes
+                                    .insert("inventory_quick_inventarize_room".to_string(), true);
+                            }
 
-                        // Use a fixed-width ComboBox as a dropdown to ensure equal width
-                        egui::ComboBox::from_id_salt("inventory_add_from_combo")
-                            .width(w)
-                            .selected_text("Add from...")
-                            .show_ui(ui, |ui| {
-                                if ui.selectable_label(false, "Add from Template").clicked() {
-                                    self.checkboxes.insert(
-                                        "inventory_add_from_template_single".to_string(),
-                                        true,
-                                    );
-                                }
-                                if ui
-                                    .selectable_label(false, "Add Multiple from Template")
-                                    .clicked()
-                                {
-                                    self.checkboxes.insert(
-                                        "inventory_add_from_template_multiple".to_string(),
-                                        true,
-                                    );
-                                }
-                                if ui
-                                    .selectable_label(false, "Add using Another Item")
-                                    .clicked()
-                                {
-                                    self.checkboxes
-                                        .insert("inventory_add_from_item_single".to_string(), true);
-                                }
-                                if ui
-                                    .selectable_label(false, "Add Multiple from Another Item")
-                                    .clicked()
-                                {
-                                    self.checkboxes.insert(
-                                        "inventory_add_from_item_multiple".to_string(),
-                                        true,
-                                    );
-                                }
-                            });
+                            // Use a fixed-width ComboBox as a dropdown to ensure equal width
+                            egui::ComboBox::from_id_salt("inventory_add_from_combo")
+                                .width(w)
+                                .selected_text("Add from...")
+                                .show_ui(ui, |ui| {
+                                    if ui.selectable_label(false, "Add from Template").clicked() {
+                                        self.checkboxes.insert(
+                                            "inventory_add_from_template_single".to_string(),
+                                            true,
+                                        );
+                                    }
+                                    if ui
+                                        .selectable_label(false, "Add Multiple from Template")
+                                        .clicked()
+                                    {
+                                        self.checkboxes.insert(
+                                            "inventory_add_from_template_multiple".to_string(),
+                                            true,
+                                        );
+                                    }
+                                    if ui
+                                        .selectable_label(false, "Add using Another Item")
+                                        .clicked()
+                                    {
+                                        self.checkboxes
+                                            .insert("inventory_add_from_item_single".to_string(), true);
+                                    }
+                                    if ui
+                                        .selectable_label(false, "Add Multiple from Another Item")
+                                        .clicked()
+                                    {
+                                        self.checkboxes.insert(
+                                            "inventory_add_from_item_multiple".to_string(),
+                                            true,
+                                        );
+                                    }
+                                });
+                        }
                     });
             });
         });
@@ -481,7 +593,7 @@ impl RibbonUI {
         });
     }
 
-    fn show_categories_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32) {
+    fn show_categories_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32, permissions: Option<&serde_json::Value>) {
         // Clear one-shot action triggers from previous frame
         for key in [
             "categories_refresh",
@@ -497,25 +609,35 @@ impl RibbonUI {
             ui.vertical(|ui| {
                 ui.label("Actions");
                 // Match Inventory layout: [Add, Edit, Delete, Refresh] (Refresh where Inventory has Print)
-                let items = vec![
-                    (
+                let mut items = Vec::new();
+                
+                if Self::check_permission(permissions, "create_category") {
+                    items.push((
                         format!("{} {}", icons::PLUS, "Add Category"),
                         "categories_add",
-                    ),
-                    (
+                    ));
+                }
+                if Self::check_permission(permissions, "edit_category") {
+                    items.push((
                         format!("{} {}", icons::PENCIL, "Edit Category"),
                         "categories_edit",
-                    ),
-                    (
+                    ));
+                }
+                if Self::check_permission(permissions, "delete_category") {
+                    items.push((
                         format!("{} {}", icons::TRASH, "Delete Category"),
                         "categories_delete",
-                    ),
-                    (
-                        format!("{} {}", icons::ARROWS_CLOCKWISE, "Refresh"),
-                        "categories_refresh",
-                    ),
-                ];
-                self.render_actions_grid_with_keys(ui, "categories_actions_grid", &items);
+                    ));
+                }
+                
+                items.push((
+                    format!("{} {}", icons::ARROWS_CLOCKWISE, "Refresh"),
+                    "categories_refresh",
+                ));
+
+                // Convert to slice of references for the helper
+                let items_refs: Vec<(String, &str)> = items.iter().map(|(s, k)| (s.clone(), *k)).collect();
+                self.render_actions_grid_with_keys(ui, "categories_actions_grid", &items_refs);
             });
         });
 
@@ -536,7 +658,7 @@ impl RibbonUI {
         });
     }
 
-    fn show_zones_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32) {
+    fn show_zones_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32, permissions: Option<&serde_json::Value>) {
         // Clear one-shot action triggers from previous frame
         for key in [
             "zones_action_add",
@@ -550,17 +672,23 @@ impl RibbonUI {
             ui.set_height(ribbon_height);
             ui.vertical(|ui| {
                 ui.label("Actions");
-                let items = vec![
-                    (
+                let mut items = Vec::new();
+                
+                if Self::check_permission(permissions, "create_zone") {
+                    items.push((
                         format!("{} {}", icons::PLUS, "Add Zone"),
                         "zones_action_add",
-                    ),
-                    (
+                    ));
+                }
+                if Self::check_permission(permissions, "edit_zone") {
+                    items.push((
                         format!("{} {}", icons::PENCIL, "Edit Zone"),
                         "zones_action_edit",
-                    ),
-                ];
-                self.render_actions_grid_with_keys(ui, "zones_actions_grid", &items);
+                    ));
+                }
+                
+                let items_refs: Vec<(String, &str)> = items.iter().map(|(s, k)| (s.clone(), *k)).collect();
+                self.render_actions_grid_with_keys(ui, "zones_actions_grid", &items_refs);
             });
         });
         ui.group(|ui| {
@@ -612,7 +740,7 @@ impl RibbonUI {
         });
     }
 
-    fn show_borrowing_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32) {
+    fn show_borrowing_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32, permissions: Option<&serde_json::Value>) {
         // Clear one-shot action triggers from previous frame
         for key in [
             "borrowing_action_checkout",
@@ -627,25 +755,34 @@ impl RibbonUI {
             ui.set_height(ribbon_height);
             ui.vertical(|ui| {
                 ui.label("Actions");
-                let items = vec![
-                    (
+                let mut items = Vec::new();
+                
+                if Self::check_permission(permissions, "checkout_asset") {
+                    items.push((
                         format!("{} {}", icons::ARROW_LEFT, "Check Out"),
                         "borrowing_action_checkout",
-                    ),
-                    (
+                    ));
+                }
+                if Self::check_permission(permissions, "return_asset") {
+                    items.push((
                         format!("{} {}", icons::ARROW_RIGHT, "Return"),
                         "borrowing_action_return",
-                    ),
-                    (
+                    ));
+                }
+                if Self::check_permission(permissions, "register_borrower") {
+                    items.push((
                         format!("{} {}", icons::PENCIL, "Register"),
                         "borrowing_action_register",
-                    ),
-                    (
-                        format!("{} {}", icons::ARROWS_CLOCKWISE, "Refresh"),
-                        "borrowing_action_refresh",
-                    ),
-                ];
-                self.render_actions_grid_with_keys(ui, "borrowing_actions_grid", &items);
+                    ));
+                }
+                
+                items.push((
+                    format!("{} {}", icons::ARROWS_CLOCKWISE, "Refresh"),
+                    "borrowing_action_refresh",
+                ));
+                
+                let items_refs: Vec<(String, &str)> = items.iter().map(|(s, k)| (s.clone(), *k)).collect();
+                self.render_actions_grid_with_keys(ui, "borrowing_actions_grid", &items_refs);
             });
         });
         ui.group(|ui| {
@@ -668,22 +805,30 @@ impl RibbonUI {
         });
     }
 
-    fn show_audits_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32) {
+    fn show_audits_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32, permissions: Option<&serde_json::Value>) {
         ui.group(|ui| {
             ui.set_height(ribbon_height);
             ui.vertical(|ui| {
                 ui.label("Actions");
-                let items = vec![
-                    ("New Audit".to_string(), "audits_action_new"),
-                    ("View Audit".to_string(), "audits_action_view"),
-                    ("Export Report".to_string(), "audits_action_export"),
-                ];
-                self.render_actions_grid_with_keys(ui, "audits_actions_grid", &items);
+                let mut items = Vec::new();
+                
+                if Self::check_permission(permissions, "create_audit") {
+                    items.push(("New Audit".to_string(), "audits_action_new"));
+                }
+                if Self::check_permission(permissions, "view_audit") {
+                    items.push(("View Audit".to_string(), "audits_action_view"));
+                }
+                if Self::check_permission(permissions, "export_audit") {
+                    items.push(("Export Report".to_string(), "audits_action_export"));
+                }
+                
+                let items_refs: Vec<(String, &str)> = items.iter().map(|(s, k)| (s.clone(), *k)).collect();
+                self.render_actions_grid_with_keys(ui, "audits_actions_grid", &items_refs);
             });
         });
     }
 
-    fn show_templates_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32) {
+    fn show_templates_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32, permissions: Option<&serde_json::Value>) {
         // Clear one-shot action triggers from previous frame
         for key in [
             "templates_action_new",
@@ -697,17 +842,23 @@ impl RibbonUI {
             ui.set_height(ribbon_height);
             ui.vertical(|ui| {
                 ui.label("Actions");
-                let items = vec![
-                    (
+                let mut items = Vec::new();
+                
+                if Self::check_permission(permissions, "create_template") {
+                    items.push((
                         format!("{} {}", icons::PLUS, "Add Template"),
                         "templates_action_new",
-                    ),
-                    (
+                    ));
+                }
+                if Self::check_permission(permissions, "edit_template") {
+                    items.push((
                         format!("{} {}", icons::PENCIL, "Edit Template"),
                         "templates_action_edit",
-                    ),
-                ];
-                self.render_actions_grid_with_keys(ui, "templates_actions_grid", &items);
+                    ));
+                }
+                
+                let items_refs: Vec<(String, &str)> = items.iter().map(|(s, k)| (s.clone(), *k)).collect();
+                self.render_actions_grid_with_keys(ui, "templates_actions_grid", &items_refs);
             });
         });
         // Import/Export removed for now
@@ -765,17 +916,25 @@ impl RibbonUI {
         });
     }
 
-    fn show_issues_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32) {
+    fn show_issues_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32, permissions: Option<&serde_json::Value>) {
         ui.group(|ui| {
             ui.set_height(ribbon_height);
             ui.vertical(|ui| {
                 ui.label("Actions");
-                let items = vec![
-                    ("Report Issue".to_string(), "issues_action_report"),
-                    ("View Issue".to_string(), "issues_action_view"),
-                    ("Resolve Issue".to_string(), "issues_action_resolve"),
-                ];
-                self.render_actions_grid_with_keys(ui, "issues_actions_grid", &items);
+                let mut items = Vec::new();
+                
+                if Self::check_permission(permissions, "report_issue") {
+                    items.push(("Report Issue".to_string(), "issues_action_report"));
+                }
+                if Self::check_permission(permissions, "view_issue") {
+                    items.push(("View Issue".to_string(), "issues_action_view"));
+                }
+                if Self::check_permission(permissions, "resolve_issue") {
+                    items.push(("Resolve Issue".to_string(), "issues_action_resolve"));
+                }
+                
+                let items_refs: Vec<(String, &str)> = items.iter().map(|(s, k)| (s.clone(), *k)).collect();
+                self.render_actions_grid_with_keys(ui, "issues_actions_grid", &items_refs);
             });
         });
         ui.group(|ui| {
@@ -817,7 +976,7 @@ impl RibbonUI {
         });
     }
 
-    fn show_suppliers_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32) {
+    fn show_suppliers_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32, permissions: Option<&serde_json::Value>) {
         for key in [
             "suppliers_action_new",
             "suppliers_action_edit",
@@ -830,17 +989,23 @@ impl RibbonUI {
             ui.set_height(ribbon_height);
             ui.vertical(|ui| {
                 ui.label("Actions");
-                let items = vec![
-                    (
+                let mut items = Vec::new();
+                
+                if Self::check_permission(permissions, "create_supplier") {
+                    items.push((
                         format!("{} {}", icons::PLUS, "New Supplier"),
                         "suppliers_action_new",
-                    ),
-                    (
+                    ));
+                }
+                if Self::check_permission(permissions, "edit_supplier") {
+                    items.push((
                         format!("{} {}", icons::PENCIL, "Edit Supplier"),
                         "suppliers_action_edit",
-                    ),
-                ];
-                self.render_actions_grid_with_keys(ui, "suppliers_actions_grid", &items);
+                    ));
+                }
+                
+                let items_refs: Vec<(String, &str)> = items.iter().map(|(s, k)| (s.clone(), *k)).collect();
+                self.render_actions_grid_with_keys(ui, "suppliers_actions_grid", &items_refs);
             });
         });
         ui.group(|ui| {
@@ -860,7 +1025,7 @@ impl RibbonUI {
         });
     }
 
-    pub fn show_printers_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32) {
+    pub fn show_printers_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32, permissions: Option<&serde_json::Value>) {
         // Clear one-shot action triggers from previous frame
         for key in [
             "printers_action_add",
@@ -875,17 +1040,22 @@ impl RibbonUI {
             ui.set_height(ribbon_height);
             ui.vertical(|ui| {
                 ui.label("Actions");
-                let items = vec![
-                    (
+                let mut items = Vec::new();
+                
+                if Self::check_permission(permissions, "create_printer") {
+                    items.push((
                         format!("{} {}", icons::PLUS, "Add Printer"),
                         "printers_action_add",
-                    ),
-                    (
-                        format!("{} {}", icons::ARROWS_CLOCKWISE, "Refresh"),
-                        "printers_action_refresh",
-                    ),
-                ];
-                self.render_actions_grid_with_keys(ui, "printers_actions_grid", &items);
+                    ));
+                }
+                
+                items.push((
+                    format!("{} {}", icons::ARROWS_CLOCKWISE, "Refresh"),
+                    "printers_action_refresh",
+                ));
+                
+                let items_refs: Vec<(String, &str)> = items.iter().map(|(s, k)| (s.clone(), *k)).collect();
+                self.render_actions_grid_with_keys(ui, "printers_actions_grid", &items_refs);
             });
         });
         ui.group(|ui| {
@@ -1011,7 +1181,7 @@ impl RibbonUI {
         });
     }
 
-    fn show_label_templates_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32) {
+    fn show_label_templates_tab(&mut self, ui: &mut egui::Ui, ribbon_height: f32, permissions: Option<&serde_json::Value>) {
         // Clear one-shot action triggers from previous frame
         for key in ["labels_action_add", "labels_action_refresh"] {
             self.checkboxes.insert(key.to_string(), false);
@@ -1021,19 +1191,25 @@ impl RibbonUI {
             ui.set_height(ribbon_height);
             ui.vertical(|ui| {
                 ui.label("Actions");
-                let items = vec![
-                    (
+                let mut items = Vec::new();
+                
+                if Self::check_permission(permissions, "create_label_template") {
+                    items.push((
                         format!("{} {}", icons::PLUS, "Add Template"),
                         "labels_action_add",
-                    ),
-                    (
-                        format!("{} {}", icons::ARROWS_CLOCKWISE, "Refresh"),
-                        "labels_action_refresh",
-                    ),
-                ];
-                self.render_actions_grid_with_keys(ui, "labels_actions_grid", &items);
+                    ));
+                }
+                
+                items.push((
+                    format!("{} {}", icons::ARROWS_CLOCKWISE, "Refresh"),
+                    "labels_action_refresh",
+                ));
+                
+                let items_refs: Vec<(String, &str)> = items.iter().map(|(s, k)| (s.clone(), *k)).collect();
+                self.render_actions_grid_with_keys(ui, "labels_actions_grid", &items_refs);
             });
         });
+
         ui.group(|ui| {
             ui.set_height(ribbon_height);
             ui.vertical(|ui| {

+ 17 - 10
src/ui/suppliers.rs

@@ -238,6 +238,7 @@ impl SuppliersView {
         ui: &mut egui::Ui,
         api_client: Option<&ApiClient>,
         ribbon: Option<&mut RibbonUI>,
+        permissions: Option<&serde_json::Value>,
     ) -> Vec<String> {
         let mut flags_to_clear = Vec::new();
 
@@ -277,7 +278,9 @@ impl SuppliersView {
                 .copied()
                 .unwrap_or(false)
             {
-                self.add_dialog.open_new(None);
+                if RibbonUI::check_permission(permissions, "create_supplier") {
+                    self.add_dialog.open_new(None);
+                }
                 flags_to_clear.push("suppliers_action_new".to_string());
             }
 
@@ -287,10 +290,12 @@ impl SuppliersView {
                 .copied()
                 .unwrap_or(false)
             {
-                if let Some(selected) = self.first_selected_supplier() {
-                    self.open_editor_with(&selected);
-                } else {
-                    log::warn!("Ribbon edit triggered but no supplier selected");
+                if RibbonUI::check_permission(permissions, "edit_supplier") {
+                    if let Some(selected) = self.first_selected_supplier() {
+                        self.open_editor_with(&selected);
+                    } else {
+                        log::warn!("Ribbon edit triggered but no supplier selected");
+                    }
                 }
                 flags_to_clear.push("suppliers_action_edit".to_string());
             }
@@ -301,11 +306,13 @@ impl SuppliersView {
                 .copied()
                 .unwrap_or(false)
             {
-                if let Some((name, id)) = self.first_selected_supplier_id_name() {
-                    self.delete_current_id = Some(id);
-                    self.delete_dialog.open(name, id.to_string());
-                } else {
-                    log::warn!("Ribbon delete triggered but no supplier selected");
+                if RibbonUI::check_permission(permissions, "delete_supplier") {
+                    if let Some((name, id)) = self.first_selected_supplier_id_name() {
+                        self.delete_current_id = Some(id);
+                        self.delete_dialog.open(name, id.to_string());
+                    } else {
+                        log::warn!("Ribbon delete triggered but no supplier selected");
+                    }
                 }
                 flags_to_clear.push("suppliers_action_delete".to_string());
             }

+ 51 - 44
src/ui/templates.rs

@@ -433,6 +433,7 @@ impl TemplatesView {
         ui: &mut egui::Ui,
         api_client: Option<&ApiClient>,
         ribbon: Option<&mut crate::ui::ribbon::RibbonUI>,
+        permissions: Option<&serde_json::Value>,
     ) -> Vec<String> {
         let mut flags_to_clear = Vec::new();
 
@@ -535,43 +536,45 @@ impl TemplatesView {
             {
                 flags_to_clear.push("templates_action_new".to_string());
 
-                // Prepare dynamic dropdown fields before opening dialog
-                if let Some(client) = api_client {
-                    self.prepare_template_edit_fields(client);
-                }
+                if crate::ui::ribbon::RibbonUI::check_permission(permissions, "create_template") {
+                    // Prepare dynamic dropdown fields before opening dialog
+                    if let Some(client) = api_client {
+                        self.prepare_template_edit_fields(client);
+                    }
 
-                // Open new template dialog with empty data (comprehensive fields for templates)
-                let empty_template = serde_json::json!({
-                    "id": "",
-                    "template_code": "",
-                    "name": "",
-                    "asset_type": "",
-                    "asset_tag_generation_string": "",
-                    "description": "",
-                    "additional_fields": null,
-                    "additional_fields_json": "{}",
-                    "category_id": "",
-                    "manufacturer": "",
-                    "model": "",
-                    "zone_id": "",
-                    "zone_plus": "",
-                    "status": "",
-                    "price": "",
-                    "warranty_until": "",
-                    "expiry_date": "",
-                    "quantity_total": "",
-                    "quantity_used": "",
-                    "supplier_id": "",
-                    "label_template_id": "",
-                    "audit_task_id": "",
-                    "lendable": false,
-                    "minimum_role_for_lending": "",
-                    "no_scan": "",
-                    "notes": "",
-                    "active": false
-                });
-                self.edit_dialog.title = "Add New Template".to_string();
-                self.open_edit_template_dialog(empty_template);
+                    // Open new template dialog with empty data (comprehensive fields for templates)
+                    let empty_template = serde_json::json!({
+                        "id": "",
+                        "template_code": "",
+                        "name": "",
+                        "asset_type": "",
+                        "asset_tag_generation_string": "",
+                        "description": "",
+                        "additional_fields": null,
+                        "additional_fields_json": "{}",
+                        "category_id": "",
+                        "manufacturer": "",
+                        "model": "",
+                        "zone_id": "",
+                        "zone_plus": "",
+                        "status": "",
+                        "price": "",
+                        "warranty_until": "",
+                        "expiry_date": "",
+                        "quantity_total": "",
+                        "quantity_used": "",
+                        "supplier_id": "",
+                        "label_template_id": "",
+                        "audit_task_id": "",
+                        "lendable": false,
+                        "minimum_role_for_lending": "",
+                        "no_scan": "",
+                        "notes": "",
+                        "active": false
+                    });
+                    self.edit_dialog.title = "Add New Template".to_string();
+                    self.open_edit_template_dialog(empty_template);
+                }
             }
 
             if *ribbon
@@ -580,10 +583,12 @@ impl TemplatesView {
                 .unwrap_or(&false)
             {
                 flags_to_clear.push("templates_action_edit".to_string());
-                // TODO: Implement edit selected template (requires selection tracking)
-                log::info!(
-                    "Edit Template clicked (requires table selection - use double-click for now)"
-                );
+                if crate::ui::ribbon::RibbonUI::check_permission(permissions, "edit_template") {
+                    // TODO: Implement edit selected template (requires selection tracking)
+                    log::info!(
+                        "Edit Template clicked (requires table selection - use double-click for now)"
+                    );
+                }
             }
 
             if *ribbon
@@ -592,10 +597,12 @@ impl TemplatesView {
                 .unwrap_or(&false)
             {
                 flags_to_clear.push("templates_action_delete".to_string());
-                // TODO: Implement delete selected templates (requires selection tracking)
-                log::info!(
-                    "Delete Template clicked (requires table selection - use right-click for now)"
-                );
+                if crate::ui::ribbon::RibbonUI::check_permission(permissions, "delete_template") {
+                    // TODO: Implement delete selected templates (requires selection tracking)
+                    log::info!(
+                        "Delete Template clicked (requires table selection - use right-click for now)"
+                    );
+                }
             }
         }
 

+ 115 - 100
src/ui/zones.rs

@@ -315,6 +315,7 @@ impl ZonesView {
         ui: &mut egui::Ui,
         api_client: Option<&ApiClient>,
         ribbon: &mut crate::ui::ribbon::RibbonUI,
+        permissions: Option<&serde_json::Value>,
     ) {
         self.ensure_loaded(api_client);
 
@@ -352,8 +353,10 @@ impl ZonesView {
             .copied()
             .unwrap_or(false)
         {
-            self.update_editor_dropdowns();
-            self.add_dialog.open_new(None);
+            if crate::ui::ribbon::RibbonUI::check_permission(permissions, "create_zone") {
+                self.update_editor_dropdowns();
+                self.add_dialog.open_new(None);
+            }
         }
         if ribbon
             .checkboxes
@@ -361,8 +364,10 @@ impl ZonesView {
             .copied()
             .unwrap_or(false)
         {
-            // Edit needs a selected zone - will be handled via context menu
-            log::info!("Edit zone clicked - use right-click context menu on a zone");
+            if crate::ui::ribbon::RibbonUI::check_permission(permissions, "edit_zone") {
+                // Edit needs a selected zone - will be handled via context menu
+                log::info!("Edit zone clicked - use right-click context menu on a zone");
+            }
         }
         if ribbon
             .checkboxes
@@ -370,8 +375,10 @@ impl ZonesView {
             .copied()
             .unwrap_or(false)
         {
-            // Remove needs a selected zone - will be handled via context menu
-            log::info!("Remove zone clicked - use right-click context menu on a zone");
+            if crate::ui::ribbon::RibbonUI::check_permission(permissions, "delete_zone") {
+                // Remove needs a selected zone - will be handled via context menu
+                log::info!("Remove zone clicked - use right-click context menu on a zone");
+            }
         }
 
         // Update show_items from ribbon
@@ -503,7 +510,7 @@ impl ZonesView {
                 .auto_shrink([false; 2])
                 .show(ui, |ui| {
                     for root in roots {
-                        self.render_zone_node(ui, root, &children, api_client);
+                        self.render_zone_node(ui, root, &children, api_client, permissions);
                     }
                 });
         }
@@ -561,6 +568,7 @@ impl ZonesView {
         zone: &serde_json::Value,
         children: &HashMap<Option<i32>, Vec<serde_json::Value>>,
         api_client: Option<&ApiClient>,
+        permissions: Option<&serde_json::Value>,
     ) {
         let id = zone.get("id").and_then(|v| v.as_i64()).unwrap_or_default() as i32;
         let code = zone
@@ -597,7 +605,7 @@ impl ZonesView {
             if let Some(kids) = children.get(&Some(id)) {
                 ui.indent(egui::Id::new(("zone_indent", id)), |ui| {
                     for child in kids {
-                        self.render_zone_node(ui, child, children, api_client);
+                        self.render_zone_node(ui, child, children, api_client, permissions);
                     }
                 });
             }
@@ -651,64 +659,69 @@ impl ZonesView {
 
         // Add context menu to header for editing
         resp.header_response.context_menu(|ui| {
-            if ui
-                .button(format!("{} Edit Zone", egui_phosphor::regular::PENCIL))
-                .clicked()
-            {
-                // Update dropdowns with current zone list
-                self.update_editor_dropdowns();
+            if crate::ui::ribbon::RibbonUI::check_permission(permissions, "edit_zone") {
+                if ui
+                    .button(format!("{} Edit Zone", egui_phosphor::regular::PENCIL))
+                    .clicked()
+                {
+                    // Update dropdowns with current zone list
+                    self.update_editor_dropdowns();
 
-                // Map the zone data to match the editor field names
-                if let Some(zone_obj) = zone.as_object() {
-                    let mut zone_for_editor = zone_obj.clone();
+                    // Map the zone data to match the editor field names
+                    if let Some(zone_obj) = zone.as_object() {
+                        let mut zone_for_editor = zone_obj.clone();
 
-                    // The data comes in with "name" (aliased from zone_name), but editor expects "zone_name"
-                    if let Some(name_value) = zone_for_editor.remove("name") {
-                        zone_for_editor.insert("zone_name".to_string(), name_value);
-                    }
+                        // The data comes in with "name" (aliased from zone_name), but editor expects "zone_name"
+                        if let Some(name_value) = zone_for_editor.remove("name") {
+                            zone_for_editor.insert("zone_name".to_string(), name_value);
+                        }
 
-                    // Convert integer fields to strings if they're numbers
-                    if let Some(audit_timeout) = zone_for_editor.get("audit_timeout_minutes") {
-                        if let Some(num) = audit_timeout.as_i64() {
-                            zone_for_editor.insert(
-                                "audit_timeout_minutes".to_string(),
-                                serde_json::json!(num.to_string()),
-                            );
+                        // Convert integer fields to strings if they're numbers
+                        if let Some(audit_timeout) = zone_for_editor.get("audit_timeout_minutes") {
+                            if let Some(num) = audit_timeout.as_i64() {
+                                zone_for_editor.insert(
+                                    "audit_timeout_minutes".to_string(),
+                                    serde_json::json!(num.to_string()),
+                                );
+                            }
                         }
-                    }
 
-                    self.edit_dialog
-                        .open(&serde_json::Value::Object(zone_for_editor));
-                    ui.close();
-                } else {
-                    log::error!("Zone data is not an object, cannot edit");
+                        self.edit_dialog
+                            .open(&serde_json::Value::Object(zone_for_editor));
+                        ui.close();
+                    } else {
+                        log::error!("Zone data is not an object, cannot edit");
+                    }
                 }
             }
-            if ui
-                .button(format!("{} Delete Zone", egui_phosphor::regular::TRASH))
-                .clicked()
-            {
-                self.pending_delete_id = Some(id);
-                self.delete_dialog
-                    .open(format!("{} - {}", code, name), id.to_string());
-                ui.close();
+            if crate::ui::ribbon::RibbonUI::check_permission(permissions, "delete_zone") {
+                if ui
+                    .button(format!("{} Delete Zone", egui_phosphor::regular::TRASH))
+                    .clicked()
+                {
+                    self.pending_delete_id = Some(id);
+                    self.delete_dialog.open(name.to_string(), id.to_string());
+                    ui.close();
+                }
             }
             ui.separator();
-            if ui
-                .button(format!("{} Add Child Zone", egui_phosphor::regular::PLUS))
-                .clicked()
-            {
-                // Open add dialog with parent_id pre-filled
-                self.pending_parent_id = Some(id);
-                self.update_editor_dropdowns();
+            if crate::ui::ribbon::RibbonUI::check_permission(permissions, "create_zone") {
+                if ui
+                    .button(format!("{} Add Child Zone", egui_phosphor::regular::PLUS))
+                    .clicked()
+                {
+                    // Open add dialog with parent_id pre-filled
+                    self.pending_parent_id = Some(id);
+                    self.update_editor_dropdowns();
 
-                // Create initial data with parent_id
-                let mut initial_data = serde_json::Map::new();
+                    // Create initial data with parent_id
+                    let mut initial_data = serde_json::Map::new();
                 initial_data.insert("parent_id".to_string(), serde_json::json!(id.to_string()));
 
                 self.add_dialog.open_new(Some(&initial_data));
                 ui.close();
             }
+            }
             if ui
                 .button(format!(
                     "{} Show Items in this Zone",
@@ -746,63 +759,65 @@ impl ZonesView {
                 ui.close();
             }
             ui.separator();
-            if ui
-                .button(format!("{} Clone Zone", egui_phosphor::regular::COPY))
-                .clicked()
-            {
-                // Open add dialog prefilled with cloned values
-                self.update_editor_dropdowns();
+            if crate::ui::ribbon::RibbonUI::check_permission(permissions, "create_zone") {
+                if ui
+                    .button(format!("{} Clone Zone", egui_phosphor::regular::COPY))
+                    .clicked()
+                {
+                    // Open add dialog prefilled with cloned values
+                    self.update_editor_dropdowns();
 
-                // Start from original zone object
-                let mut clone_map = zone.as_object().cloned().unwrap_or_default();
+                    // Start from original zone object
+                    let mut clone_map = zone.as_object().cloned().unwrap_or_default();
 
-                // Editor expects zone_name instead of name
-                if let Some(name_val) = clone_map.remove("name") {
-                    clone_map.insert("zone_name".to_string(), name_val);
-                }
+                    // Editor expects zone_name instead of name
+                    if let Some(name_val) = clone_map.remove("name") {
+                        clone_map.insert("zone_name".to_string(), name_val);
+                    }
 
-                // Clear identifiers and codes that must be unique
-                clone_map.remove("id");
-                clone_map.insert(
-                    "zone_code".to_string(),
-                    serde_json::Value::String(String::new()),
-                );
-                // Mini code is required but typically unique — leave empty to force user choice
-                clone_map.insert(
-                    "mini_code".to_string(),
-                    serde_json::Value::String(String::new()),
-                );
-
-                // Convert parent_id to string for dropdown if present
-                if let Some(p) = clone_map.get("parent_id").cloned() {
-                    let as_str = match p {
-                        serde_json::Value::Number(n) => {
-                            n.as_i64().map(|i| i.to_string()).unwrap_or_default()
+                    // Clear identifiers and codes that must be unique
+                    clone_map.remove("id");
+                    clone_map.insert(
+                        "zone_code".to_string(),
+                        serde_json::Value::String(String::new()),
+                    );
+                    // Mini code is required but typically unique — leave empty to force user choice
+                    clone_map.insert(
+                        "mini_code".to_string(),
+                        serde_json::Value::String(String::new()),
+                    );
+
+                    // Convert parent_id to string for dropdown if present
+                    if let Some(p) = clone_map.get("parent_id").cloned() {
+                        let as_str = match p {
+                            serde_json::Value::Number(n) => {
+                                n.as_i64().map(|i| i.to_string()).unwrap_or_default()
+                            }
+                            serde_json::Value::String(s) => s,
+                            _ => String::new(),
+                        };
+                        clone_map.insert("parent_id".to_string(), serde_json::Value::String(as_str));
+                    }
+
+                    // Ensure audit_timeout_minutes is string
+                    if let Some(a) = clone_map.get("audit_timeout_minutes").cloned() {
+                        if let Some(num) = a.as_i64() {
+                            clone_map.insert(
+                                "audit_timeout_minutes".to_string(),
+                                serde_json::json!(num.to_string()),
+                            );
                         }
-                        serde_json::Value::String(s) => s,
-                        _ => String::new(),
-                    };
-                    clone_map.insert("parent_id".to_string(), serde_json::Value::String(as_str));
-                }
+                    }
 
-                // Ensure audit_timeout_minutes is string
-                if let Some(a) = clone_map.get("audit_timeout_minutes").cloned() {
-                    if let Some(num) = a.as_i64() {
-                        clone_map.insert(
-                            "audit_timeout_minutes".to_string(),
-                            serde_json::json!(num.to_string()),
-                        );
+                    // Suffix the name to indicate copy
+                    if let Some(serde_json::Value::String(nm)) = clone_map.get_mut("zone_name") {
+                        nm.push_str("");
                     }
-                }
 
-                // Suffix the name to indicate copy
-                if let Some(serde_json::Value::String(nm)) = clone_map.get_mut("zone_name") {
-                    nm.push_str("");
+                    // Open prefilled Add dialog
+                    self.add_dialog.open_new(Some(&clone_map));
+                    ui.close();
                 }
-
-                // Open prefilled Add dialog
-                self.add_dialog.open_new(Some(&clone_map));
-                ui.close();
             }
         });
     }