aboutsummaryrefslogtreecommitdiff
path: root/src/ui/templates.rs
diff options
context:
space:
mode:
authorUMTS at Teleco <crt@teleco.ch>2025-12-13 02:51:15 +0100
committerUMTS at Teleco <crt@teleco.ch>2025-12-13 02:51:15 +0100
commit8323fdd73272a2882781aba3c499ba0be3dff2a6 (patch)
treeffbf86473933e69cfaeef30d5c6ea7e5b494856c /src/ui/templates.rs
committing to insanityHEADmaster
Diffstat (limited to 'src/ui/templates.rs')
-rw-r--r--src/ui/templates.rs1113
1 files changed, 1113 insertions, 0 deletions
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<serde_json::Value>,
+ loading_state: LoadingState,
+ table_renderer: TableRenderer,
+ show_column_panel: bool,
+ edit_dialog: FormBuilder,
+ pending_delete_ids: Vec<i64>,
+}
+
+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<EditorField> = 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<serde_json::Value>,
+ ) -> Result<Option<serde_json::Value>, 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::<serde_json::Value>(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<u32>) {
+ 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<String> {
+ 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<serde_json::Value> = None;
+ let mut delete_template: Option<serde_json::Value> = None;
+ let mut clone_template: Option<serde_json::Value> = None;
+
+ struct TemplateEventHandler<'a> {
+ edit_action: &'a mut Option<serde_json::Value>,
+ delete_action: &'a mut Option<serde_json::Value>,
+ clone_action: &'a mut Option<serde_json::Value>,
+ }
+
+ impl<'a> crate::core::table_renderer::TableEventHandler<serde_json::Value>
+ 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::<Vec<_>>()
+ );
+ if let Some(api) = api_client {
+ let mut id_from_updated: Option<i64> = 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::<i64>().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::<i64>().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);
+ }
+ }
+}