Files
aza/AzA march 2026 - Kopie (16)/aza_todo_mixin.py
2026-04-19 20:41:37 +02:00

2733 lines
127 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
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))