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, list[tk.IntVar | ttk.Spinbox]] = input_ui[ "medicine_vars" ] self.note_var: tk.StringVar = input_ui["note_var"] self.date_var: tk.StringVar = input_ui["date_var"] # 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 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.""" values: list[str | int] = [ date, dep, anx, slp, app, bup, hydro, gaba, prop, 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.""" 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(), self.medicine_vars["hydroxyzine"][0].get(), self.medicine_vars["gabapentin"][0].get(), self.medicine_vars["propranolol"][0].get(), 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=edit_win ) 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("") 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: for _index, row in df.iterrows(): self.tree.insert(parent="", index="end", values=list(row)) logger.debug(f"Loaded {len(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()