feat: Implement search and filter functionality in MedTrackerApp
- Added DataFilter class for managing filtering and searching of medical data. - Introduced SearchFilterWidget for UI controls related to search and filters. - Integrated search and filter features into MedTrackerApp, allowing users to filter data by date range, medicine status, and pathology scores. - Implemented quick filters for common use cases (last week, last month, high symptoms). - Enhanced data loading and display logic to accommodate filtered data. - Added error handling for data loading issues. - Updated UIManager to reflect filter status in the application. - Improved entry validation in add_new_entry method to ensure data integrity.
This commit is contained in:
+223
-25
@@ -7,16 +7,21 @@ from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from auto_save import AutoSaveManager, BackupManager
|
||||
from constants import LOG_CLEAR, LOG_LEVEL, LOG_PATH
|
||||
from data_manager import DataManager
|
||||
from error_handler import ErrorHandler
|
||||
from export_manager import ExportManager
|
||||
from export_window import ExportWindow
|
||||
from graph_manager import GraphManager
|
||||
from init import logger
|
||||
from input_validator import InputValidator
|
||||
from medicine_management_window import MedicineManagementWindow
|
||||
from medicine_manager import MedicineManager
|
||||
from pathology_management_window import PathologyManagementWindow
|
||||
from pathology_manager import PathologyManager
|
||||
from search_filter import DataFilter
|
||||
from search_filter_ui import SearchFilterWidget
|
||||
from settings_window import SettingsWindow
|
||||
from theme_manager import ThemeManager
|
||||
from ui_manager import UIManager
|
||||
@@ -49,6 +54,9 @@ class MedTrackerApp:
|
||||
# Initialize theme manager first
|
||||
self.theme_manager: ThemeManager = ThemeManager(self.root, logger)
|
||||
|
||||
# Initialize error handler
|
||||
self.error_handler = ErrorHandler(logger)
|
||||
|
||||
if LOG_LEVEL == "DEBUG":
|
||||
logger.debug(f"Script name: {sys.argv[0]}")
|
||||
logger.debug(f"Logs path: {LOG_PATH}")
|
||||
@@ -65,6 +73,9 @@ class MedTrackerApp:
|
||||
self.pathology_manager,
|
||||
self.theme_manager,
|
||||
)
|
||||
|
||||
# Update error handler with UI manager for user feedback
|
||||
self.error_handler.ui_manager = self.ui_manager
|
||||
self.data_manager: DataManager = DataManager(
|
||||
self.filename, logger, self.medicine_manager, self.pathology_manager
|
||||
)
|
||||
@@ -75,6 +86,17 @@ class MedTrackerApp:
|
||||
icon_path = "./chart-671.png"
|
||||
self.ui_manager.setup_application_icon(img_path=icon_path)
|
||||
|
||||
# Initialize auto-save and backup managers
|
||||
self.auto_save_manager = AutoSaveManager(
|
||||
save_callback=self._auto_save_callback, interval_minutes=5, logger=logger
|
||||
)
|
||||
self.backup_manager = BackupManager(data_file_path=self.filename, logger=logger)
|
||||
|
||||
# Initialize search/filter system
|
||||
self.data_filter = DataFilter()
|
||||
self.current_filtered_data = None
|
||||
self.current_filtered_data: pd.DataFrame | None = None
|
||||
|
||||
# Set up the main application UI
|
||||
self._setup_main_ui()
|
||||
|
||||
@@ -87,6 +109,12 @@ class MedTrackerApp:
|
||||
# Center the window on screen
|
||||
self._center_window()
|
||||
|
||||
# Enable auto-save by default
|
||||
self.auto_save_manager.enable_auto_save()
|
||||
|
||||
# Create initial backup
|
||||
self.backup_manager.create_backup("startup")
|
||||
|
||||
def _center_window(self) -> None:
|
||||
"""Center the main window on the screen."""
|
||||
# Update the window to get accurate dimensions
|
||||
@@ -120,8 +148,9 @@ class MedTrackerApp:
|
||||
self.root.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# Configure main frame grid for scaling
|
||||
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)
|
||||
for i in range(4): # Changed from 3 to 4 to accommodate search filter
|
||||
# Row 2 (table) gets main weight, other rows have no weight initially
|
||||
main_frame.grid_rowconfigure(i, weight=1 if i == 2 else 0)
|
||||
main_frame.grid_columnconfigure(i, weight=3 if i == 1 else 1)
|
||||
logger.debug("Main frame and root grid configured for scaling.")
|
||||
|
||||
@@ -167,6 +196,18 @@ class MedTrackerApp:
|
||||
self.tree: ttk.Treeview = table_ui["tree"]
|
||||
self.tree.bind("<Double-1>", self.handle_double_click)
|
||||
|
||||
# --- Create Search/Filter Widget ---
|
||||
self.search_filter_widget = SearchFilterWidget(
|
||||
main_frame,
|
||||
self.data_filter,
|
||||
self._on_filter_update,
|
||||
self.medicine_manager,
|
||||
self.pathology_manager,
|
||||
logger,
|
||||
)
|
||||
# Initially hidden - can be toggled with Ctrl+F
|
||||
self.search_filter_visible = False
|
||||
|
||||
# --- Create Status Bar ---
|
||||
self.status_bar = self.ui_manager.create_status_bar(main_frame)
|
||||
|
||||
@@ -214,6 +255,12 @@ class MedTrackerApp:
|
||||
tools_menu.add_command(
|
||||
label="Refresh Data", command=self.refresh_data_display, accelerator="F5"
|
||||
)
|
||||
tools_menu.add_separator()
|
||||
tools_menu.add_command(
|
||||
label="Search & Filter",
|
||||
command=self._toggle_search_filter,
|
||||
accelerator="Ctrl+F",
|
||||
)
|
||||
|
||||
# Theme menu
|
||||
theme_menu = self.theme_manager.create_themed_menu(menubar, tearoff=0)
|
||||
@@ -270,6 +317,8 @@ class MedTrackerApp:
|
||||
self.root.bind("<Control-M>", lambda e: self._open_medicine_manager())
|
||||
self.root.bind("<Control-p>", lambda e: self._open_pathology_manager())
|
||||
self.root.bind("<Control-P>", lambda e: self._open_pathology_manager())
|
||||
self.root.bind("<Control-f>", lambda e: self._toggle_search_filter())
|
||||
self.root.bind("<Control-F>", lambda e: self._toggle_search_filter())
|
||||
self.root.bind("<Delete>", lambda e: self._delete_selected_entry())
|
||||
self.root.bind("<Escape>", lambda e: self._clear_selection())
|
||||
self.root.bind("<F1>", lambda e: self._show_keyboard_shortcuts())
|
||||
@@ -286,6 +335,7 @@ class MedTrackerApp:
|
||||
logger.info(" Ctrl+R/F5: Refresh data")
|
||||
logger.info(" Ctrl+M: Manage medicines")
|
||||
logger.info(" Ctrl+P: Manage pathologies")
|
||||
logger.info(" Ctrl+F: Toggle search/filter")
|
||||
logger.info(" Delete: Delete selected entry")
|
||||
logger.info(" Escape: Clear selection")
|
||||
logger.info(" F1: Show keyboard shortcuts help")
|
||||
@@ -302,6 +352,7 @@ File Operations:
|
||||
Data Management:
|
||||
• Ctrl+N: Clear entries
|
||||
• Ctrl+R / F5: Refresh data
|
||||
• Ctrl+F: Toggle search/filter
|
||||
|
||||
Window Management:
|
||||
• Ctrl+M: Manage medicines
|
||||
@@ -440,6 +491,7 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
||||
|
||||
self.ui_manager.update_status("Deleting entry...", "info")
|
||||
if self.data_manager.delete_entry(date):
|
||||
self._mark_data_modified() # Mark for auto-save
|
||||
self.ui_manager.update_status("Entry deleted successfully!", "success")
|
||||
messagebox.showinfo(
|
||||
"Success", "Entry deleted successfully!", parent=self.root
|
||||
@@ -573,6 +625,7 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
||||
|
||||
self.ui_manager.update_status("Saving changes...", "info")
|
||||
if self.data_manager.update_entry(original_date, values):
|
||||
self._mark_data_modified() # Mark for auto-save
|
||||
edit_win.destroy()
|
||||
self.ui_manager.update_status("Entry updated successfully!", "success")
|
||||
messagebox.showinfo(
|
||||
@@ -596,14 +649,124 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
||||
messagebox.showerror("Error", "Failed to save changes", parent=edit_win)
|
||||
|
||||
def handle_window_closing(self) -> None:
|
||||
"""Handle application closing with cleanup."""
|
||||
if messagebox.askokcancel(
|
||||
"Quit", "Do you want to quit the application?", parent=self.root
|
||||
):
|
||||
# Clean up auto-save and create final backup
|
||||
if hasattr(self, "auto_save_manager"):
|
||||
self.auto_save_manager.cleanup()
|
||||
|
||||
if hasattr(self, "backup_manager"):
|
||||
self.backup_manager.create_backup("shutdown")
|
||||
self.backup_manager.cleanup_old_backups(keep_count=5)
|
||||
|
||||
self.graph_manager.close()
|
||||
self.root.destroy()
|
||||
|
||||
def _auto_save_callback(self) -> None:
|
||||
"""Callback function for auto-save operations."""
|
||||
try:
|
||||
# Force refresh of data display to ensure consistency
|
||||
self.refresh_data_display()
|
||||
logger.debug("Auto-save callback executed successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Auto-save callback failed: {e}")
|
||||
|
||||
def _toggle_search_filter(self) -> None:
|
||||
"""Toggle the search and filter panel."""
|
||||
if self.search_filter_visible:
|
||||
self.search_filter_widget.hide()
|
||||
self.search_filter_visible = False
|
||||
self.ui_manager.update_status("Search panel hidden", "info")
|
||||
else:
|
||||
self.search_filter_widget.show()
|
||||
self.search_filter_visible = True
|
||||
self.ui_manager.update_status("Search panel shown", "info")
|
||||
|
||||
def _on_filter_update(self) -> None:
|
||||
"""Handle filter updates from the search widget."""
|
||||
self.refresh_data_display(apply_filters=True)
|
||||
|
||||
def _mark_data_modified(self) -> None:
|
||||
"""Mark that data has been modified for auto-save."""
|
||||
if hasattr(self, "auto_save_manager"):
|
||||
self.auto_save_manager.mark_data_modified()
|
||||
|
||||
def add_new_entry(self) -> None:
|
||||
"""Add a new entry to the CSV file."""
|
||||
"""Add a new entry to the CSV file with validation."""
|
||||
# Validate date first
|
||||
date_str = self.date_var.get()
|
||||
is_valid_date, date_error, _ = InputValidator.validate_date(date_str)
|
||||
if not is_valid_date:
|
||||
self.ui_manager.update_status(f"Invalid date: {date_error}", "error")
|
||||
messagebox.showerror("Invalid Date", date_error, parent=self.root)
|
||||
return
|
||||
|
||||
# Validate pathology scores
|
||||
entry_data = {"date": date_str}
|
||||
|
||||
for pathology_key in self.pathology_manager.get_pathology_keys():
|
||||
score = self.pathology_vars[pathology_key].get()
|
||||
is_valid_score, score_error, validated_score = (
|
||||
InputValidator.validate_pathology_score(score)
|
||||
)
|
||||
if not is_valid_score:
|
||||
self.ui_manager.update_status(
|
||||
f"Invalid pathology score: {score_error}", "error"
|
||||
)
|
||||
messagebox.showerror(
|
||||
"Invalid Pathology Score", score_error, parent=self.root
|
||||
)
|
||||
return
|
||||
entry_data[pathology_key] = validated_score
|
||||
|
||||
# Validate medicine data
|
||||
for medicine_key in self.medicine_manager.get_medicine_keys():
|
||||
taken = self.medicine_vars[medicine_key][0].get()
|
||||
is_valid_taken, taken_error, validated_taken = (
|
||||
InputValidator.validate_medicine_taken(taken)
|
||||
)
|
||||
if not is_valid_taken:
|
||||
self.ui_manager.update_status(
|
||||
f"Invalid medicine data: {taken_error}", "error"
|
||||
)
|
||||
messagebox.showerror(
|
||||
"Invalid Medicine Data", taken_error, parent=self.root
|
||||
)
|
||||
return
|
||||
entry_data[medicine_key] = validated_taken
|
||||
|
||||
# Validate note
|
||||
note_str = self.note_var.get()
|
||||
is_valid_note, note_error, validated_note = InputValidator.validate_note(
|
||||
note_str
|
||||
)
|
||||
if not is_valid_note:
|
||||
self.ui_manager.update_status(f"Invalid note: {note_error}", "error")
|
||||
messagebox.showerror("Invalid Note", note_error, parent=self.root)
|
||||
return
|
||||
entry_data["note"] = validated_note
|
||||
|
||||
# Check entry completeness
|
||||
is_complete, missing_fields = InputValidator.validate_entry_completeness(
|
||||
entry_data
|
||||
)
|
||||
if not is_complete:
|
||||
missing_msg = "Missing required data:\n" + "\n".join(
|
||||
f"• {field}" for field in missing_fields
|
||||
)
|
||||
self.ui_manager.update_status(
|
||||
"Entry incomplete: missing required data", "warning"
|
||||
)
|
||||
result = messagebox.askyesno(
|
||||
"Incomplete Entry",
|
||||
f"{missing_msg}\n\nSave entry anyway?",
|
||||
parent=self.root,
|
||||
)
|
||||
if not result:
|
||||
return
|
||||
|
||||
# Get current doses for today
|
||||
today = self.date_var.get()
|
||||
dose_values = {}
|
||||
@@ -632,17 +795,12 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
||||
entry.append(self.medicine_vars[medicine_key][0].get())
|
||||
entry.append(dose_values[f"{medicine_key}_doses"])
|
||||
|
||||
entry.append(self.note_var.get())
|
||||
entry.append(validated_note) # Use validated note
|
||||
logger.debug(f"Adding entry: {entry}")
|
||||
|
||||
# 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._mark_data_modified() # Mark for auto-save
|
||||
self.ui_manager.update_status("Entry added successfully!", "success")
|
||||
messagebox.showinfo(
|
||||
"Success", "Entry added successfully!", parent=self.root
|
||||
@@ -678,6 +836,7 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
||||
|
||||
self.ui_manager.update_status("Deleting entry...", "info")
|
||||
if self.data_manager.delete_entry(date):
|
||||
self._mark_data_modified() # Mark for auto-save
|
||||
edit_win.destroy()
|
||||
self.ui_manager.update_status("Entry deleted successfully!", "success")
|
||||
messagebox.showinfo(
|
||||
@@ -698,19 +857,26 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
||||
self.medicine_vars[key][0].set(0)
|
||||
self.note_var.set("")
|
||||
|
||||
def refresh_data_display(self) -> None:
|
||||
def refresh_data_display(self, apply_filters: bool = False) -> None:
|
||||
"""Load data from the CSV file into the table and graph."""
|
||||
logger.debug("Loading data from CSV.")
|
||||
|
||||
# Clear existing data in the treeview efficiently
|
||||
children = self.tree.get_children()
|
||||
if children:
|
||||
self.tree.delete(*children)
|
||||
|
||||
try:
|
||||
# Clear existing data in the treeview efficiently
|
||||
children = self.tree.get_children()
|
||||
if children:
|
||||
self.tree.delete(*children)
|
||||
|
||||
# Load data from the CSV file
|
||||
df: pd.DataFrame = self.data_manager.load_data()
|
||||
|
||||
# Apply filters if requested and filters are active
|
||||
if apply_filters and self.data_filter.get_filter_summary()["has_filters"]:
|
||||
df = self.data_filter.apply_filters(df)
|
||||
self.current_filtered_data = df
|
||||
else:
|
||||
self.current_filtered_data = None
|
||||
|
||||
# Update the treeview with the data
|
||||
if not df.empty:
|
||||
# Build display columns dynamically
|
||||
@@ -743,20 +909,52 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
||||
)
|
||||
logger.debug(f"Loaded {len(display_df)} entries into treeview.")
|
||||
|
||||
# Update the graph
|
||||
self.graph_manager.update_graph(df)
|
||||
# Update the graph (always use unfiltered data for complete picture)
|
||||
original_df = self.data_manager.load_data() if apply_filters else df
|
||||
self.graph_manager.update_graph(original_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")
|
||||
if apply_filters:
|
||||
total_entries = len(self.data_manager.load_data())
|
||||
else:
|
||||
self.ui_manager.update_status("Data loaded successfully", "success")
|
||||
total_entries = len(df)
|
||||
|
||||
displayed_entries = len(df)
|
||||
|
||||
if apply_filters and self.current_filtered_data is not None:
|
||||
self.ui_manager.update_file_info(
|
||||
self.filename,
|
||||
displayed_entries,
|
||||
f"filtered ({displayed_entries}/{total_entries})",
|
||||
)
|
||||
else:
|
||||
self.ui_manager.update_file_info(self.filename, displayed_entries)
|
||||
|
||||
if displayed_entries == 0:
|
||||
status_msg = (
|
||||
"No data matches filters" if apply_filters else "No data to display"
|
||||
)
|
||||
self.ui_manager.update_status(status_msg, "warning")
|
||||
else:
|
||||
status_msg = (
|
||||
"Filtered data loaded"
|
||||
if apply_filters
|
||||
else "Data loaded successfully"
|
||||
)
|
||||
self.ui_manager.update_status(status_msg, "success")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading data: {e}")
|
||||
self.ui_manager.update_status(f"Error loading data: {str(e)}", "error")
|
||||
self.error_handler.handle_data_error(
|
||||
operation="loading",
|
||||
data_type="CSV data",
|
||||
error=e,
|
||||
recovery_suggestions=[
|
||||
"Check if the data file exists and is not corrupted",
|
||||
"Verify file permissions",
|
||||
"Try restarting the application",
|
||||
"Check available disk space",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user