This commit is contained in:
2026-06-13 22:47:31 +02:00
parent add3da5177
commit d1446fc452
8032 changed files with 2650751 additions and 1551 deletions

View File

@@ -8550,6 +8550,53 @@ async def empfang_delete(msg_id: str, request: Request):
# TASKS (practice-scoped, server-side)
# =====================================================================
def _practice_user_by_id(practice_id: str, user_id: str) -> dict | None:
uid = (user_id or "").strip()
if not uid:
return None
for u in _practice_users(practice_id):
if str(u.get("user_id") or "").strip() == uid:
return u
return None
def _apply_task_assignee_from_body(
target: dict, body: dict, practice_id: str, *, allow_legacy_name: bool = False
) -> None:
"""Zuweisung nur über stabile user_id; assignee-Anzeigename wird serverseitig gesetzt."""
pid = (practice_id or "").strip()
if "assignee_user_id" in body:
uid = (body.get("assignee_user_id") or "").strip()
if not uid:
target["assignee_user_id"] = ""
target["assignee"] = ""
return
u = _practice_user_by_id(pid, uid)
if not u:
raise HTTPException(
status_code=400,
detail="Empfänger nicht gefunden oder nicht in dieser Praxis",
)
target["assignee_user_id"] = uid
target["assignee"] = (u.get("display_name") or "").strip()
return
if "assignee" in body and allow_legacy_name:
name = (body.get("assignee") or "").strip()
target["assignee"] = name
matched = None
if name:
nl = name.lower()
for u in _practice_users(pid):
dn = (u.get("display_name") or "").strip()
if dn.lower() == nl:
matched = u
break
target["assignee_user_id"] = (
str(matched.get("user_id") or "").strip() if matched else ""
)
@router.get("/tasks")
async def empfang_tasks_list(request: Request):
pid = _resolve_practice_id(request)
@@ -8593,13 +8640,17 @@ async def empfang_tasks_create(request: Request):
"source_peer": peer_opt or "",
"source_thread_id": stid_opt or "",
"done": False,
"assignee": (body.get("assignee") or "").strip(),
"assignee": "",
"assignee_user_id": "",
"created_by": (s or {}).get("display_name", "") if s else "",
"created": time.strftime("%Y-%m-%d %H:%M:%S"),
"source_msg_id": (body.get("source_msg_id") or "").strip(),
"source_attachment_ids": att_ids,
"item_kind": item_kind,
}
_apply_task_assignee_from_body(
task, body, pid, allow_legacy_name=bool((body.get("assignee") or "").strip())
)
tasks = _load_tasks()
tasks.insert(0, task)
_save_tasks(tasks)
@@ -8608,6 +8659,7 @@ async def empfang_tasks_create(request: Request):
@router.post("/tasks/{task_id}/update")
async def empfang_tasks_update(task_id: str, request: Request):
pid = _resolve_practice_id(request)
try:
body = await request.json()
except Exception:
@@ -8616,14 +8668,22 @@ async def empfang_tasks_update(task_id: str, request: Request):
target = next((t for t in tasks if t.get("task_id") == task_id), None)
if not target:
raise HTTPException(status_code=404, detail="Aufgabe nicht gefunden")
task_pid = (target.get("practice_id") or "").strip()
if pid and task_pid and pid != task_pid:
raise HTTPException(status_code=403, detail="Aufgabe gehört nicht zu dieser Praxis")
if "done" in body:
target["done"] = bool(body["done"])
if "text" in body:
target["text"] = (body["text"] or "").strip() or target["text"]
if "title" in body:
target["title"] = (body.get("title") or "").strip()
if "assignee" in body:
target["assignee"] = (body.get("assignee") or "").strip()
if "assignee_user_id" in body:
_apply_task_assignee_from_body(target, body, task_pid or pid or "")
elif "assignee" in body:
raise HTTPException(
status_code=400,
detail="Zuweisung nur über assignee_user_id (Empfänger aus Liste wählen)",
)
if "source_meta" in body:
target["source_meta"] = (body.get("source_meta") or "").strip()
_save_tasks(tasks)
@@ -9604,6 +9664,8 @@ async def empfang_shell_context_me(request: Request):
"dm_open_external_peer_user_id": "",
"dm_open_external_link_id": "",
"dm_open_external_display_name": "",
"nav_open_kind": "",
"nav_open_assignee": "",
},
headers={"Cache-Control": "no-store, no-cache, must-revalidate"},
)
@@ -9623,6 +9685,8 @@ async def empfang_shell_context_me(request: Request):
dm_ext_uid_out = str(rec.get("dm_open_external_peer_user_id") or "")
dm_ext_lid_out = str(rec.get("dm_open_external_link_id") or "")
dm_ext_dn_out = str(rec.get("dm_open_external_display_name") or "")
nav_kind_out = str(rec.get("nav_open_kind") or "")
nav_assignee_out = str(rec.get("nav_open_assignee") or "")
# Einmalige Auslieferung: dm_open-Felder (intern wie extern) nach dem Lesen leeren.
if dm_uid_out or dm_ext_uid_out:
rec["dm_open_peer_user_id"] = ""
@@ -9633,6 +9697,10 @@ async def empfang_shell_context_me(request: Request):
rec["dm_open_external_link_id"] = ""
rec["dm_open_external_display_name"] = ""
_desktop_shell_latest_context_by_user[(pid, uid)] = rec
if nav_kind_out:
rec["nav_open_kind"] = ""
rec["nav_open_assignee"] = ""
_desktop_shell_latest_context_by_user[(pid, uid)] = rec
return JSONResponse(
content={
@@ -9649,6 +9717,8 @@ async def empfang_shell_context_me(request: Request):
"dm_open_external_peer_user_id": dm_ext_uid_out,
"dm_open_external_link_id": dm_ext_lid_out,
"dm_open_external_display_name": dm_ext_dn_out,
"nav_open_kind": nav_kind_out,
"nav_open_assignee": nav_assignee_out,
},
headers={"Cache-Control": "no-store, no-cache, must-revalidate"},
)
@@ -9745,6 +9815,61 @@ async def empfang_shell_dm_open(request: Request):
)
@router.post("/shell/nav-open")
async def empfang_shell_nav_open(request: Request):
"""Kontakt-Panel: Aufgaben-/Briefe-Badge oeffnet Ansicht in der Chat-Huelle.
Reine Wiederverwendung des Shell-Context-Stores (wie dm-open). Keine Inhalte loggen.
"""
_desktop_shell_context_cleanup()
s = _require_session(request)
pid = (s.get("practice_id") or "").strip()
uid = (s.get("user_id") or "").strip()
if not pid or not uid:
raise HTTPException(status_code=400, detail="practice_id/user_id fehlen in der Session")
try:
body_raw = await request.json()
body = body_raw if isinstance(body_raw, dict) else {}
except Exception:
body = {}
nav_kind = str(body.get("nav_open_kind") or "").strip().lower()
if nav_kind not in ("task", "letter"):
raise HTTPException(status_code=400, detail="nav_open_kind muss task oder letter sein")
nav_assignee = str(body.get("nav_open_assignee") or "").strip()[:200]
now = time.time()
expires_at = now + float(_DESKTOP_SHELL_CONTEXT_TTL_SECONDS)
rec = _desktop_shell_latest_context_by_user.get((pid, uid))
if not isinstance(rec, dict) or float(rec.get("expires_at", 0)) < now:
rec = {
"context_id": "ctx_" + secrets.token_urlsafe(24),
"practice_id": pid,
"user_id": uid,
"therapy_text": "",
"procedure_text": "",
"therapy_autocopy": False,
"procedure_autocopy": False,
}
rec["nav_open_kind"] = nav_kind
rec["nav_open_assignee"] = nav_assignee
rec["expires_at"] = expires_at
rec["updated_at"] = now
_desktop_shell_latest_context_by_user[(pid, uid)] = rec
_log.info(
"DESKTOP_SHELL_NAV_OPEN_SET practice=%s user=%s kind=%s",
pid,
uid,
nav_kind,
)
return JSONResponse(
content={"success": True},
headers={"Cache-Control": "no-store, no-cache, must-revalidate"},
)
@router.post("/shell/consume")
async def empfang_shell_consume(request: Request):
"""Tauscht shell_token gegen aza_session (HttpOnly Cookie). Einmal-Verbrauch."""