1439 lines
63 KiB
Python
1439 lines
63 KiB
Python
|
|
# -*- 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("<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()
|