|
@@ -3,6 +3,7 @@ use chrono::{Duration, Local};
|
|
|
use eframe::egui;
|
|
use eframe::egui;
|
|
|
use egui_phosphor::variants::regular as icons;
|
|
use egui_phosphor::variants::regular as icons;
|
|
|
use serde_json::Value;
|
|
use serde_json::Value;
|
|
|
|
|
+use crate::core::table_renderer::{TableRenderer, ColumnConfig, SelectionMode};
|
|
|
|
|
|
|
|
use crate::api::ApiClient;
|
|
use crate::api::ApiClient;
|
|
|
use crate::models::QueryRequest;
|
|
use crate::models::QueryRequest;
|
|
@@ -41,6 +42,9 @@ pub struct BorrowFlow {
|
|
|
pub asset_search: String,
|
|
pub asset_search: String,
|
|
|
pub asset_loading: bool,
|
|
pub asset_loading: bool,
|
|
|
|
|
|
|
|
|
|
+ // Shared table renderer for assets (kiosk-friendly, consistent with inventory view)
|
|
|
|
|
+ pub assets_table: TableRenderer,
|
|
|
|
|
+
|
|
|
// Step 2: Borrower Selection
|
|
// Step 2: Borrower Selection
|
|
|
pub borrower_selection: BorrowerSelection,
|
|
pub borrower_selection: BorrowerSelection,
|
|
|
pub registered_borrowers: Vec<Value>,
|
|
pub registered_borrowers: Vec<Value>,
|
|
@@ -48,6 +52,9 @@ pub struct BorrowFlow {
|
|
|
pub borrower_search: String,
|
|
pub borrower_search: String,
|
|
|
pub borrower_loading: bool,
|
|
pub borrower_loading: bool,
|
|
|
|
|
|
|
|
|
|
+ // Shared table renderer for borrowers
|
|
|
|
|
+ pub borrowers_table: TableRenderer,
|
|
|
|
|
+
|
|
|
// New borrower registration fields
|
|
// New borrower registration fields
|
|
|
pub new_borrower_name: String,
|
|
pub new_borrower_name: String,
|
|
|
pub new_borrower_class: String,
|
|
pub new_borrower_class: String,
|
|
@@ -73,6 +80,26 @@ pub struct BorrowFlow {
|
|
|
|
|
|
|
|
impl Default for BorrowFlow {
|
|
impl Default for BorrowFlow {
|
|
|
fn default() -> Self {
|
|
fn default() -> Self {
|
|
|
|
|
+ let assets_table = TableRenderer::new()
|
|
|
|
|
+ .with_columns(vec![
|
|
|
|
|
+ ColumnConfig::new("Asset Tag", "asset_tag").with_width(140.0),
|
|
|
|
|
+ ColumnConfig::new("Name", "name").with_width(240.0),
|
|
|
|
|
+ ColumnConfig::new("Category", "category_name").with_width(160.0),
|
|
|
|
|
+ ])
|
|
|
|
|
+ .with_default_sort("name", true)
|
|
|
|
|
+ .with_selection_mode(SelectionMode::Single);
|
|
|
|
|
+
|
|
|
|
|
+ let borrowers_table = TableRenderer::new()
|
|
|
|
|
+ .with_columns(vec![
|
|
|
|
|
+ ColumnConfig::new("Name", "name").with_width(200.0),
|
|
|
|
|
+ ColumnConfig::new("Class", "class_name").with_width(160.0),
|
|
|
|
|
+ ColumnConfig::new("Role", "role").with_width(140.0),
|
|
|
|
|
+ ColumnConfig::new("Email", "email").with_width(240.0),
|
|
|
|
|
+ ColumnConfig::new("Phone", "phone_number").with_width(160.0),
|
|
|
|
|
+ ])
|
|
|
|
|
+ .with_default_sort("name", true)
|
|
|
|
|
+ .with_selection_mode(SelectionMode::Single);
|
|
|
|
|
+
|
|
|
Self {
|
|
Self {
|
|
|
is_open: false,
|
|
is_open: false,
|
|
|
current_step: BorrowStep::SelectAsset,
|
|
current_step: BorrowStep::SelectAsset,
|
|
@@ -82,12 +109,14 @@ impl Default for BorrowFlow {
|
|
|
selected_asset: None,
|
|
selected_asset: None,
|
|
|
asset_search: String::new(),
|
|
asset_search: String::new(),
|
|
|
asset_loading: false,
|
|
asset_loading: false,
|
|
|
|
|
+ assets_table,
|
|
|
|
|
|
|
|
borrower_selection: BorrowerSelection::None,
|
|
borrower_selection: BorrowerSelection::None,
|
|
|
registered_borrowers: Vec::new(),
|
|
registered_borrowers: Vec::new(),
|
|
|
banned_borrowers: Vec::new(),
|
|
banned_borrowers: Vec::new(),
|
|
|
borrower_search: String::new(),
|
|
borrower_search: String::new(),
|
|
|
borrower_loading: false,
|
|
borrower_loading: false,
|
|
|
|
|
+ borrowers_table,
|
|
|
|
|
|
|
|
new_borrower_name: String::new(),
|
|
new_borrower_name: String::new(),
|
|
|
new_borrower_class: String::new(),
|
|
new_borrower_class: String::new(),
|
|
@@ -172,35 +201,7 @@ impl BorrowFlow {
|
|
|
.collapsible(false)
|
|
.collapsible(false)
|
|
|
.open(&mut keep_open)
|
|
.open(&mut keep_open)
|
|
|
.show(ctx, |ui| {
|
|
.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);
|
|
|
|
|
|
|
+ self.show_content(ui, api_client);
|
|
|
});
|
|
});
|
|
|
if !self.is_open {
|
|
if !self.is_open {
|
|
|
keep_open = false;
|
|
keep_open = false;
|
|
@@ -215,6 +216,51 @@ impl BorrowFlow {
|
|
|
keep_open
|
|
keep_open
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ pub fn show_content(&mut self, ui: &mut egui::Ui, api_client: &ApiClient) {
|
|
|
|
|
+ // 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();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Reserve space for footer; scroll main content within a fixed area
|
|
|
|
|
+ let footer_reserved_height: f32 = 100.0;
|
|
|
|
|
+ let content_height = (ui.available_height() - footer_reserved_height).max(200.0);
|
|
|
|
|
+
|
|
|
|
|
+ ui.allocate_ui_with_layout(
|
|
|
|
|
+ egui::vec2(ui.available_width(), content_height),
|
|
|
|
|
+ egui::Layout::top_down(egui::Align::Min),
|
|
|
|
|
+ |ui| {
|
|
|
|
|
+ egui::ScrollArea::vertical()
|
|
|
|
|
+ .id_salt("borrow_flow_main_scroll")
|
|
|
|
|
+ .max_height(content_height)
|
|
|
|
|
+ .auto_shrink([false; 2])
|
|
|
|
|
+ .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);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
fn show_progress_bar(&self, ui: &mut egui::Ui) {
|
|
fn show_progress_bar(&self, ui: &mut egui::Ui) {
|
|
|
ui.horizontal(|ui| {
|
|
ui.horizontal(|ui| {
|
|
|
let step_index = match self.current_step {
|
|
let step_index = match self.current_step {
|
|
@@ -253,41 +299,55 @@ impl BorrowFlow {
|
|
|
ui.heading("What do you want to borrow?");
|
|
ui.heading("What do you want to borrow?");
|
|
|
ui.add_space(10.0);
|
|
ui.add_space(10.0);
|
|
|
|
|
|
|
|
- // Scan field
|
|
|
|
|
|
|
+ // Unified scan/search input
|
|
|
ui.horizontal(|ui| {
|
|
ui.horizontal(|ui| {
|
|
|
- ui.label("Scan or Enter Asset Tag/ID:");
|
|
|
|
|
|
|
+ ui.label("Scan or Search (Tag/ID):");
|
|
|
|
|
+ let input_id = egui::Id::new("borrow_flow_scan_search_input");
|
|
|
let response = ui.add(
|
|
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),
|
|
|
|
|
|
|
+ egui::TextEdit::singleline(&mut self.asset_search)
|
|
|
|
|
+ .id(input_id)
|
|
|
|
|
+ .hint_text("...")
|
|
|
|
|
+ .desired_width(360.0),
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- if response.changed() && !self.scan_input.is_empty() {
|
|
|
|
|
- self.try_scan_asset(api_client);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if ui.button("Clear").clicked() {
|
|
|
|
|
- self.scan_input.clear();
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ // Focus by default
|
|
|
|
|
+ response.request_focus();
|
|
|
|
|
+
|
|
|
|
|
+ // On Enter: if exactly one unique match by tag or numeric id, select and advance
|
|
|
|
|
+ if response.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
|
|
|
|
+ let needle = self.asset_search.trim();
|
|
|
|
|
+ if !needle.is_empty() {
|
|
|
|
|
+ let mut matches: Vec<&Value> = Vec::new();
|
|
|
|
|
+ let parsed_num = needle.parse::<i64>().ok();
|
|
|
|
|
+ for asset in &self.available_assets {
|
|
|
|
|
+ let tag = asset
|
|
|
|
|
+ .get("asset_tag")
|
|
|
|
|
+ .and_then(|v| v.as_str())
|
|
|
|
|
+ .unwrap_or("");
|
|
|
|
|
+ let num = asset.get("asset_numeric_id").and_then(|v| v.as_i64());
|
|
|
|
|
+ if tag.eq_ignore_ascii_case(needle)
|
|
|
|
|
+ || (parsed_num.is_some() && num == parsed_num)
|
|
|
|
|
+ {
|
|
|
|
|
+ matches.push(asset);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- ui.add_space(10.0);
|
|
|
|
|
- ui.separator();
|
|
|
|
|
- ui.add_space(10.0);
|
|
|
|
|
|
|
+ // Deduplicate by id just in case
|
|
|
|
|
+ let mut unique: Vec<&Value> = Vec::new();
|
|
|
|
|
+ for m in matches {
|
|
|
|
|
+ let mid = m.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
|
|
|
|
|
+ if !unique.iter().any(|u| u.get("id").and_then(|v| v.as_i64()).unwrap_or(-2) == mid) {
|
|
|
|
|
+ unique.push(m);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 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 unique.len() == 1 {
|
|
|
|
|
+ self.selected_asset = Some(unique[0].clone());
|
|
|
|
|
+ self.go_to_borrower_selection(api_client);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
if ui.button("Refresh").clicked() {
|
|
if ui.button("Refresh").clicked() {
|
|
|
self.load_available_assets(api_client);
|
|
self.load_available_assets(api_client);
|
|
|
}
|
|
}
|
|
@@ -295,10 +355,10 @@ impl BorrowFlow {
|
|
|
|
|
|
|
|
ui.add_space(5.0);
|
|
ui.add_space(5.0);
|
|
|
|
|
|
|
|
- // Assets table
|
|
|
|
|
|
|
+ // Assets table (inventory-style renderer)
|
|
|
ui.label(egui::RichText::new("All Lendable Items").strong());
|
|
ui.label(egui::RichText::new("All Lendable Items").strong());
|
|
|
ui.allocate_ui_with_layout(
|
|
ui.allocate_ui_with_layout(
|
|
|
- egui::vec2(ui.available_width(), 300.0),
|
|
|
|
|
|
|
+ egui::vec2(ui.available_width(), 320.0),
|
|
|
egui::Layout::top_down(egui::Align::Min),
|
|
egui::Layout::top_down(egui::Align::Min),
|
|
|
|ui| {
|
|
|ui| {
|
|
|
self.render_assets_table(ui);
|
|
self.render_assets_table(ui);
|
|
@@ -678,8 +738,8 @@ impl BorrowFlow {
|
|
|
.add_enabled(
|
|
.add_enabled(
|
|
|
can_submit,
|
|
can_submit,
|
|
|
egui::Button::new(format!(
|
|
egui::Button::new(format!(
|
|
|
- "{} Approve & Submit",
|
|
|
|
|
- icons::ARROW_LEFT
|
|
|
|
|
|
|
+ "{} Process Checkout",
|
|
|
|
|
+ icons::CHECK_CIRCLE
|
|
|
)),
|
|
)),
|
|
|
)
|
|
)
|
|
|
.clicked()
|
|
.clicked()
|
|
@@ -719,6 +779,7 @@ impl BorrowFlow {
|
|
|
columns: Some(vec![
|
|
columns: Some(vec![
|
|
|
"assets.id".to_string(),
|
|
"assets.id".to_string(),
|
|
|
"assets.asset_tag".to_string(),
|
|
"assets.asset_tag".to_string(),
|
|
|
|
|
+ "assets.asset_numeric_id".to_string(),
|
|
|
"assets.name".to_string(),
|
|
"assets.name".to_string(),
|
|
|
"assets.category_id".to_string(),
|
|
"assets.category_id".to_string(),
|
|
|
"assets.lending_status".to_string(),
|
|
"assets.lending_status".to_string(),
|
|
@@ -924,230 +985,39 @@ impl BorrowFlow {
|
|
|
|
|
|
|
|
// Table rendering methods
|
|
// Table rendering methods
|
|
|
fn render_assets_table(&mut self, ui: &mut egui::Ui) {
|
|
fn render_assets_table(&mut self, ui: &mut egui::Ui) {
|
|
|
- use egui_extras::{Column, TableBuilder};
|
|
|
|
|
|
|
+ // Sync search query from the workflow input
|
|
|
|
|
+ self.assets_table.set_search_query(self.asset_search.clone());
|
|
|
|
|
|
|
|
- // 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);
|
|
|
|
|
|
|
+ // Prepare and render using shared renderer
|
|
|
|
|
+ let prepared = self.assets_table.prepare_json_data(&self.available_assets);
|
|
|
|
|
+ self.assets_table.render_json_table(ui, &prepared, None);
|
|
|
|
|
|
|
|
- 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());
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ // Map selection back to self.selected_asset (single-select semantics)
|
|
|
|
|
+ let sel = self.assets_table.selection.get_selected_indices();
|
|
|
|
|
+ if sel.is_empty() {
|
|
|
|
|
+ self.selected_asset = None;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ let idx = sel[0].min(prepared.len().saturating_sub(1));
|
|
|
|
|
+ self.selected_asset = Some(prepared[idx].1.clone());
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fn render_borrowers_table(&mut self, ui: &mut egui::Ui) {
|
|
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("");
|
|
|
|
|
|
|
+ // Sync search query
|
|
|
|
|
+ self.borrowers_table.set_search_query(self.borrower_search.clone());
|
|
|
|
|
|
|
|
- name.to_lowercase().contains(&search_lower)
|
|
|
|
|
- || class.to_lowercase().contains(&search_lower)
|
|
|
|
|
- || role.to_lowercase().contains(&search_lower)
|
|
|
|
|
- })
|
|
|
|
|
- .collect();
|
|
|
|
|
|
|
+ // Prepare and render
|
|
|
|
|
+ let prepared = self.borrowers_table.prepare_json_data(&self.registered_borrowers);
|
|
|
|
|
+ self.borrowers_table.render_json_table(ui, &prepared, None);
|
|
|
|
|
|
|
|
- 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());
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ // Map selection
|
|
|
|
|
+ let sel = self.borrowers_table.selection.get_selected_indices();
|
|
|
|
|
+ if sel.is_empty() {
|
|
|
|
|
+ self.borrower_selection = BorrowerSelection::None;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ let idx = sel[0].min(prepared.len().saturating_sub(1));
|
|
|
|
|
+ self.borrower_selection = BorrowerSelection::Existing(prepared[idx].1.clone());
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fn render_banned_borrowers_table(&self, ui: &mut egui::Ui) {
|
|
fn render_banned_borrowers_table(&self, ui: &mut egui::Ui) {
|