feat: Enhance dose tracking functionality in edit window and add punch button support

This commit is contained in:
William Valentin
2025-07-28 21:31:38 -07:00
parent e35a8af5c1
commit 760aa40a8c
7 changed files with 614 additions and 185 deletions
+32 -109
View File
@@ -81,15 +81,9 @@ class MedTrackerApp:
self.input_frame: ttk.Frame = input_ui["frame"]
self.symptom_vars: dict[str, tk.IntVar] = input_ui["symptom_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,
@@ -112,75 +106,6 @@ 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.")
@@ -194,14 +119,39 @@ class MedTrackerApp:
"""Create a new Toplevel window for editing an entry."""
original_date = values[0] # Store the original date
# Get the full row data from the CSV (including dose columns)
df = self.data_manager.load_data()
if not df.empty and original_date in df["date"].values:
full_row = df[df["date"] == original_date].iloc[0]
# Convert to tuple in the expected order for the edit window
full_values = (
full_row["date"],
full_row["depression"],
full_row["anxiety"],
full_row["sleep"],
full_row["appetite"],
full_row["bupropion"],
full_row["bupropion_doses"],
full_row["hydroxyzine"],
full_row["hydroxyzine_doses"],
full_row["gabapentin"],
full_row["gabapentin_doses"],
full_row["propranolol"],
full_row["propranolol_doses"],
full_row["note"],
)
else:
# Fallback to the table values if full data not found
full_values = values
# Define callbacks for edit window buttons
callbacks: dict[str, Callable] = {
"save": lambda win, *args: self._save_edit(win, original_date, *args),
"delete": lambda win: self._delete_entry(win, item_id),
}
# Create edit window using UI manager
_: tk.Toplevel = self.ui_manager.create_edit_window(values, callbacks)
# Create edit window using UI manager with full data
_: tk.Toplevel = self.ui_manager.create_edit_window(full_values, callbacks)
def _save_edit(
self,
@@ -217,36 +167,9 @@ class MedTrackerApp:
gaba: int,
prop: int,
note: str,
dose_data: dict[str, 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,
@@ -254,13 +177,13 @@ class MedTrackerApp:
slp,
app,
bup,
bup_doses,
dose_data.get("bupropion", ""),
hydro,
hydro_doses,
dose_data.get("hydroxyzine", ""),
gaba,
gaba_doses,
dose_data.get("gabapentin", ""),
prop,
prop_doses,
dose_data.get("propranolol", ""),
note,
]
+183 -70
View File
@@ -4,7 +4,7 @@ import sys
import tkinter as tk
from collections.abc import Callable
from datetime import datetime
from tkinter import ttk
from tkinter import messagebox, ttk
from typing import Any
from PIL import Image, ImageTk
@@ -124,7 +124,7 @@ class UIManager:
variable=symptom_vars[var_name],
).grid(row=idx, column=1, sticky="ew")
# Medicine tracking section with dose buttons
# Medicine tracking section (simplified)
ttk.Label(input_frame, text="Treatment:").grid(
row=4, column=0, sticky="w", padx=5, pady=2
)
@@ -132,7 +132,7 @@ class UIManager:
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
# Store medicine variables (checkboxes only)
medicine_vars: dict[str, tuple[tk.IntVar, str]] = {
"bupropion": (tk.IntVar(value=0), "Bupropion 150/300 mg"),
"hydroxyzine": (tk.IntVar(value=0), "Hydroxyzine 25mg"),
@@ -140,44 +140,12 @@ class UIManager:
"propranolol": (tk.IntVar(value=0), "Propranolol 10mg"),
}
# 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"
for idx, (_med_name, (var, text)) in enumerate(medicine_vars.items()):
# Just checkbox for medicine taken
ttk.Checkbutton(medicine_frame, text=text, variable=var).grid(
row=idx, column=0, sticky="w", padx=5, pady=2
)
# 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()
@@ -204,9 +172,6 @@ class UIManager:
"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,
}
@@ -372,9 +337,9 @@ class UIManager:
)
vars_dict.update(med_vars)
# Dose information display (read-only)
# Dose information display (editable)
current_row += 1
self._add_dose_display_to_edit(
dose_vars = self._add_dose_display_to_edit(
edit_win,
current_row,
{
@@ -384,6 +349,7 @@ class UIManager:
"propranolol": prop_doses,
},
)
vars_dict.update(dose_vars)
# Note field
current_row += 2 # Account for dose display
@@ -543,11 +509,23 @@ class UIManager:
button_frame: ttk.Frame = ttk.Frame(parent)
button_frame.grid(row=row, column=0, columnspan=2, pady=10)
# Save button
ttk.Button(
button_frame,
text="Save",
command=lambda: callbacks["save"](
# Save button - create a custom callback to handle dose data
def save_with_doses():
# Extract dose data from the text widgets
dose_data = {}
for medicine in ["bupropion", "hydroxyzine", "gabapentin", "propranolol"]:
dose_text_key = f"{medicine}_doses_text"
if dose_text_key in vars_dict and isinstance(
vars_dict[dose_text_key], tk.Text
):
raw_text = vars_dict[dose_text_key].get(1.0, tk.END).strip()
dose_data[medicine] = self._parse_dose_text(
raw_text, vars_dict["date"].get()
)
else:
dose_data[medicine] = ""
callbacks["save"](
parent,
vars_dict["date"].get(),
vars_dict["depression"].get(),
@@ -559,7 +537,13 @@ class UIManager:
vars_dict["gabapentin"].get(),
vars_dict["propranolol"].get(),
vars_dict["note"].get(),
),
dose_data,
)
ttk.Button(
button_frame,
text="Save",
command=save_with_doses,
).pack(side="left", padx=5)
# Cancel button
@@ -576,40 +560,169 @@ class UIManager:
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(
) -> dict[str, tk.Text]:
"""Add comprehensive dose tracking to edit window with punch buttons."""
ttk.Label(parent, text="Dose Tracking:").grid(
row=row, column=0, sticky="w", padx=5, pady=2
)
dose_frame = ttk.LabelFrame(parent, text="Today's Doses (Read-Only)")
dose_frame = ttk.LabelFrame(parent, text="Medicine Doses")
dose_frame.grid(row=row, column=1, padx=5, pady=2, sticky="ew")
dose_frame.grid_columnconfigure(2, weight=1)
dose_vars = {}
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
)
# Medicine label
med_label = ttk.Label(dose_frame, text=f"{medicine.title()}:")
med_label.grid(row=idx, column=0, sticky="w", padx=5, pady=2)
# Parse and display doses
# Dose entry field for new doses
dose_entry_var = tk.StringVar()
dose_entry = ttk.Entry(dose_frame, textvariable=dose_entry_var, width=12)
dose_entry.grid(row=idx, column=1, sticky="w", padx=5, pady=2)
# Store entry variable in dose_vars for access from punch button
dose_vars[f"{medicine}_entry_var"] = dose_entry_var
# Display area for existing doses (editable)
dose_text = tk.Text(dose_frame, height=3, width=40, wrap=tk.WORD)
dose_text.grid(row=idx, column=2, sticky="ew", padx=5, pady=2)
# Store text widget in dose_vars
dose_vars[f"{medicine}_doses_text"] = dose_text
# Punch button to record dose immediately
punch_button = ttk.Button(
dose_frame,
text=f"Take {medicine.title()}",
width=15,
command=lambda med=medicine: self._punch_dose_in_edit(med, dose_vars),
)
punch_button.grid(row=idx, column=3, sticky="w", padx=5, pady=2)
# Parse and format doses for editing
if doses_str:
doses_display = []
for dose_entry in doses_str.split("|"):
if ":" in dose_entry:
timestamp, dose = dose_entry.split(":", 1)
formatted_doses = []
for dose_entry_str in doses_str.split("|"):
if ":" in dose_entry_str:
timestamp, dose = dose_entry_str.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}")
formatted_doses.append(f"{time_str}: {dose}")
except ValueError:
doses_display.append(dose_entry)
formatted_doses.append(dose_entry_str)
display_text = ", ".join(doses_display) if doses_display else "None"
if formatted_doses:
dose_text.insert(1.0, "\n".join(formatted_doses))
else:
dose_text.insert(1.0, "No doses recorded")
else:
display_text = "None"
dose_text.insert(1.0, "No doses recorded")
ttk.Label(dose_frame, text=display_text, wraplength=200).grid(
row=idx, column=1, sticky="w", padx=5, pady=1
# Add help text below the dose display
help_label = ttk.Label(
dose_frame,
text="Format: HH:MM: dose",
font=("TkDefaultFont", 8),
foreground="gray",
)
help_label.grid(row=idx, column=4, sticky="w", padx=5, pady=2)
return dose_vars
def _punch_dose_in_edit(self, medicine_name: str, dose_vars: dict) -> None:
"""Handle punch dose button in edit window."""
dose_entry_var = dose_vars.get(f"{medicine_name}_entry_var")
dose_text_widget = dose_vars.get(f"{medicine_name}_doses_text")
if not dose_entry_var or not dose_text_widget:
return
dose = dose_entry_var.get().strip()
if not dose:
messagebox.showerror(
"Error",
f"Please enter a dose amount for {medicine_name}",
)
return
# Get current time
now = datetime.now()
time_str = now.strftime("%H:%M")
# Get current content
current_content = dose_text_widget.get(1.0, tk.END).strip()
# Add new dose entry
new_dose_line = f"{time_str}: {dose}"
if current_content == "No doses recorded" or not current_content:
dose_text_widget.delete(1.0, tk.END)
dose_text_widget.insert(1.0, new_dose_line)
else:
dose_text_widget.insert(tk.END, f"\n{new_dose_line}")
# Clear the entry field
dose_entry_var.set("")
# Show success message
messagebox.showinfo(
"Success",
f"{medicine_name.title()} dose recorded: {dose} at {time_str}",
)
def _parse_dose_text(self, text: str, date: str) -> str:
"""Parse dose text from edit window back to CSV format."""
if not text or text == "No doses recorded":
return ""
lines = text.strip().split("\n")
dose_entries = []
for line in lines:
line = line.strip()
if ":" in line and line != "No doses recorded":
try:
# Try to parse HH:MM: dose format
# Split on ': ' (colon followed by space) to separate time from dose
if ": " in line:
time_part, dose_part = line.split(": ", 1)
else:
# Fallback: split on first colon after HH:MM pattern
colon_indices = [
i for i, char in enumerate(line) if char == ":"
]
if len(colon_indices) >= 2:
# Take everything up to the second colon as time
second_colon_idx = colon_indices[1]
time_part = line[:second_colon_idx]
dose_part = line[second_colon_idx + 1 :].strip()
else:
continue
dose_part = dose_part.strip()
# Create timestamp for today
from datetime import datetime
time_str = time_part.strip()
# Parse just the time (HH:MM format)
time_obj = datetime.strptime(time_str, "%H:%M")
# Create full timestamp with today's date
today = datetime.strptime(date, "%m/%d/%Y")
full_timestamp = today.replace(
hour=time_obj.hour, minute=time_obj.minute, second=0
)
timestamp_str = full_timestamp.strftime("%Y-%m-%d %H:%M:%S")
dose_entries.append(f"{timestamp_str}:{dose_part}")
except ValueError:
# If parsing fails, skip this line
continue
return "|".join(dose_entries)