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("", 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()