Files
2026-05-12 01:21:25 +02:00

7366 lines
310 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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}
/* === MiniChat (?minichat=1): kompakt; Seitenleisten wie Desktop-Huelle einklappbar === */
html[data-minichat="1"] #app-layout{min-width:0}
html[data-minichat="1"] #chat-area{flex:1 1 auto;min-width:0;max-width:100%}
html[data-minichat="1"] #chat-pane,
html[data-minichat="1"] #middle-stack{min-width:0}
html[data-minichat="1"] .conv-msg-stack,
html[data-minichat="1"] .messenger-stream{max-width:100%}
html[data-minichat="1"] .conv-msg-slot.align-left,
html[data-minichat="1"] .conv-msg-slot.align-right{max-width:100%}
html[data-minichat="1"] #reply-text{min-width:0}
/* MiniChat: Nav-Toggles sichtbar wie Desktop-Huelle */
html[data-minichat="1"] #nr-sidebar-ds,
html[data-minichat="1"] #nr-tasks,
html[data-minichat="1"] #nr-dcp-context{display:flex}
html[data-minichat="1"] .desktop-mode-pill{display:inline-flex}
/* 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-pending-badge{min-width:20px;padding:2px 8px;border-radius:10px;background:#e53935;color:#fff;font-size:.7rem;font-weight:700;line-height:1.25;margin-left:auto;flex-shrink:0;font-variant-numeric:tabular-nums;box-shadow:0 1px 4px rgba(229,57,53,.35)}
.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,
html[data-minichat="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,
html[data-minichat="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,
html[data-minichat="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}
/* Eingehende, mit OK quittierte Nachricht: dezent pastellgruen markieren.
Wird ausschliesslich auf eingehenden (from-other) Bubbles gesetzt;
eigene Nachrichten erhalten in keinem Fall die ack-Klasse. */
.conv-msg.conv-msg-chat-ack{
background:linear-gradient(180deg,#f4fbf6,#e8f4ec)!important;
border-color:#c8e3cf!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 und MiniChat: Aufgaben einklappbar mit schmaler wiederaufklappbarer Rippe */
html[data-desktop-shell="1"] #tasks-panel.collapsed,
html[data-minichat="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),
html[data-minichat="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,
html[data-minichat="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,
html[data-minichat="1"] #tasks-panel:not(.collapsed) .tp-ds-collapse-btn{display:inline-flex;align-items:center;justify-content:center}
/* Desktop-Hülle und MiniChat: linke Spalte nur per Dreieck in Nav-Leiste */
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed,
html[data-minichat="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,
html[data-minichat="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,
html[data-minichat="1"] #sidebar.sidebar-ds-collapsed .sb-me-name,
html[data-minichat="1"] #sidebar.sidebar-ds-collapsed #sb-me-role,
html[data-minichat="1"] #sidebar.sidebar-ds-collapsed .sb-me-change{display:none!important}
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed .sb-me,
html[data-minichat="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,
html[data-minichat="1"] #chat-pane .desktop-context-panel,
html[data-minichat="1"] #middle-stack .desktop-context-panel,
html[data-minichat="1"] #conversation-scroll .desktop-context-panel{display:none!important}
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed .desktop-context-panel,
html[data-minichat="1"] #sidebar.sidebar-ds-collapsed .desktop-context-panel{display:none!important}
html[data-desktop-shell="1"] #sidebar.shell-dcp-off .desktop-context-panel,
html[data-minichat="1"] #sidebar.shell-dcp-off .desktop-context-panel{display:none!important}
html[data-desktop-shell="1"] #sidebar-scroll .desktop-context-panel,
html[data-minichat="1"] #sidebar-scroll .desktop-context-panel{display:block}
html[data-desktop-shell="1"] #sidebar-scroll > .desktop-context-panel,
html[data-minichat="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}
.desktop-shell-auth-hint{font-size:.82rem;line-height:1.45;color:#664d03;background:#fff3cd;border:1px solid #e6d08c;border-radius:10px;padding:10px 12px;margin:0 0 16px}
.empfang-handoff-overlay{position:fixed;inset:0;background:rgba(26,42,58,.45);z-index:220;display:flex;align-items:center;justify-content:center;padding:16px}
.empfang-handoff-overlay.hidden{display:none!important}
.empfang-handoff-box{background:#fff;border-radius:12px;padding:22px 24px;max-width:460px;width:100%;box-shadow:0 8px 32px rgba(0,0,0,.15)}
.empfang-handoff-box h3{font-size:1.1rem;color:#1a4d6d;margin-bottom:10px}
.empfang-handoff-box p{font-size:.85rem;color:#3a5a7a;margin-bottom:10px;line-height:1.45}
.empfang-handoff-code{font-family:'Consolas',monospace;font-size:1.4rem;font-weight:600;color:#1a4d6d;background:#eef4fb;border:1px solid #cddbeb;border-radius:8px;padding:12px;text-align:center;letter-spacing:2px;user-select:all;margin:6px 0 10px}
.empfang-handoff-meta{font-size:.78rem;color:#6a8a9a;margin-bottom:10px}
.empfang-handoff-btns{display:flex;gap:10px;justify-content:flex-end;margin-top:6px;flex-wrap:wrap}
.empfang-handoff-btns button{padding:8px 14px;border-radius:8px;font-size:.85rem;cursor:pointer;font-family:inherit}
.empfang-handoff-btns .ehb-cancel{background:#eef2f6;color:#3a5a7a;border:1px solid #d0dce8}
.empfang-handoff-btns .ehb-primary{background:#5B8DB3;color:#fff;border:none;font-weight:600}
.empfang-handoff-err{color:#842029;font-size:.82rem;min-height:1em;margin-top:6px}
.empfang-handoff-redeem-card{background:#eef4fb;border:1px solid #cddbeb;border-radius:10px;padding:10px 12px;margin:0 0 14px}
.empfang-handoff-redeem-card h4{font-size:.92rem;color:#1a4d6d;margin:0 0 6px}
.empfang-handoff-redeem-card p{font-size:.78rem;color:#3a5a7a;margin:0 0 6px;line-height:1.4}
.empfang-handoff-redeem-row{display:flex;gap:6px}
.empfang-handoff-redeem-row input{flex:1;border:1px solid #cddbeb;border-radius:6px;padding:6px 10px;font-family:'Consolas',monospace;font-size:.95rem;letter-spacing:1.5px;text-transform:uppercase}
.empfang-handoff-redeem-row button{background:#5B8DB3;color:#fff;border:none;border-radius:6px;padding:6px 12px;font-size:.85rem;font-weight:600;cursor:pointer;font-family:inherit}
.empfang-handoff-redeem-msg{font-size:.78rem;margin-top:6px;min-height:1em}
#empfang-shell-hint-banner.empfang-shell-hint-banner{background:#fff3cd;border-bottom:1px solid #e6d08c;color:#664d03;font-size:.88rem;line-height:1.45;padding:10px 16px;text-align:center;display:block}
#empfang-shell-hint-banner.empfang-shell-hint-banner.hidden{display:none!important}
.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-btn-secondary{background:#eef3f7;color:#3a5a72;border:1px solid #c5d4e0;margin-top:8px}
.login-btn-secondary:hover{background:#e4edf4}
.invite-landing-head{margin-bottom:12px}
.invite-landing-practice{font-size:1.02rem;color:#2a4a62;margin:6px 0}
.invite-landing-code{font-size:.85rem;color:#6a8a9a;margin:4px 0 10px}
.invite-mode-tabs{display:flex;gap:8px;margin:14px 0 12px}
.invite-tab{flex:1;padding:10px;border:1px solid #c5d4e0;border-radius:8px;background:#f4f7fa;cursor:pointer;font-family:inherit;font-size:.88rem;font-weight:600;color:#4a6a82}
.invite-tab.active{background:#5B8DB3;color:#fff;border-color:#5B8DB3}
.invite-panel{margin-top:4px}
.invite-panel.hidden{display:none!important}
.invite-session-conflict-box{padding:10px 0}
.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 type="button" class="hbtn" id="mini-browser-toggle-btn" onclick="onMiniBrowserToggleClick()"
title="Kompaktes MiniChat-Fenster (gleiche Oberfl&amp;auml;che und Daten)">MiniChat</button>
<button type="button" class="hbtn" id="empfang-handoff-btn" onclick="openEmpfangHandoffDialog()"
title="Verbindungscode f&uuml;r die installierte AzA Empfang Chat-H&uuml;lle erzeugen">Empfang-H&uuml;lle</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 id="empfang-shell-hint-banner" class="empfang-shell-hint-banner hidden" role="status" aria-live="polite"></div>
<div id="empfang-handoff-overlay" class="empfang-handoff-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="empfang-handoff-title">
<div class="empfang-handoff-box" id="empfang-handoff-box">
<h3 id="empfang-handoff-title">Mit AzA Empfang Chat-H&uuml;lle verbinden</h3>
<p>Geben Sie diesen einmaligen Verbindungscode in der installierten <strong>AzA Empfang Chat</strong>-H&uuml;lle ein. Sie wird damit serverseitig mit derselben Praxis und demselben Chat verbunden &mdash; ohne erneute Anmeldung.</p>
<div class="empfang-handoff-code" id="empfang-handoff-code">&hellip;</div>
<div class="empfang-handoff-meta" id="empfang-handoff-meta">Code wird erzeugt &hellip;</div>
<div class="empfang-handoff-err" id="empfang-handoff-err"></div>
<div class="empfang-handoff-btns">
<button type="button" class="ehb-cancel" onclick="closeEmpfangHandoffDialog()">Schliessen</button>
<button type="button" class="ehb-primary" id="empfang-handoff-copy-btn" onclick="copyEmpfangHandoffCode()">Code kopieren</button>
<button type="button" class="ehb-primary" id="empfang-handoff-renew-btn" onclick="renewEmpfangHandoffCode()" style="display:none">Neuen Code erzeugen</button>
</div>
</div>
</div>
<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="5000">Kontinuierlich (Standard, alle 5 Sek.)</option>
<option value="2000">Kontinuierlich (alle 2 Sek.)</option>
<option value="30000">Wiederholend (alle 30 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 ''; }
}
/** Chat-Auswahl in localStorage loeschen (keine Serverdaten), z. B. nach Praxiswechsel per Einladung. */
function clearEmpfangLocalSelectionAfterPracticeSwitch() {
try {
var toRemove = [];
var i;
for (i = 0; i < localStorage.length; i++) {
var k = localStorage.key(i);
if (!k) continue;
if (k.indexOf('aza_empfang_last_direct_peer::') === 0) toRemove.push(k);
}
for (i = 0; i < toRemove.length; i++) {
try { localStorage.removeItem(toRemove[i]); } catch (_e) {}
}
} catch (_e2) {}
_dmPeerRestoreDoneKey = '';
}
function setInviteLandingMode(mode) {
var n = document.getElementById('invite-panel-new');
var e = document.getElementById('invite-panel-exist');
var tNew = document.getElementById('invite-tab-new');
var tEx = document.getElementById('invite-tab-exist');
if (!n || !e) return;
var isNew = mode === 'new';
n.classList.toggle('hidden', !isNew);
e.classList.toggle('hidden', isNew);
if (tNew) tNew.classList.toggle('active', isNew);
if (tEx) tEx.classList.toggle('active', !isNew);
}
function renderInviteLandingForm(resolvedInv, inviteCode) {
var box = document.getElementById('login-box');
box.className = 'login-box login-invite-landing';
var pname = String((resolvedInv && resolvedInv.practice_name) || '').trim() || 'Praxis';
var codeEsc = esc(inviteCode);
var lastUser = '';
try { lastUser = localStorage.getItem('aza_last_login_user') || ''; } catch (_eLu) {}
box.innerHTML =
'<div class="invite-landing-head">' +
'<h2>Einladung</h2>' +
'<p class="invite-landing-practice">Einladung f\u00fcr: <strong>' + esc(pname) + '</strong></p>' +
'<p class="invite-landing-code">Einladungscode: <span class="admin-mono">' + codeEsc + '</span></p>' +
'</div>' +
desktopShellLoginHintHtml() +
empfangChatShellRedeemHtml() +
'<div class="invite-mode-tabs">' +
'<button type="button" id="invite-tab-new" class="invite-tab active" onclick="setInviteLandingMode(\'new\')">Ich bin neu</button>' +
'<button type="button" id="invite-tab-exist" class="invite-tab" onclick="setInviteLandingMode(\'exist\')">Ich habe bereits einen Zugang</button>' +
'</div>' +
'<div id="invite-panel-new" class="invite-panel">' +
'<p style="font-size:.82rem;color:#6a8a9a">Neues Empfang-Konto nur f\u00fcr diese Praxis.</p>' +
'<div class="login-field"><label>Einladungscode</label><input type="text" id="reg-code" readonly class="admin-mono" value="' + codeEsc + '"></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 type="button" class="login-btn" onclick="doRegister()">Registrieren</button>' +
'</div>' +
'<div id="invite-panel-exist" class="invite-panel hidden">' +
'<p style="font-size:.82rem;color:#6a8a9a">Mit Benutzername oder E-Mail und Passwort an der eingeladenen Praxis anmelden (keine Neuregistrierung).</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">Mit dieser Praxis verbinden / anmelden</button>' +
'</form>' +
'</div>' +
'<div class="login-error" id="login-error"></div>';
var regNm = document.getElementById('reg-name');
if (regNm) { regNm.focus(); }
}
async function renderInviteVersusSessionConflict(me, resolvedInv) {
var box = document.getElementById('login-box');
box.className = 'login-box';
var curPname = '';
try {
var r = await fetch(API_BASE + '/practice/info?practice_id=' + encodeURIComponent(me.practice_id),
{ cache: 'no-store' });
var d = await r.json().catch(function () { return {}; });
curPname = String((d && d.practice_name) || '').trim();
} catch (_e) {}
var curLabel = curPname || ('Praxis-ID ' + String(me.practice_id || '').slice(0, 12));
var invName = String((resolvedInv && resolvedInv.practice_name) || '').trim();
var invLabel = invName || ('Praxis-ID ' + String((resolvedInv && resolvedInv.practice_id) || '').slice(0, 12));
box.innerHTML =
'<div class="invite-session-conflict-box">' +
'<h2>Einladung und aktuelle Anmeldung</h2>' +
'<p style="line-height:1.45">Sie sind aktuell mit <strong>' + esc(curLabel) + '</strong> verbunden. ' +
'Dieser Link l\u00e4dt Sie zu <strong>' + esc(invLabel) + '</strong> ein.</p>' +
'<button type="button" class="login-btn" onclick="inviteConflictSwitch()">Zu ' + esc(invLabel) + ' wechseln</button>' +
'<button type="button" class="login-btn login-btn-secondary" onclick="inviteConflictStay()">In ' + esc(curLabel) + ' bleiben</button>' +
'<p style="font-size:.78rem;color:#6a8a9a;margin-top:12px">Beim Wechsel werden Sie abgemeldet und k\u00f6nnen sich ' +
'anschliessend mit dem Einladungscode in der neuen Praxis anmelden.</p>' +
'<div class="login-error" id="login-error"></div></div>';
}
async function inviteConflictSwitch() {
clearEmpfangLocalSelectionAfterPracticeSwitch();
try { await fetch(API_BASE + '/auth/logout', { method: 'POST', credentials: 'include' }); } catch (_e) {}
currentSession = null;
try {
var inv = getUrlInviteCode();
if (inv) {
var ir = await fetch(API_BASE + '/auth/resolve_invite?code=' + encodeURIComponent(inv));
var rv = await ir.json().catch(function () { return null; });
if (rv && rv.valid && rv.practice_id) {
localStorage.setItem('aza_practice_id', rv.practice_id);
sessionStorage.setItem('aza_practice_bind', 'invite_code');
}
}
} catch (_e2) {}
window.location.reload();
}
function inviteConflictStay() {
try {
var u = new URL(window.location.href);
u.searchParams.delete('invite');
var q = u.search || '';
window.location.replace(u.pathname + q + u.hash);
} catch (_e) {
window.location.reload();
}
}
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);
}
/** OK-Button (mit Haekchen) erscheint nur an eingehenden fremden Nachrichten.
* Eigene gesendete Nachrichten brauchen keine Bestaetigung; daher ''. */
function htmlChatAckButton(mid, m, kind) {
if (!messageIsForeignIncoming(m)) return '';
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>';
}
/** Haekchen-Badge nur an eingehenden, bereits quittierten Nachrichten. */
function messageShouldShowAckBadge(m) {
return !!(m && messageHasChatAck(m) && messageIsForeignIncoming(m));
}
function messageExtrasObj(mm) {
return (mm && mm.extras && typeof mm.extras === 'object') ? mm.extras : null;
}
/**
* Strikt eingehende DM an mich (nie eigene Nachricht). Reine user_id-Logik
* wenn IDs vorhanden sind. Anzeigename wird nur als Fallback verwendet,
* wenn weder eigene noch fremde user_id ermittelbar sind. Ohne sicheres
* Wissen geben wir false zurück (kein Alarm bei Unsicherheit).
*/
function messageIsForeignIncoming(mm) {
if (!mm || !currentSession) return false;
var meUidStr = String((currentSession.user_id || '')).trim();
var ex = messageExtrasObj(mm);
var sUid = ex ? String(ex.sender_user_id || '').trim() : '';
var rUid = ex ? String(ex.recipient_user_id || '').trim() : '';
if (meUidStr && sUid) {
if (sUid === meUidStr) return false;
/* Wenn Recipient-UID bekannt: nur dann eingehend, wenn an mich adressiert. */
if (rUid && rUid !== meUidStr) return false;
return true;
}
/* Fallback: kein UID-Vergleich moeglich. Konservativ: nur als foreign werten,
wenn der Anzeigename eindeutig nicht mein eigener ist. */
var meName = String((currentSession.display_name || '')).trim();
var meNameLow = meName.toLowerCase();
var absRaw = String(mm.absender || '').trim();
var absCore = absRaw.split('(')[0].trim().toLowerCase();
if (!meName) return false;
if (absCore === meNameLow) return false;
if (absRaw.indexOf(meName) === 0) return false;
return true;
}
function messageNeedsAlarmAck(mm) {
return !!(mm && mm.id && messageIsForeignIncoming(mm) && !messageHasChatAck(mm));
}
function countLocalPendingInAllMessages() {
var n = 0;
(allMessages || []).forEach(function(m) {
if (messageNeedsAlarmAck(m)) n++;
});
return n;
}
function applyPulseDmPendingAck(d) {
try {
if (!d || !Object.prototype.hasOwnProperty.call(d, 'dm_pending_ack_by_peer')) return;
var bp = d.dm_pending_ack_by_peer;
if (bp == null || typeof bp !== 'object') return;
globalDmPendingAckByPeer = bp;
var tt = parseInt(String(d.dm_pending_ack_total != null ? d.dm_pending_ack_total : ''), 10);
if (!isFinite(tt)) {
tt = 0;
Object.keys(bp).forEach(function(k) {
var c = parseInt(String(bp[k]), 10);
if (isFinite(c)) tt += c;
});
}
globalDmPendingAckTotal = tt;
} catch (_e) {}
}
function effectivePendingAlarmTotal() {
var t = parseInt(String(globalDmPendingAckTotal || 0), 10);
if (isFinite(t) && t > 0) return t;
return countLocalPendingInAllMessages();
}
function syncAlarmRepeatFromGlobalPending() {
var n = effectivePendingAlarmTotal();
if (n > 0 && soundEnabled) {
startRepeatTone();
try { startNewMsgFlash(); } catch (_e) {}
} else {
stopRepeatTone();
try { if (n <= 0) stopNewMsgFlash(); } catch (_e2) {}
}
try {
if (practiceUsers && practiceUsers.length) renderSidebarUsers();
} catch (_e3) {}
}
function chatReplyDraftIsActive() {
try {
var ta = document.getElementById('reply-text');
if (!ta) return false;
return !!String(ta.value || '').trim();
} catch (_e) { return false; }
}
function peerUidForIncomingMessage(mm) {
var ex = messageExtrasObj(mm);
if (!ex) return '';
return String(ex.sender_user_id || ex.peer_user_id || ex.peer_uid || '').trim();
}
function peerDisplayForIncomingMessage(mm) {
var uid = peerUidForIncomingMessage(mm);
if (uid && practiceUsers && practiceUsers.length) {
for (var iu = 0; iu < practiceUsers.length; iu++) {
var u = practiceUsers[iu];
if (u && getUserStableId(u) === uid && u.display_name) {
return String(u.display_name).trim();
}
}
}
var abs = String((mm && mm.absender) || '').trim();
return abs.split('(')[0].trim() || '\u2013';
}
function alarmPreviewText(mm) {
var raw = String((mm && mm.kommentar) || '').trim();
if (raw === '\u200b') raw = '';
raw = raw.replace(/\s+/g, ' ').split('\n')[0];
if (raw.length > 100) raw = raw.slice(0, 97) + '\u2026';
return raw || '\u2013';
}
function scrollMessengerToMessageId(mid) {
if (!mid) return;
try {
var midS = String(mid);
var el = document.querySelector('[data-msg-id="' + midS.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"]');
if (!el && window.CSS && typeof CSS.escape === 'function') {
el = document.querySelector('[data-msg-id="' + CSS.escape(midS) + '"]');
}
if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
} catch (_e) {}
}
function navigateIncomingForPopup(mm, force) {
if (!mm) return;
var peerUid = peerUidForIncomingMessage(mm);
var peerName = peerDisplayForIncomingMessage(mm);
if (!peerName && !peerUid) return;
if (!force && chatReplyDraftIsActive()) return;
selectDmPeer(peerName, peerUid);
var mid = mm.id;
setTimeout(function() { scrollMessengerToMessageId(mid); }, 480);
}
function maybeAutoSelectForIncoming(mm) {
if (!mm || !currentSession) return;
if (!messageIsForeignIncoming(mm) || messageHasChatAck(mm)) return;
navigateIncomingForPopup(mm, false);
}
/**
* Hinweis: Das frühere zentrale Web-Overlay
* (#empfang-incoming-overlay mit „Chat öffnen / OK / Schließen“)
* wurde bewusst entfernt. Quittiert wird ausschließlich über den
* existierenden OK-Button in der Sprechblase (chat_ack pro Nachricht
* + serverseitiges Bulk-Ack thread_until_message desselben Absenders).
*/
var previousThreadIds = new Set();
var previousMsgIds = new Set();
var justSentReply = false;
var _newMsgFlashTimer = null;
var _newMsgOrigTitle = '';
/* Entprellung Popup-bei-Nachricht: pro Message-ID; bring_toFront entprellt separat. */
var _newMsgNotifiedIds = new Set();
var _newMsgLastBringFrontMs = 0;
var globalDmPendingAckByPeer = {};
var globalDmPendingAckTotal = 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');
/* Wiederhol-Intervall fuer Chat-Alarm (Standard 5 s).
Ohne gespeicherten Wert: kontinuierlich alle 5 Sekunden bis OK. */
var repeatMs = parseInt(localStorage.getItem('empfang_repeat_ms') || '5000', 10);
if (!isFinite(repeatMs) || repeatMs < 0) repeatMs = 5000;
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 ctrl = new AbortController();
var tid = setTimeout(function() {
try { ctrl.abort(); } catch (_ab) {}
}, 10000);
var r = await fetch(API_BASE + '/auth/needs_setup', {
credentials: 'include',
signal: ctrl.signal
});
clearTimeout(tid);
var d = await r.json();
if (d.needs_setup) { renderSetupForm(); return; }
} catch (e) {}
renderLoginForm();
}
function hideLoginOverlay() {
document.getElementById('login-overlay').classList.add('hidden');
hideEmpfangShellHintBanner();
}
function desktopShellLoginHintHtml() {
if (isEmpfangChatShellMode()) {
return '<p class="desktop-shell-auth-hint" role="status">AzA Empfang Chat-H\u00fclle: Bitte mit Empfang-Login anmelden ODER unten einen Verbindungscode aus dem Browser-Empfang einl\u00f6sen. Es wird <strong>keine Arztlizenz</strong> ben\u00f6tigt.</p>';
}
if (!isDesktopShellMode()) return '';
return '<p class="desktop-shell-auth-hint" role="status">Diese Empfang-H\u00fclle wurde ohne g\u00fcltige Desktop-Sitzung gestartet. Bitte AzA Desktop \u00f6ffnen und den Empfang-Chat aus dem Hauptprogramm starten.</p>';
}
/** Verbindungscode-Einloesefeld nur in der separaten Empfang-Chat-Huelle. */
function empfangChatShellRedeemHtml() {
if (!isEmpfangChatShellMode()) return '';
return '<div class="empfang-handoff-redeem-card">' +
'<h4>Verbindungscode aus dem Browser-Empfang einl\u00f6sen</h4>' +
'<p>Im AzA-Empfang-Browser auf <em>Empfang-H\u00fclle</em> klicken und den 8-stelligen Code hier eingeben. Die H\u00fclle wird damit ohne erneute Anmeldung verbunden.</p>' +
'<div class="empfang-handoff-redeem-row">' +
'<input type="text" id="empfang-handoff-redeem-input" placeholder="ABCD-EFGH" autocomplete="off" maxlength="9">' +
'<button type="button" onclick="redeemEmpfangHandoffCode()">Einl\u00f6sen</button>' +
'</div>' +
'<div class="empfang-handoff-redeem-msg" id="empfang-handoff-redeem-msg"></div>' +
'</div>';
}
async function redeemEmpfangHandoffCode() {
var inp = document.getElementById('empfang-handoff-redeem-input');
var msg = document.getElementById('empfang-handoff-redeem-msg');
if (!inp || !msg) return;
msg.style.color = '#3a5a7a';
var raw = (inp.value || '').trim().toUpperCase();
if (!raw) {
msg.style.color = '#842029';
msg.textContent = 'Bitte Verbindungscode eingeben.';
return;
}
msg.textContent = 'Code wird gepr\u00fcft \u2026';
try {
var r = await fetch(API_BASE + '/handoff/lookup?code=' + encodeURIComponent(raw),
{ credentials: 'include', cache: 'no-store' });
var d = await r.json().catch(function() { return {}; });
if (!r.ok || !d || !d.success || !d.launch_path) {
msg.style.color = '#842029';
msg.textContent = (d && d.detail) || 'Code ung\u00fcltig oder abgelaufen.';
return;
}
msg.style.color = '#155724';
msg.textContent = 'Verbinde \u2026';
var origin = window.location.origin;
var path = String(d.launch_path || '');
var url = path.charAt(0) === '/' ? (origin + path) : path;
setTimeout(function () { window.location.replace(url); }, 60);
} catch (e) {
msg.style.color = '#842029';
msg.textContent = 'Verbindung fehlgeschlagen.';
}
}
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>' +
desktopShellLoginHintHtml() +
empfangChatShellRedeemHtml() +
'<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>' +
desktopShellLoginHintHtml() +
empfangChatShellRedeemHtml() +
'<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>' +
desktopShellLoginHintHtml() +
'<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>' +
desktopShellLoginHintHtml() +
'<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();
syncMiniBrowserToggleBtn();
// Presence-Ping darf init() / onAuthSuccess() NICHT blockieren.
// Bei langsamem oder haengendem Server werden Benutzerliste, Nachrichten und Polling
// trotzdem gestartet. Timeout 1500 ms; spaeteres Intervall (startPolling) bleibt unveraendert.
try {
await empfangPresencePing({ timeoutMs: 1500 });
} catch (_ePP) {}
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')) {
clearEmpfangLocalSelectionAfterPracticeSwitch();
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')) {
clearEmpfangLocalSelectionAfterPracticeSwitch();
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();
globalDmPendingAckByPeer = {};
globalDmPendingAckTotal = 0;
try { _newMsgNotifiedIds.clear(); } catch (_eC) {}
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();
ensureEmpfangChatShellAttr();
ensureMiniChatAttr();
syncMiniBrowserToggleBtn();
if (isMiniChatMode()) {
applyMiniChatPanelDefaults();
} else {
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 === false) {
stopPolling();
document.getElementById('login-overlay').classList.remove('hidden');
renderLoginForm();
setTimeout(function() {
var badInv = document.getElementById('login-error');
if (badInv) {
badInv.style.color = '#842029';
badInv.textContent = (resolvedInv.detail ||
'Ung\u00fcltiger oder veralteter Einladungscode. Bitte aktuellen Link aus der AzA-Hauptinstallation anfordern.');
}
}, 0);
return;
}
} catch(e) {}
}
var me = await checkAuth();
if (inviteParam && resolvedInv && resolvedInv.valid && resolvedInv.practice_id) {
if (!me || (me.practice_id && me.practice_id === resolvedInv.practice_id)) {
try {
localStorage.setItem('aza_practice_id', resolvedInv.practice_id);
} catch(e) {}
try { sessionStorage.setItem('aza_practice_bind', 'invite_code'); } catch(e2) {}
}
}
if (me && inviteParam && resolvedInv && resolvedInv.valid && resolvedInv.practice_id &&
me.practice_id && me.practice_id !== resolvedInv.practice_id) {
stopPolling();
document.getElementById('login-overlay').classList.remove('hidden');
await renderInviteVersusSessionConflict(me, resolvedInv);
return;
}
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 (isDesktopShellMode()) {
try {
var cs0 = document.getElementById('conversation-scroll');
if (cs0) {
cs0.innerHTML = '<div class="conv-empty">Diese Empfang-H\u00fclle wurde ohne g\u00fcltige Desktop-Sitzung gestartet. Bitte AzA Desktop \u00f6ffnen und den Empfang-Chat aus dem Hauptprogramm starten, oder sich hier anmelden.</div>';
}
} catch (_eCs) {}
}
if (inviteParam && resolvedInv && resolvedInv.valid && resolvedInv.practice_id) {
renderInviteLandingForm(resolvedInv, inviteParam);
} else {
showLoginOverlay();
}
return;
}
currentSession = me;
applyActiveConversationFromSession();
hideLoginOverlay();
updateSbMe();
syncMiniBrowserToggleBtn();
// Presence-Ping darf init() / onAuthSuccess() NICHT blockieren.
// Bei langsamem oder haengendem Server werden Benutzerliste, Nachrichten und Polling
// trotzdem gestartet. Timeout 1500 ms; spaeteres Intervall (startPolling) bleibt unveraendert.
try {
await empfangPresencePing({ timeoutMs: 1500 });
} catch (_ePP) {}
loadUsersFromServer();
loadTasksFromServer();
loadMessages();
loadPracticeInfo();
refreshSettingsUserSection();
refreshTaskTabsForRole();
startPolling();
if (isDesktopShellMode() || isMiniChatMode()) {
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() || isMiniChatMode())) {
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;
if (isMiniChatMode()) 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) {}
}
/** ?minichat=1: schmale Ansicht — gleiche empfang.html, Seitenleisten per CSS aus. */
function ensureMiniChatAttr() {
try {
var sp = new URLSearchParams(window.location.search);
var on = String(sp.get('minichat') || '').trim() === '1' ||
String(sp.get('mode') || '').trim().toLowerCase() === 'minichat';
if (on) document.documentElement.setAttribute('data-minichat', '1');
else document.documentElement.removeAttribute('data-minichat');
} catch (e) {}
}
function isMiniChatMode() {
try {
return document.documentElement.getAttribute('data-minichat') === '1';
} catch (e) {
return false;
}
}
function isDesktopShellMode() {
try {
return document.documentElement.getAttribute('data-desktop-shell') === '1';
} catch (e) {
return false;
}
}
/** ?empfang_chat_shell=1: separate, eigenstaendige Empfang-Chat-Huelle (kein Arzt-Desktop). */
function ensureEmpfangChatShellAttr() {
try {
var sp = new URLSearchParams(window.location.search);
var on = String(sp.get('empfang_chat_shell') || '').trim() === '1' ||
String(sp.get('shell_source') || '').trim().toLowerCase() === 'empfang_chat_shell';
if (on) document.documentElement.setAttribute('data-empfang-chat-shell', '1');
else document.documentElement.removeAttribute('data-empfang-chat-shell');
} catch (e) {}
}
function isEmpfangChatShellMode() {
try {
return document.documentElement.getAttribute('data-empfang-chat-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() {
if (isMiniChatMode()) return 'empfang_tasks_collapsed_mc';
return isDesktopShellMode() ? 'empfang_tasks_collapsed_ds' : 'empfang_tasks_collapsed';
}
/** MiniChat: Aufgaben- und Sidebar-Leiste standardmaessig eingeklappt, eigene LocalStorage-Schluessel. */
function applyMiniChatPanelDefaults() {
if (!isMiniChatMode()) return;
var sb = document.getElementById('sidebar');
var tp = document.getElementById('tasks-panel');
try {
var tc = localStorage.getItem('empfang_tasks_collapsed_mc');
if (tc === '0') {
if (tp) tp.classList.remove('collapsed');
} else {
if (tp) tp.classList.add('collapsed');
}
} catch (e) {}
try {
var sc = localStorage.getItem('empfang_sidebar_collapsed_mc');
if (sc === '0') {
if (sb) sb.classList.remove('sidebar-ds-collapsed');
} else {
if (sb) sb.classList.add('sidebar-ds-collapsed');
}
} catch (e) {}
updateSidebarNavHighlight();
}
function syncMiniBrowserToggleBtn() {
var b = document.getElementById('mini-browser-toggle-btn');
if (!b) return;
if (isMiniChatMode()) {
b.textContent = 'Browser-Chat';
b.title = 'Volle Empfang-Ansicht \u00f6ffnen oder fokussieren (ohne MiniChat)';
} else {
b.textContent = 'MiniChat';
b.title = 'Kompaktes MiniChat-Fenster (gleiche Oberfl\u00e4che und Daten)';
}
syncEmpfangHandoffBtn();
}
/** Header-Button 'Empfang-Huelle': nur im normalen Browser sichtbar (nicht in pywebview, nicht in MiniChat, nicht in Empfang-Chat-Huelle). */
function syncEmpfangHandoffBtn() {
var b = document.getElementById('empfang-handoff-btn');
if (!b) return;
var hide = false;
try {
if (typeof empfangPywebviewRuntimePresent === 'function' && empfangPywebviewRuntimePresent()) hide = true;
} catch (_e) {}
if (isMiniChatMode()) hide = true;
if (isEmpfangChatShellMode()) hide = true;
if (!currentSession) hide = true;
b.style.display = hide ? 'none' : '';
}
function onMiniBrowserToggleClick() {
if (!currentSession) {
showLoginOverlay();
return;
}
if (isMiniChatMode()) {
openBrowserChatWindow();
} else {
openMiniChatWindow();
}
}
function restoreDesktopSidebarState() {
if (!isDesktopShellMode() || isMiniChatMode()) 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() && !isMiniChatMode()) return;
var sb = document.getElementById('sidebar');
if (!sb) return;
sb.classList.toggle('sidebar-ds-collapsed');
var collapsed = sb.classList.contains('sidebar-ds-collapsed');
try {
if (isMiniChatMode()) {
localStorage.setItem('empfang_sidebar_collapsed_mc', collapsed ? '1' : '0');
} else {
localStorage.setItem('empfang_sidebar_collapsed_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() && !isMiniChatMode()) 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() && !isMiniChatMode()) {
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() && !isMiniChatMode()) 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() && !isMiniChatMode()) 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() && !isMiniChatMode()) 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() && !isMiniChatMode()) 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() && !isMiniChatMode()) 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() && !isMiniChatMode()) || !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 dmUid = String(d.dm_open_peer_user_id || '').trim();
var dmDn = String(d.dm_open_display_name || '').trim();
if (dmUid && currentSession) {
var dmLbl = dmDn || dmUid;
var dmSig = dmUid + '|' + String(d.context_id || '').trim();
if (window.__empfangDmOpenAppliedSig !== dmSig) {
window.__empfangDmOpenAppliedSig = dmSig;
try {
selectDmPeer(dmLbl, dmUid);
} catch (eDm) {}
}
}
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() && !isMiniChatMode()) {
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 uidRaw = getUserStableId(u) || '';
var badgeN = 0;
if (!isMe && uidRaw && globalDmPendingAckByPeer && globalDmPendingAckByPeer[uidRaw] != null) {
badgeN = parseInt(String(globalDmPendingAckByPeer[uidRaw]), 10) || 0;
}
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 && badgeN > 0 ? '<span class="sb-pending-badge" aria-label="OK ausstehend">' +
(badgeN > 9 ? '9+' : String(badgeN)) + '</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; }));
/* Erste neue eingehende Fremd-Nachricht ermitteln. Strikt UID-basiert via
messageIsForeignIncoming(); eigene Nachrichten werden konsequent
ausgeschlossen. Der Wiederholton selbst haengt am Pending-Stand. */
try {
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;
if (!messageIsForeignIncoming(mm)) continue;
if (messageHasChatAck(mm)) 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 || '');
syncAlarmRepeatFromGlobalPending();
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);
/* Reine Audio-Anhaenge: kein Platzhaltertext; Nicht-Audio: Kurzhinweis behalten. */
if (!onlyAud) {
parts += '<div class="mseg-hint">\u200b 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 showAck = messageShouldShowAckBadge(m);
var ackCls = showAck ? ' 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 (showAck) 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;
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 nextAck = !currentlyAck;
/* Beim Bestaetigen (ack=true): Bulk-Ack desselben Absenders/Threads bis
einschliesslich dieser Nachricht (serverseitig sicher gefiltert auf
eingehende DMs an mich, gleicher direct_conv_key, gleicher Absender).
Beim Toggle-OFF: nur diese Nachricht. */
var bodyObj = { ack: nextAck };
if (nextAck) bodyObj.scope = 'thread_until_message';
var r = await apiFetch(API_BASE + '/messages/' + encodeURIComponent(mid) + '/chat-ack', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(bodyObj)
});
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 = '';
var rootShowAck = messageShouldShowAckBadge(m);
html += '<div class="conv-doc' + (rootShowAck ? ' conv-doc-chat-ack' : '') + '">';
if (rootShowAck) 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 = messageShouldShowAckBadge(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);
}
}
/** pywebview-Laufzeit (Bridge kann lokal noch nicht bereit sein). ?desktop_shell=1 allein reicht NICHT. */
function empfangPywebviewRuntimePresent() {
try {
if (window.pywebview) return true;
var ua = String((navigator && navigator.userAgent) || '');
if (ua.indexOf('pywebview') >= 0) return true;
} catch (e) {}
return false;
}
function showEmpfangShellHintBanner(msg) {
var el = document.getElementById('empfang-shell-hint-banner');
if (!el) return;
el.textContent = msg || '';
el.classList.remove('hidden');
}
function hideEmpfangShellHintBanner() {
var el = document.getElementById('empfang-shell-hint-banner');
if (!el) return;
el.classList.add('hidden');
el.textContent = '';
}
/** True, wenn die Seite in einer pywebview-Hostumgebung laeuft (nicht nur URL-Desktop-Shell-Marker). */
function isInsidePywebviewHost() {
return empfangPywebviewRuntimePresent();
}
/** Wartet asynchron bis pywebview.api[name] verfuegbar ist (max ~timeoutMs). */
function waitForPywebviewApi(name, timeoutMs) {
var deadline = Date.now() + Math.max(0, parseInt(timeoutMs || 0, 10) || 0);
return new Promise(function (resolve) {
function check() {
try {
var a = window.pywebview && window.pywebview.api;
if (a && typeof a[name] === 'function') { resolve(a); return; }
} catch (e) {}
if (Date.now() >= deadline) { resolve(null); return; }
setTimeout(check, 80);
}
check();
});
}
/** MiniChat: gleiche Seite wie Haupt-Huelle, ?minichat=1; Desktop: zweiter pywebview-Prozess mit schmaler Groesse. */
function buildMiniChatUrl() {
try {
var u = new URL(window.location.href);
u.searchParams.set('minichat', '1');
return u.toString();
} catch (e) {
var base = window.location.pathname + window.location.search;
var sep = base.indexOf('?') >= 0 ? '&' : '?';
return window.location.origin + base + sep + 'minichat=1';
}
}
/** Volle Empfang-Ansicht ohne MiniChat (entfernt minichat=1 aus der URL). */
function buildBrowserChatUrl() {
try {
var u = new URL(window.location.href);
u.searchParams.delete('minichat');
var m = String(u.searchParams.get('mode') || '').trim().toLowerCase();
if (m === 'minichat') u.searchParams.delete('mode');
return u.toString();
} catch (e) {
return window.location.href.replace(/([?&])minichat=1(&|$)/, '$1').replace(/\?$/, '');
}
}
/** Aus MiniChat zurueck: Opener/anderes Fenster oder pywebview open_browser_chat. */
async function openBrowserChatWindow() {
if (!currentSession) {
showLoginOverlay();
return;
}
var target = buildBrowserChatUrl();
// 1) Opener (klassischer Browser-Tab, der per window.open MiniChat geoeffnet hat)
try {
if (window.opener && !window.opener.closed) {
var ok = false;
try {
ok = window.opener.location && window.opener.location.origin === window.location.origin;
} catch (eO) {}
if (ok) {
try {
window.opener.location.replace(target);
window.opener.focus();
return;
} catch (eR) {}
}
}
} catch (e) {}
// 2) Desktop-Huelle (pywebview): nur ueber API; kein window.open-Fallback
if (empfangPywebviewRuntimePresent()) {
var apiBc = await waitForPywebviewApi('open_browser_chat', 2500);
if (apiBc) {
try {
hideEmpfangShellHintBanner();
var _pr = apiBc.open_browser_chat(target);
if (_pr && typeof _pr.then === 'function') _pr.then(function () {}, function () {});
} catch (_e) {}
return;
}
showEmpfangShellHintBanner('Desktop-H\u00fclle nicht vollst\u00e4ndig initialisiert (Browser-Chat-API fehlt). Bitte AzA Desktop verwenden oder den Empfang-Starter aktualisieren.');
return;
}
// 3) Echter Browser ohne pywebview: Popup ist legitim
var mw = Math.min(1400, Math.max(1000, Math.floor(screen.availWidth * 0.72)));
var mh = Math.min(980, Math.max(780, Math.round(screen.availHeight * 0.88)));
var feat = 'width=' + mw + ',height=' + mh + ',menubar=no,toolbar=no,scrollbars=yes,resizable=yes';
var w = window.open(target, 'aza_empfang_main', feat);
if (w) {
try { w.focus(); } catch (eF) {}
hideEmpfangShellHintBanner();
} else {
alert('Browser-Chat wurde blockiert. Bitte Popups erlauben.\n\n' + target);
}
}
async function openMiniChatWindow() {
if (!currentSession) {
showLoginOverlay();
return;
}
var url = buildMiniChatUrl();
// 1) Desktop-Huelle (pywebview): nur ueber API; kein window.open-Fallback
if (empfangPywebviewRuntimePresent()) {
var apiMc = await waitForPywebviewApi('open_minichat', 2500);
if (apiMc) {
try {
hideEmpfangShellHintBanner();
var _prMc = apiMc.open_minichat(url);
if (_prMc && typeof _prMc.then === 'function') _prMc.then(function () {}, function () {});
} catch (_eMc) {}
return;
}
showEmpfangShellHintBanner('Desktop-H\u00fclle nicht vollst\u00e4ndig initialisiert (MiniChat-API fehlt). Bitte AzA Desktop verwenden oder den Empfang-Starter aktualisieren.');
return;
}
// 2) Echter Browser: Popup ist legitim
var sw = Math.min(650, Math.max(520, Math.floor(screen.availWidth / 3)));
var sh = Math.min(900, Math.max(720, Math.round(screen.availHeight * 0.82)));
var feat = 'width=' + sw + ',height=' + sh + ',menubar=no,toolbar=no,scrollbars=yes,resizable=yes';
var w = window.open(url, 'aza_empfang_minichat_' + Date.now(), feat);
if (w) {
try { w.focus(); } catch (_eMc2) {}
hideEmpfangShellHintBanner();
} else {
alert('MiniChat wurde blockiert. Bitte Popups f\u00fcr diese Seite erlauben.\n\n' + url);
}
}
/* ===================================================================
EMPFANG-CHAT-HUELLE Handoff (Browser -> separate native Empfang-Huelle)
- Nur sichtbar im normalen Browser, mit gueltiger Empfang-Session.
- Erzeugt serverseitig kurzlebigen Verbindungscode XXXX-XXXX.
- Native AzA Empfang Chat-Huelle loest den Code ein und uebernimmt
dieselbe Praxis-/Chat-Session ueber HttpOnly-Cookie.
=================================================================== */
var _empfangHandoffBusy = false;
var _empfangHandoffCountdownTimer = null;
var _empfangHandoffExpiresAt = 0;
function openEmpfangHandoffDialog() {
if (!currentSession) {
showLoginOverlay();
return;
}
var ov = document.getElementById('empfang-handoff-overlay');
if (!ov) return;
ov.classList.remove('hidden');
var codeEl = document.getElementById('empfang-handoff-code');
var metaEl = document.getElementById('empfang-handoff-meta');
var errEl = document.getElementById('empfang-handoff-err');
var renewBtn = document.getElementById('empfang-handoff-renew-btn');
if (codeEl) codeEl.textContent = '\u2026';
if (metaEl) metaEl.textContent = 'Code wird erzeugt \u2026';
if (errEl) errEl.textContent = '';
if (renewBtn) renewBtn.style.display = 'none';
requestNewEmpfangHandoffCode();
}
function closeEmpfangHandoffDialog() {
var ov = document.getElementById('empfang-handoff-overlay');
if (ov) ov.classList.add('hidden');
if (_empfangHandoffCountdownTimer) {
try { clearInterval(_empfangHandoffCountdownTimer); } catch (_eC) {}
_empfangHandoffCountdownTimer = null;
}
}
async function requestNewEmpfangHandoffCode() {
if (_empfangHandoffBusy) return;
_empfangHandoffBusy = true;
var codeEl = document.getElementById('empfang-handoff-code');
var metaEl = document.getElementById('empfang-handoff-meta');
var errEl = document.getElementById('empfang-handoff-err');
var renewBtn = document.getElementById('empfang-handoff-renew-btn');
if (errEl) errEl.textContent = '';
if (renewBtn) renewBtn.style.display = 'none';
try {
var r = await apiFetch(API_BASE + '/handoff/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}'
});
if (!r.ok) {
var d0 = await r.json().catch(function() { return {}; });
throw new Error((d0 && d0.detail) || ('HTTP ' + r.status));
}
var d = await r.json();
if (!d || !d.short_code) throw new Error('Antwort ohne Code.');
if (codeEl) codeEl.textContent = String(d.short_code);
_empfangHandoffExpiresAt = parseInt(d.expires_at_unix || 0, 10) * 1000;
updateEmpfangHandoffCountdown();
if (_empfangHandoffCountdownTimer) clearInterval(_empfangHandoffCountdownTimer);
_empfangHandoffCountdownTimer = setInterval(updateEmpfangHandoffCountdown, 1000);
} catch (e) {
if (errEl) errEl.textContent = 'Code-Erzeugung fehlgeschlagen: ' + (e && e.message ? e.message : 'Unbekannter Fehler');
if (codeEl) codeEl.textContent = '\u2014';
if (metaEl) metaEl.textContent = '';
if (renewBtn) renewBtn.style.display = '';
}
_empfangHandoffBusy = false;
}
function updateEmpfangHandoffCountdown() {
var metaEl = document.getElementById('empfang-handoff-meta');
var renewBtn = document.getElementById('empfang-handoff-renew-btn');
if (!metaEl) return;
var now = Date.now();
var leftMs = _empfangHandoffExpiresAt - now;
if (leftMs <= 0) {
metaEl.textContent = 'Code abgelaufen \u2014 bitte neuen Code erzeugen.';
if (renewBtn) renewBtn.style.display = '';
if (_empfangHandoffCountdownTimer) {
try { clearInterval(_empfangHandoffCountdownTimer); } catch (_eC) {}
_empfangHandoffCountdownTimer = null;
}
return;
}
var leftSec = Math.floor(leftMs / 1000);
var mm = Math.floor(leftSec / 60);
var ss = leftSec % 60;
metaEl.textContent = 'G\u00fcltig noch ' + mm + ':' + (ss < 10 ? '0' : '') + ss
+ ' Minute' + (mm === 1 ? '' : 'n') + ' \u2014 in der Empfang-Chat-H\u00fclle eingeben.';
}
function renewEmpfangHandoffCode() {
requestNewEmpfangHandoffCode();
}
function copyEmpfangHandoffCode() {
var codeEl = document.getElementById('empfang-handoff-code');
if (!codeEl) return;
var txt = (codeEl.textContent || '').trim();
if (!txt || txt === '\u2026' || txt === '\u2014') return;
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(txt);
var btn = document.getElementById('empfang-handoff-copy-btn');
if (btn) {
var orig = btn.textContent;
btn.textContent = 'Kopiert';
setTimeout(function() { try { btn.textContent = orig; } catch (_eR) {} }, 1500);
}
return;
}
} catch (_e) {}
try {
var range = document.createRange();
range.selectNodeContents(codeEl);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
document.execCommand('copy');
sel.removeAllRanges();
} catch (_e2) {}
}
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; }
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(){});
}
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;
var _shellPinUiAttempts = 0;
function initShellPinUi() {
var b = document.getElementById('shell-pin-btn');
if (!b) return;
var api = window.pywebview && pywebview.api;
var hasApi = !!(api && typeof api.toggle_on_top === 'function' && typeof api.get_on_top === 'function');
if (hasApi) {
_shellPinUiAttempts = 0;
b.style.display = '';
b.removeAttribute('disabled');
b.title = 'Immer im Vordergrund';
updateShellPinBtn();
return;
}
if (empfangPywebviewRuntimePresent()) {
_shellPinUiAttempts++;
if (_shellPinUiAttempts > 120) {
b.style.display = 'none';
b.removeAttribute('disabled');
_shellPinUiAttempts = 0;
return;
}
b.style.display = '';
b.setAttribute('disabled', 'disabled');
b.title = 'Immer im Vordergrund (Bridge wird vorbereitet \u2026)';
setTimeout(initShellPinUi, 250);
return;
}
_shellPinUiAttempts = 0;
b.style.display = 'none';
b.removeAttribute('disabled');
}
async function updateShellPinBtn() {
var b = document.getElementById('shell-pin-btn');
if (!b || b.style.display === 'none') return;
try {
var api = window.pywebview && pywebview.api;
if (!api || typeof api.get_on_top !== 'function') return;
var v = await 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') {
if (isInsidePywebviewHost()) {
var apiReady = await waitForPywebviewApi('toggle_on_top', 1500);
if (!apiReady) return;
api = apiReady;
} else {
return;
}
}
shellPinBusy = true;
try {
var newVal = await api.toggle_on_top();
var bb = document.getElementById('shell-pin-btn');
if (bb) {
if (newVal) {
bb.classList.add('on');
bb.title = 'Immer im Vordergrund (aktiv)';
bb.setAttribute('aria-pressed', 'true');
} else {
bb.classList.remove('on');
bb.title = 'Immer im Vordergrund';
bb.setAttribute('aria-pressed', 'false');
}
}
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 _empfangMaybeBringFront() {
try {
var now = Date.now();
if ((now - _newMsgLastBringFrontMs) < 2000) return;
_newMsgLastBringFrontMs = now;
if (window.focus) window.focus();
_empfangBringPywebviewFront();
} catch (_e) {}
}
function notifyNewIncomingMessage(mm) {
var fired = false;
try {
var mid = String((mm && mm.id) || '').trim();
if (!mid) return;
/* Pro Nachricht-ID: erstes Popup / erste Sonderbehandlung nur einmal (Polling). */
if (_newMsgNotifiedIds.has(mid)) return;
_newMsgNotifiedIds.add(mid);
if (_newMsgNotifiedIds.size > 256) {
var arr = Array.from(_newMsgNotifiedIds);
_newMsgNotifiedIds = new Set(arr.slice(arr.length - 128));
}
fired = true;
if (window.console && console.debug) console.debug('[Empfang] new incoming msg id=' + mid.slice(0, 8));
} catch (_e) {}
startNewMsgFlash();
_empfangMaybeBringFront();
if (fired) {
try { playIncomingNotificationSound('incoming'); } catch (_eS) {}
/* Kein zusaetzliches Web-Overlay mehr: Quittierung erfolgt ausschliesslich
ueber den OK-Button in der Sprechblase (chat_ack + Bulk-Ack desselben
Absenders). Auto-Auswahl des passenden Threads bleibt erhalten,
ohne den Alarm zu quittieren. */
try { maybeAutoSelectForIncoming(mm); } catch (_eA) {}
}
}
/* ===================================================================
INCOMING NOTIFICATION SOUND
- 1) Native pywebview-API (Hauptprogramm-Huelle / Empfang-Chat-Huelle):
zuverlaessig, kein Browser-Autoplay-Block, hoerbar im Hintergrund.
- 2) Web-AudioContext-Fallback (Browser): playTone(currentToneIdx).
- 3) Wenn Web-Audio noch nicht entsperrt: 'Ton aktivieren'-Banner zeigen.
- Pro Nachricht genau einmal (Dedup in notifyNewIncomingMessage).
- Keine Patientendaten / Chatinhalte werden geloggt.
=================================================================== */
function _empfangNativeSoundFire(kind) {
try {
var api = window.pywebview && window.pywebview.api;
if (!api || typeof api.play_notification_sound !== 'function') return false;
var p = api.play_notification_sound(String(kind || 'incoming'));
if (p && typeof p.then === 'function') { p.then(function(){}, function(){}); }
return true;
} catch (_e) { return false; }
}
function _empfangAudioCtxIsRunning() {
try {
return !!(audioCtx && audioCtx.state === 'running');
} catch (_e) { return false; }
}
function playIncomingNotificationSound(kind) {
if (!soundEnabled) return;
// 1) Native zuerst (zuverlaessig, kein Autoplay-Block):
var nativeOk = _empfangNativeSoundFire(kind || 'incoming');
// 2) Web-Audio zusaetzlich versuchen (im Vordergrund hoerbar; im Browser bevorzugt).
try { playTone(currentToneIdx); } catch (_eP) {}
// 3) Browser-Autoplay-Erkennung: wenn AudioContext suspended bleibt und nicht in
// einer pywebview-Huelle, Banner 'Ton aktivieren' anbieten.
setTimeout(function() {
try {
if (nativeOk) return;
if (_empfangAudioCtxIsRunning()) return;
if (typeof empfangPywebviewRuntimePresent === 'function' &&
empfangPywebviewRuntimePresent()) return;
showSoundUnlockBanner();
} catch (_eU) {}
}, 250);
}
/* "Ton aktivieren"-Banner (nur Browser, wenn AudioContext nicht erlaubt ist) */
function showSoundUnlockBanner() {
try {
if (sessionStorage.getItem('aza_sound_unlock_dismissed') === '1') return;
} catch (_e) {}
var el = document.getElementById('empfang-sound-unlock-banner');
if (el) { el.classList.remove('hidden'); return; }
el = document.createElement('div');
el.id = 'empfang-sound-unlock-banner';
el.className = 'empfang-shell-hint-banner';
el.setAttribute('role', 'status');
el.style.cursor = 'pointer';
el.innerHTML = '\u26A0 Ton fuer neue Nachrichten ist im Browser blockiert. ' +
'<button type="button" id="empfang-sound-unlock-btn" ' +
'style="margin-left:8px;padding:3px 10px;border-radius:6px;border:none;' +
'background:#5B8DB3;color:#fff;font-weight:600;cursor:pointer">Ton aktivieren</button> ' +
'<button type="button" id="empfang-sound-dismiss-btn" ' +
'style="margin-left:6px;padding:3px 10px;border-radius:6px;border:1px solid #cda93a;' +
'background:#fff8d6;color:#664d03;cursor:pointer">Ausblenden</button>';
var hdr = document.querySelector('header');
if (hdr && hdr.parentNode) hdr.parentNode.insertBefore(el, hdr.nextSibling);
else document.body.insertBefore(el, document.body.firstChild);
var b = document.getElementById('empfang-sound-unlock-btn');
if (b) b.addEventListener('click', function(ev) {
ev.preventDefault();
unlockEmpfangSound();
});
var dm = document.getElementById('empfang-sound-dismiss-btn');
if (dm) dm.addEventListener('click', function(ev) {
ev.preventDefault();
try { sessionStorage.setItem('aza_sound_unlock_dismissed', '1'); } catch (_e) {}
hideSoundUnlockBanner();
});
}
function hideSoundUnlockBanner() {
var el = document.getElementById('empfang-sound-unlock-banner');
if (el) el.classList.add('hidden');
}
function unlockEmpfangSound() {
try {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx && audioCtx.state === 'suspended') {
var pr = audioCtx.resume();
if (pr && typeof pr.then === 'function') {
pr.then(function() {
try { playTone(currentToneIdx); } catch (_eP) {}
hideSoundUnlockBanner();
syncAlarmRepeatFromGlobalPending();
}, function() {});
} else {
try { playTone(currentToneIdx); } catch (_eP) {}
hideSoundUnlockBanner();
syncAlarmRepeatFromGlobalPending();
}
} else {
try { playTone(currentToneIdx); } catch (_eP) {}
hideSoundUnlockBanner();
syncAlarmRepeatFromGlobalPending();
}
} catch (_e) {}
}
/* Erste Benutzer-Geste entsperrt AudioContext implizit. */
function _empfangSoundUnlockOnFirstGesture() {
try {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx && audioCtx.state === 'suspended') {
var pr = audioCtx.resume();
if (pr && typeof pr.then === 'function') { pr.then(function(){}, function(){}); }
}
if (_empfangAudioCtxIsRunning()) hideSoundUnlockBanner();
} catch (_e) {}
}
try {
['pointerdown', 'keydown', 'touchstart'].forEach(function(ev) {
window.addEventListener(ev, _empfangSoundUnlockOnFirstGesture, { passive: true, capture: true, once: false });
});
} catch (_eL) {}
document.addEventListener('visibilitychange', function() {
if (!document.hidden && effectivePendingAlarmTotal() <= 0) stopNewMsgFlash();
});
window.addEventListener('focus', function() {
if (effectivePendingAlarmTotal() <= 0) 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);
syncAlarmRepeatFromGlobalPending();
}
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) {}
}
/* Repeat-Ton-Loop:
- Idempotent: laeuft bereits ein Intervall und Parameter (repeatMs/soundEnabled)
unveraendert -> nichts tun. Damit reset polling (Pulse 1 s) NICHT mehr
das laengere Wiederhol-Intervall (vorher Bug: nur ein einziger Piep).
- Stoppt nur ueber stopRepeatTone(); das wird zentral durch
syncAlarmRepeatFromGlobalPending() bei pending == 0 oder
soundEnabled == false gesteuert.
- In der pywebview-Huelle wird die native Sound-API pro Tick mitgefeuert,
damit der Ton auch bei nicht-fokussiertem Fenster hoerbar bleibt. */
var _repeatToneRunningMs = 0;
var _repeatToneSoundOn = false;
function _emitRepeatTick() {
var shell = false;
try {
shell = typeof empfangPywebviewRuntimePresent === 'function' && empfangPywebviewRuntimePresent();
} catch (_eSh) {}
if (shell) {
try { _empfangNativeSoundFire('incoming'); } catch (_eN) {}
}
try { playTone(currentToneIdx); } catch (_e1) {}
}
function startRepeatTone() {
if (repeatMs <= 0 || !soundEnabled) {
stopRepeatTone();
return;
}
if (repeatInterval &&
_repeatToneRunningMs === repeatMs &&
_repeatToneSoundOn === !!soundEnabled) {
return;
}
if (repeatInterval) { clearInterval(repeatInterval); repeatInterval = null; }
_repeatToneRunningMs = repeatMs;
_repeatToneSoundOn = !!soundEnabled;
repeatInterval = setInterval(_emitRepeatTick, repeatMs);
}
function stopRepeatTone() {
if (repeatInterval) { clearInterval(repeatInterval); repeatInterval = null; }
_repeatToneRunningMs = 0;
_repeatToneSoundOn = false;
}
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);
/* Wechsel des Intervalls sofort wirksam machen, falls Alarm aktiv. */
try {
if (typeof syncAlarmRepeatFromGlobalPending === 'function') {
stopRepeatTone();
syncAlarmRepeatFromGlobalPending();
}
} catch (_e) {}
}
/* ===================================================================
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_join'
? '\u00dcber Einladungscode \u2014 bestehendes Konto der neuen Praxis zugeordnet'
: (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(opts) {
if (!currentSession) return;
var timeoutMs = 2500;
try {
if (opts && typeof opts.timeoutMs === 'number' && opts.timeoutMs > 0) {
timeoutMs = Math.min(60000, Math.max(250, opts.timeoutMs));
}
} catch (_eO) {}
var ctrl = null;
var tid = 0;
try {
if (typeof AbortController === 'function') {
ctrl = new AbortController();
tid = setTimeout(function() {
try { ctrl.abort(); } catch (_eAb) {}
}, timeoutMs);
}
} catch (_eC) {}
try {
var fetchOpts = {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: '{}',
cache: 'no-store',
};
if (ctrl) fetchOpts.signal = ctrl.signal;
var r = await fetch(API_BASE + '/presence/ping', fetchOpts);
if (r.status === 401) return;
if (!r.ok && isDesktopShellMode()) {
try { console.debug('[Empfang] presence/ping HTTP', r.status); } catch (eD) {}
}
} catch (e) {
var aborted = !!(e && (e.name === 'AbortError' || String(e.message || '').indexOf('abort') >= 0));
if (isDesktopShellMode()) {
try {
if (aborted) console.debug('[Empfang] presence/ping timeout (' + timeoutMs + ' ms)');
else console.debug('[Empfang] presence/ping error');
} catch (e2) {}
}
} finally {
if (tid) {
try { clearTimeout(tid); } catch (_eT) {}
}
}
}
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();
applyPulseDmPendingAck(d);
var t = parseInt(d.tick || 0, 10);
if (t !== lastPulseTick) {
lastPulseTick = t;
loadMessages();
}
syncAlarmRepeatFromGlobalPending();
} 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() || isMiniChatMode()) && 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() || isMiniChatMode()) && 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() {
_shellPinUiAttempts = 0;
initShellPinUi();
try {
var a = window.pywebview && window.pywebview.api;
if (a && typeof a.apply_saved_on_top === 'function') {
var p = a.apply_saved_on_top();
if (p && typeof p.then === 'function') p.then(function() {}, function() {});
}
} catch (_ePin) {}
});
} catch (e) {}
</script>
</body>
</html>