# -*- 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("", _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("", _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("", lambda e: _toggle_minimize_todo()) btn_minimize_todo.bind("", lambda e: btn_minimize_todo.configure(fg="#1a4d6d")) btn_minimize_todo.bind("", 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("", lambda e: _update_todo_font(todo_font_size[0] + 1)) _fs_btn_down.bind("", lambda e: _update_todo_font(todo_font_size[0] - 1)) for _fsw in (_fs_btn_up, _fs_btn_down): _fsw.bind("", lambda e, ww=_fsw: ww.configure(fg=_fs_fg_hover)) _fsw.bind("", 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("", lambda e: _close_info()) close_lbl.bind("", lambda e: close_lbl.configure(fg="#E87070")) close_lbl.bind("", 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("", _show_diktat_info) info_btn.bind("", lambda e: info_btn.configure(bg="#5B8DB3")) info_btn.bind("", 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("", lambda e: _start_todo_server()) phone_btn.bind("", lambda e: phone_btn.configure( bg="#5BDB7B" if _todo_server_running[0] else "#5B8DB3")) phone_btn.bind("", 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("", lambda e: _switch_tab("todo")) tab_btn_notes.bind("", lambda e: _switch_tab("notes")) tab_btn_checklist.bind("", 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("", 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("", 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("", lambda e: _add_custom_category()) _plus_btn.bind("", lambda e: _plus_btn.configure(fg="#1a4d6d")) _plus_btn.bind("", 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("", lambda e: _remove_custom_category()) _minus_btn.bind("", lambda e: _minus_btn.configure(fg="#E87070")) _minus_btn.bind("", 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("", on_entry_focus_in) entry.bind("", 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("", lambda e, dd=d: (set_date(dd), cal_win.destroy())) btn.bind("", lambda e, b=btn: b.configure(bg="#B9ECFA") if b.cget("bg") not in ("#5B8DB3",) else None) btn.bind("", 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("", 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("", 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("", lambda e: clear_date()) clear_date_btn.bind("", lambda e: clear_date_btn.configure(fg="#E87070")) clear_date_btn.bind("", 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("", 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("", _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("", _on_mousewheel) for child in widget.winfo_children(): _bind_mousewheel_recursive(child) canvas.bind("", _on_mousewheel) list_frame.bind("", _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("", _notes_entry_focus_in) notes_title_entry.bind("", _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("", _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("", 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("", _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("", _notes_mousewheel) for child in widget.winfo_children(): _bind_notes_mousewheel_recursive(child) notes_canvas.bind("", _notes_mousewheel) notes_list_frame.bind("", _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("", lambda e, i=idx: _notes_drag_start(e, i)) drag_handle.bind("", lambda e, i=idx: _notes_drag_motion(e, i)) drag_handle.bind("", lambda e, i=idx: _notes_drag_end(e, i)) drag_handle.bind("", lambda e, h=drag_handle: h.configure(fg="#5B8DB3")) drag_handle.bind("", 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("", lambda e, b=del_lbl: b.configure(fg="#E87070")) del_lbl.bind("", lambda e, b=del_lbl: b.configure(fg="#ccc")) del_lbl.bind("", lambda e, i=idx: _delete_note(i)) for _w in [row, title_lbl]: _w.bind("", lambda e, i=idx: _open_note_detail(i)) _w.bind("", lambda e, i=idx: _open_note_detail(i)) if preview_lbl: preview_lbl.bind("", lambda e, i=idx: _open_note_detail(i)) preview_lbl.bind("", 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("", _cl_focus_in) cl_title_entry.bind("", _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("", _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("", 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("", 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("", _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("", lambda e, b=del_btn: b.configure(fg="#E87070")) del_btn.bind("", 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("", 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("", lambda e, b=it_del: b.configure(fg="#E87070")) it_del.bind("", 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("", 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("", lambda e, en=add_entry: (en.delete(0, "end"), en.configure(fg="#1a4d6d")) if en.get() == "Neuer Punkt…" else None) add_entry.bind("", 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("", 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("", _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("", 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("", 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("", 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("", 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("", lambda e, i=idx: _drag_start(e, i)) drag_handle.bind("", lambda e, i=idx: _drag_motion(e, i)) drag_handle.bind("", lambda e, i=idx: _drag_end(e, i)) drag_handle.bind("", lambda e, h=drag_handle: h.configure(fg="#5B8DB3")) drag_handle.bind("", 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("", lambda e, i=idx: open_detail(i)) lbl.bind("", 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("", 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("", lambda e, b=del_lbl: b.configure(fg="#E87070")) del_lbl.bind("", lambda e, b=del_lbl: b.configure(fg="#ccc")) del_lbl.bind("", 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("", 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))