borrowing.rs 57 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618
  1. use eframe::egui;
  2. use crate::api::ApiClient;
  3. use crate::core::components::form_builder::FormBuilder;
  4. use crate::core::tables::{get_all_loans, get_borrowers_summary};
  5. use crate::core::workflows::borrow_flow::BorrowFlow;
  6. use crate::core::workflows::return_flow::ReturnFlow;
  7. use crate::core::{ColumnConfig, TableRenderer};
  8. use crate::core::{EditorField, FieldType};
  9. pub struct BorrowingView {
  10. // data
  11. loans: Vec<serde_json::Value>,
  12. borrowers: Vec<serde_json::Value>,
  13. is_loading: bool,
  14. last_error: Option<String>,
  15. // UI
  16. init_loaded: bool,
  17. show_loans_column_selector: bool,
  18. show_borrowers_column_selector: bool,
  19. // Table renderers
  20. loans_table: TableRenderer,
  21. borrowers_table: TableRenderer,
  22. // Workflows
  23. borrow_flow: BorrowFlow,
  24. return_flow: ReturnFlow,
  25. // Register borrower dialog
  26. show_register_dialog: bool,
  27. new_borrower_name: String,
  28. new_borrower_email: String,
  29. new_borrower_phone: String,
  30. new_borrower_class: String,
  31. new_borrower_role: String,
  32. register_error: Option<String>,
  33. // Edit borrower dialog (using FormBuilder)
  34. borrower_editor: FormBuilder,
  35. // Ban/Unban borrower dialog
  36. show_ban_dialog: bool,
  37. show_unban_dialog: bool,
  38. ban_borrower_data: Option<serde_json::Value>,
  39. ban_fine_amount: String,
  40. ban_reason: String,
  41. // Return item confirm dialog
  42. show_return_confirm_dialog: bool,
  43. return_loan_data: Option<serde_json::Value>,
  44. // Delete borrower confirm dialog
  45. show_delete_borrower_dialog: bool,
  46. delete_borrower_data: Option<serde_json::Value>,
  47. // Search and filtering
  48. loans_search: String,
  49. borrowers_search: String,
  50. // Navigation
  51. pub switch_to_inventory_with_borrower: Option<i64>, // borrower_id to filter by
  52. }
  53. impl BorrowingView {
  54. pub fn new() -> Self {
  55. // Define columns for loans table - ALL columns from the query
  56. let loans_columns = vec![
  57. ColumnConfig::new("Loan ID", "id").with_width(60.0).hidden(),
  58. ColumnConfig::new("Asset ID", "asset_id")
  59. .with_width(70.0)
  60. .hidden(),
  61. ColumnConfig::new("Borrower ID", "borrower_id")
  62. .with_width(80.0)
  63. .hidden(),
  64. ColumnConfig::new("Tag", "asset_tag").with_width(80.0),
  65. ColumnConfig::new("Name", "name").with_width(200.0),
  66. ColumnConfig::new("Borrower", "borrower_name").with_width(120.0),
  67. ColumnConfig::new("Class", "class_name").with_width(80.0),
  68. ColumnConfig::new("Status", "lending_status").with_width(80.0),
  69. ColumnConfig::new("Checked Out", "checkout_date").with_width(100.0),
  70. ColumnConfig::new("Due", "due_date").with_width(90.0),
  71. ColumnConfig::new("Returned", "return_date").with_width(100.0),
  72. ColumnConfig::new("Notes", "notes")
  73. .with_width(150.0)
  74. .hidden(),
  75. ];
  76. // Define columns for borrowers table - with all backend fields
  77. let borrowers_columns = vec![
  78. ColumnConfig::new("ID", "borrower_id").with_width(60.0),
  79. ColumnConfig::new("Name", "borrower_name").with_width(150.0),
  80. ColumnConfig::new("Email", "email")
  81. .with_width(150.0)
  82. .hidden(),
  83. ColumnConfig::new("Phone", "phone_number")
  84. .with_width(120.0)
  85. .hidden(),
  86. ColumnConfig::new("Class", "class_name").with_width(80.0),
  87. ColumnConfig::new("Role", "role").with_width(80.0).hidden(),
  88. ColumnConfig::new("Active", "active_loans").with_width(60.0),
  89. ColumnConfig::new("Overdue", "overdue_loans").with_width(60.0),
  90. ColumnConfig::new("Banned", "banned").with_width(60.0),
  91. ];
  92. Self {
  93. loans: vec![],
  94. borrowers: vec![],
  95. is_loading: false,
  96. last_error: None,
  97. init_loaded: false,
  98. show_loans_column_selector: false,
  99. show_borrowers_column_selector: false,
  100. loans_table: TableRenderer::new()
  101. .with_columns(loans_columns)
  102. .with_default_sort("checkout_date", false), // Sort by checkout date DESC (most recent first)
  103. borrowers_table: TableRenderer::new().with_columns(borrowers_columns),
  104. borrow_flow: BorrowFlow::new(),
  105. return_flow: ReturnFlow::new(),
  106. show_register_dialog: false,
  107. new_borrower_name: String::new(),
  108. new_borrower_email: String::new(),
  109. new_borrower_phone: String::new(),
  110. new_borrower_class: String::new(),
  111. new_borrower_role: String::new(),
  112. register_error: None,
  113. borrower_editor: {
  114. let fields = vec![
  115. EditorField {
  116. name: "borrower_id".to_string(),
  117. label: "ID".to_string(),
  118. field_type: FieldType::Text,
  119. required: false,
  120. read_only: true,
  121. },
  122. EditorField {
  123. name: "name".to_string(),
  124. label: "Name".to_string(),
  125. field_type: FieldType::Text,
  126. required: true,
  127. read_only: false,
  128. },
  129. EditorField {
  130. name: "email".to_string(),
  131. label: "Email".to_string(),
  132. field_type: FieldType::Text,
  133. required: false,
  134. read_only: false,
  135. },
  136. EditorField {
  137. name: "phone_number".to_string(),
  138. label: "Phone".to_string(),
  139. field_type: FieldType::Text,
  140. required: false,
  141. read_only: false,
  142. },
  143. EditorField {
  144. name: "class_name".to_string(),
  145. label: "Class/Department".to_string(),
  146. field_type: FieldType::Text,
  147. required: false,
  148. read_only: false,
  149. },
  150. EditorField {
  151. name: "role".to_string(),
  152. label: "Role/Type".to_string(),
  153. field_type: FieldType::Text,
  154. required: false,
  155. read_only: false,
  156. },
  157. EditorField {
  158. name: "notes".to_string(),
  159. label: "Notes".to_string(),
  160. field_type: FieldType::MultilineText,
  161. required: false,
  162. read_only: false,
  163. },
  164. EditorField {
  165. name: "banned".to_string(),
  166. label: "Banned".to_string(),
  167. field_type: FieldType::Checkbox,
  168. required: false,
  169. read_only: false,
  170. },
  171. EditorField {
  172. name: "unban_fine".to_string(),
  173. label: "Unban Fine".to_string(),
  174. field_type: FieldType::Text,
  175. required: false,
  176. read_only: false,
  177. },
  178. ];
  179. FormBuilder::new("Edit Borrower", fields)
  180. },
  181. show_ban_dialog: false,
  182. show_unban_dialog: false,
  183. ban_borrower_data: None,
  184. ban_fine_amount: String::new(),
  185. ban_reason: String::new(),
  186. show_return_confirm_dialog: false,
  187. return_loan_data: None,
  188. show_delete_borrower_dialog: false,
  189. delete_borrower_data: None,
  190. loans_search: String::new(),
  191. borrowers_search: String::new(),
  192. switch_to_inventory_with_borrower: None,
  193. }
  194. }
  195. pub fn get_filter_columns() -> Vec<(String, String)> {
  196. vec![
  197. ("Any".to_string(), "Any".to_string()),
  198. ("Asset Tag".to_string(), "assets.asset_tag".to_string()),
  199. ("Asset Name".to_string(), "assets.name".to_string()),
  200. ("Borrower Name".to_string(), "borrowers.name".to_string()),
  201. ("Class".to_string(), "borrowers.class_name".to_string()),
  202. ("Status".to_string(), "assets.lending_status".to_string()),
  203. (
  204. "Checkout Date".to_string(),
  205. "lending_history.checkout_date".to_string(),
  206. ),
  207. (
  208. "Due Date".to_string(),
  209. "lending_history.due_date".to_string(),
  210. ),
  211. (
  212. "Return Date".to_string(),
  213. "lending_history.return_date".to_string(),
  214. ),
  215. ]
  216. }
  217. fn load(&mut self, api: &ApiClient) {
  218. if self.is_loading {
  219. return;
  220. }
  221. self.is_loading = true;
  222. self.last_error = None;
  223. match get_all_loans(api, None) {
  224. Ok(list) => {
  225. self.loans = list;
  226. }
  227. Err(e) => {
  228. self.last_error = Some(e.to_string());
  229. }
  230. }
  231. if self.last_error.is_none() {
  232. match get_borrowers_summary(api) {
  233. Ok(list) => {
  234. self.borrowers = list;
  235. }
  236. Err(e) => {
  237. self.last_error = Some(e.to_string());
  238. }
  239. }
  240. }
  241. self.is_loading = false;
  242. self.init_loaded = true;
  243. }
  244. pub fn show(
  245. &mut self,
  246. ctx: &egui::Context,
  247. ui: &mut egui::Ui,
  248. api_client: Option<&ApiClient>,
  249. ribbon: &mut crate::ui::ribbon::RibbonUI,
  250. ) {
  251. ui.horizontal(|ui| {
  252. ui.heading("Borrowing");
  253. if self.is_loading {
  254. ui.spinner();
  255. ui.label("Loading...");
  256. }
  257. if let Some(err) = &self.last_error {
  258. ui.colored_label(egui::Color32::RED, err);
  259. if ui.button("Refresh").clicked() {
  260. if let Some(api) = api_client {
  261. self.load(api);
  262. }
  263. }
  264. } else if ui.button("Refresh").clicked() {
  265. if let Some(api) = api_client {
  266. self.load(api);
  267. }
  268. }
  269. });
  270. ui.separator();
  271. // Check for filter changes
  272. if ribbon
  273. .checkboxes
  274. .get("borrowing_filter_changed")
  275. .copied()
  276. .unwrap_or(false)
  277. {
  278. ribbon
  279. .checkboxes
  280. .insert("borrowing_filter_changed".to_string(), false);
  281. // For now just note that filters changed - we'll apply them client-side in render
  282. // In the future we could reload with server-side filtering
  283. }
  284. // Check for ribbon actions
  285. if let Some(api) = api_client {
  286. if ribbon
  287. .checkboxes
  288. .get("borrowing_action_checkout")
  289. .copied()
  290. .unwrap_or(false)
  291. {
  292. self.borrow_flow.open(api);
  293. }
  294. if ribbon
  295. .checkboxes
  296. .get("borrowing_action_return")
  297. .copied()
  298. .unwrap_or(false)
  299. {
  300. self.return_flow.open(api);
  301. }
  302. if ribbon
  303. .checkboxes
  304. .get("borrowing_action_register")
  305. .copied()
  306. .unwrap_or(false)
  307. {
  308. self.show_register_dialog = true;
  309. self.register_error = None;
  310. }
  311. if ribbon
  312. .checkboxes
  313. .get("borrowing_action_refresh")
  314. .copied()
  315. .unwrap_or(false)
  316. {
  317. self.load(api);
  318. }
  319. }
  320. if !self.init_loaded {
  321. if let Some(api) = api_client {
  322. self.load(api);
  323. }
  324. }
  325. // Show borrow flow if open
  326. if let Some(api) = api_client {
  327. self.borrow_flow.show(ctx, api);
  328. if self.borrow_flow.take_recent_success() {
  329. self.load(api);
  330. }
  331. }
  332. // Show return flow if open
  333. if let Some(api) = api_client {
  334. self.return_flow.show(ctx, api);
  335. if self.return_flow.take_recent_success() {
  336. self.load(api);
  337. }
  338. }
  339. // Show register dialog if open
  340. if self.show_register_dialog {
  341. if let Some(api) = api_client {
  342. self.show_register_borrower_dialog(ctx, api);
  343. }
  344. }
  345. // Show borrower editor if open
  346. if let Some(api) = api_client {
  347. if let Some(result) = self.borrower_editor.show_editor(ctx) {
  348. if let Some(data) = result {
  349. // Editor returned data - save it
  350. if let Err(e) = self.save_borrower_changes(api, &data) {
  351. log::error!("Failed to save borrower changes: {}", e);
  352. } else {
  353. self.load(api);
  354. }
  355. }
  356. // else: user cancelled
  357. }
  358. }
  359. // Show ban dialog if open
  360. if self.show_ban_dialog {
  361. if let Some(api) = api_client {
  362. self.show_ban_dialog(ctx, api);
  363. }
  364. }
  365. // Show unban dialog if open
  366. if self.show_unban_dialog {
  367. if let Some(api) = api_client {
  368. self.show_unban_dialog(ctx, api);
  369. }
  370. }
  371. // Show return confirm dialog if open
  372. if self.show_return_confirm_dialog {
  373. if let Some(api) = api_client {
  374. self.show_return_confirm_dialog(ctx, api);
  375. }
  376. }
  377. // Show delete borrower confirm dialog if open
  378. if self.show_delete_borrower_dialog {
  379. if let Some(api) = api_client {
  380. self.show_delete_borrower_dialog(ctx, api);
  381. }
  382. }
  383. // Wrap entire content in ScrollArea
  384. egui::ScrollArea::vertical().show(ui, |ui| {
  385. // Section 1: Lending history
  386. egui::CollapsingHeader::new("Lending History")
  387. .default_open(true)
  388. .show(ui, |ui| {
  389. ui.horizontal(|ui| {
  390. ui.heading("Loans");
  391. ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
  392. if ui.button("Columns").clicked() {
  393. self.show_loans_column_selector = !self.show_loans_column_selector;
  394. }
  395. });
  396. });
  397. // Search and filter controls
  398. ui.horizontal(|ui| {
  399. ui.label("Search:");
  400. ui.text_edit_singleline(&mut self.loans_search);
  401. ui.separator();
  402. // Status filters from ribbon
  403. ui.label("Show:");
  404. ui.checkbox(
  405. ribbon
  406. .checkboxes
  407. .entry("borrowing_show_normal".to_string())
  408. .or_insert(true),
  409. "Normal",
  410. );
  411. ui.checkbox(
  412. ribbon
  413. .checkboxes
  414. .entry("borrowing_show_overdue".to_string())
  415. .or_insert(true),
  416. "Overdue",
  417. );
  418. ui.checkbox(
  419. ribbon
  420. .checkboxes
  421. .entry("borrowing_show_stolen".to_string())
  422. .or_insert(true),
  423. "Stolen",
  424. );
  425. ui.checkbox(
  426. ribbon
  427. .checkboxes
  428. .entry("borrowing_show_returned".to_string())
  429. .or_insert(false),
  430. "Returned",
  431. );
  432. });
  433. ui.separator();
  434. self.render_active_loans(ui, ribbon);
  435. });
  436. ui.add_space(10.0);
  437. // Section 2: Borrowers summary
  438. egui::CollapsingHeader::new("Borrowers")
  439. .default_open(true)
  440. .show(ui, |ui| {
  441. ui.horizontal(|ui| {
  442. ui.heading("Borrowers");
  443. ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
  444. if ui.button("Columns").clicked() {
  445. self.show_borrowers_column_selector =
  446. !self.show_borrowers_column_selector;
  447. }
  448. });
  449. });
  450. // Search control
  451. ui.horizontal(|ui| {
  452. ui.label("Search:");
  453. ui.text_edit_singleline(&mut self.borrowers_search);
  454. });
  455. ui.separator();
  456. self.render_borrowers_table(ui);
  457. });
  458. }); // End ScrollArea
  459. // Show column selector windows
  460. if self.show_loans_column_selector {
  461. egui::Window::new("Loans Columns")
  462. .open(&mut self.show_loans_column_selector)
  463. .resizable(true)
  464. .default_width(250.0)
  465. .show(ctx, |ui| {
  466. self.loans_table.show_column_selector(ui, "loans");
  467. });
  468. }
  469. if self.show_borrowers_column_selector {
  470. egui::Window::new("Borrowers Columns")
  471. .open(&mut self.show_borrowers_column_selector)
  472. .resizable(true)
  473. .default_width(250.0)
  474. .show(ctx, |ui| {
  475. self.borrowers_table.show_column_selector(ui, "borrowers");
  476. });
  477. }
  478. }
  479. fn render_active_loans(&mut self, ui: &mut egui::Ui, ribbon: &crate::ui::ribbon::RibbonUI) {
  480. // Get checkbox states
  481. let show_returned = ribbon
  482. .checkboxes
  483. .get("borrowing_show_returned")
  484. .copied()
  485. .unwrap_or(false);
  486. let show_normal = ribbon
  487. .checkboxes
  488. .get("borrowing_show_normal")
  489. .copied()
  490. .unwrap_or(true);
  491. let show_overdue = ribbon
  492. .checkboxes
  493. .get("borrowing_show_overdue")
  494. .copied()
  495. .unwrap_or(true);
  496. let show_stolen = ribbon
  497. .checkboxes
  498. .get("borrowing_show_stolen")
  499. .copied()
  500. .unwrap_or(true);
  501. // Apply filters
  502. let filtered_loans: Vec<serde_json::Value> = self
  503. .loans
  504. .iter()
  505. .filter(|loan| {
  506. // First apply search filter
  507. if !self.loans_search.is_empty() {
  508. let search_lower = self.loans_search.to_lowercase();
  509. let asset_tag = loan.get("asset_tag").and_then(|v| v.as_str()).unwrap_or("");
  510. let asset_name = loan.get("name").and_then(|v| v.as_str()).unwrap_or("");
  511. let borrower_name = loan
  512. .get("borrower_name")
  513. .and_then(|v| v.as_str())
  514. .unwrap_or("");
  515. let class_name = loan
  516. .get("class_name")
  517. .and_then(|v| v.as_str())
  518. .unwrap_or("");
  519. if !(asset_tag.to_lowercase().contains(&search_lower)
  520. || asset_name.to_lowercase().contains(&search_lower)
  521. || borrower_name.to_lowercase().contains(&search_lower)
  522. || class_name.to_lowercase().contains(&search_lower))
  523. {
  524. return false;
  525. }
  526. }
  527. // Apply filter builder filters
  528. if !Self::matches_filter_builder(loan, &ribbon.filter_builder) {
  529. return false;
  530. }
  531. // Check if this loan has been returned
  532. let has_return_date = loan
  533. .get("return_date")
  534. .and_then(|v| v.as_str())
  535. .filter(|s| !s.is_empty())
  536. .is_some();
  537. // If returned, check the show_returned checkbox
  538. if has_return_date {
  539. return show_returned;
  540. }
  541. // For active loans, check the lending_status from assets table
  542. let lending_status = loan
  543. .get("lending_status")
  544. .and_then(|v| v.as_str())
  545. .unwrap_or("");
  546. // Check if stolen
  547. if lending_status == "Stolen" || lending_status == "Illegally Handed Out" {
  548. return show_stolen;
  549. }
  550. // Check if overdue
  551. if let Some(due_date_str) = loan.get("due_date").and_then(|v| v.as_str()) {
  552. let now = chrono::Local::now().format("%Y-%m-%d").to_string();
  553. if due_date_str < now.as_str() {
  554. return show_overdue;
  555. }
  556. }
  557. // Otherwise it's a normal active loan (not overdue, not stolen)
  558. show_normal
  559. })
  560. .cloned()
  561. .collect();
  562. // Derive a display status per loan to avoid confusion:
  563. // If a loan has a return_date, always show "Returned" regardless of the current asset status.
  564. // Otherwise, use the existing lending_status value (Overdue, etc. handled by DB).
  565. let mut display_loans: Vec<serde_json::Value> = Vec::with_capacity(filtered_loans.len());
  566. for loan in &filtered_loans {
  567. let mut row = loan.clone();
  568. let has_return = row
  569. .get("return_date")
  570. .and_then(|v| v.as_str())
  571. .map(|s| !s.is_empty())
  572. .unwrap_or(false);
  573. if has_return {
  574. row["lending_status"] = serde_json::Value::String("Returned".to_string());
  575. }
  576. display_loans.push(row);
  577. }
  578. let prepared_data = self.loans_table.prepare_json_data(&display_loans);
  579. // Handle loan table events (return item)
  580. let mut return_loan: Option<serde_json::Value> = None;
  581. struct LoanEventHandler<'a> {
  582. return_action: &'a mut Option<serde_json::Value>,
  583. }
  584. impl<'a> crate::core::table_renderer::TableEventHandler<serde_json::Value>
  585. for LoanEventHandler<'a>
  586. {
  587. fn on_double_click(&mut self, _item: &serde_json::Value, _row_index: usize) {
  588. // Not used for loans
  589. }
  590. fn on_context_menu(
  591. &mut self,
  592. ui: &mut egui::Ui,
  593. item: &serde_json::Value,
  594. _row_index: usize,
  595. ) {
  596. // Only show "Return Item" if the loan is active (no return_date)
  597. let has_return_date = item.get("return_date").and_then(|v| v.as_str()).is_some();
  598. if !has_return_date {
  599. if ui
  600. .button(format!(
  601. "{} Return Item",
  602. egui_phosphor::regular::ARROW_RIGHT
  603. ))
  604. .clicked()
  605. {
  606. *self.return_action = Some(item.clone());
  607. ui.close();
  608. }
  609. }
  610. }
  611. fn on_selection_changed(&mut self, _selected_indices: &[usize]) {
  612. // Not used for now
  613. }
  614. }
  615. let mut handler = LoanEventHandler {
  616. return_action: &mut return_loan,
  617. };
  618. self.loans_table
  619. .render_json_table(ui, &prepared_data, Some(&mut handler));
  620. // Store return action for processing after all rendering
  621. if let Some(loan) = return_loan {
  622. self.return_loan_data = Some(loan);
  623. self.show_return_confirm_dialog = true;
  624. }
  625. }
  626. /// Client-side filter matching for filter builder conditions
  627. fn matches_filter_builder(
  628. loan: &serde_json::Value,
  629. filter_builder: &crate::core::components::filter_builder::FilterBuilder,
  630. ) -> bool {
  631. use crate::core::components::filter_builder::FilterOperator;
  632. // If no valid conditions, don't filter
  633. if !filter_builder.filter_group.is_valid() {
  634. return true;
  635. }
  636. // Check each condition
  637. for condition in &filter_builder.filter_group.conditions {
  638. if !condition.is_valid() {
  639. continue;
  640. }
  641. // Map the filter column to the actual JSON field name
  642. let field_name = match condition.column.as_str() {
  643. "assets.asset_tag" => "asset_tag",
  644. "assets.name" => "name",
  645. "borrowers.name" => "borrower_name",
  646. "borrowers.class_name" => "class_name",
  647. "assets.lending_status" => "lending_status",
  648. "lending_history.checkout_date" => "checkout_date",
  649. "lending_history.due_date" => "due_date",
  650. "lending_history.return_date" => "return_date",
  651. _ => {
  652. // Fallback: strip table prefix if present
  653. if condition.column.contains('.') {
  654. condition
  655. .column
  656. .split('.')
  657. .last()
  658. .unwrap_or(&condition.column)
  659. } else {
  660. &condition.column
  661. }
  662. }
  663. };
  664. let field_value = loan.get(field_name).and_then(|v| v.as_str()).unwrap_or("");
  665. // Apply the operator
  666. let matches = match &condition.operator {
  667. FilterOperator::Is => field_value == condition.value,
  668. FilterOperator::IsNot => field_value != condition.value,
  669. FilterOperator::Contains => field_value
  670. .to_lowercase()
  671. .contains(&condition.value.to_lowercase()),
  672. FilterOperator::DoesntContain => !field_value
  673. .to_lowercase()
  674. .contains(&condition.value.to_lowercase()),
  675. FilterOperator::IsNull => field_value.is_empty(),
  676. FilterOperator::IsNotNull => !field_value.is_empty(),
  677. };
  678. if !matches {
  679. return false; // For now, treat as AND logic
  680. }
  681. }
  682. true
  683. }
  684. fn render_borrowers_table(&mut self, ui: &mut egui::Ui) {
  685. // Apply search filter if set
  686. let filtered_borrowers: Vec<serde_json::Value> = if self.borrowers_search.is_empty() {
  687. self.borrowers.clone()
  688. } else {
  689. let search_lower = self.borrowers_search.to_lowercase();
  690. self.borrowers
  691. .iter()
  692. .filter(|borrower| {
  693. let name = borrower
  694. .get("borrower_name")
  695. .and_then(|v| v.as_str())
  696. .unwrap_or("");
  697. let class = borrower
  698. .get("class_name")
  699. .and_then(|v| v.as_str())
  700. .unwrap_or("");
  701. name.to_lowercase().contains(&search_lower)
  702. || class.to_lowercase().contains(&search_lower)
  703. })
  704. .cloned()
  705. .collect()
  706. };
  707. let prepared_data = self.borrowers_table.prepare_json_data(&filtered_borrowers);
  708. // Store actions to perform after rendering (to avoid borrow checker issues)
  709. let mut edit_borrower: Option<serde_json::Value> = None;
  710. let mut ban_borrower: Option<serde_json::Value> = None;
  711. let mut unban_borrower: Option<serde_json::Value> = None;
  712. let mut delete_borrower: Option<serde_json::Value> = None;
  713. let mut show_items_for_borrower: Option<i64> = None;
  714. // Create event handler for context menu
  715. struct BorrowerEventHandler<'a> {
  716. edit_action: &'a mut Option<serde_json::Value>,
  717. ban_action: &'a mut Option<serde_json::Value>,
  718. unban_action: &'a mut Option<serde_json::Value>,
  719. delete_action: &'a mut Option<serde_json::Value>,
  720. show_items_action: &'a mut Option<i64>,
  721. }
  722. impl<'a> crate::core::TableEventHandler<serde_json::Value> for BorrowerEventHandler<'a> {
  723. fn on_double_click(&mut self, item: &serde_json::Value, _row_index: usize) {
  724. // Open edit dialog on double-click
  725. *self.edit_action = Some(item.clone());
  726. }
  727. fn on_context_menu(
  728. &mut self,
  729. ui: &mut egui::Ui,
  730. item: &serde_json::Value,
  731. _row_index: usize,
  732. ) {
  733. let is_banned = item
  734. .get("banned")
  735. .and_then(|v| v.as_bool())
  736. .unwrap_or(false);
  737. let borrower_id = item.get("borrower_id").and_then(|v| v.as_i64());
  738. if ui
  739. .button(format!("{} Edit Borrower", egui_phosphor::regular::PENCIL))
  740. .clicked()
  741. {
  742. *self.edit_action = Some(item.clone());
  743. ui.close();
  744. }
  745. if let Some(id) = borrower_id {
  746. if ui
  747. .button(format!(
  748. "{} Show Items Borrowed to this User",
  749. egui_phosphor::regular::PACKAGE
  750. ))
  751. .clicked()
  752. {
  753. *self.show_items_action = Some(id);
  754. ui.close();
  755. }
  756. }
  757. ui.separator();
  758. if is_banned {
  759. if ui
  760. .button(format!(
  761. "{} Unban Borrower",
  762. egui_phosphor::regular::CHECK_CIRCLE
  763. ))
  764. .clicked()
  765. {
  766. *self.unban_action = Some(item.clone());
  767. ui.close();
  768. }
  769. } else {
  770. if ui
  771. .button(format!("{} Ban Borrower", egui_phosphor::regular::PROHIBIT))
  772. .clicked()
  773. {
  774. *self.ban_action = Some(item.clone());
  775. ui.close();
  776. }
  777. }
  778. ui.separator();
  779. if ui
  780. .button(format!("{} Delete Borrower", egui_phosphor::regular::TRASH))
  781. .clicked()
  782. {
  783. *self.delete_action = Some(item.clone());
  784. ui.close();
  785. }
  786. }
  787. fn on_selection_changed(&mut self, _selected_indices: &[usize]) {
  788. // Not used for now
  789. }
  790. }
  791. let mut handler = BorrowerEventHandler {
  792. edit_action: &mut edit_borrower,
  793. ban_action: &mut ban_borrower,
  794. unban_action: &mut unban_borrower,
  795. delete_action: &mut delete_borrower,
  796. show_items_action: &mut show_items_for_borrower,
  797. };
  798. self.borrowers_table
  799. .render_json_table(ui, &prepared_data, Some(&mut handler));
  800. // Process actions after rendering
  801. if let Some(borrower) = edit_borrower {
  802. self.open_edit_borrower_dialog(borrower);
  803. }
  804. if let Some(borrower) = ban_borrower {
  805. self.open_ban_dialog(borrower);
  806. }
  807. if let Some(borrower) = unban_borrower {
  808. self.open_unban_dialog(borrower);
  809. }
  810. if let Some(borrower) = delete_borrower {
  811. self.delete_borrower_data = Some(borrower);
  812. self.show_delete_borrower_dialog = true;
  813. }
  814. if let Some(borrower_id) = show_items_for_borrower {
  815. // Set the flag to switch to inventory with this borrower filter
  816. self.switch_to_inventory_with_borrower = Some(borrower_id);
  817. }
  818. }
  819. fn show_register_borrower_dialog(&mut self, ctx: &egui::Context, api_client: &ApiClient) {
  820. egui::Window::new("Register New Borrower")
  821. .collapsible(false)
  822. .resizable(false)
  823. .default_width(400.0)
  824. .show(ctx, |ui| {
  825. ui.vertical(|ui| {
  826. ui.add_space(5.0);
  827. if let Some(err) = &self.register_error {
  828. ui.colored_label(egui::Color32::RED, err);
  829. ui.separator();
  830. }
  831. ui.horizontal(|ui| {
  832. ui.label("Name:");
  833. ui.add_sized(
  834. [250.0, 20.0],
  835. egui::TextEdit::singleline(&mut self.new_borrower_name)
  836. .hint_text("Full name"),
  837. );
  838. });
  839. ui.horizontal(|ui| {
  840. ui.label("Email:");
  841. ui.add_sized(
  842. [250.0, 20.0],
  843. egui::TextEdit::singleline(&mut self.new_borrower_email)
  844. .hint_text("email@example.com"),
  845. );
  846. });
  847. ui.horizontal(|ui| {
  848. ui.label("Phone:");
  849. ui.add_sized(
  850. [250.0, 20.0],
  851. egui::TextEdit::singleline(&mut self.new_borrower_phone)
  852. .hint_text("Phone number"),
  853. );
  854. });
  855. ui.horizontal(|ui| {
  856. ui.label("Class:");
  857. ui.add_sized(
  858. [250.0, 20.0],
  859. egui::TextEdit::singleline(&mut self.new_borrower_class)
  860. .hint_text("Class or department"),
  861. );
  862. });
  863. ui.horizontal(|ui| {
  864. ui.label("Role:");
  865. ui.add_sized(
  866. [250.0, 20.0],
  867. egui::TextEdit::singleline(&mut self.new_borrower_role)
  868. .hint_text("Student, Staff, etc."),
  869. );
  870. });
  871. ui.add_space(10.0);
  872. ui.separator();
  873. ui.horizontal(|ui| {
  874. if ui.button("Register").clicked() {
  875. if self.new_borrower_name.trim().is_empty() {
  876. self.register_error = Some("Name is required".to_string());
  877. } else {
  878. match self.register_borrower(api_client) {
  879. Ok(_) => {
  880. // Success - close dialog and reload data
  881. self.show_register_dialog = false;
  882. self.clear_register_form();
  883. self.load(api_client);
  884. }
  885. Err(e) => {
  886. self.register_error = Some(e.to_string());
  887. }
  888. }
  889. }
  890. }
  891. if ui.button("Cancel").clicked() {
  892. self.show_register_dialog = false;
  893. self.clear_register_form();
  894. }
  895. });
  896. });
  897. });
  898. }
  899. fn register_borrower(&self, api_client: &ApiClient) -> anyhow::Result<()> {
  900. use crate::models::QueryRequest;
  901. let mut borrower_data = serde_json::json!({
  902. "name": self.new_borrower_name.trim(),
  903. "banned": false,
  904. });
  905. if !self.new_borrower_email.is_empty() {
  906. borrower_data["email"] =
  907. serde_json::Value::String(self.new_borrower_email.trim().to_string());
  908. }
  909. if !self.new_borrower_phone.is_empty() {
  910. borrower_data["phone_number"] =
  911. serde_json::Value::String(self.new_borrower_phone.trim().to_string());
  912. }
  913. if !self.new_borrower_class.is_empty() {
  914. borrower_data["class_name"] =
  915. serde_json::Value::String(self.new_borrower_class.trim().to_string());
  916. }
  917. if !self.new_borrower_role.is_empty() {
  918. borrower_data["role"] =
  919. serde_json::Value::String(self.new_borrower_role.trim().to_string());
  920. }
  921. let request = QueryRequest {
  922. action: "insert".to_string(),
  923. table: "borrowers".to_string(),
  924. columns: None,
  925. r#where: None,
  926. data: Some(borrower_data),
  927. filter: None,
  928. order_by: None,
  929. limit: None,
  930. offset: None,
  931. joins: None,
  932. };
  933. let response = api_client.query(&request)?;
  934. if !response.success {
  935. return Err(anyhow::anyhow!(response
  936. .error
  937. .unwrap_or_else(|| "Failed to register borrower".to_string())));
  938. }
  939. Ok(())
  940. }
  941. fn clear_register_form(&mut self) {
  942. self.new_borrower_name.clear();
  943. self.new_borrower_email.clear();
  944. self.new_borrower_phone.clear();
  945. self.new_borrower_class.clear();
  946. self.new_borrower_role.clear();
  947. self.register_error = None;
  948. }
  949. // Edit borrower dialog methods
  950. fn open_edit_borrower_dialog(&mut self, borrower: serde_json::Value) {
  951. // The summary doesn't have all fields, so we'll populate what we have
  952. // and the editor will show empty fields for missing data
  953. let mut editor_data = serde_json::Map::new();
  954. // Map the summary fields to editor fields
  955. if let Some(id) = borrower.get("borrower_id") {
  956. editor_data.insert("borrower_id".to_string(), id.clone());
  957. editor_data.insert("id".to_string(), id.clone()); // Also set 'id' for WHERE clause
  958. }
  959. if let Some(name) = borrower.get("borrower_name") {
  960. editor_data.insert("name".to_string(), name.clone());
  961. }
  962. if let Some(email) = borrower.get("email") {
  963. if !email.is_null() && email.as_str().map(|s| !s.is_empty()).unwrap_or(false) {
  964. editor_data.insert("email".to_string(), email.clone());
  965. }
  966. }
  967. if let Some(phone) = borrower.get("phone_number") {
  968. if !phone.is_null() && phone.as_str().map(|s| !s.is_empty()).unwrap_or(false) {
  969. editor_data.insert("phone_number".to_string(), phone.clone());
  970. }
  971. }
  972. if let Some(class) = borrower.get("class_name") {
  973. if !class.is_null() && class.as_str().map(|s| !s.is_empty()).unwrap_or(false) {
  974. editor_data.insert("class_name".to_string(), class.clone());
  975. }
  976. }
  977. if let Some(role) = borrower.get("role") {
  978. if !role.is_null() && role.as_str().map(|s| !s.is_empty()).unwrap_or(false) {
  979. editor_data.insert("role".to_string(), role.clone());
  980. }
  981. }
  982. if let Some(notes) = borrower.get("notes") {
  983. if !notes.is_null() && notes.as_str().map(|s| !s.is_empty()).unwrap_or(false) {
  984. editor_data.insert("notes".to_string(), notes.clone());
  985. }
  986. }
  987. if let Some(banned) = borrower.get("banned") {
  988. editor_data.insert("banned".to_string(), banned.clone());
  989. }
  990. if let Some(unban_fine) = borrower.get("unban_fine") {
  991. if !unban_fine.is_null() {
  992. editor_data.insert("unban_fine".to_string(), unban_fine.clone());
  993. }
  994. }
  995. // Open the editor with the borrower data
  996. let value = serde_json::Value::Object(editor_data);
  997. self.borrower_editor.open(&value);
  998. }
  999. fn save_borrower_changes(
  1000. &self,
  1001. api_client: &ApiClient,
  1002. diff: &serde_json::Map<String, serde_json::Value>,
  1003. ) -> anyhow::Result<()> {
  1004. use crate::models::QueryRequest;
  1005. // Extract borrower ID from the diff (editor includes it as __editor_item_id)
  1006. let borrower_id = diff
  1007. .get("__editor_item_id")
  1008. .and_then(|v| v.as_str())
  1009. .and_then(|s| s.parse::<i64>().ok())
  1010. .or_else(|| diff.get("borrower_id").and_then(|v| v.as_i64()))
  1011. .or_else(|| diff.get("id").and_then(|v| v.as_i64()))
  1012. .ok_or_else(|| anyhow::anyhow!("Invalid borrower ID"))?;
  1013. // Build update data from the diff (exclude editor metadata)
  1014. let mut update_data = serde_json::Map::new();
  1015. for (key, value) in diff.iter() {
  1016. if !key.starts_with("__editor_") && key != "borrower_id" && key != "id" {
  1017. update_data.insert(key.clone(), value.clone());
  1018. }
  1019. }
  1020. if update_data.is_empty() {
  1021. return Ok(()); // Nothing to update
  1022. }
  1023. let request = QueryRequest {
  1024. action: "update".to_string(),
  1025. table: "borrowers".to_string(),
  1026. data: Some(serde_json::Value::Object(update_data)),
  1027. r#where: Some(serde_json::json!({"id": borrower_id})),
  1028. columns: None,
  1029. joins: None,
  1030. order_by: None,
  1031. limit: None,
  1032. offset: None,
  1033. filter: None,
  1034. };
  1035. let response = api_client.query(&request)?;
  1036. if !response.success {
  1037. return Err(anyhow::anyhow!(response
  1038. .error
  1039. .unwrap_or_else(|| "Failed to update borrower".to_string())));
  1040. }
  1041. Ok(())
  1042. }
  1043. // Ban/Unban dialog methods
  1044. fn open_ban_dialog(&mut self, borrower: serde_json::Value) {
  1045. self.ban_borrower_data = Some(borrower);
  1046. self.show_ban_dialog = true;
  1047. self.ban_fine_amount.clear();
  1048. self.ban_reason.clear();
  1049. }
  1050. fn open_unban_dialog(&mut self, borrower: serde_json::Value) {
  1051. self.ban_borrower_data = Some(borrower);
  1052. self.show_unban_dialog = true;
  1053. }
  1054. fn show_ban_dialog(&mut self, ctx: &egui::Context, api_client: &ApiClient) {
  1055. let mut keep_open = true;
  1056. let mut confirmed = false;
  1057. let mut cancelled = false;
  1058. let borrower_name = self
  1059. .ban_borrower_data
  1060. .as_ref()
  1061. .and_then(|b| b.get("borrower_name"))
  1062. .and_then(|v| v.as_str())
  1063. .unwrap_or("Unknown")
  1064. .to_string();
  1065. egui::Window::new("Ban Borrower")
  1066. .collapsible(false)
  1067. .resizable(false)
  1068. .default_width(400.0)
  1069. .open(&mut keep_open)
  1070. .show(ctx, |ui| {
  1071. ui.vertical(|ui| {
  1072. ui.add_space(5.0);
  1073. ui.label(
  1074. egui::RichText::new(format!(
  1075. "⚠ Are you sure you want to ban '{}'?",
  1076. borrower_name
  1077. ))
  1078. .color(egui::Color32::from_rgb(255, 152, 0))
  1079. .strong(),
  1080. );
  1081. ui.add_space(10.0);
  1082. ui.horizontal(|ui| {
  1083. ui.label("Fine Amount ($):");
  1084. ui.text_edit_singleline(&mut self.ban_fine_amount);
  1085. });
  1086. ui.label(
  1087. egui::RichText::new("(Optional: leave empty for no fine)")
  1088. .small()
  1089. .color(ui.visuals().weak_text_color()),
  1090. );
  1091. ui.add_space(5.0);
  1092. ui.label("Reason:");
  1093. ui.text_edit_multiline(&mut self.ban_reason);
  1094. ui.label(
  1095. egui::RichText::new("(Optional: reason for banning)")
  1096. .small()
  1097. .color(ui.visuals().weak_text_color()),
  1098. );
  1099. ui.add_space(10.0);
  1100. ui.separator();
  1101. ui.horizontal(|ui| {
  1102. if ui.button("Confirm Ban").clicked() {
  1103. confirmed = true;
  1104. }
  1105. if ui.button("Cancel").clicked() {
  1106. cancelled = true;
  1107. }
  1108. });
  1109. });
  1110. });
  1111. if confirmed {
  1112. match self.ban_borrower(api_client) {
  1113. Ok(_) => {
  1114. self.show_ban_dialog = false;
  1115. self.ban_borrower_data = None;
  1116. self.load(api_client);
  1117. }
  1118. Err(e) => {
  1119. log::error!("Failed to ban borrower: {}", e);
  1120. }
  1121. }
  1122. }
  1123. if cancelled || !keep_open {
  1124. self.show_ban_dialog = false;
  1125. self.ban_borrower_data = None;
  1126. }
  1127. }
  1128. fn show_unban_dialog(&mut self, ctx: &egui::Context, api_client: &ApiClient) {
  1129. let mut keep_open = true;
  1130. let mut confirmed = false;
  1131. let mut cancelled = false;
  1132. let borrower_name = self
  1133. .ban_borrower_data
  1134. .as_ref()
  1135. .and_then(|b| b.get("borrower_name"))
  1136. .and_then(|v| v.as_str())
  1137. .unwrap_or("Unknown")
  1138. .to_string();
  1139. egui::Window::new("Unban Borrower")
  1140. .collapsible(false)
  1141. .resizable(false)
  1142. .default_width(400.0)
  1143. .open(&mut keep_open)
  1144. .show(ctx, |ui| {
  1145. ui.vertical(|ui| {
  1146. ui.add_space(5.0);
  1147. ui.label(
  1148. egui::RichText::new(format!(
  1149. "Are you sure you want to unban '{}'?",
  1150. borrower_name
  1151. ))
  1152. .color(egui::Color32::from_rgb(76, 175, 80))
  1153. .strong(),
  1154. );
  1155. ui.add_space(10.0);
  1156. ui.separator();
  1157. ui.horizontal(|ui| {
  1158. if ui.button("Confirm Unban").clicked() {
  1159. confirmed = true;
  1160. }
  1161. if ui.button("Cancel").clicked() {
  1162. cancelled = true;
  1163. }
  1164. });
  1165. });
  1166. });
  1167. if confirmed {
  1168. match self.unban_borrower(api_client) {
  1169. Ok(_) => {
  1170. self.show_unban_dialog = false;
  1171. self.ban_borrower_data = None;
  1172. self.load(api_client);
  1173. }
  1174. Err(e) => {
  1175. log::error!("Failed to unban borrower: {}", e);
  1176. }
  1177. }
  1178. }
  1179. if cancelled || !keep_open {
  1180. self.show_unban_dialog = false;
  1181. self.ban_borrower_data = None;
  1182. }
  1183. }
  1184. fn show_return_confirm_dialog(&mut self, _ctx: &egui::Context, api_client: &ApiClient) {
  1185. // Replace the basic confirm dialog with the full Return Flow, pre-selecting the loan
  1186. if let Some(loan) = self.return_loan_data.clone() {
  1187. // Open the full-featured return flow and jump to confirmation
  1188. self.return_flow.open(api_client);
  1189. self.return_flow.selected_loan = Some(loan);
  1190. self.return_flow.current_step =
  1191. crate::core::workflows::return_flow::ReturnStep::Confirm;
  1192. }
  1193. // Close the legacy confirm dialog path
  1194. self.show_return_confirm_dialog = false;
  1195. self.return_loan_data = None;
  1196. }
  1197. fn show_delete_borrower_dialog(&mut self, ctx: &egui::Context, api_client: &ApiClient) {
  1198. let mut keep_open = true;
  1199. let mut confirmed = false;
  1200. let mut cancelled = false;
  1201. let borrower_name = self
  1202. .delete_borrower_data
  1203. .as_ref()
  1204. .and_then(|b| b.get("borrower_name"))
  1205. .and_then(|v| v.as_str())
  1206. .unwrap_or("Unknown")
  1207. .to_string();
  1208. egui::Window::new("Delete Borrower")
  1209. .collapsible(false)
  1210. .resizable(false)
  1211. .default_width(400.0)
  1212. .open(&mut keep_open)
  1213. .show(ctx, |ui| {
  1214. ui.vertical(|ui| {
  1215. ui.add_space(5.0);
  1216. ui.label(
  1217. egui::RichText::new(format!(
  1218. "Are you sure you want to delete '{}'?",
  1219. borrower_name
  1220. ))
  1221. .color(egui::Color32::RED)
  1222. .strong(),
  1223. );
  1224. ui.add_space(10.0);
  1225. ui.label(
  1226. egui::RichText::new("This action cannot be undone!")
  1227. .color(egui::Color32::RED)
  1228. .small(),
  1229. );
  1230. ui.add_space(10.0);
  1231. ui.separator();
  1232. ui.horizontal(|ui| {
  1233. if ui.button("Confirm Delete").clicked() {
  1234. confirmed = true;
  1235. }
  1236. if ui.button("Cancel").clicked() {
  1237. cancelled = true;
  1238. }
  1239. });
  1240. });
  1241. });
  1242. if confirmed {
  1243. match self.delete_borrower(api_client) {
  1244. Ok(_) => {
  1245. self.show_delete_borrower_dialog = false;
  1246. self.delete_borrower_data = None;
  1247. self.load(api_client);
  1248. }
  1249. Err(e) => {
  1250. log::error!("Failed to delete borrower: {}", e);
  1251. self.last_error = Some(format!("Delete failed: {}", e));
  1252. }
  1253. }
  1254. }
  1255. if cancelled || !keep_open {
  1256. self.show_delete_borrower_dialog = false;
  1257. self.delete_borrower_data = None;
  1258. }
  1259. }
  1260. fn ban_borrower(&self, api_client: &ApiClient) -> anyhow::Result<()> {
  1261. use crate::models::QueryRequest;
  1262. let borrower_id = self
  1263. .ban_borrower_data
  1264. .as_ref()
  1265. .and_then(|b| b.get("borrower_id"))
  1266. .and_then(|v| v.as_i64())
  1267. .ok_or_else(|| anyhow::anyhow!("Invalid borrower ID"))?;
  1268. let mut update_data = serde_json::json!({
  1269. "banned": true,
  1270. });
  1271. // Add unban fine amount if provided
  1272. if !self.ban_fine_amount.trim().is_empty() {
  1273. if let Ok(fine) = self.ban_fine_amount.trim().parse::<f64>() {
  1274. update_data["unban_fine"] = serde_json::Value::Number(
  1275. serde_json::Number::from_f64(fine).unwrap_or(serde_json::Number::from(0)),
  1276. );
  1277. }
  1278. }
  1279. // Add reason to notes if provided
  1280. if !self.ban_reason.trim().is_empty() {
  1281. update_data["notes"] = serde_json::Value::String(self.ban_reason.trim().to_string());
  1282. }
  1283. let request = QueryRequest {
  1284. action: "update".to_string(),
  1285. table: "borrowers".to_string(),
  1286. data: Some(update_data),
  1287. r#where: Some(serde_json::json!({"id": borrower_id})),
  1288. columns: None,
  1289. joins: None,
  1290. order_by: None,
  1291. limit: None,
  1292. offset: None,
  1293. filter: None,
  1294. };
  1295. let response = api_client.query(&request)?;
  1296. if !response.success {
  1297. return Err(anyhow::anyhow!(response
  1298. .error
  1299. .unwrap_or_else(|| "Failed to ban borrower".to_string())));
  1300. }
  1301. Ok(())
  1302. }
  1303. fn unban_borrower(&self, api_client: &ApiClient) -> anyhow::Result<()> {
  1304. use crate::models::QueryRequest;
  1305. let borrower_id = self
  1306. .ban_borrower_data
  1307. .as_ref()
  1308. .and_then(|b| b.get("borrower_id"))
  1309. .and_then(|v| v.as_i64())
  1310. .ok_or_else(|| anyhow::anyhow!("Invalid borrower ID"))?;
  1311. let update_data = serde_json::json!({
  1312. "banned": false,
  1313. "unban_fine": 0.0,
  1314. });
  1315. let request = QueryRequest {
  1316. action: "update".to_string(),
  1317. table: "borrowers".to_string(),
  1318. data: Some(update_data),
  1319. r#where: Some(serde_json::json!({"id": borrower_id})),
  1320. columns: None,
  1321. joins: None,
  1322. order_by: None,
  1323. limit: None,
  1324. offset: None,
  1325. filter: None,
  1326. };
  1327. let response = api_client.query(&request)?;
  1328. if !response.success {
  1329. return Err(anyhow::anyhow!(response
  1330. .error
  1331. .unwrap_or_else(|| "Failed to unban borrower".to_string())));
  1332. }
  1333. Ok(())
  1334. }
  1335. #[allow(dead_code)]
  1336. fn process_return(&self, api_client: &ApiClient) -> anyhow::Result<()> {
  1337. use crate::models::QueryRequest;
  1338. let loan_id = self
  1339. .return_loan_data
  1340. .as_ref()
  1341. .and_then(|l| l.get("id"))
  1342. .and_then(|v| v.as_i64())
  1343. .ok_or_else(|| anyhow::anyhow!("Invalid loan ID"))?;
  1344. let asset_id = self
  1345. .return_loan_data
  1346. .as_ref()
  1347. .and_then(|l| l.get("asset_id"))
  1348. .and_then(|v| v.as_i64())
  1349. .ok_or_else(|| anyhow::anyhow!("Invalid asset ID"))?;
  1350. let return_date = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
  1351. // Update lending_history to set return_date
  1352. let update_data = serde_json::json!({
  1353. "return_date": return_date
  1354. });
  1355. let request = QueryRequest {
  1356. action: "update".to_string(),
  1357. table: "lending_history".to_string(),
  1358. data: Some(update_data),
  1359. r#where: Some(serde_json::json!({"id": loan_id})),
  1360. columns: None,
  1361. joins: None,
  1362. order_by: None,
  1363. limit: None,
  1364. offset: None,
  1365. filter: None,
  1366. };
  1367. let response = api_client.query(&request)?;
  1368. if !response.success {
  1369. return Err(anyhow::anyhow!(response
  1370. .error
  1371. .unwrap_or_else(|| "Failed to update loan record".to_string())));
  1372. }
  1373. // Update asset status to "Available"
  1374. let asset_update = serde_json::json!({
  1375. "lending_status": "Available"
  1376. });
  1377. let asset_request = QueryRequest {
  1378. action: "update".to_string(),
  1379. table: "assets".to_string(),
  1380. data: Some(asset_update),
  1381. r#where: Some(serde_json::json!({"id": asset_id})),
  1382. columns: None,
  1383. joins: None,
  1384. order_by: None,
  1385. limit: None,
  1386. offset: None,
  1387. filter: None,
  1388. };
  1389. let asset_response = api_client.query(&asset_request)?;
  1390. if !asset_response.success {
  1391. return Err(anyhow::anyhow!(asset_response
  1392. .error
  1393. .unwrap_or_else(|| "Failed to update asset status".to_string())));
  1394. }
  1395. Ok(())
  1396. }
  1397. fn delete_borrower(&self, api_client: &ApiClient) -> anyhow::Result<()> {
  1398. use crate::models::QueryRequest;
  1399. let borrower_id = self
  1400. .delete_borrower_data
  1401. .as_ref()
  1402. .and_then(|b| b.get("borrower_id"))
  1403. .and_then(|v| v.as_i64())
  1404. .ok_or_else(|| anyhow::anyhow!("Invalid borrower ID"))?;
  1405. let request = QueryRequest {
  1406. action: "delete".to_string(),
  1407. table: "borrowers".to_string(),
  1408. data: None,
  1409. r#where: Some(serde_json::json!({"id": borrower_id})),
  1410. columns: None,
  1411. joins: None,
  1412. order_by: None,
  1413. limit: None,
  1414. offset: None,
  1415. filter: None,
  1416. };
  1417. let response = api_client.query(&request)?;
  1418. if !response.success {
  1419. return Err(anyhow::anyhow!(response
  1420. .error
  1421. .unwrap_or_else(|| "Failed to delete borrower".to_string())));
  1422. }
  1423. Ok(())
  1424. }
  1425. }