use eframe::egui; use std::collections::HashMap; use std::sync::{mpsc, Arc}; use tokio::sync::Mutex; use super::audits::AuditsView; use super::borrowing::BorrowingView; use super::categories::CategoriesView; use super::dashboard::DashboardView; use super::inventory::InventoryView; use super::issues::IssuesView; use super::label_templates::LabelTemplatesView; use super::login::LoginScreen; use super::printers::PrintersView; use super::ribbon::RibbonUI; use super::suppliers::SuppliersView; use super::templates::TemplatesView; use super::zones::ZonesView; use crate::api::ApiClient; use crate::config::AppConfig; use crate::models::{LoginResponse, UserInfo}; use crate::session::{SessionData, SessionManager}; pub struct BeepZoneApp { // Session management session_manager: Arc>, api_client: Option, // Current view state current_view: AppView, previous_view: Option, current_user: Option, // Per-view filter state storage view_filter_states: HashMap, // UI components login_screen: LoginScreen, dashboard: DashboardView, inventory: InventoryView, categories: CategoriesView, zones: ZonesView, borrowing: BorrowingView, audits: AuditsView, templates: TemplatesView, suppliers: SuppliersView, issues: IssuesView, printers: PrintersView, label_templates: LabelTemplatesView, ribbon_ui: Option, // Configuration #[allow(dead_code)] app_config: Option, // State login_success: Option<(String, LoginResponse)>, show_about: bool, // Status bar state server_status: ServerStatus, last_health_check: std::time::Instant, health_check_in_progress: bool, health_check_rx: Option>, // Re-authentication prompt state reauth_needed: bool, reauth_password: String, // Database outage tracking db_offline_latch: bool, last_timeout_at: Option, consecutive_healthy_checks: u8, } #[derive(Debug, Clone, Copy, PartialEq)] pub enum ServerStatus { Unknown, Connected, Disconnected, Checking, } #[derive(Debug, Clone, Copy)] struct HealthCheckResult { status: ServerStatus, reauth_required: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AppView { Login, Dashboard, Inventory, Categories, Zones, Borrowing, Audits, Templates, Suppliers, IssueTracker, Printers, LabelTemplates, } impl BeepZoneApp { pub fn new( _cc: &eframe::CreationContext<'_>, session_manager: Arc>, ) -> Self { let session_manager_blocking = session_manager.blocking_lock(); let login_screen = LoginScreen::new(&session_manager_blocking); // Try to restore session on startup let (api_client, current_view, current_user) = if let Some(session) = session_manager_blocking.get_session() { log::info!("Found saved session, attempting to restore..."); // Create API client with saved token match ApiClient::new(session.server_url.clone()) { Ok(mut client) => { client.set_token(session.token.clone()); // Verify session is still valid (tolerant) match client.check_session_valid() { Ok(true) => { log::info!( "Session restored successfully for user: {}", session.user.username ); (Some(client), AppView::Dashboard, Some(session.user.clone())) } Ok(false) => { log::warn!("Saved session check returned invalid"); (None, AppView::Login, 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())) } } } Err(e) => { log::error!("Failed to create API client: {}", e); (None, AppView::Login, None) } } } else { log::info!("No saved session found"); (None, AppView::Login, None) }; drop(session_manager_blocking); // Load configuration and initialize ribbon UI let ribbon_ui = Some(RibbonUI::default()); let app_config = None; let mut app = Self { session_manager, api_client, current_view, previous_view: None, view_filter_states: HashMap::new(), current_user, login_screen, dashboard: DashboardView::new(), inventory: InventoryView::new(), categories: CategoriesView::new(), zones: ZonesView::new(), borrowing: BorrowingView::new(), audits: AuditsView::new(), templates: TemplatesView::new(), suppliers: SuppliersView::new(), issues: IssuesView::new(), printers: PrintersView::new(), label_templates: LabelTemplatesView::new(), ribbon_ui, app_config, login_success: None, show_about: false, server_status: ServerStatus::Unknown, last_health_check: std::time::Instant::now(), health_check_in_progress: false, health_check_rx: None, reauth_needed: false, reauth_password: String::new(), db_offline_latch: false, last_timeout_at: None, consecutive_healthy_checks: 0, }; // Do initial health check if we have an API client if app.api_client.is_some() { app.request_health_check(); } app } fn handle_login_success(&mut self, server_url: String, response: LoginResponse) { // Capture username for logging before moving fields out of response let username = response.user.username.clone(); log::info!("Login successful for user: {}", username); // Create API client with token let mut api_client = match ApiClient::new(server_url.clone()) { Ok(client) => client, Err(e) => { log::error!("Failed to create API client: {}", e); // This shouldn't happen in normal operation, so just log and continue without client return; } }; api_client.set_token(response.token.clone()); self.api_client = Some(api_client); self.current_user = Some(response.user.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, remember_server: true, remember_username: true, saved_username: self.current_user.as_ref().map(|u| u.username.clone()), default_printer_id: None, last_printer_id: None, }; let mut session_manager = self.session_manager.blocking_lock(); if let Err(e) = session_manager.save_session(session_data) { log::error!("Failed to save session: {}", e); } // Switch to dashboard self.current_view = AppView::Dashboard; self.reauth_needed = false; self.reauth_password.clear(); // Load dashboard data if let Some(client) = self.api_client.as_ref() { self.dashboard.refresh_data(client); } } fn handle_reauth_success(&mut self, server_url: String, response: LoginResponse) { // Preserve current view but refresh token and user let mut new_client = match ApiClient::new(server_url.clone()) { Ok(client) => client, Err(e) => { log::error!("Failed to create API client during reauth: {}", e); self.reauth_needed = true; return; } }; new_client.set_token(response.token.clone()); // Replace client and user self.api_client = Some(new_client); self.current_user = Some(response.user.clone()); // Save updated session let session_data = SessionData { server_url, token: response.token, user: response.user, remember_server: true, remember_username: true, saved_username: self.current_user.as_ref().map(|u| u.username.clone()), default_printer_id: None, last_printer_id: None, }; let mut session_manager = self.session_manager.blocking_lock(); if let Err(e) = session_manager.save_session(session_data) { log::error!("Failed to save session after reauth: {}", e); } } fn show_top_bar(&mut self, ctx: &egui::Context, disable_actions: bool) { egui::TopBottomPanel::top("top_bar") .exact_height(45.0) .show_separator_line(false) .frame( egui::Frame::new() .fill(ctx.style().visuals.window_fill) .stroke(egui::Stroke::NONE) .inner_margin(egui::vec2(16.0, 5.0)), ) .show(ctx, |ui| { // Horizontal layout for title and controls ui.horizontal(|ui| { ui.heading("BeepZone"); ui.separator(); // User info if let Some(user) = &self.current_user { ui.label(format!("User: {} ({})", user.username, user.role)); ui.label(format!("Powah: {}", user.power)); } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.add_enabled_ui(!disable_actions, |ui| { if ui.button("About").clicked() { self.show_about = true; } if ui.button("Bye").clicked() { self.handle_logout(); } }); }); }); }); } fn show_ribbon(&mut self, ctx: &egui::Context) -> Option { let mut action_triggered = None; if let Some(ribbon_ui) = &mut self.ribbon_ui { let min_height = ribbon_ui.preferred_height(); // Outer container panel with normal background egui::TopBottomPanel::top("ribbon_container") .min_height(min_height + 16.0) .max_height(min_height + 96.0) .show_separator_line(false) .frame(egui::Frame::new().fill(if ctx.style().visuals.dark_mode { ctx.style().visuals.panel_fill } else { // Darker background in light mode egui::Color32::from_rgb(210, 210, 210) })) .show(ctx, |ui| { ui.add_space(0.0); let side_margin: f32 = 16.0; let inner_pad: f32 = 8.0; ui.horizontal(|ui| { // Left margin ui.add_space(side_margin); // Remaining width after left margin let remaining = ui.available_width(); // Leave room for right margin and inner padding on both sides of the frame let content_width = (remaining - side_margin - inner_pad * 2.0).max(0.0); // Custom ribbon background color based on theme let is_dark_mode = ctx.style().visuals.dark_mode; let ribbon_bg_color = if is_dark_mode { // Lighter gray for dark mode - more visible contrast egui::Color32::from_rgb(45, 45, 45) } else { // Lighter/white ribbon in light mode egui::Color32::from_rgb(248, 248, 248) }; egui::Frame::new() .fill(ribbon_bg_color) .inner_margin(inner_pad) .corner_radius(6.0) .show(ui, |ui| { // Constrain to the computed content width so right margin remains 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); }); // Update current view based on active ribbon tab if let Some(view_name) = ribbon_ui.get_active_view() { self.current_view = match view_name.to_lowercase().as_str() { "dashboard" => AppView::Dashboard, "inventory" => AppView::Inventory, "categories" => AppView::Categories, "zones" => AppView::Zones, "borrowing" => AppView::Borrowing, "audits" => AppView::Audits, "item templates" => AppView::Templates, "templates" => AppView::Templates, // Backwards compat "suppliers" => AppView::Suppliers, "issues" | "issue_tracker" => AppView::IssueTracker, "printers" => AppView::Printers, "label templates" => AppView::LabelTemplates, _ => self.current_view, }; } }); // Right margin ui.add_space(side_margin); }); ui.add_space(8.0); }); } else { // Fallback to simple ribbon if config failed to load egui::TopBottomPanel::top("ribbon") .exact_height(38.0) .show_separator_line(false) .show(ctx, |ui| { ui.add_space(2.0); ui.horizontal_wrapped(|ui| { ui.selectable_value( &mut self.current_view, AppView::Dashboard, "Dashboard", ); ui.selectable_value( &mut self.current_view, AppView::Inventory, "Inventory", ); ui.selectable_value( &mut self.current_view, AppView::Categories, "Categories", ); ui.selectable_value(&mut self.current_view, AppView::Zones, "Zones"); ui.selectable_value( &mut self.current_view, AppView::Borrowing, "Borrowing", ); ui.selectable_value(&mut self.current_view, AppView::Audits, "Audits"); ui.selectable_value( &mut self.current_view, AppView::Templates, "Templates", ); ui.selectable_value( &mut self.current_view, AppView::Suppliers, "Suppliers", ); ui.selectable_value( &mut self.current_view, AppView::IssueTracker, "Issues", ); }); }); } action_triggered } fn handle_logout(&mut self) { log::info!("Taking myself out"); // Logout from API if let Some(api_client) = &self.api_client { let _ = api_client.logout(); } // Clear session and reset login screen (do both while holding the lock once) { let mut session_manager = self.session_manager.blocking_lock(); if let Err(e) = session_manager.clear_session() { log::error!("Failed to clear session: {}", e); } // Reset login screen while we still have the lock self.login_screen = LoginScreen::new(&session_manager); } // Lock is dropped here // Reset state self.api_client = None; self.current_user = None; self.current_view = AppView::Login; self.server_status = ServerStatus::Unknown; } /// Force an immediate health check (used when timeout errors detected) pub fn force_health_check(&mut self) { self.last_health_check = std::time::Instant::now() - std::time::Duration::from_secs(10); self.request_health_check(); } fn request_health_check(&mut self) { if self.api_client.is_none() || self.health_check_in_progress { return; } if let Some(client) = &self.api_client { let api_client = client.clone(); let reauth_needed = self.reauth_needed; let (tx, rx) = mpsc::channel(); self.health_check_rx = Some(rx); self.health_check_in_progress = true; self.server_status = ServerStatus::Checking; self.last_health_check = std::time::Instant::now(); std::thread::spawn(move || { let result = Self::run_health_check(api_client, reauth_needed); let _ = tx.send(result); }); } } fn desired_health_interval(&self, predicted_block: bool) -> f32 { if predicted_block || self.db_offline_latch { 0.75 } else if matches!(self.server_status, ServerStatus::Connected) { 1.5 } else { 2.5 } } fn poll_health_check(&mut self) { if let Some(rx) = &self.health_check_rx { match rx.try_recv() { Ok(result) => { self.apply_health_result(result); self.health_check_rx = None; self.health_check_in_progress = false; self.last_health_check = std::time::Instant::now(); } Err(mpsc::TryRecvError::Empty) => {} Err(mpsc::TryRecvError::Disconnected) => { log::warn!("Health check worker disconnected unexpectedly"); self.health_check_rx = None; self.health_check_in_progress = false; } } } } fn apply_health_result(&mut self, result: HealthCheckResult) { if self.reauth_needed != result.reauth_required { if self.reauth_needed && !result.reauth_required { log::info!("Session valid again; clearing re-auth requirement"); } else if !self.reauth_needed && result.reauth_required { log::info!("Session invalid/expired; prompting re-auth"); } self.reauth_needed = result.reauth_required; } match result.status { ServerStatus::Disconnected => { self.db_offline_latch = true; self.last_timeout_at = Some(std::time::Instant::now()); self.consecutive_healthy_checks = 0; } ServerStatus::Connected => { self.consecutive_healthy_checks = self.consecutive_healthy_checks.saturating_add(1); if self.db_offline_latch { let timeout_cleared = self .last_timeout_at .map(|t| t.elapsed() > std::time::Duration::from_secs(2)) .unwrap_or(true); if timeout_cleared && self.consecutive_healthy_checks >= 2 { log::info!("Health checks stable; clearing database offline latch"); self.db_offline_latch = false; } } } _ => { self.consecutive_healthy_checks = 0; } } if self.db_offline_latch { self.server_status = ServerStatus::Disconnected; } else { self.server_status = result.status; } } fn run_health_check(api_client: ApiClient, mut reauth_needed: bool) -> HealthCheckResult { let connected = match api_client.check_session_valid() { Ok(true) => { reauth_needed = false; true } Ok(false) => { reauth_needed = true; true } Err(e) => { log::warn!("Session status check error: {}", e); false } }; if connected { let mut db_disconnected = false; if let Ok(true) = api_client.health_check() { if let Ok(info_opt) = api_client.health_info() { if let Some(info) = info_opt { let db_down = info.get("database").and_then(|v| v.as_str()) .map(|s| s.eq_ignore_ascii_case("disconnected")) .unwrap_or(false) || info.get("database_connected").and_then(|v| v.as_bool()) == Some(false) || info.get("db_connected").and_then(|v| v.as_bool()) == Some(false) || info .get("db") .and_then(|v| v.as_str()) .map(|s| s.eq_ignore_ascii_case("down")) .unwrap_or(false) || info .get("database") .and_then(|v| v.as_str()) .map(|s| s.eq_ignore_ascii_case("down")) .unwrap_or(false); if db_down { db_disconnected = true; } } } } if db_disconnected { log::warn!("Database disconnected; treating as offline"); HealthCheckResult { status: ServerStatus::Disconnected, reauth_required: reauth_needed, } } else { HealthCheckResult { status: ServerStatus::Connected, reauth_required: reauth_needed, } } } else { HealthCheckResult { status: ServerStatus::Disconnected, reauth_required: reauth_needed, } } } fn handle_ribbon_action(&mut self, action: String) { log::info!("Ribbon action triggered: {}", action); // Handle different action types if action.starts_with("search:") { let search_query = action.strip_prefix("search:").unwrap_or(""); log::info!("Search action: {}", search_query); // TODO: Implement search functionality } else { match action.as_str() { // Dashboard actions "refresh_dashboard" => { if let Some(api_client) = &self.api_client { self.dashboard.refresh_data(api_client); } } "customize_dashboard" => { log::info!("Customize dashboard - TODO"); } // Inventory actions "add_item" => { log::info!("Add item - TODO"); } "edit_item" => { log::info!("Edit item - TODO"); } "delete_item" => { log::info!("Delete item - TODO"); } "print_label" => { log::info!("Print label - TODO"); } // Quick actions "inventarize_quick" => { log::info!("Quick inventarize - TODO"); } "checkout_checkin" => { log::info!("Check-out/in - TODO"); } "start_room_audit" => { log::info!("Start room audit - TODO"); } "start_spot_check" => { log::info!("Start spot-check - TODO"); } _ => { log::info!("Unhandled action: {}", action); } } } } fn show_status_bar(&self, ctx: &egui::Context) { egui::TopBottomPanel::bottom("status_bar") .exact_height(24.0) .show_separator_line(false) .frame( egui::Frame::new() .fill(ctx.style().visuals.window_fill) .stroke(egui::Stroke::NONE), ) .show(ctx, |ui| { ui.horizontal(|ui| { // Seqkel inikator let (icon, text, color) = match self.server_status { ServerStatus::Connected => ( "-", if self.reauth_needed { "Server Connected • Re-auth required" } else { "Server Connected" }, egui::Color32::from_rgb(76, 175, 80), ), ServerStatus::Disconnected => { // Check if we detected database timeout recently let timeout_detected = self.dashboard.has_timeout_error(); let text = if timeout_detected { "Database Timeout - Retrying..." } else { "Server Disconnected" }; ("x", text, egui::Color32::from_rgb(244, 67, 54)) }, ServerStatus::Checking => { ("~", "Checking...", egui::Color32::from_rgb(255, 152, 0)) } ServerStatus::Unknown => ( "??????????? -", "I don't know maybe connected maybe not ???", egui::Color32::GRAY, ), }; ui.label(egui::RichText::new(icon).color(color).size(16.0)); ui.label(egui::RichText::new(text).color(color).size(12.0)); ui.separator(); // Server URL if let Some(client) = &self.api_client { ui.label( egui::RichText::new(format!("Server: {}", client.base_url())) .size(11.0) .color(egui::Color32::GRAY), ); } // User info on the right ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if let Some(user) = &self.current_user { ui.label( egui::RichText::new(format!("User: {}", user.username)) .size(11.0) .color(egui::Color32::GRAY), ); } }); }); }); } fn show_reconnect_overlay(&self, ctx: &egui::Context) { let screen_rect = ctx.viewport_rect(); let visuals = ctx.style().visuals.clone(); let dim_color = if visuals.dark_mode { egui::Color32::from_black_alpha(180) } else { egui::Color32::from_white_alpha(200) }; // Dim the entire interface let layer_id = egui::LayerId::new( egui::Order::Foreground, egui::Id::new("reconnect_overlay_bg"), ); ctx.layer_painter(layer_id) .rect_filled(screen_rect, 0.0, dim_color); // Capture input so underlying widgets don't receive clicks or keypresses egui::Area::new(egui::Id::new("reconnect_overlay_blocker")) .order(egui::Order::Foreground) .movable(false) .interactable(true) .fixed_pos(screen_rect.left_top()) .show(ctx, |ui| { ui.set_min_size(screen_rect.size()); ui.allocate_rect(ui.max_rect(), egui::Sense::click_and_drag()); }); let timeout_detected = self.dashboard.has_timeout_error(); let message = if timeout_detected { "Database temporarily unavailable. Waiting for heartbeat…" } else { "Connection to the backend was lost. Retrying…" }; // Foreground card with spinner and message egui::Area::new(egui::Id::new("reconnect_overlay_card")) .order(egui::Order::Foreground) .movable(false) .anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0)) .show(ctx, |ui| { ui.spacing_mut().item_spacing = egui::vec2(0.0, 12.0); ui.set_min_size(egui::vec2(360.0, 200.0)); egui::Frame::default() .fill(visuals.panel_fill) .stroke(egui::Stroke::new(1.0, visuals.weak_text_color())) .corner_radius(12.0) .inner_margin(egui::Margin::symmetric(32, 24)) .show(ui, |ui| { ui.vertical_centered(|ui| { ui.heading( egui::RichText::new("Reconnecting…") .color(visuals.strong_text_color()) .size(20.0), ); ui.add_space(8.0); ui.spinner(); ui.label( egui::RichText::new(message) .color(visuals.text_color()) .size(15.0), ); ui.label( egui::RichText::new( "All actions are paused until the backend recovers.", ) .color(visuals.weak_text_color()) .size(13.0), ); }); }); }); // Keep spinner animating while offline ctx.request_repaint_after(std::time::Duration::from_millis(250)); } fn should_block_interaction(&self) -> bool { self.api_client.is_some() && self.current_view != AppView::Login && (matches!(self.server_status, ServerStatus::Disconnected) || self.db_offline_latch) } /// Save current filter state before switching views fn save_filter_state_for_view(&mut self, view: AppView) { if let Some(ribbon) = &self.ribbon_ui { // Only save filter state for views that use filters if matches!( view, AppView::Inventory | AppView::Zones | AppView::Borrowing ) { self.view_filter_states .insert(view, ribbon.filter_builder.filter_group.clone()); } } } /// Restore filter state when switching to a view fn restore_filter_state_for_view(&mut self, view: AppView) { if let Some(ribbon) = &mut self.ribbon_ui { // Check if we have saved state for this view if let Some(saved_state) = self.view_filter_states.get(&view) { ribbon.filter_builder.filter_group = saved_state.clone(); } else { // No saved state - clear filters for this view (fresh start) ribbon.filter_builder.filter_group = crate::core::components::filter_builder::FilterGroup::new(); } } } } impl eframe::App for BeepZoneApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { // Detect view changes and save/restore filter state if let Some(prev_view) = self.previous_view { if prev_view != self.current_view { // Save filter state for the view we're leaving self.save_filter_state_for_view(prev_view); // Restore filter state for the view we're entering self.restore_filter_state_for_view(self.current_view); // Update available columns for the new view if let Some(ribbon) = &mut self.ribbon_ui { match self.current_view { AppView::Inventory => { // Ensure Inventory uses asset columns in the FilterBuilder ribbon.filter_builder.set_columns_for_context("assets"); } AppView::Zones => { ribbon.filter_builder.available_columns = vec![ ("Any".to_string(), "Any".to_string()), ("Zone Code".to_string(), "zones.zone_code".to_string()), ("Zone Name".to_string(), "zones.zone_name".to_string()), ]; } AppView::Borrowing => { ribbon.filter_builder.available_columns = crate::ui::borrowing::BorrowingView::get_filter_columns(); } _ => {} } } } } // Update previous view for next frame self.previous_view = Some(self.current_view); // Customize background color for light mode if !ctx.style().visuals.dark_mode { let mut style = (*ctx.style()).clone(); style.visuals.panel_fill = egui::Color32::from_rgb(210, 210, 210); style.visuals.window_fill = egui::Color32::from_rgb(210, 210, 210); ctx.set_style(style); } // Check for login success if let Some((server_url, response)) = self.login_success.take() { self.handle_login_success(server_url, response); } // Process any completed health checks and schedule new ones self.poll_health_check(); let predicted_block = self.should_block_interaction(); let health_interval = self.desired_health_interval(predicted_block); if self.api_client.is_some() && !self.health_check_in_progress && self.last_health_check.elapsed().as_secs_f32() > health_interval { self.request_health_check(); } // Show appropriate view if self.current_view == AppView::Login { self.login_screen.show(ctx, &mut self.login_success); } else { let mut block_interaction = self.should_block_interaction(); if let Some(client) = &self.api_client { if client.take_timeout_signal() { log::warn!("Backend timeout detected via API client; entering reconnect mode"); self.server_status = ServerStatus::Disconnected; self.db_offline_latch = true; self.last_timeout_at = Some(std::time::Instant::now()); self.consecutive_healthy_checks = 0; block_interaction = true; // Force an immediate health re-check self.last_health_check = std::time::Instant::now() - std::time::Duration::from_secs(10); if !self.health_check_in_progress { self.request_health_check(); } } } // When we're blocked, ensure a health check is queued so we recover ASAP if block_interaction && !self.health_check_in_progress && self.last_health_check.elapsed().as_secs_f32() > 1.0 { self.request_health_check(); } self.show_top_bar(ctx, block_interaction); let ribbon_action = if block_interaction { None } else { self.show_ribbon(ctx) }; self.show_status_bar(ctx); if !block_interaction { if let Some(action) = ribbon_action { self.handle_ribbon_action(action); } egui::CentralPanel::default().show(ctx, |ui| match self.current_view { AppView::Dashboard => { self.dashboard.show(ui, self.api_client.as_ref()); // Check if dashboard has timeout error and trigger health check if self.dashboard.has_timeout_error() { self.force_health_check(); } } AppView::Inventory => { // Handle FilterBuilder popup BEFORE showing inventory // This ensures filter changes are processed in the current frame if let Some(ribbon) = &mut self.ribbon_ui { let filter_changed = ribbon.filter_builder.show_popup(ctx); if filter_changed { ribbon .checkboxes .insert("inventory_filter_changed".to_string(), true); } } self.inventory.show( ui, self.api_client.as_ref(), self.ribbon_ui.as_mut(), &self.session_manager, ); } AppView::Categories => { self.categories .show(ui, self.api_client.as_ref(), self.ribbon_ui.as_mut()); } AppView::Zones => { if let Some(ribbon) = self.ribbon_ui.as_mut() { // Handle FilterBuilder popup BEFORE showing zones view so changes are applied in the same frame let filter_changed = ribbon.filter_builder.show_popup(ctx); if filter_changed { ribbon .checkboxes .insert("zones_filter_changed".to_string(), true); } self.zones.show(ui, self.api_client.as_ref(), ribbon); // Handle zone navigation request to inventory if let Some(zone_code) = self.zones.switch_to_inventory_with_zone.take() { log::info!("Switching to inventory with zone filter: {}", zone_code); // Save current Zones filter state let zones_filter_state = ribbon.filter_builder.filter_group.clone(); self.view_filter_states .insert(AppView::Zones, zones_filter_state); // Set zone filter using the correct column name from the JOIN ribbon.filter_builder.set_single_filter( "zones.zone_code".to_string(), crate::core::components::filter_builder::FilterOperator::Is, zone_code, ); // Switch to inventory view self.current_view = AppView::Inventory; ribbon.active_tab = "Inventory".to_string(); ribbon .checkboxes .insert("inventory_filter_changed".to_string(), true); // Update previous_view to match so next frame doesn't restore old inventory filters self.previous_view = Some(AppView::Inventory); // Request repaint to ensure the filter is applied on the next frame ctx.request_repaint(); } } else { // Fallback if no ribbon (shouldn't happen) log::warn!("No ribbon available for zones view"); } } AppView::Borrowing => { if let Some(ribbon) = self.ribbon_ui.as_mut() { // Handle FilterBuilder popup let filter_changed = ribbon.filter_builder.show_popup(ctx); if filter_changed { ribbon .checkboxes .insert("borrowing_filter_changed".to_string(), true); } self.borrowing .show(ctx, ui, self.api_client.as_ref(), ribbon); // Handle borrower navigation request to inventory if let Some(borrower_id) = self.borrowing.switch_to_inventory_with_borrower.take() { log::info!( "Switching to inventory with borrower filter: {}", borrower_id ); // Save current Borrowing filter state let borrowing_filter_state = ribbon.filter_builder.filter_group.clone(); self.view_filter_states .insert(AppView::Borrowing, borrowing_filter_state); // Set borrower filter using the current_borrower_id from assets table ribbon.filter_builder.set_single_filter( "assets.current_borrower_id".to_string(), crate::core::components::filter_builder::FilterOperator::Is, borrower_id.to_string(), ); // Switch to inventory view self.current_view = AppView::Inventory; ribbon.active_tab = "Inventory".to_string(); ribbon .checkboxes .insert("inventory_filter_changed".to_string(), true); // Update previous_view to match so next frame doesn't restore old inventory filters self.previous_view = Some(AppView::Inventory); // Request repaint to ensure the filter is applied on the next frame ctx.request_repaint(); } } else { // Fallback if no ribbon (shouldn't happen) log::warn!("No ribbon available for borrowing view"); } } 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); } AppView::Templates => { if let Some(ribbon) = self.ribbon_ui.as_mut() { // Handle FilterBuilder popup BEFORE showing templates view let filter_changed = ribbon.filter_builder.show_popup(ctx); if filter_changed { ribbon .checkboxes .insert("templates_filter_changed".to_string(), true); } let flags = self .templates .show(ui, self.api_client.as_ref(), Some(ribbon)); for flag in flags { ribbon.checkboxes.insert(flag, false); } } else { self.templates.show(ui, self.api_client.as_ref(), None); } } AppView::Suppliers => { if let Some(ribbon_ui) = self.ribbon_ui.as_mut() { let flags = self.suppliers.show( ui, self.api_client.as_ref(), Some(&mut *ribbon_ui), ); for flag in flags { ribbon_ui.checkboxes.insert(flag, false); } } else { let _ = self.suppliers.show(ui, self.api_client.as_ref(), None); } } AppView::IssueTracker => { self.issues.show(ui, self.api_client.as_ref()); } AppView::Printers => { // Render printers dropdown in ribbon if we're on printers tab if let Some(ribbon) = self.ribbon_ui.as_mut() { if ribbon.active_tab == "Printers" { self.printers .inject_dropdown_into_ribbon(ribbon, &self.session_manager); } } self.printers.render( ui, self.api_client.as_ref(), self.ribbon_ui.as_mut(), &self.session_manager, ); } AppView::LabelTemplates => { self.label_templates.render( ui, self.api_client.as_ref(), self.ribbon_ui.as_mut(), ); } AppView::Login => unreachable!(), }); } else { self.show_reconnect_overlay(ctx); } } // Re-authentication modal when needed (only outside of Login view) if self.reauth_needed && self.current_view != AppView::Login { let mut keep_open = true; egui::Window::new("Session expired") .collapsible(false) .resizable(false) .movable(true) .open(&mut keep_open) .show(ctx, |ui| { ui.label("Your session has expired or is invalid. Reenter your password to continue."); ui.add_space(8.0); let mut pw = std::mem::take(&mut self.reauth_password); let response = ui.add( egui::TextEdit::singleline(&mut pw) .password(true) .hint_text("Password") .desired_width(260.0), ); self.reauth_password = pw; ui.add_space(8.0); ui.horizontal(|ui| { let mut try_login = ui.button("Reauthenticate").clicked(); // Allow Enter to submit try_login |= response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); if try_login { if let (Some(client), Some(user)) = (self.api_client.as_mut(), self.current_user.as_ref()) { // Attempt password login to refresh token match client.login_password(&user.username, &self.reauth_password) { Ok(resp) => { let server_url = client.base_url().to_string(); self.handle_reauth_success(server_url, resp); self.reauth_needed = false; self.reauth_password.clear(); // Avoid immediate re-flagging by pushing out the next health check self.last_health_check = std::time::Instant::now(); } Err(e) => { log::error!("Reauth failed: {}", e); } } } } if ui.button("Go to Login").clicked() { self.handle_logout(); self.reauth_needed = false; self.reauth_password.clear(); } }); }); if !keep_open { // Close button pressed: just dismiss (will reappear on next check if still invalid) self.reauth_needed = false; self.reauth_password.clear(); self.last_health_check = std::time::Instant::now(); } } // About dialog if self.show_about { egui::Window::new("About BeepZone") .collapsible(false) .resizable(false) .show(ctx, |ui| { ui.heading("BeepZone Desktop Client"); ui.heading("- eGUI EMO Edition"); ui.label(format!("Version: {}", env!("CARGO_PKG_VERSION"))); ui.separator(); ui.label("A crude inventory system meant to run on any potato!"); ui.label("- Fueled by peanut butter and caffeine"); ui.label("- Backed by Spaghetti codebase supreme pro plus ultra"); ui.label("- Running at all thanks to vibe coding and sheer willpower"); ui.label("- Oles Approved"); ui.label("- Atleast tries to be a good fucking inventory system!"); ui.separator(); ui.label("Made with love (and some hatred) by crt "); ui.separator(); if ui.button("Close this goofy ah panel").clicked() { self.show_about = false; } }); } } }