| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618 |
- use eframe::egui;
- use crate::api::ApiClient;
- use crate::core::components::form_builder::FormBuilder;
- use crate::core::tables::{get_all_loans, get_borrowers_summary};
- use crate::core::workflows::borrow_flow::BorrowFlow;
- use crate::core::workflows::return_flow::ReturnFlow;
- use crate::core::{ColumnConfig, TableRenderer};
- use crate::core::{EditorField, FieldType};
- pub struct BorrowingView {
- // data
- loans: Vec<serde_json::Value>,
- borrowers: Vec<serde_json::Value>,
- is_loading: bool,
- last_error: Option<String>,
- // UI
- init_loaded: bool,
- show_loans_column_selector: bool,
- show_borrowers_column_selector: bool,
- // Table renderers
- loans_table: TableRenderer,
- borrowers_table: TableRenderer,
- // Workflows
- borrow_flow: BorrowFlow,
- return_flow: ReturnFlow,
- // Register borrower dialog
- show_register_dialog: bool,
- new_borrower_name: String,
- new_borrower_email: String,
- new_borrower_phone: String,
- new_borrower_class: String,
- new_borrower_role: String,
- register_error: Option<String>,
- // Edit borrower dialog (using FormBuilder)
- borrower_editor: FormBuilder,
- // Ban/Unban borrower dialog
- show_ban_dialog: bool,
- show_unban_dialog: bool,
- ban_borrower_data: Option<serde_json::Value>,
- ban_fine_amount: String,
- ban_reason: String,
- // Return item confirm dialog
- show_return_confirm_dialog: bool,
- return_loan_data: Option<serde_json::Value>,
- // Delete borrower confirm dialog
- show_delete_borrower_dialog: bool,
- delete_borrower_data: Option<serde_json::Value>,
- // Search and filtering
- loans_search: String,
- borrowers_search: String,
- // Navigation
- pub switch_to_inventory_with_borrower: Option<i64>, // borrower_id to filter by
- }
- impl BorrowingView {
- pub fn new() -> Self {
- // Define columns for loans table - ALL columns from the query
- let loans_columns = vec![
- ColumnConfig::new("Loan ID", "id").with_width(60.0).hidden(),
- ColumnConfig::new("Asset ID", "asset_id")
- .with_width(70.0)
- .hidden(),
- ColumnConfig::new("Borrower ID", "borrower_id")
- .with_width(80.0)
- .hidden(),
- ColumnConfig::new("Tag", "asset_tag").with_width(80.0),
- ColumnConfig::new("Name", "name").with_width(200.0),
- ColumnConfig::new("Borrower", "borrower_name").with_width(120.0),
- ColumnConfig::new("Class", "class_name").with_width(80.0),
- ColumnConfig::new("Status", "lending_status").with_width(80.0),
- ColumnConfig::new("Checked Out", "checkout_date").with_width(100.0),
- ColumnConfig::new("Due", "due_date").with_width(90.0),
- ColumnConfig::new("Returned", "return_date").with_width(100.0),
- ColumnConfig::new("Notes", "notes")
- .with_width(150.0)
- .hidden(),
- ];
- // Define columns for borrowers table - with all backend fields
- let borrowers_columns = vec![
- ColumnConfig::new("ID", "borrower_id").with_width(60.0),
- ColumnConfig::new("Name", "borrower_name").with_width(150.0),
- ColumnConfig::new("Email", "email")
- .with_width(150.0)
- .hidden(),
- ColumnConfig::new("Phone", "phone_number")
- .with_width(120.0)
- .hidden(),
- ColumnConfig::new("Class", "class_name").with_width(80.0),
- ColumnConfig::new("Role", "role").with_width(80.0).hidden(),
- ColumnConfig::new("Active", "active_loans").with_width(60.0),
- ColumnConfig::new("Overdue", "overdue_loans").with_width(60.0),
- ColumnConfig::new("Banned", "banned").with_width(60.0),
- ];
- Self {
- loans: vec![],
- borrowers: vec![],
- is_loading: false,
- last_error: None,
- init_loaded: false,
- show_loans_column_selector: false,
- show_borrowers_column_selector: false,
- loans_table: TableRenderer::new()
- .with_columns(loans_columns)
- .with_default_sort("checkout_date", false), // Sort by checkout date DESC (most recent first)
- borrowers_table: TableRenderer::new().with_columns(borrowers_columns),
- borrow_flow: BorrowFlow::new(),
- return_flow: ReturnFlow::new(),
- show_register_dialog: false,
- new_borrower_name: String::new(),
- new_borrower_email: String::new(),
- new_borrower_phone: String::new(),
- new_borrower_class: String::new(),
- new_borrower_role: String::new(),
- register_error: None,
- borrower_editor: {
- let fields = vec![
- EditorField {
- name: "borrower_id".to_string(),
- label: "ID".to_string(),
- field_type: FieldType::Text,
- required: false,
- read_only: true,
- },
- EditorField {
- name: "name".to_string(),
- label: "Name".to_string(),
- field_type: FieldType::Text,
- required: true,
- read_only: false,
- },
- EditorField {
- name: "email".to_string(),
- label: "Email".to_string(),
- field_type: FieldType::Text,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "phone_number".to_string(),
- label: "Phone".to_string(),
- field_type: FieldType::Text,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "class_name".to_string(),
- label: "Class/Department".to_string(),
- field_type: FieldType::Text,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "role".to_string(),
- label: "Role/Type".to_string(),
- field_type: FieldType::Text,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "notes".to_string(),
- label: "Notes".to_string(),
- field_type: FieldType::MultilineText,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "banned".to_string(),
- label: "Banned".to_string(),
- field_type: FieldType::Checkbox,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "unban_fine".to_string(),
- label: "Unban Fine".to_string(),
- field_type: FieldType::Text,
- required: false,
- read_only: false,
- },
- ];
- FormBuilder::new("Edit Borrower", fields)
- },
- show_ban_dialog: false,
- show_unban_dialog: false,
- ban_borrower_data: None,
- ban_fine_amount: String::new(),
- ban_reason: String::new(),
- show_return_confirm_dialog: false,
- return_loan_data: None,
- show_delete_borrower_dialog: false,
- delete_borrower_data: None,
- loans_search: String::new(),
- borrowers_search: String::new(),
- switch_to_inventory_with_borrower: None,
- }
- }
- pub fn get_filter_columns() -> Vec<(String, String)> {
- vec![
- ("Any".to_string(), "Any".to_string()),
- ("Asset Tag".to_string(), "assets.asset_tag".to_string()),
- ("Asset Name".to_string(), "assets.name".to_string()),
- ("Borrower Name".to_string(), "borrowers.name".to_string()),
- ("Class".to_string(), "borrowers.class_name".to_string()),
- ("Status".to_string(), "assets.lending_status".to_string()),
- (
- "Checkout Date".to_string(),
- "lending_history.checkout_date".to_string(),
- ),
- (
- "Due Date".to_string(),
- "lending_history.due_date".to_string(),
- ),
- (
- "Return Date".to_string(),
- "lending_history.return_date".to_string(),
- ),
- ]
- }
- fn load(&mut self, api: &ApiClient) {
- if self.is_loading {
- return;
- }
- self.is_loading = true;
- self.last_error = None;
- match get_all_loans(api, None) {
- Ok(list) => {
- self.loans = list;
- }
- Err(e) => {
- self.last_error = Some(e.to_string());
- }
- }
- if self.last_error.is_none() {
- match get_borrowers_summary(api) {
- Ok(list) => {
- self.borrowers = list;
- }
- Err(e) => {
- self.last_error = Some(e.to_string());
- }
- }
- }
- self.is_loading = false;
- self.init_loaded = true;
- }
- pub fn show(
- &mut self,
- ctx: &egui::Context,
- ui: &mut egui::Ui,
- api_client: Option<&ApiClient>,
- ribbon: &mut crate::ui::ribbon::RibbonUI,
- ) {
- ui.horizontal(|ui| {
- ui.heading("Borrowing");
- if self.is_loading {
- ui.spinner();
- ui.label("Loading...");
- }
- if let Some(err) = &self.last_error {
- ui.colored_label(egui::Color32::RED, err);
- if ui.button("Refresh").clicked() {
- if let Some(api) = api_client {
- self.load(api);
- }
- }
- } else if ui.button("Refresh").clicked() {
- if let Some(api) = api_client {
- self.load(api);
- }
- }
- });
- ui.separator();
- // Check for filter changes
- if ribbon
- .checkboxes
- .get("borrowing_filter_changed")
- .copied()
- .unwrap_or(false)
- {
- ribbon
- .checkboxes
- .insert("borrowing_filter_changed".to_string(), false);
- // For now just note that filters changed - we'll apply them client-side in render
- // In the future we could reload with server-side filtering
- }
- // Check for ribbon actions
- if let Some(api) = api_client {
- if ribbon
- .checkboxes
- .get("borrowing_action_checkout")
- .copied()
- .unwrap_or(false)
- {
- self.borrow_flow.open(api);
- }
- if ribbon
- .checkboxes
- .get("borrowing_action_return")
- .copied()
- .unwrap_or(false)
- {
- self.return_flow.open(api);
- }
- if ribbon
- .checkboxes
- .get("borrowing_action_register")
- .copied()
- .unwrap_or(false)
- {
- self.show_register_dialog = true;
- self.register_error = None;
- }
- if ribbon
- .checkboxes
- .get("borrowing_action_refresh")
- .copied()
- .unwrap_or(false)
- {
- self.load(api);
- }
- }
- if !self.init_loaded {
- if let Some(api) = api_client {
- self.load(api);
- }
- }
- // Show borrow flow if open
- if let Some(api) = api_client {
- self.borrow_flow.show(ctx, api);
- if self.borrow_flow.take_recent_success() {
- self.load(api);
- }
- }
- // Show return flow if open
- if let Some(api) = api_client {
- self.return_flow.show(ctx, api);
- if self.return_flow.take_recent_success() {
- self.load(api);
- }
- }
- // Show register dialog if open
- if self.show_register_dialog {
- if let Some(api) = api_client {
- self.show_register_borrower_dialog(ctx, api);
- }
- }
- // Show borrower editor if open
- if let Some(api) = api_client {
- if let Some(result) = self.borrower_editor.show_editor(ctx) {
- if let Some(data) = result {
- // Editor returned data - save it
- if let Err(e) = self.save_borrower_changes(api, &data) {
- log::error!("Failed to save borrower changes: {}", e);
- } else {
- self.load(api);
- }
- }
- // else: user cancelled
- }
- }
- // Show ban dialog if open
- if self.show_ban_dialog {
- if let Some(api) = api_client {
- self.show_ban_dialog(ctx, api);
- }
- }
- // Show unban dialog if open
- if self.show_unban_dialog {
- if let Some(api) = api_client {
- self.show_unban_dialog(ctx, api);
- }
- }
- // Show return confirm dialog if open
- if self.show_return_confirm_dialog {
- if let Some(api) = api_client {
- self.show_return_confirm_dialog(ctx, api);
- }
- }
- // Show delete borrower confirm dialog if open
- if self.show_delete_borrower_dialog {
- if let Some(api) = api_client {
- self.show_delete_borrower_dialog(ctx, api);
- }
- }
- // Wrap entire content in ScrollArea
- egui::ScrollArea::vertical().show(ui, |ui| {
- // Section 1: Lending history
- egui::CollapsingHeader::new("Lending History")
- .default_open(true)
- .show(ui, |ui| {
- ui.horizontal(|ui| {
- ui.heading("Loans");
- ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
- if ui.button("Columns").clicked() {
- self.show_loans_column_selector = !self.show_loans_column_selector;
- }
- });
- });
- // Search and filter controls
- ui.horizontal(|ui| {
- ui.label("Search:");
- ui.text_edit_singleline(&mut self.loans_search);
- ui.separator();
- // Status filters from ribbon
- ui.label("Show:");
- ui.checkbox(
- ribbon
- .checkboxes
- .entry("borrowing_show_normal".to_string())
- .or_insert(true),
- "Normal",
- );
- ui.checkbox(
- ribbon
- .checkboxes
- .entry("borrowing_show_overdue".to_string())
- .or_insert(true),
- "Overdue",
- );
- ui.checkbox(
- ribbon
- .checkboxes
- .entry("borrowing_show_stolen".to_string())
- .or_insert(true),
- "Stolen",
- );
- ui.checkbox(
- ribbon
- .checkboxes
- .entry("borrowing_show_returned".to_string())
- .or_insert(false),
- "Returned",
- );
- });
- ui.separator();
- self.render_active_loans(ui, ribbon);
- });
- ui.add_space(10.0);
- // Section 2: Borrowers summary
- egui::CollapsingHeader::new("Borrowers")
- .default_open(true)
- .show(ui, |ui| {
- ui.horizontal(|ui| {
- ui.heading("Borrowers");
- ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
- if ui.button("Columns").clicked() {
- self.show_borrowers_column_selector =
- !self.show_borrowers_column_selector;
- }
- });
- });
- // Search control
- ui.horizontal(|ui| {
- ui.label("Search:");
- ui.text_edit_singleline(&mut self.borrowers_search);
- });
- ui.separator();
- self.render_borrowers_table(ui);
- });
- }); // End ScrollArea
- // Show column selector windows
- if self.show_loans_column_selector {
- egui::Window::new("Loans Columns")
- .open(&mut self.show_loans_column_selector)
- .resizable(true)
- .default_width(250.0)
- .show(ctx, |ui| {
- self.loans_table.show_column_selector(ui, "loans");
- });
- }
- if self.show_borrowers_column_selector {
- egui::Window::new("Borrowers Columns")
- .open(&mut self.show_borrowers_column_selector)
- .resizable(true)
- .default_width(250.0)
- .show(ctx, |ui| {
- self.borrowers_table.show_column_selector(ui, "borrowers");
- });
- }
- }
- fn render_active_loans(&mut self, ui: &mut egui::Ui, ribbon: &crate::ui::ribbon::RibbonUI) {
- // Get checkbox states
- let show_returned = ribbon
- .checkboxes
- .get("borrowing_show_returned")
- .copied()
- .unwrap_or(false);
- let show_normal = ribbon
- .checkboxes
- .get("borrowing_show_normal")
- .copied()
- .unwrap_or(true);
- let show_overdue = ribbon
- .checkboxes
- .get("borrowing_show_overdue")
- .copied()
- .unwrap_or(true);
- let show_stolen = ribbon
- .checkboxes
- .get("borrowing_show_stolen")
- .copied()
- .unwrap_or(true);
- // Apply filters
- let filtered_loans: Vec<serde_json::Value> = self
- .loans
- .iter()
- .filter(|loan| {
- // First apply search filter
- if !self.loans_search.is_empty() {
- let search_lower = self.loans_search.to_lowercase();
- let asset_tag = loan.get("asset_tag").and_then(|v| v.as_str()).unwrap_or("");
- let asset_name = loan.get("name").and_then(|v| v.as_str()).unwrap_or("");
- let borrower_name = loan
- .get("borrower_name")
- .and_then(|v| v.as_str())
- .unwrap_or("");
- let class_name = loan
- .get("class_name")
- .and_then(|v| v.as_str())
- .unwrap_or("");
- if !(asset_tag.to_lowercase().contains(&search_lower)
- || asset_name.to_lowercase().contains(&search_lower)
- || borrower_name.to_lowercase().contains(&search_lower)
- || class_name.to_lowercase().contains(&search_lower))
- {
- return false;
- }
- }
- // Apply filter builder filters
- if !Self::matches_filter_builder(loan, &ribbon.filter_builder) {
- return false;
- }
- // Check if this loan has been returned
- let has_return_date = loan
- .get("return_date")
- .and_then(|v| v.as_str())
- .filter(|s| !s.is_empty())
- .is_some();
- // If returned, check the show_returned checkbox
- if has_return_date {
- return show_returned;
- }
- // For active loans, check the lending_status from assets table
- let lending_status = loan
- .get("lending_status")
- .and_then(|v| v.as_str())
- .unwrap_or("");
- // Check if stolen
- if lending_status == "Stolen" || lending_status == "Illegally Handed Out" {
- return show_stolen;
- }
- // Check if overdue
- if let Some(due_date_str) = loan.get("due_date").and_then(|v| v.as_str()) {
- let now = chrono::Local::now().format("%Y-%m-%d").to_string();
- if due_date_str < now.as_str() {
- return show_overdue;
- }
- }
- // Otherwise it's a normal active loan (not overdue, not stolen)
- show_normal
- })
- .cloned()
- .collect();
- // Derive a display status per loan to avoid confusion:
- // If a loan has a return_date, always show "Returned" regardless of the current asset status.
- // Otherwise, use the existing lending_status value (Overdue, etc. handled by DB).
- let mut display_loans: Vec<serde_json::Value> = Vec::with_capacity(filtered_loans.len());
- for loan in &filtered_loans {
- let mut row = loan.clone();
- let has_return = row
- .get("return_date")
- .and_then(|v| v.as_str())
- .map(|s| !s.is_empty())
- .unwrap_or(false);
- if has_return {
- row["lending_status"] = serde_json::Value::String("Returned".to_string());
- }
- display_loans.push(row);
- }
- let prepared_data = self.loans_table.prepare_json_data(&display_loans);
- // Handle loan table events (return item)
- let mut return_loan: Option<serde_json::Value> = None;
- struct LoanEventHandler<'a> {
- return_action: &'a mut Option<serde_json::Value>,
- }
- impl<'a> crate::core::table_renderer::TableEventHandler<serde_json::Value>
- for LoanEventHandler<'a>
- {
- fn on_double_click(&mut self, _item: &serde_json::Value, _row_index: usize) {
- // Not used for loans
- }
- fn on_context_menu(
- &mut self,
- ui: &mut egui::Ui,
- item: &serde_json::Value,
- _row_index: usize,
- ) {
- // Only show "Return Item" if the loan is active (no return_date)
- let has_return_date = item.get("return_date").and_then(|v| v.as_str()).is_some();
- if !has_return_date {
- if ui
- .button(format!(
- "{} Return Item",
- egui_phosphor::regular::ARROW_RIGHT
- ))
- .clicked()
- {
- *self.return_action = Some(item.clone());
- ui.close();
- }
- }
- }
- fn on_selection_changed(&mut self, _selected_indices: &[usize]) {
- // Not used for now
- }
- }
- let mut handler = LoanEventHandler {
- return_action: &mut return_loan,
- };
- self.loans_table
- .render_json_table(ui, &prepared_data, Some(&mut handler));
- // Store return action for processing after all rendering
- if let Some(loan) = return_loan {
- self.return_loan_data = Some(loan);
- self.show_return_confirm_dialog = true;
- }
- }
- /// Client-side filter matching for filter builder conditions
- fn matches_filter_builder(
- loan: &serde_json::Value,
- filter_builder: &crate::core::components::filter_builder::FilterBuilder,
- ) -> bool {
- use crate::core::components::filter_builder::FilterOperator;
- // If no valid conditions, don't filter
- if !filter_builder.filter_group.is_valid() {
- return true;
- }
- // Check each condition
- for condition in &filter_builder.filter_group.conditions {
- if !condition.is_valid() {
- continue;
- }
- // Map the filter column to the actual JSON field name
- let field_name = match condition.column.as_str() {
- "assets.asset_tag" => "asset_tag",
- "assets.name" => "name",
- "borrowers.name" => "borrower_name",
- "borrowers.class_name" => "class_name",
- "assets.lending_status" => "lending_status",
- "lending_history.checkout_date" => "checkout_date",
- "lending_history.due_date" => "due_date",
- "lending_history.return_date" => "return_date",
- _ => {
- // Fallback: strip table prefix if present
- if condition.column.contains('.') {
- condition
- .column
- .split('.')
- .last()
- .unwrap_or(&condition.column)
- } else {
- &condition.column
- }
- }
- };
- let field_value = loan.get(field_name).and_then(|v| v.as_str()).unwrap_or("");
- // Apply the operator
- let matches = match &condition.operator {
- FilterOperator::Is => field_value == condition.value,
- FilterOperator::IsNot => field_value != condition.value,
- FilterOperator::Contains => field_value
- .to_lowercase()
- .contains(&condition.value.to_lowercase()),
- FilterOperator::DoesntContain => !field_value
- .to_lowercase()
- .contains(&condition.value.to_lowercase()),
- FilterOperator::IsNull => field_value.is_empty(),
- FilterOperator::IsNotNull => !field_value.is_empty(),
- };
- if !matches {
- return false; // For now, treat as AND logic
- }
- }
- true
- }
- fn render_borrowers_table(&mut self, ui: &mut egui::Ui) {
- // Apply search filter if set
- let filtered_borrowers: Vec<serde_json::Value> = if self.borrowers_search.is_empty() {
- self.borrowers.clone()
- } else {
- let search_lower = self.borrowers_search.to_lowercase();
- self.borrowers
- .iter()
- .filter(|borrower| {
- let name = borrower
- .get("borrower_name")
- .and_then(|v| v.as_str())
- .unwrap_or("");
- let class = borrower
- .get("class_name")
- .and_then(|v| v.as_str())
- .unwrap_or("");
- name.to_lowercase().contains(&search_lower)
- || class.to_lowercase().contains(&search_lower)
- })
- .cloned()
- .collect()
- };
- let prepared_data = self.borrowers_table.prepare_json_data(&filtered_borrowers);
- // Store actions to perform after rendering (to avoid borrow checker issues)
- let mut edit_borrower: Option<serde_json::Value> = None;
- let mut ban_borrower: Option<serde_json::Value> = None;
- let mut unban_borrower: Option<serde_json::Value> = None;
- let mut delete_borrower: Option<serde_json::Value> = None;
- let mut show_items_for_borrower: Option<i64> = None;
- // Create event handler for context menu
- struct BorrowerEventHandler<'a> {
- edit_action: &'a mut Option<serde_json::Value>,
- ban_action: &'a mut Option<serde_json::Value>,
- unban_action: &'a mut Option<serde_json::Value>,
- delete_action: &'a mut Option<serde_json::Value>,
- show_items_action: &'a mut Option<i64>,
- }
- impl<'a> crate::core::TableEventHandler<serde_json::Value> for BorrowerEventHandler<'a> {
- fn on_double_click(&mut self, item: &serde_json::Value, _row_index: usize) {
- // Open edit dialog on double-click
- *self.edit_action = Some(item.clone());
- }
- fn on_context_menu(
- &mut self,
- ui: &mut egui::Ui,
- item: &serde_json::Value,
- _row_index: usize,
- ) {
- let is_banned = item
- .get("banned")
- .and_then(|v| v.as_bool())
- .unwrap_or(false);
- let borrower_id = item.get("borrower_id").and_then(|v| v.as_i64());
- if ui
- .button(format!("{} Edit Borrower", egui_phosphor::regular::PENCIL))
- .clicked()
- {
- *self.edit_action = Some(item.clone());
- ui.close();
- }
- if let Some(id) = borrower_id {
- if ui
- .button(format!(
- "{} Show Items Borrowed to this User",
- egui_phosphor::regular::PACKAGE
- ))
- .clicked()
- {
- *self.show_items_action = Some(id);
- ui.close();
- }
- }
- ui.separator();
- if is_banned {
- if ui
- .button(format!(
- "{} Unban Borrower",
- egui_phosphor::regular::CHECK_CIRCLE
- ))
- .clicked()
- {
- *self.unban_action = Some(item.clone());
- ui.close();
- }
- } else {
- if ui
- .button(format!("{} Ban Borrower", egui_phosphor::regular::PROHIBIT))
- .clicked()
- {
- *self.ban_action = Some(item.clone());
- ui.close();
- }
- }
- ui.separator();
- if ui
- .button(format!("{} Delete Borrower", egui_phosphor::regular::TRASH))
- .clicked()
- {
- *self.delete_action = Some(item.clone());
- ui.close();
- }
- }
- fn on_selection_changed(&mut self, _selected_indices: &[usize]) {
- // Not used for now
- }
- }
- let mut handler = BorrowerEventHandler {
- edit_action: &mut edit_borrower,
- ban_action: &mut ban_borrower,
- unban_action: &mut unban_borrower,
- delete_action: &mut delete_borrower,
- show_items_action: &mut show_items_for_borrower,
- };
- self.borrowers_table
- .render_json_table(ui, &prepared_data, Some(&mut handler));
- // Process actions after rendering
- if let Some(borrower) = edit_borrower {
- self.open_edit_borrower_dialog(borrower);
- }
- if let Some(borrower) = ban_borrower {
- self.open_ban_dialog(borrower);
- }
- if let Some(borrower) = unban_borrower {
- self.open_unban_dialog(borrower);
- }
- if let Some(borrower) = delete_borrower {
- self.delete_borrower_data = Some(borrower);
- self.show_delete_borrower_dialog = true;
- }
- if let Some(borrower_id) = show_items_for_borrower {
- // Set the flag to switch to inventory with this borrower filter
- self.switch_to_inventory_with_borrower = Some(borrower_id);
- }
- }
- fn show_register_borrower_dialog(&mut self, ctx: &egui::Context, api_client: &ApiClient) {
- egui::Window::new("Register New Borrower")
- .collapsible(false)
- .resizable(false)
- .default_width(400.0)
- .show(ctx, |ui| {
- ui.vertical(|ui| {
- ui.add_space(5.0);
- if let Some(err) = &self.register_error {
- ui.colored_label(egui::Color32::RED, err);
- ui.separator();
- }
- ui.horizontal(|ui| {
- ui.label("Name:");
- ui.add_sized(
- [250.0, 20.0],
- egui::TextEdit::singleline(&mut self.new_borrower_name)
- .hint_text("Full name"),
- );
- });
- ui.horizontal(|ui| {
- ui.label("Email:");
- ui.add_sized(
- [250.0, 20.0],
- egui::TextEdit::singleline(&mut self.new_borrower_email)
- .hint_text("email@example.com"),
- );
- });
- ui.horizontal(|ui| {
- ui.label("Phone:");
- ui.add_sized(
- [250.0, 20.0],
- egui::TextEdit::singleline(&mut self.new_borrower_phone)
- .hint_text("Phone number"),
- );
- });
- ui.horizontal(|ui| {
- ui.label("Class:");
- ui.add_sized(
- [250.0, 20.0],
- egui::TextEdit::singleline(&mut self.new_borrower_class)
- .hint_text("Class or department"),
- );
- });
- ui.horizontal(|ui| {
- ui.label("Role:");
- ui.add_sized(
- [250.0, 20.0],
- egui::TextEdit::singleline(&mut self.new_borrower_role)
- .hint_text("Student, Staff, etc."),
- );
- });
- ui.add_space(10.0);
- ui.separator();
- ui.horizontal(|ui| {
- if ui.button("Register").clicked() {
- if self.new_borrower_name.trim().is_empty() {
- self.register_error = Some("Name is required".to_string());
- } else {
- match self.register_borrower(api_client) {
- Ok(_) => {
- // Success - close dialog and reload data
- self.show_register_dialog = false;
- self.clear_register_form();
- self.load(api_client);
- }
- Err(e) => {
- self.register_error = Some(e.to_string());
- }
- }
- }
- }
- if ui.button("Cancel").clicked() {
- self.show_register_dialog = false;
- self.clear_register_form();
- }
- });
- });
- });
- }
- fn register_borrower(&self, api_client: &ApiClient) -> anyhow::Result<()> {
- use crate::models::QueryRequest;
- let mut borrower_data = serde_json::json!({
- "name": self.new_borrower_name.trim(),
- "banned": false,
- });
- if !self.new_borrower_email.is_empty() {
- borrower_data["email"] =
- serde_json::Value::String(self.new_borrower_email.trim().to_string());
- }
- if !self.new_borrower_phone.is_empty() {
- borrower_data["phone_number"] =
- serde_json::Value::String(self.new_borrower_phone.trim().to_string());
- }
- if !self.new_borrower_class.is_empty() {
- borrower_data["class_name"] =
- serde_json::Value::String(self.new_borrower_class.trim().to_string());
- }
- if !self.new_borrower_role.is_empty() {
- borrower_data["role"] =
- serde_json::Value::String(self.new_borrower_role.trim().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())));
- }
- Ok(())
- }
- fn clear_register_form(&mut self) {
- self.new_borrower_name.clear();
- self.new_borrower_email.clear();
- self.new_borrower_phone.clear();
- self.new_borrower_class.clear();
- self.new_borrower_role.clear();
- self.register_error = None;
- }
- // Edit borrower dialog methods
- fn open_edit_borrower_dialog(&mut self, borrower: serde_json::Value) {
- // The summary doesn't have all fields, so we'll populate what we have
- // and the editor will show empty fields for missing data
- let mut editor_data = serde_json::Map::new();
- // Map the summary fields to editor fields
- if let Some(id) = borrower.get("borrower_id") {
- editor_data.insert("borrower_id".to_string(), id.clone());
- editor_data.insert("id".to_string(), id.clone()); // Also set 'id' for WHERE clause
- }
- if let Some(name) = borrower.get("borrower_name") {
- editor_data.insert("name".to_string(), name.clone());
- }
- if let Some(email) = borrower.get("email") {
- if !email.is_null() && email.as_str().map(|s| !s.is_empty()).unwrap_or(false) {
- editor_data.insert("email".to_string(), email.clone());
- }
- }
- if let Some(phone) = borrower.get("phone_number") {
- if !phone.is_null() && phone.as_str().map(|s| !s.is_empty()).unwrap_or(false) {
- editor_data.insert("phone_number".to_string(), phone.clone());
- }
- }
- if let Some(class) = borrower.get("class_name") {
- if !class.is_null() && class.as_str().map(|s| !s.is_empty()).unwrap_or(false) {
- editor_data.insert("class_name".to_string(), class.clone());
- }
- }
- if let Some(role) = borrower.get("role") {
- if !role.is_null() && role.as_str().map(|s| !s.is_empty()).unwrap_or(false) {
- editor_data.insert("role".to_string(), role.clone());
- }
- }
- if let Some(notes) = borrower.get("notes") {
- if !notes.is_null() && notes.as_str().map(|s| !s.is_empty()).unwrap_or(false) {
- editor_data.insert("notes".to_string(), notes.clone());
- }
- }
- if let Some(banned) = borrower.get("banned") {
- editor_data.insert("banned".to_string(), banned.clone());
- }
- if let Some(unban_fine) = borrower.get("unban_fine") {
- if !unban_fine.is_null() {
- editor_data.insert("unban_fine".to_string(), unban_fine.clone());
- }
- }
- // Open the editor with the borrower data
- let value = serde_json::Value::Object(editor_data);
- self.borrower_editor.open(&value);
- }
- fn save_borrower_changes(
- &self,
- api_client: &ApiClient,
- diff: &serde_json::Map<String, serde_json::Value>,
- ) -> anyhow::Result<()> {
- use crate::models::QueryRequest;
- // Extract borrower ID from the diff (editor includes it as __editor_item_id)
- let borrower_id = diff
- .get("__editor_item_id")
- .and_then(|v| v.as_str())
- .and_then(|s| s.parse::<i64>().ok())
- .or_else(|| diff.get("borrower_id").and_then(|v| v.as_i64()))
- .or_else(|| diff.get("id").and_then(|v| v.as_i64()))
- .ok_or_else(|| anyhow::anyhow!("Invalid borrower ID"))?;
- // Build update data from the diff (exclude editor metadata)
- let mut update_data = serde_json::Map::new();
- for (key, value) in diff.iter() {
- if !key.starts_with("__editor_") && key != "borrower_id" && key != "id" {
- update_data.insert(key.clone(), value.clone());
- }
- }
- if update_data.is_empty() {
- return Ok(()); // Nothing to update
- }
- let request = QueryRequest {
- action: "update".to_string(),
- table: "borrowers".to_string(),
- data: Some(serde_json::Value::Object(update_data)),
- r#where: Some(serde_json::json!({"id": borrower_id})),
- columns: None,
- joins: None,
- order_by: None,
- limit: None,
- offset: None,
- filter: None,
- };
- let response = api_client.query(&request)?;
- if !response.success {
- return Err(anyhow::anyhow!(response
- .error
- .unwrap_or_else(|| "Failed to update borrower".to_string())));
- }
- Ok(())
- }
- // Ban/Unban dialog methods
- fn open_ban_dialog(&mut self, borrower: serde_json::Value) {
- self.ban_borrower_data = Some(borrower);
- self.show_ban_dialog = true;
- self.ban_fine_amount.clear();
- self.ban_reason.clear();
- }
- fn open_unban_dialog(&mut self, borrower: serde_json::Value) {
- self.ban_borrower_data = Some(borrower);
- self.show_unban_dialog = true;
- }
- fn show_ban_dialog(&mut self, ctx: &egui::Context, api_client: &ApiClient) {
- let mut keep_open = true;
- let mut confirmed = false;
- let mut cancelled = false;
- let borrower_name = self
- .ban_borrower_data
- .as_ref()
- .and_then(|b| b.get("borrower_name"))
- .and_then(|v| v.as_str())
- .unwrap_or("Unknown")
- .to_string();
- egui::Window::new("Ban Borrower")
- .collapsible(false)
- .resizable(false)
- .default_width(400.0)
- .open(&mut keep_open)
- .show(ctx, |ui| {
- ui.vertical(|ui| {
- ui.add_space(5.0);
- ui.label(
- egui::RichText::new(format!(
- "⚠ Are you sure you want to ban '{}'?",
- borrower_name
- ))
- .color(egui::Color32::from_rgb(255, 152, 0))
- .strong(),
- );
- ui.add_space(10.0);
- ui.horizontal(|ui| {
- ui.label("Fine Amount ($):");
- ui.text_edit_singleline(&mut self.ban_fine_amount);
- });
- ui.label(
- egui::RichText::new("(Optional: leave empty for no fine)")
- .small()
- .color(ui.visuals().weak_text_color()),
- );
- ui.add_space(5.0);
- ui.label("Reason:");
- ui.text_edit_multiline(&mut self.ban_reason);
- ui.label(
- egui::RichText::new("(Optional: reason for banning)")
- .small()
- .color(ui.visuals().weak_text_color()),
- );
- ui.add_space(10.0);
- ui.separator();
- ui.horizontal(|ui| {
- if ui.button("Confirm Ban").clicked() {
- confirmed = true;
- }
- if ui.button("Cancel").clicked() {
- cancelled = true;
- }
- });
- });
- });
- if confirmed {
- match self.ban_borrower(api_client) {
- Ok(_) => {
- self.show_ban_dialog = false;
- self.ban_borrower_data = None;
- self.load(api_client);
- }
- Err(e) => {
- log::error!("Failed to ban borrower: {}", e);
- }
- }
- }
- if cancelled || !keep_open {
- self.show_ban_dialog = false;
- self.ban_borrower_data = None;
- }
- }
- fn show_unban_dialog(&mut self, ctx: &egui::Context, api_client: &ApiClient) {
- let mut keep_open = true;
- let mut confirmed = false;
- let mut cancelled = false;
- let borrower_name = self
- .ban_borrower_data
- .as_ref()
- .and_then(|b| b.get("borrower_name"))
- .and_then(|v| v.as_str())
- .unwrap_or("Unknown")
- .to_string();
- egui::Window::new("Unban Borrower")
- .collapsible(false)
- .resizable(false)
- .default_width(400.0)
- .open(&mut keep_open)
- .show(ctx, |ui| {
- ui.vertical(|ui| {
- ui.add_space(5.0);
- ui.label(
- egui::RichText::new(format!(
- "Are you sure you want to unban '{}'?",
- borrower_name
- ))
- .color(egui::Color32::from_rgb(76, 175, 80))
- .strong(),
- );
- ui.add_space(10.0);
- ui.separator();
- ui.horizontal(|ui| {
- if ui.button("Confirm Unban").clicked() {
- confirmed = true;
- }
- if ui.button("Cancel").clicked() {
- cancelled = true;
- }
- });
- });
- });
- if confirmed {
- match self.unban_borrower(api_client) {
- Ok(_) => {
- self.show_unban_dialog = false;
- self.ban_borrower_data = None;
- self.load(api_client);
- }
- Err(e) => {
- log::error!("Failed to unban borrower: {}", e);
- }
- }
- }
- if cancelled || !keep_open {
- self.show_unban_dialog = false;
- self.ban_borrower_data = None;
- }
- }
- fn show_return_confirm_dialog(&mut self, _ctx: &egui::Context, api_client: &ApiClient) {
- // Replace the basic confirm dialog with the full Return Flow, pre-selecting the loan
- if let Some(loan) = self.return_loan_data.clone() {
- // Open the full-featured return flow and jump to confirmation
- self.return_flow.open(api_client);
- self.return_flow.selected_loan = Some(loan);
- self.return_flow.current_step =
- crate::core::workflows::return_flow::ReturnStep::Confirm;
- }
- // Close the legacy confirm dialog path
- self.show_return_confirm_dialog = false;
- self.return_loan_data = None;
- }
- fn show_delete_borrower_dialog(&mut self, ctx: &egui::Context, api_client: &ApiClient) {
- let mut keep_open = true;
- let mut confirmed = false;
- let mut cancelled = false;
- let borrower_name = self
- .delete_borrower_data
- .as_ref()
- .and_then(|b| b.get("borrower_name"))
- .and_then(|v| v.as_str())
- .unwrap_or("Unknown")
- .to_string();
- egui::Window::new("Delete Borrower")
- .collapsible(false)
- .resizable(false)
- .default_width(400.0)
- .open(&mut keep_open)
- .show(ctx, |ui| {
- ui.vertical(|ui| {
- ui.add_space(5.0);
- ui.label(
- egui::RichText::new(format!(
- "Are you sure you want to delete '{}'?",
- borrower_name
- ))
- .color(egui::Color32::RED)
- .strong(),
- );
- ui.add_space(10.0);
- ui.label(
- egui::RichText::new("This action cannot be undone!")
- .color(egui::Color32::RED)
- .small(),
- );
- ui.add_space(10.0);
- ui.separator();
- ui.horizontal(|ui| {
- if ui.button("Confirm Delete").clicked() {
- confirmed = true;
- }
- if ui.button("Cancel").clicked() {
- cancelled = true;
- }
- });
- });
- });
- if confirmed {
- match self.delete_borrower(api_client) {
- Ok(_) => {
- self.show_delete_borrower_dialog = false;
- self.delete_borrower_data = None;
- self.load(api_client);
- }
- Err(e) => {
- log::error!("Failed to delete borrower: {}", e);
- self.last_error = Some(format!("Delete failed: {}", e));
- }
- }
- }
- if cancelled || !keep_open {
- self.show_delete_borrower_dialog = false;
- self.delete_borrower_data = None;
- }
- }
- fn ban_borrower(&self, api_client: &ApiClient) -> anyhow::Result<()> {
- use crate::models::QueryRequest;
- let borrower_id = self
- .ban_borrower_data
- .as_ref()
- .and_then(|b| b.get("borrower_id"))
- .and_then(|v| v.as_i64())
- .ok_or_else(|| anyhow::anyhow!("Invalid borrower ID"))?;
- let mut update_data = serde_json::json!({
- "banned": true,
- });
- // Add unban fine amount if provided
- if !self.ban_fine_amount.trim().is_empty() {
- if let Ok(fine) = self.ban_fine_amount.trim().parse::<f64>() {
- update_data["unban_fine"] = serde_json::Value::Number(
- serde_json::Number::from_f64(fine).unwrap_or(serde_json::Number::from(0)),
- );
- }
- }
- // Add reason to notes if provided
- if !self.ban_reason.trim().is_empty() {
- update_data["notes"] = serde_json::Value::String(self.ban_reason.trim().to_string());
- }
- let request = QueryRequest {
- action: "update".to_string(),
- table: "borrowers".to_string(),
- data: Some(update_data),
- r#where: Some(serde_json::json!({"id": borrower_id})),
- columns: None,
- joins: None,
- order_by: None,
- limit: None,
- offset: None,
- filter: None,
- };
- let response = api_client.query(&request)?;
- if !response.success {
- return Err(anyhow::anyhow!(response
- .error
- .unwrap_or_else(|| "Failed to ban borrower".to_string())));
- }
- Ok(())
- }
- fn unban_borrower(&self, api_client: &ApiClient) -> anyhow::Result<()> {
- use crate::models::QueryRequest;
- let borrower_id = self
- .ban_borrower_data
- .as_ref()
- .and_then(|b| b.get("borrower_id"))
- .and_then(|v| v.as_i64())
- .ok_or_else(|| anyhow::anyhow!("Invalid borrower ID"))?;
- let update_data = serde_json::json!({
- "banned": false,
- "unban_fine": 0.0,
- });
- let request = QueryRequest {
- action: "update".to_string(),
- table: "borrowers".to_string(),
- data: Some(update_data),
- r#where: Some(serde_json::json!({"id": borrower_id})),
- columns: None,
- joins: None,
- order_by: None,
- limit: None,
- offset: None,
- filter: None,
- };
- let response = api_client.query(&request)?;
- if !response.success {
- return Err(anyhow::anyhow!(response
- .error
- .unwrap_or_else(|| "Failed to unban borrower".to_string())));
- }
- Ok(())
- }
- #[allow(dead_code)]
- fn process_return(&self, api_client: &ApiClient) -> anyhow::Result<()> {
- use crate::models::QueryRequest;
- let loan_id = self
- .return_loan_data
- .as_ref()
- .and_then(|l| l.get("id"))
- .and_then(|v| v.as_i64())
- .ok_or_else(|| anyhow::anyhow!("Invalid loan ID"))?;
- let asset_id = self
- .return_loan_data
- .as_ref()
- .and_then(|l| l.get("asset_id"))
- .and_then(|v| v.as_i64())
- .ok_or_else(|| anyhow::anyhow!("Invalid asset ID"))?;
- let return_date = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
- // Update lending_history to set return_date
- let update_data = serde_json::json!({
- "return_date": return_date
- });
- let request = QueryRequest {
- action: "update".to_string(),
- table: "lending_history".to_string(),
- data: Some(update_data),
- r#where: Some(serde_json::json!({"id": loan_id})),
- columns: None,
- joins: None,
- order_by: None,
- limit: None,
- offset: None,
- filter: None,
- };
- let response = api_client.query(&request)?;
- if !response.success {
- return Err(anyhow::anyhow!(response
- .error
- .unwrap_or_else(|| "Failed to update loan record".to_string())));
- }
- // Update asset status to "Available"
- let asset_update = serde_json::json!({
- "lending_status": "Available"
- });
- let asset_request = QueryRequest {
- action: "update".to_string(),
- table: "assets".to_string(),
- data: Some(asset_update),
- r#where: Some(serde_json::json!({"id": asset_id})),
- columns: None,
- joins: None,
- order_by: None,
- limit: None,
- offset: None,
- filter: None,
- };
- let asset_response = api_client.query(&asset_request)?;
- if !asset_response.success {
- return Err(anyhow::anyhow!(asset_response
- .error
- .unwrap_or_else(|| "Failed to update asset status".to_string())));
- }
- Ok(())
- }
- fn delete_borrower(&self, api_client: &ApiClient) -> anyhow::Result<()> {
- use crate::models::QueryRequest;
- let borrower_id = self
- .delete_borrower_data
- .as_ref()
- .and_then(|b| b.get("borrower_id"))
- .and_then(|v| v.as_i64())
- .ok_or_else(|| anyhow::anyhow!("Invalid borrower ID"))?;
- let request = QueryRequest {
- action: "delete".to_string(),
- table: "borrowers".to_string(),
- data: None,
- r#where: Some(serde_json::json!({"id": borrower_id})),
- columns: None,
- joins: None,
- order_by: None,
- limit: None,
- offset: None,
- filter: None,
- };
- let response = api_client.query(&request)?;
- if !response.success {
- return Err(anyhow::anyhow!(response
- .error
- .unwrap_or_else(|| "Failed to delete borrower".to_string())));
- }
- Ok(())
- }
- }
|