aboutsummaryrefslogtreecommitdiff
path: root/src/core/data
diff options
context:
space:
mode:
authorUMTS at Teleco <crt@teleco.ch>2025-12-13 02:51:15 +0100
committerUMTS at Teleco <crt@teleco.ch>2025-12-13 02:51:15 +0100
commit8323fdd73272a2882781aba3c499ba0be3dff2a6 (patch)
treeffbf86473933e69cfaeef30d5c6ea7e5b494856c /src/core/data
committing to insanityHEADmaster
Diffstat (limited to 'src/core/data')
-rw-r--r--src/core/data/asset_fields.rs1008
-rw-r--r--src/core/data/counters.rs43
-rw-r--r--src/core/data/data_loader.rs99
-rw-r--r--src/core/data/mod.rs8
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