diff options
| author | UMTS at Teleco <crt@teleco.ch> | 2025-12-13 02:51:15 +0100 |
|---|---|---|
| committer | UMTS at Teleco <crt@teleco.ch> | 2025-12-13 02:51:15 +0100 |
| commit | 8323fdd73272a2882781aba3c499ba0be3dff2a6 (patch) | |
| tree | ffbf86473933e69cfaeef30d5c6ea7e5b494856c /src/core/data | |
Diffstat (limited to 'src/core/data')
| -rw-r--r-- | src/core/data/asset_fields.rs | 1008 | ||||
| -rw-r--r-- | src/core/data/counters.rs | 43 | ||||
| -rw-r--r-- | src/core/data/data_loader.rs | 99 | ||||
| -rw-r--r-- | src/core/data/mod.rs | 8 |
4 files changed, 1158 insertions, 0 deletions
diff --git a/src/core/data/asset_fields.rs b/src/core/data/asset_fields.rs new file mode 100644 index 0000000..c9b6a78 --- /dev/null +++ b/src/core/data/asset_fields.rs @@ -0,0 +1,1008 @@ +use crate::api::ApiClient; +use crate::core::components::form_builder::FormBuilder; +use crate::core::{EditorField, FieldType}; +use serde_json::Value; + +/// Struct to hold commonly used dropdown options for assets +pub struct AssetDropdownOptions { + pub asset_types: Vec<(String, String)>, + pub status_options: Vec<(String, String)>, + pub lending_status_options: Vec<(String, String)>, + pub no_scan_options: Vec<(String, String)>, + pub zone_plus_options: Vec<(String, String)>, + pub category_options: Vec<(String, String)>, + pub zone_options: Vec<(String, String)>, + pub supplier_options: Vec<(String, String)>, + pub label_template_options: Vec<(String, String)>, + pub audit_task_options: Vec<(String, String)>, +} + +impl AssetDropdownOptions { + /// Create dropdown options by fetching from API + pub fn new(api_client: &ApiClient) -> Self { + // Static options + let asset_types = vec![ + ("N".to_string(), "Normal".to_string()), + ("B".to_string(), "Basic".to_string()), + ("L".to_string(), "License".to_string()), + ("C".to_string(), "Consumable".to_string()), + ]; + + // Status options: include full set supported by schema. Some installations may use "Retired" while others use "Scrapped". + // We include both to allow selection wherever the backend enum allows it. + let status_options = vec![ + ("Good".to_string(), "Good".to_string()), + ("Attention".to_string(), "Attention".to_string()), + ("Faulty".to_string(), "Faulty".to_string()), + ("Missing".to_string(), "Missing".to_string()), + ("In Repair".to_string(), "In Repair".to_string()), + ("In Transit".to_string(), "In Transit".to_string()), + ("Expired".to_string(), "Expired".to_string()), + ("Unmanaged".to_string(), "Unmanaged".to_string()), + ("Retired".to_string(), "Retired".to_string()), + ("Scrapped".to_string(), "Scrapped".to_string()), + ]; + + let lending_status_options = vec![ + ("Available".to_string(), "Available".to_string()), + ("Borrowed".to_string(), "Borrowed".to_string()), + ("Overdue".to_string(), "Overdue".to_string()), + ("Deployed".to_string(), "Deployed".to_string()), + ( + "Illegally Handed Out".to_string(), + "Illegally Handed Out".to_string(), + ), + ("Stolen".to_string(), "Stolen".to_string()), + ]; + + let no_scan_options = vec![ + ("No".to_string(), "No".to_string()), + ("Ask".to_string(), "Ask".to_string()), + ("Yes".to_string(), "Yes".to_string()), + ]; + + let zone_plus_options = vec![ + ("".into(), "".into()), + ("Floating Local".into(), "Floating Local".into()), + ("Floating Global".into(), "Floating Global".into()), + ("Clarify".into(), "Clarify".into()), + ]; + + // Fetch categories from API + let mut category_options: Vec<(String, String)> = Vec::new(); + if let Ok(resp) = api_client.select( + "categories", + Some(vec!["id".into(), "category_name".into()]), + None, + Some(vec![crate::models::OrderBy { + column: "category_name".into(), + direction: "ASC".into(), + }]), + None, + ) { + if resp.success { + if let Some(data) = resp.data { + for row in data { + let id = row + .get("id") + .and_then(|v| v.as_i64()) + .map(|n| n.to_string()) + .unwrap_or_default(); + let name = row + .get("category_name") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + category_options.push((id, name)); + } + } + } + } + + // Fetch zones from API + let mut zone_options: Vec<(String, String)> = Vec::new(); + if let Ok(resp) = api_client.select( + "zones", + Some(vec!["id".into(), "zone_code".into(), "zone_name".into()]), + None, + Some(vec![crate::models::OrderBy { + column: "zone_code".into(), + direction: "ASC".into(), + }]), + None, + ) { + if resp.success { + if let Some(data) = resp.data { + for row in data { + let id = row + .get("id") + .and_then(|v| v.as_i64()) + .map(|n| n.to_string()) + .unwrap_or_default(); + let code = row + .get("zone_code") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let name = row + .get("zone_name") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + zone_options.push((id, format!("{} - {}", code, name))); + } + } + } + } + + // Fetch suppliers from API + let mut supplier_options: Vec<(String, String)> = Vec::new(); + if let Ok(resp) = api_client.select( + "suppliers", + Some(vec!["id".into(), "name".into()]), + None, + Some(vec![crate::models::OrderBy { + column: "name".into(), + direction: "ASC".into(), + }]), + None, + ) { + if resp.success { + if let Some(data) = resp.data { + for row in data { + let id = row + .get("id") + .and_then(|v| v.as_i64()) + .map(|n| n.to_string()) + .unwrap_or_default(); + let name = row + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + supplier_options.push((id, name)); + } + } + } + } + + // Fetch label templates for dropdown + let mut label_template_options: Vec<(String, String)> = Vec::new(); + if let Ok(resp) = api_client.select( + "label_templates", + Some(vec!["id".into(), "template_name".into()]), + None, + Some(vec![crate::models::OrderBy { + column: "template_name".into(), + direction: "ASC".into(), + }]), + None, + ) { + if resp.success { + if let Some(data) = resp.data { + for row in data { + let id = row + .get("id") + .and_then(|v| v.as_i64()) + .map(|n| n.to_string()) + .unwrap_or_default(); + let name = row + .get("template_name") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + label_template_options.push((id, name)); + } + } + } + } + + // Fetch audit tasks and add default "None" option + let mut audit_task_options: Vec<(String, String)> = + vec![(String::new(), "-- None --".to_string())]; + if let Ok(resp) = api_client.select( + "audit_tasks", + Some(vec!["id".into(), "task_name".into()]), + None, + Some(vec![crate::models::OrderBy { + column: "task_name".into(), + direction: "ASC".into(), + }]), + None, + ) { + if resp.success { + if let Some(data) = resp.data { + for row in data { + if let Some(id) = row.get("id").and_then(|v| v.as_i64()) { + let name = row + .get("task_name") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + audit_task_options.push((id.to_string(), name)); + } + } + } + } + } + + Self { + asset_types, + status_options, + lending_status_options, + no_scan_options, + zone_plus_options, + category_options, + zone_options, + supplier_options, + label_template_options, + audit_task_options, + } + } +} + +/// Asset field configuration builder - provides standardized field definitions for asset forms +pub struct AssetFieldBuilder; + +impl AssetFieldBuilder { + /// Create a Full Add dialog that shows (nearly) all asset fields similar to Advanced Edit, + /// but configured for inserting a new asset (no ID fields, audit fields stay read-only). + pub fn create_full_add_dialog(api_client: &ApiClient) -> FormBuilder { + let options = AssetDropdownOptions::new(api_client); + + let fields: Vec<EditorField> = vec![ + // Core identifiers for new record + EditorField { + name: "asset_tag".into(), + label: "Asset Tag".into(), + field_type: FieldType::Text, + required: true, + read_only: false, + }, + EditorField { + name: "asset_type".into(), + label: "Type".into(), + field_type: FieldType::Dropdown(options.asset_types.clone()), + required: true, + read_only: false, + }, + EditorField { + name: "name".into(), + label: "Name".into(), + field_type: FieldType::Text, + required: true, + read_only: false, + }, + // Classification + EditorField { + name: "category_id".into(), + label: "Category".into(), + field_type: FieldType::Dropdown(options.category_options.clone()), + required: false, + read_only: false, + }, + // Quick add category + EditorField { + name: "new_category_name".into(), + label: "Add Category - Name".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "new_category_code".into(), + label: "Add Category - Code".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + // Location + EditorField { + name: "zone_id".into(), + label: "Zone".into(), + field_type: FieldType::Dropdown(options.zone_options.clone()), + required: false, + read_only: false, + }, + // Quick add zone + EditorField { + name: "new_zone_parent_id".into(), + label: "Add Zone - Parent".into(), + field_type: FieldType::Dropdown(options.zone_options.clone()), + required: false, + read_only: false, + }, + EditorField { + name: "new_zone_mini_code".into(), + label: "Add Zone - Mini Code".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "new_zone_name".into(), + label: "Add Zone - Name".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "zone_plus".into(), + label: "Zone Plus".into(), + field_type: FieldType::Dropdown(options.zone_plus_options.clone()), + required: false, + read_only: false, + }, + EditorField { + name: "no_scan".into(), + label: "No Scan".into(), + field_type: FieldType::Dropdown(options.no_scan_options.clone()), + required: false, + read_only: false, + }, + // Make/model + EditorField { + name: "manufacturer".into(), + label: "Manufacturer".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "model".into(), + label: "Model".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "serial_number".into(), + label: "Serial Number".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + // Status + EditorField { + name: "status".into(), + label: "Status".into(), + field_type: FieldType::Dropdown(options.status_options.clone()), + required: true, + read_only: false, + }, + // Financial / dates + EditorField { + name: "price".into(), + label: "Price".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "purchase_date".into(), + label: "Purchase Date".into(), + field_type: FieldType::Date, + required: false, + read_only: false, + }, + EditorField { + name: "warranty_until".into(), + label: "Warranty Until".into(), + field_type: FieldType::Date, + required: false, + read_only: false, + }, + EditorField { + name: "expiry_date".into(), + label: "Expiry Date".into(), + field_type: FieldType::Date, + required: false, + read_only: false, + }, + // Lendable + EditorField { + name: "lendable".into(), + label: "Lendable".into(), + field_type: FieldType::Checkbox, + required: false, + read_only: false, + }, + EditorField { + name: "lending_status".into(), + label: "Lending Status".into(), + field_type: FieldType::Dropdown(options.lending_status_options.clone()), + required: false, + read_only: false, + }, + EditorField { + name: "due_date".into(), + label: "Due Date".into(), + field_type: FieldType::Date, + required: false, + read_only: false, + }, + // Supplier + EditorField { + name: "supplier_id".into(), + label: "Supplier".into(), + field_type: FieldType::Dropdown(options.supplier_options.clone()), + required: false, + read_only: false, + }, + // Quick add supplier + EditorField { + name: "new_supplier_name".into(), + label: "Add Supplier - Name".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + // Label template + EditorField { + name: "label_template_id".into(), + label: "Label Template".into(), + field_type: FieldType::Dropdown(options.label_template_options.clone()), + required: false, + read_only: false, + }, + // Notes + EditorField { + name: "notes".into(), + label: "Notes".into(), + field_type: FieldType::MultilineText, + required: false, + read_only: false, + }, + // Optional: print on add toggle + EditorField { + name: "print_label".into(), + label: "Print Label".into(), + field_type: FieldType::Checkbox, + required: false, + read_only: false, + }, + // Audit/meta (read-only informational) + EditorField { + name: "created_date".into(), + label: "Created Date".into(), + field_type: FieldType::Text, + required: false, + read_only: true, + }, + EditorField { + name: "created_by_username".into(), + label: "Created By".into(), + field_type: FieldType::Text, + required: false, + read_only: true, + }, + EditorField { + name: "last_modified_date".into(), + label: "Last Modified".into(), + field_type: FieldType::Text, + required: false, + read_only: true, + }, + EditorField { + name: "last_modified_by_username".into(), + label: "Modified By".into(), + field_type: FieldType::Text, + required: false, + read_only: true, + }, + ]; + + let mut dialog = FormBuilder::new("Add Asset (Full)", fields); + + // Prefill sensible defaults + let mut preset = serde_json::Map::new(); + preset.insert("asset_type".to_string(), Value::String("N".to_string())); + preset.insert("status".to_string(), Value::String("Good".to_string())); + preset.insert("lendable".to_string(), Value::Bool(true)); + preset.insert("print_label".to_string(), Value::Bool(true)); + dialog.open_new(Some(&preset)); + + dialog + } + /// Create a comprehensive Advanced Edit dialog with all asset fields + pub fn create_advanced_edit_dialog(api_client: &ApiClient) -> FormBuilder { + let options = AssetDropdownOptions::new(api_client); + + let fields: Vec<EditorField> = vec![ + // Identifiers (read-only) + EditorField { + name: "id".into(), + label: "ID".into(), + field_type: FieldType::Text, + required: false, + read_only: true, + }, + EditorField { + name: "asset_numeric_id".into(), + label: "Numeric ID".into(), + field_type: FieldType::Text, + required: false, + read_only: true, + }, + EditorField { + name: "asset_tag".into(), + label: "Asset Tag".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + // Core fields + EditorField { + name: "asset_type".into(), + label: "Type".into(), + field_type: FieldType::Dropdown(options.asset_types), + required: true, + read_only: false, + }, + EditorField { + name: "name".into(), + label: "Name".into(), + field_type: FieldType::Text, + required: true, + read_only: false, + }, + EditorField { + name: "description".into(), + label: "Description".into(), + field_type: FieldType::MultilineText, + required: false, + read_only: false, + }, + // Classification + EditorField { + name: "category_id".into(), + label: "Category".into(), + field_type: FieldType::Dropdown(options.category_options.clone()), + required: false, + read_only: false, + }, + // Quick add category + EditorField { + name: "new_category_name".into(), + label: "Add Category - Name".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "new_category_code".into(), + label: "Add Category - Code".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + // Location + EditorField { + name: "zone_id".into(), + label: "Zone".into(), + field_type: FieldType::Dropdown(options.zone_options.clone()), + required: false, + read_only: false, + }, + // Quick add zone + EditorField { + name: "new_zone_parent_id".into(), + label: "Add Zone - Parent".into(), + field_type: FieldType::Dropdown(options.zone_options.clone()), + required: false, + read_only: false, + }, + EditorField { + name: "new_zone_mini_code".into(), + label: "Add Zone - Mini Code".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "new_zone_name".into(), + label: "Add Zone - Name".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "zone_plus".into(), + label: "Zone Plus".into(), + field_type: FieldType::Dropdown(options.zone_plus_options), + required: false, + read_only: false, + }, + EditorField { + name: "no_scan".into(), + label: "No Scan".into(), + field_type: FieldType::Dropdown(options.no_scan_options), + required: false, + read_only: false, + }, + EditorField { + name: "audit_task_id".into(), + label: "Audit Task".into(), + field_type: FieldType::Dropdown(options.audit_task_options.clone()), + required: false, + read_only: false, + }, + // Make/model + EditorField { + name: "manufacturer".into(), + label: "Manufacturer".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "model".into(), + label: "Model".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "serial_number".into(), + label: "Serial Number".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + // Status + EditorField { + name: "status".into(), + label: "Status".into(), + field_type: FieldType::Dropdown(options.status_options), + required: true, + read_only: false, + }, + // Financial / dates + EditorField { + name: "price".into(), + label: "Price".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "purchase_date".into(), + label: "Purchase Date".into(), + field_type: FieldType::Date, + required: false, + read_only: false, + }, + EditorField { + name: "warranty_until".into(), + label: "Warranty Until".into(), + field_type: FieldType::Date, + required: false, + read_only: false, + }, + EditorField { + name: "expiry_date".into(), + label: "Expiry Date".into(), + field_type: FieldType::Date, + required: false, + read_only: false, + }, + // Lendable + EditorField { + name: "lendable".into(), + label: "Lendable".into(), + field_type: FieldType::Checkbox, + required: false, + read_only: false, + }, + EditorField { + name: "lending_status".into(), + label: "Lending Status".into(), + field_type: FieldType::Dropdown(options.lending_status_options), + required: false, + read_only: false, + }, + EditorField { + name: "due_date".into(), + label: "Due Date".into(), + field_type: FieldType::Date, + required: false, + read_only: false, + }, + EditorField { + name: "supplier_id".into(), + label: "Supplier".into(), + field_type: FieldType::Dropdown(options.supplier_options), + required: false, + read_only: false, + }, + // Quick add supplier + EditorField { + name: "new_supplier_name".into(), + label: "Add Supplier - Name".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + // Borrowers (read-only historical) + EditorField { + name: "previous_borrower_id".into(), + label: "Prev Borrower".into(), + field_type: FieldType::Text, + required: false, + read_only: true, + }, + EditorField { + name: "current_borrower_id".into(), + label: "Current Borrower".into(), + field_type: FieldType::Text, + required: false, + read_only: true, + }, + // Label template selection + EditorField { + name: "label_template_id".into(), + label: "Label Template".into(), + field_type: FieldType::Dropdown(options.label_template_options.clone()), + required: false, + read_only: false, + }, + // Notes / images + EditorField { + name: "notes".into(), + label: "Notes".into(), + field_type: FieldType::MultilineText, + required: false, + read_only: false, + }, + // Audit/meta (read-only) + EditorField { + name: "created_date".into(), + label: "Created Date".into(), + field_type: FieldType::Text, + required: false, + read_only: true, + }, + EditorField { + name: "created_by_username".into(), + label: "Created By".into(), + field_type: FieldType::Text, + required: false, + read_only: true, + }, + EditorField { + name: "last_modified_date".into(), + label: "Last Modified".into(), + field_type: FieldType::Text, + required: false, + read_only: true, + }, + EditorField { + name: "last_modified_by_username".into(), + label: "Modified By".into(), + field_type: FieldType::Text, + required: false, + read_only: true, + }, + ]; + + FormBuilder::new("Advanced Edit Asset", fields) + } + + /// Create an Easy Edit dialog with essential asset fields only + pub fn create_easy_edit_dialog(api_client: &ApiClient) -> FormBuilder { + let options = AssetDropdownOptions::new(api_client); + + let fields = vec![ + EditorField { + name: "asset_tag".to_string(), + label: "Asset Tag".to_string(), + field_type: FieldType::Text, + required: true, + read_only: false, + }, + EditorField { + name: "asset_type".to_string(), + label: "Type".to_string(), + field_type: FieldType::Dropdown(options.asset_types), + required: true, + read_only: false, + }, + EditorField { + name: "name".to_string(), + label: "Name".to_string(), + field_type: FieldType::Text, + required: true, + read_only: false, + }, + EditorField { + name: "category_id".to_string(), + label: "Category".to_string(), + field_type: FieldType::Dropdown(options.category_options), + required: false, + read_only: false, + }, + EditorField { + name: "manufacturer".to_string(), + label: "Manufacturer".to_string(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "model".to_string(), + label: "Model".to_string(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "zone_id".to_string(), + label: "Zone".to_string(), + field_type: FieldType::Dropdown(options.zone_options), + required: false, + read_only: false, + }, + EditorField { + name: "status".to_string(), + label: "Status".to_string(), + field_type: FieldType::Dropdown(options.status_options), + required: true, + read_only: false, + }, + EditorField { + name: "lendable".to_string(), + label: "Lendable".to_string(), + field_type: FieldType::Checkbox, + required: false, + read_only: false, + }, + ]; + + FormBuilder::new("Easy Edit Asset", fields) + } + + /// Create an Add Asset dialog with quick-add functionality for categories/zones/suppliers + pub fn create_add_dialog_with_preset(api_client: &ApiClient) -> FormBuilder { + let options = AssetDropdownOptions::new(api_client); + + let fields = vec![ + EditorField { + name: "asset_tag".to_string(), + label: "Asset Tag".to_string(), + field_type: FieldType::Text, + required: true, + read_only: false, + }, + EditorField { + name: "asset_type".to_string(), + label: "Type".to_string(), + field_type: FieldType::Dropdown(options.asset_types), + required: true, + read_only: false, + }, + EditorField { + name: "name".to_string(), + label: "Name".to_string(), + field_type: FieldType::Text, + required: true, + read_only: false, + }, + // Category dropdown + add-new placeholders + EditorField { + name: "category_id".to_string(), + label: "Category".to_string(), + field_type: FieldType::Dropdown(options.category_options), + required: false, + read_only: false, + }, + // Label template selection + EditorField { + name: "label_template_id".to_string(), + label: "Label Template".to_string(), + field_type: FieldType::Dropdown(options.label_template_options.clone()), + required: false, + read_only: false, + }, + // Print label option + EditorField { + name: "print_label".to_string(), + label: "Print Label".to_string(), + field_type: FieldType::Checkbox, + required: false, + read_only: false, + }, + // Add new category name/code as text fields + EditorField { + name: "new_category_name".to_string(), + label: "Add New Category - Name".to_string(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "new_category_code".to_string(), + label: "Add New Category - Code".to_string(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "manufacturer".to_string(), + label: "Manufacturer".to_string(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "model".to_string(), + label: "Model".to_string(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "zone_id".to_string(), + label: "Zone".to_string(), + field_type: FieldType::Dropdown(options.zone_options), + required: false, + read_only: false, + }, + EditorField { + name: "status".to_string(), + label: "Status".to_string(), + field_type: FieldType::Dropdown(options.status_options), + required: true, + read_only: false, + }, + EditorField { + name: "lendable".to_string(), + label: "Lendable".to_string(), + field_type: FieldType::Checkbox, + required: false, + read_only: false, + }, + ]; + + let mut dialog = FormBuilder::new("Add Asset", fields); + + // Open with sensible defaults + let mut preset = serde_json::Map::new(); + preset.insert("asset_type".to_string(), Value::String("N".to_string())); + preset.insert("status".to_string(), Value::String("Good".to_string())); + preset.insert("lendable".to_string(), Value::Bool(true)); + // Default to printing a label after adding when possible + preset.insert("print_label".to_string(), Value::Bool(true)); + + dialog.open_new(Some(&preset)); + dialog + } + + /// Get the list of fields that are allowed to be updated via API + #[allow(dead_code)] + pub fn get_allowed_update_fields() -> Vec<String> { + vec![ + "asset_tag".to_string(), + "asset_type".to_string(), + "name".to_string(), + "description".to_string(), + "category_id".to_string(), + "zone_id".to_string(), + "zone_plus".to_string(), + "no_scan".to_string(), + "manufacturer".to_string(), + "model".to_string(), + "serial_number".to_string(), + "status".to_string(), + "price".to_string(), + "purchase_date".to_string(), + "warranty_until".to_string(), + "expiry_date".to_string(), + "lendable".to_string(), + "lending_status".to_string(), + "due_date".to_string(), + "supplier_id".to_string(), + "notes".to_string(), + "label_template_id".to_string(), + ] + } +} diff --git a/src/core/data/counters.rs b/src/core/data/counters.rs new file mode 100644 index 0000000..485a590 --- /dev/null +++ b/src/core/data/counters.rs @@ -0,0 +1,43 @@ +use crate::api::ApiClient; +use anyhow::Result; +use serde_json::Value; + +/// Generic counter function - can count anything from any table with any conditions +/// +/// # Examples +/// ``` +/// // Count all assets +/// count_entities(api, "assets", None)?; +/// +/// // Count available assets +/// count_entities(api, "assets", Some(json!({"lending_status": "Available"})))?; +/// +/// // Count with multiple conditions +/// count_entities(api, "assets", Some(json!({ +/// "lendable": true, +/// "lending_status": "Available" +/// })))?; +/// ``` +pub fn count_entities( + api_client: &ApiClient, + table: &str, + where_conditions: Option<Value>, +) -> Result<i32> { + log::debug!("Counting {} with conditions: {:?}", table, where_conditions); + let response = api_client.count(table, where_conditions)?; + log::debug!( + "Count response: success={}, data={:?}", + response.success, + response.data + ); + + // Check for database timeout errors + if !response.success { + if crate::api::ApiClient::is_database_timeout_error(&response.error) { + log::warn!("Database timeout detected while counting {}", table); + } + anyhow::bail!("API error: {:?}", response.error); + } + + Ok(response.data.unwrap_or(0)) +} diff --git a/src/core/data/data_loader.rs b/src/core/data/data_loader.rs new file mode 100644 index 0000000..7eb4125 --- /dev/null +++ b/src/core/data/data_loader.rs @@ -0,0 +1,99 @@ +use crate::api::ApiClient; +use serde_json::Value; + +/// Loading state management for UI views +#[derive(Default)] +pub struct LoadingState { + pub is_loading: bool, + pub last_error: Option<String>, + pub last_load_time: Option<std::time::Instant>, +} + +impl LoadingState { + pub fn new() -> Self { + Self::default() + } + + pub fn start_loading(&mut self) { + self.is_loading = true; + self.last_error = None; + self.last_load_time = Some(std::time::Instant::now()); + } + + pub fn finish_loading(&mut self, error: Option<String>) { + self.is_loading = false; + self.last_error = error; + } + + pub fn finish_success(&mut self) { + self.finish_loading(None); + } + + pub fn finish_error(&mut self, error: String) { + self.finish_loading(Some(error)); + } + + #[allow(dead_code)] + pub fn has_error(&self) -> bool { + self.last_error.is_some() + } + + pub fn get_error(&self) -> Option<&str> { + self.last_error.as_deref() + } + + #[allow(dead_code)] + pub fn should_auto_retry(&self, retry_after_seconds: u64) -> bool { + if let (Some(error), Some(load_time)) = (&self.last_error, self.last_load_time) { + !error.is_empty() && load_time.elapsed().as_secs() > retry_after_seconds + } else { + false + } + } +} + +/// Data loader for assets +pub struct DataLoader; + +impl DataLoader { + pub fn load_assets( + api_client: &ApiClient, + limit: Option<u32>, + where_clause: Option<Value>, + filter: Option<Value>, + ) -> Result<Vec<Value>, String> { + log::info!( + "Loading inventory assets (limit={:?}, where={:?}, filter={:?})...", + limit, + where_clause, + filter + ); + + // Use select_with_joins to load assets with zone and category data + let response = api_client + .select_with_joins( + "assets", + None, // columns (None = all) + where_clause, + filter, + None, // order_by + limit, + None, // joins (None = use default joins) + ) + .map_err(|e| format!("Failed to load assets: {}", e))?; + + if !response.success { + // Check if this is a database timeout error + if ApiClient::is_database_timeout_error(&response.error) { + log::warn!("Database timeout detected while loading assets"); + } + let error_msg = format!("API error: {:?}", response.error); + log::error!("{}", error_msg); + return Err(error_msg); + } + + let assets = response.data.unwrap_or_default(); + log::info!("Loaded {} assets successfully (with JOINs)", assets.len()); + Ok(assets) + } +} diff --git a/src/core/data/mod.rs b/src/core/data/mod.rs new file mode 100644 index 0000000..edb61ab --- /dev/null +++ b/src/core/data/mod.rs @@ -0,0 +1,8 @@ +/// Data management and dropdown options +pub mod asset_fields; +pub mod counters; +pub mod data_loader; + +pub use asset_fields::*; +pub use data_loader::*; +// counters module available but not currently used |
