729 lines
26 KiB
Python
729 lines
26 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
|
|
|
|
|
|
class UIManager:
|
|
"""Handle UI creation and management for the application."""
|
|
|
|
def __init__(self, root: tk.Tk, logger: logging.Logger) -> None:
|
|
self.root: tk.Tk = root
|
|
self.logger: logging.Logger = logger
|
|
|
|
def setup_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)
|
|
|
|
# Configure canvas scrolling
|
|
def configure_scroll_region(event=None):
|
|
canvas.configure(scrollregion=canvas.bbox("all"))
|
|
|
|
def on_mousewheel(event):
|
|
canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
|
|
|
|
input_frame.bind("<Configure>", configure_scroll_region)
|
|
canvas.bind("<MouseWheel>", on_mousewheel) # Windows/Linux
|
|
canvas.bind("<Button-4>", lambda e: canvas.yview_scroll(-1, "units")) # Linux
|
|
canvas.bind("<Button-5>", lambda e: canvas.yview_scroll(1, "units")) # Linux
|
|
|
|
# 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)
|
|
|
|
canvas.bind("<Configure>", configure_canvas_width)
|
|
|
|
# Create variables for symptoms
|
|
symptom_vars: dict[str, tk.IntVar] = {
|
|
"depression": tk.IntVar(value=0),
|
|
"anxiety": tk.IntVar(value=0),
|
|
"sleep": tk.IntVar(value=0),
|
|
"appetite": tk.IntVar(value=0),
|
|
}
|
|
|
|
# Create scales for symptoms
|
|
symptom_labels: list[tuple[str, str]] = [
|
|
("Depression (0-10):", "depression"),
|
|
("Anxiety (0-10):", "anxiety"),
|
|
("Sleep Quality (0-10):", "sleep"),
|
|
("Appetite (0-10):", "appetite"),
|
|
]
|
|
|
|
for idx, (label, var_name) in enumerate(symptom_labels):
|
|
ttk.Label(input_frame, text=label).grid(
|
|
row=idx, column=0, sticky="w", padx=5, pady=2
|
|
)
|
|
ttk.Scale(
|
|
input_frame,
|
|
from_=0,
|
|
to=10,
|
|
orient=tk.HORIZONTAL,
|
|
variable=symptom_vars[var_name],
|
|
).grid(row=idx, column=1, sticky="ew")
|
|
|
|
# Medicine tracking section (simplified)
|
|
ttk.Label(input_frame, text="Treatment:").grid(
|
|
row=4, column=0, sticky="w", padx=5, pady=2
|
|
)
|
|
medicine_frame = ttk.LabelFrame(input_frame, text="Medicine")
|
|
medicine_frame.grid(row=4, column=1, padx=0, pady=10, sticky="nsew")
|
|
medicine_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
# Store medicine variables (checkboxes only)
|
|
medicine_vars: dict[str, tuple[tk.IntVar, str]] = {
|
|
"bupropion": (tk.IntVar(value=0), "Bupropion 150/300 mg"),
|
|
"hydroxyzine": (tk.IntVar(value=0), "Hydroxyzine 25mg"),
|
|
"gabapentin": (tk.IntVar(value=0), "Gabapentin 100mg"),
|
|
"propranolol": (tk.IntVar(value=0), "Propranolol 10mg"),
|
|
}
|
|
|
|
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
|
|
note_var: tk.StringVar = tk.StringVar()
|
|
date_var: tk.StringVar = tk.StringVar()
|
|
|
|
ttk.Label(input_frame, text="Note:").grid(
|
|
row=5, column=0, sticky="w", padx=5, pady=2
|
|
)
|
|
ttk.Entry(input_frame, textvariable=note_var).grid(
|
|
row=5, column=1, sticky="ew", padx=5, pady=2
|
|
)
|
|
|
|
ttk.Label(input_frame, text="Date (mm/dd/yyyy):").grid(
|
|
row=6, column=0, sticky="w", padx=5, pady=2
|
|
)
|
|
ttk.Entry(input_frame, textvariable=date_var, justify="center").grid(
|
|
row=6, column=1, sticky="ew", padx=5, pady=2
|
|
)
|
|
|
|
# Set default date to today
|
|
date_var.set(datetime.now().strftime("%m/%d/%Y"))
|
|
|
|
# Return all UI elements and variables
|
|
return {
|
|
"frame": main_container,
|
|
"symptom_vars": symptom_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)
|
|
|
|
columns: list[str] = [
|
|
"Date",
|
|
"Depression",
|
|
"Anxiety",
|
|
"Sleep",
|
|
"Appetite",
|
|
"Bupropion",
|
|
"Hydroxyzine",
|
|
"Gabapentin",
|
|
"Propranolol",
|
|
"Note",
|
|
]
|
|
|
|
tree: ttk.Treeview = ttk.Treeview(table_frame, columns=columns, show="headings")
|
|
|
|
col_labels: list[str] = [
|
|
"Date",
|
|
"Depression",
|
|
"Anxiety",
|
|
"Sleep",
|
|
"Appetite",
|
|
"Bupropion 150/300 mg",
|
|
"Hydroxyzine 25mg",
|
|
"Gabapentin 100mg",
|
|
"Propranolol 10mg",
|
|
"Note",
|
|
]
|
|
|
|
for col, label in zip(columns, col_labels, strict=False):
|
|
tree.heading(col, text=label)
|
|
|
|
col_settings: list[tuple[str, int, str]] = [
|
|
("Date", 80, "center"),
|
|
("Depression", 80, "center"),
|
|
("Anxiety", 80, "center"),
|
|
("Sleep", 80, "center"),
|
|
("Appetite", 80, "center"),
|
|
("Bupropion", 120, "center"),
|
|
("Hydroxyzine", 120, "center"),
|
|
("Gabapentin", 120, "center"),
|
|
("Propranolol", 120, "center"),
|
|
("Note", 300, "w"),
|
|
]
|
|
|
|
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_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_edit_window(
|
|
self, values: tuple[str, ...], callbacks: dict[str, Callable]
|
|
) -> tk.Toplevel:
|
|
"""Create a new window for editing an entry."""
|
|
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(400, 300)
|
|
|
|
# Configure grid columns to expand properly
|
|
edit_win.grid_columnconfigure(1, weight=1)
|
|
|
|
# Unpack values - handle both old and new CSV formats
|
|
if len(values) == 10:
|
|
# Old format: date, dep, anx, slp, app, bup, hydro, gaba, prop, note
|
|
date, dep, anx, slp, app, bup, hydro, gaba, prop, note = values
|
|
bup_doses, hydro_doses, gaba_doses, prop_doses = "", "", "", ""
|
|
elif len(values) == 14:
|
|
# New format with dose tracking
|
|
(
|
|
date,
|
|
dep,
|
|
anx,
|
|
slp,
|
|
app,
|
|
bup,
|
|
bup_doses,
|
|
hydro,
|
|
hydro_doses,
|
|
gaba,
|
|
gaba_doses,
|
|
prop,
|
|
prop_doses,
|
|
note,
|
|
) = values
|
|
else:
|
|
# Fallback for unexpected format
|
|
self.logger.warning(f"Unexpected number of values in edit: {len(values)}")
|
|
# Pad with default values
|
|
values_list = list(values) + [""] * (14 - len(values))
|
|
(
|
|
date,
|
|
dep,
|
|
anx,
|
|
slp,
|
|
app,
|
|
bup,
|
|
bup_doses,
|
|
hydro,
|
|
hydro_doses,
|
|
gaba,
|
|
gaba_doses,
|
|
prop,
|
|
prop_doses,
|
|
note,
|
|
) = values_list[:14]
|
|
|
|
# Create variables and fields
|
|
vars_dict = self._create_edit_fields(edit_win, date, dep, anx, slp, app)
|
|
|
|
# Medicine checkboxes
|
|
current_row = 6 # After the 5 fields (date, dep, anx, slp, app)
|
|
med_vars = self._create_medicine_checkboxes(
|
|
edit_win, current_row, bup, hydro, gaba, prop
|
|
)
|
|
vars_dict.update(med_vars)
|
|
|
|
# Dose information display (editable)
|
|
current_row += 1
|
|
dose_vars = self._add_dose_display_to_edit(
|
|
edit_win,
|
|
current_row,
|
|
{
|
|
"bupropion": bup_doses,
|
|
"hydroxyzine": hydro_doses,
|
|
"gabapentin": gaba_doses,
|
|
"propranolol": prop_doses,
|
|
},
|
|
)
|
|
vars_dict.update(dose_vars)
|
|
|
|
# Note field
|
|
current_row += 2 # Account for dose display
|
|
vars_dict["note"] = tk.StringVar(value=str(note))
|
|
ttk.Label(edit_win, text="Note:").grid(
|
|
row=current_row, column=0, sticky="w", padx=5, pady=2
|
|
)
|
|
ttk.Entry(edit_win, textvariable=vars_dict["note"]).grid(
|
|
row=current_row, column=1, sticky="ew", padx=5, pady=2
|
|
)
|
|
|
|
# Buttons
|
|
current_row += 1
|
|
self._add_edit_window_buttons(edit_win, current_row, vars_dict, callbacks)
|
|
|
|
# Make window modal
|
|
edit_win.update_idletasks()
|
|
edit_win.focus_set()
|
|
edit_win.grab_set()
|
|
|
|
return edit_win
|
|
|
|
def _create_edit_fields(
|
|
self,
|
|
parent: tk.Toplevel,
|
|
date: str,
|
|
dep: int,
|
|
anx: int,
|
|
slp: int,
|
|
app: int,
|
|
) -> dict[str, tk.StringVar | tk.IntVar]:
|
|
"""Create fields for editing entry values."""
|
|
vars_dict: dict[str, tk.StringVar | tk.IntVar] = {}
|
|
|
|
# Ensure values are converted to appropriate types
|
|
try:
|
|
app = int(app) if app != "" else 0
|
|
except (ValueError, TypeError):
|
|
self.logger.warning(f"Invalid appetite value: {app}, defaulting to 0")
|
|
app = 0
|
|
|
|
value_map = {
|
|
"date": date,
|
|
"depression": dep,
|
|
"anxiety": anx,
|
|
"sleep": slp,
|
|
"appetite": app,
|
|
}
|
|
|
|
fields = [
|
|
("Date", tk.StringVar, "date"),
|
|
("Depression (0-10)", tk.IntVar, "depression"),
|
|
("Anxiety (0-10)", tk.IntVar, "anxiety"),
|
|
("Sleep (0-10)", tk.IntVar, "sleep"),
|
|
("Appetite (0-10)", tk.IntVar, "appetite"),
|
|
]
|
|
|
|
for idx, (label, var_type, key) in enumerate(fields):
|
|
try:
|
|
value = value_map[key]
|
|
if var_type == tk.IntVar:
|
|
try:
|
|
value = int(float(value))
|
|
except (ValueError, TypeError):
|
|
value = 0
|
|
self.logger.warning(
|
|
f"Failed to convert {key} value: {value}, defaulting to 0"
|
|
)
|
|
else:
|
|
value = str(value)
|
|
except (ValueError, TypeError, KeyError):
|
|
value = 0 if var_type == tk.IntVar else ""
|
|
self.logger.warning(
|
|
f"Missing or invalid value for {key}, defaulting to {value}"
|
|
)
|
|
|
|
vars_dict[key] = var_type(value=value)
|
|
ttk.Label(parent, text=f"{label}:").grid(
|
|
row=idx + 1, column=0, sticky="w", padx=5, pady=2
|
|
)
|
|
|
|
if var_type == tk.IntVar:
|
|
self._create_scale_with_label(parent, idx + 1, vars_dict[key], value)
|
|
else:
|
|
ttk.Entry(parent, textvariable=vars_dict[key]).grid(
|
|
row=idx + 1, column=1, sticky="ew"
|
|
)
|
|
|
|
return vars_dict
|
|
|
|
def _create_scale_with_label(
|
|
self, parent: tk.Toplevel, row: int, var: tk.IntVar, value: int
|
|
) -> None:
|
|
"""Create a scale with a value label."""
|
|
scale_frame: ttk.Frame = ttk.Frame(parent)
|
|
scale_frame.grid(row=row, column=1, sticky="ew", padx=5, pady=2)
|
|
scale_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
scale = ttk.Scale(
|
|
scale_frame, from_=0, to=10, variable=var, orient=tk.HORIZONTAL
|
|
)
|
|
scale.grid(row=0, column=0, sticky="ew", padx=5)
|
|
|
|
# Add a value label to show the current value
|
|
value_label = ttk.Label(scale_frame, width=3)
|
|
value_label.grid(row=0, column=1, padx=(5, 0))
|
|
|
|
# Update label when scale value changes
|
|
def update_label(event=None):
|
|
value_label.configure(text=str(var.get()))
|
|
|
|
scale.bind("<Motion>", update_label)
|
|
scale.bind("<ButtonRelease-1>", update_label)
|
|
update_label() # Set initial value
|
|
scale.set(value) # Explicitly set scale value
|
|
|
|
def _create_medicine_checkboxes(
|
|
self,
|
|
parent: tk.Toplevel,
|
|
row: int,
|
|
bup: int,
|
|
hydro: int,
|
|
gaba: int,
|
|
prop: int,
|
|
) -> dict[str, tk.IntVar]:
|
|
"""Create medicine checkboxes in the edit window."""
|
|
ttk.Label(parent, text="Treatment:").grid(
|
|
row=row, column=0, sticky="w", padx=5, pady=2
|
|
)
|
|
medicine_frame: ttk.LabelFrame = ttk.LabelFrame(parent, text="Medicine")
|
|
medicine_frame.grid(row=row, column=1, padx=0, pady=10, sticky="nsew")
|
|
|
|
medicine_vars: dict[str, tuple[int, str]] = {
|
|
"bupropion": (bup, "Bupropion 150/300 mg"),
|
|
"hydroxyzine": (hydro, "Hydroxyzine 25mg"),
|
|
"gabapentin": (gaba, "Gabapentin 100mg"),
|
|
"propranolol": (prop, "Propranolol 10mg"),
|
|
}
|
|
|
|
vars_dict: dict[str, tk.IntVar] = {}
|
|
for idx, (key, (value, label)) in enumerate(medicine_vars.items()):
|
|
vars_dict[key] = tk.IntVar(value=int(value))
|
|
ttk.Checkbutton(medicine_frame, text=label, variable=vars_dict[key]).grid(
|
|
row=idx, column=0, sticky="w", padx=5, pady=2
|
|
)
|
|
|
|
return vars_dict
|
|
|
|
def _add_edit_window_buttons(
|
|
self,
|
|
parent: tk.Toplevel,
|
|
row: int,
|
|
vars_dict: dict[str, Any],
|
|
callbacks: dict[str, Callable],
|
|
) -> None:
|
|
"""Add buttons to the edit window."""
|
|
button_frame: ttk.Frame = ttk.Frame(parent)
|
|
button_frame.grid(row=row, column=0, columnspan=2, pady=10)
|
|
|
|
# Save button - create a custom callback to handle dose data
|
|
def save_with_doses():
|
|
# Extract dose data from the text widgets
|
|
dose_data = {}
|
|
for medicine in ["bupropion", "hydroxyzine", "gabapentin", "propranolol"]:
|
|
dose_text_key = f"{medicine}_doses_text"
|
|
if dose_text_key in vars_dict and isinstance(
|
|
vars_dict[dose_text_key], tk.Text
|
|
):
|
|
raw_text = vars_dict[dose_text_key].get(1.0, tk.END).strip()
|
|
dose_data[medicine] = self._parse_dose_text(
|
|
raw_text, vars_dict["date"].get()
|
|
)
|
|
else:
|
|
dose_data[medicine] = ""
|
|
|
|
callbacks["save"](
|
|
parent,
|
|
vars_dict["date"].get(),
|
|
vars_dict["depression"].get(),
|
|
vars_dict["anxiety"].get(),
|
|
vars_dict["sleep"].get(),
|
|
vars_dict["appetite"].get(),
|
|
vars_dict["bupropion"].get(),
|
|
vars_dict["hydroxyzine"].get(),
|
|
vars_dict["gabapentin"].get(),
|
|
vars_dict["propranolol"].get(),
|
|
vars_dict["note"].get(),
|
|
dose_data,
|
|
)
|
|
|
|
ttk.Button(
|
|
button_frame,
|
|
text="Save",
|
|
command=save_with_doses,
|
|
).pack(side="left", padx=5)
|
|
|
|
# Cancel button
|
|
ttk.Button(button_frame, text="Cancel", command=parent.destroy).pack(
|
|
side="left", padx=5
|
|
)
|
|
|
|
# Delete button
|
|
ttk.Button(
|
|
button_frame,
|
|
text="Delete",
|
|
command=lambda: callbacks["delete"](parent),
|
|
).pack(side="left", padx=5)
|
|
|
|
def _add_dose_display_to_edit(
|
|
self, parent: tk.Toplevel, row: int, dose_data: dict[str, str]
|
|
) -> dict[str, tk.Text]:
|
|
"""Add comprehensive dose tracking to edit window with punch buttons."""
|
|
ttk.Label(parent, text="Dose Tracking:").grid(
|
|
row=row, column=0, sticky="w", padx=5, pady=2
|
|
)
|
|
|
|
dose_frame = ttk.LabelFrame(parent, text="Medicine Doses")
|
|
dose_frame.grid(row=row, column=1, padx=5, pady=2, sticky="ew")
|
|
dose_frame.grid_columnconfigure(2, weight=1)
|
|
|
|
dose_vars = {}
|
|
|
|
for idx, (medicine, doses_str) in enumerate(dose_data.items()):
|
|
# Medicine label
|
|
med_label = ttk.Label(dose_frame, text=f"{medicine.title()}:")
|
|
med_label.grid(row=idx, column=0, sticky="w", padx=5, pady=2)
|
|
|
|
# Dose entry field for new doses
|
|
dose_entry_var = tk.StringVar()
|
|
dose_entry = ttk.Entry(dose_frame, textvariable=dose_entry_var, width=12)
|
|
dose_entry.grid(row=idx, column=1, sticky="w", padx=5, pady=2)
|
|
|
|
# Store entry variable in dose_vars for access from punch button
|
|
dose_vars[f"{medicine}_entry_var"] = dose_entry_var
|
|
|
|
# Display area for existing doses (editable)
|
|
dose_text = tk.Text(dose_frame, height=3, width=40, wrap=tk.WORD)
|
|
dose_text.grid(row=idx, column=2, sticky="ew", padx=5, pady=2)
|
|
|
|
# Store text widget in dose_vars
|
|
dose_vars[f"{medicine}_doses_text"] = dose_text
|
|
|
|
# Punch button to record dose immediately
|
|
punch_button = ttk.Button(
|
|
dose_frame,
|
|
text=f"Take {medicine.title()}",
|
|
width=15,
|
|
command=lambda med=medicine: self._punch_dose_in_edit(med, dose_vars),
|
|
)
|
|
punch_button.grid(row=idx, column=3, sticky="w", padx=5, pady=2)
|
|
|
|
# Parse and format doses for editing
|
|
if doses_str:
|
|
formatted_doses = []
|
|
for dose_entry_str in doses_str.split("|"):
|
|
if ":" in dose_entry_str:
|
|
timestamp, dose = dose_entry_str.split(":", 1)
|
|
# Format timestamp for display
|
|
try:
|
|
dt = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S")
|
|
time_str = dt.strftime("%H:%M")
|
|
formatted_doses.append(f"{time_str}: {dose}")
|
|
except ValueError:
|
|
formatted_doses.append(dose_entry_str)
|
|
|
|
if formatted_doses:
|
|
dose_text.insert(1.0, "\n".join(formatted_doses))
|
|
else:
|
|
dose_text.insert(1.0, "No doses recorded")
|
|
else:
|
|
dose_text.insert(1.0, "No doses recorded")
|
|
|
|
# Add help text below the dose display
|
|
help_label = ttk.Label(
|
|
dose_frame,
|
|
text="Format: HH:MM: dose",
|
|
font=("TkDefaultFont", 8),
|
|
foreground="gray",
|
|
)
|
|
help_label.grid(row=idx, column=4, sticky="w", padx=5, pady=2)
|
|
|
|
return dose_vars
|
|
|
|
def _punch_dose_in_edit(self, medicine_name: str, dose_vars: dict) -> None:
|
|
"""Handle punch dose button in edit window."""
|
|
dose_entry_var = dose_vars.get(f"{medicine_name}_entry_var")
|
|
dose_text_widget = dose_vars.get(f"{medicine_name}_doses_text")
|
|
|
|
if not dose_entry_var or not dose_text_widget:
|
|
return
|
|
|
|
dose = dose_entry_var.get().strip()
|
|
|
|
if not dose:
|
|
messagebox.showerror(
|
|
"Error",
|
|
f"Please enter a dose amount for {medicine_name}",
|
|
)
|
|
return
|
|
|
|
# Get current time
|
|
now = datetime.now()
|
|
time_str = now.strftime("%H:%M")
|
|
|
|
# Get current content
|
|
current_content = dose_text_widget.get(1.0, tk.END).strip()
|
|
|
|
# Add new dose entry
|
|
new_dose_line = f"{time_str}: {dose}"
|
|
|
|
if current_content == "No doses recorded" or not current_content:
|
|
dose_text_widget.delete(1.0, tk.END)
|
|
dose_text_widget.insert(1.0, new_dose_line)
|
|
else:
|
|
dose_text_widget.insert(tk.END, f"\n{new_dose_line}")
|
|
|
|
# Clear the entry field
|
|
dose_entry_var.set("")
|
|
|
|
# Show success message
|
|
messagebox.showinfo(
|
|
"Success",
|
|
f"{medicine_name.title()} dose recorded: {dose} at {time_str}",
|
|
)
|
|
|
|
def _parse_dose_text(self, text: str, date: str) -> str:
|
|
"""Parse dose text from edit window back to CSV format."""
|
|
if not text or text == "No doses recorded":
|
|
return ""
|
|
|
|
lines = text.strip().split("\n")
|
|
dose_entries = []
|
|
|
|
for line in lines:
|
|
line = line.strip()
|
|
if ":" in line and line != "No doses recorded":
|
|
try:
|
|
# Try to parse HH:MM: dose format
|
|
# Split on ': ' (colon followed by space) to separate time from dose
|
|
if ": " in line:
|
|
time_part, dose_part = line.split(": ", 1)
|
|
else:
|
|
# Fallback: split on first colon after HH:MM pattern
|
|
colon_indices = [
|
|
i for i, char in enumerate(line) if char == ":"
|
|
]
|
|
if len(colon_indices) >= 2:
|
|
# Take everything up to the second colon as time
|
|
second_colon_idx = colon_indices[1]
|
|
time_part = line[:second_colon_idx]
|
|
dose_part = line[second_colon_idx + 1 :].strip()
|
|
else:
|
|
continue
|
|
|
|
dose_part = dose_part.strip()
|
|
|
|
# Create timestamp for today
|
|
from datetime import datetime
|
|
|
|
time_str = time_part.strip()
|
|
# Parse just the time (HH:MM format)
|
|
time_obj = datetime.strptime(time_str, "%H:%M")
|
|
|
|
# Create full timestamp with today's date
|
|
today = datetime.strptime(date, "%m/%d/%Y")
|
|
full_timestamp = today.replace(
|
|
hour=time_obj.hour, minute=time_obj.minute, second=0
|
|
)
|
|
|
|
timestamp_str = full_timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
dose_entries.append(f"{timestamp_str}:{dose_part}")
|
|
except ValueError:
|
|
# If parsing fails, skip this line
|
|
continue
|
|
|
|
return "|".join(dose_entries)
|