# -*- coding: utf-8 -*- """AzA Bibliothek — Werkzeugfenster im AzA-Blau/Karten-Stil (wie KI-Guthaben). Bibliothek ist NUR für Medikamente, Begriffe, Personen/Signaturen, Korrekturen. Prompt-Vorlagen sind ein eigenes Modul und NICHT Teil dieser UI. """ from __future__ import annotations import tkinter as tk from tkinter import messagebox, ttk from typing import Any, Callable, List from aza_bibliothek import ( CATEGORY_LABELS, CATEGORY_TO_STORE, PRIVATE_UI_CATEGORIES, PUBLIC_UI_CATEGORIES, adopt_public_entry, delete_private_correction, list_private_correction_rows, list_public_entries, save_private_correction, toggle_private_correction_active, ) # AzA-Design (identisch KI-Guthaben-Fenster) _BG = "#E8F4FA" _HDR_BG = "#1A4D6D" _HDR_FG = "#FFFFFF" _SUB_BG = "#D0E8F5" _CARD_BG = "#FFFFFF" _CARD_BD = "#C8D8E8" _ACCENT = "#5B8DB3" _ACCENT_HOV = "#4A7A9E" _TEXT = "#1A3D55" _TEXT_SUB = "#607890" _WARN = "#B06000" _FF = "Segoe UI" # Fenstergröße (großes AzA-Werkzeugfenster) _WIN_W = 1300 _WIN_H = 880 _WIN_MIN_W = 1100 _WIN_MIN_H = 760 def _pill(parent, text: str, *, active: bool, command: Callable[[], None]): lbl = tk.Label( parent, text=text, font=(_FF, 10), bg=_ACCENT if active else _CARD_BG, fg="#FFFFFF" if active else _TEXT, padx=12, pady=7, cursor="hand2", highlightthickness=1, highlightbackground=_CARD_BD, anchor="w", ) lbl.bind("", lambda _e: command()) return lbl def _btn(parent, text: str, command, *, primary: bool = False, danger: bool = False): bg = "#C04040" if danger else (_ACCENT if primary else _CARD_BG) fg = "#FFFFFF" if (primary or danger) else _ACCENT return tk.Button( parent, text=text, command=command, font=(_FF, 10), bg=bg, fg=fg, activebackground=_ACCENT_HOV if primary else _SUB_BG, relief="flat", padx=14, pady=6, cursor="hand2", highlightthickness=1, highlightbackground=_CARD_BD, ) def _center_top(win, w: int, h: int) -> None: """Zentriert das Fenster horizontal, leicht oben (analog AzA-Werkzeugfenster).""" try: win.update_idletasks() sw = win.winfo_screenwidth() sh = win.winfo_screenheight() x = max(0, (sw - w) // 2) y = max(0, int(sh * 0.06)) win.geometry(f"{w}x{h}+{x}+{y}") except Exception: win.geometry(f"{w}x{h}") def _bring_to_front(win) -> None: """Kurz topmost, dann wieder normal — wie bestehende AzA-Tools.""" try: win.lift() win.attributes("-topmost", True) win.after(300, lambda: _safe_untopmost(win)) win.focus_force() except Exception: pass def _safe_untopmost(win) -> None: try: if win.winfo_exists(): win.attributes("-topmost", False) except Exception: pass def open_bibliothek_window(app: Any) -> None: """Singleton Bibliothek-Fenster.""" existing = getattr(app, "_bibliothek_win", None) if existing is not None: try: if existing.winfo_exists(): existing.lift() _bring_to_front(existing) return except Exception: pass try: app._sync_practice_users_to_bibliothek() except Exception: pass win = tk.Toplevel(app) app._bibliothek_win = win win.title("Bibliothek") win.configure(bg=_BG) win.minsize(_WIN_MIN_W, _WIN_MIN_H) _center_top(win, _WIN_W, _WIN_H) try: win.transient(app) except Exception: pass state = {"scope": "private", "category": "medication", "selected": None} # ── Header ──────────────────────────────────────────────────────────────── hdr = tk.Frame(win, bg=_HDR_BG) hdr.pack(fill="x") tk.Label(hdr, text="Bibliothek", font=(_FF, 15, "bold"), bg=_HDR_BG, fg=_HDR_FG, padx=20, pady=14).pack(side="left") close_x = tk.Label(hdr, text="✕", font=(_FF, 12), bg=_HDR_BG, fg="#A0C4D8", cursor="hand2", padx=18) close_x.pack(side="right") sub = tk.Frame(win, bg=_SUB_BG) sub.pack(fill="x") tk.Label( sub, text="Medizinische Begriffe, Medikamente, Personen/Signaturen und Korrekturen verwalten. " "Eigene Einträge werden bevorzugt verwendet. Öffentliche Einträge können übernommen werden.", font=(_FF, 9), bg=_SUB_BG, fg=_TEXT_SUB, padx=20, pady=8, wraplength=_WIN_W - 60, justify="left", ).pack(anchor="w") body = tk.Frame(win, bg=_BG) body.pack(fill="both", expand=True, padx=16, pady=12) # ── Linke Navigation ────────────────────────────────────────────────────── nav = tk.Frame(body, bg=_CARD_BG, highlightthickness=1, highlightbackground=_CARD_BD, width=260) nav.pack(side="left", fill="y", padx=(0, 14)) nav.pack_propagate(False) nav_inner = tk.Frame(nav, bg=_CARD_BG, padx=12, pady=14) nav_inner.pack(fill="both", expand=True) tk.Label(nav_inner, text="Bibliothek", font=(_FF, 9, "bold"), bg=_CARD_BG, fg=_TEXT_SUB, anchor="w").pack(fill="x", pady=(0, 4)) scope_pills: dict[str, tk.Label] = {} scope_frame = tk.Frame(nav_inner, bg=_CARD_BG) scope_frame.pack(fill="x") tk.Label(nav_inner, text="Kategorien", font=(_FF, 9, "bold"), bg=_CARD_BG, fg=_TEXT_SUB, anchor="w").pack(fill="x", pady=(14, 4)) cat_frame = tk.Frame(nav_inner, bg=_CARD_BG) cat_frame.pack(fill="x") # ── Rechte Karte ────────────────────────────────────────────────────────── card = tk.Frame(body, bg=_CARD_BG, highlightthickness=1, highlightbackground=_CARD_BD) card.pack(side="left", fill="both", expand=True) title_lbl = tk.Label(card, text="", font=(_FF, 13, "bold"), bg=_CARD_BG, fg=_TEXT, anchor="w") title_lbl.pack(fill="x", padx=18, pady=(14, 2)) subtitle_lbl = tk.Label(card, text="", font=(_FF, 9), bg=_CARD_BG, fg=_TEXT_SUB, anchor="w", wraplength=_WIN_W - 360, justify="left") subtitle_lbl.pack(fill="x", padx=18, pady=(0, 10)) search_var = tk.StringVar() search_row = tk.Frame(card, bg=_CARD_BG) search_row.pack(fill="x", padx=18, pady=(0, 8)) tk.Label(search_row, text="Suchen:", bg=_CARD_BG, fg=_TEXT, font=(_FF, 10)).pack(side="left") search_ent = tk.Entry(search_row, textvariable=search_var, font=(_FF, 11), width=44, relief="flat", highlightthickness=1, highlightbackground=_CARD_BD) search_ent.pack(side="left", padx=10) hint_lbl = tk.Label( card, text="Diese Bibliothek hilft bei der korrekten Schreibweise. Sie ersetzt keine medizinische Prüfung.", font=(_FF, 9), bg="#FFF8EE", fg=_WARN, padx=12, pady=7, anchor="w", ) hint_lbl.pack(fill="x", padx=18, pady=(0, 8)) list_frame = tk.Frame(card, bg=_CARD_BG) list_frame.pack(fill="both", expand=True, padx=18, pady=4) listbox = tk.Listbox( list_frame, font=(_FF, 11), height=16, bg="#FAFCFE", fg=_TEXT, selectbackground=_ACCENT, selectforeground="#FFFFFF", relief="flat", highlightthickness=1, highlightbackground=_CARD_BD, ) scroll = ttk.Scrollbar(list_frame, orient="vertical", command=listbox.yview) listbox.configure(yscrollcommand=scroll.set) listbox.pack(side="left", fill="both", expand=True) scroll.pack(side="right", fill="y") detail = tk.Frame(card, bg=_CARD_BG) detail.pack(fill="x", padx=18, pady=10) detail_vars = {"falsch": tk.StringVar(), "richtig": tk.StringVar(), "note": tk.StringVar()} tk.Label(detail, text="Hörvariante / Begriff:", bg=_CARD_BG, font=(_FF, 9)).grid(row=0, column=0, sticky="w") falsch_ent = tk.Entry(detail, textvariable=detail_vars["falsch"], width=34, font=(_FF, 11)) falsch_ent.grid(row=0, column=1, padx=8) tk.Label(detail, text="Bevorzugte Schreibweise:", bg=_CARD_BG, font=(_FF, 9)).grid(row=1, column=0, sticky="w", pady=6) richtig_ent = tk.Entry(detail, textvariable=detail_vars["richtig"], width=34, font=(_FF, 11)) richtig_ent.grid(row=1, column=1, padx=8, pady=6) meta_lbl = tk.Label(detail, textvariable=detail_vars["note"], bg=_CARD_BG, fg=_TEXT_SUB, font=(_FF, 9), wraplength=_WIN_W - 420, justify="left") meta_lbl.grid(row=2, column=0, columnspan=2, sticky="w", pady=(4, 0)) btn_row = tk.Frame(card, bg=_CARD_BG) btn_row.pack(fill="x", padx=18, pady=(6, 16)) rows_cache: List[dict] = [] def _close(): try: app._bibliothek_win = None except Exception: pass try: win.destroy() except Exception: pass close_x.bind("", lambda _e: _close()) win.protocol("WM_DELETE_WINDOW", _close) cat_pills: dict[str, tk.Label] = {} def _rebuild_category_pills(): for w in cat_frame.winfo_children(): w.destroy() cat_pills.clear() cats = PRIVATE_UI_CATEGORIES if state["scope"] == "private" else PUBLIC_UI_CATEGORIES for cat in cats: p = _pill(cat_frame, CATEGORY_LABELS.get(cat, cat), active=(cat == state["category"]), command=lambda c=cat: _set_category(c)) p.pack(fill="x", pady=2) cat_pills[cat] = p def _visible_rows() -> List[dict]: q = (search_var.get() or "").strip().lower() out = [] for row in rows_cache: if q and q not in _format_row_label(row).lower(): continue out.append(row) return out def _format_row_label(row: dict) -> str: if row.get("scope") == "public" and row.get("category") == "medication": sub = row.get("active_substance") base = row.get("preferred_spelling") or row.get("term") or "" return f"{base} ({sub}) · AzA kuratiert" if sub else f"{base} · AzA kuratiert" falsch = row.get("term") or "" richtig = row.get("preferred_spelling") or "" inact = "" if row.get("active", True) else " [inaktiv]" src = row.get("source") or "" if falsch and richtig and falsch.lower() != richtig.lower(): return f"{falsch} → {richtig}{inact} ({src})" return f"{richtig or falsch}{inact} ({src})" def _load_rows() -> List[dict]: if state["scope"] == "private": from aza_persistence import load_korrekturen rows = list_private_correction_rows(load_korrekturen()) if state["category"] == "correction": return rows return [r for r in rows if r.get("category") == state["category"]] return list_public_entries(category=state["category"]) def _refresh_list(): nonlocal rows_cache rows_cache = _load_rows() listbox.delete(0, "end") state["selected"] = None detail_vars["falsch"].set("") detail_vars["richtig"].set("") detail_vars["note"].set("") if state["scope"] == "private": title_lbl.config(text="Eigene Bibliothek") subtitle_lbl.config( text="Diese Einträge werden für Ihre Transkriptionen, Korrekturen und Dokumente bevorzugt verwendet.") else: title_lbl.config(text="Öffentliche Bibliothek") subtitle_lbl.config( text="Von AzA kuratierte oder von Ärztinnen und Ärzten veröffentlichte Einträge. " "Übernommene Einträge werden als eigene Kopie gespeichert.") for row in _visible_rows(): listbox.insert("end", _format_row_label(row)) _rebuild_buttons() def _on_select(_e=None): sel = listbox.curselection() if not sel: return visible = _visible_rows() if sel[0] >= len(visible): return row = visible[sel[0]] state["selected"] = row detail_vars["falsch"].set(row.get("term") or "") detail_vars["richtig"].set(row.get("preferred_spelling") or row.get("term") or "") parts = [] if row.get("active_substance"): parts.append(f"Wirkstoff: {row['active_substance']}") if row.get("source"): parts.append(f"Herkunft: {row['source']}") if not row.get("active", True): parts.append("Status: inaktiv") detail_vars["note"].set(" · ".join(parts)) _rebuild_buttons() listbox.bind("<>", _on_select) search_var.trace_add("write", lambda *_a: _refresh_list()) # ── Aktionen ─────────────────────────────────────────────────────────────── def _store_for_state() -> str: sel = state.get("selected") if sel and sel.get("store_category"): return sel["store_category"] return CATEGORY_TO_STORE.get(state["category"], "begriffe") def _new_entry(): state["selected"] = None detail_vars["falsch"].set("") detail_vars["richtig"].set("") detail_vars["note"].set("") listbox.selection_clear(0, "end") falsch_ent.focus_set() _rebuild_buttons() def _save_private(): f = detail_vars["falsch"].get().strip() r = detail_vars["richtig"].get().strip() if not f or not r: messagebox.showinfo("Bibliothek", "Bitte Hörvariante und bevorzugte Schreibweise ausfüllen.", parent=win) return save_private_correction(_store_for_state(), f, r, active=True) _refresh_list() try: app.set_status("Bibliothek: Eintrag gespeichert.") except Exception: pass def _toggle_active(): row = state.get("selected") if not row: return toggle_private_correction_active(row.get("store_category") or "begriffe", row.get("term") or "") _refresh_list() def _delete_private(): row = state.get("selected") if not row: return if not messagebox.askyesno("Löschen?", "Eintrag wirklich löschen?", parent=win): return delete_private_correction(row.get("store_category") or "begriffe", row.get("term") or "") _refresh_list() def _adopt_public(): row = state.get("selected") if not row: messagebox.showinfo("Bibliothek", "Bitte zuerst einen öffentlichen Eintrag auswählen.", parent=win) return ok, msg = adopt_public_entry(row) messagebox.showinfo("Bibliothek", msg, parent=win) def _details_public(): row = state.get("selected") if not row: messagebox.showinfo("Bibliothek", "Bitte zuerst einen Eintrag auswählen.", parent=win) return lines = [] if row.get("brand_name"): lines.append(f"Markenname: {row['brand_name']}") if row.get("active_substance"): lines.append(f"Wirkstoff: {row['active_substance']}") if row.get("term"): lines.append(f"Begriff: {row['term']}") if row.get("preferred_spelling"): lines.append(f"Bevorzugt: {row['preferred_spelling']}") if row.get("market_region"): lines.append(f"Region: {row['market_region']}") if row.get("source"): lines.append(f"Herkunft: {row['source']}") messagebox.showinfo("Details", "\n".join(lines) or "Keine Details.", parent=win) def _rebuild_buttons(): for w in btn_row.winfo_children(): w.destroy() has_sel = state.get("selected") is not None if state["scope"] == "private": _btn(btn_row, "Neuer Eintrag", _new_entry).pack(side="left", padx=(0, 8)) _btn(btn_row, "Speichern", _save_private, primary=True).pack(side="left", padx=(0, 8)) if has_sel: row = state["selected"] if row.get("active", True): _btn(btn_row, "Deaktivieren", _toggle_active).pack(side="left", padx=(0, 8)) else: _btn(btn_row, "Aktivieren", _toggle_active).pack(side="left", padx=(0, 8)) _btn(btn_row, "Löschen", _delete_private, danger=True).pack(side="left", padx=(0, 8)) else: _btn(btn_row, "Übernehmen", _adopt_public, primary=True).pack(side="left", padx=(0, 8)) _btn(btn_row, "Details", _details_public).pack(side="left", padx=(0, 8)) _btn(btn_row, "Aktualisieren", _refresh_list).pack(side="left", padx=(0, 8)) _btn(btn_row, "Schliessen", _close).pack(side="right") def _set_scope(scope: str): state["scope"] = scope cats = PRIVATE_UI_CATEGORIES if scope == "private" else PUBLIC_UI_CATEGORIES if state["category"] not in cats: state["category"] = cats[0] for k, p in scope_pills.items(): p.config(bg=_ACCENT if k == scope else _CARD_BG, fg="#FFFFFF" if k == scope else _TEXT) # Detailfelder bei öffentlicher Bibliothek nur lesend nutzen editable = "normal" if scope == "private" else "disabled" try: falsch_ent.config(state=editable) richtig_ent.config(state=editable) except Exception: pass _rebuild_category_pills() _refresh_list() def _set_category(cat: str): state["category"] = cat _rebuild_category_pills() _refresh_list() scope_pills["private"] = _pill(scope_frame, "Eigene Bibliothek", active=True, command=lambda: _set_scope("private")) scope_pills["private"].pack(fill="x", pady=2) scope_pills["public"] = _pill(scope_frame, "Öffentliche Bibliothek", active=False, command=lambda: _set_scope("public")) scope_pills["public"].pack(fill="x", pady=2) _rebuild_category_pills() _refresh_list() _bring_to_front(win)