766 lines
30 KiB
Python
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()
|