use anyhow::Result; use chrono::{Duration, Local}; use eframe::egui; use egui_phosphor::variants::regular as icons; use serde_json::Value; use crate::api::ApiClient; use crate::models::QueryRequest; #[derive(Debug, Clone, PartialEq)] pub enum BorrowStep { SelectAsset, SelectBorrower, SelectDuration, Confirm, } #[derive(Debug, Clone, PartialEq)] pub enum BorrowerSelection { None, Existing(Value), // Existing borrower data NewRegistration { // New borrower being registered name: String, department: String, // "class" in the UI borrower_type: String, // "role" in the UI phone: String, email: String, }, } pub struct BorrowFlow { // State pub is_open: bool, pub current_step: BorrowStep, // Step 1: Asset Selection pub scan_input: String, pub available_assets: Vec, pub selected_asset: Option, pub asset_search: String, pub asset_loading: bool, // Step 2: Borrower Selection pub borrower_selection: BorrowerSelection, pub registered_borrowers: Vec, pub banned_borrowers: Vec, pub borrower_search: String, pub borrower_loading: bool, // New borrower registration fields pub new_borrower_name: String, pub new_borrower_class: String, pub new_borrower_role: String, pub new_borrower_phone: String, pub new_borrower_email: String, // Step 3: Duration Selection pub selected_duration_days: Option, pub custom_due_date: String, // Step 4: Confirmation pub lending_notes: String, // Confirmation for lending risky items (Faulty/Attention) pub confirm_risky_asset: bool, // Error handling pub error_message: Option, pub success_message: Option, pub just_completed_successfully: bool, } impl Default for BorrowFlow { fn default() -> Self { Self { is_open: false, current_step: BorrowStep::SelectAsset, scan_input: String::new(), available_assets: Vec::new(), selected_asset: None, asset_search: String::new(), asset_loading: false, borrower_selection: BorrowerSelection::None, registered_borrowers: Vec::new(), banned_borrowers: Vec::new(), borrower_search: String::new(), borrower_loading: false, new_borrower_name: String::new(), new_borrower_class: String::new(), new_borrower_role: String::from("Student"), new_borrower_phone: String::new(), new_borrower_email: String::new(), selected_duration_days: None, custom_due_date: String::new(), lending_notes: String::new(), confirm_risky_asset: false, error_message: None, success_message: None, just_completed_successfully: false, } } } impl BorrowFlow { pub fn new() -> Self { Self::default() } pub fn open(&mut self, api_client: &ApiClient) { self.is_open = true; self.current_step = BorrowStep::SelectAsset; self.reset_fields(); self.just_completed_successfully = false; self.load_available_assets(api_client); } pub fn close(&mut self) { self.is_open = false; self.reset_fields(); } pub fn take_recent_success(&mut self) -> bool { if self.just_completed_successfully { self.just_completed_successfully = false; true } else { false } } fn reset_fields(&mut self) { self.scan_input.clear(); self.available_assets.clear(); self.selected_asset = None; self.asset_search.clear(); self.borrower_selection = BorrowerSelection::None; self.registered_borrowers.clear(); self.banned_borrowers.clear(); self.borrower_search.clear(); self.new_borrower_name.clear(); self.new_borrower_class.clear(); self.new_borrower_role = String::from("Student"); self.new_borrower_phone.clear(); self.new_borrower_email.clear(); self.selected_duration_days = None; self.custom_due_date.clear(); self.lending_notes.clear(); self.confirm_risky_asset = false; self.error_message = None; self.success_message = None; } pub fn show(&mut self, ctx: &egui::Context, api_client: &ApiClient) -> bool { let was_open = self.is_open; let mut keep_open = self.is_open; egui::Window::new("Borrow an Item") .id(egui::Id::new("borrow_flow_main_window")) .default_size(egui::vec2(1100.0, 800.0)) .resizable(true) .collapsible(false) .open(&mut keep_open) .show(ctx, |ui| { // Progress indicator self.show_progress_bar(ui); ui.separator(); // Show error/success messages if let Some(err) = &self.error_message { ui.colored_label(egui::Color32::RED, err); ui.separator(); } if let Some(msg) = &self.success_message { ui.colored_label(egui::Color32::GREEN, msg); ui.separator(); } // Main content area egui::ScrollArea::vertical() .id_salt("borrow_flow_main_scroll") .show(ui, |ui| match self.current_step { BorrowStep::SelectAsset => self.show_asset_selection(ui, api_client), BorrowStep::SelectBorrower => self.show_borrower_selection(ui, api_client), BorrowStep::SelectDuration => self.show_duration_selection(ui), BorrowStep::Confirm => self.show_confirmation(ui), }); ui.separator(); // Navigation buttons self.show_navigation_buttons(ui, api_client); }); if !self.is_open { keep_open = false; } self.is_open = keep_open; if !keep_open && was_open { self.close(); } keep_open } fn show_progress_bar(&self, ui: &mut egui::Ui) { ui.horizontal(|ui| { let step_index = match self.current_step { BorrowStep::SelectAsset => 0, BorrowStep::SelectBorrower => 1, BorrowStep::SelectDuration => 2, BorrowStep::Confirm => 3, }; let steps = [ (icons::PACKAGE, "Asset"), (icons::USER, "Borrower"), (icons::CLOCK, "Duration"), (icons::CHECK_CIRCLE, "Confirm"), ]; for (i, (icon, step_name)) in steps.iter().enumerate() { let color = if i == step_index { egui::Color32::from_rgb(100, 149, 237) } else if i < step_index { egui::Color32::from_rgb(60, 179, 113) } else { egui::Color32::GRAY }; ui.colored_label(color, format!("{} {}", icon, step_name)); if i < steps.len() - 1 { ui.add_space(5.0); ui.label(icons::CARET_RIGHT); ui.add_space(5.0); } } }); } fn show_asset_selection(&mut self, ui: &mut egui::Ui, api_client: &ApiClient) { ui.heading("What do you want to borrow?"); ui.add_space(10.0); // Scan field ui.horizontal(|ui| { ui.label("Scan or Enter Asset Tag/ID:"); let response = ui.add( egui::TextEdit::singleline(&mut self.scan_input) .id(egui::Id::new("borrow_flow_scan_input")) .hint_text("Scan barcode or type asset tag...") .desired_width(300.0), ); if response.changed() && !self.scan_input.is_empty() { self.try_scan_asset(api_client); } if ui.button("Clear").clicked() { self.scan_input.clear(); } }); ui.add_space(10.0); ui.separator(); ui.add_space(10.0); // Search bar ui.horizontal(|ui| { ui.label("Search:"); if ui .add( egui::TextEdit::singleline(&mut self.asset_search) .id(egui::Id::new("borrow_flow_asset_search")), ) .changed() { // Filter is applied in the table rendering } if ui.button("Refresh").clicked() { self.load_available_assets(api_client); } }); ui.add_space(5.0); // Assets table ui.label(egui::RichText::new("All Lendable Items").strong()); ui.allocate_ui_with_layout( egui::vec2(ui.available_width(), 300.0), egui::Layout::top_down(egui::Align::Min), |ui| { self.render_assets_table(ui); }, ); } fn show_borrower_selection(&mut self, ui: &mut egui::Ui, api_client: &ApiClient) { ui.heading("Who will borrow it?"); ui.add_space(10.0); // New borrower registration section egui::CollapsingHeader::new(egui::RichText::new("Register New Borrower").strong()) .id_salt("borrow_flow_new_borrower_header") .default_open(false) .show(ui, |ui| { ui.horizontal(|ui| { ui.label("Name:"); ui.add( egui::TextEdit::singleline(&mut self.new_borrower_name) .id(egui::Id::new("borrow_flow_new_borrower_name")), ); }); ui.horizontal(|ui| { ui.label("Class:"); ui.add( egui::TextEdit::singleline(&mut self.new_borrower_class) .id(egui::Id::new("borrow_flow_new_borrower_class")), ); }); ui.horizontal(|ui| { ui.label("Role:"); ui.add( egui::TextEdit::singleline(&mut self.new_borrower_role) .id(egui::Id::new("borrow_flow_new_borrower_role")) .hint_text("e.g. Student, Faculty, Staff, External"), ); }); ui.horizontal(|ui| { ui.label("Phone (optional):"); ui.add( egui::TextEdit::singleline(&mut self.new_borrower_phone) .id(egui::Id::new("borrow_flow_new_borrower_phone")), ); }); ui.horizontal(|ui| { ui.label("Email (optional):"); ui.add( egui::TextEdit::singleline(&mut self.new_borrower_email) .id(egui::Id::new("borrow_flow_new_borrower_email")), ); }); ui.add_space(5.0); if ui.button("Use This New Borrower").clicked() { if self.new_borrower_name.trim().is_empty() { self.error_message = Some("Name is required".to_string()); } else { self.borrower_selection = BorrowerSelection::NewRegistration { name: self.new_borrower_name.clone(), department: self.new_borrower_class.clone(), borrower_type: self.new_borrower_role.clone(), phone: self.new_borrower_phone.clone(), email: self.new_borrower_email.clone(), }; self.error_message = None; } } }); ui.add_space(10.0); ui.separator(); ui.add_space(10.0); // Banned borrowers warning section if !self.banned_borrowers.is_empty() { ui.colored_label( egui::Color32::RED, egui::RichText::new("WARNING: DO NOT LEND TO THESE BORROWERS!").strong(), ); ui.allocate_ui_with_layout( egui::vec2(ui.available_width(), 150.0), egui::Layout::top_down(egui::Align::Min), |ui| { self.render_banned_borrowers_table(ui); }, ); ui.add_space(10.0); ui.separator(); ui.add_space(10.0); } // Registered borrowers section ui.label(egui::RichText::new("Select Registered Borrower").strong()); ui.horizontal(|ui| { ui.label("Search:"); if ui .add( egui::TextEdit::singleline(&mut self.borrower_search) .id(egui::Id::new("borrow_flow_borrower_search")), ) .changed() { // Filter is applied in table rendering } if ui.button("Refresh").clicked() { self.load_borrowers(api_client); } }); ui.add_space(5.0); ui.allocate_ui_with_layout( egui::vec2(ui.available_width(), 300.0), egui::Layout::top_down(egui::Align::Min), |ui| { self.render_borrowers_table(ui); }, ); } fn show_duration_selection(&mut self, ui: &mut egui::Ui) { ui.heading("How long does the borrower need it?"); ui.add_space(10.0); ui.label(egui::RichText::new("Common Timeframes:").strong()); ui.add_space(5.0); // Common duration buttons in a grid egui::Grid::new("duration_grid") .num_columns(4) .spacing(egui::vec2(8.0, 8.0)) .show(ui, |ui| { for (days, label) in [(1, "1 Day"), (2, "2 Days"), (3, "3 Days"), (4, "4 Days")] { let selected = self.selected_duration_days == Some(days); if ui.selectable_label(selected, label).clicked() { self.selected_duration_days = Some(days); self.custom_due_date.clear(); } } ui.end_row(); for (days, label) in [(5, "5 Days"), (6, "6 Days"), (7, "1 Week"), (14, "2 Weeks")] { let selected = self.selected_duration_days == Some(days); if ui.selectable_label(selected, label).clicked() { self.selected_duration_days = Some(days); self.custom_due_date.clear(); } } ui.end_row(); }); ui.add_space(10.0); ui.separator(); ui.add_space(10.0); // Special option: Deploy (indefinite) - separate from time options ui.horizontal(|ui| { ui.label("Special:"); let selected = self.selected_duration_days == Some(0); let deploy_label = format!("{} Deploy (Indefinite)", icons::ROCKET_LAUNCH); if ui.selectable_label(selected, deploy_label).clicked() { self.selected_duration_days = Some(0); self.custom_due_date.clear(); } }); ui.add_space(10.0); ui.separator(); ui.add_space(10.0); ui.label("Or specify a custom date (YYYY-MM-DD):"); ui.horizontal(|ui| { ui.add( egui::TextEdit::singleline(&mut self.custom_due_date) .id(egui::Id::new("borrow_flow_custom_due_date")), ); if ui.button("Clear").clicked() { self.custom_due_date.clear(); self.selected_duration_days = None; } }); if !self.custom_due_date.is_empty() { self.selected_duration_days = None; } } fn show_confirmation(&mut self, ui: &mut egui::Ui) { ui.heading("Overview"); ui.add_space(10.0); // Summary box egui::Frame::group(ui.style()).show(ui, |ui| { ui.label(egui::RichText::new("You will authorize lending:").strong()); ui.add_space(5.0); // Asset info if let Some(asset) = &self.selected_asset { let tag = asset .get("asset_tag") .and_then(|v| v.as_str()) .unwrap_or("N/A"); let name = asset.get("name").and_then(|v| v.as_str()).unwrap_or("N/A"); ui.label(format!("Asset: {} - {}", tag, name)); } // Borrower info match &self.borrower_selection { BorrowerSelection::Existing(borrower) => { let name = borrower .get("name") .and_then(|v| v.as_str()) .unwrap_or("N/A"); let class = borrower .get("class_name") .and_then(|v| v.as_str()) .unwrap_or(""); ui.label(format!("Borrower: {} ({})", name, class)); } BorrowerSelection::NewRegistration { name, department, borrower_type, .. } => { ui.label(format!( "New Borrower: {} ({}) - {}", name, department, borrower_type )); } BorrowerSelection::None => { ui.colored_label(egui::Color32::RED, "WARNING: No borrower selected!"); } } // Duration info if let Some(days) = self.selected_duration_days { if days == 0 { ui.label(format!( "{} Deployed (Indefinite - No due date)", icons::ROCKET_LAUNCH )); } else { let due_date = Local::now() + Duration::days(days as i64); ui.label(format!( "Duration: {} days (Due: {})", days, due_date.format("%Y-%m-%d") )); } } else if !self.custom_due_date.is_empty() { ui.label(format!("Due Date: {}", self.custom_due_date)); } else { ui.colored_label(egui::Color32::RED, "WARNING: No duration selected!"); } }); ui.add_space(15.0); // Risk warning for Faulty/Attention assets if let Some(asset) = &self.selected_asset { let status = asset.get("status").and_then(|v| v.as_str()).unwrap_or(""); if status == "Faulty" || status == "Attention" { let (color, label) = if status == "Faulty" { ( egui::Color32::from_rgb(244, 67, 54), "This item is marked as Faulty and may be unsafe or unusable.", ) } else { ( egui::Color32::from_rgb(255, 193, 7), "This item has Attention status and may have minor defects.", ) }; egui::Frame::group(ui.style()).show(ui, |ui| { ui.colored_label(color, label); ui.add_space(6.0); ui.horizontal(|ui| { ui.checkbox( &mut self.confirm_risky_asset, "I acknowledge the issues and still wish to lend this item", ); }); }); ui.add_space(10.0); } } // Optional notes ui.label("Optional Lending Notes:"); ui.add( egui::TextEdit::multiline(&mut self.lending_notes) .id(egui::Id::new("borrow_flow_lending_notes")) .desired_width(f32::INFINITY) .desired_rows(4), ); } fn show_navigation_buttons(&mut self, ui: &mut egui::Ui, api_client: &ApiClient) { ui.horizontal(|ui| { // Back button if self.current_step != BorrowStep::SelectAsset { if ui.button(format!("{} Back", icons::ARROW_LEFT)).clicked() { self.go_back(); } } ui.add_space(10.0); // Cancel button if ui.button(format!("{} Cancel", icons::X)).clicked() { self.close(); } // Spacer ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { // Next/Approve button match self.current_step { BorrowStep::SelectAsset => { let enabled = self.selected_asset.is_some(); if ui .add_enabled( enabled, egui::Button::new(format!("{} Next", icons::ARROW_RIGHT)), ) .clicked() { self.go_to_borrower_selection(api_client); } } BorrowStep::SelectBorrower => { let enabled = self.borrower_selection != BorrowerSelection::None; if ui .add_enabled( enabled, egui::Button::new(format!("{} Next", icons::ARROW_RIGHT)), ) .clicked() { self.current_step = BorrowStep::SelectDuration; self.error_message = None; } } BorrowStep::SelectDuration => { let enabled = self.selected_duration_days.is_some() || !self.custom_due_date.is_empty(); if ui .add_enabled( enabled, egui::Button::new(format!("{} Next", icons::ARROW_RIGHT)), ) .clicked() { self.current_step = BorrowStep::Confirm; self.error_message = None; } } BorrowStep::Confirm => { // If asset is risky (Faulty/Attention), require explicit acknowledgment before enabling submit let mut risky_requires_ack = false; if let Some(asset) = &self.selected_asset { let status = asset.get("status").and_then(|v| v.as_str()).unwrap_or(""); if status == "Faulty" || status == "Attention" { risky_requires_ack = true; } } let can_submit = !risky_requires_ack || self.confirm_risky_asset; if ui .add_enabled( can_submit, egui::Button::new(format!( "{} Approve & Submit", icons::ARROW_LEFT )), ) .clicked() { self.submit_lending(api_client); } } } }); }); } fn go_back(&mut self) { self.error_message = None; self.current_step = match self.current_step { BorrowStep::SelectAsset => BorrowStep::SelectAsset, BorrowStep::SelectBorrower => BorrowStep::SelectAsset, BorrowStep::SelectDuration => BorrowStep::SelectBorrower, BorrowStep::Confirm => BorrowStep::SelectDuration, }; } fn go_to_borrower_selection(&mut self, api_client: &ApiClient) { self.current_step = BorrowStep::SelectBorrower; self.load_borrowers(api_client); self.error_message = None; } // Data loading methods fn load_available_assets(&mut self, api_client: &ApiClient) { self.asset_loading = true; self.error_message = None; let request = QueryRequest { action: "select".to_string(), table: "assets".to_string(), columns: Some(vec![ "assets.id".to_string(), "assets.asset_tag".to_string(), "assets.name".to_string(), "assets.category_id".to_string(), "assets.lending_status".to_string(), "categories.category_name".to_string(), ]), r#where: Some(serde_json::json!({ "assets.lendable": true, "assets.lending_status": "Available" })), data: None, filter: None, order_by: None, limit: None, offset: None, joins: Some(vec![crate::models::Join { table: "categories".to_string(), on: "assets.category_id = categories.id".to_string(), join_type: "LEFT".to_string(), }]), }; match api_client.query(&request) { Ok(response) => { if response.success { if let Some(data) = &response.data { if let Some(arr) = data.as_array() { self.available_assets = arr.clone(); } } } else { self.error_message = Some( response .error .unwrap_or_else(|| "Failed to load assets".to_string()), ); } } Err(e) => { self.error_message = Some(format!("Error loading assets: {}", e)); } } self.asset_loading = false; } fn try_scan_asset(&mut self, api_client: &ApiClient) { let scan_value = self.scan_input.trim(); if scan_value.is_empty() { return; } // Try to find by asset_tag or id let request = QueryRequest { action: "select".to_string(), table: "assets".to_string(), columns: Some(vec![ "assets.id".to_string(), "assets.asset_tag".to_string(), "assets.name".to_string(), "assets.category_id".to_string(), "assets.lending_status".to_string(), "assets.lendable".to_string(), "categories.category_name".to_string(), ]), r#where: Some(serde_json::json!({ "assets.asset_tag": scan_value })), data: None, filter: None, order_by: None, limit: None, offset: None, joins: Some(vec![crate::models::Join { table: "categories".to_string(), on: "assets.category_id = categories.id".to_string(), join_type: "LEFT".to_string(), }]), }; match api_client.query(&request) { Ok(response) => { if response.success { if let Some(data) = &response.data { if let Some(arr) = data.as_array() { if let Some(asset) = arr.first() { // Verify it's lendable and available let lendable = asset .get("lendable") .and_then(|v| v.as_bool()) .unwrap_or(false); let status = asset .get("lending_status") .and_then(|v| v.as_str()) .unwrap_or(""); if lendable && status == "Available" { self.selected_asset = Some(asset.clone()); self.error_message = None; } else { self.error_message = Some(format!( "Asset '{}' is not available for lending", scan_value )); } } else { self.error_message = Some(format!("Asset '{}' not found", scan_value)); } } } } else { self.error_message = Some(response.error.unwrap_or_else(|| "Scan failed".to_string())); } } Err(e) => { self.error_message = Some(format!("Scan error: {}", e)); } } } fn load_borrowers(&mut self, api_client: &ApiClient) { self.borrower_loading = true; self.error_message = None; // Load registered (non-banned) borrowers let request = QueryRequest { action: "select".to_string(), table: "borrowers".to_string(), columns: Some(vec![ "id".to_string(), "name".to_string(), "email".to_string(), "phone_number".to_string(), "role".to_string(), "class_name".to_string(), "banned".to_string(), ]), r#where: Some(serde_json::json!({ "banned": false })), data: None, filter: None, order_by: None, limit: None, offset: None, joins: None, }; match api_client.query(&request) { Ok(response) => { if response.success { if let Some(data) = &response.data { if let Some(arr) = data.as_array() { self.registered_borrowers = arr.clone(); } } } } Err(e) => { self.error_message = Some(format!("Error loading borrowers: {}", e)); } } // Load banned borrowers let banned_request = QueryRequest { action: "select".to_string(), table: "borrowers".to_string(), columns: Some(vec![ "id".to_string(), "name".to_string(), "class_name".to_string(), "unban_fine".to_string(), ]), r#where: Some(serde_json::json!({ "banned": true })), data: None, filter: None, order_by: None, limit: None, offset: None, joins: None, }; match api_client.query(&banned_request) { Ok(response) => { if response.success { if let Some(data) = &response.data { if let Some(arr) = data.as_array() { self.banned_borrowers = arr.clone(); } } } } Err(_) => { // Don't overwrite error message if we already have one } } self.borrower_loading = false; } // Table rendering methods fn render_assets_table(&mut self, ui: &mut egui::Ui) { use egui_extras::{Column, TableBuilder}; // Filter assets based on search let filtered_assets: Vec<&Value> = self .available_assets .iter() .filter(|asset| { if self.asset_search.is_empty() { return true; } let search_lower = self.asset_search.to_lowercase(); let tag = asset .get("asset_tag") .and_then(|v| v.as_str()) .unwrap_or(""); let name = asset.get("name").and_then(|v| v.as_str()).unwrap_or(""); let category = asset .get("category_name") .and_then(|v| v.as_str()) .unwrap_or(""); tag.to_lowercase().contains(&search_lower) || name.to_lowercase().contains(&search_lower) || category.to_lowercase().contains(&search_lower) }) .collect(); TableBuilder::new(ui) .id_salt("borrow_flow_assets_table") .striped(true) .resizable(true) .sense(egui::Sense::click()) .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) .column(Column::auto().resizable(true)) .column(Column::remainder().resizable(true)) .column(Column::auto().resizable(true)) .column(Column::auto().resizable(true)) .min_scrolled_height(0.0) .max_scroll_height(300.0) .header(22.0, |mut header| { header.col(|ui| { ui.strong("Asset Tag"); }); header.col(|ui| { ui.strong("Name"); }); header.col(|ui| { ui.strong("Category"); }); header.col(|ui| { ui.strong("Action"); }); }) .body(|mut body| { for asset in filtered_assets { body.row(20.0, |mut row| { let asset_id = asset.get("id").and_then(|v| v.as_i64()).unwrap_or(0); let is_selected = self .selected_asset .as_ref() .and_then(|s| s.get("id").and_then(|v| v.as_i64())) .map(|id| id == asset_id) .unwrap_or(false); row.col(|ui| { ui.label( asset .get("asset_tag") .and_then(|v| v.as_str()) .unwrap_or("N/A"), ); }); row.col(|ui| { ui.label(asset.get("name").and_then(|v| v.as_str()).unwrap_or("N/A")); }); row.col(|ui| { ui.label( asset .get("category_name") .and_then(|v| v.as_str()) .unwrap_or("N/A"), ); }); row.col(|ui| { if is_selected { ui.colored_label(egui::Color32::GREEN, "Selected"); } else { let button_id = format!("select_asset_{}", asset_id); if ui.button("Select").on_hover_text(&button_id).clicked() { self.selected_asset = Some((*asset).clone()); } } }); }); } }); } fn render_borrowers_table(&mut self, ui: &mut egui::Ui) { use egui_extras::{Column, TableBuilder}; // Filter borrowers based on search let filtered_borrowers: Vec<&Value> = self .registered_borrowers .iter() .filter(|borrower| { if self.borrower_search.is_empty() { return true; } let search_lower = self.borrower_search.to_lowercase(); let name = borrower.get("name").and_then(|v| v.as_str()).unwrap_or(""); let class = borrower .get("class_name") .and_then(|v| v.as_str()) .unwrap_or(""); let role = borrower.get("role").and_then(|v| v.as_str()).unwrap_or(""); name.to_lowercase().contains(&search_lower) || class.to_lowercase().contains(&search_lower) || role.to_lowercase().contains(&search_lower) }) .collect(); TableBuilder::new(ui) .id_salt("borrow_flow_borrowers_table") .striped(true) .resizable(true) .sense(egui::Sense::click()) .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) .column(Column::auto().resizable(true)) .column(Column::auto().resizable(true)) .column(Column::auto().resizable(true)) .column(Column::remainder().resizable(true)) .column(Column::auto().resizable(true)) .column(Column::auto().resizable(true)) .min_scrolled_height(0.0) .max_scroll_height(300.0) .header(22.0, |mut header| { header.col(|ui| { ui.strong("Name"); }); header.col(|ui| { ui.strong("Class"); }); header.col(|ui| { ui.strong("Role"); }); header.col(|ui| { ui.strong("Email"); }); header.col(|ui| { ui.strong("Phone"); }); header.col(|ui| { ui.strong("Action"); }); }) .body(|mut body| { for borrower in filtered_borrowers { body.row(20.0, |mut row| { let borrower_id = borrower.get("id").and_then(|v| v.as_i64()).unwrap_or(0); let is_selected = match &self.borrower_selection { BorrowerSelection::Existing(b) => b .get("id") .and_then(|v| v.as_i64()) .map(|id| id == borrower_id) .unwrap_or(false), _ => false, }; row.col(|ui| { ui.label( borrower .get("name") .and_then(|v| v.as_str()) .unwrap_or("N/A"), ); }); row.col(|ui| { ui.label( borrower .get("class_name") .and_then(|v| v.as_str()) .unwrap_or("N/A"), ); }); row.col(|ui| { ui.label( borrower .get("role") .and_then(|v| v.as_str()) .unwrap_or("N/A"), ); }); row.col(|ui| { ui.label( borrower .get("email") .and_then(|v| v.as_str()) .unwrap_or("N/A"), ); }); row.col(|ui| { ui.label( borrower .get("phone_number") .and_then(|v| v.as_str()) .unwrap_or("N/A"), ); }); row.col(|ui| { if is_selected { ui.colored_label(egui::Color32::GREEN, "Selected"); } else { let button_id = format!("select_borrower_{}", borrower_id); if ui.button("Select").on_hover_text(&button_id).clicked() { self.borrower_selection = BorrowerSelection::Existing((*borrower).clone()); } } }); }); } }); } fn render_banned_borrowers_table(&self, ui: &mut egui::Ui) { use egui_extras::{Column, TableBuilder}; TableBuilder::new(ui) .id_salt("borrow_flow_banned_borrowers_table") .striped(true) .resizable(true) .sense(egui::Sense::click()) .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) .column(Column::auto().resizable(true)) .column(Column::auto().resizable(true)) .column(Column::remainder().resizable(true)) .min_scrolled_height(0.0) .max_scroll_height(150.0) .header(22.0, |mut header| { header.col(|ui| { ui.strong("Name"); }); header.col(|ui| { ui.strong("Class/Dept"); }); header.col(|ui| { ui.strong("Unban Fine"); }); }) .body(|mut body| { for borrower in &self.banned_borrowers { body.row(20.0, |mut row| { row.col(|ui| { ui.colored_label( egui::Color32::RED, borrower .get("name") .and_then(|v| v.as_str()) .unwrap_or("N/A"), ); }); row.col(|ui| { ui.label( borrower .get("class_name") .and_then(|v| v.as_str()) .unwrap_or("N/A"), ); }); row.col(|ui| { ui.label( borrower .get("unban_fine") .and_then(|v| v.as_f64()) .map(|f| format!("${:.2}", f)) .unwrap_or("N/A".to_string()), ); }); }); } }); } // Submission method fn submit_lending(&mut self, api_client: &ApiClient) { self.error_message = None; self.success_message = None; // Validate all required data let asset = match &self.selected_asset { Some(a) => a, None => { self.error_message = Some("No asset selected".to_string()); return; } }; let asset_id = match asset.get("id").and_then(|v| v.as_i64()) { Some(id) => id, None => { self.error_message = Some("Invalid asset ID".to_string()); return; } }; // Calculate due date (0 days = deployment/indefinite, no due date) let due_date_str = if let Some(days) = self.selected_duration_days { if days == 0 { // Deployment mode: no due date String::new() } else { let due = Local::now() + Duration::days(days as i64); due.format("%Y-%m-%d").to_string() } } else if !self.custom_due_date.is_empty() { self.custom_due_date.clone() } else { self.error_message = Some("No duration selected".to_string()); return; }; // Handle borrower (either create new or use existing) let borrower_id = match &self.borrower_selection { BorrowerSelection::Existing(borrower) => { match borrower.get("id").and_then(|v| v.as_i64()) { Some(id) => id, None => { self.error_message = Some("Invalid borrower ID".to_string()); return; } } } BorrowerSelection::NewRegistration { name, department, borrower_type, phone, email, } => { // First register the new borrower match self.register_new_borrower( api_client, name, department, borrower_type, phone, email, ) { Ok(id) => id, Err(e) => { self.error_message = Some(format!("Failed to register borrower: {}", e)); return; } } } BorrowerSelection::None => { self.error_message = Some("No borrower selected".to_string()); return; } }; // Create lending history record let checkout_date = Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); let mut lending_data = serde_json::json!({ "asset_id": asset_id, "borrower_id": borrower_id, "checkout_date": checkout_date }); // Only set due_date if not deployment mode if !due_date_str.is_empty() { lending_data["due_date"] = serde_json::Value::String(due_date_str.clone()); } if !self.lending_notes.is_empty() { lending_data["notes"] = serde_json::Value::String(self.lending_notes.clone()); } let lending_request = QueryRequest { action: "insert".to_string(), table: "lending_history".to_string(), columns: None, r#where: None, data: Some(lending_data), filter: None, order_by: None, limit: None, offset: None, joins: None, }; match api_client.query(&lending_request) { Ok(response) => { if !response.success { self.error_message = Some( response .error .unwrap_or_else(|| "Failed to create lending record".to_string()), ); return; } } Err(e) => { self.error_message = Some(format!("Error creating lending record: {}", e)); return; } } // Update asset status to "Borrowed" or "Deployed" based on duration let lending_status = if self.selected_duration_days == Some(0) { "Deployed" } else { "Borrowed" }; let mut asset_update_data = serde_json::json!({ "lending_status": lending_status, "current_borrower_id": borrower_id }); if !due_date_str.is_empty() { asset_update_data["due_date"] = serde_json::Value::String(due_date_str.clone()); } let asset_update = QueryRequest { action: "update".to_string(), table: "assets".to_string(), columns: None, r#where: Some(serde_json::json!({ "id": asset_id })), data: Some(asset_update_data), filter: None, order_by: None, limit: None, offset: None, joins: None, }; match api_client.query(&asset_update) { Ok(response) => { if response.success { self.just_completed_successfully = true; self.success_message = Some("Item successfully lent!".to_string()); // Auto-close after a brief success message // In a real app, you might want to add a delay here self.close(); } else { self.error_message = Some( response .error .unwrap_or_else(|| "Failed to update asset status".to_string()), ); } } Err(e) => { self.error_message = Some(format!("Error updating asset: {}", e)); } } } fn register_new_borrower( &self, api_client: &ApiClient, name: &str, department: &str, borrower_type: &str, phone: &str, email: &str, ) -> Result { let mut borrower_data = serde_json::json!({ "name": name, "role": borrower_type, "class_name": department, }); if !phone.is_empty() { borrower_data["phone_number"] = serde_json::Value::String(phone.to_string()); } if !email.is_empty() { borrower_data["email"] = serde_json::Value::String(email.to_string()); } let request = QueryRequest { action: "insert".to_string(), table: "borrowers".to_string(), columns: None, r#where: None, data: Some(borrower_data), filter: None, order_by: None, limit: None, offset: None, joins: None, }; let response = api_client.query(&request)?; if !response.success { return Err(anyhow::anyhow!(response .error .unwrap_or_else(|| "Failed to register borrower".to_string()))); } // Get the newly created borrower ID from response if let Some(data) = &response.data { if let Some(id) = data.get("id").and_then(|v| v.as_i64()) { Ok(id) } else if let Some(id) = data.get("inserted_id").and_then(|v| v.as_i64()) { Ok(id) } else { Err(anyhow::anyhow!( "Failed to get new borrower ID from response" )) } } else { Err(anyhow::anyhow!( "No data returned from borrower registration" )) } } }