757 lines
70 KiB
Plaintext
757 lines
70 KiB
Plaintext
diff --git a/AzA march 2026/basis14.py b/AzA march 2026/basis14.py
|
|
index 60c684d..1ccfe0d 100644
|
|
--- a/AzA march 2026/basis14.py
|
|
+++ b/AzA march 2026/basis14.py
|
|
@@ -468,165 +468,176 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
|
|
if self._autotext_data.get("kommentare_auto_open", False):
|
|
self.after(1000, self._open_kommentare_fenster)
|
|
|
|
if not self.api_key_present:
|
|
try:
|
|
_show_openai_key_setup_dialog()
|
|
if has_openai_api_key():
|
|
api_key = get_openai_api_key()
|
|
self.client = OpenAI(api_key=api_key) if api_key else None
|
|
self.api_key_present = True
|
|
except Exception:
|
|
pass
|
|
|
|
def _dispatch_start_module(self):
|
|
"""Öffnet das per Launcher gewählte Modul."""
|
|
mod = self._start_module
|
|
if not mod or mod == "kg":
|
|
return
|
|
try:
|
|
dispatch = {
|
|
"ki": lambda: None,
|
|
"notizen": lambda: self._start_audio_notiz_addon(silent=False),
|
|
"translator": self._open_uebersetzer,
|
|
"medwork_chat": self._open_docapp,
|
|
"praxis_chat": self._open_docapp,
|
|
}
|
|
handler = dispatch.get(mod)
|
|
if handler is None:
|
|
messagebox.showinfo(
|
|
"Modul nicht verf├╝gbar",
|
|
f"Das Modul '{mod}' ist noch nicht vollständig integriert.\n"
|
|
"Sie k├Ânnen es manuell ├╝ber die Seitenleiste starten.",
|
|
)
|
|
return
|
|
if mod in ("notizen", "translator", "medwork_chat", "praxis_chat"):
|
|
if not self._check_ai_consent():
|
|
return
|
|
if mod in ("translator", "notizen", "medwork_chat", "praxis_chat"):
|
|
self.iconify()
|
|
handler()
|
|
except Exception as e:
|
|
messagebox.showerror("Modul-Start", f"Fehler beim Öffnen von '{mod}':\n{e}")
|
|
|
|
_LOGIN_INTERVAL_DAYS = 7
|
|
|
|
def _login_needed(self) -> bool:
|
|
"""True wenn der letzte Login länger als _LOGIN_INTERVAL_DAYS her ist."""
|
|
last_ts = self._user_profile.get("last_login_ts")
|
|
if not isinstance(last_ts, (int, float)):
|
|
return True
|
|
elapsed = time.time() - float(last_ts)
|
|
return elapsed > (self._LOGIN_INTERVAL_DAYS * 86400)
|
|
|
|
def _record_login(self):
|
|
self._user_profile["last_login_ts"] = int(time.time())
|
|
save_user_profile(self._user_profile)
|
|
|
|
def check_license_status(self):
|
|
self.license_mode = "demo"
|
|
license_mode = "DEMO"
|
|
license_reason = "unknown"
|
|
valid_until = None
|
|
cache = _load_license_cache()
|
|
if cache and _cache_is_fresh(cache) and _cache_license_valid(cache):
|
|
license_mode = "ACTIVE"
|
|
license_reason = "offline_cache"
|
|
valid_until = cache.get("valid_until")
|
|
self.license_mode = "active"
|
|
print(f"[LICENSE] mode={license_mode} reason={license_reason} valid_until={_format_valid_until_ts(valid_until)}")
|
|
return
|
|
|
|
try:
|
|
backend_url = self.get_backend_url()
|
|
api_token = self.get_backend_token()
|
|
print(f"[LICENSE] status_url={backend_url}/license/status")
|
|
response = None
|
|
status_code = None
|
|
last_exc = None
|
|
device_id = _get_or_create_device_id()
|
|
headers = {"X-API-Token": api_token, "X-Device-Id": device_id}
|
|
+ _profile_email = ""
|
|
+ try:
|
|
+ _prof = load_user_profile()
|
|
+ _profile_email = (_prof.get("email") or "").strip()
|
|
+ except Exception:
|
|
+ pass
|
|
+ _lic_params: dict = {}
|
|
+ if _profile_email:
|
|
+ _lic_params["email"] = _profile_email
|
|
+ print(f"[LICENSE] customer_email={_profile_email}")
|
|
for attempt in range(1, 7):
|
|
try:
|
|
response = requests.get(
|
|
f"{backend_url}/license/status",
|
|
headers=headers,
|
|
+ params=_lic_params,
|
|
timeout=5,
|
|
)
|
|
status_code = response.status_code
|
|
response.raise_for_status()
|
|
print(f"[LICENSE] status_code={status_code} attempt={attempt}/6")
|
|
break
|
|
except requests.HTTPError as http_exc:
|
|
last_exc = http_exc
|
|
print(f"[LICENSE] status_code={status_code} attempt={attempt}/6")
|
|
break
|
|
except requests.RequestException as req_exc:
|
|
last_exc = req_exc
|
|
print(f"[LICENSE] retry={attempt}/6 reason={req_exc}")
|
|
if attempt < 6:
|
|
time.sleep(1.0)
|
|
if isinstance(last_exc, requests.HTTPError):
|
|
if status_code in (401, 403):
|
|
license_mode = "DEMO"
|
|
license_reason = "unauthorized"
|
|
else:
|
|
if cache and _cache_license_valid(cache):
|
|
license_mode = "ACTIVE"
|
|
license_reason = "offline_cache"
|
|
valid_until = cache.get("valid_until")
|
|
else:
|
|
license_mode = "DEMO"
|
|
license_reason = "no_backend"
|
|
valid_until = cache.get("valid_until") if isinstance(cache, dict) else None
|
|
self.license_mode = "active" if license_mode == "ACTIVE" else "demo"
|
|
print(f"[LICENSE] mode={license_mode} reason={license_reason} valid_until={_format_valid_until_ts(valid_until)}")
|
|
return
|
|
if response is None:
|
|
raise RuntimeError(last_exc or "license status request failed")
|
|
|
|
try:
|
|
data = response.json()
|
|
except Exception:
|
|
data = {}
|
|
|
|
resp_valid = bool(data.get("valid")) if isinstance(data, dict) else False
|
|
resp_valid_until = data.get("valid_until") if isinstance(data, dict) else None
|
|
payload = {"valid": resp_valid, "valid_until": resp_valid_until, "cached_at": time.time()}
|
|
_save_license_cache(payload)
|
|
now = int(time.time())
|
|
valid_until = resp_valid_until if isinstance(resp_valid_until, (int, float)) else None
|
|
|
|
if resp_valid and isinstance(resp_valid_until, (int, float)) and int(resp_valid_until) > now:
|
|
license_mode = "ACTIVE"
|
|
license_reason = "online"
|
|
valid_until = int(resp_valid_until)
|
|
elif isinstance(resp_valid_until, (int, float)) and int(resp_valid_until) <= now:
|
|
license_mode = "DEMO"
|
|
license_reason = "expired"
|
|
valid_until = int(resp_valid_until)
|
|
else:
|
|
license_mode = "DEMO"
|
|
license_reason = "not_valid"
|
|
valid_until = resp_valid_until if isinstance(resp_valid_until, (int, float)) else None
|
|
except Exception as e:
|
|
print(f"[LICENSE] exception={e}")
|
|
if cache and _cache_license_valid(cache):
|
|
license_mode = "ACTIVE"
|
|
license_reason = "offline_cache"
|
|
valid_until = cache.get("valid_until")
|
|
else:
|
|
license_mode = "DEMO"
|
|
license_reason = "no_backend"
|
|
valid_until = cache.get("valid_until") if isinstance(cache, dict) else None
|
|
|
|
self.license_mode = "active" if license_mode == "ACTIVE" else "demo"
|
|
print(f"[LICENSE] mode={license_mode} reason={license_reason} valid_until={_format_valid_until_ts(valid_until)}")
|
|
|
|
def _build_ui(self):
|
|
try:
|
|
def_font = tkfont.nametofont("TkDefaultFont")
|
|
font_size = max(10, def_font.actual()["size"]) # Mindestens Gr├Âe 10 f├╝r bessere Lesbarkeit
|
|
self._text_font = (def_font.actual()["family"], font_size)
|
|
except Exception:
|
|
self._text_font = ("Segoe UI", 10)
|
|
|
|
@@ -2282,369 +2293,385 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
|
|
justify="left").pack(anchor="w", pady=(4, 12))
|
|
|
|
codes_text = tk.Text(form, font=("Consolas", 14), bg="#F0F8FF",
|
|
fg="#1a4d6d", relief="flat", bd=1,
|
|
height=len(codes), width=20)
|
|
codes_text.pack(pady=(0, 12))
|
|
for i, code in enumerate(codes, 1):
|
|
codes_text.insert("end", f" {i}. {code}\n")
|
|
codes_text.configure(state="disabled")
|
|
|
|
def do_copy():
|
|
dlg.clipboard_clear()
|
|
dlg.clipboard_append("\n".join(codes))
|
|
messagebox.showinfo("Kopiert",
|
|
"Backup-Codes in Zwischenablage kopiert.", parent=dlg)
|
|
|
|
btn_row = tk.Frame(form, bg="#E8F4FA")
|
|
btn_row.pack()
|
|
tk.Button(btn_row, text="Kopieren", font=("Segoe UI", 10),
|
|
bg="#C8DDE6", fg="#1a4d6d", relief="flat", padx=12,
|
|
pady=4, cursor="hand2", command=do_copy).pack(side="left", padx=4)
|
|
tk.Button(btn_row, text="Ich habe die Codes gesichert",
|
|
font=("Segoe UI", 10, "bold"),
|
|
bg="#27AE60", fg="white", relief="flat", padx=12,
|
|
pady=4, cursor="hand2",
|
|
command=dlg.destroy).pack(side="left", padx=4)
|
|
|
|
dlg.protocol("WM_DELETE_WINDOW", dlg.destroy)
|
|
self.wait_window(dlg)
|
|
|
|
def _show_registration_dialog(self):
|
|
"""Erstregistrierung: Profil + Passwort festlegen."""
|
|
dlg = tk.Toplevel(self)
|
|
dlg.title("Registrierung AzA Profil")
|
|
dlg.configure(bg="#E8F4FA")
|
|
dlg.resizable(True, True)
|
|
dlg.geometry("420x640")
|
|
dlg.minsize(380, 520)
|
|
add_resize_grip(dlg, 380, 520)
|
|
self._register_window(dlg)
|
|
dlg.attributes("-topmost", True)
|
|
dlg.grab_set()
|
|
center_window(dlg, 420, 640)
|
|
|
|
tk.Label(dlg, text="ƒæñ Willkommen bei AzA", font=("Segoe UI", 16, "bold"),
|
|
bg="#B9ECFA", fg="#1a4d6d").pack(fill="x", ipady=12)
|
|
tk.Label(dlg, text="Bitte erfassen Sie Ihr Profil und legen Sie ein Passwort fest:",
|
|
font=("Segoe UI", 9), bg="#E8F4FA", fg="#4a8aaa").pack(fill="x", padx=16, pady=(8, 4))
|
|
|
|
form = tk.Frame(dlg, bg="#E8F4FA", padx=20, pady=8)
|
|
form.pack(fill="x")
|
|
|
|
tk.Label(form, text="Name / Titel:", font=("Segoe UI", 10, "bold"),
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
|
|
name_entry = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
|
|
relief="flat", bd=0)
|
|
name_entry.pack(fill="x", ipady=4, pady=(0, 6))
|
|
name_entry.insert(0, self._user_profile.get("name", ""))
|
|
|
|
tk.Label(form, text="Fachrichtung:", font=("Segoe UI", 10, "bold"),
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
|
|
spec_entry = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
|
|
relief="flat", bd=0)
|
|
spec_entry.pack(fill="x", ipady=4, pady=(0, 6))
|
|
spec_entry.insert(0, self._user_profile.get("specialty", ""))
|
|
|
|
tk.Label(form, text="Praxis / Klinik:", font=("Segoe UI", 10, "bold"),
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
|
|
clinic_entry = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
|
|
relief="flat", bd=0)
|
|
clinic_entry.pack(fill="x", ipady=4, pady=(0, 6))
|
|
clinic_entry.insert(0, self._user_profile.get("clinic", ""))
|
|
|
|
tk.Label(form, text="Code (ZSR/GLN, optional):", font=("Segoe UI", 10, "bold"),
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
|
|
code_entry = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
|
|
relief="flat", bd=0)
|
|
code_entry.pack(fill="x", ipady=4, pady=(0, 6))
|
|
code_entry.insert(0, self._user_profile.get("code", ""))
|
|
|
|
+ tk.Label(form, text="E-Mail (Kauf-Adresse):", font=("Segoe UI", 10, "bold"),
|
|
+ bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
|
|
+ email_entry = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
|
|
+ relief="flat", bd=0)
|
|
+ email_entry.pack(fill="x", ipady=4, pady=(0, 6))
|
|
+ email_entry.insert(0, self._user_profile.get("email", ""))
|
|
+
|
|
sep = tk.Frame(form, bg="#B9ECFA", height=1)
|
|
sep.pack(fill="x", pady=(6, 6))
|
|
|
|
tk.Label(form, text="ƒöæ Passwort festlegen:", font=("Segoe UI", 10, "bold"),
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
|
|
pw_entry = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
|
|
relief="flat", bd=0, show="")
|
|
pw_entry.pack(fill="x", ipady=4, pady=(0, 6))
|
|
|
|
tk.Label(form, text="Passwort bestätigen:", font=("Segoe UI", 10, "bold"),
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
|
|
pw_confirm_entry = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
|
|
relief="flat", bd=0, show="")
|
|
pw_confirm_entry.pack(fill="x", ipady=4, pady=(0, 6))
|
|
|
|
def do_save():
|
|
name = name_entry.get().strip()
|
|
if not name:
|
|
messagebox.showwarning("Pflichtfeld", "Bitte geben Sie Ihren Namen ein.", parent=dlg)
|
|
return
|
|
pw = pw_entry.get()
|
|
pw_confirm = pw_confirm_entry.get()
|
|
if not pw:
|
|
messagebox.showwarning("Pflichtfeld", "Bitte legen Sie ein Passwort fest.", parent=dlg)
|
|
return
|
|
if len(pw) < 4:
|
|
messagebox.showwarning("Passwort zu kurz", "Das Passwort muss mindestens 4 Zeichen lang sein.", parent=dlg)
|
|
return
|
|
if pw != pw_confirm:
|
|
messagebox.showerror("Fehler", "Die Passw├Ârter stimmen nicht ├╝berein.", parent=dlg)
|
|
pw_confirm_entry.delete(0, "end")
|
|
pw_confirm_entry.focus_set()
|
|
return
|
|
self._user_profile = {
|
|
"name": name,
|
|
"specialty": spec_entry.get().strip(),
|
|
"clinic": clinic_entry.get().strip(),
|
|
"code": code_entry.get().strip(),
|
|
+ "email": email_entry.get().strip(),
|
|
"password_hash": self._hash_password(pw),
|
|
}
|
|
+ save_user_profile(self._user_profile)
|
|
self._record_login()
|
|
dlg.destroy()
|
|
|
|
tk.Button(dlg, text="ƒÆ¥ Registrieren & Starten", font=("Segoe UI", 11, "bold"),
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|
relief="flat", bd=0, padx=20, pady=6, cursor="hand2",
|
|
command=do_save).pack(pady=12)
|
|
|
|
dlg.protocol("WM_DELETE_WINDOW", lambda: sys.exit(0))
|
|
name_entry.focus_set()
|
|
self.wait_window(dlg)
|
|
|
|
def _show_activation_dialog(self):
|
|
"""Dialog zum Eingeben/Aktualisieren des Aktivierungsschl├╝ssels."""
|
|
from aza_activation import load_activation_key
|
|
current_key = load_activation_key() or ""
|
|
allowed, status_msg = check_app_access()
|
|
|
|
dlg = tk.Toplevel(self)
|
|
dlg.title("AZA Aktivierung")
|
|
dlg.configure(bg="#E8F4FA")
|
|
dlg.resizable(False, False)
|
|
dlg.geometry("460x300")
|
|
dlg.attributes("-topmost", True)
|
|
self._register_window(dlg)
|
|
center_window(dlg, 460, 300)
|
|
|
|
tk.Label(dlg, text="AZA Aktivierung", font=("Segoe UI", 14, "bold"),
|
|
bg="#B9ECFA", fg="#1a4d6d").pack(fill="x", ipady=10)
|
|
|
|
tk.Label(dlg, text=f"Status: {status_msg}", font=("Segoe UI", 9),
|
|
bg="#E8F4FA", fg="#2a7a3a" if allowed else "#c04040",
|
|
wraplength=420, justify="left").pack(fill="x", padx=16, pady=(8, 4))
|
|
|
|
form = tk.Frame(dlg, bg="#E8F4FA", padx=16, pady=4)
|
|
form.pack(fill="x")
|
|
|
|
tk.Label(form, text="Aktivierungsschl├╝ssel:", font=("Segoe UI", 10, "bold"),
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w")
|
|
key_entry = tk.Entry(form, font=("Consolas", 12), bg="white", fg="#1a4d6d",
|
|
relief="flat", bd=0)
|
|
key_entry.pack(fill="x", ipady=4, pady=(0, 4))
|
|
if current_key:
|
|
key_entry.insert(0, current_key)
|
|
|
|
result_label = tk.Label(form, text="", font=("Segoe UI", 9),
|
|
bg="#E8F4FA", fg="#888888")
|
|
result_label.pack(fill="x")
|
|
|
|
def do_save():
|
|
k = key_entry.get().strip()
|
|
if not k:
|
|
result_label.configure(text="Bitte Schl├╝ssel eingeben.", fg="#E05050")
|
|
return
|
|
valid, expiry, reason = validate_key(k)
|
|
if valid:
|
|
save_activation_key(k)
|
|
result_label.configure(text=f"Gespeichert: {reason}", fg="#2a7a3a")
|
|
self.set_status(f"Aktivierung: {reason}")
|
|
else:
|
|
result_label.configure(text=reason, fg="#E05050")
|
|
|
|
btn_frame = tk.Frame(dlg, bg="#E8F4FA")
|
|
btn_frame.pack(pady=8)
|
|
tk.Button(btn_frame, text="Speichern", font=("Segoe UI", 10, "bold"),
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|
relief="flat", bd=0, padx=16, pady=4, cursor="hand2",
|
|
command=do_save).pack(side="left", padx=4)
|
|
tk.Button(btn_frame, text="Schliessen", font=("Segoe UI", 10),
|
|
bg="#cccccc", fg="#333333", relief="flat", bd=0,
|
|
padx=16, pady=4, cursor="hand2",
|
|
command=dlg.destroy).pack(side="left", padx=4)
|
|
|
|
def _show_profile_editor(self):
|
|
"""Öffnet ein Fenster zum Bearbeiten des Benutzerprofils (inkl. Passwort ändern)."""
|
|
dlg = tk.Toplevel(self)
|
|
dlg.title("Profil bearbeiten")
|
|
dlg.configure(bg="#E8F4FA")
|
|
dlg.resizable(False, False)
|
|
dlg.geometry("380x440")
|
|
dlg.attributes("-topmost", True)
|
|
self._register_window(dlg)
|
|
center_window(dlg, 380, 440)
|
|
|
|
tk.Label(dlg, text="ƒæñ Profil bearbeiten", font=("Segoe UI", 13, "bold"),
|
|
bg="#B9ECFA", fg="#1a4d6d").pack(fill="x", ipady=8)
|
|
|
|
form = tk.Frame(dlg, bg="#E8F4FA", padx=20, pady=8)
|
|
form.pack(fill="x")
|
|
|
|
tk.Label(form, text="Name / Titel:", font=("Segoe UI", 10, "bold"),
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(8, 0))
|
|
name_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d", relief="flat")
|
|
name_e.pack(fill="x", ipady=4, pady=(0, 6))
|
|
name_e.insert(0, self._user_profile.get("name", ""))
|
|
|
|
tk.Label(form, text="Fachrichtung:", font=("Segoe UI", 10, "bold"),
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
|
|
spec_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d", relief="flat")
|
|
spec_e.pack(fill="x", ipady=4, pady=(0, 6))
|
|
spec_e.insert(0, self._user_profile.get("specialty", ""))
|
|
|
|
tk.Label(form, text="Praxis / Klinik:", font=("Segoe UI", 10, "bold"),
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
|
|
clinic_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d", relief="flat")
|
|
clinic_e.pack(fill="x", ipady=4, pady=(0, 6))
|
|
clinic_e.insert(0, self._user_profile.get("clinic", ""))
|
|
|
|
tk.Label(form, text="Code (ZSR/GLN, optional):", font=("Segoe UI", 10, "bold"),
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
|
|
code_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d", relief="flat")
|
|
code_e.pack(fill="x", ipady=4, pady=(0, 6))
|
|
code_e.insert(0, self._user_profile.get("code", ""))
|
|
|
|
+ tk.Label(form, text="E-Mail (Kauf-Adresse):", font=("Segoe UI", 10, "bold"),
|
|
+ bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
|
|
+ email_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d", relief="flat")
|
|
+ email_e.pack(fill="x", ipady=4, pady=(0, 6))
|
|
+ email_e.insert(0, self._user_profile.get("email", ""))
|
|
+
|
|
sep = tk.Frame(form, bg="#B9ECFA", height=1)
|
|
sep.pack(fill="x", pady=(8, 6))
|
|
|
|
tk.Label(form, text="ƒöæ Passwort ├ñndern (optional):", font=("Segoe UI", 10, "bold"),
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(2, 0))
|
|
tk.Label(form, text="Leer lassen, um das Passwort beizubehalten.",
|
|
font=("Segoe UI", 8), bg="#E8F4FA", fg="#888").pack(anchor="w")
|
|
|
|
pw_old_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
|
|
relief="flat", bd=0, show="")
|
|
pw_new_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
|
|
relief="flat", bd=0, show="")
|
|
pw_confirm_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
|
|
relief="flat", bd=0, show="")
|
|
|
|
if self._user_profile.get("password_hash"):
|
|
tk.Label(form, text="Altes Passwort:", font=("Segoe UI", 9),
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(4, 0))
|
|
pw_old_e.pack(fill="x", ipady=3, pady=(0, 4))
|
|
|
|
tk.Label(form, text="Neues Passwort:", font=("Segoe UI", 9),
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(2, 0))
|
|
pw_new_e.pack(fill="x", ipady=3, pady=(0, 4))
|
|
tk.Label(form, text="Neues Passwort bestätigen:", font=("Segoe UI", 9),
|
|
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(2, 0))
|
|
pw_confirm_e.pack(fill="x", ipady=3, pady=(0, 4))
|
|
|
|
def do_save():
|
|
name = name_e.get().strip()
|
|
if not name:
|
|
messagebox.showwarning("Pflichtfeld", "Name darf nicht leer sein.", parent=dlg)
|
|
return
|
|
new_pw = pw_new_e.get()
|
|
new_pw_confirm = pw_confirm_e.get()
|
|
old_hash = self._user_profile.get("password_hash", "")
|
|
|
|
if new_pw:
|
|
if old_hash and not self._verify_password(pw_old_e.get(), old_hash):
|
|
messagebox.showerror("Fehler", "Das alte Passwort ist nicht korrekt.", parent=dlg)
|
|
return
|
|
if len(new_pw) < 4:
|
|
messagebox.showwarning("Zu kurz", "Das neue Passwort muss mindestens 4 Zeichen lang sein.", parent=dlg)
|
|
return
|
|
if new_pw != new_pw_confirm:
|
|
messagebox.showerror("Fehler", "Die neuen Passw├Ârter stimmen nicht ├╝berein.", parent=dlg)
|
|
return
|
|
pw_hash = self._hash_password(new_pw)
|
|
else:
|
|
pw_hash = old_hash
|
|
|
|
updated = {
|
|
"name": name,
|
|
"specialty": spec_e.get().strip(),
|
|
"clinic": clinic_e.get().strip(),
|
|
"code": code_e.get().strip(),
|
|
+ "email": email_e.get().strip(),
|
|
"password_hash": pw_hash,
|
|
}
|
|
for k in ("totp_secret_enc", "totp_active", "backup_codes"):
|
|
if k in self._user_profile:
|
|
updated[k] = self._user_profile[k]
|
|
self._user_profile = updated
|
|
save_user_profile(self._user_profile)
|
|
self.set_status(f"Profil gespeichert: {name}")
|
|
dlg.destroy()
|
|
|
|
btn_row = tk.Frame(dlg, bg="#E8F4FA")
|
|
btn_row.pack(pady=10)
|
|
tk.Button(btn_row, text="ƒÆ¥ Speichern", font=("Segoe UI", 10, "bold"),
|
|
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
|
|
relief="flat", padx=16, pady=4, cursor="hand2",
|
|
command=do_save).pack(side="left", padx=6)
|
|
tk.Button(btn_row, text="Abbrechen", font=("Segoe UI", 10),
|
|
bg="#C8DDE6", fg="#1a4d6d", activebackground="#B8CDD6",
|
|
relief="flat", padx=12, pady=4, cursor="hand2",
|
|
command=dlg.destroy).pack(side="left", padx=6)
|
|
|
|
if is_2fa_enabled():
|
|
sep2 = tk.Frame(dlg, bg="#B9ECFA", height=1)
|
|
sep2.pack(fill="x", padx=20, pady=(4, 4))
|
|
tfa_frame = tk.Frame(dlg, bg="#E8F4FA", padx=20)
|
|
tfa_frame.pack(fill="x")
|
|
is_active = self._user_profile.get("totp_active", False)
|
|
status_text = "2FA aktiv" if is_active else "2FA nicht aktiv"
|
|
status_color = "#27AE60" if is_active else "#E74C3C"
|
|
tk.Label(tfa_frame, text=f"ƒöÉ {status_text}",
|
|
font=("Segoe UI", 10, "bold"), bg="#E8F4FA",
|
|
fg=status_color).pack(side="left")
|
|
|
|
def do_toggle_2fa():
|
|
if is_active:
|
|
if messagebox.askyesno("2FA deaktivieren",
|
|
"Zwei-Faktor-Authentifizierung wirklich deaktivieren?\n\n"
|
|
"Dies verringert die Sicherheit Ihres Kontos.",
|
|
parent=dlg):
|
|
self._user_profile.pop("totp_secret_enc", None)
|
|
self._user_profile.pop("backup_codes", None)
|
|
self._user_profile["totp_active"] = False
|
|
save_user_profile(self._user_profile)
|
|
dlg.destroy()
|
|
self._show_profile_editor()
|
|
else:
|
|
pw_check = simpledialog.askstring("Passwort",
|
|
"Bitte Passwort eingeben:", show="*", parent=dlg)
|
|
if pw_check and self._verify_password(pw_check,
|
|
self._user_profile.get("password_hash", "")):
|
|
dlg.destroy()
|
|
self._show_2fa_setup(pw_check)
|
|
elif pw_check:
|
|
messagebox.showerror("Fehler",
|
|
"Falsches Passwort.", parent=dlg)
|
|
|
|
btn_text = "Deaktivieren" if is_active else "2FA einrichten"
|
|
btn_color = "#E74C3C" if is_active else "#27AE60"
|
|
tk.Button(tfa_frame, text=btn_text, font=("Segoe UI", 9),
|
|
bg=btn_color, fg="white", relief="flat",
|
|
padx=8, pady=2, cursor="hand2",
|
|
command=do_toggle_2fa).pack(side="right")
|
|
|
|
def _reset_window_positions(self):
|
|
"""Setzt alle gespeicherten Fensterpositionen und KG-Einstellungen zur├╝ck."""
|
|
answer = messagebox.askyesno(
|
|
"Fensterpositionen zur├╝cksetzen",
|
|
"Alle Fensterpositionen und KG-Einstellungen zur├╝cksetzen?\n\n"
|
|
"Beim nächsten Start werden alle Fenster\n"
|
|
"in der Bildschirmmitte ge├Âffnet.\n"
|
|
"Die KG-Detailstufe (K├╝rzer/Ausf├╝hrlicher)\n"
|
|
"wird auf Standard zur├╝ckgesetzt.",
|
|
parent=self,
|
|
)
|
|
if not answer:
|
|
return
|
|
deleted = reset_all_window_positions()
|
|
self._update_kg_detail_display()
|
|
self._soap_section_levels = {k: 0 for k in _SOAP_SECTIONS}
|
|
self._update_soap_section_display()
|
|
@@ -8525,165 +8552,188 @@ def _show_openai_key_setup_dialog():
|
|
result["action"] = "skip"
|
|
dlg.destroy()
|
|
|
|
key_entry.bind("<Return>", lambda e: do_activate())
|
|
|
|
btn_area = tk.Frame(content, bg=_BG)
|
|
btn_area.pack(fill="x", pady=(0, 10))
|
|
|
|
btn_primary = tk.Button(
|
|
btn_area, text="\u2713 Schl├╝ssel aktivieren",
|
|
font=("Segoe UI", 11, "bold"),
|
|
bg=_ACCENT, fg="white", activebackground=_ACCENT_HOVER,
|
|
activeforeground="white",
|
|
relief="flat", bd=0, padx=24, pady=10, cursor="hand2",
|
|
command=do_activate,
|
|
)
|
|
btn_primary.pack(side="left")
|
|
|
|
def _on_enter_p(e):
|
|
btn_primary.configure(bg=_ACCENT_HOVER)
|
|
def _on_leave_p(e):
|
|
btn_primary.configure(bg=_ACCENT)
|
|
btn_primary.bind("<Enter>", _on_enter_p)
|
|
btn_primary.bind("<Leave>", _on_leave_p)
|
|
|
|
btn_skip = tk.Button(
|
|
btn_area, text="Später",
|
|
font=("Segoe UI", 10),
|
|
bg=_BG, fg=_SUBTLE, activebackground="#F0F0F0",
|
|
relief="solid", bd=1, padx=18, pady=8, cursor="hand2",
|
|
highlightbackground=_BORDER,
|
|
command=do_skip,
|
|
)
|
|
btn_skip.pack(side="left", padx=(12, 0))
|
|
|
|
links = tk.Frame(content, bg=_BG)
|
|
links.pack(fill="x")
|
|
|
|
if has_helper:
|
|
lnk_setup = tk.Label(links, text="\u2192 Einrichtungsassistent starten",
|
|
font=("Segoe UI", 9, "underline"), fg=_SUBTLE, bg=_BG,
|
|
cursor="hand2")
|
|
lnk_setup.pack(anchor="w", pady=(0, 2))
|
|
lnk_setup.bind("<Button-1>", lambda e: do_helper())
|
|
|
|
lnk_config = tk.Label(links, text="\u2192 Konfiguration manuell ├Âffnen",
|
|
font=("Segoe UI", 9, "underline"), fg=_SUBTLE, bg=_BG,
|
|
cursor="hand2")
|
|
lnk_config.pack(anchor="w")
|
|
lnk_config.bind("<Button-1>", lambda e: do_config())
|
|
|
|
dlg.protocol("WM_DELETE_WINDOW", do_skip)
|
|
dlg.grab_set()
|
|
dlg.wait_window(dlg)
|
|
|
|
if result["action"] == "stored":
|
|
return
|
|
elif result["action"] == "setup":
|
|
success = _run_setup_helper()
|
|
if success and has_openai_api_key():
|
|
return
|
|
if success:
|
|
messagebox.showinfo(
|
|
"Einrichtung",
|
|
"Die Einrichtung wurde abgeschlossen.\n\n"
|
|
"Bitte starten Sie AZA neu, damit die\n"
|
|
"KI-Funktionen aktiv werden.",
|
|
)
|
|
else:
|
|
messagebox.showinfo(
|
|
"Einrichtung",
|
|
"Die automatische Einrichtung ist auf diesem\n"
|
|
"System nicht verf├╝gbar.\n\n"
|
|
"Bitte nutzen Sie den Startmen├╝-Eintrag\n"
|
|
"\"AZA \u2013 OpenAI Schl├╝ssel einrichten\".",
|
|
)
|
|
elif result["action"] == "config":
|
|
open_runtime_config_in_editor()
|
|
|
|
|
|
+def _has_remote_backend() -> bool:
|
|
+ """True when a non-localhost backend URL is configured."""
|
|
+ url = os.getenv("MEDWORK_BACKEND_URL", "").strip()
|
|
+ if not url:
|
|
+ for base in (os.path.dirname(os.path.abspath(__file__)), os.getcwd()):
|
|
+ p = os.path.join(base, "backend_url.txt")
|
|
+ if os.path.isfile(p):
|
|
+ try:
|
|
+ with open(p, "r", encoding="utf-8-sig") as f:
|
|
+ url = f.read().replace("\ufeff", "").strip()
|
|
+ except Exception:
|
|
+ continue
|
|
+ if url:
|
|
+ break
|
|
+ if not url:
|
|
+ return False
|
|
+ return not any(h in url for h in ("127.0.0.1", "localhost", "0.0.0.0"))
|
|
+
|
|
+
|
|
def _show_activation_gate() -> bool:
|
|
"""Pr├╝ft Zugang und zeigt bei Bedarf Schl├╝ssel-Eingabe.
|
|
|
|
Returns True wenn die App starten darf, False wenn beendet werden soll.
|
|
"""
|
|
+ if _has_remote_backend():
|
|
+ print("[ACTIVATION] Remote-Backend konfiguriert ÔÇô lokales Aktivierungs-Gate uebersprungen. Backend-Lizenzstatus ist fuehrend.")
|
|
+ return True
|
|
+
|
|
allowed, msg = check_app_access()
|
|
|
|
if allowed:
|
|
print(f"[ACTIVATION] {msg}")
|
|
stored = load_activation_key()
|
|
is_trial = not stored or not validate_key(stored)[0]
|
|
if is_trial:
|
|
trial_msg = msg + "\n\nSie k\u00f6nnen jetzt einen Aktivierungsschl\u00fcssel eingeben\noder die Testversion weiter nutzen."
|
|
root = tk.Tk()
|
|
root.withdraw()
|
|
result = _activation_key_dialog(root, trial_msg, can_continue=True)
|
|
root.destroy()
|
|
if result:
|
|
valid, expiry, reason = validate_key(result)
|
|
if valid:
|
|
save_activation_key(result)
|
|
print(f"[ACTIVATION] Schl├╝ssel akzeptiert: {reason}")
|
|
return True
|
|
|
|
while True:
|
|
root = tk.Tk()
|
|
root.withdraw()
|
|
result = _activation_key_dialog(root, msg)
|
|
root.destroy()
|
|
if result is None:
|
|
return False
|
|
valid, expiry, reason = validate_key(result)
|
|
if valid:
|
|
save_activation_key(result)
|
|
print(f"[ACTIVATION] Schl├╝ssel akzeptiert: {reason}")
|
|
return True
|
|
msg = f"Schl├╝ssel ung├╝ltig: {reason}\nBitte erneut versuchen."
|
|
|
|
|
|
def _activation_key_dialog(parent, message: str, can_continue: bool = False) -> Optional[str]:
|
|
"""Premium-Dialog zur Eingabe eines Aktivierungsschl├╝ssels.
|
|
|
|
can_continue=True: Trial-Modus ÔÇô "Weiter ohne Schl├╝ssel" statt "Beenden".
|
|
"""
|
|
_BG = "#FFFFFF"
|
|
_ACCENT = "#0078D7"
|
|
_ACCENT_HOVER = "#005FA3"
|
|
_TEXT = "#2D3436"
|
|
_SUBTLE = "#636E72"
|
|
_BORDER = "#E2E8F0"
|
|
|
|
result = {"key": None}
|
|
|
|
dlg = tk.Toplevel(parent)
|
|
dlg.title("AZA \u2013 Aktivierung")
|
|
dlg.configure(bg=_BG)
|
|
dlg.resizable(True, True)
|
|
w, h = 540, 520
|
|
dlg.minsize(460, 420)
|
|
dlg.geometry(f"{w}x{h}")
|
|
dlg.attributes("-topmost", True)
|
|
|
|
try:
|
|
dlg.update_idletasks()
|
|
sw = dlg.winfo_screenwidth()
|
|
sh = dlg.winfo_screenheight()
|
|
dlg.geometry(f"{w}x{h}+{(sw - w) // 2}+{(sh - h) // 2}")
|
|
except Exception:
|
|
pass
|
|
|
|
content = tk.Frame(dlg, bg=_BG)
|
|
content.pack(fill="both", expand=True, padx=40, pady=28)
|
|
|
|
tk.Label(content, text="\U0001F511",
|
|
font=("Segoe UI", 28), fg=_ACCENT, bg=_BG).pack(anchor="w")
|
|
|
|
title_text = "AZA Aktivierung" if not can_continue else "AZA \u2013 Testversion aktiv"
|
|
tk.Label(content, text=title_text,
|
|
font=("Segoe UI", 18, "bold"), fg=_TEXT, bg=_BG
|
|
).pack(anchor="w", pady=(6, 4))
|
|
|
|
tk.Label(content, text=message, font=("Segoe UI", 10),
|
|
fg=_SUBTLE, bg=_BG, wraplength=420,
|
|
justify="left").pack(anchor="w", pady=(0, 16))
|
|
|