label_templates.rs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. use crate::api::ApiClient;
  2. use crate::core::components::form_builder::FormBuilder;
  3. use crate::core::components::interactions::ConfirmDialog;
  4. use crate::core::{ColumnConfig, TableEventHandler, TableRenderer};
  5. use crate::core::{EditorField, FieldType};
  6. use crate::ui::ribbon::RibbonUI;
  7. use eframe::egui;
  8. use serde_json::Value;
  9. pub struct LabelTemplatesView {
  10. templates: Vec<serde_json::Value>,
  11. is_loading: bool,
  12. last_error: Option<String>,
  13. initial_load_done: bool,
  14. // Table renderer
  15. table_renderer: TableRenderer,
  16. // Editor dialogs
  17. edit_dialog: FormBuilder,
  18. add_dialog: FormBuilder,
  19. delete_dialog: ConfirmDialog,
  20. // Pending operations
  21. pending_delete_id: Option<i64>,
  22. pending_edit_id: Option<i64>,
  23. }
  24. impl LabelTemplatesView {
  25. pub fn new() -> Self {
  26. let edit_dialog = Self::create_edit_dialog();
  27. let add_dialog = Self::create_add_dialog();
  28. // Define columns for label_templates table
  29. let columns = vec![
  30. ColumnConfig::new("ID", "id").with_width(60.0).hidden(),
  31. ColumnConfig::new("Template Code", "template_code").with_width(150.0),
  32. ColumnConfig::new("Template Name", "template_name").with_width(200.0),
  33. ColumnConfig::new("Layout JSON", "layout_json")
  34. .with_width(250.0)
  35. .hidden(),
  36. ];
  37. Self {
  38. templates: Vec::new(),
  39. is_loading: false,
  40. last_error: None,
  41. initial_load_done: false,
  42. table_renderer: TableRenderer::new()
  43. .with_columns(columns)
  44. .with_default_sort("template_name", true)
  45. .with_search_fields(vec![
  46. "template_code".to_string(),
  47. "template_name".to_string(),
  48. ]),
  49. edit_dialog,
  50. add_dialog,
  51. delete_dialog: ConfirmDialog::new(
  52. "Delete Label Template",
  53. "Are you sure you want to delete this label template?",
  54. ),
  55. pending_delete_id: None,
  56. pending_edit_id: None,
  57. }
  58. }
  59. fn create_edit_dialog() -> FormBuilder {
  60. FormBuilder::new(
  61. "Edit Label Template",
  62. vec![
  63. EditorField {
  64. name: "id".into(),
  65. label: "ID".into(),
  66. field_type: FieldType::Text,
  67. required: false,
  68. read_only: true,
  69. },
  70. EditorField {
  71. name: "template_code".into(),
  72. label: "Template Code".into(),
  73. field_type: FieldType::Text,
  74. required: true,
  75. read_only: false,
  76. },
  77. EditorField {
  78. name: "template_name".into(),
  79. label: "Template Name".into(),
  80. field_type: FieldType::Text,
  81. required: true,
  82. read_only: false,
  83. },
  84. EditorField {
  85. name: "layout_json".into(),
  86. label: "Layout JSON".into(),
  87. field_type: FieldType::MultilineText,
  88. required: true,
  89. read_only: false,
  90. },
  91. ],
  92. )
  93. }
  94. fn create_add_dialog() -> FormBuilder {
  95. FormBuilder::new(
  96. "Add Label Template",
  97. vec![
  98. EditorField {
  99. name: "template_code".into(),
  100. label: "Template Code".into(),
  101. field_type: FieldType::Text,
  102. required: true,
  103. read_only: false,
  104. },
  105. EditorField {
  106. name: "template_name".into(),
  107. label: "Template Name".into(),
  108. field_type: FieldType::Text,
  109. required: true,
  110. read_only: false,
  111. },
  112. EditorField {
  113. name: "layout_json".into(),
  114. label: "Layout JSON".into(),
  115. field_type: FieldType::MultilineText,
  116. required: true,
  117. read_only: false,
  118. },
  119. ],
  120. )
  121. }
  122. fn ensure_loaded(&mut self, api_client: Option<&ApiClient>) {
  123. if self.is_loading || self.initial_load_done {
  124. return;
  125. }
  126. if let Some(client) = api_client {
  127. self.load_templates(client);
  128. }
  129. }
  130. fn load_templates(&mut self, api_client: &ApiClient) {
  131. use crate::core::tables::get_label_templates;
  132. self.is_loading = true;
  133. self.last_error = None;
  134. match get_label_templates(api_client) {
  135. Ok(list) => {
  136. self.templates = list;
  137. self.is_loading = false;
  138. self.initial_load_done = true;
  139. }
  140. Err(e) => {
  141. self.last_error = Some(e.to_string());
  142. self.is_loading = false;
  143. self.initial_load_done = true;
  144. }
  145. }
  146. }
  147. pub fn render(
  148. &mut self,
  149. ui: &mut egui::Ui,
  150. api_client: Option<&ApiClient>,
  151. ribbon_ui: Option<&mut RibbonUI>,
  152. permissions: Option<&serde_json::Value>,
  153. ) {
  154. self.ensure_loaded(api_client);
  155. // Get search query from ribbon first (before mutable borrow)
  156. let search_query = ribbon_ui
  157. .as_ref()
  158. .and_then(|r| r.search_texts.get("labels_search"))
  159. .map(|s| s.clone())
  160. .unwrap_or_default();
  161. // Apply search to table renderer
  162. self.table_renderer.search_query = search_query;
  163. // Handle ribbon actions
  164. if let Some(ribbon) = ribbon_ui {
  165. if ribbon
  166. .checkboxes
  167. .get("labels_action_add")
  168. .copied()
  169. .unwrap_or(false)
  170. {
  171. if RibbonUI::check_permission(permissions, "create_label_template") {
  172. // Provide helpful default layout JSON template matching database schema
  173. let layout_json = r##"{
  174. "version": "1.0",
  175. "background": "#FFFFFF",
  176. "elements": [
  177. {
  178. "type": "text",
  179. "field": "{{asset_tag}}",
  180. "x": 5,
  181. "y": 10,
  182. "fontSize": 14,
  183. "fontWeight": "bold",
  184. "fontFamily": "Arial"
  185. },
  186. {
  187. "type": "text",
  188. "field": "{{name}}",
  189. "x": 5,
  190. "y": 28,
  191. "fontSize": 10,
  192. "fontFamily": "Arial"
  193. },
  194. {
  195. "type": "qrcode",
  196. "field": "{{asset_tag}}",
  197. "x": 5,
  198. "y": 50,
  199. "size": 40
  200. }
  201. ]
  202. }"##;
  203. let default_data = serde_json::json!({
  204. "layout_json": layout_json
  205. });
  206. self.add_dialog.open(&default_data);
  207. }
  208. }
  209. if ribbon
  210. .checkboxes
  211. .get("labels_action_refresh")
  212. .copied()
  213. .unwrap_or(false)
  214. {
  215. if let Some(client) = api_client {
  216. self.load_templates(client);
  217. }
  218. }
  219. }
  220. // Error message
  221. let mut clear_error = false;
  222. if let Some(err) = &self.last_error {
  223. ui.horizontal(|ui| {
  224. ui.colored_label(egui::Color32::RED, format!("Error: {}", err));
  225. if ui.button("Close").clicked() {
  226. clear_error = true;
  227. }
  228. });
  229. ui.separator();
  230. }
  231. if clear_error {
  232. self.last_error = None;
  233. }
  234. // Loading indicator
  235. if self.is_loading {
  236. ui.spinner();
  237. ui.label("Loading label templates...");
  238. return;
  239. }
  240. // Render table with event handling
  241. self.render_table_with_events(ui, api_client, permissions);
  242. // Handle dialogs
  243. self.handle_dialogs(ui, api_client);
  244. // Process deferred actions from context menus
  245. self.process_deferred_actions(ui, api_client);
  246. }
  247. fn render_table_with_events(
  248. &mut self,
  249. ui: &mut egui::Ui,
  250. api_client: Option<&ApiClient>,
  251. permissions: Option<&serde_json::Value>,
  252. ) {
  253. let templates_clone = self.templates.clone();
  254. let prepared_data = self.table_renderer.prepare_json_data(&templates_clone);
  255. let mut deferred_actions: Vec<DeferredAction> = Vec::new();
  256. let mut temp_handler = TempTemplatesEventHandler {
  257. api_client,
  258. deferred_actions: &mut deferred_actions,
  259. permissions,
  260. };
  261. self.table_renderer
  262. .render_json_table(ui, &prepared_data, Some(&mut temp_handler));
  263. self.process_temp_deferred_actions(deferred_actions, api_client);
  264. }
  265. fn process_temp_deferred_actions(
  266. &mut self,
  267. actions: Vec<DeferredAction>,
  268. _api_client: Option<&ApiClient>,
  269. ) {
  270. for action in actions {
  271. match action {
  272. DeferredAction::DoubleClick(template) => {
  273. log::info!(
  274. "Processing double-click edit for template: {:?}",
  275. template.get("template_name")
  276. );
  277. self.edit_dialog.open(&template);
  278. if let Some(id) = template.get("id").and_then(|v| v.as_i64()) {
  279. self.pending_edit_id = Some(id);
  280. }
  281. }
  282. DeferredAction::ContextEdit(template) => {
  283. log::info!(
  284. "Processing context menu edit for template: {:?}",
  285. template.get("template_name")
  286. );
  287. self.edit_dialog.open(&template);
  288. if let Some(id) = template.get("id").and_then(|v| v.as_i64()) {
  289. self.pending_edit_id = Some(id);
  290. }
  291. }
  292. DeferredAction::ContextDelete(template) => {
  293. let name = template
  294. .get("template_name")
  295. .and_then(|v| v.as_str())
  296. .unwrap_or("Unknown");
  297. let id = template.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
  298. log::info!("Processing context menu delete for template: {}", name);
  299. self.pending_delete_id = Some(id);
  300. self.delete_dialog.open(name.to_string(), id.to_string());
  301. }
  302. DeferredAction::ContextClone(template) => {
  303. log::info!(
  304. "Processing context menu clone for template: {:?}",
  305. template.get("template_name")
  306. );
  307. // Build payload for Add dialog using shared helper
  308. let mut cloned = crate::core::components::prepare_cloned_value(
  309. &template,
  310. &["id", "template_code"],
  311. Some("template_name"),
  312. Some(""),
  313. );
  314. // Ensure layout_json is a string for the editor
  315. if let Some(obj) = cloned.as_object_mut() {
  316. if let Some(v) = template.get("layout_json") {
  317. let as_string = if let Some(s) = v.as_str() {
  318. s.to_string()
  319. } else {
  320. serde_json::to_string_pretty(v).unwrap_or_else(|_| "{}".to_string())
  321. };
  322. obj.insert(
  323. "layout_json".to_string(),
  324. serde_json::Value::String(as_string),
  325. );
  326. }
  327. }
  328. self.add_dialog.title = "Add Label Template".to_string();
  329. self.add_dialog.open(&cloned);
  330. }
  331. }
  332. }
  333. }
  334. fn handle_dialogs(&mut self, ui: &mut egui::Ui, api_client: Option<&ApiClient>) {
  335. // Delete confirmation dialog
  336. if let Some(confirmed) = self.delete_dialog.show_dialog(ui.ctx()) {
  337. if confirmed {
  338. if let (Some(id), Some(client)) = (self.pending_delete_id, api_client) {
  339. let where_clause = serde_json::json!({"id": id});
  340. match client.delete("label_templates", where_clause) {
  341. Ok(resp) => {
  342. if resp.success {
  343. log::info!("Label template {} deleted successfully", id);
  344. self.load_templates(client);
  345. } else {
  346. self.last_error = Some(format!("Delete failed: {:?}", resp.error));
  347. log::error!("Delete failed: {:?}", resp.error);
  348. }
  349. }
  350. Err(e) => {
  351. self.last_error = Some(format!("Failed to delete template: {}", e));
  352. log::error!("Failed to delete template: {}", e);
  353. }
  354. }
  355. }
  356. self.pending_delete_id = None;
  357. }
  358. }
  359. // Edit dialog
  360. if let Some(Some(updated)) = self.edit_dialog.show_editor(ui.ctx()) {
  361. if let (Some(id), Some(client)) = (self.pending_edit_id, api_client) {
  362. let where_clause = serde_json::json!({"id": id});
  363. let mut to_update = updated;
  364. // Remove editor metadata
  365. let mut meta_keys: Vec<String> = to_update
  366. .keys()
  367. .filter(|k| k.starts_with("__editor_"))
  368. .cloned()
  369. .collect();
  370. // Also remove __editor_item_id specifically
  371. if to_update.contains_key("__editor_item_id") {
  372. meta_keys.push("__editor_item_id".to_string());
  373. }
  374. for k in meta_keys {
  375. to_update.remove(&k);
  376. }
  377. // Send layout_json as actual JSON object
  378. if let Some(val) = to_update.get_mut("layout_json") {
  379. if let Some(s) = val.as_str() {
  380. match serde_json::from_str::<serde_json::Value>(s) {
  381. Ok(json_val) => {
  382. // Send as actual JSON object, not string
  383. *val = json_val;
  384. }
  385. Err(e) => {
  386. self.last_error = Some(format!("Layout JSON is invalid: {}", e));
  387. return;
  388. }
  389. }
  390. }
  391. }
  392. match client.update(
  393. "label_templates",
  394. serde_json::Value::Object(to_update.clone()),
  395. where_clause,
  396. ) {
  397. Ok(resp) => {
  398. if resp.success {
  399. log::info!("Label template {} updated successfully", id);
  400. self.load_templates(client);
  401. } else {
  402. self.last_error = Some(format!("Update failed: {:?}", resp.error));
  403. log::error!("Update failed: {:?}", resp.error);
  404. }
  405. }
  406. Err(e) => {
  407. self.last_error = Some(format!("Failed to update template: {}", e));
  408. log::error!("Failed to update template: {}", e);
  409. }
  410. }
  411. self.pending_edit_id = None;
  412. }
  413. }
  414. // Add dialog
  415. if let Some(Some(new_data)) = self.add_dialog.show_editor(ui.ctx()) {
  416. if let Some(client) = api_client {
  417. let mut payload = new_data;
  418. // Strip any editor metadata that may have leaked in
  419. let meta_strip: Vec<String> = payload
  420. .keys()
  421. .filter(|k| k.starts_with("__editor_"))
  422. .cloned()
  423. .collect();
  424. for k in meta_strip {
  425. payload.remove(&k);
  426. }
  427. // Send layout_json as actual JSON object
  428. if let Some(val) = payload.get_mut("layout_json") {
  429. if let Some(s) = val.as_str() {
  430. match serde_json::from_str::<serde_json::Value>(s) {
  431. Ok(json_val) => {
  432. // Send as actual JSON object, not string
  433. *val = json_val;
  434. }
  435. Err(e) => {
  436. self.last_error = Some(format!("Layout JSON is invalid: {}", e));
  437. return;
  438. }
  439. }
  440. }
  441. }
  442. match client.insert("label_templates", serde_json::Value::Object(payload)) {
  443. Ok(resp) => {
  444. if resp.success {
  445. log::info!("Label template added successfully");
  446. self.load_templates(client);
  447. } else {
  448. self.last_error = Some(format!("Insert failed: {:?}", resp.error));
  449. log::error!("Insert failed: {:?}", resp.error);
  450. }
  451. }
  452. Err(e) => {
  453. self.last_error = Some(format!("Failed to add template: {}", e));
  454. log::error!("Failed to add template: {}", e);
  455. }
  456. }
  457. }
  458. }
  459. }
  460. fn process_deferred_actions(&mut self, ui: &mut egui::Ui, _api_client: Option<&ApiClient>) {
  461. // Handle double-click edit
  462. if let Some(template) = ui
  463. .ctx()
  464. .data_mut(|d| d.remove_temp::<Value>(egui::Id::new("label_double_click_edit")))
  465. {
  466. log::info!(
  467. "Processing double-click edit for template: {:?}",
  468. template.get("template_name")
  469. );
  470. self.edit_dialog.open(&template);
  471. if let Some(id) = template.get("id").and_then(|v| v.as_i64()) {
  472. self.pending_edit_id = Some(id);
  473. }
  474. }
  475. // Handle context menu actions
  476. if let Some(template) = ui
  477. .ctx()
  478. .data_mut(|d| d.remove_temp::<Value>(egui::Id::new("label_context_menu_edit")))
  479. {
  480. log::info!(
  481. "Processing context menu edit for template: {:?}",
  482. template.get("template_name")
  483. );
  484. self.edit_dialog.open(&template);
  485. if let Some(id) = template.get("id").and_then(|v| v.as_i64()) {
  486. self.pending_edit_id = Some(id);
  487. }
  488. }
  489. if let Some(template) = ui
  490. .ctx()
  491. .data_mut(|d| d.remove_temp::<Value>(egui::Id::new("label_context_menu_delete")))
  492. {
  493. let name = template
  494. .get("template_name")
  495. .and_then(|v| v.as_str())
  496. .unwrap_or("Unknown");
  497. let id = template.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
  498. log::info!("Processing context menu delete for template: {}", name);
  499. self.pending_delete_id = Some(id);
  500. self.delete_dialog.open(name.to_string(), id.to_string());
  501. }
  502. }
  503. }
  504. impl Default for LabelTemplatesView {
  505. fn default() -> Self {
  506. Self::new()
  507. }
  508. }
  509. #[derive(Clone)]
  510. enum DeferredAction {
  511. DoubleClick(Value),
  512. ContextEdit(Value),
  513. ContextDelete(Value),
  514. ContextClone(Value),
  515. }
  516. // Temporary event handler that collects actions for later processing
  517. struct TempTemplatesEventHandler<'a> {
  518. #[allow(dead_code)]
  519. api_client: Option<&'a ApiClient>,
  520. deferred_actions: &'a mut Vec<DeferredAction>,
  521. permissions: Option<&'a serde_json::Value>,
  522. }
  523. impl<'a> TableEventHandler<Value> for TempTemplatesEventHandler<'a> {
  524. fn on_double_click(&mut self, item: &Value, _row_index: usize) {
  525. if RibbonUI::check_permission(self.permissions, "edit_label_template") {
  526. log::info!(
  527. "Double-click detected on template: {:?}",
  528. item.get("template_name")
  529. );
  530. self.deferred_actions
  531. .push(DeferredAction::DoubleClick(item.clone()));
  532. }
  533. }
  534. fn on_context_menu(&mut self, ui: &mut egui::Ui, item: &Value, _row_index: usize) {
  535. if RibbonUI::check_permission(self.permissions, "edit_label_template") {
  536. if ui
  537. .button(format!("{} Edit Template", egui_phosphor::regular::PENCIL))
  538. .clicked()
  539. {
  540. log::info!(
  541. "Context menu edit clicked for template: {:?}",
  542. item.get("template_name")
  543. );
  544. self.deferred_actions
  545. .push(DeferredAction::ContextEdit(item.clone()));
  546. ui.close();
  547. }
  548. ui.separator();
  549. }
  550. if RibbonUI::check_permission(self.permissions, "create_label_template") {
  551. if ui
  552. .button(format!("{} Clone Template", egui_phosphor::regular::COPY))
  553. .clicked()
  554. {
  555. log::info!(
  556. "Context menu clone clicked for template: {:?}",
  557. item.get("template_name")
  558. );
  559. self.deferred_actions
  560. .push(DeferredAction::ContextClone(item.clone()));
  561. ui.close();
  562. }
  563. ui.separator();
  564. }
  565. if RibbonUI::check_permission(self.permissions, "delete_label_template") {
  566. if ui
  567. .button(format!("{} Delete Template", egui_phosphor::regular::TRASH))
  568. .clicked()
  569. {
  570. log::info!(
  571. "Context menu delete clicked for template: {:?}",
  572. item.get("template_name")
  573. );
  574. self.deferred_actions
  575. .push(DeferredAction::ContextDelete(item.clone()));
  576. ui.close();
  577. }
  578. }
  579. }
  580. fn on_selection_changed(&mut self, selected_indices: &[usize]) {
  581. log::debug!("Template selection changed: {:?}", selected_indices);
  582. }
  583. }