aboutsummaryrefslogtreecommitdiff
path: root/src/ui/categories.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/categories.rs
committing to insanityHEADmaster
Diffstat (limited to 'src/ui/categories.rs')
-rw-r--r--src/ui/categories.rs892
1 files changed, 892 insertions, 0 deletions
diff --git a/src/ui/categories.rs b/src/ui/categories.rs
new file mode 100644
index 0000000..3b119e5
--- /dev/null
+++ b/src/ui/categories.rs
@@ -0,0 +1,892 @@
+use crate::api::ApiClient;
+use crate::core::components::form_builder::FormBuilder;
+use crate::core::components::interactions::ConfirmDialog;
+use crate::core::table_renderer::{ColumnConfig, TableEventHandler, TableRenderer};
+use crate::core::tables::get_categories;
+use crate::core::{EditorField, FieldType};
+use crate::ui::ribbon::RibbonUI;
+use eframe::egui;
+use serde_json::Value;
+
+pub struct CategoriesView {
+ categories: Vec<serde_json::Value>,
+ is_loading: bool,
+ last_error: Option<String>,
+ initial_load_done: bool,
+ load_attempted: bool, // New field to track if we've tried loading
+
+ // Editor dialogs
+ edit_dialog: FormBuilder,
+ add_dialog: FormBuilder,
+ delete_dialog: ConfirmDialog,
+
+ // Pending operations
+ pending_delete_ids: Vec<i64>, // Changed from Option<i64> to Vec<i64> for bulk delete support
+ pending_edit_ids: Vec<i64>, // Changed from Option<i64> to Vec<i64> for bulk edit support
+
+ // Table rendering
+ table_renderer: crate::core::table_renderer::TableRenderer,
+}
+
+impl CategoriesView {
+ pub fn new() -> Self {
+ let edit_dialog = Self::create_edit_dialog();
+ let add_dialog = Self::create_placeholder_add_dialog();
+
+ // Define columns for categories table - code before name as requested
+ let columns = vec![
+ ColumnConfig::new("ID", "id").with_width(60.0).hidden(),
+ ColumnConfig::new("Category Code", "category_code").with_width(120.0),
+ ColumnConfig::new("Category Name", "category_name").with_width(200.0),
+ ColumnConfig::new("Description", "category_description").with_width(300.0),
+ ColumnConfig::new("Parent ID", "parent_id")
+ .with_width(80.0)
+ .hidden(),
+ ColumnConfig::new("Parent Category", "parent_category_name").with_width(150.0),
+ ];
+
+ Self {
+ categories: Vec::new(),
+ is_loading: false,
+ last_error: None,
+ initial_load_done: false,
+ load_attempted: false,
+ edit_dialog,
+ add_dialog,
+ delete_dialog: ConfirmDialog::new(
+ "Delete Category",
+ "Are you sure you want to delete this category? This will affect all assets using this category.",
+ ),
+ pending_delete_ids: Vec::new(),
+ pending_edit_ids: Vec::new(),
+ table_renderer: TableRenderer::new()
+ .with_columns(columns)
+ .with_default_sort("category_code", true) // Sort by category code alphabetically
+ .with_search_fields(vec![
+ "category_name".to_string(),
+ "category_code".to_string(),
+ "category_description".to_string(),
+ "parent_category_name".to_string(),
+ ]),
+ }
+ }
+
+ fn create_edit_dialog() -> FormBuilder {
+ FormBuilder::new(
+ "Edit Category",
+ vec![
+ EditorField {
+ name: "id".into(),
+ label: "ID".into(),
+ field_type: FieldType::Text,
+ required: false,
+ read_only: true,
+ },
+ EditorField {
+ name: "category_name".into(),
+ label: "Category Name".into(),
+ field_type: FieldType::Text,
+ required: true,
+ read_only: false,
+ },
+ EditorField {
+ name: "category_code".into(),
+ label: "Category Code".into(),
+ field_type: FieldType::Text,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "category_description".into(),
+ label: "Description".into(),
+ field_type: FieldType::MultilineText,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "parent_id".into(),
+ label: "Parent Category".into(),
+ field_type: FieldType::Text, // TODO: Make this a dropdown with other categories
+ required: false,
+ read_only: false,
+ },
+ ],
+ )
+ }
+
+ fn create_placeholder_add_dialog() -> FormBuilder {
+ FormBuilder::new(
+ "Add Category",
+ vec![
+ EditorField {
+ name: "category_name".into(),
+ label: "Category Name".into(),
+ field_type: FieldType::Text,
+ required: true,
+ read_only: false,
+ },
+ EditorField {
+ name: "category_code".into(),
+ label: "Category Code".into(),
+ field_type: FieldType::Text,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "category_description".into(),
+ label: "Description".into(),
+ field_type: FieldType::MultilineText,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "parent_id".into(),
+ label: "Parent Category".into(),
+ field_type: FieldType::Text, // Will be updated to dropdown when opened
+ required: false,
+ read_only: false,
+ },
+ ],
+ )
+ }
+
+ fn create_add_dialog_with_options(&self) -> FormBuilder {
+ let category_options = self.create_category_dropdown_options(None);
+
+ FormBuilder::new(
+ "Add Category",
+ vec![
+ EditorField {
+ name: "category_name".into(),
+ label: "Category Name".into(),
+ field_type: FieldType::Text,
+ required: true,
+ read_only: false,
+ },
+ EditorField {
+ name: "category_code".into(),
+ label: "Category Code".into(),
+ field_type: FieldType::Text,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "category_description".into(),
+ label: "Description".into(),
+ field_type: FieldType::MultilineText,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "parent_id".into(),
+ label: "Parent Category".into(),
+ field_type: FieldType::Dropdown(category_options),
+ required: false,
+ read_only: false,
+ },
+ ],
+ )
+ }
+
+ fn load_categories(&mut self, api_client: &ApiClient) {
+ // Don't start a new load if we're already loading
+ if self.is_loading {
+ return;
+ }
+
+ self.is_loading = true;
+ self.last_error = None;
+ self.load_attempted = true;
+
+ match get_categories(api_client, Some(200)) {
+ Ok(categories) => {
+ self.categories = categories;
+ self.initial_load_done = true;
+ log::info!(
+ "Categories loaded successfully: {} items",
+ self.categories.len()
+ );
+ }
+ Err(e) => {
+ let error_msg = format!("Error loading categories: {}", e);
+ log::error!("{}", error_msg);
+ self.last_error = Some(error_msg);
+ }
+ }
+
+ self.is_loading = false;
+ }
+
+ /// Get selected category IDs for bulk operations (works with filtered view)
+ fn get_selected_ids(&self) -> Vec<i64> {
+ let filtered_data = self.table_renderer.prepare_json_data(&self.categories);
+ let mut ids = Vec::new();
+ for &row_idx in &self.table_renderer.selection.selected_rows {
+ // prepared_data contains tuples of (original_index, &Value)
+ if let Some((_orig_idx, category)) = filtered_data.get(row_idx) {
+ if let Some(id) = category.get("id").and_then(|v| v.as_i64()) {
+ ids.push(id);
+ }
+ }
+ }
+ ids
+ }
+
+ /// Sanitize form data for categories before sending to the API.
+ /// - Removes internal editor fields prefixed with __editor_
+ /// - Converts empty-string parent_id to JSON null
+ /// - Coerces numeric parent_id strings to numbers
+ fn sanitize_category_map(
+ form_data: &serde_json::Map<String, Value>,
+ ) -> serde_json::Map<String, Value> {
+ let mut out = serde_json::Map::new();
+ for (k, v) in form_data.iter() {
+ // Skip internal editor fields
+ if k.starts_with("__editor_") {
+ continue;
+ }
+
+ if k == "parent_id" {
+ // parent_id might be sent as "" for None. Convert to null.
+ if v.is_null() {
+ out.insert(k.clone(), Value::Null);
+ continue;
+ }
+
+ if let Some(s) = v.as_str() {
+ let s_trim = s.trim();
+ if s_trim.is_empty() {
+ out.insert(k.clone(), Value::Null);
+ continue;
+ }
+ // Try parse integer
+ if let Ok(n) = s_trim.parse::<i64>() {
+ out.insert(k.clone(), Value::Number((n).into()));
+ continue;
+ }
+ // Fallback: keep as string
+ out.insert(k.clone(), Value::String(s_trim.to_string()));
+ continue;
+ }
+
+ // If it's already a number, keep it
+ if v.is_i64() || v.is_u64() || v.is_f64() {
+ out.insert(k.clone(), v.clone());
+ continue;
+ }
+
+ // Anything else -> keep as-is
+ out.insert(k.clone(), v.clone());
+ continue;
+ }
+
+ // For everything else, just copy through
+ out.insert(k.clone(), v.clone());
+ }
+ out
+ }
+
+ fn create_category(
+ &mut self,
+ api_client: &ApiClient,
+ form_data: &serde_json::Map<String, Value>,
+ ) {
+ // Sanitize and coerce form data (convert empty parent_id -> null, remove internal fields)
+ let sanitized = Self::sanitize_category_map(form_data);
+ let values = serde_json::Value::Object(sanitized);
+
+ match api_client.insert("categories", values) {
+ Ok(resp) if resp.success => {
+ log::info!("Category created successfully");
+ self.load_categories(api_client); // Reload to get fresh data
+ }
+ Ok(resp) => {
+ let error_msg = format!("Create failed: {:?}", resp.error);
+ log::error!("{}", error_msg);
+ self.last_error = Some(error_msg);
+ }
+ Err(e) => {
+ let error_msg = format!("Create error: {}", e);
+ log::error!("{}", error_msg);
+ self.last_error = Some(error_msg);
+ }
+ }
+ }
+
+ fn update_category(
+ &mut self,
+ api_client: &ApiClient,
+ category_id: i64,
+ form_data: &serde_json::Map<String, Value>,
+ ) {
+ // Sanitize form data (remove internal fields, coerce parent_id). Also ensure we don't send id.
+ let mut filtered_data = Self::sanitize_category_map(form_data);
+ filtered_data.remove("id");
+
+ // Convert form data to JSON object
+ let values = serde_json::Value::Object(filtered_data);
+ let where_clause = serde_json::json!({"id": category_id});
+
+ match api_client.update("categories", values, where_clause) {
+ Ok(resp) if resp.success => {
+ log::info!("Category updated successfully");
+ self.load_categories(api_client); // Reload to get fresh data
+ }
+ Ok(resp) => {
+ let error_msg = format!("Update failed: {:?}", resp.error);
+ log::error!("{}", error_msg);
+
+ // Check for foreign key constraint errors
+ if let Some(err_str) = resp.error.as_ref() {
+ if err_str.contains("foreign key constraint") {
+ self.last_error = Some(
+ "Cannot update category: Invalid parent category reference."
+ .to_string(),
+ );
+ } else {
+ self.last_error = Some(error_msg);
+ }
+ } else {
+ self.last_error = Some(error_msg);
+ }
+ }
+ Err(e) => {
+ let error_msg = format!("Update error: {}", e);
+ log::error!("{}", error_msg);
+ self.last_error = Some(error_msg);
+ }
+ }
+ }
+
+ fn delete_category(&mut self, api_client: &ApiClient, category_id: i64) {
+ let where_clause = serde_json::json!({"id": category_id});
+ match api_client.delete("categories", where_clause) {
+ Ok(resp) if resp.success => {
+ log::info!("Category deleted successfully");
+ self.load_categories(api_client); // Reload to get fresh data
+ }
+ Ok(resp) => {
+ let error_msg = format!("Delete failed: {:?}", resp.error);
+ log::error!("{}", error_msg);
+
+ // Check for foreign key constraint errors and provide user-friendly message
+ if let Some(err_str) = resp.error.as_ref() {
+ if err_str.contains("foreign key constraint")
+ || err_str.contains("Cannot delete or update a parent row")
+ {
+ self.last_error = Some(
+ "Cannot delete category: It is being used by other categories as their parent, or by assets. \
+ Please reassign dependent items first.".to_string()
+ );
+ } else {
+ self.last_error = Some(error_msg);
+ }
+ } else {
+ self.last_error = Some(error_msg);
+ }
+ }
+ Err(e) => {
+ let error_msg = format!("Delete error: {}", e);
+ log::error!("{}", error_msg);
+
+ // Check for foreign key constraint in error message
+ let err_lower = error_msg.to_lowercase();
+ if err_lower.contains("foreign key") || err_lower.contains("constraint") {
+ self.last_error = Some(
+ "Cannot delete category: It is being used by other categories as their parent, or by assets. \
+ Please reassign dependent items first.".to_string()
+ );
+ } else {
+ self.last_error = Some(error_msg);
+ }
+ }
+ }
+ }
+
+ pub fn show(
+ &mut self,
+ ui: &mut egui::Ui,
+ api_client: Option<&ApiClient>,
+ ribbon: Option<&mut RibbonUI>,
+ ) -> Vec<String> {
+ let mut flags_to_clear = Vec::new();
+
+ // Handle context menu actions and double-click
+ if let Some(item) = ui.ctx().data_mut(|d| {
+ d.remove_temp::<serde_json::Value>(egui::Id::new("cat_double_click_edit"))
+ }) {
+ self.open_editor_with(&item);
+ ui.ctx().request_repaint();
+ }
+
+ if let Some(item) = ui.ctx().data_mut(|d| {
+ d.remove_temp::<serde_json::Value>(egui::Id::new("cat_context_menu_edit"))
+ }) {
+ self.open_editor_with(&item);
+ ui.ctx().request_repaint();
+ }
+
+ if let Some(item) = ui.ctx().data_mut(|d| {
+ d.remove_temp::<serde_json::Value>(egui::Id::new("cat_context_menu_delete"))
+ }) {
+ let name = item
+ .get("category_name")
+ .and_then(|v| v.as_str())
+ .unwrap_or("Unknown")
+ .to_string();
+ let id = item.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
+ self.pending_delete_ids = vec![id]; // Changed to vector
+ self.delete_dialog.open(name, id.to_string());
+ ui.ctx().request_repaint();
+ }
+
+ // Auto-load on first show, but only try once unless user explicitly requests retry
+ if !self.initial_load_done && !self.is_loading && !self.load_attempted {
+ if let Some(client) = api_client {
+ log::info!("Categories view never loaded, triggering initial auto-load");
+ self.load_categories(client);
+ }
+ }
+
+ // Extract search query and handle ribbon actions
+ let search_query = if let Some(ribbon) = ribbon.as_ref() {
+ let query = ribbon
+ .search_texts
+ .get("categories_search")
+ .filter(|s| !s.trim().is_empty())
+ .map(|s| s.as_str());
+
+ // Handle ribbon actions
+ if ribbon
+ .checkboxes
+ .get("categories_refresh")
+ .copied()
+ .unwrap_or(false)
+ {
+ if let Some(client) = api_client {
+ // Reset error state and allow fresh load
+ self.last_error = None;
+ self.load_categories(client);
+ }
+ flags_to_clear.push("categories_refresh".to_string());
+ }
+
+ if ribbon
+ .checkboxes
+ .get("categories_add")
+ .copied()
+ .unwrap_or(false)
+ {
+ // Create a new add dialog with current category options
+ self.add_dialog = self.create_add_dialog_with_options();
+ self.add_dialog.open(&serde_json::json!({})); // Open with empty data
+ flags_to_clear.push("categories_add".to_string());
+ }
+
+ if ribbon
+ .checkboxes
+ .get("categories_edit")
+ .copied()
+ .unwrap_or(false)
+ {
+ // Get selected category IDs
+ let selected_ids = self.get_selected_ids();
+
+ if !selected_ids.is_empty() {
+ // For edit, only edit the first selected category (bulk edit of categories is complex)
+ if let Some(&first_id) = selected_ids.first() {
+ // Clone the category to avoid borrowing issues
+ let category = self
+ .categories
+ .iter()
+ .find(|c| c.get("id").and_then(|v| v.as_i64()) == Some(first_id))
+ .cloned();
+
+ if let Some(cat) = category {
+ self.open_editor_with(&cat);
+ }
+ }
+ } else {
+ log::warn!("Edit requested but no categories selected");
+ }
+ flags_to_clear.push("categories_edit".to_string());
+ }
+
+ if ribbon
+ .checkboxes
+ .get("categories_delete")
+ .copied()
+ .unwrap_or(false)
+ {
+ // Get selected category IDs for bulk delete
+ let selected_ids = self.get_selected_ids();
+
+ if !selected_ids.is_empty() {
+ self.pending_delete_ids = selected_ids.clone();
+ let count = selected_ids.len();
+
+ // Show dialog with appropriate message for single or multiple deletes
+ let message =
+ if count == 1 {
+ // Get the category name for single delete
+ if let Some(category) = self.categories.iter().find(|c| {
+ c.get("id").and_then(|v| v.as_i64()) == Some(selected_ids[0])
+ }) {
+ category
+ .get("category_name")
+ .and_then(|v| v.as_str())
+ .unwrap_or("Unknown")
+ .to_string()
+ } else {
+ "Unknown".to_string()
+ }
+ } else {
+ format!("{} categories", count)
+ };
+
+ self.delete_dialog
+ .open(message, format!("IDs: {:?}", selected_ids));
+ } else {
+ log::warn!("Delete requested but no categories selected");
+ }
+ flags_to_clear.push("categories_delete".to_string());
+ }
+
+ query
+ } else {
+ None
+ };
+
+ // Top toolbar
+ ui.horizontal(|ui| {
+ ui.heading("Categories");
+
+ if self.is_loading {
+ ui.spinner();
+ ui.label("Loading...");
+ } else {
+ ui.label(format!("{} categories", self.categories.len()));
+ }
+
+ ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
+ if ui.button("➕ Add Category").clicked() {
+ self.add_dialog.open_new(None);
+ }
+
+ if ui.button("Refresh").clicked() {
+ if let Some(client) = api_client {
+ // Reset error state and allow fresh load
+ self.last_error = None;
+ self.load_categories(client);
+ }
+ }
+ });
+ });
+
+ ui.separator();
+
+ // Error display with retry option
+ if let Some(error) = &self.last_error {
+ ui.colored_label(egui::Color32::RED, format!("Error: {}", error));
+ ui.horizontal(|ui| {
+ if ui.button("Try Again").clicked() {
+ if let Some(client) = api_client {
+ // Reset state and try loading again
+ self.load_attempted = false;
+ self.initial_load_done = false;
+ self.load_categories(client);
+ }
+ }
+ if ui.button("Clear Error").clicked() {
+ self.last_error = None;
+ }
+ });
+ ui.separator();
+ }
+
+ // Categories table
+ if !self.is_loading && !self.categories.is_empty() {
+ self.render_table(ui, search_query);
+ } else if !self.is_loading {
+ ui.centered_and_justified(|ui| {
+ ui.label("No categories found. Click 'Add Category' to create one.");
+ });
+ }
+
+ // Handle dialogs
+ if let Some(api_client) = api_client {
+ // Add dialog
+ if let Some(result) = self.add_dialog.show_editor(ui.ctx()) {
+ if let Some(category_data) = result {
+ log::info!("Creating new category: {:?}", category_data);
+ self.create_category(api_client, &category_data);
+ }
+ }
+
+ // Edit dialog
+ if let Some(result) = self.edit_dialog.show_editor(ui.ctx()) {
+ if let Some(category_data) = result {
+ // Support bulk edit: if pending_edit_ids is empty, try to get ID from dialog
+ let ids_to_edit: Vec<i64> = if !self.pending_edit_ids.is_empty() {
+ std::mem::take(&mut self.pending_edit_ids)
+ } else {
+ // Single edit from dialog - extract ID from __editor_item_id or category data
+ category_data
+ .get("__editor_item_id")
+ .and_then(|v| v.as_str())
+ .or_else(|| category_data.get("id").and_then(|v| v.as_str()))
+ .and_then(|s| s.parse::<i64>().ok())
+ .map(|id| vec![id])
+ .unwrap_or_default()
+ };
+
+ for category_id in ids_to_edit {
+ log::info!("Updating category {}: {:?}", category_id, category_data);
+ self.update_category(api_client, category_id, &category_data);
+ }
+ }
+ }
+
+ // Delete dialog - support bulk delete
+ if let Some(confirmed) = self.delete_dialog.show_dialog(ui.ctx()) {
+ log::info!(
+ "Delete dialog result: confirmed={}, pending_delete_ids={:?}",
+ confirmed,
+ self.pending_delete_ids
+ );
+ if confirmed && !self.pending_delete_ids.is_empty() {
+ // Clone the IDs to avoid borrowing issues
+ let ids_to_delete = self.pending_delete_ids.clone();
+ for category_id in ids_to_delete {
+ log::info!("Deleting category: {}", category_id);
+ self.delete_category(api_client, category_id);
+ }
+ }
+ self.pending_delete_ids.clear();
+ }
+ }
+
+ flags_to_clear
+ }
+
+ fn render_table(&mut self, ui: &mut egui::Ui, search_query: Option<&str>) {
+ // Apply search query to TableRenderer (clear if empty)
+ match search_query {
+ Some(query) => self.table_renderer.set_search_query(query.to_string()),
+ None => self.table_renderer.set_search_query(String::new()), // Clear search when empty
+ }
+
+ // Prepare sorted/filtered data
+ let prepared_data = self.table_renderer.prepare_json_data(&self.categories);
+
+ // Create temporary event handler for deferred actions
+ let mut deferred_actions = Vec::new();
+ let mut event_handler = TempCategoriesEventHandler {
+ deferred_actions: &mut deferred_actions,
+ };
+
+ // Render the table with TableRenderer
+ self.table_renderer
+ .render_json_table(ui, &prepared_data, Some(&mut event_handler));
+
+ // Process deferred actions
+ for action in deferred_actions {
+ match action {
+ DeferredCategoryAction::DoubleClick(category) => {
+ self.open_editor_with(&category);
+ }
+ DeferredCategoryAction::ContextEdit(category) => {
+ self.open_editor_with(&category);
+ }
+ DeferredCategoryAction::ContextDelete(category) => {
+ let name = category
+ .get("category_name")
+ .and_then(|v| v.as_str())
+ .unwrap_or("Unknown")
+ .to_string();
+ let id = category.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
+ self.pending_delete_ids = vec![id]; // Changed to vector
+ self.delete_dialog.open(name, id.to_string());
+ }
+ DeferredCategoryAction::ContextClone(category) => {
+ // Prepare Add dialog with up-to-date dropdown options
+ self.add_dialog = self.create_add_dialog_with_options();
+
+ // Use the shared helper to clear ID/code and suffix the name
+ let cloned = crate::core::components::prepare_cloned_value(
+ &category,
+ &["id", "category_code"],
+ Some("category_name"),
+ Some(""),
+ );
+
+ self.add_dialog.title = "Add Category".to_string();
+ self.add_dialog.open(&cloned);
+ }
+ }
+ }
+ }
+
+ fn create_category_dropdown_options(&self, exclude_id: Option<i64>) -> Vec<(String, String)> {
+ let mut options = vec![("".to_string(), "None (Root Category)".to_string())];
+
+ for category in &self.categories {
+ if let Some(id) = category.get("id").and_then(|v| v.as_i64()) {
+ // Exclude the current category to prevent circular references
+ if let Some(exclude) = exclude_id {
+ if id == exclude {
+ continue;
+ }
+ }
+
+ let name = category
+ .get("category_name")
+ .and_then(|v| v.as_str())
+ .unwrap_or("Unknown")
+ .to_string();
+ let code = category
+ .get("category_code")
+ .and_then(|v| v.as_str())
+ .unwrap_or("");
+
+ let display_name = if code.is_empty() {
+ name
+ } else {
+ format!("{} - {}", code, name)
+ };
+
+ options.push((id.to_string(), display_name));
+ }
+ }
+
+ options
+ }
+
+ fn create_edit_dialog_with_options(&self, exclude_id: Option<i64>) -> FormBuilder {
+ let category_options = self.create_category_dropdown_options(exclude_id);
+
+ FormBuilder::new(
+ "Edit Category",
+ vec![
+ EditorField {
+ name: "id".into(),
+ label: "ID".into(),
+ field_type: FieldType::Text,
+ required: false,
+ read_only: true,
+ },
+ EditorField {
+ name: "category_name".into(),
+ label: "Category Name".into(),
+ field_type: FieldType::Text,
+ required: true,
+ read_only: false,
+ },
+ EditorField {
+ name: "category_code".into(),
+ label: "Category Code".into(),
+ field_type: FieldType::Text,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "category_description".into(),
+ label: "Description".into(),
+ field_type: FieldType::MultilineText,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "parent_id".into(),
+ label: "Parent Category".into(),
+ field_type: FieldType::Dropdown(category_options),
+ required: false,
+ read_only: false,
+ },
+ ],
+ )
+ }
+
+ fn open_editor_with(&mut self, item: &serde_json::Value) {
+ let category_id = item.get("id").and_then(|v| v.as_i64());
+
+ // Clear pending_edit_ids since we're opening a single-item editor
+ // The ID will be extracted from the dialog data when saving
+ self.pending_edit_ids.clear();
+
+ // Create a new editor with current category options (excluding this category)
+ self.edit_dialog = self.create_edit_dialog_with_options(category_id);
+ self.edit_dialog.open(item);
+ }
+}
+
+// Deferred actions for categories table
+#[derive(Debug)]
+enum DeferredCategoryAction {
+ DoubleClick(Value),
+ ContextEdit(Value),
+ ContextDelete(Value),
+ ContextClone(Value),
+}
+
+// Temporary event handler that collects actions for later processing
+struct TempCategoriesEventHandler<'a> {
+ deferred_actions: &'a mut Vec<DeferredCategoryAction>,
+}
+
+impl<'a> TableEventHandler<Value> for TempCategoriesEventHandler<'a> {
+ fn on_double_click(&mut self, item: &Value, _row_index: usize) {
+ log::info!(
+ "Double-click detected on category: {:?}",
+ item.get("category_name")
+ );
+ self.deferred_actions
+ .push(DeferredCategoryAction::DoubleClick(item.clone()));
+ }
+
+ fn on_context_menu(&mut self, ui: &mut egui::Ui, item: &Value, _row_index: usize) {
+ if ui
+ .button(format!("{} Edit", egui_phosphor::regular::PENCIL))
+ .clicked()
+ {
+ log::info!(
+ "Context menu edit clicked for category: {:?}",
+ item.get("category_name")
+ );
+ self.deferred_actions
+ .push(DeferredCategoryAction::ContextEdit(item.clone()));
+ ui.close();
+ }
+ ui.separator();
+ if ui
+ .button(format!("{} Clone Category", egui_phosphor::regular::COPY))
+ .clicked()
+ {
+ log::info!(
+ "Context menu clone clicked for category: {:?}",
+ item.get("category_name")
+ );
+ self.deferred_actions
+ .push(DeferredCategoryAction::ContextClone(item.clone()));
+ ui.close();
+ }
+ ui.separator();
+ if ui
+ .button(format!("{} Delete", egui_phosphor::regular::TRASH))
+ .clicked()
+ {
+ log::info!(
+ "Context menu delete clicked for category: {:?}",
+ item.get("category_name")
+ );
+ self.deferred_actions
+ .push(DeferredCategoryAction::ContextDelete(item.clone()));
+ ui.close();
+ }
+ }
+
+ fn on_selection_changed(&mut self, _selected_indices: &[usize]) {
+ // Selection handling is managed by the main CategoriesView
+ // We don't need to do anything here for now
+ }
+}