| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- 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::<String>();
- return format!("{} {}/{}/{}", time, d, m, year_short);
- }
- }
- }
- }
- date_str.to_string()
- }
- pub struct DashboardView {
- stats: DashboardStats,
- is_loading: bool,
- last_error: Option<String>,
- data_loaded: bool,
- asset_changes: Vec<serde_json::Value>,
- issue_changes: Vec<serde_json::Value>,
- }
- 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<T: std::fmt::Display>(
- &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()
- }
- }
|