diff options
Diffstat (limited to 'src/ui/label_templates.rs')
| -rw-r--r-- | src/ui/label_templates.rs | 607 |
1 files changed, 607 insertions, 0 deletions
diff --git a/src/ui/label_templates.rs b/src/ui/label_templates.rs new file mode 100644 index 0000000..fbe373e --- /dev/null +++ b/src/ui/label_templates.rs @@ -0,0 +1,607 @@ +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<serde_json::Value>, + is_loading: bool, + last_error: Option<String>, + 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<i64>, + pending_edit_id: Option<i64>, +} + +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<DeferredAction> = 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<DeferredAction>, + _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<String> = 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::<serde_json::Value>(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<String> = 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::<serde_json::Value>(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::<Value>(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::<Value>(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::<Value>(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<DeferredAction>, +} + +impl<'a> TableEventHandler<Value> 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); + } +} |
