# -*- coding: utf-8 -*- """ AzA MedWork - Kommunikation für medizinisches Fachpersonal Praxisintern (wie Softros) und zwischen Ärzten (wie Facebook). Später auch auf Apple Watch / Smartwatch / Handy. """ import os import sys import json import uuid import random import tkinter as tk from tkinter import ttk, messagebox, scrolledtext, simpledialog from datetime import datetime, date, timedelta # ─── Pfade & Konfiguration ─── CONFIG_FILENAME = "aza_docapp_config.json" MESSAGES_FILENAME = "aza_medwork_messages.json" CONTACTS_FILENAME = "kg_diktat_medwork_contacts.json" USER_PROFILE_FILENAME = "kg_diktat_user_profile.json" def _base_dir(): return os.path.dirname(os.path.abspath(__file__)) def _config_path(): return os.path.join(_base_dir(), CONFIG_FILENAME) def _messages_path(): return os.path.join(_base_dir(), MESSAGES_FILENAME) def _contacts_path(): return os.path.join(_base_dir(), CONTACTS_FILENAME) def _user_profile_path(): return os.path.join(_base_dir(), USER_PROFILE_FILENAME) def load_json(path): try: if os.path.isfile(path): with open(path, "r", encoding="utf-8") as f: return json.load(f) except Exception: pass return None def save_json(path, data): try: with open(path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) except Exception: pass def load_docapp_config(): return load_json(_config_path()) or {} def save_docapp_config(data): save_json(_config_path(), data) def load_messages(): return load_json(_messages_path()) or [] def save_messages(msgs): save_json(_messages_path(), msgs) def load_contacts(): data = load_json(_contacts_path()) if isinstance(data, list): return data return [ "Dr. med. Muster (Innere Medizin)", "Dr. med. Beispiel (Chirurgie)", "Dr. med. Test (Kardiologie)", "Praxis-Team intern", ] def save_contacts(contacts): save_json(_contacts_path(), contacts) def load_user_profile(): return load_json(_user_profile_path()) or {} def save_user_profile(profile): save_json(_user_profile_path(), profile) def _load_font_sizes(): try: p = os.path.join(_base_dir(), "aza_font_sizes.json") if os.path.isfile(p): with open(p, "r", encoding="utf-8") as f: return json.load(f) except Exception: pass return {} def _save_font_size(key, size): try: d = _load_font_sizes() d[key] = size p = os.path.join(_base_dir(), "aza_font_sizes.json") with open(p, "w", encoding="utf-8") as f: json.dump(d, f, indent=2, ensure_ascii=False) except Exception: pass def add_text_font_size_control(parent_frame, text_widget, initial_size=10, label="Aa", bg_color="#F5FCFF", save_key=None): """▲▼-Pfeile für Textfeld-Schriftgröße (5–20pt), unauffällig im Hintergrund.""" if save_key: saved = _load_font_sizes().get(save_key) if saved is not None: initial_size = int(saved) _size = [max(5, min(20, initial_size))] _fg = "#8AAFC0" _fg_hover = "#1a4d6d" cf = tk.Frame(parent_frame, bg=bg_color, highlightthickness=0, bd=0) cf.pack(side="right", padx=4) tk.Label(cf, text=label, font=("Segoe UI", 8), bg=bg_color, fg=_fg).pack(side="left", padx=(0, 1)) size_lbl = tk.Label(cf, text=str(_size[0]), font=("Segoe UI", 8), bg=bg_color, fg=_fg, width=2, anchor="center") size_lbl.pack(side="left") def _apply(ns): ns = max(5, min(20, ns)) _size[0] = ns size_lbl.configure(text=str(ns)) try: text_widget.configure(font=("Segoe UI", ns)) except Exception: pass if save_key: _save_font_size(save_key, ns) try: text_widget.configure(font=("Segoe UI", _size[0])) except Exception: pass btn_up = tk.Label(cf, text="\u25B2", font=("Segoe UI", 7), bg=bg_color, fg=_fg, cursor="hand2", bd=0, highlightthickness=0) btn_up.pack(side="left", padx=(2, 0)) btn_down = tk.Label(cf, text="\u25BC", font=("Segoe UI", 7), bg=bg_color, fg=_fg, cursor="hand2", bd=0, highlightthickness=0) btn_down.pack(side="left") btn_up.bind("", lambda e: _apply(_size[0] + 1)) btn_down.bind("", lambda e: _apply(_size[0] - 1)) for w in (btn_up, btn_down): w.bind("", lambda e, ww=w: ww.configure(fg=_fg_hover)) w.bind("", lambda e, ww=w: ww.configure(fg=_fg)) return _size # ─── Farbpalette (hellblau, passend zu basis14.py) ─── C_HEADER = "#5B8DB3" C_HEADER_HOVER = "#4A7A9E" C_BG = "#E8F4FA" C_CARD = "#FFFFFF" C_INPUT_BG = "#F5FCFF" C_ACCENT = "#7EC8E3" C_ACCENT_DARK = "#5B8DB3" C_TEXT = "#1a4d6d" C_TEXT_LIGHT = "#4a8aaa" C_TEXT_MUTED = "#8ab0c4" C_BORDER = "#D0E8F0" C_STATUS_BG = "#B9ECFA" C_SIDEBAR = "#D4EEF7" C_BTN_PRIMARY = "#5B8DB3" C_BTN_FG = "#FFFFFF" C_BTN_SEC = "#B9ECFA" C_GREEN = "#68C49C" # WhatsApp-Farben (nur für Chat-Ansicht) WA_HEADER = "#075E54" WA_HEADER_LIGHT = "#128C7E" WA_CHAT_BG = "#E5DDD5" WA_BUBBLE_OUT = "#DCF8C6" WA_BUBBLE_IN = "#FFFFFF" WA_TEXT_TIME = "#667781" WA_BLUE_TICK = "#53BDEB" WA_DATE_BADGE = "#E1F2FB" WA_DATE_TEXT = "#667781" WA_UNREAD_GREEN = "#25D366" class MedWorkApp: def __init__(self, root): self.root = root self.root.title("MedWork \u2013 Kommunikation f\u00fcr \u00c4rzte") self.root.geometry("1100x700") self.root.minsize(900, 600) self.root.configure(bg=C_BG) self.user = load_user_profile() if not self.user.get("name"): self.user = {"name": "Benutzer", "specialty": "", "clinic": ""} self.contacts = load_contacts() self.messages = load_messages() if not self.messages and self.contacts: self._create_demo_messages() self._active_chat_contact = None self._chat_mw_func = None self.posts = [ {"author": "Dr. Johann Schmidt", "specialty": "Kardiologie", "time": "vor 2 Stunden", "content": "Interessanter Fall heute: Patient mit komplexer Arrhythmie. Hat jemand Erfahrung mit \u00e4hnlichen F\u00e4llen?", "likes": 12, "comments": 5, "liked": False}, {"author": "Prof. Dr. Anna M\u00fcller", "specialty": "Neurologie", "time": "vor 5 Stunden", "content": "Neue Studie zu Alzheimer-Pr\u00e4vention ver\u00f6ffentlicht. Sehr empfehlenswert!", "likes": 45, "comments": 18, "liked": False}, {"author": "Dr. Thomas Klein", "specialty": "Orthop\u00e4die", "time": "vor 1 Tag", "content": "Workshop zu minimalinvasiven Eingriffen n\u00e4chsten Monat. Wer hat Interesse?", "likes": 23, "comments": 9, "liked": False}, ] config = load_docapp_config() saved_geom = config.get("geometry", "") if saved_geom: try: self.root.geometry(saved_geom) except Exception: pass self.root.protocol("WM_DELETE_WINDOW", self._on_close) self._current_view = "chat" self._build_ui() def _on_close(self): config = load_docapp_config() config["geometry"] = self.root.geometry() save_docapp_config(config) save_contacts(self.contacts) save_messages(self.messages) self.root.destroy() def _create_demo_messages(self): now = datetime.now() today = now.strftime("%Y-%m-%d") yesterday = (now - timedelta(days=1)).strftime("%Y-%m-%d") demos = [] if len(self.contacts) > 0: c = self.contacts[0] name = c.split("(")[0].strip() if "(" in c else c demos += [ {"id": str(uuid.uuid4()), "contact": c, "from": name, "text": "Guten Tag, k\u00f6nnen Sie mir bitte den Befund zusenden?", "time": "09:15", "date": yesterday, "read": True, "delivered": True}, {"id": str(uuid.uuid4()), "contact": c, "from": self.user.get("name", "Benutzer"), "text": "Ja, schicke ich Ihnen heute noch zu.", "time": "09:30", "date": yesterday, "read": True, "delivered": True}, {"id": str(uuid.uuid4()), "contact": c, "from": name, "text": "Perfekt, vielen Dank!", "time": "09:32", "date": yesterday, "read": True, "delivered": True}, ] if len(self.contacts) > 1: c = self.contacts[1] name = c.split("(")[0].strip() if "(" in c else c demos += [ {"id": str(uuid.uuid4()), "contact": c, "from": name, "text": "Haben Sie n\u00e4chste Woche Zeit f\u00fcr eine Besprechung?", "time": "14:20", "date": today, "read": False, "delivered": True}, ] if len(self.contacts) > 2: c = self.contacts[2] name = c.split("(")[0].strip() if "(" in c else c demos += [ {"id": str(uuid.uuid4()), "contact": c, "from": self.user.get("name", "Benutzer"), "text": "Das EKG sieht unauff\u00e4llig aus.", "time": "11:00", "date": today, "read": True, "delivered": True}, {"id": str(uuid.uuid4()), "contact": c, "from": name, "text": "Danke f\u00fcr die schnelle R\u00fcckmeldung!", "time": "11:15", "date": today, "read": True, "delivered": True}, ] self.messages = demos save_messages(self.messages) # ─── UI Aufbau ─── def _build_ui(self): self._create_header() self._main_frame = tk.Frame(self.root, bg=C_BG) self._main_frame.pack(fill="both", expand=True) self._left_sidebar = tk.Frame(self._main_frame, bg=C_SIDEBAR, width=220) self._left_sidebar.pack(side="left", fill="y", padx=(8, 4), pady=8) self._left_sidebar.pack_propagate(False) self._content_area = tk.Frame(self._main_frame, bg=C_BG) self._content_area.pack(side="left", fill="both", expand=True, padx=4, pady=8) self._right_sidebar = tk.Frame(self._main_frame, bg=C_SIDEBAR, width=220) self._right_sidebar.pack(side="right", fill="y", padx=(4, 8), pady=8) self._right_sidebar.pack_propagate(False) self._create_left_sidebar() self._show_chat() self._create_right_sidebar() self.status_var = tk.StringVar(value=f"Angemeldet als {self.user['name']}") tk.Label(self.root, textvariable=self.status_var, bg=C_STATUS_BG, fg=C_TEXT, font=("Segoe UI", 9), anchor="w", padx=12, pady=3).pack(side="bottom", fill="x") def _create_header(self): header = tk.Frame(self.root, bg=C_HEADER, height=52) header.pack(side="top", fill="x") header.pack_propagate(False) tk.Label(header, text="MedWork", bg=C_HEADER, fg=C_BTN_FG, font=("Segoe UI", 17, "bold")).pack(side="left", padx=16, pady=6) tk.Label(header, text="von Arzt zu Arzt", bg=C_HEADER, fg="#B9ECFA", font=("Segoe UI", 8, "italic")).pack(side="left", pady=6) btn_frame = tk.Frame(header, bg=C_HEADER) btn_frame.pack(side="right", padx=12) for text, cmd in [ ("\U0001f4ac Chat", lambda: self._switch_view("chat")), ("\U0001f3e0 Start", lambda: self._switch_view("feed")), ("\U0001f465 Kollegen", lambda: self._switch_view("colleagues")), ("\U0001f514", lambda: self._switch_view("notifications")), ("\U0001f464", self._edit_profile), ]: b = tk.Button(btn_frame, text=text, command=cmd, bg=C_HEADER, fg=C_BTN_FG, font=("Segoe UI", 10), relief="flat", padx=8, pady=4, cursor="hand2", activebackground=C_HEADER_HOVER, activeforeground=C_BTN_FG) b.pack(side="left", padx=2) b.bind("", lambda e, bt=b: bt.configure(bg=C_HEADER_HOVER)) b.bind("", lambda e, bt=b: bt.configure(bg=C_HEADER)) def _create_left_sidebar(self): parent = self._left_sidebar profile = tk.Frame(parent, bg=C_CARD, padx=10, pady=10) profile.pack(fill="x", padx=6, pady=(6, 4)) tk.Label(profile, text="\U0001f464", font=("Segoe UI", 20), bg=C_CARD, fg=C_ACCENT).pack(anchor="w") tk.Label(profile, text=self.user.get("name", ""), bg=C_CARD, fg=C_TEXT, font=("Segoe UI", 10, "bold"), anchor="w").pack(fill="x", pady=(2, 0)) if self.user.get("specialty"): tk.Label(profile, text=self.user["specialty"], bg=C_CARD, fg=C_TEXT_LIGHT, font=("Segoe UI", 8), anchor="w").pack(fill="x") if self.user.get("clinic"): tk.Label(profile, text=self.user["clinic"], bg=C_CARD, fg=C_TEXT_MUTED, font=("Segoe UI", 8), anchor="w").pack(fill="x") menu_items = [ ("\U0001f4ac Chat", lambda: self._switch_view("chat")), ("\U0001f4f0 Newsfeed", lambda: self._switch_view("feed")), ("\U0001f465 Kollegen", lambda: self._switch_view("colleagues")), ("\U0001f4c5 Veranstaltungen", lambda: self._switch_view("events")), ("\u2699 Einstellungen", self._edit_profile), ] for text, cmd in menu_items: b = tk.Button(parent, text=text, command=cmd, bg=C_SIDEBAR, fg=C_TEXT, font=("Segoe UI", 9), relief="flat", anchor="w", padx=10, pady=5, cursor="hand2", activebackground=C_ACCENT, activeforeground=C_BTN_FG) b.pack(fill="x", pady=1, padx=4) b.bind("", lambda e, bt=b: bt.configure(bg=C_CARD)) b.bind("", lambda e, bt=b: bt.configure(bg=C_SIDEBAR)) tk.Label(parent, text="\U0001f7e2 Online", bg=C_SIDEBAR, fg=C_GREEN, font=("Segoe UI", 8)).pack(anchor="w", padx=10, pady=(12, 4), side="bottom") def _create_right_sidebar(self): parent = self._right_sidebar tk.Label(parent, text="\U0001f4cc Kontakte", bg=C_SIDEBAR, fg=C_TEXT, font=("Segoe UI", 10, "bold"), anchor="w", padx=10).pack(fill="x", pady=(10, 4)) self._contacts_list_frame = tk.Frame(parent, bg=C_SIDEBAR) self._contacts_list_frame.pack(fill="x", padx=4) self._rebuild_contacts_sidebar() tk.Button(parent, text="\u2795 Kontakt hinzuf\u00fcgen", command=self._add_contact, bg=C_SIDEBAR, fg=C_TEXT_LIGHT, font=("Segoe UI", 8), relief="flat", cursor="hand2", activebackground=C_CARD).pack(fill="x", padx=10, pady=4) tk.Frame(parent, bg=C_BORDER, height=1).pack(fill="x", padx=6, pady=8) tk.Label(parent, text="\U0001f4c5 N\u00e4chste Events", bg=C_SIDEBAR, fg=C_TEXT, font=("Segoe UI", 10, "bold"), anchor="w", padx=10).pack(fill="x", pady=(0, 4)) events = [("Kardiologie-Symposium", "15. M\u00e4rz"), ("Fortbildung KI", "22. M\u00e4rz"), ("Praxis-Meeting", "Montags")] for title, when in events: f = tk.Frame(parent, bg=C_SIDEBAR, padx=10, pady=1) f.pack(fill="x") tk.Label(f, text=title, bg=C_SIDEBAR, fg=C_TEXT, font=("Segoe UI", 8), anchor="w").pack(fill="x") tk.Label(f, text=when, bg=C_SIDEBAR, fg=C_TEXT_MUTED, font=("Segoe UI", 7), anchor="w").pack(fill="x") def _rebuild_contacts_sidebar(self): for w in self._contacts_list_frame.winfo_children(): w.destroy() for c in self.contacts[:8]: name = c.split("(")[0].strip() if "(" in c else c b = tk.Button(self._contacts_list_frame, text=f" {name}", bg=C_SIDEBAR, fg=C_TEXT, font=("Segoe UI", 8), relief="flat", anchor="w", cursor="hand2", command=lambda cn=c: self._open_chat_with(cn), activebackground=C_CARD) b.pack(fill="x", pady=1) b.bind("", lambda e, bt=b: bt.configure(bg=C_CARD)) b.bind("", lambda e, bt=b: bt.configure(bg=C_SIDEBAR)) # ─── View-Switching ─── def _switch_view(self, view): self._current_view = view for w in self._content_area.winfo_children(): w.destroy() if view == "feed": self._show_feed() elif view == "colleagues": self._show_colleagues() elif view == "chat": self._show_chat() elif view == "notifications": self._show_notifications() elif view == "events": self._show_events() self.status_var.set(f"{view.title()} \u2013 {self.user['name']}") # ─── Feed ─── def _show_feed(self): parent = self._content_area create_frame = tk.Frame(parent, bg=C_CARD, highlightbackground=C_BORDER, highlightthickness=1) create_frame.pack(fill="x", pady=(0, 8)) inner = tk.Frame(create_frame, bg=C_CARD, padx=12, pady=8) inner.pack(fill="x") tk.Label(inner, text="Was m\u00f6chten Sie teilen?", bg=C_CARD, fg=C_TEXT, font=("Segoe UI", 10, "bold"), anchor="w").pack(fill="x", pady=(0, 4)) entry_row = tk.Frame(inner, bg=C_CARD) entry_row.pack(fill="x") self._post_entry = tk.Entry(entry_row, font=("Segoe UI", 10), bg=C_INPUT_BG, fg=C_TEXT_MUTED, relief="flat", insertbackground=C_TEXT) self._post_entry.pack(side="left", fill="x", expand=True, ipady=6, padx=(0, 6)) self._post_entry.insert(0, "Gedanken, Fragen oder F\u00e4lle teilen\u2026") self._post_entry.bind("", lambda e: self._placeholder_focus(self._post_entry, "Gedanken, Fragen oder F\u00e4lle teilen\u2026", True)) self._post_entry.bind("", lambda e: self._placeholder_focus(self._post_entry, "Gedanken, Fragen oder F\u00e4lle teilen\u2026", False)) tk.Button(entry_row, text="Posten", command=self._create_post, bg=C_BTN_PRIMARY, fg=C_BTN_FG, font=("Segoe UI", 10, "bold"), relief="flat", padx=14, pady=3, cursor="hand2", activebackground=C_HEADER_HOVER).pack(side="right") canvas_f = tk.Frame(parent, bg=C_BG) canvas_f.pack(fill="both", expand=True) canvas = tk.Canvas(canvas_f, bg=C_BG, highlightthickness=0) sb = ttk.Scrollbar(canvas_f, orient="vertical", command=canvas.yview) scroll_frame = tk.Frame(canvas, bg=C_BG) scroll_frame.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) canvas.create_window((0, 0), window=scroll_frame, anchor="nw", tags="fw") canvas.configure(yscrollcommand=sb.set) canvas.bind("", lambda e: canvas.itemconfig("fw", width=e.width)) canvas.bind("", lambda e: canvas.yview_scroll(int(-1 * (e.delta / 120)), "units")) canvas.pack(side="left", fill="both", expand=True) sb.pack(side="right", fill="y") for post in self.posts: self._create_post_widget(scroll_frame, post) def _create_post_widget(self, parent, post): card = tk.Frame(parent, bg=C_CARD, highlightbackground=C_BORDER, highlightthickness=1) card.pack(fill="x", pady=(0, 6), padx=2) head = tk.Frame(card, bg=C_CARD, padx=12, pady=6) head.pack(fill="x") tk.Label(head, text=post["author"], bg=C_CARD, fg=C_TEXT, font=("Segoe UI", 10, "bold"), anchor="w").pack(fill="x") tk.Label(head, text=f"{post['specialty']} \u00b7 {post['time']}", bg=C_CARD, fg=C_TEXT_MUTED, font=("Segoe UI", 8), anchor="w").pack(fill="x") tk.Label(card, text=post["content"], bg=C_CARD, fg=C_TEXT, font=("Segoe UI", 10), anchor="w", wraplength=500, justify="left", padx=12).pack(fill="x", pady=(0, 6)) tk.Frame(card, bg=C_BORDER, height=1).pack(fill="x") stats = tk.Frame(card, bg=C_CARD, padx=12, pady=3) stats.pack(fill="x") likes_lbl = tk.Label(stats, text=f"\U0001f44d {post['likes']} \U0001f4ac {post['comments']}", bg=C_CARD, fg=C_TEXT_MUTED, font=("Segoe UI", 8)) likes_lbl.pack(side="left") tk.Frame(card, bg=C_BORDER, height=1).pack(fill="x") actions = tk.Frame(card, bg=C_CARD, padx=6, pady=3) actions.pack(fill="x") def do_like(): post["liked"] = not post.get("liked", False) post["likes"] += 1 if post["liked"] else -1 likes_lbl.configure(text=f"\U0001f44d {post['likes']} \U0001f4ac {post['comments']}") like_btn.configure(fg=C_ACCENT_DARK if post["liked"] else C_TEXT_LIGHT) like_btn = tk.Button(actions, text="\U0001f44d Gef\u00e4llt mir", bg=C_CARD, fg=C_ACCENT_DARK if post.get("liked") else C_TEXT_LIGHT, font=("Segoe UI", 9), relief="flat", cursor="hand2", command=do_like) like_btn.pack(side="left", expand=True) tk.Button(actions, text="\U0001f4ac Kommentieren", bg=C_CARD, fg=C_TEXT_LIGHT, font=("Segoe UI", 9), relief="flat", cursor="hand2", command=lambda: self._comment_on_post(post)).pack(side="left", expand=True) tk.Button(actions, text="\u2197 Teilen", bg=C_CARD, fg=C_TEXT_LIGHT, font=("Segoe UI", 9), relief="flat", cursor="hand2", command=lambda: self.status_var.set("Teilen-Funktion kommt bald!")).pack(side="left", expand=True) def _create_post(self): text = self._post_entry.get().strip() if not text or text == "Gedanken, Fragen oder F\u00e4lle teilen\u2026": return new_post = { "author": self.user["name"], "specialty": self.user.get("specialty", ""), "time": "gerade eben", "content": text, "likes": 0, "comments": 0, "liked": False, } self.posts.insert(0, new_post) self._post_entry.delete(0, "end") self._post_entry.insert(0, "Gedanken, Fragen oder F\u00e4lle teilen\u2026") self._post_entry.configure(fg=C_TEXT_MUTED) self._switch_view("feed") self.status_var.set("Post ver\u00f6ffentlicht!") def _comment_on_post(self, post): text = simpledialog.askstring("Kommentar", f"Kommentar zu {post['author']}:", parent=self.root) if text and text.strip(): post["comments"] += 1 self.status_var.set("Kommentar gesendet!") # ─── Kollegen ─── def _show_colleagues(self): parent = self._content_area tk.Label(parent, text="\U0001f465 Kollegen & Kontakte", bg=C_BG, fg=C_TEXT, font=("Segoe UI", 14, "bold")).pack(anchor="w", padx=8, pady=(0, 8)) for c in self.contacts: row = tk.Frame(parent, bg=C_CARD, highlightbackground=C_BORDER, highlightthickness=1) row.pack(fill="x", pady=2, padx=4) inner = tk.Frame(row, bg=C_CARD, padx=12, pady=8) inner.pack(fill="x") name = c.split("(")[0].strip() if "(" in c else c spec = c.split("(")[1].rstrip(")") if "(" in c else "" tk.Label(inner, text=name, bg=C_CARD, fg=C_TEXT, font=("Segoe UI", 10, "bold"), anchor="w").pack(side="left") if spec: tk.Label(inner, text=f" ({spec})", bg=C_CARD, fg=C_TEXT_MUTED, font=("Segoe UI", 9)).pack(side="left") tk.Button(inner, text="\U0001f4ac Chat", bg=C_ACCENT, fg=C_BTN_FG, font=("Segoe UI", 8), relief="flat", cursor="hand2", padx=8, command=lambda cn=c: self._open_chat_with(cn)).pack(side="right", padx=4) tk.Button(inner, text="\u2715", bg=C_CARD, fg="#ccc", font=("Segoe UI", 9), relief="flat", cursor="hand2", command=lambda cn=c: self._remove_contact(cn)).pack(side="right") add_row = tk.Frame(parent, bg=C_BG, pady=8) add_row.pack(fill="x", padx=4) tk.Button(add_row, text="\u2795 Neuen Kontakt hinzuf\u00fcgen", command=self._add_contact, bg=C_BTN_PRIMARY, fg=C_BTN_FG, font=("Segoe UI", 10), relief="flat", padx=14, pady=4, cursor="hand2").pack(anchor="w") # ═══════════════════════════════════════════════ # ─── Chat (WhatsApp-Stil) ─── # ═══════════════════════════════════════════════ def _show_chat(self): """Chat-\u00dcbersicht: Links Kontakte, rechts Chat (WhatsApp-Layout).""" parent = self._content_area chat_split = tk.Frame(parent, bg=WA_CHAT_BG) chat_split.pack(fill="both", expand=True) # ─── Linke Spalte: Kontaktliste ─── left_col = tk.Frame(chat_split, bg="#FFFFFF", width=280) left_col.pack(side="left", fill="y") left_col.pack_propagate(False) contact_header = tk.Frame(left_col, bg=WA_HEADER, height=48) contact_header.pack(fill="x") contact_header.pack_propagate(False) tk.Label(contact_header, text="\U0001f4ac Chats", bg=WA_HEADER, fg="white", font=("Segoe UI", 12, "bold")).pack(side="left", padx=12, pady=8) add_btn = tk.Label(contact_header, text="\u2795", font=("Segoe UI", 11), bg=WA_HEADER, fg="white", cursor="hand2", padx=8) add_btn.pack(side="right", pady=8) add_btn.bind("", lambda e: self._add_contact()) # Suchfeld search_frame = tk.Frame(left_col, bg="#F6F6F6", pady=6, padx=8) search_frame.pack(fill="x") search_inner = tk.Frame(search_frame, bg="white", padx=6, pady=3, highlightbackground="#DDD", highlightthickness=1) search_inner.pack(fill="x") tk.Label(search_inner, text="\U0001f50d", bg="white", fg="#999", font=("Segoe UI", 9)).pack(side="left") self._chat_search_var = tk.StringVar() tk.Entry(search_inner, textvariable=self._chat_search_var, font=("Segoe UI", 9), bg="white", fg="#333", relief="flat", insertbackground="#333").pack( side="left", fill="x", expand=True, ipady=2, padx=4) # Trennlinie tk.Frame(chat_split, bg="#E0E0E0", width=1).pack(side="left", fill="y") # ─── Rechte Spalte: Chat-Bereich ─── self._chat_right_col = tk.Frame(chat_split, bg=WA_CHAT_BG) self._chat_right_col.pack(side="left", fill="both", expand=True) # Kontakt-Liste (scrollbar) contact_list_frame = tk.Frame(left_col, bg="white") contact_list_frame.pack(fill="both", expand=True) contact_canvas = tk.Canvas(contact_list_frame, bg="white", highlightthickness=0) contact_sb = ttk.Scrollbar(contact_list_frame, orient="vertical", command=contact_canvas.yview) contact_inner = tk.Frame(contact_canvas, bg="white") contact_inner.bind("", lambda e: contact_canvas.configure(scrollregion=contact_canvas.bbox("all"))) contact_canvas.create_window((0, 0), window=contact_inner, anchor="nw", tags="cl") contact_canvas.configure(yscrollcommand=contact_sb.set) contact_canvas.bind("", lambda e: contact_canvas.itemconfig("cl", width=e.width)) def _cl_mw(event): contact_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") contact_canvas.bind("", _cl_mw) contact_inner.bind("", _cl_mw) contact_canvas.pack(side="left", fill="both", expand=True) contact_sb.pack(side="right", fill="y") self._chat_contact_inner = contact_inner self._chat_contact_mw = _cl_mw self._chat_search_var.trace_add("write", lambda *a: self._rebuild_chat_contact_list()) self._rebuild_chat_contact_list() if self._active_chat_contact: self._show_chat_messages(self._active_chat_contact) else: self._show_empty_chat() def _rebuild_chat_contact_list(self): for w in self._chat_contact_inner.winfo_children(): w.destroy() search = self._chat_search_var.get().strip().lower() if hasattr(self, '_chat_search_var') else "" filtered = [c for c in self.contacts if search in c.lower()] if search else self.contacts for c in filtered: self._create_chat_contact_row(c) def _create_chat_contact_row(self, contact): parent = self._chat_contact_inner name = contact.split("(")[0].strip() if "(" in contact else contact msgs = [m for m in self.messages if m.get("contact") == contact] last_msg_text = "" last_msg_time = "" if msgs: last = msgs[-1] prefix = "Sie: " if last.get("from") == self.user["name"] else "" last_msg_text = prefix + last.get("text", "")[:40] if len(last.get("text", "")) > 40: last_msg_text += "\u2026" last_msg_time = last.get("time", "") unread = sum(1 for m in msgs if not m.get("read") and m.get("from") != self.user["name"]) is_selected = self._active_chat_contact == contact row_bg = "#E0F2F1" if is_selected else "white" row = tk.Frame(parent, bg=row_bg, cursor="hand2") row.pack(fill="x") tk.Frame(parent, bg="#F0F0F0", height=1).pack(fill="x", padx=(62, 0)) # Avatar av_frame = tk.Frame(row, bg=row_bg, width=46, height=46) av_frame.pack(side="left", padx=(10, 8), pady=8) av_frame.pack_propagate(False) initials = "".join([w[0].upper() for w in name.split()[:2] if w]) colors = ["#128C7E", "#075E54", "#25D366", "#009688", "#00897B", "#00796B"] av_bg = colors[hash(contact) % len(colors)] av_canvas = tk.Canvas(av_frame, width=42, height=42, bg=row_bg, highlightthickness=0) av_canvas.pack(expand=True) av_canvas.create_oval(1, 1, 41, 41, fill=av_bg, outline="") av_canvas.create_text(21, 21, text=initials, fill="white", font=("Segoe UI", 12, "bold")) # Info info = tk.Frame(row, bg=row_bg) info.pack(side="left", fill="both", expand=True, pady=8, padx=(0, 8)) top = tk.Frame(info, bg=row_bg) top.pack(fill="x") name_f = ("Segoe UI", 10, "bold") if unread else ("Segoe UI", 10) tk.Label(top, text=name, bg=row_bg, fg="#303030", font=name_f, anchor="w").pack( side="left", fill="x", expand=True) time_fg = WA_UNREAD_GREEN if unread else "#667781" tk.Label(top, text=last_msg_time, bg=row_bg, fg=time_fg, font=("Segoe UI", 8)).pack(side="right") bot = tk.Frame(info, bg=row_bg) bot.pack(fill="x") msg_f = ("Segoe UI", 9, "bold") if unread else ("Segoe UI", 9) msg_fg = "#303030" if unread else "#667781" tk.Label(bot, text=last_msg_text or "Noch keine Nachrichten", bg=row_bg, fg=msg_fg, font=msg_f, anchor="w").pack(side="left", fill="x", expand=True) if unread: tk.Label(bot, text=str(unread), bg=WA_UNREAD_GREEN, fg="white", font=("Segoe UI", 8, "bold"), padx=5, pady=1).pack(side="right") def select(e, c=contact): self._active_chat_contact = c for m in self.messages: if m.get("contact") == c and m.get("from") != self.user["name"]: m["read"] = True save_messages(self.messages) self._rebuild_chat_contact_list() self._show_chat_messages(c) all_widgets = [row, av_frame, av_canvas, info, top, bot] for widget in all_widgets: widget.bind("", select) widget.bind("", self._chat_contact_mw) for child in widget.winfo_children(): child.bind("", select) child.bind("", self._chat_contact_mw) def on_enter(e): if self._active_chat_contact != contact: for w in [row, av_frame, info, top, bot]: w.configure(bg="#F5F6F6") for ch in w.winfo_children(): try: ch.configure(bg="#F5F6F6") except Exception: pass def on_leave(e): bg = "#E0F2F1" if self._active_chat_contact == contact else "white" for w in [row, av_frame, info, top, bot]: w.configure(bg=bg) for ch in w.winfo_children(): try: ch.configure(bg=bg) except Exception: pass row.bind("", on_enter) row.bind("", on_leave) def ctx_menu(e, c=contact): ctx = tk.Menu(self.root, tearoff=0, font=("Segoe UI", 9)) ctx.add_command(label="\U0001f4ac Chat \u00f6ffnen", command=lambda: select(None, c)) ctx.add_command(label="\U0001f5d1 Chat l\u00f6schen", command=lambda: self._clear_chat(c)) ctx.add_separator() ctx.add_command(label="\u2715 Kontakt entfernen", command=lambda: self._remove_contact(c)) ctx.post(e.x_root, e.y_root) row.bind("", ctx_menu) def _show_empty_chat(self): for w in self._chat_right_col.winfo_children(): w.destroy() center = tk.Frame(self._chat_right_col, bg=WA_CHAT_BG) center.pack(fill="both", expand=True) inner = tk.Frame(center, bg=WA_CHAT_BG) inner.place(relx=0.5, rely=0.45, anchor="center") tk.Label(inner, text="\U0001f4ac", font=("Segoe UI", 48), bg=WA_CHAT_BG, fg="#667781").pack() tk.Label(inner, text="MedWork Chat", font=("Segoe UI", 20), bg=WA_CHAT_BG, fg="#667781").pack(pady=(8, 4)) tk.Label(inner, text="W\u00e4hlen Sie einen Kontakt, um den Chat zu starten.", font=("Segoe UI", 11), bg=WA_CHAT_BG, fg="#667781").pack() def _show_chat_messages(self, contact): for w in self._chat_right_col.winfo_children(): w.destroy() name = contact.split("(")[0].strip() if "(" in contact else contact spec = contact.split("(")[1].rstrip(")") if "(" in contact else "" # ─── Chat-Header ─── header = tk.Frame(self._chat_right_col, bg=WA_HEADER, height=48) header.pack(fill="x") header.pack_propagate(False) initials = "".join([w[0].upper() for w in name.split()[:2] if w]) colors = ["#128C7E", "#075E54", "#25D366", "#009688", "#00897B", "#00796B"] av_bg = colors[hash(contact) % len(colors)] av = tk.Canvas(header, width=34, height=34, bg=WA_HEADER, highlightthickness=0) av.pack(side="left", padx=(10, 8), pady=7) av.create_oval(1, 1, 33, 33, fill=av_bg, outline="") av.create_text(17, 17, text=initials, fill="white", font=("Segoe UI", 10, "bold")) name_f = tk.Frame(header, bg=WA_HEADER) name_f.pack(side="left", fill="x", expand=True) tk.Label(name_f, text=name, bg=WA_HEADER, fg="white", font=("Segoe UI", 11, "bold"), anchor="w").pack(fill="x") if spec: tk.Label(name_f, text=spec, bg=WA_HEADER, fg="#A8D5CF", font=("Segoe UI", 8), anchor="w").pack(fill="x") hdr_btns = tk.Frame(header, bg=WA_HEADER) hdr_btns.pack(side="right", padx=8) # Telefon-Button phone_btn = tk.Label(hdr_btns, text="\U0001f4de", font=("Segoe UI", 14), bg=WA_HEADER, fg="white", cursor="hand2", padx=6) phone_btn.pack(side="left") phone_btn.bind("", lambda e: self._start_call(contact)) phone_btn.bind("", lambda e: phone_btn.configure(fg="#A8D5CF")) phone_btn.bind("", lambda e: phone_btn.configure(fg="white")) # Video-Button video_btn = tk.Label(hdr_btns, text="\U0001f4f9", font=("Segoe UI", 14), bg=WA_HEADER, fg="white", cursor="hand2", padx=6) video_btn.pack(side="left") video_btn.bind("", lambda e: self._start_call(contact, video=True)) video_btn.bind("", lambda e: video_btn.configure(fg="#A8D5CF")) video_btn.bind("", lambda e: video_btn.configure(fg="white")) search_btn = tk.Label(hdr_btns, text="\U0001f50d", font=("Segoe UI", 13), bg=WA_HEADER, fg="white", cursor="hand2", padx=6) search_btn.pack(side="left") search_btn.bind("", lambda e: self._search_in_chat(contact)) menu_btn = tk.Label(hdr_btns, text="\u22ee", font=("Segoe UI", 15, "bold"), bg=WA_HEADER, fg="white", cursor="hand2", padx=6) menu_btn.pack(side="left") menu_btn.bind("", lambda e: self._show_chat_menu(e, contact)) # ─── Chat-Nachrichten ─── chat_frame = tk.Frame(self._chat_right_col, bg=WA_CHAT_BG) chat_frame.pack(fill="both", expand=True) chat_canvas = tk.Canvas(chat_frame, bg=WA_CHAT_BG, highlightthickness=0) chat_sb = ttk.Scrollbar(chat_frame, orient="vertical", command=chat_canvas.yview) chat_inner = tk.Frame(chat_canvas, bg=WA_CHAT_BG) chat_inner.bind("", lambda e: chat_canvas.configure(scrollregion=chat_canvas.bbox("all"))) chat_canvas.create_window((0, 0), window=chat_inner, anchor="nw", tags="chat_win") chat_canvas.configure(yscrollcommand=chat_sb.set) chat_canvas.bind("", lambda e: chat_canvas.itemconfig("chat_win", width=e.width)) chat_canvas.pack(side="left", fill="both", expand=True) chat_sb.pack(side="right", fill="y") def _chat_mw(event): chat_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") chat_canvas.bind("", _chat_mw) chat_inner.bind("", _chat_mw) self._chat_mw_func = _chat_mw msgs = [m for m in self.messages if m.get("contact") == contact] self._render_messages(chat_inner, msgs) chat_canvas.update_idletasks() chat_canvas.yview_moveto(1.0) # ─── Eingabe-Leiste ─── input_bar = tk.Frame(self._chat_right_col, bg="#F0F0F0", padx=8, pady=6) input_bar.pack(fill="x") emoji_btn = tk.Label(input_bar, text="\U0001f60a", font=("Segoe UI", 15), bg="#F0F0F0", fg="#667781", cursor="hand2") emoji_btn.pack(side="left", padx=(4, 4)) attach_btn = tk.Label(input_bar, text="\U0001f4ce", font=("Segoe UI", 13), bg="#F0F0F0", fg="#667781", cursor="hand2") attach_btn.pack(side="left", padx=(0, 4)) msg_entry = tk.Entry(input_bar, font=("Segoe UI", 11), bg="white", fg="#303030", relief="flat", insertbackground="#303030") msg_entry.pack(side="left", fill="x", expand=True, ipady=7, padx=(0, 6)) msg_entry.focus_set() send_btn = tk.Label(input_bar, text="\u27a4", font=("Segoe UI", 17, "bold"), bg=WA_HEADER_LIGHT, fg="white", cursor="hand2", padx=10, pady=4) send_btn.pack(side="right") def send_msg(event=None): text = msg_entry.get().strip() if not text: return now = datetime.now() new_msg = { "id": str(uuid.uuid4()), "contact": contact, "from": self.user["name"], "text": text, "time": now.strftime("%H:%M"), "date": now.strftime("%Y-%m-%d"), "read": True, "delivered": True, } self.messages.append(new_msg) save_messages(self.messages) msg_entry.delete(0, "end") self._add_single_message(chat_inner, new_msg, True) chat_canvas.update_idletasks() chat_canvas.yview_moveto(1.0) self._rebuild_chat_contact_list() self.root.after(1500, lambda: self._simulate_reply(contact, chat_inner, chat_canvas)) msg_entry.bind("", send_msg) send_btn.bind("", lambda e: send_msg()) def _render_messages(self, parent, msgs): last_date = None for m in msgs: msg_date = m.get("date", date.today().isoformat()) if msg_date != last_date: last_date = msg_date self._add_date_separator(parent, msg_date) is_me = m.get("from") == self.user["name"] self._add_single_message(parent, m, is_me) def _add_date_separator(self, parent, date_str): sep = tk.Frame(parent, bg=WA_CHAT_BG, pady=8) sep.pack(fill="x") display = self._format_date_label(date_str) badge = tk.Label(sep, text=display, bg=WA_DATE_BADGE, fg=WA_DATE_TEXT, font=("Segoe UI", 8), padx=12, pady=3) badge.pack() if self._chat_mw_func: sep.bind("", self._chat_mw_func) badge.bind("", self._chat_mw_func) def _format_date_label(self, date_str): try: d = date.fromisoformat(date_str) today = date.today() diff = (today - d).days if diff == 0: return "Heute" elif diff == 1: return "Gestern" elif diff < 7: days = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"] return days[d.weekday()] else: return d.strftime("%d.%m.%Y") except Exception: return date_str def _add_single_message(self, parent, msg, is_me): row = tk.Frame(parent, bg=WA_CHAT_BG, pady=1) row.pack(fill="x") anchor = "e" if is_me else "w" bubble_bg = WA_BUBBLE_OUT if is_me else WA_BUBBLE_IN bubble = tk.Frame(row, bg=bubble_bg, padx=10, pady=6) bubble.pack(anchor=anchor, padx=(60 if is_me else 10, 10 if is_me else 60)) tk.Label(bubble, text=msg["text"], bg=bubble_bg, fg="#303030", font=("Segoe UI", 10), wraplength=350, justify="left", anchor="w").pack(fill="x", anchor="w") meta = tk.Frame(bubble, bg=bubble_bg) meta.pack(fill="x", anchor="e") time_text = msg.get("time", "") if is_me: read = msg.get("read", False) delivered = msg.get("delivered", False) if read: tick, tick_color = " \u2713\u2713", WA_BLUE_TICK elif delivered: tick, tick_color = " \u2713\u2713", "#667781" else: tick, tick_color = " \u2713", "#667781" tk.Label(meta, text=time_text, bg=bubble_bg, fg=WA_TEXT_TIME, font=("Segoe UI", 7), anchor="e").pack(side="right", padx=(4, 0)) tk.Label(meta, text=tick, bg=bubble_bg, fg=tick_color, font=("Segoe UI", 8), anchor="e").pack(side="right") else: tk.Label(meta, text=time_text, bg=bubble_bg, fg=WA_TEXT_TIME, font=("Segoe UI", 7), anchor="e").pack(side="right") if self._chat_mw_func: for w in [row, bubble, meta]: w.bind("", self._chat_mw_func) for child in w.winfo_children(): child.bind("", self._chat_mw_func) def _simulate_reply(self, contact, chat_inner, chat_canvas): name = contact.split("(")[0].strip() if "(" in contact else contact replies = [ "Vielen Dank f\u00fcr die Nachricht!", "Verstanden, ich melde mich dazu.", "OK, wird erledigt.", "Danke f\u00fcr die Info.", "Perfekt, so machen wir das.", "Ich schaue mir das an.", "Gerne, bis dann!", ] now = datetime.now() reply_msg = { "id": str(uuid.uuid4()), "contact": contact, "from": name, "text": random.choice(replies), "time": now.strftime("%H:%M"), "date": now.strftime("%Y-%m-%d"), "read": True if self._active_chat_contact == contact else False, "delivered": True, } self.messages.append(reply_msg) save_messages(self.messages) if self._active_chat_contact == contact: self._add_single_message(chat_inner, reply_msg, False) chat_canvas.update_idletasks() chat_canvas.yview_moveto(1.0) if hasattr(self, '_chat_contact_inner'): self._rebuild_chat_contact_list() # ─── Anruf-Funktion ─── def _start_call(self, contact, video=False): """Startet einen Anruf (Audio oder Video) mit WhatsApp-\u00e4hnlicher UI.""" name = contact.split("(")[0].strip() if "(" in contact else contact spec = contact.split("(")[1].rstrip(")") if "(" in contact else "" call_type = "Videoanruf" if video else "Sprachanruf" # Anruf-Fenster call_win = tk.Toplevel(self.root) call_win.title(f"{call_type} \u2013 {name}") call_win.geometry("380x580") call_win.resizable(False, False) call_win.attributes("-topmost", True) call_win.configure(bg="#1B3A2D") # Zustand call_active = [True] call_connected = [False] call_muted = [False] call_speaker = [False] call_seconds = [0] timer_id = [None] # ─── Oberer Bereich (Name, Status) ─── top_area = tk.Frame(call_win, bg="#1B3A2D") top_area.pack(fill="x", pady=(40, 0)) # Avatar-Kreis av_size = 100 av_canvas = tk.Canvas(top_area, width=av_size, height=av_size, bg="#1B3A2D", highlightthickness=0) av_canvas.pack() initials = "".join([w[0].upper() for w in name.split()[:2] if w]) colors = ["#128C7E", "#075E54", "#25D366", "#009688", "#00897B", "#00796B"] av_bg = colors[hash(contact) % len(colors)] av_canvas.create_oval(4, 4, av_size - 4, av_size - 4, fill=av_bg, outline="") av_canvas.create_text(av_size // 2, av_size // 2, text=initials, fill="white", font=("Segoe UI", 28, "bold")) tk.Label(top_area, text=name, bg="#1B3A2D", fg="white", font=("Segoe UI", 18, "bold")).pack(pady=(12, 2)) if spec: tk.Label(top_area, text=spec, bg="#1B3A2D", fg="#A8D5CF", font=("Segoe UI", 10)).pack() status_label = tk.Label(top_area, text="Klingelt\u2026", bg="#1B3A2D", fg="#8BB8A8", font=("Segoe UI", 12)) status_label.pack(pady=(8, 0)) timer_label = tk.Label(top_area, text="", bg="#1B3A2D", fg="white", font=("Segoe UI", 14)) timer_label.pack(pady=(4, 0)) # ─── Klingelton-Animation ─── ring_dots = [0] def animate_ringing(): if not call_active[0] or call_connected[0]: return dots = "." * (ring_dots[0] % 4) status_label.configure(text=f"Klingelt{dots}") ring_dots[0] += 1 timer_id[0] = call_win.after(600, animate_ringing) animate_ringing() # ─── Timer ─── def format_time(seconds): m, s = divmod(seconds, 60) h, m = divmod(m, 60) if h > 0: return f"{h}:{m:02d}:{s:02d}" return f"{m:02d}:{s:02d}" def update_timer(): if not call_active[0] or not call_connected[0]: return call_seconds[0] += 1 timer_label.configure(text=format_time(call_seconds[0])) timer_id[0] = call_win.after(1000, update_timer) # ─── Verbindung simulieren ─── def connect_call(): if not call_active[0]: return call_connected[0] = True status_label.configure(text="Verbunden", fg="#25D366") timer_label.configure(text="00:00") update_timer() # Chat-Nachricht hinzuf\u00fcgen now = datetime.now() call_msg = { "id": str(uuid.uuid4()), "contact": contact, "from": self.user["name"], "text": f"\U0001f4de {call_type} gestartet", "time": now.strftime("%H:%M"), "date": now.strftime("%Y-%m-%d"), "read": True, "delivered": True, } self.messages.append(call_msg) save_messages(self.messages) call_win.after(2500, connect_call) # ─── Mittelbereich: verschl\u00fcsselungs-Info ─── mid_area = tk.Frame(call_win, bg="#1B3A2D") mid_area.pack(fill="both", expand=True) if video: tk.Label(mid_area, text="\U0001f4f9", font=("Segoe UI", 40), bg="#1B3A2D", fg="#4A7A5E").pack(pady=20) tk.Label(mid_area, text="Kamera aktiv", bg="#1B3A2D", fg="#6B9B7E", font=("Segoe UI", 9)).pack() tk.Label(mid_area, text="\U0001f512 Ende-zu-Ende-verschl\u00fcsselt", bg="#1B3A2D", fg="#6B9B7E", font=("Segoe UI", 8)).pack(side="bottom", pady=8) # ─── Unterer Bereich: Buttons ─── btn_area = tk.Frame(call_win, bg="#1B3A2D", pady=20) btn_area.pack(fill="x") btn_row1 = tk.Frame(btn_area, bg="#1B3A2D") btn_row1.pack() btn_row2 = tk.Frame(btn_area, bg="#1B3A2D") btn_row2.pack(pady=(16, 0)) def create_call_btn(parent, icon, label_text, bg_color, fg_color, size=56): """Erstellt einen runden Anruf-Button.""" frame = tk.Frame(parent, bg="#1B3A2D") frame.pack(side="left", padx=16) c = tk.Canvas(frame, width=size, height=size, bg="#1B3A2D", highlightthickness=0, cursor="hand2") c.pack() c.create_oval(2, 2, size - 2, size - 2, fill=bg_color, outline="") c.create_text(size // 2, size // 2, text=icon, fill=fg_color, font=("Segoe UI", 16)) tk.Label(frame, text=label_text, bg="#1B3A2D", fg="#8BB8A8", font=("Segoe UI", 8)).pack(pady=(4, 0)) return c, frame # Stummschalten mute_canvas, mute_frame = create_call_btn( btn_row1, "\U0001f507", "Stumm", "#2E5641", "white") def toggle_mute(e=None): call_muted[0] = not call_muted[0] if call_muted[0]: mute_canvas.delete("all") mute_canvas.create_oval(2, 2, 54, 54, fill="white", outline="") mute_canvas.create_text(28, 28, text="\U0001f507", fill="#1B3A2D", font=("Segoe UI", 16)) else: mute_canvas.delete("all") mute_canvas.create_oval(2, 2, 54, 54, fill="#2E5641", outline="") mute_canvas.create_text(28, 28, text="\U0001f507", fill="white", font=("Segoe UI", 16)) mute_canvas.bind("", toggle_mute) # Lautsprecher speaker_canvas, speaker_frame = create_call_btn( btn_row1, "\U0001f50a", "Lautspr.", "#2E5641", "white") def toggle_speaker(e=None): call_speaker[0] = not call_speaker[0] if call_speaker[0]: speaker_canvas.delete("all") speaker_canvas.create_oval(2, 2, 54, 54, fill="white", outline="") speaker_canvas.create_text(28, 28, text="\U0001f50a", fill="#1B3A2D", font=("Segoe UI", 16)) else: speaker_canvas.delete("all") speaker_canvas.create_oval(2, 2, 54, 54, fill="#2E5641", outline="") speaker_canvas.create_text(28, 28, text="\U0001f50a", fill="white", font=("Segoe UI", 16)) speaker_canvas.bind("", toggle_speaker) # Video umschalten (nur bei Videoanruf) if video: cam_canvas, cam_frame = create_call_btn( btn_row1, "\U0001f4f7", "Kamera", "#2E5641", "white") cam_on = [True] def toggle_cam(e=None): cam_on[0] = not cam_on[0] if cam_on[0]: cam_canvas.delete("all") cam_canvas.create_oval(2, 2, 54, 54, fill="#2E5641", outline="") cam_canvas.create_text(28, 28, text="\U0001f4f7", fill="white", font=("Segoe UI", 16)) else: cam_canvas.delete("all") cam_canvas.create_oval(2, 2, 54, 54, fill="white", outline="") cam_canvas.create_text(28, 28, text="\U0001f4f7", fill="#1B3A2D", font=("Segoe UI", 16)) cam_canvas.bind("", toggle_cam) # Auflegen (rot, gr\u00f6\u00dfer) hangup_canvas = tk.Canvas(btn_row2, width=66, height=66, bg="#1B3A2D", highlightthickness=0, cursor="hand2") hangup_canvas.pack() hangup_canvas.create_oval(2, 2, 64, 64, fill="#E74C3C", outline="") hangup_canvas.create_text(33, 33, text="\U0001f4de", fill="white", font=("Segoe UI", 20)) def end_call(e=None): call_active[0] = False call_connected[0] = False if timer_id[0]: try: call_win.after_cancel(timer_id[0]) except Exception: pass duration = format_time(call_seconds[0]) if call_seconds[0] > 0 else "Nicht verbunden" status_label.configure(text="Anruf beendet", fg="#E74C3C") timer_label.configure(text=duration) # Anruf-Ende in Chat-Verlauf speichern now = datetime.now() end_msg = { "id": str(uuid.uuid4()), "contact": contact, "from": self.user["name"], "text": f"\U0001f4de {call_type} beendet ({duration})", "time": now.strftime("%H:%M"), "date": now.strftime("%Y-%m-%d"), "read": True, "delivered": True, } self.messages.append(end_msg) save_messages(self.messages) call_win.after(1500, call_win.destroy) hangup_canvas.bind("", end_call) tk.Label(btn_row2, text="Auflegen", bg="#1B3A2D", fg="#E74C3C", font=("Segoe UI", 9, "bold")).pack(pady=(4, 0)) # Fenster-Schlie\u00dfen = Auflegen def on_close(): if call_active[0]: end_call() else: call_win.destroy() call_win.protocol("WM_DELETE_WINDOW", on_close) # ─── Chat-Hilfsfunktionen ─── def _show_chat_menu(self, event, contact): ctx = tk.Menu(self.root, tearoff=0, font=("Segoe UI", 9)) ctx.add_command(label="\U0001f50d Suchen", command=lambda: self._search_in_chat(contact)) ctx.add_command(label="\U0001f5d1 Chat l\u00f6schen", command=lambda: self._clear_chat(contact)) ctx.add_separator() ctx.add_command(label="\u2715 Kontakt entfernen", command=lambda: self._remove_contact(contact)) ctx.post(event.x_root, event.y_root) def _search_in_chat(self, contact): query = simpledialog.askstring("Suche", f"In Chat mit {contact.split('(')[0].strip()} suchen:", parent=self.root) if not query or not query.strip(): return q = query.strip().lower() matches = [m for m in self.messages if m.get("contact") == contact and q in m.get("text", "").lower()] if not matches: messagebox.showinfo("Suche", f"Keine Ergebnisse f\u00fcr \"{query}\".", parent=self.root) else: results = "\n\n".join([f"[{m.get('time','')}] {m.get('from','')}: {m.get('text','')}" for m in matches[-10:]]) messagebox.showinfo("Suche", f"{len(matches)} Treffer:\n\n{results}", parent=self.root) def _clear_chat(self, contact): if messagebox.askyesno("Chat l\u00f6schen", f"Alle Nachrichten mit {contact.split('(')[0].strip()} l\u00f6schen?", parent=self.root): self.messages = [m for m in self.messages if m.get("contact") != contact] save_messages(self.messages) if hasattr(self, '_chat_right_col'): self._show_chat_messages(contact) if hasattr(self, '_chat_contact_inner'): self._rebuild_chat_contact_list() def _open_chat_with(self, contact): self._active_chat_contact = contact self._switch_view("chat") # ─── Kontakte verwalten ─── def _add_contact(self): name = simpledialog.askstring("Kontakt hinzuf\u00fcgen", "Name (Fachrichtung):", parent=self.root) if name and name.strip(): self.contacts.append(name.strip()) save_contacts(self.contacts) self._rebuild_contacts_sidebar() if self._current_view == "chat" and hasattr(self, '_chat_contact_inner'): self._rebuild_chat_contact_list() elif self._current_view == "colleagues": self._switch_view("colleagues") self.status_var.set(f"Kontakt hinzugef\u00fcgt: {name.strip()}") def _remove_contact(self, contact): if contact in self.contacts: if messagebox.askyesno("Kontakt entfernen", f"Kontakt entfernen?\n{contact}", parent=self.root): self.contacts.remove(contact) save_contacts(self.contacts) self._rebuild_contacts_sidebar() if self._active_chat_contact == contact: self._active_chat_contact = None if self._current_view == "chat": if hasattr(self, '_chat_contact_inner'): self._rebuild_chat_contact_list() if hasattr(self, '_chat_right_col'): self._show_empty_chat() elif self._current_view == "colleagues": self._switch_view("colleagues") # ─── Benachrichtigungen ─── def _show_notifications(self): parent = self._content_area tk.Label(parent, text="\U0001f514 Benachrichtigungen", bg=C_BG, fg=C_TEXT, font=("Segoe UI", 14, "bold")).pack(anchor="w", padx=8, pady=(0, 8)) notifs = [ ("\U0001f44d", "Dr. Schmidt hat Ihren Beitrag geliked", "vor 10 Min."), ("\U0001f4ac", "Prof. M\u00fcller hat kommentiert", "vor 1 Std."), ("\U0001f465", "Dr. Weber m\u00f6chte Sie als Kontakt hinzuf\u00fcgen", "vor 3 Std."), ("\U0001f4c5", "Kardiologie-Symposium in 2 Wochen", "heute"), ] for icon, text, time in notifs: row = tk.Frame(parent, bg=C_CARD, highlightbackground=C_BORDER, highlightthickness=1) row.pack(fill="x", pady=2, padx=4) inner = tk.Frame(row, bg=C_CARD, padx=12, pady=8) inner.pack(fill="x") tk.Label(inner, text=icon, bg=C_CARD, font=("Segoe UI", 16)).pack(side="left", padx=(0, 8)) info = tk.Frame(inner, bg=C_CARD) info.pack(side="left", fill="x", expand=True) tk.Label(info, text=text, bg=C_CARD, fg=C_TEXT, font=("Segoe UI", 10), anchor="w").pack(fill="x") tk.Label(info, text=time, bg=C_CARD, fg=C_TEXT_MUTED, font=("Segoe UI", 8), anchor="w").pack(fill="x") # ─── Events ─── def _show_events(self): parent = self._content_area tk.Label(parent, text="\U0001f4c5 Veranstaltungen", bg=C_BG, fg=C_TEXT, font=("Segoe UI", 14, "bold")).pack(anchor="w", padx=8, pady=(0, 8)) events = [ ("Kardiologie-Symposium", "15. M\u00e4rz 2026", "Universit\u00e4tsspital Z\u00fcrich", "Neue Therapieans\u00e4tze"), ("Fortbildung KI in Medizin", "22. M\u00e4rz 2026", "Online", "KI-Anwendungen in der Diagnostik"), ("Praxis-Meeting", "Jeden Montag 08:00", "Praxis Lindengut", "Wochenbesprechung"), ] for title, when, where, desc in events: card = tk.Frame(parent, bg=C_CARD, highlightbackground=C_BORDER, highlightthickness=1) card.pack(fill="x", pady=3, padx=4) inner = tk.Frame(card, bg=C_CARD, padx=12, pady=8) inner.pack(fill="x") tk.Label(inner, text=title, bg=C_CARD, fg=C_TEXT, font=("Segoe UI", 11, "bold"), anchor="w").pack(fill="x") tk.Label(inner, text=f"\U0001f4c5 {when} \u00b7 \U0001f4cd {where}", bg=C_CARD, fg=C_TEXT_LIGHT, font=("Segoe UI", 9), anchor="w").pack(fill="x") tk.Label(inner, text=desc, bg=C_CARD, fg=C_TEXT_MUTED, font=("Segoe UI", 9), anchor="w").pack(fill="x", pady=(2, 0)) # ─── Profil ─── def _edit_profile(self): dlg = tk.Toplevel(self.root) dlg.title("Profil bearbeiten") dlg.configure(bg=C_BG) dlg.geometry("380x280") dlg.attributes("-topmost", True) dlg.resizable(False, False) tk.Label(dlg, text="\U0001f464 Profil", font=("Segoe UI", 13, "bold"), bg=C_STATUS_BG, fg=C_TEXT).pack(fill="x", ipady=8) form = tk.Frame(dlg, bg=C_BG, padx=16, pady=8) form.pack(fill="x") fields = [("Name / Titel:", "name"), ("Fachrichtung:", "specialty"), ("Praxis / Klinik:", "clinic")] entries = {} for label, key in fields: tk.Label(form, text=label, font=("Segoe UI", 9, "bold"), bg=C_BG, fg=C_TEXT).pack(anchor="w", pady=(4, 0)) e = tk.Entry(form, font=("Segoe UI", 10), bg="white", fg=C_TEXT, relief="flat") e.pack(fill="x", ipady=3, pady=(0, 4)) e.insert(0, self.user.get(key, "")) entries[key] = e def do_save(): self.user["name"] = entries["name"].get().strip() or "Benutzer" self.user["specialty"] = entries["specialty"].get().strip() self.user["clinic"] = entries["clinic"].get().strip() save_user_profile(self.user) self.status_var.set(f"Profil gespeichert: {self.user['name']}") dlg.destroy() tk.Button(dlg, text="\U0001f4be Speichern", command=do_save, bg=C_BTN_PRIMARY, fg=C_BTN_FG, font=("Segoe UI", 10, "bold"), relief="flat", padx=16, pady=4, cursor="hand2").pack(pady=10) # ─── Helfer ─── def _placeholder_focus(self, entry, placeholder, focus_in): if focus_in: if entry.get() == placeholder: entry.delete(0, "end") entry.configure(fg=C_TEXT) else: if not entry.get().strip(): entry.insert(0, placeholder) entry.configure(fg=C_TEXT_MUTED) def main(): root = tk.Tk() app = MedWorkApp(root) root.mainloop() if __name__ == "__main__": main()