Files
aza/AzA march 2026 - Kopie (16)/aza_todo_mixin.py

2733 lines
127 KiB
Python
Raw Normal View History

2026-04-19 20:41:37 +02:00
# -*- coding: utf-8 -*-
"""
TodoMixin _open_todo_window Methode als Mixin-Klasse.
"""
import os
import tkinter as tk
from tkinter import ttk, messagebox
from tkinter.scrolledtext import ScrolledText
from datetime import datetime
import threading
import json
import re
from aza_persistence import (
load_todos,
save_todos,
load_notes,
save_notes,
load_checklists,
save_checklists,
load_todo_geometry,
save_todo_geometry,
load_todo_inbox,
save_todo_inbox,
send_todo_to_inbox,
cloud_pull_todos,
cloud_pull_notes,
cloud_get_status,
extract_date_from_todo_text,
load_user_profile,
load_todo_settings,
save_todo_settings,
)
from aza_ui_helpers import (
add_tooltip,
center_window,
add_resize_grip,
add_text_font_size_control,
setup_window_geometry_saving,
apply_initial_scaling_to_window,
load_text_font_size,
save_text_font_size,
)
from aza_audio import AudioRecorder
class TodoMixin:
"""Mixin-Klasse für das To-Do-Fenster."""
def _open_todo_window(self):
"""Öffnet ein Todoist-ähnliches To-do Fenster mit Diktat, Kalender und Überfällig-Markierung."""
from datetime import datetime, date, timedelta
import calendar as cal_mod
TODO_MIN_W, TODO_MIN_H = 500, 560
win = tk.Toplevel(self)
win.title("To-do")
win.minsize(TODO_MIN_W, TODO_MIN_H)
win.configure(bg="#E8F4FA")
win.attributes("-topmost", True)
self._register_window(win)
saved_geom = load_todo_geometry()
if saved_geom:
# Gespeicherte Grösse übernehmen, aber immer zentrieren
try:
size_part = saved_geom.split("+")[0]
w_saved, h_saved = [int(x) for x in size_part.split("x")]
except (ValueError, IndexError):
w_saved, h_saved = TODO_MIN_W, TODO_MIN_H
win.geometry(f"{w_saved}x{h_saved}")
center_window(win, w_saved, h_saved)
else:
win.geometry(f"{TODO_MIN_W}x{TODO_MIN_H}")
center_window(win, TODO_MIN_W, TODO_MIN_H)
_geom_after = [None]
def _save_geom(event=None):
if _geom_after[0]:
win.after_cancel(_geom_after[0])
_geom_after[0] = win.after(400, lambda: save_todo_geometry(win.geometry()))
win.bind("<Configure>", _save_geom)
# Aufnahme-State
todo_recorder = [None]
is_recording = [False]
def on_close():
if is_recording[0] and todo_recorder[0]:
try:
is_recording[0] = False
wav_path = todo_recorder[0].stop_and_save_wav()
if os.path.exists(wav_path):
os.remove(wav_path)
except Exception:
pass
todo_recorder[0] = None
if _notes_is_recording[0] and _notes_recorder[0]:
try:
_notes_is_recording[0] = False
wav_path = _notes_recorder[0].stop_and_save_wav()
if os.path.exists(wav_path):
os.remove(wav_path)
except Exception:
pass
_notes_recorder[0] = None
try:
save_todo_geometry(win.geometry())
except Exception:
pass
try:
save_todo_settings({
"active_tab": _active_tab[0],
"active_category": _active_category[0],
"custom_categories": _custom_categories,
})
except Exception:
pass
if hasattr(self, "_aza_windows"):
self._aza_windows.discard(win)
win.destroy()
win.protocol("WM_DELETE_WINDOW", on_close)
todos = load_todos()
todo_widgets = []
# Inbox prüfen: empfangene Aufgaben importieren
inbox = load_todo_inbox()
my_name = self._user_profile.get("name", "")
imported_count = 0
remaining_inbox = []
for inbox_item in inbox:
if inbox_item.get("recipient", "") == my_name or not inbox_item.get("recipient"):
new_todo = {
"id": inbox_item.get("id", int(datetime.now().timestamp() * 1000)),
"text": inbox_item.get("text", ""),
"done": False,
"date": inbox_item.get("date"),
"priority": inbox_item.get("priority", 0),
"notes": inbox_item.get("notes", ""),
"created": inbox_item.get("sent_at", datetime.now().isoformat()),
"sender": inbox_item.get("sender", ""),
}
todos.append(new_todo)
imported_count += 1
else:
remaining_inbox.append(inbox_item)
if imported_count > 0:
save_todo_inbox(remaining_inbox)
save_todos(todos)
# ─── Header ───
header = tk.Frame(win, bg="#B9ECFA")
header.pack(fill="x")
tk.Label(header, text="📋 To-do Liste", font=("Segoe UI", 14, "bold"),
bg="#B9ECFA", fg="#1a4d6d").pack(side="left", padx=12, pady=8)
_todo_minimized = [False]
_todo_geom_before = [None]
_todo_restoring = [False]
def _restore_todo_content():
"""Inhalt wiederherstellen (aus minimiertem Zustand)."""
if not _todo_minimized[0]:
return
_todo_restoring[0] = True
counter_bar.pack(fill="x", after=header)
tab_bar.pack(fill="x", after=counter_bar)
_all_pages[_active_tab[0]].pack(fill="both", expand=True, after=tab_bar)
btn_minimize_todo.configure(text="")
_todo_minimized[0] = False
win.minsize(500, 560)
win.after(200, lambda: _todo_restoring.__setitem__(0, False))
def _toggle_minimize_todo():
if _todo_minimized[0]:
_restore_todo_content()
if _todo_geom_before[0]:
try:
win.geometry(_todo_geom_before[0])
except Exception:
pass
else:
_todo_geom_before[0] = win.geometry()
for key, page in _all_pages.items():
page.pack_forget()
tab_bar.pack_forget()
counter_bar.pack_forget()
btn_minimize_todo.configure(text="")
_todo_minimized[0] = True
win.minsize(220, 50)
w_cur = win.winfo_width()
win.geometry(f"{w_cur}x50")
def _on_todo_configure(e):
if e.widget is not win:
return
if _todo_minimized[0] and not _todo_restoring[0] and e.height > 80:
_restore_todo_content()
win.bind("<Configure>", _on_todo_configure, add="+")
btn_minimize_todo = tk.Label(header, text="", font=("Segoe UI", 14, "bold"),
bg="#B9ECFA", fg="#5A90B0", cursor="hand2", padx=6)
btn_minimize_todo.pack(side="right", padx=(0, 8))
btn_minimize_todo.bind("<Button-1>", lambda e: _toggle_minimize_todo())
btn_minimize_todo.bind("<Enter>", lambda e: btn_minimize_todo.configure(fg="#1a4d6d"))
btn_minimize_todo.bind("<Leave>", lambda e: btn_minimize_todo.configure(fg="#5A90B0"))
win._aza_minimize = _toggle_minimize_todo
win._aza_is_minimized = lambda: _todo_minimized[0]
if hasattr(self, "_aza_windows"):
self._aza_windows.add(win)
todo_font_size_key = "todo_list"
todo_font_size = [load_text_font_size(todo_font_size_key, 11)]
_fs_fg = "#8AAFC0"
_fs_fg_hover = "#1a4d6d"
font_ctrl = tk.Frame(header, bg="#B9ECFA", highlightthickness=0, bd=0)
font_ctrl.pack(side="right", padx=(0, 4), pady=6)
tk.Label(font_ctrl, text="Aa", font=("Segoe UI", 8),
bg="#B9ECFA", fg=_fs_fg).pack(side="left", padx=(0, 1))
_fs_size_lbl = tk.Label(font_ctrl, text=str(todo_font_size[0]),
font=("Segoe UI", 8), bg="#B9ECFA", fg=_fs_fg,
width=2, anchor="center")
_fs_size_lbl.pack(side="left")
def _update_todo_font(new_size):
new_size = max(7, min(16, new_size))
todo_font_size[0] = new_size
_fs_size_lbl.configure(text=str(new_size))
save_text_font_size(todo_font_size_key, new_size)
rebuild_list()
_fs_btn_up = tk.Label(font_ctrl, text="\u25B2", font=("Segoe UI", 7),
bg="#B9ECFA", fg=_fs_fg, cursor="hand2",
bd=0, highlightthickness=0, padx=0, pady=0)
_fs_btn_up.pack(side="left", padx=(2, 0))
_fs_btn_down = tk.Label(font_ctrl, text="\u25BC", font=("Segoe UI", 7),
bg="#B9ECFA", fg=_fs_fg, cursor="hand2",
bd=0, highlightthickness=0, padx=0, pady=0)
_fs_btn_down.pack(side="left", padx=(0, 0))
_fs_btn_up.bind("<Button-1>", lambda e: _update_todo_font(todo_font_size[0] + 1))
_fs_btn_down.bind("<Button-1>", lambda e: _update_todo_font(todo_font_size[0] - 1))
for _fsw in (_fs_btn_up, _fs_btn_down):
_fsw.bind("<Enter>", lambda e, ww=_fsw: ww.configure(fg=_fs_fg_hover))
_fsw.bind("<Leave>", lambda e, ww=_fsw: ww.configure(fg=_fs_fg))
# Info-Button (ⓘ) für Diktat-Datumserkennung
_info_popup = [None]
def _show_diktat_info(event=None):
if _info_popup[0] and _info_popup[0].winfo_exists():
_info_popup[0].lift()
return
tip = tk.Toplevel(win)
tip.title("🎤 Diktat-Datumserkennung")
tip.attributes("-topmost", True)
tip.configure(bg="#E8F4FA")
tip.resizable(False, False)
self._register_window(tip)
def _close_info():
_info_popup[0] = None
tip.destroy()
tip.protocol("WM_DELETE_WINDOW", _close_info)
tip_header = tk.Frame(tip, bg="#B9ECFA")
tip_header.pack(fill="x")
tk.Label(tip_header, text="🎤 Datumserkennung beim Diktieren",
font=("Segoe UI", 10, "bold"), bg="#B9ECFA", fg="#1a4d6d",
anchor="w").pack(side="left", padx=10, pady=6)
close_lbl = tk.Label(tip_header, text="", font=("Segoe UI", 11, "bold"),
bg="#B9ECFA", fg="#999", cursor="hand2")
close_lbl.pack(side="right", padx=6, pady=6)
close_lbl.bind("<Button-1>", lambda e: _close_info())
close_lbl.bind("<Enter>", lambda e: close_lbl.configure(fg="#E87070"))
close_lbl.bind("<Leave>", lambda e: close_lbl.configure(fg="#999"))
inner = tk.Frame(tip, bg="white", padx=12, pady=8)
inner.pack(fill="both", expand=True, padx=1, pady=(0, 1))
tk.Label(inner, text="Datum im Diktat wird automatisch erkannt und gesetzt:",
font=("Segoe UI", 8), bg="white", fg="#666",
anchor="w").pack(fill="x", pady=(0, 6))
table = tk.Frame(inner, bg="white")
table.pack(fill="x")
headers = ["Beispiel-Diktat", "Erkanntes Datum"]
for c, h in enumerate(headers):
tk.Label(table, text=h, font=("Segoe UI", 8, "bold"), bg="#E8F4FA",
fg="#1a4d6d", padx=6, pady=2, anchor="w").grid(row=0, column=c, sticky="ew", padx=1, pady=1)
examples = [
("«…bis morgen»", "morgen"),
("«…bis nächsten Mittwoch»", "nächster Mi"),
("«…bis 20. März»", "20.03."),
("«…in 3 Tagen»", "+3 Tage"),
("«…bis Ende Woche»", "Freitag"),
("«…bis Ende Monat»", "Monatsende"),
("«…am 15.4.»", "15.04."),
("«…übermorgen»", "übermorgen"),
("«…in einer Woche»", "+1 Woche"),
("«…in zwei Monaten»", "+2 Monate"),
]
for r, (ex, dt_txt) in enumerate(examples, start=1):
bg_r = "#F5FCFF" if r % 2 == 0 else "white"
tk.Label(table, text=ex, font=("Segoe UI", 8), bg=bg_r,
fg="#1a4d6d", padx=6, pady=1, anchor="w").grid(row=r, column=0, sticky="ew", padx=1)
tk.Label(table, text=dt_txt, font=("Segoe UI", 8, "bold"), bg=bg_r,
fg="#5B8DB3", padx=6, pady=1, anchor="w").grid(row=r, column=1, sticky="ew", padx=1)
tip.update_idletasks()
x = win.winfo_rootx() + info_btn.winfo_x()
y = win.winfo_rooty() + info_btn.winfo_y() + info_btn.winfo_height() + 4
tip.geometry(f"+{max(0, x)}+{y}")
_info_popup[0] = tip
info_btn = tk.Label(header, text="", font=("Segoe UI", 11, "bold"),
bg="#7EC8E3", fg="white", cursor="hand2",
relief="flat", bd=0, padx=4, pady=2)
info_btn.pack(side="right", padx=(0, 8), pady=8)
info_btn.bind("<Button-1>", _show_diktat_info)
info_btn.bind("<Enter>", lambda e: info_btn.configure(bg="#5B8DB3"))
info_btn.bind("<Leave>", lambda e: info_btn.configure(bg="#7EC8E3"))
# 📱 iPhone-App-Button (startet Webserver + zeigt QR-Code)
_todo_server_thread = [None]
_todo_server_running = [False]
def _start_todo_server():
if _todo_server_running[0]:
_show_qr_window()
return
try:
from todo_server import _get_local_ip, PORT, TodoHandler, TodoServer, _add_firewall_rule, _supabase_push
import urllib.request
local_ip = _get_local_ip()
url = f"http://{local_ip}:{PORT}"
# Todos nach Supabase pushen
try:
_supabase_push(todos)
except Exception:
pass
# Firewall-Regel
_add_firewall_rule()
_server_ref = [None]
_server_error = [None]
_server_ready = threading.Event()
def run_server():
try:
server = TodoServer(("0.0.0.0", PORT), TodoHandler)
_server_ref[0] = server
_server_ready.set()
server.serve_forever()
except Exception as e:
_server_error[0] = str(e)
_server_ready.set()
t = threading.Thread(target=run_server, daemon=True)
t.start()
_todo_server_thread[0] = t
_server_ready.wait(timeout=3)
if _server_error[0]:
messagebox.showerror(
"Server-Fehler",
f"Server konnte nicht starten:\n{_server_error[0]}\n\n"
f"Evtl. Port {PORT} belegt Programm neu starten.",
parent=win)
return
test_ok = False
try:
resp = urllib.request.urlopen(f"http://localhost:{PORT}/", timeout=3)
if resp.status == 200:
test_ok = True
except Exception:
pass
if not test_ok:
messagebox.showerror(
"Server-Fehler",
"Server gestartet, antwortet aber nicht.\n"
"Bitte Programm neu starten und erneut versuchen.",
parent=win)
return
_todo_server_running[0] = True
phone_btn.configure(bg="#5BDB7B")
win.after(300, lambda: _show_qr_window(url))
except Exception as e:
messagebox.showerror(
"Fehler beim Starten",
f"Der To-Do-Server konnte nicht gestartet werden.\n\n"
f"Fehler: {e}",
parent=win)
def _show_qr_window(url=None):
if url is None:
try:
from todo_server import _get_local_ip, PORT
url = f"http://{_get_local_ip()}:{PORT}"
except Exception:
return
try:
qr_win = tk.Toplevel(win)
qr_win.title("📱 iPhone To-Do App")
qr_win.attributes("-topmost", True)
qr_win.configure(bg="#E8F4FA")
qr_win.resizable(False, False)
qr_win.geometry("360x660")
center_window(qr_win, 360, 660)
self._register_window(qr_win)
tk.Label(qr_win, text="📱 AzA To-Do iPhone App", font=("Segoe UI", 14, "bold"),
bg="#B9ECFA", fg="#1a4d6d").pack(fill="x", ipady=10)
# Cloud-Status
cloud_frame = tk.Frame(qr_win, bg="#EDF7ED", padx=10, pady=4)
cloud_frame.pack(fill="x", padx=16, pady=(8, 0))
tk.Label(cloud_frame,
text="☁ Supabase Cloud-Sync aktiv App funktioniert weltweit!",
font=("Segoe UI", 9, "bold"),
bg="#EDF7ED", fg="#2A7A2A").pack(anchor="w")
tk.Label(cloud_frame,
text="Die App synchronisiert automatisch, egal ob\n"
"WLAN, 5G, unterwegs von überall auf der Welt.\n"
"Kein laufender PC nötig!",
font=("Segoe UI", 8), bg="#EDF7ED", fg="#2A7A2A",
justify="left").pack(anchor="w")
tk.Label(qr_win, text="Schritt 1: QR-Code scannen (einmalig, im WLAN):",
font=("Segoe UI", 10, "bold"), bg="#E8F4FA", fg="#1a4d6d"
).pack(fill="x", padx=16, pady=(10, 4))
qr_frame = tk.Frame(qr_win, bg="white", padx=10, pady=10)
qr_frame.pack(pady=4)
qr_generated = False
try:
import qrcode
from PIL import ImageTk
img = qrcode.make(url, box_size=6, border=2)
img_tk = ImageTk.PhotoImage(img)
qr_label = tk.Label(qr_frame, image=img_tk, bg="white")
qr_label.image = img_tk
qr_label.pack()
qr_generated = True
except Exception:
pass
if not qr_generated:
tk.Label(qr_frame,
text="QR-Code nicht verfügbar.\n\n"
"Bitte installieren:\n"
"py -3.11 -m pip install qrcode[pil]\n\n"
"Alternativ diese URL im\n"
"iPhone-Browser öffnen:",
font=("Segoe UI", 9), bg="white", fg="#C03030",
justify="center").pack(padx=20, pady=10)
url_label = tk.Label(qr_win, text=url, font=("Segoe UI", 11, "bold"),
bg="#E8F4FA", fg="#5B8DB3", cursor="hand2")
url_label.pack(pady=2)
tk.Label(qr_win, text="Schritt 2: Auf dem iPhone → Teilen → Zum\n"
"Home-Bildschirm hinzufügen (AzA-Logo erscheint!)",
font=("Segoe UI", 9, "bold"), bg="#E8F4FA", fg="#1a4d6d",
justify="center").pack(padx=16, pady=(4, 2))
tk.Label(qr_win, text="Danach funktioniert die App eigenständig \n"
"auch ohne WLAN, von überall auf der Welt!",
font=("Segoe UI", 8), bg="#E8F4FA", fg="#4a8aaa",
justify="center").pack(padx=16, pady=(0, 4))
# Server-Status
status_bg = "#EDF7ED" if _todo_server_running[0] else "#FDECEC"
status_fg = "#2A7A2A" if _todo_server_running[0] else "#C03030"
status_txt = "● Server läuft (für Ersteinrichtung)" if _todo_server_running[0] \
else "● Server nicht gestartet"
tk.Label(qr_win, text=status_txt, font=("Segoe UI", 9, "bold"),
bg=status_bg, fg=status_fg).pack(fill="x", padx=16, ipady=3, pady=(2, 0))
# Firewall-Hilfe
help_frame = tk.Frame(qr_win, bg="#FFF8E0", padx=10, pady=3)
help_frame.pack(fill="x", padx=16, pady=(4, 0))
tk.Label(help_frame,
text="Falls iPhone nicht verbindet: Firewall öffnen\n"
"→ Python bei Privat + Öffentlich anhaken",
font=("Segoe UI", 7), bg="#FFF8E0", fg="#806000",
justify="left").pack(anchor="w")
def _open_firewall_settings():
try:
os.startfile("firewall.cpl")
except Exception:
pass
def _open_in_browser():
import webbrowser
webbrowser.open(url)
def _close_qr():
qr_win.destroy()
btn_row = tk.Frame(qr_win, bg="#E8F4FA")
btn_row.pack(pady=6)
tk.Button(btn_row, text="🔥 Firewall", font=("Segoe UI", 8),
bg="#F0D060", fg="#604000", activebackground="#E0C050",
relief="flat", padx=8, pady=2, cursor="hand2",
command=_open_firewall_settings).pack(side="left", padx=3)
tk.Button(btn_row, text="🌐 Browser testen", font=("Segoe UI", 8),
bg="#7EC8E3", fg="#1a4d6d", activebackground="#6CB8D3",
relief="flat", padx=8, pady=2, cursor="hand2",
command=_open_in_browser).pack(side="left", padx=3)
tk.Button(btn_row, text="Schliessen", font=("Segoe UI", 8),
bg="#C8DDE6", fg="#1a4d6d", activebackground="#B8CDD6",
relief="flat", padx=8, pady=2, cursor="hand2",
command=_close_qr).pack(side="left", padx=3)
except Exception as e:
messagebox.showerror("Fehler", f"QR-Fenster konnte nicht geöffnet werden:\n{e}", parent=win)
phone_btn = tk.Label(header, text=" 📱 ", font=("Segoe UI", 11, "bold"),
bg="#7EC8E3", fg="white", cursor="hand2",
relief="flat", bd=0, padx=4, pady=2)
phone_btn.pack(side="right", padx=(0, 4), pady=8)
phone_btn.bind("<Button-1>", lambda e: _start_todo_server())
phone_btn.bind("<Enter>", lambda e: phone_btn.configure(
bg="#5BDB7B" if _todo_server_running[0] else "#5B8DB3"))
phone_btn.bind("<Leave>", lambda e: phone_btn.configure(
bg="#5BDB7B" if _todo_server_running[0] else "#7EC8E3"))
counter_bar = tk.Frame(win, bg="#B9ECFA")
counter_bar.pack(fill="x")
counter_label = tk.Label(counter_bar, text="", font=("Segoe UI", 9),
bg="#B9ECFA", fg="#4a8aaa")
counter_label.pack(side="left", padx=12, pady=(0, 4))
# ─── Gespeicherte Todo-Einstellungen laden ───
_todo_settings = load_todo_settings()
_saved_tab = _todo_settings.get("active_tab", "todo")
_saved_category = _todo_settings.get("active_category", "Allgemein")
_custom_categories = _todo_settings.get("custom_categories", [])
_DEFAULT_CATEGORIES = ["Allgemein", "MPA", "Ärzte"]
def _build_category_list():
return _DEFAULT_CATEGORIES + _custom_categories + ["Alle"]
# ─── Tab-Leiste (Aufgaben / Notizen / Checkliste) ───
tab_bar = tk.Frame(win, bg="#A8D8E8")
tab_bar.pack(fill="x")
_active_tab = [_saved_tab if _saved_tab in ("todo", "notes", "checklist") else "todo"]
todo_page = tk.Frame(win, bg="#E8F4FA")
notes_page = tk.Frame(win, bg="#E8F4FA")
checklist_page = tk.Frame(win, bg="#E8F4FA")
_tab_btn_style_active = {"bg": "#E8F4FA", "fg": "#1a4d6d", "font": ("Segoe UI", 10, "bold")}
_tab_btn_style_inactive = {"bg": "#A8D8E8", "fg": "#5A90B0", "font": ("Segoe UI", 10)}
tab_btn_todo = tk.Label(tab_bar, text="📋 Aufgaben", cursor="hand2",
padx=16, pady=6, **(_tab_btn_style_active if _active_tab[0] == "todo" else _tab_btn_style_inactive))
tab_btn_todo.pack(side="left")
tab_btn_notes = tk.Label(tab_bar, text="📝 Notizen", cursor="hand2",
padx=16, pady=6, **(_tab_btn_style_active if _active_tab[0] == "notes" else _tab_btn_style_inactive))
tab_btn_notes.pack(side="left")
tab_btn_checklist = tk.Label(tab_bar, text="✅ Checkliste", cursor="hand2",
padx=16, pady=6, **(_tab_btn_style_active if _active_tab[0] == "checklist" else _tab_btn_style_inactive))
tab_btn_checklist.pack(side="left")
_all_tab_btns = {"todo": tab_btn_todo, "notes": tab_btn_notes, "checklist": tab_btn_checklist}
_all_pages = {"todo": todo_page, "notes": notes_page, "checklist": checklist_page}
def _switch_tab(tab_name):
_active_tab[0] = tab_name
for key, btn in _all_tab_btns.items():
btn.configure(**((_tab_btn_style_active if key == tab_name else _tab_btn_style_inactive)))
for key, page in _all_pages.items():
page.pack_forget()
_all_pages[tab_name].pack(fill="both", expand=True)
if tab_name == "notes":
_rebuild_notes_list()
elif tab_name == "checklist":
_rebuild_checklist()
tab_btn_todo.bind("<Button-1>", lambda e: _switch_tab("todo"))
tab_btn_notes.bind("<Button-1>", lambda e: _switch_tab("notes"))
tab_btn_checklist.bind("<Button-1>", lambda e: _switch_tab("checklist"))
_all_pages[_active_tab[0]].pack(fill="both", expand=True)
_need_initial_rebuild = [_active_tab[0]]
# ─── Aufgabenbereiche (Kategorie-Filter) ───
_TODO_CATEGORIES = _build_category_list()
_initial_cat = _saved_category if _saved_category in _TODO_CATEGORIES else "Allgemein"
_active_category = [_initial_cat]
cat_bar = tk.Frame(todo_page, bg="#D4EEF7", pady=2, padx=8)
cat_bar.pack(fill="x")
tk.Label(cat_bar, text="Bereich:", font=("Segoe UI", 8),
bg="#D4EEF7", fg="#5A90B0").pack(side="left", padx=(0, 4))
_cat_btns_frame = tk.Frame(cat_bar, bg="#D4EEF7")
_cat_btns_frame.pack(side="left", fill="x", expand=True)
_cat_btns = {}
_cat_active_style = {"bg": "#5B8DB3", "fg": "white"}
_cat_inactive_style = {"bg": "#C8DDE6", "fg": "#1a4d6d"}
def _set_category_filter(cat):
_active_category[0] = cat
for c, b in _cat_btns.items():
if c == cat:
b.configure(**_cat_active_style)
else:
b.configure(**_cat_inactive_style)
rebuild_list()
def _rebuild_cat_buttons():
for w in _cat_btns_frame.winfo_children():
w.destroy()
_cat_btns.clear()
_TODO_CATEGORIES.clear()
_TODO_CATEGORIES.extend(_build_category_list())
if _active_category[0] not in _TODO_CATEGORIES:
_active_category[0] = "Allgemein"
for cat in _TODO_CATEGORIES:
style = _cat_active_style if cat == _active_category[0] else _cat_inactive_style
b = tk.Label(_cat_btns_frame, text=cat, font=("Segoe UI", 8),
cursor="hand2", padx=8, pady=2, relief="flat", bd=0,
**style)
b.pack(side="left", padx=1)
b.bind("<Button-1>", lambda e, c=cat: _set_category_filter(c))
_cat_btns[cat] = b
_update_cat_combobox()
def _add_custom_category():
dlg = tk.Toplevel(win)
dlg.title("Neuen Bereich erstellen")
dlg.configure(bg="#E8F4FA")
dlg.resizable(False, False)
dlg.attributes("-topmost", True)
dlg.grab_set()
center_window(dlg, 320, 130)
self._register_window(dlg)
tk.Label(dlg, text="Name des neuen Bereichs:",
font=("Segoe UI", 10), bg="#E8F4FA", fg="#1a4d6d").pack(padx=16, pady=(12, 4))
name_var = tk.StringVar()
name_entry = tk.Entry(dlg, textvariable=name_var, font=("Segoe UI", 10),
bg="white", fg="#1a4d6d", relief="flat", bd=0,
insertbackground="#1a4d6d")
name_entry.pack(padx=16, fill="x", ipady=4)
name_entry.focus_set()
btn_row = tk.Frame(dlg, bg="#E8F4FA")
btn_row.pack(pady=10)
def _do_add():
name = name_var.get().strip()
if not name:
return
all_cats = _DEFAULT_CATEGORIES + _custom_categories + ["Alle"]
if name in all_cats:
messagebox.showwarning("Bereich existiert",
f"Der Bereich «{name}» existiert bereits.",
parent=dlg)
return
_custom_categories.append(name)
_rebuild_cat_buttons()
_set_category_filter(name)
dlg.destroy()
name_entry.bind("<Return>", lambda e: _do_add())
tk.Button(btn_row, text="Erstellen", font=("Segoe UI", 9, "bold"),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
activeforeground="white", relief="flat", bd=0,
padx=12, pady=2, cursor="hand2",
command=_do_add).pack(side="left", padx=4)
tk.Button(btn_row, text="Abbrechen", font=("Segoe UI", 9),
bg="#C8DDE6", fg="#1a4d6d", activebackground="#B8CDD6",
relief="flat", bd=0, padx=12, pady=2, cursor="hand2",
command=dlg.destroy).pack(side="left", padx=4)
def _remove_custom_category():
removable = [c for c in _custom_categories]
if not removable:
messagebox.showinfo("Keine eigenen Bereiche",
"Es gibt keine selbst erstellten Bereiche zum Entfernen.\n"
"Die Standardbereiche (Allgemein, MPA, Ärzte, Alle) können nicht entfernt werden.",
parent=win)
return
dlg = tk.Toplevel(win)
dlg.title("Bereich entfernen")
dlg.configure(bg="#E8F4FA")
dlg.resizable(False, False)
dlg.attributes("-topmost", True)
dlg.grab_set()
center_window(dlg, 320, 160)
self._register_window(dlg)
tk.Label(dlg, text="Welchen Bereich möchten Sie entfernen?",
font=("Segoe UI", 10), bg="#E8F4FA", fg="#1a4d6d").pack(padx=16, pady=(12, 4))
sel_var = tk.StringVar(value=removable[0])
combo = ttk.Combobox(dlg, textvariable=sel_var, values=removable,
state="readonly", font=("Segoe UI", 10), width=20)
combo.pack(padx=16, pady=4)
btn_row = tk.Frame(dlg, bg="#E8F4FA")
btn_row.pack(pady=10)
def _do_remove():
name = sel_var.get()
if not name:
return
confirm = messagebox.askyesno(
"Bereich entfernen",
f"Möchten Sie den Bereich «{name}» wirklich entfernen?\n\n"
f"Die Aufgaben in diesem Bereich werden nicht gelöscht,\n"
f"sondern in «Allgemein» verschoben.",
parent=dlg)
if not confirm:
return
_custom_categories.remove(name)
for todo in todos:
if todo.get("category") == name:
todo["category"] = "Allgemein"
save_todos(todos)
if _active_category[0] == name:
_active_category[0] = "Allgemein"
_rebuild_cat_buttons()
rebuild_list()
dlg.destroy()
tk.Button(btn_row, text="Entfernen", font=("Segoe UI", 9, "bold"),
bg="#E87070", fg="white", activebackground="#D06060",
activeforeground="white", relief="flat", bd=0,
padx=12, pady=2, cursor="hand2",
command=_do_remove).pack(side="left", padx=4)
tk.Button(btn_row, text="Abbrechen", font=("Segoe UI", 9),
bg="#C8DDE6", fg="#1a4d6d", activebackground="#B8CDD6",
relief="flat", bd=0, padx=12, pady=2, cursor="hand2",
command=dlg.destroy).pack(side="left", padx=4)
_cat_pm_frame = tk.Frame(cat_bar, bg="#D4EEF7")
_cat_pm_frame.pack(side="right")
_plus_btn = tk.Label(_cat_pm_frame, text="", font=("Segoe UI", 9, "bold"),
bg="#D4EEF7", fg="#5B8DB3", cursor="hand2", padx=2)
_plus_btn.pack(side="left")
_plus_btn.bind("<Button-1>", lambda e: _add_custom_category())
_plus_btn.bind("<Enter>", lambda e: _plus_btn.configure(fg="#1a4d6d"))
_plus_btn.bind("<Leave>", lambda e: _plus_btn.configure(fg="#5B8DB3"))
_minus_btn = tk.Label(_cat_pm_frame, text="", font=("Segoe UI", 9, "bold"),
bg="#D4EEF7", fg="#5B8DB3", cursor="hand2", padx=2)
_minus_btn.pack(side="left")
_minus_btn.bind("<Button-1>", lambda e: _remove_custom_category())
_minus_btn.bind("<Enter>", lambda e: _minus_btn.configure(fg="#E87070"))
_minus_btn.bind("<Leave>", lambda e: _minus_btn.configure(fg="#5B8DB3"))
# ─── Eingabebereich ───
input_frame = tk.Frame(todo_page, bg="#D4EEF7", pady=8, padx=8)
input_frame.pack(fill="x")
# Zeile 1: Textfeld + Aufnahme-Button
entry_frame = tk.Frame(input_frame, bg="#D4EEF7")
entry_frame.pack(fill="x")
entry_var = tk.StringVar()
entry = tk.Entry(entry_frame, textvariable=entry_var, font=("Segoe UI", 11),
bg="white", fg="#1a4d6d", relief="flat", bd=0,
insertbackground="#1a4d6d")
entry.pack(side="left", fill="x", expand=True, ipady=6, padx=(0, 6))
entry.insert(0, "Neue Aufgabe eingeben oder diktieren…")
entry.configure(fg="#999")
def on_entry_focus_in(e):
if entry.get() == "Neue Aufgabe eingeben oder diktieren…":
entry.delete(0, "end")
entry.configure(fg="#1a4d6d")
def on_entry_focus_out(e):
if not entry.get().strip():
entry.insert(0, "Neue Aufgabe eingeben oder diktieren…")
entry.configure(fg="#999")
entry.bind("<FocusIn>", on_entry_focus_in)
entry.bind("<FocusOut>", on_entry_focus_out)
# Aufnahme-Button
rec_btn = tk.Button(entry_frame, text="", font=("Segoe UI", 14),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
activeforeground="white", relief="flat", bd=0,
width=3, cursor="hand2")
rec_btn.pack(side="right")
rec_status_var = tk.StringVar(value="")
rec_status_label = tk.Label(input_frame, textvariable=rec_status_var,
font=("Segoe UI", 8), bg="#D4EEF7", fg="#5B8DB3")
rec_status_label.pack(fill="x")
def toggle_todo_record():
if not self.ensure_ready():
return
if not is_recording[0]:
rec = AudioRecorder()
todo_recorder[0] = rec
try:
rec.start()
is_recording[0] = True
rec_btn.configure(text="", bg="#C03030", activebackground="#A02020")
rec_status_var.set("🔴 Aufnahme läuft… Klicken zum Stoppen.")
except Exception as e:
messagebox.showerror("Aufnahme-Fehler", str(e), parent=win)
rec_status_var.set("")
else:
is_recording[0] = False
rec = todo_recorder[0]
rec_btn.configure(text="", bg="#5B8DB3", activebackground="#4A7A9E")
rec_status_var.set("Transkribiere…")
def worker():
try:
wav_path = rec.stop_and_save_wav()
import wave
try:
with wave.open(wav_path, 'rb') as wf:
dur = wf.getnframes() / float(wf.getframerate())
if dur < 0.3:
if os.path.exists(wav_path):
os.remove(wav_path)
todo_recorder[0] = None
self.after(0, lambda: rec_status_var.set("Kein Audio erkannt."))
return
except Exception:
pass
transcript_result = self.transcribe_wav(wav_path)
if hasattr(transcript_result, 'text'):
text = transcript_result.text
elif isinstance(transcript_result, str):
text = transcript_result
else:
text = ""
try:
if os.path.exists(wav_path):
os.remove(wav_path)
except Exception:
pass
if not text or not text.strip():
todo_recorder[0] = None
self.after(0, lambda: rec_status_var.set("Kein Text erkannt."))
return
text = self._diktat_apply_punctuation(text)
cleaned_text, extracted_date = extract_date_from_todo_text(text)
def _done(ct=cleaned_text, ed=extracted_date):
todo_recorder[0] = None
current = entry_var.get()
if current == "Neue Aufgabe eingeben oder diktieren…":
current = ""
new_text = (current + " " + ct).strip() if current.strip() else ct.strip()
entry_var.set(new_text)
entry.configure(fg="#1a4d6d")
entry.icursor("end")
if ed:
set_date(ed)
rec_status_var.set(f"✓ Diktiert Datum erkannt: {ed.strftime('%d.%m.%Y')}")
else:
rec_status_var.set("✓ Diktiert bei Bedarf bearbeiten, dann hinzufügen.")
self.after(0, _done)
except Exception as e:
self.after(0, lambda: rec_status_var.set(f"Fehler: {e}"))
todo_recorder[0] = None
threading.Thread(target=worker, daemon=True).start()
rec_btn.configure(command=toggle_todo_record)
# Zeile 2: Datum (Kalender) + Hinzufügen
date_frame = tk.Frame(input_frame, bg="#D4EEF7")
date_frame.pack(fill="x", pady=(4, 0))
tk.Label(date_frame, text="Fällig:", font=("Segoe UI", 9),
bg="#D4EEF7", fg="#4a8aaa").pack(side="left")
selected_date = [None]
date_display = tk.Label(date_frame, text="kein Datum", font=("Segoe UI", 10),
bg="#D4EEF7", fg="#999")
date_display.pack(side="left", padx=(4, 4))
def set_date(d):
selected_date[0] = d
if d:
date_display.configure(text=d.strftime("%d.%m.%Y"), fg="#1a4d6d")
else:
date_display.configure(text="kein Datum", fg="#999")
def clear_date():
set_date(None)
# ─── Kalender-Popup ───
def open_calendar():
cal_win = tk.Toplevel(win)
cal_win.title("Datum wählen")
cal_win.attributes("-topmost", True)
cal_win.configure(bg="#E8F4FA")
cal_win.resizable(False, False)
self._register_window(cal_win)
today = date.today()
current = [selected_date[0].year if selected_date[0] else today.year,
selected_date[0].month if selected_date[0] else today.month]
nav = tk.Frame(cal_win, bg="#B9ECFA")
nav.pack(fill="x")
month_label = tk.Label(nav, text="", font=("Segoe UI", 11, "bold"),
bg="#B9ECFA", fg="#1a4d6d")
def update_cal():
month_label.configure(
text=f"{cal_mod.month_name[current[1]]} {current[0]}")
for w in days_frame.winfo_children():
w.destroy()
weekdays = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
for c, wd in enumerate(weekdays):
fg = "#C03030" if c >= 5 else "#4a8aaa"
tk.Label(days_frame, text=wd, font=("Segoe UI", 9, "bold"),
bg="#E8F4FA", fg=fg, width=4).grid(row=0, column=c)
first_wd, num_days = cal_mod.monthrange(current[0], current[1])
row = 1
col = first_wd
for day in range(1, num_days + 1):
d = date(current[0], current[1], day)
is_today = (d == today)
is_selected = (d == selected_date[0])
is_past = (d < today)
if is_selected:
bg, fg = "#5B8DB3", "white"
elif is_today:
bg, fg = "#D4EEF7", "#1a4d6d"
else:
bg, fg = "#F5FCFF", ("#999" if is_past else "#1a4d6d")
btn = tk.Label(days_frame, text=str(day), font=("Segoe UI", 10),
bg=bg, fg=fg, width=4, cursor="hand2",
relief="flat", bd=0)
btn.grid(row=row, column=col, padx=1, pady=1)
btn.bind("<Button-1>", lambda e, dd=d: (set_date(dd), cal_win.destroy()))
btn.bind("<Enter>", lambda e, b=btn: b.configure(bg="#B9ECFA") if b.cget("bg") not in ("#5B8DB3",) else None)
btn.bind("<Leave>", lambda e, b=btn, bg0=bg: b.configure(bg=bg0))
col += 1
if col > 6:
col = 0
row += 1
def prev_month():
current[1] -= 1
if current[1] < 1:
current[1] = 12
current[0] -= 1
update_cal()
def next_month():
current[1] += 1
if current[1] > 12:
current[1] = 1
current[0] += 1
update_cal()
btn_prev = tk.Label(nav, text="", font=("Segoe UI", 12), bg="#B9ECFA",
fg="#1a4d6d", cursor="hand2", padx=8)
btn_prev.pack(side="left")
btn_prev.bind("<Button-1>", lambda e: prev_month())
month_label.pack(side="left", fill="x", expand=True)
btn_next = tk.Label(nav, text="", font=("Segoe UI", 12), bg="#B9ECFA",
fg="#1a4d6d", cursor="hand2", padx=8)
btn_next.pack(side="right")
btn_next.bind("<Button-1>", lambda e: next_month())
days_frame = tk.Frame(cal_win, bg="#E8F4FA", padx=4, pady=4)
days_frame.pack(fill="both")
# Schnellauswahl
quick = tk.Frame(cal_win, bg="#D4EEF7", pady=4)
quick.pack(fill="x")
for label_text, delta in [("Heute", 0), ("Morgen", 1), ("+1 Wo", 7)]:
d = today + timedelta(days=delta)
tk.Button(quick, text=label_text, font=("Segoe UI", 8),
bg="#C8DDE6", fg="#1a4d6d", activebackground="#B8CDD6",
relief="flat", bd=0, padx=6, pady=1, cursor="hand2",
command=lambda dd=d: (set_date(dd), cal_win.destroy())
).pack(side="left", padx=2)
tk.Button(quick, text="Kein Datum", font=("Segoe UI", 8),
bg="#E0E0E0", fg="#666", activebackground="#D0D0D0",
relief="flat", bd=0, padx=6, pady=1, cursor="hand2",
command=lambda: (clear_date(), cal_win.destroy())
).pack(side="right", padx=2)
update_cal()
cal_win.update_idletasks()
x = win.winfo_x() + 50
y = win.winfo_y() + 120
cal_win.geometry(f"+{x}+{y}")
cal_btn = tk.Button(date_frame, text="📅", font=("Segoe UI", 12),
bg="#D4EEF7", fg="#1a4d6d", activebackground="#C4DEE7",
relief="flat", bd=0, cursor="hand2", command=open_calendar)
cal_btn.pack(side="left", padx=(0, 4))
clear_date_btn = tk.Label(date_frame, text="", font=("Segoe UI", 9),
bg="#D4EEF7", fg="#ccc", cursor="hand2")
clear_date_btn.pack(side="left")
clear_date_btn.bind("<Button-1>", lambda e: clear_date())
clear_date_btn.bind("<Enter>", lambda e: clear_date_btn.configure(fg="#E87070"))
clear_date_btn.bind("<Leave>", lambda e: clear_date_btn.configure(fg="#ccc"))
def _get_new_todo_category():
cat = _active_category[0]
return "Allgemein" if cat == "Alle" else cat
def _update_cat_combobox():
pass
_rebuild_cat_buttons()
add_btn = tk.Button(date_frame, text=" Hinzufügen", font=("Segoe UI", 10, "bold"),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
activeforeground="white", relief="flat", bd=0, padx=12, pady=2,
cursor="hand2")
add_btn.pack(side="right", padx=(0, 8))
# ─── Scrollbarer Bereich für To-dos ───
list_outer = tk.Frame(todo_page, bg="#E8F4FA")
list_outer.pack(fill="both", expand=True, padx=8, pady=(4, 0))
canvas = tk.Canvas(list_outer, bg="#E8F4FA", highlightthickness=0)
scrollbar = ttk.Scrollbar(list_outer, orient="vertical", command=canvas.yview)
list_frame = tk.Frame(canvas, bg="#E8F4FA")
list_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.create_window((0, 0), window=list_frame, anchor="nw", tags="list_win")
canvas.configure(yscrollcommand=scrollbar.set)
def _resize_list(event):
canvas.itemconfig("list_win", width=event.width)
canvas.bind("<Configure>", _resize_list)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
def _on_mousewheel(event):
canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
def _bind_mousewheel_recursive(widget):
"""Bindet Mausrad-Scrolling an Widget und alle Kinder (rekursiv)."""
widget.bind("<MouseWheel>", _on_mousewheel)
for child in widget.winfo_children():
_bind_mousewheel_recursive(child)
canvas.bind("<MouseWheel>", _on_mousewheel)
list_frame.bind("<MouseWheel>", _on_mousewheel)
# ─── Fusszeile ───
footer = tk.Frame(todo_page, bg="#D4EEF7", pady=4)
footer.pack(fill="x", side="bottom")
btn_del_done = tk.Button(footer, text="🗑 Erledigte löschen", font=("Segoe UI", 9),
bg="#C8DDE6", fg="#1a4d6d", activebackground="#B8CDD6",
relief="flat", bd=0, padx=8, pady=2, cursor="hand2")
btn_del_done.pack(side="left", padx=8)
btn_email = tk.Button(footer, text="✉ E-Mail senden", font=("Segoe UI", 9),
bg="#A8D8E8", fg="#1a4d6d", activebackground="#98C8D8",
relief="flat", bd=0, padx=8, pady=2, cursor="hand2")
btn_email.pack(side="right", padx=(0, 8))
btn_medwork = tk.Button(footer, text="📤 An MedWork senden", font=("Segoe UI", 9),
bg="#7EC8E3", fg="#1a4d6d", activebackground="#6CB8D3",
relief="flat", bd=0, padx=8, pady=2, cursor="hand2")
btn_medwork.pack(side="right", padx=(0, 4))
# Senden-Auswahl (Häkchen-Tracking)
send_selection = {} # idx -> BooleanVar
# MedWork-Kontakte (erweiterbar)
medwork_contacts = [
"Dr. med. Muster (Innere Medizin)",
"Dr. med. Beispiel (Chirurgie)",
"Dr. med. Test (Kardiologie)",
"Praxis-Team intern",
]
MEDWORK_CONTACTS_FILE = "kg_diktat_medwork_contacts.json"
def _load_medwork_contacts():
try:
p = os.path.join(os.path.dirname(os.path.abspath(__file__)), MEDWORK_CONTACTS_FILE)
if os.path.isfile(p):
with open(p, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
return medwork_contacts[:]
def _save_medwork_contacts(contacts):
try:
p = os.path.join(os.path.dirname(os.path.abspath(__file__)), MEDWORK_CONTACTS_FILE)
with open(p, "w", encoding="utf-8") as f:
json.dump(contacts, f, indent=2, ensure_ascii=False)
except Exception:
pass
def _get_selected_items():
"""Gibt die mit 'senden'-Häkchen ausgewählten To-dos zurück."""
selected = []
for i, sv in send_selection.items():
if sv.get() and 0 <= i < len(todos):
selected.append(todos[i])
return selected
def _format_todo_for_email(t):
"""Formatiert ein To-do für E-Mail-Text."""
prio_map = {1: "HOCH", 2: "MITTEL", 0: ""}
line = f"- {t.get('text', '')}"
if t.get("date"):
try:
dt = datetime.strptime(t["date"], "%Y-%m-%d")
line += f" [Fällig: {dt.strftime('%d.%m.%Y')}]"
except Exception:
line += f" [Fällig: {t['date']}]"
p = prio_map.get(t.get("priority", 0), "")
if p:
line += f" [Priorität: {p}]"
if t.get("notes", "").strip():
line += f"\n Notiz: {t['notes'].strip()}"
return line
def _send_items_to_medwork(items):
"""Öffnet Dialog zum Senden von To-do(s) an MedWork (in die Inbox des Empfängers)."""
if not items:
messagebox.showinfo("MedWork", "Keine Aufgaben ausgewählt.", parent=win)
return
mw_win = tk.Toplevel(win)
mw_win.title("An MedWork senden")
mw_win.attributes("-topmost", True)
mw_win.configure(bg="#E8F4FA")
mw_win.resizable(False, False)
mw_win.geometry("420x440")
center_window(mw_win, 420, 440)
self._register_window(mw_win)
tk.Label(mw_win, text="📤 Aufgabe(n) an MedWork senden", font=("Segoe UI", 12, "bold"),
bg="#B9ECFA", fg="#1a4d6d").pack(fill="x", ipady=8)
# Aufgaben-Vorschau
tk.Label(mw_win, text=f"{len(items)} Aufgabe(n):", font=("Segoe UI", 9, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", padx=12, pady=(8, 2))
preview_f = tk.Frame(mw_win, bg="white", padx=6, pady=4)
preview_f.pack(fill="x", padx=12, pady=(0, 8))
for it in items[:5]:
tk.Label(preview_f, text=f"{it['text'][:55]}{'' if len(it.get('text',''))>55 else ''}",
font=("Segoe UI", 9), bg="white", fg="#1a4d6d", anchor="w").pack(fill="x")
if len(items) > 5:
tk.Label(preview_f, text=f"… und {len(items)-5} weitere",
font=("Segoe UI", 8, "italic"), bg="white", fg="#999").pack(fill="x")
# Kontakt-Auswahl
tk.Label(mw_win, text="Empfänger:", font=("Segoe UI", 9, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(anchor="w", padx=12, pady=(4, 2))
contacts = _load_medwork_contacts()
contact_var = tk.StringVar()
contact_var.set(contacts[0] if contacts else "")
contact_menu = ttk.Combobox(mw_win, textvariable=contact_var, values=contacts,
font=("Segoe UI", 10))
contact_menu.pack(fill="x", padx=12, pady=(0, 8))
# Nachricht mit Diktat-Button
msg_label_row = tk.Frame(mw_win, bg="#E8F4FA")
msg_label_row.pack(fill="x", padx=12, pady=(4, 2))
tk.Label(msg_label_row, text="Nachricht (optional):", font=("Segoe UI", 9, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(side="left")
mw_recorder = [None]
mw_is_recording = [False]
mw_rec_status = tk.StringVar(value="")
mw_rec_btn = tk.Button(msg_label_row, text="", font=("Segoe UI", 11),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
activeforeground="white", relief="flat", bd=0,
width=3, cursor="hand2")
mw_rec_btn.pack(side="right")
msg_text = tk.Text(mw_win, font=("Segoe UI", 10), height=4, bg="white",
fg="#1a4d6d", relief="flat", bd=1)
msg_text.pack(fill="x", padx=12, pady=(0, 2))
tk.Label(mw_win, textvariable=mw_rec_status, font=("Segoe UI", 8),
bg="#E8F4FA", fg="#5B8DB3").pack(fill="x", padx=12, pady=(0, 6))
def _mw_toggle_record():
if not self.ensure_ready():
return
if not mw_is_recording[0]:
rec = AudioRecorder()
mw_recorder[0] = rec
try:
rec.start()
mw_is_recording[0] = True
mw_rec_btn.configure(text="", bg="#C03030", activebackground="#A02020")
mw_rec_status.set("🔴 Aufnahme läuft…")
except Exception as e:
messagebox.showerror("Aufnahme-Fehler", str(e), parent=mw_win)
mw_rec_status.set("")
else:
mw_is_recording[0] = False
rec = mw_recorder[0]
mw_rec_btn.configure(text="", bg="#5B8DB3", activebackground="#4A7A9E")
mw_rec_status.set("Transkribiere…")
def worker():
try:
wav_path = rec.stop_and_save_wav()
import wave as _wave
try:
with _wave.open(wav_path, 'rb') as wf:
dur = wf.getnframes() / float(wf.getframerate())
if dur < 0.3:
if os.path.exists(wav_path):
os.remove(wav_path)
mw_recorder[0] = None
self.after(0, lambda: mw_rec_status.set("Kein Audio erkannt."))
return
except Exception:
pass
transcript_result = self.transcribe_wav(wav_path)
if hasattr(transcript_result, 'text'):
text = transcript_result.text
elif isinstance(transcript_result, str):
text = transcript_result
else:
text = ""
try:
if os.path.exists(wav_path):
os.remove(wav_path)
except Exception:
pass
if not text or not text.strip():
mw_recorder[0] = None
self.after(0, lambda: mw_rec_status.set("Kein Text erkannt."))
return
text = self._diktat_apply_punctuation(text)
def _done():
mw_recorder[0] = None
current = msg_text.get("1.0", "end").strip()
new_t = (current + " " + text).strip() if current else text.strip()
msg_text.delete("1.0", "end")
msg_text.insert("1.0", new_t)
mw_rec_status.set("✓ Diktiert.")
self.after(0, _done)
except Exception as e:
self.after(0, lambda: mw_rec_status.set(f"Fehler: {e}"))
mw_recorder[0] = None
threading.Thread(target=worker, daemon=True).start()
mw_rec_btn.configure(command=_mw_toggle_record)
def _mw_on_close():
if mw_is_recording[0] and mw_recorder[0]:
try:
mw_is_recording[0] = False
wp = mw_recorder[0].stop_and_save_wav()
if os.path.exists(wp):
os.remove(wp)
except Exception:
pass
mw_recorder[0] = None
mw_win.destroy()
mw_win.protocol("WM_DELETE_WINDOW", _mw_on_close)
def do_send():
contact = contact_var.get().strip()
if not contact:
messagebox.showwarning("MedWork", "Bitte Empfänger eingeben.", parent=mw_win)
return
if contact not in contacts:
contacts.append(contact)
_save_medwork_contacts(contacts)
sender_name = self._user_profile.get("name", "Unbekannt")
for it in items:
send_todo_to_inbox(it, sender_name, contact)
messagebox.showinfo(
"MedWork",
f"{len(items)} Aufgabe(n) gesendet an:\n{contact}\n\n"
f"Die Aufgaben erscheinen in der To-Do-Liste des Empfängers.",
parent=mw_win,
)
mw_win.destroy()
tk.Button(mw_win, text="📤 Senden", font=("Segoe UI", 11, "bold"),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
relief="flat", bd=0, padx=20, pady=6, cursor="hand2",
command=do_send).pack(pady=12)
def _send_items_via_email(items):
"""Öffnet den Standard-E-Mail-Client mit allen To-do-Infos."""
import urllib.parse
if not items:
messagebox.showinfo("E-Mail", "Keine Aufgaben ausgewählt.", parent=win)
return
sender = self._user_profile.get("name", "")
subject = f"To-Do Liste von {sender}" if sender else "To-Do Liste"
body_lines = [f"To-Do Liste ({date.today().strftime('%d.%m.%Y')})", ""]
for it in items:
body_lines.append(_format_todo_for_email(it))
body_lines.append("")
body_lines.append(f"Gesendet von AzA {sender}")
body = "\n".join(body_lines)
mailto_url = f"mailto:?subject={urllib.parse.quote(subject)}&body={urllib.parse.quote(body)}"
try:
os.startfile(mailto_url)
except Exception as e:
messagebox.showerror("E-Mail", f"Konnte E-Mail-Client nicht öffnen:\n{e}", parent=win)
def send_to_medwork():
selected = _get_selected_items()
if selected:
_send_items_to_medwork(selected)
else:
open_items = [t for t in todos if not t.get("done")]
if not open_items:
messagebox.showinfo("MedWork", "Keine offenen Aufgaben zum Senden.", parent=win)
return
_send_items_to_medwork(open_items)
def send_email():
selected = _get_selected_items()
if selected:
_send_items_via_email(selected)
else:
all_items = [t for t in todos if not t.get("done")]
if not all_items:
messagebox.showinfo("E-Mail", "Keine offenen Aufgaben zum Senden.", parent=win)
return
_send_items_via_email(all_items)
btn_medwork.configure(command=send_to_medwork)
btn_email.configure(command=send_email)
# ─── Notizen-Seite ───
user_notes = load_notes()
notes_widgets = []
_notes_recorder = [None]
_notes_is_recording = [False]
_notes_target_widget = [None]
notes_input_frame = tk.Frame(notes_page, bg="#D4EEF7", pady=8, padx=8)
notes_input_frame.pack(fill="x")
notes_rec_status_var = tk.StringVar(value="")
def _notes_generic_toggle_record(target_widget, btn_ref):
if not self.ensure_ready():
return
if not _notes_is_recording[0]:
rec = AudioRecorder()
_notes_recorder[0] = rec
_notes_target_widget[0] = target_widget
try:
rec.start()
_notes_is_recording[0] = True
btn_ref.configure(text="", bg="#C03030", activebackground="#A02020")
notes_rec_status_var.set("🔴 Aufnahme läuft…")
except Exception as e:
messagebox.showerror("Aufnahme-Fehler", str(e), parent=win)
notes_rec_status_var.set("")
else:
_notes_is_recording[0] = False
rec = _notes_recorder[0]
btn_ref.configure(text="", bg="#5B8DB3", activebackground="#4A7A9E")
notes_rec_status_var.set("Transkribiere…")
tw_ref = _notes_target_widget[0]
is_entry = isinstance(tw_ref, tk.Entry)
def worker():
try:
wav_path = rec.stop_and_save_wav()
import wave as _wave
try:
with _wave.open(wav_path, 'rb') as wf:
dur = wf.getnframes() / float(wf.getframerate())
if dur < 0.3:
if os.path.exists(wav_path):
os.remove(wav_path)
_notes_recorder[0] = None
self.after(0, lambda: notes_rec_status_var.set("Kein Audio erkannt."))
return
except Exception:
pass
transcript_result = self.transcribe_wav(wav_path)
if hasattr(transcript_result, 'text'):
text = transcript_result.text
elif isinstance(transcript_result, str):
text = transcript_result
else:
text = ""
try:
if os.path.exists(wav_path):
os.remove(wav_path)
except Exception:
pass
if not text or not text.strip():
_notes_recorder[0] = None
self.after(0, lambda: notes_rec_status_var.set("Kein Text erkannt."))
return
text = self._diktat_apply_punctuation(text)
def _done(t=text):
_notes_recorder[0] = None
if is_entry:
current = tw_ref.get()
if current == "Neue Notiz Titel eingeben…":
current = ""
new_text = (current + " " + t).strip() if current.strip() else t.strip()
tw_ref.delete(0, "end")
tw_ref.insert(0, new_text)
tw_ref.configure(fg="#1a4d6d")
else:
current = tw_ref.get("1.0", "end").strip()
new_text = (current + " " + t).strip() if current else t.strip()
tw_ref.delete("1.0", "end")
tw_ref.insert("1.0", new_text)
notes_rec_status_var.set("✓ Diktiert.")
self.after(0, _done)
except Exception as e:
self.after(0, lambda: notes_rec_status_var.set(f"Fehler: {e}"))
_notes_recorder[0] = None
threading.Thread(target=worker, daemon=True).start()
# Titel-Zeile mit Diktier-Button
tk.Label(notes_input_frame, text="Titel:", font=("Segoe UI", 9, "bold"),
bg="#D4EEF7", fg="#1a4d6d").pack(anchor="w")
notes_entry_frame = tk.Frame(notes_input_frame, bg="#D4EEF7")
notes_entry_frame.pack(fill="x")
notes_title_var = tk.StringVar()
notes_title_entry = tk.Entry(notes_entry_frame, textvariable=notes_title_var,
font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
relief="flat", bd=0, insertbackground="#1a4d6d")
notes_title_entry.insert(0, "Neue Notiz Titel eingeben…")
notes_title_entry.configure(fg="#999")
notes_title_rec_btn = tk.Button(notes_entry_frame, text="", font=("Segoe UI", 11),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
activeforeground="white", relief="flat", bd=0,
width=3, cursor="hand2")
notes_title_rec_btn.pack(side="right")
notes_title_rec_btn.configure(
command=lambda: _notes_generic_toggle_record(notes_title_entry, notes_title_rec_btn))
notes_title_entry.pack(side="left", fill="x", expand=True, ipady=6, padx=(0, 4))
def _notes_entry_focus_in(e):
if notes_title_entry.get() == "Neue Notiz Titel eingeben…":
notes_title_entry.delete(0, "end")
notes_title_entry.configure(fg="#1a4d6d")
def _notes_entry_focus_out(e):
if not notes_title_entry.get().strip():
notes_title_entry.insert(0, "Neue Notiz Titel eingeben…")
notes_title_entry.configure(fg="#999")
notes_title_entry.bind("<FocusIn>", _notes_entry_focus_in)
notes_title_entry.bind("<FocusOut>", _notes_entry_focus_out)
# Inhalt-Zeile mit Diktier-Button
notes_text_label_row = tk.Frame(notes_input_frame, bg="#D4EEF7")
notes_text_label_row.pack(fill="x", pady=(6, 0))
tk.Label(notes_text_label_row, text="Inhalt:", font=("Segoe UI", 9, "bold"),
bg="#D4EEF7", fg="#1a4d6d").pack(side="left")
notes_text_rec_btn = tk.Button(notes_text_label_row, text="", font=("Segoe UI", 11),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
activeforeground="white", relief="flat", bd=0,
width=3, cursor="hand2")
notes_text_rec_btn.pack(side="right")
notes_text_input = tk.Text(notes_input_frame, font=("Segoe UI", 10), height=3,
bg="white", fg="#1a4d6d", relief="flat", bd=1, wrap="word")
notes_text_input.pack(fill="x", pady=(2, 0))
notes_text_rec_btn.configure(
command=lambda: _notes_generic_toggle_record(notes_text_input, notes_text_rec_btn))
tk.Label(notes_input_frame, textvariable=notes_rec_status_var, font=("Segoe UI", 8),
bg="#D4EEF7", fg="#5B8DB3").pack(fill="x")
notes_add_frame = tk.Frame(notes_input_frame, bg="#D4EEF7")
notes_add_frame.pack(fill="x", pady=(4, 0))
def _add_note(event=None):
title = notes_title_var.get().strip()
if not title or title == "Neue Notiz Titel eingeben…":
title = "Notiz"
text_content = notes_text_input.get("1.0", "end").strip()
new_note = {
"id": int(datetime.now().timestamp() * 1000),
"title": title,
"text": text_content,
"created": datetime.now().isoformat(),
}
user_notes.append(new_note)
notes_title_var.set("")
notes_title_entry.delete(0, "end")
notes_title_entry.insert(0, "Neue Notiz Titel eingeben…")
notes_title_entry.configure(fg="#999")
notes_text_input.delete("1.0", "end")
notes_rec_status_var.set("")
save_notes(user_notes)
_rebuild_notes_list()
notes_title_entry.bind("<Return>", _add_note)
tk.Button(notes_add_frame, text=" Notiz hinzufügen", font=("Segoe UI", 10, "bold"),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
activeforeground="white", relief="flat", bd=0, padx=12, pady=2,
cursor="hand2", command=_add_note).pack(side="right")
# Scrollbarer Bereich für Notizen
notes_list_outer = tk.Frame(notes_page, bg="#E8F4FA")
notes_list_outer.pack(fill="both", expand=True, padx=8, pady=(4, 0))
notes_canvas = tk.Canvas(notes_list_outer, bg="#E8F4FA", highlightthickness=0)
notes_scrollbar = ttk.Scrollbar(notes_list_outer, orient="vertical", command=notes_canvas.yview)
notes_list_frame = tk.Frame(notes_canvas, bg="#E8F4FA")
notes_list_frame.bind("<Configure>",
lambda e: notes_canvas.configure(scrollregion=notes_canvas.bbox("all")))
notes_canvas.create_window((0, 0), window=notes_list_frame, anchor="nw", tags="notes_win")
notes_canvas.configure(yscrollcommand=notes_scrollbar.set)
def _notes_resize(event):
notes_canvas.itemconfig("notes_win", width=event.width)
notes_canvas.bind("<Configure>", _notes_resize)
notes_canvas.pack(side="left", fill="both", expand=True)
notes_scrollbar.pack(side="right", fill="y")
def _notes_mousewheel(event):
notes_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
def _bind_notes_mousewheel_recursive(widget):
widget.bind("<MouseWheel>", _notes_mousewheel)
for child in widget.winfo_children():
_bind_notes_mousewheel_recursive(child)
notes_canvas.bind("<MouseWheel>", _notes_mousewheel)
notes_list_frame.bind("<MouseWheel>", _notes_mousewheel)
# Notizen-Fusszeile
notes_footer = tk.Frame(notes_page, bg="#D4EEF7", pady=4)
notes_footer.pack(fill="x", side="bottom")
notes_counter_label = tk.Label(notes_footer, text="", font=("Segoe UI", 9),
bg="#D4EEF7", fg="#4a8aaa")
notes_counter_label.pack(side="left", padx=8)
def _open_note_detail(idx):
if idx < 0 or idx >= len(user_notes):
return
note = user_notes[idx]
nd = tk.Toplevel(win)
nd.title("Notiz bearbeiten")
nd.attributes("-topmost", True)
nd.configure(bg="#E8F4FA")
nd.resizable(True, True)
nd.geometry("460x420")
center_window(nd, 460, 420)
self._register_window(nd)
tk.Label(nd, text="📝 Notiz bearbeiten", font=("Segoe UI", 12, "bold"),
bg="#B9ECFA", fg="#1a4d6d").pack(fill="x", ipady=8)
nd_content = tk.Frame(nd, bg="#E8F4FA", padx=12, pady=8)
nd_content.pack(fill="both", expand=True)
nd_recorder = [None]
nd_is_recording = [False]
nd_target = [None]
def nd_on_close():
if nd_is_recording[0] and nd_recorder[0]:
try:
nd_is_recording[0] = False
wp = nd_recorder[0].stop_and_save_wav()
if os.path.exists(wp):
os.remove(wp)
except Exception:
pass
nd_recorder[0] = None
nd.destroy()
nd.protocol("WM_DELETE_WINDOW", nd_on_close)
def _nd_toggle_record(target_widget, btn_ref, status_var):
if not self.ensure_ready():
return
if not nd_is_recording[0]:
rec = AudioRecorder()
nd_recorder[0] = rec
nd_target[0] = target_widget
try:
rec.start()
nd_is_recording[0] = True
btn_ref.configure(text="", bg="#C03030", activebackground="#A02020")
status_var.set("🔴 Aufnahme läuft…")
except Exception as e:
messagebox.showerror("Aufnahme-Fehler", str(e), parent=nd)
status_var.set("")
else:
nd_is_recording[0] = False
rec = nd_recorder[0]
btn_ref.configure(text="", bg="#5B8DB3", activebackground="#4A7A9E")
status_var.set("Transkribiere…")
tw = nd_target[0]
def worker():
try:
wav_path = rec.stop_and_save_wav()
import wave as _wave
try:
with _wave.open(wav_path, 'rb') as wf:
dur = wf.getnframes() / float(wf.getframerate())
if dur < 0.3:
if os.path.exists(wav_path):
os.remove(wav_path)
nd_recorder[0] = None
self.after(0, lambda: status_var.set("Kein Audio erkannt."))
return
except Exception:
pass
transcript_result = self.transcribe_wav(wav_path)
if hasattr(transcript_result, 'text'):
text = transcript_result.text
elif isinstance(transcript_result, str):
text = transcript_result
else:
text = ""
try:
if os.path.exists(wav_path):
os.remove(wav_path)
except Exception:
pass
if not text or not text.strip():
nd_recorder[0] = None
self.after(0, lambda: status_var.set("Kein Text erkannt."))
return
text = self._diktat_apply_punctuation(text)
def _done(t=text):
nd_recorder[0] = None
current = tw.get("1.0", "end").strip()
new_text = (current + " " + t).strip() if current else t.strip()
tw.delete("1.0", "end")
tw.insert("1.0", new_text)
status_var.set("✓ Diktiert.")
self.after(0, _done)
except Exception as e:
self.after(0, lambda: status_var.set(f"Fehler: {e}"))
nd_recorder[0] = None
threading.Thread(target=worker, daemon=True).start()
nd_rec_status = tk.StringVar(value="")
# Titel
nd_title_row = tk.Frame(nd_content, bg="#E8F4FA")
nd_title_row.pack(fill="x", pady=(0, 2))
tk.Label(nd_title_row, text="Titel:", font=("Segoe UI", 9, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(side="left")
nd_title_rec_btn = tk.Button(nd_title_row, text="", font=("Segoe UI", 11),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
activeforeground="white", relief="flat", bd=0,
width=3, cursor="hand2")
nd_title_rec_btn.pack(side="right")
nd_title_edit = tk.Text(nd_content, font=("Segoe UI", 11), height=1, bg="white",
fg="#1a4d6d", relief="flat", bd=1, wrap="word")
nd_title_edit.pack(fill="x")
nd_title_edit.insert("1.0", note.get("title", ""))
nd_title_rec_btn.configure(
command=lambda: _nd_toggle_record(nd_title_edit, nd_title_rec_btn, nd_rec_status))
# Text
nd_text_row = tk.Frame(nd_content, bg="#E8F4FA")
nd_text_row.pack(fill="x", pady=(8, 2))
tk.Label(nd_text_row, text="Inhalt:", font=("Segoe UI", 9, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(side="left")
nd_text_rec_btn = tk.Button(nd_text_row, text="", font=("Segoe UI", 11),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
activeforeground="white", relief="flat", bd=0,
width=3, cursor="hand2")
nd_text_rec_btn.pack(side="right")
nd_text_edit = tk.Text(nd_content, font=("Segoe UI", 10), height=8, bg="white",
fg="#1a4d6d", relief="flat", bd=1, wrap="word")
nd_text_edit.pack(fill="both", expand=True)
add_text_font_size_control(nd_text_row, nd_text_edit, initial_size=10, bg_color="#E8F4FA", save_key="todo_note_detail")
nd_text_edit.insert("1.0", note.get("text", ""))
nd_text_rec_btn.configure(
command=lambda: _nd_toggle_record(nd_text_edit, nd_text_rec_btn, nd_rec_status))
tk.Label(nd_content, textvariable=nd_rec_status, font=("Segoe UI", 8),
bg="#E8F4FA", fg="#5B8DB3").pack(fill="x", pady=(0, 4))
def _save_note():
new_title = nd_title_edit.get("1.0", "end").strip()
if not new_title:
new_title = "Notiz"
user_notes[idx]["title"] = new_title
user_notes[idx]["text"] = nd_text_edit.get("1.0", "end").strip()
save_notes(user_notes)
nd.destroy()
_rebuild_notes_list()
nd_btn_frame = tk.Frame(nd, bg="#D4EEF7", pady=8)
nd_btn_frame.pack(fill="x", side="bottom")
tk.Button(nd_btn_frame, text="💾 Speichern", font=("Segoe UI", 11, "bold"),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
relief="flat", bd=0, padx=20, pady=4, cursor="hand2",
command=_save_note).pack(side="left", padx=12)
tk.Button(nd_btn_frame, text="Abbrechen", font=("Segoe UI", 10),
bg="#C8DDE6", fg="#1a4d6d", activebackground="#B8CDD6",
relief="flat", bd=0, padx=12, pady=4, cursor="hand2",
command=nd.destroy).pack(side="right", padx=12)
def _delete_note(idx):
if 0 <= idx < len(user_notes):
user_notes.pop(idx)
save_notes(user_notes)
_rebuild_notes_list()
_notes_drag = {"active": False, "idx": None, "indicator": None}
_notes_border_frames = []
def _notes_drag_start(event, idx):
_notes_drag["active"] = True
_notes_drag["idx"] = idx
event.widget.configure(cursor="fleur")
if 0 <= idx < len(_notes_border_frames):
_notes_border_frames[idx].configure(bg="#5B8DB3")
def _notes_drag_motion(event, idx):
if not _notes_drag["active"]:
return
abs_y = event.widget.winfo_rooty() + event.y
target = _notes_find_drop_target(abs_y)
if _notes_drag.get("indicator"):
_notes_drag["indicator"].destroy()
_notes_drag["indicator"] = None
if target is not None and target != _notes_drag["idx"]:
insert_before = target
if insert_before < len(_notes_border_frames):
ind = tk.Frame(notes_list_frame, bg="#5B8DB3", height=3)
ind.pack(before=_notes_border_frames[insert_before], fill="x", pady=0)
else:
ind = tk.Frame(notes_list_frame, bg="#5B8DB3", height=3)
ind.pack(fill="x", pady=0)
_notes_drag["indicator"] = ind
def _notes_drag_end(event, idx):
if not _notes_drag["active"]:
return
_notes_drag["active"] = False
if _notes_drag.get("indicator"):
_notes_drag["indicator"].destroy()
_notes_drag["indicator"] = None
event.widget.configure(cursor="hand2")
abs_y = event.widget.winfo_rooty() + event.y
target = _notes_find_drop_target(abs_y)
src = _notes_drag["idx"]
if target is not None and target != src:
item = user_notes.pop(src)
if target > src:
target -= 1
user_notes.insert(target, item)
save_notes(user_notes)
_rebuild_notes_list()
else:
if 0 <= src < len(_notes_border_frames):
_rebuild_notes_list()
def _notes_find_drop_target(abs_y):
for i, bf in enumerate(_notes_border_frames):
try:
wy = bf.winfo_rooty()
wh = bf.winfo_height()
mid = wy + wh // 2
if abs_y < mid:
return i
except Exception:
pass
return len(_notes_border_frames)
def _rebuild_notes_list():
for w in notes_list_frame.winfo_children():
w.destroy()
notes_widgets.clear()
_notes_border_frames.clear()
if not user_notes:
tk.Label(notes_list_frame, text="Noch keine Notizen vorhanden.\n"
"Erstellen Sie eine neue Notiz oben.",
font=("Segoe UI", 10), bg="#E8F4FA", fg="#999",
justify="center").pack(pady=40)
notes_counter_label.configure(text="0 Notizen")
return
fs = todo_font_size[0]
for idx, note in enumerate(user_notes):
border_frame = tk.Frame(notes_list_frame, bg="#B0D8E8", padx=1, pady=1)
border_frame.pack(fill="x", pady=2)
_notes_border_frames.append(border_frame)
row = tk.Frame(border_frame, bg="white", padx=8, pady=8, cursor="hand2")
row.pack(fill="x")
top_row = tk.Frame(row, bg="white")
top_row.pack(fill="x")
drag_handle = tk.Label(top_row, text="", font=("Segoe UI", 12, "bold"),
bg="white", fg="#B0C8D8", cursor="hand2")
drag_handle.pack(side="left", padx=(0, 6))
drag_handle.bind("<Button-1>", lambda e, i=idx: _notes_drag_start(e, i))
drag_handle.bind("<B1-Motion>", lambda e, i=idx: _notes_drag_motion(e, i))
drag_handle.bind("<ButtonRelease-1>", lambda e, i=idx: _notes_drag_end(e, i))
drag_handle.bind("<Enter>", lambda e, h=drag_handle: h.configure(fg="#5B8DB3"))
drag_handle.bind("<Leave>", lambda e, h=drag_handle: h.configure(fg="#B0C8D8"))
title_text = note.get("title", "Notiz")
title_lbl = tk.Label(top_row, text=title_text, font=("Segoe UI", fs, "bold"),
bg="white", fg="#1a4d6d", anchor="w",
cursor="hand2", wraplength=320, justify="left")
title_lbl.pack(side="left", fill="x", expand=True)
note_text = note.get("text", "").strip()
preview_lbl = None
if note_text:
preview = note_text[:120] + ("\u2026" if len(note_text) > 120 else "")
preview_lbl = tk.Label(row, text=preview, font=("Segoe UI", max(fs - 1, 8)),
bg="white", fg="#5A8898", anchor="w",
cursor="hand2", wraplength=350, justify="left")
preview_lbl.pack(fill="x", anchor="w", pady=(2, 0))
bottom_row = tk.Frame(row, bg="white")
bottom_row.pack(fill="x", pady=(4, 0))
created = note.get("created", "")
try:
dt_obj = datetime.fromisoformat(created)
date_str = dt_obj.strftime("%d.%m.%Y %H:%M")
except Exception:
date_str = ""
if date_str:
tk.Label(bottom_row, text=date_str, font=("Segoe UI", 7),
bg="white", fg="#B0C8D8").pack(side="left")
del_lbl = tk.Label(bottom_row, text="\u2715", font=("Segoe UI", 10),
bg="white", fg="#ccc", cursor="hand2")
del_lbl.pack(side="right")
del_lbl.bind("<Enter>", lambda e, b=del_lbl: b.configure(fg="#E87070"))
del_lbl.bind("<Leave>", lambda e, b=del_lbl: b.configure(fg="#ccc"))
del_lbl.bind("<Button-1>", lambda e, i=idx: _delete_note(i))
for _w in [row, title_lbl]:
_w.bind("<Button-1>", lambda e, i=idx: _open_note_detail(i))
_w.bind("<Double-Button-1>", lambda e, i=idx: _open_note_detail(i))
if preview_lbl:
preview_lbl.bind("<Button-1>", lambda e, i=idx: _open_note_detail(i))
preview_lbl.bind("<Double-Button-1>", lambda e, i=idx: _open_note_detail(i))
notes_widgets.append(row)
notes_counter_label.configure(text=f"{len(user_notes)} Notiz{'en' if len(user_notes) != 1 else ''}")
_bind_notes_mousewheel_recursive(notes_list_frame)
# ═══════════════════════════════════════════════════
# ─── Checkliste-Seite ───
# ═══════════════════════════════════════════════════
user_checklists = load_checklists()
cl_input_frame = tk.Frame(checklist_page, bg="#D4EEF7", pady=8, padx=8)
cl_input_frame.pack(fill="x")
tk.Label(cl_input_frame, text="Neue Checkliste:", font=("Segoe UI", 9, "bold"),
bg="#D4EEF7", fg="#1a4d6d").pack(anchor="w")
cl_title_frame = tk.Frame(cl_input_frame, bg="#D4EEF7")
cl_title_frame.pack(fill="x", pady=(2, 0))
cl_title_var = tk.StringVar()
cl_title_entry = tk.Entry(cl_title_frame, textvariable=cl_title_var,
font=("Segoe UI", 11), bg="white", fg="#1a4d6d",
relief="flat", bd=0, insertbackground="#1a4d6d")
cl_title_entry.pack(side="left", fill="x", expand=True, ipady=4, padx=(0, 6))
cl_title_entry.insert(0, "Titel der Checkliste…")
cl_title_entry.configure(fg="#999")
def _cl_focus_in(e):
if cl_title_entry.get() == "Titel der Checkliste…":
cl_title_entry.delete(0, "end")
cl_title_entry.configure(fg="#1a4d6d")
def _cl_focus_out(e):
if not cl_title_entry.get().strip():
cl_title_entry.insert(0, "Titel der Checkliste…")
cl_title_entry.configure(fg="#999")
cl_title_entry.bind("<FocusIn>", _cl_focus_in)
cl_title_entry.bind("<FocusOut>", _cl_focus_out)
def _add_checklist(event=None):
title = cl_title_var.get().strip()
if not title or title == "Titel der Checkliste…":
return
new_cl = {
"id": int(datetime.now().timestamp() * 1000),
"title": title,
"items": [],
"created": datetime.now().isoformat(),
}
user_checklists.append(new_cl)
save_checklists(user_checklists)
cl_title_var.set("")
cl_title_entry.delete(0, "end")
cl_title_entry.insert(0, "Titel der Checkliste…")
cl_title_entry.configure(fg="#999")
_rebuild_checklist()
cl_title_entry.bind("<Return>", _add_checklist)
tk.Button(cl_title_frame, text="", font=("Segoe UI", 11, "bold"),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
relief="flat", bd=0, padx=8, cursor="hand2",
command=_add_checklist).pack(side="right")
cl_list_outer = tk.Frame(checklist_page, bg="#E8F4FA")
cl_list_outer.pack(fill="both", expand=True, padx=8, pady=(4, 0))
cl_canvas = tk.Canvas(cl_list_outer, bg="#E8F4FA", highlightthickness=0)
cl_sb = ttk.Scrollbar(cl_list_outer, orient="vertical", command=cl_canvas.yview)
cl_list_frame = tk.Frame(cl_canvas, bg="#E8F4FA")
cl_list_frame.bind("<Configure>", lambda e: cl_canvas.configure(scrollregion=cl_canvas.bbox("all")))
cl_canvas.create_window((0, 0), window=cl_list_frame, anchor="nw", tags="cl_win")
cl_canvas.configure(yscrollcommand=cl_sb.set)
cl_canvas.bind("<Configure>", lambda e: cl_canvas.itemconfig("cl_win", width=e.width))
cl_canvas.pack(side="left", fill="both", expand=True)
cl_sb.pack(side="right", fill="y")
def _cl_mw(event):
cl_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
cl_canvas.bind("<MouseWheel>", _cl_mw)
cl_footer = tk.Frame(checklist_page, bg="#D4EEF7", pady=4)
cl_footer.pack(fill="x", side="bottom")
cl_counter = tk.Label(cl_footer, text="", font=("Segoe UI", 9),
bg="#D4EEF7", fg="#4a8aaa")
cl_counter.pack(side="left", padx=8)
def _rebuild_checklist():
for w in cl_list_frame.winfo_children():
w.destroy()
if not user_checklists:
tk.Label(cl_list_frame, text="Noch keine Checklisten vorhanden.\n"
"Erstellen Sie eine neue Checkliste oben.",
font=("Segoe UI", 10), bg="#E8F4FA", fg="#999",
justify="center").pack(pady=40)
cl_counter.configure(text="0 Checklisten")
return
for cl_idx, cl in enumerate(user_checklists):
border = tk.Frame(cl_list_frame, bg="#D0E8F0", padx=1, pady=1)
border.pack(fill="x", pady=2)
card = tk.Frame(border, bg="white", padx=8, pady=6)
card.pack(fill="x")
# Header: Titel + Löschen
hdr = tk.Frame(card, bg="white")
hdr.pack(fill="x")
tk.Label(hdr, text=cl.get("title", "Checkliste"),
font=("Segoe UI", 10, "bold"), bg="white", fg="#1a4d6d",
anchor="w").pack(side="left", fill="x", expand=True)
items_done = sum(1 for it in cl.get("items", []) if it.get("done"))
items_total = len(cl.get("items", []))
progress_text = f"{items_done}/{items_total}" if items_total else "leer"
tk.Label(hdr, text=progress_text, font=("Segoe UI", 8),
bg="white", fg="#5B8DB3").pack(side="right", padx=(4, 0))
del_btn = tk.Label(hdr, text="", font=("Segoe UI", 9),
bg="white", fg="#ccc", cursor="hand2")
del_btn.pack(side="right", padx=(8, 0))
del_btn.bind("<Enter>", lambda e, b=del_btn: b.configure(fg="#E87070"))
del_btn.bind("<Leave>", lambda e, b=del_btn: b.configure(fg="#ccc"))
def _del_cl(i=cl_idx):
user_checklists.pop(i)
save_checklists(user_checklists)
_rebuild_checklist()
del_btn.bind("<Button-1>", lambda e, i=cl_idx: _del_cl(i))
# Items
items = cl.get("items", [])
for it_idx, item in enumerate(items):
it_frame = tk.Frame(card, bg="white")
it_frame.pack(fill="x", pady=1)
var = tk.BooleanVar(value=item.get("done", False))
def _toggle_item(ci=cl_idx, ii=it_idx, v=var):
user_checklists[ci]["items"][ii]["done"] = v.get()
save_checklists(user_checklists)
_rebuild_checklist()
cb = tk.Checkbutton(it_frame, variable=var, command=lambda ci=cl_idx, ii=it_idx, v=var: _toggle_item(ci, ii, v),
bg="white", activebackground="white")
cb.pack(side="left")
txt = item.get("text", "")
fg_c = "#999" if item.get("done") else "#1a4d6d"
font_c = ("Segoe UI", 9, "overstrike") if item.get("done") else ("Segoe UI", 9)
tk.Label(it_frame, text=txt, font=font_c, bg="white", fg=fg_c,
anchor="w").pack(side="left", fill="x", expand=True)
it_del = tk.Label(it_frame, text="", font=("Segoe UI", 7),
bg="white", fg="#ddd", cursor="hand2")
it_del.pack(side="right")
it_del.bind("<Enter>", lambda e, b=it_del: b.configure(fg="#E87070"))
it_del.bind("<Leave>", lambda e, b=it_del: b.configure(fg="#ddd"))
def _del_item(ci=cl_idx, ii=it_idx):
user_checklists[ci]["items"].pop(ii)
save_checklists(user_checklists)
_rebuild_checklist()
it_del.bind("<Button-1>", lambda e, ci=cl_idx, ii=it_idx: _del_item(ci, ii))
# Neues Item hinzufügen
add_frame = tk.Frame(card, bg="white")
add_frame.pack(fill="x", pady=(4, 0))
add_var = tk.StringVar()
add_entry = tk.Entry(add_frame, textvariable=add_var, font=("Segoe UI", 9),
bg="#F5FCFF", fg="#1a4d6d", relief="flat", bd=1,
insertbackground="#1a4d6d")
add_entry.pack(side="left", fill="x", expand=True, ipady=2, padx=(0, 4))
add_entry.insert(0, "Neuer Punkt…")
add_entry.configure(fg="#999")
add_entry.bind("<FocusIn>", lambda e, en=add_entry: (en.delete(0, "end"), en.configure(fg="#1a4d6d")) if en.get() == "Neuer Punkt…" else None)
add_entry.bind("<FocusOut>", lambda e, en=add_entry: (en.insert(0, "Neuer Punkt…"), en.configure(fg="#999")) if not en.get().strip() else None)
def _add_item(ci=cl_idx, v=add_var, en=add_entry):
txt = v.get().strip()
if not txt or txt == "Neuer Punkt…":
return
user_checklists[ci]["items"].append({"text": txt, "done": False})
save_checklists(user_checklists)
_rebuild_checklist()
add_entry.bind("<Return>", lambda e, ci=cl_idx, v=add_var, en=add_entry: _add_item(ci, v, en))
tk.Button(add_frame, text="", font=("Segoe UI", 9, "bold"),
bg="#7EC8E3", fg="white", activebackground="#6CB8D3",
relief="flat", bd=0, padx=4, cursor="hand2",
command=lambda ci=cl_idx, v=add_var, en=add_entry: _add_item(ci, v, en)).pack(side="right")
cl_counter.configure(text=f"{len(user_checklists)} Checkliste{'n' if len(user_checklists) != 1 else ''}")
def _cl_mw_recursive(widget):
widget.bind("<MouseWheel>", _cl_mw)
for child in widget.winfo_children():
_cl_mw_recursive(child)
_cl_mw_recursive(cl_list_frame)
# ─── Detail-Fenster ───
def open_detail(idx):
if idx < 0 or idx >= len(todos):
return
todo = todos[idx]
det = tk.Toplevel(win)
det.title("Aufgabe bearbeiten")
det.attributes("-topmost", True)
det.configure(bg="#E8F4FA")
det.resizable(True, True)
det.geometry("440x500")
center_window(det, 440, 500)
self._register_window(det)
# Header
tk.Label(det, text="✏ Aufgabe bearbeiten", font=("Segoe UI", 12, "bold"),
bg="#B9ECFA", fg="#1a4d6d").pack(fill="x", ipady=8)
content = tk.Frame(det, bg="#E8F4FA", padx=12, pady=8)
content.pack(fill="both", expand=True)
det_recorder = [None]
det_is_recording = [False]
det_target_widget = [None] # welches Text-Widget gerade diktiert wird
def det_on_close():
if det_is_recording[0] and det_recorder[0]:
try:
det_is_recording[0] = False
wp = det_recorder[0].stop_and_save_wav()
if os.path.exists(wp):
os.remove(wp)
except Exception:
pass
det_recorder[0] = None
det.destroy()
det.protocol("WM_DELETE_WINDOW", det_on_close)
def _det_toggle_record(target_text_widget, rec_btn_ref, status_var):
if not self.ensure_ready():
return
if not det_is_recording[0]:
rec = AudioRecorder()
det_recorder[0] = rec
det_target_widget[0] = target_text_widget
try:
rec.start()
det_is_recording[0] = True
rec_btn_ref.configure(text="", bg="#C03030", activebackground="#A02020")
status_var.set("🔴 Aufnahme läuft…")
except Exception as e:
messagebox.showerror("Aufnahme-Fehler", str(e), parent=det)
status_var.set("")
else:
det_is_recording[0] = False
rec = det_recorder[0]
rec_btn_ref.configure(text="", bg="#5B8DB3", activebackground="#4A7A9E")
status_var.set("Transkribiere…")
tw_ref = det_target_widget[0]
def worker():
try:
wav_path = rec.stop_and_save_wav()
import wave as _wave
try:
with _wave.open(wav_path, 'rb') as wf:
dur = wf.getnframes() / float(wf.getframerate())
if dur < 0.3:
if os.path.exists(wav_path):
os.remove(wav_path)
det_recorder[0] = None
self.after(0, lambda: status_var.set("Kein Audio erkannt."))
return
except Exception:
pass
transcript_result = self.transcribe_wav(wav_path)
if hasattr(transcript_result, 'text'):
text = transcript_result.text
elif isinstance(transcript_result, str):
text = transcript_result
else:
text = ""
try:
if os.path.exists(wav_path):
os.remove(wav_path)
except Exception:
pass
if not text or not text.strip():
det_recorder[0] = None
self.after(0, lambda: status_var.set("Kein Text erkannt."))
return
text = self._diktat_apply_punctuation(text)
cleaned_text, extracted_date = extract_date_from_todo_text(text)
def _done(ct=cleaned_text, ed=extracted_date):
det_recorder[0] = None
current = tw_ref.get("1.0", "end").strip()
new_text = (current + " " + ct).strip() if current else ct.strip()
tw_ref.delete("1.0", "end")
tw_ref.insert("1.0", new_text)
if ed:
det_set_date(ed)
status_var.set(f"✓ Datum erkannt: {ed.strftime('%d.%m.%Y')}")
else:
status_var.set("✓ Diktiert.")
self.after(0, _done)
except Exception as e:
self.after(0, lambda: status_var.set(f"Fehler: {e}"))
det_recorder[0] = None
threading.Thread(target=worker, daemon=True).start()
# Aufgabentext
text_label_row = tk.Frame(content, bg="#E8F4FA")
text_label_row.pack(fill="x", pady=(0, 2))
tk.Label(text_label_row, text="Aufgabe:", font=("Segoe UI", 9, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(side="left")
text_rec_status = tk.StringVar(value="")
text_rec_btn = tk.Button(text_label_row, text="", font=("Segoe UI", 11),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
activeforeground="white", relief="flat", bd=0,
width=3, cursor="hand2")
text_rec_btn.pack(side="right")
text_rec_btn.configure(
command=lambda: _det_toggle_record(text_edit, text_rec_btn, text_rec_status))
text_edit = tk.Text(content, font=("Segoe UI", 11), height=3, bg="white",
fg="#1a4d6d", relief="flat", bd=1, wrap="word")
text_edit.pack(fill="x")
text_edit.insert("1.0", todo.get("text", ""))
tk.Label(content, textvariable=text_rec_status, font=("Segoe UI", 8),
bg="#E8F4FA", fg="#5B8DB3").pack(fill="x", pady=(0, 6))
# Notizen
notes_label_row = tk.Frame(content, bg="#E8F4FA")
notes_label_row.pack(fill="x", pady=(0, 2))
tk.Label(notes_label_row, text="Notizen:", font=("Segoe UI", 9, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(side="left")
notes_rec_status = tk.StringVar(value="")
notes_rec_btn = tk.Button(notes_label_row, text="", font=("Segoe UI", 11),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
activeforeground="white", relief="flat", bd=0,
width=3, cursor="hand2")
notes_rec_btn.pack(side="right")
notes_rec_btn.configure(
command=lambda: _det_toggle_record(notes_edit, notes_rec_btn, notes_rec_status))
notes_edit = tk.Text(content, font=("Segoe UI", 10), height=4, bg="white",
fg="#1a4d6d", relief="flat", bd=1, wrap="word")
notes_edit.pack(fill="x")
notes_edit.insert("1.0", todo.get("notes", ""))
tk.Label(content, textvariable=notes_rec_status, font=("Segoe UI", 8),
bg="#E8F4FA", fg="#5B8DB3").pack(fill="x", pady=(0, 6))
# Datum
det_date = [None]
if todo.get("date"):
try:
det_date[0] = datetime.strptime(todo["date"], "%Y-%m-%d").date()
except Exception:
pass
date_row = tk.Frame(content, bg="#E8F4FA")
date_row.pack(fill="x", pady=(0, 8))
tk.Label(date_row, text="Fällig:", font=("Segoe UI", 9, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(side="left")
det_date_lbl = tk.Label(date_row, text="kein Datum", font=("Segoe UI", 10),
bg="#E8F4FA", fg="#999")
det_date_lbl.pack(side="left", padx=(4, 4))
def det_set_date(d):
det_date[0] = d
if d:
det_date_lbl.configure(text=d.strftime("%d.%m.%Y"), fg="#1a4d6d")
else:
det_date_lbl.configure(text="kein Datum", fg="#999")
if det_date[0]:
det_set_date(det_date[0])
def det_open_cal():
dcal = tk.Toplevel(det)
dcal.title("Datum wählen")
dcal.attributes("-topmost", True)
dcal.configure(bg="#E8F4FA")
dcal.resizable(False, False)
self._register_window(dcal)
t = date.today()
cur = [det_date[0].year if det_date[0] else t.year,
det_date[0].month if det_date[0] else t.month]
nav2 = tk.Frame(dcal, bg="#B9ECFA")
nav2.pack(fill="x")
ml2 = tk.Label(nav2, text="", font=("Segoe UI", 11, "bold"),
bg="#B9ECFA", fg="#1a4d6d")
df2 = tk.Frame(dcal, bg="#E8F4FA", padx=4, pady=4)
df2.pack(fill="both")
def upd2():
ml2.configure(text=f"{cal_mod.month_name[cur[1]]} {cur[0]}")
for w2 in df2.winfo_children():
w2.destroy()
for c2, wd2 in enumerate(["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]):
fg2 = "#C03030" if c2 >= 5 else "#4a8aaa"
tk.Label(df2, text=wd2, font=("Segoe UI", 9, "bold"),
bg="#E8F4FA", fg=fg2, width=4).grid(row=0, column=c2)
fw, nd = cal_mod.monthrange(cur[0], cur[1])
r2, c2 = 1, fw
for dy in range(1, nd + 1):
dd = date(cur[0], cur[1], dy)
sel = (dd == det_date[0])
bg2 = "#5B8DB3" if sel else ("#D4EEF7" if dd == t else "#F5FCFF")
fg2 = "white" if sel else ("#999" if dd < t else "#1a4d6d")
lb2 = tk.Label(df2, text=str(dy), font=("Segoe UI", 10),
bg=bg2, fg=fg2, width=4, cursor="hand2")
lb2.grid(row=r2, column=c2, padx=1, pady=1)
lb2.bind("<Button-1>", lambda e, d2=dd: (det_set_date(d2), dcal.destroy()))
c2 += 1
if c2 > 6:
c2 = 0
r2 += 1
def pm2():
cur[1] -= 1
if cur[1] < 1:
cur[1] = 12
cur[0] -= 1
upd2()
def nm2():
cur[1] += 1
if cur[1] > 12:
cur[1] = 1
cur[0] += 1
upd2()
tk.Label(nav2, text="", font=("Segoe UI", 12), bg="#B9ECFA",
fg="#1a4d6d", cursor="hand2", padx=8).pack(side="left")
nav2.winfo_children()[-1].bind("<Button-1>", lambda e: pm2())
ml2.pack(side="left", fill="x", expand=True)
tk.Label(nav2, text="", font=("Segoe UI", 12), bg="#B9ECFA",
fg="#1a4d6d", cursor="hand2", padx=8).pack(side="right")
nav2.winfo_children()[-1].bind("<Button-1>", lambda e: nm2())
q2 = tk.Frame(dcal, bg="#D4EEF7", pady=4)
q2.pack(fill="x")
for lt, dl in [("Heute", 0), ("Morgen", 1), ("+1 Wo", 7)]:
dd = t + timedelta(days=dl)
tk.Button(q2, text=lt, font=("Segoe UI", 8), bg="#C8DDE6", fg="#1a4d6d",
relief="flat", bd=0, padx=6, cursor="hand2",
command=lambda d2=dd: (det_set_date(d2), dcal.destroy())).pack(side="left", padx=2)
tk.Button(q2, text="Kein Datum", font=("Segoe UI", 8), bg="#E0E0E0", fg="#666",
relief="flat", bd=0, padx=6, cursor="hand2",
command=lambda: (det_set_date(None), dcal.destroy())).pack(side="right", padx=2)
upd2()
dcal.update_idletasks()
dcal.geometry(f"+{det.winfo_x()+30}+{det.winfo_y()+100}")
tk.Button(date_row, text="📅", font=("Segoe UI", 12), bg="#E8F4FA", fg="#1a4d6d",
relief="flat", bd=0, cursor="hand2", command=det_open_cal).pack(side="left")
# Priorität
prio_row = tk.Frame(content, bg="#E8F4FA")
prio_row.pack(fill="x", pady=(0, 8))
tk.Label(prio_row, text="Priorität:", font=("Segoe UI", 9, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(side="left")
prio_var = tk.IntVar(value=todo.get("priority", 0))
prio_colors = {0: ("#E8F4FA", "#1a4d6d"), 1: ("#F8D0D0", "#A03030"), 2: ("#F8ECC8", "#806000")}
prio_labels = {0: "Keine", 1: "🔴 Hoch", 2: "🟡 Mittel"}
prio_btns = {}
def set_prio(p):
prio_var.set(p)
for pp, b in prio_btns.items():
bg_c, fg_c = prio_colors[pp]
if pp == p:
b.configure(relief="solid", bd=2)
else:
b.configure(relief="flat", bd=1)
for p_val in [1, 2, 0]:
bg_c, fg_c = prio_colors[p_val]
b = tk.Button(prio_row, text=prio_labels[p_val], font=("Segoe UI", 9),
bg=bg_c, fg=fg_c, activebackground=bg_c,
relief="solid" if p_val == prio_var.get() else "flat",
bd=2 if p_val == prio_var.get() else 1,
padx=8, pady=1, cursor="hand2",
command=lambda pv=p_val: set_prio(pv))
b.pack(side="left", padx=(6, 0))
prio_btns[p_val] = b
# Speichern
def save_detail():
new_text = text_edit.get("1.0", "end").strip()
if not new_text:
messagebox.showwarning("Fehler", "Aufgabentext darf nicht leer sein.", parent=det)
return
todos[idx]["text"] = new_text
todos[idx]["notes"] = notes_edit.get("1.0", "end").strip()
todos[idx]["date"] = det_date[0].isoformat() if det_date[0] else None
todos[idx]["priority"] = prio_var.get()
det.destroy()
rebuild_list()
btn_frame = tk.Frame(det, bg="#D4EEF7", pady=8)
btn_frame.pack(fill="x", side="bottom")
tk.Button(btn_frame, text="💾 Speichern", font=("Segoe UI", 11, "bold"),
bg="#5B8DB3", fg="white", activebackground="#4A7A9E",
relief="flat", bd=0, padx=20, pady=4, cursor="hand2",
command=save_detail).pack(side="left", padx=12)
tk.Button(btn_frame, text="Abbrechen", font=("Segoe UI", 10),
bg="#C8DDE6", fg="#1a4d6d", activebackground="#B8CDD6",
relief="flat", bd=0, padx=12, pady=4, cursor="hand2",
command=det.destroy).pack(side="right", padx=12)
# ─── Logik ───
def update_counter():
done = sum(1 for t in todos if t.get("done"))
total = len(todos)
overdue = 0
today_str = date.today().isoformat()
for t in todos:
if not t.get("done") and t.get("date") and t["date"] < today_str:
overdue += 1
text = f"{done}/{total} erledigt"
if overdue:
text += f" · {overdue} überfällig"
counter_label.configure(text=text)
# ─── Rechtsklick-Kontextmenü ───
ctx_menu = tk.Menu(win, tearoff=0, font=("Segoe UI", 9))
_ctx_idx = [None]
def _ctx_set_prio(p):
i = _ctx_idx[0]
if i is not None and 0 <= i < len(todos):
todos[i]["priority"] = p
rebuild_list()
def _ctx_send_medwork():
i = _ctx_idx[0]
if i is not None and 0 <= i < len(todos):
_send_items_to_medwork([todos[i]])
def _ctx_send_email():
i = _ctx_idx[0]
if i is not None and 0 <= i < len(todos):
_send_items_via_email([todos[i]])
def _show_context_menu(event, idx):
_ctx_idx[0] = idx
ctx_menu.delete(0, "end")
prio_menu = tk.Menu(ctx_menu, tearoff=0, font=("Segoe UI", 9))
prio_menu.add_command(label="🔴 Hoch (1)", command=lambda: _ctx_set_prio(1))
prio_menu.add_command(label="🟡 Mittel (2)", command=lambda: _ctx_set_prio(2))
prio_menu.add_command(label="Keine", command=lambda: _ctx_set_prio(0))
ctx_menu.add_cascade(label="Priorität setzen", menu=prio_menu)
ctx_menu.add_separator()
ctx_menu.add_command(label="📤 An MedWork senden", command=_ctx_send_medwork)
ctx_menu.add_command(label="✉ Per E-Mail senden", command=_ctx_send_email)
ctx_menu.tk_popup(event.x_root, event.y_root)
# ─── Drag-and-Drop State ───
_drag = {"active": False, "idx": None, "indicator": None}
_border_frames = []
def _drag_start(event, idx):
_drag["active"] = True
_drag["idx"] = idx
event.widget.configure(cursor="fleur")
if 0 <= idx < len(_border_frames):
_border_frames[idx].configure(bg="#5B8DB3")
def _drag_motion(event, idx):
if not _drag["active"]:
return
abs_y = event.widget.winfo_rooty() + event.y
target = _find_drop_target(abs_y)
if _drag.get("indicator"):
_drag["indicator"].destroy()
_drag["indicator"] = None
if target is not None and target != _drag["idx"]:
insert_before = target
if insert_before < len(_border_frames):
ind = tk.Frame(list_frame, bg="#5B8DB3", height=3)
ind.pack(before=_border_frames[insert_before], fill="x", pady=0)
else:
ind = tk.Frame(list_frame, bg="#5B8DB3", height=3)
ind.pack(fill="x", pady=0)
_drag["indicator"] = ind
def _drag_end(event, idx):
if not _drag["active"]:
return
_drag["active"] = False
if _drag.get("indicator"):
_drag["indicator"].destroy()
_drag["indicator"] = None
event.widget.configure(cursor="hand2")
abs_y = event.widget.winfo_rooty() + event.y
target = _find_drop_target(abs_y)
src = _drag["idx"]
if target is not None and target != src:
item = todos.pop(src)
if target > src:
target -= 1
todos.insert(target, item)
save_todos(todos)
rebuild_list()
else:
if 0 <= src < len(_border_frames):
rebuild_list()
def _find_drop_target(abs_y):
for i, bf in enumerate(_border_frames):
try:
wy = bf.winfo_rooty()
wh = bf.winfo_height()
mid = wy + wh // 2
if abs_y < mid:
return i
except Exception:
pass
return len(_border_frames)
def rebuild_list():
for w in list_frame.winfo_children():
w.destroy()
todo_widgets.clear()
send_selection.clear()
_border_frames.clear()
today_str = date.today().isoformat()
active_cat = _active_category[0]
for idx, todo in enumerate(todos):
todo_cat = todo.get("category", "Allgemein")
if active_cat != "Alle" and todo_cat != active_cat:
continue
is_done = todo.get("done", False)
has_date = bool(todo.get("date"))
is_overdue = has_date and not is_done and todo["date"] < today_str
prio = todo.get("priority", 0)
from_sender = todo.get("sender", "")
if is_overdue:
row_bg = "#FDECEC"
border_color = "#E87070"
elif is_done:
row_bg = "#EDF7ED"
border_color = "#A8D8A8"
elif prio == 1:
row_bg = "#FDE8E8"
border_color = "#E8A0A0"
elif prio == 2:
row_bg = "#FDF5E0"
border_color = "#E8D090"
else:
row_bg = "white"
border_color = "#D0E8F0"
border_frame = tk.Frame(list_frame, bg=border_color, padx=1, pady=1)
border_frame.pack(fill="x", pady=2)
_border_frames.append(border_frame)
row = tk.Frame(border_frame, bg=row_bg, padx=4, pady=6)
row.pack(fill="x")
# Rechtsklick auf Zeile
row.bind("<Button-3>", lambda e, i=idx: _show_context_menu(e, i))
# ≡ Drag-Handle
drag_handle = tk.Label(row, text="", font=("Segoe UI", 12, "bold"),
bg=row_bg, fg="#B0C8D8", cursor="hand2")
drag_handle.pack(side="left", padx=(0, 2))
drag_handle.bind("<Button-1>", lambda e, i=idx: _drag_start(e, i))
drag_handle.bind("<B1-Motion>", lambda e, i=idx: _drag_motion(e, i))
drag_handle.bind("<ButtonRelease-1>", lambda e, i=idx: _drag_end(e, i))
drag_handle.bind("<Enter>", lambda e, h=drag_handle: h.configure(fg="#5B8DB3"))
drag_handle.bind("<Leave>", lambda e, h=drag_handle: h.configure(fg="#B0C8D8"))
# Checkbox erledigt
var = tk.BooleanVar(value=is_done)
cb = tk.Checkbutton(row, variable=var, bg=row_bg, activebackground=row_bg,
command=lambda i=idx, v=var: toggle_done(i, v))
cb.pack(side="left")
# Prioritäts-Indikator
if prio == 1 and not is_done:
tk.Label(row, text="🔴", font=("Segoe UI", 8), bg=row_bg).pack(side="left")
elif prio == 2 and not is_done:
tk.Label(row, text="🟡", font=("Segoe UI", 8), bg=row_bg).pack(side="left")
# Kategorie-Badge
_cat_colors = {"Allgemein": "#B9ECFA", "MPA": "#D4EEF7", "Ärzte": "#C8E0F0"}
_cat_fg = {"Allgemein": "#4a8aaa", "MPA": "#2E7D5B", "Ärzte": "#5B6DB3"}
if active_cat == "Alle":
cat_text = todo_cat[:3]
cat_bg = _cat_colors.get(todo_cat, "#D0E8F0")
cat_fgc = _cat_fg.get(todo_cat, "#4a8aaa")
tk.Label(row, text=cat_text, font=("Segoe UI", 6),
bg=cat_bg, fg=cat_fgc, padx=3, pady=0,
relief="flat", bd=0).pack(side="left", padx=(0, 2))
# Absender-Info bei empfangenen Aufgaben
if from_sender:
tk.Label(row, text=f"{from_sender}", font=("Segoe UI", 7, "italic"),
bg=row_bg, fg="#7EC8E3").pack(side="left", padx=(0, 4))
# Text (klickbar → öffnet Detail)
text_fg = "#999" if is_done else ("#C03030" if is_overdue else "#1a4d6d")
fs = todo_font_size[0]
text_font = ("Segoe UI", fs, "overstrike") if is_done else ("Segoe UI", fs)
lbl = tk.Label(row, text=todo["text"], font=text_font, bg=row_bg, fg=text_fg,
anchor="w", wraplength=250, justify="left", cursor="hand2")
lbl.pack(side="left", fill="x", expand=True, padx=(4, 4))
lbl.bind("<Button-1>", lambda e, i=idx: open_detail(i))
lbl.bind("<Button-3>", lambda e, i=idx: _show_context_menu(e, i))
# Notiz-Indikator
if todo.get("notes", "").strip():
tk.Label(row, text="📝", font=("Segoe UI", 8), bg=row_bg).pack(side="left")
right_f = tk.Frame(row, bg=row_bg)
right_f.pack(side="right")
# "senden"-Checkbox (unauffällig, rechts)
send_var = tk.BooleanVar(value=False)
send_selection[idx] = send_var
send_cb = tk.Checkbutton(right_f, variable=send_var, bg=row_bg,
activebackground=row_bg, highlightthickness=0, bd=0)
send_cb.pack(side="right", padx=(4, 0))
send_lbl = tk.Label(right_f, text="senden", font=("Segoe UI", 7),
bg=row_bg, fg="#A0C8D8", cursor="hand2")
send_lbl.pack(side="right")
send_lbl.bind("<Button-1>", lambda e, sv=send_var: sv.set(not sv.get()))
if has_date:
try:
dt = datetime.strptime(todo["date"], "%Y-%m-%d")
date_text = dt.strftime("%d.%m.%Y")
except Exception:
date_text = todo["date"]
if is_overdue:
date_fg = "#C03030"
date_text = f"{date_text}"
elif is_done:
date_fg = "#999"
else:
date_fg = "#4a8aaa"
tk.Label(right_f, text=date_text, font=("Segoe UI", 8), bg=row_bg,
fg=date_fg).pack(side="right", padx=(0, 6))
del_lbl = tk.Label(right_f, text="", font=("Segoe UI", 10), bg=row_bg,
fg="#ccc", cursor="hand2")
del_lbl.pack(side="right", padx=(0, 4))
del_lbl.bind("<Enter>", lambda e, b=del_lbl: b.configure(fg="#E87070"))
del_lbl.bind("<Leave>", lambda e, b=del_lbl: b.configure(fg="#ccc"))
del_lbl.bind("<Button-1>", lambda e, i=idx: delete_todo(i))
todo_widgets.append(row)
update_counter()
save_todos(todos)
_bind_mousewheel_recursive(list_frame)
def add_todo(event=None):
text = entry_var.get().strip()
if not text or text == "Neue Aufgabe eingeben oder diktieren…":
return
date_iso = selected_date[0].isoformat() if selected_date[0] else None
new_item = {
"id": int(datetime.now().timestamp() * 1000),
"text": text,
"done": False,
"date": date_iso,
"priority": 0,
"notes": "",
"created": datetime.now().isoformat(),
"category": _get_new_todo_category(),
}
todos.append(new_item)
entry_var.set("")
entry.delete(0, "end")
entry.insert(0, "Neue Aufgabe eingeben oder diktieren…")
entry.configure(fg="#999")
clear_date()
rec_status_var.set("")
rebuild_list()
def toggle_done(idx, var):
if 0 <= idx < len(todos):
todos[idx]["done"] = var.get()
rebuild_list()
def delete_todo(idx):
if 0 <= idx < len(todos):
todos.pop(idx)
rebuild_list()
def delete_done():
i = 0
while i < len(todos):
if todos[i].get("done"):
todos.pop(i)
else:
i += 1
rebuild_list()
add_btn.configure(command=add_todo)
entry.bind("<Return>", add_todo)
btn_del_done.configure(command=delete_done)
rebuild_list()
entry.focus_set()
if _need_initial_rebuild[0] == "notes":
win.after(200, _rebuild_notes_list)
elif _need_initial_rebuild[0] == "checklist":
win.after(200, _rebuild_checklist)
if imported_count > 0:
win.after(300, lambda: messagebox.showinfo(
"Neue Aufgaben",
f"📥 {imported_count} neue Aufgabe(n) empfangen!",
parent=win))