use eframe::egui; use std::collections::HashSet; use crate::api::ApiClient; use crate::core::components::form_builder::FormBuilder; use crate::core::components::interactions::ConfirmDialog; use crate::core::counters::count_entities; use crate::core::tables::get_suppliers; use crate::core::{EditorField, FieldType}; use crate::ui::ribbon::RibbonUI; #[derive(Clone)] struct ColumnConfig { name: String, field: String, visible: bool, } pub struct SuppliersView { rows: Vec, display_rows: Vec, is_loading: bool, last_error: Option, init_loaded: bool, // Columns & selector columns: Vec, show_column_panel: bool, // Selection & interactions selected_row: Option, last_click_time: Option, last_click_row: Option, selected_rows: HashSet, selection_anchor: Option, // Dialogs delete_dialog: ConfirmDialog, edit_dialog: FormBuilder, add_dialog: FormBuilder, // Track ids for operations edit_current_id: Option, delete_current_id: Option, } impl SuppliersView { pub fn new() -> Self { let columns = vec![ ColumnConfig { name: "Name".into(), field: "name".into(), visible: true, }, ColumnConfig { name: "Contact".into(), field: "contact".into(), visible: true, }, ColumnConfig { name: "Email".into(), field: "email".into(), visible: true, }, ColumnConfig { name: "Phone".into(), field: "phone".into(), visible: true, }, ColumnConfig { name: "Website".into(), field: "website".into(), visible: true, }, ColumnConfig { name: "Items".into(), field: "items_count".into(), visible: true, }, // Hidden by default ColumnConfig { name: "Notes".into(), field: "notes".into(), visible: false, }, ColumnConfig { name: "Created".into(), field: "created_at".into(), visible: false, }, ColumnConfig { name: "ID".into(), field: "id".into(), visible: false, }, ]; Self { rows: vec![], display_rows: vec![], is_loading: false, last_error: None, init_loaded: false, columns, show_column_panel: false, selected_row: None, last_click_time: None, last_click_row: None, selected_rows: HashSet::new(), selection_anchor: None, delete_dialog: ConfirmDialog::new( "Delete Supplier", "Are you sure you want to delete this supplier?", ), edit_dialog: FormBuilder::new( "Edit Supplier", vec![ EditorField { name: "name".into(), label: "Name".into(), field_type: FieldType::Text, required: true, read_only: false, }, EditorField { name: "contact".into(), label: "Contact".into(), field_type: FieldType::Text, required: false, read_only: false, }, EditorField { name: "email".into(), label: "Email".into(), field_type: FieldType::Text, required: false, read_only: false, }, EditorField { name: "phone".into(), label: "Phone".into(), field_type: FieldType::Text, required: false, read_only: false, }, EditorField { name: "website".into(), label: "Website".into(), field_type: FieldType::Text, required: false, read_only: false, }, EditorField { name: "notes".into(), label: "Notes".into(), field_type: FieldType::MultilineText, required: false, read_only: false, }, ], ), add_dialog: FormBuilder::new( "Add Supplier", vec![ EditorField { name: "name".into(), label: "Name".into(), field_type: FieldType::Text, required: true, read_only: false, }, EditorField { name: "contact".into(), label: "Contact".into(), field_type: FieldType::Text, required: false, read_only: false, }, EditorField { name: "email".into(), label: "Email".into(), field_type: FieldType::Text, required: false, read_only: false, }, EditorField { name: "phone".into(), label: "Phone".into(), field_type: FieldType::Text, required: false, read_only: false, }, EditorField { name: "website".into(), label: "Website".into(), field_type: FieldType::Text, required: false, read_only: false, }, EditorField { name: "notes".into(), label: "Notes".into(), field_type: FieldType::MultilineText, required: false, read_only: false, }, ], ), edit_current_id: None, delete_current_id: None, } } fn load(&mut self, api: &ApiClient) { if self.is_loading { return; } self.is_loading = true; self.last_error = None; match get_suppliers(api, Some(200)) { Ok(mut list) => { // Compute items_count per supplier for row in &mut list { if let Some(id) = row.get("id").and_then(|v| v.as_i64()) { let where_clause = serde_json::json!({"supplier_id": id}); let count = count_entities(api, "assets", Some(where_clause)).unwrap_or(0); row.as_object_mut().map(|o| { o.insert("items_count".into(), serde_json::json!(count)); }); } } self.rows = list; self.display_rows = self.rows.clone(); } Err(e) => self.last_error = Some(e.to_string()), } self.is_loading = false; self.init_loaded = true; } pub fn show( &mut self, ui: &mut egui::Ui, api_client: Option<&ApiClient>, ribbon: Option<&mut RibbonUI>, ) -> Vec { let mut flags_to_clear = Vec::new(); // Apply simple search from ribbon (by name/code/contact/email/phone/website) if let Some(ribbon) = ribbon.as_ref() { let term = ribbon .search_texts .get("supplier_search") .cloned() .unwrap_or_default(); let tl = term.to_lowercase(); if tl.is_empty() { self.display_rows = self.rows.clone(); } else { self.display_rows = self .rows .iter() .filter(|row| { let fields = ["name", "contact", "email", "phone", "website"]; fields.iter().any(|f| { row.get(*f) .and_then(|v| v.as_str()) .map(|s| s.to_lowercase().contains(&tl)) .unwrap_or(false) }) }) .cloned() .collect(); } } if let Some(ribbon) = ribbon.as_ref() { // Handle ribbon actions before rendering if ribbon .checkboxes .get("suppliers_action_new") .copied() .unwrap_or(false) { self.add_dialog.open_new(None); flags_to_clear.push("suppliers_action_new".to_string()); } if ribbon .checkboxes .get("suppliers_action_edit") .copied() .unwrap_or(false) { if let Some(selected) = self.first_selected_supplier() { self.open_editor_with(&selected); } else { log::warn!("Ribbon edit triggered but no supplier selected"); } flags_to_clear.push("suppliers_action_edit".to_string()); } if ribbon .checkboxes .get("suppliers_action_delete") .copied() .unwrap_or(false) { if let Some((name, id)) = self.first_selected_supplier_id_name() { self.delete_current_id = Some(id); self.delete_dialog.open(name, id.to_string()); } else { log::warn!("Ribbon delete triggered but no supplier selected"); } flags_to_clear.push("suppliers_action_delete".to_string()); } } ui.horizontal(|ui| { ui.heading("Suppliers"); 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(); let button_text = if self.show_column_panel { "Hide Column Selector" } else { "Show Column Selector" }; if ui.button(button_text).clicked() { self.show_column_panel = !self.show_column_panel; } }); ui.separator(); if !self.init_loaded { if let Some(api) = api_client { self.load(api); } } // Column selector window if self.show_column_panel { let ctx = ui.ctx(); let screen_rect = ctx.input(|i| { i.viewport().inner_rect.unwrap_or(egui::Rect::from_min_max( egui::pos2(0.0, 0.0), egui::pos2(1920.0, 1080.0), )) }); let max_w = (screen_rect.width() - 20.0).max(220.0); let max_h = (screen_rect.height() - 100.0).max(200.0); egui::Window::new("Column Selector") .collapsible(false) .resizable(true) .default_width(260.0) .default_height(360.0) .anchor(egui::Align2::RIGHT_TOP, [-10.0, 90.0]) .open(&mut self.show_column_panel) .min_size(egui::vec2(220.0, 200.0)) .max_size(egui::vec2(max_w, max_h)) .frame(egui::Frame { fill: egui::Color32::from_rgb(30, 30, 30), stroke: egui::Stroke::new(1.0, egui::Color32::from_gray(60)), inner_margin: egui::Margin::from(10.0), outer_margin: egui::Margin::ZERO, corner_radius: 6.0.into(), shadow: egui::epaint::Shadow::NONE, }) .show(ctx, |ui| { ui.heading("Columns"); ui.separator(); ui.add_space(8.0); egui::ScrollArea::vertical() .auto_shrink([false; 2]) .show(ui, |ui| { for column in &mut self.columns { ui.checkbox(&mut column.visible, &column.name); } }); ui.add_space(8.0); ui.separator(); ui.horizontal(|ui| { if ui.button("Show All").clicked() { for col in &mut self.columns { col.visible = true; } } if ui.button("Hide All").clicked() { for col in &mut self.columns { col.visible = false; } } }); }); } // compute visible columns and render table let visible_columns: Vec = self.columns.iter().filter(|c| c.visible).cloned().collect(); self.render_table(ui, &visible_columns); // Process selection and dialog events let ctx = ui.ctx(); // selection handled inline via checkbox/row clicks if let Some(row_idx) = ctx.data_mut(|d| d.remove_temp::(egui::Id::new("sup_double_click_idx"))) { self.selected_row = Some(row_idx); self.last_click_row = None; self.last_click_time = None; } if let Some(item) = ctx.data_mut(|d| { d.remove_temp::(egui::Id::new("sup_double_click_edit")) }) { self.open_editor_with(&item); ctx.request_repaint(); } if let Some(item) = ctx.data_mut(|d| { d.remove_temp::(egui::Id::new("sup_context_menu_edit")) }) { self.open_editor_with(&item); ctx.request_repaint(); } if let Some(item) = ctx.data_mut(|d| { d.remove_temp::(egui::Id::new("sup_context_menu_clone")) }) { // Prepare cloned payload: clear id, suffix name, keep other fields let cloned = crate::core::components::prepare_cloned_value( &item, &["id"], Some("name"), Some(""), ); if let Some(obj) = cloned.as_object() { self.add_dialog.open_new(Some(obj)); } ctx.request_repaint(); } if let Some(item) = ctx.data_mut(|d| { d.remove_temp::(egui::Id::new("sup_context_menu_delete")) }) { let name = item .get("name") .and_then(|v| v.as_str()) .unwrap_or("Unknown") .to_string(); let id = item.get("id").and_then(|v| v.as_i64()).unwrap_or(-1); self.delete_current_id = Some(id); self.delete_dialog.open(name, id.to_string()); ctx.request_repaint(); } // Handle delete confirmation dialog if let Some(confirmed) = self.delete_dialog.show_dialog(ctx) { if confirmed { if let (Some(api), Some(id)) = (api_client, self.delete_current_id) { let where_clause = serde_json::json!({"id": id}); match api.delete("suppliers", where_clause) { Ok(resp) if resp.success => { self.load(api); } Ok(resp) => { self.last_error = Some(format!("Delete failed: {:?}", resp.error)); } Err(e) => { self.last_error = Some(format!("Delete error: {}", e)); } } } } } // Handle edit dialog save if let Some(result) = self.edit_dialog.show_editor(ctx) { if let Some(updated) = result { if let (Some(api), Some(id)) = (api_client, self.edit_current_id) { let mut updated = updated; strip_editor_metadata(&mut updated); let values = serde_json::Value::Object(updated); let where_clause = serde_json::json!({"id": id}); match api.update("suppliers", values, where_clause) { Ok(resp) if resp.success => { self.load(api); } Ok(resp) => { self.last_error = Some(format!("Update failed: {:?}", resp.error)); } Err(e) => { self.last_error = Some(format!("Update error: {}", e)); } } } } } // Handle add dialog (clone -> insert) self.handle_add_dialog(ui.ctx(), api_client); flags_to_clear } fn render_table(&mut self, ui: &mut egui::Ui, visible_columns: &Vec) { use egui_extras::{Column, TableBuilder}; let mut table = TableBuilder::new(ui) .striped(true) .resizable(true) .cell_layout(egui::Layout::left_to_right(egui::Align::Center)); // Add selection checkbox column first table = table.column(Column::initial(28.0)); for _ in 0..visible_columns.len() { table = table.column(Column::remainder()); } table .header(22.0, |mut header| { // Select-all checkbox header.col(|ui| { let all_selected = self .display_rows .iter() .enumerate() .all(|(i, _)| self.selected_rows.contains(&i)); let mut chk = all_selected; if ui .checkbox(&mut chk, "") .on_hover_text("Select All") .clicked() { if chk { self.selected_rows = (0..self.display_rows.len()).collect(); } else { self.selected_rows.clear(); } } }); for col in visible_columns { header.col(|ui| { ui.strong(&col.name); }); } }) .body(|mut body| { for (idx, r) in self.display_rows.iter().enumerate() { let r_clone = r.clone(); let is_selected = self.selected_rows.contains(&idx); body.row(20.0, |mut row| { if is_selected { row.set_selected(true); } // Checkbox column row.col(|ui| { let mut checked = self.selected_rows.contains(&idx); let resp = ui.checkbox(&mut checked, ""); if resp.changed() { let mods = ui.input(|i| i.modifiers); if mods.shift { let anchor = self.selection_anchor.unwrap_or(idx); let (a, b) = if anchor <= idx { (anchor, idx) } else { (idx, anchor) }; for i in a..=b { if checked { self.selected_rows.insert(i); } else { self.selected_rows.remove(&i); } } } else if mods.command || mods.ctrl { if checked { self.selected_rows.insert(idx); } else { self.selected_rows.remove(&idx); } self.selection_anchor = Some(idx); } else { self.selected_rows.clear(); if checked { self.selected_rows.insert(idx); self.selection_anchor = Some(idx); } } } }); let mut combined_cell_response: Option = None; for col in visible_columns { row.col(|ui| { let resp = render_supplier_cell(ui, &r_clone, &col.field); combined_cell_response = Some(match combined_cell_response.take() { Some(prev) => prev.union(resp), None => resp, }); }); } let mut row_resp = row.response(); if let Some(cell_r) = combined_cell_response { row_resp = row_resp.union(cell_r); } if row_resp.clicked() { let now = std::time::Instant::now(); let is_double_click = if let (Some(last_time), Some(last_row)) = (self.last_click_time, self.last_click_row) { last_row == idx && now.duration_since(last_time).as_millis() < 500 } else { false }; if is_double_click { row_resp.ctx.data_mut(|d| { d.insert_temp(egui::Id::new("sup_double_click_idx"), idx); d.insert_temp( egui::Id::new("sup_double_click_edit"), r_clone.clone(), ); }); } else { // Multi-select on row click let mods = row_resp.ctx.input(|i| i.modifiers); if mods.shift { let anchor = self.selection_anchor.unwrap_or(idx); let (a, b) = if anchor <= idx { (anchor, idx) } else { (idx, anchor) }; for i in a..=b { self.selected_rows.insert(i); } } else if mods.command || mods.ctrl { if self.selected_rows.contains(&idx) { self.selected_rows.remove(&idx); } else { self.selected_rows.insert(idx); } self.selection_anchor = Some(idx); } else { self.selected_rows.clear(); self.selected_rows.insert(idx); self.selection_anchor = Some(idx); } self.last_click_time = Some(now); self.last_click_row = Some(idx); } } row_resp.context_menu(|ui| { if ui .button(format!("{} Edit", egui_phosphor::regular::PENCIL)) .clicked() { ui.ctx().data_mut(|d| { d.insert_temp( egui::Id::new("sup_context_menu_edit"), r_clone.clone(), ) }); ui.close(); } if ui .button(format!("{} Clone", egui_phosphor::regular::COPY)) .clicked() { ui.ctx().data_mut(|d| { d.insert_temp( egui::Id::new("sup_context_menu_clone"), r_clone.clone(), ) }); ui.close(); } ui.separator(); if ui .button(format!("{} Delete", egui_phosphor::regular::TRASH)) .clicked() { ui.ctx().data_mut(|d| { d.insert_temp( egui::Id::new("sup_context_menu_delete"), r_clone.clone(), ) }); ui.close(); } }); }); } }); } fn open_editor_with(&mut self, item: &serde_json::Value) { self.edit_current_id = item.get("id").and_then(|v| v.as_i64()); self.edit_dialog.open(item); } fn first_selected_supplier(&self) -> Option { self.first_selected_index() .and_then(|idx| self.display_rows.get(idx).cloned()) } fn first_selected_supplier_id_name(&self) -> Option<(String, i64)> { self.first_selected_index().and_then(|idx| { self.display_rows.get(idx).and_then(|row| { let name = row .get("name") .and_then(|v| v.as_str()) .unwrap_or("Unknown") .to_string(); let id = row.get("id").and_then(|v| v.as_i64())?; Some((name, id)) }) }) } fn first_selected_index(&self) -> Option { if self.selected_rows.is_empty() { None } else { self.selected_rows.iter().copied().min() } } } fn label_trunc(ui: &mut egui::Ui, text: &str, max: usize) -> egui::Response { if text.len() > max { let short = format!("{}…", &text[..max]); ui.label(short).on_hover_ui(|ui| { ui.label(text); }) } else { ui.label(text) } } fn render_supplier_cell(ui: &mut egui::Ui, row: &serde_json::Value, field: &str) -> egui::Response { let t = |k: &str| row.get(k).and_then(|v| v.as_str()).unwrap_or(""); match field { "id" => ui.label( row.get("id") .and_then(|v| v.as_i64()) .unwrap_or(0) .to_string(), ), "name" => ui.label(t("name")), "contact" => ui.label(t("contact")), "email" => ui.label(t("email")), "phone" => ui.label(t("phone")), "website" => ui.label(t("website")), "notes" => label_trunc(ui, t("notes"), 60), "created_at" => ui.label(t("created_at")), "items_count" => ui.label( row.get("items_count") .and_then(|v| v.as_i64()) .unwrap_or(0) .to_string(), ), other => ui.label(format!("{}", other)), } } // Handle add dialog save and insert impl SuppliersView { fn handle_add_dialog(&mut self, ctx: &egui::Context, api_client: Option<&ApiClient>) { if let Some(Some(mut new_data)) = self.add_dialog.show_editor(ctx) { if let Some(api) = api_client { strip_editor_metadata(&mut new_data); match api.insert("suppliers", serde_json::Value::Object(new_data)) { Ok(resp) if resp.success => { self.load(api); } Ok(resp) => { self.last_error = Some(format!("Insert failed: {:?}", resp.error)); } Err(e) => { self.last_error = Some(format!("Insert error: {}", e)); } } } } } } fn strip_editor_metadata(map: &mut serde_json::Map) { let meta_keys: Vec = map .keys() .filter(|k| k.starts_with("__editor_")) .cloned() .collect(); for key in meta_keys { map.remove(&key); } }