Files
aza/AzA march 2026 - Kopie (27)/empfang.html

6225 lines
261 KiB
HTML
Raw Normal View History

2026-05-20 00:09:28 +02:00
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<link rel="icon" href="/empfang/favicon.ico" sizes="any"/>
<link rel="icon" href="aza_logo.png" type="image/png"/>
<title>AzA-Empfang</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;overflow:hidden}
/* rem bezieht sich auf html — muss mit Basisschrift (9pt) übereinstimmen, bis applyFontSize() läuft */
html{font-size:9pt}
body{font-family:'Segoe UI',system-ui,sans-serif;background:#eaf1f8;color:#1a2a3a;font-size:9pt;display:flex;flex-direction:column}
:root{
--aza-blue:#5B8DB3;
--aza-blue-deep:#356488;
--aza-blue-soft:#dceaf4;
--aza-surface:#f6fafd;
--aza-panel:#ffffff;
--aza-rail-bg:linear-gradient(180deg,#3d6f92 0%,#305d7d 52%,#2a5472 100%);
/* Logo-Blase: gleiche Farbfamilie wie Audio-Kachel / Diktat-Button (#eef5fb, Rand wie .btn-dictate) */
--aza-rail-logo-bubble-bg:linear-gradient(180deg,#eef6fb,#e4eef6);
--aza-rail-logo-bubble-border:1px solid #c8dae8;
--aza-rail-active:rgba(255,255,255,.22);
--aza-shadow-sm:0 1px 3px rgba(28,62,94,.06);
--aza-shadow-md:0 4px 20px rgba(28,62,94,.09);
}
/* === Header === */
header{background:linear-gradient(135deg,#5B8DB3,#3a6d93);color:#fff;padding:10px 20px;display:flex;align-items:flex-start;justify-content:space-between;box-shadow:var(--aza-shadow-md);flex-shrink:0;gap:8px;z-index:50}
.header-title-block{display:flex;flex-direction:column;align-items:flex-start;gap:2px;line-height:1.22;min-width:0;padding-top:2px}
header h1{font-size:1.2rem;font-weight:600;letter-spacing:.3px;white-space:nowrap;margin:0}
#header-practice-sub{font-size:.72rem;font-weight:500;opacity:.9;letter-spacing:.02px;white-space:normal;max-width:min(62vw,480px)}
.practice-identity-card{background:#f4f9fc;border:1px solid #d8e4ef;border-radius:12px;padding:12px 14px;margin-bottom:8px;line-height:1.45;color:#2a4660;font-size:.82rem}
.practice-identity-card .pi-main{font-weight:700;font-size:.95rem;color:#1a4d6d;margin-bottom:8px}
.practice-identity-card .pi-row{margin:5px 0;display:flex;flex-wrap:wrap;gap:6px 12px;align-items:center}
.pi-k{color:#6a8499;font-weight:600;font-size:.71rem;text-transform:uppercase;letter-spacing:.06em}
.pi-v.pi-mono{font-family:ui-monospace,Consolas,Menlo,monospace;font-size:.76rem;color:#334e66;word-break:break-all}
.pi-code{font-family:ui-monospace,Consolas,Menlo,monospace;background:#eef4fb;padding:3px 8px;border-radius:6px;user-select:all;font-size:.78rem}
.pi-hint{font-size:.75rem;color:#7a909e;margin:.35rem 0 0;line-height:1.4}
.pi-status{font-size:.78rem;color:#4a677a;margin:.55rem 0 0;line-height:1.45}
.sb-users-hint{padding:2px 18px 11px;font-size:.67rem;line-height:1.38;color:#7a909e;margin-top:-2px}
.header-right{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.hbtn{background:none;border:1px solid rgba(255,255,255,.3);color:#fff;border-radius:5px;padding:3px 9px;font-size:.78rem;cursor:pointer;transition:background .15s;white-space:nowrap}
.hbtn:hover{background:rgba(255,255,255,.15)}
.hbtn.on{background:rgba(255,255,255,.25);border-color:rgba(255,255,255,.55)}
.hbtn.muted{opacity:.5}
.hbtn.active-sort{background:rgba(255,255,255,.25)}
.badge{background:rgba(255,255,255,.2);border-radius:16px;padding:3px 12px;font-size:.8rem}
.font-ctrl{display:flex;align-items:center;gap:2px}
.font-ctrl span{font-size:.72rem;opacity:.8}
.font-ctrl button{background:none;border:1px solid rgba(255,255,255,.25);color:#fff;border-radius:3px;width:20px;height:20px;cursor:pointer;font-size:.65rem;line-height:1;display:flex;align-items:center;justify-content:center}
.font-ctrl button:hover{background:rgba(255,255,255,.15)}
.vol-ctrl{display:flex;align-items:center;gap:3px}
.vol-ctrl input[type=range]{width:50px;accent-color:#fff;cursor:pointer}
.vol-ctrl span{font-size:.7rem;opacity:.8;min-width:26px;text-align:right}
/* === Settings Panel === */
.settings-panel{display:none;background:#fff;border-bottom:1px solid #e0e4e8;padding:12px 20px;flex-shrink:0;z-index:40}
.settings-panel.open{display:block}
.settings-inner h3{font-size:.9rem;color:#1a4d6d;margin-bottom:8px}
.setting-row{display:flex;align-items:center;gap:8px;margin-bottom:6px;flex-wrap:wrap}
.setting-row label{font-size:.82rem;color:#3a5a7a}
.setting-row select,.setting-row input[type=number]{border:1px solid #ccc;border-radius:4px;padding:3px 6px;font-size:.82rem}
.setting-row input[type=number]{width:60px}
.btn-test-small{background:#e8f0f8;color:#2a5a8a;border:1px solid #c8d8e8;border-radius:4px;padding:2px 8px;font-size:.78rem;cursor:pointer}
.btn-test-small:hover{background:#d4e4f0}
.btn-cleanup{background:#f8e8e8;color:#8a2a2a;border:1px solid #e0c8c8;border-radius:4px;padding:2px 8px;font-size:.78rem;cursor:pointer}
.btn-cleanup:hover{background:#f0d4d4}
/* === App layout: rail + list + main + tasks === */
#app-layout{display:flex;flex:1;min-height:0;overflow:hidden}
/* Schmale Icon-Leiste */
#nav-rail{
width:72px;min-width:72px;flex-shrink:0;
background:var(--aza-rail-bg);
display:flex;flex-direction:column;align-items:center;
padding:12px 0 16px;border-right:1px solid rgba(255,255,255,.12);
box-shadow:inset -1px 0 0 rgba(0,0,0,.06);
z-index:20;
}
.nr-brand{
width:44px;height:44px;border-radius:12px;background:var(--aza-rail-logo-bubble-bg);
color:#fff;font-size:.68rem;font-weight:700;letter-spacing:.5px;
display:flex;align-items:center;justify-content:center;margin-bottom:14px;
border:var(--aza-rail-logo-bubble-border);
overflow:hidden;padding:3px;box-sizing:border-box;
}
.nr-brand img{
width:100%;height:100%;object-fit:contain;display:block;border-radius:8px;
}
.nr-btn{
position:relative;
width:44px;height:44px;border-radius:11px;border:none;cursor:pointer;margin-bottom:8px;
background:transparent;color:rgba(255,255,255,.88);font-size:1.15rem;line-height:1;
display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s;
}
.nr-btn:hover{background:rgba(255,255,255,.12);color:#fff}
.nr-btn.nr-active{background:var(--aza-rail-active);box-shadow:0 0 0 1px rgba(255,255,255,.35);color:#fff}
#sb-badge-open.nr-open-badge{
position:absolute;top:3px;right:3px;min-width:1em;font-size:.54rem;line-height:1;text-align:center;
font-weight:800;color:#fff;background:#3d7aaf;border-radius:6px;padding:1px 3px;font-variant-numeric:tabular-nums
}
.nr-spacer{flex:1;min-height:8px}
/* Linke Gesamtspalte (Desktop-Hülle): nur mit data-desktop-shell */
#nr-sidebar-ds{display:none}
html[data-desktop-shell="1"] #nr-sidebar-ds{display:flex}
#nr-dcp-context{display:none;font-size:.95rem}
html[data-desktop-shell="1"] #nr-dcp-context{display:flex}
/* === Listenspalte / Kanäle + Threads === */
#sidebar{width:min(100%,320px);min-width:260px;max-width:360px;background:var(--aza-surface);border-right:1px solid #dfe8f2;display:flex;flex-direction:column;flex-shrink:0;overflow:hidden}
#sidebar-scroll{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;scroll-behavior:smooth}
#sidebar-scroll::-webkit-scrollbar{width:8px}
#sidebar-scroll::-webkit-scrollbar-thumb{background:#c8d9e8;border-radius:8px}
.thread-list-section{margin-top:4px;padding-bottom:20px}
.thread-list-section .tl-section-label{
font-size:.65rem;font-weight:700;color:#7a8ea2;text-transform:uppercase;letter-spacing:.9px;
padding:12px 18px 8px;
}
.thread-list-caption{font-size:.8rem;font-weight:600;color:#1a4d6d;padding:0 18px 10px;line-height:1.35}
#thread-list-mount{padding:0 12px 16px}
.thread-list-empty{font-size:.82rem;color:#8a9aaa;padding:16px 18px;line-height:1.45}
.nav-card-row{display:flex;gap:8px;padding:0 14px 14px}
.nav-card{
flex:1;background:var(--aza-panel);border:1px solid #e2ebf4;border-radius:14px;padding:12px 10px;
cursor:pointer;text-align:left;transition:box-shadow .18s,border-color .15s,transform .12s;
box-shadow:var(--aza-shadow-sm);min-width:0;font-family:inherit;
}
.nav-card:hover{box-shadow:var(--aza-shadow-md);border-color:#c5d8ea;transform:translateY(-1px)}
.nav-card.active{border-color:var(--aza-blue);background:linear-gradient(145deg,#f2f8fc,#e8f3fa);box-shadow:0 0 0 1px rgba(91,141,179,.2)}
.nav-card-title{display:block;font-weight:700;font-size:.85rem;color:#1a3a5a}
.nav-card-meta{display:block;font-size:.68rem;color:#7a8ea2;margin-top:3px}
.sb-me{padding:12px 14px;background:#f4f8fc;border-bottom:1px solid #eef2f6;display:flex;align-items:center;gap:8px}
.sb-me-dot{width:8px;height:8px;border-radius:50%;background:#6bc47f;flex-shrink:0}
.sb-me-name{font-weight:600;font-size:.88rem;color:#1a3a5a;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sb-me-change{background:none;border:none;color:#8a9aaa;cursor:pointer;font-size:.72rem;padding:2px 4px}
.sb-me-change:hover{color:#5B8DB3}
.sb-section{font-size:.68rem;font-weight:700;color:#8a9aaa;text-transform:uppercase;letter-spacing:.8px;padding:14px 14px 5px}
.sb-section-future{color:#b8c4cc;padding-top:18px}
.sb-placeholder{padding:2px 18px 10px;font-size:.72rem;color:#aab8c4;line-height:1.35}
.nav-card-meta #sb-badge-open{font-weight:700;color:#356488;font-variant-numeric:tabular-nums}
.sb-user{padding:10px 12px;margin:0 12px 6px;background:var(--aza-panel);border:1px solid #e8eff6;border-radius:12px;font-size:.82rem;color:#3a5a7a;cursor:pointer;display:flex;align-items:center;gap:9px;transition:background .14s,border-color .14s,box-shadow .14s}
.sb-user-disabled{opacity:.55;cursor:not-allowed}
.sb-user-disabled:hover{background:var(--aza-panel)}
.dm-input-error{box-shadow:0 0 0 2px rgba(220,53,69,.45)}
.sb-user:hover{background:#f0f7fc;border-color:#d0dfea;box-shadow:var(--aza-shadow-sm)}
.sb-user.me{font-weight:600;color:#1a3a5a;background:linear-gradient(135deg,#f4f9fd,#eaf3fa);border-color:#bcd4e8}
.sb-user.dm-selected{border-color:var(--aza-blue);box-shadow:0 0 0 1px rgba(91,141,179,.2);background:linear-gradient(145deg,#f2f8fc,#e8f3fa)}
/* Presence: dezent mint — Auswahl (dm-selected) bleibt dominant */
.sb-user.sb-user-presence-active:not(.dm-selected){
background:linear-gradient(135deg,#f4faf7,#eef6f1);
border-color:#cfe8db;
}
.sb-user.sb-user-presence-active:not(.dm-selected):hover{
background:linear-gradient(135deg,#eef8f2,#e8f4ec);
border-color:#b8d9c9;
}
.sb-user.me.sb-user-presence-active:not(.dm-selected){
background:linear-gradient(135deg,#f2faf5,#eaf5ee);
border-color:#c5e0d4;
}
.sb-user.dm-selected.sb-user-presence-active{
border-color:var(--aza-blue);
box-shadow:0 0 0 2px rgba(91,141,179,.28);
background:linear-gradient(145deg,#e8f2fc,#e8f4ee);
}
.sb-users-wrap{padding:6px 0 8px}
.sb-user.sb-user-self{cursor:pointer}
.sb-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.sb-dot.online{background:#6bc47f}
.sb-dot.away{background:#e8c547}
.sb-dot.offline{background:#ccc}
.sb-user-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sb-group-tag{font-size:.62rem;color:#95a5b5;background:#f4f6f9;padding:1px 5px;border-radius:6px;font-weight:500}
.sb-add{padding:6px 14px 6px 18px;font-size:.78rem;color:#5B8DB3;cursor:pointer;display:flex;align-items:center;gap:6px;transition:background .12s}
.sb-add:hover{background:#f4f8fc}
.sb-add-input{display:none;padding:4px 14px 8px 18px}
.sb-add-input.open{display:block}
.sb-add-input input{width:100%;border:1px solid #ccc;border-radius:4px;padding:4px 8px;font-size:.82rem;font-family:inherit}
/* === Hauptereich Conversation === */
#chat-area{flex:1;display:flex;flex-direction:column;min-width:0;background:#f0f6fb}
#chat-pane{
flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden;
}
/* Threadliste liegt in der Mitte (Messenger), nicht mehr in der Sidebar */
#middle-stack{display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden}
.inbox-thread-panel{
flex:0 1 auto;
max-height:min(44vh,460px);min-height:0;overflow:hidden;
display:flex;flex-direction:column;
background:linear-gradient(180deg,#f6fafd 0%,#eef5fb 100%);
border-bottom:1px solid #dfe8ef;
}
.inbox-thread-panel.hidden{display:none!important}
.itp-inner{flex:1;overflow-y:auto;padding:14px clamp(14px,2.5vw,32px)}
.itp-inner::-webkit-scrollbar{width:8px}
.itp-inner::-webkit-scrollbar-thumb{background:#c8d9e8;border-radius:8px}
.inbox-thread-panel .thread-list-caption{font-size:.82rem;font-weight:700;color:#1a4d6d;padding:0 4px 10px;line-height:1.35}
.inbox-thread-panel #thread-list-mount{padding:0 2px 10px}
#chat-top-bar{
padding:14px 22px;background:var(--aza-panel);border-bottom:1px solid #dfe8ef;
display:flex;align-items:center;gap:12px;flex-shrink:0;min-height:56px;
box-shadow:var(--aza-shadow-sm);
}
.ctb-back{
background:#eef6fb;border:1px solid #d0e2ef;color:var(--aza-blue-deep);cursor:pointer;font-size:.82rem;font-weight:600;
padding:8px 12px;border-radius:10px;display:none;align-items:center;gap:6px;font-family:inherit;
transition:background .15s,border-color .15s;
}
.ctb-back:hover{background:#e4eef6;border-color:#b8cce0}
.ctb-back.visible{display:inline-flex}
html[data-desktop-shell="1"] #ctb-back{display:none!important}
.ctb-title{font-weight:700;font-size:1.05rem;color:#1a2d42;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;letter-spacing:.01em}
#chat-top-bar:has(.ctb-back:not(.visible)) .ctb-title{text-align:center}
.ctb-actions{display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end}
.shell-patient-strip{
display:none;align-items:center;flex-wrap:wrap;gap:10px;padding:8px 18px;background:#eef4fb;border-bottom:1px solid #dfe8ef;flex-shrink:0;
}
html[data-desktop-shell="1"] .shell-patient-strip{display:flex}
.shell-patient-strip .sp-pill{display:inline-flex;align-items:center;gap:8px;background:var(--aza-panel);border:1px solid #d8e6f2;border-radius:999px;padding:5px 12px;font-size:.73rem;color:#466684}
.shell-patient-strip .sp-label{font-weight:700;color:#72899f;letter-spacing:.02em}
.shell-patient-strip .sp-value{font-weight:700;color:#1a4d6d;min-width:2ch}
.shell-patient-strip .sp-x{
margin-left:2px;border:none;background:transparent;color:#aab8c8;cursor:pointer;font-size:.95rem;line-height:1;padding:2px 5px;border-radius:6px;font-family:inherit;
}
.shell-patient-strip .sp-x:hover{color:#943535;background:#fdeeed}
.shell-patient-strip .sp-pinsel-btn{
border:1px solid #cfe0ec;background:#f6fafd;color:#466684;font-size:.71rem;font-weight:700;padding:6px 12px;border-radius:999px;cursor:pointer;font-family:inherit;line-height:1.25;
}
.shell-patient-strip .sp-pinsel-btn:hover{border-color:#9ebdd4;background:#eef6fb;color:#294a62}
.shell-pinsel-active .shell-patient-strip{border-bottom-color:#c5dcea}
.shell-pinsel-active .shell-patient-strip .sp-pinsel-btn{background:#f9e2e6;border-color:#efbfc6;color:#721c24}
.shell-patient-strip .sp-pinsel-msg{font-size:.68rem;color:#7a909f;line-height:1.35;flex:1 1 140px;min-width:0}
.shell-patient-strip .sp-pinsel-diag{
flex:1 1 100%;font-family:ui-monospace,Consolas,Menlo,monospace;font-size:.58rem;line-height:1.35;color:#8a9cae;
margin:2px 0 0;padding:0 2px 0 0;font-weight:500;opacity:.92;letter-spacing:.01em;min-height:1.1em;
}
.shell-pinsel-active .shell-patient-strip .sp-pinsel-diag{color:#75899a;}
/* Patienten-Zeile in linker Spalte (Desktop-Huelle): gleiche IDs/Logik wie zuvor */
html[data-desktop-shell="1"] #sidebar-scroll > .shell-patient-strip{
margin:6px 10px 14px;padding:10px 12px;border-radius:12px;border:1px solid #e4edf2;
background:linear-gradient(180deg,#f8fbfe,#f2f7fb);box-shadow:0 2px 10px rgba(28,62,94,.045);
}
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed .shell-patient-strip{display:none!important}
.dcp-preview.dcp-preview-click{cursor:pointer;transition:border-color .15s ease,background .15s ease}
.dcp-preview.dcp-preview-click:hover{border-color:#b8cce0;background:#eef5fb}
.ctb-btn{
background:#f2f8fc;color:#29577a;border:1px solid #d6e6f2;border-radius:9px;padding:6px 12px;font-size:.76rem;font-weight:600;
cursor:pointer;font-family:inherit;transition:background .14s,border-color .14s;
}
.ctb-btn:hover{background:#e8f2fa;border-color:#c2d9eb}
.ctb-btn.done{background:#dff3e8;color:#1f5c32;border-color:#b8dfc8}
.ctb-btn.done:hover{background:#cae9d8}
.ctb-btn.del{color:#943535;background:#fdeeed;border-color:#f0cbcb}
.ctb-btn.del:hover{background:#f9dede}
.ctb-btn-mini{background:#f8fafc;color:#466684;border:1px solid #d6e6f2;border-radius:8px;padding:5px 9px;font-size:.72rem;font-weight:600;cursor:pointer;font-family:inherit;line-height:1}
.ctb-btn-mini:hover{background:#eef6fb}
.ctb-btn-mini.del{color:#943535;background:#fdeeed;border-color:#f0cbcb}
.details-ctb{position:relative;flex-shrink:0}
.details-ctb > summary.ctb-sum{
list-style:none;cursor:pointer;background:#f8fafc;color:#466684;border:1px solid #d6e6f2;border-radius:8px;padding:5px 10px;font-size:.85rem;line-height:1;font-weight:600;
}
.details-ctb > summary.ctb-sum::-webkit-details-marker{display:none}
.details-ctb > summary.ctb-sum::before{content:'\2022\2022\2022';letter-spacing:1px}
.details-ctb[open] > summary.ctb-sum{background:#eef6fb;border-color:#b8cce0}
.ctb-dd-menu{
position:absolute;right:0;top:calc(100% + 4px);min-width:180px;background:var(--aza-panel);border:1px solid #dfe8ef;border-radius:10px;
box-shadow:var(--aza-shadow-md);padding:6px 0;z-index:80;
}
.ctb-dd-menu button{
display:block;width:100%;text-align:left;border:none;background:none;padding:9px 14px;font-size:.82rem;color:#294a62;cursor:pointer;font-family:inherit;
}
.ctb-dd-menu button:hover{background:#f2f9fc;color:#152c40}
/* Nachricht · dezente Aufgabe-Aktion */
.conv-msg-slot{display:flex;flex-direction:column;align-items:flex-end;gap:6px;margin-bottom:0;position:relative;width:fit-content}
.conv-msg-slot.align-left{align-items:flex-start;align-self:flex-start}
.conv-msg-slot.align-right{margin-left:auto}
.msg-act-hit{
position:absolute;top:8px;width:42px;height:52px;display:flex;align-items:flex-start;justify-content:center;opacity:0;transition:opacity .15s ease;pointer-events:none;
}
.conv-msg-slot.align-left .msg-act-hit{left:calc(100% + 8px);}
.conv-msg-slot.align-right .msg-act-hit{right:calc(100% + 8px);}
.conv-msg-slot:hover .msg-act-hit,.conv-msg-slot:focus-within .msg-act-hit,.conv-doc:hover .conv-doc-msg-acts,.conv-doc:focus-within .conv-doc-msg-acts{opacity:1;pointer-events:auto}
.conv-doc{position:relative}
.msg-act-kebab{font-family:inherit;color:#6a8499;background:transparent;border:1px solid transparent;border-radius:8px;width:34px;height:30px;line-height:1;font-size:.9rem;padding:0;cursor:pointer;color:#55708a;background:#f8fafc;border-color:#dfe8ef}
.msg-act-kebab:focus,.msg-act-kebab:hover{outline:none;border-color:#b8cce0;color:#294a62}
.task-quick-panel{position:fixed;inset:0;z-index:220;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(24,52,74,.38)}
.task-quick-panel.hidden{display:none!important}
.tqp-sheet{background:var(--aza-panel);border-radius:14px;box-shadow:0 14px 40px rgba(28,62,94,.18);padding:22px 24px;width:min(440px,calc(100vw - 32px));border:1px solid #dfe8ef;max-height:90vh;overflow-y:auto}
.tqp-sheet h4{margin:0 0 14px;color:#1a4d6d;font-size:1.02rem;font-weight:700}
.tqp-row{margin-bottom:12px}
.tqp-row label{display:block;font-size:.72rem;color:#55708a;font-weight:700;margin-bottom:5px;text-transform:uppercase;letter-spacing:.04em}
.tqp-sheet input[type=text],.tqp-sheet select{width:100%;border:1px solid #cfe0ec;border-radius:10px;padding:9px 12px;font:inherit;font-size:.88rem;color:#263545;background:#fafcfe}
.tqp-sheet textarea{width:100%;border:1px solid #cfe0ec;border-radius:10px;padding:10px 12px;font:inherit;font-size:.86rem;line-height:1.45;color:#263545;background:#fafcfe;resize:vertical;min-height:92px}
.tqp-meta{font-size:.78rem;color:#7a909f;line-height:1.35;margin:-4px 0 14px;background:#f4f9fc;padding:10px 12px;border-radius:10px;border:1px solid #e4eef5}
.tqp-actions{display:flex;gap:10px;margin-top:16px;flex-wrap:wrap;justify-content:flex-end}
.btn-tqp-primary{background:linear-gradient(180deg,var(--aza-blue),#4a86ad);color:#fff;border:none;border-radius:10px;padding:10px 20px;font-weight:700;font-size:.82rem;font-family:inherit;cursor:pointer}
.btn-tqp-primary:hover{filter:brightness(1.04)}
.btn-tqp-ghost{border:1px solid #cce0ea;background:#fafcfe;color:#466684;padding:10px 16px;font-size:.82rem;border-radius:10px;font-family:inherit;cursor:pointer;font-weight:600}
#conversation-scroll{
flex:1;overflow-y:auto;padding:22px clamp(18px,3vw,40px);min-height:0;
scrollbar-gutter:stable;background:#f3f8fc;
}
#conversation-scroll.conv-panel-compact{padding:14px clamp(14px,2.5vw,28px)}
#conversation-scroll::-webkit-scrollbar{width:10px}
#conversation-scroll::-webkit-scrollbar-thumb{background:#c5d9ea;border-radius:10px;border:2px solid #f0f6fb}
.main-pane-empty{min-height:50%;display:flex;align-items:center;justify-content:center;padding:48px 24px}
.main-pane-empty.main-pane-empty--compact{min-height:auto;padding:20px 16px 12px}
.main-pane-empty-card{
background:var(--aza-panel);border:1px solid #e4edf5;border-radius:18px;padding:36px 40px;text-align:center;max-width:420px;
box-shadow:var(--aza-shadow-md);
}
.main-pane-empty-card .mpe-title{display:block;font-size:1.1rem;font-weight:700;color:#1a4d6d;margin-bottom:10px}
.main-pane-empty-card p{font-size:.9rem;color:#6a8499;line-height:1.55}
/* Thread-Karten (Listenspalte) */
.tl-item{
background:var(--aza-panel);border-radius:14px;padding:12px 14px;margin-bottom:10px;cursor:pointer;
border:1px solid #e6eef6;border-left:4px solid var(--aza-blue);box-shadow:var(--aza-shadow-sm);
transition:border-color .15s,box-shadow .15s,transform .12s;
}
.tl-item:hover{box-shadow:var(--aza-shadow-md);border-color:#d0dfea;transform:translateY(-1px)}
.tl-item.tl-active{border-color:#8eb8d6;background:linear-gradient(135deg,#f5fafe,#ecf4fb);box-shadow:0 0 0 1px rgba(91,141,179,.28)}
.tl-item.done{border-left-color:#7eb89a;border-color:#dfece3;opacity:.88}
.tl-item.done:hover{opacity:1}
.tl-row1{display:flex;align-items:flex-start;gap:8px;margin-bottom:6px}
.tl-patient{font-weight:700;font-size:.86rem;color:#1a3348;flex:1;min-width:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;line-height:1.35}
.tl-time{font-size:.68rem;color:#8aa0b5;white-space:nowrap;font-weight:500}
.tl-row2{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.tl-preview{font-size:.74rem;color:#6a8499;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tl-replies{font-size:.68rem;color:var(--aza-blue-deep);white-space:nowrap;font-weight:600}
.tl-status{font-size:.65rem;padding:3px 9px;border-radius:999px;font-weight:700;white-space:nowrap}
.tl-status.offen{background:#fff8e6;color:#7a6120}
.tl-status.erledigt{background:#dff3e8;color:#1f6634}
.conv-empty{text-align:center;padding:40px 20px;color:#8a9aaa;font-size:.9rem;line-height:1.5}
/* Dokumentkopf Thread */
.conv-doc{background:var(--aza-panel);border-radius:16px;padding:18px 20px;margin:0 auto 18px;max-width:720px;border:1px solid #e4eef5;
box-shadow:var(--aza-shadow-md);border-left:4px solid var(--aza-blue);}
.conv-doc-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px solid #eef4f9}
.conv-doc-sender{font-weight:700;font-size:.92rem;color:#1a3548}
.conv-doc-time{font-size:.72rem;color:#8aa0b5}
.conv-field{margin-bottom:8px}
.conv-field-label{font-size:.7rem;font-weight:700;color:var(--aza-blue-deep);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px;cursor:pointer;user-select:none}
.conv-field-label .tri{font-size:.55rem;margin-right:4px;display:inline-block;transition:transform .15s}
.conv-field-label .tri.closed{transform:rotate(-90deg)}
.conv-field-value{white-space:pre-wrap;line-height:1.6;font-size:.9rem;color:#263545}
.conv-field-value.collapsed{display:none}
.conv-attach{margin-top:10px}
.conv-attach img{max-width:100%;max-height:280px;border-radius:12px;border:1px solid #e8eff5}
.conv-msg-stack{max-width:720px;margin:0 auto;display:flex;flex-direction:column;gap:14px;padding-bottom:8px}
.messenger-stream{max-width:880px}
.messenger-date-sep{clear:both;text-align:center;margin:18px 0 14px;-webkit-user-select:none;user-select:none}
.messenger-date-sep span{
display:inline-block;padding:5px 14px;border-radius:999px;background:rgba(255,255,255,.95);
border:1px solid #d8e6f4;font-size:.7rem;font-weight:700;color:#5a7590;
letter-spacing:.02em;box-shadow:0 1px 2px rgba(28,62,94,.06);
}
.conv-msg-slot.align-right{width:fit-content;margin-left:auto;max-width:min(96%,780px);}
.conv-msg-slot.align-left{width:fit-content;margin-right:auto;max-width:min(96%,780px);}
.conv-msg{
padding:14px 16px;margin-bottom:0;border-radius:16px;font-size:.9rem;position:relative;line-height:1.55;
max-width:85%;width:fit-content;border:1px solid transparent;
box-shadow:var(--aza-shadow-sm);
}
.conv-msg.from-other{
background:var(--aza-panel);border-color:#e6ecf3;margin-right:auto;align-self:flex-start;
border-bottom-left-radius:6px;border-top-left-radius:16px;border-top-right-radius:16px;border-bottom-right-radius:16px;
}
.conv-msg.from-me{
background:linear-gradient(165deg,#d9ebf8,#cae3f5);border-color:#aecfe4;margin-left:auto;align-self:flex-end;
border-bottom-right-radius:6px;border-top-right-radius:16px;border-bottom-left-radius:16px;border-top-left-radius:16px;
}
.conv-msg-meta{font-size:.68rem;color:#7590a8;margin-bottom:8px;display:flex;flex-wrap:wrap;gap:8px;line-height:1.35;align-items:baseline;justify-content:space-between;width:100%}
.conv-msg-meta .conv-msg-time{margin-left:auto;font-weight:600;color:#94aaba}
.conv-msg-meta span:last-child{font-weight:500;color:#94aaba}
.mseg-block{margin:8px 0 0}
.mseg-lbl{display:block;font-size:.62rem;text-transform:uppercase;letter-spacing:.04em;color:#5a7590;font-weight:700;margin-bottom:3px}
.mseg-txt{white-space:pre-wrap;line-height:1.55;font-size:.88rem}
.mseg-attach{margin-top:8px}
.messenger-msg-core{min-width:0}
.messenger-kom{margin-top:6px}
.conv-msg-sender{font-weight:700;color:#466684}
.conv-msg-text{white-space:pre-wrap}
.conv-msg{padding-right:12px;padding-bottom:32px}
.conv-msg-acts{
position:absolute;bottom:6px;right:10px;display:flex;flex-wrap:wrap;gap:3px;justify-content:flex-end;max-width:calc(100% - 14px);
opacity:0;transition:opacity .15s ease;z-index:2;pointer-events:none;
}
.conv-msg:focus-within .conv-msg-acts,.conv-msg:hover .conv-msg-acts{opacity:.92;pointer-events:auto}
.conv-msg-act-in,.conv-msg-act-letter{
padding:3px 7px;margin:0;border-radius:999px;font-size:.61rem;font-weight:600;letter-spacing:.02em;text-transform:none;
border:1px solid #dce8f2;background:rgba(255,255,255,.94);cursor:pointer;line-height:1.25;color:#5a7390;
font-family:inherit;box-shadow:0 1px 2px rgba(28,62,94,.06);
}
.conv-msg-act-letter{
border-color:#e2daf0;color:#5c4b7a;background:rgba(249,246,253,.94);
}
.conv-msg-act-in:hover,.conv-msg-act-in:focus{border-color:#98bfd9;color:#1e4d6f;outline:none;background:#fff}
.conv-msg-act-letter:hover,.conv-msg-act-letter:focus{border-color:#c4aed8;color:#3d2f55;outline:none;background:#fff}
.conv-msg-act-ok{
padding:3px 7px;margin:0;border-radius:999px;font-size:.61rem;font-weight:600;letter-spacing:.02em;
border:1px solid #c0dcc4;background:rgba(255,255,255,.94);cursor:pointer;line-height:1.25;color:#3d6b45;
font-family:inherit;box-shadow:0 1px 2px rgba(40,100,55,.07);
}
.conv-msg-act-ok.on{
border-color:#7eb89a;background:linear-gradient(180deg,#e8f6ec,#dff3e4);color:#235032;font-weight:700;
}
.conv-msg-act-ok:hover,.conv-msg-act-ok:focus{outline:none;border-color:#7eb89a;background:#fff;color:#1f5c32}
.conv-msg-act-tr{
padding:3px 7px;margin:0;border-radius:999px;font-size:.61rem;font-weight:600;letter-spacing:.02em;
border:1px solid #c9dde8;background:rgba(255,255,255,.94);cursor:pointer;line-height:1.25;color:#356488;
font-family:inherit;box-shadow:0 1px 2px rgba(28,62,94,.06);
}
.conv-msg-act-tr:hover:not(:disabled),.conv-msg-act-tr:focus:not(:disabled){border-color:#7aa3c2;color:#1a4d6d;outline:none;background:#fff}
.conv-msg-act-tr:disabled{opacity:.55;cursor:not-allowed}
background:linear-gradient(180deg,#f4fbf6,#e8f4ec)!important;border-color:#b8dcc0!important;
}
.conv-msg.conv-msg-chat-ack.from-me{
background:linear-gradient(165deg,#dff3e6,#d0ecdb)!important;border-color:#9cc9a8!important;
}
.conv-msg-ack-badge{
position:absolute;top:8px;right:10px;width:22px;height:22px;border-radius:50%;
background:linear-gradient(180deg,#5aab6e,#3d8f54);color:#fff;font-size:.68rem;font-weight:800;
display:flex;align-items:center;justify-content:center;box-shadow:0 1px 4px rgba(40,120,60,.22);
line-height:1;z-index:4;pointer-events:none;
}
.conv-doc-chat-ack{
background:linear-gradient(180deg,#f4fbf6,#e8f4ec)!important;border-color:#b8dcc0!important;
border-left-color:#5aab6e!important;
}
.conv-doc-ack-badge{
position:absolute;top:12px;right:14px;width:24px;height:24px;border-radius:50%;
background:linear-gradient(180deg,#5aab6e,#3d8f54);color:#fff;font-size:.72rem;font-weight:800;
display:flex;align-items:center;justify-content:center;box-shadow:0 1px 4px rgba(40,120,60,.22);
z-index:4;pointer-events:none;
}
.conv-doc-msg-acts{
position:absolute;top:10px;right:10px;display:flex;flex-wrap:wrap;gap:3px;justify-content:flex-end;max-width:calc(100% - 20px);
opacity:0;transition:opacity .15s ease;z-index:3;pointer-events:none;
}
.conv-doc:hover .conv-doc-msg-acts,.conv-doc:focus-within .conv-doc-msg-acts{opacity:1;pointer-events:auto}
.conv-doc-msg-act,.conv-doc-msg-act-letter{
padding:4px 8px;border-radius:999px;font-size:.61rem;font-weight:700;
font-family:inherit;
border:1px solid #dce8f2;background:rgba(252,253,254,.96);cursor:pointer;color:#5a7390;line-height:1.2;
box-shadow:0 1px 3px rgba(28,62,94,.07);
}
.conv-doc-msg-act-letter{
border-color:#e2daf0;color:#5c4b7a;background:rgba(253,251,255,.96);
}
.conv-doc-msg-act:hover,.conv-doc-msg-act:focus{border-color:#98bfd9;color:#1e4d6f;outline:none;background:#fff}
.conv-doc-msg-act-letter:hover,.conv-doc-msg-act-letter:focus{border-color:#c4aed8;color:#3d2f55;outline:none;background:#fff}
.conv-doc-msg-act-ok{
padding:4px 8px;border-radius:999px;font-size:.61rem;font-weight:700;font-family:inherit;
border:1px solid #c0dcc4;background:rgba(252,253,254,.96);cursor:pointer;color:#3d6b45;line-height:1.2;
box-shadow:0 1px 3px rgba(40,100,55,.07);
}
.conv-doc-msg-act-ok.on{
border-color:#7eb89a;background:linear-gradient(180deg,#e8f6ec,#dff3e4);color:#235032;
}
.conv-doc-msg-act-ok:hover,.conv-doc-msg-act-ok:focus{outline:none;border-color:#7eb89a;background:#fff}
/* Untere Eingabezeile */
#chat-input-wrap{
flex-shrink:0;background:var(--aza-panel);border-top:1px solid #dfe8ef;padding:14px clamp(14px,2.5vw,28px) 18px;
box-shadow:0 -4px 18px rgba(45,94,132,.06);
}
/* Leerer / neuer Chat: Composer optisch höher (Assistenz-/Messenger-Flow) */
#chat-pane.chat-composer-centered #chat-input-wrap{
flex:1;display:flex;flex-direction:column;justify-content:center;
background:transparent;border-top:none;box-shadow:none;
padding-top:10px;padding-bottom:clamp(32px,9vh,100px);
}
#chat-pane.chat-composer-centered #chat-input-bar{max-width:720px;margin:0 auto;width:100%}
#chat-pane.chat-composer-centered #conversation-scroll{
flex:0 1 auto;
max-height:min(38vh,320px);
min-height:min(120px,18vh);
}
#chat-input-wrap.no-thread-input{display:none}
#chat-input-bar{
display:none;margin:0 auto;max-width:720px;width:100%;
gap:12px;align-items:stretch;flex-wrap:nowrap;
}
#chat-input-bar.visible{display:flex}
#chat-input-bar .input-grow{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px}
#chat-input-bar textarea{
width:100%;border:1px solid #d0dee9;border-radius:16px;padding:14px 16px;font-family:inherit;font-size:.92rem;line-height:1.45;
resize:none;min-height:86px;max-height:200px;outline:none;transition:border-color .15s,box-shadow .15s;background:#fafcfe;
}
#chat-input-bar textarea:focus{border-color:#8eb8d6;box-shadow:0 0 0 3px rgba(91,141,179,.14)}
.input-actions-col{display:flex;flex-direction:column;align-items:stretch;gap:8px;flex-shrink:0;justify-content:flex-end}
.input-actions-row{display:flex;flex-direction:row;gap:7px;justify-content:center;align-items:center}
.btn-send{background:linear-gradient(180deg,var(--aza-blue),#4a86ad);color:#fff;border:none;border-radius:13px;padding:16px 22px;font-weight:700;font-size:.86rem;font-family:inherit;cursor:pointer;box-shadow:0 4px 12px rgba(61,118,157,.35);transition:transform .12s,box-shadow .12s;min-height:52px;}
.btn-send:hover{transform:translateY(-1px);box-shadow:0 6px 16px rgba(61,118,157,.42)}
.btn-dictate{
background:#eef5fb;color:#356488;border:1px solid #c8dae8;border-radius:13px;padding:11px 12px;cursor:pointer;font-size:1.2rem;line-height:1;
font-family:inherit;min-width:48px;min-height:46px;display:flex;align-items:center;justify-content:center;}
.btn-dictate:hover{background:#e4eef6;border-color:#b8cce0}
.btn-dictate.recording{background:#fde8e8;border-color:#eab8b8;color:#8b2a2a;animation:pulse 1.5s infinite}
.btn-audio-rec{
background:#f4f6f8;color:#475d6e;border:1px solid #c5d2dc;border-radius:12px;padding:8px 9px;cursor:pointer;font-size:.92rem;line-height:1;
font-family:inherit;min-width:40px;min-height:40px;display:flex;align-items:center;justify-content:center;}
.btn-audio-rec:hover{background:#e8edf2;border-color:#b0c0cc}
.btn-audio-rec.recording{background:#fdeeed;border-color:#d6a8a8;color:#7a2a2a;animation:pulse 1.5s infinite}
.aza-audio-msg{
display:flex;align-items:center;gap:10px;margin-top:8px;padding:10px 12px;background:rgba(255,255,255,.72);
border:1px solid #d4e3ee;border-radius:12px;max-width:100%;cursor:pointer;box-sizing:border-box;
}
.aza-audio-msg:focus{outline:2px solid #8eb8d6;outline-offset:1px}
.aza-audio-play{
flex-shrink:0;width:36px;height:36px;border-radius:50%;border:1px solid #9ebdd4;background:#eef5fb;color:#1a4d6d;
font-size:.85rem;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;font-family:inherit;
}
.aza-audio-play:hover{background:#e4eef6;border-color:#7aa3c2}
.aza-audio-label{font-size:.84rem;color:#405a6e;font-weight:600}
.aza-audio-dur{font-size:.72rem;color:#7a909f;font-variant-numeric:tabular-nums}
.aza-audio-transcript{
margin-top:10px;padding:10px 12px;background:rgba(255,255,255,.55);border:1px solid #dbe8f0;border-radius:10px;
font-size:.82rem;line-height:1.45;color:#2f4a5c;white-space:pre-wrap;max-width:100%;box-sizing:border-box;
}
.aza-audio-tr-status{margin-top:8px;font-size:.75rem;color:#5a7390;min-height:1.2em}
.aza-audio-tr-status.aza-tr-err{color:#9a3540}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.65}}
@media (max-width:1100px){
#sidebar{min-width:240px;width:280px}
.conv-msg,.conv-doc{max-width:92%!important;width:auto!important}
}
@media (max-width:900px){
#nav-rail{width:60px;min-width:60px;padding:10px 0}
.nr-btn,.nr-brand{width:40px;height:40px;font-size:1rem}
}
/* === Tasks Panel (right) === */
#tasks-panel{width:268px;min-width:210px;background:var(--aza-panel);border-left:1px solid #dfe8ef;display:flex;flex-direction:column;flex-shrink:0;overflow:hidden;transition:width .2s ease,min-width .2s ease,opacity .15s ease}
#tasks-panel.collapsed{width:0!important;min-width:0!important;opacity:0;border:none;padding:0;margin:0;overflow:hidden;pointer-events:none}
/* Desktop-Hülle: Aufgaben einklappbar mit schmaler wiederaufklappbarer Rippe */
html[data-desktop-shell="1"] #tasks-panel.collapsed{
width:40px!important;min-width:40px!important;opacity:1!important;padding:0!important;margin:0!important;
overflow:visible!important;pointer-events:auto!important;border-left:1px solid #dfe8ef!important;background:linear-gradient(180deg,#f7fafc,#f0f6fa);
align-items:center;justify-content:flex-start;
}
html[data-desktop-shell="1"] #tasks-panel.collapsed > :not(.tasks-ds-expand-tab){display:none!important}
.tasks-ds-expand-tab{
display:none;width:100%;min-height:120px;margin:0;padding:10px 0;border:none;background:transparent;color:#466684;font-size:.72rem;font-weight:700;font-family:inherit;cursor:pointer;
flex-direction:column;align-items:center;gap:6px;line-height:1.2;text-align:center;
}
.tasks-ds-expand-tab .tasks-ds-ico{font-size:1.1rem;line-height:1;color:#5B8DB3}
.tasks-ds-expand-tab .tasks-ds-lbl{writing-mode:vertical-rl;transform:rotate(180deg);letter-spacing:.04em;text-transform:uppercase;font-size:.65rem;color:#55708a}
html[data-desktop-shell="1"] #tasks-panel.collapsed .tasks-ds-expand-tab{display:flex!important}
.tp-ds-collapse-btn{display:none;background:none;border:none;color:#7a909f;font-size:1rem;cursor:pointer;padding:4px 6px;line-height:1;border-radius:8px;font-family:inherit}
.tp-ds-collapse-btn:hover{color:#294a62;background:rgba(91,141,179,.08)}
html[data-desktop-shell="1"] #tasks-panel:not(.collapsed) .tp-ds-collapse-btn{display:inline-flex;align-items:center;justify-content:center}
/* Desktop-Hülle: linke Spalte nur per Dreieck in Nav-Leiste; eingeklappt ohne schmale Rest-Leiste */
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed{
width:0!important;min-width:0!important;max-width:0!important;
border:none!important;padding:0!important;margin:0!important;
overflow:hidden!important;pointer-events:none!important;
}
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed #sidebar-scroll{display:none!important}
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed .sb-me-name,
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed #sb-me-role,
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed .sb-me-change{display:none!important}
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed .sb-me{justify-content:center;padding:10px 4px}
/* Header-Badge Desktop-Modus */
.desktop-mode-pill{display:none;align-self:flex-start;margin-top:4px;font-size:.65rem;font-weight:700;color:#466684;background:linear-gradient(180deg,#eef6fb,#e4eef6);border:1px solid #c8dae8;padding:4px 10px;border-radius:999px;text-transform:none;letter-spacing:.02em}
html[data-desktop-shell="1"] .desktop-mode-pill{display:inline-flex}
/* Kontext / Textbausteine (Desktop-Hülle): nur links, unterhalb der Listen im Scrollbereich — nicht im Chat */
.desktop-context-panel{display:none;margin:0;padding:0;flex-shrink:0}
html[data-desktop-shell="1"] #chat-pane .desktop-context-panel,
html[data-desktop-shell="1"] #middle-stack .desktop-context-panel,
html[data-desktop-shell="1"] #conversation-scroll .desktop-context-panel{display:none!important}
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed .desktop-context-panel{display:none!important}
html[data-desktop-shell="1"] #sidebar.shell-dcp-off .desktop-context-panel{display:none!important}
html[data-desktop-shell="1"] #sidebar-scroll .desktop-context-panel{display:block}
html[data-desktop-shell="1"] #sidebar-scroll > .desktop-context-panel{
margin:0;padding:10px 10px 16px;border-top:1px solid #e4edf2;
background:linear-gradient(180deg,rgba(244,249,253,.92),rgba(246,250,253,.96));
}
.dcp-card{border-radius:14px;background:linear-gradient(180deg,#fafcfe,#fff);border:1px solid #e4eaf1;box-shadow:0 4px 18px rgba(28,62,94,.06);overflow:hidden}
.dcp-head{display:flex;align-items:center;justify-content:space-between;width:100%;gap:12px;padding:12px 14px;border:none;background:transparent;font:inherit;cursor:pointer;text-align:left}
.dcp-head:hover{background:rgba(91,141,179,.06)}
.dcp-head h3{margin:0;font-size:.82rem;color:#1a4d6d;font-weight:700}
.dcp-chevron{font-size:.75rem;color:#7a909f;line-height:1;transition:transform .2s ease;display:inline-block}
.dcp-panel-collapsed .dcp-chevron{transform:rotate(-90deg)}
.dcp-panel-collapsed .dcp-body{display:none!important}
.dcp-hint{font-size:.75rem;line-height:1.5;color:#6a8499;margin:0 0 12px}
.dcp-mini-grid{display:flex;flex-direction:column;gap:10px}
.dcp-mini{
padding:11px 12px;border-radius:12px;border:1px solid #e8eef4;background:#fff;
}
.dcp-mini h4{margin:0 0 6px;font-size:.68rem;font-weight:700;color:#294a62;text-transform:uppercase;letter-spacing:.03em}
.dcp-mini p{margin:0;font-size:.72rem;line-height:1.45;color:#72899e}
.dcp-preview{
max-height:112px;overflow-y:auto;font-size:.76rem;line-height:1.42;color:#334a5f;
white-space:pre-wrap;word-break:break-word;padding:8px 10px;background:#f6f9fc;border-radius:10px;border:1px solid #e4ecf3;
margin:0 0 8px;min-height:2.4em;
}
.dcp-mini .dcp-actions{margin-top:8px}
.dcp-foot{margin-top:4px;font-size:.68rem;color:#7a909f;min-height:1.15em}
.dcp-mini label.dcp-cb{display:inline-flex;align-items:center;gap:6px;cursor:pointer;font-weight:600;font-size:.72rem;color:#466684;margin:4px 0 2px}
.dcp-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
.dcp-act{
border:1px solid #cfe0ec;background:#f6fafd;color:#466684;font-size:.72rem;font-weight:700;padding:8px 12px;border-radius:10px;
cursor:pointer;font-family:inherit;
}
.dcp-act:hover{border-color:#9ebdd4;color:#1a4d6d;background:#eef6fb}
.dcp-act:disabled{opacity:.45;cursor:not-allowed}
.tp-header{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:#fafcfe;border-bottom:1px solid #eef2f6;font-weight:600;color:#1a4d6d;font-size:.88rem}
.tp-header-main{display:flex;flex-direction:column;gap:2px;min-width:0}
.tp-header-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}
.tp-show-done-filter{
display:flex;align-items:center;gap:7px;margin:0;padding:6px 12px 8px;background:#fafcfe;border-bottom:1px solid #eef2f6;
font-size:.68rem;font-weight:600;color:#6a8499;cursor:pointer;user-select:none;font-family:inherit;
}
.tp-show-done-filter input[type=checkbox]{width:13px;height:13px;margin:0;accent-color:#5B8DB3;cursor:pointer;flex-shrink:0;border-radius:3px}
.tp-show-done-filter .tp-done-lbl{padding-top:.5px;line-height:1.35;font-weight:600;color:#73899f}
.tp-todo-sep{
height:1px;margin:14px 10px 10px;background:linear-gradient(90deg,transparent,#d8e6f2 14%,#d8e6f2 86%,transparent);
opacity:.85;pointer-events:none;
}
.tp-input-row{display:none;padding:6px 8px;border-bottom:1px solid #eef2f6}
.tp-input-row.open{display:block}
.tp-input-row input{width:100%;border:1px solid #ccc;border-radius:4px;padding:4px 8px;font-size:.82rem;font-family:inherit}
#todo-list{flex:1;overflow-y:auto;padding:10px}
.todo-card{
margin-bottom:10px;border-radius:14px;background:#f9fbfd;border:1px solid #e4eaf1;padding:12px 12px;
transition:opacity .18s ease,background .14s ease,transform .14s ease;animation:fadeInCard .35s ease;
}
.todo-card.todo-done{border-color:#d6eadc;background:#f3faf5;opacity:.85}
@keyframes fadeInCard{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
@keyframes taskFlashHl{from{box-shadow:0 0 0 2px rgba(91,141,179,.85)}to{box-shadow:none}}
.todo-card.todo-new-highlight{animation:taskFlashHl .6s ease 2;border-color:#8eb8d6}
.todo-card.todo-letter-card{border-left:3px solid #a68fd4;background:#faf8fc;border-color:#e8e4f0}
.todo-card.todo-done.todo-soft-done:not(.todo-new-highlight){
border-color:#dae6ef;background:#eef5f9;opacity:.92;
}
.todo-card.todo-done.todo-soft-done.todo-letter-card:not(.todo-new-highlight){
background:#f3eff9;border-color:#e4dcf0;
}
.todo-soft-done:not(.todo-new-highlight) .tc-title{color:#3a5570}
.todo-soft-done:not(.todo-new-highlight) .tc-meta{color:#8799a9}
.todo-soft-done:not(.todo-new-highlight) .tc-body{color:#5f738a}
.tc-kind-letter{
display:inline-block;font-size:.61rem;font-weight:800;color:#5c4b7a;background:#ebe6f6;border-radius:6px;padding:2px 7px;margin:0 0 5px;
letter-spacing:.03em;
}
.tc-title{font-weight:700;font-size:.86rem;line-height:1.35;color:#1a3548;display:block;margin-bottom:3px;padding:2px 0;outline:none;word-break:break-word}
.tc-body{font-size:.78rem;line-height:1.45;color:#40566a;outline:none;padding:4px 0;min-height:1.35em;border-top:1px solid #ecf2f6;margin-top:4px;padding-top:8px;word-break:break-word}
.tc-body:empty{display:none}
.tc-done-row{display:flex;align-items:flex-start;gap:10px}
.tc-done-row label{font-size:.8rem;line-height:1.35;color:#355c48;cursor:pointer;font-weight:600}
.tc-done-row input[type=checkbox]{margin-top:3px;width:18px;height:18px;accent-color:#46915d;cursor:pointer;flex-shrink:0}
.tc-row-actions{display:flex;gap:10px;margin-top:10px;padding-top:8px;border-top:1px solid #ecf2f6;justify-content:flex-end;flex-wrap:wrap}
.tc-btn-mini{background:none;border:none;font-size:.72rem;font-weight:700;color:#5B8DB3;cursor:pointer;padding:4px;font-family:inherit}
.tc-btn-mini:hover{text-decoration:underline}
.tc-del{color:#aab8c8!important;font-weight:600!important}
.tc-del:hover{color:#c94a4a!important}
.tp-sub{font-weight:600;font-size:.92rem;display:block;line-height:1.3}
.tp-seg{display:flex;gap:4px;padding:8px 10px;background:#fafcfe;border-bottom:1px solid #eef2f6;flex-shrink:0;flex-wrap:wrap}
.tp-seg-btn{
flex:1;min-width:0;border:1px solid #dbe6ef;background:#fff;color:#466684;font-size:.7rem;font-weight:700;font-family:inherit;
border-radius:999px;padding:7px 8px;cursor:pointer;transition:background .14s,border-color .14s,color .14s;
}
.tp-seg-btn:hover{border-color:#b8cce0;color:#294a62}
.tp-seg-btn.tp-on{background:linear-gradient(180deg,#eef6fb,#e4eef6);border-color:#9ebdd4;color:#1a3a5a;box-shadow:0 0 0 1px rgba(91,141,179,.12)}
.tp-seg-btn.hidden{display:none!important}
.todo-empty-msg{text-align:center;padding:28px 16px;line-height:1.45;color:#7a909f;font-size:.8rem;background:#fafcfe;border-radius:12px;margin:12px;border:1px dashed #dbe6ef}
.settings-user-mgmt{margin-top:14px;padding-top:14px;border-top:1px solid #eee}
.settings-user-mgmt h3{margin-bottom:8px}
.settings-user-mgmt p{font-size:.78rem;color:#6a8499;margin-bottom:8px;line-height:1.45}
.add-user-overlay{position:fixed;inset:0;z-index:230;background:rgba(24,52,74,.45);display:flex;align-items:center;justify-content:center;padding:20px}
.add-user-overlay.hidden{display:none!important}
.add-user-box{background:var(--aza-panel);border-radius:14px;padding:22px 24px;width:min(400px,100%);border:1px solid #dfe8ef;box-shadow:0 14px 40px rgba(28,62,94,.2)}
.add-user-box h3{font-size:1.02rem;color:#1a4d6d;margin-bottom:10px}
.add-user-box .row{margin-bottom:12px}
.add-user-box label{display:block;font-size:.72rem;font-weight:700;color:#55708a;margin-bottom:4px}
.add-user-box input,.add-user-box select{width:100%;border:1px solid #cfe0ec;border-radius:10px;padding:9px 11px;font:inherit;font-size:.86rem}
.add-user-btns{display:flex;gap:10px;justify-content:flex-end;margin-top:16px;flex-wrap:wrap}
.add-user-btns button{padding:9px 16px;border-radius:10px;font:inherit;font-size:.82rem;cursor:pointer;font-weight:600}
.add-user-btns .adu-cancel{background:#f4f6f8;border:1px solid #dde2e8;color:#3a4a5a}
.add-user-btns .adu-go{background:linear-gradient(180deg,var(--aza-blue),#4a86ad);color:#fff;border:none}
.tc-meta{font-size:.69rem;line-height:1.4;color:#72899e;margin-bottom:6px}
.tc-body:not(:focus){
display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;
}
.tc-actions-bar{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:6px;padding-top:8px;border-top:1px solid #ecf2f6;flex-wrap:wrap}
.tc-btn-done{
background:linear-gradient(180deg,#dff3e8,#cae9d8);border:1px solid #a8d4b8;color:#14532a;font-weight:700;font-size:.74rem;padding:8px 14px;border-radius:10px;cursor:pointer;font-family:inherit;
}
.tc-btn-done:hover{filter:brightness(1.03)}
.tc-done-lbl{font-size:.72rem;font-weight:700;color:#1f6634;background:#eaf6ee;padding:6px 10px;border-radius:8px}
.tc-more{position:relative}
.tc-more > summary{
list-style:none;cursor:pointer;font-size:1.1rem;line-height:1;color:#94aaba;padding:4px 8px;border-radius:8px;font-weight:700;
}
.tc-more > summary::-webkit-details-marker{display:none}
.tc-more[open] > summary{background:#f0f6fb;color:#466684}
.tc-more-menu{
position:absolute;right:0;bottom:100%;margin-bottom:4px;min-width:160px;background:var(--aza-panel);border:1px solid #dfe8ef;border-radius:10px;
box-shadow:var(--aza-shadow-md);padding:6px 0;z-index:40;
}
.tc-more-menu button{
display:block;width:100%;text-align:left;border:none;background:none;padding:8px 12px;font-size:.74rem;color:#294a62;cursor:pointer;font-family:inherit;
}
.tc-more-menu button:hover{background:#f2f9fc}
.tc-more-menu button.danger{color:#a34a4a}
.todo-item{display:flex;align-items:flex-start;gap:4px;padding:4px 6px;border-radius:5px;margin-bottom:2px;background:#f8fafe;font-size:.8rem}
.todo-item .todo-assignee{font-size:.68rem;color:#8a9aaa;margin-top:1px}
/* === Status Bar === */
.status-bar{padding:5px 16px;font-size:.72rem;color:#8a9aaa;background:#fff;border-top:1px solid #eef2f6;display:flex;justify-content:center;gap:12px;flex-shrink:0}
/* === Utility === */
.copied-flash{animation:copiedFl .6s ease}
@keyframes copiedFl{0%{background:#d4edda}100%{background:transparent}}
/* === Login Overlay === */
#login-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:#f0f4f8;z-index:200;display:flex;align-items:center;justify-content:center}
#login-overlay.hidden{display:none}
.login-practice-context{font-size:.82rem;line-height:1.45;color:#3a5570;text-align:center;background:#e4eef7;border:1px solid #cddbeb;border-radius:10px;padding:8px 12px;margin:0 auto 14px;max-width:92%}
.login-box{background:#fff;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.12);padding:36px;width:380px;max-width:90vw}
.login-box h2{font-size:1.3rem;color:#1a4d6d;margin-bottom:6px}
.login-box p{font-size:.85rem;color:#6a8a9a;margin-bottom:20px}
.login-field{margin-bottom:14px}
.login-field label{display:block;font-size:.82rem;color:#3a5a7a;margin-bottom:4px;font-weight:600}
.login-field input,.login-field select{width:100%;border:1px solid #d0dce8;border-radius:6px;padding:8px 12px;font-size:.9rem;font-family:inherit;outline:none;transition:border-color .15s}
.login-field input:focus,.login-field select:focus{border-color:#5B8DB3}
.login-btn{width:100%;background:#5B8DB3;color:#fff;border:none;border-radius:8px;padding:10px;font-size:.95rem;font-weight:600;cursor:pointer;font-family:inherit;transition:background .15s}
.login-btn:hover{background:#4A7A9E}
.login-error{color:#dc3545;font-size:.82rem;margin-top:8px;min-height:1.2em}
.login-switch{text-align:center;margin-top:14px;font-size:.82rem;color:#6a8a9a}
/* Neuer Chat (Popup) */
.new-chat-overlay{position:fixed;inset:0;background:rgba(26,42,58,.45);z-index:200;display:flex;align-items:center;justify-content:center;padding:16px}
.new-chat-overlay.hidden{display:none!important}
.new-chat-box{background:#fff;border-radius:12px;padding:22px 24px;max-width:400px;width:100%;box-shadow:0 8px 32px rgba(0,0,0,.15)}
.new-chat-box h3{font-size:1.1rem;color:#1a4d6d;margin-bottom:8px}
.new-chat-box p{font-size:.85rem;color:#6a8a9a;margin-bottom:14px;line-height:1.45}
.nc-opt{display:block;margin:8px 0;font-size:.88rem;cursor:pointer}
.nc-opt input{margin-right:8px}
#nc-peer-select{width:100%;margin-top:10px;padding:8px;border:1px solid #d0dce8;border-radius:6px;font-size:.88rem;font-family:inherit}
.nc-btns{display:flex;gap:10px;justify-content:flex-end;margin-top:18px}
.nc-btns button{padding:8px 16px;border-radius:8px;font-size:.85rem;cursor:pointer;font-family:inherit}
.nc-btns .nc-cancel{background:#eef2f6;color:#3a5a7a;border:1px solid #d0dce8}
.nc-btns .nc-go{background:#5B8DB3;color:#fff;border:none;font-weight:600}
#reply-att-hint{font-size:.72rem;color:#5B8DB3;min-height:1em;margin:4px 0 0 2px}
.login-switch a{color:#5B8DB3;cursor:pointer;text-decoration:underline}
/* Registrierung: Überschrift, Fließtext, Labels, Felder, Button, Link alles 9pt */
.login-box.login-register,
.login-box.login-register h2,
.login-box.login-register p,
.login-box.login-register label,
.login-box.login-register input,
.login-box.login-register select,
.login-box.login-register button,
.login-box.login-register .login-error,
.login-box.login-register .login-switch,
.login-box.login-register .login-switch a{font-size:9pt!important}
.login-box.login-register h2{font-weight:600}
/* === Admin Panel === */
.admin-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.3);z-index:150;align-items:flex-start;justify-content:center;padding-top:40px;overflow-y:auto}
.admin-overlay.open{display:flex}
.admin-panel{background:#fff;border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,.15);width:720px;max-width:94vw;max-height:calc(100vh - 80px);overflow-y:auto;padding:0}
.admin-header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid #e0e4e8;position:sticky;top:0;background:#fff;border-radius:12px 12px 0 0;z-index:5}
.admin-header h2{font-size:1.15rem;color:#1a4d6d;font-weight:600}
.admin-close{background:none;border:none;font-size:1.3rem;color:#8a9aaa;cursor:pointer;padding:4px 8px;border-radius:4px}
.admin-close:hover{background:#f0f4f8;color:#1a4d6d}
.admin-tabs{display:flex;gap:0;border-bottom:1px solid #e0e4e8;padding:0 24px;background:#fafcfe}
.admin-tab{padding:10px 18px;font-size:.85rem;color:#6a8a9a;cursor:pointer;border-bottom:2px solid transparent;font-weight:500;transition:all .15s;font-family:inherit;background:none;border-top:none;border-left:none;border-right:none}
.admin-tab:hover{color:#1a4d6d;background:#f0f4f8}
.admin-tab.active{color:#5B8DB3;border-bottom-color:#5B8DB3;font-weight:600}
.admin-body{padding:20px 24px}
.admin-table{width:100%;border-collapse:collapse;font-size:.82rem}
.admin-table th{text-align:left;padding:8px 10px;color:#5B8DB3;font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.4px;border-bottom:2px solid #e0e4e8}
.admin-table td{padding:8px 10px;border-bottom:1px solid #f0f4f8;vertical-align:middle}
.admin-table tr:hover td{background:#fafcfe}
.admin-badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:.72rem;font-weight:600}
.admin-badge.admin{background:#e8d5f5;color:#6b3fa0}
.admin-badge.arzt{background:#d4edda;color:#155724}
.admin-badge.mpa{background:#d6eaf8;color:#1a5276}
.admin-badge.empfang{background:#fff3cd;color:#856404}
.admin-badge.active{background:#d4edda;color:#155724}
.admin-badge.deactivated{background:#f8d7da;color:#721c24}
.admin-badge.trusted{background:#d4edda;color:#155724}
.admin-badge.blocked{background:#f8d7da;color:#721c24}
.admin-badge.pending_conn{background:#fff3cd;color:#856404}
.admin-badge.active_conn{background:#d4edda;color:#155724}
.admin-badge.revoked{background:#e2e3e5;color:#6c757d}
.admin-btn{background:#e8f0f8;color:#2a5a8a;border:1px solid #d0dce8;border-radius:4px;padding:3px 10px;font-size:.75rem;cursor:pointer;font-family:inherit;transition:all .12s;white-space:nowrap}
.admin-btn:hover{background:#d4e4f0}
.admin-btn.danger{color:#8a2a2a;background:#f8e8e8;border-color:#e8c8c8}
.admin-btn.danger:hover{background:#f0d4d4}
.admin-btn.success{color:#155724;background:#d4edda;border-color:#c3e6cb}
.admin-btn.success:hover{background:#c3e6cb}
.admin-section-title{font-size:.88rem;font-weight:600;color:#1a4d6d;margin:16px 0 8px}
.admin-info-row{display:flex;align-items:center;gap:8px;margin-bottom:6px;font-size:.82rem;flex-wrap:wrap}
.admin-info-label{color:#6a8a9a;min-width:120px}
.admin-info-value{color:#1a3a5a;font-weight:500}
.admin-mono{font-family:monospace;background:#f0f4f8;padding:2px 8px;border-radius:4px;user-select:all}
.admin-empty{text-align:center;padding:24px;color:#8a9aaa;font-size:.85rem}
.admin-add-row{display:flex;gap:6px;margin-top:12px;flex-wrap:wrap;align-items:center}
.admin-add-row input,.admin-add-row select{border:1px solid #d0dce8;border-radius:4px;padding:4px 8px;font-size:.82rem;font-family:inherit}
.admin-add-row input{flex:1;min-width:120px}
@media print{
header,.settings-panel,#nav-rail,#sidebar,#tasks-panel,.status-bar,#chat-input-wrap,#chat-input-bar,#chat-top-bar{display:none!important}
#app-layout{display:block!important}
#chat-area{display:block!important}
.tl-item{break-inside:avoid;box-shadow:none}
.conv-doc,.conv-msg{break-inside:avoid;box-shadow:none}
}
</style>
</head>
<body>
<div id="login-overlay">
<div class="login-box" id="login-box">
<p style="text-align:center;color:#6a8a9a;font-size:.9rem;margin:0;line-height:1.45">Oberfl&auml;che wird geladen&hellip;</p>
</div>
</div>
<header>
<div class="header-title-block">
<h1>AzA-Empfang</h1>
<span class="desktop-mode-pill" aria-label="Desktop-H&uuml;lle">Desktop-H&uuml;lle</span>
<div id="header-practice-sub" aria-live="polite"></div>
</div>
<div class="header-right">
<div class="font-ctrl" id="font-ctrl-wrap" title="Schriftgr&ouml;sse der ganzen Seite (100% = Standard). Mit Pfeilen vergr&ouml;ssern oder verkleinern.">
<span>Aa</span>
<span id="font-size-display">100%</span>
<button type="button" onclick="changeFontSize(1)" aria-label="Schrift gr&ouml;sser">&#9650;</button>
<button type="button" onclick="changeFontSize(-1)" aria-label="Schrift kleiner">&#9660;</button>
</div>
<button class="hbtn" onclick="openNewChatDialog()" title="Neuen Chat in eigenem Fenster">+ Neuer Chat</button>
<button class="hbtn" id="sound-btn" onclick="toggleSound()" title="Ton an/aus">&#128276; Ton</button>
<div class="vol-ctrl" title="Lautst&auml;rke">
<input type="range" id="vol-slider" min="0" max="300" step="5" oninput="setVolume(this.value)">
<span id="vol-display">100%</span>
</div>
<button type="button" class="hbtn" id="shell-pin-btn" onclick="toggleShellPin()" style="display:none" title="Immer im Vordergrund" aria-pressed="false">&#128204;</button>
<button class="hbtn" id="sort-btn" onclick="toggleSort()" title="Sortierung">&#8645;</button>
<span class="badge" id="count-badge">&ndash;</span>
<button class="hbtn" onclick="toggleSettings()" title="Einstellungen">&#9881;</button>
</div>
</header>
<div class="settings-panel" id="settings-panel">
<div class="settings-inner">
<h3>Ton-Einstellungen</h3>
<div class="setting-row">
<label for="tone-select">Benachrichtigungston:</label>
<select id="tone-select" onchange="saveSoundSettings()"></select>
<button class="btn-test-small" onclick="testCurrentSound()">Testen</button>
</div>
<div class="setting-row">
<label for="repeat-select">Ton-Modus:</label>
<select id="repeat-select" onchange="onRepeatChange()">
<option value="30000">Wiederholend (Standard, alle 30 Sek.)</option>
<option value="5000">Kontinuierlich (alle 5 Sek.)</option>
<option value="2000">Kontinuierlich (alle 2 Sek.)</option>
<option value="0">Einmalig</option>
<option value="custom">Eigenes Intervall</option>
</select>
<span id="custom-sec-wrap" style="display:none">
<input type="number" id="custom-sec" min="1" max="600" value="10" style="width:55px" onchange="onRepeatChange()"> Sek.
</span>
</div>
<h3 style="margin-top:12px">Praxis / Chat-Identit&auml;t</h3>
<div id="practice-info-section" style="margin-bottom:6px"></div>
<h3 style="margin-top:12px">Aufbewahrung</h3>
<div class="setting-row">
<label for="cleanup-days">Nachrichten &auml;lter als</label>
<input type="number" id="cleanup-days" min="1" max="365" value="30" style="width:55px"> Tage
<button class="btn-cleanup" onclick="doCleanup()">Jetzt bereinigen</button>
<span id="cleanup-result" style="font-size:.78rem;color:#6a8a9a"></span>
</div>
<div id="admin-panel-link" style="display:none;margin-top:12px;padding-top:12px;border-top:1px solid #eee">
<button class="btn-test-small" onclick="openAdmin()" style="font-size:.85rem;padding:6px 16px">Praxis-Verwaltung</button>
</div>
<div class="settings-user-mgmt" id="settings-user-mgmt" style="display:none">
<h3>Benutzer</h3>
<p>Neue Konten f&uuml;r die Praxis nur mit sicherem Passwort anlegen.</p>
<button type="button" class="btn-test-small" onclick="openAddUserDialog()">Neuen Benutzer anlegen</button>
</div>
<div style="margin-top:16px;padding-top:12px;border-top:1px solid #eee">
<button class="btn-cleanup" onclick="doLogout()" style="background:#e8e8f0;color:#3a3a5a;border-color:#c8c8d8">Abmelden</button>
</div>
</div>
</div>
<div id="app-layout">
<nav id="nav-rail" aria-label="Hauptnavigation">
<div class="nr-brand" title="AzA-Empfang">
<img src="aza_logo.png" alt="" width="38" height="38" decoding="async" fetchpriority="high"/>
</div>
<button type="button" class="nr-btn" id="nr-sidebar-ds" onclick="toggleDesktopSidebarCollapsed()"
title="Linke Spalte (Benutzer &amp; Kontext) ausblenden" aria-expanded="true">&#9664;</button>
<button type="button" class="nr-btn" id="nr-dcp-context" onclick="toggleDesktopShellContextRail()"
title="Kontext / Textbausteine ein- oder ausblenden"
aria-label="Kontext und Textbausteine"
aria-expanded="true" aria-controls="desktop-context-panel">&#128203;</button>
<button type="button" class="nr-btn" id="nr-tasks" onclick="toggleTasksPanel()" title="Aufgaben ein-/ausblenden">&#9745;</button>
<span id="sb-badge-open" style="display:none"></span>
<div class="nr-spacer"></div>
<button type="button" class="nr-btn" onclick="toggleSettings()" title="Einstellungen">&#9881;</button>
</nav>
<!-- Linksspalte: Kontext, Kanäle, Benutzer -->
<aside id="sidebar">
<div class="sb-me" id="sb-me">
<div class="sb-me-dot"></div>
<div style="flex:1;overflow:hidden">
<div class="sb-me-name" id="sb-me-name">Nicht angemeldet</div>
<div id="sb-me-role" style="font-size:.68rem;color:#8a9aaa"></div>
</div>
<button class="sb-me-change" onclick="doLogout()" title="Abmelden">&#x2192;</button>
</div>
<div id="sidebar-scroll">
<div class="sb-section">Benutzer dieser Praxis</div>
<div class="sb-users-hint" id="sb-users-hint" aria-live="polite"></div>
<div class="sb-users-wrap" id="sb-users" aria-label="Benutzer dieser Praxis"></div>
<div class="sb-section sb-section-future" title="Vorbereitet f&uuml;r eine sp&auml;tere Ausbaustufe">Externe Kontakte</div>
<div class="sb-placeholder" id="sb-external-placeholder">Sp&auml;ter: andere Praxen und externe Kontakte (eigenbereich).</div>
<div id="shell-patient-strip" class="shell-patient-strip" aria-label="Patientennummer Desktop-Huelle">
<div class="sp-pill">
<span class="sp-label">Patienten-Nr.</span>
<span id="shell-patient-value" class="sp-value">&#8212;</span>
<button type="button" class="sp-x" id="shell-patient-clear" onclick="shellClearPatientNr()" title="Patientennummer l&ouml;schen">&times;</button>
</div>
<button type="button" class="sp-pinsel-btn" id="shell-pinsel-btn" onclick="toggleShellPinsel()" title="Markierung oder Doppelklick \u2014 Nr. uebernehmen">&#x1f58c; Pinsel</button>
<span id="shell-pinsel-hint" class="sp-pinsel-msg" aria-live="polite"></span>
<span id="shell-pinsel-diag" class="sp-pinsel-diag" aria-live="polite"></span>
</div>
<div class="desktop-context-panel" id="desktop-context-panel" aria-label="Kontext und Textbausteine">
<div class="dcp-card dcp-panel-collapsed" id="dcp-panel-card">
<button type="button" class="dcp-head" id="dcp-toggle-btn" onclick="toggleDesktopContextPanel()" aria-expanded="false" aria-controls="dcp-panel-body">
<h3>Kontext / Textbausteine</h3>
<span class="dcp-chevron" aria-hidden="true">&#9660;</span>
</button>
<div class="dcp-body" id="dcp-panel-body">
<div class="dcp-mini-grid">
<div class="dcp-mini">
<h4>Therapieplan</h4>
<div class="dcp-preview dcp-preview-click" id="dcp-preview-ther" aria-live="polite" tabindex="0" role="button" title="In Nachricht einf&#252;gen" onclick="dcpCopyTherapy()" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();dcpCopyTherapy();}"></div>
<label class="dcp-cb"><input type="checkbox" id="dcp-cb-ther"> Autocopy</label>
<div class="dcp-actions">
<button type="button" class="dcp-act" onclick="dcpCopyTherapy()">Kopieren</button>
</div>
<div class="dcp-foot" id="dcp-foot-ther"></div>
</div>
<div class="dcp-mini">
<h4>Procedere</h4>
<div class="dcp-preview dcp-preview-click" id="dcp-preview-proc" aria-live="polite" tabindex="0" role="button" title="In Nachricht einf&#252;gen" onclick="dcpCopyProc()" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();dcpCopyProc();}"></div>
<label class="dcp-cb"><input type="checkbox" id="dcp-cb-proc"> Autocopy</label>
<div class="dcp-actions">
<button type="button" class="dcp-act" onclick="dcpCopyProc()">Kopieren</button>
</div>
<div class="dcp-foot" id="dcp-foot-proc"></div>
</div>
</div>
<div class="dcp-actions">
<button type="button" class="dcp-act" onclick="dcpPasteFromClipboard()" title="F&uuml;gt lesbaren Klartext aus der Zwischenablage an die Eingabe an (HTTPS; ggf. Browser-Berechtigung).">
Zwischenablage in Nachricht
</button>
</div>
</div>
</div>
</div>
</div>
</aside>
<!-- Haupbereich: aktiver Chat -->
<main id="chat-area">
<div id="chat-pane">
<div id="chat-top-bar">
<button class="ctb-back" id="ctb-back" onclick="smartBackChatTop()" type="button">&#9664; Zur&uuml;ck</button>
<div class="ctb-title" id="ctb-title">AzA-Empfang</div>
<div class="ctb-actions" id="ctb-actions"></div>
</div>
<div id="middle-stack">
<div id="inbox-thread-panel" class="inbox-thread-panel hidden" aria-label="Unterhaltungen auswählen">
<div class="itp-inner">
<div class="thread-list-caption itp-caption" id="thread-list-caption"></div>
<div id="thread-list-mount"></div>
</div>
</div>
<div id="conversation-scroll">
<div class="conv-empty">Nachrichten werden geladen&hellip;</div>
</div>
</div>
<div id="chat-input-wrap">
<div id="chat-input-bar">
<div class="input-grow">
<textarea id="reply-text" placeholder="Nachricht schreiben&hellip; (Enter senden, Umschalt+Enter Zeile)" rows="3" onkeydown="if(event.key==='Enter'&&!event.shiftKey){sendReply();event.preventDefault()}" oninput="autoGrowTextarea(this)"></textarea>
<div id="reply-att-hint"></div>
</div>
<div class="input-actions-col">
<div class="input-actions-row">
<button type="button" class="btn-dictate" id="dictate-btn" onclick="toggleDictation()" title="Diktieren (Spracherkennung)">&#127908;</button>
<button type="button" class="btn-audio-rec" id="audio-rec-btn" onclick="toggleAudioRecording()" title="Sprachmemo: Klick starten, erneut klicken zum Stoppen und Senden">&#9210;</button>
</div>
<button type="button" class="btn-send" onclick="sendReply()">Senden</button>
</div>
</div>
</div>
</div>
</main>
<aside id="tasks-panel">
<button type="button" class="tasks-ds-expand-tab" id="tasks-ds-expand-tab" onclick="toggleTasksPanel()" title="Aufgaben einblenden">
<span class="tasks-ds-ico" aria-hidden="true">&#9745;</span>
<span class="tasks-ds-lbl">Aufgaben</span>
</button>
<div class="tp-header">
<div class="tp-header-main">
<span class="tp-sub">Aufgaben</span>
</div>
<div class="tp-header-actions">
<button type="button" class="tp-ds-collapse-btn" id="tp-ds-collapse-btn" onclick="toggleTasksPanel()" title="Aufgaben ausblenden">&#9654;</button>
</div>
</div>
<label class="tp-show-done-filter" for="tp-show-completed">
<input type="checkbox" id="tp-show-completed" autocomplete="off" onchange="onShowCompletedTasksChange()">
<span class="tp-done-lbl">Erledigte anzeigen</span>
</label>
<div class="tp-seg" id="tp-seg" role="tablist" aria-label="Aufgaben filtern">
<button type="button" class="tp-seg-btn" data-tp="mine" id="tp-tab-mine" onclick="setTaskTab('mine')">Meine</button>
<button type="button" class="tp-seg-btn" data-tp="sent" id="tp-tab-sent" onclick="setTaskTab('sent')">Gesendet</button>
<button type="button" class="tp-seg-btn" data-tp="letters" id="tp-tab-letters" onclick="setTaskTab('letters')">Briefe</button>
</div>
<div class="tp-input-row" id="todo-input-row">
<input type="text" id="todo-inline-input" placeholder="Aufgabe eingeben..." onkeydown="if(event.key==='Enter'){addTodoInline();event.preventDefault()}">
</div>
<div id="todo-list"></div>
</aside>
</div>
<!-- Schnell-Anlage Aufgabe aus Chatnachricht -->
<div id="task-quick-panel" class="task-quick-panel hidden" aria-hidden="true" onclick="if(event.target===this)closeTaskQuickPanel()">
<div class="tqp-sheet" role="dialog" aria-labelledby="tqp-heading" onclick="event.stopPropagation()">
<h4 id="tqp-heading">Aufgabe aus Nachricht</h4>
<div class="tqp-meta" id="tqp-source-line"></div>
<input type="hidden" id="tqp-msg-id" value="">
<input type="hidden" id="tqp-thread-id" value="">
<div class="tqp-row">
<label for="tqp-title">Kurztitel</label>
<input type="text" id="tqp-title" maxlength="220" autocomplete="off">
</div>
<div class="tqp-row">
<label for="tqp-assignee">Zuweisen</label>
<select id="tqp-assignee"></select>
</div>
<div class="tqp-row">
<label for="tqp-body">Beschreibung</label>
<textarea id="tqp-body" rows="4"></textarea>
</div>
<div class="tqp-actions">
<button type="button" class="btn-tqp-ghost" onclick="closeTaskQuickPanel()">Abbrechen</button>
<button type="button" class="btn-tqp-primary" onclick="submitTaskQuickPanel()">Aufgabe anlegen</button>
</div>
</div>
</div>
<div id="new-chat-overlay" class="new-chat-overlay hidden" onclick="if(event.target===this)closeNewChatDialog()">
<div class="new-chat-box" onclick="event.stopPropagation()">
<h3>Neuer Chat</h3>
<p>W&auml;hlen Sie, wo der Chat laufen soll. Es &ouml;ffnet sich ein <strong>zus&auml;tzliches Fenster</strong> (Browser oder Desktop-H&uuml;lle) &mdash; Sie k&ouml;nnen dort in Ruhe schreiben, <strong>Bilder per Drag &amp; Drop oder Strg+V</strong> einf&uuml;gen.</p>
<label class="nc-opt"><input type="radio" name="nc-mode" value="general" checked onchange="syncNcPeerSelect()"> Allgemein (Praxis-Inbox, wie Hauptansicht)</label>
<label class="nc-opt"><input type="radio" name="nc-mode" value="dm" onchange="syncNcPeerSelect()"> Direkt mit Benutzer</label>
<select id="nc-peer-select" disabled aria-label="Benutzer w&auml;hlen">
<option value="">&mdash; Benutzer w&auml;hlen &mdash;</option>
</select>
<div class="nc-btns">
<button type="button" class="nc-cancel" onclick="closeNewChatDialog()">Abbrechen</button>
<button type="button" class="nc-go" onclick="launchNewChatWindow()">Fenster &ouml;ffnen</button>
</div>
</div>
</div>
<div id="add-user-overlay" class="add-user-overlay hidden" onclick="if(event.target===this)closeAddUserDialog()">
<div class="add-user-box" onclick="event.stopPropagation()">
<h3>Neuen Benutzer anlegen</h3>
<div class="row">
<label for="adu-name">Benutzername (Anzeige)</label>
<input type="text" id="adu-name" maxlength="120" autocomplete="off" placeholder="z. B. Maria Muster">
</div>
<div class="row">
<label for="adu-role">Rolle</label>
<select id="adu-role">
<option value="mpa">MPA</option>
<option value="arzt">Arzt / &Auml;rztin</option>
<option value="empfang" id="adu-opt-empfang">Empfang</option>
<option value="admin" id="adu-opt-admin">Admin</option>
</select>
</div>
<div class="row">
<label for="adu-pass">Passwort</label>
<input type="password" id="adu-pass" autocomplete="new-password" placeholder="mindestens 4 Zeichen">
</div>
<div class="row">
<label for="adu-pass2">Passwort wiederholen</label>
<input type="password" id="adu-pass2" autocomplete="new-password">
</div>
<div class="adu-err login-error" id="adu-error" style="margin-top:4px;font-size:.82rem;min-height:1.15em;color:#dc3545"></div>
<div class="add-user-btns">
<button type="button" class="adu-cancel" onclick="closeAddUserDialog()">Abbrechen</button>
<button type="button" class="adu-go" onclick="submitSecureAddUser()">Anlegen</button>
</div>
</div>
</div>
<div class="admin-overlay" id="admin-overlay">
<div class="admin-panel">
<div class="admin-header">
<h2>Praxis-Verwaltung</h2>
<button class="admin-close" onclick="closeAdmin()">&times;</button>
</div>
<div class="admin-tabs" id="admin-tabs">
<button class="admin-tab active" onclick="showAdminTab('praxis')">Praxis</button>
<button class="admin-tab" onclick="showAdminTab('users')">Benutzer</button>
<button class="admin-tab" onclick="showAdminTab('devices')">Ger&auml;te</button>
<button class="admin-tab" onclick="showAdminTab('channels')">Kan&auml;le</button>
<button class="admin-tab" onclick="showAdminTab('federation')">Verbindungen</button>
</div>
<div class="admin-body" id="admin-body"></div>
</div>
</div>
<div class="status-bar">
<span>Live-Pulse ca. 1&nbsp;s, Sicherheits-Refresh 15&nbsp;s (gleiche Server-Conversation wie Desktop)</span>
<span style="opacity:.5" id="ui-version">v2026.05.07.dmv2</span>
<span id="dm-diag" style="margin-left:14px;font-family:monospace;font-size:.72rem;color:#7a8ea2"></span>
</div>
<script>
/* ===================================================================
STATE
=================================================================== */
var API_BASE = window.location.origin + '/empfang';
function getPracticeIdOrEmpty() {
try { return localStorage.getItem('aza_practice_id') || ''; } catch (e) { return ''; }
}
function getUrlInviteCode() {
try {
return (new URLSearchParams(window.location.search).get('invite') || '').trim();
} catch (e) { return ''; }
}
var currentSession = null;
var practiceUsers = [];
var lastPracticeInfoForHint = null;
var serverTasks = [];
var allMessages = [];
var allThreads = [];
var activeView = 'allgemein';
var activeDmPeer = null;
var activeDmPeerUid = '';
/** Eine konsolidierte Conversation-Quelle: Klick / Senden / laden. */
var activeConversation = {
mode: 'all',
practiceId: '',
currentUserId: '',
currentUserKey: '',
currentDisplayName: '',
targetUserId: '',
targetUserKey: '',
targetDisplayName: ''
};
function applyActiveConversationFromSession() {
if (!currentSession) return;
activeConversation.practiceId = String(currentSession.practice_id || '').trim();
activeConversation.currentUserId = String(currentSession.user_id || '').trim();
activeConversation.currentUserKey = activeConversation.currentUserId;
activeConversation.currentDisplayName = String(currentSession.display_name || '').trim();
}
function syncLegacyDmFromActiveConversation() {
if (activeConversation.mode === 'direct' && activeConversation.targetDisplayName) {
activeDmPeer = activeConversation.targetDisplayName;
activeDmPeerUid = String(activeConversation.targetUserId || activeConversation.targetUserKey || '').trim();
} else {
activeDmPeer = null;
activeDmPeerUid = '';
}
}
function peerUidForDisplay(dn) {
var n = String(dn || '').trim();
if (!n) return '';
for (var i = 0; i < practiceUsers.length; i++) {
var u = practiceUsers[i];
if (!u) continue;
if (String(u.display_name || '').trim() === n) return String(u.user_id || '').trim();
}
return '';
}
function dmDirectConvKey(meUid, peerUid) {
if (!currentSession) return '';
var pid = String(currentSession.practice_id || '').trim();
if (!pid || !meUid || !peerUid) return '';
var a = meUid <= peerUid ? meUid : peerUid;
var b = meUid <= peerUid ? peerUid : meUid;
return pid + '|direct|' + a + '|' + b;
}
var dmWantsThreadList = false;
var taskTabFilter = 'mine';
try {
taskTabFilter = localStorage.getItem('empfang_task_tab') || 'mine';
if (taskTabFilter === 'team') {
taskTabFilter = 'letters';
try { localStorage.setItem('empfang_task_tab', 'letters'); } catch (eMigr) {}
}
} catch (eTab) {}
/** Erledigte Eintraege in der Aufgabenliste einblenden (localStorage aza_empfang_show_completed_tasks) */
var _showCompletedTasks = false;
try {
_showCompletedTasks = localStorage.getItem('aza_empfang_show_completed_tasks') === '1';
} catch (eComp) {}
/** Nachrichten-ID → Kontext für „Als Aufgabe“ */
var _azaMsgLookup = {};
/** nach Anlage kurz highlighten */
var _taskHighlightId = null;
var activeThreadId = null;
var lastDataHash = '';
var _loadMessagesSeq = 0;
var _dmV2State = { lastConvKey: '', lastSentMsgId: '', lastLoadCount: 0 };
/** Pro Login-Session nur ein Restore-Versuch · Key practice|user */
var _dmPeerRestoreDoneKey = '';
function dmDiagWrite(label) {
try {
var el = document.getElementById('dm-diag');
if (!el) return;
var p = activeConversation || {};
el.textContent = label + ' | mode=' + (p.mode || '?') +
' me=' + (p.currentUserId || '-').slice(0,8) +
' peer=' + (p.targetUserId || '-').slice(0,8) +
' conv=' + (_dmV2State.lastConvKey || '-').slice(-12) +
' load=' + _dmV2State.lastLoadCount +
' lastMsg=' + (_dmV2State.lastSentMsgId || '-');
} catch (e) {}
}
function messageHasChatAck(m) {
return !!(m && m.extras && m.extras.chat_ack);
}
function htmlChatAckButton(mid, m, kind) {
var ack = messageHasChatAck(m);
var cls = (kind === 'doc' ? 'conv-doc-msg-act-ok' : 'conv-msg-act-ok') + (ack ? ' on' : '');
var lbl = ack ? '\u2713 OK' : 'OK';
return '<button type="button" class="' + cls + '" data-mid="' + escAttr(mid) + '" data-ack="' +
(ack ? '1' : '0') + '" title="Als erledigt markieren (erneut klicken zum Zur\u00fccknehmen)">' + lbl + '</button>';
}
var previousThreadIds = new Set();
var previousMsgIds = new Set();
var justSentReply = false;
var _newMsgFlashTimer = null;
var _newMsgOrigTitle = '';
/* Entprellung Popup-bei-Nachricht: pro Message-ID + globaler Cooldown.
Verhindert Endlosschleife und mehrfaches bring_to_front pro Tick. */
var _newMsgNotifiedIds = new Set();
var _newMsgLastNotifyMs = 0;
var _empfangBringFrontInflight = false;
var pollTimer = null;
var userPollTimer = null;
var fallbackTimer = null;
var presencePingTimer = null;
var EMPFANG_PRESENCE_PING_MS = 25000;
var pendingReplyAttachments = [];
var REPLY_ATTACH_MAX_BYTES = 2 * 1024 * 1024;
var AUDIO_ATTACH_MAX_BYTES = 1536 * 1024;
var AUDIO_RECORD_MAX_MS = 120000;
var _azaMediaRecorder = null;
var _azaAudioStream = null;
var _azaAudioChunks = [];
var _azaAudioRecordTimer = null;
var _azaAudioRecordMime = '';
var soundEnabled = localStorage.getItem('empfang_sound') !== 'off';
var audioCtx = null;
var EMPFANG_FONT_BASE_PT = 9;
var currentFontSize = parseInt(localStorage.getItem('empfang_font_size'), 10) || EMPFANG_FONT_BASE_PT;
var volume = parseFloat(localStorage.getItem('empfang_volume') || '1.0');
var repeatMs = parseInt(localStorage.getItem('empfang_repeat_ms') || '30000', 10);
var repeatInterval = null;
var recognition = null;
var isRecording = false;
var _sortRaw = localStorage.getItem('empfang_sort');
if (_sortRaw === 'desc' || _sortRaw === null || _sortRaw === '') localStorage.setItem('empfang_sort', 'newest');
var sortNewestFirst = localStorage.getItem('empfang_sort') !== 'asc';
var TONE_PRESETS = [
{name:'Sanftes Glockenspiel', notes:[{f:523,d:.15},{f:659,d:.15},{f:784,d:.3}], wave:'sine', vol:.12},
{name:'Zwei-Ton Harmonisch', notes:[{f:392,d:.2},{f:523,d:.3}], wave:'sine', vol:.12},
{name:'Drei-Ton Melodie', notes:[{f:523,d:.12},{f:587,d:.12},{f:659,d:.28}], wave:'sine', vol:.11},
{name:'Kristallklar', notes:[{f:1319,d:.5}], wave:'sine', vol:.07},
{name:'Warmer Akkord', notes:[{f:262,d:.4}], wave:'triangle', vol:.14},
{name:'Aufstieg', notes:[{f:262,d:.09},{f:330,d:.09},{f:392,d:.09},{f:523,d:.22}], wave:'sine', vol:.10},
{name:'Sanfte Welle', notes:[{f:440,d:.55}], wave:'sine', vol:.10},
{name:'Tropfen', notes:[{f:659,d:.12},{f:587,d:.12},{f:523,d:.28}], wave:'sine', vol:.10},
{name:'Morgengruss', notes:[{f:523,d:.14},{f:392,d:.14},{f:523,d:.28}], wave:'sine', vol:.12},
{name:'Zephyr', notes:[{f:880,d:.45}], wave:'sine', vol:.06},
{name:'Bambus', notes:[{f:330,d:.18},{f:440,d:.28}], wave:'triangle', vol:.12},
{name:'Silberglocke', notes:[{f:988,d:.45}], wave:'sine', vol:.08},
{name:'Meditation', notes:[{f:262,d:.65}], wave:'sine', vol:.12},
{name:'Horizont', notes:[{f:587,d:.18},{f:880,d:.32}], wave:'sine', vol:.10},
{name:'Stille Post', notes:[{f:784,d:.4}], wave:'sine', vol:.08},
];
var currentToneIdx = parseInt(localStorage.getItem('empfang_tone_idx') || '0', 10);
/* ===================================================================
API WRAPPER (global 401 handling)
=================================================================== */
async function apiFetch(url, opts) {
opts = opts || {};
if (!opts.credentials) opts.credentials = 'include';
var meth = ((opts.method || 'GET') + '').toUpperCase();
if (meth === 'GET' && opts.cache === undefined)
opts.cache = 'no-store';
var r = await fetch(url, opts);
if (r.status === 401) {
currentSession = null;
stopPolling();
showLoginOverlay();
throw new Error('auth');
}
return r;
}
/* ===================================================================
AUTH
=================================================================== */
async function checkAuth() {
try {
var r = await fetch(API_BASE + '/auth/me', {credentials: 'include', cache: 'no-store'});
if (r.status === 401) return null;
var d = await r.json();
if (d.authenticated) return d;
return null;
} catch(e) { return null; }
}
async function refreshLoginPracticeContextBanner() {
var el = document.getElementById('login-practice-context');
if (!el) return;
var pid = getPracticeIdOrEmpty();
if (!pid) {
el.textContent = '';
el.style.display = 'none';
return;
}
el.style.display = 'block';
el.textContent = 'Praxis wird ermittelt\u2026';
try {
var r = await fetch(
API_BASE + '/practice/info?practice_id=' + encodeURIComponent(pid),
{cache: 'no-store'}
);
var d = await r.json().catch(function() { return {}; });
var pname = String((d && d.practice_name) || '').trim();
var shortPid = pid.length > 14 ? pid.slice(0, 12) + '\u2026' : pid;
if (pname) {
el.textContent = 'Praxis: ' + pname;
} else if (shortPid) {
el.textContent = 'Praxis-ID: ' + shortPid;
} else {
el.textContent = '';
el.style.display = 'none';
}
} catch (err) {
el.textContent = 'Praxis-ID: ' + (pid.length > 14 ? pid.slice(0, 12) + '\u2026' : pid);
}
}
async function showLoginOverlay() {
stopPolling();
var overlay = document.getElementById('login-overlay');
overlay.classList.remove('hidden');
try {
var r = await fetch(API_BASE + '/auth/needs_setup', {credentials: 'include'});
var d = await r.json();
if (d.needs_setup) { renderSetupForm(); return; }
} catch(e) {}
renderLoginForm();
}
function hideLoginOverlay() {
document.getElementById('login-overlay').classList.add('hidden');
}
function renderSetupForm() {
var box = document.getElementById('login-box');
box.className = 'login-box';
box.innerHTML =
'<div class="login-practice-context" id="login-practice-context" style="display:none"></div>' +
'<h2>AzA-Empfang einrichten</h2>' +
'<p>Willkommen! Richten Sie Ihre Praxis und den ersten Administrator ein.</p>' +
'<div class="login-field"><label>Praxisname</label><input type="text" id="setup-practice" placeholder="z.B. Hausarztpraxis Winterthur"></div>' +
'<div class="login-field"><label>Ihr Name</label><input type="text" id="setup-name" autocomplete="username" placeholder="z.B. Dr. M\u00fcller"></div>' +
'<div class="login-field"><label>E-Mail (Lizenz-/Admin-E-Mail)</label><input type="email" id="setup-email" autocomplete="email" placeholder="praxis@beispiel.ch"></div>' +
'<div class="login-field"><label>Passwort (min. 4 Zeichen)</label><input type="password" id="setup-pass" autocomplete="new-password"></div>' +
'<button class="login-btn" onclick="doSetup()">Praxis einrichten</button>' +
'<div class="login-error" id="login-error"></div>';
document.getElementById('setup-practice').focus();
refreshLoginPracticeContextBanner();
}
function renderLoginForm() {
var box = document.getElementById('login-box');
box.className = 'login-box';
var lastUser = localStorage.getItem('aza_last_login_user') || '';
box.innerHTML =
'<div class="login-practice-context" id="login-practice-context" style="display:none"></div>' +
'<h2>Anmelden</h2>' +
'<p>AzA-Empfang &ndash; Bitte anmelden</p>' +
'<form onsubmit="event.preventDefault();doLogin()">' +
'<div class="login-field"><label>Benutzername oder E-Mail</label><input type="text" id="login-name" autocomplete="username" value="' + esc(lastUser) + '"></div>' +
'<div class="login-field"><label>Passwort</label><input type="password" id="login-pass" autocomplete="current-password"></div>' +
'<button type="submit" class="login-btn">Anmelden</button>' +
'</form>' +
'<p style="font-size:.78rem;color:#6a8a9a;margin:8px 0 0;line-height:1.45">'
+ 'Benutzername = der <strong>Login-Name</strong> aus der Hauptinstallation (kann z.&nbsp;B. &laquo;Alica Keys&raquo; heissen). '
+ 'E-Mail funktioniert nur, wenn genau ein Konto in <em>Ihrer</em> Praxis diese Adresse hat.</p>' +
'<div class="login-error" id="login-error"></div>' +
'<div class="login-switch"><a onclick="renderForgotPasswordForm()">Passwort vergessen?</a></div>' +
'<div class="login-switch" style="margin-top:16px;padding-top:12px;border-top:1px solid #eee">' +
'<p style="font-size:.82rem;color:#6a8a9a;margin-bottom:6px">Noch kein Konto? Registrieren Sie sich mit dem Einladungscode Ihrer Praxis.</p>' +
'<a onclick="renderRegisterForm()" style="font-weight:600">Registrieren &rarr;</a>' +
'</div>';
var nameInp = document.getElementById('login-name');
var passInp = document.getElementById('login-pass');
if (lastUser) { passInp.focus(); } else { nameInp.focus(); }
refreshLoginPracticeContextBanner();
}
function renderForgotPasswordForm() {
var box = document.getElementById('login-box');
box.className = 'login-box';
var pid = getPracticeIdOrEmpty();
box.innerHTML =
'<div class="login-practice-context" id="login-practice-context" style="display:none"></div>' +
'<h2>Passwort vergessen</h2>' +
'<p>Geben Sie Ihren <strong>Benutzernamen (Login-Namen)</strong> ein'
+ (pid ? '.' : ' oder falls eindeutig Ihre zur Praxis geh\u00f6rende E-Mail.') + '</p>' +
'<p style="font-size:.82rem;color:#6a8a9a">Wenn mehrere Konten dieselbe '
+ 'E-Mail-Adresse nutzen, muss der Login mit dem eindeutigen Benutzernamen erfolgen '
+ '\u2014 das System kann dann nicht automatisch zwischen den Konten w\u00e4hlen.</p>' +
'<div class="login-field"><label>Benutzername oder E-Mail</label><input type="text" id="forgot-login" autocomplete="username" placeholder="z. B. Alicia Keys oder name@praxis.ch"></div>' +
'<button class="login-btn" onclick="doForgotPassword()">Reset-Link senden</button>' +
'<div class="login-error" id="login-error"></div>' +
'<div class="login-switch"><a onclick="renderLoginForm()">&larr; Zur\u00fcck zur Anmeldung</a></div>';
document.getElementById('forgot-login').focus();
refreshLoginPracticeContextBanner();
}
function _formatForgotResultHint(d) {
var base = (d.message || '').trim();
var mask = (d.target_email_masked || '').trim();
if (d.mail_delivered && mask && base.indexOf(mask) < 0) {
return base + (base ? ' [' : '[') + mask + ']';
}
return base;
}
async function doForgotPassword() {
var raw = (document.getElementById('forgot-login').value || '').trim();
var errEl = document.getElementById('login-error');
errEl.style.color = '';
if (!raw) { errEl.style.color = '#842029'; errEl.textContent = 'Bitte Login-Namen oder E-Mail eingeben.'; return; }
try {
var r = await fetch(API_BASE + '/auth/forgot_password', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({login: raw, practice_id: getPracticeIdOrEmpty()})
});
var d = await r.json().catch(function() { return {}; });
if (d.step === 'ambiguous_email') {
errEl.style.color = '#842029';
errEl.textContent = d.message ||
'Diese E-Mail ist mehreren Konten zugeordnet \u2014 bitte mit Benutzernamen fortfahren.';
return;
}
if (d.step === 'no_email' || (d.success === false && d.step === 'no_email')) {
errEl.style.color = '#842029';
errEl.textContent = d.message || 'Keine E-Mail hinterlegt.';
return;
}
if (d.step === 'mail_failed' || (!d.mail_delivered && d.attempted_delivery)) {
errEl.style.color = '#842029';
errEl.textContent = d.message || 'E-Mail-Versand fehlgeschlagen.';
return;
}
if (d.step === 'none') {
errEl.style.color = '#356488';
errEl.textContent = d.message || '';
return;
}
if (d.success === false && d.message) {
errEl.style.color = '#842029';
errEl.textContent = d.message;
return;
}
errEl.style.color = '#155724';
errEl.textContent = _formatForgotResultHint(d) || d.message || 'Wenn ein passendes Konto existiert, wurde ein Link gesendet.';
} catch(e) {
errEl.style.color = '#842029';
errEl.textContent = 'Fehler beim Senden.';
}
}
function stripResetTokenFromUrl() {
try {
var p = window.location.pathname || '';
var sp = new URLSearchParams(window.location.search);
sp.delete('reset_token');
var q = sp.toString();
history.replaceState(null, '', p + (q ? '?' + q : ''));
} catch (e) {}
}
function renderResetPasswordInvalid(msg) {
stripResetTokenFromUrl();
var box = document.getElementById('login-box');
box.className = 'login-box';
box.innerHTML =
'<h2>Link ung\u00fcltig</h2>' +
'<p class="login-error" id="login-error" style="display:block;color:#842029">' + esc(msg || 'Ung\u00fcltiger oder abgelaufener Link.') + '</p>' +
'<div class="login-switch"><a onclick="renderLoginForm()">Zur Anmeldung</a></div>';
}
function renderResetPasswordForm(resetToken) {
var box = document.getElementById('login-box');
box.className = 'login-box';
box.innerHTML =
'<h2>Passwort zur\u00fccksetzen</h2>' +
'<p>Sie haben eine Passwort-Zur\u00fccksetzung angefordert.</p>' +
'<p>Bitte w\u00e4hlen Sie ein neues Passwort (min. 4 Zeichen).</p>' +
'<div class="login-field"><label>Neues Passwort</label><input type="password" id="reset-pass" autocomplete="new-password"></div>' +
'<div class="login-field"><label>Neues Passwort best\u00e4tigen</label><input type="password" id="reset-pass2" autocomplete="new-password"></div>' +
'<button class="login-btn" onclick="doResetPassword(\'' + resetToken + '\')">Neues Passwort festlegen</button>' +
'<div class="login-error" id="login-error"></div>';
document.getElementById('reset-pass').focus();
}
async function doResetPassword(resetToken) {
var pass1 = document.getElementById('reset-pass').value;
var pass2 = document.getElementById('reset-pass2').value;
var errEl = document.getElementById('login-error');
if (!pass1 || pass1.length < 4) { errEl.textContent = 'Passwort muss mindestens 4 Zeichen haben.'; return; }
if (pass1 !== pass2) { errEl.textContent = 'Passw\u00f6rter stimmen nicht \u00fcberein.'; return; }
try {
var r = await fetch(API_BASE + '/auth/reset_password', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({reset_token: resetToken, password: pass1})
});
var d = await r.json().catch(function() { return {}; });
if (r.ok && d.success) {
stripResetTokenFromUrl();
var pref = (d.login_name || d.display_name || '').trim();
if (pref) { try { localStorage.setItem('aza_last_login_user', pref); } catch (e2) {} }
errEl.style.color = '#155724';
errEl.textContent = 'Passwort wurde erfolgreich ge\u00e4ndert. Sie k\u00f6nnen sich jetzt anmelden.';
setTimeout(renderLoginForm, 2000);
} else {
var fail = d.detail;
if (typeof fail === 'string') {
errEl.textContent = fail;
} else if (Array.isArray(fail) && fail[0] && fail[0].msg) {
errEl.textContent = fail.map(function(x) { return x.msg; }).join(' ');
} else {
errEl.textContent = d.message || 'Passwort konnte nicht zur\u00fcckgesetzt werden.';
}
}
} catch(e) { errEl.textContent = 'Passwort konnte nicht zur\u00fcckgesetzt werden.'; }
}
function renderRegisterForm() {
var box = document.getElementById('login-box');
box.className = 'login-box login-register';
box.innerHTML =
'<div class="login-practice-context" id="login-practice-context" style="display:none"></div>' +
'<h2>registrieren Sie sich f\u00fcr den Chat</h2>' +
'<p>Ihr Administrator hat Ihnen einen Einladungscode gegeben.</p>' +
'<div class="login-field"><label>Einladungscode</label><input type="text" id="reg-code" placeholder="z.B. CHAT-AB12-CD34"></div>' +
'<div class="login-field"><label>Ihr Name</label><input type="text" id="reg-name" autocomplete="username" placeholder="z.B. Sandra M\u00fcller"></div>' +
'<div class="login-field"><label>Passwort (min. 4 Zeichen)</label><input type="password" id="reg-pass" autocomplete="new-password"></div>' +
'<div class="login-field"><label>Ihre Rolle</label>' +
'<select id="reg-role">' +
'<option value="empfang">Empfang</option>' +
'<option value="mpa">MPA</option>' +
'<option value="arzt">Arzt / \u00c4rztin</option>' +
'</select>' +
'</div>' +
'<button class="login-btn" onclick="doRegister()">Registrieren</button>' +
'<div class="login-error" id="login-error"></div>' +
'<div class="login-switch"><a onclick="renderLoginForm()">Bereits registriert? &rarr; Anmelden</a></div>';
document.getElementById('reg-code').focus();
refreshLoginPracticeContextBanner();
}
async function onAuthSuccess() {
var me = await checkAuth();
if (!me) return false;
currentSession = me;
applyActiveConversationFromSession();
hideLoginOverlay();
updateSbMe();
await empfangPresencePing();
loadUsersFromServer();
loadTasksFromServer();
loadMessages();
loadPracticeInfo();
refreshSettingsUserSection();
refreshTaskTabsForRole();
startPolling();
return true;
}
async function doSetup() {
var practiceName = (document.getElementById('setup-practice').value || '').trim();
var name = (document.getElementById('setup-name').value || '').trim();
var email = (document.getElementById('setup-email').value || '').trim();
var pass = document.getElementById('setup-pass').value || '';
var errEl = document.getElementById('login-error');
if (!practiceName) { errEl.textContent = 'Bitte Praxisnamen eingeben.'; return; }
if (!name) { errEl.textContent = 'Bitte Ihren Namen eingeben.'; return; }
if (!pass || pass.length < 4) { errEl.textContent = 'Passwort (min. 4 Zeichen) erforderlich.'; return; }
try {
var r = await fetch(API_BASE + '/auth/setup', {
method: 'POST', credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: name, password: pass, practice_name: practiceName, email: email})
});
if (r.ok) {
try {
var sd = await r.json();
if (sd.practice_id) localStorage.setItem('aza_practice_id', sd.practice_id);
} catch(e) {}
if (!await onAuthSuccess()) errEl.textContent = 'Einrichtung fehlgeschlagen.';
} else {
var d = await r.json().catch(function(){ return {}; });
errEl.textContent = d.detail || d.error || 'Einrichtung fehlgeschlagen.';
}
} catch(e) { errEl.textContent = 'Verbindungsfehler.'; }
}
async function doLogin() {
var name = (document.getElementById('login-name').value || '').trim();
var pass = document.getElementById('login-pass').value || '';
var errEl = document.getElementById('login-error');
errEl.style.color = '';
if (!name || !pass) { errEl.textContent = 'Benutzername/E-Mail und Passwort erforderlich.'; return; }
var inv = getUrlInviteCode();
try {
var r = await fetch(API_BASE + '/auth/login', {
method: 'POST', credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
name: name,
password: pass,
practice_id: getPracticeIdOrEmpty(),
invite_code: inv
})
});
if (r.ok) {
try {
var ld = await r.json();
if (ld.practice_id) localStorage.setItem('aza_practice_id', ld.practice_id);
if (ld.practice_bind_source) try {
sessionStorage.setItem('aza_practice_bind', ld.practice_bind_source);
} catch (eB) {}
var saveUser = (ld.display_name || name).trim();
localStorage.setItem('aza_last_login_user', saveUser);
try {
var u = new URL(window.location.href);
if (u.searchParams.get('invite')) {
u.searchParams.delete('invite');
history.replaceState({}, '', u.pathname + u.search + u.hash);
}
} catch (eu) {}
} catch(e) {}
if (!await onAuthSuccess()) errEl.textContent = 'Anmeldung fehlgeschlagen.';
} else {
var d = await r.json().catch(function(){ return {}; });
errEl.style.color = '#842029';
var det = d.detail;
if (typeof det === 'object' && det !== null && det.message) {
errEl.textContent = det.message;
} else if (typeof det === 'string') {
errEl.textContent = det;
} else {
errEl.textContent = d.error || 'Anmeldung fehlgeschlagen.';
}
}
} catch(e) { errEl.style.color = '#842029'; errEl.textContent = 'Verbindungsfehler.'; }
}
async function doRegister() {
var code = (document.getElementById('reg-code').value || '').trim();
var name = (document.getElementById('reg-name').value || '').trim();
var pass = document.getElementById('reg-pass').value || '';
var role = document.getElementById('reg-role').value || 'empfang';
var errEl = document.getElementById('login-error');
if (!code || !name || !pass) { errEl.textContent = 'Alle Felder erforderlich.'; return; }
try {
var r = await fetch(API_BASE + '/auth/register', {
method: 'POST', credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({invite_code: code, name: name, password: pass, role: role})
});
if (r.ok) {
try {
var rd = await r.json();
if (rd.practice_id) localStorage.setItem('aza_practice_id', rd.practice_id);
if (rd.practice_bind_source) try {
sessionStorage.setItem('aza_practice_bind', rd.practice_bind_source);
} catch (eBr) {}
try {
var ur = new URL(window.location.href);
if (ur.searchParams.get('invite')) {
ur.searchParams.delete('invite');
history.replaceState({}, '', ur.pathname + ur.search + ur.hash);
}
} catch (er) {}
} catch(e) {}
if (!await onAuthSuccess()) errEl.textContent = 'Registrierung fehlgeschlagen.';
} else {
var d = await r.json().catch(function(){ return {}; });
errEl.textContent = d.detail || d.error || 'Registrierung fehlgeschlagen.';
}
} catch(e) { errEl.textContent = 'Verbindungsfehler.'; }
}
async function doLogout() {
try { await fetch(API_BASE + '/auth/logout', {method: 'POST', credentials: 'include'}); } catch(e) {}
currentSession = null;
practiceUsers = [];
lastPracticeInfoForHint = null;
serverTasks = [];
allMessages = [];
allThreads = [];
activeConversation.mode = 'all';
activeConversation.practiceId = '';
activeConversation.currentUserId = '';
activeConversation.currentUserKey = '';
activeConversation.currentDisplayName = '';
activeConversation.targetUserId = '';
activeConversation.targetUserKey = '';
activeConversation.targetDisplayName = '';
activeDmPeer = null;
activeDmPeerUid = '';
activeThreadId = null;
activeView = 'allgemein';
lastDataHash = '';
previousThreadIds = new Set();
previousMsgIds = new Set();
try { stopNewMsgFlash(); } catch (_eF) {}
stopPolling();
stopRepeatTone();
updateSbMe();
document.getElementById('sb-users').innerHTML = '';
document.getElementById('todo-list').innerHTML = '';
document.getElementById('conversation-scroll').innerHTML = '<div class="conv-empty">Nachrichten werden geladen&hellip;</div>';
document.getElementById('thread-list-mount').innerHTML = '';
document.getElementById('count-badge').textContent = '\u2013';
document.getElementById('sb-badge-open').textContent = '';
var _hps = document.getElementById('header-practice-sub');
if (_hps) _hps.textContent = '';
var _suh = document.getElementById('sb-users-hint');
if (_suh) _suh.textContent = '';
var _pis = document.getElementById('practice-info-section');
if (_pis) _pis.innerHTML = '';
try { sessionStorage.removeItem('aza_practice_bind'); } catch(eSb) {}
_dmPeerRestoreDoneKey = '';
showLoginOverlay();
}
/* ===================================================================
INIT
=================================================================== */
(async function init() {
var sel = document.getElementById('tone-select');
if (sel) {
TONE_PRESETS.forEach(function(t, i) {
var o = document.createElement('option');
o.value = i; o.textContent = t.name;
if (i === currentToneIdx) o.selected = true;
sel.appendChild(o);
});
}
var vs = document.getElementById('vol-slider');
var vd = document.getElementById('vol-display');
if (vs) vs.value = Math.round(volume * 100);
if (vd) vd.textContent = Math.round(volume * 100) + '%';
initRepeatUI();
updateSortBtn();
updateSoundBtn();
applyFontSize();
setupReplyBarDropPaste();
bindConversationMsgActionsOnce();
bindSidebarUserClicks();
initShellPinUi();
ensureDesktopShellAttr();
restoreTasksPanelState();
restoreDesktopSidebarState();
applyDesktopShellChrome();
applyDesktopShellContextRail();
var urlParams = new URLSearchParams(window.location.search);
var resetToken = urlParams.get('reset_token');
if (resetToken) {
stopPolling();
document.getElementById('login-overlay').classList.remove('hidden');
try {
var vr = await fetch(
API_BASE + '/auth/reset_verify?reset_token=' + encodeURIComponent(resetToken)
);
var vd = await vr.json().catch(function() { return {valid: false}; });
if (!vd.valid) {
renderResetPasswordInvalid(vd.detail || 'Ung\u00fcltiger oder abgelaufener Link.');
return;
}
renderResetPasswordForm(resetToken);
} catch (e) {
renderResetPasswordInvalid('Verbindungsfehler. Bitte sp\u00e4ter erneut versuchen.');
}
return;
}
var inviteParam = (urlParams.get('invite') || '').trim();
var resolvedInv = null;
if (inviteParam) {
try {
var ir = await fetch(API_BASE + '/auth/resolve_invite?code=' + encodeURIComponent(inviteParam));
resolvedInv = await ir.json().catch(function() { return null; });
if (resolvedInv && resolvedInv.valid && resolvedInv.practice_id) {
try {
localStorage.setItem('aza_practice_id', resolvedInv.practice_id);
} catch(e) {}
try { sessionStorage.setItem('aza_practice_bind', 'invite_code'); } catch(e2) {}
} else if (resolvedInv && resolvedInv.valid === false) {
stopPolling();
document.getElementById('login-overlay').classList.remove('hidden');
showLoginOverlay();
var badInv = document.getElementById('login-error');
if (badInv) {
badInv.style.color = '#842029';
badInv.textContent = 'Ung\u00fcltiger oder veralteter Einladungscode. Bitte aktuellen Link aus der AzA-Hauptinstallation anfordern.';
}
return;
}
} catch(e) {}
}
var me = await checkAuth();
if (me && inviteParam && resolvedInv && resolvedInv.valid && resolvedInv.practice_id &&
me.practice_id && me.practice_id !== resolvedInv.practice_id) {
try { await fetch(API_BASE + '/auth/logout', {method: 'POST', credentials: 'include'}); } catch(e) {}
me = null;
try {
localStorage.setItem('aza_practice_id', resolvedInv.practice_id);
} catch(e) {}
try { sessionStorage.setItem('aza_practice_mismatch_cleared', '1'); } catch(e3) {}
}
if (me && me.practice_id) {
try { localStorage.setItem('aza_practice_id', me.practice_id); } catch(e) {}
}
if (!me) {
stopPolling();
document.getElementById('login-overlay').classList.remove('hidden');
if (inviteParam) {
renderRegisterForm();
setTimeout(function() {
var codeInput = document.getElementById('reg-code');
if (codeInput) codeInput.value = inviteParam;
}, 50);
} else {
showLoginOverlay();
}
return;
}
currentSession = me;
applyActiveConversationFromSession();
hideLoginOverlay();
updateSbMe();
await empfangPresencePing();
loadUsersFromServer();
loadTasksFromServer();
loadMessages();
loadPracticeInfo();
refreshSettingsUserSection();
refreshTaskTabsForRole();
startPolling();
if (isDesktopShellMode()) {
setTimeout(function() {
try {
refreshDesktopShellContextFromServer(true);
} catch (eD) {}
}, 380);
}
})();
/* ===================================================================
SIDEBAR + NAVIGATION (Chats / Benutzer / Direktchat V1, nur eigene Praxis)
=================================================================== */
function senderDisplayCore(abs) {
return (abs || '').split('(')[0].trim();
}
function isGeneralInboxThread(t) {
var ex = t.root.extras || {};
if (ex.recipients && Array.isArray(ex.recipients) && ex.recipients.length >= 2) return false;
var rcpt = (ex.recipient || '').trim();
return !rcpt || rcpt === 'Alle';
}
function threadTouchesDmPeer(t, peerName) {
if (!peerName || !currentSession) return false;
var myName = currentSession.display_name.trim();
var pn = (peerName || '').trim();
var meUid = String((currentSession.user_id || '')).trim();
var pUid = String(activeDmPeerUid || '').trim();
var wantKey = (meUid && pUid) ? dmDirectConvKey(meUid, pUid) : '';
var msgs = t.all || [];
for (var i = 0; i < msgs.length; i++) {
var m = msgs[i];
var ex = m.extras || {};
var km = String(ex.direct_conv_key || '').trim();
if (wantKey && km && km === wantKey) return true;
var rcpts = ex.recipients;
if (rcpts && Array.isArray(rcpts) && rcpts.length >= 2) continue;
var rcpt = (ex.recipient || '').trim();
if (!rcpt || rcpt === 'Alle') continue;
if (rcpt.indexOf(',') >= 0) continue;
var sender = senderDisplayCore(m.absender);
var sortedNeed = [myName, pn].sort().join('\x00');
var sortedGot = [sender, rcpt].sort().join('\x00');
if (sortedNeed === sortedGot) return true;
}
return false;
}
function getFilteredThreads() {
var base = allThreads.filter(function(t) {
return activeView === 'archiv'
? t.root.status === 'erledigt'
: t.root.status === 'offen';
});
if (activeDmPeer) {
return base.filter(function(t) { return threadTouchesDmPeer(t, activeDmPeer); });
}
if (activeView === 'archiv') return base.filter(isGeneralInboxThread);
return base.filter(isGeneralInboxThread);
}
function shouldShowCentralInbox() {
if (messengerLayoutActive()) return false;
if (!currentSession || activeThreadId) return false;
var ft = getFilteredThreads();
if (activeDmPeer) return ft.length > 1 || dmWantsThreadList;
return ft.length > 0;
}
function conversationIsSparse(th) {
if (!th || !th.root) return false;
if (th.replies.length > 0) return false;
var m = th.root;
if (m.extras && m.extras.attachments && m.extras.attachments.length) return false;
var k = String(m.kommentar || '').replace(/\u200b/g, '').trim();
if (k) return false;
if ((m.medikamente || '').trim()) return false;
if ((m.therapieplan || '').trim()) return false;
if ((m.procedere || '').trim()) return false;
return true;
}
function maybeAutoPickSingleDmThread() {
if (messengerLayoutActive()) return;
if (activeThreadId || !activeDmPeer || dmWantsThreadList) return;
var ft = getFilteredThreads();
if (ft.length !== 1) return;
activeThreadId = ft[0].tid;
}
function syncChatShellLayout() {
var pane = document.getElementById('chat-pane');
var inboxPanel = document.getElementById('inbox-thread-panel');
var conv = document.getElementById('conversation-scroll');
if (!pane || !inboxPanel || !conv) return;
if (messengerLayoutActive()) {
inboxPanel.classList.add('hidden');
conv.classList.remove('conv-panel-compact');
pane.classList.remove('chat-composer-centered');
return;
}
var inboxOn = shouldShowCentralInbox();
inboxPanel.classList.toggle('hidden', !inboxOn);
conv.classList.toggle('conv-panel-compact', !!inboxOn);
var th0 = activeThreadId ? allThreads.find(function(h) { return h.tid === activeThreadId; }) : null;
var sparseThread = !!(activeThreadId && !inboxOn && conversationIsSparse(th0));
var dmFreeComposer = !!(activeDmPeer && !activeThreadId && currentSession &&
String(activeDmPeer).trim() !== String(currentSession.display_name || '').trim());
var ftN = getFilteredThreads().length;
var centered = sparseThread || (dmFreeComposer && ftN <= 1);
pane.classList.toggle('chat-composer-centered', !!centered);
var tb = document.getElementById('ctb-title');
if (tb && !activeThreadId) {
if (inboxOn) {
tb.textContent = activeDmPeer ? formatDmChatTitle(activeDmPeer) :
(activeView === 'archiv' ? 'Archiv Unterhaltungen w\u00e4hlen' : 'Allgemein Posteingang');
}
}
}
function taskItemKind(t) {
var k = String((t && t.item_kind) || '').trim().toLowerCase();
return k === 'letter' ? 'letter' : 'task';
}
function namesRoughlyEqual(a, b) {
return String(a || '').trim().toLowerCase() === String(b || '').trim().toLowerCase();
}
function filteredTasksForUi() {
var me = currentSession ? String(currentSession.display_name || '').trim() : '';
var list = serverTasks.slice();
if (taskTabFilter === 'mine')
return list.filter(function(t) { return taskItemKind(t) === 'task' && namesRoughlyEqual(t.assignee, me); });
if (taskTabFilter === 'sent')
return list.filter(function(t) { return taskItemKind(t) === 'task' && namesRoughlyEqual(t.created_by, me); });
/* Briefe: standardmaessig nur offene; mit „Erledigte anzeigen“ auch erledigte */
return list.filter(function(t) {
if (taskItemKind(t) !== 'letter') return false;
if (_showCompletedTasks) return true;
return !t.done;
});
}
function setTaskTab(tab) {
taskTabFilter = tab;
try { localStorage.setItem('empfang_task_tab', tab); } catch (es) {}
document.querySelectorAll('.tp-seg-btn').forEach(function(b) {
b.classList.toggle('tp-on', b.getAttribute('data-tp') === tab);
});
renderTodos();
}
function refreshTaskTabsForRole() {
/* Frueher: Team-Tab nur fuer Admin/Empfang. Briefe fuer alle Nutzer mit Empfangsansicht sichtbar. */
var lettersBtn = document.getElementById('tp-tab-letters');
if (lettersBtn) lettersBtn.classList.remove('hidden');
if (taskTabFilter === 'team') setTaskTab('letters');
document.querySelectorAll('.tp-seg-btn').forEach(function(b) {
b.classList.toggle('tp-on', b.getAttribute('data-tp') === taskTabFilter);
});
}
function syncTaskTabButtons() {
document.querySelectorAll('.tp-seg-btn').forEach(function(b) {
if (b.classList.contains('hidden')) return;
b.classList.toggle('tp-on', b.getAttribute('data-tp') === taskTabFilter);
});
}
function countGeneralOpenThreads() {
return allThreads.filter(function(t) {
return t.root.status === 'offen' && isGeneralInboxThread(t);
}).length;
}
function deviceRecentlyActive(dev) {
var la = dev && dev.last_active;
if (!la) return false;
var t = Date.parse(String(la).replace(' ', 'T'));
if (isNaN(t)) return false;
return Date.now() - t < 120000;
}
function userPresenceServerOnline(u) {
return !!(u && u.presence_online === true);
}
function userPresenceFromDevices(u) {
var devs = u && u.devices;
if (!devs || !devs.length) return false;
for (var i = 0; i < devs.length; i++) {
if (deviceRecentlyActive(devs[i])) return true;
}
return false;
}
/** Anzeige (Punkt + Pastell): zuerst Server-Presence, dann Geraete, dann Self mit gueltiger Session. */
function userPresenceEffectiveOnline(u) {
if (!u) return false;
if (userPresenceServerOnline(u)) return true;
if (userPresenceFromDevices(u)) return true;
if (isPracticeUserSelf(u) && currentSession) return true;
return false;
}
function userPresenceDotClass(u) {
return userPresenceEffectiveOnline(u) ? 'online' : 'offline';
}
/** Nur fuer Fremde (Sortierung): kein Self-Fallback — Self steht immer unten. */
function isUserOnlineOrActive(u) {
if (!u || isPracticeUserSelf(u)) return false;
if (userPresenceServerOnline(u)) return true;
if (userPresenceFromDevices(u)) return true;
return false;
}
function getUserStableId(u) {
if (!u || typeof u !== 'object') return '';
var keys = ['user_id', 'userId', 'uid', 'id', 'empfang_user_id'];
for (var i = 0; i < keys.length; i++) {
var v = u[keys[i]];
var s = String(v != null ? v : '').trim();
if (s) return s;
}
return '';
}
function getCurrentStableUserId() {
if (!currentSession || typeof currentSession !== 'object') return '';
var keys = ['user_id', 'userId', 'uid', 'id', 'empfang_user_id'];
for (var i = 0; i < keys.length; i++) {
var v = currentSession[keys[i]];
var s = String(v != null ? v : '').trim();
if (s) return s;
}
try {
var c = activeConversation && String(activeConversation.currentUserId || '').trim();
if (c) return c;
} catch (e) {}
return '';
}
function normalizeDisplayMatch(s) {
return String(s || '').trim().toLowerCase();
}
/** Eigener Account: stabile User-IDs (alle gaengigen Felder); sonst Displayname-Vergleich. */
function isPracticeUserSelf(u) {
if (!u) return false;
var bid = getUserStableId(u);
var cid = getCurrentStableUserId();
if (cid && bid && cid === bid) return true;
if (!currentSession) return false;
var un = normalizeDisplayMatch(u.display_name);
var sn = normalizeDisplayMatch(currentSession.display_name || currentSession.name);
return !!un && !!sn && un === sn;
}
function lastDmPeerStorageKey(practiceId, userId) {
return 'aza_empfang_last_direct_peer::' + String(practiceId || '').trim() + '::' + String(userId || '').trim();
}
function urlSpecifiesDirectChatPeer() {
try {
var sp = new URLSearchParams(window.location.search);
var keys = ['peer_user_id', 'peer_uid', 'dm_peer', 'open_user', 'recipient_user_id', 'recipient'];
for (var ki = 0; ki < keys.length; ki++) {
if ((sp.get(keys[ki]) || '').trim()) return true;
}
} catch (eUr) {}
return false;
}
function findPracticeUserByStableId(peerUid) {
var want = String(peerUid || '').trim();
if (!want) return null;
var found = null;
var n = 0;
for (var i = 0; i < practiceUsers.length; i++) {
var u = practiceUsers[i];
if (!u) continue;
if (String(getUserStableId(u) || '').trim() === want) {
n++;
found = u;
}
}
return n === 1 ? found : null;
}
function clearLastDirectPeerStorage() {
if (!currentSession) return;
try {
localStorage.removeItem(lastDmPeerStorageKey(
String(currentSession.practice_id || '').trim(),
String(getCurrentStableUserId() || '').trim()
));
} catch (eClr) {}
}
function persistLastDirectPeerSnapshot() {
if (!currentSession) return;
applyActiveConversationFromSession();
if (activeConversation.mode !== 'direct') return;
var peerUid = String(activeConversation.targetUserId || '').trim();
var pid = String(activeConversation.practiceId || currentSession.practice_id || '').trim();
var meUid = String(getCurrentStableUserId() || '').trim();
if (!pid || !meUid || !peerUid || peerUid === meUid) return;
var buddy = findPracticeUserByStableId(peerUid);
if (!buddy || isPracticeUserSelf(buddy)) return;
var peerDn = String(buddy.display_name || '').trim();
if (!peerDn) return;
var o = {
v: 1,
practice_id: pid,
user_id: meUid,
peer_user_id: peerUid,
peer_display_name: peerDn,
saved_at: Date.now()
};
try {
localStorage.setItem(lastDmPeerStorageKey(pid, meUid), JSON.stringify(o));
} catch (eSt) {}
}
function tryRestoreLastDirectPeerOnce() {
if (!currentSession) return;
var pid = String(currentSession.practice_id || '').trim();
var meUid = String(getCurrentStableUserId() || '').trim();
if (!pid || !meUid || !practiceUsers.length) return;
var sk = pid + '|' + meUid;
if (_dmPeerRestoreDoneKey === sk) return;
_dmPeerRestoreDoneKey = sk;
if (urlSpecifiesDirectChatPeer()) return;
var tgt = String(activeConversation.targetUserId || '').trim();
if (activeConversation.mode === 'direct' && tgt && tgt !== meUid) return;
var raw;
try {
raw = localStorage.getItem(lastDmPeerStorageKey(pid, meUid));
} catch (eG) { return; }
if (!raw) return;
var o;
try { o = JSON.parse(raw); } catch (eJ) { return; }
if (!o || typeof o !== 'object') return;
if (String(o.practice_id || '').trim() !== pid) return;
if (String(o.user_id || '').trim() !== meUid) return;
var peerUid = String(o.peer_user_id || '').trim();
if (!peerUid || peerUid === meUid) return;
var buddy = findPracticeUserByStableId(peerUid);
if (!buddy || isPracticeUserSelf(buddy)) return;
var dname = String(buddy.display_name || '').trim() || String(o.peer_display_name || '').trim();
if (!dname) return;
selectDmPeer(dname, peerUid);
}
function userSortRank(u) {
if (isPracticeUserSelf(u)) return 2;
if (isUserOnlineOrActive(u)) return 0;
return 1;
}
function localeCompareDisplayName(a, b) {
var na = String(a && a.display_name || '');
var nb = String(b && b.display_name || '');
try {
return na.localeCompare(nb, 'de', { sensitivity: 'base' });
} catch (e) {
return na.localeCompare(nb);
}
}
function updateSidebarNavHighlight() {
document.querySelectorAll('.nav-card[data-view]').forEach(function(el) {
var v = el.getAttribute('data-view');
el.classList.toggle('active', !activeDmPeer && activeView === v);
});
document.querySelectorAll('#sb-users .sb-user[data-peer]').forEach(function(el) {
var enc = el.getAttribute('data-peer') || '';
var p = enc;
try {
p = decodeURIComponent(enc);
} catch (e) {}
el.classList.toggle('dm-selected', !!(activeDmPeer && p === activeDmPeer));
});
var ni = document.getElementById('nr-inbox');
var na = document.getElementById('nr-archiv');
if (ni) ni.classList.toggle('nr-active', !activeDmPeer && activeView === 'allgemein');
if (na) na.classList.toggle('nr-active', !activeDmPeer && activeView === 'archiv');
var nt = document.getElementById('nr-tasks');
var tp = document.getElementById('tasks-panel');
if (nt && tp) nt.classList.toggle('nr-active', !tp.classList.contains('collapsed'));
var ns = document.getElementById('nr-sidebar-ds');
var sb = document.getElementById('sidebar');
if (ns && sb && isDesktopShellMode()) {
var sc = sb.classList.contains('sidebar-ds-collapsed');
ns.classList.toggle('nr-active', !sc);
ns.setAttribute('aria-expanded', sc ? 'false' : 'true');
ns.title = sc
? 'Linke Spalte einblenden (Benutzer & Kontext)'
: 'Linke Spalte ausblenden (Benutzer & Kontext)';
ns.textContent = sc ? '\u25B6' : '\u25C0';
}
}
function toggleTasksPanel() {
var p = document.getElementById('tasks-panel');
if (!p) return;
p.classList.toggle('collapsed');
try {
localStorage.setItem(tasksCollapsedStorageKey(), p.classList.contains('collapsed') ? '1' : '0');
} catch (e) {}
updateSidebarNavHighlight();
}
function restoreTasksPanelState() {
var p = document.getElementById('tasks-panel');
if (!p) return;
try {
var c = localStorage.getItem(tasksCollapsedStorageKey());
if (isDesktopShellMode()) {
if (c === '0') p.classList.remove('collapsed');
else p.classList.add('collapsed');
} else {
if (c === '1') p.classList.add('collapsed');
else p.classList.remove('collapsed');
}
} catch (e) {}
}
/** ?desktop_shell=1: eigener Oberflaechenmodus ohne normale Ansicht zu aendern (data-desktop-shell, Panels). */
function ensureDesktopShellAttr() {
try {
var sp = new URLSearchParams(window.location.search);
if (String(sp.get('desktop_shell') || '').trim() === '1') {
document.documentElement.setAttribute('data-desktop-shell', '1');
} else {
document.documentElement.removeAttribute('data-desktop-shell');
}
} catch (e) {}
}
function isDesktopShellMode() {
try {
return document.documentElement.getAttribute('data-desktop-shell') === '1';
} catch (e) {
return false;
}
}
/** DM-Kopfzeile: nur Kontaktname (Browser und Desktop-Huelle gleich). */
function formatDmChatTitle(peer) {
var p = String(peer || '').trim();
if (!p) return '';
return p;
}
/** Wie formatDmChatTitle, fuer innerHTML (Namen escapen). */
function formatDmChatTitleEscaped(peer) {
var p = String(peer || '').trim();
if (!p) return '';
return esc(p);
}
function messengerBackBtnShouldShow() {
return false;
}
function tasksCollapsedStorageKey() {
return isDesktopShellMode() ? 'empfang_tasks_collapsed_ds' : 'empfang_tasks_collapsed';
}
function restoreDesktopSidebarState() {
if (!isDesktopShellMode()) return;
var sb = document.getElementById('sidebar');
if (!sb) return;
try {
if (localStorage.getItem('empfang_sidebar_collapsed_ds') === '1') sb.classList.add('sidebar-ds-collapsed');
else sb.classList.remove('sidebar-ds-collapsed');
} catch (e) {}
updateSidebarNavHighlight();
}
function toggleDesktopSidebarCollapsed() {
if (!isDesktopShellMode()) return;
var sb = document.getElementById('sidebar');
if (!sb) return;
sb.classList.toggle('sidebar-ds-collapsed');
try {
localStorage.setItem('empfang_sidebar_collapsed_ds', sb.classList.contains('sidebar-ds-collapsed') ? '1' : '0');
} catch (e) {}
updateSidebarNavHighlight();
}
var LS_DESKTOP_SHELL_DCP_VISIBLE = 'desktopShellContextVisible';
function readDesktopShellContextRailVisible() {
try {
var v = localStorage.getItem(LS_DESKTOP_SHELL_DCP_VISIBLE);
if (v === '0' || v === 'false') return false;
return true;
} catch (e) {
return true;
}
}
function toggleDesktopShellContextRail() {
if (!isDesktopShellMode()) return;
try {
var vis = readDesktopShellContextRailVisible();
localStorage.setItem(LS_DESKTOP_SHELL_DCP_VISIBLE, vis ? '0' : '1');
} catch (e) {}
applyDesktopShellContextRail();
}
function applyDesktopShellContextRail() {
var nr = document.getElementById('nr-dcp-context');
var sb = document.getElementById('sidebar');
if (!isDesktopShellMode()) {
if (nr) nr.classList.remove('nr-active');
if (sb) sb.classList.remove('shell-dcp-off');
return;
}
var vis = readDesktopShellContextRailVisible();
if (sb) sb.classList.toggle('shell-dcp-off', !vis);
if (nr) {
nr.classList.toggle('nr-active', vis);
nr.setAttribute('aria-expanded', vis ? 'true' : 'false');
nr.title = vis
? 'Kontext / Textbausteine ausblenden (Panel in linker Spalte)'
: 'Kontext / Textbausteine einblenden (Panel in linker Spalte)';
}
}
function restoreDesktopContextPanelState() {
if (!isDesktopShellMode()) return;
var card = document.getElementById('dcp-panel-card');
var btn = document.getElementById('dcp-toggle-btn');
if (!card || !btn) return;
try {
var v = localStorage.getItem('empfang_dcp_collapsed_ds');
if (v === '0') {
card.classList.remove('dcp-panel-collapsed');
btn.setAttribute('aria-expanded', 'true');
} else {
card.classList.add('dcp-panel-collapsed');
btn.setAttribute('aria-expanded', 'false');
}
} catch (e) {}
}
function toggleDesktopContextPanel() {
if (!isDesktopShellMode()) return;
var card = document.getElementById('dcp-panel-card');
var btn = document.getElementById('dcp-toggle-btn');
if (!card || !btn) return;
card.classList.toggle('dcp-panel-collapsed');
var collapsed = card.classList.contains('dcp-panel-collapsed');
btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
try {
localStorage.setItem('empfang_dcp_collapsed_ds', collapsed ? '1' : '0');
} catch (e) {}
}
/** Zwischenablage an Eingabe anhaengen — kein Logging des Inhalts. */
function dcpPasteFromClipboard() {
if (!navigator.clipboard || !navigator.clipboard.readText) {
alert('Zwischenablage hier nicht direkt lesbar.\nBitte in das Eingabefeld mit Strg+V einf\u00fcgen.');
return;
}
navigator.clipboard.readText().then(function(t) {
var ta = document.getElementById('reply-text');
var chunk = (t || '').trim();
if (!ta || !chunk) return;
var cur = String(ta.value || '');
var sep = cur && !/\s$/.test(cur) ? '\n' : '';
ta.value = cur + sep + chunk;
try {
ta.dispatchEvent(new Event('input', {bubbles: true}));
} catch (eIn) {}
try {
autoGrowTextarea(ta);
} catch (eAg) {}
}).catch(function() {
alert('Kein Lesezugriff auf die Zwischenablage.\nBitte Strg+V im Eingabefeld verwenden.');
});
}
if (typeof window.__dcpAutocopyDone === 'undefined') window.__dcpAutocopyDone = Object.create(null);
if (typeof window.__shellPatientNr === 'undefined') window.__shellPatientNr = '';
if (typeof window.__shellPinselActive === 'undefined') window.__shellPinselActive = false;
if (typeof window.__shellPinselBridgeMode === 'undefined') window.__shellPinselBridgeMode = false;
function shellPinselGetUiVersion() {
try {
var el = document.getElementById('ui-version');
return el ? String(el.textContent || '').trim() : '';
} catch (eUv) {
return '';
}
}
function shellPinselDiagSet(msg) {
if (!isDesktopShellMode()) return;
var el = document.getElementById('shell-pinsel-diag');
if (el) el.textContent = msg || '';
}
function shellPinselProbeBridge() {
var py = false;
var ap = false;
var sp = false;
try {
py = !!window.pywebview;
} catch (e1) {}
try {
ap = !!(window.pywebview && window.pywebview.api);
} catch (e2) {}
try {
sp = !!(window.pywebview && window.pywebview.api &&
typeof window.pywebview.api.start_patient_nr_pick === 'function');
} catch (e3) {}
return {
pywebview: py,
api: ap,
start_pick: sp,
uiVer: shellPinselGetUiVersion()
};
}
function dcpAppendToReply(text) {
var chunk = String(text || '').trim();
if (!chunk) return false;
var ta = document.getElementById('reply-text');
if (!ta) return false;
var cur = String(ta.value || '').trim();
var next = cur ? (cur + '\n\n' + chunk) : chunk;
ta.value = next;
try {
ta.dispatchEvent(new Event('input', {bubbles: true}));
} catch (eIn) {}
try {
autoGrowTextarea(ta);
} catch (eAg) {}
return true;
}
function dcpFootFlash(id, msg) {
var el = document.getElementById(id);
if (!el) return;
el.textContent = msg || '';
if (msg) {
var t0 = msg;
setTimeout(function() {
try {
if (el && el.textContent === t0) el.textContent = '';
} catch (e1) {}
}, 2400);
}
}
function dcpWriteClipboard(txt, footId) {
var s = String(txt || '');
if (!s.trim()) {
dcpFootFlash(footId, 'Nichts zu kopieren.');
return;
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(s).then(function() {
dcpFootFlash(footId, 'Kopiert.');
}).catch(function() {
dcpClipboardFallbackExec(s, footId);
});
} else {
dcpClipboardFallbackExec(s, footId);
}
}
function dcpClipboardFallbackExec(s, footId) {
try {
var ta = document.createElement('textarea');
ta.value = s;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.focus();
ta.select();
var ok = document.execCommand('copy');
document.body.removeChild(ta);
dcpFootFlash(footId, ok ? 'Kopiert.' : 'Kopieren nicht m\u00f6glich.');
} catch (e) {
dcpFootFlash(footId, 'Kopieren nicht m\u00f6glich.');
}
}
function dcpTryClipboardQuiet(s) {
var t = String(s || '').trim();
if (!t) return;
if (navigator.clipboard && navigator.clipboard.writeText)
navigator.clipboard.writeText(t).catch(function() {});
}
function dcpInsertContextIntoReply(raw, emptyMsg, footId) {
var chunk = String(raw || '').trim();
if (!chunk) {
dcpFootFlash(footId, emptyMsg || 'Nichts geladen.');
return;
}
if (!document.getElementById('reply-text')) {
dcpFootFlash(footId, 'Kein Eingabefeld.');
return;
}
if (!dcpAppendToReply(chunk)) {
dcpFootFlash(footId, emptyMsg || 'Konnte nicht einf\u00fcgen.');
return;
}
dcpFootFlash(footId, 'In Nachricht eingef\u00fcgt.');
dcpTryClipboardQuiet(chunk);
}
function dcpCopyTherapy() {
var raw = (window.__dcpLastCtx && window.__dcpLastCtx.therapy_text) || '';
dcpInsertContextIntoReply(raw, 'Kein Therapietext geladen.', 'dcp-foot-ther');
}
function dcpCopyProc() {
var raw = (window.__dcpLastCtx && window.__dcpLastCtx.procedure_text) || '';
dcpInsertContextIntoReply(raw, 'Kein Procedere geladen.', 'dcp-foot-proc');
}
function shellPatientStripUiUpdate() {
var el = document.getElementById('shell-patient-value');
if (!el) return;
var n = String(window.__shellPatientNr || '').trim();
el.textContent = n ? n : '\u2014';
}
function shellClearPatientNr() {
window.__shellPatientNr = '';
shellPatientStripUiUpdate();
}
function shellExtractPatientNrFromText(text) {
var s = String(text || '').trim();
if (!s) return '';
var m = s.match(/\d{2,12}/);
if (m) return m[0];
var c = s.replace(/[\s\-\/\u00ad]+/g, '');
return /^\d{2,12}$/.test(c) ? c : '';
}
function shellPrependNrLineToReply(nn) {
var line = 'Nr.: ' + String(nn || '').trim();
if (!line || line === 'Nr.:') return;
var ta = document.getElementById('reply-text');
if (!ta) return;
var trimmed = String(ta.value || '').trim();
if (!trimmed) {
ta.value = line;
} else {
var esc = String(nn).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
try {
if (new RegExp('^Nr\\.?:\\s*' + esc + '(\\s|$)', 'im').test(trimmed.split(/\r?\n/, 1)[0])) return;
} catch (ere) {}
if (trimmed.indexOf(line) === 0) return;
ta.value = line + '\n\n' + trimmed;
}
try { ta.dispatchEvent(new Event('input', { bubbles: true })); } catch (e1) {}
try { autoGrowTextarea(ta); } catch (e2) {}
}
function shellPinselDeactivate(fromSuccess) {
window.__shellPinselActive = false;
window.__shellPinselBridgeMode = false;
document.body.classList.remove('shell-pinsel-active');
try { document.removeEventListener('mouseup', shellPinselOnMouseUp, true); } catch (eRm) {}
try { document.removeEventListener('dblclick', shellPinselOnDblClick, true); } catch (eRm2) {}
var ih = document.getElementById('shell-pinsel-hint');
if (ih && !fromSuccess) {
ih.textContent = '';
ih.dataset.sticky = '';
}
}
function shellPinselTryConsumeSelection() {
if (!window.__shellPinselActive) return false;
var txt = '';
try {
var sel = window.getSelection();
txt = sel ? String(sel.toString() || '').trim() : '';
} catch (eSel) {}
if (!txt) return false;
var nn = shellExtractPatientNrFromText(txt);
if (!nn) {
var ih = document.getElementById('shell-pinsel-hint');
if (ih) {
ih.textContent = 'Keine g\u00fcltige Patientennummer erkannt.';
ih.dataset.sticky = '1';
setTimeout(function() {
try {
ih.dataset.sticky = '';
if (window.__shellPinselActive) ih.textContent = 'Patientennummer markieren oder doppelklicken \u2026';
else ih.textContent = '';
} catch (eTo) {}
}, 2600);
}
return false;
}
window.__shellPatientNr = nn;
shellPatientStripUiUpdate();
shellPrependNrLineToReply(nn);
var ih2 = document.getElementById('shell-pinsel-hint');
if (ih2) {
ih2.textContent = 'Nr. \u00fcbernommen.';
ih2.dataset.sticky = '';
}
shellPinselDeactivate(true);
return true;
}
function shellPinselOnMouseUp(ev) {
if (!window.__shellPinselActive) return;
if (window.__shellPinselBridgeMode) return;
if (window.__shellPinselArmAt && Date.now() - window.__shellPinselArmAt < 350) return;
var t = ev.target;
if (t && t.closest && t.closest('#shell-patient-strip')) return;
setTimeout(function() { shellPinselTryConsumeSelection(); }, 25);
}
function shellPinselOnDblClick(ev) {
if (!window.__shellPinselActive) return;
if (window.__shellPinselBridgeMode) return;
if (window.__shellPinselArmAt && Date.now() - window.__shellPinselArmAt < 350) return;
var t = ev.target;
if (t && t.closest && t.closest('#shell-patient-strip')) return;
setTimeout(function() { shellPinselTryConsumeSelection(); }, 45);
}
function shellPinselUsesPywebviewBridge() {
try {
return !!(window.pywebview && window.pywebview.api &&
typeof window.pywebview.api.start_patient_nr_pick === 'function');
} catch (ePw) {
return false;
}
}
/** Abschluss eines globalen Pick-Zyklus (Clipboard) — Erfolgs-Hinweis nicht loeschen */
function shellFinalizeGlobalPickMechanismUi() {
window.__shellPinselBridgeMode = false;
if (!window.__shellPinselActive) return;
shellPinselDeactivate(false);
try {
shellPinselDiagSet('Pinsel: bereit');
} catch (eFin) {}
}
/** Volles UI-Reset (Abbruch, Fehler beim Start), inkl. Hinweis-Leerung */
function shellGlobalPinselResetUi() {
window.__shellPinselBridgeMode = false;
shellPinselDeactivate(false);
try {
shellPinselDiagSet('Pinsel: bereit');
} catch (eGlo) {}
}
function shellReceivePatientNrPickInvalid() {
try {
shellPinselDiagSet('Pinsel: Fehler: validated_rejected');
} catch (eDinv) {}
var ih = document.getElementById('shell-pinsel-hint');
if (ih) {
ih.textContent = 'Keine g\u00fcltige Patientennummer erkannt.';
ih.dataset.sticky = '1';
setTimeout(function() {
try {
ih.dataset.sticky = '';
ih.textContent = '';
} catch (eIh) {}
}, 2600);
}
window.__shellPinselBridgeMode = false;
shellPinselDeactivate(false);
}
/** Callback aus Desktop-Python (JSON-sicher uebergeben) */
function shellReceivePatientNrFromDesktop(nn) {
try {
if (window.console && console.debug) {
console.debug('[pinsel] shellReceivePatientNrFromDesktop called');
}
} catch (eDbg) {}
try {
shellPinselDiagSet('Pinsel: Nummer uebernommen');
} catch (eDn) {}
var n = String(nn || '').trim();
window.__shellPinselBridgeMode = false;
if (!n) {
shellPinselDeactivate(false);
return;
}
window.__shellPatientNr = n;
shellPatientStripUiUpdate();
shellPrependNrLineToReply(n);
var ih2 = document.getElementById('shell-pinsel-hint');
if (ih2) {
ih2.textContent = 'Nr. \u00fcbernommen.';
ih2.dataset.sticky = '';
}
shellPinselDeactivate(true);
}
/** Fallback: Nur Textauswahl innerhalb dieser Seite */
function shellPinselArmDocumentSelectionOnly() {
if (!isDesktopShellMode()) return;
window.__shellPinselArmAt = Date.now();
window.__shellPinselActive = true;
window.__shellPinselBridgeMode = false;
document.body.classList.add('shell-pinsel-active');
var ih = document.getElementById('shell-pinsel-hint');
if (ih) {
ih.textContent = 'Patientennummer markieren oder doppelklicken \u2026';
ih.dataset.sticky = '';
}
document.addEventListener('mouseup', shellPinselOnMouseUp, true);
document.addEventListener('dblclick', shellPinselOnDblClick, true);
}
function toggleShellPinsel() {
if (!isDesktopShellMode()) return;
var pb = shellPinselProbeBridge();
try {
if (window.console && console.debug) {
console.debug('[pinsel] ui-version=' + (pb.uiVer || '(missing)'));
console.debug('[pinsel] pywebview present ' + pb.pywebview);
console.debug('[pinsel] api present ' + pb.api);
console.debug('[pinsel] start_patient_nr_pick present ' + pb.start_pick);
}
} catch (eLog0) {}
if (window.__shellPinselActive) {
if (window.__shellPinselBridgeMode) {
try {
if (window.pywebview && window.pywebview.api &&
typeof window.pywebview.api.cancel_patient_nr_pick === 'function') {
window.pywebview.api.cancel_patient_nr_pick();
}
} catch (eC) {
window.__shellPinselBridgeMode = false;
shellPinselDeactivate(false);
try {
shellPinselDiagSet('Pinsel: Fehler: cancel_bridge');
} catch (eDg) {}
}
return;
}
shellPinselDeactivate(false);
try {
shellPinselDiagSet('Pinsel: bereit');
} catch (eDg2) {}
return;
}
if (pb.start_pick) {
shellPinselDiagSet('Pinsel: Bridge verfuegbar — Python starten …');
} else {
shellPinselDiagSet('Pinsel: WebView-Bridge fehlt (In-Page-Fallback aktiv)');
shellPinselArmDocumentSelectionOnly();
return;
}
window.__shellPinselArmAt = Date.now();
window.__shellPinselActive = true;
window.__shellPinselBridgeMode = true;
document.body.classList.add('shell-pinsel-active');
var ihn = document.getElementById('shell-pinsel-hint');
if (ihn) {
ihn.textContent = 'Patientennummer woanders markieren oder doppelklicken \u2026';
ihn.dataset.sticky = '';
}
try {
var pr = window.pywebview.api.start_patient_nr_pick();
if (pr && typeof pr.then === 'function') {
pr.then(function(r) {
try {
if (!r) {
shellPinselDiagSet('Pinsel: Fehler: api_leer');
return;
}
if (r.ok === false && r.reason === 'no_input_hook') {
shellPinselDiagSet('Pinsel: Fehler: unavailable');
shellPinselArmDocumentSelectionOnly();
return;
}
if (r.ok === false && r.reason === 'no_window') {
shellPinselDiagSet('Pinsel: Fehler: no_window');
return;
}
if (r.cancelled) {
shellPinselDiagSet('Pinsel: abgebrochen');
return;
}
shellPinselDiagSet('Pinsel: Python aktiv — Auswahl extern …');
} catch (eR) {}
}).catch(function() {
window.__shellPinselBridgeMode = false;
shellPinselDeactivate(false);
try {
shellPinselDiagSet('Pinsel: Fehler: api_promise');
} catch (ePe) {}
});
}
} catch (eStart) {
window.__shellPinselBridgeMode = false;
try {
shellPinselDiagSet('Pinsel: Fehler: api_throw');
} catch (eSt) {}
shellPinselArmDocumentSelectionOnly();
return;
}
}
async function refreshDesktopShellContextFromServer(runAutocopy) {
if (!isDesktopShellMode() || !currentSession) return;
try {
var r = await apiFetch(API_BASE + '/shell/context/me');
if (!r || !r.ok) return;
var d = await r.json();
if (!d || d.success === false) return;
window.__dcpLastCtx = {
therapy_text: String(d.therapy_text || '').trim(),
procedure_text: String(d.procedure_text || '').trim(),
therapy_autocopy: !!d.therapy_autocopy,
procedure_autocopy: !!d.procedure_autocopy,
context_id: String(d.context_id || '').trim(),
};
var pt = document.getElementById('dcp-preview-ther');
var pp = document.getElementById('dcp-preview-proc');
var nt = window.__dcpLastCtx.therapy_text;
var np = window.__dcpLastCtx.procedure_text;
if (pt) {
pt.textContent = nt;
}
if (pp) {
pp.textContent = np;
}
var cbT = document.getElementById('dcp-cb-ther');
var cbP = document.getElementById('dcp-cb-proc');
if (cbT) cbT.checked = !!d.therapy_autocopy;
if (cbP) cbP.checked = !!d.procedure_autocopy;
var cid = window.__dcpLastCtx.context_id;
if (runAutocopy && cid && !window.__dcpAutocopyDone[cid]) {
var ut = document.getElementById('dcp-cb-ther');
var up = document.getElementById('dcp-cb-proc');
var wantT = !!(ut && ut.checked && nt);
var wantP = !!(up && up.checked && np);
var parts = [];
if (wantT) parts.push('Therapie:\n' + nt);
if (wantP) parts.push('Procedere:\n' + np);
if (parts.length) {
dcpAppendToReply(parts.join('\n\n'));
}
window.__dcpAutocopyDone[cid] = true;
}
} catch (e) {
try {
if (isDesktopShellMode() && window.console && console.debug) {
console.debug('[Empfang] Kontext laden fehlgeschlagen');
}
} catch (e2) {}
}
}
function applyDesktopShellChrome() {
if (!isDesktopShellMode()) {
try {
if (window.__shellPinselActive) shellPinselDeactivate(false);
} catch (eSd) {}
return;
}
try {
var nt = document.getElementById('nr-tasks');
if (nt) nt.style.removeProperty('display');
} catch (eRm) {}
restoreDesktopContextPanelState();
updateSidebarNavHighlight();
try {
shellPatientStripUiUpdate();
} catch (ePu) {}
try {
shellPinselDiagSet('Pinsel: bereit');
} catch (eDiag0) {}
}
function setView(view) {
activeView = view;
activeThreadId = null;
applyActiveConversationFromSession();
activeConversation.mode = 'all';
activeConversation.targetUserId = '';
activeConversation.targetUserKey = '';
activeConversation.targetDisplayName = '';
syncLegacyDmFromActiveConversation();
dmWantsThreadList = false;
updateSidebarNavHighlight();
lastDataHash = '';
clearLastDirectPeerStorage();
loadMessages();
}
function selectDmPeer(displayName, peerUserIdOpt) {
if (!currentSession) {
showLoginOverlay();
return;
}
applyActiveConversationFromSession();
var raw = String(displayName || '').trim();
var peer = raw;
try {
peer = decodeURIComponent(raw);
} catch (e) {}
peer = peer.trim();
if (!peer) return;
if (peer === currentSession.display_name) {
clearLastDirectPeerStorage();
activeConversation.mode = 'all';
activeConversation.targetUserId = '';
activeConversation.targetUserKey = '';
activeConversation.targetDisplayName = '';
syncLegacyDmFromActiveConversation();
activeThreadId = null;
dmWantsThreadList = false;
updateSidebarNavHighlight();
lastDataHash = '';
loadMessages();
return;
}
activeConversation.mode = 'direct';
activeConversation.targetDisplayName = peer;
activeConversation.targetUserId = String(peerUserIdOpt || peerUidForDisplay(peer) || '').trim();
activeConversation.targetUserKey = activeConversation.targetUserId;
syncLegacyDmFromActiveConversation();
activeThreadId = null;
dmWantsThreadList = false;
updateSidebarNavHighlight();
lastDataHash = '';
try {
console.debug('[Empfang] activeConversation (DM)', JSON.stringify({
mode: activeConversation.mode,
practiceId: activeConversation.practiceId,
me: activeConversation.currentUserId,
peer: activeConversation.targetUserId,
convKeyGuess: dmDirectConvKey(activeConversation.currentUserId, activeConversation.targetUserId)
}));
} catch(eDbg) {}
loadMessages();
persistLastDirectPeerSnapshot();
}
function bindSidebarUserClicks() {
var host = document.getElementById('sb-users');
if (!host || host._azaSbBound) return;
host._azaSbBound = true;
host.addEventListener('click', function(ev) {
var row = ev.target.closest('.sb-user[data-peer]');
if (!row) return;
if (row.getAttribute('data-self') === '1') {
try {
var d = document.getElementById('dm-diag');
if (d) d.textContent = 'Selbstchat nicht moeglich.';
} catch (e) {}
return;
}
var uid = row.getAttribute('data-peer-uid') || '';
selectDmPeer(row.getAttribute('data-peer'), uid);
});
}
function renderSidebarUsers() {
var el = document.getElementById('sb-users');
if (!practiceUsers.length) {
el.innerHTML = '<div style="padding:4px 18px;font-size:.78rem;color:#bbb">Keine Benutzer</div>';
updateSidebarNavHighlight();
return;
}
var sorted = practiceUsers.slice().sort(function(a, b) {
var ra = userSortRank(a);
var rb = userSortRank(b);
if (ra !== rb) return ra - rb;
return localeCompareDisplayName(a, b);
});
el.innerHTML = sorted.map(function(u) {
var name = u.display_name || '';
var isMe = isPracticeUserSelf(u);
var online = userPresenceEffectiveOnline(u);
var dotCls = online ? 'online' : 'offline';
var presActive = !!online;
var uidAttr = escAttr(getUserStableId(u));
var uidHtml = uidAttr ? ' data-peer-uid="' + uidAttr + '"' : '';
var clsExtra = (isMe ? ' me sb-user-self sb-user-disabled' : '') +
(presActive ? ' sb-user-presence-active' : '');
var title = isMe ? 'Das sind Sie selbst (Selbstchat ist nicht m\u00f6glich)' : ('Direktchat mit ' + esc(name).replace(/"/g, '&quot;'));
return '<div class="sb-user' + clsExtra + '" data-peer="' + encodeURIComponent(name) + '"' + uidHtml +
(isMe ? ' data-self="1"' : '') + ' title="' + title + '">' +
'<span class="sb-dot ' + dotCls + '"></span>' +
'<span class="sb-user-name">' + esc(name) + '</span>' +
(u.role ? '<span class="sb-group-tag">' + esc(u.role) + '</span>' : '') +
(isMe ? '<span class="sb-self-tag" style="margin-left:auto;font-size:.65rem;color:#8a9aaa">Sie</span>' : '') +
'</div>';
}).join('');
updateSidebarNavHighlight();
}
function updateSbMe() {
var nameEl = document.getElementById('sb-me-name');
var roleEl = document.getElementById('sb-me-role');
if (currentSession) {
nameEl.textContent = 'Angemeldet als ' + String(currentSession.display_name || '').trim();
roleEl.textContent = currentSession.role || '';
} else {
nameEl.textContent = 'Nicht angemeldet';
roleEl.textContent = '';
}
}
function refreshSettingsUserSection() {
var box = document.getElementById('settings-user-mgmt');
if (!box) return;
var r = currentSession && currentSession.role ? String(currentSession.role).toLowerCase() : '';
var ok = r === 'admin' || r === 'empfang';
box.style.display = ok ? 'block' : 'none';
var oa = document.getElementById('adu-opt-admin');
var oe = document.getElementById('adu-opt-empfang');
if (oa) oa.hidden = r !== 'admin';
if (oe) oe.hidden = r !== 'admin';
}
function openAddUserDialog() {
if (!currentSession) return;
var role = currentSession.role ? String(currentSession.role).toLowerCase() : '';
if (role !== 'admin' && role !== 'empfang') return;
var pan = document.getElementById('add-user-overlay');
if (!pan) return;
document.getElementById('adu-name').value = '';
document.getElementById('adu-pass').value = '';
document.getElementById('adu-pass2').value = '';
document.getElementById('adu-error').textContent = '';
document.getElementById('adu-role').value = 'mpa';
pan.classList.remove('hidden');
setTimeout(function() { try { document.getElementById('adu-name').focus(); } catch (ea) {} }, 50);
}
function closeAddUserDialog() {
var pan = document.getElementById('add-user-overlay');
if (pan) pan.classList.add('hidden');
}
async function submitSecureAddUser() {
var err = document.getElementById('adu-error');
var name = (document.getElementById('adu-name').value || '').trim();
var role = (document.getElementById('adu-role').value || 'mpa').trim();
var p1 = document.getElementById('adu-pass').value || '';
var p2 = document.getElementById('adu-pass2').value || '';
if (!name) { err.textContent = 'Bitte einen Namen eingeben.'; return; }
if (p1.length < 4) { err.textContent = 'Passwort mindestens 4 Zeichen.'; return; }
if (p1 !== p2) { err.textContent = 'Passw\u00f6rter stimmen nicht \u00fcberein.'; return; }
err.textContent = '';
try {
var res = await apiFetch(API_BASE + '/users', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'add_secure',
name: name,
role: role,
password: p1,
password_repeat: p2
})
});
var dj = await res.json().catch(function() { return {}; });
if (!res.ok) {
var msg = dj.detail;
if (typeof msg !== 'string') {
if (Array.isArray(msg) && msg[0] && msg[0].msg) msg = msg.map(function(x) { return x.msg; }).join(' ');
else msg = dj.message || '';
}
err.textContent = msg || 'Anlage fehlgeschlagen.';
return;
}
closeAddUserDialog();
await loadUsersFromServer();
notifyOtherTabs();
} catch (eAu) {
err.textContent = 'Verbindungsfehler.';
}
}
/* ===================================================================
USERS (server only)
=================================================================== */
async function loadUsersFromServer() {
try {
var r = await apiFetch(API_BASE + '/users');
var d = await r.json();
practiceUsers = d.users_full || [];
renderSidebarUsers();
if (activeConversation.mode === 'direct' && activeConversation.targetDisplayName) {
var looked = peerUidForDisplay(activeConversation.targetDisplayName);
if (looked && looked !== activeConversation.targetUserId) {
activeConversation.targetUserId = looked;
activeConversation.targetUserKey = looked;
syncLegacyDmFromActiveConversation();
lastDataHash = '';
loadMessages();
}
}
if (lastPracticeInfoForHint) {
updateSbUsersPracticeHint(
String(lastPracticeInfoForHint.practice_id || '').trim(),
String(lastPracticeInfoForHint.practice_name || '').trim()
);
}
tryRestoreLastDirectPeerOnce();
} catch(e) {}
}
/* ===================================================================
MESSAGES
=================================================================== */
async function loadMessages() {
try {
if (!currentSession) return;
applyActiveConversationFromSession();
var mySeq = ++_loadMessagesSeq;
var pidQ = String((currentSession.practice_id || activeConversation.practiceId || '')).trim();
var meUid = String((currentSession.user_id || '')).trim();
var d;
var msgs = [];
var convKey = '';
if (activeConversation.mode === 'direct') {
var peerUid = String(activeConversation.targetUserId || '').trim();
if (!pidQ || !meUid || !peerUid || meUid === peerUid) {
if (mySeq !== _loadMessagesSeq) return;
allMessages = [];
_dmV2State.lastLoadCount = 0;
_dmV2State.lastConvKey = '';
dmDiagWrite('LOAD_BLOCKED');
renderView();
return;
}
var qs2 = 'practice_id=' + encodeURIComponent(pidQ) +
'&sender_user_id=' + encodeURIComponent(meUid) +
'&recipient_user_id=' + encodeURIComponent(peerUid);
console.debug('[Empfang] AZA_CHAT_LOAD mode=direct practice=' + pidQ +
' me=' + meUid + ' peer=' + peerUid);
var r2 = await apiFetch(API_BASE + '/dm/conversation?' + qs2);
if (!r2.ok) {
if (mySeq !== _loadMessagesSeq) return;
var errTxt = '';
try { var ej = await r2.json(); errTxt = (ej && (ej.detail || ej.error)) || ''; } catch (_e) {}
console.warn('[Empfang] dm/conversation HTTP ' + r2.status + ' ' + errTxt);
allMessages = [];
_dmV2State.lastLoadCount = 0;
dmDiagWrite('LOAD_HTTP_' + r2.status);
renderView();
return;
}
d = await r2.json();
msgs = d.messages || [];
convKey = d.conversation_key || '';
_dmV2State.lastConvKey = convKey;
_dmV2State.lastLoadCount = msgs.length;
console.debug('[Empfang] AZA_CHAT_LOAD count=' + msgs.length + ' conv=' + convKey);
} else {
var audienceParam = '';
var meParam = (currentSession.display_name || '').trim();
var qs = 'audience=' + encodeURIComponent(audienceParam);
if (meParam) qs += '&me=' + encodeURIComponent(meParam);
if (meUid) qs += '&me_user_id=' + encodeURIComponent(meUid);
if (pidQ) qs += '&practice_id=' + encodeURIComponent(pidQ);
console.debug('[Empfang] loadConversation legacy mode=all (read-only legacy view)');
var r = await apiFetch(API_BASE + '/conversation?' + qs);
d = await r.json();
msgs = d.messages || [];
_dmV2State.lastConvKey = '';
_dmV2State.lastLoadCount = msgs.length;
}
dmDiagWrite('LOAD_OK');
if (mySeq !== _loadMessagesSeq) return;
var raw = JSON.stringify({
aud: audienceParam,
tick: d.tick || 0,
msgs: msgs
});
if (raw === lastDataHash) return;
lastDataHash = raw;
allMessages = msgs;
allMessages.forEach(function(m) { if (!m.thread_id) m.thread_id = m.id; });
buildThreads();
var currentIds = new Set(allThreads.map(function(t){ return t.tid; }));
var hasNewDesktop = false;
currentIds.forEach(function(tid) {
if (!previousThreadIds.has(tid)) {
var t = allThreads.find(function(th){ return th.tid === tid; });
if (t && !(t.root.absender || '').includes('Empfang')) hasNewDesktop = true;
}
});
if (previousThreadIds.size > 0 && hasNewDesktop && !justSentReply) {
playTone(currentToneIdx);
startRepeatTone();
}
/* Pop-up bei echter neuer Fremd-Nachricht (nicht eigene, nicht alte) */
try {
var meName = String((currentSession && currentSession.display_name) || '').trim();
var meNameLow = meName.toLowerCase();
var meUidStr = String((currentSession && currentSession.user_id) || '').trim();
var sentMsgId = String(_dmV2State.lastSentMsgId || '').trim();
var nowMsgIds = new Set();
var newIncoming = null;
(allMessages || []).forEach(function(m) {
if (!m || !m.id) return;
nowMsgIds.add(String(m.id));
});
if (previousMsgIds.size > 0 && !justSentReply) {
for (var i = 0; i < (allMessages || []).length; i++) {
var mm = allMessages[i];
if (!mm || !mm.id) continue;
var midS = String(mm.id);
if (previousMsgIds.has(midS)) continue;
if (sentMsgId && midS === sentMsgId) continue;
var ex = (mm.extras && typeof mm.extras === 'object') ? mm.extras : null;
var sUid = ex ? String(ex.sender_user_id || '').trim() : '';
if (meUidStr && sUid && sUid === meUidStr) continue;
var absRaw = String(mm.absender || '').trim();
var absCore = absRaw.split('(')[0].trim().toLowerCase();
if (meNameLow && (absCore === meNameLow ||
(meName && absRaw.indexOf(meName) === 0))) continue;
newIncoming = mm;
break;
}
}
if (newIncoming) notifyNewIncomingMessage(newIncoming);
previousMsgIds = nowMsgIds;
} catch (_eN) {}
justSentReply = false;
previousThreadIds = currentIds;
var openCount = countGeneralOpenThreads();
document.getElementById('count-badge').textContent = openCount + ' offen';
var ob = document.getElementById('sb-badge-open');
if (ob) ob.textContent = String(openCount || '');
renderView();
} catch (e) {
if (e.message === 'auth') return;
var cs = document.getElementById('conversation-scroll');
if (cs) cs.innerHTML =
'<div class="conv-empty">Backend nicht erreichbar.<br>Bitte Verbindung pr&uuml;fen.</div>';
}
}
function buildThreads() {
var threadMap = {};
allMessages.forEach(function(m) {
if (!threadMap[m.thread_id]) threadMap[m.thread_id] = [];
threadMap[m.thread_id].push(m);
});
allThreads = [];
Object.keys(threadMap).forEach(function(tid) {
var arr = threadMap[tid].slice().sort(function(a, b) {
var sa = a.empfangen || a.zeitstempel || '';
var sb = b.empfangen || b.zeitstempel || '';
var tsa = parseEmpfangChatInstantMs(sa);
var tsb = parseEmpfangChatInstantMs(sb);
if (isFinite(tsa) && isFinite(tsb) && tsa !== tsb) return tsa - tsb;
return sa.localeCompare(sb);
});
var root = arr.find(function(m){ return m.id === tid; }) || arr[0];
var replies = arr.filter(function(m){ return m.id !== root.id; });
var lastTime = arr[arr.length-1].empfangen || arr[arr.length-1].zeitstempel || '';
allThreads.push({ tid:tid, root:root, replies:replies, lastTime:lastTime, all:arr });
});
allThreads.sort(function(a, b) {
var ao = a.root.status !== 'erledigt' ? 1 : 0;
var bo = b.root.status !== 'erledigt' ? 1 : 0;
if (ao !== bo) return bo - ao;
var ta = parseEmpfangChatInstantMs(a.lastTime);
var tb = parseEmpfangChatInstantMs(b.lastTime);
if (isFinite(ta) && isFinite(tb) && ta !== tb) {
return sortNewestFirst ? (tb - ta) : (ta - tb);
}
return sortNewestFirst
? b.lastTime.localeCompare(a.lastTime)
: a.lastTime.localeCompare(b.lastTime);
});
}
/* ===================================================================
MESSENGER-VERLAUF (eine zusammenhaengende Timeline, kein Kartensammelsurium)
=================================================================== */
function messengerLayoutActive() {
return !!(currentSession);
}
function messagesFilteredForMessenger(msgs) {
var slice = msgs.slice();
if (activeDmPeer) return slice;
if (activeView === 'archiv') {
return slice.filter(function(m) { return m.status === 'erledigt'; });
}
return slice.filter(function(m) { return m.status !== 'erledigt'; });
}
var AZA_DEFAULT_PRACTICE_TZ = 'Europe/Zurich';
function empfangChatTimeZoneId() {
try {
var d = lastPracticeInfoForHint;
var tz = d && String(d.practice_timezone || d.timezone || '').trim();
if (tz) return tz;
} catch (e) {}
return AZA_DEFAULT_PRACTICE_TZ;
}
function parseEmpfangChatInstantMs(raw) {
var s = String(raw || '').trim();
if (!s) return NaN;
var norm = s.replace(' ', 'T');
var hasTz = norm.indexOf('Z') >= 0 || /[+\-]\d{2}:\d{2}$/.test(norm);
if (hasTz) {
var tzParsed = Date.parse(norm);
return isFinite(tzParsed) ? tzParsed : NaN;
}
var m = s.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}(?::\d{2})?)/);
if (m) {
var u = Date.parse(m[1] + 'T' + m[2] + 'Z');
if (isFinite(u)) return u;
}
var fb = Date.parse(norm);
return isFinite(fb) ? fb : NaN;
}
function formatEmpfangChatClockFromMs(ms) {
if (!isFinite(ms)) return '';
try {
return new Intl.DateTimeFormat('de-CH', {
timeZone: empfangChatTimeZoneId(),
hour: '2-digit',
minute: '2-digit',
hour12: false
}).format(new Date(ms));
} catch (e) {
var d = new Date(ms);
var h = d.getHours();
var mi = d.getMinutes();
return (h < 10 ? '0' : '') + h + ':' + (mi < 10 ? '0' : '') + mi;
}
}
function formatEmpfangChatClockHHMM(raw) {
return formatEmpfangChatClockFromMs(parseEmpfangChatInstantMs(raw));
}
function formatMessengerDayKeyFromMs(ms) {
if (!isFinite(ms)) return '';
try {
var parts = new Intl.DateTimeFormat('en-CA', {
timeZone: empfangChatTimeZoneId(),
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).formatToParts(new Date(ms));
var y = '';
var mo = '';
var da = '';
for (var ip = 0; ip < parts.length; ip++) {
var p = parts[ip];
if (p.type === 'year') y = p.value;
if (p.type === 'month') mo = p.value;
if (p.type === 'day') da = p.value;
}
if (y && mo && da) return y + '-' + mo + '-' + da;
} catch (e) {}
return '';
}
function formatEmpfangChatDateTimeLong(raw) {
var ms = parseEmpfangChatInstantMs(raw);
if (!isFinite(ms)) return String(raw || '');
try {
return new Intl.DateTimeFormat('de-CH', {
timeZone: empfangChatTimeZoneId(),
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
}).format(new Date(ms));
} catch (e) {
return String(raw || '');
}
}
function formatThreadListTimeDisplay(raw) {
var ms = parseEmpfangChatInstantMs(raw);
var st = String(raw || '');
if (!isFinite(ms)) {
var timeF = st.slice(11, 16) || '';
var dateF = st.slice(0, 10) || '';
var today = new Date().toISOString().slice(0, 10);
return dateF === today ? timeF : (dateF.slice(5) + ' ' + timeF);
}
var todayKey = formatMessengerDayKeyFromMs(Date.now());
var msgKey = formatMessengerDayKeyFromMs(ms);
var timeStr = formatEmpfangChatClockFromMs(ms);
if (msgKey === todayKey) return timeStr;
return msgKey.slice(5) + ' ' + timeStr;
}
function formatMessengerDayKey(iso) {
return formatMessengerDayKeyFromMs(parseEmpfangChatInstantMs(iso));
}
function formatMessengerDateLabel(iso) {
var ms = parseEmpfangChatInstantMs(iso);
if (!isFinite(ms)) return '';
var msgKey = formatMessengerDayKeyFromMs(ms);
var todayKey = formatMessengerDayKeyFromMs(Date.now());
if (msgKey === todayKey) return 'Heute';
var yestKey = formatMessengerDayKeyFromMs(Date.now() - 86400000);
if (msgKey === yestKey) return 'Gestern';
try {
var parts = new Intl.DateTimeFormat('de-CH', {
timeZone: empfangChatTimeZoneId(),
day: 'numeric',
month: 'numeric',
year: 'numeric'
}).formatToParts(new Date(ms));
var da = '';
var mo = '';
var ye = '';
for (var ip = 0; ip < parts.length; ip++) {
var p = parts[ip];
if (p.type === 'day') da = p.value;
if (p.type === 'month') mo = p.value;
if (p.type === 'year') ye = p.value;
}
if (da && mo && ye) return da + '.' + mo + '.' + ye;
} catch (e) {}
return '';
}
function messengerChronoSorted(msgs) {
return msgs.slice().sort(function(a, b) {
var sa = a.empfangen || a.zeitstempel || '';
var sb = b.empfangen || b.zeitstempel || '';
var ta = parseEmpfangChatInstantMs(sa);
var tb = parseEmpfangChatInstantMs(sb);
if (isFinite(ta) && isFinite(tb) && ta !== tb) return ta - tb;
return sa.localeCompare(sb);
});
}
function messengerAnchorMessageId() {
var viewMsgs = messengerChronoSorted(messagesFilteredForMessenger(allMessages));
if (!viewMsgs.length) return null;
var last = viewMsgs[viewMsgs.length - 1];
return last.id || null;
}
function messengerBodyStructuredHtml(mm) {
var parts = '';
function block(label, txt) {
if (!txt || !String(txt).trim()) return '';
return '<div class="mseg-block"><span class="mseg-lbl">' + esc(label) + '</span>' +
'<div class="mseg-txt">' + esc(String(txt)) + '</div></div>';
}
parts += block('Therapieplan', mm.therapieplan);
parts += block('Procedere', mm.procedere);
parts += block('Medikamente', mm.medikamente);
var rawKom = (mm.kommentar || '').trim();
var hasKb = rawKom && rawKom !== '\u200b';
if (hasKb) {
parts += '<div class="mseg-txt messenger-kom">' + esc(rawKom) + '</div>';
} else if (!parts && mm.extras && mm.extras.attachments && mm.extras.attachments.length) {
var att = mm.extras.attachments;
var onlyAud = att.every(attachmentIsAudio);
parts += '<div class="mseg-hint">\u200b ' + (onlyAud ? 'Sprachmemo siehe unten.' : 'Anhang siehe unten.') + '</div>';
}
return parts;
}
function renderMessengerConversation() {
var c = document.getElementById('conversation-scroll');
var wrap = document.getElementById('chat-input-wrap');
var backBtn = document.getElementById('ctb-back');
var titleEl = document.getElementById('ctb-title');
var actionsEl = document.getElementById('ctb-actions');
var inputBar = document.getElementById('chat-input-bar');
if (!c || !currentSession) return;
_azaMsgLookup = {};
if (wrap) wrap.classList.remove('no-thread-input');
if (inputBar) {
if (activeDmPeer) inputBar.classList.add('visible');
else {
inputBar.classList.remove('visible');
if (wrap) wrap.classList.add('no-thread-input');
}
}
if (titleEl) {
if (activeDmPeer) titleEl.textContent = formatDmChatTitle(activeDmPeer);
else titleEl.textContent = 'Bitte Benutzer w\u00e4hlen';
}
if (backBtn) {
backBtn.classList.toggle('visible', messengerBackBtnShouldShow());
}
if (actionsEl) actionsEl.innerHTML = '';
var filt = messengerChronoSorted(messagesFilteredForMessenger(allMessages));
function registerTaskMeta(mm, chatLine) {
var rootBodyTxt = buildRootTaskBodyText(mm);
var tid = mm.thread_id || mm.id || '';
var asg = suggestAssigneeForTask(mm.absender || '', activeDmPeer || '');
_azaMsgLookup[mm.id] = {
msgId: mm.id,
threadId: tid,
senderLabel: senderDisplayCore(mm.absender || '') || '-',
when: formatEmpfangChatDateTimeLong(mm.zeitstempel || mm.empfangen || ''),
rawBody: rootBodyTxt,
assignGuess: asg,
chatLabel: chatLine
};
}
var chatCtx = activeDmPeer ? ('Direktchat mit ' + activeDmPeer) :
(activeView === 'archiv' ? 'Archiv · Allgemein' : 'Allgemein');
var lastDayKey = '';
if (!filt.length) {
var eh = '';
if (activeDmPeer) eh = 'Mit ' + esc(activeDmPeer) + ' gibt es in diesem Direktchat noch keine Nachrichten. Unten direkt schreiben.';
else eh = 'Bitte links einen Benutzer ausw\u00e4hlen, um einen Direktchat zu starten.';
c.innerHTML = '<div class="conv-empty messenger-empty">' + eh + '</div>';
bindConversationMsgActionsOnce();
return;
}
var myName = String(currentSession.display_name || '').trim();
var myLow = myName.toLowerCase();
var html = '<div class="conv-msg-stack messenger-stream messenger-timeline">';
filt.forEach(function(m) {
var tsFull = m.empfangen || m.zeitstempel || '';
var dk = formatMessengerDayKey(tsFull);
if (dk && dk !== lastDayKey) {
lastDayKey = dk;
html += '<div class="messenger-date-sep" role="presentation"><span>' +
esc(formatMessengerDateLabel(tsFull)) + '</span></div>';
}
registerTaskMeta(m, chatCtx);
var senderLine = senderDisplayCore(m.absender || '') || '\u2013';
var sCoreLow = senderLine.toLowerCase();
var outgoing = sCoreLow === myLow ||
(myName && (m.absender || '').trim().indexOf(myName) === 0);
var align = outgoing ? 'align-right' : 'align-left';
var frm = outgoing ? 'from-me' : 'from-other';
var ackCls = messageHasChatAck(m) ? ' conv-msg-chat-ack' : '';
var timeShort = formatEmpfangChatClockHHMM(tsFull) || tsFull.slice(0, 10) || '';
var senderSmall = outgoing ? '' : ('<span class="conv-msg-sender">' + esc(senderLine) + '</span>');
var bodyInner = messengerBodyStructuredHtml(m);
if (m.extras && m.extras.attachments && m.extras.attachments.length) {
bodyInner += '<div class="mseg-attach">';
m.extras.attachments.forEach(function(a, aidx) {
var isImg = /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(a.name || '');
if (isImg && a.data) {
var ext = (a.name || '').split('.').pop().toLowerCase();
var mime = ext === 'png' ? 'image/png' : (ext === 'gif' ? 'image/gif' : 'image/jpeg');
bodyInner += '<div style="margin:6px 0"><img src="data:' + mime + ';base64,' +
String(a.data || '') + '" alt="' + esc(a.name || 'Bild') + '"></div>';
} else if (attachmentIsAudio(a) && a.data) {
bodyInner += htmlAudioAttachmentPlayer(a, m.id, aidx);
} else {
bodyInner += '<div style="margin:2px 0;font-size:.78rem">\uD83D\uDCCE ' + esc(a.name || 'Anhang') + '</div>';
}
});
bodyInner += '</div>';
}
html += '<div class="conv-msg-slot ' + align + '" data-msg-id="' + escAttr(m.id || '') + '">';
html += '<div class="conv-msg ' + frm + ackCls + '" role="article" tabindex="-1">';
if (messageHasChatAck(m)) html += '<span class="conv-msg-ack-badge" aria-hidden="true">\u2713</span>';
html += '<div class="conv-msg-meta">' + senderSmall +
'<span class="conv-msg-time">' + esc(timeShort || '\u2013') + '</span></div>';
html += '<div class="conv-msg-text messenger-msg-core">' + (bodyInner || '<span style="opacity:.72">(Nachricht ohne Text)</span>') + '</div>';
html += '<div class="conv-msg-acts">' +
'<button type="button" class="conv-msg-act-in" data-mid="' + escAttr(m.id || '') +
'" title="Aufgabe aus Nachricht">+ Aufgabe</button>' +
'<button type="button" class="conv-msg-act-letter" data-mid="' + escAttr(m.id || '') +
'" title="Brief aus Nachricht">+ Brief</button>' +
htmlMessengerTranscribeButton(m) +
htmlChatAckButton(m.id, m, 'msg') + '</div>';
html += '</div></div>';
});
html += '</div>';
c.innerHTML = html;
bindConversationMsgActionsOnce();
requestAnimationFrame(function() {
try {
var sp = document.getElementById('conversation-scroll');
if (sp) sp.scrollTop = sp.scrollHeight;
} catch (er) {}
});
}
/* ===================================================================
VIEW RENDERING
=================================================================== */
function renderView() {
bindConversationMsgActionsOnce();
maybeAutoPickSingleDmThread();
renderThreadList();
if (messengerLayoutActive()) {
renderMessengerConversation();
syncChatShellLayout();
return;
}
if (activeThreadId) {
renderConversation(activeThreadId);
} else {
clearConversationPane();
}
syncChatShellLayout();
}
function clearConversationPane() {
var main = document.getElementById('conversation-scroll');
var backBtn = document.getElementById('ctb-back');
var inputBar = document.getElementById('chat-input-bar');
var wrap = document.getElementById('chat-input-wrap');
var titleEl = document.getElementById('ctb-title');
var actionsEl = document.getElementById('ctb-actions');
var hideComposer = function() {
if (backBtn) backBtn.classList.remove('visible');
if (inputBar) inputBar.classList.remove('visible');
if (wrap) wrap.classList.add('no-thread-input');
if (actionsEl) actionsEl.innerHTML = '';
};
/* Direktchat gewählt, noch keine Thread-ID: Composer sofort aktiv */
if (activeDmPeer && currentSession && String(activeDmPeer).trim() !== String(currentSession.display_name || '').trim()) {
if (wrap) wrap.classList.remove('no-thread-input');
if (inputBar) inputBar.classList.add('visible');
if (backBtn && messengerBackBtnShouldShow()) backBtn.classList.add('visible');
if (titleEl) titleEl.textContent = formatDmChatTitle(activeDmPeer);
if (actionsEl) actionsEl.innerHTML = '';
if (main) {
var ftsDm = getFilteredThreads();
var multiDm = ftsDm.length > 1 || dmWantsThreadList;
main.innerHTML = multiDm
? '<div class="main-pane-empty main-pane-empty--compact"><div class="main-pane-empty-card">' +
'<span class="mpe-title">' + formatDmChatTitleEscaped(activeDmPeer) + '</span>' +
'<p>Oben sind bestehende Unterhaltungen gelistet. W\u00e4hlen Sie eine aus &mdash; ' +
'oder schreiben Sie unten, um weiterzumachen.</p></div></div>'
: '<div class="main-pane-empty"><div class="main-pane-empty-card">' +
'<span class="mpe-title">Neuer Direktchat</span>' +
'<p>Ihre Nachricht erscheint direkt im Verlauf sobald Sie sie senden oder ein Bild einf\u00fcgen. ' +
'Schreibpartner: <strong>' + esc(activeDmPeer) + '</strong>.</p>' +
'</div></div>';
}
setTimeout(function() {
try { document.getElementById('reply-text').focus(); } catch (ex) {}
}, 80);
return;
}
hideComposer();
if (titleEl) titleEl.textContent = activeView === 'archiv' ? 'Archiv' : 'AzA-Empfang';
if (main) {
main.innerHTML = '<div class="main-pane-empty"><div class="main-pane-empty-card">' +
'<span class="mpe-title">Kein Gespr\u00e4ch ausgew\u00e4hlt</span>' +
'<p>W\u00e4hlen Sie oben die passende Unterhaltung aus der Liste in der Mitte &mdash; oder links unter <strong>Benutzer</strong> einen Kontakt.</p>' +
'</div></div>';
}
}
function renderThreadList() {
var c = document.getElementById('thread-list-mount');
var caption = document.getElementById('thread-list-caption');
if (!c) return;
var backBtn = document.getElementById('ctb-back');
var actionsEl = document.getElementById('ctb-actions');
/* Listen-Kopf Kontextzeile — nicht den grossen Haupttitel ueberschreiben */
var capText = '';
if (activeDmPeer) {
var dmSx = activeView === 'archiv' ? 'Archiv mit' : 'Direktchat mit';
capText = dmSx + ' ' + activeDmPeer;
} else if (activeView === 'archiv') {
capText = 'Erledigte Unterhaltungen (Archiv)';
} else {
capText = 'Offene Unterhaltungen (Allgemein)';
}
if (caption) caption.textContent = capText;
if (!activeThreadId && !activeDmPeer) {
if (backBtn) backBtn.classList.remove('visible');
if (actionsEl) actionsEl.innerHTML = '';
}
var filtered = getFilteredThreads();
if (!filtered.length) {
var hint;
if (activeDmPeer) {
hint = activeView === 'archiv'
? ('Keine erledigten Unterhaltungen mit ' + esc(activeDmPeer) + '.')
: ('Mit ' + esc(activeDmPeer) + ' liegt noch keine offene Unterhaltung vor. Unten direkt neu schreiben.');
} else {
hint = activeView === 'archiv' ? 'Keine erledigten Nachrichten.' : 'Keine offenen Nachrichten.';
}
c.innerHTML = '<div class="thread-list-empty">' + hint + '</div>';
return;
}
c.innerHTML = filtered.map(function(t) {
var m = t.root;
var isDone = m.status === 'erledigt';
var isTask = m.extras && m.extras.is_task;
var replyCount = t.replies.length;
var timeDisp = formatThreadListTimeDisplay(m.zeitstempel || m.empfangen || '');
var rawKom = (m.kommentar || '').trim();
if (rawKom === '\u200b') rawKom = '';
var preview = rawKom.replace(/\s+/g, ' ');
if (!preview) preview = '';
preview = preview.split('\n')[0];
if (preview.length > 96) preview = preview.slice(0, 93) + '\u2026';
if (!preview && replyCount) preview = replyCount + ' Antworten';
if (!preview) preview = (m.absender || '').split('(')[0].trim() || '\u2013';
var activeCls = activeThreadId && t.tid === activeThreadId ? ' tl-active' : '';
return '<div class="tl-item' + activeCls + (isDone ? ' done' : '') + '" onclick="selectThread(\'' + t.tid + '\')" role="button" tabindex="0">' +
'<div class="tl-row1">' +
'<div class="tl-patient">' + (isTask ? '\u2611 ' : '') + esc(m.patient || 'Ohne Patientenangabe') + '</div>' +
'<div class="tl-time">' + esc(timeDisp) + '</div>' +
'</div>' +
'<div class="tl-row2">' +
'<div class="tl-preview">' + esc(preview) + '</div>' +
(replyCount ? '<span class="tl-replies">' + replyCount + ' Antw.</span>' : '') +
'<span class="tl-status ' + (isDone ? 'erledigt' : 'offen') + '">' + (isDone ? 'Erledigt' : 'Offen') + '</span>' +
'</div>' +
'</div>';
}).join('');
}
function selectThread(tid) {
activeThreadId = tid;
dmWantsThreadList = false;
stopRepeatTone();
renderView();
}
function backToList() {
activeThreadId = null;
if (activeDmPeer) dmWantsThreadList = true;
renderView();
}
function smartBackChatTop() {
if (activeThreadId) {
backToList();
return;
}
if (activeDmPeer || activeConversation.mode === 'direct') {
applyActiveConversationFromSession();
activeConversation.mode = 'all';
activeConversation.targetUserId = '';
activeConversation.targetUserKey = '';
activeConversation.targetDisplayName = '';
syncLegacyDmFromActiveConversation();
activeThreadId = null;
dmWantsThreadList = false;
updateSidebarNavHighlight();
lastDataHash = '';
clearLastDirectPeerStorage();
loadMessages();
}
}
function escAttr(str) {
return String(str || '').replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/'/g, '&#39;');
}
function attachmentIsAudio(a) {
if (!a) return false;
if (a.kind === 'audio') return true;
var m = String(a.mime || '').toLowerCase();
if (m.indexOf('audio/') === 0) return true;
var n = String(a.name || '').toLowerCase();
return /\.(webm|ogg|opus|wav|mp3|m4a)$/i.test(n);
}
function messageAudioNeedsTranscribeButton(m) {
var att = m && m.extras && m.extras.attachments;
if (!att || !att.length) return false;
for (var i = 0; i < att.length; i++) {
if (!attachmentIsAudio(att[i])) continue;
if (!String(att[i].transcript || '').trim()) return true;
}
return false;
}
function htmlMessengerTranscribeButton(m) {
if (!messageAudioNeedsTranscribeButton(m)) return '';
return '<button type="button" class="conv-msg-act-tr" data-mid="' + escAttr(m.id || '') +
'" title="Sprachmemo transkribieren (serverseitig)">Text</button>';
}
function htmlAudioAttachmentPlayer(a, msgId, attIdx) {
var mime = String(a.mime || 'audio/webm');
if (!/^audio\/[a-z0-9.+-]+$/i.test(mime)) mime = 'audio/webm';
var b64 = String(a.data || '').replace(/\s/g, '');
if (!b64) return '';
var src = 'data:' + mime + ';base64,' + b64;
var durTxt = '';
if (a.duration_sec != null && isFinite(a.duration_sec)) {
var s = Math.max(0, Math.floor(Number(a.duration_sec)));
var mm = Math.floor(s / 60);
var rr = s % 60;
durTxt = mm + ':' + (rr < 10 ? '0' : '') + rr;
}
var tr = String(a.transcript || '').trim();
var trHtml = tr
? '<div class="aza-audio-transcript">' + esc(tr) + '</div>'
: '';
var mid = msgId != null ? String(msgId) : '';
var ix = attIdx != null ? String(attIdx) : '0';
return '<div class="aza-audio-msg" tabindex="0" role="group" aria-label="Audioaufnahme" data-msg-id="' +
escAttr(mid) + '" data-audio-idx="' + escAttr(ix) + '">' +
'<audio preload="metadata" src="' + src + '" class="aza-audio-el"></audio>' +
'<button type="button" class="aza-audio-play" aria-label="Abspielen">\u25b6</button>' +
'<span class="aza-audio-label">Audioaufnahme</span>' +
(durTxt ? '<span class="aza-audio-dur">' + esc(durTxt) + '</span>' : '') +
trHtml +
'<div class="aza-audio-tr-status"></div>' +
'</div>';
}
function taskSnippetFromKomment(k) {
var t = String(k || '').replace(/\u200b/g, '').trim();
if (!t) return '(Nachricht ohne Klartext)';
if (t.length > 620) return t.slice(0, 617) + '\u2026';
return t;
}
function buildRootTaskBodyText(mm) {
var parts = [];
if ((mm.patient || '').trim()) parts.push(String(mm.patient).trim());
if ((mm.medikamente || '').trim()) parts.push('Medikamente: ' + String(mm.medikamente).trim().split('\n')[0]);
if ((mm.therapieplan || '').trim()) parts.push('Therapieplan: ' + String(mm.therapieplan).trim().split('\n')[0]);
if ((mm.procedere || '').trim()) parts.push('Procedere: ' + String(mm.procedere).trim().split('\n')[0]);
var rk = String(mm.kommentar || '').replace(/\u200b/g, '').trim();
if (rk) parts.push(rk);
var j = parts.join('\n\n').trim();
if (!j && mm.absender) j = '(Stammnachricht von ' + senderDisplayCore(mm.absender || '') + ')';
return j || '(Kein automatischer Text)';
}
function firstLineTitle(body) {
var line = String(body || '').split('\n')[0].trim().replace(/^Medikamente:\s*/i, '').trim();
if (!line || line.charAt(0) === '(') line = String(body || '').split('\n').slice(1).join(' ').trim() || line;
if (!line) line = 'Neue Aufgabe';
if (line.length > 96) line = line.slice(0, 93) + '\u2026';
return line;
}
function suggestAssigneeForTask(senderAbs, dmPeerStr) {
if (!currentSession) return (dmPeerStr || '').trim();
var me = currentSession.display_name.trim();
var s = senderDisplayCore(senderAbs || '').trim();
if (dmPeerStr) {
if (s === me) return dmPeerStr.trim();
/* Nachricht kam vom anderen Kontakt → Auftrag typischerweise bei mir als Empfang */
return me;
}
return me;
}
function composeTaskMetaLine(assigneeGuess, chatLine, whenShort) {
var p = [];
if ((assigneeGuess || '').trim()) p.push('f\u00fcr ' + assigneeGuess.trim());
if ((chatLine || '').trim()) p.push('aus ' + chatLine.trim());
if ((whenShort || '').trim()) p.push(whenShort.trim());
return p.join(' \u00b7 ');
}
function briefDefaultTitleFromRec(rec) {
var g = rec && String(rec.assignGuess || '').trim();
if (g) return 'Brief f\u00fcr ' + g;
var peer = activeDmPeer && String(activeDmPeer).trim();
if (peer) return 'Brief f\u00fcr ' + peer;
return 'Brief aus Chatnachricht';
}
async function createBriefFromMsgRec(rec) {
if (!rec || !currentSession) return;
var body = String(rec.rawBody || '').trim() || '(ohne weiteren Text)';
var title = briefDefaultTitleFromRec(rec);
var assignee = String(rec.assignGuess || '').trim();
try {
var res = await apiFetch(API_BASE + '/tasks', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: title,
text: body,
assignee: assignee,
item_kind: 'letter',
source_msg_id: rec.msgId || '',
source_thread_id: rec.threadId || '',
source_meta: composeTaskMetaLine(
assignee,
(rec.chatLabel || 'AzA-Empfang'),
(rec.when || '')
),
source_peer: activeDmPeer || ''
})
});
var dj = await res.json().catch(function() { return {}; });
var newId = dj.task && dj.task.task_id ? dj.task.task_id : '';
if (res.ok && newId) _taskHighlightId = newId;
ensureTasksPanelOpen();
setTaskTab('letters');
await loadTasksFromServer();
notifyOtherTabs();
if (_taskHighlightId) {
setTimeout(function() {
_taskHighlightId = null;
renderTodos();
}, 4600);
}
} catch (eb) {}
}
async function toggleMessageChatAck(mid, currentlyAck) {
if (!currentSession || !mid) return;
try {
var r = await apiFetch(API_BASE + '/messages/' + encodeURIComponent(mid) + '/chat-ack', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ ack: !currentlyAck })
});
if (!r.ok) return;
lastDataHash = '';
loadMessages();
notifyOtherTabs();
} catch (e) {
if (e && e.message === 'auth') return;
}
}
function empfangScopeForTranscribeUi(mid) {
if (!mid) return null;
var slot = document.querySelector('.conv-msg-slot[data-msg-id="' + mid + '"]');
if (slot) return slot;
var trb = document.querySelector('.conv-msg-act-tr[data-mid="' + mid + '"]');
if (trb) {
return trb.closest('.conv-msg') || trb.closest('.conv-doc');
}
return null;
}
function empfangClearAudioTrStatus(scope) {
if (!scope) return;
scope.querySelectorAll('.aza-audio-tr-status').forEach(function(el) {
el.textContent = '';
el.classList.remove('aza-tr-err');
});
}
function empfangSetAudioTrStatus(mid, text, isErr) {
var scope = empfangScopeForTranscribeUi(mid);
if (!scope) return;
var el = scope.querySelector('.aza-audio-tr-status');
if (!el) return;
el.textContent = text || '';
el.classList.toggle('aza-tr-err', !!isErr);
}
async function empfangTranscribeAudioMessage(mid, btn) {
if (!mid || (btn && btn.disabled)) return;
var scope = empfangScopeForTranscribeUi(mid);
if (btn) btn.disabled = true;
try {
empfangSetAudioTrStatus(mid, 'Transkription l\u00e4uft\u2026', false);
var r = await apiFetch(API_BASE + '/messages/' + encodeURIComponent(mid) + '/transcribe-audio', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}'
});
if (!r.ok) {
var detail = 'Transkription fehlgeschlagen.';
try {
var ej = await r.json();
detail = (ej && (ej.detail || ej.error)) || detail;
} catch (_e1) {}
empfangSetAudioTrStatus(mid, String(detail), true);
return;
}
var d = await r.json();
if (!d || !d.success || !String(d.transcript || '').trim()) {
empfangSetAudioTrStatus(mid, 'Kein Transkript erhalten.', true);
return;
}
empfangClearAudioTrStatus(scope);
lastDataHash = '';
await loadMessages();
} catch (_e2) {
empfangSetAudioTrStatus(mid, 'Netzwerkfehler.', true);
} finally {
if (btn) btn.disabled = false;
}
}
function bindConversationMsgActionsOnce() {
var root = document.getElementById('conversation-scroll');
if (!root) return;
if (!root._azaAudioEnded) {
root._azaAudioEnded = true;
root.addEventListener('ended', function(ev) {
var t = ev.target;
if (!t || !t.classList || !t.classList.contains('aza-audio-el')) return;
var wrap = t.closest('.aza-audio-msg');
if (!wrap) return;
var btnP = wrap.querySelector('.aza-audio-play');
if (btnP) btnP.textContent = '\u25b6';
}, true);
}
if (root._azaMsgActs) return;
root._azaMsgActs = true;
root.addEventListener('click', function(ev) {
var trBtn = ev.target.closest('.conv-msg-act-tr');
if (trBtn) {
var tmid = trBtn.getAttribute('data-mid');
if (!tmid) return;
ev.preventDefault();
ev.stopPropagation();
empfangTranscribeAudioMessage(tmid, trBtn);
return;
}
var audioWrap = ev.target.closest('.aza-audio-msg');
if (audioWrap) {
var aud = audioWrap.querySelector('audio.aza-audio-el');
if (aud) {
ev.preventDefault();
ev.stopPropagation();
var btnP = audioWrap.querySelector('.aza-audio-play');
if (aud.paused) {
aud.play().catch(function() {});
if (btnP) btnP.textContent = '\u23F8';
} else {
aud.pause();
if (btnP) btnP.textContent = '\u25b6';
}
}
return;
}
var okBtn = ev.target.closest('.conv-msg-act-ok, .conv-doc-msg-act-ok');
if (okBtn) {
var okm = okBtn.getAttribute('data-mid');
if (!okm) return;
ev.preventDefault();
ev.stopPropagation();
toggleMessageChatAck(okm, okBtn.getAttribute('data-ack') === '1');
return;
}
var btn = ev.target.closest('.conv-msg-act-in, .conv-doc-msg-act, .conv-msg-act-letter, .conv-doc-msg-act-letter');
if (!btn) return;
var mid = btn.getAttribute('data-mid');
var rec = (_azaMsgLookup || {})[mid];
if (!rec) return;
ev.preventDefault();
ev.stopPropagation();
if (btn.classList.contains('conv-msg-act-letter') || btn.classList.contains('conv-doc-msg-act-letter')) {
createBriefFromMsgRec(rec);
return;
}
openTaskQuickPanelFromMsg(rec);
});
}
function renderConversation(tid) {
var t = allThreads.find(function(th){ return th.tid === tid; });
if (!t) { backToList(); return; }
var m = t.root;
var isDone = m.status === 'erledigt';
var c = document.getElementById('conversation-scroll');
var wrap = document.getElementById('chat-input-wrap');
var backBtn = document.getElementById('ctb-back');
var titleEl = document.getElementById('ctb-title');
var actionsEl = document.getElementById('ctb-actions');
var inputBar = document.getElementById('chat-input-bar');
if (!c) return;
_azaMsgLookup = {};
if (wrap) wrap.classList.remove('no-thread-input');
if (backBtn) backBtn.classList.toggle('visible', !isDesktopShellMode());
if (titleEl) {
if (activeDmPeer) titleEl.textContent = formatDmChatTitle(activeDmPeer);
else titleEl.textContent = (m.patient || '').trim() || 'Allgemein';
}
if (inputBar) inputBar.classList.add('visible');
if (actionsEl) {
actionsEl.innerHTML =
'<details id="chat-top-more" class="details-ctb"><summary class="ctb-sum" aria-label="Men\u00fc"></summary>' +
'<div class="ctb-dd-menu">' +
(!isDone ? '<button type="button" onclick="markDone(\'' + m.id + '\');closeChatTopDetails()">Erledigt</button>' : '') +
'<button type="button" onclick="deleteMsg(\'' + m.id + '\');closeChatTopDetails()">Unterhaltung l\u00f6schen</button>' +
'<button type="button" onclick="copyThread(\'' + tid + '\');closeChatTopDetails()">Kopieren</button>' +
'<button type="button" onclick="saveThread(\'' + tid + '\');closeChatTopDetails()">Speichern\u2026</button>' +
'<button type="button" onclick="printThread(\'' + tid + '\');closeChatTopDetails()">Drucken</button>' +
'</div></details>';
}
var rootBodyTxt = buildRootTaskBodyText(m);
var chatLineRoot = activeDmPeer ? ('Direktchat mit ' + activeDmPeer) : ('Posteingang / ' + ((m.patient || '').trim() || 'Konversation'));
var asgR = suggestAssigneeForTask(m.absender || '', activeDmPeer || '');
_azaMsgLookup[m.id] = {
msgId: m.id,
threadId: tid,
senderLabel: senderDisplayCore(m.absender || '') || '-',
when: formatEmpfangChatDateTimeLong(m.zeitstempel || m.empfangen || ''),
rawBody: rootBodyTxt,
assignGuess: asgR,
chatLabel: chatLineRoot
};
var html = '';
html += '<div class="conv-doc' + (messageHasChatAck(m) ? ' conv-doc-chat-ack' : '') + '">';
if (messageHasChatAck(m)) html += '<span class="conv-doc-ack-badge" aria-hidden="true">\u2713</span>';
html += '<div class="conv-doc-msg-acts">' +
'<button type="button" class="conv-doc-msg-act" data-mid="' + escAttr(m.id) + '" title="Aufgabe aus Nachricht">+ Aufgabe</button>' +
'<button type="button" class="conv-doc-msg-act-letter" data-mid="' + escAttr(m.id) + '" title="Brief aus Nachricht">+ Brief</button>' +
htmlMessengerTranscribeButton(m) +
htmlChatAckButton(m.id, m, 'doc') +
'</div>';
html += '<div class="conv-doc-head">';
html += '<span class="conv-doc-sender">' + esc(m.absender || '\u2013') + '</span>';
html += '<span class="conv-doc-time">' + esc(formatEmpfangChatDateTimeLong(m.zeitstempel || m.empfangen || '')) + '</span>';
html += '</div>';
var labelled = [];
if (m.therapieplan) labelled.push({l:'Therapieplan', v:m.therapieplan});
if (m.procedere) labelled.push({l:'Procedere', v:m.procedere});
if (m.medikamente) labelled.push({l:'Medikamente', v:m.medikamente});
var rawKom = (m.kommentar || '').trim();
var hasContent = labelled.length > 0 || (rawKom && rawKom !== '\u200b');
labelled.forEach(function(f) {
html += '<div class="conv-field">' +
'<div class="conv-field-label" onclick="toggleConvField(this)">' +
'<span class="tri">&#9660;</span>' + esc(f.l) +
'</div>' +
'<div class="conv-field-value">' + esc(f.v) + '</div>' +
'</div>';
});
if (rawKom && rawKom !== '\u200b') {
html += '<div class="conv-field-value" style="margin-top:' +
(labelled.length ? '6px' : '0') + '">' + esc(rawKom) + '</div>';
}
if (!hasContent) {
html += '<div style="color:#8a9aaa;font-size:.85rem">Keine Inhalte</div>';
}
if (m.extras && m.extras.attachments && m.extras.attachments.length) {
html += '<div class="conv-attach">';
m.extras.attachments.forEach(function(a, aidx) {
var isImg = /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(a.name);
if (isImg && a.data) {
var ext = a.name.split('.').pop().toLowerCase();
var mime = ext === 'png' ? 'image/png' : (ext === 'gif' ? 'image/gif' : 'image/jpeg');
html += '<div style="margin:4px 0"><img src="data:' + mime + ';base64,' + a.data +
'" alt="' + esc(a.name) + '"><div style="font-size:.7rem;color:#8a9aaa">' + esc(a.name) + '</div></div>';
} else if (attachmentIsAudio(a) && a.data) {
html += htmlAudioAttachmentPlayer(a, m.id, aidx);
} else {
html += '<div style="margin:2px 0;font-size:.85rem">\uD83D\uDCCE ' + esc(a.name) + '</div>';
}
});
html += '</div>';
}
html += '</div>';
if (t.replies.length) {
var sortedReplies = sortNewestFirst ? t.replies.slice().reverse() : t.replies;
html += '<div class="conv-msg-stack">';
sortedReplies.forEach(function(r) {
var msgBody = taskSnippetFromKomment(r.kommentar || '');
var asgRepl = suggestAssigneeForTask(r.absender || '', activeDmPeer || '');
var chatLineR = activeDmPeer ? ('Direktchat mit ' + activeDmPeer) : chatLineRoot;
_azaMsgLookup[r.id] = {
msgId: r.id,
threadId: tid,
senderLabel: senderDisplayCore(r.absender || '') || '-',
when: formatEmpfangChatDateTimeLong(r.zeitstempel || r.empfangen || ''),
rawBody: msgBody,
assignGuess: asgRepl,
chatLabel: chatLineR
};
var isFromEmpfang = (r.absender || '').includes('Empfang');
var rAck = messageHasChatAck(r);
var rAckCls = rAck ? ' conv-msg-chat-ack' : '';
var attHtml = '';
var ratt = r.extras && r.extras.attachments;
if (ratt && ratt.length) {
ratt.forEach(function(a, aidx) {
if (a.data && a.name && /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(a.name)) {
var ext = a.name.split('.').pop().toLowerCase();
var mime = ext === 'png' ? 'image/png' : (ext === 'gif' ? 'image/gif' : 'image/jpeg');
attHtml += '<div style="margin-top:6px"><img src="data:' + mime + ';base64,' + a.data +
'" alt="' + esc(a.name) + '" style="max-width:100%;max-height:220px;border-radius:10px;border:1px solid #e8eff5"></div>';
} else if (attachmentIsAudio(a) && a.data) {
attHtml += htmlAudioAttachmentPlayer(a, r.id, aidx);
}
});
}
html += '<div class="conv-msg ' + (isFromEmpfang ? 'from-me' : 'from-other') + rAckCls + '" tabindex="-1">';
if (rAck) html += '<span class="conv-msg-ack-badge" aria-hidden="true">\u2713</span>';
html += '<div class="conv-msg-meta">' +
'<span class="conv-msg-sender">' + esc(r.absender || '') + '</span>' +
'<span>' + esc(formatEmpfangChatClockHHMM(r.zeitstempel || r.empfangen || '')) + '</span>' +
'</div>' +
'<div class="conv-msg-text">' + esc(r.kommentar || '') + '</div>' + attHtml +
'<div class="conv-msg-acts">' +
'<button type="button" class="conv-msg-act-in" data-mid="' + escAttr(r.id) + '" title="Aufgabe aus Nachricht">+ Aufgabe</button>' +
'<button type="button" class="conv-msg-act-letter" data-mid="' + escAttr(r.id) + '" title="Brief aus Nachricht">+ Brief</button>' +
htmlMessengerTranscribeButton(r) +
htmlChatAckButton(r.id, r, 'msg') +
'</div>' +
'</div>';
});
html += '</div>';
}
c.innerHTML = html;
c.scrollTop = sortNewestFirst ? 0 : c.scrollHeight;
}
function closeChatTopDetails() {
var d = document.getElementById('chat-top-more');
if (d) d.open = false;
}
document.addEventListener('click', function(ev) {
var det = document.getElementById('chat-top-more');
if (!det || !det.open) return;
if (det.contains(ev.target)) return;
det.open = false;
});
function toggleConvField(labelEl) {
var tri = labelEl.querySelector('.tri');
var val = labelEl.nextElementSibling;
if (!val) return;
var hide = !val.classList.contains('collapsed');
val.classList.toggle('collapsed', hide);
if (tri) tri.classList.toggle('closed', hide);
}
/* ===================================================================
INLINE REPLY
=================================================================== */
function updateReplyAttHint() {
var el = document.getElementById('reply-att-hint');
if (!el) return;
if (el.getAttribute('data-aza-temp-hint') === '1') return;
var nImg = 0;
var nAud = 0;
pendingReplyAttachments.forEach(function(a) {
if (attachmentIsAudio(a)) nAud++;
else nImg++;
});
if (!nImg && !nAud) {
el.textContent = '';
return;
}
var parts = [];
if (nImg) parts.push(nImg + ' Bild(er)');
if (nAud) parts.push(nAud + ' Audio');
el.textContent = parts.join(', ') + ' angeh\u00e4ngt';
}
function queueReplyImageFiles(files) {
discardAudioRecordingDiscardOnly();
Array.from(files || []).forEach(function(file) {
if (!file.type || file.type.indexOf('image/') !== 0) return;
if (file.size > REPLY_ATTACH_MAX_BYTES) return;
var fr = new FileReader();
fr.onload = function() {
var s = fr.result || '';
var i = s.indexOf(',');
pendingReplyAttachments.push({
name: file.name || 'bild.png',
data: i >= 0 ? s.slice(i + 1) : s
});
updateReplyAttHint();
};
fr.readAsDataURL(file);
});
}
function setupReplyBarDropPaste() {
var ta = document.getElementById('reply-text');
var bar = document.getElementById('chat-input-bar');
if (!ta || !bar) return;
ta.addEventListener('paste', function(e) {
var items = e.clipboardData && e.clipboardData.items;
if (!items) return;
for (var i = 0; i < items.length; i++) {
if (items[i].type && items[i].type.indexOf('image/') === 0) {
e.preventDefault();
var f = items[i].getAsFile();
if (f) queueReplyImageFiles([f]);
}
}
});
bar.addEventListener('dragover', function(e) { e.preventDefault(); });
bar.addEventListener('drop', function(e) {
e.preventDefault();
if (e.dataTransfer && e.dataTransfer.files) queueReplyImageFiles(e.dataTransfer.files);
});
}
function openNewChatDialog() {
if (!currentSession) {
showLoginOverlay();
return;
}
var sel = document.getElementById('nc-peer-select');
sel.innerHTML = '<option value="">\u2014 Benutzer w\u00e4hlen \u2014</option>';
(practiceUsers || []).forEach(function(u) {
if (!u || isPracticeUserSelf(u)) return;
var o = document.createElement('option');
o.value = u.display_name;
o.textContent = u.display_name + (u.role ? ' (' + u.role + ')' : '');
if (u.user_id) o.setAttribute('data-peer-uid', String(u.user_id));
sel.appendChild(o);
});
var gr = document.querySelector('input[name="nc-mode"][value="general"]');
if (gr) gr.checked = true;
syncNcPeerSelect();
document.getElementById('new-chat-overlay').classList.remove('hidden');
}
function closeNewChatDialog() {
document.getElementById('new-chat-overlay').classList.add('hidden');
}
function syncNcPeerSelect() {
var mode = (document.querySelector('input[name="nc-mode"]:checked') || {}).value;
document.getElementById('nc-peer-select').disabled = mode !== 'dm';
}
function launchNewChatWindow() {
var mode = (document.querySelector('input[name="nc-mode"]:checked') || {}).value || 'general';
var selWin = document.getElementById('nc-peer-select');
var peer = (selWin.value || '').trim();
var opt0 = selWin.selectedOptions && selWin.selectedOptions[0];
var puid = (opt0 && opt0.getAttribute('data-peer-uid')) || '';
if (mode === 'dm' && !peer) {
alert('Bitte einen Benutzer ausw\u00e4hlen.');
return;
}
closeNewChatDialog();
var q = mode === 'general' ? 'mode=general' : ('mode=dm&peer=' + encodeURIComponent(peer) +
(puid ? '&peer_uid=' + encodeURIComponent(puid) : ''));
var url = window.location.origin + '/empfang/chatwin.html?' + q;
var wname = 'aza_empfang_chat_' + String(peer || 'allgemein').replace(/\W/g, '_') + '_' + Date.now();
var w = window.open(url, wname, 'width=560,height=720,menubar=no,toolbar=no,scrollbars=yes,resizable=yes');
if (!w) {
alert('Das Chat-Fenster wurde blockiert. Bitte Popups f\u00fcr diese Seite erlauben, oder die Adresse manuell \u00f6ffnen:\n\n' + url);
}
}
async function sendReply() {
if (!currentSession) return;
if (activeView === 'archiv' && !activeDmPeer) return;
discardAudioRecordingDiscardOnly();
stopDictation();
var ta = document.getElementById('reply-text');
if (!ta) return;
var text = ta.value.trim();
if (!text && !pendingReplyAttachments.length) return;
stopDictation();
try {
var absenderLbl = currentSession.display_name + ' (Empfang)';
var ts = new Date().toISOString().slice(0, 19).replace('T', ' ');
var attachments = null;
if (pendingReplyAttachments.length) {
attachments = pendingReplyAttachments.slice();
pendingReplyAttachments = [];
updateReplyAttHint();
}
var kom = text || (attachments && attachments.length ? '\u200b' : '');
if (!kom && !attachments) return;
applyActiveConversationFromSession();
if (activeConversation.mode !== 'direct') {
ta.value = '';
var pidWarn = document.getElementById('chat-input-bar');
if (pidWarn) pidWarn.classList.add('dm-input-error');
alert('Bitte links einen Benutzer ausw\u00e4hlen, an den die Nachricht gehen soll. ' +
'In dieser Praxis gibt es nur Personenchats (kein Allgemein-Chat).');
ta.value = text;
return;
}
var peerName = String(activeConversation.targetDisplayName || '').trim();
var pdu = String(activeConversation.targetUserId || '').trim();
var sdu = String((currentSession && currentSession.user_id) || '').trim();
var pidSend = String(activeConversation.practiceId || currentSession.practice_id || '').trim();
if (!pdu) {
try { pdu = String(peerUidForDisplay(peerName) || '').trim(); } catch (e) { pdu = ''; }
if (pdu) {
activeConversation.targetUserId = pdu;
activeConversation.targetUserKey = pdu;
syncLegacyDmFromActiveConversation();
}
}
if (!peerName || !pdu || !sdu || !pidSend) {
console.warn('[Empfang] AZA_CHAT_SEND_BLOCKED unvollstaendig', {
mode: activeConversation.mode,
peerName: peerName ? '(set)' : '(empty)',
target_uid: pdu ? '(set)' : '(empty)',
sender_uid: sdu ? '(set)' : '(empty)',
practice_id: pidSend ? '(set)' : '(empty)'
});
dmDiagWrite('SEND_BLOCKED_INCOMPLETE');
alert('Direktchat konnte technisch nicht eindeutig zugeordnet werden.\n' +
'Bitte den Benutzer links erneut ausw\u00e4hlen.');
return;
}
if (pdu === sdu) {
console.warn('[Empfang] AZA_CHAT_SEND_BLOCKED selfchat');
dmDiagWrite('SEND_BLOCKED_SELFCHAT');
alert('Selbstchat ist nicht m\u00f6glich. Bitte einen anderen Benutzer w\u00e4hlen.');
return;
}
var clientMsgId = (Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8));
var sendBody = {
practice_id: pidSend,
sender_user_id: sdu,
recipient_user_id: pdu,
text: kom,
client_msg_id: clientMsgId,
attachments: attachments || []
};
console.debug('[Empfang] AZA_CHAT_SEND mode=direct practice=' + pidSend +
' sender=' + sdu + ' recipient=' + pdu + ' client_msg_id=' + clientMsgId);
dmDiagWrite('SEND_START');
var rDm;
try {
rDm = await apiFetch(API_BASE + '/dm/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sendBody)
});
} catch (eNet) {
console.warn('[Empfang] AZA_CHAT_SEND network', eNet);
dmDiagWrite('SEND_NETWORK_ERR');
alert('Senden fehlgeschlagen (Netzwerk). Bitte sp\u00e4ter erneut versuchen. Nachricht bleibt erhalten.');
return;
}
if (!rDm.ok) {
var detail = '';
try { var errd = await rDm.json(); detail = errd && (errd.detail || errd.error) || ''; } catch (_e) {}
console.warn('[Empfang] AZA_CHAT_SEND HTTP ' + rDm.status + ' ' + detail);
dmDiagWrite('SEND_HTTP_' + rDm.status);
alert('Direktversand abgewiesen (HTTP ' + rDm.status + '): ' + (detail || 'Bitte Diagnose pr\u00fcfen.'));
return;
}
var sd = {};
try { sd = await rDm.json(); } catch (_eJ) {}
if (!sd || !sd.success || !sd.message_id || sd.mode !== 'direct') {
console.warn('[Empfang] AZA_CHAT_SEND unexpected response', sd);
dmDiagWrite('SEND_BAD_RESPONSE');
alert('Direktversand technisch nicht best\u00e4tigt. Bitte Diagnose pr\u00fcfen. Nachricht bleibt erhalten.');
return;
}
_dmV2State.lastSentMsgId = String(sd.message_id || '');
_dmV2State.lastConvKey = String(sd.conversation_key || '');
console.debug('[Empfang] AZA_CHAT_SEND ok msg=' + _dmV2State.lastSentMsgId +
' conv=' + _dmV2State.lastConvKey);
ta.value = '';
ta.style.height = '56px';
justSentReply = true;
activeThreadId = null;
lastDataHash = '';
await loadMessages();
var found = (allMessages || []).some(function(m) { return m && m.id === _dmV2State.lastSentMsgId; });
if (!found) {
console.warn('[Empfang] AZA_CHAT_VERIFY_FAIL gesendet, aber nicht im geladenen Verlauf (soft retry)',
{ sent_msg_id: _dmV2State.lastSentMsgId, sent_conv: _dmV2State.lastConvKey,
loaded_count: allMessages.length });
dmDiagWrite('VERIFY_FAIL_RETRY');
// Kein modaler Alert: stiller Nachladeversuch (z. B. Pulse-/Speicherlag), Diagnose siehe Statuszeile.
var _retryMsgId = _dmV2State.lastSentMsgId;
setTimeout(function () {
try {
lastDataHash = '';
activeThreadId = null;
var _p = loadMessages();
if (_p && typeof _p.then === 'function') {
_p.then(function () {
try {
var ok = (allMessages || []).some(function (m) { return m && m.id === _retryMsgId; });
dmDiagWrite(ok ? 'VERIFY_OK_RETRY' : 'VERIFY_PENDING');
} catch (_eDr) {}
}).catch(function () {});
}
} catch (_eR) {}
}, 700);
} else {
dmDiagWrite('VERIFY_OK');
}
notifyOtherTabs();
return;
} catch (e) {}
}
function autoGrowTextarea(el) {
el.style.height = '56px';
el.style.height = Math.min(el.scrollHeight, 200) + 'px';
}
/* ===================================================================
ACTIONS (markDone, delete, copy, save, print)
=================================================================== */
async function markDone(id) {
try { await apiFetch(API_BASE + '/messages/' + id + '/done', {method:'POST'}); } catch(e) { return; }
stopRepeatTone();
lastDataHash = '';
loadMessages();
notifyOtherTabs();
}
async function deleteMsg(id) {
try { await apiFetch(API_BASE + '/messages/' + id, {method:'DELETE'}); } catch(e) { return; }
activeThreadId = null;
lastDataHash = '';
loadMessages();
notifyOtherTabs();
}
function copyThread(tid) {
var t = allThreads.find(function(th){ return th.tid === tid; });
if (!t) return;
var m = t.root;
var parts = [];
if (m.patient) parts.push('Patient: ' + m.patient);
if (m.therapieplan) parts.push('Therapieplan:\n' + m.therapieplan);
if (m.procedere) parts.push('Procedere:\n' + m.procedere);
if (m.kommentar) parts.push('Kommentar:\n' + m.kommentar);
parts.push('Absender: ' + (m.absender||'\u2013') + ' \u00b7 ' + formatEmpfangChatDateTimeLong(m.zeitstempel||m.empfangen||''));
if (t.replies.length) {
parts.push('\n--- Chat-Verlauf ---');
t.replies.forEach(function(r) {
parts.push((r.absender||'') + ' (' + formatEmpfangChatDateTimeLong(r.zeitstempel||r.empfangen||'') + '):\n' + (r.kommentar||''));
});
}
navigator.clipboard.writeText(parts.join('\n\n')).catch(function(){});
stopRepeatTone();
}
function saveThread(tid) {
var t = allThreads.find(function(th){ return th.tid === tid; });
if (!t) return;
var m = t.root;
var lines = ['=== AzA-Empfang Chat-Verlauf ===',
'Patient/Nr.: ' + (m.patient || '-'),
'Absender: ' + (m.absender || '-'),
'Zeitpunkt: ' + formatEmpfangChatDateTimeLong(m.zeitstempel || m.empfangen || ''), ''];
if (m.therapieplan) lines.push('THERAPIEPLAN:\n' + m.therapieplan + '\n');
if (m.procedere) lines.push('PROCEDERE:\n' + m.procedere + '\n');
if (m.kommentar) lines.push('KOMMENTAR:\n' + m.kommentar + '\n');
if (t.replies.length) {
lines.push('--- Chat-Verlauf ---');
t.replies.forEach(function(r) {
lines.push('[' + (r.absender||'') + '] ' + formatEmpfangChatDateTimeLong(r.zeitstempel||r.empfangen||''));
lines.push(r.kommentar || ''); lines.push('');
});
}
var blob = new Blob([lines.join('\n')], {type:'text/plain;charset=utf-8'});
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'chat_' + (m.patient || tid).replace(/[^a-zA-Z0-9]/g, '_') + '.txt';
a.click(); URL.revokeObjectURL(a.href);
}
function printThread(tid) {
var t = allThreads.find(function(th){ return th.tid === tid; });
if (!t) return;
var m = t.root;
var w = window.open('', '_blank', 'width=600,height=600');
var h = '<html><head><title>Empfang</title>' +
'<style>body{font-family:"Segoe UI",sans-serif;padding:30px;color:#1a2a3a}' +
'h2{color:#5B8DB3;margin-bottom:16px}' +
'.f{margin-bottom:12px}.fl{font-size:.8rem;font-weight:bold;color:#5B8DB3;text-transform:uppercase}' +
'.fv{margin-top:2px;white-space:pre-wrap}' +
'.meta{font-size:.8rem;color:#888;margin-top:20px;border-top:1px solid #ddd;padding-top:10px}' +
'.reply{margin-top:10px;padding:8px 12px;background:#f8f8f8;border-left:3px solid #999;border-radius:4px}' +
'.rs{font-weight:bold;font-size:.85rem}</style></head><body>' +
'<h2>' + esc(m.patient||'Empfangsnachricht') + '</h2>' +
(m.therapieplan ? '<div class="f"><div class="fl">Therapieplan</div><div class="fv">' + esc(m.therapieplan) + '</div></div>' : '') +
(m.procedere ? '<div class="f"><div class="fl">Procedere</div><div class="fv">' + esc(m.procedere) + '</div></div>' : '') +
(m.kommentar ? '<div class="f"><div class="fl">Kommentar</div><div class="fv">' + esc(m.kommentar) + '</div></div>' : '') +
'<div class="meta">Absender: ' + esc(m.absender||'\u2013') + ' \u00b7 ' + esc(formatEmpfangChatDateTimeLong(m.zeitstempel||m.empfangen||'')) + '</div>' +
(t.replies.length ? '<h3 style="margin-top:20px">Chat-Verlauf</h3>' +
t.replies.map(function(r){
return '<div class="reply"><div class="rs">' + esc(r.absender||'') + ' &middot; ' +
esc(formatEmpfangChatDateTimeLong(r.zeitstempel||r.empfangen||'')) + '</div><div class="fv">' + esc(r.kommentar||'') + '</div></div>';
}).join('') : '') +
'</body></html>';
w.document.write(h); w.document.close(); w.print();
}
async function doCleanup() {
var days = parseInt(document.getElementById('cleanup-days').value, 10) || 30;
var el = document.getElementById('cleanup-result');
el.textContent = 'Bereinige...';
try {
var r = await apiFetch(API_BASE + '/cleanup', {
method:'POST', headers:{'Content-Type':'application/json'},
body:JSON.stringify({max_age_days:days})
});
var d = await r.json();
el.textContent = d.removed + ' entfernt, ' + d.remaining + ' verbleibend';
if (d.removed > 0) { lastDataHash = ''; loadMessages(); }
} catch(e) {
if (e.message !== 'auth') el.textContent = 'Fehler';
}
}
/* ===================================================================
DESKTOP-HÜLLE (pywebview): immer im Vordergrund
=================================================================== */
var shellPinBusy = false;
function initShellPinUi() {
var b = document.getElementById('shell-pin-btn');
if (!b) return;
var api = window.pywebview && pywebview.api;
if (!api || typeof api.toggle_on_top !== 'function' || typeof api.get_on_top !== 'function') {
b.style.display = 'none';
return;
}
b.style.display = '';
updateShellPinBtn();
}
async function updateShellPinBtn() {
var b = document.getElementById('shell-pin-btn');
if (!b || b.style.display === 'none') return;
try {
var v = await pywebview.api.get_on_top();
if (v) {
b.classList.add('on');
b.title = 'Immer im Vordergrund (aktiv)';
b.setAttribute('aria-pressed', 'true');
} else {
b.classList.remove('on');
b.title = 'Immer im Vordergrund';
b.setAttribute('aria-pressed', 'false');
}
} catch (e) {}
}
async function toggleShellPin() {
if (shellPinBusy) return;
var b = document.getElementById('shell-pin-btn');
if (!b || b.style.display === 'none') return;
var api = window.pywebview && pywebview.api;
if (!api || typeof api.toggle_on_top !== 'function') return;
shellPinBusy = true;
try {
await api.toggle_on_top();
await updateShellPinBtn();
} catch (e) {}
shellPinBusy = false;
}
/* ===================================================================
NEUE EINGEHENDE NACHRICHT — Hinweis & Fenster nach vorne
- Browser: Titel-Flash (bestmoeglich, da window.focus() oft blockiert).
- Desktop-Huelle (pywebview): bring_to_front via JS-API; ruft Win32-API
in aza_empfang_webview.py auf. Nur wenn pywebview-API verfuegbar ist.
- Keine Patientendaten/Chattexte loggen oder anzeigen.
=================================================================== */
function stopNewMsgFlash() {
try {
if (_newMsgFlashTimer) { clearInterval(_newMsgFlashTimer); _newMsgFlashTimer = null; }
if (_newMsgOrigTitle) { document.title = _newMsgOrigTitle; }
} catch (_e) {}
}
function startNewMsgFlash() {
try {
if (!_newMsgOrigTitle) _newMsgOrigTitle = document.title || 'AzA-Empfang';
if (_newMsgFlashTimer) return;
var alt = '\u2709 Neue Nachricht \u2022 ' + _newMsgOrigTitle;
var on = false;
document.title = alt;
_newMsgFlashTimer = setInterval(function() {
on = !on;
try { document.title = on ? alt : _newMsgOrigTitle; } catch (_e) {}
}, 1100);
} catch (_e) {}
}
function _empfangBringPywebviewFront() {
try {
var api = (window.pywebview && window.pywebview.api) || null;
if (!api) return false;
if (typeof api.bring_to_front !== 'function') return false;
if (_empfangBringFrontInflight) return false;
_empfangBringFrontInflight = true;
var done = function(){
setTimeout(function(){ _empfangBringFrontInflight = false; }, 800);
};
var p = api.bring_to_front();
if (p && typeof p.then === 'function') { p.then(done, done); }
else { done(); }
return true;
} catch (_e) { _empfangBringFrontInflight = false; return false; }
}
function notifyNewIncomingMessage(mm) {
try {
var mid = String((mm && mm.id) || '').trim();
if (!mid) return;
/* Pro-ID-Sperre: derselbe Nachrichtenstand darf nie wieder ein Popup ausloesen. */
if (_newMsgNotifiedIds.has(mid)) return;
/* Globaler Cooldown: maximal ein Popup pro 2000 ms. */
var now = Date.now();
if ((now - _newMsgLastNotifyMs) < 2000) return;
_newMsgLastNotifyMs = now;
_newMsgNotifiedIds.add(mid);
/* Set begrenzen, damit es nicht unbegrenzt waechst. */
if (_newMsgNotifiedIds.size > 256) {
var arr = Array.from(_newMsgNotifiedIds);
_newMsgNotifiedIds = new Set(arr.slice(arr.length - 128));
}
if (window.console && console.debug) console.debug('[Empfang] new incoming msg id=' + mid.slice(0, 8));
} catch (_e) {}
startNewMsgFlash();
try { if (window.focus) window.focus(); } catch (_e) {}
_empfangBringPywebviewFront();
}
document.addEventListener('visibilitychange', function() {
if (!document.hidden) stopNewMsgFlash();
});
window.addEventListener('focus', function() { stopNewMsgFlash(); });
/* ===================================================================
SOUND
=================================================================== */
function updateSoundBtn() {
var btn = document.getElementById('sound-btn');
if (soundEnabled) { btn.innerHTML = '&#128276; Ton'; btn.classList.remove('muted'); }
else { btn.innerHTML = '&#128277; Aus'; btn.classList.add('muted'); }
}
function toggleSound() {
soundEnabled = !soundEnabled;
localStorage.setItem('empfang_sound', soundEnabled ? 'on' : 'off');
updateSoundBtn();
if (soundEnabled) playTone(currentToneIdx);
}
function testCurrentSound() {
var prev = soundEnabled; soundEnabled = true;
playTone(currentToneIdx); soundEnabled = prev;
}
function playTone(idx) {
if (!soundEnabled) return;
var preset = TONE_PRESETS[idx] || TONE_PRESETS[0];
var vol = preset.vol * Math.max(0, volume);
try {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume();
var t = audioCtx.currentTime;
preset.notes.forEach(function(note) {
var g = audioCtx.createGain();
g.connect(audioCtx.destination);
g.gain.setValueAtTime(vol, t);
g.gain.linearRampToValueAtTime(vol * 0.6, t + note.d * 0.4);
g.gain.exponentialRampToValueAtTime(0.001, t + note.d);
var o = audioCtx.createOscillator();
o.type = preset.wave; o.frequency.setValueAtTime(note.f, t);
o.connect(g); o.start(t); o.stop(t + note.d + 0.05);
t += note.d;
});
} catch(e) {}
}
function startRepeatTone() {
stopRepeatTone();
if (repeatMs > 0 && soundEnabled) {
repeatInterval = setInterval(function(){ playTone(currentToneIdx); }, repeatMs);
}
}
function stopRepeatTone() {
if (repeatInterval) { clearInterval(repeatInterval); repeatInterval = null; }
}
function setVolume(v) {
volume = parseInt(v, 10) / 100;
document.getElementById('vol-display').textContent = Math.round(volume * 100) + '%';
localStorage.setItem('empfang_volume', volume);
}
function saveSoundSettings() {
currentToneIdx = parseInt(document.getElementById('tone-select').value, 10);
localStorage.setItem('empfang_tone_idx', currentToneIdx);
onRepeatChange();
}
function initRepeatUI() {
var sel = document.getElementById('repeat-select');
var known = ['0','2000','5000','30000'];
if (known.indexOf(String(repeatMs)) >= 0) {
sel.value = String(repeatMs);
document.getElementById('custom-sec-wrap').style.display = 'none';
} else if (repeatMs > 0) {
sel.value = 'custom';
document.getElementById('custom-sec-wrap').style.display = 'inline';
document.getElementById('custom-sec').value = Math.round(repeatMs / 1000);
} else { sel.value = '0'; }
}
function onRepeatChange() {
var sel = document.getElementById('repeat-select');
var cw = document.getElementById('custom-sec-wrap');
if (sel.value === 'custom') {
cw.style.display = 'inline';
repeatMs = Math.max(1, parseInt(document.getElementById('custom-sec').value, 10)) * 1000;
} else { cw.style.display = 'none'; repeatMs = parseInt(sel.value, 10); }
localStorage.setItem('empfang_repeat_ms', repeatMs);
}
/* ===================================================================
FONT / UI
=================================================================== */
function applyFontSize() {
var fs = currentFontSize + 'pt';
/* rem bezieht sich auf <html>: nur body zu setzen skaliert die meisten Inhalte nicht */
document.documentElement.style.fontSize = fs;
document.body.style.fontSize = fs;
var pct = Math.round((currentFontSize / EMPFANG_FONT_BASE_PT) * 100);
var disp = document.getElementById('font-size-display');
if (disp) disp.textContent = pct + '%';
var wrap = document.getElementById('font-ctrl-wrap');
if (wrap) wrap.title = 'Schriftgröße: ' + pct + '% (' + currentFontSize + ' pt, 100% = ' + EMPFANG_FONT_BASE_PT + ' pt)';
localStorage.setItem('empfang_font_size', String(currentFontSize));
}
function changeFontSize(d) {
currentFontSize = Math.max(5, Math.min(20, currentFontSize + d));
applyFontSize();
}
function toggleSettings() {
document.getElementById('settings-panel').classList.toggle('open');
}
function toggleSort() {
sortNewestFirst = !sortNewestFirst;
localStorage.setItem('empfang_sort', sortNewestFirst ? 'newest' : 'asc');
updateSortBtn();
lastDataHash = '';
loadMessages();
}
function updateSortBtn() {
var btn = document.getElementById('sort-btn');
if (sortNewestFirst) { btn.innerHTML = '&#8593; Neu'; btn.classList.add('active-sort'); }
else { btn.innerHTML = '&#8595; Alt'; btn.classList.remove('active-sort'); }
}
function _azaStopAudioStreamTracks() {
if (_azaAudioStream) {
try {
_azaAudioStream.getTracks().forEach(function(tr) {
try { tr.stop(); } catch (e1) {}
});
} catch (e2) {}
_azaAudioStream = null;
}
}
function updateAudioRecBtn() {
var btn = document.getElementById('audio-rec-btn');
if (!btn) return;
var rec = _azaMediaRecorder && _azaMediaRecorder.state === 'recording';
if (rec) {
btn.classList.add('recording');
btn.title = 'Aufnahme stoppen und senden';
btn.setAttribute('aria-pressed', 'true');
} else {
btn.classList.remove('recording');
btn.title = 'Sprachmemo: Klick starten, erneut klicken zum Stoppen und Senden';
btn.setAttribute('aria-pressed', 'false');
}
}
function showReplyHintTemporary(msg, ms) {
var hi = document.getElementById('reply-att-hint');
if (!hi || !msg) return;
hi.setAttribute('data-aza-temp-hint', '1');
hi.style.color = '#9a3540';
hi.textContent = msg;
clearTimeout(window._azaReplyHintTimer);
window._azaReplyHintTimer = setTimeout(function() {
try {
hi.style.color = '';
hi.removeAttribute('data-aza-temp-hint');
if (typeof updateReplyAttHint === 'function') updateReplyAttHint();
} catch (ex) {}
}, ms || 8000);
}
function discardAudioRecordingDiscardOnly() {
if (_azaAudioRecordTimer) {
clearTimeout(_azaAudioRecordTimer);
_azaAudioRecordTimer = null;
}
if (_azaMediaRecorder) {
try {
if (_azaMediaRecorder.state === 'recording') {
_azaMediaRecorder.ondataavailable = null;
_azaMediaRecorder.onstop = function() {
_azaMediaRecorder = null;
_azaAudioChunks = [];
_azaStopAudioStreamTracks();
updateAudioRecBtn();
};
_azaMediaRecorder.stop();
return;
}
} catch (ex) {}
}
_azaMediaRecorder = null;
_azaAudioChunks = [];
_azaStopAudioStreamTracks();
updateAudioRecBtn();
}
function finalizeAudioRecordingAndSend() {
if (_azaAudioRecordTimer) {
clearTimeout(_azaAudioRecordTimer);
_azaAudioRecordTimer = null;
}
var mr = _azaMediaRecorder;
if (!mr || mr.state !== 'recording') {
discardAudioRecordingDiscardOnly();
return;
}
mr.onstop = function() {
var blobType = _azaAudioRecordMime || mr.mimeType || 'audio/webm';
_azaMediaRecorder = null;
_azaStopAudioStreamTracks();
updateAudioRecBtn();
var blob;
try {
blob = new Blob(_azaAudioChunks, { type: blobType });
} catch (eB) {
_azaAudioChunks = [];
showReplyHintTemporary('Aufnahme konnte nicht erstellt werden.', 8000);
return;
}
_azaAudioChunks = [];
if (!blob || blob.size < 1) {
showReplyHintTemporary('Aufnahme leer.', 5000);
return;
}
if (blob.size > AUDIO_ATTACH_MAX_BYTES) {
showReplyHintTemporary('Aufnahme zu gro\u00df (max. ca. ' + Math.round(AUDIO_ATTACH_MAX_BYTES / 1024) + ' KB).', 10000);
return;
}
var fr = new FileReader();
fr.onload = function() {
var s = fr.result || '';
var i = s.indexOf(',');
var b64 = i >= 0 ? s.slice(i + 1) : '';
if (!b64) {
showReplyHintTemporary('Aufnahme konnte nicht verpackt werden.', 8000);
return;
}
pendingReplyAttachments.push({
name: 'sprachmemo.webm',
mime: blob.type || blobType || 'audio/webm',
kind: 'audio',
data: b64
});
updateReplyAttHint();
sendReply();
};
fr.onerror = function() {
showReplyHintTemporary('Aufnahme konnte nicht gelesen werden.', 8000);
};
try {
fr.readAsDataURL(blob);
} catch (eR) {
showReplyHintTemporary('Aufnahme-Fehler.', 8000);
}
};
try {
mr.stop();
} catch (eS) {
discardAudioRecordingDiscardOnly();
}
}
function toggleAudioRecording() {
if (_azaMediaRecorder && _azaMediaRecorder.state === 'recording') {
finalizeAudioRecordingAndSend();
return;
}
if (!window.MediaRecorder || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
showReplyHintTemporary('Sprachmemo hier nicht verf\u00fcgbar (Browser).', 10000);
return;
}
stopDictation();
var mimePick = '';
if (window.MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) {
mimePick = 'audio/webm;codecs=opus';
} else if (window.MediaRecorder.isTypeSupported('audio/webm')) {
mimePick = 'audio/webm';
}
_azaAudioRecordMime = mimePick || '';
navigator.mediaDevices.getUserMedia({ audio: true })
.then(function(stream) {
_azaAudioStream = stream;
var opts = mimePick ? { mimeType: mimePick } : undefined;
var mr = new MediaRecorder(stream, opts);
_azaMediaRecorder = mr;
_azaAudioChunks = [];
mr.ondataavailable = function(ev) {
try {
if (ev.data && ev.data.size) _azaAudioChunks.push(ev.data);
} catch (e0) {}
};
mr.onstop = function() {};
mr.start(180);
updateAudioRecBtn();
_azaAudioRecordTimer = setTimeout(function() {
if (_azaMediaRecorder && _azaMediaRecorder.state === 'recording') {
showReplyHintTemporary('H\u00f6chstdauer ' + (AUDIO_RECORD_MAX_MS / 1000) + ' s erreicht. Sende\u2026', 6000);
finalizeAudioRecordingAndSend();
}
}, AUDIO_RECORD_MAX_MS);
})
.catch(function() {
_azaStopAudioStreamTracks();
_azaMediaRecorder = null;
updateAudioRecBtn();
showReplyHintTemporary('Mikrofon nicht freigegeben oder nicht verf\u00fcgbar.', 12000);
});
}
/* ===================================================================
DICTATION
=================================================================== */
function toggleDictation() {
if (isRecording) { stopDictation(); return; }
discardAudioRecordingDiscardOnly();
var SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SR) { return; }
recognition = new SR();
recognition.lang = 'de-DE';
recognition.continuous = true;
recognition.interimResults = false;
recognition.onresult = function(ev) {
var ta = document.getElementById('reply-text');
for (var i = ev.resultIndex; i < ev.results.length; i++) {
if (ev.results[i].isFinal) ta.value += ev.results[i][0].transcript + ' ';
}
autoGrowTextarea(ta);
};
recognition.onerror = function(ev) {
stopDictation();
if (!ev || ev.error !== 'not-allowed') return;
if (!isDesktopShellMode() || window._azaMicDeniedHintShown) return;
window._azaMicDeniedHintShown = true;
var hi = document.getElementById('reply-att-hint');
if (!hi) return;
hi.removeAttribute('data-aza-temp-hint');
var prevCol = hi.style.color;
hi.style.color = '#9a3540';
hi.textContent = 'Mikrofonzugriff wurde blockiert. Bitte in Windows/WebView-Berechtigungen erlauben.';
clearTimeout(window._azaMicDeniedHintTimer);
window._azaMicDeniedHintTimer = setTimeout(function() {
try {
hi.style.color = prevCol || '';
if (typeof updateReplyAttHint === 'function') updateReplyAttHint();
} catch (ex) {}
}, 12000);
};
recognition.onend = function() {
if (isRecording) { try { recognition.start(); } catch(e) { stopDictation(); } }
};
recognition.start();
isRecording = true;
updateDictateBtn();
}
function stopDictation() {
if (recognition) { try { recognition.stop(); } catch(e){} recognition = null; }
isRecording = false;
updateDictateBtn();
}
function updateDictateBtn() {
var btn = document.getElementById('dictate-btn');
if (!btn) return;
if (isRecording) { btn.innerHTML = '&#9209;'; btn.classList.add('recording'); btn.title = 'Stoppen'; }
else { btn.innerHTML = '&#127908;'; btn.classList.remove('recording'); btn.title = 'Diktieren'; }
}
/* ===================================================================
TASKS (server only)
=================================================================== */
function bindTaskEscapeOnce() {
if (document.body._azaTaskEsc) return;
document.body._azaTaskEsc = true;
document.addEventListener('keydown', function(ev) {
if (ev.key !== 'Escape') return;
var qq = document.getElementById('task-quick-panel');
if (qq && !qq.classList.contains('hidden')) closeTaskQuickPanel();
});
}
function taskCardMetaLine(tk) {
if (!tk) return '';
var bits = [];
if ((tk.assignee || '').trim()) bits.push('f\u00fcr ' + tk.assignee.trim());
if ((tk.created_by || '').trim()) bits.push('von ' + String(tk.created_by).trim());
if (((tk.source_thread_id || '').trim()) || ((tk.source_msg_id || '').trim())) bits.push('aus Chat');
if (!bits.length && tk.source_meta && String(tk.source_meta).trim())
return String(tk.source_meta).trim();
return bits.join(' \u00b7 ');
}
function todoDetailBodyShown(tk) {
var full = String(tk.text || '').trim();
if (!full) return '';
if (tk.title && tk.title.trim()) {
var tit = tk.title.trim();
var reStrip = new RegExp('^' + tit.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*(\\n|$)');
return full.replace(reStrip, '').trim().replace(/^[\n\r]+/, '');
}
var fl = firstLineTitle(full);
var head = full.split('\n')[0].trim();
if (head === fl) return full.slice(head.length).trim().replace(/^[\n\r]+/, '') || '';
return full;
}
function todoDisplayTitle(tk) {
if (tk.title && tk.title.trim()) return tk.title.trim();
return firstLineTitle(tk.text || '');
}
function ensureTasksPanelOpen() {
var p = document.getElementById('tasks-panel');
if (!p) return;
if (p.classList.contains('collapsed')) {
p.classList.remove('collapsed');
try { localStorage.setItem(tasksCollapsedStorageKey(), '0'); } catch (e2) {}
updateSidebarNavHighlight();
}
}
function fillTqpAssigneeSelect(sel, preferMatch) {
if (!sel) return;
sel.innerHTML = '';
var zo = document.createElement('option');
zo.value = '';
zo.textContent = '-- optional --';
sel.appendChild(zo);
var names = (practiceUsers || []).map(function(u) { return u && u.display_name; }).filter(Boolean);
names.sort(function(a,b){ return String(a).localeCompare(String(b), 'de'); });
names.forEach(function(nm) {
var o = document.createElement('option');
o.value = nm;
o.textContent = nm;
sel.appendChild(o);
});
if (preferMatch) {
var want = String(preferMatch || '').trim();
for (var i = 0; i < sel.options.length; i++) {
if (sel.options[i].value === want) {
sel.selectedIndex = i;
break;
}
}
}
}
function smartDefaultTaskTitle(rec) {
var peer = String(activeDmPeer || '').trim();
if (peer) return 'Aufgabe f\u00fcr ' + peer;
if (!currentSession) return firstLineTitle(rec.rawBody || '');
var me = currentSession.display_name.trim();
var asg = String(rec.assignGuess || '').trim();
if (asg && !namesRoughlyEqual(asg, me)) return 'Aufgabe f\u00fcr ' + asg;
if (me) return 'Aufgabe f\u00fcr ' + me;
return firstLineTitle(rec.rawBody || '');
}
function closeTaskQuickPanel() {
var pan = document.getElementById('task-quick-panel');
if (!pan) return;
pan.classList.add('hidden');
pan.setAttribute('aria-hidden', 'true');
}
function openTaskQuickPanelFromMsg(rec) {
bindTaskEscapeOnce();
var pan = document.getElementById('task-quick-panel');
if (!pan || !rec) return;
fillTqpAssigneeSelect(document.getElementById('tqp-assignee'), rec.assignGuess);
document.getElementById('tqp-msg-id').value = rec.msgId || '';
document.getElementById('tqp-thread-id').value = rec.threadId || '';
document.getElementById('tqp-title').value = smartDefaultTaskTitle(rec);
document.getElementById('tqp-body').value = rec.rawBody || '';
var metaTop = composeTaskMetaLine(rec.assignGuess || '', rec.chatLabel || '', '') +
'<br/><span style="opacity:.88">Nachricht von ' + esc(rec.senderLabel || '') +
((rec.when || '').trim() ? (' \u2013 ' + esc(String(rec.when).trim())) : '') + '</span>';
document.getElementById('tqp-source-line').innerHTML = metaTop;
pan.classList.remove('hidden');
pan.setAttribute('aria-hidden', 'false');
setTimeout(function() {
try {
document.getElementById('tqp-title').focus();
document.getElementById('tqp-title').select();
} catch (es) {}
}, 80);
}
async function submitTaskQuickPanel() {
var titEl = document.getElementById('tqp-title');
var bodyEl = document.getElementById('tqp-body');
var asgEl = document.getElementById('tqp-assignee');
if (!titEl || !bodyEl) return;
var mid = (document.getElementById('tqp-msg-id').value || '').trim();
var lu = (_azaMsgLookup || {})[mid] || {};
var tit = (titEl.value || '').trim();
var body = (bodyEl.value || '').trim();
if (!body) body = '(ohne weiteren Text)';
if (!tit) tit = firstLineTitle(body);
var asg = ((asgEl && asgEl.value) ? asgEl.value : '').trim() || (lu.assignGuess || '').trim();
try {
var res = await apiFetch(API_BASE + '/tasks', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: tit,
text: body,
assignee: ((asgEl && asgEl.value) ? asgEl.value : '').trim(),
source_msg_id: mid,
source_thread_id: (document.getElementById('tqp-thread-id').value || '').trim(),
source_meta: composeTaskMetaLine(
asg,
(lu.chatLabel || 'AzA-Empfang'),
(lu.when || '')
),
source_peer: activeDmPeer || ''
})
});
var dj = await res.json().catch(function() { return {}; });
var newId = dj.task && dj.task.task_id ? dj.task.task_id : '';
if (res.ok && newId) _taskHighlightId = newId;
ensureTasksPanelOpen();
closeTaskQuickPanel();
await loadTasksFromServer();
notifyOtherTabs();
if (_taskHighlightId) {
setTimeout(function() {
_taskHighlightId = null;
renderTodos();
}, 4600);
}
} catch (e5) {}
}
async function loadTasksFromServer() {
try {
var r = await apiFetch(API_BASE + '/tasks');
var d = await r.json();
serverTasks = d.tasks || [];
renderTodos();
} catch(e) {}
}
function onShowCompletedTasksChange() {
var cb = document.getElementById('tp-show-completed');
_showCompletedTasks = !!(cb && cb.checked);
try {
localStorage.setItem('aza_empfang_show_completed_tasks', _showCompletedTasks ? '1' : '0');
} catch (eCh) {}
renderTodos();
}
function buildTodoCardHtml(t) {
var tidEsc = String(t.task_id || '').replace(/'/g, "\\'");
var titleTxt = todoDisplayTitle(t);
var bodyExtra = todoDetailBodyShown(t);
var meta = taskCardMetaLine(t);
var hl = (_taskHighlightId && t.task_id === _taskHighlightId) ? ' todo-new-highlight' : '';
var letterMark = taskItemKind(t) === 'letter';
var kindBadge = letterMark ? '<span class="tc-kind-letter">Brief</span>' : '';
var softDone = (t.done && _showCompletedTasks) ? ' todo-soft-done' : '';
var stPrimary = !t.done
? '<button type="button" class="tc-btn-done" onclick="toggleTodo(\'' + tidEsc + '\')">Erledigt</button>'
: '<span class="tc-done-lbl">Erledigt</span>';
var menuReopen = t.done
? '<button type="button" onclick="toggleTodo(\'' + tidEsc + '\')">Wieder \u00f6ffnen</button>'
: '';
return '<div class="todo-card' + hl + (t.done ? ' todo-done' : '') + softDone + (letterMark ? ' todo-letter-card' : '') + '" data-task-id="' + escAttr(String(t.task_id || '')) + '">' +
kindBadge +
'<span class="tc-title" tabindex="0" contenteditable="' + (!t.done ? 'true' : 'false') + '" spellcheck="true" ' +
'onkeydown="if(event.key===\'Enter\'){event.preventDefault()}" ' +
'onblur="blurTodoTitle(\'' + tidEsc + '\',this)">' + esc(titleTxt) + '</span>' +
'<div class="tc-meta">' + esc(meta) + '</div>' +
'<div class="tc-body" tabindex="0" contenteditable="' + (!t.done ? 'true' : 'false') + '" spellcheck="true" ' +
'data-placeholder="Kontext" ' +
'onblur="blurTodoDetail(\'' + tidEsc + '\',this)">' + esc(bodyExtra) + '</div>' +
'<div class="tc-actions-bar">' + stPrimary +
'<details class="tc-more" onclick="event.stopPropagation()"><summary>\u2022\u2022\u2022</summary>' +
'<div class="tc-more-menu">' + menuReopen +
'<button type="button" onclick="assignTodo(\'' + tidEsc + '\')">Zuweisen\u2026</button>' +
'<button type="button" onclick="copyTodoBrief(\'' + tidEsc + '\')">Kopieren</button>' +
'<button type="button" class="danger" onclick="delTodo(\'' + tidEsc + '\')">L\u00f6schen</button>' +
'</div></details></div>' +
'</div>';
}
function renderTodos() {
var el = document.getElementById('todo-list');
if (!el) return;
syncTaskTabButtons();
var cb = document.getElementById('tp-show-completed');
if (cb) cb.checked = _showCompletedTasks;
var list = filteredTasksForUi();
var openL = [];
var doneL = [];
list.forEach(function(t) {
if (t.done) doneL.push(t);
else openL.push(t);
});
var fragments = [];
openL.forEach(function(t) { fragments.push(buildTodoCardHtml(t)); });
if (_showCompletedTasks) {
if (openL.length && doneL.length) {
fragments.push('<div class="tp-todo-sep" role="presentation"></div>');
}
doneL.forEach(function(t) { fragments.push(buildTodoCardHtml(t)); });
}
if (!fragments.length) {
var hint = 'Noch keine Aufgaben in dieser Ansicht.';
if (taskTabFilter === 'mine') hint = 'Keine Aufgaben f\u00fcr Sie zugewiesen.';
else if (taskTabFilter === 'sent') hint = 'Sie haben hier noch keine Aufgaben verteilt.';
else if (taskTabFilter === 'letters') hint = 'Keine Briefe offen.';
var hintFoot = 'Aus einer Chatnachricht \u00fcber „+ Aufgabe\u201c anlegen.';
if (taskTabFilter === 'letters') hintFoot = 'Brief \u00fcber „+ Brief\u201c in einer Nachricht anlegen.';
if (!_showCompletedTasks && !openL.length && doneL.length) {
if (taskTabFilter === 'letters') {
hint = 'Keine Briefe offen.';
hintFoot = 'Erledigte Briefe zeigen Sie mit „Erledigte anzeigen\u201c.';
} else {
hint = 'Keine offenen Aufgaben in dieser Ansicht.';
hintFoot = '„Erledigte anzeigen\u201c einblenden, um abgeschlossene Eintr\u00e4ge zu sehen.';
}
}
el.innerHTML =
'<div class="todo-empty-msg"><strong>' + esc(hint) + '</strong><br/>' +
hintFoot + '</div>';
return;
}
el.innerHTML = fragments.join('');
}
async function blurTodoTitle(taskId, box) {
var task = serverTasks.find(function(tt) { return String(tt.task_id) === taskId; });
if (!task || task.done) return;
var nw = (box.textContent || '').trim();
if (!nw) { renderTodos(); return; }
var prev = todoDisplayTitle(task);
if (nw === prev) return;
try {
await apiFetch(API_BASE + '/tasks/' + taskId + '/update', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({title: nw})
});
await loadTasksFromServer();
} catch(e) {}
}
async function blurTodoDetail(taskId, box) {
var task = serverTasks.find(function(tt) { return String(tt.task_id) === taskId; });
if (!task || task.done) return;
var titleKept = todoDisplayTitle(task);
var nw = (box.textContent || '').trim();
/* Voller Kartentext: Titel optional getrennt */
var fullText = (titleKept + (nw ? '\n\n' + nw : '')).trim();
if (fullText === String(task.text || '').trim()) return;
try {
await apiFetch(API_BASE + '/tasks/' + taskId + '/update', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({text: fullText})
});
await loadTasksFromServer();
} catch(e) {}
}
function copyTodoBrief(taskId) {
var tk = serverTasks.find(function(tt) { return String(tt.task_id) === taskId; });
if (!tk) return;
var s = todoDisplayTitle(tk) + '\n\n' + String(tk.text || '').trim();
try { navigator.clipboard.writeText(s).catch(function(){}); } catch (ecl) {}
}
function toggleTodoInput() {
var row = document.getElementById('todo-input-row');
var inp = document.getElementById('todo-inline-input');
var isOpen = row.classList.toggle('open');
if (isOpen) { inp.value = ''; inp.focus(); }
}
async function addTodoInline() {
var inp = document.getElementById('todo-inline-input');
var text = (inp.value || '').trim();
if (!text) return;
try {
await apiFetch(API_BASE + '/tasks', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: firstLineTitle(text),
text: text,
assignee: ''
})
});
inp.value = '';
inp.focus();
await loadTasksFromServer();
notifyOtherTabs();
} catch(e) {}
}
async function toggleTodo(taskId) {
var task = serverTasks.find(function(t) { return String(t.task_id) === taskId; });
if (!task) return;
try {
await apiFetch(API_BASE + '/tasks/' + taskId + '/update', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({done: !task.done})
});
await loadTasksFromServer();
notifyOtherTabs();
} catch(e) {}
}
async function delTodo(taskId) {
try {
await apiFetch(API_BASE + '/tasks/' + taskId, {method: 'DELETE'});
await loadTasksFromServer();
notifyOtherTabs();
} catch(e) {}
}
async function assignTodo(taskId) {
var task = serverTasks.find(function(t) { return String(t.task_id) === taskId; });
if (!task) return;
var names = practiceUsers.map(function(u) { return u.display_name; }).join(', ');
var name = prompt('Zuweisen an (' + (names || 'keine bekannt') + '):', task.assignee || '');
if (name === null) return;
try {
await apiFetch(API_BASE + '/tasks/' + taskId + '/update', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({assignee: name.trim()})
});
await loadTasksFromServer();
notifyOtherTabs();
} catch(e) {}
}
/* ===================================================================
SETTINGS (Practice info)
=================================================================== */
function updateSbUsersPracticeHint(pid, pname) {
var h = document.getElementById('sb-users-hint');
if (!h) return;
if (!currentSession || !(pid || '').trim()) {
h.textContent = '';
return;
}
var label = ((pname || '').trim()) || 'Praxis-Chat';
h.textContent = 'Gleicher Datenraum wie in AzA («An Empfang senden»). Namen entsprechen dem gespeicherten Anzeigenamen — Praxis: ' + label + '.';
}
function applyHeaderPracticeSub(d) {
var sub = document.getElementById('header-practice-sub');
if (!sub) return;
var pname = String((d && d.practice_name) || '').trim() || 'Praxis-Chat';
var pid = String((d && d.practice_id) || '').trim();
var shortId = pid ? (pid.length > 14 ? pid.slice(0, 12) + '\u2026' : pid) : '';
if (!currentSession) {
sub.textContent = '';
return;
}
if (!pid) {
sub.textContent = pname + '\u2003·\u2003Praxiszuordnung pr\u00fcfen';
return;
}
sub.textContent = pname + (shortId ? '\u2003·\u2003ID ' + shortId : '') + '\u2003·\u2003mit Praxis-Chat verbunden';
}
async function loadPracticeInfo() {
try {
var r = await apiFetch(API_BASE + '/practice/info');
var d = await r.json();
var el = document.getElementById('practice-info-section');
if (!el) return;
var role = currentSession ? String(currentSession.role || '').toLowerCase() : '';
var isAdmin = role === 'admin';
var canSeeInvite = isAdmin || role === 'empfang';
var pname = String(d.practice_name || '').trim();
var showTitle = pname || 'Praxis-Chat';
var pidVal = String(d.practice_id || '').trim();
var codeVal = String(d.invite_code || '').trim();
lastPracticeInfoForHint = d;
var statusTxt = pidVal
? 'Mit Praxis-Chat verbunden. Konten entsprechen der Hauptsoftware («An Empfang senden», dieselbe Schnittstelle auf dem Server).'
: 'Praxiszuordnung unklar \u2014 bitte neu anmelden oder Ger\u00e4te-Verkn\u00fcpfung in den Einstellungen pr\u00fcfen.';
try {
if (sessionStorage.getItem('aza_practice_mismatch_cleared') === '1') {
statusTxt = 'Praxis wurde an den Einladungscode aus der Hauptinstallation angeglichen. Bitte erneut mit Ihrem Praxis-Benutzer anmelden.';
sessionStorage.removeItem('aza_practice_mismatch_cleared');
}
} catch(eM) {}
var bindSrc = '';
try { bindSrc = sessionStorage.getItem('aza_practice_bind') || ''; } catch(eB0) {}
var bindHuman = bindSrc === 'invite_code'
? '\u00dcber Einladungscode (URL oder Anmeldung) \u2014 ma\u00dfgeblich'
: (bindSrc === 'stored_practice_id'
? 'Gespeicherte Praxis-ID im Browser'
: (bindSrc === 'account'
? 'Benutzerkonto / Server-Session'
: 'Browser-Sitzung'));
var html = '';
html += '<div class="practice-identity-card">';
html += '<div class="pi-main">' + esc(showTitle) + '</div>';
html += '<div class="pi-row"><span class="pi-k">Practice-ID</span> <span class="pi-v pi-mono">' + esc(pidVal || '\u2014') + '</span></div>';
html += '<div class="pi-row"><span class="pi-k">Quelle</span> <span class="pi-v">' + esc(bindHuman) + '</span></div>';
if (codeVal && canSeeInvite && currentSession) {
html += '<div class="pi-row"><span class="pi-k">Einladung</span>';
html += '<span class="pi-code" id="invite-code-display">' + esc(codeVal) + '</span>';
html += '<button type="button" class="btn-test-small" onclick="copyInviteCode()">Kopieren</button>';
if (isAdmin) {
html += '<button type="button" class="btn-test-small" onclick="regenerateInviteCode()">Neuen Code</button>';
}
html += '</div>';
html += '<p class="pi-hint">F\u00fcr neue Mitarbeiter: Code weitergeben oder Link mit diesem Code verschicken.</p>';
}
html += '<p class="pi-status">' + esc(statusTxt) + '</p>';
html += '</div>';
el.innerHTML = html;
applyHeaderPracticeSub(d);
updateSbUsersPracticeHint(pidVal, pname);
var adminLink = document.getElementById('admin-panel-link');
if (adminLink) {
adminLink.style.display = isAdmin ? 'block' : 'none';
}
refreshSettingsUserSection();
} catch(e) {}
}
function copyInviteCode() {
var el = document.getElementById('invite-code-display');
if (el) navigator.clipboard.writeText(el.textContent.trim()).catch(function(){});
}
async function regenerateInviteCode() {
if (!confirm('Neuen Einladungscode erzeugen?\nDer alte Code wird ungueltig.')) return;
try {
var r = await apiFetch(API_BASE + '/auth/regenerate_invite', {method:'POST'});
var d = await r.json();
if (d.success) loadPracticeInfo();
} catch(e) {}
}
/* ===================================================================
ADMIN PANEL
=================================================================== */
function openAdmin() {
document.getElementById('admin-overlay').classList.add('open');
showAdminTab('praxis');
}
function closeAdmin() {
document.getElementById('admin-overlay').classList.remove('open');
}
function showAdminTab(tab) {
var tabs = document.querySelectorAll('.admin-tab');
var tabNames = ['praxis','users','devices','channels','federation'];
tabs.forEach(function(t, i) { t.classList.toggle('active', tabNames[i] === tab); });
if (tab === 'praxis') loadAdminPraxis();
else if (tab === 'users') loadAdminUsers();
else if (tab === 'devices') loadAdminDevices();
else if (tab === 'channels') loadAdminChannels();
else if (tab === 'federation') loadAdminFederation();
}
async function loadAdminPraxis() {
var body = document.getElementById('admin-body');
try {
var r = await apiFetch(API_BASE + '/practice/info');
var d = await r.json();
var html = '<div class="admin-section-title">Praxis-Informationen</div>';
html += '<div class="admin-info-row"><span class="admin-info-label">Praxisname:</span><span class="admin-info-value">' + esc(d.practice_name || '-') + '</span></div>';
html += '<div class="admin-info-row"><span class="admin-info-label">Benutzer:</span><span class="admin-info-value">' + d.user_count + '</span></div>';
html += '<div class="admin-info-row"><span class="admin-info-label">Nachrichten:</span><span class="admin-info-value">' + d.message_count + ' (' + d.open_count + ' offen)</span></div>';
if (d.admin_email) {
html += '<div class="admin-info-row"><span class="admin-info-label">Admin-E-Mail:</span><span class="admin-info-value">' + esc(d.admin_email) + '</span></div>';
}
if (d.invite_code) {
html += '<div class="admin-section-title" style="margin-top:20px">Einladungslink</div>';
html += '<div class="admin-info-row"><span class="admin-info-label">Code:</span><span class="admin-mono">' + esc(d.invite_code) + '</span>';
html += ' <button class="admin-btn" onclick="copyInviteCode()">Kopieren</button>';
html += ' <button class="admin-btn" onclick="regenerateInviteCode().then(function(){loadAdminPraxis()})">Neuen Code</button></div>';
var link = window.location.origin + '/empfang/?invite=' + encodeURIComponent(d.invite_code);
html += '<div class="admin-info-row"><span class="admin-info-label">Link:</span><span class="admin-mono" style="font-size:.75rem;word-break:break-all">' + esc(link) + '</span>';
html += ' <button class="admin-btn" onclick="navigator.clipboard.writeText(\'' + link.replace(/'/g, "\\'") + '\')">Link kopieren</button></div>';
html += '<div style="font-size:.78rem;color:#8a9aaa;margin-top:6px">Diesen Link an Mitarbeiter senden, damit sie sich registrieren k\u00f6nnen.</div>';
}
body.innerHTML = html;
} catch(e) { body.innerHTML = '<div class="admin-empty">Fehler beim Laden.</div>'; }
}
async function loadAdminUsers() {
var body = document.getElementById('admin-body');
body.innerHTML = '<div class="admin-empty">Laden...</div>';
try {
var r = await apiFetch(API_BASE + '/admin/users');
var d = await r.json();
var users = d.users || [];
var html = '<div class="admin-section-title">Benutzer (' + users.length + ')</div>';
if (!users.length) { html += '<div class="admin-empty">Keine Benutzer</div>'; body.innerHTML = html; return; }
html += '<table class="admin-table"><thead><tr><th>Name</th><th>Rolle</th><th>Status</th><th>Letzter Login</th><th>Aktionen</th></tr></thead><tbody>';
users.forEach(function(u) {
var roleClass = u.role || 'mpa';
var statusClass = u.status === 'deactivated' ? 'deactivated' : 'active';
html += '<tr>';
html += '<td><strong>' + esc(u.display_name) + '</strong>' + (u.email ? '<br><span style="font-size:.72rem;color:#8a9aaa">' + esc(u.email) + '</span>' : '') + '</td>';
html += '<td><span class="admin-badge ' + roleClass + '">' + esc(u.role) + '</span></td>';
html += '<td><span class="admin-badge ' + statusClass + '">' + (u.status === 'deactivated' ? 'Deaktiviert' : 'Aktiv') + '</span></td>';
html += '<td style="font-size:.75rem;color:#8a9aaa">' + esc(u.last_login || '-') + '</td>';
html += '<td style="white-space:nowrap">';
if (u.user_id !== currentSession.user_id) {
html += '<select onchange="changeUserRole(\'' + u.user_id + '\',this.value)" style="font-size:.75rem;padding:2px 4px;border:1px solid #ddd;border-radius:3px;margin-right:4px">';
['arzt','mpa','empfang'].forEach(function(ro) {
html += '<option value="' + ro + '"' + (u.role === ro ? ' selected' : '') + '>' + ro + '</option>';
});
html += '</select>';
if (u.status === 'deactivated') {
html += '<button class="admin-btn success" onclick="activateUser(\'' + u.user_id + '\')">Aktivieren</button> ';
} else {
html += '<button class="admin-btn" onclick="deactivateUser(\'' + u.user_id + '\')">Deaktivieren</button> ';
}
html += '<button class="admin-btn" onclick="resetPassword(\'' + u.user_id + '\',\'' + esc(u.display_name).replace(/'/g,"\\'") + '\')">PW Reset</button> ';
html += '<button class="admin-btn danger" onclick="deleteUser(\'' + u.user_id + '\',\'' + esc(u.display_name).replace(/'/g,"\\'") + '\')">L\u00f6schen</button>';
} else {
html += '<span style="font-size:.75rem;color:#8a9aaa">(Sie)</span>';
}
html += '</td></tr>';
});
html += '</tbody></table>';
body.innerHTML = html;
} catch(e) { body.innerHTML = '<div class="admin-empty">Fehler beim Laden.</div>'; }
}
async function changeUserRole(userId, newRole) {
try {
await apiFetch(API_BASE + '/admin/users/' + userId + '/role', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({role: newRole})
});
loadAdminUsers();
} catch(e) {}
}
async function deactivateUser(userId) {
if (!confirm('Benutzer wirklich deaktivieren?')) return;
try {
await apiFetch(API_BASE + '/admin/users/' + userId + '/deactivate', {method:'POST'});
loadAdminUsers();
} catch(e) {}
}
async function activateUser(userId) {
try {
await apiFetch(API_BASE + '/admin/users/' + userId + '/activate', {method:'POST'});
loadAdminUsers();
} catch(e) {}
}
async function resetPassword(userId, userName) {
var p1 = window.prompt('Neues Passwort fuer ' + userName + ' (min. 4 Zeichen):');
if (!p1 || String(p1).length < 4) return;
var p2 = window.prompt('Neues Passwort erneut eingeben:');
if (p1 !== p2) {
alert('Die Passwoerter stimmen nicht ueberein.');
return;
}
try {
var r = await apiFetch(API_BASE + '/admin/users/' + userId + '/reset_password', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({new_password: p1})
});
if (!r.ok) {
try {
var dj = await r.json();
alert(dj.detail || 'Passwort konnte nicht gespeichert werden.');
} catch (e2) { alert('Passwort konnte nicht gespeichert werden.'); }
return;
}
alert('Passwort wurde gespeichert.');
loadAdminUsers();
} catch(e) { alert('Netzwerkfehler.'); }
}
async function deleteUser(userId, userName) {
if (!confirm('Benutzer "' + userName + '" endg\u00fcltig l\u00f6schen?\n\nDiese Aktion kann nicht r\u00fcckg\u00e4ngig gemacht werden.')) return;
try {
await apiFetch(API_BASE + '/admin/users/' + userId, {method:'DELETE'});
loadAdminUsers();
loadUsersFromServer();
} catch(e) {}
}
async function loadAdminDevices() {
var body = document.getElementById('admin-body');
body.innerHTML = '<div class="admin-empty">Laden...</div>';
try {
var r = await apiFetch(API_BASE + '/admin/devices');
var d = await r.json();
var devices = d.devices || [];
var html = '<div class="admin-section-title">Ger\u00e4te (' + devices.length + ')</div>';
if (!devices.length) { html += '<div class="admin-empty">Keine Ger\u00e4te registriert</div>'; body.innerHTML = html; return; }
html += '<table class="admin-table"><thead><tr><th>Benutzer</th><th>Ger\u00e4t</th><th>Typ</th><th>Letzter Zugriff</th><th>Status</th><th>Aktionen</th></tr></thead><tbody>';
devices.forEach(function(dev) {
var trustClass = dev.trust_status === 'blocked' ? 'blocked' : 'trusted';
html += '<tr>';
html += '<td>' + esc(dev.user_name || dev.user_id) + '</td>';
html += '<td>' + esc(dev.device_name) + '</td>';
html += '<td style="font-size:.75rem">' + esc(dev.platform) + ' / ' + esc(dev.device_type) + '</td>';
html += '<td style="font-size:.75rem;color:#8a9aaa">' + esc(dev.last_active || '-') + '</td>';
html += '<td><span class="admin-badge ' + trustClass + '">' + esc(dev.trust_status) + '</span></td>';
html += '<td>';
if (dev.trust_status !== 'blocked') {
html += '<button class="admin-btn danger" onclick="blockDevice(\'' + dev.device_id + '\')">Sperren</button> ';
}
html += '<button class="admin-btn danger" onclick="deleteDevice(\'' + dev.device_id + '\')">L\u00f6schen</button>';
html += '</td></tr>';
});
html += '</tbody></table>';
body.innerHTML = html;
} catch(e) { body.innerHTML = '<div class="admin-empty">Fehler beim Laden.</div>'; }
}
async function blockDevice(deviceId) {
try {
await apiFetch(API_BASE + '/admin/devices/' + deviceId + '/block', {method:'POST'});
loadAdminDevices();
} catch(e) {}
}
async function deleteDevice(deviceId) {
if (!confirm('Ger\u00e4t entfernen?')) return;
try {
await apiFetch(API_BASE + '/admin/devices/' + deviceId, {method:'DELETE'});
loadAdminDevices();
} catch(e) {}
}
async function loadAdminChannels() {
var body = document.getElementById('admin-body');
body.innerHTML = '<div class="admin-empty">Laden...</div>';
try {
var r = await apiFetch(API_BASE + '/channels');
var d = await r.json();
var channels = d.channels || [];
var html = '<div class="admin-section-title">Kan\u00e4le (' + channels.length + ')</div>';
html += '<table class="admin-table"><thead><tr><th>Name</th><th>Typ</th><th>Rollen</th><th>Aktionen</th></tr></thead><tbody>';
channels.forEach(function(ch) {
html += '<tr>';
html += '<td><strong>' + esc(ch.name) + '</strong></td>';
html += '<td><span class="admin-badge ' + (ch.scope === 'external' ? 'pending_conn' : 'active') + '">' + esc(ch.scope) + '</span></td>';
html += '<td style="font-size:.75rem">' + (ch.allowed_roles && ch.allowed_roles.length ? esc(ch.allowed_roles.join(', ')) : 'Alle') + '</td>';
html += '<td>';
if (ch.channel_type !== 'external') {
html += '<button class="admin-btn danger" onclick="deleteChannel(\'' + ch.channel_id + '\',\'' + esc(ch.name).replace(/'/g,"\\'") + '\')">L\u00f6schen</button>';
}
html += '</td></tr>';
});
html += '</tbody></table>';
html += '<div class="admin-add-row">';
html += '<input type="text" id="new-channel-name" placeholder="Neuer Kanalname...">';
html += '<select id="new-channel-roles"><option value="">Alle Rollen</option><option value="arzt,admin">Nur \u00c4rzte</option><option value="mpa,admin">Nur MPA</option><option value="empfang,admin">Nur Empfang</option></select>';
html += '<button class="admin-btn success" onclick="createChannel()">Kanal erstellen</button>';
html += '</div>';
body.innerHTML = html;
} catch(e) { body.innerHTML = '<div class="admin-empty">Fehler beim Laden.</div>'; }
}
async function createChannel() {
var name = (document.getElementById('new-channel-name').value || '').trim();
if (!name) return;
var rolesStr = document.getElementById('new-channel-roles').value;
var roles = rolesStr ? rolesStr.split(',') : [];
try {
await apiFetch(API_BASE + '/channels', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({name: name, allowed_roles: roles})
});
loadAdminChannels();
} catch(e) {}
}
async function deleteChannel(channelId, channelName) {
if (!confirm('Kanal "' + channelName + '" l\u00f6schen?')) return;
try {
await apiFetch(API_BASE + '/channels/' + channelId, {method:'DELETE'});
loadAdminChannels();
} catch(e) {}
}
async function loadAdminFederation() {
var body = document.getElementById('admin-body');
body.innerHTML = '<div class="admin-empty">Laden...</div>';
try {
var r = await apiFetch(API_BASE + '/federation/connections');
var d = await r.json();
var conns = d.connections || [];
var html = '<div class="admin-section-title">Praxis-Verbindungen</div>';
if (conns.length) {
html += '<table class="admin-table"><thead><tr><th>Verbundene Praxis</th><th>Status</th><th>Seit</th><th>Aktionen</th></tr></thead><tbody>';
conns.forEach(function(c) {
var otherName = c.practice_a_name || c.practice_b_name || '-';
if (c.practice_a_id === currentSession.practice_id) otherName = c.practice_b_name || '(ausstehend)';
else otherName = c.practice_a_name || '-';
var statusClass = c.status === 'active' ? 'active_conn' : (c.status === 'pending' ? 'pending_conn' : 'revoked');
html += '<tr>';
html += '<td>' + esc(otherName) + '</td>';
html += '<td><span class="admin-badge ' + statusClass + '">' + esc(c.status) + '</span></td>';
html += '<td style="font-size:.75rem;color:#8a9aaa">' + esc(c.created_at || '-') + '</td>';
html += '<td>';
if (c.status === 'active' || c.status === 'pending') {
html += '<button class="admin-btn danger" onclick="revokeConnection(\'' + c.connection_id + '\')">Trennen</button>';
}
html += '</td></tr>';
});
html += '</tbody></table>';
} else {
html += '<div class="admin-empty">Keine Verbindungen zu anderen Praxen</div>';
}
html += '<div class="admin-section-title" style="margin-top:24px">Neue Verbindung</div>';
html += '<div class="admin-add-row">';
html += '<button class="admin-btn success" onclick="createFederationInvite()">Einladung erstellen</button>';
html += '<input type="text" id="accept-federation-token" placeholder="Einladungscode einer anderen Praxis...">';
html += '<button class="admin-btn" onclick="acceptFederationInvite()">Verbindung annehmen</button>';
html += '</div>';
html += '<div id="federation-invite-result" style="margin-top:8px"></div>';
body.innerHTML = html;
} catch(e) { body.innerHTML = '<div class="admin-empty">Fehler beim Laden.</div>'; }
}
async function createFederationInvite() {
try {
var r = await apiFetch(API_BASE + '/federation/invite', {method:'POST', headers:{'Content-Type':'application/json'}, body:'{}'});
var d = await r.json();
if (d.invite_token) {
var el = document.getElementById('federation-invite-result');
el.innerHTML = '<div class="admin-info-row"><span class="admin-info-label">Einladungscode:</span><span class="admin-mono">' + esc(d.invite_token) + '</span> <button class="admin-btn" onclick="navigator.clipboard.writeText(\'' + d.invite_token + '\')">Kopieren</button></div>' +
'<div style="font-size:.78rem;color:#8a9aaa">Diesen Code an den Admin der anderen Praxis weitergeben.</div>';
}
} catch(e) {}
}
async function acceptFederationInvite() {
var token = (document.getElementById('accept-federation-token').value || '').trim();
if (!token) return;
try {
var r = await apiFetch(API_BASE + '/federation/accept', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({invite_token: token})
});
var d = await r.json();
if (d.success) {
document.getElementById('accept-federation-token').value = '';
loadAdminFederation();
} else {
alert(d.detail || 'Verbindung fehlgeschlagen.');
}
} catch(e) { alert('Fehler bei der Verbindung.'); }
}
async function revokeConnection(connId) {
if (!confirm('Verbindung wirklich trennen?')) return;
try {
await apiFetch(API_BASE + '/federation/connections/' + connId + '/revoke', {method:'POST'});
loadAdminFederation();
} catch(e) {}
}
/* ===================================================================
UTILITY
=================================================================== */
function esc(s) {
var d = document.createElement('div');
d.textContent = s || '';
return d.innerHTML;
}
function copyText(el) {
var t = (el.textContent || '').trim();
if (!t) return;
navigator.clipboard.writeText(t).then(function(){
el.classList.add('copied-flash');
setTimeout(function(){ el.classList.remove('copied-flash'); }, 600);
});
}
/* ===================================================================
POLLING + CROSS-TAB SYNC
- Schneller Live-Pulse alle 1 s gegen /empfang/pulse: nur dann
loadMessages(), wenn der Server-Tick wechselt. Damit erscheinen
neue Nachrichten und das Tonsignal in unter 1 s, statt nach 10 s
wie bei der alten setInterval-Lösung.
- Fallback: alle 15 s sicherheitshalber komplett neu laden.
=================================================================== */
var lastPulseTick = -1;
async function empfangPresencePing() {
if (!currentSession) return;
try {
var r = await fetch(API_BASE + '/presence/ping', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: '{}',
cache: 'no-store',
});
if (r.status === 401) return;
if (!r.ok && isDesktopShellMode()) {
try { console.debug('[Empfang] presence/ping HTTP', r.status); } catch (eD) {}
}
} catch (e) {
if (isDesktopShellMode()) {
try { console.debug('[Empfang] presence/ping error', e); } catch (e2) {}
}
}
}
async function pulseTick() {
try {
var pulseUrl = API_BASE + '/pulse';
var ppid = currentSession && String(currentSession.practice_id || '').trim();
if (ppid) pulseUrl += '?practice_id=' + encodeURIComponent(ppid);
var r = await apiFetch(pulseUrl);
if (!r || !r.ok) return;
var d = await r.json();
var t = parseInt(d.tick || 0, 10);
if (t !== lastPulseTick) {
lastPulseTick = t;
loadMessages();
}
} catch(e) {}
}
function startPolling() {
stopPolling();
// Live-Pulse: <1 s Reaktion auf neue Nachrichten.
pollTimer = setInterval(pulseTick, 1000);
empfangPresencePing();
presencePingTimer = setInterval(empfangPresencePing, EMPFANG_PRESENCE_PING_MS);
// Sicherheits-Refresh + Aufgaben/Benutzer.
fallbackTimer = setInterval(function() {
loadMessages();
loadTasksFromServer();
if (isDesktopShellMode() && typeof refreshDesktopShellContextFromServer === 'function') {
try { refreshDesktopShellContextFromServer(false); } catch (_e) {}
}
}, 15000);
userPollTimer = setInterval(loadUsersFromServer, 30000);
// Sofort einmal anstossen.
pulseTick();
}
function stopPolling() {
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
if (fallbackTimer) { clearInterval(fallbackTimer); fallbackTimer = null; }
if (userPollTimer) { clearInterval(userPollTimer); userPollTimer = null; }
if (presencePingTimer) { clearInterval(presencePingTimer); presencePingTimer = null; }
}
document.addEventListener('visibilitychange', function() {
if (!document.hidden && currentSession) {
lastDataHash = '';
empfangPresencePing();
loadMessages();
loadUsersFromServer();
loadTasksFromServer();
if (isDesktopShellMode() && typeof refreshDesktopShellContextFromServer === 'function') {
try { refreshDesktopShellContextFromServer(false); } catch (_e) {}
}
}
});
var bc = null;
try {
bc = new BroadcastChannel('aza_empfang_sync');
bc.onmessage = function(ev) {
if (ev.data === 'reload' && currentSession) {
lastDataHash = '';
loadMessages();
loadTasksFromServer();
}
};
} catch(e) {}
function notifyOtherTabs() {
try { if (bc) bc.postMessage('reload'); } catch(e) {}
}
try {
window.addEventListener('pywebviewready', function() { initShellPinUi(); });
} catch (e) {}
</script>
</body>
</html>