diff --git a/src/thechart/ui/ui_manager.py b/src/thechart/ui/ui_manager.py index 2ed28e4..6bf2a37 100644 --- a/src/thechart/ui/ui_manager.py +++ b/src/thechart/ui/ui_manager.py @@ -725,15 +725,77 @@ class UIManager: scale.bind("", _mk_scale_cmd(key, scale)) - # Medicines section + # Medicines section (with dose preview and quick add) ttk.Label(content, text="Medicines:").grid(row=row, column=0, sticky="w") meds_frame = ttk.Frame(content) meds_frame.grid(row=row, column=1, sticky="ew", padx=8, pady=4) meds_frame.grid_columnconfigure(0, weight=1) row += 1 + # Helpers for dose formatting/parsing + def _format_storage_to_bullets(storage: str) -> str: + lines: list[str] = [] + if not storage: + return "No doses recorded" + for token in str(storage).split("|"): + token = token.strip() + if not token: + continue + # Expect "YYYY-MM-DD HH:MM:SS:dose" + try: + ts_part, dose_part = token.rsplit(":", 1) + try: + ts = datetime.strptime(ts_part.strip(), "%Y-%m-%d %H:%M:%S") + except ValueError: + ts = datetime.fromisoformat(ts_part.strip()) + time_str = ts.strftime("%I:%M %p").lstrip("0") + lines.append(f"• {time_str} - {dose_part.strip()}") + except Exception: + # Fallback: keep raw + lines.append(token) + return "\n".join(lines) if lines else "No doses recorded" + + def _build_storage_entry(date_s: str, time_s: str, dose_s: str) -> str: + # Parse date + try: + d = datetime.strptime(date_s, "%Y-%m-%d") + except ValueError: + try: + d = datetime.strptime(date_s, "%m/%d/%Y") + except ValueError: + d = datetime.fromisoformat(date_s) + # Parse time (prefer 24h, then 12h) + t_s = time_s.strip() + try: + t = datetime.strptime(t_s, "%H:%M") + except ValueError: + try: + t = datetime.strptime(t_s, "%I:%M %p") + except ValueError: + # If empty, use current time + now = datetime.now() + t = now.replace(second=0, microsecond=0) + stamp = d.replace(hour=t.hour, minute=t.minute, second=0, microsecond=0) + ts_str = stamp.strftime("%Y-%m-%d %H:%M:%S") + # Normalize dose string: append 'mg' if it looks numeric + ds = dose_s.strip() + if ( + ds + and ds.replace(".", "", 1).isdigit() + and not ds.lower().endswith("mg") + ): + ds = f"{ds}mg" + return f"{ts_str}:{ds}" + medicine_vars: dict[str, tk.IntVar] = {} + added_doses: dict[str, list[tuple[str, str]]] = {k: [] for k in medicine_keys} + dose_previews: dict[str, tk.StringVar] = {} + for i, key in enumerate(medicine_keys): + row_frame = ttk.Frame(meds_frame) + row_frame.grid(row=i, column=0, sticky="ew", pady=(0, 6)) + row_frame.grid_columnconfigure(0, weight=1) + medicine_vars[key] = tk.IntVar(value=medicine_taken.get(key, 0)) med = self.medicine_manager.get_medicine(key) text = ( @@ -742,12 +804,106 @@ class UIManager: else key.capitalize() ) chk = ttk.Checkbutton( - meds_frame, + row_frame, text=text, variable=medicine_vars[key], style="Modern.TCheckbutton", ) - chk.grid(row=i, column=0, sticky="w", padx=2, pady=2) + chk.grid(row=0, column=0, sticky="w") + + # Dose preview + quick add panel + preview_var = tk.StringVar( + value=_format_storage_to_bullets(medicine_doses_str.get(key, "")) + ) + dose_previews[key] = preview_var + + panel = ttk.Frame(row_frame) + panel.grid(row=1, column=0, sticky="ew", padx=18) + panel.grid_columnconfigure(1, weight=1) + + ttk.Label(panel, text="Doses:").grid(row=0, column=0, sticky="nw") + ttk.Label(panel, textvariable=preview_var, justify="left").grid( + row=0, column=1, sticky="ew" + ) + + # Add controls + add_row = ttk.Frame(panel) + add_row.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(4, 0)) + ttk.Label(add_row, text="Time (HH:MM or 12h):").grid( + row=0, column=0, sticky="w" + ) + time_var = tk.StringVar() + ttk.Entry(add_row, textvariable=time_var, width=10).grid( + row=0, column=1, sticky="w", padx=(4, 8) + ) + ttk.Label(add_row, text="Dose:").grid(row=0, column=2, sticky="w") + dose_var = tk.StringVar() + ttk.Entry(add_row, textvariable=dose_var, width=10).grid( + row=0, column=3, sticky="w", padx=(4, 8) + ) + + def _do_add( + k: str, t_v: tk.StringVar, d_v: tk.StringVar, pv: tk.StringVar + ) -> None: + t_s = t_v.get().strip() + d_s = d_v.get().strip() + if not d_s: + return + # Record added dose; preview shows bullet line + added_doses[k].append((t_s or datetime.now().strftime("%H:%M"), d_s)) + # Update preview + try: + entry = _build_storage_entry( + date_var.get(), t_s or datetime.now().strftime("%H:%M"), d_s + ) + new_preview = pv.get() + as_bullet = _format_storage_to_bullets(entry) + new_preview = ( + "No doses recorded" + if not new_preview + or new_preview.startswith("No doses recorded") + else new_preview + ) + pv.set( + as_bullet + if new_preview == "No doses recorded" + else f"{new_preview}\n{as_bullet}" + ) + except Exception: + # Best-effort UI update; save path will still append robustly + pass + # Clear inputs for next add + t_v.set("") + d_v.set("") + + ttk.Button( + add_row, + text="Add", + command=lambda k=key, tv=time_var, dv=dose_var, pv=preview_var: _do_add( + k, tv, dv, pv + ), + style="Action.TButton", + ).grid(row=0, column=4, sticky="w") + + # Quick dose shortcuts + qd = self.medicine_manager.get_quick_doses(key) + if qd: + qrow = ttk.Frame(panel) + qrow.grid(row=2, column=0, columnspan=2, sticky="w", pady=(2, 0)) + ttk.Label(qrow, text="Quick:").grid(row=0, column=0, sticky="w") + for j, amt in enumerate(qd): + ttk.Button( + qrow, + text=f"{amt}mg", + command=( + lambda k=key, a=amt, tv=time_var, pv=preview_var: _do_add( + k, + tv, + tk.StringVar(value=a), + pv, + ) + ), + ).grid(row=0, column=j + 1, padx=2) # Note field ttk.Label(content, text="Note:").grid(row=row, column=0, sticky="nw") @@ -772,8 +928,22 @@ class UIManager: args.append(int(medicine_vars[key].get())) # Note args.append(note_var.get()) - # Preserve existing dose strings unless caller offers an editor elsewhere - dose_map = {k: medicine_doses_str.get(k, "") for k in medicine_keys} + # Merge any newly added doses into existing strings + dose_map: dict[str, str] = {} + for k in medicine_keys: + base = medicine_doses_str.get(k, "").strip() + new_entries = [ + _build_storage_entry(date_var.get(), t, d) + for (t, d) in added_doses.get(k, []) + ] + merged = base + if new_entries: + merged = ( + f"{base}|{'|'.join(new_entries)}" + if base + else "|".join(new_entries) + ) + dose_map[k] = merged args.append(dose_map) with suppress(Exception): callbacks.get("save")(win, *args)