From fdcc210fc4d469a29ac496b68630c14529fecf0a Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sat, 2 Aug 2025 10:31:17 -0700 Subject: [PATCH] feat: add status bar to UI for improved user feedback and information display --- src/main.py | 99 ++++++++++++++++++++++++++++++++------------ src/ui_manager.py | 102 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+), 26 deletions(-) diff --git a/src/main.py b/src/main.py index b049f8d..b44a509 100644 --- a/src/main.py +++ b/src/main.py @@ -108,7 +108,7 @@ class MedTrackerApp: self.root.grid_columnconfigure(0, weight=1) # Configure main frame grid for scaling - for i in range(2): + for i in range(3): # Changed from 2 to 3 to accommodate status bar main_frame.grid_rowconfigure(i, weight=1 if i == 1 else 0) main_frame.grid_columnconfigure(i, weight=3 if i == 1 else 1) logger.debug("Main frame and root grid configured for scaling.") @@ -155,9 +155,15 @@ class MedTrackerApp: self.tree: ttk.Treeview = table_ui["tree"] self.tree.bind("", self.handle_double_click) + # --- Create Status Bar --- + self.status_bar = self.ui_manager.create_status_bar(main_frame) + # Load data self.refresh_data_display() + # Initialize status bar with ready message + self.ui_manager.update_status("Application ready", "info") + def _setup_menu(self) -> None: """Set up the menu bar.""" menubar = tk.Menu(self.root) @@ -182,22 +188,29 @@ class MedTrackerApp: def _open_export_window(self) -> None: """Open the export window.""" + self.ui_manager.update_status("Opening export window", "info") ExportWindow(self.root, self.export_manager) def _open_pathology_manager(self) -> None: """Open the pathology management window.""" + self.ui_manager.update_status("Opening pathology manager", "info") PathologyManagementWindow( self.root, self.pathology_manager, self._refresh_ui_after_config_change ) def _open_medicine_manager(self) -> None: """Open the medicine management window.""" + self.ui_manager.update_status("Opening medicine manager", "info") MedicineManagementWindow( self.root, self.medicine_manager, self._refresh_ui_after_config_change ) def _refresh_ui_after_config_change(self) -> None: """Refresh UI components after pathology or medicine configuration changes.""" + self.ui_manager.update_status( + "Refreshing UI after configuration change", "info" + ) + # Clear caches in optimized data manager if hasattr(self.data_manager, "_invalidate_cache"): self.data_manager._invalidate_cache() @@ -238,14 +251,22 @@ class MedTrackerApp: # Refresh data display self.refresh_data_display() + # Update status to show completion + self.ui_manager.update_status("UI refreshed successfully", "success") + def handle_double_click(self, event: tk.Event) -> None: """Handle double-click event to edit an entry.""" logger.debug("Double-click event triggered on treeview.") if len(self.tree.get_children()) > 0: item_id = self.tree.selection()[0] item_values = self.tree.item(item_id, "values") + self.ui_manager.update_status( + f"Opening entry for {item_values[0]} for editing", "info" + ) logger.debug(f"Editing item_id={item_id}, values={item_values}") self._create_edit_window(item_id, item_values) + else: + self.ui_manager.update_status("No entries to edit", "warning") def _create_edit_window(self, item_id: str, values: tuple[str, ...]) -> None: """Create a new Toplevel window for editing an entry.""" @@ -347,8 +368,10 @@ class MedTrackerApp: values.append(note) + self.ui_manager.update_status("Saving changes...", "info") if self.data_manager.update_entry(original_date, values): edit_win.destroy() + self.ui_manager.update_status("Entry updated successfully!", "success") messagebox.showinfo( "Success", "Entry updated successfully!", parent=self.root ) @@ -358,6 +381,7 @@ class MedTrackerApp: # Check if it's a duplicate date issue df = self.data_manager.load_data() if original_date != date and not df.empty and date in df["date"].values: + self.ui_manager.update_status("Duplicate date found", "error") messagebox.showerror( "Error", f"An entry for date '{date}' already exists. " @@ -365,6 +389,7 @@ class MedTrackerApp: parent=edit_win, ) else: + self.ui_manager.update_status("Failed to save changes", "error") messagebox.showerror("Error", "Failed to save changes", parent=edit_win) def handle_window_closing(self) -> None: @@ -409,10 +434,13 @@ class MedTrackerApp: # Check if date is empty if not self.date_var.get().strip(): + self.ui_manager.update_status("Please enter a date", "error") messagebox.showerror("Error", "Please enter a date.", parent=self.root) return + self.ui_manager.update_status("Adding new entry...", "info") if self.data_manager.add_entry(entry): + self.ui_manager.update_status("Entry added successfully!", "success") messagebox.showinfo( "Success", "Entry added successfully!", parent=self.root ) @@ -422,6 +450,7 @@ class MedTrackerApp: # Check if it's a duplicate date by trying to load existing data df = self.data_manager.load_data() if not df.empty and self.date_var.get() in df["date"].values: + self.ui_manager.update_status("Duplicate entry found", "error") messagebox.showerror( "Error", f"An entry for date '{self.date_var.get()}' already exists. " @@ -429,6 +458,7 @@ class MedTrackerApp: parent=self.root, ) else: + self.ui_manager.update_status("Failed to add entry", "error") messagebox.showerror("Error", "Failed to add entry", parent=self.root) def _delete_entry(self, edit_win: tk.Toplevel, item_id: str) -> None: @@ -443,13 +473,16 @@ class MedTrackerApp: date: str = self.tree.item(item_id, "values")[0] logger.debug(f"Deleting entry with date={date}") + self.ui_manager.update_status("Deleting entry...", "info") if self.data_manager.delete_entry(date): edit_win.destroy() + self.ui_manager.update_status("Entry deleted successfully!", "success") messagebox.showinfo( "Success", "Entry deleted successfully!", parent=self.root ) self.refresh_data_display() else: + self.ui_manager.update_status("Failed to delete entry", "error") messagebox.showerror("Error", "Failed to delete entry", parent=edit_win) def _clear_entries(self) -> None: @@ -471,38 +504,52 @@ class MedTrackerApp: if children: self.tree.delete(*children) - # Load data from the CSV file - df: pd.DataFrame = self.data_manager.load_data() + try: + # Load data from the CSV file + df: pd.DataFrame = self.data_manager.load_data() - # Update the treeview with the data - if not df.empty: - # Build display columns dynamically (exclude dose columns for table view) - display_columns = ["date"] + # Update the treeview with the data + if not df.empty: + # Build display columns dynamically + # (exclude dose columns for table view) + display_columns = ["date"] - # Add pathology columns - for pathology_key in self.pathology_manager.get_pathology_keys(): - display_columns.append(pathology_key) + # Add pathology columns + for pathology_key in self.pathology_manager.get_pathology_keys(): + display_columns.append(pathology_key) - # Add medicine columns (without dose columns) - for medicine_key in self.medicine_manager.get_medicine_keys(): - display_columns.append(medicine_key) + # Add medicine columns (without dose columns) + for medicine_key in self.medicine_manager.get_medicine_keys(): + display_columns.append(medicine_key) - display_columns.append("note") + display_columns.append("note") - # Filter to only the columns we want to display - if all(col in df.columns for col in display_columns): - display_df = df[display_columns] + # Filter to only the columns we want to display + if all(col in df.columns for col in display_columns): + display_df = df[display_columns] + else: + # Fallback - just use all columns + display_df = df + + # Batch insert for better performance + for _index, row in display_df.iterrows(): + self.tree.insert(parent="", index="end", values=list(row)) + logger.debug(f"Loaded {len(display_df)} entries into treeview.") + + # Update the graph + self.graph_manager.update_graph(df) + + # Update status bar with file info + entry_count = len(df) if not df.empty else 0 + self.ui_manager.update_file_info(self.filename, entry_count) + if entry_count == 0: + self.ui_manager.update_status("No data to display", "warning") else: - # Fallback - just use all columns - display_df = df + self.ui_manager.update_status("Data loaded successfully", "success") - # Batch insert for better performance - for _index, row in display_df.iterrows(): - self.tree.insert(parent="", index="end", values=list(row)) - logger.debug(f"Loaded {len(display_df)} entries into treeview.") - - # Update the graph - self.graph_manager.update_graph(df) + except Exception as e: + logger.error(f"Error loading data: {e}") + self.ui_manager.update_status(f"Error loading data: {str(e)}", "error") if __name__ == "__main__": diff --git a/src/ui_manager.py b/src/ui_manager.py index fd0f5b2..6649801 100644 --- a/src/ui_manager.py +++ b/src/ui_manager.py @@ -28,6 +28,11 @@ class UIManager: self.medicine_manager = medicine_manager self.pathology_manager = pathology_manager + # Status bar attributes + self.status_bar: tk.Frame | None = None + self.status_label: tk.Label | None = None + self.file_info_label: tk.Label | None = None + def setup_application_icon(self, img_path: str) -> bool: """Set up the application icon.""" try: @@ -297,6 +302,103 @@ class UIManager: return button_frame + def create_status_bar(self, parent_frame: tk.Widget) -> tk.Frame: + """Create and configure the status bar at the bottom of the application.""" + # Create the status bar frame + self.status_bar = tk.Frame(parent_frame, relief=tk.SUNKEN, bd=1) + self.status_bar.grid(row=2, column=0, columnspan=2, sticky="ew", padx=5, pady=2) + + # Configure the parent to make the status bar stretch + parent_frame.grid_columnconfigure(0, weight=1) + + # Create status message label (left side) + self.status_label = tk.Label( + self.status_bar, + text="Ready", + anchor=tk.W, + font=("TkDefaultFont", 9), + padx=10, + pady=2, + ) + self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True) + + # Create file info label (right side) + self.file_info_label = tk.Label( + self.status_bar, + text="", + anchor=tk.E, + font=("TkDefaultFont", 9), + padx=10, + pady=2, + ) + self.file_info_label.pack(side=tk.RIGHT) + + return self.status_bar + + def update_status(self, message: str, message_type: str = "info") -> None: + """ + Update the status bar with a message. + + Args: + message: The message to display + message_type: Type of message ('info', 'success', 'warning', 'error') + """ + if not self.status_label: + return + + # Color mapping for different message types + colors = { + "info": "#000000", # Black + "success": "#28A745", # Green + "warning": "#FFC107", # Yellow/Orange + "error": "#DC3545", # Red + } + + color = colors.get(message_type, "#000000") + self.status_label.config(text=message, fg=color) + + # Clear the message after 5 seconds for non-info messages + if message_type != "info": + self.root.after(5000, lambda: self.update_status("Ready", "info")) + + def update_file_info(self, filename: str, entry_count: int = 0) -> None: + """ + Update the file information in the status bar. + + Args: + filename: Name of the current data file + entry_count: Number of entries in the file + """ + if not self.file_info_label: + return + + file_display = os.path.basename(filename) if filename else "No file" + info_text = f"{file_display}" + if entry_count > 0: + info_text += f" ({entry_count} entries)" + + self.file_info_label.config(text=info_text) + + def show_status_message(self, message: str, duration: int = 3000) -> None: + """ + Show a temporary status message for a specific duration. + + Args: + message: The message to display + duration: How long to show the message in milliseconds + """ + if not self.status_label: + return + + original_text = self.status_label.cget("text") + original_color = self.status_label.cget("fg") + + self.status_label.config(text=message, fg="#2E86AB") + self.root.after( + duration, + lambda: self.status_label.config(text=original_text, fg=original_color), + ) + def create_edit_window( self, values: tuple[str, ...], callbacks: dict[str, Callable] ) -> tk.Toplevel: