aboutsummaryrefslogtreecommitdiff
path: root/src/ui/inventory.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/inventory.rs')
-rw-r--r--src/ui/inventory.rs1933
1 files changed, 1933 insertions, 0 deletions
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<Value>,
+ 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<crate::core::print::PrintDialog>,
+ show_print_dialog: bool,
+
+ // Bulk action state
+ pending_delete_ids: Vec<i64>,
+ pending_edit_ids: Vec<i64>,
+ 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<u32>,
+ where_clause: Option<Value>,
+ filter: Option<Value>,
+ ) {
+ 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<u32>,
+ 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<i64> = 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<i64, (Option<String>, Option<String>)> = 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<i64, String> = 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<i64> {
+ 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::<i64>() {
+ ids.push(n);
+ }
+ }
+ }
+ }
+ ids
+ }
+
+ /// Find an asset by ID
+ fn find_asset_by_id(&self, id: i64) -> Option<Value> {
+ 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<u32>,
+ ) {
+ 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<String, Value>,
+ limit: Option<u32>,
+ ) {
+ 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<tokio::sync::Mutex<crate::session::SessionManager>>,
+ ) {
+ // 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<DeferredAction> = 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<DeferredAction>,
+ api_client: Option<&ApiClient>,
+ session_manager: &std::sync::Arc<tokio::sync::Mutex<crate::session::SessionManager>>,
+ ) {
+ 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<u32>,
+ mut data: serde_json::Map<String, Value>,
+ ) -> Option<i64> {
+ 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<tokio::sync::Mutex<crate::session::SessionManager>>,
+ ) {
+ 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<String, String>,
+ ) {
+ 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<String, String>,
+ _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<tokio::sync::Mutex<crate::session::SessionManager>>,
+ ) {
+ // 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<u32>,
+ ribbon_ui: Option<&crate::ui::ribbon::RibbonUI>,
+ session_manager: &std::sync::Arc<tokio::sync::Mutex<crate::session::SessionManager>>,
+ ) -> 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)
+ } 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<u32>,
+ session_manager: &std::sync::Arc<tokio::sync::Mutex<crate::session::SessionManager>>,
+ ) {
+ // 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<crate::core::print::PrintOptions> = None;
+ let mut completed_asset_data: Option<HashMap<String, String>> = 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<u32>,
+ session_manager: &std::sync::Arc<tokio::sync::Mutex<crate::session::SessionManager>>,
+ ) {
+ // Handle double-click edit
+ if let Some(asset) = ui
+ .ctx()
+ .data_mut(|d| d.remove_temp::<Value>(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::<Value>(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::<Value>(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::<Value>(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::<Value>(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::<Value>(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<tokio::sync::Mutex<crate::session::SessionManager>>,
+ ) -> Vec<String> {
+ 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<Value>) -> Option<Value> {
+ 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<DeferredAction>,
+}
+
+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()));
+ }
+
+ 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);
+ }
+}