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
+182 -10
View File
@@ -53,10 +53,49 @@ class UIManager:
def create_input_frame(self, parent_frame: ttk.Frame) -> dict[str, Any]:
"""Create and configure the input frame with all widgets."""
input_frame: ttk.LabelFrame = ttk.LabelFrame(parent_frame, text="New Entry")
input_frame.grid(row=1, column=0, padx=10, pady=10, sticky="nsew")
# Create main container for the scrollable input frame
main_container = ttk.LabelFrame(parent_frame, text="New Entry")
main_container.grid(row=1, column=0, padx=10, pady=10, sticky="nsew")
main_container.grid_rowconfigure(0, weight=1)
main_container.grid_columnconfigure(0, weight=1)
# Create canvas and scrollbar for scrolling
canvas = tk.Canvas(main_container, highlightthickness=0)
scrollbar = ttk.Scrollbar(
main_container, orient="vertical", command=canvas.yview
)
canvas.configure(yscrollcommand=scrollbar.set)
# Create the actual input frame inside the canvas
input_frame = ttk.Frame(canvas)
input_frame.grid_columnconfigure(1, weight=1)
# Configure canvas scrolling
def configure_scroll_region(event=None):
canvas.configure(scrollregion=canvas.bbox("all"))
def on_mousewheel(event):
canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
input_frame.bind("<Configure>", configure_scroll_region)
canvas.bind("<MouseWheel>", on_mousewheel) # Windows/Linux
canvas.bind("<Button-4>", lambda e: canvas.yview_scroll(-1, "units")) # Linux
canvas.bind("<Button-5>", lambda e: canvas.yview_scroll(1, "units")) # Linux
# Place canvas and scrollbar in the container
canvas.grid(row=0, column=0, sticky="nsew")
scrollbar.grid(row=0, column=1, sticky="ns")
# Create window in canvas for the input frame
canvas_window = canvas.create_window((0, 0), window=input_frame, anchor="nw")
# Configure canvas window width to fill available space
def configure_canvas_width(event=None):
canvas_width = canvas.winfo_width()
canvas.itemconfig(canvas_window, width=canvas_width)
canvas.bind("<Configure>", configure_canvas_width)
# Create variables for symptoms
symptom_vars: dict[str, tk.IntVar] = {
"depression": tk.IntVar(value=0),
@@ -85,13 +124,15 @@ class UIManager:
variable=symptom_vars[var_name],
).grid(row=idx, column=1, sticky="ew")
# Medicine checkboxes
# Medicine tracking section with dose buttons
ttk.Label(input_frame, text="Treatment:").grid(
row=4, column=0, sticky="w", padx=5, pady=2
)
medicine_frame = ttk.LabelFrame(input_frame, text="Medicine")
medicine_frame.grid(row=4, column=1, padx=0, pady=10, sticky="nsew")
medicine_frame.grid_columnconfigure(0, weight=1)
# Store medicine variables and dose tracking
medicine_vars: dict[str, tuple[tk.IntVar, str]] = {
"bupropion": (tk.IntVar(value=0), "Bupropion 150/300 mg"),
"hydroxyzine": (tk.IntVar(value=0), "Hydroxyzine 25mg"),
@@ -99,11 +140,44 @@ class UIManager:
"propranolol": (tk.IntVar(value=0), "Propranolol 10mg"),
}
for idx, (_name, (var, text)) in enumerate(medicine_vars.items()):
ttk.Checkbutton(medicine_frame, text=text, variable=var).grid(
row=idx, column=0, sticky="w", padx=5, pady=2
# Store dose tracking elements for callback assignment later
dose_buttons: dict[str, ttk.Button] = {}
dose_entries: dict[str, ttk.Entry] = {}
dose_displays: dict[str, tk.Text] = {}
for idx, (med_name, (var, text)) in enumerate(medicine_vars.items()):
# Create a sub-frame for each medicine
med_sub_frame = ttk.Frame(medicine_frame)
med_sub_frame.grid(row=idx, column=0, sticky="ew", padx=5, pady=2)
med_sub_frame.grid_columnconfigure(1, weight=1)
# Checkbox for medicine taken
ttk.Checkbutton(med_sub_frame, text=text, variable=var).grid(
row=0, column=0, sticky="w"
)
# Dose tracking frame
dose_frame = ttk.Frame(med_sub_frame)
dose_frame.grid(row=0, column=1, sticky="ew", padx=(10, 0))
dose_frame.grid_columnconfigure(0, weight=1)
# Dose entry and button
dose_var = tk.StringVar()
dose_entry = ttk.Entry(dose_frame, textvariable=dose_var, width=10)
dose_entry.grid(row=0, column=0, sticky="ew", padx=(0, 5))
dose_button = ttk.Button(dose_frame, text=f"Take {med_name.title()}")
dose_button.grid(row=0, column=1)
# Display area for today's doses (read-only)
dose_display = tk.Text(dose_frame, height=2, width=30, wrap=tk.WORD)
dose_display.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(2, 0))
dose_display.config(state=tk.DISABLED)
dose_buttons[med_name] = dose_button
dose_entries[med_name] = dose_entry
dose_displays[med_name] = dose_display
# Note and Date fields
note_var: tk.StringVar = tk.StringVar()
date_var: tk.StringVar = tk.StringVar()
@@ -127,9 +201,12 @@ class UIManager:
# Return all UI elements and variables
return {
"frame": input_frame,
"frame": main_container,
"symptom_vars": symptom_vars,
"medicine_vars": medicine_vars,
"dose_buttons": dose_buttons,
"dose_entries": dose_entries,
"dose_displays": dose_displays,
"note_var": note_var,
"date_var": date_var,
}
@@ -240,8 +317,50 @@ class UIManager:
# Configure grid columns to expand properly
edit_win.grid_columnconfigure(1, weight=1)
# Unpack values
date, dep, anx, slp, app, bup, hydro, gaba, prop, note = values
# Unpack values - handle both old and new CSV formats
if len(values) == 10:
# Old format: date, dep, anx, slp, app, bup, hydro, gaba, prop, note
date, dep, anx, slp, app, bup, hydro, gaba, prop, note = values
bup_doses, hydro_doses, gaba_doses, prop_doses = "", "", "", ""
elif len(values) == 14:
# New format with dose tracking
(
date,
dep,
anx,
slp,
app,
bup,
bup_doses,
hydro,
hydro_doses,
gaba,
gaba_doses,
prop,
prop_doses,
note,
) = values
else:
# Fallback for unexpected format
self.logger.warning(f"Unexpected number of values in edit: {len(values)}")
# Pad with default values
values_list = list(values) + [""] * (14 - len(values))
(
date,
dep,
anx,
slp,
app,
bup,
bup_doses,
hydro,
hydro_doses,
gaba,
gaba_doses,
prop,
prop_doses,
note,
) = values_list[:14]
# Create variables and fields
vars_dict = self._create_edit_fields(edit_win, date, dep, anx, slp, app)
@@ -253,8 +372,21 @@ class UIManager:
)
vars_dict.update(med_vars)
# Note field
# Dose information display (read-only)
current_row += 1
self._add_dose_display_to_edit(
edit_win,
current_row,
{
"bupropion": bup_doses,
"hydroxyzine": hydro_doses,
"gabapentin": gaba_doses,
"propranolol": prop_doses,
},
)
# Note field
current_row += 2 # Account for dose display
vars_dict["note"] = tk.StringVar(value=str(note))
ttk.Label(edit_win, text="Note:").grid(
row=current_row, column=0, sticky="w", padx=5, pady=2
@@ -441,3 +573,43 @@ class UIManager:
text="Delete",
command=lambda: callbacks["delete"](parent),
).pack(side="left", padx=5)
def _add_dose_display_to_edit(
self, parent: tk.Toplevel, row: int, dose_data: dict[str, str]
) -> None:
"""Add dose information display to edit window."""
ttk.Label(parent, text="Recorded Doses:").grid(
row=row, column=0, sticky="w", padx=5, pady=2
)
dose_frame = ttk.LabelFrame(parent, text="Today's Doses (Read-Only)")
dose_frame.grid(row=row, column=1, padx=5, pady=2, sticky="ew")
for idx, (medicine, doses_str) in enumerate(dose_data.items()):
ttk.Label(dose_frame, text=f"{medicine.title()}:").grid(
row=idx, column=0, sticky="w", padx=5, pady=1
)
# Parse and display doses
if doses_str:
doses_display = []
for dose_entry in doses_str.split("|"):
if ":" in dose_entry:
timestamp, dose = dose_entry.split(":", 1)
# Format timestamp for display
try:
from datetime import datetime
dt = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S")
time_str = dt.strftime("%H:%M")
doses_display.append(f"{time_str}: {dose}")
except ValueError:
doses_display.append(dose_entry)
display_text = ", ".join(doses_display) if doses_display else "None"
else:
display_text = "None"
ttk.Label(dose_frame, text=display_text, wraplength=200).grid(
row=idx, column=1, sticky="w", padx=5, pady=1
)