2026-06-10 22:55:03 +02:00
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""
|
|
|
|
|
|
aza_doku_vorlagen.py — Doku-Prompt-Fenster (AzA-Stil)
|
|
|
|
|
|
|
|
|
|
|
|
Zentrale Prompt-Verwaltung fuer medizinische Dokumente (UI: Doku-Prompt).
|
|
|
|
|
|
Interne Datei/IDs bleiben aus Kompatibilitaetsgruenden unveraendert.
|
|
|
|
|
|
|
|
|
|
|
|
Kernprinzipien:
|
|
|
|
|
|
- AzA-Grundvorlage ist Systemvorlage und wird nicht direkt ueberschrieben.
|
|
|
|
|
|
- Benutzer darf den Inhalt im Editor bearbeiten und als neue Vorlage speichern.
|
|
|
|
|
|
- Immer genau eine aktive Vorlage pro Dokumenttyp.
|
|
|
|
|
|
- Alle Daten lokal in aza_document_templates.json.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
import os
|
|
|
|
|
|
import re
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
import time
|
|
|
|
|
|
import tkinter as tk
|
|
|
|
|
|
from datetime import datetime, timezone
|
2026-06-13 22:47:31 +02:00
|
|
|
|
from tkinter import ttk
|
2026-06-10 22:55:03 +02:00
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
2026-06-13 22:47:31 +02:00
|
|
|
|
from aza_ui_helpers import (
|
|
|
|
|
|
aza_askstring,
|
|
|
|
|
|
aza_askyesno,
|
|
|
|
|
|
aza_showerror,
|
|
|
|
|
|
aza_showinfo,
|
|
|
|
|
|
aza_showwarning,
|
|
|
|
|
|
make_modal_topmost,
|
|
|
|
|
|
release_modal_dialog,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-10 22:55:03 +02:00
|
|
|
|
SCHEMA_VERSION = 2
|
|
|
|
|
|
MAX_PROMPT_CHARS = 32000
|
|
|
|
|
|
|
|
|
|
|
|
# ── AzA-Designkonstanten (identisch mit KI-Guthaben-Fenster) ──────────────────
|
|
|
|
|
|
_BG = "#E8F4FA"
|
|
|
|
|
|
_HDR_BG = "#1A4D6D"
|
|
|
|
|
|
_HDR_FG = "#FFFFFF"
|
|
|
|
|
|
_SUB_BG = "#D0E8F5"
|
|
|
|
|
|
_CARD_BG = "#FFFFFF"
|
|
|
|
|
|
_CARD_BD = "#C8D8E8"
|
|
|
|
|
|
_ACCENT = "#5B8DB3"
|
|
|
|
|
|
_TEXT = "#1A3D55"
|
|
|
|
|
|
_TEXT_SUB = "#607890"
|
|
|
|
|
|
_BTN_BLUE = "#5B8DB3"
|
|
|
|
|
|
_BTN_ACT = "#5B8DB3" # Einheitlich AzA-Blau (wie Speichern/Als neue Vorlage)
|
|
|
|
|
|
_BTN_RESET = "#C04040" # Rot nur fuer destruktive Aktion "Zurücksetzen"
|
|
|
|
|
|
_BTN_CLOSE = "#5B8DB3" # Einheitlich AzA-Blau
|
|
|
|
|
|
_FF = "Segoe UI"
|
|
|
|
|
|
|
|
|
|
|
|
# ── Dokumenttypen (Reihenfolge exakt laut Spezifikation) ─────────────────────
|
|
|
|
|
|
DOC_TYPES = [
|
|
|
|
|
|
("kg", "Krankengeschichte"),
|
|
|
|
|
|
("verlauf", "Verlauf"),
|
|
|
|
|
|
("brief", "Brief"),
|
|
|
|
|
|
("rezept", "Rezept"),
|
|
|
|
|
|
("op_bericht", "OP-Bericht"),
|
|
|
|
|
|
("kogu", "KOGU"),
|
|
|
|
|
|
("arztzeugnis", "Arztzeugnis"),
|
|
|
|
|
|
("eigenes_dokument", "Eigenes Dokument"),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
DOC_TYPE_KEYS = [dt for dt, _ in DOC_TYPES]
|
|
|
|
|
|
|
|
|
|
|
|
# Status-Label pro Dokumenttyp (für dynamische "Erstelle …"-Statusmeldung).
|
|
|
|
|
|
DOC_TYPE_STATUS_LABELS = {
|
|
|
|
|
|
"kg": "Krankengeschichte",
|
|
|
|
|
|
"verlauf": "Verlauf",
|
|
|
|
|
|
"brief": "Brief",
|
|
|
|
|
|
"rezept": "Rezept",
|
|
|
|
|
|
"op_bericht": "OP-Bericht",
|
|
|
|
|
|
"kogu": "KOGU",
|
|
|
|
|
|
"arztzeugnis": "Arztzeugnis",
|
|
|
|
|
|
"eigenes_dokument": "Eigenes Dokument",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def doc_type_status_label(doc_type: str) -> str:
|
|
|
|
|
|
"""Reines Mapping doc_type → Anzeigename für Statusmeldungen."""
|
|
|
|
|
|
return DOC_TYPE_STATUS_LABELS.get(doc_type or "kg", "Krankengeschichte")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolve_doc_generation_route(doc_type: str) -> str:
|
|
|
|
|
|
"""Reines Routing: welcher Generierungspfad gehört zum Dokumenttyp.
|
|
|
|
|
|
|
|
|
|
|
|
Rückgabe:
|
|
|
|
|
|
"kg" → Krankengeschichte-Generator (summarize_text)
|
|
|
|
|
|
"form" → Formularpfad (Arztzeugnis)
|
|
|
|
|
|
"generate" → generate_document_to_main_field(doc_type)
|
|
|
|
|
|
"""
|
|
|
|
|
|
dt = doc_type or "kg"
|
|
|
|
|
|
if dt == "kg":
|
|
|
|
|
|
return "kg"
|
|
|
|
|
|
if dt == "arztzeugnis":
|
|
|
|
|
|
return "form"
|
|
|
|
|
|
if dt in ("verlauf", "brief", "rezept", "op_bericht", "kogu", "eigenes_dokument"):
|
|
|
|
|
|
return "generate"
|
|
|
|
|
|
return "kg"
|
|
|
|
|
|
|
|
|
|
|
|
DOC_TYPE_CREATE_LABELS = {
|
|
|
|
|
|
"kg": "KG erstellen",
|
|
|
|
|
|
"verlauf": "Verlauf erstellen",
|
|
|
|
|
|
"brief": "Brief erstellen",
|
|
|
|
|
|
"rezept": "Rezept erstellen",
|
|
|
|
|
|
"op_bericht": "OP-Bericht erstellen",
|
|
|
|
|
|
"kogu": "KOGU erstellen",
|
|
|
|
|
|
"arztzeugnis": "Arztzeugnis erstellen",
|
|
|
|
|
|
"eigenes_dokument": "Dokument erstellen",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
DOC_TYPE_COPY_LABELS = {
|
|
|
|
|
|
"kg": "KG kopieren",
|
|
|
|
|
|
"verlauf": "Verlauf kopieren",
|
|
|
|
|
|
"brief": "Brief kopieren",
|
|
|
|
|
|
"rezept": "Rezept kopieren",
|
|
|
|
|
|
"op_bericht": "OP-Bericht kopieren",
|
|
|
|
|
|
"kogu": "KOGU kopieren",
|
|
|
|
|
|
"arztzeugnis": "Arztzeugnis kopieren",
|
|
|
|
|
|
"eigenes_dokument": "Dokument kopieren",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 22:47:31 +02:00
|
|
|
|
# Kurzlabels fürs Hauptfenster (verhindern abgeschnittene Buttons bei langen Typen).
|
|
|
|
|
|
DOC_TYPE_CREATE_LABELS_SHORT = {
|
|
|
|
|
|
"kg": "KG erstellen",
|
|
|
|
|
|
"verlauf": "Verl. erst.",
|
|
|
|
|
|
"brief": "Brief erst.",
|
|
|
|
|
|
"rezept": "Rezept erst.",
|
|
|
|
|
|
"op_bericht": "OP erst.",
|
|
|
|
|
|
"kogu": "KOGU erst.",
|
|
|
|
|
|
"arztzeugnis": "Zeugnis",
|
|
|
|
|
|
"eigenes_dokument": "Eig. Dok. erst.",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
DOC_TYPE_COPY_LABELS_SHORT = {
|
|
|
|
|
|
"kg": "KG kopieren",
|
|
|
|
|
|
"verlauf": "Verl. kop.",
|
|
|
|
|
|
"brief": "Brief kop.",
|
|
|
|
|
|
"rezept": "Rezept kop.",
|
|
|
|
|
|
"op_bericht": "OP kop.",
|
|
|
|
|
|
"kogu": "KOGU kop.",
|
|
|
|
|
|
"arztzeugnis": "Zeugnis kop.",
|
|
|
|
|
|
"eigenes_dokument": "Eig. Dok. kop.",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_local_publish_payload(payload: dict) -> None:
|
|
|
|
|
|
"""Bereitet eine Veröffentlichung lokal vor (Queue im Daten-Verzeichnis).
|
|
|
|
|
|
|
|
|
|
|
|
So geht beim fehlenden Server-Deploy nichts verloren und es entsteht keine
|
|
|
|
|
|
falsche Erfolgsmeldung.
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
import os
|
|
|
|
|
|
from aza_config import get_writable_data_dir
|
|
|
|
|
|
path = os.path.join(get_writable_data_dir(), "doku_prompt_publish_queue.json")
|
|
|
|
|
|
existing = []
|
|
|
|
|
|
if os.path.isfile(path):
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(path, encoding="utf-8") as fh:
|
|
|
|
|
|
existing = json.load(fh) or []
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
existing = []
|
|
|
|
|
|
if not isinstance(existing, list):
|
|
|
|
|
|
existing = []
|
|
|
|
|
|
existing.append(payload)
|
|
|
|
|
|
tmp = path + ".tmp"
|
|
|
|
|
|
with open(tmp, "w", encoding="utf-8") as fh:
|
|
|
|
|
|
json.dump(existing, fh, ensure_ascii=False, indent=2)
|
|
|
|
|
|
os.replace(tmp, path)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _publish_queue_path() -> str:
|
|
|
|
|
|
from aza_config import get_writable_data_dir
|
|
|
|
|
|
return os.path.join(get_writable_data_dir(), "doku_prompt_publish_queue.json")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _read_publish_queue() -> list[dict[str, Any]]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
path = _publish_queue_path()
|
|
|
|
|
|
if not os.path.isfile(path):
|
|
|
|
|
|
return []
|
|
|
|
|
|
with open(path, encoding="utf-8") as fh:
|
|
|
|
|
|
raw = json.load(fh) or []
|
|
|
|
|
|
return raw if isinstance(raw, list) else []
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _write_publish_queue(items: list[dict[str, Any]]) -> None:
|
|
|
|
|
|
path = _publish_queue_path()
|
|
|
|
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
|
|
|
|
tmp = path + ".tmp"
|
|
|
|
|
|
with open(tmp, "w", encoding="utf-8") as fh:
|
|
|
|
|
|
json.dump(items, fh, ensure_ascii=False, indent=2)
|
|
|
|
|
|
os.replace(tmp, path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_local_publish_queue_item(queue_index: int, updates: dict[str, Any]) -> bool:
|
|
|
|
|
|
"""Aktualisiert einen Eintrag der lokalen Publish-Queue (Metadaten-Bearbeitung)."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
items = _read_publish_queue()
|
|
|
|
|
|
if queue_index < 0 or queue_index >= len(items):
|
|
|
|
|
|
return False
|
|
|
|
|
|
if not isinstance(items[queue_index], dict):
|
|
|
|
|
|
return False
|
|
|
|
|
|
items[queue_index].update(updates)
|
|
|
|
|
|
_write_publish_queue(items)
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def remove_local_publish_queue_item(queue_index: int) -> bool:
|
|
|
|
|
|
"""Entfernt einen Eintrag aus der lokalen Publish-Queue (eigene Veröffentlichung)."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
items = _read_publish_queue()
|
|
|
|
|
|
if queue_index < 0 or queue_index >= len(items):
|
|
|
|
|
|
return False
|
|
|
|
|
|
del items[queue_index]
|
|
|
|
|
|
_write_publish_queue(items)
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def public_item_search_text(item: dict[str, Any]) -> str:
|
|
|
|
|
|
"""Suchtext für lokale Filterung öffentlicher Vorlagen."""
|
|
|
|
|
|
if not isinstance(item, dict):
|
|
|
|
|
|
return ""
|
|
|
|
|
|
parts = [
|
|
|
|
|
|
item.get("doc_type") or "",
|
|
|
|
|
|
doc_type_label(item.get("doc_type") or ""),
|
|
|
|
|
|
item.get("title") or "",
|
|
|
|
|
|
item.get("author_display") or item.get("author") or "",
|
|
|
|
|
|
item.get("location") or item.get("city") or "",
|
|
|
|
|
|
item.get("specialty") or "",
|
|
|
|
|
|
item.get("description") or "",
|
|
|
|
|
|
item.get("content") or "",
|
|
|
|
|
|
]
|
|
|
|
|
|
return " ".join(str(p) for p in parts).lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def filter_public_items(items: list[dict[str, Any]], query: str) -> list[dict[str, Any]]:
|
|
|
|
|
|
"""Filtert öffentliche Vorlagen lokal nach Metadaten und Prompttext."""
|
|
|
|
|
|
q = (query or "").strip().lower()
|
|
|
|
|
|
if not q:
|
|
|
|
|
|
return list(items)
|
|
|
|
|
|
return [it for it in items if isinstance(it, dict) and q in public_item_search_text(it)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_local_prepared_public_items() -> list[dict[str, Any]]:
|
|
|
|
|
|
"""Lokale Publish-Queue als Platzhalter für öffentliche Vorlagen (kein Server-Deploy)."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
raw = _read_publish_queue()
|
|
|
|
|
|
items: list[dict[str, Any]] = []
|
|
|
|
|
|
for idx, p in enumerate(raw):
|
|
|
|
|
|
if not isinstance(p, dict):
|
|
|
|
|
|
continue
|
|
|
|
|
|
dt = p.get("document_type_key") or ""
|
|
|
|
|
|
items.append({
|
|
|
|
|
|
"doc_type": dt,
|
|
|
|
|
|
"title": p.get("title") or "Ohne Titel",
|
|
|
|
|
|
"author_display": p.get("author_display_name") or "",
|
|
|
|
|
|
"author": p.get("author_display_name") or "",
|
|
|
|
|
|
"author_initials": p.get("author_initials") or "",
|
|
|
|
|
|
"author_user_id": p.get("author_user_id") or "",
|
|
|
|
|
|
"location": p.get("region") or "",
|
|
|
|
|
|
"city": p.get("region") or "",
|
|
|
|
|
|
"specialty": p.get("specialty") or "",
|
|
|
|
|
|
"description": p.get("description") or "",
|
|
|
|
|
|
"content": p.get("prompt_text") or "",
|
|
|
|
|
|
"id": p.get("source_template_id") or "",
|
|
|
|
|
|
"server_id": "",
|
|
|
|
|
|
"_local_prepared": True,
|
|
|
|
|
|
"_queue_index": idx,
|
|
|
|
|
|
})
|
|
|
|
|
|
return items
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolve_current_user_id(app: Any) -> str:
|
|
|
|
|
|
uid, _ = _app_user_practice_ids(app)
|
|
|
|
|
|
return uid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_own_public_item(app: Any, item: dict[str, Any]) -> bool:
|
|
|
|
|
|
"""Eigene Veröffentlichung erkennen (user_id oder Autoren-Fallback bei lokaler Queue)."""
|
|
|
|
|
|
if not isinstance(item, dict):
|
|
|
|
|
|
return False
|
|
|
|
|
|
uid = resolve_current_user_id(app)
|
|
|
|
|
|
item_uid = str(item.get("author_user_id") or "").strip()
|
|
|
|
|
|
if uid and item_uid:
|
|
|
|
|
|
return item_uid == uid
|
|
|
|
|
|
if item.get("_local_prepared"):
|
|
|
|
|
|
me = resolve_default_author_display(app).strip().lower()
|
|
|
|
|
|
theirs = str(item.get("author_display") or item.get("author") or "").strip().lower()
|
|
|
|
|
|
if me and theirs and me == theirs:
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_rating_average_de(avg: float | None) -> str:
|
|
|
|
|
|
if avg is None:
|
|
|
|
|
|
return "—"
|
|
|
|
|
|
return f"{float(avg):.1f}".replace(".", ",")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_rating_count_label(count: int) -> str:
|
|
|
|
|
|
n = int(count or 0)
|
|
|
|
|
|
if n == 1:
|
|
|
|
|
|
return "1 Bewertung"
|
|
|
|
|
|
return f"{n} Bewertungen"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_star_unicode(avg: float | None) -> str:
|
|
|
|
|
|
if avg is None or float(avg) <= 0:
|
|
|
|
|
|
return "☆☆☆☆☆"
|
|
|
|
|
|
steps = max(0, min(10, int(round(float(avg) * 2))))
|
|
|
|
|
|
full = steps // 2
|
|
|
|
|
|
half = steps % 2 == 1
|
|
|
|
|
|
chars = "★" * full
|
|
|
|
|
|
if half:
|
|
|
|
|
|
chars += "½"
|
|
|
|
|
|
chars += "☆" * (5 - full - (1 if half else 0))
|
|
|
|
|
|
return chars
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_public_rating_summary(avg: float | None, count: int) -> str:
|
|
|
|
|
|
n = int(count or 0)
|
|
|
|
|
|
if n <= 0:
|
|
|
|
|
|
return "Noch keine Bewertungen · 0 Bewertungen"
|
|
|
|
|
|
return f"{render_star_unicode(avg)} {format_rating_average_de(avg)} · {format_rating_count_label(n)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_public_rating_list_text(avg: float | None, count: int) -> str:
|
|
|
|
|
|
n = int(count or 0)
|
|
|
|
|
|
if n <= 0:
|
|
|
|
|
|
return "Noch keine Bewertungen"
|
|
|
|
|
|
return f"{render_star_unicode(avg)} {format_rating_average_de(avg)} ({format_rating_count_label(n)})"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_rating_fields_to_item(item: dict[str, Any], payload: dict[str, Any]) -> None:
|
|
|
|
|
|
if not isinstance(item, dict) or not isinstance(payload, dict):
|
|
|
|
|
|
return
|
|
|
|
|
|
for key in ("rating_average", "rating_count", "my_rating", "can_rate"):
|
|
|
|
|
|
if key in payload:
|
|
|
|
|
|
item[key] = payload[key]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_half_star_rating_bar(
|
|
|
|
|
|
parent: tk.Misc,
|
|
|
|
|
|
*,
|
|
|
|
|
|
bg: str = _SUB_BG,
|
|
|
|
|
|
on_save,
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""Interaktive Halbstern-Leiste (Klick waehlt, Speichern sendet)."""
|
|
|
|
|
|
frame = tk.Frame(parent, bg=bg)
|
|
|
|
|
|
state: dict[str, Any] = {"value": 0.0, "enabled": True, "busy": False}
|
|
|
|
|
|
value_var = tk.StringVar(value="")
|
|
|
|
|
|
star_canvas = tk.Canvas(frame, width=118, height=26, bg=bg, highlightthickness=0, cursor="hand2")
|
|
|
|
|
|
star_canvas.pack(side="left")
|
|
|
|
|
|
btn_save = tk.Button(
|
|
|
|
|
|
frame, text="Speichern", font=(_FF, 8), bg=_BTN_BLUE, fg="white",
|
|
|
|
|
|
activebackground="#4A7A9B", relief="flat", padx=8, pady=2, cursor="hand2",
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_save.pack(side="left", padx=(8, 0))
|
|
|
|
|
|
|
|
|
|
|
|
def _draw() -> None:
|
|
|
|
|
|
star_canvas.delete("all")
|
|
|
|
|
|
val = float(state.get("value") or 0)
|
|
|
|
|
|
for i in range(5):
|
|
|
|
|
|
x0 = i * 22 + 2
|
|
|
|
|
|
mid = x0 + 11
|
|
|
|
|
|
left_active = val >= (i + 0.5)
|
|
|
|
|
|
right_active = val >= (i + 1.0)
|
|
|
|
|
|
star_canvas.create_text(
|
|
|
|
|
|
mid - 5, 13, text="★" if left_active else "☆",
|
|
|
|
|
|
font=(_FF, 13), fill="#C8921A" if left_active else "#B0BEC5",
|
|
|
|
|
|
)
|
|
|
|
|
|
star_canvas.create_text(
|
|
|
|
|
|
mid + 5, 13, text="★" if right_active else "☆",
|
|
|
|
|
|
font=(_FF, 13), fill="#C8921A" if right_active else "#B0BEC5",
|
|
|
|
|
|
)
|
|
|
|
|
|
if val > 0:
|
|
|
|
|
|
value_var.set(f"{format_rating_average_de(val)} Sterne")
|
|
|
|
|
|
else:
|
|
|
|
|
|
value_var.set("Bitte Sterne wählen")
|
|
|
|
|
|
|
|
|
|
|
|
def _pick(event) -> None:
|
|
|
|
|
|
if not state.get("enabled") or state.get("busy"):
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
x = max(0, min(109, int(event.x)))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return
|
|
|
|
|
|
step = int(x / 11) + 1
|
|
|
|
|
|
state["value"] = min(5.0, max(0.5, step * 0.5))
|
|
|
|
|
|
_draw()
|
|
|
|
|
|
|
|
|
|
|
|
def _save() -> None:
|
|
|
|
|
|
if not state.get("enabled") or state.get("busy"):
|
|
|
|
|
|
return
|
|
|
|
|
|
val = float(state.get("value") or 0)
|
|
|
|
|
|
if val <= 0:
|
|
|
|
|
|
return
|
|
|
|
|
|
state["busy"] = True
|
|
|
|
|
|
try:
|
|
|
|
|
|
btn_save.config(state="disabled")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
try:
|
|
|
|
|
|
on_save(val)
|
|
|
|
|
|
finally:
|
|
|
|
|
|
state["busy"] = False
|
|
|
|
|
|
try:
|
|
|
|
|
|
btn_save.config(state="normal" if state.get("enabled") else "disabled")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
star_canvas.bind("<Button-1>", _pick)
|
|
|
|
|
|
btn_save.config(command=_save)
|
|
|
|
|
|
|
|
|
|
|
|
def set_value(v: float | None) -> None:
|
|
|
|
|
|
state["value"] = float(v or 0)
|
|
|
|
|
|
_draw()
|
|
|
|
|
|
|
|
|
|
|
|
def set_enabled(enabled: bool) -> None:
|
|
|
|
|
|
state["enabled"] = bool(enabled)
|
|
|
|
|
|
try:
|
|
|
|
|
|
star_canvas.config(cursor="hand2" if enabled else "arrow")
|
|
|
|
|
|
btn_save.config(state="normal" if enabled else "disabled")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
_draw()
|
|
|
|
|
|
return {"frame": frame, "value_var": value_var, "set_value": set_value, "set_enabled": set_enabled}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def refresh_doku_prompt_window(app: Any, doc_type: str | None = None, tpl_id: str | None = None) -> None:
|
|
|
|
|
|
"""Aktualisiert das offene Doku-Prompt-Fenster (Dropdown nach Übernahme)."""
|
|
|
|
|
|
cb = getattr(app, "_doku_vorlage_refresh_cb", None)
|
|
|
|
|
|
if not callable(cb):
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
def _do_refresh():
|
|
|
|
|
|
try:
|
|
|
|
|
|
cb(doc_type, tpl_id)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
app.after(0, _do_refresh)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
_do_refresh()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def adopt_public_template_to_own(app: Any, item: dict[str, Any], *, activate: bool = True) -> str | None:
|
|
|
|
|
|
"""Übernimmt öffentliche Vorlage in denselben Speicherpfad wie „Als neue Vorlage“."""
|
|
|
|
|
|
dt = (item.get("doc_type") or "kg").strip()
|
|
|
|
|
|
if dt not in DOC_TYPE_KEYS:
|
|
|
|
|
|
dt = "kg"
|
|
|
|
|
|
uid, pid = _app_user_practice_ids(app)
|
|
|
|
|
|
new_id = copy_public_template_as_own(
|
|
|
|
|
|
dt,
|
|
|
|
|
|
content=item.get("content") or "",
|
|
|
|
|
|
title=item.get("title") or "Übernommene Vorlage",
|
|
|
|
|
|
description=item.get("description") or "",
|
|
|
|
|
|
source_author=item.get("author_display") or item.get("author") or "",
|
|
|
|
|
|
source_server_id=item.get("id") or item.get("server_id") or "",
|
|
|
|
|
|
user_id=uid,
|
|
|
|
|
|
practice_id=pid,
|
|
|
|
|
|
activate=activate,
|
|
|
|
|
|
)
|
|
|
|
|
|
if new_id:
|
|
|
|
|
|
refresh_doku_prompt_window(app, doc_type=dt, tpl_id=new_id)
|
|
|
|
|
|
return new_id
|
|
|
|
|
|
|
2026-06-10 22:55:03 +02:00
|
|
|
|
VERLAUF_DEFAULT_PROMPT = (
|
|
|
|
|
|
"Erstelle aus dem Transkript einen präzisen medizinischen Verlaufsbericht. "
|
|
|
|
|
|
"Beschreibe die Entwicklung seit der letzten Konsultation, den aktuellen subjektiven Zustand, "
|
|
|
|
|
|
"relevante neue oder persistierende Beschwerden, objektive Befunde, relevante Resultate, "
|
|
|
|
|
|
"die Wirkung und Verträglichkeit der bisherigen Therapie, erfolgte Therapieanpassungen "
|
|
|
|
|
|
"sowie das weitere Vorgehen. Schreibe chronologisch, sachlich und knapp. "
|
|
|
|
|
|
"Wiederhole die vollständige Anamnese nicht unnötig. "
|
|
|
|
|
|
"Verwende ausschliesslich Angaben aus dem Transkript und dem vorhandenen Kontext. "
|
|
|
|
|
|
"Erfinde keine Diagnosen, Befunde, Medikamente oder Massnahmen. "
|
|
|
|
|
|
"Gliedere sinnvoll in Verlauf, Befunde, Beurteilung und weiteres Vorgehen, "
|
|
|
|
|
|
"sofern genügend Informationen vorhanden sind."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
EIGENES_DOKUMENT_DEFAULT_PROMPT = (
|
|
|
|
|
|
"Erstelle aus dem Transkript ein medizinisches Dokument gemäss den untenstehenden "
|
|
|
|
|
|
"Anweisungen. Verwende ausschliesslich Informationen aus dem Transkript und dem "
|
|
|
|
|
|
"vorhandenen Kontext. Erfinde keine Diagnosen, Befunde, Medikamente oder Massnahmen. "
|
|
|
|
|
|
"Schreibe sachlich, professionell und in Schweizer medizinischer Schreibweise."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ── AzA-Grundvorlagen (verständlich formuliert, für Ärzte) ────────────────────
|
|
|
|
|
|
AZA_DEFAULT_TEMPLATES: dict[str, str] = {
|
|
|
|
|
|
"kg": (
|
|
|
|
|
|
"Reihenfolge: 1. Diagnose, 2. Anamnese, 3. Befund, 4. Therapie, 5. Procedere\n\n"
|
|
|
|
|
|
"Schreibregeln:\n"
|
|
|
|
|
|
"- Aus der Transkription eine klare medizinische Krankengeschichte erstellen.\n"
|
|
|
|
|
|
"- Diagnosen nummerieren (1. 2. 3. ...). KEIN Bulletpoint vor der Nummer.\n"
|
|
|
|
|
|
" Richtig: 1. Diagnose Falsch: • 1. Diagnose\n"
|
|
|
|
|
|
"- Wenn 'Diagnosen gliedern' aktiv ist: Therapien möglichst direkt unter die\n"
|
|
|
|
|
|
" passende Diagnose schreiben, wenn eindeutig zuordenbar.\n"
|
|
|
|
|
|
"- Unklare Therapien separat unter 'Therapie' aufführen.\n"
|
|
|
|
|
|
"- Verlauf, Kontrollen und Wiedervorstellung unter 'Procedere', nicht unter 'Therapie'.\n"
|
|
|
|
|
|
"- Medikamente mit Wirkstoff in Klammern, wenn sicher bekannt:\n"
|
|
|
|
|
|
" Dermovate Creme (Clobetasol), Dafalgan (Paracetamol), Fucidin (Fusidinsäure).\n"
|
|
|
|
|
|
"- 'Status nach' als Standard verwenden, nicht 'Zustand nach'.\n"
|
|
|
|
|
|
"- ICD-Codes aus dem Input übernehmen, wenn vorhanden. Keine ICD-Codes erfinden.\n"
|
|
|
|
|
|
"- Keine Diagnosen, Therapien oder Daten erfinden.\n"
|
|
|
|
|
|
"- Keine genannten Diagnosen weglassen.\n"
|
|
|
|
|
|
"- Unterpunkte unter Diagnosen einrücken (Bindestrich).\n"
|
|
|
|
|
|
"- Schreibstil: medizinisch, professionell, kompakt.\n\n"
|
|
|
|
|
|
"Hinweis: Die Diagnose-/Therapie-Gliederung (Therapie direkt unter Diagnose,\n"
|
|
|
|
|
|
"Procedere getrennt) wird zusätzlich durch das Häkchen 'Diagnosen gliedern' gesteuert."
|
|
|
|
|
|
),
|
|
|
|
|
|
"brief": (
|
|
|
|
|
|
"Reihenfolge: 1. Diagnose, 2. Anlass, 3. Befunde, 4. Therapie, 5. Procedere/Empfehlung\n\n"
|
|
|
|
|
|
"Schreibregeln:\n"
|
|
|
|
|
|
"- Diagnosen nummerieren (1. 2. 3. ...).\n"
|
|
|
|
|
|
"- Wenn 'Diagnosen gliedern' aktiv ist: Therapien möglichst direkt\n"
|
|
|
|
|
|
" unter die passende Diagnose schreiben.\n"
|
|
|
|
|
|
"- Verlauf, Kontrollen und Wiedervorstellung unter Procedere,\n"
|
|
|
|
|
|
" nicht unter Therapie.\n"
|
|
|
|
|
|
"- Medikamente mit Wirkstoff in Klammern angeben, wenn sicher bekannt.\n"
|
|
|
|
|
|
" Beispiel: Dermovate Creme (Clobetasol), Dafalgan (Paracetamol).\n"
|
|
|
|
|
|
"- 'Status nach' als Standard verwenden, nicht 'Zustand nach'.\n"
|
|
|
|
|
|
"- ICD-Codes aus dem Input übernehmen, wenn vorhanden.\n"
|
|
|
|
|
|
"- Nichts erfinden. Medizinisch, professionell und kompakt schreiben.\n\n"
|
|
|
|
|
|
"Hinweis: Die Diagnose-/Therapie-Gliederung (Therapie direkt unter\n"
|
|
|
|
|
|
"Diagnose, Procedere getrennt) wird zusätzlich durch das Häkchen\n"
|
|
|
|
|
|
"'Diagnosen gliedern' gesteuert."
|
|
|
|
|
|
),
|
|
|
|
|
|
"rezept": (
|
|
|
|
|
|
"Rezeptvorlage – Schreibregeln:\n\n"
|
|
|
|
|
|
"- Nur Medikamente aufführen, die im Transkript genannt wurden.\n"
|
|
|
|
|
|
"- Dosierung nur aus dem Transkript übernehmen, nichts erfinden.\n"
|
|
|
|
|
|
"- Markenname mit Wirkstoff in Klammern angeben, wenn sicher bekannt.\n"
|
|
|
|
|
|
" Beispiel: Dermovate Creme (Clobetasol) 2x täglich.\n"
|
|
|
|
|
|
"- Generikum bevorzugen, wenn nur der Wirkstoff genannt wurde.\n"
|
|
|
|
|
|
"- Keine ICD-Codes erfinden, wenn nicht vorhanden."
|
|
|
|
|
|
),
|
|
|
|
|
|
"op_bericht": (
|
|
|
|
|
|
"OP-Bericht / Eingriffsbericht – Reihenfolge:\n\n"
|
|
|
|
|
|
"1. Diagnose (mit ICD-Code, falls vorhanden)\n"
|
|
|
|
|
|
"2. Indikation\n"
|
|
|
|
|
|
"3. Eingriff / Prozedur\n"
|
|
|
|
|
|
"4. Befund / Histologie\n"
|
|
|
|
|
|
"5. Verlauf / Procedere\n\n"
|
|
|
|
|
|
"Schreibregeln:\n"
|
|
|
|
|
|
"- 'Status nach' als Standard, nicht 'Zustand nach'.\n"
|
|
|
|
|
|
"- Keine Befunde oder Histologien erfinden.\n"
|
|
|
|
|
|
"- ICD-Codes nur übernehmen, wenn im Transkript genannt.\n"
|
|
|
|
|
|
"- Medizinisch korrekter Fachstil."
|
|
|
|
|
|
),
|
|
|
|
|
|
"kogu": (
|
|
|
|
|
|
"Kostengutsprache – Reihenfolge:\n\n"
|
|
|
|
|
|
"1. Patient / Versicherung\n"
|
|
|
|
|
|
"2. Diagnose (ICD-Code falls vorhanden)\n"
|
|
|
|
|
|
"3. Klinische Begründung / Notwendigkeit\n"
|
|
|
|
|
|
"4. Beantragte Massnahme / Behandlung\n"
|
|
|
|
|
|
"5. Kosten / Tarifpositionen (nur wenn im Transkript genannt)\n"
|
|
|
|
|
|
"6. Datum / Unterschrift\n\n"
|
|
|
|
|
|
"Schreibregeln:\n"
|
|
|
|
|
|
"- Kosten und Tarifpositionen nur aufnehmen, wenn genannt.\n"
|
|
|
|
|
|
"- Medizinische Begründung sachlich und klinisch formulieren.\n"
|
|
|
|
|
|
"- Keine Diagnosen oder Befunde erfinden."
|
|
|
|
|
|
),
|
|
|
|
|
|
"arztzeugnis": (
|
|
|
|
|
|
"Arztzeugnis – Reihenfolge:\n\n"
|
|
|
|
|
|
"1. Angaben Patient/in\n"
|
|
|
|
|
|
"2. Diagnose (ICD-Code falls vorhanden)\n"
|
|
|
|
|
|
"3. Behandlungszeitraum\n"
|
|
|
|
|
|
"4. Arbeitsfähigkeit / Arbeitsunfähigkeit\n"
|
|
|
|
|
|
"5. Besondere Anmerkungen (nur aus Transkript)\n"
|
|
|
|
|
|
"6. Datum, Arzt/Ärztin\n\n"
|
|
|
|
|
|
"Schreibregeln:\n"
|
|
|
|
|
|
"- Zeiträume nur aus Transkript übernehmen, nichts erfinden.\n"
|
|
|
|
|
|
"- Diagnose medizinisch korrekt, ICD-Code wenn vorhanden.\n"
|
|
|
|
|
|
"- Sachlicher, formeller Schreibstil."
|
|
|
|
|
|
),
|
|
|
|
|
|
"verlauf": VERLAUF_DEFAULT_PROMPT,
|
|
|
|
|
|
"eigenes_dokument": EIGENES_DOKUMENT_DEFAULT_PROMPT,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Persistenz ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def _templates_json_path() -> str:
|
|
|
|
|
|
try:
|
|
|
|
|
|
from aza_config import get_writable_data_dir
|
|
|
|
|
|
return os.path.join(get_writable_data_dir(), "aza_document_templates.json")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return os.path.join(os.path.expanduser("~"), "AppData", "Roaming",
|
|
|
|
|
|
"AZA Desktop", "aza_document_templates.json")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _utc_now() -> str:
|
|
|
|
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def sanitize_prompt_text(text: str) -> str:
|
|
|
|
|
|
"""Plaintext only — keine HTML/Script-Injection."""
|
|
|
|
|
|
if not text:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
s = str(text)
|
|
|
|
|
|
s = re.sub(r"(?is)<script[^>]*>.*?</script>", "", s)
|
|
|
|
|
|
s = re.sub(r"(?is)<style[^>]*>.*?</style>", "", s)
|
|
|
|
|
|
s = re.sub(r"<[^>]+>", "", s)
|
|
|
|
|
|
s = re.sub(r"(?i)javascript\s*:", "", s)
|
|
|
|
|
|
s = re.sub(r"(?i)data\s*:", "", s)
|
|
|
|
|
|
s = s.replace("\x00", "")
|
|
|
|
|
|
if len(s) > MAX_PROMPT_CHARS:
|
|
|
|
|
|
s = s[:MAX_PROMPT_CHARS]
|
|
|
|
|
|
return s.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _new_template_entry(doc_type: str, *, now: str | None = None) -> dict[str, Any]:
|
|
|
|
|
|
ts = now or _utc_now()
|
|
|
|
|
|
return {
|
|
|
|
|
|
"id": "aza_default",
|
|
|
|
|
|
"name": "AzA-Grundvorlage",
|
|
|
|
|
|
"is_system": True,
|
|
|
|
|
|
"content": AZA_DEFAULT_TEMPLATES.get(doc_type, ""),
|
|
|
|
|
|
"description": "",
|
|
|
|
|
|
"created_at": ts,
|
|
|
|
|
|
"updated_at": ts,
|
|
|
|
|
|
"revision": 1,
|
|
|
|
|
|
"user_id": "",
|
|
|
|
|
|
"practice_id": "",
|
|
|
|
|
|
"server_id": "",
|
|
|
|
|
|
"published": False,
|
|
|
|
|
|
"sync_updated_at": "",
|
|
|
|
|
|
"source_author": "",
|
|
|
|
|
|
"source_server_id": "",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_template_fields(t: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
|
if not isinstance(t, dict):
|
|
|
|
|
|
return _new_template_entry("kg")
|
|
|
|
|
|
t.setdefault("description", "")
|
|
|
|
|
|
t.setdefault("revision", 1)
|
|
|
|
|
|
t.setdefault("user_id", "")
|
|
|
|
|
|
t.setdefault("practice_id", "")
|
|
|
|
|
|
t.setdefault("server_id", "")
|
|
|
|
|
|
t.setdefault("published", False)
|
|
|
|
|
|
t.setdefault("sync_updated_at", "")
|
|
|
|
|
|
t.setdefault("source_author", "")
|
|
|
|
|
|
t.setdefault("source_server_id", "")
|
|
|
|
|
|
if "content" in t:
|
|
|
|
|
|
t["content"] = sanitize_prompt_text(t.get("content") or "")
|
|
|
|
|
|
return t
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _default_structure() -> dict[str, Any]:
|
|
|
|
|
|
now = _utc_now()
|
|
|
|
|
|
return {
|
|
|
|
|
|
"schema_version": SCHEMA_VERSION,
|
|
|
|
|
|
"active": {dt: "aza_default" for dt, _ in DOC_TYPES},
|
|
|
|
|
|
"templates": {
|
|
|
|
|
|
dt: [_new_template_entry(dt, now=now)]
|
|
|
|
|
|
for dt, _ in DOC_TYPES
|
|
|
|
|
|
},
|
|
|
|
|
|
"sync_meta": {"last_sync_at": "", "conflicts": []},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _backup_json_before_migration(path: str) -> None:
|
|
|
|
|
|
if not os.path.isfile(path):
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
|
|
shutil.copy2(path, path + f".pre_migrate_{stamp}.bak")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _migrate_data(d: dict[str, Any]) -> tuple[dict[str, Any], bool]:
|
|
|
|
|
|
"""Migration schema v1→v2: fehlende Typen ergänzen, Metadaten anreichern."""
|
|
|
|
|
|
changed = False
|
|
|
|
|
|
now = _utc_now()
|
|
|
|
|
|
sv = int(d.get("schema_version") or 1)
|
|
|
|
|
|
|
|
|
|
|
|
for dt, _ in DOC_TYPES:
|
|
|
|
|
|
if dt not in d.get("templates", {}):
|
|
|
|
|
|
d.setdefault("templates", {})[dt] = [_new_template_entry(dt, now=now)]
|
|
|
|
|
|
changed = True
|
|
|
|
|
|
if dt not in d.get("active", {}):
|
|
|
|
|
|
d.setdefault("active", {})[dt] = "aza_default"
|
|
|
|
|
|
changed = True
|
|
|
|
|
|
tpls = d.get("templates", {}).get(dt, [])
|
|
|
|
|
|
for i, t in enumerate(tpls):
|
|
|
|
|
|
if isinstance(t, dict):
|
|
|
|
|
|
before = json.dumps(t, sort_keys=True)
|
|
|
|
|
|
tpls[i] = _ensure_template_fields(t)
|
|
|
|
|
|
if json.dumps(tpls[i], sort_keys=True) != before:
|
|
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
|
|
|
|
if "sync_meta" not in d:
|
|
|
|
|
|
d["sync_meta"] = {"last_sync_at": "", "conflicts": []}
|
|
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
|
|
|
|
if sv < SCHEMA_VERSION:
|
|
|
|
|
|
d["schema_version"] = SCHEMA_VERSION
|
|
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
|
|
|
|
return d, changed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load() -> dict[str, Any]:
|
|
|
|
|
|
path = _templates_json_path()
|
|
|
|
|
|
if os.path.isfile(path):
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
|
|
|
|
d = json.load(f)
|
|
|
|
|
|
if isinstance(d, dict) and d.get("schema_version") in (1, 2):
|
|
|
|
|
|
d, changed = _migrate_data(d)
|
|
|
|
|
|
if changed:
|
|
|
|
|
|
try:
|
|
|
|
|
|
_backup_json_before_migration(path)
|
|
|
|
|
|
_save(d)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return d
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return _default_structure()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_all_templates() -> dict[str, Any]:
|
|
|
|
|
|
"""Alias fuer externe Module (z. B. aza_text_windows_mixin)."""
|
|
|
|
|
|
return _load()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _save(data: dict[str, Any]) -> None:
|
|
|
|
|
|
path = _templates_json_path()
|
|
|
|
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
|
|
|
|
tmp = path + ".tmp"
|
|
|
|
|
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
|
|
|
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
|
|
|
|
os.replace(tmp, path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_active_template_content(doc_type: str) -> str:
|
|
|
|
|
|
try:
|
|
|
|
|
|
data = _load()
|
|
|
|
|
|
aid = data.get("active", {}).get(doc_type, "aza_default")
|
|
|
|
|
|
for t in data.get("templates", {}).get(doc_type, []):
|
|
|
|
|
|
if isinstance(t, dict) and t.get("id") == aid:
|
|
|
|
|
|
return sanitize_prompt_text(t.get("content") or "")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return AZA_DEFAULT_TEMPLATES.get(doc_type, "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_active_template_id(doc_type: str) -> str:
|
|
|
|
|
|
try:
|
|
|
|
|
|
data = _load()
|
|
|
|
|
|
return str(data.get("active", {}).get(doc_type, "aza_default"))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return "aza_default"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def doc_type_label(doc_type: str) -> str:
|
|
|
|
|
|
for dt, label in DOC_TYPES:
|
|
|
|
|
|
if dt == doc_type:
|
|
|
|
|
|
return label
|
|
|
|
|
|
return doc_type
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def copy_public_template_as_own(
|
|
|
|
|
|
doc_type: str,
|
|
|
|
|
|
*,
|
|
|
|
|
|
content: str,
|
|
|
|
|
|
title: str,
|
2026-06-13 22:47:31 +02:00
|
|
|
|
description: str = "",
|
2026-06-10 22:55:03 +02:00
|
|
|
|
source_author: str = "",
|
|
|
|
|
|
source_server_id: str = "",
|
2026-06-13 22:47:31 +02:00
|
|
|
|
user_id: str = "",
|
|
|
|
|
|
practice_id: str = "",
|
|
|
|
|
|
activate: bool = False,
|
2026-06-10 22:55:03 +02:00
|
|
|
|
) -> str | None:
|
|
|
|
|
|
"""Übernimmt eine öffentliche Vorlage als neue eigene Kopie (neue ID)."""
|
2026-06-13 22:47:31 +02:00
|
|
|
|
if doc_type not in DOC_TYPE_KEYS:
|
|
|
|
|
|
doc_type = "kg"
|
2026-06-10 22:55:03 +02:00
|
|
|
|
data = _load()
|
|
|
|
|
|
name = (title or "Übernommene Vorlage").strip()
|
|
|
|
|
|
new_id = f"user_{int(time.time())}_{doc_type}"
|
|
|
|
|
|
now = _utc_now()
|
|
|
|
|
|
new_tpl = {
|
|
|
|
|
|
"id": new_id,
|
|
|
|
|
|
"name": name,
|
|
|
|
|
|
"is_system": False,
|
|
|
|
|
|
"content": sanitize_prompt_text(content),
|
2026-06-13 22:47:31 +02:00
|
|
|
|
"description": (description or "").strip(),
|
2026-06-10 22:55:03 +02:00
|
|
|
|
"created_at": now,
|
|
|
|
|
|
"updated_at": now,
|
|
|
|
|
|
"revision": 1,
|
2026-06-13 22:47:31 +02:00
|
|
|
|
"user_id": (user_id or "").strip(),
|
|
|
|
|
|
"practice_id": (practice_id or "").strip(),
|
2026-06-10 22:55:03 +02:00
|
|
|
|
"server_id": "",
|
|
|
|
|
|
"published": False,
|
|
|
|
|
|
"sync_updated_at": "",
|
|
|
|
|
|
"source_author": (source_author or "").strip(),
|
|
|
|
|
|
"source_server_id": (source_server_id or "").strip(),
|
|
|
|
|
|
}
|
|
|
|
|
|
data.setdefault("templates", {}).setdefault(doc_type, []).append(new_tpl)
|
2026-06-13 22:47:31 +02:00
|
|
|
|
if activate:
|
|
|
|
|
|
data.setdefault("active", {})[doc_type] = new_id
|
2026-06-10 22:55:03 +02:00
|
|
|
|
_save(data)
|
|
|
|
|
|
return new_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Sync / Veröffentlichung (Client, kein Deploy in diesem Block) ─────────────
|
|
|
|
|
|
|
|
|
|
|
|
def _app_user_practice_ids(app: Any) -> tuple[str, str]:
|
|
|
|
|
|
user_id = ""
|
|
|
|
|
|
practice_id = ""
|
|
|
|
|
|
try:
|
|
|
|
|
|
practice_id = (app.get_practice_id() or "").strip()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
try:
|
|
|
|
|
|
user_id = (getattr(app, "_empfang_self_user_id", lambda: "")() or "").strip()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
user_id = ""
|
|
|
|
|
|
return user_id, practice_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def sync_private_doku_prompts(app: Any) -> bool:
|
|
|
|
|
|
"""Synchronisiert private Benutzer-Prompts mit dem Server (falls erreichbar)."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from aza_doku_prompt_sync import sync_doku_prompts_with_server
|
|
|
|
|
|
return sync_doku_prompts_with_server(app)
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
return False
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def publish_template_to_server(app: Any, doc_type: str, tpl: dict[str, Any],
|
|
|
|
|
|
title: str, description: str = "") -> tuple[bool, str]:
|
|
|
|
|
|
"""Veröffentlicht aktuelle Vorlage auf dem Server (nur wenn angemeldet)."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from aza_doku_prompt_sync import publish_doku_prompt
|
|
|
|
|
|
return publish_doku_prompt(app, doc_type, tpl, title, description)
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
return False, "Sync-Modul nicht verfügbar."
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return False, str(e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_public_prompt_library(app: Any) -> list[dict[str, Any]]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
from aza_doku_prompt_sync import fetch_public_doku_prompts
|
|
|
|
|
|
return fetch_public_doku_prompts(app)
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
return []
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-13 22:47:31 +02:00
|
|
|
|
def fetch_public_prompt_library_result(app: Any) -> tuple[list[dict[str, Any]], str | None]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
from aza_doku_prompt_sync import fetch_public_doku_prompts_result
|
|
|
|
|
|
return fetch_public_doku_prompts_result(app)
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
return [], None
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return [], "__CONN_ERROR__"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Fenster-Hilfen (analog aza_bibliothek_ui) ─────────────────────────────────
|
|
|
|
|
|
_DOKU_WIN_W, _DOKU_WIN_H = 1080, 820
|
|
|
|
|
|
_DOKU_WIN_MIN_W, _DOKU_WIN_MIN_H = 980, 700
|
|
|
|
|
|
_PUBLIC_WIN_W, _PUBLIC_WIN_H = 1180, 860
|
|
|
|
|
|
_PUBLIC_WIN_MIN_W, _PUBLIC_WIN_MIN_H = 1050, 760
|
|
|
|
|
|
_PUBLISH_DLG_W, _PUBLISH_DLG_H = 720, 860
|
|
|
|
|
|
_PUBLISH_DLG_MIN_W, _PUBLISH_DLG_MIN_H = 650, 760
|
|
|
|
|
|
|
|
|
|
|
|
_PUBLISH_ROUTE_MSG = (
|
|
|
|
|
|
"Öffentliche Veröffentlichung ist lokal vorbereitet, "
|
|
|
|
|
|
"benötigt aber noch den Server-Deploy."
|
|
|
|
|
|
)
|
|
|
|
|
|
_PUBLIC_LIB_PLACEHOLDER = (
|
|
|
|
|
|
"Öffentliche Vorlagen sind lokal vorbereitet. "
|
|
|
|
|
|
"Die Live-Bibliothek benötigt noch den Server-Deploy."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _doku_center_top(win: tk.Toplevel, w: int, h: int, *, parent=None) -> None:
|
|
|
|
|
|
from aza_ui_helpers import center_tool_window
|
|
|
|
|
|
center_tool_window(win, w, h, parent=parent, y_ratio=0.06)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _doku_bring_to_front(win: tk.Toplevel) -> None:
|
|
|
|
|
|
from aza_ui_helpers import bring_tool_window_to_front
|
|
|
|
|
|
bring_tool_window_to_front(win)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_tool_window_geometry(
|
|
|
|
|
|
win: tk.Toplevel, w: int, h: int, min_w: int, min_h: int, *, parent=None,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""AzA-Werkzeugfenster: zentriert, im Vordergrund, Höhe begrenzt (Action-Bar sichtbar)."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
win.update_idletasks()
|
|
|
|
|
|
sh = win.winfo_screenheight()
|
|
|
|
|
|
h = min(h, max(min_h, sh - 80))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
win.minsize(min_w, min_h)
|
|
|
|
|
|
_doku_center_top(win, w, h, parent=parent)
|
|
|
|
|
|
_doku_bring_to_front(win)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _profile_field(app: Any, keys: tuple[str, ...], default: str = "") -> str:
|
|
|
|
|
|
try:
|
|
|
|
|
|
prof = getattr(app, "_user_profile", {}) or {}
|
|
|
|
|
|
for k in keys:
|
|
|
|
|
|
v = str(prof.get(k) or "").strip()
|
|
|
|
|
|
if v:
|
|
|
|
|
|
return v
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return default
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _author_initials(name: str) -> str:
|
|
|
|
|
|
parts = [p for p in (name or "").split() if p]
|
|
|
|
|
|
if not parts:
|
|
|
|
|
|
return "?"
|
|
|
|
|
|
if len(parts) == 1:
|
|
|
|
|
|
return parts[0][:2].upper()
|
|
|
|
|
|
return f"{parts[0][0]}{parts[-1][0]}".upper()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolve_default_author_display(app: Any) -> str:
|
|
|
|
|
|
try:
|
|
|
|
|
|
from aza_doku_prompt_sync import resolve_author_display
|
|
|
|
|
|
return resolve_author_display(app)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return _profile_field(app, ("display_name", "name"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolve_default_location(app: Any) -> str:
|
|
|
|
|
|
return _profile_field(
|
|
|
|
|
|
app,
|
|
|
|
|
|
("city", "location", "ort", "practice_city", "practice_location"),
|
|
|
|
|
|
"Winterthur",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolve_default_specialty(app: Any) -> str:
|
|
|
|
|
|
return _profile_field(
|
|
|
|
|
|
app,
|
|
|
|
|
|
("specialty", "fachrichtung", "specialisation", "fachgebiet"),
|
|
|
|
|
|
"Dermatologie",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolve_default_author_initials(app: Any) -> str:
|
|
|
|
|
|
author = resolve_default_author_display(app)
|
|
|
|
|
|
initials = _author_initials(author)
|
|
|
|
|
|
return initials if initials and initials != "?" else "A.S."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _wire_text_dictation(
|
|
|
|
|
|
app: Any,
|
|
|
|
|
|
parent: tk.Misc,
|
|
|
|
|
|
text_widget: tk.Text,
|
|
|
|
|
|
status_var: tk.StringVar,
|
|
|
|
|
|
btn_rec: tk.Button,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""Mini-Diktat in ein Textfeld — gleiches Muster wie Arztzeugnis-Fenster."""
|
|
|
|
|
|
recorder: list[Any] = [None]
|
|
|
|
|
|
is_recording = [False]
|
|
|
|
|
|
|
|
|
|
|
|
def _toggle():
|
|
|
|
|
|
if is_recording[0]:
|
|
|
|
|
|
is_recording[0] = False
|
|
|
|
|
|
btn_rec.configure(text="⏺ Diktieren", bg=_BTN_BLUE)
|
|
|
|
|
|
status_var.set("Transkribiere …")
|
|
|
|
|
|
rec = recorder[0]
|
|
|
|
|
|
|
|
|
|
|
|
def _worker():
|
|
|
|
|
|
try:
|
|
|
|
|
|
wav_path = rec.stop_and_save_wav() if rec else None
|
|
|
|
|
|
if not wav_path:
|
|
|
|
|
|
app.after(0, lambda: status_var.set("Aufnahme fehlgeschlagen."))
|
|
|
|
|
|
return
|
|
|
|
|
|
text = app.transcribe_wav(wav_path)
|
|
|
|
|
|
if not text:
|
|
|
|
|
|
app.after(0, lambda: status_var.set("Kein Text erkannt."))
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
def _insert():
|
|
|
|
|
|
cur = text_widget.get("1.0", "end-1c").strip()
|
|
|
|
|
|
if cur:
|
|
|
|
|
|
text_widget.insert("end", "\n" + text)
|
|
|
|
|
|
else:
|
|
|
|
|
|
text_widget.insert("1.0", text)
|
|
|
|
|
|
status_var.set("Diktat eingefügt.")
|
|
|
|
|
|
|
|
|
|
|
|
app.after(0, _insert)
|
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
|
app.after(0, lambda: status_var.set(f"Fehler: {exc}"))
|
|
|
|
|
|
|
|
|
|
|
|
import threading
|
|
|
|
|
|
threading.Thread(target=_worker, daemon=True).start()
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
mic_ok = True
|
|
|
|
|
|
try:
|
|
|
|
|
|
mic_ok = bool(app._ensure_microphone_ready())
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
if not mic_ok:
|
|
|
|
|
|
status_var.set("Mikrofon nicht bereit.")
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
from aza_audio import AudioRecorder
|
|
|
|
|
|
recorder[0] = AudioRecorder()
|
|
|
|
|
|
recorder[0].start()
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
status_var.set(f"Fehler: {exc}")
|
|
|
|
|
|
return
|
|
|
|
|
|
is_recording[0] = True
|
|
|
|
|
|
btn_rec.configure(text="⏹ Stoppen", bg="#C03030")
|
|
|
|
|
|
status_var.set("Aufnahme läuft …")
|
|
|
|
|
|
|
|
|
|
|
|
btn_rec.configure(command=_toggle)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def open_doku_publish_dialog(
|
|
|
|
|
|
app: Any,
|
|
|
|
|
|
parent: tk.Misc,
|
|
|
|
|
|
*,
|
|
|
|
|
|
doc_type: str,
|
|
|
|
|
|
default_title: str,
|
|
|
|
|
|
on_confirm,
|
|
|
|
|
|
mode: str = "publish",
|
|
|
|
|
|
initial_values: dict[str, Any] | None = None,
|
|
|
|
|
|
confirm_label: str = "Veröffentlichen",
|
|
|
|
|
|
window_title: str = "Vorlage veröffentlichen",
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""AzA-Werkzeugfenster für Veröffentlichungs-Metadaten (kein simpledialog)."""
|
|
|
|
|
|
iv = initial_values or {}
|
|
|
|
|
|
start_dt = (iv.get("doc_type") or doc_type or "kg").strip()
|
|
|
|
|
|
if start_dt not in DOC_TYPE_KEYS:
|
|
|
|
|
|
start_dt = doc_type if doc_type in DOC_TYPE_KEYS else "kg"
|
|
|
|
|
|
|
|
|
|
|
|
dlg = tk.Toplevel(parent)
|
|
|
|
|
|
dlg.title(window_title)
|
|
|
|
|
|
dlg.configure(bg=_BG)
|
|
|
|
|
|
dlg.resizable(True, True)
|
|
|
|
|
|
_apply_tool_window_geometry(
|
|
|
|
|
|
dlg, _PUBLISH_DLG_W, _PUBLISH_DLG_H, _PUBLISH_DLG_MIN_W, _PUBLISH_DLG_MIN_H, parent=parent,
|
|
|
|
|
|
)
|
|
|
|
|
|
make_modal_topmost(dlg, parent)
|
|
|
|
|
|
|
|
|
|
|
|
hdr = tk.Frame(dlg, bg=_HDR_BG)
|
|
|
|
|
|
hdr.pack(side="top", fill="x")
|
|
|
|
|
|
tk.Label(hdr, text=window_title, font=(_FF, 12, "bold"),
|
|
|
|
|
|
bg=_HDR_BG, fg=_HDR_FG, padx=16, pady=10).pack(side="left")
|
|
|
|
|
|
|
|
|
|
|
|
btn_row = tk.Frame(dlg, bg="#E8F0F5", pady=10)
|
|
|
|
|
|
btn_row.pack(side="bottom", fill="x")
|
|
|
|
|
|
|
|
|
|
|
|
body = tk.Frame(dlg, bg=_BG, padx=16, pady=10)
|
|
|
|
|
|
body.pack(side="top", fill="both", expand=True)
|
|
|
|
|
|
body.columnconfigure(1, weight=1)
|
|
|
|
|
|
|
|
|
|
|
|
def _lbl(row, text):
|
|
|
|
|
|
tk.Label(body, text=text, font=(_FF, 9, "bold"),
|
|
|
|
|
|
bg=_BG, fg=_TEXT, anchor="w").grid(row=row, column=0, sticky="w", pady=(6, 2))
|
|
|
|
|
|
|
|
|
|
|
|
def _ent(row, value=""):
|
|
|
|
|
|
var = tk.StringVar(value=value)
|
|
|
|
|
|
ent = tk.Entry(body, textvariable=var, font=(_FF, 9), bg="#FFFFFF", fg=_TEXT,
|
|
|
|
|
|
relief="flat", highlightbackground=_CARD_BD, highlightthickness=1)
|
|
|
|
|
|
ent.grid(row=row, column=1, sticky="ew", pady=(6, 2), ipady=4)
|
|
|
|
|
|
return var
|
|
|
|
|
|
|
|
|
|
|
|
doc_labels = [lbl for _, lbl in DOC_TYPES]
|
|
|
|
|
|
doc_label_var = tk.StringVar(value=doc_type_label(start_dt))
|
|
|
|
|
|
_lbl(0, "Dokumenttyp:")
|
|
|
|
|
|
doc_combo = ttk.Combobox(
|
|
|
|
|
|
body, textvariable=doc_label_var, values=doc_labels,
|
|
|
|
|
|
state="readonly", font=(_FF, 9),
|
|
|
|
|
|
)
|
|
|
|
|
|
doc_combo.grid(row=0, column=1, sticky="ew", pady=(6, 2))
|
|
|
|
|
|
|
|
|
|
|
|
def _get_selected_doc_type() -> str:
|
|
|
|
|
|
lbl = doc_label_var.get()
|
|
|
|
|
|
for dt_key, dt_lbl in DOC_TYPES:
|
|
|
|
|
|
if dt_lbl == lbl:
|
|
|
|
|
|
return dt_key
|
|
|
|
|
|
return start_dt
|
|
|
|
|
|
|
|
|
|
|
|
_lbl(1, "Vorlagenname:")
|
|
|
|
|
|
title_var = _ent(1, iv.get("title") or default_title)
|
|
|
|
|
|
_lbl(2, "Verfasser:")
|
|
|
|
|
|
author_var = _ent(2, iv.get("author") or resolve_default_author_display(app))
|
|
|
|
|
|
_lbl(3, "Initialen:")
|
|
|
|
|
|
initials_var = _ent(3, iv.get("initials") or resolve_default_author_initials(app))
|
|
|
|
|
|
_lbl(4, "Ort:")
|
|
|
|
|
|
location_var = _ent(4, iv.get("location") or resolve_default_location(app))
|
|
|
|
|
|
_lbl(5, "Fachrichtung:")
|
|
|
|
|
|
specialty_var = _ent(5, iv.get("specialty") or resolve_default_specialty(app))
|
|
|
|
|
|
|
|
|
|
|
|
tk.Label(body, text="Beschreibung:", font=(_FF, 9, "bold"),
|
|
|
|
|
|
bg=_BG, fg=_TEXT, anchor="nw").grid(row=6, column=0, sticky="nw", pady=(10, 2))
|
|
|
|
|
|
desc_frame = tk.Frame(body, bg=_CARD_BG, highlightbackground=_CARD_BD, highlightthickness=1)
|
|
|
|
|
|
desc_frame.grid(row=6, column=1, sticky="nsew", pady=(10, 2))
|
|
|
|
|
|
body.rowconfigure(6, weight=1)
|
|
|
|
|
|
desc_vsb = tk.Scrollbar(desc_frame, orient="vertical")
|
|
|
|
|
|
desc_vsb.pack(side="right", fill="y")
|
|
|
|
|
|
desc_height = 7 if mode == "edit" else 9
|
|
|
|
|
|
desc_txt = tk.Text(desc_frame, font=(_FF, 9), bg="#FFFFFF", fg=_TEXT,
|
|
|
|
|
|
wrap="word", height=desc_height, yscrollcommand=desc_vsb.set,
|
|
|
|
|
|
padx=8, pady=8, relief="flat")
|
|
|
|
|
|
desc_txt.pack(side="left", fill="both", expand=True)
|
|
|
|
|
|
desc_vsb.config(command=desc_txt.yview)
|
|
|
|
|
|
desc_init = (iv.get("description") or "").strip()
|
|
|
|
|
|
if desc_init:
|
|
|
|
|
|
desc_txt.insert("1.0", desc_init)
|
|
|
|
|
|
|
|
|
|
|
|
dict_row = tk.Frame(body, bg=_BG)
|
|
|
|
|
|
dict_row.grid(row=7, column=1, sticky="w", pady=(4, 0))
|
|
|
|
|
|
dict_status = tk.StringVar(value="")
|
|
|
|
|
|
btn_dict = tk.Button(dict_row, text="⏺ Diktieren", font=(_FF, 9, "bold"),
|
|
|
|
|
|
bg=_BTN_BLUE, fg="#FFFFFF", relief="flat", bd=0,
|
|
|
|
|
|
padx=12, pady=4, cursor="hand2")
|
|
|
|
|
|
btn_dict.pack(side="left")
|
|
|
|
|
|
tk.Label(dict_row, textvariable=dict_status, font=(_FF, 8),
|
|
|
|
|
|
bg=_BG, fg=_TEXT_SUB).pack(side="left", padx=(8, 0))
|
|
|
|
|
|
_wire_text_dictation(app, dlg, desc_txt, dict_status, btn_dict)
|
|
|
|
|
|
|
|
|
|
|
|
if mode == "publish":
|
|
|
|
|
|
hint = tk.Label(
|
|
|
|
|
|
body,
|
|
|
|
|
|
text="Bitte prüfen: keine Patientendaten oder vertraulichen Praxisdaten.",
|
|
|
|
|
|
font=(_FF, 8), bg=_BG, fg=_TEXT_SUB, wraplength=480, justify="left",
|
|
|
|
|
|
)
|
|
|
|
|
|
hint.grid(row=8, column=0, columnspan=2, sticky="w", pady=(8, 0))
|
|
|
|
|
|
|
|
|
|
|
|
def _cancel():
|
|
|
|
|
|
release_modal_dialog(dlg, parent)
|
|
|
|
|
|
dlg.destroy()
|
|
|
|
|
|
|
|
|
|
|
|
def _confirm():
|
|
|
|
|
|
title = title_var.get().strip()
|
|
|
|
|
|
if not title:
|
|
|
|
|
|
aza_showinfo("Hinweis", "Bitte einen Vorlagennamen eingeben.", parent=dlg)
|
|
|
|
|
|
return
|
|
|
|
|
|
meta = {
|
|
|
|
|
|
"doc_type": _get_selected_doc_type(),
|
|
|
|
|
|
"title": title,
|
|
|
|
|
|
"description": desc_txt.get("1.0", "end-1c").strip(),
|
|
|
|
|
|
"author": author_var.get().strip(),
|
|
|
|
|
|
"initials": initials_var.get().strip(),
|
|
|
|
|
|
"location": location_var.get().strip(),
|
|
|
|
|
|
"specialty": specialty_var.get().strip(),
|
|
|
|
|
|
}
|
|
|
|
|
|
release_modal_dialog(dlg, parent)
|
|
|
|
|
|
dlg.destroy()
|
|
|
|
|
|
try:
|
|
|
|
|
|
on_confirm(meta)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
tk.Button(btn_row, text=confirm_label, command=_confirm,
|
|
|
|
|
|
font=(_FF, 9, "bold"), bg=_BTN_BLUE, fg="#FFFFFF",
|
|
|
|
|
|
relief="flat", bd=0, padx=14, pady=6, cursor="hand2").pack(side="left", padx=(16, 8))
|
|
|
|
|
|
tk.Button(btn_row, text="Abbrechen", command=_cancel,
|
|
|
|
|
|
font=(_FF, 9, "bold"), bg="#8AAFC8", fg="#FFFFFF",
|
|
|
|
|
|
relief="flat", bd=0, padx=14, pady=6, cursor="hand2").pack(side="left")
|
|
|
|
|
|
dlg.protocol("WM_DELETE_WINDOW", _cancel)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def open_public_doku_vorlagen_window(app: Any, parent: tk.Misc | None = None) -> None:
|
|
|
|
|
|
"""Öffentliche Vorlagen — separates Werkzeugfenster (lokal, kein Serverzwang)."""
|
|
|
|
|
|
existing = getattr(app, "_public_doku_vorlagen_win", None)
|
|
|
|
|
|
if existing is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
if existing.winfo_exists():
|
|
|
|
|
|
existing.lift()
|
|
|
|
|
|
_doku_bring_to_front(existing)
|
|
|
|
|
|
return
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
owner = parent or app
|
|
|
|
|
|
win = tk.Toplevel(owner)
|
|
|
|
|
|
app._public_doku_vorlagen_win = win
|
|
|
|
|
|
win.title("Öffentliche Vorlagen")
|
|
|
|
|
|
win.configure(bg=_BG)
|
|
|
|
|
|
win.resizable(True, True)
|
|
|
|
|
|
_apply_tool_window_geometry(
|
|
|
|
|
|
win, _PUBLIC_WIN_W, _PUBLIC_WIN_H, _PUBLIC_WIN_MIN_W, _PUBLIC_WIN_MIN_H, parent=owner,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if hasattr(app, "_register_window"):
|
|
|
|
|
|
try:
|
|
|
|
|
|
app._register_window(win)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
author = resolve_default_author_display(app)
|
|
|
|
|
|
initials = _author_initials(author)
|
|
|
|
|
|
location = resolve_default_location(app)
|
|
|
|
|
|
specialty = resolve_default_specialty(app)
|
|
|
|
|
|
|
|
|
|
|
|
hdr = tk.Frame(win, bg=_HDR_BG)
|
|
|
|
|
|
hdr.pack(fill="x")
|
|
|
|
|
|
tk.Label(hdr, text="Öffentliche Vorlagen", font=(_FF, 13, "bold"),
|
|
|
|
|
|
bg=_HDR_BG, fg=_HDR_FG, padx=18, pady=10).pack(side="left")
|
|
|
|
|
|
close_x = tk.Label(hdr, text="✕", font=(_FF, 11), bg=_HDR_BG, fg="#A0C4D8",
|
|
|
|
|
|
cursor="hand2", padx=14)
|
|
|
|
|
|
close_x.pack(side="right")
|
|
|
|
|
|
|
|
|
|
|
|
meta = tk.Frame(win, bg=_SUB_BG)
|
|
|
|
|
|
meta.pack(side="top", fill="x")
|
|
|
|
|
|
tk.Label(
|
|
|
|
|
|
meta,
|
|
|
|
|
|
text=(
|
|
|
|
|
|
f"Standard für Veröffentlichungen: Verfasser {author or initials} "
|
|
|
|
|
|
f"· Ort {location} · Fachrichtung {specialty}"
|
|
|
|
|
|
),
|
|
|
|
|
|
font=(_FF, 8), bg=_SUB_BG, fg=_TEXT_SUB, padx=18, pady=5, anchor="w",
|
|
|
|
|
|
).pack(fill="x")
|
|
|
|
|
|
|
|
|
|
|
|
action_bar = tk.Frame(win, bg="#E8F0F5", pady=8)
|
|
|
|
|
|
action_bar.pack(side="bottom", fill="x")
|
|
|
|
|
|
|
|
|
|
|
|
body = tk.Frame(win, bg=_BG)
|
|
|
|
|
|
body.pack(side="top", fill="both", expand=True, padx=12, pady=10)
|
|
|
|
|
|
body.rowconfigure(2, weight=3)
|
|
|
|
|
|
body.rowconfigure(3, weight=2)
|
|
|
|
|
|
body.columnconfigure(0, weight=1)
|
|
|
|
|
|
|
|
|
|
|
|
search_row = tk.Frame(body, bg=_BG)
|
|
|
|
|
|
search_row.grid(row=0, column=0, sticky="ew", pady=(0, 6))
|
|
|
|
|
|
search_row.columnconfigure(1, weight=1)
|
|
|
|
|
|
tk.Label(search_row, text="Suchen:", font=(_FF, 8, "bold"),
|
|
|
|
|
|
bg=_BG, fg=_TEXT).grid(row=0, column=0, sticky="w", padx=(0, 6))
|
|
|
|
|
|
search_var = tk.StringVar(value="")
|
|
|
|
|
|
search_entry = tk.Entry(search_row, textvariable=search_var, font=(_FF, 9),
|
|
|
|
|
|
bg="#FFFFFF", fg=_TEXT, relief="flat",
|
|
|
|
|
|
highlightbackground=_CARD_BD, highlightthickness=1)
|
|
|
|
|
|
search_entry.grid(row=0, column=1, sticky="ew", ipady=3)
|
|
|
|
|
|
|
|
|
|
|
|
status_var = tk.StringVar(value="Lade öffentliche Vorlagen …")
|
|
|
|
|
|
tk.Label(body, textvariable=status_var, font=(_FF, 8),
|
|
|
|
|
|
bg=_BG, fg=_TEXT_SUB, anchor="w").grid(row=1, column=0, sticky="ew", pady=(0, 6))
|
|
|
|
|
|
|
|
|
|
|
|
table_frame = tk.Frame(body, bg=_CARD_BG,
|
|
|
|
|
|
highlightbackground=_CARD_BD, highlightthickness=1)
|
|
|
|
|
|
table_frame.grid(row=2, column=0, sticky="nsew")
|
|
|
|
|
|
table_frame.rowconfigure(0, weight=1)
|
|
|
|
|
|
table_frame.columnconfigure(0, weight=1)
|
|
|
|
|
|
|
|
|
|
|
|
pub_style = ttk.Style(win)
|
|
|
|
|
|
pub_style.configure("Public.Treeview", font=(_FF, 8), rowheight=26)
|
|
|
|
|
|
pub_style.configure("Public.Treeview.Heading", font=(_FF, 8, "bold"))
|
|
|
|
|
|
|
|
|
|
|
|
cols = ("doc_type", "title", "rating", "author", "location", "specialty", "status")
|
|
|
|
|
|
tree = ttk.Treeview(
|
|
|
|
|
|
table_frame, columns=cols, show="headings", selectmode="browse",
|
|
|
|
|
|
height=12, style="Public.Treeview",
|
|
|
|
|
|
)
|
|
|
|
|
|
headings = {
|
|
|
|
|
|
"doc_type": "Dokumenttyp",
|
|
|
|
|
|
"title": "Vorlagenname",
|
|
|
|
|
|
"rating": "Bewertung",
|
|
|
|
|
|
"author": "Verfasser",
|
|
|
|
|
|
"location": "Ort",
|
|
|
|
|
|
"specialty": "Fachrichtung",
|
|
|
|
|
|
"status": "Status",
|
|
|
|
|
|
}
|
|
|
|
|
|
widths = {
|
|
|
|
|
|
"doc_type": 100, "title": 170, "rating": 150, "author": 120,
|
|
|
|
|
|
"location": 80, "specialty": 110, "status": 120,
|
|
|
|
|
|
}
|
|
|
|
|
|
for col in cols:
|
|
|
|
|
|
tree.heading(col, text=headings[col])
|
|
|
|
|
|
tree.column(col, width=widths[col], minwidth=60, stretch=(col == "title"))
|
|
|
|
|
|
vsb = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)
|
|
|
|
|
|
tree.configure(yscrollcommand=vsb.set)
|
|
|
|
|
|
tree.grid(row=0, column=0, sticky="nsew", padx=6, pady=6)
|
|
|
|
|
|
vsb.grid(row=0, column=1, sticky="ns", pady=6)
|
|
|
|
|
|
|
|
|
|
|
|
detail_outer = tk.Frame(body, bg=_SUB_BG, highlightbackground=_CARD_BD, highlightthickness=1)
|
|
|
|
|
|
detail_outer.grid(row=3, column=0, sticky="nsew", pady=(8, 0))
|
|
|
|
|
|
detail_outer.rowconfigure(0, weight=1)
|
|
|
|
|
|
detail_outer.columnconfigure(0, weight=1)
|
|
|
|
|
|
detail_vsb = tk.Scrollbar(detail_outer, orient="vertical")
|
|
|
|
|
|
detail_vsb.grid(row=0, column=1, sticky="ns")
|
|
|
|
|
|
detail_canvas = tk.Canvas(detail_outer, bg=_SUB_BG, highlightthickness=0,
|
|
|
|
|
|
yscrollcommand=detail_vsb.set)
|
|
|
|
|
|
detail_canvas.grid(row=0, column=0, sticky="nsew")
|
|
|
|
|
|
detail_vsb.config(command=detail_canvas.yview)
|
|
|
|
|
|
detail_frame = tk.Frame(detail_canvas, bg=_SUB_BG)
|
|
|
|
|
|
detail_canvas_window = detail_canvas.create_window((0, 0), window=detail_frame, anchor="nw")
|
|
|
|
|
|
|
|
|
|
|
|
def _detail_scroll_update(_evt=None):
|
|
|
|
|
|
detail_canvas.configure(scrollregion=detail_canvas.bbox("all"))
|
|
|
|
|
|
try:
|
|
|
|
|
|
detail_canvas.itemconfig(detail_canvas_window, width=detail_canvas.winfo_width())
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
detail_frame.bind("<Configure>", _detail_scroll_update)
|
|
|
|
|
|
detail_canvas.bind("<Configure>", _detail_scroll_update)
|
|
|
|
|
|
|
|
|
|
|
|
detail_vars: dict[str, tk.StringVar] = {}
|
|
|
|
|
|
detail_rows = [
|
|
|
|
|
|
("doc_type", "Dokumenttyp"),
|
|
|
|
|
|
("title", "Vorlagenname"),
|
|
|
|
|
|
("author", "Verfasser"),
|
|
|
|
|
|
("location", "Ort"),
|
|
|
|
|
|
("specialty", "Fachrichtung"),
|
|
|
|
|
|
]
|
|
|
|
|
|
for r, (key, label) in enumerate(detail_rows):
|
|
|
|
|
|
tk.Label(detail_frame, text=f"{label}:", font=(_FF, 8, "bold"),
|
|
|
|
|
|
bg=_SUB_BG, fg=_TEXT_SUB, anchor="w").grid(
|
|
|
|
|
|
row=r, column=0, sticky="w", padx=(10, 6), pady=2,
|
|
|
|
|
|
)
|
|
|
|
|
|
detail_vars[key] = tk.StringVar(value="—")
|
|
|
|
|
|
tk.Label(detail_frame, textvariable=detail_vars[key], font=(_FF, 8),
|
|
|
|
|
|
bg=_SUB_BG, fg=_TEXT, anchor="w").grid(
|
|
|
|
|
|
row=r, column=1, sticky="w", pady=2, padx=(0, 10),
|
|
|
|
|
|
)
|
|
|
|
|
|
desc_row = len(detail_rows)
|
|
|
|
|
|
tk.Label(detail_frame, text="Beschreibung:", font=(_FF, 8, "bold"),
|
|
|
|
|
|
bg=_SUB_BG, fg=_TEXT_SUB, anchor="nw").grid(
|
|
|
|
|
|
row=desc_row, column=0, sticky="nw", padx=(10, 6), pady=(4, 2),
|
|
|
|
|
|
)
|
|
|
|
|
|
detail_desc = tk.Text(
|
|
|
|
|
|
detail_frame, font=(_FF, 8), bg=_SUB_BG, fg=_TEXT,
|
|
|
|
|
|
wrap="word", height=3, relief="flat", bd=0, padx=0, pady=2,
|
|
|
|
|
|
)
|
|
|
|
|
|
detail_desc.grid(row=desc_row, column=1, sticky="ew", pady=(4, 2), padx=(0, 10))
|
|
|
|
|
|
detail_desc.config(state="disabled")
|
|
|
|
|
|
prompt_row = desc_row + 1
|
|
|
|
|
|
tk.Label(detail_frame, text="Prompt:", font=(_FF, 8, "bold"),
|
|
|
|
|
|
bg=_SUB_BG, fg=_TEXT_SUB, anchor="nw").grid(
|
|
|
|
|
|
row=prompt_row, column=0, sticky="nw", padx=(10, 6), pady=(4, 8),
|
|
|
|
|
|
)
|
|
|
|
|
|
detail_prompt = tk.Text(
|
|
|
|
|
|
detail_frame, font=(_FF, 8), bg=_SUB_BG, fg=_TEXT,
|
|
|
|
|
|
wrap="word", height=6, relief="flat", bd=0, padx=0, pady=2,
|
|
|
|
|
|
)
|
|
|
|
|
|
detail_prompt.grid(row=prompt_row, column=1, sticky="ew", pady=(4, 8), padx=(0, 10))
|
|
|
|
|
|
detail_prompt.config(state="disabled")
|
|
|
|
|
|
detail_frame.columnconfigure(1, weight=1)
|
|
|
|
|
|
|
|
|
|
|
|
rating_row = prompt_row + 1
|
|
|
|
|
|
tk.Label(detail_frame, text="Bewertung:", font=(_FF, 8, "bold"),
|
|
|
|
|
|
bg=_SUB_BG, fg=_TEXT_SUB, anchor="nw").grid(
|
|
|
|
|
|
row=rating_row, column=0, sticky="nw", padx=(10, 6), pady=(4, 8),
|
|
|
|
|
|
)
|
|
|
|
|
|
rating_block = tk.Frame(detail_frame, bg=_SUB_BG)
|
|
|
|
|
|
rating_block.grid(row=rating_row, column=1, sticky="ew", pady=(4, 8), padx=(0, 10))
|
|
|
|
|
|
rating_summary_var = tk.StringVar(value="Noch keine Bewertungen · 0 Bewertungen")
|
|
|
|
|
|
tk.Label(
|
|
|
|
|
|
rating_block, textvariable=rating_summary_var, font=(_FF, 8),
|
|
|
|
|
|
bg=_SUB_BG, fg=_TEXT, anchor="w", justify="left",
|
|
|
|
|
|
).pack(anchor="w")
|
|
|
|
|
|
rating_own_hint_var = tk.StringVar(value="")
|
|
|
|
|
|
tk.Label(
|
|
|
|
|
|
rating_block, textvariable=rating_own_hint_var, font=(_FF, 8),
|
|
|
|
|
|
bg=_SUB_BG, fg=_TEXT_SUB, anchor="w", justify="left",
|
|
|
|
|
|
).pack(anchor="w", pady=(2, 4))
|
|
|
|
|
|
rating_user_fr = tk.Frame(rating_block, bg=_SUB_BG)
|
|
|
|
|
|
rating_user_fr.pack(anchor="w", fill="x")
|
|
|
|
|
|
tk.Label(
|
|
|
|
|
|
rating_user_fr, text="Ihre Bewertung:", font=(_FF, 8, "bold"),
|
|
|
|
|
|
bg=_SUB_BG, fg=_TEXT_SUB,
|
|
|
|
|
|
).pack(side="left", padx=(0, 8))
|
|
|
|
|
|
rating_inflight = {"busy": False}
|
|
|
|
|
|
|
|
|
|
|
|
def _submit_rating(val: float) -> None:
|
|
|
|
|
|
item = _selected_item()
|
|
|
|
|
|
if not item or rating_inflight["busy"]:
|
|
|
|
|
|
return
|
|
|
|
|
|
pid = str(item.get("server_id") or item.get("id") or "").strip()
|
|
|
|
|
|
if not pid:
|
|
|
|
|
|
aza_showwarning("Hinweis", "Diese Vorlage kann noch nicht bewertet werden.", parent=win)
|
|
|
|
|
|
return
|
|
|
|
|
|
rating_inflight["busy"] = True
|
|
|
|
|
|
try:
|
|
|
|
|
|
from aza_doku_prompt_sync import rate_public_doku_prompt_result
|
|
|
|
|
|
ok, msg, payload = rate_public_doku_prompt_result(app, pid, val)
|
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
|
aza_showerror("Fehler", f"Bewertung fehlgeschlagen:\n{exc}", parent=win)
|
|
|
|
|
|
rating_inflight["busy"] = False
|
|
|
|
|
|
return
|
|
|
|
|
|
rating_inflight["busy"] = False
|
|
|
|
|
|
if not ok:
|
|
|
|
|
|
if msg == "__ROUTE_MISSING__":
|
|
|
|
|
|
aza_showinfo(
|
|
|
|
|
|
"Noch nicht live",
|
|
|
|
|
|
"Die Bewertungsfunktion ist auf dem Server noch nicht deployt.",
|
|
|
|
|
|
parent=win,
|
|
|
|
|
|
)
|
|
|
|
|
|
elif msg == "__CONN_ERROR__":
|
|
|
|
|
|
aza_showwarning(
|
|
|
|
|
|
"Server nicht erreichbar",
|
|
|
|
|
|
"Bitte später erneut versuchen.",
|
|
|
|
|
|
parent=win,
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
aza_showwarning("Fehler", msg or "Bewertung nicht möglich.", parent=win)
|
|
|
|
|
|
return
|
|
|
|
|
|
_apply_rating_fields_to_item(item, payload)
|
|
|
|
|
|
for src in all_source_items:
|
|
|
|
|
|
if str(src.get("id") or src.get("server_id") or "") == pid:
|
|
|
|
|
|
_apply_rating_fields_to_item(src, payload)
|
|
|
|
|
|
_apply_search()
|
|
|
|
|
|
sel = tree.selection()
|
|
|
|
|
|
if sel:
|
|
|
|
|
|
_show_detail(tree_iid_to_item.get(sel[0]))
|
|
|
|
|
|
aza_showinfo("Gespeichert", "Bewertung wurde gespeichert.", parent=win)
|
|
|
|
|
|
|
|
|
|
|
|
rating_bar = build_half_star_rating_bar(
|
|
|
|
|
|
rating_user_fr, bg=_SUB_BG, on_save=_submit_rating,
|
|
|
|
|
|
)
|
|
|
|
|
|
rating_bar["frame"].pack(side="left")
|
|
|
|
|
|
tk.Label(
|
|
|
|
|
|
rating_user_fr, textvariable=rating_bar["value_var"], font=(_FF, 8),
|
|
|
|
|
|
bg=_SUB_BG, fg=_TEXT_SUB,
|
|
|
|
|
|
).pack(side="left", padx=(8, 0))
|
|
|
|
|
|
|
|
|
|
|
|
all_source_items: list[dict[str, Any]] = []
|
|
|
|
|
|
library_items: list[dict[str, Any]] = []
|
|
|
|
|
|
tree_iid_to_item: dict[str, dict[str, Any]] = {}
|
|
|
|
|
|
|
|
|
|
|
|
def _close():
|
|
|
|
|
|
try:
|
|
|
|
|
|
app._public_doku_vorlagen_win = None
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
try:
|
|
|
|
|
|
win.destroy()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def _selected_item() -> dict[str, Any] | None:
|
|
|
|
|
|
sel = tree.selection()
|
|
|
|
|
|
if not sel:
|
|
|
|
|
|
return None
|
|
|
|
|
|
return tree_iid_to_item.get(sel[0])
|
|
|
|
|
|
|
|
|
|
|
|
def _show_detail(item: dict[str, Any] | None) -> None:
|
|
|
|
|
|
if not item:
|
|
|
|
|
|
for v in detail_vars.values():
|
|
|
|
|
|
v.set("—")
|
|
|
|
|
|
detail_desc.config(state="normal")
|
|
|
|
|
|
detail_desc.delete("1.0", "end")
|
|
|
|
|
|
detail_desc.config(state="disabled")
|
|
|
|
|
|
detail_prompt.config(state="normal")
|
|
|
|
|
|
detail_prompt.delete("1.0", "end")
|
|
|
|
|
|
detail_prompt.config(state="disabled")
|
|
|
|
|
|
rating_summary_var.set("Noch keine Bewertungen · 0 Bewertungen")
|
|
|
|
|
|
rating_own_hint_var.set("")
|
|
|
|
|
|
rating_bar["set_value"](0)
|
|
|
|
|
|
rating_bar["set_enabled"](False)
|
|
|
|
|
|
_detail_scroll_update()
|
|
|
|
|
|
return
|
|
|
|
|
|
dt_lbl = doc_type_label(item.get("doc_type") or "")
|
|
|
|
|
|
detail_vars["doc_type"].set(dt_lbl)
|
|
|
|
|
|
detail_vars["title"].set(item.get("title") or "Ohne Titel")
|
|
|
|
|
|
detail_vars["author"].set(item.get("author_display") or item.get("author") or "—")
|
|
|
|
|
|
detail_vars["location"].set(item.get("location") or item.get("city") or location)
|
|
|
|
|
|
detail_vars["specialty"].set(item.get("specialty") or specialty)
|
|
|
|
|
|
desc = (item.get("description") or "").strip() or "—"
|
|
|
|
|
|
detail_desc.config(state="normal")
|
|
|
|
|
|
detail_desc.delete("1.0", "end")
|
|
|
|
|
|
detail_desc.insert("1.0", desc)
|
|
|
|
|
|
detail_desc.config(state="disabled")
|
|
|
|
|
|
prompt = (item.get("content") or "").strip() or "—"
|
|
|
|
|
|
detail_prompt.config(state="normal")
|
|
|
|
|
|
detail_prompt.delete("1.0", "end")
|
|
|
|
|
|
detail_prompt.insert("1.0", prompt)
|
|
|
|
|
|
detail_prompt.config(state="disabled")
|
|
|
|
|
|
avg = item.get("rating_average")
|
|
|
|
|
|
cnt = int(item.get("rating_count") or 0)
|
|
|
|
|
|
rating_summary_var.set(format_public_rating_summary(avg, cnt))
|
|
|
|
|
|
own = is_own_public_item(app, item)
|
|
|
|
|
|
can_rate = bool(item.get("can_rate")) and not own and not item.get("_local_prepared")
|
|
|
|
|
|
if own:
|
|
|
|
|
|
rating_own_hint_var.set("Die eigene Vorlage kann nicht bewertet werden.")
|
|
|
|
|
|
rating_bar["set_enabled"](False)
|
|
|
|
|
|
rating_bar["set_value"](item.get("my_rating") or 0)
|
|
|
|
|
|
elif can_rate:
|
|
|
|
|
|
rating_own_hint_var.set("")
|
|
|
|
|
|
rating_bar["set_enabled"](True)
|
|
|
|
|
|
rating_bar["set_value"](item.get("my_rating") or 0)
|
|
|
|
|
|
else:
|
|
|
|
|
|
rating_own_hint_var.set("")
|
|
|
|
|
|
rating_bar["set_enabled"](False)
|
|
|
|
|
|
rating_bar["set_value"](item.get("my_rating") or 0)
|
|
|
|
|
|
_detail_scroll_update()
|
|
|
|
|
|
|
|
|
|
|
|
def _update_btn_state(_evt=None):
|
|
|
|
|
|
item = _selected_item()
|
|
|
|
|
|
has_sel = item is not None
|
|
|
|
|
|
own = is_own_public_item(app, item) if item else False
|
|
|
|
|
|
from aza_ui_helpers import set_tool_action_button_enabled
|
|
|
|
|
|
set_tool_action_button_enabled(btn_adopt, has_sel)
|
|
|
|
|
|
set_tool_action_button_enabled(btn_edit, own)
|
|
|
|
|
|
set_tool_action_button_enabled(btn_delete, own)
|
|
|
|
|
|
|
|
|
|
|
|
def _render_tree(items: list[dict[str, Any]], *, status_note: str = "") -> None:
|
|
|
|
|
|
for row in tree.get_children():
|
|
|
|
|
|
tree.delete(row)
|
|
|
|
|
|
library_items.clear()
|
|
|
|
|
|
tree_iid_to_item.clear()
|
|
|
|
|
|
for item in items:
|
|
|
|
|
|
if not isinstance(item, dict):
|
|
|
|
|
|
continue
|
|
|
|
|
|
dt = item.get("doc_type") or ""
|
|
|
|
|
|
library_items.append(item)
|
|
|
|
|
|
own = is_own_public_item(app, item)
|
|
|
|
|
|
iid = tree.insert(
|
|
|
|
|
|
"", "end",
|
|
|
|
|
|
values=(
|
|
|
|
|
|
doc_type_label(dt),
|
|
|
|
|
|
item.get("title") or "Ohne Titel",
|
|
|
|
|
|
format_public_rating_list_text(
|
|
|
|
|
|
item.get("rating_average"),
|
|
|
|
|
|
int(item.get("rating_count") or 0),
|
|
|
|
|
|
),
|
|
|
|
|
|
item.get("author_display") or item.get("author") or "—",
|
|
|
|
|
|
item.get("location") or item.get("city") or location,
|
|
|
|
|
|
item.get("specialty") or specialty,
|
|
|
|
|
|
"Eigene Veröffentlichung" if own else "",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
tree_iid_to_item[iid] = item
|
|
|
|
|
|
if status_note:
|
|
|
|
|
|
status_var.set(status_note)
|
|
|
|
|
|
_show_detail(None)
|
|
|
|
|
|
_update_btn_state()
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_search(*_evt) -> None:
|
|
|
|
|
|
filtered = filter_public_items(all_source_items, search_var.get())
|
|
|
|
|
|
total = len(all_source_items)
|
|
|
|
|
|
shown = len(filtered)
|
|
|
|
|
|
if total and shown != total:
|
|
|
|
|
|
note = f"{shown} von {total} Vorlage(n) (gefiltert)."
|
|
|
|
|
|
elif total:
|
|
|
|
|
|
note = f"{shown} öffentliche Vorlage(n)."
|
|
|
|
|
|
else:
|
|
|
|
|
|
note = status_var.get()
|
|
|
|
|
|
_render_tree(filtered, status_note=note)
|
|
|
|
|
|
|
|
|
|
|
|
def _set_source_items(items: list[dict[str, Any]]) -> None:
|
|
|
|
|
|
all_source_items.clear()
|
|
|
|
|
|
all_source_items.extend(items)
|
|
|
|
|
|
_apply_search()
|
|
|
|
|
|
|
|
|
|
|
|
def _populate(items: list[dict[str, Any]]) -> None:
|
|
|
|
|
|
merged = list(items) if items else []
|
|
|
|
|
|
if not merged:
|
|
|
|
|
|
local = load_local_prepared_public_items()
|
|
|
|
|
|
if local:
|
|
|
|
|
|
merged = local
|
|
|
|
|
|
_set_source_items(local)
|
|
|
|
|
|
status_var.set(
|
|
|
|
|
|
f"{len(local)} lokal vorbereitete Vorlage(n) "
|
|
|
|
|
|
f"(Live-Bibliothek benötigt noch Server-Deploy)."
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
status_var.set(_PUBLIC_LIB_PLACEHOLDER)
|
|
|
|
|
|
_set_source_items([])
|
|
|
|
|
|
return
|
|
|
|
|
|
if any(it.get("_local_prepared") for it in merged):
|
|
|
|
|
|
status_var.set(f"{len(merged)} Vorlage(n) (teilweise lokal vorbereitet).")
|
|
|
|
|
|
else:
|
|
|
|
|
|
status_var.set(f"{len(merged)} öffentliche Vorlage(n) geladen.")
|
|
|
|
|
|
_set_source_items(merged)
|
|
|
|
|
|
|
|
|
|
|
|
def _adopt_selected():
|
|
|
|
|
|
item = _selected_item()
|
|
|
|
|
|
if not item:
|
|
|
|
|
|
aza_showinfo("Hinweis", "Bitte eine Vorlage auswählen.", parent=win)
|
|
|
|
|
|
return
|
|
|
|
|
|
content = (item.get("content") or "").strip()
|
|
|
|
|
|
if not content:
|
|
|
|
|
|
aza_showwarning(
|
|
|
|
|
|
"Hinweis",
|
|
|
|
|
|
"Diese Vorlage enthält keinen Prompttext und kann nicht übernommen werden.",
|
|
|
|
|
|
parent=win,
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
dt = (item.get("doc_type") or "kg").strip()
|
|
|
|
|
|
if dt not in DOC_TYPE_KEYS:
|
|
|
|
|
|
aza_showwarning(
|
|
|
|
|
|
"Hinweis",
|
|
|
|
|
|
f"Unbekannter Dokumenttyp '{dt}'. Übernahme nicht möglich.",
|
|
|
|
|
|
parent=win,
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
new_id = adopt_public_template_to_own(app, item, activate=True)
|
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
|
aza_showerror(
|
|
|
|
|
|
"Fehler",
|
|
|
|
|
|
f"Übernahme fehlgeschlagen:\n{exc}",
|
|
|
|
|
|
parent=win,
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
if new_id:
|
|
|
|
|
|
dt_lbl = doc_type_label(dt)
|
|
|
|
|
|
aza_showinfo(
|
|
|
|
|
|
"Übernommen",
|
|
|
|
|
|
f"Vorlage wurde als eigene {dt_lbl}-Vorlage übernommen.\n"
|
|
|
|
|
|
"Sie erscheint im Doku-Prompt-Dropdown.",
|
|
|
|
|
|
parent=win,
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
aza_showwarning(
|
|
|
|
|
|
"Fehler",
|
|
|
|
|
|
"Vorlage konnte nicht als eigene Vorlage gespeichert werden.",
|
|
|
|
|
|
parent=win,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _delete_selected():
|
|
|
|
|
|
item = _selected_item()
|
|
|
|
|
|
if not item or not is_own_public_item(app, item):
|
|
|
|
|
|
return
|
|
|
|
|
|
qidx = item.get("_queue_index")
|
|
|
|
|
|
server_id = str(item.get("server_id") or item.get("id") or "").strip()
|
|
|
|
|
|
if qidx is None and server_id:
|
|
|
|
|
|
title = item.get("title") or "Ohne Titel"
|
|
|
|
|
|
if not aza_askyesno(
|
|
|
|
|
|
"Veröffentlichung zurückziehen?",
|
|
|
|
|
|
f"Veröffentlichung '{title}' vom Server entfernen?\n\n"
|
|
|
|
|
|
"Ihre private Vorlage (falls übernommen) bleibt erhalten.",
|
|
|
|
|
|
parent=win,
|
|
|
|
|
|
):
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
from aza_doku_prompt_sync import unpublish_doku_prompt
|
|
|
|
|
|
ok, msg = unpublish_doku_prompt(app, server_id)
|
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
|
aza_showerror("Fehler", f"Entfernen fehlgeschlagen:\n{exc}", parent=win)
|
|
|
|
|
|
return
|
|
|
|
|
|
if ok:
|
|
|
|
|
|
aza_showinfo("Entfernt", "Veröffentlichung wurde zurückgezogen.", parent=win)
|
|
|
|
|
|
_load_async()
|
|
|
|
|
|
elif msg == "__ROUTE_MISSING__":
|
|
|
|
|
|
aza_showinfo(
|
|
|
|
|
|
"Noch nicht live",
|
|
|
|
|
|
"Die Server-Funktion zum Zurückziehen ist noch nicht deployt.\n"
|
|
|
|
|
|
"Lokal vorbereitete Veröffentlichungen können weiterhin entfernt werden.",
|
|
|
|
|
|
parent=win,
|
|
|
|
|
|
)
|
|
|
|
|
|
elif msg == "__CONN_ERROR__":
|
|
|
|
|
|
aza_showwarning(
|
|
|
|
|
|
"Server nicht erreichbar",
|
|
|
|
|
|
"Der Server ist derzeit nicht erreichbar. Bitte später erneut versuchen.",
|
|
|
|
|
|
parent=win,
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
aza_showwarning("Fehler", msg or "Veröffentlichung konnte nicht entfernt werden.", parent=win)
|
|
|
|
|
|
return
|
|
|
|
|
|
if qidx is None:
|
|
|
|
|
|
aza_showinfo(
|
|
|
|
|
|
"Hinweis",
|
|
|
|
|
|
"Nur lokal vorbereitete eigene Veröffentlichungen können hier gelöscht werden.",
|
|
|
|
|
|
parent=win,
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
title = item.get("title") or "Ohne Titel"
|
|
|
|
|
|
if not aza_askyesno(
|
|
|
|
|
|
"Löschen?",
|
|
|
|
|
|
f"Veröffentlichung '{title}' aus der lokalen Queue entfernen?\n\n"
|
|
|
|
|
|
"Ihre private Vorlage (falls übernommen) bleibt erhalten.",
|
|
|
|
|
|
parent=win,
|
|
|
|
|
|
):
|
|
|
|
|
|
return
|
|
|
|
|
|
if remove_local_publish_queue_item(int(qidx)):
|
|
|
|
|
|
refreshed = load_local_prepared_public_items()
|
|
|
|
|
|
_set_source_items(refreshed)
|
|
|
|
|
|
status_var.set(f"{len(refreshed)} lokal vorbereitete Vorlage(n).")
|
|
|
|
|
|
aza_showinfo("Gelöscht", "Veröffentlichung wurde entfernt.", parent=win)
|
|
|
|
|
|
else:
|
|
|
|
|
|
aza_showwarning("Fehler", "Veröffentlichung konnte nicht gelöscht werden.", parent=win)
|
|
|
|
|
|
|
|
|
|
|
|
def _edit_selected():
|
|
|
|
|
|
item = _selected_item()
|
|
|
|
|
|
if not item or not is_own_public_item(app, item):
|
|
|
|
|
|
return
|
|
|
|
|
|
qidx = item.get("_queue_index")
|
|
|
|
|
|
server_id = str(item.get("server_id") or item.get("id") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
def _on_edit_confirm(meta: dict[str, Any]):
|
|
|
|
|
|
pub_dt = meta.get("doc_type") or item.get("doc_type") or "kg"
|
|
|
|
|
|
updates = {
|
|
|
|
|
|
"document_type_key": pub_dt,
|
|
|
|
|
|
"document_type_label": doc_type_label(pub_dt),
|
|
|
|
|
|
"title": meta.get("title") or "",
|
|
|
|
|
|
"description": meta.get("description") or "",
|
|
|
|
|
|
"author_display_name": meta.get("author") or "",
|
|
|
|
|
|
"author_initials": meta.get("initials") or "",
|
|
|
|
|
|
"specialty": meta.get("specialty") or "",
|
|
|
|
|
|
"region": meta.get("location") or "",
|
|
|
|
|
|
}
|
|
|
|
|
|
if qidx is not None and update_local_publish_queue_item(int(qidx), updates):
|
|
|
|
|
|
refreshed = load_local_prepared_public_items()
|
|
|
|
|
|
_set_source_items(refreshed)
|
|
|
|
|
|
aza_showinfo("Gespeichert", "Metadaten aktualisiert.", parent=win)
|
|
|
|
|
|
return
|
|
|
|
|
|
if server_id and qidx is None:
|
|
|
|
|
|
tpl = {
|
|
|
|
|
|
"content": item.get("content") or "",
|
|
|
|
|
|
"server_id": server_id,
|
|
|
|
|
|
"specialty": meta.get("specialty") or item.get("specialty") or "",
|
|
|
|
|
|
"city": meta.get("location") or item.get("city") or "",
|
|
|
|
|
|
}
|
|
|
|
|
|
ok, msg = publish_template_to_server(
|
|
|
|
|
|
app, pub_dt, tpl,
|
|
|
|
|
|
meta.get("title") or item.get("title") or "",
|
|
|
|
|
|
meta.get("description") or item.get("description") or "",
|
|
|
|
|
|
)
|
|
|
|
|
|
if ok:
|
|
|
|
|
|
aza_showinfo("Gespeichert", "Veröffentlichung auf dem Server aktualisiert.", parent=win)
|
|
|
|
|
|
_load_async()
|
|
|
|
|
|
elif msg == "__ROUTE_MISSING__":
|
|
|
|
|
|
aza_showinfo(
|
|
|
|
|
|
"Noch nicht live",
|
|
|
|
|
|
"Die Server-Funktion zum Bearbeiten ist noch nicht deployt.",
|
|
|
|
|
|
parent=win,
|
|
|
|
|
|
)
|
|
|
|
|
|
elif msg == "__CONN_ERROR__":
|
|
|
|
|
|
aza_showwarning(
|
|
|
|
|
|
"Server nicht erreichbar",
|
|
|
|
|
|
"Der Server ist derzeit nicht erreichbar. Bitte später erneut versuchen.",
|
|
|
|
|
|
parent=win,
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
aza_showwarning("Fehler", msg or "Metadaten konnten nicht gespeichert werden.", parent=win)
|
|
|
|
|
|
return
|
|
|
|
|
|
aza_showwarning("Fehler", "Metadaten konnten nicht gespeichert werden.", parent=win)
|
|
|
|
|
|
|
|
|
|
|
|
open_doku_publish_dialog(
|
|
|
|
|
|
app, win,
|
|
|
|
|
|
doc_type=item.get("doc_type") or "kg",
|
|
|
|
|
|
default_title=item.get("title") or "Ohne Titel",
|
|
|
|
|
|
on_confirm=_on_edit_confirm,
|
|
|
|
|
|
mode="edit",
|
|
|
|
|
|
initial_values={
|
|
|
|
|
|
"doc_type": item.get("doc_type") or "kg",
|
|
|
|
|
|
"title": item.get("title") or "",
|
|
|
|
|
|
"author": item.get("author_display") or item.get("author") or "",
|
|
|
|
|
|
"initials": item.get("author_initials") or "",
|
|
|
|
|
|
"location": item.get("location") or item.get("city") or "",
|
|
|
|
|
|
"specialty": item.get("specialty") or "",
|
|
|
|
|
|
"description": item.get("description") or "",
|
|
|
|
|
|
},
|
|
|
|
|
|
confirm_label="Speichern",
|
|
|
|
|
|
window_title="Metadaten bearbeiten",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _on_tree_select(_evt=None):
|
|
|
|
|
|
_show_detail(_selected_item())
|
|
|
|
|
|
_update_btn_state()
|
|
|
|
|
|
|
|
|
|
|
|
def _on_double_click(_evt=None):
|
|
|
|
|
|
item = _selected_item()
|
|
|
|
|
|
if item and is_own_public_item(app, item):
|
|
|
|
|
|
_edit_selected()
|
|
|
|
|
|
|
|
|
|
|
|
tree.bind("<<TreeviewSelect>>", _on_tree_select)
|
|
|
|
|
|
tree.bind("<Double-Button-1>", _on_double_click)
|
|
|
|
|
|
search_var.trace_add("write", lambda *_: _apply_search())
|
|
|
|
|
|
|
|
|
|
|
|
def _load_async():
|
|
|
|
|
|
import threading
|
|
|
|
|
|
|
|
|
|
|
|
def _worker():
|
|
|
|
|
|
try:
|
|
|
|
|
|
items, err = fetch_public_prompt_library_result(app)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
items, err = [], "__CONN_ERROR__"
|
|
|
|
|
|
if not items:
|
|
|
|
|
|
local = load_local_prepared_public_items()
|
|
|
|
|
|
if local:
|
|
|
|
|
|
note = (
|
|
|
|
|
|
f"{len(local)} lokal vorbereitete Vorlage(n) "
|
|
|
|
|
|
f"(Live-Bibliothek benötigt noch Server-Deploy)."
|
|
|
|
|
|
)
|
|
|
|
|
|
if err == "__CONN_ERROR__":
|
|
|
|
|
|
note = (
|
|
|
|
|
|
f"{len(local)} lokal vorbereitete Vorlage(n) "
|
|
|
|
|
|
f"(Server nicht erreichbar — lokaler Fallback)."
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
app.after(0, lambda: _populate_local_fallback(local, note))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return
|
|
|
|
|
|
if err == "__ROUTE_MISSING__":
|
|
|
|
|
|
note = _PUBLIC_LIB_PLACEHOLDER
|
|
|
|
|
|
elif err == "__CONN_ERROR__":
|
|
|
|
|
|
note = (
|
|
|
|
|
|
"Server nicht erreichbar. "
|
|
|
|
|
|
"Lokal vorbereitete Vorlagen werden angezeigt, sobald vorhanden."
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
note = _PUBLIC_LIB_PLACEHOLDER
|
|
|
|
|
|
try:
|
|
|
|
|
|
app.after(0, lambda: _populate_empty(note))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
app.after(0, lambda: _populate(items))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def _populate_local_fallback(local_items, note):
|
|
|
|
|
|
_populate(local_items)
|
|
|
|
|
|
status_var.set(note)
|
|
|
|
|
|
|
|
|
|
|
|
def _populate_empty(note):
|
|
|
|
|
|
status_var.set(note)
|
|
|
|
|
|
_set_source_items([])
|
|
|
|
|
|
|
|
|
|
|
|
threading.Thread(target=_worker, daemon=True).start()
|
|
|
|
|
|
|
|
|
|
|
|
from aza_ui_helpers import create_tool_action_button, set_tool_action_button_enabled
|
|
|
|
|
|
|
|
|
|
|
|
btn_adopt = create_tool_action_button(
|
|
|
|
|
|
action_bar, "In eigene Vorlage übernehmen", _adopt_selected, kind="primary",
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_adopt.pack(side="left", padx=(12, 4))
|
|
|
|
|
|
set_tool_action_button_enabled(btn_adopt, False)
|
|
|
|
|
|
btn_edit = create_tool_action_button(
|
|
|
|
|
|
action_bar, "Bearbeiten", _edit_selected, kind="secondary",
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_edit.pack(side="left", padx=4)
|
|
|
|
|
|
set_tool_action_button_enabled(btn_edit, False)
|
|
|
|
|
|
btn_delete = create_tool_action_button(
|
|
|
|
|
|
action_bar, "Löschen", _delete_selected, kind="danger",
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_delete.pack(side="left", padx=4)
|
|
|
|
|
|
set_tool_action_button_enabled(btn_delete, False)
|
|
|
|
|
|
create_tool_action_button(
|
|
|
|
|
|
action_bar, "Schliessen", _close, kind="close", width=10,
|
|
|
|
|
|
).pack(side="right", padx=(4, 12))
|
|
|
|
|
|
|
|
|
|
|
|
close_x.bind("<Button-1>", lambda e: _close())
|
|
|
|
|
|
win.protocol("WM_DELETE_WINDOW", _close)
|
|
|
|
|
|
_load_async()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-10 22:55:03 +02:00
|
|
|
|
# ── Fenster ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def open_doku_vorlage_fenster(app: Any) -> None:
|
|
|
|
|
|
"""Oeffnet das Doku-Prompt-Fenster. Singleton."""
|
|
|
|
|
|
existing = getattr(app, "_doku_vorlage_win", None)
|
|
|
|
|
|
if existing is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
if existing.winfo_exists():
|
|
|
|
|
|
existing.lift(); existing.focus_force(); return
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
win = tk.Toplevel(app)
|
|
|
|
|
|
app._doku_vorlage_win = win
|
|
|
|
|
|
win.title("Doku-Prompt")
|
|
|
|
|
|
win.configure(bg=_BG)
|
|
|
|
|
|
win.resizable(True, True)
|
2026-06-13 22:47:31 +02:00
|
|
|
|
win.minsize(_DOKU_WIN_MIN_W, _DOKU_WIN_MIN_H)
|
|
|
|
|
|
_doku_center_top(win, _DOKU_WIN_W, _DOKU_WIN_H, parent=app)
|
2026-06-10 22:55:03 +02:00
|
|
|
|
|
|
|
|
|
|
if hasattr(app, "_register_window"):
|
|
|
|
|
|
try: app._register_window(win)
|
|
|
|
|
|
except Exception: pass
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
import threading
|
|
|
|
|
|
threading.Thread(target=lambda: sync_private_doku_prompts(app), daemon=True).start()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# ── Header ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
hdr = tk.Frame(win, bg=_HDR_BG)
|
|
|
|
|
|
hdr.pack(fill="x")
|
|
|
|
|
|
tk.Label(hdr, text="📄 Doku-Prompt", font=(_FF, 13, "bold"),
|
|
|
|
|
|
bg=_HDR_BG, fg=_HDR_FG, padx=18, pady=10).pack(side="left")
|
|
|
|
|
|
close_x = tk.Label(hdr, text="✕", font=(_FF, 11), bg=_HDR_BG, fg="#A0C4D8",
|
2026-06-13 22:47:31 +02:00
|
|
|
|
cursor="hand2", padx=14)
|
2026-06-10 22:55:03 +02:00
|
|
|
|
close_x.pack(side="right")
|
|
|
|
|
|
|
|
|
|
|
|
sub = tk.Frame(win, bg=_SUB_BG)
|
|
|
|
|
|
sub.pack(fill="x")
|
|
|
|
|
|
tk.Label(sub,
|
|
|
|
|
|
text="Prompts für medizinische Dokumente anpassen. "
|
|
|
|
|
|
"Diagnose-/Therapie-Gliederung wird über das Häkchen 'Diagnosen gliedern' gesteuert.",
|
|
|
|
|
|
font=(_FF, 8), bg=_SUB_BG, fg=_TEXT_SUB,
|
|
|
|
|
|
padx=18, pady=5, wraplength=780, justify="left").pack(anchor="w")
|
|
|
|
|
|
|
|
|
|
|
|
# ── Hauptbereich ──────────────────────────────────────────────────────────
|
|
|
|
|
|
body = tk.Frame(win, bg=_BG)
|
|
|
|
|
|
body.pack(fill="both", expand=True, padx=10, pady=8)
|
|
|
|
|
|
body.columnconfigure(1, weight=1)
|
|
|
|
|
|
body.rowconfigure(0, weight=1)
|
|
|
|
|
|
|
|
|
|
|
|
# Linke Spalte — Dokumenttypen
|
|
|
|
|
|
left = tk.Frame(body, bg=_CARD_BG, bd=0,
|
|
|
|
|
|
highlightbackground=_CARD_BD, highlightthickness=1, width=140)
|
|
|
|
|
|
left.grid(row=0, column=0, sticky="nsew", padx=(0, 8))
|
|
|
|
|
|
left.grid_propagate(False)
|
|
|
|
|
|
|
|
|
|
|
|
tk.Label(left, text="Dokumenttyp", font=(_FF, 9, "bold"),
|
|
|
|
|
|
bg="#E0EEF7", fg=_TEXT, pady=6).pack(fill="x")
|
|
|
|
|
|
|
|
|
|
|
|
# Rechte Karte — Bearbeitungsfläche
|
|
|
|
|
|
right = tk.Frame(body, bg=_CARD_BG, bd=0,
|
|
|
|
|
|
highlightbackground=_CARD_BD, highlightthickness=1)
|
|
|
|
|
|
right.grid(row=0, column=1, sticky="nsew")
|
|
|
|
|
|
right.rowconfigure(2, weight=1)
|
|
|
|
|
|
right.columnconfigure(0, weight=1)
|
|
|
|
|
|
|
|
|
|
|
|
# ── State-Variablen ───────────────────────────────────────────────────────
|
|
|
|
|
|
_data: list[dict] = [_load()]
|
|
|
|
|
|
_cur_dt: list[str] = ["kg"]
|
|
|
|
|
|
_cur_tpl: list[dict] = [{}]
|
|
|
|
|
|
_dirty: list[bool] = [False]
|
|
|
|
|
|
|
|
|
|
|
|
combo_var = [None]
|
|
|
|
|
|
combo_widget = [None]
|
|
|
|
|
|
txt_content = [None]
|
|
|
|
|
|
lbl_doctype = [None]
|
|
|
|
|
|
lbl_active_info = [None]
|
|
|
|
|
|
lbl_mode = [None]
|
|
|
|
|
|
btn_save = [None]
|
|
|
|
|
|
btn_activate = [None]
|
|
|
|
|
|
|
|
|
|
|
|
def _tpls(dt): return _data[0].get("templates", {}).get(dt, [])
|
|
|
|
|
|
def _aid(dt): return _data[0].get("active", {}).get(dt, "aza_default")
|
|
|
|
|
|
def _by_id(dt, tid):
|
|
|
|
|
|
for t in _tpls(dt):
|
|
|
|
|
|
if isinstance(t, dict) and t.get("id") == tid: return t
|
|
|
|
|
|
return None
|
|
|
|
|
|
def _dname(t):
|
|
|
|
|
|
n = (t.get("name") or "Unbenannt").strip()
|
|
|
|
|
|
return f"★ {n}" if t.get("is_system") else n
|
|
|
|
|
|
|
|
|
|
|
|
def _refresh(dt: str, tpl_id: str | None = None):
|
|
|
|
|
|
templates = _tpls(dt)
|
|
|
|
|
|
active_id = _aid(dt)
|
|
|
|
|
|
target_id = tpl_id or active_id
|
|
|
|
|
|
tpl = _by_id(dt, target_id)
|
|
|
|
|
|
if tpl is None and templates:
|
|
|
|
|
|
tpl = templates[0]
|
|
|
|
|
|
if tpl is None:
|
|
|
|
|
|
tpl = {"id": "aza_default", "name": "AzA-Grundvorlage",
|
|
|
|
|
|
"is_system": True, "content": AZA_DEFAULT_TEMPLATES.get(dt, "")}
|
|
|
|
|
|
_cur_tpl[0] = tpl
|
|
|
|
|
|
|
|
|
|
|
|
# Combo
|
|
|
|
|
|
names = [_dname(t) for t in templates if isinstance(t, dict)]
|
|
|
|
|
|
if combo_widget[0]:
|
|
|
|
|
|
combo_widget[0]["values"] = names
|
|
|
|
|
|
combo_var[0].set(_dname(tpl))
|
|
|
|
|
|
|
|
|
|
|
|
# Label Dokumenttyp
|
|
|
|
|
|
_, dlabel = next((d for d in DOC_TYPES if d[0] == dt), (dt, dt))
|
|
|
|
|
|
if lbl_doctype[0]:
|
|
|
|
|
|
lbl_doctype[0].config(text=dlabel)
|
|
|
|
|
|
|
|
|
|
|
|
# Textfeld — IMMER beschreibbar, unabhaengig von Systemvorlage
|
|
|
|
|
|
if txt_content[0]:
|
|
|
|
|
|
txt_content[0].config(state="normal")
|
|
|
|
|
|
txt_content[0].delete("1.0", "end")
|
|
|
|
|
|
txt_content[0].insert("1.0", tpl.get("content") or "")
|
|
|
|
|
|
txt_content[0].config(state="normal", bg="#FFFFFF", fg=_TEXT)
|
|
|
|
|
|
|
|
|
|
|
|
# Aktive-Info
|
|
|
|
|
|
at = _by_id(dt, active_id)
|
|
|
|
|
|
aname = _dname(at) if at else active_id
|
|
|
|
|
|
if lbl_active_info[0]:
|
|
|
|
|
|
lbl_active_info[0].config(text=f"Aktive Vorlage: {aname}")
|
|
|
|
|
|
|
|
|
|
|
|
is_system = bool(tpl.get("is_system"))
|
|
|
|
|
|
is_active = (tpl.get("id") == active_id)
|
|
|
|
|
|
|
|
|
|
|
|
# Modus-Hinweis
|
|
|
|
|
|
if lbl_mode[0]:
|
|
|
|
|
|
if is_system:
|
|
|
|
|
|
lbl_mode[0].config(
|
|
|
|
|
|
text="Systemvorlage – 'Speichern' nicht direkt möglich → 'Als neue Vorlage speichern'",
|
|
|
|
|
|
fg="#B06000")
|
|
|
|
|
|
else:
|
|
|
|
|
|
lbl_mode[0].config(
|
|
|
|
|
|
text="Benutzervorlage – direkt bearbeitbar und speicherbar.",
|
|
|
|
|
|
fg="#2A7A4A")
|
|
|
|
|
|
|
|
|
|
|
|
# Buttons
|
|
|
|
|
|
if btn_activate[0]:
|
|
|
|
|
|
btn_activate[0].config(
|
|
|
|
|
|
text="Aktiv" if is_active else "Als aktiv setzen",
|
|
|
|
|
|
bg="#8AAFC8" if is_active else _BTN_ACT,
|
|
|
|
|
|
state="disabled" if is_active else "normal",
|
|
|
|
|
|
)
|
|
|
|
|
|
if btn_save[0]:
|
|
|
|
|
|
# Speichern bei Systemvorlage deaktivieren — klare Führung
|
|
|
|
|
|
btn_save[0].config(
|
|
|
|
|
|
state="disabled" if is_system else "normal",
|
|
|
|
|
|
bg="#8AAFC8" if is_system else _BTN_BLUE,
|
|
|
|
|
|
)
|
|
|
|
|
|
_dirty[0] = False
|
|
|
|
|
|
|
|
|
|
|
|
def _on_combo(evt=None):
|
|
|
|
|
|
dt = _cur_dt[0]
|
|
|
|
|
|
sel = combo_var[0].get() if combo_var[0] else ""
|
|
|
|
|
|
for t in _tpls(dt):
|
|
|
|
|
|
if isinstance(t, dict) and _dname(t) == sel:
|
|
|
|
|
|
_refresh(dt, t.get("id")); return
|
|
|
|
|
|
|
|
|
|
|
|
def _do_save():
|
|
|
|
|
|
tpl = _cur_tpl[0]
|
|
|
|
|
|
if tpl.get("is_system"):
|
2026-06-13 22:47:31 +02:00
|
|
|
|
aza_showinfo(
|
2026-06-10 22:55:03 +02:00
|
|
|
|
"Systemvorlage",
|
|
|
|
|
|
"Die AzA-Grundvorlage kann nicht direkt überschrieben werden.\n\n"
|
|
|
|
|
|
"Bitte verwenden Sie 'Als neue Vorlage speichern', um eine\n"
|
|
|
|
|
|
"eigene Variante zu erstellen.",
|
|
|
|
|
|
parent=win)
|
|
|
|
|
|
return
|
|
|
|
|
|
content = sanitize_prompt_text(txt_content[0].get("1.0", "end") if txt_content[0] else "")
|
|
|
|
|
|
tpl["content"] = content
|
|
|
|
|
|
tpl["updated_at"] = _utc_now()
|
|
|
|
|
|
tpl["revision"] = int(tpl.get("revision") or 0) + 1
|
|
|
|
|
|
try:
|
|
|
|
|
|
_save(_data[0])
|
|
|
|
|
|
except Exception as e:
|
2026-06-13 22:47:31 +02:00
|
|
|
|
aza_showerror("Fehler", str(e), parent=win); return
|
2026-06-10 22:55:03 +02:00
|
|
|
|
_dirty[0] = False
|
2026-06-13 22:47:31 +02:00
|
|
|
|
aza_showinfo("Gespeichert", "Vorlage gespeichert.", parent=win)
|
2026-06-10 22:55:03 +02:00
|
|
|
|
|
|
|
|
|
|
def _do_save_new():
|
|
|
|
|
|
dt = _cur_dt[0]
|
2026-06-13 22:47:31 +02:00
|
|
|
|
name = aza_askstring("Neue Vorlage", "Name der neuen Vorlage:", parent=win)
|
2026-06-10 22:55:03 +02:00
|
|
|
|
if not name or not name.strip(): return
|
|
|
|
|
|
content = sanitize_prompt_text(txt_content[0].get("1.0", "end") if txt_content[0] else "")
|
|
|
|
|
|
new_id = f"user_{int(time.time())}_{dt}"
|
|
|
|
|
|
uid, pid = _app_user_practice_ids(app)
|
|
|
|
|
|
new_tpl = {
|
|
|
|
|
|
"id": new_id, "name": name.strip(), "is_system": False,
|
|
|
|
|
|
"content": content, "description": "",
|
|
|
|
|
|
"created_at": _utc_now(), "updated_at": _utc_now(),
|
|
|
|
|
|
"revision": 1, "user_id": uid, "practice_id": pid,
|
|
|
|
|
|
"server_id": "", "published": False, "sync_updated_at": "",
|
|
|
|
|
|
"source_author": "", "source_server_id": "",
|
|
|
|
|
|
}
|
|
|
|
|
|
_data[0].setdefault("templates", {}).setdefault(dt, []).append(new_tpl)
|
2026-06-13 22:47:31 +02:00
|
|
|
|
activate = aza_askyesno("Aktivieren?",
|
2026-06-10 22:55:03 +02:00
|
|
|
|
f"Neue Vorlage '{name.strip()}' gespeichert.\nJetzt aktivieren?", parent=win)
|
|
|
|
|
|
if activate:
|
|
|
|
|
|
_data[0].setdefault("active", {})[dt] = new_id
|
|
|
|
|
|
try:
|
|
|
|
|
|
_save(_data[0])
|
|
|
|
|
|
except Exception as e:
|
2026-06-13 22:47:31 +02:00
|
|
|
|
aza_showerror("Fehler", str(e), parent=win); return
|
2026-06-10 22:55:03 +02:00
|
|
|
|
_refresh(dt, new_id if activate else _aid(dt))
|
|
|
|
|
|
|
|
|
|
|
|
def _do_activate():
|
|
|
|
|
|
dt = _cur_dt[0]; tpl = _cur_tpl[0]; tid = tpl.get("id", "")
|
|
|
|
|
|
if not tid: return
|
|
|
|
|
|
_data[0].setdefault("active", {})[dt] = tid
|
|
|
|
|
|
try: _save(_data[0])
|
|
|
|
|
|
except Exception as e:
|
2026-06-13 22:47:31 +02:00
|
|
|
|
aza_showerror("Fehler", str(e), parent=win); return
|
2026-06-10 22:55:03 +02:00
|
|
|
|
_refresh(dt, tid)
|
|
|
|
|
|
|
|
|
|
|
|
def _do_reset():
|
|
|
|
|
|
dt = _cur_dt[0]
|
|
|
|
|
|
_, dlabel = next((d for d in DOC_TYPES if d[0] == dt), (dt, dt))
|
2026-06-13 22:47:31 +02:00
|
|
|
|
if not aza_askyesno(
|
2026-06-10 22:55:03 +02:00
|
|
|
|
"Zurücksetzen?",
|
|
|
|
|
|
f"'{dlabel}' auf AzA-Grundvorlage zurücksetzen?\n\n"
|
|
|
|
|
|
"Die aktive Vorlage wird auf AzA-Grundvorlage gesetzt.\n"
|
|
|
|
|
|
"Ihre eigenen Varianten werden NICHT gelöscht.", parent=win):
|
|
|
|
|
|
return
|
|
|
|
|
|
templates = _tpls(dt)
|
|
|
|
|
|
has_default = any(isinstance(t, dict) and t.get("id") == "aza_default"
|
|
|
|
|
|
for t in templates)
|
|
|
|
|
|
if not has_default:
|
|
|
|
|
|
now = _utc_now()
|
|
|
|
|
|
_data[0].setdefault("templates", {}).setdefault(dt, []).insert(0, {
|
|
|
|
|
|
"id": "aza_default", "name": "AzA-Grundvorlage", "is_system": True,
|
|
|
|
|
|
"content": AZA_DEFAULT_TEMPLATES.get(dt, ""),
|
|
|
|
|
|
"created_at": now, "updated_at": now})
|
|
|
|
|
|
_data[0].setdefault("active", {})[dt] = "aza_default"
|
|
|
|
|
|
try: _save(_data[0])
|
|
|
|
|
|
except Exception as e:
|
2026-06-13 22:47:31 +02:00
|
|
|
|
aza_showerror("Fehler", str(e), parent=win); return
|
2026-06-10 22:55:03 +02:00
|
|
|
|
_refresh(dt, "aza_default")
|
|
|
|
|
|
|
|
|
|
|
|
def _do_delete():
|
|
|
|
|
|
dt = _cur_dt[0]
|
|
|
|
|
|
tpl = _cur_tpl[0]
|
|
|
|
|
|
if tpl.get("is_system"):
|
2026-06-13 22:47:31 +02:00
|
|
|
|
aza_showinfo("Systemvorlage", "AzA-Grundvorlage kann nicht gelöscht werden.", parent=win)
|
2026-06-10 22:55:03 +02:00
|
|
|
|
return
|
|
|
|
|
|
tid = tpl.get("id", "")
|
|
|
|
|
|
if not tid:
|
|
|
|
|
|
return
|
|
|
|
|
|
name = (tpl.get("name") or "Unbenannt").strip()
|
2026-06-13 22:47:31 +02:00
|
|
|
|
if not aza_askyesno("Löschen?", f"Vorlage '{name}' wirklich löschen?", parent=win):
|
2026-06-10 22:55:03 +02:00
|
|
|
|
return
|
|
|
|
|
|
templates = _tpls(dt)
|
|
|
|
|
|
_data[0]["templates"][dt] = [t for t in templates if not (isinstance(t, dict) and t.get("id") == tid)]
|
|
|
|
|
|
if _aid(dt) == tid:
|
|
|
|
|
|
_data[0].setdefault("active", {})[dt] = "aza_default"
|
|
|
|
|
|
try:
|
|
|
|
|
|
_save(_data[0])
|
|
|
|
|
|
except Exception as e:
|
2026-06-13 22:47:31 +02:00
|
|
|
|
aza_showerror("Fehler", str(e), parent=win)
|
2026-06-10 22:55:03 +02:00
|
|
|
|
return
|
|
|
|
|
|
_refresh(dt, _aid(dt))
|
|
|
|
|
|
|
|
|
|
|
|
def _do_publish():
|
|
|
|
|
|
dt = _cur_dt[0]
|
|
|
|
|
|
tpl = dict(_cur_tpl[0])
|
|
|
|
|
|
content = sanitize_prompt_text(txt_content[0].get("1.0", "end") if txt_content[0] else "")
|
|
|
|
|
|
if not content:
|
2026-06-13 22:47:31 +02:00
|
|
|
|
aza_showinfo("Hinweis", "Bitte zuerst einen Prompt-Inhalt eingeben.", parent=win)
|
2026-06-10 22:55:03 +02:00
|
|
|
|
return
|
|
|
|
|
|
tpl["content"] = content
|
|
|
|
|
|
default_title = (tpl.get("name") or doc_type_label(dt)).strip()
|
|
|
|
|
|
|
2026-06-13 22:47:31 +02:00
|
|
|
|
def _publish_done(ok: bool, msg: str):
|
|
|
|
|
|
try:
|
|
|
|
|
|
app.set_status("Bereit.")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
if ok:
|
|
|
|
|
|
tpl["published"] = True
|
|
|
|
|
|
tpl["updated_at"] = _utc_now()
|
|
|
|
|
|
_save(_data[0])
|
|
|
|
|
|
aza_showinfo(
|
|
|
|
|
|
"Veröffentlicht",
|
|
|
|
|
|
"Prompt wurde für andere angemeldete Ärzte veröffentlicht.",
|
|
|
|
|
|
parent=win,
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
if msg == "__ROUTE_MISSING__" or (msg or "").strip().lower() in ("not found", "notfound"):
|
|
|
|
|
|
aza_showinfo("Veröffentlichung vorbereitet", _PUBLISH_ROUTE_MSG, parent=win)
|
|
|
|
|
|
return
|
|
|
|
|
|
if msg == "__CONN_ERROR__":
|
|
|
|
|
|
aza_showwarning(
|
|
|
|
|
|
"Server nicht erreichbar",
|
|
|
|
|
|
"Der Server ist derzeit nicht erreichbar.\n\n"
|
|
|
|
|
|
"Die Veröffentlichung wurde lokal vorbereitet und geht nicht verloren. "
|
|
|
|
|
|
"Bitte später erneut versuchen.",
|
|
|
|
|
|
parent=win,
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
aza_showwarning(
|
|
|
|
|
|
"Veröffentlichung",
|
|
|
|
|
|
(msg or "Veröffentlichung nicht möglich.") +
|
|
|
|
|
|
"\n\n(Die Vorlage wurde lokal vorbereitet.)",
|
|
|
|
|
|
parent=win,
|
|
|
|
|
|
)
|
2026-06-10 22:55:03 +02:00
|
|
|
|
|
2026-06-13 22:47:31 +02:00
|
|
|
|
def _on_publish_confirm(meta: dict[str, Any]):
|
|
|
|
|
|
title = meta.get("title") or ""
|
|
|
|
|
|
desc = meta.get("description") or ""
|
|
|
|
|
|
author_display = meta.get("author") or ""
|
|
|
|
|
|
pub_dt = meta.get("doc_type") or dt
|
|
|
|
|
|
uid, _ = _app_user_practice_ids(app)
|
|
|
|
|
|
try:
|
|
|
|
|
|
save_local_publish_payload({
|
|
|
|
|
|
"document_type_key": pub_dt,
|
|
|
|
|
|
"document_type_label": doc_type_label(pub_dt),
|
|
|
|
|
|
"title": title,
|
|
|
|
|
|
"description": desc,
|
|
|
|
|
|
"prompt_text": content,
|
|
|
|
|
|
"author_display_name": author_display,
|
|
|
|
|
|
"author_initials": meta.get("initials") or "",
|
|
|
|
|
|
"author_user_id": uid,
|
|
|
|
|
|
"specialty": meta.get("specialty") or "",
|
|
|
|
|
|
"language": "de",
|
|
|
|
|
|
"region": meta.get("location") or "",
|
|
|
|
|
|
"status": "prepared",
|
|
|
|
|
|
"created_at": _utc_now(),
|
|
|
|
|
|
"revision": int(tpl.get("revision") or 1),
|
|
|
|
|
|
"source_template_id": tpl.get("id") or "",
|
|
|
|
|
|
})
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
app.set_status("Veröffentliche Prompt …")
|
|
|
|
|
|
|
|
|
|
|
|
def _worker():
|
|
|
|
|
|
try:
|
|
|
|
|
|
ok, msg = publish_template_to_server(app, pub_dt, tpl, title, desc)
|
|
|
|
|
|
except Exception as e: # noqa: BLE001
|
|
|
|
|
|
ok, msg = False, str(e)
|
|
|
|
|
|
app.after(0, lambda: _publish_done(ok, msg))
|
|
|
|
|
|
|
|
|
|
|
|
import threading
|
|
|
|
|
|
threading.Thread(target=_worker, daemon=True).start()
|
|
|
|
|
|
|
|
|
|
|
|
open_doku_publish_dialog(
|
|
|
|
|
|
app, win, doc_type=dt, default_title=default_title, on_confirm=_on_publish_confirm,
|
2026-06-10 22:55:03 +02:00
|
|
|
|
)
|
2026-06-13 22:47:31 +02:00
|
|
|
|
|
|
|
|
|
|
def _external_refresh(dt=None, tpl_id=None):
|
|
|
|
|
|
_data[0] = _load()
|
|
|
|
|
|
target_dt = dt or _cur_dt[0]
|
|
|
|
|
|
if target_dt != _cur_dt[0]:
|
|
|
|
|
|
_sel_dt(target_dt)
|
|
|
|
|
|
else:
|
|
|
|
|
|
_refresh(target_dt, tpl_id or _aid(target_dt))
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
app._doku_vorlage_refresh_cb = _external_refresh
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
2026-06-10 22:55:03 +02:00
|
|
|
|
|
|
|
|
|
|
def _close():
|
|
|
|
|
|
try: app._doku_vorlage_win = None
|
|
|
|
|
|
except Exception: pass
|
2026-06-13 22:47:31 +02:00
|
|
|
|
try: app._doku_vorlage_refresh_cb = None
|
|
|
|
|
|
except Exception: pass
|
2026-06-10 22:55:03 +02:00
|
|
|
|
try: win.destroy()
|
|
|
|
|
|
except Exception: pass
|
|
|
|
|
|
|
|
|
|
|
|
close_x.bind("<Button-1>", lambda e: _close())
|
|
|
|
|
|
win.protocol("WM_DELETE_WINDOW", _close)
|
|
|
|
|
|
|
|
|
|
|
|
# ── Linke Buttons (Dokumenttypen) ─────────────────────────────────────────
|
|
|
|
|
|
_dt_btns: dict[str, tk.Label] = {}
|
|
|
|
|
|
|
|
|
|
|
|
def _sel_dt(dt: str):
|
|
|
|
|
|
_cur_dt[0] = dt
|
|
|
|
|
|
for k, b in _dt_btns.items():
|
|
|
|
|
|
b.config(bg=_ACCENT if k == dt else _CARD_BG,
|
|
|
|
|
|
fg="#FFFFFF" if k == dt else _TEXT)
|
|
|
|
|
|
_refresh(dt)
|
|
|
|
|
|
|
|
|
|
|
|
for dt_key, dt_label in DOC_TYPES:
|
|
|
|
|
|
b = tk.Label(left, text=dt_label, font=(_FF, 9),
|
|
|
|
|
|
bg=_CARD_BG, fg=_TEXT, cursor="hand2",
|
|
|
|
|
|
pady=9, anchor="w", padx=12)
|
|
|
|
|
|
b.pack(fill="x")
|
|
|
|
|
|
b.bind("<Button-1>", lambda e, dk=dt_key: _sel_dt(dk))
|
|
|
|
|
|
_dt_btns[dt_key] = b
|
|
|
|
|
|
|
2026-06-13 22:47:31 +02:00
|
|
|
|
tk.Frame(left, bg=_CARD_BD, height=1).pack(fill="x", padx=10, pady=(10, 6))
|
|
|
|
|
|
btn_public_left = tk.Button(
|
|
|
|
|
|
left, text="Öffentliche Vorlagen",
|
|
|
|
|
|
command=lambda: open_public_doku_vorlagen_window(app, parent=win),
|
|
|
|
|
|
font=(_FF, 8, "bold"), bg="#3A6F8F", fg="#FFFFFF",
|
|
|
|
|
|
relief="flat", bd=0, padx=8, pady=8, cursor="hand2",
|
|
|
|
|
|
activebackground="#2E5F7A",
|
|
|
|
|
|
)
|
|
|
|
|
|
btn_public_left.pack(fill="x", padx=10, pady=(0, 10))
|
|
|
|
|
|
|
2026-06-10 22:55:03 +02:00
|
|
|
|
# ── Rechte Karte ──────────────────────────────────────────────────────────
|
|
|
|
|
|
# Zeile 0: Header (Dokumenttyp + aktive Vorlage)
|
|
|
|
|
|
top_bar = tk.Frame(right, bg="#E0EEF7")
|
|
|
|
|
|
top_bar.grid(row=0, column=0, sticky="ew")
|
|
|
|
|
|
|
|
|
|
|
|
lbl_doctype[0] = tk.Label(top_bar, text="Krankengeschichte", font=(_FF, 10, "bold"),
|
|
|
|
|
|
bg="#E0EEF7", fg=_TEXT, padx=12, pady=6)
|
|
|
|
|
|
lbl_doctype[0].pack(side="left")
|
|
|
|
|
|
lbl_active_info[0] = tk.Label(top_bar, text="Aktive Vorlage: AzA-Grundvorlage",
|
|
|
|
|
|
font=(_FF, 8), bg="#E0EEF7", fg=_TEXT_SUB, padx=8)
|
|
|
|
|
|
lbl_active_info[0].pack(side="left")
|
|
|
|
|
|
|
|
|
|
|
|
# Zeile 1: Vorlage-Auswahl + Modus-Hinweis
|
|
|
|
|
|
sel_bar = tk.Frame(right, bg=_CARD_BG)
|
|
|
|
|
|
sel_bar.grid(row=1, column=0, sticky="ew", padx=10, pady=(6, 2))
|
|
|
|
|
|
|
|
|
|
|
|
tk.Label(sel_bar, text="Vorlage:", font=(_FF, 8),
|
|
|
|
|
|
bg=_CARD_BG, fg=_TEXT_SUB).pack(side="left")
|
|
|
|
|
|
combo_var[0] = tk.StringVar()
|
|
|
|
|
|
combo_widget[0] = ttk.Combobox(sel_bar, textvariable=combo_var[0],
|
|
|
|
|
|
state="readonly", width=32, font=(_FF, 9))
|
|
|
|
|
|
combo_widget[0].pack(side="left", padx=(4, 12))
|
|
|
|
|
|
combo_widget[0].bind("<<ComboboxSelected>>", _on_combo)
|
|
|
|
|
|
|
|
|
|
|
|
lbl_mode[0] = tk.Label(sel_bar, text="", font=(_FF, 7),
|
|
|
|
|
|
bg=_CARD_BG, fg="#B06000", padx=4)
|
|
|
|
|
|
lbl_mode[0].pack(side="left")
|
|
|
|
|
|
|
|
|
|
|
|
# Zeile 2: Textfeld (expandierend)
|
|
|
|
|
|
txt_frame = tk.Frame(right, bg=_CARD_BG)
|
|
|
|
|
|
txt_frame.grid(row=2, column=0, sticky="nsew", padx=10, pady=(2, 4))
|
|
|
|
|
|
txt_frame.rowconfigure(0, weight=1)
|
|
|
|
|
|
txt_frame.columnconfigure(0, weight=1)
|
|
|
|
|
|
|
|
|
|
|
|
vsb = tk.Scrollbar(txt_frame, orient="vertical")
|
|
|
|
|
|
vsb.grid(row=0, column=1, sticky="ns")
|
|
|
|
|
|
txt_content[0] = tk.Text(txt_frame, font=(_FF, 9),
|
|
|
|
|
|
bg="#FFFFFF", fg=_TEXT,
|
|
|
|
|
|
relief="flat", bd=0, wrap="word",
|
|
|
|
|
|
yscrollcommand=vsb.set,
|
|
|
|
|
|
highlightbackground=_CARD_BD, highlightthickness=1,
|
|
|
|
|
|
padx=8, pady=8)
|
|
|
|
|
|
txt_content[0].grid(row=0, column=0, sticky="nsew")
|
|
|
|
|
|
vsb.config(command=txt_content[0].yview)
|
|
|
|
|
|
txt_content[0].bind("<KeyRelease>", lambda e: _dirty.__setitem__(0, True))
|
|
|
|
|
|
|
|
|
|
|
|
# Zeile 3: Button-Leiste (feste Höhe, nie abgeschnitten)
|
|
|
|
|
|
btn_row = tk.Frame(right, bg="#E8F0F5", pady=8)
|
|
|
|
|
|
btn_row.grid(row=3, column=0, sticky="ew", padx=0)
|
|
|
|
|
|
|
|
|
|
|
|
def _mk(parent, text, cmd, bg, fg="#FFFFFF", width=16):
|
|
|
|
|
|
return tk.Button(parent, text=text, command=cmd,
|
|
|
|
|
|
font=(_FF, 8, "bold"), bg=bg, fg=fg,
|
|
|
|
|
|
relief="flat", bd=0, padx=10, pady=5,
|
|
|
|
|
|
cursor="hand2", width=width,
|
|
|
|
|
|
activebackground=bg)
|
|
|
|
|
|
|
|
|
|
|
|
btn_save[0] = _mk(btn_row, "Speichern", _do_save, _BTN_BLUE, width=12)
|
|
|
|
|
|
btn_save[0].pack(side="left", padx=(10, 4))
|
|
|
|
|
|
_mk(btn_row, "Als neue Vorlage", _do_save_new, _BTN_BLUE, width=16).pack(side="left", padx=4)
|
|
|
|
|
|
btn_activate[0] = _mk(btn_row, "Als aktiv setzen", _do_activate, _BTN_ACT, width=15)
|
|
|
|
|
|
btn_activate[0].pack(side="left", padx=4)
|
|
|
|
|
|
_mk(btn_row, "Löschen", _do_delete, _BTN_RESET, width=10).pack(side="left", padx=4)
|
|
|
|
|
|
btn_publish = _mk(
|
2026-06-13 22:47:31 +02:00
|
|
|
|
btn_row, "Veröffentlichen", _do_publish, _BTN_BLUE, width=14,
|
2026-06-10 22:55:03 +02:00
|
|
|
|
)
|
|
|
|
|
|
btn_publish.pack(side="left", padx=4)
|
|
|
|
|
|
try:
|
|
|
|
|
|
from aza_ui_helpers import add_tooltip
|
|
|
|
|
|
add_tooltip(btn_publish, "Aktuellen Prompt für andere angemeldete Ärzte freigeben.")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
_mk(btn_row, "Schließen", _close, _BTN_CLOSE, width=10).pack(side="right", padx=(4, 10))
|
|
|
|
|
|
_mk(btn_row, "↩ Auf Grundvorlage", _do_reset, _BTN_RESET, width=18).pack(side="right", padx=4)
|
|
|
|
|
|
|
|
|
|
|
|
# Initialer Zustand — immer Krankengeschichte
|
|
|
|
|
|
_sel_dt("kg")
|