| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144 |
- 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>,
- permissions: Option<&serde_json::Value>,
- ) -> 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());
- if crate::ui::ribbon::RibbonUI::check_permission(permissions, "create_template") {
- // 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());
- if crate::ui::ribbon::RibbonUI::check_permission(permissions, "edit_template") {
- // 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());
- if crate::ui::ribbon::RibbonUI::check_permission(permissions, "delete_template") {
- // 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();
- let ignored_fields = [
- "id",
- "created_at",
- "audit_task_name",
- "category_name",
- "category_code",
- "zone_name",
- "supplier_name",
- "label_template_name",
- "zone_code",
- ];
- for (key, value) in cleaned.iter() {
- if key.starts_with("__editor_") || ignored_fields.contains(&key.as_str()) {
- 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();
- let ignored_fields = [
- "id",
- "created_at",
- "audit_task_name",
- "category_name",
- "category_code",
- "zone_name",
- "supplier_name",
- "label_template_name",
- "zone_code",
- ];
- for (key, value) in values.iter() {
- if key.starts_with("__editor_") || ignored_fields.contains(&key.as_str()) {
- 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);
- }
- }
- }
|