e35a8af5c1
- 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.
446 lines
16 KiB
Python
446 lines
16 KiB
Python
import os
|
|
import sys
|
|
import tkinter as tk
|
|
from collections.abc import Callable
|
|
from tkinter import messagebox
|
|
from typing import Any
|
|
|
|
import pandas as pd
|
|
|
|
from constants import LOG_LEVEL, LOG_PATH
|
|
from data_manager import DataManager
|
|
from graph_manager import GraphManager
|
|
from init import logger
|
|
from ui_manager import UIManager
|
|
|
|
|
|
class MedTrackerApp:
|
|
def __init__(self, root: tk.Tk) -> None:
|
|
self.root: tk.Tk = root
|
|
self.root.resizable(True, True)
|
|
self.root.title("Thechart - medication tracker")
|
|
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
|
|
|
# Set up data file
|
|
self.filename: str = "thechart_data.csv"
|
|
first_argument: str = ""
|
|
|
|
if len(sys.argv) > 1:
|
|
first_argument: str = sys.argv[1]
|
|
if os.path.exists(first_argument):
|
|
self.filename = first_argument
|
|
logger.info(f"Using data file: {first_argument}")
|
|
else:
|
|
logger.warning(
|
|
f"Data file {first_argument} doesn't exist. \
|
|
Using default file: {self.filename}"
|
|
)
|
|
|
|
if LOG_LEVEL == "DEBUG":
|
|
logger.debug(f"Script name: {sys.argv[0]}")
|
|
logger.debug(f"Logs path: {LOG_PATH}")
|
|
logger.debug(f"First argument: {first_argument}")
|
|
|
|
# Initialize managers
|
|
self.ui_manager: UIManager = UIManager(root, logger)
|
|
self.data_manager: DataManager = DataManager(self.filename, logger)
|
|
|
|
# Set up application icon
|
|
icon_path: str = "chart-671.png"
|
|
if not os.path.exists(icon_path) and os.path.exists("./chart-671.png"):
|
|
icon_path = "./chart-671.png"
|
|
self.ui_manager.setup_icon(img_path=icon_path)
|
|
|
|
# Set up the main application UI
|
|
self._setup_main_ui()
|
|
|
|
def _setup_main_ui(self) -> None:
|
|
"""Set up the main UI components."""
|
|
import tkinter.ttk as ttk
|
|
|
|
# --- Main Frame ---
|
|
main_frame: ttk.Frame = ttk.Frame(self.root, padding="10")
|
|
main_frame.grid(row=0, column=0, sticky="nsew")
|
|
|
|
# Configure root window grid
|
|
self.root.grid_rowconfigure(0, weight=1)
|
|
self.root.grid_columnconfigure(0, weight=1)
|
|
|
|
# Configure main frame grid for scaling
|
|
for i in range(2):
|
|
main_frame.grid_rowconfigure(i, weight=1 if i == 1 else 0)
|
|
main_frame.grid_columnconfigure(i, weight=3 if i == 1 else 1)
|
|
logger.debug("Main frame and root grid configured for scaling.")
|
|
|
|
# --- Create Graph Frame ---
|
|
graph_frame: ttk.Frame = self.ui_manager.create_graph_frame(main_frame)
|
|
self.graph_manager: GraphManager = GraphManager(graph_frame)
|
|
|
|
# --- Create Input Frame ---
|
|
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, 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,
|
|
[
|
|
{
|
|
"text": "Add Entry",
|
|
"command": self.add_entry,
|
|
"fill": "both",
|
|
"expand": True,
|
|
},
|
|
{"text": "Quit", "command": self.on_closing},
|
|
],
|
|
)
|
|
|
|
# --- Create Table Frame ---
|
|
table_ui: dict[str, Any] = self.ui_manager.create_table_frame(main_frame)
|
|
self.tree: ttk.Treeview = table_ui["tree"]
|
|
self.tree.bind("<Double-1>", self.on_double_click)
|
|
|
|
# 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.")
|
|
if len(self.tree.get_children()) > 0:
|
|
item_id = self.tree.selection()[0]
|
|
item_values = self.tree.item(item_id, "values")
|
|
logger.debug(f"Editing item_id={item_id}, values={item_values}")
|
|
self._create_edit_window(item_id, item_values)
|
|
|
|
def _create_edit_window(self, item_id: str, values: tuple[str, ...]) -> None:
|
|
"""Create a new Toplevel window for editing an entry."""
|
|
original_date = values[0] # Store the original date
|
|
|
|
# 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)
|
|
|
|
def _save_edit(
|
|
self,
|
|
edit_win: tk.Toplevel,
|
|
original_date: str,
|
|
date: str,
|
|
dep: int,
|
|
anx: int,
|
|
slp: int,
|
|
app: int,
|
|
bup: int,
|
|
hydro: int,
|
|
gaba: int,
|
|
prop: int,
|
|
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,
|
|
anx,
|
|
slp,
|
|
app,
|
|
bup,
|
|
bup_doses,
|
|
hydro,
|
|
hydro_doses,
|
|
gaba,
|
|
gaba_doses,
|
|
prop,
|
|
prop_doses,
|
|
note,
|
|
]
|
|
|
|
if self.data_manager.update_entry(original_date, values):
|
|
edit_win.destroy()
|
|
messagebox.showinfo(
|
|
"Success", "Entry updated successfully!", parent=self.root
|
|
)
|
|
self._clear_entries()
|
|
self.load_data()
|
|
else:
|
|
# Check if it's a duplicate date issue
|
|
df = self.data_manager.load_data()
|
|
if original_date != date and not df.empty and date in df["date"].values:
|
|
messagebox.showerror(
|
|
"Error",
|
|
f"An entry for date '{date}' already exists. "
|
|
"Please use a different date.",
|
|
parent=edit_win,
|
|
)
|
|
else:
|
|
messagebox.showerror("Error", "Failed to save changes", parent=edit_win)
|
|
|
|
def on_closing(self) -> None:
|
|
if messagebox.askokcancel(
|
|
"Quit", "Do you want to quit the application?", parent=self.root
|
|
):
|
|
self.graph_manager.close()
|
|
self.root.destroy()
|
|
|
|
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(),
|
|
self.symptom_vars["anxiety"].get(),
|
|
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}")
|
|
|
|
# Check if date is empty
|
|
if not self.date_var.get().strip():
|
|
messagebox.showerror("Error", "Please enter a date.", parent=self.root)
|
|
return
|
|
|
|
if self.data_manager.add_entry(entry):
|
|
messagebox.showinfo(
|
|
"Success", "Entry added successfully!", parent=self.root
|
|
)
|
|
self._clear_entries()
|
|
self.load_data()
|
|
else:
|
|
# Check if it's a duplicate date by trying to load existing data
|
|
df = self.data_manager.load_data()
|
|
if not df.empty and self.date_var.get() in df["date"].values:
|
|
messagebox.showerror(
|
|
"Error",
|
|
f"An entry for date '{self.date_var.get()}' already exists. "
|
|
"Please use a different date or edit the existing entry.",
|
|
parent=self.root,
|
|
)
|
|
else:
|
|
messagebox.showerror("Error", "Failed to add entry", parent=self.root)
|
|
|
|
def _delete_entry(self, edit_win: tk.Toplevel, item_id: str) -> None:
|
|
"""Delete the selected entry from the CSV file."""
|
|
logger.debug(f"Delete requested for item_id={item_id}")
|
|
if messagebox.askyesno(
|
|
"Delete Entry",
|
|
"Are you sure you want to delete this entry?",
|
|
parent=edit_win,
|
|
):
|
|
# Get the date of the entry to delete
|
|
date: str = self.tree.item(item_id, "values")[0]
|
|
logger.debug(f"Deleting entry with date={date}")
|
|
|
|
if self.data_manager.delete_entry(date):
|
|
edit_win.destroy()
|
|
messagebox.showinfo(
|
|
"Success", "Entry deleted successfully!", parent=self.root
|
|
)
|
|
self.load_data()
|
|
else:
|
|
messagebox.showerror("Error", "Failed to delete entry", parent=edit_win)
|
|
|
|
def _clear_entries(self) -> None:
|
|
"""Clear all input fields."""
|
|
logger.debug("Clearing input fields.")
|
|
self.date_var.set("")
|
|
for key in self.symptom_vars:
|
|
self.symptom_vars[key].set(0)
|
|
for key in self.medicine_vars:
|
|
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.")
|
|
|
|
# Clear existing data in the treeview
|
|
for i in self.tree.get_children():
|
|
self.tree.delete(i)
|
|
|
|
# Load data from the CSV file
|
|
df: pd.DataFrame = self.data_manager.load_data()
|
|
|
|
# Update the treeview with the data
|
|
if not df.empty:
|
|
# 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(display_df)} entries into treeview.")
|
|
|
|
# Update the graph
|
|
self.graph_manager.update_graph(df)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
root: tk.Tk = tk.Tk()
|
|
app: MedTrackerApp = MedTrackerApp(root)
|
|
root.mainloop()
|