feat: Enhance UI feedback and improve data filtering logic

This commit is contained in:
William Valentin
2025-08-08 11:32:43 -07:00
parent 0252691e89
commit 61c8c72cf7
4 changed files with 137 additions and 42 deletions
+31 -14
View File
@@ -1017,9 +1017,8 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
self._mark_data_modified() # Mark for auto-save
edit_win.destroy()
self.ui_manager.update_status("Entry updated successfully!", "success")
messagebox.showinfo(
"Success", "Entry updated successfully!", parent=self.root
)
if hasattr(self.ui_manager, "show_toast"):
self.ui_manager.show_toast("Entry updated", 1500)
self._clear_entries()
self.refresh_data_display()
new_date = values[0]
@@ -1170,11 +1169,23 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
return
entry_data["note"] = validated_note
# Check entry completeness
is_complete, missing_fields = InputValidator.validate_entry_completeness(
entry_data
# Check entry completeness: require date and at least one of
# (any pathology score > 0) or (any medicine taken == 1)
missing_fields: list[str] = []
if not entry_data.get("date"):
missing_fields.append("Date")
has_pathology = any(
entry_data.get(k, 0) > 0
for k in self.pathology_manager.get_pathology_keys()
)
if not is_complete:
has_medicine = any(
entry_data.get(k, 0) == 1 for k in self.medicine_manager.get_medicine_keys()
)
if not (has_pathology or has_medicine):
missing_fields.append("At least one pathology score or medicine entry")
if missing_fields:
missing_msg = "Missing required data:\n" + "\n".join(
f"{field}" for field in missing_fields
)
@@ -1224,9 +1235,8 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
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
)
if hasattr(self.ui_manager, "show_toast"):
self.ui_manager.show_toast("Entry added", 1500)
self._clear_entries()
self.refresh_data_display()
added_date = entry[0]
@@ -1278,9 +1288,8 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
self._mark_data_modified() # Mark for auto-save
edit_win.destroy()
self.ui_manager.update_status("Entry deleted successfully!", "success")
messagebox.showinfo(
"Success", "Entry deleted successfully!", parent=self.root
)
if hasattr(self.ui_manager, "show_toast"):
self.ui_manager.show_toast("Entry deleted", 1500)
self.refresh_data_display()
if deleted_row:
@@ -1308,7 +1317,11 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
def _clear_entries(self) -> None:
"""Clear all input fields."""
logger.debug("Clearing input fields.")
self.date_var.set("")
# Keep date practical: default to today's date after clear
try:
self.date_var.set(datetime.now().strftime("%m/%d/%Y"))
except Exception:
self.date_var.set("")
for key in self.pathology_vars:
self.pathology_vars[key].set(0)
for key in self.medicine_vars:
@@ -1336,6 +1349,10 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
# Use efficient tree update to reduce flickering
self._update_tree_efficiently(df)
# Reapply last sort state if any
if hasattr(self.ui_manager, "reapply_last_sort"):
self.ui_manager.reapply_last_sort(self.tree)
# Update the graph (always use unfiltered data for complete picture)
# Graph gets preprocessed, use dedicated cached transformation
if hasattr(self.data_manager, "get_graph_ready_data"):
+13 -5
View File
@@ -192,11 +192,19 @@ class DataFilter:
for medicine_key, should_be_taken in medicine_filters.items():
if medicine_key in df.columns:
col = df[medicine_key]
# Medicine columns in tests contain empty string when not taken
if should_be_taken:
mask &= col.astype(str).str.len() > 0
else:
mask &= col.astype(str).str.len() == 0
# Prefer numeric/boolean interpretation when possible (0/1)
try:
numeric = pd.to_numeric(col, errors="coerce")
if should_be_taken:
mask &= numeric.fillna(0) == 1
else:
mask &= numeric.fillna(0) == 0
except Exception:
# Fallback to truthy string length semantics
if should_be_taken:
mask &= col.astype(str).str.len() > 0
else:
mask &= col.astype(str).str.len() == 0
return df[mask]
+22 -19
View File
@@ -38,33 +38,29 @@ class SearchFilterWidget:
self.pathology_manager = pathology_manager
self.logger = logger
# Initialize visibility state
# Visibility and UI init state
self.is_visible = False
self.search_history = SearchHistory()
self._ui_initialized = False
self.frame: ttk.LabelFrame | None = None
# Debouncing mechanism to reduce filter update frequency
self._update_timer = None
self._debounce_delay = 300 # milliseconds
self._update_timer: str | None = None
self._debounce_delay = 450 # milliseconds
# UI state variables
# History and UI state variables
self.search_history = SearchHistory()
self.search_var = tk.StringVar()
self.start_date_var = tk.StringVar()
self.end_date_var = tk.StringVar()
# Medicine filter variables
self.medicine_vars = {}
# Pathology filter variables
self.pathology_min_vars = {}
self.pathology_max_vars = {}
self._setup_ui()
self._bind_events()
# Medicine and pathology filter variables
self.medicine_vars: dict[str, tk.StringVar] = {}
self.pathology_min_vars: dict[str, tk.StringVar] = {}
self.pathology_max_vars: dict[str, tk.StringVar] = {}
def _setup_ui(self) -> None:
"""Set up the search and filter UI."""
# Main container - remove height limit to allow full horizontal stretch
# Main container
self.frame = ttk.LabelFrame(self.parent, text="Search & Filter", padding="5")
# Create main content frame without scrolling - use horizontal layout
@@ -442,12 +438,17 @@ class SearchFilterWidget:
self.status_label.config(text=status_text)
def get_widget(self) -> ttk.LabelFrame:
"""Get the main widget for embedding in UI."""
def get_widget(self) -> ttk.LabelFrame | None:
"""Get the main widget for embedding in UI (may be None until shown)."""
return self.frame
def show(self) -> None:
"""Show the search filter widget and configure the parent row."""
if not self._ui_initialized:
self._setup_ui()
self._bind_events()
self._ui_initialized = True
assert self.frame is not None
self.frame.grid(row=1, column=0, columnspan=3, sticky="nsew", padx=5, pady=2)
# Configure the parent grid row for horizontal layout (smaller minsize)
if hasattr(self.parent, "grid_rowconfigure"):
@@ -457,6 +458,8 @@ class SearchFilterWidget:
def hide(self) -> None:
"""Hide the search filter widget and reset the parent row."""
if not self.frame:
return
self.frame.grid_remove()
# Reset the parent grid row to not allocate space when hidden
if hasattr(self.parent, "grid_rowconfigure"):
@@ -466,7 +469,7 @@ class SearchFilterWidget:
def toggle(self) -> None:
"""Toggle visibility of the search and filter widget."""
if self.frame.winfo_viewable():
if self.frame and self.frame.winfo_viewable():
self.hide()
else:
self.show()
+71 -4
View File
@@ -392,10 +392,15 @@ class UIManager:
# Column sort state tracking
self._tree_sort_directions: dict[str, bool] = {}
self._last_sorted_column: str | None = None
self._last_sorted_ascending: bool | None = None
def make_sort_callback(col_name: str):
def _callback():
self.sort_tree_column(tree, col_name)
# Remember last sort state
self._last_sorted_column = col_name
self._last_sorted_ascending = self._tree_sort_directions.get(col_name)
return _callback
@@ -407,10 +412,12 @@ class UIManager:
tree.pack(side="left", fill="both", expand=True)
# Add scrollbar with optimized scroll handling
scrollbar = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)
tree.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side="right", fill="y")
# Add scrollbars with optimized scroll handling
vscroll = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)
hscroll = ttk.Scrollbar(table_frame, orient="horizontal", command=tree.xview)
tree.configure(yscrollcommand=vscroll.set, xscrollcommand=hscroll.set)
vscroll.pack(side="right", fill="y")
hscroll.pack(side="bottom", fill="x")
# Optimize tree scrolling performance
self._optimize_tree_scrolling(tree)
@@ -452,6 +459,50 @@ class UIManager:
# Update heading arrow (basic glyph)
direction_glyph = "" if ascending else ""
tree.heading(column, text=f"{column} {direction_glyph}")
# Re-apply alternating row tags after sort
self.normalize_tree_stripes(tree)
def _sort_tree_column_direction(
self, tree: ttk.Treeview, column: str, ascending: bool
) -> None:
"""Sort a treeview column in a specific direction without toggling state."""
data = []
for item in tree.get_children(""):
values = tree.item(item, "values")
try:
col_index = tree["columns"].index(column)
except ValueError:
continue
data.append((values[col_index], item, values))
def try_cast(v: Any):
for caster in (int, float):
try:
return caster(v)
except Exception:
continue
return str(v)
data.sort(key=lambda tup: try_cast(tup[0]), reverse=not ascending)
for index, (_value, item, _vals) in enumerate(data):
tree.move(item, "", index)
direction_glyph = "" if ascending else ""
tree.heading(column, text=f"{column} {direction_glyph}")
# Re-apply alternating row tags after sort
self.normalize_tree_stripes(tree)
def reapply_last_sort(self, tree: ttk.Treeview) -> None:
"""Reapply the last known sort to the tree after data refresh."""
if not self._last_sorted_column or self._last_sorted_ascending is None:
return
import contextlib
with contextlib.suppress(Exception):
self._sort_tree_column_direction(
tree, self._last_sorted_column, bool(self._last_sorted_ascending)
)
def diff_update_tree(self, tree: ttk.Treeview, df: pd.DataFrame) -> None:
"""Apply minimal changes to treeview vs full rebuild.
@@ -518,6 +569,22 @@ class UIManager:
tag = "evenrow" if idx % 2 == 0 else "oddrow"
tree.insert("", "end", values=list(row), tags=(tag,))
# Ensure alternating stripes are normalized after updates
self.normalize_tree_stripes(tree)
def normalize_tree_stripes(self, tree: ttk.Treeview) -> None:
"""Normalize alternating row tags based on current visual order.
Keeps even/odd striping consistent after inserts, deletes, and sorts.
"""
try:
for idx, item in enumerate(tree.get_children("")):
tag = "evenrow" if idx % 2 == 0 else "oddrow"
tree.item(item, tags=(tag,))
except Exception:
# Best-effort visual enhancement; ignore errors
pass
def create_graph_frame(self, parent_frame: ttk.Frame) -> ttk.LabelFrame:
"""Create and configure the graph frame."""
graph_frame: ttk.LabelFrame = ttk.LabelFrame(