1444 lines
56 KiB
Python
1444 lines
56 KiB
Python
import logging
|
|
import os
|
|
import sys
|
|
import tkinter as tk
|
|
from collections.abc import Callable
|
|
from datetime import datetime
|
|
from tkinter import messagebox, ttk
|
|
from typing import Any
|
|
|
|
from PIL import Image, ImageTk
|
|
|
|
from medicine_manager import MedicineManager
|
|
from pathology_manager import PathologyManager
|
|
|
|
|
|
class UIManager:
|
|
"""Handle UI creation and management for the application."""
|
|
|
|
def __init__(
|
|
self,
|
|
root: tk.Tk,
|
|
logger: logging.Logger,
|
|
medicine_manager: MedicineManager,
|
|
pathology_manager: PathologyManager,
|
|
) -> None:
|
|
self.root: tk.Tk = root
|
|
self.logger: logging.Logger = logger
|
|
self.medicine_manager = medicine_manager
|
|
self.pathology_manager = pathology_manager
|
|
|
|
# Status bar attributes
|
|
self.status_bar: tk.Frame | None = None
|
|
self.status_label: tk.Label | None = None
|
|
self.file_info_label: tk.Label | None = None
|
|
|
|
def setup_application_icon(self, img_path: str) -> bool:
|
|
"""Set up the application icon."""
|
|
try:
|
|
self.logger.info(f"Trying to load icon from: {img_path}")
|
|
# Try to find the icon in various locations
|
|
# Check if we're in PyInstaller bundle
|
|
if not os.path.exists(img_path) and hasattr(sys, "_MEIPASS"):
|
|
# PyInstaller creates a temp folder and stores path in _MEIPASS
|
|
base_path: str = sys._MEIPASS
|
|
potential_paths: list[str] = [
|
|
os.path.join(base_path, os.path.basename(img_path)),
|
|
os.path.join(base_path, "chart-671.png"),
|
|
]
|
|
for path in potential_paths:
|
|
if os.path.exists(path):
|
|
self.logger.info(f"Found icon in PyInstaller bundle: {path}")
|
|
img_path = path
|
|
break
|
|
|
|
icon_image: Image.Image = Image.open(img_path)
|
|
icon_image = icon_image.resize(
|
|
size=(32, 32), resample=Image.Resampling.NEAREST
|
|
)
|
|
icon_photo: ImageTk.PhotoImage = ImageTk.PhotoImage(image=icon_image)
|
|
self.root.iconphoto(True, icon_photo)
|
|
self.root.wm_iconphoto(True, icon_photo)
|
|
return True
|
|
except FileNotFoundError:
|
|
self.logger.warning(f"Icon file not found at {img_path}")
|
|
return False
|
|
except Exception as e:
|
|
self.logger.error(f"Error setting icon: {str(e)}")
|
|
return False
|
|
|
|
def create_input_frame(self, parent_frame: ttk.Frame) -> dict[str, Any]:
|
|
"""Create and configure the input frame with all widgets."""
|
|
# Create main container for the scrollable input frame
|
|
main_container = ttk.LabelFrame(parent_frame, text="New Entry")
|
|
main_container.grid(row=1, column=0, padx=10, pady=10, sticky="nsew")
|
|
main_container.grid_rowconfigure(0, weight=1)
|
|
main_container.grid_columnconfigure(0, weight=1)
|
|
|
|
# Create canvas and scrollbar for scrolling
|
|
canvas = tk.Canvas(main_container, highlightthickness=0)
|
|
scrollbar = ttk.Scrollbar(
|
|
main_container, orient="vertical", command=canvas.yview
|
|
)
|
|
canvas.configure(yscrollcommand=scrollbar.set)
|
|
|
|
# Create the actual input frame inside the canvas
|
|
input_frame = ttk.Frame(canvas)
|
|
input_frame.grid_columnconfigure(1, weight=1)
|
|
|
|
# Place canvas and scrollbar in the container
|
|
canvas.grid(row=0, column=0, sticky="nsew")
|
|
scrollbar.grid(row=0, column=1, sticky="ns")
|
|
|
|
# Create window in canvas for the input frame
|
|
canvas_window = canvas.create_window((0, 0), window=input_frame, anchor="nw")
|
|
|
|
# Configure canvas window width to fill available space
|
|
def configure_canvas_width(event=None):
|
|
canvas_width = canvas.winfo_width()
|
|
canvas.itemconfig(canvas_window, width=canvas_width)
|
|
|
|
# Configure canvas scrolling
|
|
def configure_scroll_region(event=None):
|
|
canvas.configure(scrollregion=canvas.bbox("all"))
|
|
|
|
def on_mousewheel(event):
|
|
# Check if canvas is scrollable before scrolling
|
|
if canvas.cget("scrollregion"):
|
|
canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
|
|
|
|
def on_mousewheel_linux_up(event):
|
|
# Linux mouse wheel up
|
|
if canvas.cget("scrollregion"):
|
|
canvas.yview_scroll(-1, "units")
|
|
|
|
def on_mousewheel_linux_down(event):
|
|
# Linux mouse wheel down
|
|
if canvas.cget("scrollregion"):
|
|
canvas.yview_scroll(1, "units")
|
|
|
|
input_frame.bind("<Configure>", configure_scroll_region)
|
|
canvas.bind("<Configure>", configure_canvas_width)
|
|
|
|
# Bind mouse wheel events to canvas and main container
|
|
canvas.bind("<MouseWheel>", on_mousewheel) # Windows/Linux
|
|
canvas.bind("<Button-4>", on_mousewheel_linux_up) # Linux
|
|
canvas.bind("<Button-5>", on_mousewheel_linux_down) # Linux
|
|
main_container.bind("<MouseWheel>", on_mousewheel) # Windows/Linux
|
|
main_container.bind("<Button-4>", on_mousewheel_linux_up) # Linux
|
|
main_container.bind("<Button-5>", on_mousewheel_linux_down) # Linux
|
|
|
|
# Bind mouse wheel to input frame and its children for better scrolling
|
|
self._bind_mousewheel_to_widget_tree(input_frame, canvas)
|
|
|
|
# Set focus to canvas to ensure it receives scroll events
|
|
canvas.focus_set()
|
|
|
|
# Add mouse enter event to manage focus for scrolling
|
|
def on_mouse_enter(event):
|
|
canvas.focus_set()
|
|
|
|
main_container.bind("<Enter>", on_mouse_enter)
|
|
canvas.bind("<Enter>", on_mouse_enter)
|
|
|
|
# Create variables for pathologies dynamically
|
|
pathology_vars: dict[str, tk.IntVar] = {}
|
|
for pathology_key in self.pathology_manager.get_pathology_keys():
|
|
pathology_vars[pathology_key] = tk.IntVar(value=0)
|
|
|
|
# Create enhanced scales for pathologies dynamically
|
|
pathology_configs = []
|
|
for pathology in self.pathology_manager.get_all_pathologies().values():
|
|
pathology_configs.append((pathology.display_name, pathology.key))
|
|
|
|
# Configure input frame columns for better layout
|
|
input_frame.grid_columnconfigure(1, weight=1)
|
|
|
|
for idx, (label, var_name) in enumerate(pathology_configs):
|
|
self._create_enhanced_pathology_scale(
|
|
input_frame, idx, label, var_name, 0, pathology_vars
|
|
)
|
|
|
|
# Medicine tracking section (simplified) - adjust row number dynamically
|
|
medicine_row = len(pathology_configs)
|
|
ttk.Label(input_frame, text="Treatment:").grid(
|
|
row=medicine_row, column=0, sticky="w", padx=5, pady=2
|
|
)
|
|
medicine_frame = ttk.LabelFrame(input_frame, text="Medicine")
|
|
medicine_frame.grid(row=medicine_row, column=1, padx=0, pady=10, sticky="nsew")
|
|
medicine_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
# Store medicine variables (checkboxes only) - dynamic based on medicine manager
|
|
medicine_vars: dict[str, tuple[tk.IntVar, str]] = {}
|
|
|
|
for medicine_key in self.medicine_manager.get_medicine_keys():
|
|
medicine = self.medicine_manager.get_medicine(medicine_key)
|
|
if medicine:
|
|
var = tk.IntVar(value=0)
|
|
text = f"{medicine.display_name} {medicine.dosage_info}"
|
|
medicine_vars[medicine_key] = (var, text)
|
|
|
|
for idx, (_med_name, (var, text)) in enumerate(medicine_vars.items()):
|
|
# Just checkbox for medicine taken
|
|
ttk.Checkbutton(medicine_frame, text=text, variable=var).grid(
|
|
row=idx, column=0, sticky="w", padx=5, pady=2
|
|
)
|
|
|
|
# Note and Date fields - adjust row numbers
|
|
note_row = medicine_row + 1
|
|
date_row = medicine_row + 2
|
|
|
|
note_var: tk.StringVar = tk.StringVar()
|
|
date_var: tk.StringVar = tk.StringVar()
|
|
|
|
ttk.Label(input_frame, text="Note:").grid(
|
|
row=note_row, column=0, sticky="w", padx=5, pady=2
|
|
)
|
|
ttk.Entry(input_frame, textvariable=note_var).grid(
|
|
row=note_row, column=1, sticky="ew", padx=5, pady=2
|
|
)
|
|
|
|
ttk.Label(input_frame, text="Date (mm/dd/yyyy):").grid(
|
|
row=date_row, column=0, sticky="w", padx=5, pady=2
|
|
)
|
|
ttk.Entry(input_frame, textvariable=date_var, justify="center").grid(
|
|
row=date_row, column=1, sticky="ew", padx=5, pady=2
|
|
)
|
|
|
|
# Set default date to today
|
|
date_var.set(datetime.now().strftime("%m/%d/%Y"))
|
|
|
|
# Ensure mouse wheel binding is applied to all newly created widgets
|
|
main_container.update_idletasks()
|
|
canvas.configure(scrollregion=canvas.bbox("all"))
|
|
self._bind_mousewheel_to_widget_tree(input_frame, canvas)
|
|
|
|
# Return all UI elements and variables
|
|
return {
|
|
"frame": main_container,
|
|
"pathology_vars": pathology_vars,
|
|
"medicine_vars": medicine_vars,
|
|
"note_var": note_var,
|
|
"date_var": date_var,
|
|
}
|
|
|
|
def create_table_frame(self, parent_frame: ttk.Frame) -> dict[str, Any]:
|
|
"""Create and configure the table frame with a treeview."""
|
|
table_frame: ttk.LabelFrame = ttk.LabelFrame(
|
|
parent_frame, text="Log (Double-click to edit)"
|
|
)
|
|
table_frame.grid(row=1, column=1, padx=10, pady=10, sticky="nsew")
|
|
|
|
# Configure table frame to expand
|
|
table_frame.grid_rowconfigure(0, weight=1)
|
|
table_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
# Build columns dynamically
|
|
columns: list[str] = ["Date"]
|
|
col_labels: list[str] = ["Date"]
|
|
col_settings: list[tuple[str, int, str]] = [("Date", 80, "center")]
|
|
|
|
# Add pathology columns dynamically
|
|
for pathology_key in self.pathology_manager.get_pathology_keys():
|
|
pathology = self.pathology_manager.get_pathology(pathology_key)
|
|
if pathology:
|
|
columns.append(pathology.display_name)
|
|
col_labels.append(pathology.display_name)
|
|
col_settings.append((pathology.display_name, 80, "center"))
|
|
|
|
# Add medicine columns dynamically
|
|
for medicine_key in self.medicine_manager.get_medicine_keys():
|
|
medicine = self.medicine_manager.get_medicine(medicine_key)
|
|
if medicine:
|
|
columns.append(medicine.display_name)
|
|
col_labels.append(f"{medicine.display_name} {medicine.dosage_info}")
|
|
col_settings.append((medicine.display_name, 120, "center"))
|
|
|
|
columns.append("Note")
|
|
col_labels.append("Note")
|
|
col_settings.append(("Note", 300, "w"))
|
|
|
|
tree: ttk.Treeview = ttk.Treeview(table_frame, columns=columns, show="headings")
|
|
|
|
for col, label in zip(columns, col_labels, strict=False):
|
|
tree.heading(col, text=label)
|
|
|
|
for col, width, anchor in col_settings:
|
|
tree.column(col, width=width, anchor=anchor)
|
|
|
|
tree.pack(side="left", fill="both", expand=True)
|
|
|
|
# Add scrollbar
|
|
scrollbar = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)
|
|
tree.configure(yscrollcommand=scrollbar.set)
|
|
scrollbar.pack(side="right", fill="y")
|
|
|
|
return {"frame": table_frame, "tree": tree}
|
|
|
|
def create_graph_frame(self, parent_frame: ttk.Frame) -> ttk.LabelFrame:
|
|
"""Create and configure the graph frame."""
|
|
graph_frame: ttk.LabelFrame = ttk.LabelFrame(parent_frame, text="Evolution")
|
|
graph_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=10, sticky="nsew")
|
|
return graph_frame
|
|
|
|
def add_action_buttons(
|
|
self, frame: ttk.Frame, buttons_config: list[dict[str, Any]]
|
|
) -> ttk.Frame:
|
|
"""Add buttons to a frame based on configuration."""
|
|
button_frame: ttk.Frame = ttk.Frame(frame)
|
|
button_frame.grid(row=7, column=0, columnspan=2, pady=10)
|
|
|
|
for btn_config in buttons_config:
|
|
ttk.Button(
|
|
button_frame,
|
|
text=btn_config["text"],
|
|
command=btn_config["command"],
|
|
).pack(
|
|
side="left",
|
|
padx=5,
|
|
fill=btn_config.get("fill", None),
|
|
expand=btn_config.get("expand", False),
|
|
)
|
|
|
|
return button_frame
|
|
|
|
def create_status_bar(self, parent_frame: tk.Widget) -> tk.Frame:
|
|
"""Create and configure the status bar at the bottom of the application."""
|
|
# Create the status bar frame
|
|
self.status_bar = tk.Frame(parent_frame, relief=tk.SUNKEN, bd=1)
|
|
self.status_bar.grid(row=2, column=0, columnspan=2, sticky="ew", padx=5, pady=2)
|
|
|
|
# Configure the parent to make the status bar stretch
|
|
parent_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
# Create status message label (left side)
|
|
self.status_label = tk.Label(
|
|
self.status_bar,
|
|
text="Ready",
|
|
anchor=tk.W,
|
|
font=("TkDefaultFont", 9),
|
|
padx=10,
|
|
pady=2,
|
|
)
|
|
self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
|
|
# Create file info label (right side)
|
|
self.file_info_label = tk.Label(
|
|
self.status_bar,
|
|
text="",
|
|
anchor=tk.E,
|
|
font=("TkDefaultFont", 9),
|
|
padx=10,
|
|
pady=2,
|
|
)
|
|
self.file_info_label.pack(side=tk.RIGHT)
|
|
|
|
return self.status_bar
|
|
|
|
def update_status(self, message: str, message_type: str = "info") -> None:
|
|
"""
|
|
Update the status bar with a message.
|
|
|
|
Args:
|
|
message: The message to display
|
|
message_type: Type of message ('info', 'success', 'warning', 'error')
|
|
"""
|
|
if not self.status_label:
|
|
return
|
|
|
|
# Color mapping for different message types
|
|
colors = {
|
|
"info": "#000000", # Black
|
|
"success": "#28A745", # Green
|
|
"warning": "#FFC107", # Yellow/Orange
|
|
"error": "#DC3545", # Red
|
|
}
|
|
|
|
color = colors.get(message_type, "#000000")
|
|
self.status_label.config(text=message, fg=color)
|
|
|
|
# Clear the message after 5 seconds for non-info messages
|
|
if message_type != "info":
|
|
self.root.after(5000, lambda: self.update_status("Ready", "info"))
|
|
|
|
def update_file_info(self, filename: str, entry_count: int = 0) -> None:
|
|
"""
|
|
Update the file information in the status bar.
|
|
|
|
Args:
|
|
filename: Name of the current data file
|
|
entry_count: Number of entries in the file
|
|
"""
|
|
if not self.file_info_label:
|
|
return
|
|
|
|
file_display = os.path.basename(filename) if filename else "No file"
|
|
info_text = f"{file_display}"
|
|
if entry_count > 0:
|
|
info_text += f" ({entry_count} entries)"
|
|
|
|
self.file_info_label.config(text=info_text)
|
|
|
|
def show_status_message(self, message: str, duration: int = 3000) -> None:
|
|
"""
|
|
Show a temporary status message for a specific duration.
|
|
|
|
Args:
|
|
message: The message to display
|
|
duration: How long to show the message in milliseconds
|
|
"""
|
|
if not self.status_label:
|
|
return
|
|
|
|
original_text = self.status_label.cget("text")
|
|
original_color = self.status_label.cget("fg")
|
|
|
|
self.status_label.config(text=message, fg="#2E86AB")
|
|
self.root.after(
|
|
duration,
|
|
lambda: self.status_label.config(text=original_text, fg=original_color),
|
|
)
|
|
|
|
def create_edit_window(
|
|
self, values: tuple[str, ...], callbacks: dict[str, Callable]
|
|
) -> tk.Toplevel:
|
|
"""Create a new window for editing an entry with improved UI."""
|
|
edit_win: tk.Toplevel = tk.Toplevel(master=self.root)
|
|
edit_win.title("Edit Entry")
|
|
edit_win.transient(self.root) # Make window modal
|
|
edit_win.minsize(600, 700)
|
|
edit_win.geometry("800x800")
|
|
|
|
# Create scrollable container
|
|
canvas = tk.Canvas(edit_win, highlightthickness=0)
|
|
scrollbar = ttk.Scrollbar(edit_win, orient="vertical", command=canvas.yview)
|
|
canvas.configure(yscrollcommand=scrollbar.set)
|
|
|
|
# Configure main container with padding inside the canvas
|
|
main_container = ttk.Frame(canvas, padding="20")
|
|
|
|
# Pack canvas and scrollbar
|
|
canvas.pack(side="left", fill="both", expand=True)
|
|
scrollbar.pack(side="right", fill="y")
|
|
|
|
# Create window in canvas for the main container
|
|
canvas_window = canvas.create_window((0, 0), window=main_container, anchor="nw")
|
|
|
|
# Configure grid for main container
|
|
main_container.grid_columnconfigure(0, weight=1)
|
|
|
|
# Configure scrolling
|
|
def configure_scroll_region(event=None):
|
|
canvas.configure(scrollregion=canvas.bbox("all"))
|
|
|
|
def configure_canvas_width(event=None):
|
|
canvas_width = canvas.winfo_width()
|
|
canvas.itemconfig(canvas_window, width=canvas_width)
|
|
|
|
def on_mousewheel(event):
|
|
# Check if canvas is scrollable before scrolling
|
|
if canvas.cget("scrollregion"):
|
|
canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
|
|
|
|
def on_mousewheel_linux_up(event):
|
|
# Linux mouse wheel up
|
|
if canvas.cget("scrollregion"):
|
|
canvas.yview_scroll(-1, "units")
|
|
|
|
def on_mousewheel_linux_down(event):
|
|
# Linux mouse wheel down
|
|
if canvas.cget("scrollregion"):
|
|
canvas.yview_scroll(1, "units")
|
|
|
|
main_container.bind("<Configure>", configure_scroll_region)
|
|
canvas.bind("<Configure>", configure_canvas_width)
|
|
|
|
# Bind mouse wheel events to canvas and edit window
|
|
canvas.bind("<MouseWheel>", on_mousewheel) # Windows/Linux
|
|
canvas.bind("<Button-4>", on_mousewheel_linux_up) # Linux
|
|
canvas.bind("<Button-5>", on_mousewheel_linux_down) # Linux
|
|
edit_win.bind("<MouseWheel>", on_mousewheel) # Windows/Linux
|
|
edit_win.bind("<Button-4>", on_mousewheel_linux_up) # Linux
|
|
edit_win.bind("<Button-5>", on_mousewheel_linux_down) # Linux
|
|
|
|
# Bind mouse wheel to main container and its children for better scrolling
|
|
self._bind_mousewheel_to_widget_tree(main_container, canvas)
|
|
|
|
# Set focus to canvas to ensure it receives scroll events
|
|
canvas.focus_set()
|
|
|
|
# Add mouse enter event to manage focus for scrolling
|
|
def on_mouse_enter(event):
|
|
canvas.focus_set()
|
|
|
|
edit_win.bind("<Enter>", on_mouse_enter)
|
|
canvas.bind("<Enter>", on_mouse_enter)
|
|
|
|
# Unpack values dynamically
|
|
# Expected format: date, pathology1, pathology2, ...,
|
|
# medicine1, medicine1_doses, medicine2, medicine2_doses, ..., note
|
|
|
|
# Parse values dynamically
|
|
values_list = list(values)
|
|
|
|
# Extract date
|
|
date = values_list[0] if len(values_list) > 0 else ""
|
|
|
|
# Extract pathology values
|
|
pathology_values = {}
|
|
pathology_keys = self.pathology_manager.get_pathology_keys()
|
|
for i, pathology_key in enumerate(pathology_keys):
|
|
if i + 1 < len(values_list):
|
|
pathology_values[pathology_key] = values_list[i + 1]
|
|
else:
|
|
pathology_values[pathology_key] = 0
|
|
|
|
# Extract medicine values and doses
|
|
medicine_values = {}
|
|
medicine_doses = {}
|
|
medicine_keys = self.medicine_manager.get_medicine_keys()
|
|
|
|
# Start index after date and pathologies
|
|
medicine_start_idx = 1 + len(pathology_keys)
|
|
|
|
for i, medicine_key in enumerate(medicine_keys):
|
|
# Each medicine has 2 values: checkbox value and doses string
|
|
checkbox_idx = medicine_start_idx + (i * 2)
|
|
doses_idx = medicine_start_idx + (i * 2) + 1
|
|
|
|
if checkbox_idx < len(values_list):
|
|
medicine_values[medicine_key] = values_list[checkbox_idx]
|
|
else:
|
|
medicine_values[medicine_key] = 0
|
|
|
|
if doses_idx < len(values_list):
|
|
medicine_doses[medicine_key] = values_list[doses_idx]
|
|
else:
|
|
medicine_doses[medicine_key] = ""
|
|
|
|
# Extract note (should be the last value)
|
|
note = values_list[-1] if len(values_list) > 0 else ""
|
|
|
|
# Create improved UI sections
|
|
vars_dict = self._create_edit_ui(
|
|
main_container,
|
|
date,
|
|
pathology_values,
|
|
medicine_values,
|
|
medicine_doses,
|
|
note,
|
|
)
|
|
|
|
# Add action buttons
|
|
self._add_edit_buttons(main_container, vars_dict, callbacks, edit_win)
|
|
|
|
# Update scroll region after adding all content
|
|
edit_win.update_idletasks()
|
|
canvas.configure(scrollregion=canvas.bbox("all"))
|
|
|
|
# Ensure mouse wheel binding is applied to all newly created widgets
|
|
self._bind_mousewheel_to_widget_tree(main_container, canvas)
|
|
|
|
# Make window modal
|
|
edit_win.focus_set()
|
|
edit_win.grab_set()
|
|
|
|
return edit_win
|
|
|
|
def _create_edit_ui(
|
|
self,
|
|
parent: ttk.Frame,
|
|
date: str,
|
|
pathology_values: dict[str, int],
|
|
medicine_values: dict[str, int],
|
|
medicine_doses: dict[str, str],
|
|
note: str,
|
|
) -> dict[str, Any]:
|
|
"""Create UI layout for edit window with dynamic pathologies and medicines."""
|
|
vars_dict = {}
|
|
row = 0
|
|
|
|
# Header with entry date
|
|
header_frame = ttk.Frame(parent)
|
|
header_frame.grid(row=row, column=0, sticky="ew", pady=(0, 20))
|
|
header_frame.grid_columnconfigure(1, weight=1)
|
|
|
|
ttk.Label(
|
|
header_frame, text="Editing Entry for:", font=("TkDefaultFont", 12, "bold")
|
|
).grid(row=0, column=0, sticky="w")
|
|
|
|
vars_dict["date"] = tk.StringVar(value=str(date))
|
|
date_entry = ttk.Entry(
|
|
header_frame,
|
|
textvariable=vars_dict["date"],
|
|
font=("TkDefaultFont", 12),
|
|
width=15,
|
|
)
|
|
date_entry.grid(row=0, column=1, sticky="w", padx=(10, 0))
|
|
|
|
row += 1
|
|
|
|
# Pathologies section
|
|
pathologies_frame = ttk.LabelFrame(
|
|
parent, text="Daily Pathologies", padding="15"
|
|
)
|
|
pathologies_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15))
|
|
pathologies_frame.grid_columnconfigure(1, weight=1)
|
|
|
|
# Create pathology scales dynamically
|
|
for i, (pathology_key, value) in enumerate(pathology_values.items()):
|
|
pathology = self.pathology_manager.get_pathology(pathology_key)
|
|
if pathology:
|
|
label = f"{pathology.display_name} ({pathology.scale_info})"
|
|
self._create_symptom_scale(
|
|
pathologies_frame, i, label, pathology_key, value, vars_dict
|
|
)
|
|
|
|
row += 1
|
|
|
|
# Medications section
|
|
meds_frame = ttk.LabelFrame(parent, text="Medications Taken", padding="15")
|
|
meds_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15))
|
|
meds_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
# Create medicine checkboxes dynamically
|
|
med_vars = self._create_medicine_section(meds_frame, medicine_values)
|
|
vars_dict.update(med_vars)
|
|
|
|
row += 1
|
|
|
|
# Dose tracking section
|
|
dose_frame = ttk.LabelFrame(parent, text="Dose Tracking", padding="15")
|
|
dose_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15))
|
|
dose_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
dose_vars = self._create_dose_tracking(dose_frame, medicine_doses)
|
|
vars_dict.update(dose_vars)
|
|
|
|
row += 1
|
|
|
|
# Notes section
|
|
notes_frame = ttk.LabelFrame(parent, text="Notes", padding="15")
|
|
notes_frame.grid(row=row, column=0, sticky="ew", pady=(0, 20))
|
|
notes_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
vars_dict["note"] = tk.StringVar(value=str(note))
|
|
note_text = tk.Text(
|
|
notes_frame,
|
|
height=4,
|
|
width=50,
|
|
wrap=tk.WORD,
|
|
font=("TkDefaultFont", 10),
|
|
relief="solid",
|
|
borderwidth=1,
|
|
)
|
|
note_text.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
|
note_text.insert("1.0", str(note))
|
|
vars_dict["note_text"] = note_text # Store the widget for access during save
|
|
|
|
# Bind text widget to string var for easy access
|
|
def update_note(*args):
|
|
vars_dict["note"].set(note_text.get("1.0", tk.END).strip())
|
|
|
|
note_text.bind("<KeyRelease>", update_note)
|
|
note_text.bind("<FocusOut>", update_note)
|
|
|
|
return vars_dict
|
|
|
|
def _create_symptom_scale(
|
|
self,
|
|
parent: ttk.Frame,
|
|
row: int,
|
|
label: str,
|
|
key: str,
|
|
value: int,
|
|
vars_dict: dict[str, Any],
|
|
) -> None:
|
|
"""Create a symptom scale with visual feedback."""
|
|
# Ensure value is properly converted
|
|
try:
|
|
value = int(float(value)) if value not in ["", None] else 0
|
|
except (ValueError, TypeError):
|
|
value = 0
|
|
|
|
vars_dict[key] = tk.IntVar(value=value)
|
|
|
|
# Label
|
|
ttk.Label(parent, text=f"{label}:", font=("TkDefaultFont", 10, "bold")).grid(
|
|
row=row, column=0, sticky="w", pady=8
|
|
)
|
|
|
|
# Scale container
|
|
scale_container = ttk.Frame(parent)
|
|
scale_container.grid(row=row, column=1, sticky="ew", padx=(20, 0), pady=8)
|
|
scale_container.grid_columnconfigure(0, weight=1)
|
|
|
|
# Scale with value labels
|
|
scale_frame = ttk.Frame(scale_container)
|
|
scale_frame.grid(row=0, column=0, sticky="ew")
|
|
scale_frame.grid_columnconfigure(1, weight=1)
|
|
|
|
# Current value display
|
|
value_label = ttk.Label(
|
|
scale_frame,
|
|
text=str(value),
|
|
font=("TkDefaultFont", 12, "bold"),
|
|
foreground="#2E86AB",
|
|
width=3,
|
|
)
|
|
value_label.grid(row=0, column=0, padx=(0, 10))
|
|
|
|
# Scale widget
|
|
scale = ttk.Scale(
|
|
scale_frame,
|
|
from_=0,
|
|
to=10,
|
|
variable=vars_dict[key],
|
|
orient=tk.HORIZONTAL,
|
|
length=300,
|
|
)
|
|
scale.grid(row=0, column=1, sticky="ew")
|
|
|
|
# Scale labels (0, 5, 10)
|
|
labels_frame = ttk.Frame(scale_container)
|
|
labels_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0))
|
|
|
|
ttk.Label(labels_frame, text="0", font=("TkDefaultFont", 8)).grid(
|
|
row=0, column=0, sticky="w"
|
|
)
|
|
labels_frame.grid_columnconfigure(1, weight=1)
|
|
ttk.Label(labels_frame, text="5", font=("TkDefaultFont", 8)).grid(
|
|
row=0, column=1
|
|
)
|
|
ttk.Label(labels_frame, text="10", font=("TkDefaultFont", 8)).grid(
|
|
row=0, column=2, sticky="e"
|
|
)
|
|
|
|
# Update label when scale changes
|
|
def update_value_label(event=None):
|
|
current_val = vars_dict[key].get()
|
|
value_label.configure(text=str(current_val))
|
|
# Change color based on value
|
|
if current_val <= 3:
|
|
value_label.configure(foreground="#28A745") # Green for low/good
|
|
elif current_val <= 6:
|
|
value_label.configure(foreground="#FFC107") # Yellow for medium
|
|
else:
|
|
value_label.configure(foreground="#DC3545") # Red for high/bad
|
|
|
|
scale.bind("<Motion>", update_value_label)
|
|
scale.bind("<ButtonRelease-1>", update_value_label)
|
|
scale.bind("<KeyRelease>", update_value_label)
|
|
update_value_label() # Set initial color
|
|
|
|
def _create_enhanced_pathology_scale(
|
|
self,
|
|
parent: ttk.Frame,
|
|
row: int,
|
|
label: str,
|
|
key: str,
|
|
value: int,
|
|
vars_dict: dict[str, tk.IntVar],
|
|
) -> None:
|
|
"""Create enhanced pathology scale for new entry form."""
|
|
# Ensure value is properly converted
|
|
try:
|
|
value = int(float(value)) if value not in ["", None] else 0
|
|
except (ValueError, TypeError):
|
|
value = 0
|
|
|
|
# Get pathology configuration
|
|
pathology = self.pathology_manager.get_pathology(key)
|
|
if not pathology:
|
|
# Fallback for missing pathology
|
|
pathology_info = f"{label} (0-10):"
|
|
scale_min, scale_max = 0, 10
|
|
scale_orientation = "normal"
|
|
else:
|
|
pathology_info = f"{pathology.display_name} ({pathology.scale_info}):"
|
|
scale_min, scale_max = pathology.scale_min, pathology.scale_max
|
|
scale_orientation = pathology.scale_orientation
|
|
|
|
# Label
|
|
label_widget = ttk.Label(
|
|
parent, text=pathology_info, font=("TkDefaultFont", 10, "bold")
|
|
)
|
|
label_widget.grid(row=row, column=0, sticky="w", padx=5, pady=8)
|
|
|
|
# Scale container
|
|
scale_container = ttk.Frame(parent)
|
|
scale_container.grid(row=row, column=1, sticky="ew", padx=(20, 5), pady=8)
|
|
scale_container.grid_columnconfigure(0, weight=1)
|
|
|
|
# Scale with value labels
|
|
scale_frame = ttk.Frame(scale_container)
|
|
scale_frame.grid(row=0, column=0, sticky="ew")
|
|
scale_frame.grid_columnconfigure(1, weight=1)
|
|
|
|
# Current value display
|
|
value_label = ttk.Label(
|
|
scale_frame,
|
|
text=str(value),
|
|
font=("TkDefaultFont", 12, "bold"),
|
|
foreground="#2E86AB",
|
|
width=3,
|
|
)
|
|
value_label.grid(row=0, column=0, padx=(0, 10))
|
|
|
|
# Scale widget
|
|
scale = ttk.Scale(
|
|
scale_frame,
|
|
from_=scale_min,
|
|
to=scale_max,
|
|
variable=vars_dict[key],
|
|
orient=tk.HORIZONTAL,
|
|
length=250,
|
|
)
|
|
scale.grid(row=0, column=1, sticky="ew")
|
|
|
|
# Scale labels
|
|
labels_frame = ttk.Frame(scale_container)
|
|
labels_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0))
|
|
|
|
ttk.Label(labels_frame, text=str(scale_min), font=("TkDefaultFont", 8)).grid(
|
|
row=0, column=0, sticky="w"
|
|
)
|
|
labels_frame.grid_columnconfigure(1, weight=1)
|
|
mid_value = (scale_min + scale_max) // 2
|
|
ttk.Label(labels_frame, text=str(mid_value), font=("TkDefaultFont", 8)).grid(
|
|
row=0, column=1
|
|
)
|
|
ttk.Label(labels_frame, text=str(scale_max), font=("TkDefaultFont", 8)).grid(
|
|
row=0, column=2, sticky="e"
|
|
)
|
|
|
|
# Update label when scale changes
|
|
def update_value_label_pathology(event=None):
|
|
current_val = vars_dict[key].get()
|
|
value_label.configure(text=str(current_val))
|
|
# Change color based on value and orientation
|
|
if scale_orientation == "inverted":
|
|
# For inverted scales (like sleep, appetite), higher is better
|
|
if current_val >= scale_max * 0.7:
|
|
value_label.configure(foreground="#28A745") # Green for good
|
|
elif current_val >= scale_max * 0.4:
|
|
value_label.configure(foreground="#FFC107") # Yellow for medium
|
|
else:
|
|
value_label.configure(foreground="#DC3545") # Red for bad
|
|
else:
|
|
# For normal scales (like depression, anxiety), lower is better
|
|
if current_val <= scale_max * 0.3:
|
|
value_label.configure(foreground="#28A745") # Green for good
|
|
elif current_val <= scale_max * 0.6:
|
|
value_label.configure(foreground="#FFC107") # Yellow for medium
|
|
else:
|
|
value_label.configure(foreground="#DC3545") # Red for bad
|
|
|
|
scale.bind("<Motion>", update_value_label_pathology)
|
|
scale.bind("<ButtonRelease-1>", update_value_label_pathology)
|
|
scale.bind("<KeyRelease>", update_value_label_pathology)
|
|
update_value_label_pathology() # Set initial color
|
|
|
|
def _create_medicine_section(
|
|
self, parent: ttk.Frame, medicine_values: dict[str, int]
|
|
) -> dict[str, tk.IntVar]:
|
|
"""Create medicine checkboxes dynamically."""
|
|
vars_dict = {}
|
|
|
|
# Create a grid layout for medicines
|
|
medicine_items = []
|
|
for medicine_key, value in medicine_values.items():
|
|
medicine = self.medicine_manager.get_medicine(medicine_key)
|
|
if medicine:
|
|
medicine_items.append(
|
|
(
|
|
medicine_key,
|
|
value,
|
|
medicine.display_name,
|
|
medicine.dosage_info,
|
|
medicine.color,
|
|
)
|
|
)
|
|
|
|
# Create medicine cards in a 2-column layout
|
|
for i, (key, value, name, dose, _color) in enumerate(medicine_items):
|
|
row = i // 2
|
|
col = i % 2
|
|
|
|
# Medicine card frame
|
|
med_card = ttk.Frame(parent, relief="solid", borderwidth=1)
|
|
med_card.grid(row=row, column=col, sticky="ew", padx=5, pady=5)
|
|
parent.grid_columnconfigure(col, weight=1)
|
|
|
|
vars_dict[key] = tk.IntVar(value=int(value))
|
|
|
|
# Checkbox with medicine name
|
|
check_frame = ttk.Frame(med_card)
|
|
check_frame.pack(fill="x", padx=10, pady=8)
|
|
|
|
checkbox = ttk.Checkbutton(
|
|
check_frame,
|
|
text=f"{name} ({dose})",
|
|
variable=vars_dict[key],
|
|
style="Medicine.TCheckbutton",
|
|
)
|
|
checkbox.pack(anchor="w")
|
|
|
|
return vars_dict
|
|
|
|
def _create_dose_tracking(
|
|
self, parent: ttk.Frame, medicine_doses: dict[str, str]
|
|
) -> dict[str, Any]:
|
|
"""Create dose tracking interface dynamically."""
|
|
vars_dict = {}
|
|
|
|
# Create notebook for organized dose tracking
|
|
notebook = ttk.Notebook(parent)
|
|
notebook.pack(fill="both", expand=True)
|
|
|
|
for medicine_key, dose_str in medicine_doses.items():
|
|
medicine = self.medicine_manager.get_medicine(medicine_key)
|
|
if not medicine:
|
|
continue
|
|
|
|
# Create tab for each medicine
|
|
tab_frame = ttk.Frame(notebook)
|
|
notebook.add(tab_frame, text=medicine.display_name)
|
|
|
|
# Configure tab layout
|
|
tab_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
# Quick dose entry section
|
|
entry_frame = ttk.LabelFrame(tab_frame, text="Add New Dose", padding="10")
|
|
entry_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=5)
|
|
entry_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
# Dose entry
|
|
dose_entry_var = tk.StringVar()
|
|
vars_dict[f"{medicine_key}_dose_entry"] = dose_entry_var
|
|
|
|
dose_entry = ttk.Entry(entry_frame, textvariable=dose_entry_var, width=12)
|
|
dose_entry.grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
|
|
|
# Quick dose buttons
|
|
quick_frame = ttk.Frame(entry_frame)
|
|
quick_frame.grid(row=0, column=1, padx=10, pady=5, sticky="w")
|
|
|
|
# Create the dose StringVar that will be used for saving
|
|
dose_string_var = tk.StringVar(value=str(dose_str))
|
|
vars_dict[f"{medicine_key}_doses"] = dose_string_var
|
|
|
|
# Punch button - updated to use the StringVar properly
|
|
def create_punch_callback(med_key, entry_var, dose_var):
|
|
def punch_dose():
|
|
dose = entry_var.get().strip()
|
|
if dose:
|
|
from datetime import datetime
|
|
|
|
timestamp = datetime.now().strftime("%H:%M")
|
|
new_dose = f"{timestamp}: {dose}"
|
|
|
|
current_doses = dose_var.get()
|
|
if current_doses and current_doses.strip():
|
|
dose_var.set(current_doses + f"\n{new_dose}")
|
|
else:
|
|
dose_var.set(new_dose)
|
|
|
|
entry_var.set("")
|
|
|
|
return punch_dose
|
|
|
|
punch_btn = ttk.Button(
|
|
quick_frame,
|
|
text=f"Take {medicine.display_name}",
|
|
command=create_punch_callback(
|
|
medicine_key, dose_entry_var, dose_string_var
|
|
),
|
|
width=15,
|
|
)
|
|
punch_btn.grid(row=0, column=0, padx=5)
|
|
|
|
# Quick dose buttons
|
|
quick_doses = self.medicine_manager.get_quick_doses(medicine_key)
|
|
for i, dose in enumerate(quick_doses[:3]): # Limit to 3 quick doses
|
|
|
|
def create_quick_callback(d, entry_var=dose_entry_var):
|
|
return lambda: entry_var.set(d)
|
|
|
|
btn = ttk.Button(
|
|
quick_frame,
|
|
text=f"{dose}mg",
|
|
command=create_quick_callback(dose),
|
|
width=8,
|
|
)
|
|
btn.grid(row=0, column=i + 1, padx=2)
|
|
|
|
# Dose history section
|
|
history_frame = ttk.LabelFrame(
|
|
tab_frame, text="Dose History (HH:MM: dose)", padding="10"
|
|
)
|
|
history_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=5)
|
|
history_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
# Dose display text area
|
|
dose_text = tk.Text(
|
|
history_frame,
|
|
height=3,
|
|
width=40,
|
|
wrap=tk.WORD,
|
|
font=("Consolas", 9),
|
|
relief="solid",
|
|
borderwidth=1,
|
|
)
|
|
dose_text.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
|
|
|
# Populate with existing doses using the proper formatting method
|
|
self._populate_dose_history(dose_text, dose_str)
|
|
|
|
# Bind text widget to update string var - fixed closure issue
|
|
def create_update_callback(text_widget, dose_var):
|
|
def update_doses(*args):
|
|
content = text_widget.get("1.0", tk.END).strip()
|
|
dose_var.set(content)
|
|
|
|
return update_doses
|
|
|
|
update_callback = create_update_callback(dose_text, dose_string_var)
|
|
dose_text.bind("<KeyRelease>", update_callback)
|
|
dose_text.bind("<FocusOut>", update_callback)
|
|
|
|
# Also update text widget when StringVar changes (for punch button)
|
|
def create_var_to_text_callback(text_widget, string_var):
|
|
def update_text_from_var(*args):
|
|
current_text = text_widget.get("1.0", tk.END).strip()
|
|
var_content = string_var.get()
|
|
if current_text != var_content:
|
|
text_widget.delete("1.0", tk.END)
|
|
text_widget.insert("1.0", var_content)
|
|
|
|
return update_text_from_var
|
|
|
|
var_to_text_callback = create_var_to_text_callback(
|
|
dose_text, dose_string_var
|
|
)
|
|
dose_string_var.trace("w", var_to_text_callback)
|
|
|
|
# Scrollbar for dose text
|
|
dose_scroll = ttk.Scrollbar(
|
|
history_frame, orient="vertical", command=dose_text.yview
|
|
)
|
|
dose_scroll.grid(row=0, column=1, sticky="ns")
|
|
dose_text.configure(yscrollcommand=dose_scroll.set)
|
|
|
|
# Store reference to text widget for save function
|
|
vars_dict[f"{medicine_key}_dose_text"] = dose_text
|
|
|
|
return vars_dict
|
|
|
|
def _get_quick_doses(self, medicine_key: str) -> list[str]:
|
|
"""Get common dose amounts for quick selection."""
|
|
return self.medicine_manager.get_quick_doses(medicine_key)
|
|
|
|
def _populate_dose_history(self, text_widget: tk.Text, doses_str: str) -> None:
|
|
"""Populate dose history text widget with formatted dose data."""
|
|
text_widget.configure(state="normal")
|
|
text_widget.delete(1.0, tk.END)
|
|
|
|
if not doses_str or str(doses_str) == "nan":
|
|
text_widget.insert(1.0, "No doses recorded today")
|
|
# Keep text widget enabled for editing
|
|
return
|
|
|
|
doses_str = str(doses_str)
|
|
formatted_doses = []
|
|
|
|
for dose_entry in doses_str.split("|"):
|
|
if ":" in dose_entry:
|
|
# Split on the last colon to separate timestamp from dose
|
|
parts = dose_entry.rsplit(":", 1)
|
|
if len(parts) == 2:
|
|
timestamp, dose = parts
|
|
try:
|
|
dt = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S")
|
|
time_str = dt.strftime("%I:%M %p")
|
|
formatted_doses.append(f"• {time_str} - {dose}")
|
|
except ValueError:
|
|
# Handle cases where the timestamp might be malformed
|
|
formatted_doses.append(f"• {dose_entry}")
|
|
else:
|
|
formatted_doses.append(f"• {dose_entry}")
|
|
else:
|
|
formatted_doses.append(f"• {dose_entry}")
|
|
|
|
if formatted_doses:
|
|
text_widget.insert(1.0, "\n".join(formatted_doses))
|
|
else:
|
|
text_widget.insert(1.0, "No doses recorded today")
|
|
|
|
# Always keep text widget enabled for user editing
|
|
|
|
def _take_dose(
|
|
self,
|
|
med_name: str,
|
|
entry_var: tk.StringVar,
|
|
med_key: str,
|
|
vars_dict: dict[str, Any],
|
|
) -> None:
|
|
"""Handle taking a dose with feedback and state management."""
|
|
dose = entry_var.get().strip()
|
|
|
|
# Get the dose text widget - this is what the save function reads from
|
|
dose_text_widget = vars_dict.get(f"{med_key}_doses_text")
|
|
if not dose_text_widget:
|
|
self.logger.error(f"Dose text widget not found for {med_key}")
|
|
return
|
|
|
|
# Find the parent edit window
|
|
parent_window = dose_text_widget.winfo_toplevel()
|
|
|
|
if not dose:
|
|
messagebox.showerror(
|
|
"Error",
|
|
f"Please enter a dose amount for {med_name}",
|
|
parent=parent_window,
|
|
)
|
|
return
|
|
|
|
# Get current time and timestamp
|
|
now = datetime.now()
|
|
time_str = now.strftime("%I:%M %p")
|
|
|
|
# Ensure text widget is enabled
|
|
dose_text_widget.configure(state="normal")
|
|
|
|
# Get current content from the text widget
|
|
current_content = dose_text_widget.get(1.0, tk.END).strip()
|
|
self.logger.debug(f"Current content before adding dose: '{current_content}'")
|
|
|
|
# Create new dose entry in the display format
|
|
new_dose_line = f"• {time_str} - {dose}"
|
|
self.logger.debug(f"New dose line: '{new_dose_line}'")
|
|
|
|
# Add the new dose to the text widget
|
|
if current_content == "No doses recorded today" or not current_content:
|
|
dose_text_widget.delete(1.0, tk.END)
|
|
dose_text_widget.insert(1.0, new_dose_line)
|
|
self.logger.debug("Added first dose")
|
|
else:
|
|
# Append to existing content with proper formatting
|
|
updated_content = current_content + f"\n{new_dose_line}"
|
|
self.logger.debug(f"Updated content: '{updated_content}'")
|
|
dose_text_widget.delete(1.0, tk.END)
|
|
dose_text_widget.insert(1.0, updated_content)
|
|
self.logger.debug("Added subsequent dose")
|
|
|
|
# Verify what's actually in the widget after insertion
|
|
final_content = dose_text_widget.get(1.0, tk.END).strip()
|
|
self.logger.debug(f"Final content in widget: '{final_content}'")
|
|
|
|
# Clear entry field
|
|
entry_var.set("")
|
|
|
|
# Success feedback
|
|
messagebox.showinfo(
|
|
"Dose Recorded",
|
|
f"{med_name} dose of {dose} recorded at {time_str}",
|
|
parent=parent_window,
|
|
)
|
|
|
|
def _add_edit_buttons(
|
|
self,
|
|
parent: ttk.Frame,
|
|
vars_dict: dict[str, Any],
|
|
callbacks: dict[str, Callable],
|
|
edit_win: tk.Toplevel,
|
|
) -> None:
|
|
"""Add action buttons to edit window."""
|
|
button_frame = ttk.Frame(parent)
|
|
button_frame.grid(row=999, column=0, sticky="ew", pady=(20, 0))
|
|
button_frame.grid_columnconfigure((0, 1, 2), weight=1)
|
|
|
|
# Save button
|
|
def save_with_data():
|
|
self.logger.debug("=== SAVE FUNCTION CALLED ===")
|
|
|
|
# Get note text from Text widget
|
|
note_text_widget = vars_dict.get("note_text")
|
|
self.logger.debug(f"note_text_widget found: {note_text_widget is not None}")
|
|
self.logger.debug(f"vars_dict keys: {list(vars_dict.keys())}")
|
|
|
|
note_content = ""
|
|
if note_text_widget:
|
|
try:
|
|
note_content = note_text_widget.get(1.0, tk.END).strip()
|
|
self.logger.debug(f"Note content from widget: '{note_content}'")
|
|
except Exception as e:
|
|
self.logger.error(f"Error getting note from text widget: {e}")
|
|
# Fallback to StringVar
|
|
note_var = vars_dict.get("note")
|
|
if note_var:
|
|
note_content = note_var.get()
|
|
self.logger.debug(
|
|
f"Note content from StringVar fallback: '{note_content}'"
|
|
)
|
|
else:
|
|
# Fallback to StringVar if note_text widget not found
|
|
note_var = vars_dict.get("note")
|
|
if note_var:
|
|
note_content = note_var.get()
|
|
self.logger.debug(f"Note content from StringVar: '{note_content}'")
|
|
else:
|
|
self.logger.error("No note widget or StringVar found!")
|
|
|
|
self.logger.debug(f"Final note_content: '{note_content}'")
|
|
|
|
# Extract dose data dynamically from all medicines
|
|
dose_data = {}
|
|
medicines = self.medicine_manager.get_all_medicines()
|
|
for medicine_key in medicines:
|
|
dose_var_key = f"{medicine_key}_doses"
|
|
dose_text_key = f"{medicine_key}_dose_text"
|
|
self.logger.debug(f"Processing {medicine_key}...")
|
|
|
|
# Prioritize Text widget if it exists (it has the most current data)
|
|
if dose_text_key in vars_dict:
|
|
# Read directly from Text widget
|
|
dose_text_widget = vars_dict[dose_text_key]
|
|
raw_text = dose_text_widget.get(1.0, tk.END).strip()
|
|
self.logger.debug(
|
|
f"Raw text from Text widget for {medicine_key}: '{raw_text}'"
|
|
)
|
|
elif dose_var_key in vars_dict:
|
|
# Fall back to StringVar
|
|
if isinstance(vars_dict[dose_var_key], tk.StringVar):
|
|
raw_text = vars_dict[dose_var_key].get().strip()
|
|
elif isinstance(vars_dict[dose_var_key], tk.Text):
|
|
raw_text = vars_dict[dose_var_key].get(1.0, tk.END).strip()
|
|
else:
|
|
raw_text = str(vars_dict[dose_var_key]).strip()
|
|
self.logger.debug(
|
|
f"Raw text from StringVar for {medicine_key}: '{raw_text}'"
|
|
)
|
|
else:
|
|
raw_text = ""
|
|
self.logger.debug(f"No dose data found for {medicine_key}")
|
|
|
|
if raw_text:
|
|
parsed_dose = self._parse_dose_history_for_saving(
|
|
raw_text, vars_dict["date"].get()
|
|
)
|
|
dose_data[medicine_key] = parsed_dose
|
|
self.logger.debug(
|
|
f"Parsed dose for {medicine_key}: '{parsed_dose}'"
|
|
)
|
|
else:
|
|
dose_data[medicine_key] = ""
|
|
|
|
self.logger.debug(f"Final dose_data: {dose_data}")
|
|
|
|
# Build dynamic callback arguments
|
|
callback_args = [edit_win, vars_dict["date"].get()]
|
|
|
|
# Add pathology values
|
|
pathologies = self.pathology_manager.get_all_pathologies()
|
|
for pathology_key in pathologies:
|
|
callback_args.append(vars_dict[pathology_key].get())
|
|
|
|
# Add medicine values
|
|
medicines = self.medicine_manager.get_all_medicines()
|
|
for medicine_key in medicines:
|
|
callback_args.append(vars_dict[medicine_key].get())
|
|
|
|
# Add note and dose data
|
|
callback_args.extend([note_content, dose_data])
|
|
|
|
self.logger.debug(
|
|
f"Calling save callback with {len(callback_args)} arguments"
|
|
)
|
|
callbacks["save"](*callback_args)
|
|
|
|
save_btn = ttk.Button(
|
|
button_frame,
|
|
text="💾 Save Changes",
|
|
style="Accent.TButton",
|
|
command=save_with_data,
|
|
)
|
|
save_btn.grid(row=0, column=0, sticky="ew", padx=(0, 5))
|
|
|
|
# Cancel button
|
|
cancel_btn = ttk.Button(
|
|
button_frame, text="❌ Cancel", command=edit_win.destroy
|
|
)
|
|
cancel_btn.grid(row=0, column=1, sticky="ew", padx=5)
|
|
|
|
# Delete button
|
|
delete_btn = ttk.Button(
|
|
button_frame,
|
|
text="🗑️ Delete Entry",
|
|
style="Danger.TButton",
|
|
command=lambda: callbacks["delete"](edit_win),
|
|
)
|
|
delete_btn.grid(row=0, column=2, sticky="ew", padx=(5, 0))
|
|
|
|
def _parse_dose_history_for_saving(self, text: str, date_str: str) -> str:
|
|
"""
|
|
Parse the user-edited dose history back into the storable format,
|
|
supporting add/delete/edit.
|
|
"""
|
|
self.logger.debug("=== PARSING DOSE HISTORY ===")
|
|
self.logger.debug(f"Input text: '{text}'")
|
|
self.logger.debug(f"Date string: '{date_str}'")
|
|
|
|
if not text or "No doses recorded" in text:
|
|
self.logger.debug("No doses to parse, returning empty string")
|
|
return ""
|
|
|
|
lines = text.strip().split("\n")
|
|
self.logger.debug(f"Split into {len(lines)} lines: {lines}")
|
|
dose_entries = []
|
|
|
|
for line_num, line in enumerate(lines):
|
|
line = line.strip()
|
|
self.logger.debug(f"Processing line {line_num}: '{line}'")
|
|
if not line or line.lower().startswith("no doses recorded"):
|
|
self.logger.debug("Empty or placeholder line, skipping")
|
|
continue
|
|
|
|
# Handle bullet point format: "• HH:MM AM/PM - dose"
|
|
if line.startswith("•") and " - " in line:
|
|
try:
|
|
content = line.lstrip("• ").strip()
|
|
self.logger.debug(f"Bullet point content: '{content}'")
|
|
time_part, dose_part = content.split(" - ", 1)
|
|
self.logger.debug(
|
|
f"Time part: '{time_part}', Dose part: '{dose_part}'"
|
|
)
|
|
|
|
# Try parsing as 12-hour (with AM/PM)
|
|
try:
|
|
time_obj = datetime.strptime(time_part.strip(), "%I:%M %p")
|
|
except ValueError:
|
|
# Try 24-hour format fallback
|
|
time_obj = datetime.strptime(time_part.strip(), "%H:%M")
|
|
|
|
# Try different date formats
|
|
try:
|
|
entry_date = datetime.strptime(date_str, "%Y-%m-%d")
|
|
except ValueError:
|
|
try:
|
|
entry_date = datetime.strptime(date_str, "%m/%d/%Y")
|
|
except ValueError:
|
|
# If both fail, try ISO format
|
|
entry_date = datetime.fromisoformat(date_str)
|
|
|
|
full_timestamp = entry_date.replace(
|
|
hour=time_obj.hour,
|
|
minute=time_obj.minute,
|
|
second=0,
|
|
microsecond=0,
|
|
)
|
|
timestamp_str = full_timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
dose_entry = f"{timestamp_str}:{dose_part.strip()}"
|
|
dose_entries.append(dose_entry)
|
|
self.logger.debug(f"Added dose entry: '{dose_entry}'")
|
|
except Exception as e:
|
|
self.logger.warning(
|
|
f"Could not parse dose line: '{line}'. Error: {e}"
|
|
)
|
|
continue
|
|
|
|
# Handle simple format: "HH:MM dose" or "HH:MM: dose"
|
|
elif ":" in line and not line.startswith("•"):
|
|
try:
|
|
# Try to parse as "HH:MM dose" or "HH:MM: dose"
|
|
if " " in line:
|
|
time_part, dose_part = line.split(" ", 1)
|
|
time_part = time_part.rstrip(":")
|
|
# Try 24-hour format first
|
|
try:
|
|
time_obj = datetime.strptime(time_part, "%H:%M")
|
|
except ValueError:
|
|
# Try 12-hour format
|
|
time_obj = datetime.strptime(time_part, "%I:%M")
|
|
|
|
# Try different date formats
|
|
try:
|
|
entry_date = datetime.strptime(date_str, "%Y-%m-%d")
|
|
except ValueError:
|
|
try:
|
|
entry_date = datetime.strptime(date_str, "%m/%d/%Y")
|
|
except ValueError:
|
|
# If both fail, try ISO format
|
|
entry_date = datetime.fromisoformat(date_str)
|
|
|
|
full_timestamp = entry_date.replace(
|
|
hour=time_obj.hour,
|
|
minute=time_obj.minute,
|
|
second=0,
|
|
microsecond=0,
|
|
)
|
|
timestamp_str = full_timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
dose_entries.append(f"{timestamp_str}:{dose_part.strip()}")
|
|
self.logger.debug(
|
|
"Added simple dose entry: '%s:%s'",
|
|
timestamp_str,
|
|
dose_part.strip(),
|
|
)
|
|
except Exception as e:
|
|
self.logger.warning(
|
|
f"Could not parse simple dose line: '{line}'. Error: {e}"
|
|
)
|
|
continue
|
|
|
|
# If user just types a dose (no time), store as-is with no timestamp
|
|
elif line:
|
|
self.logger.debug(f"Line with no time, storing as-is: '{line}'")
|
|
dose_entries.append(line)
|
|
|
|
result = "|".join(dose_entries)
|
|
self.logger.debug(f"Final parsed result: '{result}'")
|
|
return result
|
|
|
|
def _bind_mousewheel_to_widget_tree(
|
|
self, widget: tk.Widget, canvas: tk.Canvas
|
|
) -> None:
|
|
"""Recursively bind mouse wheel events to all widgets in the tree."""
|
|
|
|
def on_mousewheel(event):
|
|
# Check if canvas is scrollable before scrolling
|
|
if canvas.cget("scrollregion"):
|
|
canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
|
|
|
|
def on_mousewheel_linux_up(event):
|
|
if canvas.cget("scrollregion"):
|
|
canvas.yview_scroll(-1, "units")
|
|
|
|
def on_mousewheel_linux_down(event):
|
|
if canvas.cget("scrollregion"):
|
|
canvas.yview_scroll(1, "units")
|
|
|
|
# Bind to the widget itself
|
|
try:
|
|
widget.bind("<MouseWheel>", on_mousewheel)
|
|
widget.bind("<Button-4>", on_mousewheel_linux_up)
|
|
widget.bind("<Button-5>", on_mousewheel_linux_down)
|
|
except tk.TclError:
|
|
# Some widgets might not support binding
|
|
pass
|
|
|
|
# Recursively bind to all children
|
|
try:
|
|
for child in widget.winfo_children():
|
|
# Skip widgets that have their own scrolling behavior or are problematic
|
|
skip_types = (tk.Text, tk.Listbox, tk.Canvas, ttk.Notebook)
|
|
if not isinstance(child, skip_types):
|
|
self._bind_mousewheel_to_widget_tree(child, canvas)
|
|
elif isinstance(child, ttk.Notebook):
|
|
# For notebooks, bind to their tab frames
|
|
for tab_id in child.tabs():
|
|
tab_widget = child.nametowidget(tab_id)
|
|
self._bind_mousewheel_to_widget_tree(tab_widget, canvas)
|
|
except tk.TclError:
|
|
# Handle potential errors when accessing children
|
|
pass
|