Implement dose tracking functionality and enhance CSV migration

- Added a new migration script to introduce dose tracking columns in the CSV.
- Updated DataManager to handle new dose tracking columns and methods for adding doses.
- Enhanced MedTrackerApp to support dose entry and display for each medicine.
- Modified UIManager to create a scrollable input frame with dose tracking elements.
- Implemented tests for delete functionality, dose tracking, edit functionality, and scrollable input.
- Updated existing tests to ensure compatibility with the new CSV format and dose tracking features.
This commit is contained in:
William Valentin
2025-07-28 20:52:29 -07:00
parent d5423e98c0
commit e35a8af5c1
14 changed files with 1790 additions and 500 deletions
+167 -6
View File
@@ -80,12 +80,16 @@ class MedTrackerApp:
input_ui: dict[str, Any] = self.ui_manager.create_input_frame(main_frame)
self.input_frame: ttk.Frame = input_ui["frame"]
self.symptom_vars: dict[str, tk.IntVar] = input_ui["symptom_vars"]
self.medicine_vars: dict[str, list[tk.IntVar | ttk.Spinbox]] = input_ui[
"medicine_vars"
]
self.medicine_vars: dict[str, tuple[tk.IntVar, str]] = input_ui["medicine_vars"]
self.dose_buttons: dict[str, ttk.Button] = input_ui["dose_buttons"]
self.dose_entries: dict[str, ttk.Entry] = input_ui["dose_entries"]
self.dose_displays: dict[str, tk.Text] = input_ui["dose_displays"]
self.note_var: tk.StringVar = input_ui["note_var"]
self.date_var: tk.StringVar = input_ui["date_var"]
# Set up dose button callbacks
self._setup_dose_button_callbacks()
# Add buttons to input frame
self.ui_manager.add_buttons(
self.input_frame,
@@ -108,6 +112,75 @@ class MedTrackerApp:
# Load data
self.load_data()
def _setup_dose_button_callbacks(self) -> None:
"""Set up callbacks for dose tracking buttons."""
for medicine_name, button in self.dose_buttons.items():
button.config(
command=lambda med=medicine_name: self._take_medicine_dose(med)
)
# Update dose displays for today
self._update_dose_displays()
def _take_medicine_dose(self, medicine_name: str) -> None:
"""Record a dose of medicine taken right now."""
dose_entry = self.dose_entries[medicine_name]
dose = dose_entry.get().strip()
if not dose:
messagebox.showerror(
"Error",
f"Please enter a dose amount for {medicine_name}",
parent=self.root,
)
return
# Use today's date
today = self.date_var.get()
if not today:
from datetime import datetime
today = datetime.now().strftime("%m/%d/%Y")
self.date_var.set(today)
if self.data_manager.add_medicine_dose(today, medicine_name, dose):
messagebox.showinfo(
"Success",
f"{medicine_name.title()} dose recorded: {dose}",
parent=self.root,
)
# Clear dose entry
dose_entry.delete(0, tk.END)
# Update displays and reload data
self._update_dose_displays()
self.load_data()
else:
messagebox.showerror(
"Error", f"Failed to record {medicine_name} dose", parent=self.root
)
def _update_dose_displays(self) -> None:
"""Update the dose display areas with today's doses."""
today = self.date_var.get()
if not today:
return
for medicine_name, display in self.dose_displays.items():
doses = self.data_manager.get_today_medicine_doses(today, medicine_name)
display.config(state=tk.NORMAL)
display.delete(1.0, tk.END)
if doses:
dose_text = "\n".join(
[f"{timestamp}: {dose}" for timestamp, dose in doses]
)
display.insert(1.0, dose_text)
else:
display.insert(1.0, "No doses recorded today")
display.config(state=tk.DISABLED)
def on_double_click(self, event: tk.Event) -> None:
"""Handle double-click event to edit an entry."""
logger.debug("Double-click event triggered on treeview.")
@@ -146,6 +219,34 @@ class MedTrackerApp:
note: str,
) -> None:
"""Save the edited data to the CSV file."""
# Get existing dose data for this date to preserve it
bup_doses = ""
hydro_doses = ""
gaba_doses = ""
prop_doses = ""
# Try to get existing dose data
try:
existing_bup = self.data_manager.get_today_medicine_doses(
original_date, "bupropion"
)
existing_hydro = self.data_manager.get_today_medicine_doses(
original_date, "hydroxyzine"
)
existing_gaba = self.data_manager.get_today_medicine_doses(
original_date, "gabapentin"
)
existing_prop = self.data_manager.get_today_medicine_doses(
original_date, "propranolol"
)
bup_doses = "|".join([f"{ts}:{dose}" for ts, dose in existing_bup])
hydro_doses = "|".join([f"{ts}:{dose}" for ts, dose in existing_hydro])
gaba_doses = "|".join([f"{ts}:{dose}" for ts, dose in existing_gaba])
prop_doses = "|".join([f"{ts}:{dose}" for ts, dose in existing_prop])
except Exception as e:
logger.warning(f"Could not retrieve existing dose data: {e}")
values: list[str | int] = [
date,
dep,
@@ -153,9 +254,13 @@ class MedTrackerApp:
slp,
app,
bup,
bup_doses,
hydro,
hydro_doses,
gaba,
gaba_doses,
prop,
prop_doses,
note,
]
@@ -188,6 +293,30 @@ class MedTrackerApp:
def add_entry(self) -> None:
"""Add a new entry to the CSV file."""
# Get current doses for today
today = self.date_var.get()
bupropion_doses = ""
hydroxyzine_doses = ""
gabapentin_doses = ""
propranolol_doses = ""
if today:
bup_doses = self.data_manager.get_today_medicine_doses(today, "bupropion")
hydroxyzine_doses_list = self.data_manager.get_today_medicine_doses(
today, "hydroxyzine"
)
gaba_doses = self.data_manager.get_today_medicine_doses(today, "gabapentin")
prop_doses = self.data_manager.get_today_medicine_doses(
today, "propranolol"
)
bupropion_doses = "|".join([f"{ts}:{dose}" for ts, dose in bup_doses])
hydroxyzine_doses = "|".join(
[f"{ts}:{dose}" for ts, dose in hydroxyzine_doses_list]
)
gabapentin_doses = "|".join([f"{ts}:{dose}" for ts, dose in gaba_doses])
propranolol_doses = "|".join([f"{ts}:{dose}" for ts, dose in prop_doses])
entry: list[str | int] = [
self.date_var.get(),
self.symptom_vars["depression"].get(),
@@ -195,9 +324,13 @@ class MedTrackerApp:
self.symptom_vars["sleep"].get(),
self.symptom_vars["appetite"].get(),
self.medicine_vars["bupropion"][0].get(),
bupropion_doses,
self.medicine_vars["hydroxyzine"][0].get(),
hydroxyzine_doses,
self.medicine_vars["gabapentin"][0].get(),
gabapentin_doses,
self.medicine_vars["propranolol"][0].get(),
propranolol_doses,
self.note_var.get(),
]
logger.debug(f"Adding entry: {entry}")
@@ -241,7 +374,7 @@ class MedTrackerApp:
if self.data_manager.delete_entry(date):
edit_win.destroy()
messagebox.showinfo(
"Success", "Entry deleted successfully!", parent=edit_win
"Success", "Entry deleted successfully!", parent=self.root
)
self.load_data()
else:
@@ -257,6 +390,13 @@ class MedTrackerApp:
self.medicine_vars[key][0].set(0)
self.note_var.set("")
# Clear dose entry fields
for entry in self.dose_entries.values():
entry.delete(0, tk.END)
# Update dose displays
self._update_dose_displays()
def load_data(self) -> None:
"""Load data from the CSV file into the table and graph."""
logger.debug("Loading data from CSV.")
@@ -270,9 +410,30 @@ class MedTrackerApp:
# Update the treeview with the data
if not df.empty:
for _index, row in df.iterrows():
# Only show user-friendly columns in the table (not the dose columns)
display_columns = [
"date",
"depression",
"anxiety",
"sleep",
"appetite",
"bupropion",
"hydroxyzine",
"gabapentin",
"propranolol",
"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]
else:
# Fallback for old CSV format - just use all columns
display_df = df
for _index, row in display_df.iterrows():
self.tree.insert(parent="", index="end", values=list(row))
logger.debug(f"Loaded {len(df)} entries into treeview.")
logger.debug(f"Loaded {len(display_df)} entries into treeview.")
# Update the graph
self.graph_manager.update_graph(df)