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_CLEAR, LOG_LEVEL, LOG_PATH from data_manager import DataManager from export_manager import ExportManager from export_window import ExportWindow 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 settings_window import SettingsWindow from theme_manager import ThemeManager 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}" ) logger.info(f"Log level: {LOG_LEVEL}") # Initialize theme manager first self.theme_manager: ThemeManager = ThemeManager(self.root, logger) if LOG_LEVEL == "DEBUG": logger.debug(f"Script name: {sys.argv[0]}") logger.debug(f"Logs path: {LOG_PATH}") logger.debug(f"Log clear: {LOG_CLEAR}") 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.theme_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() # Setup keyboard shortcuts self._setup_keyboard_shortcuts() # Center the window on screen self._center_window() def _center_window(self) -> None: """Center the main window on the screen.""" # Update the window to get accurate dimensions self.root.update_idletasks() # Get window dimensions window_width = self.root.winfo_reqwidth() window_height = self.root.winfo_reqheight() # Get screen dimensions screen_width = self.root.winfo_screenwidth() screen_height = self.root.winfo_screenheight() # Calculate position to center the window x = (screen_width // 2) - (window_width // 2) y = (screen_height // 2) - (window_height // 2) # Set the window geometry self.root.geometry(f"{window_width}x{window_height}+{x}+{y}") 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", style="Card.TFrame") 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(3): # Changed from 2 to 3 to accommodate status bar 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 ) # Initialize export manager self.export_manager: ExportManager = ExportManager( self.data_manager, self.graph_manager, self.medicine_manager, self.pathology_manager, logger, ) # --- 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 (Ctrl+S)", "command": self.add_new_entry, "fill": "both", "expand": True, }, {"text": "Quit (Ctrl+Q)", "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) # --- Create Status Bar --- self.status_bar = self.ui_manager.create_status_bar(main_frame) # Load data self.refresh_data_display() # Initialize status bar with ready message self.ui_manager.update_status("Application ready", "info") def _setup_menu(self) -> None: """Set up the menu bar.""" menubar = self.theme_manager.create_themed_menu(self.root) self.root.config(menu=menubar) # File menu file_menu = self.theme_manager.create_themed_menu(menubar, tearoff=0) menubar.add_cascade(label="File", menu=file_menu) file_menu.add_command( label="Export Data...", command=self._open_export_window, accelerator="Ctrl+E", ) file_menu.add_separator() file_menu.add_command( label="Exit", command=self.handle_window_closing, accelerator="Ctrl+Q" ) # Tools menu tools_menu = self.theme_manager.create_themed_menu(menubar, tearoff=0) menubar.add_cascade(label="Tools", menu=tools_menu) tools_menu.add_command( label="Manage Pathologies...", command=self._open_pathology_manager, accelerator="Ctrl+P", ) tools_menu.add_command( label="Manage Medicines...", command=self._open_medicine_manager, accelerator="Ctrl+M", ) tools_menu.add_separator() tools_menu.add_command( label="Clear Entries", command=self._clear_entries, accelerator="Ctrl+N" ) tools_menu.add_command( label="Refresh Data", command=self.refresh_data_display, accelerator="F5" ) # Theme menu theme_menu = self.theme_manager.create_themed_menu(menubar, tearoff=0) menubar.add_cascade(label="Theme", menu=theme_menu) # Add quick theme options available_themes = self.theme_manager.get_available_themes() current_theme = self.theme_manager.get_current_theme() for theme in available_themes: theme_menu.add_radiobutton( label=theme.title(), command=lambda t=theme: self._change_theme(t), value=theme == current_theme, ) theme_menu.add_separator() theme_menu.add_command( label="More Settings...", command=self._open_settings_window, ) # Help menu help_menu = self.theme_manager.create_themed_menu(menubar, tearoff=0) menubar.add_cascade(label="Help", menu=help_menu) help_menu.add_command( label="Settings...", command=self._open_settings_window, accelerator="F2", ) help_menu.add_separator() help_menu.add_command( label="Keyboard Shortcuts", command=self._show_keyboard_shortcuts, accelerator="F1", ) help_menu.add_command(label="About", command=self._show_about_dialog) def _setup_keyboard_shortcuts(self) -> None: """Set up keyboard shortcuts for common actions.""" # Bind keyboard shortcuts to the main window self.root.bind("", lambda e: self.add_new_entry()) self.root.bind("", lambda e: self.add_new_entry()) self.root.bind("", lambda e: self.handle_window_closing()) self.root.bind("", lambda e: self.handle_window_closing()) self.root.bind("", lambda e: self._open_export_window()) self.root.bind("", lambda e: self._open_export_window()) self.root.bind("", lambda e: self._clear_entries()) self.root.bind("", lambda e: self._clear_entries()) self.root.bind("", lambda e: self.refresh_data_display()) self.root.bind("", lambda e: self.refresh_data_display()) self.root.bind("", lambda e: self.refresh_data_display()) self.root.bind("", lambda e: self._open_medicine_manager()) self.root.bind("", lambda e: self._open_medicine_manager()) self.root.bind("", lambda e: self._open_pathology_manager()) self.root.bind("", lambda e: self._open_pathology_manager()) self.root.bind("", lambda e: self._delete_selected_entry()) self.root.bind("", lambda e: self._clear_selection()) self.root.bind("", lambda e: self._show_keyboard_shortcuts()) self.root.bind("", lambda e: self._open_settings_window()) # Make the window focusable so it can receive key events self.root.focus_set() logger.info("Keyboard shortcuts configured:") logger.info(" Ctrl+S: Save/Add new entry") logger.info(" Ctrl+Q: Quit application") logger.info(" Ctrl+E: Export data") logger.info(" Ctrl+N: Clear entries") logger.info(" Ctrl+R/F5: Refresh data") logger.info(" Ctrl+M: Manage medicines") logger.info(" Ctrl+P: Manage pathologies") logger.info(" Delete: Delete selected entry") logger.info(" Escape: Clear selection") logger.info(" F1: Show keyboard shortcuts help") def _show_keyboard_shortcuts(self) -> None: """Show a dialog with keyboard shortcuts information.""" shortcuts_text = """Keyboard Shortcuts: File Operations: • Ctrl+S: Save/Add new entry • Ctrl+Q: Quit application • Ctrl+E: Export data Data Management: • Ctrl+N: Clear entries • Ctrl+R / F5: Refresh data Window Management: • Ctrl+M: Manage medicines • Ctrl+P: Manage pathologies Table Operations: • Delete: Delete selected entry • Escape: Clear selection • Double-click: Edit entry Help: • F1: Show this help dialog • F2: Open settings window""" messagebox.showinfo("Keyboard Shortcuts", shortcuts_text, parent=self.root) def _change_theme(self, theme_name: str) -> None: """Change the application theme.""" if self.theme_manager.apply_theme(theme_name): self.ui_manager.update_status( f"Theme changed to: {theme_name.title()}", "info" ) # Refresh the menu to update radio button selection self._setup_menu() else: self.ui_manager.update_status( f"Failed to apply theme: {theme_name}", "error" ) def _show_about_dialog(self) -> None: """Show about dialog.""" about_text = """TheChart - Medication Tracker A simple application for tracking medications and pathologies. Features: • Add daily medication and pathology entries • Visual graphs and charts • Data export capabilities • Keyboard shortcuts for efficiency Use Ctrl+S to save entries and Ctrl+Q to quit.""" messagebox.showinfo("About TheChart", about_text, parent=self.root) def _open_export_window(self) -> None: """Open the export window.""" self.ui_manager.update_status("Opening export window", "info") ExportWindow(self.root, self.export_manager) def _open_pathology_manager(self) -> None: """Open the pathology management window.""" self.ui_manager.update_status("Opening pathology manager", "info") PathologyManagementWindow( self.root, self.pathology_manager, self._refresh_ui_after_config_change ) def _open_medicine_manager(self) -> None: """Open the medicine management window.""" self.ui_manager.update_status("Opening medicine manager", "info") MedicineManagementWindow( self.root, self.medicine_manager, self._refresh_ui_after_config_change ) def _open_settings_window(self) -> None: """Open the settings window.""" self.ui_manager.update_status("Opening settings window", "info") SettingsWindow(self.root, self.theme_manager, self.ui_manager) def _refresh_ui_after_config_change(self) -> None: """Refresh UI components after pathology or medicine configuration changes.""" self.ui_manager.update_status( "Refreshing UI after configuration change", "info" ) # Clear caches in optimized data manager if hasattr(self.data_manager, "_invalidate_cache"): self.data_manager._invalidate_cache() self.data_manager._headers_cache = None self.data_manager._dtype_cache = None # 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 (Ctrl+S)", "command": self.add_new_entry, "fill": "both", "expand": True, }, {"text": "Quit (Ctrl+Q)", "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() # Update status to show completion self.ui_manager.update_status("UI refreshed successfully", "success") def _delete_selected_entry(self) -> None: """Delete the currently selected entry in the table.""" selection = self.tree.selection() if not selection: self.ui_manager.update_status("No entry selected for deletion", "warning") return item_id = selection[0] item_values = self.tree.item(item_id, "values") if messagebox.askyesno( "Delete Entry", f"Are you sure you want to delete the entry for {item_values[0]}?", parent=self.root, ): date: str = item_values[0] logger.debug(f"Deleting entry with date={date}") self.ui_manager.update_status("Deleting entry...", "info") if self.data_manager.delete_entry(date): self.ui_manager.update_status("Entry deleted successfully!", "success") messagebox.showinfo( "Success", "Entry deleted successfully!", parent=self.root ) self.refresh_data_display() else: self.ui_manager.update_status("Failed to delete entry", "error") messagebox.showerror( "Error", "Failed to delete entry", parent=self.root ) def _clear_selection(self) -> None: """Clear the current selection in the table.""" if self.tree.selection(): self.tree.selection_remove(self.tree.selection()) self.ui_manager.update_status("Selection cleared", "info") 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") self.ui_manager.update_status( f"Opening entry for {item_values[0]} for editing", "info" ) logger.debug(f"Editing item_id={item_id}, values={item_values}") self._create_edit_window(item_id, item_values) else: self.ui_manager.update_status("No entries to edit", "warning") 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) self.ui_manager.update_status("Saving changes...", "info") if self.data_manager.update_entry(original_date, values): edit_win.destroy() self.ui_manager.update_status("Entry updated successfully!", "success") 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: self.ui_manager.update_status("Duplicate date found", "error") messagebox.showerror( "Error", f"An entry for date '{date}' already exists. " "Please use a different date.", parent=edit_win, ) else: self.ui_manager.update_status("Failed to save changes", "error") 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(): self.ui_manager.update_status("Please enter a date", "error") messagebox.showerror("Error", "Please enter a date.", parent=self.root) return self.ui_manager.update_status("Adding new entry...", "info") if self.data_manager.add_entry(entry): self.ui_manager.update_status("Entry added successfully!", "success") 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: self.ui_manager.update_status("Duplicate entry found", "error") 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: self.ui_manager.update_status("Failed to add entry", "error") 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}") self.ui_manager.update_status("Deleting entry...", "info") if self.data_manager.delete_entry(date): edit_win.destroy() self.ui_manager.update_status("Entry deleted successfully!", "success") messagebox.showinfo( "Success", "Entry deleted successfully!", parent=self.root ) self.refresh_data_display() else: self.ui_manager.update_status("Failed to delete entry", "error") 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 efficiently children = self.tree.get_children() if children: self.tree.delete(*children) try: # 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"] # Add pathology columns for pathology_key in self.pathology_manager.get_pathology_keys(): display_columns.append(pathology_key) # 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 # Batch insert for better performance with alternating row colors for index, row in display_df.iterrows(): # Add alternating row tags for better visibility tag = "evenrow" if index % 2 == 0 else "oddrow" self.tree.insert( parent="", index="end", values=list(row), tags=(tag,) ) logger.debug(f"Loaded {len(display_df)} entries into treeview.") # Update the graph self.graph_manager.update_graph(df) # Update status bar with file info entry_count = len(df) if not df.empty else 0 self.ui_manager.update_file_info(self.filename, entry_count) if entry_count == 0: self.ui_manager.update_status("No data to display", "warning") else: self.ui_manager.update_status("Data loaded successfully", "success") except Exception as e: logger.error(f"Error loading data: {e}") self.ui_manager.update_status(f"Error loading data: {str(e)}", "error") if __name__ == "__main__": root: tk.Tk = tk.Tk() app: MedTrackerApp = MedTrackerApp(root) root.mainloop()