categories.rs 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905
  1. use crate::api::ApiClient;
  2. use crate::core::components::form_builder::FormBuilder;
  3. use crate::core::components::interactions::ConfirmDialog;
  4. use crate::core::table_renderer::{ColumnConfig, TableEventHandler, TableRenderer};
  5. use crate::core::tables::get_categories;
  6. use crate::core::{EditorField, FieldType};
  7. use crate::ui::ribbon::RibbonUI;
  8. use eframe::egui;
  9. use serde_json::Value;
  10. pub struct CategoriesView {
  11. categories: Vec<serde_json::Value>,
  12. is_loading: bool,
  13. last_error: Option<String>,
  14. initial_load_done: bool,
  15. load_attempted: bool, // New field to track if we've tried loading
  16. // Editor dialogs
  17. edit_dialog: FormBuilder,
  18. add_dialog: FormBuilder,
  19. delete_dialog: ConfirmDialog,
  20. // Pending operations
  21. pending_delete_ids: Vec<i64>, // Changed from Option<i64> to Vec<i64> for bulk delete support
  22. pending_edit_ids: Vec<i64>, // Changed from Option<i64> to Vec<i64> for bulk edit support
  23. // Table rendering
  24. table_renderer: crate::core::table_renderer::TableRenderer,
  25. }
  26. impl CategoriesView {
  27. pub fn new() -> Self {
  28. let edit_dialog = Self::create_edit_dialog();
  29. let add_dialog = Self::create_placeholder_add_dialog();
  30. // Define columns for categories table - code before name as requested
  31. let columns = vec![
  32. ColumnConfig::new("ID", "id").with_width(60.0).hidden(),
  33. ColumnConfig::new("Category Code", "category_code").with_width(120.0),
  34. ColumnConfig::new("Category Name", "category_name").with_width(200.0),
  35. ColumnConfig::new("Description", "category_description").with_width(300.0),
  36. ColumnConfig::new("Parent ID", "parent_id")
  37. .with_width(80.0)
  38. .hidden(),
  39. ColumnConfig::new("Parent Category", "parent_category_name").with_width(150.0),
  40. ];
  41. Self {
  42. categories: Vec::new(),
  43. is_loading: false,
  44. last_error: None,
  45. initial_load_done: false,
  46. load_attempted: false,
  47. edit_dialog,
  48. add_dialog,
  49. delete_dialog: ConfirmDialog::new(
  50. "Delete Category",
  51. "Are you sure you want to delete this category? This will affect all assets using this category.",
  52. ),
  53. pending_delete_ids: Vec::new(),
  54. pending_edit_ids: Vec::new(),
  55. table_renderer: TableRenderer::new()
  56. .with_columns(columns)
  57. .with_default_sort("category_code", true) // Sort by category code alphabetically
  58. .with_search_fields(vec![
  59. "category_name".to_string(),
  60. "category_code".to_string(),
  61. "category_description".to_string(),
  62. "parent_category_name".to_string(),
  63. ]),
  64. }
  65. }
  66. fn create_edit_dialog() -> FormBuilder {
  67. FormBuilder::new(
  68. "Edit Category",
  69. vec![
  70. EditorField {
  71. name: "id".into(),
  72. label: "ID".into(),
  73. field_type: FieldType::Text,
  74. required: false,
  75. read_only: true,
  76. },
  77. EditorField {
  78. name: "category_name".into(),
  79. label: "Category Name".into(),
  80. field_type: FieldType::Text,
  81. required: true,
  82. read_only: false,
  83. },
  84. EditorField {
  85. name: "category_code".into(),
  86. label: "Category Code".into(),
  87. field_type: FieldType::Text,
  88. required: false,
  89. read_only: false,
  90. },
  91. EditorField {
  92. name: "category_description".into(),
  93. label: "Description".into(),
  94. field_type: FieldType::MultilineText,
  95. required: false,
  96. read_only: false,
  97. },
  98. EditorField {
  99. name: "parent_id".into(),
  100. label: "Parent Category".into(),
  101. field_type: FieldType::Text, // TODO: Make this a dropdown with other categories
  102. required: false,
  103. read_only: false,
  104. },
  105. ],
  106. )
  107. }
  108. fn create_placeholder_add_dialog() -> FormBuilder {
  109. FormBuilder::new(
  110. "Add Category",
  111. vec![
  112. EditorField {
  113. name: "category_name".into(),
  114. label: "Category Name".into(),
  115. field_type: FieldType::Text,
  116. required: true,
  117. read_only: false,
  118. },
  119. EditorField {
  120. name: "category_code".into(),
  121. label: "Category Code".into(),
  122. field_type: FieldType::Text,
  123. required: false,
  124. read_only: false,
  125. },
  126. EditorField {
  127. name: "category_description".into(),
  128. label: "Description".into(),
  129. field_type: FieldType::MultilineText,
  130. required: false,
  131. read_only: false,
  132. },
  133. EditorField {
  134. name: "parent_id".into(),
  135. label: "Parent Category".into(),
  136. field_type: FieldType::Text, // Will be updated to dropdown when opened
  137. required: false,
  138. read_only: false,
  139. },
  140. ],
  141. )
  142. }
  143. fn create_add_dialog_with_options(&self) -> FormBuilder {
  144. let category_options = self.create_category_dropdown_options(None);
  145. FormBuilder::new(
  146. "Add Category",
  147. vec![
  148. EditorField {
  149. name: "category_name".into(),
  150. label: "Category Name".into(),
  151. field_type: FieldType::Text,
  152. required: true,
  153. read_only: false,
  154. },
  155. EditorField {
  156. name: "category_code".into(),
  157. label: "Category Code".into(),
  158. field_type: FieldType::Text,
  159. required: false,
  160. read_only: false,
  161. },
  162. EditorField {
  163. name: "category_description".into(),
  164. label: "Description".into(),
  165. field_type: FieldType::MultilineText,
  166. required: false,
  167. read_only: false,
  168. },
  169. EditorField {
  170. name: "parent_id".into(),
  171. label: "Parent Category".into(),
  172. field_type: FieldType::Dropdown(category_options),
  173. required: false,
  174. read_only: false,
  175. },
  176. ],
  177. )
  178. }
  179. fn load_categories(&mut self, api_client: &ApiClient) {
  180. // Don't start a new load if we're already loading
  181. if self.is_loading {
  182. return;
  183. }
  184. self.is_loading = true;
  185. self.last_error = None;
  186. self.load_attempted = true;
  187. match get_categories(api_client, Some(200)) {
  188. Ok(categories) => {
  189. self.categories = categories;
  190. self.initial_load_done = true;
  191. log::info!(
  192. "Categories loaded successfully: {} items",
  193. self.categories.len()
  194. );
  195. }
  196. Err(e) => {
  197. let error_msg = format!("Error loading categories: {}", e);
  198. log::error!("{}", error_msg);
  199. self.last_error = Some(error_msg);
  200. }
  201. }
  202. self.is_loading = false;
  203. }
  204. /// Get selected category IDs for bulk operations (works with filtered view)
  205. fn get_selected_ids(&self) -> Vec<i64> {
  206. let filtered_data = self.table_renderer.prepare_json_data(&self.categories);
  207. let mut ids = Vec::new();
  208. for &row_idx in &self.table_renderer.selection.selected_rows {
  209. // prepared_data contains tuples of (original_index, &Value)
  210. if let Some((_orig_idx, category)) = filtered_data.get(row_idx) {
  211. if let Some(id) = category.get("id").and_then(|v| v.as_i64()) {
  212. ids.push(id);
  213. }
  214. }
  215. }
  216. ids
  217. }
  218. /// Sanitize form data for categories before sending to the API.
  219. /// - Removes internal editor fields prefixed with __editor_
  220. /// - Converts empty-string parent_id to JSON null
  221. /// - Coerces numeric parent_id strings to numbers
  222. fn sanitize_category_map(
  223. form_data: &serde_json::Map<String, Value>,
  224. ) -> serde_json::Map<String, Value> {
  225. let mut out = serde_json::Map::new();
  226. for (k, v) in form_data.iter() {
  227. // Skip internal editor fields
  228. if k.starts_with("__editor_") {
  229. continue;
  230. }
  231. if k == "parent_id" {
  232. // parent_id might be sent as "" for None. Convert to null.
  233. if v.is_null() {
  234. out.insert(k.clone(), Value::Null);
  235. continue;
  236. }
  237. if let Some(s) = v.as_str() {
  238. let s_trim = s.trim();
  239. if s_trim.is_empty() {
  240. out.insert(k.clone(), Value::Null);
  241. continue;
  242. }
  243. // Try parse integer
  244. if let Ok(n) = s_trim.parse::<i64>() {
  245. out.insert(k.clone(), Value::Number((n).into()));
  246. continue;
  247. }
  248. // Fallback: keep as string
  249. out.insert(k.clone(), Value::String(s_trim.to_string()));
  250. continue;
  251. }
  252. // If it's already a number, keep it
  253. if v.is_i64() || v.is_u64() || v.is_f64() {
  254. out.insert(k.clone(), v.clone());
  255. continue;
  256. }
  257. // Anything else -> keep as-is
  258. out.insert(k.clone(), v.clone());
  259. continue;
  260. }
  261. // For everything else, just copy through
  262. out.insert(k.clone(), v.clone());
  263. }
  264. out
  265. }
  266. fn create_category(
  267. &mut self,
  268. api_client: &ApiClient,
  269. form_data: &serde_json::Map<String, Value>,
  270. ) {
  271. // Sanitize and coerce form data (convert empty parent_id -> null, remove internal fields)
  272. let sanitized = Self::sanitize_category_map(form_data);
  273. let values = serde_json::Value::Object(sanitized);
  274. match api_client.insert("categories", values) {
  275. Ok(resp) if resp.success => {
  276. log::info!("Category created successfully");
  277. self.load_categories(api_client); // Reload to get fresh data
  278. }
  279. Ok(resp) => {
  280. let error_msg = format!("Create failed: {:?}", resp.error);
  281. log::error!("{}", error_msg);
  282. self.last_error = Some(error_msg);
  283. }
  284. Err(e) => {
  285. let error_msg = format!("Create error: {}", e);
  286. log::error!("{}", error_msg);
  287. self.last_error = Some(error_msg);
  288. }
  289. }
  290. }
  291. fn update_category(
  292. &mut self,
  293. api_client: &ApiClient,
  294. category_id: i64,
  295. form_data: &serde_json::Map<String, Value>,
  296. ) {
  297. // Sanitize form data (remove internal fields, coerce parent_id). Also ensure we don't send id.
  298. let mut filtered_data = Self::sanitize_category_map(form_data);
  299. filtered_data.remove("id");
  300. // Convert form data to JSON object
  301. let values = serde_json::Value::Object(filtered_data);
  302. let where_clause = serde_json::json!({"id": category_id});
  303. match api_client.update("categories", values, where_clause) {
  304. Ok(resp) if resp.success => {
  305. log::info!("Category updated successfully");
  306. self.load_categories(api_client); // Reload to get fresh data
  307. }
  308. Ok(resp) => {
  309. let error_msg = format!("Update failed: {:?}", resp.error);
  310. log::error!("{}", error_msg);
  311. // Check for foreign key constraint errors
  312. if let Some(err_str) = resp.error.as_ref() {
  313. if err_str.contains("foreign key constraint") {
  314. self.last_error = Some(
  315. "Cannot update category: Invalid parent category reference."
  316. .to_string(),
  317. );
  318. } else {
  319. self.last_error = Some(error_msg);
  320. }
  321. } else {
  322. self.last_error = Some(error_msg);
  323. }
  324. }
  325. Err(e) => {
  326. let error_msg = format!("Update error: {}", e);
  327. log::error!("{}", error_msg);
  328. self.last_error = Some(error_msg);
  329. }
  330. }
  331. }
  332. fn delete_category(&mut self, api_client: &ApiClient, category_id: i64) {
  333. let where_clause = serde_json::json!({"id": category_id});
  334. match api_client.delete("categories", where_clause) {
  335. Ok(resp) if resp.success => {
  336. log::info!("Category deleted successfully");
  337. self.load_categories(api_client); // Reload to get fresh data
  338. }
  339. Ok(resp) => {
  340. let error_msg = format!("Delete failed: {:?}", resp.error);
  341. log::error!("{}", error_msg);
  342. // Check for foreign key constraint errors and provide user-friendly message
  343. if let Some(err_str) = resp.error.as_ref() {
  344. if err_str.contains("foreign key constraint")
  345. || err_str.contains("Cannot delete or update a parent row")
  346. {
  347. self.last_error = Some(
  348. "Cannot delete category: It is being used by other categories as their parent, or by assets. \
  349. Please reassign dependent items first.".to_string()
  350. );
  351. } else {
  352. self.last_error = Some(error_msg);
  353. }
  354. } else {
  355. self.last_error = Some(error_msg);
  356. }
  357. }
  358. Err(e) => {
  359. let error_msg = format!("Delete error: {}", e);
  360. log::error!("{}", error_msg);
  361. // Check for foreign key constraint in error message
  362. let err_lower = error_msg.to_lowercase();
  363. if err_lower.contains("foreign key") || err_lower.contains("constraint") {
  364. self.last_error = Some(
  365. "Cannot delete category: It is being used by other categories as their parent, or by assets. \
  366. Please reassign dependent items first.".to_string()
  367. );
  368. } else {
  369. self.last_error = Some(error_msg);
  370. }
  371. }
  372. }
  373. }
  374. pub fn show(
  375. &mut self,
  376. ui: &mut egui::Ui,
  377. api_client: Option<&ApiClient>,
  378. ribbon: Option<&mut RibbonUI>,
  379. permissions: Option<&serde_json::Value>,
  380. ) -> Vec<String> {
  381. let mut flags_to_clear = Vec::new();
  382. // Handle context menu actions and double-click
  383. if let Some(item) = ui.ctx().data_mut(|d| {
  384. d.remove_temp::<serde_json::Value>(egui::Id::new("cat_double_click_edit"))
  385. }) {
  386. if RibbonUI::check_permission(permissions, "edit_category") {
  387. self.open_editor_with(&item);
  388. ui.ctx().request_repaint();
  389. }
  390. }
  391. if let Some(item) = ui.ctx().data_mut(|d| {
  392. d.remove_temp::<serde_json::Value>(egui::Id::new("cat_context_menu_edit"))
  393. }) {
  394. if RibbonUI::check_permission(permissions, "edit_category") {
  395. self.open_editor_with(&item);
  396. ui.ctx().request_repaint();
  397. }
  398. }
  399. if let Some(item) = ui.ctx().data_mut(|d| {
  400. d.remove_temp::<serde_json::Value>(egui::Id::new("cat_context_menu_delete"))
  401. }) {
  402. if RibbonUI::check_permission(permissions, "delete_category") {
  403. let name = item
  404. .get("category_name")
  405. .and_then(|v| v.as_str())
  406. .unwrap_or("Unknown")
  407. .to_string();
  408. let id = item.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
  409. self.pending_delete_ids = vec![id]; // Changed to vector
  410. self.delete_dialog.open(name, id.to_string());
  411. ui.ctx().request_repaint();
  412. }
  413. }
  414. // Auto-load on first show, but only try once unless user explicitly requests retry
  415. if !self.initial_load_done && !self.is_loading && !self.load_attempted {
  416. if let Some(client) = api_client {
  417. log::info!("Categories view never loaded, triggering initial auto-load");
  418. self.load_categories(client);
  419. }
  420. }
  421. // Extract search query and handle ribbon actions
  422. let search_query = if let Some(ribbon) = ribbon.as_ref() {
  423. let query = ribbon
  424. .search_texts
  425. .get("categories_search")
  426. .filter(|s| !s.trim().is_empty())
  427. .map(|s| s.as_str());
  428. // Handle ribbon actions
  429. if ribbon
  430. .checkboxes
  431. .get("categories_refresh")
  432. .copied()
  433. .unwrap_or(false)
  434. {
  435. if let Some(client) = api_client {
  436. // Reset error state and allow fresh load
  437. self.last_error = None;
  438. self.load_categories(client);
  439. }
  440. flags_to_clear.push("categories_refresh".to_string());
  441. }
  442. if ribbon
  443. .checkboxes
  444. .get("categories_add")
  445. .copied()
  446. .unwrap_or(false)
  447. {
  448. if RibbonUI::check_permission(permissions, "create_category") {
  449. // Create a new add dialog with current category options
  450. self.add_dialog = self.create_add_dialog_with_options();
  451. self.add_dialog.open(&serde_json::json!({})); // Open with empty data
  452. }
  453. flags_to_clear.push("categories_add".to_string());
  454. }
  455. if ribbon
  456. .checkboxes
  457. .get("categories_edit")
  458. .copied()
  459. .unwrap_or(false)
  460. {
  461. if RibbonUI::check_permission(permissions, "edit_category") {
  462. // Get selected category IDs
  463. let selected_ids = self.get_selected_ids();
  464. if !selected_ids.is_empty() {
  465. // For edit, only edit the first selected category (bulk edit of categories is complex)
  466. if let Some(&first_id) = selected_ids.first() {
  467. // Clone the category to avoid borrowing issues
  468. let category = self
  469. .categories
  470. .iter()
  471. .find(|c| c.get("id").and_then(|v| v.as_i64()) == Some(first_id))
  472. .cloned();
  473. if let Some(cat) = category {
  474. self.open_editor_with(&cat);
  475. }
  476. }
  477. } else {
  478. log::warn!("Edit requested but no categories selected");
  479. }
  480. }
  481. flags_to_clear.push("categories_edit".to_string());
  482. }
  483. if ribbon
  484. .checkboxes
  485. .get("categories_delete")
  486. .copied()
  487. .unwrap_or(false)
  488. {
  489. if RibbonUI::check_permission(permissions, "delete_category") {
  490. // Get selected category IDs for bulk delete
  491. let selected_ids = self.get_selected_ids();
  492. if !selected_ids.is_empty() {
  493. self.pending_delete_ids = selected_ids.clone();
  494. let count = selected_ids.len();
  495. // Show dialog with appropriate message for single or multiple deletes
  496. let message =
  497. if count == 1 {
  498. // Get the category name for single delete
  499. if let Some(category) = self.categories.iter().find(|c| {
  500. c.get("id").and_then(|v| v.as_i64()) == Some(selected_ids[0])
  501. }) {
  502. category
  503. .get("category_name")
  504. .and_then(|v| v.as_str())
  505. .unwrap_or("Unknown")
  506. .to_string()
  507. } else {
  508. "Unknown".to_string()
  509. }
  510. } else {
  511. format!("{} categories", count)
  512. };
  513. self.delete_dialog
  514. .open(message, format!("IDs: {:?}", selected_ids));
  515. } else {
  516. log::warn!("Delete requested but no categories selected");
  517. }
  518. }
  519. flags_to_clear.push("categories_delete".to_string());
  520. }
  521. query
  522. } else {
  523. None
  524. };
  525. // Top toolbar
  526. ui.horizontal(|ui| {
  527. ui.heading("Categories");
  528. if self.is_loading {
  529. ui.spinner();
  530. ui.label("Loading...");
  531. } else {
  532. ui.label(format!("{} categories", self.categories.len()));
  533. }
  534. ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
  535. if ui.button("➕ Add Category").clicked() {
  536. self.add_dialog.open_new(None);
  537. }
  538. if ui.button("Refresh").clicked() {
  539. if let Some(client) = api_client {
  540. // Reset error state and allow fresh load
  541. self.last_error = None;
  542. self.load_categories(client);
  543. }
  544. }
  545. });
  546. });
  547. ui.separator();
  548. // Error display with retry option
  549. if let Some(error) = &self.last_error {
  550. ui.colored_label(egui::Color32::RED, format!("Error: {}", error));
  551. ui.horizontal(|ui| {
  552. if ui.button("Try Again").clicked() {
  553. if let Some(client) = api_client {
  554. // Reset state and try loading again
  555. self.load_attempted = false;
  556. self.initial_load_done = false;
  557. self.load_categories(client);
  558. }
  559. }
  560. if ui.button("Clear Error").clicked() {
  561. self.last_error = None;
  562. }
  563. });
  564. ui.separator();
  565. }
  566. // Categories table
  567. if !self.is_loading && !self.categories.is_empty() {
  568. self.render_table(ui, search_query);
  569. } else if !self.is_loading {
  570. ui.centered_and_justified(|ui| {
  571. ui.label("No categories found. Click 'Add Category' to create one.");
  572. });
  573. }
  574. // Handle dialogs
  575. if let Some(api_client) = api_client {
  576. // Add dialog
  577. if let Some(result) = self.add_dialog.show_editor(ui.ctx()) {
  578. if let Some(category_data) = result {
  579. log::info!("Creating new category: {:?}", category_data);
  580. self.create_category(api_client, &category_data);
  581. }
  582. }
  583. // Edit dialog
  584. if let Some(result) = self.edit_dialog.show_editor(ui.ctx()) {
  585. if let Some(category_data) = result {
  586. // Support bulk edit: if pending_edit_ids is empty, try to get ID from dialog
  587. let ids_to_edit: Vec<i64> = if !self.pending_edit_ids.is_empty() {
  588. std::mem::take(&mut self.pending_edit_ids)
  589. } else {
  590. // Single edit from dialog - extract ID from __editor_item_id or category data
  591. category_data
  592. .get("__editor_item_id")
  593. .and_then(|v| v.as_str())
  594. .or_else(|| category_data.get("id").and_then(|v| v.as_str()))
  595. .and_then(|s| s.parse::<i64>().ok())
  596. .map(|id| vec![id])
  597. .unwrap_or_default()
  598. };
  599. for category_id in ids_to_edit {
  600. log::info!("Updating category {}: {:?}", category_id, category_data);
  601. self.update_category(api_client, category_id, &category_data);
  602. }
  603. }
  604. }
  605. // Delete dialog - support bulk delete
  606. if let Some(confirmed) = self.delete_dialog.show_dialog(ui.ctx()) {
  607. log::info!(
  608. "Delete dialog result: confirmed={}, pending_delete_ids={:?}",
  609. confirmed,
  610. self.pending_delete_ids
  611. );
  612. if confirmed && !self.pending_delete_ids.is_empty() {
  613. // Clone the IDs to avoid borrowing issues
  614. let ids_to_delete = self.pending_delete_ids.clone();
  615. for category_id in ids_to_delete {
  616. log::info!("Deleting category: {}", category_id);
  617. self.delete_category(api_client, category_id);
  618. }
  619. }
  620. self.pending_delete_ids.clear();
  621. }
  622. }
  623. flags_to_clear
  624. }
  625. fn render_table(&mut self, ui: &mut egui::Ui, search_query: Option<&str>) {
  626. // Apply search query to TableRenderer (clear if empty)
  627. match search_query {
  628. Some(query) => self.table_renderer.set_search_query(query.to_string()),
  629. None => self.table_renderer.set_search_query(String::new()), // Clear search when empty
  630. }
  631. // Prepare sorted/filtered data
  632. let prepared_data = self.table_renderer.prepare_json_data(&self.categories);
  633. // Create temporary event handler for deferred actions
  634. let mut deferred_actions = Vec::new();
  635. let mut event_handler = TempCategoriesEventHandler {
  636. deferred_actions: &mut deferred_actions,
  637. };
  638. // Render the table with TableRenderer
  639. self.table_renderer
  640. .render_json_table(ui, &prepared_data, Some(&mut event_handler));
  641. // Process deferred actions
  642. for action in deferred_actions {
  643. match action {
  644. DeferredCategoryAction::DoubleClick(category) => {
  645. self.open_editor_with(&category);
  646. }
  647. DeferredCategoryAction::ContextEdit(category) => {
  648. self.open_editor_with(&category);
  649. }
  650. DeferredCategoryAction::ContextDelete(category) => {
  651. let name = category
  652. .get("category_name")
  653. .and_then(|v| v.as_str())
  654. .unwrap_or("Unknown")
  655. .to_string();
  656. let id = category.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
  657. self.pending_delete_ids = vec![id]; // Changed to vector
  658. self.delete_dialog.open(name, id.to_string());
  659. }
  660. DeferredCategoryAction::ContextClone(category) => {
  661. // Prepare Add dialog with up-to-date dropdown options
  662. self.add_dialog = self.create_add_dialog_with_options();
  663. // Use the shared helper to clear ID/code and suffix the name
  664. let cloned = crate::core::components::prepare_cloned_value(
  665. &category,
  666. &["id", "category_code"],
  667. Some("category_name"),
  668. Some(""),
  669. );
  670. self.add_dialog.title = "Add Category".to_string();
  671. self.add_dialog.open(&cloned);
  672. }
  673. }
  674. }
  675. }
  676. fn create_category_dropdown_options(&self, exclude_id: Option<i64>) -> Vec<(String, String)> {
  677. let mut options = vec![("".to_string(), "None (Root Category)".to_string())];
  678. for category in &self.categories {
  679. if let Some(id) = category.get("id").and_then(|v| v.as_i64()) {
  680. // Exclude the current category to prevent circular references
  681. if let Some(exclude) = exclude_id {
  682. if id == exclude {
  683. continue;
  684. }
  685. }
  686. let name = category
  687. .get("category_name")
  688. .and_then(|v| v.as_str())
  689. .unwrap_or("Unknown")
  690. .to_string();
  691. let code = category
  692. .get("category_code")
  693. .and_then(|v| v.as_str())
  694. .unwrap_or("");
  695. let display_name = if code.is_empty() {
  696. name
  697. } else {
  698. format!("{} - {}", code, name)
  699. };
  700. options.push((id.to_string(), display_name));
  701. }
  702. }
  703. options
  704. }
  705. fn create_edit_dialog_with_options(&self, exclude_id: Option<i64>) -> FormBuilder {
  706. let category_options = self.create_category_dropdown_options(exclude_id);
  707. FormBuilder::new(
  708. "Edit Category",
  709. vec![
  710. EditorField {
  711. name: "id".into(),
  712. label: "ID".into(),
  713. field_type: FieldType::Text,
  714. required: false,
  715. read_only: true,
  716. },
  717. EditorField {
  718. name: "category_name".into(),
  719. label: "Category Name".into(),
  720. field_type: FieldType::Text,
  721. required: true,
  722. read_only: false,
  723. },
  724. EditorField {
  725. name: "category_code".into(),
  726. label: "Category Code".into(),
  727. field_type: FieldType::Text,
  728. required: false,
  729. read_only: false,
  730. },
  731. EditorField {
  732. name: "category_description".into(),
  733. label: "Description".into(),
  734. field_type: FieldType::MultilineText,
  735. required: false,
  736. read_only: false,
  737. },
  738. EditorField {
  739. name: "parent_id".into(),
  740. label: "Parent Category".into(),
  741. field_type: FieldType::Dropdown(category_options),
  742. required: false,
  743. read_only: false,
  744. },
  745. ],
  746. )
  747. }
  748. fn open_editor_with(&mut self, item: &serde_json::Value) {
  749. let category_id = item.get("id").and_then(|v| v.as_i64());
  750. // Clear pending_edit_ids since we're opening a single-item editor
  751. // The ID will be extracted from the dialog data when saving
  752. self.pending_edit_ids.clear();
  753. // Create a new editor with current category options (excluding this category)
  754. self.edit_dialog = self.create_edit_dialog_with_options(category_id);
  755. self.edit_dialog.open(item);
  756. }
  757. }
  758. // Deferred actions for categories table
  759. #[derive(Debug)]
  760. enum DeferredCategoryAction {
  761. DoubleClick(Value),
  762. ContextEdit(Value),
  763. ContextDelete(Value),
  764. ContextClone(Value),
  765. }
  766. // Temporary event handler that collects actions for later processing
  767. struct TempCategoriesEventHandler<'a> {
  768. deferred_actions: &'a mut Vec<DeferredCategoryAction>,
  769. }
  770. impl<'a> TableEventHandler<Value> for TempCategoriesEventHandler<'a> {
  771. fn on_double_click(&mut self, item: &Value, _row_index: usize) {
  772. log::info!(
  773. "Double-click detected on category: {:?}",
  774. item.get("category_name")
  775. );
  776. self.deferred_actions
  777. .push(DeferredCategoryAction::DoubleClick(item.clone()));
  778. }
  779. fn on_context_menu(&mut self, ui: &mut egui::Ui, item: &Value, _row_index: usize) {
  780. if ui
  781. .button(format!("{} Edit", egui_phosphor::regular::PENCIL))
  782. .clicked()
  783. {
  784. log::info!(
  785. "Context menu edit clicked for category: {:?}",
  786. item.get("category_name")
  787. );
  788. self.deferred_actions
  789. .push(DeferredCategoryAction::ContextEdit(item.clone()));
  790. ui.close();
  791. }
  792. ui.separator();
  793. if ui
  794. .button(format!("{} Clone Category", egui_phosphor::regular::COPY))
  795. .clicked()
  796. {
  797. log::info!(
  798. "Context menu clone clicked for category: {:?}",
  799. item.get("category_name")
  800. );
  801. self.deferred_actions
  802. .push(DeferredCategoryAction::ContextClone(item.clone()));
  803. ui.close();
  804. }
  805. ui.separator();
  806. if ui
  807. .button(format!("{} Delete", egui_phosphor::regular::TRASH))
  808. .clicked()
  809. {
  810. log::info!(
  811. "Context menu delete clicked for category: {:?}",
  812. item.get("category_name")
  813. );
  814. self.deferred_actions
  815. .push(DeferredCategoryAction::ContextDelete(item.clone()));
  816. ui.close();
  817. }
  818. }
  819. fn on_selection_changed(&mut self, _selected_indices: &[usize]) {
  820. // Selection handling is managed by the main CategoriesView
  821. // We don't need to do anything here for now
  822. }
  823. }