From 8323fdd73272a2882781aba3c499ba0be3dff2a6 Mon Sep 17 00:00:00 2001 From: UMTS at Teleco Date: Sat, 13 Dec 2025 02:51:15 +0100 Subject: committing to insanity --- src/ui/templates.rs | 1113 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1113 insertions(+) create mode 100644 src/ui/templates.rs (limited to 'src/ui/templates.rs') diff --git a/src/ui/templates.rs b/src/ui/templates.rs new file mode 100644 index 0000000..9fd46a5 --- /dev/null +++ b/src/ui/templates.rs @@ -0,0 +1,1113 @@ +use crate::api::ApiClient; +use crate::core::asset_fields::AssetDropdownOptions; +use crate::core::components::form_builder::FormBuilder; +use crate::core::tables::get_templates; +use crate::core::{ColumnConfig, LoadingState, TableRenderer}; +use crate::core::{EditorField, FieldType}; +use eframe::egui; + +pub struct TemplatesView { + templates: Vec, + loading_state: LoadingState, + table_renderer: TableRenderer, + show_column_panel: bool, + edit_dialog: FormBuilder, + pending_delete_ids: Vec, +} + +impl TemplatesView { + pub fn new() -> Self { + let columns = vec![ + ColumnConfig::new("ID", "id").with_width(60.0).hidden(), + ColumnConfig::new("Template Code", "template_code").with_width(120.0), + ColumnConfig::new("Name", "name").with_width(200.0), + ColumnConfig::new("Asset Type", "asset_type").with_width(80.0), + ColumnConfig::new("Description", "description").with_width(250.0), + ColumnConfig::new("Asset Tag Generation String", "asset_tag_generation_string") + .with_width(200.0), + ColumnConfig::new("Label Template", "label_template_name") + .with_width(120.0) + .hidden(), + ColumnConfig::new("Label Template ID", "label_template_id") + .with_width(80.0) + .hidden(), + ColumnConfig::new("Audit Task", "audit_task_name") + .with_width(140.0) + .hidden(), + ColumnConfig::new("Audit Task ID", "audit_task_id") + .with_width(80.0) + .hidden(), + ColumnConfig::new("Category", "category_name").with_width(120.0), + ColumnConfig::new("Manufacturer", "manufacturer") + .with_width(120.0) + .hidden(), + ColumnConfig::new("Model", "model") + .with_width(120.0) + .hidden(), + ColumnConfig::new("Zone", "zone_name") + .with_width(100.0) + .hidden(), + ColumnConfig::new("Zone Code", "zone_code") + .with_width(80.0) + .hidden(), + ColumnConfig::new("Zone+", "zone_plus") + .with_width(100.0) + .hidden(), + ColumnConfig::new("Zone Note", "zone_note") + .with_width(150.0) + .hidden(), + ColumnConfig::new("Status", "status") + .with_width(100.0) + .hidden(), + ColumnConfig::new("Price", "price") + .with_width(80.0) + .hidden(), + ColumnConfig::new("Purchase Date", "purchase_date") + .with_width(100.0) + .hidden(), + ColumnConfig::new("Purchase Now?", "purchase_date_now") + .with_width(110.0) + .hidden(), + ColumnConfig::new("Warranty Until", "warranty_until") + .with_width(100.0) + .hidden(), + ColumnConfig::new("Warranty Auto?", "warranty_auto") + .with_width(110.0) + .hidden(), + ColumnConfig::new("Warranty Amount", "warranty_auto_amount") + .with_width(110.0) + .hidden(), + ColumnConfig::new("Warranty Unit", "warranty_auto_unit") + .with_width(100.0) + .hidden(), + ColumnConfig::new("Expiry Date", "expiry_date") + .with_width(100.0) + .hidden(), + ColumnConfig::new("Expiry Auto?", "expiry_auto") + .with_width(100.0) + .hidden(), + ColumnConfig::new("Expiry Amount", "expiry_auto_amount") + .with_width(110.0) + .hidden(), + ColumnConfig::new("Expiry Unit", "expiry_auto_unit") + .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("Lending Status", "lending_status") + .with_width(120.0) + .hidden(), + ColumnConfig::new("Min Role", "minimum_role_for_lending") + .with_width(80.0) + .hidden(), + ColumnConfig::new("No Scan", "no_scan") + .with_width(70.0) + .hidden(), + ColumnConfig::new("Notes", "notes") + .with_width(200.0) + .hidden(), + ColumnConfig::new("Active", "active") + .with_width(70.0) + .hidden(), + ColumnConfig::new("Created Date", "created_at") + .with_width(140.0) + .hidden(), + ]; + Self { + templates: Vec::new(), + loading_state: LoadingState::new(), + table_renderer: TableRenderer::new() + .with_columns(columns) + .with_default_sort("created_at", false), + show_column_panel: false, + edit_dialog: FormBuilder::new("Template Editor", vec![]), + pending_delete_ids: Vec::new(), + } + } + + fn prepare_template_edit_fields(&mut self, api_client: &ApiClient) { + let options = AssetDropdownOptions::new(api_client); + + let fields: Vec = vec![ + // Basic identifiers + EditorField { + name: "template_code".into(), + label: "Template Code".into(), + field_type: FieldType::Text, + required: true, + read_only: false, + }, + EditorField { + name: "name".into(), + label: "Template Name".into(), + field_type: FieldType::Text, + required: true, + read_only: false, + }, + // Asset tag generation + EditorField { + name: "asset_tag_generation_string".into(), + label: "Asset Tag Generation String".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + // Type / status + EditorField { + name: "asset_type".into(), + label: "Asset Type".into(), + field_type: FieldType::Dropdown({ + let mut asset_type_opts = vec![("".to_string(), "-- None --".to_string())]; + asset_type_opts.extend(options.asset_types.clone()); + asset_type_opts + }), + required: false, + read_only: false, + }, + EditorField { + name: "status".into(), + label: "Default Status".into(), + field_type: FieldType::Dropdown({ + let mut status_opts = vec![("".to_string(), "-- None --".to_string())]; + status_opts.extend(options.status_options.clone()); + status_opts + }), + required: false, + read_only: false, + }, + // Zone and zone-plus + EditorField { + name: "zone_id".into(), + label: "Default Zone".into(), + field_type: FieldType::Dropdown({ + let mut zone_opts = vec![("".to_string(), "-- None --".to_string())]; + zone_opts.extend(options.zone_options.clone()); + zone_opts + }), + required: false, + read_only: false, + }, + EditorField { + name: "zone_plus".into(), + label: "Zone+".into(), + field_type: FieldType::Dropdown({ + let mut zone_plus_opts = vec![("".to_string(), "-- None --".to_string())]; + zone_plus_opts.extend(options.zone_plus_options.clone()); + zone_plus_opts + }), + required: false, + read_only: false, + }, + // No-scan option + EditorField { + name: "no_scan".into(), + label: "No Scan".into(), + field_type: FieldType::Dropdown(options.no_scan_options.clone()), + required: false, + read_only: false, + }, + // Purchase / warranty / expiry + EditorField { + name: "purchase_date".into(), + label: "Purchase Date".into(), + field_type: FieldType::Date, + required: false, + read_only: false, + }, + EditorField { + name: "purchase_date_now".into(), + label: "Use current date (Purchase)".into(), + field_type: FieldType::Checkbox, + 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: "warranty_auto".into(), + label: "Auto-calc Warranty".into(), + field_type: FieldType::Checkbox, + required: false, + read_only: false, + }, + EditorField { + name: "warranty_auto_amount".into(), + label: "Warranty Auto Amount".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "warranty_auto_unit".into(), + label: "Warranty Auto Unit".into(), + field_type: FieldType::Dropdown(vec![ + ("days".to_string(), "Days".to_string()), + ("years".to_string(), "Years".to_string()), + ]), + required: false, + read_only: false, + }, + EditorField { + name: "expiry_date".into(), + label: "Expiry Date".into(), + field_type: FieldType::Date, + required: false, + read_only: false, + }, + EditorField { + name: "expiry_auto".into(), + label: "Auto-calc Expiry".into(), + field_type: FieldType::Checkbox, + required: false, + read_only: false, + }, + EditorField { + name: "expiry_auto_amount".into(), + label: "Expiry Auto Amount".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "expiry_auto_unit".into(), + label: "Expiry Auto Unit".into(), + field_type: FieldType::Dropdown(vec![ + ("days".to_string(), "Days".to_string()), + ("years".to_string(), "Years".to_string()), + ]), + required: false, + read_only: false, + }, + // Financial / lending / supplier + EditorField { + name: "price".into(), + label: "Price".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + 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({ + let mut lending_status_opts = vec![("".to_string(), "-- None --".to_string())]; + lending_status_opts.extend(options.lending_status_options.clone()); + lending_status_opts + }), + required: false, + read_only: false, + }, + EditorField { + name: "supplier_id".into(), + label: "Supplier".into(), + field_type: FieldType::Dropdown({ + let mut supplier_opts = vec![("".to_string(), "-- None --".to_string())]; + supplier_opts.extend(options.supplier_options.clone()); + supplier_opts + }), + required: false, + read_only: false, + }, + // Label template + EditorField { + name: "label_template_id".into(), + label: "Label Template".into(), + field_type: FieldType::Dropdown({ + let mut label_template_opts = vec![("".to_string(), "-- None --".to_string())]; + label_template_opts.extend(options.label_template_options.clone()); + label_template_opts + }), + required: false, + read_only: false, + }, + EditorField { + name: "audit_task_id".into(), + label: "Default Audit Task".into(), + field_type: FieldType::Dropdown(options.audit_task_options.clone()), + required: false, + read_only: false, + }, + // Defaults for created assets + EditorField { + name: "category_id".into(), + label: "Default Category".into(), + field_type: FieldType::Dropdown({ + let mut category_opts = vec![("".to_string(), "-- None --".to_string())]; + category_opts.extend(options.category_options.clone()); + category_opts + }), + required: false, + read_only: false, + }, + EditorField { + name: "manufacturer".into(), + label: "Default Manufacturer".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "model".into(), + label: "Default Model".into(), + field_type: FieldType::Text, + required: false, + read_only: false, + }, + EditorField { + name: "description".into(), + label: "Description".into(), + field_type: FieldType::MultilineText, + required: false, + read_only: false, + }, + EditorField { + name: "additional_fields_json".into(), + label: "Additional Fields (JSON)".into(), + field_type: FieldType::MultilineText, + required: false, + read_only: false, + }, + ]; + + self.edit_dialog = FormBuilder::new("Template Editor", fields); + } + + fn parse_additional_fields_input( + raw: Option, + ) -> Result, String> { + match raw { + Some(serde_json::Value::String(s)) => { + let trimmed = s.trim(); + if trimmed.is_empty() { + Ok(Some(serde_json::Value::Null)) + } else { + serde_json::from_str::(trimmed) + .map(Some) + .map_err(|e| e.to_string()) + } + } + Some(serde_json::Value::Null) => Ok(Some(serde_json::Value::Null)), + Some(other) => Ok(Some(other)), + None => Ok(None), + } + } + + fn load_templates(&mut self, api_client: &ApiClient, limit: Option) { + self.loading_state.start_loading(); + + match get_templates(api_client, limit) { + Ok(templates) => { + self.templates = templates; + self.loading_state.finish_success(); + } + Err(e) => { + self.loading_state.finish_error(e.to_string()); + } + } + } + + pub fn show( + &mut self, + ui: &mut egui::Ui, + api_client: Option<&ApiClient>, + ribbon: Option<&mut crate::ui::ribbon::RibbonUI>, + ) -> Vec { + let mut flags_to_clear = Vec::new(); + + // Get limit from ribbon + let limit = ribbon + .as_ref() + .and_then(|r| r.number_fields.get("templates_limit")) + .copied() + .or(Some(200)); + + // Top toolbar + ui.horizontal(|ui| { + ui.heading("Templates"); + + if self.loading_state.is_loading { + ui.spinner(); + ui.label("Loading..."); + } + + if let Some(err) = &self.loading_state.last_error { + ui.colored_label(egui::Color32::RED, err); + if ui.button("Refresh").clicked() { + if let Some(api) = api_client { + self.loading_state.last_error = None; + self.load_templates(api, limit); + } + } + } else if ui.button("Refresh").clicked() { + if let Some(api) = api_client { + self.load_templates(api, limit); + } + } + + ui.separator(); + + if ui.button("Columns").clicked() { + self.show_column_panel = !self.show_column_panel; + } + }); + + ui.separator(); + + // Auto-load on first view + if self.templates.is_empty() + && !self.loading_state.is_loading + && self.loading_state.last_error.is_none() + && self.loading_state.last_load_time.is_none() + { + if let Some(api) = api_client { + log::info!("Templates view never loaded, triggering initial auto-load"); + self.load_templates(api, limit); + } + } + + // Handle ribbon events + if let Some(ribbon) = ribbon.as_ref() { + // Handle filter changes + if *ribbon + .checkboxes + .get("templates_filter_changed") + .unwrap_or(&false) + { + flags_to_clear.push("templates_filter_changed".to_string()); + if let Some(client) = api_client { + self.loading_state.last_error = None; + + // Get user-defined filters from FilterBuilder + let user_filter = ribbon.filter_builder.get_filter_json("templates"); + + // Debug: Log the filter to see what we're getting + if let Some(ref cf) = user_filter { + log::info!("Template filter: {:?}", cf); + } else { + log::info!("No filter conditions (showing all templates)"); + } + + self.load_templates(client, limit); + return flags_to_clear; // Early return to avoid duplicate loading + } + } + + // Handle limit changes + if *ribbon + .checkboxes + .get("templates_limit_changed") + .unwrap_or(&false) + { + flags_to_clear.push("templates_limit_changed".to_string()); + if let Some(client) = api_client { + self.loading_state.last_error = None; + self.load_templates(client, limit); + } + } + + // Handle ribbon actions + if *ribbon + .checkboxes + .get("templates_action_new") + .unwrap_or(&false) + { + flags_to_clear.push("templates_action_new".to_string()); + + // Prepare dynamic dropdown fields before opening dialog + if let Some(client) = api_client { + self.prepare_template_edit_fields(client); + } + + // Open new template dialog with empty data (comprehensive fields for templates) + let empty_template = serde_json::json!({ + "id": "", + "template_code": "", + "name": "", + "asset_type": "", + "asset_tag_generation_string": "", + "description": "", + "additional_fields": null, + "additional_fields_json": "{}", + "category_id": "", + "manufacturer": "", + "model": "", + "zone_id": "", + "zone_plus": "", + "status": "", + "price": "", + "warranty_until": "", + "expiry_date": "", + "quantity_total": "", + "quantity_used": "", + "supplier_id": "", + "label_template_id": "", + "audit_task_id": "", + "lendable": false, + "minimum_role_for_lending": "", + "no_scan": "", + "notes": "", + "active": false + }); + self.edit_dialog.title = "Add New Template".to_string(); + self.open_edit_template_dialog(empty_template); + } + + if *ribbon + .checkboxes + .get("templates_action_edit") + .unwrap_or(&false) + { + flags_to_clear.push("templates_action_edit".to_string()); + // TODO: Implement edit selected template (requires selection tracking) + log::info!( + "Edit Template clicked (requires table selection - use double-click for now)" + ); + } + + if *ribbon + .checkboxes + .get("templates_action_delete") + .unwrap_or(&false) + { + flags_to_clear.push("templates_action_delete".to_string()); + // TODO: Implement delete selected templates (requires selection tracking) + log::info!( + "Delete Template clicked (requires table selection - use right-click for now)" + ); + } + } + + // Render table with event handler + let mut edit_template: Option = None; + let mut delete_template: Option = None; + let mut clone_template: Option = None; + + struct TemplateEventHandler<'a> { + edit_action: &'a mut Option, + delete_action: &'a mut Option, + clone_action: &'a mut Option, + } + + impl<'a> crate::core::table_renderer::TableEventHandler + for TemplateEventHandler<'a> + { + fn on_double_click(&mut self, item: &serde_json::Value, _row_index: usize) { + *self.edit_action = Some(item.clone()); + } + + fn on_context_menu( + &mut self, + ui: &mut egui::Ui, + item: &serde_json::Value, + _row_index: usize, + ) { + if ui + .button(format!("{} Edit Template", egui_phosphor::regular::PENCIL)) + .clicked() + { + *self.edit_action = Some(item.clone()); + ui.close(); + } + + ui.separator(); + + if ui + .button(format!("{} Clone Template", egui_phosphor::regular::COPY)) + .clicked() + { + *self.clone_action = Some(item.clone()); + ui.close(); + } + + ui.separator(); + + if ui + .button(format!("{} Delete Template", egui_phosphor::regular::TRASH)) + .clicked() + { + *self.delete_action = Some(item.clone()); + ui.close(); + } + } + + fn on_selection_changed(&mut self, _selected_indices: &[usize]) { + // Not used for now + } + } + + let mut handler = TemplateEventHandler { + edit_action: &mut edit_template, + delete_action: &mut delete_template, + clone_action: &mut clone_template, + }; + + let prepared_data = self.table_renderer.prepare_json_data(&self.templates); + self.table_renderer + .render_json_table(ui, &prepared_data, Some(&mut handler)); + + // Process actions after rendering + if let Some(template) = edit_template { + // Prepare dynamic dropdown fields before opening dialog + if let Some(client) = api_client { + self.prepare_template_edit_fields(client); + } + self.open_edit_template_dialog(template); + } + if let Some(template) = delete_template { + if let Some(id) = template.get("id").and_then(|v| v.as_i64()) { + self.pending_delete_ids.push(id); + } + } + + // Handle clone action: open Add New dialog pre-filled with selected template values + if let Some(template) = clone_template { + // Prepare dynamic dropdown fields before opening dialog + if let Some(client) = api_client { + self.prepare_template_edit_fields(client); + } + + // Use shared clone helper to prepare new-item payload + let cloned = crate::core::components::prepare_cloned_value( + &template, + &["id", "template_code"], + Some("name"), + Some(""), + ); + + self.edit_dialog.title = "Add New Template".to_string(); + self.open_edit_template_dialog(cloned); + } + + // Show column selector if open + 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(), + "template_code" + | "name" + | "asset_type" + | "description" + | "asset_tag_generation_string" + | "label_template_name" + | "label_template_id" + ) { + ui.checkbox(&mut column.visible, &column.name); + } + } + }); + + columns[0].add_space(5.0); + + columns[0].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" + ) { + 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_name" + | "zone_code" + | "zone_plus" + | "zone_note" + | "status" + ) { + ui.checkbox(&mut column.visible, &column.name); + } + } + }); + + // Right column + columns[1].group(|ui| { + ui.strong("Financial, Dates & Auto-Calc"); + ui.separator(); + for column in &mut self.table_renderer.columns { + if matches!( + column.field.as_str(), + "price" + | "purchase_date" + | "purchase_date_now" + | "warranty_until" + | "warranty_auto" + | "warranty_auto_amount" + | "warranty_auto_unit" + | "expiry_date" + | "expiry_auto" + | "expiry_auto_amount" + | "expiry_auto_unit" + | "created_at" + ) { + ui.checkbox(&mut column.visible, &column.name); + } + } + }); + + columns[1].add_space(5.0); + + columns[1].group(|ui| { + ui.strong("Quantities & Lending"); + ui.separator(); + for column in &mut self.table_renderer.columns { + if matches!( + column.field.as_str(), + "quantity_total" + | "quantity_used" + | "lendable" + | "lending_status" + | "minimum_role_for_lending" + | "no_scan" + ) { + 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" | "supplier_name" | "notes" | "active" + ) { + 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 (matching the initial setup) + for column in &mut self.table_renderer.columns { + column.visible = matches!( + column.field.as_str(), + "template_code" + | "name" + | "asset_type" + | "description" + | "category_name" + ); + } + } + }); + }); + } + + // Handle pending deletes + if !self.pending_delete_ids.is_empty() { + if let Some(api) = api_client { + let ids_to_delete = self.pending_delete_ids.clone(); + self.pending_delete_ids.clear(); + + for id in ids_to_delete { + let where_clause = serde_json::json!({"id": id}); + match api.delete("templates", where_clause) { + Ok(resp) if resp.success => { + log::info!("Template {} deleted successfully", id); + } + Ok(resp) => { + self.loading_state.last_error = + Some(format!("Delete failed: {:?}", resp.error)); + } + Err(e) => { + self.loading_state.last_error = Some(format!("Delete error: {}", e)); + } + } + } + + // Reload after deletes + self.load_templates(api, limit); + } + } + + // Handle edit dialog save + let ctx = ui.ctx(); + if let Some(result) = self.edit_dialog.show_editor(ctx) { + log::info!( + "🎯 Templates received editor result: {:?}", + result.is_some() + ); + if let Some(updated) = result { + log::info!( + "🎯 Processing template save with data keys: {:?}", + updated.keys().collect::>() + ); + if let Some(api) = api_client { + let mut id_from_updated: Option = None; + if let Some(id_val) = updated.get("id") { + log::info!("Raw id_val for template save: {:?}", id_val); + id_from_updated = if let Some(s) = id_val.as_str() { + if s.trim().is_empty() { + None + } else { + s.trim().parse::().ok() + } + } else { + id_val.as_i64() + }; + } else if let Some(meta_id_val) = updated.get("__editor_item_id") { + log::info!( + "No 'id' in diff; checking __editor_item_id: {:?}", + meta_id_val + ); + id_from_updated = match meta_id_val { + serde_json::Value::String(s) => { + let s = s.trim(); + if s.is_empty() { + None + } else { + s.parse::().ok() + } + } + serde_json::Value::Number(n) => n.as_i64(), + _ => None, + }; + } + if let Some(id) = id_from_updated { + log::info!("Entering UPDATE template path for id {}", id); + let mut cleaned = updated.clone(); + cleaned.remove("__editor_item_id"); + + let additional_fields_update = match Self::parse_additional_fields_input( + cleaned.remove("additional_fields_json"), + ) { + Ok(val) => val, + Err(err) => { + let msg = format!("Additional Fields JSON is invalid: {}", err); + log::error!("{}", msg); + self.loading_state.last_error = Some(msg); + return flags_to_clear; + } + }; + + // Filter empty strings to NULL for UPDATE too + let mut filtered_values = serde_json::Map::new(); + for (key, value) in cleaned.iter() { + if key.starts_with("__editor_") { + continue; + } + match value { + serde_json::Value::String(s) if s.trim().is_empty() => { + filtered_values.insert(key.clone(), serde_json::Value::Null); + } + _ => { + filtered_values.insert(key.clone(), value.clone()); + } + } + } + if let Some(val) = additional_fields_update { + filtered_values.insert("additional_fields".to_string(), val); + } + let values = serde_json::Value::Object(filtered_values); + let where_clause = serde_json::json!({"id": id}); + log::info!( + "Sending UPDATE request: values={:?} where={:?}", + values, + where_clause + ); + match api.update("templates", values, where_clause) { + Ok(resp) if resp.success => { + log::info!("Template {} updated successfully", id); + self.load_templates(api, limit); + } + Ok(resp) => { + let err = format!("Update failed: {:?}", resp.error); + log::error!("{}", err); + self.loading_state.last_error = Some(err); + } + Err(e) => { + let err = format!("Update error: {}", e); + log::error!("{}", err); + self.loading_state.last_error = Some(err); + } + } + } else { + log::info!("🆕 Entering INSERT template path (no valid ID detected)"); + let mut values = updated.clone(); + values.remove("id"); + values.remove("__editor_item_id"); + + let additional_fields_insert = match Self::parse_additional_fields_input( + values.remove("additional_fields_json"), + ) { + Ok(val) => val, + Err(err) => { + let msg = format!("Additional Fields JSON is invalid: {}", err); + log::error!("{}", msg); + self.loading_state.last_error = Some(msg); + return flags_to_clear; + } + }; + let mut filtered_values = serde_json::Map::new(); + for (key, value) in values.iter() { + if key.starts_with("__editor_") { + continue; + } + match value { + serde_json::Value::String(s) if s.trim().is_empty() => { + filtered_values.insert(key.clone(), serde_json::Value::Null); + } + _ => { + filtered_values.insert(key.clone(), value.clone()); + } + } + } + if let Some(val) = additional_fields_insert { + filtered_values.insert("additional_fields".to_string(), val); + } + let values = serde_json::Value::Object(filtered_values); + log::info!("➡️ Sending INSERT request for template: {:?}", values); + match api.insert("templates", values) { + Ok(resp) if resp.success => { + log::info!( + "✅ New template created successfully (id={:?})", + resp.data + ); + self.load_templates(api, limit); + } + Ok(resp) => { + let error_msg = format!("Insert failed: {:?}", resp.error); + log::error!("Template insert failed: {}", error_msg); + self.loading_state.last_error = Some(error_msg); + } + Err(e) => { + let error_msg = format!("Insert error: {}", e); + log::error!("Template insert error: {}", error_msg); + self.loading_state.last_error = Some(error_msg); + } + } + } + } + } + } + + flags_to_clear + } + + fn open_edit_template_dialog(&mut self, mut template: serde_json::Value) { + // Determine whether we are creating a new template (no ID or empty/zero ID) + let is_new = match template.get("id") { + Some(serde_json::Value::String(s)) => s.trim().is_empty(), + Some(serde_json::Value::Number(n)) => n.as_i64().map(|id| id <= 0).unwrap_or(true), + Some(serde_json::Value::Null) | None => true, + _ => false, + }; + + self.edit_dialog.title = if is_new { + "Add New Template".to_string() + } else { + "Edit Template".to_string() + }; + + if let Some(obj) = template.as_object_mut() { + let pretty_json = if let Some(existing) = + obj.get("additional_fields_json").and_then(|v| v.as_str()) + { + existing.to_string() + } else { + match obj.get("additional_fields") { + Some(serde_json::Value::Null) | None => String::new(), + Some(serde_json::Value::String(s)) => s.clone(), + Some(value) => { + serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string()) + } + } + }; + obj.insert( + "additional_fields_json".to_string(), + serde_json::Value::String(pretty_json), + ); + } + + // Debug log to check the template data being passed to editor + log::info!( + "Template data for editor: {}", + serde_json::to_string_pretty(&template) + .unwrap_or_else(|_| "failed to serialize".to_string()) + ); + + if is_new { + // Use open_new so cloned templates keep their preset values when saved + if let Some(obj) = template.as_object() { + self.edit_dialog.open_new(Some(obj)); + } else { + self.edit_dialog.open_new(None); + } + } else { + self.edit_dialog.open(&template); + } + } +} -- cgit v1.2.3-70-g09d2