import os import sys import tkinter as tk from collections.abc import Callable from tkinter import messagebox, ttk 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 medicine_management_window import MedicineManagementWindow from medicine_manager import MedicineManager from pathology_management_window import PathologyManagementWindow from pathology_manager import PathologyManager 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.handle_window_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.medicine_manager: MedicineManager = MedicineManager(logger=logger) self.pathology_manager: PathologyManager = PathologyManager(logger=logger) self.ui_manager: UIManager = UIManager( root, logger, self.medicine_manager, self.pathology_manager ) self.data_manager: DataManager = DataManager( self.filename, logger, self.medicine_manager, self.pathology_manager ) # 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_application_icon(img_path=icon_path) # Set up the main application UI self._setup_main_ui() # Add menu bar self._setup_menu() 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, self.medicine_manager, self.pathology_manager ) # --- 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.pathology_vars: dict[str, tk.IntVar] = input_ui["pathology_vars"] self.medicine_vars: dict[str, tuple[tk.IntVar, str]] = 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_action_buttons( self.input_frame, [ { "text": "Add Entry", "command": self.add_new_entry, "fill": "both", "expand": True, }, {"text": "Quit", "command": self.handle_window_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.handle_double_click) # Load data self.refresh_data_display() def _setup_menu(self) -> None: """Set up the menu bar.""" menubar = tk.Menu(self.root) self.root.config(menu=menubar) # Tools menu tools_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="Tools", menu=tools_menu) tools_menu.add_command( label="Manage Pathologies...", command=self._open_pathology_manager ) tools_menu.add_command( label="Manage Medicines...", command=self._open_medicine_manager ) def _open_pathology_manager(self) -> None: """Open the pathology management window.""" PathologyManagementWindow( self.root, self.pathology_manager, self._refresh_ui_after_config_change ) def _open_medicine_manager(self) -> None: """Open the medicine management window.""" MedicineManagementWindow( self.root, self.medicine_manager, self._refresh_ui_after_config_change ) def _refresh_ui_after_config_change(self) -> None: """Refresh UI components after pathology or medicine configuration changes.""" # Recreate the input frame with new pathologies and medicines self.input_frame.destroy() input_ui: dict[str, Any] = self.ui_manager.create_input_frame( self.input_frame.master ) self.input_frame: ttk.Frame = input_ui["frame"] self.pathology_vars: dict[str, tk.IntVar] = input_ui["pathology_vars"] self.medicine_vars: dict[str, tuple[tk.IntVar, str]] = input_ui["medicine_vars"] # Add buttons to input frame self.ui_manager.add_action_buttons( self.input_frame, [ { "text": "Add Entry", "command": self.add_new_entry, "fill": "both", "expand": True, }, {"text": "Quit", "command": self.handle_window_closing}, ], ) # Recreate the table with new columns self.tree.destroy() table_ui: dict[str, Any] = self.ui_manager.create_table_frame( self.tree.master.master ) self.tree: ttk.Treeview = table_ui["tree"] self.tree.bind("", self.handle_double_click) # Refresh data display self.refresh_data_display() def handle_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 # 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"]] # Add pathology data dynamically for pathology_key in self.pathology_manager.get_pathology_keys(): if pathology_key in full_row: full_values.append(full_row[pathology_key]) else: full_values.append(0) # Add medicine data dynamically for medicine_key in self.medicine_manager.get_medicine_keys(): if medicine_key in full_row: full_values.append(full_row[medicine_key]) full_values.append(full_row.get(f"{medicine_key}_doses", "")) else: full_values.extend([0, ""]) full_values.append(full_row["note"]) full_values = tuple(full_values) 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 with full data _: tk.Toplevel = self.ui_manager.create_edit_window(full_values, callbacks) def _save_edit( self, edit_win: tk.Toplevel, original_date: str, *args, ) -> None: """Save edited data to CSV file with dynamic pathology/medicine support.""" # Parse dynamic arguments # Format: date, pathology1, pathology2, ..., medicine1, medicine2, # ..., note, dose_data if len(args) < 2: # At minimum need date and note messagebox.showerror("Error", "Invalid save data format", parent=edit_win) return # Extract arguments date = args[0] # Get pathology count to extract values pathology_keys = self.pathology_manager.get_pathology_keys() medicine_keys = self.medicine_manager.get_medicine_keys() # Expected format: date, pathology_values..., medicine_values..., # note, dose_data expected_pathology_count = len(pathology_keys) expected_medicine_count = len(medicine_keys) # Extract pathology values pathology_values = [] for i in range(expected_pathology_count): if i + 1 < len(args): pathology_values.append(args[i + 1]) else: pathology_values.append(0) # Extract medicine values medicine_values = [] medicine_start_idx = 1 + expected_pathology_count for i in range(expected_medicine_count): if medicine_start_idx + i < len(args): medicine_values.append(args[medicine_start_idx + i]) else: medicine_values.append(0) # Extract note and dose data (last two arguments) note = args[-2] if len(args) >= 2 else "" dose_data = args[-1] if len(args) >= 1 else {} # Build the values list for data manager values = [date] values.extend(pathology_values) # Add medicine data dynamically for i, medicine_key in enumerate(medicine_keys): values.append(medicine_values[i] if i < len(medicine_values) else 0) values.append(dose_data.get(medicine_key, "")) values.append(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.refresh_data_display() 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 handle_window_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_new_entry(self) -> None: """Add a new entry to the CSV file.""" # Get current doses for today today = self.date_var.get() dose_values = {} if today: # Get doses for all medicines dynamically for medicine_key in self.medicine_manager.get_medicine_keys(): doses = self.data_manager.get_today_medicine_doses(today, medicine_key) dose_values[f"{medicine_key}_doses"] = "|".join( [f"{ts}:{dose}" for ts, dose in doses] ) else: # Set empty doses for all medicines for medicine_key in self.medicine_manager.get_medicine_keys(): dose_values[f"{medicine_key}_doses"] = "" # Build entry dynamically entry: list[str | int] = [self.date_var.get()] # Add pathology data dynamically for pathology_key in self.pathology_manager.get_pathology_keys(): entry.append(self.pathology_vars[pathology_key].get()) # Add medicine data for medicine_key in self.medicine_manager.get_medicine_keys(): entry.append(self.medicine_vars[medicine_key][0].get()) entry.append(dose_values[f"{medicine_key}_doses"]) entry.append(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.refresh_data_display() 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.refresh_data_display() 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.pathology_vars: self.pathology_vars[key].set(0) for key in self.medicine_vars: self.medicine_vars[key][0].set(0) self.note_var.set("") def refresh_data_display(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: # Build display columns dynamically (exclude dose columns for table view) display_columns = ["date", "depression", "anxiety", "sleep", "appetite"] # Add medicine columns (without dose columns) for medicine_key in self.medicine_manager.get_medicine_keys(): display_columns.append(medicine_key) display_columns.append("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 - 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()