use crate::api::ApiClient; use crate::core::components::form_builder::FormBuilder; use crate::core::components::interactions::ConfirmDialog; use crate::core::{ColumnConfig, TableEventHandler, TableRenderer}; use crate::core::{EditorField, FieldType}; use crate::ui::ribbon::RibbonUI; use eframe::egui; use serde_json::Value; pub struct LabelTemplatesView { templates: Vec, is_loading: bool, last_error: Option, initial_load_done: bool, // Table renderer table_renderer: TableRenderer, // Editor dialogs edit_dialog: FormBuilder, add_dialog: FormBuilder, delete_dialog: ConfirmDialog, // Pending operations pending_delete_id: Option, pending_edit_id: Option, } impl LabelTemplatesView { pub fn new() -> Self { let edit_dialog = Self::create_edit_dialog(); let add_dialog = Self::create_add_dialog(); // Define columns for label_templates table let columns = vec![ ColumnConfig::new("ID", "id").with_width(60.0).hidden(), ColumnConfig::new("Template Code", "template_code").with_width(150.0), ColumnConfig::new("Template Name", "template_name").with_width(200.0), ColumnConfig::new("Layout JSON", "layout_json") .with_width(250.0) .hidden(), ]; Self { templates: Vec::new(), is_loading: false, last_error: None, initial_load_done: false, table_renderer: TableRenderer::new() .with_columns(columns) .with_default_sort("template_name", true) .with_search_fields(vec![ "template_code".to_string(), "template_name".to_string(), ]), edit_dialog, add_dialog, delete_dialog: ConfirmDialog::new( "Delete Label Template", "Are you sure you want to delete this label template?", ), pending_delete_id: None, pending_edit_id: None, } } fn create_edit_dialog() -> FormBuilder { FormBuilder::new( "Edit Label Template", vec![ EditorField { name: "id".into(), label: "ID".into(), field_type: FieldType::Text, required: false, read_only: true, }, EditorField { name: "template_code".into(), label: "Template Code".into(), field_type: FieldType::Text, required: true, read_only: false, }, EditorField { name: "template_name".into(), label: "Template Name".into(), field_type: FieldType::Text, required: true, read_only: false, }, EditorField { name: "layout_json".into(), label: "Layout JSON".into(), field_type: FieldType::MultilineText, required: true, read_only: false, }, ], ) } fn create_add_dialog() -> FormBuilder { FormBuilder::new( "Add Label Template", vec![ EditorField { name: "template_code".into(), label: "Template Code".into(), field_type: FieldType::Text, required: true, read_only: false, }, EditorField { name: "template_name".into(), label: "Template Name".into(), field_type: FieldType::Text, required: true, read_only: false, }, EditorField { name: "layout_json".into(), label: "Layout JSON".into(), field_type: FieldType::MultilineText, required: true, read_only: false, }, ], ) } 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_templates(client); } } fn load_templates(&mut self, api_client: &ApiClient) { use crate::core::tables::get_label_templates; self.is_loading = true; self.last_error = None; match get_label_templates(api_client) { Ok(list) => { self.templates = list; self.is_loading = false; self.initial_load_done = true; } Err(e) => { self.last_error = Some(e.to_string()); self.is_loading = false; self.initial_load_done = true; } } } pub fn render( &mut self, ui: &mut egui::Ui, api_client: Option<&ApiClient>, ribbon_ui: Option<&mut RibbonUI>, ) { self.ensure_loaded(api_client); // Get search query from ribbon first (before mutable borrow) let search_query = ribbon_ui .as_ref() .and_then(|r| r.search_texts.get("labels_search")) .map(|s| s.clone()) .unwrap_or_default(); // Apply search to table renderer self.table_renderer.search_query = search_query; // Handle ribbon actions if let Some(ribbon) = ribbon_ui { if ribbon .checkboxes .get("labels_action_add") .copied() .unwrap_or(false) { // Provide helpful default layout JSON template matching database schema let layout_json = r##"{ "version": "1.0", "background": "#FFFFFF", "elements": [ { "type": "text", "field": "{{asset_tag}}", "x": 5, "y": 10, "fontSize": 14, "fontWeight": "bold", "fontFamily": "Arial" }, { "type": "text", "field": "{{name}}", "x": 5, "y": 28, "fontSize": 10, "fontFamily": "Arial" }, { "type": "qrcode", "field": "{{asset_tag}}", "x": 5, "y": 50, "size": 40 } ] }"##; let default_data = serde_json::json!({ "layout_json": layout_json }); self.add_dialog.open(&default_data); } if ribbon .checkboxes .get("labels_action_refresh") .copied() .unwrap_or(false) { if let Some(client) = api_client { self.load_templates(client); } } } // Error message let mut clear_error = false; if let Some(err) = &self.last_error { ui.horizontal(|ui| { ui.colored_label(egui::Color32::RED, format!("Error: {}", err)); if ui.button("Close").clicked() { clear_error = true; } }); ui.separator(); } if clear_error { self.last_error = None; } // Loading indicator if self.is_loading { ui.spinner(); ui.label("Loading label templates..."); return; } // Render table with event handling self.render_table_with_events(ui, api_client); // Handle dialogs self.handle_dialogs(ui, api_client); // Process deferred actions from context menus self.process_deferred_actions(ui, api_client); } fn render_table_with_events(&mut self, ui: &mut egui::Ui, api_client: Option<&ApiClient>) { let templates_clone = self.templates.clone(); let prepared_data = self.table_renderer.prepare_json_data(&templates_clone); let mut deferred_actions: Vec = Vec::new(); let mut temp_handler = TempTemplatesEventHandler { api_client, deferred_actions: &mut deferred_actions, }; self.table_renderer .render_json_table(ui, &prepared_data, Some(&mut temp_handler)); self.process_temp_deferred_actions(deferred_actions, api_client); } fn process_temp_deferred_actions( &mut self, actions: Vec, _api_client: Option<&ApiClient>, ) { for action in actions { match action { DeferredAction::DoubleClick(template) => { log::info!( "Processing double-click edit for template: {:?}", template.get("template_name") ); self.edit_dialog.open(&template); if let Some(id) = template.get("id").and_then(|v| v.as_i64()) { self.pending_edit_id = Some(id); } } DeferredAction::ContextEdit(template) => { log::info!( "Processing context menu edit for template: {:?}", template.get("template_name") ); self.edit_dialog.open(&template); if let Some(id) = template.get("id").and_then(|v| v.as_i64()) { self.pending_edit_id = Some(id); } } DeferredAction::ContextDelete(template) => { let name = template .get("template_name") .and_then(|v| v.as_str()) .unwrap_or("Unknown"); let id = template.get("id").and_then(|v| v.as_i64()).unwrap_or(-1); log::info!("Processing context menu delete for template: {}", name); self.pending_delete_id = Some(id); self.delete_dialog.open(name.to_string(), id.to_string()); } DeferredAction::ContextClone(template) => { log::info!( "Processing context menu clone for template: {:?}", template.get("template_name") ); // Build payload for Add dialog using shared helper let mut cloned = crate::core::components::prepare_cloned_value( &template, &["id", "template_code"], Some("template_name"), Some(""), ); // Ensure layout_json is a string for the editor if let Some(obj) = cloned.as_object_mut() { if let Some(v) = template.get("layout_json") { let as_string = if let Some(s) = v.as_str() { s.to_string() } else { serde_json::to_string_pretty(v).unwrap_or_else(|_| "{}".to_string()) }; obj.insert( "layout_json".to_string(), serde_json::Value::String(as_string), ); } } self.add_dialog.title = "Add Label Template".to_string(); self.add_dialog.open(&cloned); } } } } fn handle_dialogs(&mut self, ui: &mut egui::Ui, api_client: Option<&ApiClient>) { // Delete confirmation dialog if let Some(confirmed) = self.delete_dialog.show_dialog(ui.ctx()) { if confirmed { if let (Some(id), Some(client)) = (self.pending_delete_id, api_client) { let where_clause = serde_json::json!({"id": id}); match client.delete("label_templates", where_clause) { Ok(resp) => { if resp.success { log::info!("Label template {} deleted successfully", id); self.load_templates(client); } else { self.last_error = Some(format!("Delete failed: {:?}", resp.error)); log::error!("Delete failed: {:?}", resp.error); } } Err(e) => { self.last_error = Some(format!("Failed to delete template: {}", e)); log::error!("Failed to delete template: {}", e); } } } self.pending_delete_id = None; } } // Edit dialog if let Some(Some(updated)) = self.edit_dialog.show_editor(ui.ctx()) { if let (Some(id), Some(client)) = (self.pending_edit_id, api_client) { let where_clause = serde_json::json!({"id": id}); let mut to_update = updated; // Remove editor metadata let mut meta_keys: Vec = to_update .keys() .filter(|k| k.starts_with("__editor_")) .cloned() .collect(); // Also remove __editor_item_id specifically if to_update.contains_key("__editor_item_id") { meta_keys.push("__editor_item_id".to_string()); } for k in meta_keys { to_update.remove(&k); } // Send layout_json as actual JSON object if let Some(val) = to_update.get_mut("layout_json") { if let Some(s) = val.as_str() { match serde_json::from_str::(s) { Ok(json_val) => { // Send as actual JSON object, not string *val = json_val; } Err(e) => { self.last_error = Some(format!("Layout JSON is invalid: {}", e)); return; } } } } match client.update( "label_templates", serde_json::Value::Object(to_update.clone()), where_clause, ) { Ok(resp) => { if resp.success { log::info!("Label template {} updated successfully", id); self.load_templates(client); } else { self.last_error = Some(format!("Update failed: {:?}", resp.error)); log::error!("Update failed: {:?}", resp.error); } } Err(e) => { self.last_error = Some(format!("Failed to update template: {}", e)); log::error!("Failed to update template: {}", e); } } self.pending_edit_id = None; } } // Add dialog if let Some(Some(new_data)) = self.add_dialog.show_editor(ui.ctx()) { if let Some(client) = api_client { let mut payload = new_data; // Strip any editor metadata that may have leaked in let meta_strip: Vec = payload .keys() .filter(|k| k.starts_with("__editor_")) .cloned() .collect(); for k in meta_strip { payload.remove(&k); } // Send layout_json as actual JSON object if let Some(val) = payload.get_mut("layout_json") { if let Some(s) = val.as_str() { match serde_json::from_str::(s) { Ok(json_val) => { // Send as actual JSON object, not string *val = json_val; } Err(e) => { self.last_error = Some(format!("Layout JSON is invalid: {}", e)); return; } } } } match client.insert("label_templates", serde_json::Value::Object(payload)) { Ok(resp) => { if resp.success { log::info!("Label template added successfully"); self.load_templates(client); } else { self.last_error = Some(format!("Insert failed: {:?}", resp.error)); log::error!("Insert failed: {:?}", resp.error); } } Err(e) => { self.last_error = Some(format!("Failed to add template: {}", e)); log::error!("Failed to add template: {}", e); } } } } } fn process_deferred_actions(&mut self, ui: &mut egui::Ui, _api_client: Option<&ApiClient>) { // Handle double-click edit if let Some(template) = ui .ctx() .data_mut(|d| d.remove_temp::(egui::Id::new("label_double_click_edit"))) { log::info!( "Processing double-click edit for template: {:?}", template.get("template_name") ); self.edit_dialog.open(&template); if let Some(id) = template.get("id").and_then(|v| v.as_i64()) { self.pending_edit_id = Some(id); } } // Handle context menu actions if let Some(template) = ui .ctx() .data_mut(|d| d.remove_temp::(egui::Id::new("label_context_menu_edit"))) { log::info!( "Processing context menu edit for template: {:?}", template.get("template_name") ); self.edit_dialog.open(&template); if let Some(id) = template.get("id").and_then(|v| v.as_i64()) { self.pending_edit_id = Some(id); } } if let Some(template) = ui .ctx() .data_mut(|d| d.remove_temp::(egui::Id::new("label_context_menu_delete"))) { let name = template .get("template_name") .and_then(|v| v.as_str()) .unwrap_or("Unknown"); let id = template.get("id").and_then(|v| v.as_i64()).unwrap_or(-1); log::info!("Processing context menu delete for template: {}", name); self.pending_delete_id = Some(id); self.delete_dialog.open(name.to_string(), id.to_string()); } } } impl Default for LabelTemplatesView { fn default() -> Self { Self::new() } } #[derive(Clone)] enum DeferredAction { DoubleClick(Value), ContextEdit(Value), ContextDelete(Value), ContextClone(Value), } // Temporary event handler that collects actions for later processing struct TempTemplatesEventHandler<'a> { #[allow(dead_code)] api_client: Option<&'a ApiClient>, deferred_actions: &'a mut Vec, } impl<'a> TableEventHandler for TempTemplatesEventHandler<'a> { fn on_double_click(&mut self, item: &Value, _row_index: usize) { log::info!( "Double-click detected on template: {:?}", item.get("template_name") ); self.deferred_actions .push(DeferredAction::DoubleClick(item.clone())); } fn on_context_menu(&mut self, ui: &mut egui::Ui, item: &Value, _row_index: usize) { if ui .button(format!("{} Edit Template", egui_phosphor::regular::PENCIL)) .clicked() { log::info!( "Context menu edit clicked for template: {:?}", item.get("template_name") ); self.deferred_actions .push(DeferredAction::ContextEdit(item.clone())); ui.close(); } ui.separator(); if ui .button(format!("{} Clone Template", egui_phosphor::regular::COPY)) .clicked() { log::info!( "Context menu clone clicked for template: {:?}", item.get("template_name") ); self.deferred_actions .push(DeferredAction::ContextClone(item.clone())); ui.close(); } ui.separator(); if ui .button(format!("{} Delete Template", egui_phosphor::regular::TRASH)) .clicked() { log::info!( "Context menu delete clicked for template: {:?}", item.get("template_name") ); self.deferred_actions .push(DeferredAction::ContextDelete(item.clone())); ui.close(); } } fn on_selection_changed(&mut self, selected_indices: &[usize]) { log::debug!("Template selection changed: {:?}", selected_indices); } }