use eframe::egui; use egui_extras::{Column, TableBuilder}; use crate::api::ApiClient; use crate::core::{fetch_dashboard_stats, get_asset_changes, get_issue_changes}; use crate::models::DashboardStats; fn format_date_short(date_str: &str) -> String { // Parse ISO format like "2024-10-17T01:05:14Z" and return "01:05 17/10/24" if let Some(parts) = date_str.split('T').next() { if let Some(time_part) = date_str.split('T').nth(1) { let time = &time_part[..5]; // HH:MM if let Some((y, rest)) = parts.split_once('-') { if let Some((m, d)) = rest.split_once('-') { let year_short = y.chars().skip(2).collect::(); return format!("{} {}/{}/{}", time, d, m, year_short); } } } } date_str.to_string() } pub struct DashboardView { stats: DashboardStats, is_loading: bool, last_error: Option, data_loaded: bool, asset_changes: Vec, issue_changes: Vec, } impl DashboardView { pub fn new() -> Self { Self { stats: DashboardStats::default(), is_loading: false, last_error: None, data_loaded: false, asset_changes: Vec::new(), issue_changes: Vec::new(), } } pub fn refresh_data(&mut self, api_client: &ApiClient) { self.is_loading = true; self.last_error = None; // Fetch dashboard stats using core module log::info!("Refreshing dashboard data..."); match fetch_dashboard_stats(api_client) { Ok(stats) => { log::info!( "Dashboard stats loaded: {} total assets", stats.total_assets ); self.stats = stats; // Load recent changes using core module self.asset_changes = get_asset_changes(api_client, 15).unwrap_or_default(); self.issue_changes = get_issue_changes(api_client, 12).unwrap_or_default(); self.is_loading = false; self.data_loaded = true; } Err(err) => { log::error!("Failed to load dashboard stats: {}", err); self.last_error = Some(format!("Failed to load stats: {}", err)); self.is_loading = false; } } } /// Check if the last error was a database timeout pub fn has_timeout_error(&self) -> bool { if let Some(error) = &self.last_error { error.contains("Database temporarily unavailable") } else { false } } pub fn show(&mut self, ui: &mut egui::Ui, api_client: Option<&ApiClient>) { // Auto-load data on first show if !self.data_loaded && !self.is_loading { if let Some(client) = api_client { self.refresh_data(client); } } ui.heading("Dashboard"); ui.separator(); ui.horizontal(|ui| { if ui.button("Refresh").clicked() { if let Some(client) = api_client { self.refresh_data(client); } } if self.is_loading { ui.spinner(); ui.label("Loading..."); } }); ui.add_space(12.0); // Error display if let Some(error) = &self.last_error { ui.label(format!("Error: {}", error)); ui.add_space(8.0); } // Stats cards - using horizontal layout with equal widths and padding ui.horizontal(|ui| { let available_width = ui.available_width(); let side_padding = 20.0; // Equal padding on both sides let spacing = 16.0; let frame_margin = 16.0 * 2.0; // inner_margin on both sides let stroke_width = 1.0 * 2.0; // stroke on both sides let total_card_overhead = frame_margin + stroke_width; // Calculate card content width accounting for frame overhead and side padding let usable_width = available_width - (side_padding * 2.0); let card_width = ((usable_width - (spacing * 2.0)) / 3.0) - total_card_overhead; // Add left padding ui.add_space(side_padding); self.show_stat_card( ui, "Total Assets", self.stats.total_assets, egui::Color32::from_rgb(33, 150, 243), card_width, ); ui.add_space(spacing); self.show_stat_card( ui, "Okay Items", self.stats.okay_items, egui::Color32::from_rgb(76, 175, 80), card_width, ); ui.add_space(spacing); self.show_stat_card( ui, "Attention", self.stats.attention_items, egui::Color32::from_rgb(244, 67, 54), card_width, ); // Add right padding (this will naturally happen with the remaining space) ui.add_space(side_padding); }); ui.add_space(24.0); // Recent changes tables side-by-side, fill remaining height let full_h = ui.available_height(); ui.horizontal(|ui| { let spacing = 16.0; let available = ui.available_width(); let half = (available - spacing) / 2.0; // Left column: Asset changes ui.allocate_ui_with_layout( egui::vec2(half, full_h), egui::Layout::top_down(egui::Align::Min), |ui| { ui.set_width(half); let col_w = ui.available_width(); ui.set_max_width(col_w); ui.heading("Recent Asset Changes"); ui.separator(); ui.add_space(8.0); if self.asset_changes.is_empty() { ui.label("No recent asset changes"); } else { ui.push_id("asset_changes_table", |ui| { let col_w = ui.available_width(); ui.set_width(col_w); // Set table body height based on remaining space let body_h = (ui.available_height() - 36.0).max(180.0); let table = TableBuilder::new(ui) .striped(true) .resizable(false) .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) .column(Column::remainder()) .column(Column::exact(140.0)) .column(Column::exact(120.0)) .column(Column::exact(120.0)) .min_scrolled_height(body_h); table .header(20.0, |mut header| { header.col(|ui| { ui.strong("Asset"); }); header.col(|ui| { ui.strong("Change"); }); header.col(|ui| { ui.strong("Date"); }); header.col(|ui| { ui.strong("User"); }); }) .body(|mut body| { for change in &self.asset_changes { let asset = change .get("asset_tag") .and_then(|v| v.as_str()) .unwrap_or("N/A"); let summary = change .get("changes") .and_then(|v| v.as_str()) .unwrap_or("N/A"); let date_raw = change .get("date") .and_then(|v| v.as_str()) .unwrap_or("N/A"); let date = format_date_short(date_raw); let user = change .get("user") .and_then(|v| v.as_str()) .unwrap_or("System"); body.row(24.0, |mut row| { row.col(|ui| { ui.add(egui::Label::new(asset).truncate()); }); row.col(|ui| { let label = egui::Label::new(summary).truncate(); ui.add(label).on_hover_text(summary); }); row.col(|ui| { ui.add(egui::Label::new(&date).truncate()); }); row.col(|ui| { ui.add(egui::Label::new(user).truncate()); }); }); } }); }); } }, ); ui.add_space(spacing); // Right column: Issue changes ui.allocate_ui_with_layout( egui::vec2(half, full_h), egui::Layout::top_down(egui::Align::Min), |ui| { ui.set_width(half); let col_w = ui.available_width(); ui.set_max_width(col_w); ui.heading("Recent Issue Updates"); ui.separator(); ui.add_space(8.0); if self.issue_changes.is_empty() { ui.label("No recent issue updates"); } else { ui.push_id("issue_changes_table", |ui| { let col_w = ui.available_width(); ui.set_width(col_w); let body_h = (ui.available_height() - 36.0).max(180.0); let table = TableBuilder::new(ui) .striped(true) .resizable(false) .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) .column(Column::remainder()) .column(Column::exact(140.0)) .column(Column::exact(120.0)) .column(Column::exact(120.0)) .min_scrolled_height(body_h); table .header(20.0, |mut header| { header.col(|ui| { ui.strong("Issue"); }); header.col(|ui| { ui.strong("Change"); }); header.col(|ui| { ui.strong("Date"); }); header.col(|ui| { ui.strong("User"); }); }) .body(|mut body| { for change in &self.issue_changes { let issue = change .get("issue") .and_then(|v| v.as_str()) .unwrap_or("N/A"); let summary = change .get("changes") .and_then(|v| v.as_str()) .unwrap_or("N/A"); let date_raw = change .get("date") .and_then(|v| v.as_str()) .unwrap_or("N/A"); let date = format_date_short(date_raw); let user = change .get("user") .and_then(|v| v.as_str()) .unwrap_or("System"); body.row(24.0, |mut row| { row.col(|ui| { ui.add(egui::Label::new(issue).truncate()); }); row.col(|ui| { let label = egui::Label::new(summary).truncate(); ui.add(label).on_hover_text(summary); }); row.col(|ui| { ui.add(egui::Label::new(&date).truncate()); }); row.col(|ui| { ui.add(egui::Label::new(user).truncate()); }); }); } }); }); } }, ); }); } fn show_stat_card( &self, ui: &mut egui::Ui, label: &str, value: T, color: egui::Color32, width: f32, ) { // Use default widget background - adapts to light/dark mode automatically egui::Frame::default() .corner_radius(8.0) .inner_margin(16.0) .fill(ui.visuals().widgets.noninteractive.weak_bg_fill) .stroke(egui::Stroke::new(1.5, color)) .show(ui, |ui| { ui.set_min_width(width); ui.set_max_width(width); ui.set_min_height(100.0); ui.vertical_centered(|ui| { ui.label(egui::RichText::new(label).size(14.0).color(color)); ui.add_space(8.0); ui.label( egui::RichText::new(format!("{}", value)) .size(32.0) .strong(), ); }); }); } } impl Default for DashboardView { fn default() -> Self { Self::new() } }