aboutsummaryrefslogtreecommitdiff
path: root/src/ui/dashboard.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/dashboard.rs')
-rw-r--r--src/ui/dashboard.rs384
1 files changed, 384 insertions, 0 deletions
diff --git a/src/ui/dashboard.rs b/src/ui/dashboard.rs
new file mode 100644
index 0000000..3a40c97
--- /dev/null
+++ b/src/ui/dashboard.rs
@@ -0,0 +1,384 @@
+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()
+ }
+}