V5 komplett: Auth, Admin, Federation, Channels, Devices, Cockpit, Profil, Autotext-Fix, Uebersetzer-Fix

Made-with: Cursor
This commit is contained in:
2026-04-20 14:38:16 +02:00
parent c53bba4587
commit dcce7107ab
9 changed files with 2254 additions and 320 deletions

View File

@@ -503,7 +503,7 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
self._autotext_data.setdefault("eventsSort", "soonest")
self._autotext_data.setdefault("eventsMonthsAhead", 13)
self._autotext_data.setdefault("selectedLanguage", "system")
self._autotext_data.setdefault("user_specialty_default", "dermatology")
self._autotext_data.setdefault("user_specialty_default", "")
self._autotext_data.setdefault("user_specialties_selected", [])
self._autotext_data.setdefault("ui_font_delta", -1)
self._autotext_data.setdefault("notizen_open_on_start", True)
@@ -2881,61 +2881,104 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
dlg.title("Profil bearbeiten")
dlg.configure(bg="#E8F4FA")
dlg.resizable(True, True)
dlg.geometry("400x560")
dlg.minsize(380, 480)
dlg.geometry("520x820")
dlg.minsize(460, 680)
dlg.attributes("-topmost", True)
self._register_window(dlg)
center_window(dlg, 400, 560)
center_window(dlg, 520, 820)
tk.Label(dlg, text="👤 Profil bearbeiten", font=("Segoe UI", 13, "bold"),
tk.Label(dlg, text="\U0001f464 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)
scroll_canvas = tk.Canvas(dlg, bg="#E8F4FA", highlightthickness=0)
scroll_vsb = ttk.Scrollbar(dlg, orient="vertical", command=scroll_canvas.yview)
scroll_canvas.configure(yscrollcommand=scroll_vsb.set)
scroll_vsb.pack(side="right", fill="y")
scroll_canvas.pack(side="left", fill="both", expand=True)
scroll_inner = tk.Frame(scroll_canvas, bg="#E8F4FA")
scroll_win = scroll_canvas.create_window((0, 0), window=scroll_inner, anchor="nw")
scroll_inner.bind("<Configure>", lambda e: scroll_canvas.configure(scrollregion=scroll_canvas.bbox("all")))
scroll_canvas.bind("<Configure>", lambda e: scroll_canvas.itemconfigure(scroll_win, width=e.width))
scroll_canvas.bind_all("<MouseWheel>", lambda e: scroll_canvas.yview_scroll(-1 * (e.delta // 120), "units"))
dlg.bind("<Destroy>", lambda e: scroll_canvas.unbind_all("<MouseWheel>") if e.widget is dlg else None)
form = tk.Frame(scroll_inner, 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", ""))
_FLD_FONT = ("Segoe UI", 9)
_LBL_FONT = ("Segoe UI", 9, "bold")
_PH_COLOR = "#aaa"
_FG_COLOR = "#1a4d6d"
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", ""))
def _make_placeholder_entry(parent, placeholder, initial=""):
e = tk.Entry(parent, font=_FLD_FONT, bg="white", fg=_FG_COLOR, relief="flat")
e.pack(fill="x", ipady=4, pady=(0, 6))
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", ""))
def _set_ph():
if not e.get().strip():
e.delete(0, "end")
e.insert(0, placeholder)
e.configure(fg=_PH_COLOR)
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", ""))
def _on_focus_in(ev):
if e.get() == placeholder and e.cget("fg") == _PH_COLOR:
e.delete(0, "end")
e.configure(fg=_FG_COLOR)
tk.Label(form, text="E-Mail (optional):", font=("Segoe UI", 10),
bg="#E8F4FA", fg="#888").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", ""))
def _on_focus_out(ev):
_set_ph()
def _get_val():
v = e.get().strip()
return "" if v == placeholder else v
e.bind("<FocusIn>", _on_focus_in)
e.bind("<FocusOut>", _on_focus_out)
e._get_real_value = _get_val
if initial and initial != placeholder:
e.insert(0, initial)
e.configure(fg=_FG_COLOR)
else:
_set_ph()
return e
tk.Label(form, text="Name / Titel:", font=_LBL_FONT,
bg="#E8F4FA", fg=_FG_COLOR).pack(anchor="w", pady=(8, 0))
_name_initial = self._user_profile.get("name", "")
if _name_initial == "Benutzer":
_name_initial = ""
name_e = _make_placeholder_entry(form, "Ihr Name / Titel eingeben...", _name_initial)
tk.Label(form, text="Fachrichtung:", font=_LBL_FONT,
bg="#E8F4FA", fg=_FG_COLOR).pack(anchor="w", pady=(4, 0))
spec_e = _make_placeholder_entry(form, "z.B. Dermatologie", self._user_profile.get("specialty", ""))
tk.Label(form, text="Praxis / Klinik:", font=_LBL_FONT,
bg="#E8F4FA", fg=_FG_COLOR).pack(anchor="w", pady=(4, 0))
clinic_e = _make_placeholder_entry(form, "z.B. Hautarztpraxis Winterthur", self._user_profile.get("clinic", ""))
tk.Label(form, text="Code (ZSR/GLN, optional):", font=_LBL_FONT,
bg="#E8F4FA", fg=_FG_COLOR).pack(anchor="w", pady=(4, 0))
code_e = _make_placeholder_entry(form, "ZSR / GLN", self._user_profile.get("code", ""))
tk.Label(form, text="E-Mail:", font=_LBL_FONT,
bg="#E8F4FA", fg=_FG_COLOR).pack(anchor="w", pady=(4, 0))
email_e = _make_placeholder_entry(form, "praxis@beispiel.ch", 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="\U0001f511 Passwort \u00e4ndern (optional):", font=_LBL_FONT,
bg="#E8F4FA", fg=_FG_COLOR).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",
pw_old_e = tk.Entry(form, font=_FLD_FONT, bg="white", fg=_FG_COLOR,
relief="flat", bd=0, show="")
pw_new_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
pw_new_e = tk.Entry(form, font=_FLD_FONT, bg="white", fg=_FG_COLOR,
relief="flat", bd=0, show="")
pw_confirm_e = tk.Entry(form, font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
pw_confirm_e = tk.Entry(form, font=_FLD_FONT, bg="white", fg=_FG_COLOR,
relief="flat", bd=0, show="")
if self._user_profile.get("password_hash"):
@@ -2951,7 +2994,7 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
pw_confirm_e.pack(fill="x", ipady=3, pady=(0, 4))
def do_save():
name = name_e.get().strip()
name = name_e._get_real_value() if hasattr(name_e, '_get_real_value') else name_e.get().strip()
if not name:
messagebox.showwarning("Pflichtfeld", "Name darf nicht leer sein.", parent=dlg)
return
@@ -2967,18 +3010,19 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
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)
messagebox.showerror("Fehler", "Die neuen Passw\u00f6rter stimmen nicht \u00fcberein.", parent=dlg)
return
pw_hash = self._hash_password(new_pw)
else:
pw_hash = old_hash
_gv = lambda e: e._get_real_value() if hasattr(e, '_get_real_value') else e.get().strip()
updated = {
"name": name,
"specialty": spec_e.get().strip(),
"clinic": clinic_e.get().strip(),
"code": code_e.get().strip(),
"email": email_e.get().strip(),
"specialty": _gv(spec_e),
"clinic": _gv(clinic_e),
"code": _gv(code_e),
"email": _gv(email_e),
"password_hash": pw_hash,
}
for k in ("totp_secret_enc", "totp_active", "backup_codes"):
@@ -2989,9 +3033,90 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
self.set_status(f"Profil gespeichert: {name}")
dlg.destroy()
btn_row = tk.Frame(dlg, bg="#E8F4FA")
# --- Lizenz / Einladung ---
lic_sep = tk.Frame(form, bg="#B9ECFA", height=1)
lic_sep.pack(fill="x", pady=(10, 6))
tk.Label(form, text="\U0001f4cb Lizenz & Einladung", font=("Segoe UI", 10, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", pady=(2, 4))
_lic_key = (self._user_profile.get("license_key") or "").strip()
_lic_email = (self._user_profile.get("email") or "").strip()
if _lic_key:
tk.Label(form, text="Lizenznummer:", font=("Segoe UI", 9),
bg="#E8F4FA", fg="#5B8DB3").pack(anchor="w")
_lic_lbl = tk.Label(form, text=_lic_key, font=("Consolas", 9),
bg="#f0f4f8", fg="#1a3a5a", relief="flat",
padx=6, pady=2, cursor="hand2", anchor="w")
_lic_lbl.pack(fill="x", pady=(0, 6))
_lic_lbl.bind("<Button-1>", lambda e: (
dlg.clipboard_clear(), dlg.clipboard_append(_lic_key),
self.set_status("Lizenznummer kopiert.")))
_invite_frame = tk.Frame(form, bg="#E8F4FA")
_invite_frame.pack(fill="x", pady=(0, 4))
_invite_label = tk.Label(_invite_frame, text="Einladungscode:", font=("Segoe UI", 9),
bg="#E8F4FA", fg="#5B8DB3")
_invite_label.pack(anchor="w")
_invite_val = tk.Label(_invite_frame, text="Wird geladen...", font=("Consolas", 9),
bg="#f0f4f8", fg="#1a3a5a", relief="flat",
padx=6, pady=2, anchor="w")
_invite_val.pack(fill="x")
_link_frame = tk.Frame(form, bg="#E8F4FA")
_link_frame.pack(fill="x", pady=(0, 6))
_link_label = tk.Label(_link_frame, text="Einladungslink:", font=("Segoe UI", 9),
bg="#E8F4FA", fg="#5B8DB3")
_link_label.pack(anchor="w")
_link_val = tk.Label(_link_frame, text="", font=("Segoe UI", 8),
bg="#f0f4f8", fg="#1a3a5a", relief="flat",
padx=6, pady=2, anchor="w", wraplength=440, justify="left")
_link_val.pack(fill="x")
_link_btn_row = tk.Frame(form, bg="#E8F4FA")
_link_btn_row.pack(fill="x", pady=(0, 8))
def _copy_invite_link():
text = _link_val.cget("text")
if text:
dlg.clipboard_clear()
dlg.clipboard_append(text)
self.set_status("Einladungslink kopiert.")
tk.Button(_link_btn_row, text="Link kopieren", font=("Segoe UI", 9),
bg="#e8f0f8", fg="#2a5a8a", relief="flat", padx=10, pady=3,
cursor="hand2", command=_copy_invite_link).pack(side="left", padx=(0, 6))
def _load_invite_info():
try:
bu = self.get_backend_url()
bt = self.get_backend_token()
r = requests.get(f"{bu}/empfang/practice/info",
headers={"X-API-Token": bt}, timeout=5)
if r.status_code == 200:
d = r.json()
code = d.get("invite_code", "")
pname = d.get("practice_name", "")
if code:
link = f"https://empfang.aza-medwork.ch/?invite={code}"
self.after(0, lambda: _invite_val.configure(text=code))
self.after(0, lambda: _link_val.configure(text=link))
else:
self.after(0, lambda: _invite_val.configure(text=""))
self.after(0, lambda: _link_val.configure(text=""))
self.after(0, lambda: _invite_frame.pack_forget())
self.after(0, lambda: _link_frame.pack_forget())
self.after(0, lambda: _link_btn_row.pack_forget())
except Exception:
self.after(0, lambda: _invite_frame.pack_forget())
self.after(0, lambda: _link_frame.pack_forget())
self.after(0, lambda: _link_btn_row.pack_forget())
threading.Thread(target=_load_invite_info, daemon=True).start()
# --- Buttons ---
btn_row = tk.Frame(scroll_inner, bg="#E8F4FA")
btn_row.pack(pady=(12, 10))
tk.Button(btn_row, text="💾 Speichern", font=("Segoe UI", 11, "bold"),
tk.Button(btn_row, text="\U0001f4be Speichern", font=("Segoe UI", 11, "bold"),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
relief="flat", padx=20, pady=6, cursor="hand2",
command=do_save).pack(side="left", padx=8)
@@ -3001,9 +3126,9 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
command=dlg.destroy).pack(side="left", padx=8)
if is_2fa_enabled():
sep2 = tk.Frame(dlg, bg="#B9ECFA", height=1)
sep2 = tk.Frame(scroll_inner, bg="#B9ECFA", height=1)
sep2.pack(fill="x", padx=20, pady=(4, 4))
tfa_frame = tk.Frame(dlg, bg="#E8F4FA", padx=20)
tfa_frame = tk.Frame(scroll_inner, 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"
@@ -4001,22 +4126,24 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
dlg.transient(self)
dlg.grab_set()
dlg.configure(bg="#F2F8FC")
dlg.geometry("520x620")
dlg.minsize(420, 500)
add_resize_grip(dlg, 420, 500)
center_window(dlg, 520, 620)
body = tk.Frame(dlg, bg="#F2F8FC", padx=12, pady=12)
body.pack(fill="both", expand=True)
tk.Label(body, text="Primäres Fachgebiet (Erststart)", bg="#F2F8FC", fg="#1a4d6d", font=("Segoe UI", 11, "bold")).pack(anchor="w")
primary_var = tk.StringVar(value="dermatology")
primary_var = tk.StringVar(value="")
label_to_key = {label: key for key, label in catalog}
key_to_label = {key: label for key, label in catalog}
combo_val = tk.StringVar(value=key_to_label.get("dermatology", "Dermatologie"))
combo = ttk.Combobox(body, values=[label for _, label in catalog], textvariable=combo_val, state="readonly", width=32)
combo_val = tk.StringVar(value="")
combo = ttk.Combobox(body, values=[""] + [label for _, label in catalog], textvariable=combo_val, state="readonly", width=32)
combo.pack(anchor="w", pady=(6, 10))
tk.Label(body, text="Weitere Fachgebiete (optional)", bg="#F2F8FC", fg="#1a4d6d").pack(anchor="w")
vars_map = {}
for key, label in catalog:
v = tk.BooleanVar(value=(key == "dermatology"))
v = tk.BooleanVar(value=False)
vars_map[key] = v
tk.Checkbutton(body, text=label, variable=v, bg="#F2F8FC", activebackground="#F2F8FC", selectcolor="#E7F4FA").pack(anchor="w")
@@ -5185,7 +5312,7 @@ WICHTIG unbedingt einhalten:
dlg.resizable(True, True)
self._register_window(dlg)
_empfang_font_size = [load_text_font_size("empfang_dlg", 10)]
_empfang_font_size = [load_text_font_size("empfang_dlg", 9)]
_empfang_text_widgets: list = []
def _save_prefs():
@@ -7386,11 +7513,12 @@ WICHTIG unbedingt einhalten:
dlg.title("KI-Einwilligung erforderlich")
dlg.transient(self)
dlg.grab_set()
dlg.geometry("680x520")
dlg.minsize(500, 400)
dlg.geometry("920x720")
dlg.minsize(700, 550)
dlg.attributes("-topmost", True)
self._register_window(dlg)
add_resize_grip(dlg, 500, 400)
add_resize_grip(dlg, 700, 550)
center_window(dlg, 920, 720)
frame = ttk.Frame(dlg, padding=12)
frame.pack(fill="both", expand=True)
@@ -8769,7 +8897,11 @@ WICHTIG:
text_widget.bind("<FocusIn>", on_focus_in, add="+")
text_widget.bind("<FocusOut>", on_focus_out, add="+")
_last_expansion = [0.0, ""]
def on_keyrelease(event):
if getattr(self, "_autotext_injecting", [False])[0]:
return
if not getattr(self, "_autotext_data", {}).get("enabled", True):
return
entries = (self._autotext_data.get("entries") or {})
@@ -8792,7 +8924,12 @@ WICHTIG:
word = text_before[word_start:word_end]
if not word or word not in entries:
return
now = time.time()
if _last_expansion[1] == word and now - _last_expansion[0] < 1.0:
return
expansion = entries[word]
_last_expansion[0] = now
_last_expansion[1] = word
start_idx = text_widget.index(f"{insert} - {len(word) + 1} chars")
text_widget.delete(start_idx, insert)
text_widget.insert(start_idx, expansion + last_char)