aboutsummaryrefslogtreecommitdiff
path: root/src/ui/label_templates.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/label_templates.rs')
-rw-r--r--src/ui/label_templates.rs607
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);
+ }
+}