Files
aza/AzA march 2026 - Kopie (18)/aza_docapp.py
2026-04-22 22:33:46 +02:00

1439 lines
63 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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 (520pt), 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("<Button-1>", lambda e: _apply(_size[0] + 1))
btn_down.bind("<Button-1>", lambda e: _apply(_size[0] - 1))
for w in (btn_up, btn_down):
w.bind("<Enter>", lambda e, ww=w: ww.configure(fg=_fg_hover))
w.bind("<Leave>", 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("<Enter>", lambda e, bt=b: bt.configure(bg=C_HEADER_HOVER))
b.bind("<Leave>", 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("<Enter>", lambda e, bt=b: bt.configure(bg=C_CARD))
b.bind("<Leave>", 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("<Enter>", lambda e, bt=b: bt.configure(bg=C_CARD))
b.bind("<Leave>", 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("<FocusIn>", lambda e: self._placeholder_focus(self._post_entry, "Gedanken, Fragen oder F\u00e4lle teilen\u2026", True))
self._post_entry.bind("<FocusOut>", 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("<Configure>", 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("<Configure>", lambda e: canvas.itemconfig("fw", width=e.width))
canvas.bind("<MouseWheel>", 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("<Button-1>", 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("<Configure>",
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("<Configure>",
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("<MouseWheel>", _cl_mw)
contact_inner.bind("<MouseWheel>", _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("<Button-1>", select)
widget.bind("<MouseWheel>", self._chat_contact_mw)
for child in widget.winfo_children():
child.bind("<Button-1>", select)
child.bind("<MouseWheel>", 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("<Enter>", on_enter)
row.bind("<Leave>", 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("<Button-3>", 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("<Button-1>", lambda e: self._start_call(contact))
phone_btn.bind("<Enter>", lambda e: phone_btn.configure(fg="#A8D5CF"))
phone_btn.bind("<Leave>", 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("<Button-1>", lambda e: self._start_call(contact, video=True))
video_btn.bind("<Enter>", lambda e: video_btn.configure(fg="#A8D5CF"))
video_btn.bind("<Leave>", 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("<Button-1>", 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("<Button-1>", 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("<Configure>",
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("<Configure>",
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("<MouseWheel>", _chat_mw)
chat_inner.bind("<MouseWheel>", _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("<Return>", send_msg)
send_btn.bind("<Button-1>", 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("<MouseWheel>", self._chat_mw_func)
badge.bind("<MouseWheel>", 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("<MouseWheel>", self._chat_mw_func)
for child in w.winfo_children():
child.bind("<MouseWheel>", 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("<Button-1>", 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("<Button-1>", 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("<Button-1>", 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("<Button-1>", 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()