Files
thechart/src/main.py
T
William Valentin df9738ab17
Build and Push Docker Image / build-and-push (push) Has been cancelled
feat: enhance menu theming with comprehensive documentation and testing support
2025-08-05 14:06:42 -07:00

766 lines
30 KiB
Python

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("<Double-1>", 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("<Control-s>", lambda e: self.add_new_entry())
self.root.bind("<Control-S>", lambda e: self.add_new_entry())
self.root.bind("<Control-q>", lambda e: self.handle_window_closing())
self.root.bind("<Control-Q>", lambda e: self.handle_window_closing())
self.root.bind("<Control-e>", lambda e: self._open_export_window())
self.root.bind("<Control-E>", lambda e: self._open_export_window())
self.root.bind("<Control-n>", lambda e: self._clear_entries())
self.root.bind("<Control-N>", lambda e: self._clear_entries())
self.root.bind("<Control-r>", lambda e: self.refresh_data_display())
self.root.bind("<Control-R>", lambda e: self.refresh_data_display())
self.root.bind("<F5>", lambda e: self.refresh_data_display())
self.root.bind("<Control-m>", lambda e: self._open_medicine_manager())
self.root.bind("<Control-M>", lambda e: self._open_medicine_manager())
self.root.bind("<Control-p>", lambda e: self._open_pathology_manager())
self.root.bind("<Control-P>", lambda e: self._open_pathology_manager())
self.root.bind("<Delete>", lambda e: self._delete_selected_entry())
self.root.bind("<Escape>", lambda e: self._clear_selection())
self.root.bind("<F1>", lambda e: self._show_keyboard_shortcuts())
self.root.bind("<F2>", 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("<Double-1>", 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()