981 lines
38 KiB
Python
981 lines
38 KiB
Python
|
|
# -*- 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
|
|||
|
|
from tkinter import messagebox, simpledialog, ttk
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
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",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
source_author: str = "",
|
|||
|
|
source_server_id: str = "",
|
|||
|
|
) -> str | None:
|
|||
|
|
"""Übernimmt eine öffentliche Vorlage als neue eigene Kopie (neue ID)."""
|
|||
|
|
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),
|
|||
|
|
"description": "",
|
|||
|
|
"created_at": now,
|
|||
|
|
"updated_at": now,
|
|||
|
|
"revision": 1,
|
|||
|
|
"user_id": "",
|
|||
|
|
"practice_id": "",
|
|||
|
|
"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)
|
|||
|
|
_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 []
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── 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
|
|||
|
|
|
|||
|
|
W, H = 820, 600
|
|||
|
|
|
|||
|
|
win = tk.Toplevel(app)
|
|||
|
|
app._doku_vorlage_win = win
|
|||
|
|
win.title("Doku-Prompt")
|
|||
|
|
win.configure(bg=_BG)
|
|||
|
|
win.resizable(True, True)
|
|||
|
|
win.minsize(700, 520)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
win.transient(app)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
win.update_idletasks()
|
|||
|
|
sw = win.winfo_screenwidth()
|
|||
|
|
x = max(0, (sw - W) // 2)
|
|||
|
|
win.geometry(f"{W}x{H}+{x}+60")
|
|||
|
|
except Exception:
|
|||
|
|
win.geometry(f"{W}x{H}")
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
win.lift(); win.focus_force()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
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",
|
|||
|
|
cursor="hand2", padx=14)
|
|||
|
|
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]
|
|||
|
|
library_items: list[list[dict]] = [[]]
|
|||
|
|
library_listbox: list[tk.Listbox | None] = [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"):
|
|||
|
|
messagebox.showinfo(
|
|||
|
|
"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:
|
|||
|
|
messagebox.showerror("Fehler", str(e), parent=win); return
|
|||
|
|
_dirty[0] = False
|
|||
|
|
messagebox.showinfo("Gespeichert", "Vorlage gespeichert.", parent=win)
|
|||
|
|
|
|||
|
|
def _do_save_new():
|
|||
|
|
dt = _cur_dt[0]
|
|||
|
|
name = simpledialog.askstring("Neue Vorlage", "Name der neuen Vorlage:", parent=win)
|
|||
|
|
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)
|
|||
|
|
activate = messagebox.askyesno("Aktivieren?",
|
|||
|
|
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:
|
|||
|
|
messagebox.showerror("Fehler", str(e), parent=win); return
|
|||
|
|
_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:
|
|||
|
|
messagebox.showerror("Fehler", str(e), parent=win); return
|
|||
|
|
_refresh(dt, tid)
|
|||
|
|
|
|||
|
|
def _do_reset():
|
|||
|
|
dt = _cur_dt[0]
|
|||
|
|
_, dlabel = next((d for d in DOC_TYPES if d[0] == dt), (dt, dt))
|
|||
|
|
if not messagebox.askyesno(
|
|||
|
|
"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:
|
|||
|
|
messagebox.showerror("Fehler", str(e), parent=win); return
|
|||
|
|
_refresh(dt, "aza_default")
|
|||
|
|
|
|||
|
|
def _do_delete():
|
|||
|
|
dt = _cur_dt[0]
|
|||
|
|
tpl = _cur_tpl[0]
|
|||
|
|
if tpl.get("is_system"):
|
|||
|
|
messagebox.showinfo("Systemvorlage", "AzA-Grundvorlage kann nicht gelöscht werden.", parent=win)
|
|||
|
|
return
|
|||
|
|
tid = tpl.get("id", "")
|
|||
|
|
if not tid:
|
|||
|
|
return
|
|||
|
|
name = (tpl.get("name") or "Unbenannt").strip()
|
|||
|
|
if not messagebox.askyesno("Löschen?", f"Vorlage '{name}' wirklich löschen?", parent=win):
|
|||
|
|
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:
|
|||
|
|
messagebox.showerror("Fehler", str(e), parent=win)
|
|||
|
|
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:
|
|||
|
|
messagebox.showinfo("Hinweis", "Bitte zuerst einen Prompt-Inhalt eingeben.", parent=win)
|
|||
|
|
return
|
|||
|
|
tpl["content"] = content
|
|||
|
|
default_title = (tpl.get("name") or doc_type_label(dt)).strip()
|
|||
|
|
title = simpledialog.askstring("Titel", "Titel für Veröffentlichung:", initialvalue=default_title, parent=win)
|
|||
|
|
if not title or not title.strip():
|
|||
|
|
return
|
|||
|
|
desc = simpledialog.askstring("Beschreibung", "Kurze Beschreibung (optional):", parent=win) or ""
|
|||
|
|
preview = content[:600] + ("…" if len(content) > 600 else "")
|
|||
|
|
if not messagebox.askyesno(
|
|||
|
|
"Veröffentlichen?",
|
|||
|
|
f"Dokumenttyp: {doc_type_label(dt)}\nTitel: {title.strip()}\n\nVorschau:\n{preview}\n\n"
|
|||
|
|
"Bitte prüfen Sie, dass diese Vorlage keine Patientendaten oder "
|
|||
|
|
"vertraulichen Praxisdaten enthält.\n\n"
|
|||
|
|
"Für andere angemeldete Ärzte veröffentlichen?",
|
|||
|
|
parent=win,
|
|||
|
|
):
|
|||
|
|
return
|
|||
|
|
ok, msg = publish_template_to_server(app, dt, tpl, title.strip(), desc.strip())
|
|||
|
|
if ok:
|
|||
|
|
tpl["published"] = True
|
|||
|
|
tpl["updated_at"] = _utc_now()
|
|||
|
|
_save(_data[0])
|
|||
|
|
messagebox.showinfo("Veröffentlicht", "Prompt wurde veröffentlicht.", parent=win)
|
|||
|
|
_refresh_library()
|
|||
|
|
else:
|
|||
|
|
messagebox.showwarning("Veröffentlichung", msg or "Server nicht erreichbar — lokal gespeichert.", parent=win)
|
|||
|
|
|
|||
|
|
def _refresh_library():
|
|||
|
|
if not library_listbox[0]:
|
|||
|
|
return
|
|||
|
|
library_listbox[0].delete(0, "end")
|
|||
|
|
library_items[0].clear()
|
|||
|
|
for item in fetch_public_prompt_library(app):
|
|||
|
|
if not isinstance(item, dict):
|
|||
|
|
continue
|
|||
|
|
dt = item.get("doc_type") or ""
|
|||
|
|
title = item.get("title") or "Ohne Titel"
|
|||
|
|
author = item.get("author_display") or item.get("author") or "Unbekannt"
|
|||
|
|
label = f"[{doc_type_label(dt)}] {title} — {author}"
|
|||
|
|
library_listbox[0].insert("end", label)
|
|||
|
|
library_items[0].append(item)
|
|||
|
|
|
|||
|
|
def _adopt_library_item(as_new: bool = True):
|
|||
|
|
sel = library_listbox[0].curselection() if library_listbox[0] else ()
|
|||
|
|
if not sel:
|
|||
|
|
messagebox.showinfo("Hinweis", "Bitte eine Vorlage aus der Bibliothek wählen.", parent=win)
|
|||
|
|
return
|
|||
|
|
item = library_items[0][sel[0]]
|
|||
|
|
dt = item.get("doc_type") or _cur_dt[0]
|
|||
|
|
content = item.get("content") or ""
|
|||
|
|
title = item.get("title") or "Übernommene Vorlage"
|
|||
|
|
author = item.get("author_display") or item.get("author") or ""
|
|||
|
|
sid = item.get("id") or item.get("server_id") or ""
|
|||
|
|
new_id = copy_public_template_as_own(
|
|||
|
|
dt, content=content, title=title,
|
|||
|
|
source_author=author, source_server_id=sid,
|
|||
|
|
)
|
|||
|
|
if new_id:
|
|||
|
|
_sel_dt(dt)
|
|||
|
|
_refresh(dt, new_id)
|
|||
|
|
messagebox.showinfo("Übernommen", "Vorlage als eigene Kopie gespeichert.", parent=win)
|
|||
|
|
|
|||
|
|
def _close():
|
|||
|
|
try: app._doku_vorlage_win = None
|
|||
|
|
except Exception: pass
|
|||
|
|
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
|
|||
|
|
|
|||
|
|
# ── 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(
|
|||
|
|
btn_row, "Für Ärzte veröffentlichen", _do_publish, _BTN_BLUE, width=24,
|
|||
|
|
)
|
|||
|
|
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)
|
|||
|
|
|
|||
|
|
# Öffentliche Prompt-Bibliothek
|
|||
|
|
lib_frame = tk.Frame(win, bg=_BG)
|
|||
|
|
lib_frame.pack(fill="x", padx=10, pady=(0, 8))
|
|||
|
|
tk.Label(lib_frame, text="Öffentliche Prompt-Bibliothek", font=(_FF, 9, "bold"),
|
|||
|
|
bg=_BG, fg=_TEXT).pack(anchor="w")
|
|||
|
|
lib_inner = tk.Frame(lib_frame, bg=_CARD_BG,
|
|||
|
|
highlightbackground=_CARD_BD, highlightthickness=1)
|
|||
|
|
lib_inner.pack(fill="x", pady=(4, 0))
|
|||
|
|
lb = tk.Listbox(lib_inner, font=(_FF, 8), height=4, bg="#FFFFFF", fg=_TEXT,
|
|||
|
|
selectbackground=_ACCENT, activestyle="none")
|
|||
|
|
lb.pack(side="left", fill="both", expand=True, padx=6, pady=6)
|
|||
|
|
library_listbox[0] = lb
|
|||
|
|
lib_btns = tk.Frame(lib_inner, bg=_CARD_BG)
|
|||
|
|
lib_btns.pack(side="right", fill="y", padx=6, pady=6)
|
|||
|
|
tk.Button(lib_btns, text="Aktualisieren", command=_refresh_library,
|
|||
|
|
font=(_FF, 8), bg=_BTN_BLUE, fg="#FFFFFF", relief="flat",
|
|||
|
|
cursor="hand2", padx=8, pady=4).pack(fill="x", pady=(0, 4))
|
|||
|
|
tk.Button(lib_btns, text="Als eigene Vorlage\nübernehmen", command=lambda: _adopt_library_item(True),
|
|||
|
|
font=(_FF, 8), bg=_BTN_BLUE, fg="#FFFFFF", relief="flat",
|
|||
|
|
cursor="hand2", padx=8, pady=4).pack(fill="x")
|
|||
|
|
|
|||
|
|
# Initialer Zustand — immer Krankengeschichte
|
|||
|
|
_sel_dt("kg")
|
|||
|
|
_refresh_library()
|