| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625 |
- 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>,
- permissions: Option<&serde_json::Value>,
- ) {
- 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)
- {
- if RibbonUI::check_permission(permissions, "create_label_template") {
- // 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, permissions);
- // 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>,
- permissions: Option<&serde_json::Value>,
- ) {
- 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,
- permissions,
- };
- 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>,
- permissions: Option<&'a serde_json::Value>,
- }
- impl<'a> TableEventHandler<Value> for TempTemplatesEventHandler<'a> {
- fn on_double_click(&mut self, item: &Value, _row_index: usize) {
- if RibbonUI::check_permission(self.permissions, "edit_label_template") {
- 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 RibbonUI::check_permission(self.permissions, "edit_label_template") {
- 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 RibbonUI::check_permission(self.permissions, "create_label_template") {
- 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 RibbonUI::check_permission(self.permissions, "delete_label_template") {
- 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);
- }
- }
|