Files
thechart/src/main.py

285 lines
10 KiB
Python

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