diff options
Diffstat (limited to 'src/ui/zones.rs')
| -rw-r--r-- | src/ui/zones.rs | 990 |
1 files changed, 990 insertions, 0 deletions
diff --git a/src/ui/zones.rs b/src/ui/zones.rs new file mode 100644 index 0000000..d331642 --- /dev/null +++ b/src/ui/zones.rs @@ -0,0 +1,990 @@ +use eframe::egui; +use std::collections::{HashMap, HashSet}; + +use crate::core::components::form_builder::FormBuilder; +use crate::core::components::interactions::ConfirmDialog; +use crate::core::{EditorField, FieldType}; + +use crate::api::ApiClient; +use crate::core::tables::get_assets_in_zone; + +pub struct ZonesView { + zones: Vec<serde_json::Value>, + is_loading: bool, + last_error: Option<String>, + // UI state + show_items: bool, + search_query: String, + // Cache: assets per zone id + zone_assets: HashMap<i32, Vec<serde_json::Value>>, + // Request guards + initial_load_done: bool, + zone_assets_attempted: HashSet<i32>, + zone_assets_failed: HashSet<i32>, + // Editor dialogs for zones + edit_dialog: FormBuilder, + add_dialog: FormBuilder, + delete_dialog: ConfirmDialog, + // Pending operation + pending_delete_id: Option<i32>, + pending_parent_id: Option<i32>, // For "Add Child Zone" + // Navigation request + pub switch_to_inventory_with_zone: Option<String>, // zone_code to filter by + // Print dialog + print_dialog: Option<crate::core::print::PrintDialog>, + show_print_dialog: bool, + force_expand_state: Option<bool>, +} + +impl ZonesView { + pub fn new() -> Self { + // Create basic editors first, they'll be updated with dropdowns when API client is available + let edit_dialog = Self::create_edit_dialog(Vec::new()); + let add_dialog = Self::create_add_dialog(Vec::new()); + + Self { + zones: Vec::new(), + is_loading: false, + last_error: None, + show_items: true, + search_query: String::new(), + zone_assets: HashMap::new(), + initial_load_done: false, + zone_assets_attempted: HashSet::new(), + zone_assets_failed: HashSet::new(), + edit_dialog, + add_dialog, + delete_dialog: ConfirmDialog::new("Delete Zone", "Are you sure you want to delete this zone? All items in this zone will lose their zone assignment."), + pending_delete_id: None, + pending_parent_id: None, + switch_to_inventory_with_zone: None, + print_dialog: None, + show_print_dialog: false, + force_expand_state: None, + } + } + + /// Check if a zone or any of its descendants have items + fn zone_or_descendants_have_items( + &self, + zone_id: i32, + all_zones: &[serde_json::Value], + ) -> bool { + // Check if this zone has items + if self + .zone_assets + .get(&zone_id) + .map(|assets| !assets.is_empty()) + .unwrap_or(false) + { + return true; + } + + // Check if any children have items (recursively) + for z in all_zones { + if let Some(parent_id) = z + .get("parent_id") + .and_then(|v| v.as_i64()) + .map(|v| v as i32) + { + if parent_id == zone_id { + if let Some(child_id) = z.get("id").and_then(|v| v.as_i64()).map(|v| v as i32) { + if self.zone_or_descendants_have_items(child_id, all_zones) { + return true; + } + } + } + } + } + + false + } + + /// Create edit dialog with zone options for parent selection + fn create_edit_dialog(parent_options: Vec<(String, String)>) -> FormBuilder { + let zone_type_options = vec![ + ("Building".to_string(), "Building".to_string()), + ("Floor".to_string(), "Floor".to_string()), + ("Room".to_string(), "Room".to_string()), + ("Storage Area".to_string(), "Storage Area".to_string()), + ]; + + FormBuilder::new( + "Edit Zone", + vec![ + EditorField { + name: "id".into(), + label: "ID".into(), + field_type: FieldType::Text, + required: false, + read_only: true, + }, + EditorField { + name: "mini_code".into(), + label: "Mini Code".into(), + field_type: FieldType::Text, + required: true, + read_only: false, + }, + EditorField { + name: "zone_code".into(), + label: "Full Zone Code".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "zone_name".into(), + label: "Name".into(), + field_type: FieldType::Text, + required: true, + read_only: false, + }, + EditorField { + name: "zone_type".into(), + label: "Type".into(), + field_type: FieldType::Dropdown(zone_type_options.clone()), + required: false, + read_only: false, + }, + EditorField { + name: "parent_id".into(), + label: "Parent Zone".into(), + field_type: FieldType::Dropdown(parent_options.clone()), + required: false, + read_only: false, + }, + EditorField { + name: "include_in_parent".into(), + label: "Include in Parent".into(), + field_type: FieldType::Checkbox, + required: false, + read_only: false, + }, + EditorField { + name: "audit_timeout_minutes".into(), + label: "Audit Timeout (minutes)".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "zone_notes".into(), + label: "Notes".into(), + field_type: FieldType::MultilineText, + required: false, + read_only: false, + }, + ], + ) + } + + /// Create add dialog with zone options for parent selection + fn create_add_dialog(parent_options: Vec<(String, String)>) -> FormBuilder { + let zone_type_options = vec![ + ("Building".to_string(), "Building".to_string()), + ("Floor".to_string(), "Floor".to_string()), + ("Room".to_string(), "Room".to_string()), + ("Storage Area".to_string(), "Storage Area".to_string()), + ]; + + FormBuilder::new( + "Add Zone", + vec![ + EditorField { + name: "mini_code".into(), + label: "Mini Code".into(), + field_type: FieldType::Text, + required: true, + read_only: false, + }, + EditorField { + name: "zone_code".into(), + label: "Full Zone Code".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "zone_name".into(), + label: "Name".into(), + field_type: FieldType::Text, + required: true, + read_only: false, + }, + EditorField { + name: "zone_type".into(), + label: "Type".into(), + field_type: FieldType::Dropdown(zone_type_options), + required: false, + read_only: false, + }, + EditorField { + name: "parent_id".into(), + label: "Parent Zone".into(), + field_type: FieldType::Dropdown(parent_options), + required: false, + read_only: false, + }, + EditorField { + name: "include_in_parent".into(), + label: "Include in Parent".into(), + field_type: FieldType::Checkbox, + required: false, + read_only: false, + }, + EditorField { + name: "audit_timeout_minutes".into(), + label: "Audit Timeout (minutes)".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "zone_notes".into(), + label: "Notes".into(), + field_type: FieldType::MultilineText, + required: false, + read_only: false, + }, + ], + ) + } + + /// Update editor dialogs with current zone list for parent dropdown + fn update_editor_dropdowns(&mut self) { + let parent_options: Vec<(String, String)> = + std::iter::once(("".to_string(), "(None)".to_string())) + .chain(self.zones.iter().filter_map(|z| { + let id = z.get("id")?.as_i64()?.to_string(); + let code = z.get("zone_code")?.as_str()?; + let name = z.get("name").or_else(|| z.get("zone_name"))?.as_str()?; + Some((id, format!("{} - {}", code, name))) + })) + .collect(); + + self.edit_dialog = Self::create_edit_dialog(parent_options.clone()); + self.add_dialog = Self::create_add_dialog(parent_options); + } + + fn ensure_loaded(&mut self, api_client: Option<&ApiClient>) { + if self.is_loading || self.initial_load_done { + return; + } + if let Some(client) = api_client { + self.load_zones(client, None); + } + } + + /// Load zones with optional filter + fn load_zones(&mut self, api_client: &ApiClient, filter: Option<serde_json::Value>) { + use crate::core::tables::get_all_zones_with_filter; + + self.is_loading = true; + self.last_error = None; + match get_all_zones_with_filter(api_client, filter) { + Ok(list) => { + self.zones = list; + self.is_loading = false; + self.initial_load_done = true; + // Update editor dropdowns with new zone list + self.update_editor_dropdowns(); + } + Err(e) => { + self.last_error = Some(e.to_string()); + self.is_loading = false; + self.initial_load_done = true; + } + } + } + + /// Refresh zones data from the API + pub fn refresh(&mut self, api_client: Option<&ApiClient>) { + self.zones.clear(); + self.is_loading = false; + self.initial_load_done = false; + self.zone_assets.clear(); + self.zone_assets_attempted.clear(); + self.zone_assets_failed.clear(); + self.last_error = None; + self.ensure_loaded(api_client); + } + + pub fn show( + &mut self, + ui: &mut egui::Ui, + api_client: Option<&ApiClient>, + ribbon: &mut crate::ui::ribbon::RibbonUI, + ) { + self.ensure_loaded(api_client); + + // Handle filter changes from FilterBuilder + if ribbon + .checkboxes + .get("zones_filter_changed") + .copied() + .unwrap_or(false) + { + ribbon + .checkboxes + .insert("zones_filter_changed".to_string(), false); + if let Some(client) = api_client { + self.last_error = None; + + // Get user-defined filters from FilterBuilder + let user_filter = ribbon.filter_builder.get_filter_json("zones"); + + // Debug: Log the filter to see what we're getting + if let Some(ref cf) = user_filter { + log::info!("Zones filter: {:?}", cf); + } else { + log::info!("No filter conditions (showing all zones)"); + } + + self.load_zones(client, user_filter); + } + } + + // Handle ribbon actions + if ribbon + .checkboxes + .get("zones_action_add") + .copied() + .unwrap_or(false) + { + self.update_editor_dropdowns(); + self.add_dialog.open_new(None); + } + if ribbon + .checkboxes + .get("zones_action_edit") + .copied() + .unwrap_or(false) + { + // Edit needs a selected zone - will be handled via context menu + log::info!("Edit zone clicked - use right-click context menu on a zone"); + } + if ribbon + .checkboxes + .get("zones_action_remove") + .copied() + .unwrap_or(false) + { + // Remove needs a selected zone - will be handled via context menu + log::info!("Remove zone clicked - use right-click context menu on a zone"); + } + + // Update show_items from ribbon + self.show_items = ribbon + .checkboxes + .get("zones_show_items") + .copied() + .unwrap_or(false); + + // Get show_empty preference from ribbon + let show_empty = ribbon + .checkboxes + .get("zones_show_empty") + .copied() + .unwrap_or(true); + + ui.horizontal(|ui| { + ui.heading("Zones"); + if self.is_loading { + ui.spinner(); + ui.label("Loading zones..."); + } + if let Some(err) = &self.last_error { + ui.colored_label(egui::Color32::RED, err); + if ui.button("Refresh").clicked() { + self.refresh(api_client); + } + } + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("Collapse All").clicked() { + self.set_all_open(false, ui.ctx()); + } + if ui.button("Expand All").clicked() { + self.set_all_open(true, ui.ctx()); + } + if ui.button("➕ Add Zone").clicked() { + self.update_editor_dropdowns(); + self.add_dialog.open_new(None); + } + }); + }); + + // Search bar + ui.horizontal(|ui| { + ui.label("Search:"); + ui.text_edit_singleline(&mut self.search_query); + if ui.button("Clear").clicked() { + self.search_query.clear(); + } + }); + + ui.separator(); + + // If there was an error loading zones, stop here until user refreshes + if self.last_error.is_some() { + return; + } + if self.zones.is_empty() { + return; + } + + // Default: expand all once on first successful load + // Filter zones by search query (case-insensitive) + let search_lower = self.search_query.to_lowercase(); + let filtered_zones: Vec<serde_json::Value> = self + .zones + .iter() + .filter(|z| { + // Apply search filter + if !search_lower.is_empty() { + let code_match = z + .get("zone_code") + .and_then(|v| v.as_str()) + .map(|s| s.to_lowercase().contains(&search_lower)) + .unwrap_or(false); + let mini_match = z + .get("mini_code") + .and_then(|v| v.as_str()) + .map(|s| s.to_lowercase().contains(&search_lower)) + .unwrap_or(false); + let name_match = z + .get("name") + .or_else(|| z.get("zone_name")) + .and_then(|v| v.as_str()) + .map(|s| s.to_lowercase().contains(&search_lower)) + .unwrap_or(false); + if !code_match && !mini_match && !name_match { + return false; + } + } + + // Apply empty filter + if !show_empty { + let zone_id = z.get("id").and_then(|v| v.as_i64()).map(|v| v as i32); + if let Some(id) = zone_id { + // Only show zones that have items or have descendants with items + if !self.zone_or_descendants_have_items(id, &self.zones) { + return false; + } + } + } + + true + }) + .cloned() + .collect(); + + // Build parent -> children map + let mut children: HashMap<Option<i32>, Vec<serde_json::Value>> = HashMap::new(); + for z in &filtered_zones { + let parent = z + .get("parent_id") + .and_then(|v| v.as_i64()) + .map(|v| v as i32); + children.entry(parent).or_default().push(z.clone()); + } + // Sort children lists by zone_code + for list in children.values_mut() { + list.sort_by(|a, b| { + a.get("zone_code") + .and_then(|v| v.as_str()) + .cmp(&b.get("zone_code").and_then(|v| v.as_str())) + }); + } + + // Render roots (parent = None) + if let Some(roots) = children.get(&None) { + egui::ScrollArea::both() + .auto_shrink([false; 2]) + .show(ui, |ui| { + for root in roots { + self.render_zone_node(ui, root, &children, api_client); + } + }); + } + + // Clear the one-shot expand/collapse request after applying it + if self.force_expand_state.is_some() { + self.force_expand_state = None; + } + + // Show zone editor dialogs + if let Some(result) = self.edit_dialog.show_editor(ui.ctx()) { + if let Some(updated) = result { + log::info!("Zone updated: {:?}", updated); + if let Some(client) = api_client { + self.update_zone(client, &updated); + } + } + } + + if let Some(result) = self.add_dialog.show_editor(ui.ctx()) { + if let Some(new_zone) = result { + log::info!("Zone added: {:?}", new_zone); + if let Some(client) = api_client { + self.create_zone(client, &new_zone); + } + } + } + + // Show delete confirmation dialog + if let Some(should_delete) = self.delete_dialog.show_dialog(ui.ctx()) { + if should_delete { + if let Some(zone_id) = self.pending_delete_id { + log::info!("Deleting zone ID: {}", zone_id); + if let Some(client) = api_client { + self.delete_zone(client, zone_id); + } + } + } + self.pending_delete_id = None; + } + + // Show print dialog + if let Some(print_dialog) = &mut self.print_dialog { + let print_complete = + print_dialog.show(ui.ctx(), &mut self.show_print_dialog, api_client); + if print_complete || !self.show_print_dialog { + self.print_dialog = None; + } + } + } + + fn render_zone_node( + &mut self, + ui: &mut egui::Ui, + zone: &serde_json::Value, + children: &HashMap<Option<i32>, Vec<serde_json::Value>>, + api_client: Option<&ApiClient>, + ) { + let id = zone.get("id").and_then(|v| v.as_i64()).unwrap_or_default() as i32; + let code = zone + .get("zone_code") + .and_then(|v| v.as_str()) + .unwrap_or("?"); + let mini = zone.get("mini_code").and_then(|v| v.as_str()).unwrap_or(""); + let name = zone.get("name").and_then(|v| v.as_str()).unwrap_or(""); + let label = if mini.is_empty() { + format!("{} — {}", code, name) + } else { + // Subtle mini code display in parentheses + format!("{} — {} ({})", code, name, mini) + }; + + let mut header = egui::CollapsingHeader::new(label.clone()) + .id_salt(("zone", id)) + .default_open(true); + + if let Some(force) = self.force_expand_state { + let ctx = ui.ctx(); + let id_key = egui::Id::new(label.clone()).with(("zone", id)); + let mut state = egui::collapsing_header::CollapsingState::load_with_default_open( + ctx, id_key, force, + ); + state.set_open(force); + state.store(ctx); + + header = header.open(Some(force)); + } + + let resp = header.show(ui, |ui| { + // Children zones + if let Some(kids) = children.get(&Some(id)) { + ui.indent(egui::Id::new(("zone_indent", id)), |ui| { + for child in kids { + self.render_zone_node(ui, child, children, api_client); + } + }); + } + + // Optional: items in this zone + if self.show_items { + ui.indent(egui::Id::new(("zone_items", id)), |ui| { + ui.spacing_mut().item_spacing.y = 2.0; + // Load from cache or fetch + if !self.zone_assets.contains_key(&id) + && !self.zone_assets_attempted.contains(&id) + && !self.zone_assets_failed.contains(&id) + { + self.zone_assets_attempted.insert(id); + if let Some(client) = api_client { + match get_assets_in_zone(client, id, Some(200)) { + Ok(list) => { + self.zone_assets.insert(id, list); + } + Err(e) => { + self.zone_assets_failed.insert(id); + ui.colored_label( + egui::Color32::RED, + format!("Failed to load items: {}", e), + ); + } + } + } + } + if self.zone_assets_failed.contains(&id) && !self.zone_assets.contains_key(&id) + { + ui.colored_label( + egui::Color32::RED, + "(items failed to load – use Refresh at top)", + ); + } + if let Some(items) = self.zone_assets.get(&id) { + for a in items { + let tag = a.get("asset_tag").and_then(|v| v.as_str()).unwrap_or("?"); + let nm = a.get("name").and_then(|v| v.as_str()).unwrap_or(""); + let status = a.get("status").and_then(|v| v.as_str()).unwrap_or(""); + ui.label(format!("• [{}] {} ({})", tag, nm, status)); + } + if items.is_empty() { + ui.label("(no items)"); + } + } + }); + } + }); + + // Add context menu to header for editing + resp.header_response.context_menu(|ui| { + if ui + .button(format!("{} Edit Zone", egui_phosphor::regular::PENCIL)) + .clicked() + { + // Update dropdowns with current zone list + self.update_editor_dropdowns(); + + // Map the zone data to match the editor field names + if let Some(zone_obj) = zone.as_object() { + let mut zone_for_editor = zone_obj.clone(); + + // The data comes in with "name" (aliased from zone_name), but editor expects "zone_name" + if let Some(name_value) = zone_for_editor.remove("name") { + zone_for_editor.insert("zone_name".to_string(), name_value); + } + + // Convert integer fields to strings if they're numbers + if let Some(audit_timeout) = zone_for_editor.get("audit_timeout_minutes") { + if let Some(num) = audit_timeout.as_i64() { + zone_for_editor.insert( + "audit_timeout_minutes".to_string(), + serde_json::json!(num.to_string()), + ); + } + } + + self.edit_dialog + .open(&serde_json::Value::Object(zone_for_editor)); + ui.close(); + } else { + log::error!("Zone data is not an object, cannot edit"); + } + } + if ui + .button(format!("{} Delete Zone", egui_phosphor::regular::TRASH)) + .clicked() + { + self.pending_delete_id = Some(id); + self.delete_dialog + .open(format!("{} - {}", code, name), id.to_string()); + ui.close(); + } + ui.separator(); + if ui + .button(format!("{} Add Child Zone", egui_phosphor::regular::PLUS)) + .clicked() + { + // Open add dialog with parent_id pre-filled + self.pending_parent_id = Some(id); + self.update_editor_dropdowns(); + + // Create initial data with parent_id + let mut initial_data = serde_json::Map::new(); + initial_data.insert("parent_id".to_string(), serde_json::json!(id.to_string())); + + self.add_dialog.open_new(Some(&initial_data)); + ui.close(); + } + if ui + .button(format!( + "{} Show Items in this Zone", + egui_phosphor::regular::PACKAGE + )) + .clicked() + { + // Set the flag to switch to inventory with this zone filter + self.switch_to_inventory_with_zone = Some(code.to_string()); + ui.close(); + } + if ui + .button(format!( + "{} Print Zone Label", + egui_phosphor::regular::PRINTER + )) + .clicked() + { + // Prepare zone data for printing + let mut print_data = std::collections::HashMap::new(); + print_data.insert("zone_code".to_string(), code.to_string()); + print_data.insert("zone_name".to_string(), name.to_string()); + + // Extract additional fields from zone JSON + if let Some(zone_type) = zone.get("zone_type").and_then(|v| v.as_str()) { + print_data.insert("zone_type".to_string(), zone_type.to_string()); + } + if let Some(zone_barcode) = zone.get("zone_barcode").and_then(|v| v.as_str()) { + print_data.insert("zone_barcode".to_string(), zone_barcode.to_string()); + } + + // Create new print dialog with zone data + self.print_dialog = Some(crate::core::print::PrintDialog::new(print_data)); + self.show_print_dialog = true; + ui.close(); + } + ui.separator(); + if ui + .button(format!("{} Clone Zone", egui_phosphor::regular::COPY)) + .clicked() + { + // Open add dialog prefilled with cloned values + self.update_editor_dropdowns(); + + // Start from original zone object + let mut clone_map = zone.as_object().cloned().unwrap_or_default(); + + // Editor expects zone_name instead of name + if let Some(name_val) = clone_map.remove("name") { + clone_map.insert("zone_name".to_string(), name_val); + } + + // Clear identifiers and codes that must be unique + clone_map.remove("id"); + clone_map.insert( + "zone_code".to_string(), + serde_json::Value::String(String::new()), + ); + // Mini code is required but typically unique — leave empty to force user choice + clone_map.insert( + "mini_code".to_string(), + serde_json::Value::String(String::new()), + ); + + // Convert parent_id to string for dropdown if present + if let Some(p) = clone_map.get("parent_id").cloned() { + let as_str = match p { + serde_json::Value::Number(n) => { + n.as_i64().map(|i| i.to_string()).unwrap_or_default() + } + serde_json::Value::String(s) => s, + _ => String::new(), + }; + clone_map.insert("parent_id".to_string(), serde_json::Value::String(as_str)); + } + + // Ensure audit_timeout_minutes is string + if let Some(a) = clone_map.get("audit_timeout_minutes").cloned() { + if let Some(num) = a.as_i64() { + clone_map.insert( + "audit_timeout_minutes".to_string(), + serde_json::json!(num.to_string()), + ); + } + } + + // Suffix the name to indicate copy + if let Some(serde_json::Value::String(nm)) = clone_map.get_mut("zone_name") { + nm.push_str(""); + } + + // Open prefilled Add dialog + self.add_dialog.open_new(Some(&clone_map)); + ui.close(); + } + }); + } + + /// Create a new zone via API + fn create_zone( + &mut self, + api_client: &ApiClient, + zone_data: &serde_json::Map<String, serde_json::Value>, + ) { + use crate::models::QueryRequest; + + // Clean the data - remove empty parent_id and convert audit_timeout_minutes to integer + let mut clean_data = zone_data.clone(); + + if let Some(parent_id) = clean_data.get("parent_id") { + if parent_id.as_str() == Some("") || parent_id.is_null() { + clean_data.remove("parent_id"); + } + } + + // Do not auto-generate full zone_code here; use user-provided value as-is for now. + + // Convert audit_timeout_minutes string to integer if present and not empty + if let Some(audit_timeout) = clean_data.get("audit_timeout_minutes") { + if let Some(s) = audit_timeout.as_str() { + if !s.is_empty() { + if let Ok(num) = s.parse::<i64>() { + clean_data + .insert("audit_timeout_minutes".to_string(), serde_json::json!(num)); + } + } else { + clean_data.remove("audit_timeout_minutes"); + } + } + } + + let request = QueryRequest { + action: "insert".to_string(), + table: "zones".to_string(), + columns: None, + data: Some(serde_json::Value::Object(clean_data)), + r#where: None, + filter: None, + order_by: None, + limit: None, + offset: None, + joins: None, + }; + + match api_client.query(&request) { + Ok(response) => { + if response.success { + self.refresh(Some(api_client)); + } else { + log::error!("Failed to create zone: {:?}", response.error); + } + } + Err(e) => { + log::error!("Error creating zone: {}", e); + } + } + } + + /// Update an existing zone via API + fn update_zone( + &mut self, + api_client: &ApiClient, + zone_data: &serde_json::Map<String, serde_json::Value>, + ) { + use crate::models::QueryRequest; + + // Try to get ID from various possible locations + let zone_id = zone_data + .get("id") + .and_then(|v| { + v.as_i64() + .or_else(|| v.as_str().and_then(|s| s.parse().ok())) + }) + .or_else(|| { + // Check for __editor_item_id which is set by the editor for read-only ID fields + zone_data + .get("__editor_item_id") + .and_then(|v| v.as_str().and_then(|s| s.parse().ok())) + }) + .unwrap_or_else(|| { + log::error!("Zone update attempted without ID"); + 0 + }); + + // Create a clean data object without the editor metadata and without the id field + let mut clean_data = zone_data.clone(); + clean_data.remove("__editor_item_id"); + clean_data.remove("id"); // Don't include id in the update data, only in WHERE + + // Remove empty parent_id + if let Some(parent_id) = clean_data.get("parent_id") { + if parent_id.as_str() == Some("") || parent_id.is_null() { + clean_data.remove("parent_id"); + } + } + + // Do not auto-generate full zone_code during updates either; keep user value intact. + + // Convert audit_timeout_minutes string to integer if present and not empty + if let Some(audit_timeout) = clean_data.get("audit_timeout_minutes") { + if let Some(s) = audit_timeout.as_str() { + if !s.is_empty() { + if let Ok(num) = s.parse::<i64>() { + clean_data + .insert("audit_timeout_minutes".to_string(), serde_json::json!(num)); + } + } else { + clean_data.remove("audit_timeout_minutes"); + } + } + } + + let request = QueryRequest { + action: "update".to_string(), + table: "zones".to_string(), + columns: None, + data: Some(serde_json::Value::Object(clean_data)), + r#where: Some(serde_json::json!({ + "id": zone_id + })), + filter: None, + order_by: None, + limit: None, + offset: None, + joins: None, + }; + + match api_client.query(&request) { + Ok(response) => { + if response.success { + self.refresh(Some(api_client)); + } else { + log::error!("Failed to update zone: {:?}", response.error); + } + } + Err(e) => { + log::error!("Error updating zone: {}", e); + } + } + } + + /// Delete a zone via API + fn delete_zone(&mut self, api_client: &ApiClient, zone_id: i32) { + use crate::models::QueryRequest; + + let request = QueryRequest { + action: "delete".to_string(), + table: "zones".to_string(), + columns: None, + data: None, + r#where: Some(serde_json::json!({ + "id": zone_id + })), + filter: None, + order_by: None, + limit: None, + offset: None, + joins: None, + }; + + match api_client.query(&request) { + Ok(response) => { + if response.success { + self.refresh(Some(api_client)); + } else { + log::error!("Failed to delete zone: {:?}", response.error); + } + } + Err(e) => { + log::error!("Error deleting zone: {}", e); + } + } + } + + fn set_all_open(&mut self, open: bool, ctx: &egui::Context) { + self.force_expand_state = Some(open); + ctx.request_repaint(); + } +} |
