From 8323fdd73272a2882781aba3c499ba0be3dff2a6 Mon Sep 17 00:00:00 2001 From: UMTS at Teleco Date: Sat, 13 Dec 2025 02:51:15 +0100 Subject: committing to insanity --- src/ui/inventory.rs | 1933 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1933 insertions(+) create mode 100644 src/ui/inventory.rs (limited to 'src/ui/inventory.rs') diff --git a/src/ui/inventory.rs b/src/ui/inventory.rs new file mode 100644 index 0000000..67fac93 --- /dev/null +++ b/src/ui/inventory.rs @@ -0,0 +1,1933 @@ +use crate::api::ApiClient; +use crate::core::components::help::{show_help_window, HelpWindowOptions}; +use crate::core::workflows::borrow_flow::{BorrowFlow, BorrowStep}; +use crate::core::workflows::return_flow::{ReturnFlow, ReturnStep}; +use crate::core::{ + components::form_builder::FormBuilder, components::interactions::ConfirmDialog, + workflows::AddFromTemplateWorkflow, AssetFieldBuilder, AssetOperations, ColumnConfig, + DataLoader, LoadingState, TableEventHandler, TableRenderer, +}; +use eframe::egui; +use egui_commonmark::CommonMarkCache; +use serde_json::Value; +use std::collections::HashMap; + +pub struct InventoryView { + // Data + assets: Vec, + loading_state: LoadingState, + + // Table and UI components + table_renderer: TableRenderer, + show_column_panel: bool, + + // Filter state tracking + last_show_retired_state: bool, + last_item_lookup: String, + + // Dialogs + delete_dialog: ConfirmDialog, + edit_dialog: FormBuilder, + add_dialog: FormBuilder, + advanced_edit_dialog: FormBuilder, + print_dialog: Option, + show_print_dialog: bool, + + // Bulk action state + pending_delete_ids: Vec, + pending_edit_ids: Vec, + is_bulk_edit: bool, + + // Workflows + add_from_template_workflow: AddFromTemplateWorkflow, + borrow_flow: BorrowFlow, + return_flow: ReturnFlow, + + // Help + show_help: bool, + help_cache: CommonMarkCache, +} + +impl InventoryView { + pub fn new() -> Self { + // Define all available columns from the assets table schema + let columns = vec![ + ColumnConfig::new("ID", "id").with_width(60.0).hidden(), + ColumnConfig::new("Asset Tag", "asset_tag").with_width(120.0), + ColumnConfig::new("Numeric ID", "asset_numeric_id") + .with_width(100.0) + .hidden(), + ColumnConfig::new("Type", "asset_type").with_width(60.0), + ColumnConfig::new("Name", "name").with_width(180.0), + ColumnConfig::new("Category", "category_name").with_width(90.0), + ColumnConfig::new("Manufacturer", "manufacturer").with_width(100.0), + ColumnConfig::new("Model", "model").with_width(100.0), + ColumnConfig::new("Serial Number", "serial_number") + .with_width(130.0) + .hidden(), + ColumnConfig::new("Zone", "zone_code").with_width(80.0), + ColumnConfig::new("Label Template", "label_template_name") + .with_width(160.0) + .hidden(), + ColumnConfig::new("Label Template ID", "label_template_id") + .with_width(80.0) + .hidden(), + ColumnConfig::new("Zone Plus", "zone_plus") + .with_width(100.0) + .hidden(), + ColumnConfig::new("Zone Note", "zone_note") + .with_width(150.0) + .hidden(), + ColumnConfig::new("Status", "status").with_width(80.0), + ColumnConfig::new("Last Audit", "last_audit") + .with_width(100.0) + .hidden(), + ColumnConfig::new("Last Audit Status", "last_audit_status") + .with_width(120.0) + .hidden(), + ColumnConfig::new("Price", "price") + .with_width(90.0) + .hidden(), + ColumnConfig::new("Purchase Date", "purchase_date") + .with_width(110.0) + .hidden(), + ColumnConfig::new("Warranty Until", "warranty_until") + .with_width(110.0) + .hidden(), + ColumnConfig::new("Expiry Date", "expiry_date") + .with_width(100.0) + .hidden(), + ColumnConfig::new("Qty Available", "quantity_available") + .with_width(90.0) + .hidden(), + ColumnConfig::new("Qty Total", "quantity_total") + .with_width(80.0) + .hidden(), + ColumnConfig::new("Qty Used", "quantity_used") + .with_width(80.0) + .hidden(), + ColumnConfig::new("Supplier", "supplier_name") + .with_width(120.0) + .hidden(), + ColumnConfig::new("Lendable", "lendable") + .with_width(80.0) + .hidden(), + ColumnConfig::new("Min Role", "minimum_role_for_lending") + .with_width(80.0) + .hidden(), + ColumnConfig::new("Lending Status", "lending_status").with_width(70.0), + ColumnConfig::new("Current Borrower", "current_borrower_name") + .with_width(130.0) + .hidden(), + ColumnConfig::new("Due Date", "due_date") + .with_width(100.0) + .hidden(), + ColumnConfig::new("Previous Borrower", "previous_borrower_name") + .with_width(130.0) + .hidden(), + ColumnConfig::new("No Scan", "no_scan") + .with_width(70.0) + .hidden(), + ColumnConfig::new("Notes", "notes") + .with_width(200.0) + .hidden(), + ColumnConfig::new("Created Date", "created_date") + .with_width(140.0) + .hidden(), + ColumnConfig::new("Created By", "created_by_username") + .with_width(100.0) + .hidden(), + ColumnConfig::new("Last Modified", "last_modified_date").with_width(70.0), // Visible by default + ColumnConfig::new("Modified By", "last_modified_by_username") + .with_width(100.0) + .hidden(), + ]; + + Self { + assets: Vec::new(), + loading_state: LoadingState::new(), + table_renderer: TableRenderer::new() + .with_columns(columns) + .with_default_sort("last_modified_date", false), // Sort by last modified, newest first + show_column_panel: false, + last_show_retired_state: true, // Default to showing retired items (matches ribbon default) + last_item_lookup: String::new(), + delete_dialog: ConfirmDialog::new( + "Delete Asset", + "Are you sure you want to delete this asset?", + ), + edit_dialog: FormBuilder::new("Edit Asset", vec![]), + add_dialog: FormBuilder::new("Add Asset", vec![]), + advanced_edit_dialog: FormBuilder::new("Advanced Edit Asset", vec![]), + print_dialog: None, + show_print_dialog: false, + pending_delete_ids: Vec::new(), + pending_edit_ids: Vec::new(), + is_bulk_edit: false, + add_from_template_workflow: AddFromTemplateWorkflow::new(), + borrow_flow: BorrowFlow::new(), + return_flow: ReturnFlow::new(), + show_help: false, + help_cache: CommonMarkCache::default(), + } + } + + /// Load assets from the API + fn load_assets( + &mut self, + api_client: &ApiClient, + limit: Option, + where_clause: Option, + filter: Option, + ) { + self.loading_state.start_loading(); + + match DataLoader::load_assets(api_client, limit, where_clause, filter) { + Ok(assets) => { + self.assets = assets; + // Enrich borrower/due_date columns from lending_history for accuracy + self.enrich_loans_for_visible_assets(api_client); + self.loading_state.finish_success(); + } + Err(e) => { + self.loading_state.finish_error(e); + } + } + } + + /// Load assets with retired filter applied + fn load_assets_with_filter( + &mut self, + api_client: &ApiClient, + limit: Option, + show_retired: bool, + ) { + let filter = if show_retired { + None // Show all items including retired + } else { + // Filter out retired items: WHERE status != 'Retired' + Some(serde_json::json!({ + "and": [ + { + "column": "assets.status", + "op": "!=", + "value": "Retired" + } + ] + })) + }; + + self.load_assets(api_client, limit, None, filter); + } + + /// Enrich current/previous borrower and due_date using active and recent loans + fn enrich_loans_for_visible_assets(&mut self, api_client: &ApiClient) { + // Collect visible asset IDs + let mut ids: Vec = Vec::new(); + for asset in &self.assets { + if let Some(id) = asset.get("id").and_then(|v| v.as_i64()) { + ids.push(id); + } + } + if ids.is_empty() { + return; + } + + // Build active loans map: asset_id -> (borrower_name, due_date) + let mut active_map: HashMap, Option)> = HashMap::new(); + if let Ok(active_loans) = crate::core::get_active_loans(api_client, None) { + for row in active_loans { + let aid = row.get("asset_id").and_then(|v| v.as_i64()); + if let Some(asset_id) = aid { + let borrower_name = row + .get("borrower_name") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let due = row + .get("due_date") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + active_map.insert(asset_id, (borrower_name, due)); + } + } + } + + // Build recent returns map: asset_id -> borrower_name (most recent return) + let mut recent_return_map: HashMap = HashMap::new(); + if let Ok(recent_returns) = + crate::core::get_recent_returns_for_assets(api_client, &ids, Some(1), None) + { + for row in recent_returns { + if let Some(asset_id) = row.get("asset_id").and_then(|v| v.as_i64()) { + if let Some(name) = row.get("borrower_name").and_then(|v| v.as_str()) { + // Only set if not already set (keep most recent as we sorted desc server-side) + recent_return_map + .entry(asset_id) + .or_insert_with(|| name.to_string()); + } + } + } + } + + // Apply enrichment to assets + for asset in &mut self.assets { + let aid = asset.get("id").and_then(|v| v.as_i64()); + if let Some(asset_id) = aid { + // Current borrower and due_date from active loan (authoritative) + if let Some((borrower_name_opt, due_opt)) = active_map.get(&asset_id) { + if let Some(obj) = asset.as_object_mut() { + if let Some(bname) = borrower_name_opt { + obj.insert( + "current_borrower_name".to_string(), + Value::String(bname.clone()), + ); + } + if let Some(due) = due_opt { + obj.insert("due_date".to_string(), Value::String(due.clone())); + } + } + } + + // Previous borrower from most recent returned loan (only when not currently borrowed) + if !active_map.contains_key(&asset_id) { + if let Some(prev_name) = recent_return_map.get(&asset_id) { + if let Some(obj) = asset.as_object_mut() { + obj.insert( + "previous_borrower_name".to_string(), + Value::String(prev_name.clone()), + ); + } + } + } + } + } + } + + /// Get selected asset IDs for bulk operations (works with filtered view) + fn get_selected_ids(&self) -> Vec { + let filtered_data = self.table_renderer.prepare_json_data(&self.assets); + let mut ids = Vec::new(); + for &row_idx in &self.table_renderer.selection.selected_rows { + if let Some((_, asset)) = filtered_data.get(row_idx) { + if let Some(id) = asset.get("id").and_then(|v| v.as_i64()) { + ids.push(id); + } else if let Some(s) = asset.get("id").and_then(|v| v.as_str()) { + if let Ok(n) = s.parse::() { + ids.push(n); + } + } + } + } + ids + } + + /// Find an asset by ID + fn find_asset_by_id(&self, id: i64) -> Option { + AssetOperations::find_by_id(&self.assets, id, |asset| { + asset.get("id").and_then(|v| v.as_i64()) + }) + } + + /// Prepare field configurations for different dialog types + fn prepare_advanced_edit_fields(&mut self, api_client: &ApiClient) { + self.advanced_edit_dialog = AssetFieldBuilder::create_advanced_edit_dialog(api_client); + } + + fn prepare_easy_edit_fields(&mut self, api_client: &ApiClient) { + self.edit_dialog = AssetFieldBuilder::create_easy_edit_dialog(api_client); + } + + fn prepare_add_asset_editor(&mut self, api_client: &ApiClient) { + self.add_dialog = AssetFieldBuilder::create_add_dialog_with_preset(api_client); + } + + /// Helper method to open easy edit dialog with specific asset + fn open_easy_edit_with(&mut self, item: &serde_json::Value, api_client: Option<&ApiClient>) { + log::info!("=== OPENING EASY EDIT ==="); + log::info!("Asset data: {:?}", item); + let asset_id = item.get("id").and_then(|v| v.as_i64()).or_else(|| { + item.get("id") + .and_then(|v| v.as_str()) + .and_then(|s| s.parse().ok()) + }); + log::info!("Extracted asset ID: {:?}", asset_id); + + if let Some(client) = api_client { + self.prepare_easy_edit_fields(client); + } + self.edit_dialog.title = "Easy Edit Asset".to_string(); + self.edit_dialog.open(item); + + log::info!( + "After opening, dialog item_id: {:?}", + self.edit_dialog.item_id + ); + } + + /// Perform item lookup based on asset tag or numeric ID + fn perform_item_lookup( + &mut self, + lookup_text: &str, + api_client: Option<&ApiClient>, + limit: Option, + ) { + if lookup_text != self.last_item_lookup { + self.last_item_lookup = lookup_text.to_string(); + + if !lookup_text.is_empty() { + if let Some(client) = api_client { + // Build filter with OR and LIKE for asset_tag OR asset_numeric_id + let filter = serde_json::json!({ + "or": [ + { + "column": "assets.asset_tag", + "op": "like", + "value": format!("%{}%", lookup_text) + }, + { + "column": "assets.asset_numeric_id", + "op": "like", + "value": format!("%{}%", lookup_text) + } + ] + }); + self.load_assets(client, limit, None, Some(filter)); + } + } else { + // Clear search when lookup is empty + if let Some(client) = api_client { + self.load_assets(client, limit, None, None); + } + } + } + } + + /// Apply updates using the extracted operations module + fn apply_updates( + &mut self, + api: &ApiClient, + updated: serde_json::Map, + limit: Option, + ) { + let assets = self.assets.clone(); + let easy_id = self.edit_dialog.item_id.clone(); + let advanced_id = self.advanced_edit_dialog.item_id.clone(); + + AssetOperations::apply_updates( + api, + updated, + &mut self.pending_edit_ids, + easy_id.as_deref(), + advanced_id.as_deref(), + |id| { + AssetOperations::find_by_id(&assets, id, |asset| { + asset.get("id").and_then(|v| v.as_i64()) + }) + }, + limit, + |api_client, limit| { + match DataLoader::load_assets(api_client, limit, None, None) { + Ok(_) => { /* Assets will be reloaded after this call */ } + Err(e) => log::error!("Failed to reload assets: {}", e), + } + }, + ); + + // Reload assets after update + match DataLoader::load_assets(api, limit, None, None) { + Ok(assets) => { + self.assets = assets; + } + Err(e) => log::error!("Failed to reload assets after update: {}", e), + } + + // Reset selection state after updates + self.is_bulk_edit = false; + self.table_renderer.selection.clear_selection(); + } + + fn render_table_with_events( + &mut self, + ui: &mut egui::Ui, + api_client: Option<&ApiClient>, + session_manager: &std::sync::Arc>, + ) { + // We need to work around Rust's borrowing rules here + // First, get the data we need + let assets_clone = self.assets.clone(); + let prepared_data = self.table_renderer.prepare_json_data(&assets_clone); + + // Create a temporary event handler that stores actions for later processing + let mut deferred_actions: Vec = Vec::new(); + let mut temp_handler = TempInventoryEventHandler { + deferred_actions: &mut deferred_actions, + }; + + // Render table with the temporary event handler + self.table_renderer + .render_json_table(ui, &prepared_data, Some(&mut temp_handler)); + + // Process the deferred actions + self.process_temp_deferred_actions(deferred_actions, api_client, session_manager); + } + + fn process_temp_deferred_actions( + &mut self, + actions: Vec, + api_client: Option<&ApiClient>, + session_manager: &std::sync::Arc>, + ) { + for action in actions { + match action { + DeferredAction::DoubleClick(asset) => { + log::info!( + "Processing double-click edit for asset: {:?}", + asset.get("name") + ); + self.open_easy_edit_with(&asset, api_client); + } + DeferredAction::ContextClone(asset) => { + log::info!( + "Processing context menu clone for asset: {:?}", + asset.get("name") + ); + if let Some(client) = api_client { + // Use full add dialog so all fields are available when cloning + self.add_dialog = + crate::core::asset_fields::AssetFieldBuilder::create_full_add_dialog( + client, + ); + } + // Prepare cloned payload using shared helper + let cloned = crate::core::components::prepare_cloned_value( + &asset, + &[ + "id", + "asset_numeric_id", + "created_date", + "created_at", + "last_modified_date", + "last_modified", + "last_modified_by", + "last_modified_by_username", + "current_borrower_name", + "previous_borrower_name", + "last_audit", + "last_audit_status", + "due_date", + ], + Some("name"), + Some(""), + ); + self.add_dialog.title = "Add Asset".to_string(); + if let Some(obj) = cloned.as_object() { + // Use open_new so all preset fields are treated as new values and will be saved + self.add_dialog.open_new(Some(obj)); + } else { + self.add_dialog.open(&cloned); + } + } + DeferredAction::ContextEdit(asset) => { + log::info!( + "Processing context menu edit for asset: {:?}", + asset.get("name") + ); + self.open_easy_edit_with(&asset, api_client); + } + DeferredAction::ContextAdvancedEdit(asset) => { + log::info!( + "Processing context menu advanced edit for asset: {:?}", + asset.get("name") + ); + if let Some(client) = api_client { + self.prepare_advanced_edit_fields(client); + } + self.advanced_edit_dialog.open(&asset); + } + DeferredAction::ContextDelete(asset) => { + if let Some(id) = asset.get("id").and_then(|v| v.as_i64()) { + log::info!("Processing context menu delete for asset ID: {}", id); + self.pending_delete_ids = vec![id]; + let name = asset + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("Unknown") + .to_string(); + self.delete_dialog.open(name, id.to_string()); + } + } + DeferredAction::ContextLend(asset) => { + log::info!( + "Opening borrow flow from inventory for asset: {:?}", + asset.get("name") + ); + if let Some(client) = api_client { + // Open flow, then preselect this asset and skip to borrower step + self.borrow_flow.open(client); + self.borrow_flow.selected_asset = Some(asset.clone()); + self.borrow_flow.current_step = BorrowStep::SelectBorrower; + } + } + DeferredAction::ContextReturn(asset) => { + log::info!( + "Opening return flow from inventory for asset: {:?}", + asset.get("name") + ); + if let Some(client) = api_client { + self.return_flow.open(client); + // Try to preselect the matching active loan by asset_id or asset_tag + let asset_id = asset.get("id").and_then(|v| v.as_i64()); + let asset_tag = asset.get("asset_tag").and_then(|v| v.as_str()); + if let Some(loan) = self + .return_flow + .active_loans + .iter() + .find(|loan| { + let loan_asset_id = loan.get("asset_id").and_then(|v| v.as_i64()); + let loan_tag = loan.get("asset_tag").and_then(|v| v.as_str()); + (asset_id.is_some() && loan_asset_id == asset_id) + || (asset_tag.is_some() && loan_tag == asset_tag) + }) + .cloned() + { + self.return_flow.selected_loan = Some(loan); + self.return_flow.current_step = ReturnStep::Confirm; + } + } + } + DeferredAction::ContextPrintLabel(asset) => { + log::info!("Processing print label for asset: {:?}", asset.get("name")); + self.open_print_dialog(&asset, api_client, false, session_manager); + } + DeferredAction::ContextAdvancedPrint(asset) => { + log::info!( + "Processing advanced print for asset: {:?}", + asset.get("name") + ); + self.open_print_dialog(&asset, api_client, true, session_manager); + } + } + } + } + + /// Insert new asset and return its DB id if available + fn insert_new_asset( + &mut self, + client: &ApiClient, + limit: Option, + mut data: serde_json::Map, + ) -> Option { + AssetOperations::preprocess_quick_adds(client, &mut data); + AssetOperations::insert_new_asset(client, data, limit, |api_client, limit| { + self.load_assets(api_client, limit, None, None) + }) + } + + /// Open print dialog for an asset + fn open_print_dialog( + &mut self, + asset: &Value, + api_client: Option<&ApiClient>, + force_advanced: bool, + session_manager: &std::sync::Arc>, + ) { + use std::collections::HashMap; + + // Extract asset data as strings for template rendering + let mut asset_data = HashMap::new(); + if let Some(obj) = asset.as_object() { + for (key, value) in obj { + let value_str = match value { + Value::String(s) => s.clone(), + Value::Number(n) => n.to_string(), + Value::Bool(b) => b.to_string(), + Value::Null => String::new(), + _ => serde_json::to_string(value).unwrap_or_default(), + }; + asset_data.insert(key.clone(), value_str); + } + } + + // Get label template ID from asset + let label_template_id = asset.get("label_template_id").and_then(|v| v.as_i64()); + + // Get default and last-used printer from session + let (default_printer_id, last_printer_id) = { + let guard = session_manager.blocking_lock(); + let default_id = guard.get_default_printer_id(); + let last_printer = guard.get_last_print_preferences(); + (default_id, last_printer) + }; + + // Smart logic: if not forcing advanced AND both default printer and template are set, print directly + if !force_advanced && default_printer_id.is_some() && label_template_id.is_some() { + // Print directly without dialog + log::info!("Printing directly with default printer and template"); + if let Some(client) = api_client { + if let (Some(printer_id), Some(template_id)) = + (default_printer_id, label_template_id) + { + self.execute_print(client, printer_id, template_id, &asset_data); + } + } + } else { + // Show dialog + let mut dialog = crate::core::print::PrintDialog::new(asset_data); + dialog = dialog.with_defaults(default_printer_id, label_template_id, last_printer_id); + + if let Some(client) = api_client { + if let Err(e) = dialog.load_data(client) { + log::error!("Failed to load print dialog data: {}", e); + } + } + + self.print_dialog = Some(dialog); + self.show_print_dialog = true; + } + } + + /// Execute print job via print dialog + fn execute_print( + &mut self, + api_client: &ApiClient, + printer_id: i64, + template_id: i64, + asset_data: &HashMap, + ) { + log::info!( + "Executing print: printer_id={}, template_id={}", + printer_id, + template_id + ); + + // Create a print dialog with the options and execute + // The dialog handles all printer settings loading, JSON parsing, and printing + let mut dialog = crate::core::print::PrintDialog::new(asset_data.clone()).with_defaults( + Some(printer_id), + Some(template_id), + None, + ); + + match dialog.execute_print(api_client) { + Ok(_) => { + log::info!("Successfully printed label"); + self.log_print_history(api_client, asset_data, printer_id, template_id, "Success"); + } + Err(e) => { + log::error!("Failed to print label: {}", e); + self.log_print_history( + api_client, + asset_data, + printer_id, + template_id, + &format!("Error: {}", e), + ); + } + } + } + + fn log_print_history( + &self, + _api_client: &ApiClient, + _asset_data: &HashMap, + _printer_id: i64, + _template_id: i64, + status: &str, + ) { + // Print history logging disabled - backend doesn't support raw SQL queries + // and using insert() requires __editor_item_id column which is a log table issue + // TODO: Either add raw SQL support to backend or create a dedicated /print-history endpoint + log::debug!("Print job status: {}", status); + } + + pub fn show( + &mut self, + ui: &mut egui::Ui, + api_client: Option<&ApiClient>, + ribbon_ui: Option<&mut crate::ui::ribbon::RibbonUI>, + session_manager: &std::sync::Arc>, + ) { + // 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 + // Also skip auto-load if there's a pending filter change (which will load filtered results) + let has_pending_filter = ribbon_ui + .as_ref() + .map(|r| { + *r.checkboxes + .get("inventory_filter_changed") + .unwrap_or(&false) + }) + .unwrap_or(false); + + if self.assets.is_empty() + && !self.loading_state.is_loading + && self.loading_state.last_error.is_none() + && self.loading_state.last_load_time.is_none() // Only auto-load if we've never attempted a load + && !has_pending_filter + // Don't auto-load if a filter is about to be applied + { + if let Some(client) = api_client { + log::info!("Inventory view never loaded, triggering initial auto-load"); + // Respect retired filter state from ribbon during auto-load + let show_retired = ribbon_ui + .as_ref() + .map(|r| *r.checkboxes.get("show_retired").unwrap_or(&true)) + .unwrap_or(true); + self.load_assets_with_filter(client, Some(100), show_retired); + } + } + + // Render content and get flags to clear + let flags_to_clear = self.render_content( + ui, + api_client, + Some(100), + ribbon_ui.as_ref().map(|r| &**r), + session_manager, + ); + + // Clear the flags after processing + if let Some(ribbon) = ribbon_ui { + for flag in flags_to_clear { + ribbon.checkboxes.insert(flag, false); + } + } + } + + fn render_content( + &mut self, + ui: &mut egui::Ui, + api_client: Option<&ApiClient>, + limit: Option, + ribbon_ui: Option<&crate::ui::ribbon::RibbonUI>, + session_manager: &std::sync::Arc>, + ) -> Vec { + // Handle ribbon actions first + let flags_to_clear = if let Some(ribbon) = ribbon_ui { + self.handle_ribbon_actions(ribbon, api_client, session_manager) + } else { + Vec::new() + }; + + // Top toolbar with search and actions + ui.horizontal(|ui| { + ui.label("Search:"); + let mut search_changed = false; + ui.add_enabled_ui(!self.loading_state.is_loading, |ui| { + search_changed = ui + .text_edit_singleline(&mut self.table_renderer.search_query) + .changed(); + }); + + ui.separator(); + + // Action buttons + if let Some(client) = api_client { + if ui.button("Refresh").clicked() { + // Clear any previous error state when user explicitly refreshes + self.loading_state.last_error = None; + + // Respect current filters when refreshing + if let Some(ribbon) = ribbon_ui { + let show_retired = ribbon + .checkboxes + .get("show_retired") + .copied() + .unwrap_or(true); + let user_filter = ribbon.filter_builder.get_filter_json("assets"); + let combined_filter = self.combine_filters(show_retired, user_filter); + self.load_assets(client, limit, None, combined_filter); + } else { + self.load_assets(client, limit, None, None); + } + } + + if ui.button("➕ Add Asset").clicked() { + self.prepare_add_asset_editor(client); + } + + // Show selection count but no buttons (use right-click instead) + let selected_count = self.table_renderer.selection.get_selected_count(); + if selected_count > 0 { + ui.label(format!("{} selected", selected_count)); + } + } + + ui.separator(); + + if ui.button("⚙ Columns").clicked() { + self.show_column_panel = !self.show_column_panel; + } + }); + + ui.separator(); + + // Show loading state + if self.loading_state.is_loading { + ui.horizontal(|ui| { + ui.spinner(); + ui.label("Loading assets..."); + }); + } + + // Show errors + if let Some(error) = self.loading_state.get_error() { + ui.colored_label(egui::Color32::RED, format!("Error: {}", error)); + } + + // Column configuration panel + if self.show_column_panel { + egui::Window::new("Column Configuration") + .open(&mut self.show_column_panel) + .resizable(true) + .movable(true) + .default_width(350.0) + .min_width(300.0) + .max_width(500.0) + .max_height(600.0) + .default_pos([200.0, 150.0]) + .show(ui.ctx(), |ui| { + ui.label("Show/Hide Columns:"); + ui.separator(); + + // Scrollable area for columns + egui::ScrollArea::vertical() + .max_height(450.0) + .show(ui, |ui| { + // Use columns layout to make better use of width while keeping groups intact + ui.columns(2, |columns| { + // Left column + columns[0].group(|ui| { + ui.strong("Basic Information"); + ui.separator(); + for column in &mut self.table_renderer.columns { + if matches!( + column.field.as_str(), + "name" | "asset_tag" | "asset_type" | "status" + ) { + ui.checkbox(&mut column.visible, &column.name); + } + } + }); + + columns[0].add_space(5.0); + + columns[0].group(|ui| { + ui.strong("Location & Status"); + ui.separator(); + for column in &mut self.table_renderer.columns { + if matches!( + column.field.as_str(), + "zone_code" + | "zone_plus" + | "zone_note" + | "lending_status" + ) { + ui.checkbox(&mut column.visible, &column.name); + } + } + }); + + columns[0].add_space(5.0); + + columns[0].group(|ui| { + ui.strong("Quantities & Lending"); + ui.separator(); + for column in &mut self.table_renderer.columns { + if matches!( + column.field.as_str(), + "quantity_available" + | "quantity_total" + | "quantity_used" + | "lendable" + | "minimum_role_for_lending" + | "current_borrower_name" + | "due_date" + | "previous_borrower_name" + ) { + ui.checkbox(&mut column.visible, &column.name); + } + } + }); + + // Right column + columns[1].group(|ui| { + ui.strong("Classification"); + ui.separator(); + for column in &mut self.table_renderer.columns { + if matches!( + column.field.as_str(), + "category_name" + | "manufacturer" + | "model" + | "serial_number" + ) { + ui.checkbox(&mut column.visible, &column.name); + } + } + }); + + columns[1].add_space(5.0); + + columns[1].group(|ui| { + ui.strong("Financial & Dates"); + ui.separator(); + for column in &mut self.table_renderer.columns { + if matches!( + column.field.as_str(), + "price" + | "purchase_date" + | "warranty_until" + | "expiry_date" + | "last_audit" + | "last_audit_status" + ) { + ui.checkbox(&mut column.visible, &column.name); + } + } + }); + + columns[1].add_space(5.0); + + columns[1].group(|ui| { + ui.strong("Metadata & Other"); + ui.separator(); + for column in &mut self.table_renderer.columns { + if matches!( + column.field.as_str(), + "id" | "asset_numeric_id" + | "supplier_name" + | "no_scan" + | "notes" + | "created_date" + | "created_by_username" + | "last_modified_date" + | "last_modified_by_username" + ) { + ui.checkbox(&mut column.visible, &column.name); + } + } + }); + }); + }); + + ui.separator(); + ui.columns(3, |columns| { + if columns[0].button("Show All").clicked() { + for column in &mut self.table_renderer.columns { + column.visible = true; + } + } + if columns[1].button("Hide All").clicked() { + for column in &mut self.table_renderer.columns { + column.visible = false; + } + } + if columns[2].button("Reset to Default").clicked() { + // Reset to default visibility + for column in &mut self.table_renderer.columns { + column.visible = matches!( + column.field.as_str(), + "asset_tag" + | "asset_type" + | "name" + | "category_name" + | "manufacturer" + | "model" + | "zone_code" + | "status" + | "lending_status" + | "last_modified_date" + ); + } + } + }); + }); + } + + // Render table with event handling + self.render_table_with_events(ui, api_client, session_manager); + + // Handle dialogs + self.handle_dialogs(ui, api_client, limit, session_manager); + + // Process deferred actions from table events + self.process_deferred_actions(ui, api_client, limit, session_manager); + + flags_to_clear + } + + fn handle_dialogs( + &mut self, + ui: &mut egui::Ui, + api_client: Option<&ApiClient>, + limit: Option, + session_manager: &std::sync::Arc>, + ) { + // Delete confirmation dialog + if let Some(confirmed) = self.delete_dialog.show_dialog(ui.ctx()) { + log::info!( + "Delete dialog result: confirmed={}, pending_delete_ids={:?}", + confirmed, + self.pending_delete_ids + ); + if confirmed && !self.pending_delete_ids.is_empty() { + if let Some(client) = api_client { + for id in &self.pending_delete_ids { + let where_clause = serde_json::json!({"id": id}); + log::info!( + "Sending DELETE for asset id {} with where {:?}", + id, + where_clause + ); + match client.delete("assets", where_clause) { + Ok(resp) => { + if resp.success { + let deleted = resp.data.unwrap_or(0); + log::info!( + "Delete success for asset {} ({} row(s) affected)", + id, + deleted + ); + } else { + log::error!( + "Server rejected delete for asset {}: {:?}", + id, + resp.error + ); + } + } + Err(e) => { + log::error!("Failed to delete asset {}: {}", id, e); + } + } + } + // Reload after attempting deletes + self.load_assets(client, limit, None, None); + } else { + log::error!("No API client available for delete operation"); + } + self.pending_delete_ids.clear(); + } + } + + // Edit dialogs + if let Some(Some(updated)) = self.edit_dialog.show_editor(ui.ctx()) { + if let Some(client) = api_client { + self.apply_updates(client, updated, limit); + } + } + + if let Some(Some(updated)) = self.advanced_edit_dialog.show_editor(ui.ctx()) { + if let Some(client) = api_client { + self.apply_updates(client, updated, limit); + } + } + + if let Some(Some(mut new_data)) = self.add_dialog.show_editor(ui.ctx()) { + if let Some(client) = api_client { + // Check if user requested label printing after add + let print_after_add = new_data + .get("print_label") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + // Preflight: enforce asset_tag uniqueness if provided + if let Some(tag) = new_data.get("asset_tag").and_then(|v| v.as_str()) { + let tag = tag.trim(); + if !tag.is_empty() { + let where_clause = serde_json::json!({ "asset_tag": tag }); + if let Ok(resp) = client.select( + "assets", + Some(vec!["id".into()]), + Some(where_clause), + None, + Some(1), + ) { + if resp.success { + if let Some(rows) = resp.data { + if !rows.is_empty() { + // Tag already exists; reopen editor and require change + log::warn!("Asset tag '{}' already exists; prompting user to change it", tag); + // Put back the print flag and reopen the dialog with current values + self.add_dialog.title = "Add Asset".to_string(); + self.add_dialog + .open(&serde_json::Value::Object(new_data.clone())); + return; // Don't proceed to insert or print + } + } + } else { + log::error!("Tag uniqueness check failed: {:?}", resp.error); + } + } + } + } + + // Prepare asset data snapshot for printing (before filtering removes fields) + let mut print_snapshot = new_data.clone(); + // Remove the UI-only flag so it doesn't get sent to the server + new_data.remove("print_label"); + + // Insert the asset and capture DB id + let inserted_id = self.insert_new_asset(client, limit, new_data); + + // If requested, trigger printing with smart defaults: + // - If default printer and label_template_id are set -> print directly + // - Otherwise open the print dialog to ask what to do + if print_after_add { + if let Some(id) = inserted_id { + let id_val = Value::Number(id.into()); + // Expose id under both `id` and `asset_id` so label templates can pick it up + print_snapshot.insert("id".to_string(), id_val.clone()); + print_snapshot.insert("asset_id".to_string(), id_val); + // Try to also include asset_numeric_id from the freshly reloaded assets + if let Some(row) = self + .assets + .iter() + .find(|r| r.get("id").and_then(|v| v.as_i64()) == Some(id)) + { + if let Some(n) = row.get("asset_numeric_id").and_then(|v| v.as_i64()) { + print_snapshot.insert( + "asset_numeric_id".to_string(), + Value::Number(n.into()), + ); + } + } + } + let asset_val = Value::Object(print_snapshot); + self.open_print_dialog(&asset_val, api_client, false, session_manager); + } + } + } + + // Workflow handling + if let Some(client) = api_client { + // Handle add from template workflow + if let Some(asset_data) = self.add_from_template_workflow.show(ui, client) { + if let Value::Object(mut map) = asset_data { + // Extract optional print flag before insert + let print_after_add = map + .get("print_label") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + // Keep a snapshot for printing (includes label_template_id etc.) + let mut print_snapshot = map.clone(); + // Remove UI-only field so it doesn't get sent to server + map.remove("print_label"); + + // Insert the asset and capture DB id + let inserted_id = self.insert_new_asset(client, limit, map); + + // If requested, perform printing via smart defaults + if print_after_add { + if let Some(id) = inserted_id { + let id_val = Value::Number(id.into()); + print_snapshot.insert("id".to_string(), id_val.clone()); + print_snapshot.insert("asset_id".to_string(), id_val); + // Try to also include asset_numeric_id from the freshly reloaded assets + if let Some(row) = self + .assets + .iter() + .find(|r| r.get("id").and_then(|v| v.as_i64()) == Some(id)) + { + if let Some(n) = + row.get("asset_numeric_id").and_then(|v| v.as_i64()) + { + print_snapshot.insert( + "asset_numeric_id".to_string(), + Value::Number(n.into()), + ); + } + } + } + let asset_val = Value::Object(print_snapshot); + self.open_print_dialog(&asset_val, api_client, false, session_manager); + } + } + } + + // Show help window if requested + if self.show_help { + const HELP_TEXT: &str = r#"# Inventory Management + +## Quick Actions +- **Add Asset**: Create new assets from templates or blank forms +- **Edit Asset**: Modify existing asset details +- **Delete Asset**: Remove assets (requires confirmation) +- **Print Label**: Generate and print asset labels + +## Borrowing/Lending +- **Borrow**: Check out assets to users +- **Return**: Check assets back in + +## Filtering +- Use **Show Retired** to include/exclude retired assets +- **Item Lookup** searches across multiple fields +- Click **Filters** to build advanced queries + +## Tips +- Double-click a row to quick-edit +- Right-click for context menu options +- Use Ctrl+Click to select multiple items +"#; + show_help_window( + ui.ctx(), + &mut self.help_cache, + "inventory_help", + "Inventory Help", + HELP_TEXT, + &mut self.show_help, + HelpWindowOptions::default(), + ); + } + + // Show borrow/return flows if open and reload when they complete successfully + self.borrow_flow.show(ui.ctx(), client); + if self.borrow_flow.take_recent_success() { + self.load_assets(client, limit, None, None); + } + + if self.return_flow.show(ui.ctx(), client) { + // still open + } else if self.return_flow.success_message.is_some() { + self.load_assets(client, limit, None, None); + } + } + + // Print dialog + if self.show_print_dialog { + let mut should_clear_dialog = false; + let mut completed_options: Option = None; + let mut completed_asset_data: Option> = None; + + if let Some(dialog) = self.print_dialog.as_mut() { + let mut open = self.show_print_dialog; + let completed = dialog.show(ui.ctx(), &mut open, api_client); + self.show_print_dialog = open; + + if completed { + completed_options = Some(dialog.options().clone()); + completed_asset_data = Some(dialog.asset_data().clone()); + should_clear_dialog = true; + } else if !self.show_print_dialog { + // Dialog was closed without completing the print job + should_clear_dialog = true; + } + } else { + self.show_print_dialog = false; + } + + if should_clear_dialog { + self.print_dialog = None; + } + + if let (Some(options), Some(asset_data)) = (completed_options, completed_asset_data) { + if let (Some(printer_id), Some(template_id)) = + (options.printer_id, options.label_template_id) + { + if let Some(client) = api_client { + self.log_print_history( + client, + &asset_data, + printer_id, + template_id, + "Success", + ); + } + } + } + } + } + + fn process_deferred_actions( + &mut self, + ui: &mut egui::Ui, + api_client: Option<&ApiClient>, + _limit: Option, + session_manager: &std::sync::Arc>, + ) { + // Handle double-click edit + if let Some(asset) = ui + .ctx() + .data_mut(|d| d.remove_temp::(egui::Id::new("double_click_edit"))) + { + log::info!( + "Processing double-click edit for asset: {:?}", + asset.get("name") + ); + self.open_easy_edit_with(&asset, api_client); + } + + // Handle context menu actions + if let Some(asset) = ui + .ctx() + .data_mut(|d| d.remove_temp::(egui::Id::new("context_menu_edit"))) + { + log::info!( + "Processing context menu edit for asset: {:?}", + asset.get("name") + ); + self.open_easy_edit_with(&asset, api_client); + } + + if let Some(asset) = ui + .ctx() + .data_mut(|d| d.remove_temp::(egui::Id::new("context_menu_edit_adv"))) + { + log::info!( + "Processing context menu advanced edit for asset: {:?}", + asset.get("name") + ); + if let Some(client) = api_client { + self.prepare_advanced_edit_fields(client); + } + self.advanced_edit_dialog.open(&asset); + } + + if let Some(asset) = ui + .ctx() + .data_mut(|d| d.remove_temp::(egui::Id::new("context_menu_delete"))) + { + if let Some(id) = asset.get("id").and_then(|v| v.as_i64()) { + log::info!("Processing context menu delete for asset ID: {}", id); + self.pending_delete_ids = vec![id]; + self.delete_dialog.show = true; + } + } + + // Handle print label actions + if let Some(asset) = ui + .ctx() + .data_mut(|d| d.remove_temp::(egui::Id::new("context_menu_print"))) + { + log::info!( + "Processing context menu print for asset: {:?}", + asset.get("name") + ); + self.open_print_dialog(&asset, api_client, false, session_manager); + } + + if let Some(asset) = ui + .ctx() + .data_mut(|d| d.remove_temp::(egui::Id::new("context_menu_print_adv"))) + { + log::info!( + "Processing context menu advanced print for asset: {:?}", + asset.get("name") + ); + self.open_print_dialog(&asset, api_client, true, session_manager); + } + } + + /// Handle ribbon checkbox actions (main integration point) + /// Returns a list of flags that should be cleared after processing + fn handle_ribbon_actions( + &mut self, + ribbon: &crate::ui::ribbon::RibbonUI, + api_client: Option<&ApiClient>, + session_manager: &std::sync::Arc>, + ) -> Vec { + let mut flags_to_clear = Vec::new(); + + // Handle help button + if *ribbon + .checkboxes + .get("inventory_show_help") + .unwrap_or(&false) + { + flags_to_clear.push("inventory_show_help".to_string()); + self.show_help = true; + } + + // Handle limit settings first + let limit = if *ribbon + .checkboxes + .get("inventory_no_limit") + .unwrap_or(&false) + { + None + } else { + Some(*ribbon.number_fields.get("inventory_limit").unwrap_or(&100)) + }; + + // Check if retired filter state changed and trigger refresh if needed + let show_retired = *ribbon.checkboxes.get("show_retired").unwrap_or(&true); + if self.last_show_retired_state != show_retired { + self.last_show_retired_state = show_retired; + if let Some(client) = api_client { + self.loading_state.last_error = None; + self.load_assets_with_filter(client, limit, show_retired); + return flags_to_clear; // Early return to avoid duplicate loading + } + } + + // Handle filter builder changes + if *ribbon + .checkboxes + .get("inventory_filter_changed") + .unwrap_or(&false) + { + flags_to_clear.push("inventory_filter_changed".to_string()); + if let Some(client) = api_client { + self.loading_state.last_error = None; + + // Get current show_retired setting + let show_retired = ribbon + .checkboxes + .get("show_retired") + .copied() + .unwrap_or(true); + + // Get user-defined filters from FilterBuilder + let user_filter = ribbon.filter_builder.get_filter_json("assets"); + + // Combine retired filter with user filters + let combined_filter = self.combine_filters(show_retired, user_filter); + + // Debug: Log the filter to see what we're getting + if let Some(ref cf) = combined_filter { + log::info!("Combined filter: {:?}", cf); + } else { + log::info!("No filter conditions (showing all assets)"); + } + + self.load_assets(client, limit, None, combined_filter); + return flags_to_clear; // Early return to avoid duplicate loading + } + } + + let selected_ids = self.get_selected_ids(); + + if *ribbon + .checkboxes + .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 *ribbon + .checkboxes + .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()); + } + } 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(), + ); + } + } + } + + if *ribbon + .checkboxes + .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); + } + } else { + self.edit_dialog.open_new(None); + } + } + } + + if *ribbon + .checkboxes + .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); + } + } else { + self.advanced_edit_dialog.open_new(None); + } + } + } + + if *ribbon + .checkboxes + .get("inventory_action_print_label") + .unwrap_or(&false) + { + if !selected_ids.is_empty() && selected_ids.len() == 1 { + if let Some(asset) = self.find_asset_by_id(selected_ids[0]) { + log::info!("Print label requested for asset: {:?}", asset.get("name")); + // Check if Alt is held for advanced print (ctx not available here, so just default) + self.open_print_dialog(&asset, api_client, false, session_manager); + } + } else if !selected_ids.is_empty() { + log::warn!("Bulk label printing not yet implemented"); + } + } + + // Handle item lookup + if *ribbon + .checkboxes + .get("item_lookup_trigger") + .unwrap_or(&false) + { + if let Some(lookup_text) = ribbon.search_texts.get("item_lookup") { + self.perform_item_lookup(lookup_text, api_client, limit); + } + } + + // Handle limit refresh trigger (when limit changes or refresh is needed) + if *ribbon + .checkboxes + .get("inventory_limit_refresh_trigger") + .unwrap_or(&false) + { + if let Some(client) = api_client { + // Clear any previous error state when refreshing via ribbon + self.loading_state.last_error = None; + self.load_assets(client, limit, None, None); + } + } + + // Handle workflow actions + if *ribbon + .checkboxes + .get("inventory_add_from_template_single") + .unwrap_or(&false) + { + flags_to_clear.push("inventory_add_from_template_single".to_string()); + if let Some(client) = api_client { + self.add_from_template_workflow.start_single_mode(client); + } + } + + if *ribbon + .checkboxes + .get("inventory_add_from_template_multiple") + .unwrap_or(&false) + { + flags_to_clear.push("inventory_add_from_template_multiple".to_string()); + if let Some(client) = api_client { + self.add_from_template_workflow.start_multiple_mode(client); + } + } + + flags_to_clear + } + + /// Combine retired filter with user-defined filters + fn combine_filters(&self, show_retired: bool, user_filter: Option) -> Option { + if show_retired && user_filter.is_none() { + // No filtering needed + return None; + } + + let mut conditions = Vec::new(); + + // Add retired filter if needed + if !show_retired { + conditions.push(serde_json::json!({ + "column": "assets.status", + "op": "!=", + "value": "Retired" + })); + } + + // Add user filter conditions (sanitized for inventory joins) + if let Some(mut filter) = user_filter { + // Map columns from other views (e.g., borrowers.*) to inventory's JOIN aliases + self.sanitize_filter_for_inventory(&mut filter); + if let Some(and_array) = filter.get("and").and_then(|v| v.as_array()) { + conditions.extend(and_array.iter().cloned()); + } else if let Some(_or_array) = filter.get("or").and_then(|v| v.as_array()) { + // Wrap OR conditions to maintain precedence + conditions.push(filter); + } else { + // Single condition + conditions.push(filter); + } + } + + // Return appropriate filter structure + match conditions.len() { + 0 => None, + 1 => { + if let Some(mut only) = conditions.into_iter().next() { + // Final pass to sanitize single-condition filters + self.sanitize_filter_for_inventory(&mut only); + Some(only) + } else { + None + } + } + _ => { + let mut combined = serde_json::json!({ + "and": conditions + }); + self.sanitize_filter_for_inventory(&mut combined); + Some(combined) + } + } + } + + /// Rewrite filter column names to match inventory JOIN aliases + fn sanitize_filter_for_inventory(&self, filter: &mut Value) { + fn rewrite_column(col: &str) -> String { + match col { + // Borrowing view columns → inventory aliases + "borrowers.name" => "current_borrower.name".to_string(), + "borrowers.class_name" => "current_borrower.class_name".to_string(), + // Fallback: leave unchanged + _ => col.to_string(), + } + } + + match filter { + Value::Object(map) => { + // If this object has a `column`, rewrite it + if let Some(Value::String(col)) = map.get_mut("column") { + let new_col = rewrite_column(col); + *col = new_col; + } + // Recurse into possible logical groups + if let Some(Value::Array(arr)) = map.get_mut("and") { + for v in arr.iter_mut() { + self.sanitize_filter_for_inventory(v); + } + } + if let Some(Value::Array(arr)) = map.get_mut("or") { + for v in arr.iter_mut() { + self.sanitize_filter_for_inventory(v); + } + } + } + Value::Array(arr) => { + for v in arr.iter_mut() { + self.sanitize_filter_for_inventory(v); + } + } + _ => {} + } + } +} + +impl Default for InventoryView { + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone)] +enum DeferredAction { + DoubleClick(Value), + ContextEdit(Value), + ContextAdvancedEdit(Value), + ContextDelete(Value), + ContextLend(Value), + ContextReturn(Value), + ContextPrintLabel(Value), + ContextAdvancedPrint(Value), + ContextClone(Value), +} + +// Temporary event handler that collects actions for later processing +struct TempInventoryEventHandler<'a> { + deferred_actions: &'a mut Vec, +} + +impl<'a> TableEventHandler 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())); + } + + 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 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(); + } + + ui.separator(); + + if ui + .button(format!("{} Print Label", egui_phosphor::regular::PRINTER)) + .clicked() + { + log::info!( + "Context menu print label clicked for asset: {:?}", + item.get("name") + ); + self.deferred_actions + .push(DeferredAction::ContextPrintLabel(item.clone())); + ui.close(); + } + + if ui + .button(format!( + "{} Advanced Print", + egui_phosphor::regular::PRINTER + )) + .clicked() + { + log::info!( + "Context menu advanced print clicked for asset: {:?}", + item.get("name") + ); + self.deferred_actions + .push(DeferredAction::ContextAdvancedPrint(item.clone())); + ui.close(); + } + + ui.separator(); + + // Lend/Return options for lendable assets + let lendable = match item.get("lendable") { + Some(serde_json::Value::Bool(b)) => *b, + Some(serde_json::Value::Number(n)) => n.as_i64() == Some(1) || n.as_u64() == Some(1), + Some(serde_json::Value::String(s)) => { + let s = s.to_lowercase(); + s == "true" || s == "1" || s == "yes" || s == "y" + } + _ => false, + }; + // Only act when we have an explicit lending_status; blank usually means non-lendable or unmanaged + let status = item + .get("lending_status") + .and_then(|v| v.as_str()) + .map(|s| s.trim()) + .unwrap_or(""); + + 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(); + } + } 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(); + } + } + 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(); + } + } + + fn on_selection_changed(&mut self, selected_indices: &[usize]) { + log::debug!("Selection changed: {:?}", selected_indices); + } +} -- cgit v1.2.3-70-g09d2