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

4403 lines
196 KiB
HTML
Raw Normal View History

2026-05-08 14:53:53 +02:00
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%235B8DB3'/><text x='16' y='22' font-family='Arial' font-size='11' font-weight='bold' fill='white' text-anchor='middle'>AzA</text></svg>">
<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%);
--aza-rail-active:rgba(255,255,255,.22);
--aza-shadow-sm:0 1px 3px rgba(28,62,94,.06);
--aza-shadow-md:0 4px 20px rgba(28,62,94,.09);
}
/* === Header === */
header{background:linear-gradient(135deg,#5B8DB3,#3a6d93);color:#fff;padding:10px 20px;display:flex;align-items:flex-start;justify-content:space-between;box-shadow:var(--aza-shadow-md);flex-shrink:0;gap:8px;z-index:50}
.header-title-block{display:flex;flex-direction:column;align-items:flex-start;gap:2px;line-height:1.22;min-width:0;padding-top:2px}
header h1{font-size:1.2rem;font-weight:600;letter-spacing:.3px;white-space:nowrap;margin:0}
#header-practice-sub{font-size:.72rem;font-weight:500;opacity:.9;letter-spacing:.02px;white-space:normal;max-width:min(62vw,480px)}
.practice-identity-card{background:#f4f9fc;border:1px solid #d8e4ef;border-radius:12px;padding:12px 14px;margin-bottom:8px;line-height:1.45;color:#2a4660;font-size:.82rem}
.practice-identity-card .pi-main{font-weight:700;font-size:.95rem;color:#1a4d6d;margin-bottom:8px}
.practice-identity-card .pi-row{margin:5px 0;display:flex;flex-wrap:wrap;gap:6px 12px;align-items:center}
.pi-k{color:#6a8499;font-weight:600;font-size:.71rem;text-transform:uppercase;letter-spacing:.06em}
.pi-v.pi-mono{font-family:ui-monospace,Consolas,Menlo,monospace;font-size:.76rem;color:#334e66;word-break:break-all}
.pi-code{font-family:ui-monospace,Consolas,Menlo,monospace;background:#eef4fb;padding:3px 8px;border-radius:6px;user-select:all;font-size:.78rem}
.pi-hint{font-size:.75rem;color:#7a909e;margin:.35rem 0 0;line-height:1.4}
.pi-status{font-size:.78rem;color:#4a677a;margin:.55rem 0 0;line-height:1.45}
.sb-users-hint{padding:2px 18px 11px;font-size:.67rem;line-height:1.38;color:#7a909e;margin-top:-2px}
.header-right{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.hbtn{background:none;border:1px solid rgba(255,255,255,.3);color:#fff;border-radius:5px;padding:3px 9px;font-size:.78rem;cursor:pointer;transition:background .15s;white-space:nowrap}
.hbtn:hover{background:rgba(255,255,255,.15)}
.hbtn.on{background:rgba(255,255,255,.25);border-color:rgba(255,255,255,.55)}
.hbtn.muted{opacity:.5}
.hbtn.active-sort{background:rgba(255,255,255,.25)}
.badge{background:rgba(255,255,255,.2);border-radius:16px;padding:3px 12px;font-size:.8rem}
.font-ctrl{display:flex;align-items:center;gap:2px}
.font-ctrl span{font-size:.72rem;opacity:.8}
.font-ctrl button{background:none;border:1px solid rgba(255,255,255,.25);color:#fff;border-radius:3px;width:20px;height:20px;cursor:pointer;font-size:.65rem;line-height:1;display:flex;align-items:center;justify-content:center}
.font-ctrl button:hover{background:rgba(255,255,255,.15)}
.vol-ctrl{display:flex;align-items:center;gap:3px}
.vol-ctrl input[type=range]{width:50px;accent-color:#fff;cursor:pointer}
.vol-ctrl span{font-size:.7rem;opacity:.8;min-width:26px;text-align:right}
/* === Settings Panel === */
.settings-panel{display:none;background:#fff;border-bottom:1px solid #e0e4e8;padding:12px 20px;flex-shrink:0;z-index:40}
.settings-panel.open{display:block}
.settings-inner h3{font-size:.9rem;color:#1a4d6d;margin-bottom:8px}
.setting-row{display:flex;align-items:center;gap:8px;margin-bottom:6px;flex-wrap:wrap}
.setting-row label{font-size:.82rem;color:#3a5a7a}
.setting-row select,.setting-row input[type=number]{border:1px solid #ccc;border-radius:4px;padding:3px 6px;font-size:.82rem}
.setting-row input[type=number]{width:60px}
.btn-test-small{background:#e8f0f8;color:#2a5a8a;border:1px solid #c8d8e8;border-radius:4px;padding:2px 8px;font-size:.78rem;cursor:pointer}
.btn-test-small:hover{background:#d4e4f0}
.btn-cleanup{background:#f8e8e8;color:#8a2a2a;border:1px solid #e0c8c8;border-radius:4px;padding:2px 8px;font-size:.78rem;cursor:pointer}
.btn-cleanup:hover{background:#f0d4d4}
/* === App layout: rail + list + main + tasks === */
#app-layout{display:flex;flex:1;min-height:0;overflow:hidden}
/* Schmale Icon-Leiste */
#nav-rail{
width:72px;min-width:72px;flex-shrink:0;
background:var(--aza-rail-bg);
display:flex;flex-direction:column;align-items:center;
padding:12px 0 16px;border-right:1px solid rgba(255,255,255,.12);
box-shadow:inset -1px 0 0 rgba(0,0,0,.06);
z-index:20;
}
.nr-brand{
width:44px;height:44px;border-radius:12px;background:rgba(255,255,255,.15);
color:#fff;font-size:.68rem;font-weight:700;letter-spacing:.5px;
display:flex;align-items:center;justify-content:center;margin-bottom:14px;
border:1px solid rgba(255,255,255,.28);
}
.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}
/* === 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;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)}
.sb-users-wrap{padding:6px 0 8px}
.sb-user.sb-user-self{cursor:pointer}
.sb-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.sb-dot.online{background:#6bc47f}
.sb-dot.away{background:#e8c547}
.sb-dot.offline{background:#ccc}
.sb-user-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sb-group-tag{font-size:.62rem;color:#95a5b5;background:#f4f6f9;padding:1px 5px;border-radius:6px;font-weight:500}
.sb-add{padding:6px 14px 6px 18px;font-size:.78rem;color:#5B8DB3;cursor:pointer;display:flex;align-items:center;gap:6px;transition:background .12s}
.sb-add:hover{background:#f4f8fc}
.sb-add-input{display:none;padding:4px 14px 8px 18px}
.sb-add-input.open{display:block}
.sb-add-input input{width:100%;border:1px solid #ccc;border-radius:4px;padding:4px 8px;font-size:.82rem;font-family:inherit}
/* === Hauptereich Conversation === */
#chat-area{flex:1;display:flex;flex-direction:column;min-width:0;background:#f0f6fb}
#chat-pane{
flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden;
}
/* Threadliste liegt in der Mitte (Messenger), nicht mehr in der Sidebar */
#middle-stack{display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden}
.inbox-thread-panel{
flex:0 1 auto;
max-height:min(44vh,460px);min-height:0;overflow:hidden;
display:flex;flex-direction:column;
background:linear-gradient(180deg,#f6fafd 0%,#eef5fb 100%);
border-bottom:1px solid #dfe8ef;
}
.inbox-thread-panel.hidden{display:none!important}
.itp-inner{flex:1;overflow-y:auto;padding:14px clamp(14px,2.5vw,32px)}
.itp-inner::-webkit-scrollbar{width:8px}
.itp-inner::-webkit-scrollbar-thumb{background:#c8d9e8;border-radius:8px}
.inbox-thread-panel .thread-list-caption{font-size:.82rem;font-weight:700;color:#1a4d6d;padding:0 4px 10px;line-height:1.35}
.inbox-thread-panel #thread-list-mount{padding:0 2px 10px}
#chat-top-bar{
padding:14px 22px;background:var(--aza-panel);border-bottom:1px solid #dfe8ef;
display:flex;align-items:center;gap:12px;flex-shrink:0;min-height:56px;
box-shadow:var(--aza-shadow-sm);
}
.ctb-back{
background:#eef6fb;border:1px solid #d0e2ef;color:var(--aza-blue-deep);cursor:pointer;font-size:.82rem;font-weight:600;
padding:8px 12px;border-radius:10px;display:none;align-items:center;gap:6px;font-family:inherit;
transition:background .15s,border-color .15s;
}
.ctb-back:hover{background:#e4eef6;border-color:#b8cce0}
.ctb-back.visible{display:inline-flex}
.ctb-title{font-weight:700;font-size:1.05rem;color:#1a2d42;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;letter-spacing:.01em}
.ctb-actions{display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end}
.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-act,.conv-doc:focus-within .conv-doc-msg-act{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-act-in{
position:absolute;bottom:6px;right:10px;
padding:3px 8px;margin:0;border-radius:999px;font-size:.65rem;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;
opacity:0;transition:opacity .15s ease;z-index:2;font-family:inherit;box-shadow:0 1px 2px rgba(28,62,94,.06);
}
.conv-msg:focus-within .conv-msg-act-in,.conv-msg:hover .conv-msg-act-in{opacity:.92}
.conv-msg-act-in:hover,.conv-msg-act-in:focus{border-color:#98bfd9;color:#1e4d6f;outline:none;background:#fff}
.conv-doc-msg-act{
position:absolute;top:10px;right:10px;
padding:4px 10px;border-radius:999px;font-size:.65rem;font-weight:700;
opacity:0;transition:opacity .15s ease;z-index:3;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:hover .conv-doc-msg-act,.conv-doc:focus-within .conv-doc-msg-act{opacity:1}
/* 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:flex-end;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:52px;max-height:140px;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-btns{display:flex;gap:8px;flex-shrink:0;align-items:center;padding-bottom:2px}
.btn-send{background:linear-gradient(180deg,var(--aza-blue),#4a86ad);color:#fff;border:none;border-radius:13px;padding:14px 22px;font-weight:700;font-size:.82rem;font-family:inherit;cursor:pointer;box-shadow:0 4px 12px rgba(61,118,157,.35);transition:transform .12s,box-shadow .12s;}
.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:13px;cursor:pointer;font-size:1.05rem;line-height:1;
font-family:inherit;min-width:50px;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}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.65}}
@media (max-width:1100px){
#sidebar{min-width:240px;width:280px}
.conv-msg,.conv-doc{max-width:92%!important;width:auto!important}
}
@media (max-width:900px){
#nav-rail{width:60px;min-width:60px;padding:10px 0}
.nr-btn,.nr-brand{width:40px;height:40px;font-size:1rem}
}
/* === Tasks Panel (right) === */
#tasks-panel{width:268px;min-width:210px;background:var(--aza-panel);border-left:1px solid #dfe8ef;display:flex;flex-direction:column;flex-shrink:0;overflow:hidden;transition:width .2s ease,min-width .2s ease,opacity .15s ease}
#tasks-panel.collapsed{width:0!important;min-width:0!important;opacity:0;border:none;padding:0;margin:0;overflow:hidden;pointer-events:none}
/* Desktop-Hülle: Aufgaben einklappbar mit schmaler wiederaufklappbarer Rippe */
html[data-desktop-shell="1"] #tasks-panel.collapsed{
width:40px!important;min-width:40px!important;opacity:1!important;padding:0!important;margin:0!important;
overflow:visible!important;pointer-events:auto!important;border-left:1px solid #dfe8ef!important;background:linear-gradient(180deg,#f7fafc,#f0f6fa);
align-items:center;justify-content:flex-start;
}
html[data-desktop-shell="1"] #tasks-panel.collapsed > :not(.tasks-ds-expand-tab){display:none!important}
.tasks-ds-expand-tab{
display:none;width:100%;min-height:120px;margin:0;padding:10px 0;border:none;background:transparent;color:#466684;font-size:.72rem;font-weight:700;font-family:inherit;cursor:pointer;
flex-direction:column;align-items:center;gap:6px;line-height:1.2;text-align:center;
}
.tasks-ds-expand-tab .tasks-ds-ico{font-size:1.1rem;line-height:1;color:#5B8DB3}
.tasks-ds-expand-tab .tasks-ds-lbl{writing-mode:vertical-rl;transform:rotate(180deg);letter-spacing:.04em;text-transform:uppercase;font-size:.65rem;color:#55708a}
html[data-desktop-shell="1"] #tasks-panel.collapsed .tasks-ds-expand-tab{display:flex!important}
.tp-ds-collapse-btn{display:none;background:none;border:none;color:#7a909f;font-size:1rem;cursor:pointer;padding:4px 6px;line-height:1;border-radius:8px;font-family:inherit}
.tp-ds-collapse-btn:hover{color:#294a62;background:rgba(91,141,179,.08)}
html[data-desktop-shell="1"] #tasks-panel:not(.collapsed) .tp-ds-collapse-btn{display:inline-flex;align-items:center;justify-content:center}
/* Desktop-Hülle: linke Liste einklappbar */
.ds-sidebar-tools{display:none;flex-shrink:0;flex-direction:column;gap:4px;padding:6px 8px;border-bottom:1px solid #eef2f6;background:var(--aza-surface)}
html[data-desktop-shell="1"] .ds-sidebar-tools{display:flex}
.ds-sb-btn{font-size:.68rem;font-weight:700;font-family:inherit;border:1px solid #cfe0ec;border-radius:999px;padding:7px 10px;background:#fff;color:#466684;cursor:pointer;line-height:1.2;text-align:center}
.ds-sb-btn:hover{border-color:#9ebdd4;color:#1a4d6d;background:#fafcfe}
.ds-when-collapsed{display:none!important}
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed .ds-when-expanded{display:none!important}
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed .ds-when-collapsed{display:inline-flex!important;justify-content:center;align-items:center;padding:10px 0;width:100%;border-radius:8px;font-size:.85rem}
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed{width:52px!important;min-width:52px!important;max-width:52px!important}
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed #sidebar-scroll{display:none!important}
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed .sb-me-name,
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed #sb-me-role,
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed .sb-me-change{display:none!important}
html[data-desktop-shell="1"] #sidebar.sidebar-ds-collapsed .sb-me{justify-content:center;padding:10px 4px}
/* Header-Badge Desktop-Modus */
.desktop-mode-pill{display:none;align-self:flex-start;margin-top:4px;font-size:.65rem;font-weight:700;color:#466684;background:linear-gradient(180deg,#eef6fb,#e4eef6);border:1px solid #c8dae8;padding:4px 10px;border-radius:999px;text-transform:none;letter-spacing:.02em}
html[data-desktop-shell="1"] .desktop-mode-pill{display:inline-flex}
/* Kontext / Textbausteine (Desktop-Hülle) */
.desktop-context-panel{display:none;margin:0 14px 8px;padding:0;flex-shrink:0}
html[data-desktop-shell="1"] .desktop-context-panel{display:block}
.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-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-add-btn{background:#5B8DB3;color:#fff;border:none;border-radius:50%;width:22px;height:22px;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1}
.tp-add-btn:hover{background:#4A7A9E}
.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}
.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-hint{font-size:.68rem;font-weight:600;color:#7a909f;display:block;line-height:1.35;text-transform:none;letter-spacing:0;margin-top:2px;font-weight:500}
.tp-seg{display:flex;gap:4px;padding:8px 10px;background:#fafcfe;border-bottom:1px solid #eef2f6;flex-shrink:0;flex-wrap:wrap}
.tp-seg-btn{
flex:1;min-width:0;border:1px solid #dbe6ef;background:#fff;color:#466684;font-size:.7rem;font-weight:700;font-family:inherit;
border-radius:999px;padding:7px 8px;cursor:pointer;transition:background .14s,border-color .14s,color .14s;
}
.tp-seg-btn:hover{border-color:#b8cce0;color:#294a62}
.tp-seg-btn.tp-on{background:linear-gradient(180deg,#eef6fb,#e4eef6);border-color:#9ebdd4;color:#1a3a5a;box-shadow:0 0 0 1px rgba(91,141,179,.12)}
.tp-seg-btn.hidden{display:none!important}
.todo-empty-msg{text-align:center;padding:28px 16px;line-height:1.45;color:#7a909f;font-size:.8rem;background:#fafcfe;border-radius:12px;margin:12px;border:1px dashed #dbe6ef}
.settings-user-mgmt{margin-top:14px;padding-top:14px;border-top:1px solid #eee}
.settings-user-mgmt h3{margin-bottom:8px}
.settings-user-mgmt p{font-size:.78rem;color:#6a8499;margin-bottom:8px;line-height:1.45}
.add-user-overlay{position:fixed;inset:0;z-index:230;background:rgba(24,52,74,.45);display:flex;align-items:center;justify-content:center;padding:20px}
.add-user-overlay.hidden{display:none!important}
.add-user-box{background:var(--aza-panel);border-radius:14px;padding:22px 24px;width:min(400px,100%);border:1px solid #dfe8ef;box-shadow:0 14px 40px rgba(28,62,94,.2)}
.add-user-box h3{font-size:1.02rem;color:#1a4d6d;margin-bottom:10px}
.add-user-box .row{margin-bottom:12px}
.add-user-box label{display:block;font-size:.72rem;font-weight:700;color:#55708a;margin-bottom:4px}
.add-user-box input,.add-user-box select{width:100%;border:1px solid #cfe0ec;border-radius:10px;padding:9px 11px;font:inherit;font-size:.86rem}
.add-user-btns{display:flex;gap:10px;justify-content:flex-end;margin-top:16px;flex-wrap:wrap}
.add-user-btns button{padding:9px 16px;border-radius:10px;font:inherit;font-size:.82rem;cursor:pointer;font-weight:600}
.add-user-btns .adu-cancel{background:#f4f6f8;border:1px solid #dde2e8;color:#3a4a5a}
.add-user-btns .adu-go{background:linear-gradient(180deg,var(--aza-blue),#4a86ad);color:#fff;border:none}
.tc-meta{font-size:.69rem;line-height:1.4;color:#72899e;margin-bottom:6px}
.tc-body:not(:focus){
display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;
}
.tc-actions-bar{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:6px;padding-top:8px;border-top:1px solid #ecf2f6;flex-wrap:wrap}
.tc-btn-done{
background:linear-gradient(180deg,#dff3e8,#cae9d8);border:1px solid #a8d4b8;color:#14532a;font-weight:700;font-size:.74rem;padding:8px 14px;border-radius:10px;cursor:pointer;font-family:inherit;
}
.tc-btn-done:hover{filter:brightness(1.03)}
.tc-done-lbl{font-size:.72rem;font-weight:700;color:#1f6634;background:#eaf6ee;padding:6px 10px;border-radius:8px}
.tc-more{position:relative}
.tc-more > summary{
list-style:none;cursor:pointer;font-size:1.1rem;line-height:1;color:#94aaba;padding:4px 8px;border-radius:8px;font-weight:700;
}
.tc-more > summary::-webkit-details-marker{display:none}
.tc-more[open] > summary{background:#f0f6fb;color:#466684}
.tc-more-menu{
position:absolute;right:0;bottom:100%;margin-bottom:4px;min-width:160px;background:var(--aza-panel);border:1px solid #dfe8ef;border-radius:10px;
box-shadow:var(--aza-shadow-md);padding:6px 0;z-index:40;
}
.tc-more-menu button{
display:block;width:100%;text-align:left;border:none;background:none;padding:8px 12px;font-size:.74rem;color:#294a62;cursor:pointer;font-family:inherit;
}
.tc-more-menu button:hover{background:#f2f9fc}
.tc-more-menu button.danger{color:#a34a4a}
.todo-item{display:flex;align-items:flex-start;gap:4px;padding:4px 6px;border-radius:5px;margin-bottom:2px;background:#f8fafe;font-size:.8rem}
.todo-item .todo-assignee{font-size:.68rem;color:#8a9aaa;margin-top:1px}
/* === Status Bar === */
.status-bar{padding:5px 16px;font-size:.72rem;color:#8a9aaa;background:#fff;border-top:1px solid #eef2f6;display:flex;justify-content:center;gap:12px;flex-shrink:0}
/* === Utility === */
.copied-flash{animation:copiedFl .6s ease}
@keyframes copiedFl{0%{background:#d4edda}100%{background:transparent}}
/* === Login Overlay === */
#login-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:#f0f4f8;z-index:200;display:flex;align-items:center;justify-content:center}
#login-overlay.hidden{display:none}
.login-box{background:#fff;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.12);padding:36px;width:380px;max-width:90vw}
.login-box h2{font-size:1.3rem;color:#1a4d6d;margin-bottom:6px}
.login-box p{font-size:.85rem;color:#6a8a9a;margin-bottom:20px}
.login-field{margin-bottom:14px}
.login-field label{display:block;font-size:.82rem;color:#3a5a7a;margin-bottom:4px;font-weight:600}
.login-field input,.login-field select{width:100%;border:1px solid #d0dce8;border-radius:6px;padding:8px 12px;font-size:.9rem;font-family:inherit;outline:none;transition:border-color .15s}
.login-field input:focus,.login-field select:focus{border-color:#5B8DB3}
.login-btn{width:100%;background:#5B8DB3;color:#fff;border:none;border-radius:8px;padding:10px;font-size:.95rem;font-weight:600;cursor:pointer;font-family:inherit;transition:background .15s}
.login-btn:hover{background:#4A7A9E}
.login-error{color:#dc3545;font-size:.82rem;margin-top:8px;min-height:1.2em}
.login-switch{text-align:center;margin-top:14px;font-size:.82rem;color:#6a8a9a}
/* Neuer Chat (Popup) */
.new-chat-overlay{position:fixed;inset:0;background:rgba(26,42,58,.45);z-index:200;display:flex;align-items:center;justify-content:center;padding:16px}
.new-chat-overlay.hidden{display:none!important}
.new-chat-box{background:#fff;border-radius:12px;padding:22px 24px;max-width:400px;width:100%;box-shadow:0 8px 32px rgba(0,0,0,.15)}
.new-chat-box h3{font-size:1.1rem;color:#1a4d6d;margin-bottom:8px}
.new-chat-box p{font-size:.85rem;color:#6a8a9a;margin-bottom:14px;line-height:1.45}
.nc-opt{display:block;margin:8px 0;font-size:.88rem;cursor:pointer}
.nc-opt input{margin-right:8px}
#nc-peer-select{width:100%;margin-top:10px;padding:8px;border:1px solid #d0dce8;border-radius:6px;font-size:.88rem;font-family:inherit}
.nc-btns{display:flex;gap:10px;justify-content:flex-end;margin-top:18px}
.nc-btns button{padding:8px 16px;border-radius:8px;font-size:.85rem;cursor:pointer;font-family:inherit}
.nc-btns .nc-cancel{background:#eef2f6;color:#3a5a7a;border:1px solid #d0dce8}
.nc-btns .nc-go{background:#5B8DB3;color:#fff;border:none;font-weight:600}
#reply-att-hint{font-size:.72rem;color:#5B8DB3;min-height:1em;margin:4px 0 0 2px}
.login-switch a{color:#5B8DB3;cursor:pointer;text-decoration:underline}
/* Registrierung: Überschrift, Fließtext, Labels, Felder, Button, Link alles 9pt */
.login-box.login-register,
.login-box.login-register h2,
.login-box.login-register p,
.login-box.login-register label,
.login-box.login-register input,
.login-box.login-register select,
.login-box.login-register button,
.login-box.login-register .login-error,
.login-box.login-register .login-switch,
.login-box.login-register .login-switch a{font-size:9pt!important}
.login-box.login-register h2{font-weight:600}
/* === Admin Panel === */
.admin-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.3);z-index:150;align-items:flex-start;justify-content:center;padding-top:40px;overflow-y:auto}
.admin-overlay.open{display:flex}
.admin-panel{background:#fff;border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,.15);width:720px;max-width:94vw;max-height:calc(100vh - 80px);overflow-y:auto;padding:0}
.admin-header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid #e0e4e8;position:sticky;top:0;background:#fff;border-radius:12px 12px 0 0;z-index:5}
.admin-header h2{font-size:1.15rem;color:#1a4d6d;font-weight:600}
.admin-close{background:none;border:none;font-size:1.3rem;color:#8a9aaa;cursor:pointer;padding:4px 8px;border-radius:4px}
.admin-close:hover{background:#f0f4f8;color:#1a4d6d}
.admin-tabs{display:flex;gap:0;border-bottom:1px solid #e0e4e8;padding:0 24px;background:#fafcfe}
.admin-tab{padding:10px 18px;font-size:.85rem;color:#6a8a9a;cursor:pointer;border-bottom:2px solid transparent;font-weight:500;transition:all .15s;font-family:inherit;background:none;border-top:none;border-left:none;border-right:none}
.admin-tab:hover{color:#1a4d6d;background:#f0f4f8}
.admin-tab.active{color:#5B8DB3;border-bottom-color:#5B8DB3;font-weight:600}
.admin-body{padding:20px 24px}
.admin-table{width:100%;border-collapse:collapse;font-size:.82rem}
.admin-table th{text-align:left;padding:8px 10px;color:#5B8DB3;font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.4px;border-bottom:2px solid #e0e4e8}
.admin-table td{padding:8px 10px;border-bottom:1px solid #f0f4f8;vertical-align:middle}
.admin-table tr:hover td{background:#fafcfe}
.admin-badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:.72rem;font-weight:600}
.admin-badge.admin{background:#e8d5f5;color:#6b3fa0}
.admin-badge.arzt{background:#d4edda;color:#155724}
.admin-badge.mpa{background:#d6eaf8;color:#1a5276}
.admin-badge.empfang{background:#fff3cd;color:#856404}
.admin-badge.active{background:#d4edda;color:#155724}
.admin-badge.deactivated{background:#f8d7da;color:#721c24}
.admin-badge.trusted{background:#d4edda;color:#155724}
.admin-badge.blocked{background:#f8d7da;color:#721c24}
.admin-badge.pending_conn{background:#fff3cd;color:#856404}
.admin-badge.active_conn{background:#d4edda;color:#155724}
.admin-badge.revoked{background:#e2e3e5;color:#6c757d}
.admin-btn{background:#e8f0f8;color:#2a5a8a;border:1px solid #d0dce8;border-radius:4px;padding:3px 10px;font-size:.75rem;cursor:pointer;font-family:inherit;transition:all .12s;white-space:nowrap}
.admin-btn:hover{background:#d4e4f0}
.admin-btn.danger{color:#8a2a2a;background:#f8e8e8;border-color:#e8c8c8}
.admin-btn.danger:hover{background:#f0d4d4}
.admin-btn.success{color:#155724;background:#d4edda;border-color:#c3e6cb}
.admin-btn.success:hover{background:#c3e6cb}
.admin-section-title{font-size:.88rem;font-weight:600;color:#1a4d6d;margin:16px 0 8px}
.admin-info-row{display:flex;align-items:center;gap:8px;margin-bottom:6px;font-size:.82rem;flex-wrap:wrap}
.admin-info-label{color:#6a8a9a;min-width:120px}
.admin-info-value{color:#1a3a5a;font-weight:500}
.admin-mono{font-family:monospace;background:#f0f4f8;padding:2px 8px;border-radius:4px;user-select:all}
.admin-empty{text-align:center;padding:24px;color:#8a9aaa;font-size:.85rem}
.admin-add-row{display:flex;gap:6px;margin-top:12px;flex-wrap:wrap;align-items:center}
.admin-add-row input,.admin-add-row select{border:1px solid #d0dce8;border-radius:4px;padding:4px 8px;font-size:.82rem;font-family:inherit}
.admin-add-row input{flex:1;min-width:120px}
@media print{
header,.settings-panel,#nav-rail,#sidebar,#tasks-panel,.status-bar,#chat-input-wrap,#chat-input-bar,#chat-top-bar{display:none!important}
#app-layout{display:block!important}
#chat-area{display:block!important}
.tl-item{break-inside:avoid;box-shadow:none}
.conv-doc,.conv-msg{break-inside:avoid;box-shadow:none}
}
</style>
</head>
<body>
<div id="login-overlay">
<div class="login-box" id="login-box">
<p style="text-align:center;color:#6a8a9a;font-size:.9rem;margin:0;line-height:1.45">Oberfl&auml;che wird geladen&hellip;</p>
</div>
</div>
<header>
<div class="header-title-block">
<h1>AzA-Empfang</h1>
<span class="desktop-mode-pill" aria-label="Desktop-H&uuml;lle">Desktop-H&uuml;lle</span>
<div id="header-practice-sub" aria-live="polite"></div>
</div>
<div class="header-right">
<div class="font-ctrl" id="font-ctrl-wrap" title="Schriftgr&ouml;sse der ganzen Seite (100% = Standard). Mit Pfeilen vergr&ouml;ssern oder verkleinern.">
<span>Aa</span>
<span id="font-size-display">100%</span>
<button type="button" onclick="changeFontSize(1)" aria-label="Schrift gr&ouml;sser">&#9650;</button>
<button type="button" onclick="changeFontSize(-1)" aria-label="Schrift kleiner">&#9660;</button>
</div>
<button class="hbtn" onclick="openNewChatDialog()" title="Neuen Chat in eigenem Fenster">+ Neuer Chat</button>
<button class="hbtn" id="sound-btn" onclick="toggleSound()" title="Ton an/aus">&#128276; Ton</button>
<div class="vol-ctrl" title="Lautst&auml;rke">
<input type="range" id="vol-slider" min="0" max="300" step="5" oninput="setVolume(this.value)">
<span id="vol-display">100%</span>
</div>
<button type="button" class="hbtn" id="shell-pin-btn" onclick="toggleShellPin()" style="display:none" title="Immer im Vordergrund" aria-pressed="false">&#128204;</button>
<button class="hbtn" id="sort-btn" onclick="toggleSort()" title="Sortierung">&#8645;</button>
<span class="badge" id="count-badge">&ndash;</span>
<button class="hbtn" onclick="toggleSettings()" title="Einstellungen">&#9881;</button>
</div>
</header>
<div class="settings-panel" id="settings-panel">
<div class="settings-inner">
<h3>Ton-Einstellungen</h3>
<div class="setting-row">
<label for="tone-select">Benachrichtigungston:</label>
<select id="tone-select" onchange="saveSoundSettings()"></select>
<button class="btn-test-small" onclick="testCurrentSound()">Testen</button>
</div>
<div class="setting-row">
<label for="repeat-select">Ton-Modus:</label>
<select id="repeat-select" onchange="onRepeatChange()">
<option value="30000">Wiederholend (Standard, alle 30 Sek.)</option>
<option value="5000">Kontinuierlich (alle 5 Sek.)</option>
<option value="2000">Kontinuierlich (alle 2 Sek.)</option>
<option value="0">Einmalig</option>
<option value="custom">Eigenes Intervall</option>
</select>
<span id="custom-sec-wrap" style="display:none">
<input type="number" id="custom-sec" min="1" max="600" value="10" style="width:55px" onchange="onRepeatChange()"> Sek.
</span>
</div>
<h3 style="margin-top:12px">Praxis / Chat-Identit&auml;t</h3>
<div id="practice-info-section" style="margin-bottom:6px"></div>
<h3 style="margin-top:12px">Aufbewahrung</h3>
<div class="setting-row">
<label for="cleanup-days">Nachrichten &auml;lter als</label>
<input type="number" id="cleanup-days" min="1" max="365" value="30" style="width:55px"> Tage
<button class="btn-cleanup" onclick="doCleanup()">Jetzt bereinigen</button>
<span id="cleanup-result" style="font-size:.78rem;color:#6a8a9a"></span>
</div>
<div id="admin-panel-link" style="display:none;margin-top:12px;padding-top:12px;border-top:1px solid #eee">
<button class="btn-test-small" onclick="openAdmin()" style="font-size:.85rem;padding:6px 16px">Praxis-Verwaltung</button>
</div>
<div class="settings-user-mgmt" id="settings-user-mgmt" style="display:none">
<h3>Benutzer</h3>
<p>Neue Konten f&uuml;r die Praxis nur mit sicherem Passwort anlegen.</p>
<button type="button" class="btn-test-small" onclick="openAddUserDialog()">Neuen Benutzer anlegen</button>
</div>
<div style="margin-top:16px;padding-top:12px;border-top:1px solid #eee">
<button class="btn-cleanup" onclick="doLogout()" style="background:#e8e8f0;color:#3a3a5a;border-color:#c8c8d8">Abmelden</button>
</div>
</div>
</div>
<div id="app-layout">
<nav id="nav-rail" aria-label="Hauptnavigation">
<div class="nr-brand" title="AzA-Empfang">AzA</div>
<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 class="ds-sidebar-tools" id="ds-sidebar-tools">
<button type="button" class="ds-sb-btn ds-when-expanded" id="ds-sb-collapse-btn" onclick="toggleDesktopSidebarCollapsed()">Benutzerliste ausblenden</button>
<button type="button" class="ds-sb-btn ds-when-collapsed" id="ds-sb-expand-btn" onclick="toggleDesktopSidebarCollapsed()" title="Benutzerliste anzeigen">&#9776;</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>
</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 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">
<p class="dcp-hint">
Therapieplan und Procedere aus der Krankengeschichte werden hier in einem sp&auml;teren Schritt
<strong>ohne Daten in der URL</strong> aus AzA Desktop &uuml;bernommen (z.&nbsp;B. kurzlebiger <code style="font-size:.88em">context_id</code> am Server).
Bis dahin: Inhalt wie gewohnt im Desktop-Dialog &raquo;An Empfang senden&laquo; nutzen oder kopieren und unten einf&uuml;gen.
</p>
<div class="dcp-mini-grid">
<div class="dcp-mini">
<h4>Therapieplan</h4>
<p>Platzhalter &mdash; keine medizinischen Rohdaten in dieser Oberfl&auml;chenversion.</p>
</div>
<div class="dcp-mini">
<h4>Procedere</h4>
<p>Platzhalter &mdash; Daten&uuml;bergabe erfolgt erst nach sicherem Backend-Konzept.</p>
</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>
<button type="button" class="dcp-act" disabled title="Reserviert f&uuml;r sicheren Desktop-Kontext.">
Aus Desktop &uuml;bernehmen
</button>
</div>
</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="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){sendReply();event.preventDefault()}" oninput="autoGrowTextarea(this)"></textarea>
<div id="reply-att-hint"></div>
</div>
<div class="input-btns">
<button class="btn-dictate" id="dictate-btn" onclick="toggleDictation()" title="Diktieren">&#127908;</button>
<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>
<span class="tp-sub">Aufgaben</span>
<span class="tp-hint">Aus Nachrichten oder hier anlegen.</span>
</div>
<div style="display:flex;align-items:center;gap:4px;flex-shrink:0">
<button type="button" class="tp-ds-collapse-btn" id="tp-ds-collapse-btn" onclick="toggleTasksPanel()" title="Aufgaben ausblenden">&#9654;</button>
<button type="button" class="tp-add-btn" onclick="toggleTodoInput()" title="Neue Aufgabe">+</button>
</div>
</div>
<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 hidden" data-tp="team" id="tp-tab-team" onclick="setTaskTab('team')">Team</button>
</div>
<div class="tp-input-row" id="todo-input-row">
<input type="text" id="todo-inline-input" placeholder="Aufgabe eingeben..." onkeydown="if(event.key==='Enter'){addTodoInline();event.preventDefault()}">
</div>
<div id="todo-list"></div>
</aside>
</div>
<!-- Schnell-Anlage Aufgabe aus Chatnachricht -->
<div id="task-quick-panel" class="task-quick-panel hidden" aria-hidden="true" onclick="if(event.target===this)closeTaskQuickPanel()">
<div class="tqp-sheet" role="dialog" aria-labelledby="tqp-heading" onclick="event.stopPropagation()">
<h4 id="tqp-heading">Aufgabe aus Nachricht</h4>
<div class="tqp-meta" id="tqp-source-line"></div>
<input type="hidden" id="tqp-msg-id" value="">
<input type="hidden" id="tqp-thread-id" value="">
<div class="tqp-row">
<label for="tqp-title">Kurztitel</label>
<input type="text" id="tqp-title" maxlength="220" autocomplete="off">
</div>
<div class="tqp-row">
<label for="tqp-assignee">Zuweisen</label>
<select id="tqp-assignee"></select>
</div>
<div class="tqp-row">
<label for="tqp-body">Beschreibung</label>
<textarea id="tqp-body" rows="4"></textarea>
</div>
<div class="tqp-actions">
<button type="button" class="btn-tqp-ghost" onclick="closeTaskQuickPanel()">Abbrechen</button>
<button type="button" class="btn-tqp-primary" onclick="submitTaskQuickPanel()">Aufgabe anlegen</button>
</div>
</div>
</div>
<div id="new-chat-overlay" class="new-chat-overlay hidden" onclick="if(event.target===this)closeNewChatDialog()">
<div class="new-chat-box" onclick="event.stopPropagation()">
<h3>Neuer Chat</h3>
<p>W&auml;hlen Sie, wo der Chat laufen soll. Es &ouml;ffnet sich ein <strong>zus&auml;tzliches Fenster</strong> (Browser oder Desktop-H&uuml;lle) &mdash; Sie k&ouml;nnen dort in Ruhe schreiben, <strong>Bilder per Drag &amp; Drop oder Strg+V</strong> einf&uuml;gen.</p>
<label class="nc-opt"><input type="radio" name="nc-mode" value="general" checked onchange="syncNcPeerSelect()"> Allgemein (Praxis-Inbox, wie Hauptansicht)</label>
<label class="nc-opt"><input type="radio" name="nc-mode" value="dm" onchange="syncNcPeerSelect()"> Direkt mit Benutzer</label>
<select id="nc-peer-select" disabled aria-label="Benutzer w&auml;hlen">
<option value="">&mdash; Benutzer w&auml;hlen &mdash;</option>
</select>
<div class="nc-btns">
<button type="button" class="nc-cancel" onclick="closeNewChatDialog()">Abbrechen</button>
<button type="button" class="nc-go" onclick="launchNewChatWindow()">Fenster &ouml;ffnen</button>
</div>
</div>
</div>
<div id="add-user-overlay" class="add-user-overlay hidden" onclick="if(event.target===this)closeAddUserDialog()">
<div class="add-user-box" onclick="event.stopPropagation()">
<h3>Neuen Benutzer anlegen</h3>
<div class="row">
<label for="adu-name">Benutzername (Anzeige)</label>
<input type="text" id="adu-name" maxlength="120" autocomplete="off" placeholder="z. B. Maria Muster">
</div>
<div class="row">
<label for="adu-role">Rolle</label>
<select id="adu-role">
<option value="mpa">MPA</option>
<option value="arzt">Arzt / &Auml;rztin</option>
<option value="empfang" id="adu-opt-empfang">Empfang</option>
<option value="admin" id="adu-opt-admin">Admin</option>
</select>
</div>
<div class="row">
<label for="adu-pass">Passwort</label>
<input type="password" id="adu-pass" autocomplete="new-password" placeholder="mindestens 4 Zeichen">
</div>
<div class="row">
<label for="adu-pass2">Passwort wiederholen</label>
<input type="password" id="adu-pass2" autocomplete="new-password">
</div>
<div class="adu-err login-error" id="adu-error" style="margin-top:4px;font-size:.82rem;min-height:1.15em;color:#dc3545"></div>
<div class="add-user-btns">
<button type="button" class="adu-cancel" onclick="closeAddUserDialog()">Abbrechen</button>
<button type="button" class="adu-go" onclick="submitSecureAddUser()">Anlegen</button>
</div>
</div>
</div>
<div class="admin-overlay" id="admin-overlay">
<div class="admin-panel">
<div class="admin-header">
<h2>Praxis-Verwaltung</h2>
<button class="admin-close" onclick="closeAdmin()">&times;</button>
</div>
<div class="admin-tabs" id="admin-tabs">
<button class="admin-tab active" onclick="showAdminTab('praxis')">Praxis</button>
<button class="admin-tab" onclick="showAdminTab('users')">Benutzer</button>
<button class="admin-tab" onclick="showAdminTab('devices')">Ger&auml;te</button>
<button class="admin-tab" onclick="showAdminTab('channels')">Kan&auml;le</button>
<button class="admin-tab" onclick="showAdminTab('federation')">Verbindungen</button>
</div>
<div class="admin-body" id="admin-body"></div>
</div>
</div>
<div class="status-bar">
<span>Live-Pulse ca. 1&nbsp;s, Sicherheits-Refresh 15&nbsp;s (gleiche Server-Conversation wie Desktop)</span>
<span style="opacity:.5" id="ui-version">v2026.05.07.dmv2</span>
<span id="dm-diag" style="margin-left:14px;font-family:monospace;font-size:.72rem;color:#7a8ea2"></span>
</div>
<script>
/* ===================================================================
STATE
=================================================================== */
var API_BASE = window.location.origin + '/empfang';
function getPracticeIdOrEmpty() {
try { return localStorage.getItem('aza_practice_id') || ''; } catch (e) { return ''; }
}
function getUrlInviteCode() {
try {
return (new URLSearchParams(window.location.search).get('invite') || '').trim();
} catch (e) { return ''; }
}
var currentSession = null;
var practiceUsers = [];
var lastPracticeInfoForHint = null;
var serverTasks = [];
var allMessages = [];
var allThreads = [];
var activeView = 'allgemein';
var activeDmPeer = null;
var activeDmPeerUid = '';
/** Eine konsolidierte Conversation-Quelle: Klick / Senden / laden. */
var activeConversation = {
mode: 'all',
practiceId: '',
currentUserId: '',
currentUserKey: '',
currentDisplayName: '',
targetUserId: '',
targetUserKey: '',
targetDisplayName: ''
};
function applyActiveConversationFromSession() {
if (!currentSession) return;
activeConversation.practiceId = String(currentSession.practice_id || '').trim();
activeConversation.currentUserId = String(currentSession.user_id || '').trim();
activeConversation.currentUserKey = activeConversation.currentUserId;
activeConversation.currentDisplayName = String(currentSession.display_name || '').trim();
}
function syncLegacyDmFromActiveConversation() {
if (activeConversation.mode === 'direct' && activeConversation.targetDisplayName) {
activeDmPeer = activeConversation.targetDisplayName;
activeDmPeerUid = String(activeConversation.targetUserId || activeConversation.targetUserKey || '').trim();
} else {
activeDmPeer = null;
activeDmPeerUid = '';
}
}
function peerUidForDisplay(dn) {
var n = String(dn || '').trim();
if (!n) return '';
for (var i = 0; i < practiceUsers.length; i++) {
var u = practiceUsers[i];
if (!u) continue;
if (String(u.display_name || '').trim() === n) return String(u.user_id || '').trim();
}
return '';
}
function dmDirectConvKey(meUid, peerUid) {
if (!currentSession) return '';
var pid = String(currentSession.practice_id || '').trim();
if (!pid || !meUid || !peerUid) return '';
var a = meUid <= peerUid ? meUid : peerUid;
var b = meUid <= peerUid ? peerUid : meUid;
return pid + '|direct|' + a + '|' + b;
}
var dmWantsThreadList = false;
var taskTabFilter = 'mine';
try { taskTabFilter = localStorage.getItem('empfang_task_tab') || 'mine'; } catch (eTab) {}
/** 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 };
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) {}
}
var previousThreadIds = new Set();
var justSentReply = false;
var pollTimer = null;
var userPollTimer = null;
var fallbackTimer = null;
var pendingReplyAttachments = [];
var REPLY_ATTACH_MAX_BYTES = 2 * 1024 * 1024;
var soundEnabled = localStorage.getItem('empfang_sound') !== 'off';
var audioCtx = null;
var EMPFANG_FONT_BASE_PT = 9;
var currentFontSize = parseInt(localStorage.getItem('empfang_font_size'), 10) || EMPFANG_FONT_BASE_PT;
var volume = parseFloat(localStorage.getItem('empfang_volume') || '1.0');
var repeatMs = parseInt(localStorage.getItem('empfang_repeat_ms') || '30000', 10);
var repeatInterval = null;
var recognition = null;
var isRecording = false;
var _sortRaw = localStorage.getItem('empfang_sort');
if (_sortRaw === 'desc' || _sortRaw === null || _sortRaw === '') localStorage.setItem('empfang_sort', 'newest');
var sortNewestFirst = localStorage.getItem('empfang_sort') !== 'asc';
var TONE_PRESETS = [
{name:'Sanftes Glockenspiel', notes:[{f:523,d:.15},{f:659,d:.15},{f:784,d:.3}], wave:'sine', vol:.12},
{name:'Zwei-Ton Harmonisch', notes:[{f:392,d:.2},{f:523,d:.3}], wave:'sine', vol:.12},
{name:'Drei-Ton Melodie', notes:[{f:523,d:.12},{f:587,d:.12},{f:659,d:.28}], wave:'sine', vol:.11},
{name:'Kristallklar', notes:[{f:1319,d:.5}], wave:'sine', vol:.07},
{name:'Warmer Akkord', notes:[{f:262,d:.4}], wave:'triangle', vol:.14},
{name:'Aufstieg', notes:[{f:262,d:.09},{f:330,d:.09},{f:392,d:.09},{f:523,d:.22}], wave:'sine', vol:.10},
{name:'Sanfte Welle', notes:[{f:440,d:.55}], wave:'sine', vol:.10},
{name:'Tropfen', notes:[{f:659,d:.12},{f:587,d:.12},{f:523,d:.28}], wave:'sine', vol:.10},
{name:'Morgengruss', notes:[{f:523,d:.14},{f:392,d:.14},{f:523,d:.28}], wave:'sine', vol:.12},
{name:'Zephyr', notes:[{f:880,d:.45}], wave:'sine', vol:.06},
{name:'Bambus', notes:[{f:330,d:.18},{f:440,d:.28}], wave:'triangle', vol:.12},
{name:'Silberglocke', notes:[{f:988,d:.45}], wave:'sine', vol:.08},
{name:'Meditation', notes:[{f:262,d:.65}], wave:'sine', vol:.12},
{name:'Horizont', notes:[{f:587,d:.18},{f:880,d:.32}], wave:'sine', vol:.10},
{name:'Stille Post', notes:[{f:784,d:.4}], wave:'sine', vol:.08},
];
var currentToneIdx = parseInt(localStorage.getItem('empfang_tone_idx') || '0', 10);
/* ===================================================================
API WRAPPER (global 401 handling)
=================================================================== */
async function apiFetch(url, opts) {
opts = opts || {};
if (!opts.credentials) opts.credentials = 'include';
var meth = ((opts.method || 'GET') + '').toUpperCase();
if (meth === 'GET' && opts.cache === undefined)
opts.cache = 'no-store';
var r = await fetch(url, opts);
if (r.status === 401) {
currentSession = null;
stopPolling();
showLoginOverlay();
throw new Error('auth');
}
return r;
}
/* ===================================================================
AUTH
=================================================================== */
async function checkAuth() {
try {
var r = await fetch(API_BASE + '/auth/me', {credentials: 'include', cache: 'no-store'});
if (r.status === 401) return null;
var d = await r.json();
if (d.authenticated) return d;
return null;
} catch(e) { return null; }
}
async function showLoginOverlay() {
stopPolling();
var overlay = document.getElementById('login-overlay');
overlay.classList.remove('hidden');
try {
var r = await fetch(API_BASE + '/auth/needs_setup', {credentials: 'include'});
var d = await r.json();
if (d.needs_setup) { renderSetupForm(); return; }
} catch(e) {}
renderLoginForm();
}
function hideLoginOverlay() {
document.getElementById('login-overlay').classList.add('hidden');
}
function renderSetupForm() {
var box = document.getElementById('login-box');
box.className = 'login-box';
box.innerHTML =
'<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();
}
function renderLoginForm() {
var box = document.getElementById('login-box');
box.className = 'login-box';
var lastUser = localStorage.getItem('aza_last_login_user') || '';
box.innerHTML =
'<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>' +
'<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(); }
}
function renderForgotPasswordForm() {
var box = document.getElementById('login-box');
box.className = 'login-box';
var pid = getPracticeIdOrEmpty();
box.innerHTML =
'<h2>Passwort vergessen</h2>' +
'<p>Geben Sie Ihren <strong>Benutzernamen</strong> (wie in der Praxis angezeigt) oder Ihre <strong>E-Mail-Adresse</strong> ein.</p>' +
'<p style="font-size:.82rem;color:#6a8a9a">Nach einer erfolgreichen Anmeldung speichert der Browser die Praxiszuordnung &mdash; dann ist der Ablauf einfacher.</p>' +
(pid ? '' : '<p style="font-size:.78rem;color:#a67c00">Hinweis: Auf diesem Ger\u00e4t ist noch keine Praxis gespeichert. Benutzername oder E-Mail trotzdem m\u00f6glich; bei mehreren Konten ggf. zus\u00e4tzliche Auswahl.</p>') +
'<div class="login-field"><label>Benutzername oder E-Mail</label><input type="text" id="forgot-login" autocomplete="username" placeholder="z. B. Suro 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();
}
function renderForgotPickUser(loginEmail, candidates) {
var box = document.getElementById('login-box');
box.className = 'login-box';
var buttons = (candidates || []).map(function(c, i) {
var lab = esc(c.display_name || '') + (c.practice_name ? ' \u2013 ' + esc(c.practice_name) : '');
return '<button type="button" class="login-btn" style="margin-bottom:8px;width:100%;text-align:left" onclick="doForgotPickCandidate(' + i + ')">' + lab + '</button>';
}).join('');
box.innerHTML =
'<h2>Passwort vergessen</h2>' +
'<p>Diese E-Mail-Adresse ist <strong>mehreren Benutzerkonten</strong> zugeordnet. Bitte w\u00e4hlen Sie Ihr Konto oder geben Sie Ihren Benutzernamen ein.</p>' +
'<div class="login-field" style="margin-top:10px">' + buttons + '</div>' +
'<div class="login-field"><label>Benutzername (exakt)</label><input type="text" id="forgot-pick-manual" autocomplete="username" placeholder="Wie in der Praxis angezeigt"></div>' +
'<button class="login-btn" onclick="doForgotPickManual()">Auswahl best\u00e4tigen</button>' +
'<div class="login-error" id="login-error"></div>' +
'<div class="login-switch"><a onclick="renderForgotPasswordForm()">&larr; Zur\u00fcck</a></div>';
window._forgotCandidates = candidates;
window._forgotLoginEmail = loginEmail;
document.getElementById('forgot-pick-manual').focus();
}
async function doForgotPickCandidate(i) {
var c = (window._forgotCandidates || [])[i];
if (!c) return;
await _postForgotSecondStep(window._forgotLoginEmail, c.display_name, c.practice_id);
}
async function doForgotPickManual() {
var v = (document.getElementById('forgot-pick-manual').value || '').trim();
var errEl = document.getElementById('login-error');
var cand = window._forgotCandidates || [];
var hit = null;
for (var j = 0; j < cand.length; j++) {
if ((cand[j].display_name || '') === v) { hit = cand[j]; break; }
}
if (!hit) {
errEl.style.color = '#842029';
errEl.textContent = 'Kein passender Benutzername. Bitte exakt wie in der Praxis w\u00e4hlen oder eingeben.';
return;
}
await _postForgotSecondStep(window._forgotLoginEmail, hit.display_name, hit.practice_id);
}
async function _postForgotSecondStep(loginEmail, displayName, practiceId) {
var errEl = document.getElementById('login-error');
errEl.style.color = '';
errEl.textContent = '';
try {
var r = await fetch(API_BASE + '/auth/forgot_password', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
login: loginEmail,
display_name: displayName,
practice_id: practiceId || getPracticeIdOrEmpty()
})
});
var d = await r.json().catch(function() { 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.success === false && d.message) {
errEl.style.color = '#842029';
errEl.textContent = d.message;
return;
}
errEl.style.color = '#155724';
errEl.textContent = d.message || 'Link wurde gesendet.';
} catch (e) {
errEl.style.color = '#842029';
errEl.textContent = 'Fehler beim Senden.';
}
}
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 Benutzername 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 === 'pick_user' && d.candidates && d.candidates.length) {
renderForgotPickUser(d.login || raw, d.candidates);
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.success === false && d.message) {
errEl.style.color = '#842029';
errEl.textContent = d.message;
return;
}
errEl.style.color = '#155724';
errEl.textContent = 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.display_name || d.email || '').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 =
'<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();
}
async function onAuthSuccess() {
var me = await checkAuth();
if (!me) return false;
currentSession = me;
applyActiveConversationFromSession();
hideLoginOverlay();
updateSbMe();
loadUsersFromServer();
loadTasksFromServer();
loadMessages();
loadPracticeInfo();
refreshSettingsUserSection();
refreshTaskTabsForRole();
startPolling();
return true;
}
async function doSetup() {
var practiceName = (document.getElementById('setup-practice').value || '').trim();
var name = (document.getElementById('setup-name').value || '').trim();
var email = (document.getElementById('setup-email').value || '').trim();
var pass = document.getElementById('setup-pass').value || '';
var errEl = document.getElementById('login-error');
if (!practiceName) { errEl.textContent = 'Bitte Praxisnamen eingeben.'; return; }
if (!name) { errEl.textContent = 'Bitte Ihren Namen eingeben.'; return; }
if (!pass || pass.length < 4) { errEl.textContent = 'Passwort (min. 4 Zeichen) erforderlich.'; return; }
try {
var r = await fetch(API_BASE + '/auth/setup', {
method: 'POST', credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: name, password: pass, practice_name: practiceName, email: email})
});
if (r.ok) {
try {
var sd = await r.json();
if (sd.practice_id) localStorage.setItem('aza_practice_id', sd.practice_id);
} catch(e) {}
if (!await onAuthSuccess()) errEl.textContent = 'Einrichtung fehlgeschlagen.';
} else {
var d = await r.json().catch(function(){ return {}; });
errEl.textContent = d.detail || d.error || 'Einrichtung fehlgeschlagen.';
}
} catch(e) { errEl.textContent = 'Verbindungsfehler.'; }
}
async function doLogin() {
var name = (document.getElementById('login-name').value || '').trim();
var pass = document.getElementById('login-pass').value || '';
var errEl = document.getElementById('login-error');
errEl.style.color = '';
if (!name || !pass) { errEl.textContent = 'Benutzername/E-Mail und Passwort erforderlich.'; return; }
var inv = getUrlInviteCode();
try {
var r = await fetch(API_BASE + '/auth/login', {
method: 'POST', credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
name: name,
password: pass,
practice_id: getPracticeIdOrEmpty(),
invite_code: inv
})
});
if (r.ok) {
try {
var ld = await r.json();
if (ld.practice_id) localStorage.setItem('aza_practice_id', ld.practice_id);
if (ld.practice_bind_source) try {
sessionStorage.setItem('aza_practice_bind', ld.practice_bind_source);
} catch (eB) {}
var saveUser = (ld.display_name || name).trim();
localStorage.setItem('aza_last_login_user', saveUser);
try {
var u = new URL(window.location.href);
if (u.searchParams.get('invite')) {
u.searchParams.delete('invite');
history.replaceState({}, '', u.pathname + u.search + u.hash);
}
} catch (eu) {}
} catch(e) {}
if (!await onAuthSuccess()) errEl.textContent = 'Anmeldung fehlgeschlagen.';
} else {
var d = await r.json().catch(function(){ return {}; });
errEl.style.color = '#842029';
var det = d.detail;
if (typeof det === 'object' && det !== null && det.message) {
errEl.textContent = det.message;
} else if (typeof det === 'string') {
errEl.textContent = det;
} else {
errEl.textContent = d.error || 'Anmeldung fehlgeschlagen.';
}
}
} catch(e) { errEl.style.color = '#842029'; errEl.textContent = 'Verbindungsfehler.'; }
}
async function doRegister() {
var code = (document.getElementById('reg-code').value || '').trim();
var name = (document.getElementById('reg-name').value || '').trim();
var pass = document.getElementById('reg-pass').value || '';
var role = document.getElementById('reg-role').value || 'empfang';
var errEl = document.getElementById('login-error');
if (!code || !name || !pass) { errEl.textContent = 'Alle Felder erforderlich.'; return; }
try {
var r = await fetch(API_BASE + '/auth/register', {
method: 'POST', credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({invite_code: code, name: name, password: pass, role: role})
});
if (r.ok) {
try {
var rd = await r.json();
if (rd.practice_id) localStorage.setItem('aza_practice_id', rd.practice_id);
if (rd.practice_bind_source) try {
sessionStorage.setItem('aza_practice_bind', rd.practice_bind_source);
} catch (eBr) {}
try {
var ur = new URL(window.location.href);
if (ur.searchParams.get('invite')) {
ur.searchParams.delete('invite');
history.replaceState({}, '', ur.pathname + ur.search + ur.hash);
}
} catch (er) {}
} catch(e) {}
if (!await onAuthSuccess()) errEl.textContent = 'Registrierung fehlgeschlagen.';
} else {
var d = await r.json().catch(function(){ return {}; });
errEl.textContent = d.detail || d.error || 'Registrierung fehlgeschlagen.';
}
} catch(e) { errEl.textContent = 'Verbindungsfehler.'; }
}
async function doLogout() {
try { await fetch(API_BASE + '/auth/logout', {method: 'POST', credentials: 'include'}); } catch(e) {}
currentSession = null;
practiceUsers = [];
lastPracticeInfoForHint = null;
serverTasks = [];
allMessages = [];
allThreads = [];
activeConversation.mode = 'all';
activeConversation.practiceId = '';
activeConversation.currentUserId = '';
activeConversation.currentUserKey = '';
activeConversation.currentDisplayName = '';
activeConversation.targetUserId = '';
activeConversation.targetUserKey = '';
activeConversation.targetDisplayName = '';
activeDmPeer = null;
activeDmPeerUid = '';
activeThreadId = null;
activeView = 'allgemein';
lastDataHash = '';
previousThreadIds = new Set();
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) {}
showLoginOverlay();
}
/* ===================================================================
INIT
=================================================================== */
(async function init() {
var sel = document.getElementById('tone-select');
if (sel) {
TONE_PRESETS.forEach(function(t, i) {
var o = document.createElement('option');
o.value = i; o.textContent = t.name;
if (i === currentToneIdx) o.selected = true;
sel.appendChild(o);
});
}
var vs = document.getElementById('vol-slider');
var vd = document.getElementById('vol-display');
if (vs) vs.value = Math.round(volume * 100);
if (vd) vd.textContent = Math.round(volume * 100) + '%';
initRepeatUI();
updateSortBtn();
updateSoundBtn();
applyFontSize();
setupReplyBarDropPaste();
bindConversationMsgActionsOnce();
bindSidebarUserClicks();
initShellPinUi();
ensureDesktopShellAttr();
restoreTasksPanelState();
restoreDesktopSidebarState();
applyDesktopShellChrome();
var urlParams = new URLSearchParams(window.location.search);
var resetToken = urlParams.get('reset_token');
if (resetToken) {
stopPolling();
document.getElementById('login-overlay').classList.remove('hidden');
try {
var vr = await fetch(
API_BASE + '/auth/reset_verify?reset_token=' + encodeURIComponent(resetToken)
);
var vd = await vr.json().catch(function() { return {valid: false}; });
if (!vd.valid) {
renderResetPasswordInvalid(vd.detail || 'Ung\u00fcltiger oder abgelaufener Link.');
return;
}
renderResetPasswordForm(resetToken);
} catch (e) {
renderResetPasswordInvalid('Verbindungsfehler. Bitte sp\u00e4ter erneut versuchen.');
}
return;
}
var inviteParam = (urlParams.get('invite') || '').trim();
var resolvedInv = null;
if (inviteParam) {
try {
var ir = await fetch(API_BASE + '/auth/resolve_invite?code=' + encodeURIComponent(inviteParam));
resolvedInv = await ir.json().catch(function() { return null; });
if (resolvedInv && resolvedInv.valid && resolvedInv.practice_id) {
try {
localStorage.setItem('aza_practice_id', resolvedInv.practice_id);
} catch(e) {}
try { sessionStorage.setItem('aza_practice_bind', 'invite_code'); } catch(e2) {}
} else if (resolvedInv && resolvedInv.valid === false) {
stopPolling();
document.getElementById('login-overlay').classList.remove('hidden');
showLoginOverlay();
var badInv = document.getElementById('login-error');
if (badInv) {
badInv.style.color = '#842029';
badInv.textContent = 'Ung\u00fcltiger oder veralteter Einladungscode. Bitte aktuellen Link aus der AzA-Hauptinstallation anfordern.';
}
return;
}
} catch(e) {}
}
var me = await checkAuth();
if (me && inviteParam && resolvedInv && resolvedInv.valid && resolvedInv.practice_id &&
me.practice_id && me.practice_id !== resolvedInv.practice_id) {
try { await fetch(API_BASE + '/auth/logout', {method: 'POST', credentials: 'include'}); } catch(e) {}
me = null;
try {
localStorage.setItem('aza_practice_id', resolvedInv.practice_id);
} catch(e) {}
try { sessionStorage.setItem('aza_practice_mismatch_cleared', '1'); } catch(e3) {}
}
if (me && me.practice_id) {
try { localStorage.setItem('aza_practice_id', me.practice_id); } catch(e) {}
}
if (!me) {
stopPolling();
document.getElementById('login-overlay').classList.remove('hidden');
if (inviteParam) {
renderRegisterForm();
setTimeout(function() {
var codeInput = document.getElementById('reg-code');
if (codeInput) codeInput.value = inviteParam;
}, 50);
} else {
showLoginOverlay();
}
return;
}
currentSession = me;
applyActiveConversationFromSession();
hideLoginOverlay();
updateSbMe();
loadUsersFromServer();
loadTasksFromServer();
loadMessages();
loadPracticeInfo();
refreshSettingsUserSection();
refreshTaskTabsForRole();
startPolling();
})();
/* ===================================================================
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 ? ('Direkt: ' + activeDmPeer) :
(activeView === 'archiv' ? 'Archiv Unterhaltungen w\u00e4hlen' : 'Allgemein Posteingang');
}
}
}
function namesRoughlyEqual(a, b) {
return String(a || '').trim().toLowerCase() === String(b || '').trim().toLowerCase();
}
function canSeeTeamTasks() {
var r = currentSession && currentSession.role ? String(currentSession.role).toLowerCase() : '';
return r === 'admin' || r === 'empfang';
}
function filteredTasksForUi() {
var me = currentSession ? String(currentSession.display_name || '').trim() : '';
var list = serverTasks.slice();
if (taskTabFilter === 'mine')
return list.filter(function(t) { return namesRoughlyEqual(t.assignee, me); });
if (taskTabFilter === 'sent')
return list.filter(function(t) { return namesRoughlyEqual(t.created_by, me); });
return list.filter(function(t) { 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() {
var teamBtn = document.getElementById('tp-tab-team');
if (!teamBtn) return;
var show = canSeeTeamTasks();
teamBtn.classList.toggle('hidden', !show);
if (!show && taskTabFilter === 'team') setTaskTab('mine');
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 userPresenceDotClass(u, isMe) {
if (isMe) return 'online';
var devs = u.devices;
if (devs && devs.length) {
for (var i = 0; i < devs.length; i++) {
if (deviceRecentlyActive(devs[i])) return 'online';
}
}
return 'offline';
}
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'));
}
function toggleTasksPanel() {
var p = document.getElementById('tasks-panel');
if (!p) return;
p.classList.toggle('collapsed');
try {
localStorage.setItem(tasksCollapsedStorageKey(), p.classList.contains('collapsed') ? '1' : '0');
} catch (e) {}
updateSidebarNavHighlight();
}
function restoreTasksPanelState() {
var p = document.getElementById('tasks-panel');
if (!p) return;
try {
var c = localStorage.getItem(tasksCollapsedStorageKey());
if (isDesktopShellMode()) {
if (c === '0') p.classList.remove('collapsed');
else p.classList.add('collapsed');
} else {
if (c === '1') p.classList.add('collapsed');
else p.classList.remove('collapsed');
}
} catch (e) {}
}
/** ?desktop_shell=1: eigener Oberflaechenmodus ohne normale Ansicht zu aendern (data-desktop-shell, Panels). */
function ensureDesktopShellAttr() {
try {
var sp = new URLSearchParams(window.location.search);
if (String(sp.get('desktop_shell') || '').trim() === '1') {
document.documentElement.setAttribute('data-desktop-shell', '1');
} else {
document.documentElement.removeAttribute('data-desktop-shell');
}
} catch (e) {}
}
function isDesktopShellMode() {
try {
return document.documentElement.getAttribute('data-desktop-shell') === '1';
} catch (e) {
return false;
}
}
function tasksCollapsedStorageKey() {
return isDesktopShellMode() ? 'empfang_tasks_collapsed_ds' : 'empfang_tasks_collapsed';
}
function restoreDesktopSidebarState() {
if (!isDesktopShellMode()) return;
var sb = document.getElementById('sidebar');
if (!sb) return;
try {
if (localStorage.getItem('empfang_sidebar_collapsed_ds') === '1') sb.classList.add('sidebar-ds-collapsed');
else sb.classList.remove('sidebar-ds-collapsed');
} catch (e) {}
}
function toggleDesktopSidebarCollapsed() {
if (!isDesktopShellMode()) return;
var sb = document.getElementById('sidebar');
if (!sb) return;
sb.classList.toggle('sidebar-ds-collapsed');
try {
localStorage.setItem('empfang_sidebar_collapsed_ds', sb.classList.contains('sidebar-ds-collapsed') ? '1' : '0');
} catch (e) {}
}
function restoreDesktopContextPanelState() {
if (!isDesktopShellMode()) return;
var card = document.getElementById('dcp-panel-card');
var btn = document.getElementById('dcp-toggle-btn');
if (!card || !btn) return;
try {
var v = localStorage.getItem('empfang_dcp_collapsed_ds');
if (v === '0') {
card.classList.remove('dcp-panel-collapsed');
btn.setAttribute('aria-expanded', 'true');
} else {
card.classList.add('dcp-panel-collapsed');
btn.setAttribute('aria-expanded', 'false');
}
} catch (e) {}
}
function toggleDesktopContextPanel() {
if (!isDesktopShellMode()) return;
var card = document.getElementById('dcp-panel-card');
var btn = document.getElementById('dcp-toggle-btn');
if (!card || !btn) return;
card.classList.toggle('dcp-panel-collapsed');
var collapsed = card.classList.contains('dcp-panel-collapsed');
btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
try {
localStorage.setItem('empfang_dcp_collapsed_ds', collapsed ? '1' : '0');
} catch (e) {}
}
/** Zwischenablage an Eingabe anhaengen — kein Logging des Inhalts. */
function dcpPasteFromClipboard() {
if (!navigator.clipboard || !navigator.clipboard.readText) {
alert('Zwischenablage hier nicht direkt lesbar.\nBitte in das Eingabefeld mit Strg+V einf\u00fcgen.');
return;
}
navigator.clipboard.readText().then(function(t) {
var ta = document.getElementById('reply-text');
var chunk = (t || '').trim();
if (!ta || !chunk) return;
var cur = String(ta.value || '');
var sep = cur && !/\s$/.test(cur) ? '\n' : '';
ta.value = cur + sep + chunk;
try {
ta.dispatchEvent(new Event('input', {bubbles: true}));
} catch (eIn) {}
try {
autoGrowTextarea(ta);
} catch (eAg) {}
}).catch(function() {
alert('Kein Lesezugriff auf die Zwischenablage.\nBitte Strg+V im Eingabefeld verwenden.');
});
}
function applyDesktopShellChrome() {
if (!isDesktopShellMode()) return;
try {
var nt = document.getElementById('nr-tasks');
if (nt) nt.style.removeProperty('display');
} catch (eRm) {}
restoreDesktopContextPanelState();
updateSidebarNavHighlight();
}
function setView(view) {
activeView = view;
activeThreadId = null;
applyActiveConversationFromSession();
activeConversation.mode = 'all';
activeConversation.targetUserId = '';
activeConversation.targetUserKey = '';
activeConversation.targetDisplayName = '';
syncLegacyDmFromActiveConversation();
dmWantsThreadList = false;
updateSidebarNavHighlight();
lastDataHash = '';
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) {
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();
}
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 na = String(a && a.display_name || '');
var nb = String(b && b.display_name || '');
try {
return na.localeCompare(nb, 'de', { sensitivity: 'base' });
} catch (lc) {
return na.localeCompare(nb);
}
});
el.innerHTML = sorted.map(function(u) {
var name = u.display_name || '';
var isMe = currentSession && name === currentSession.display_name;
var dotCls = userPresenceDotClass(u, isMe);
var uidAttr = escAttr(String(u.user_id || ''));
var uidHtml = uidAttr ? ' data-peer-uid="' + uidAttr + '"' : '';
var clsExtra = isMe ? ' me sb-user-self sb-user-disabled' : '';
var title = isMe ? 'Das sind Sie selbst (Selbstchat ist nicht m\u00f6glich)' : ('Direktchat mit ' + esc(name).replace(/"/g, '&quot;'));
return '<div class="sb-user' + clsExtra + '" data-peer="' + encodeURIComponent(name) + '"' + uidHtml +
(isMe ? ' data-self="1"' : '') + ' title="' + title + '">' +
'<span class="sb-dot ' + dotCls + '"></span>' +
'<span class="sb-user-name">' + esc(name) + '</span>' +
(u.role ? '<span class="sb-group-tag">' + esc(u.role) + '</span>' : '') +
(isMe ? '<span class="sb-self-tag" style="margin-left:auto;font-size:.65rem;color:#8a9aaa">Sie</span>' : '') +
'</div>';
}).join('');
updateSidebarNavHighlight();
}
function updateSbMe() {
var nameEl = document.getElementById('sb-me-name');
var roleEl = document.getElementById('sb-me-role');
if (currentSession) {
nameEl.textContent = 'Angemeldet als ' + String(currentSession.display_name || '').trim();
roleEl.textContent = currentSession.role || '';
} else {
nameEl.textContent = 'Nicht angemeldet';
roleEl.textContent = '';
}
}
function refreshSettingsUserSection() {
var box = document.getElementById('settings-user-mgmt');
if (!box) return;
var r = currentSession && currentSession.role ? String(currentSession.role).toLowerCase() : '';
var ok = r === 'admin' || r === 'empfang';
box.style.display = ok ? 'block' : 'none';
var oa = document.getElementById('adu-opt-admin');
var oe = document.getElementById('adu-opt-empfang');
if (oa) oa.hidden = r !== 'admin';
if (oe) oe.hidden = r !== 'admin';
}
function openAddUserDialog() {
if (!currentSession) return;
var role = currentSession.role ? String(currentSession.role).toLowerCase() : '';
if (role !== 'admin' && role !== 'empfang') return;
var pan = document.getElementById('add-user-overlay');
if (!pan) return;
document.getElementById('adu-name').value = '';
document.getElementById('adu-pass').value = '';
document.getElementById('adu-pass2').value = '';
document.getElementById('adu-error').textContent = '';
document.getElementById('adu-role').value = 'mpa';
pan.classList.remove('hidden');
setTimeout(function() { try { document.getElementById('adu-name').focus(); } catch (ea) {} }, 50);
}
function closeAddUserDialog() {
var pan = document.getElementById('add-user-overlay');
if (pan) pan.classList.add('hidden');
}
async function submitSecureAddUser() {
var err = document.getElementById('adu-error');
var name = (document.getElementById('adu-name').value || '').trim();
var role = (document.getElementById('adu-role').value || 'mpa').trim();
var p1 = document.getElementById('adu-pass').value || '';
var p2 = document.getElementById('adu-pass2').value || '';
if (!name) { err.textContent = 'Bitte einen Namen eingeben.'; return; }
if (p1.length < 4) { err.textContent = 'Passwort mindestens 4 Zeichen.'; return; }
if (p1 !== p2) { err.textContent = 'Passw\u00f6rter stimmen nicht \u00fcberein.'; return; }
err.textContent = '';
try {
var res = await apiFetch(API_BASE + '/users', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'add_secure',
name: name,
role: role,
password: p1,
password_repeat: p2
})
});
var dj = await res.json().catch(function() { return {}; });
if (!res.ok) {
var msg = dj.detail;
if (typeof msg !== 'string') {
if (Array.isArray(msg) && msg[0] && msg[0].msg) msg = msg.map(function(x) { return x.msg; }).join(' ');
else msg = dj.message || '';
}
err.textContent = msg || 'Anlage fehlgeschlagen.';
return;
}
closeAddUserDialog();
await loadUsersFromServer();
notifyOtherTabs();
} catch (eAu) {
err.textContent = 'Verbindungsfehler.';
}
}
/* ===================================================================
USERS (server only)
=================================================================== */
async function loadUsersFromServer() {
try {
var r = await apiFetch(API_BASE + '/users');
var d = await r.json();
practiceUsers = d.users_full || [];
renderSidebarUsers();
if (activeConversation.mode === 'direct' && activeConversation.targetDisplayName) {
var looked = peerUidForDisplay(activeConversation.targetDisplayName);
if (looked && looked !== activeConversation.targetUserId) {
activeConversation.targetUserId = looked;
activeConversation.targetUserKey = looked;
syncLegacyDmFromActiveConversation();
lastDataHash = '';
loadMessages();
}
}
if (lastPracticeInfoForHint) {
updateSbUsersPracticeHint(
String(lastPracticeInfoForHint.practice_id || '').trim(),
String(lastPracticeInfoForHint.practice_name || '').trim()
);
}
} catch(e) {}
}
/* ===================================================================
MESSAGES
=================================================================== */
async function loadMessages() {
try {
if (!currentSession) return;
applyActiveConversationFromSession();
var mySeq = ++_loadMessagesSeq;
var pidQ = String((currentSession.practice_id || activeConversation.practiceId || '')).trim();
var meUid = String((currentSession.user_id || '')).trim();
var d;
var msgs = [];
var convKey = '';
if (activeConversation.mode === 'direct') {
var peerUid = String(activeConversation.targetUserId || '').trim();
if (!pidQ || !meUid || !peerUid || meUid === peerUid) {
if (mySeq !== _loadMessagesSeq) return;
allMessages = [];
_dmV2State.lastLoadCount = 0;
_dmV2State.lastConvKey = '';
dmDiagWrite('LOAD_BLOCKED');
renderView();
return;
}
var qs2 = 'practice_id=' + encodeURIComponent(pidQ) +
'&sender_user_id=' + encodeURIComponent(meUid) +
'&recipient_user_id=' + encodeURIComponent(peerUid);
console.debug('[Empfang] AZA_CHAT_LOAD mode=direct practice=' + pidQ +
' me=' + meUid + ' peer=' + peerUid);
var r2 = await apiFetch(API_BASE + '/dm/conversation?' + qs2);
if (!r2.ok) {
if (mySeq !== _loadMessagesSeq) return;
var errTxt = '';
try { var ej = await r2.json(); errTxt = (ej && (ej.detail || ej.error)) || ''; } catch (_e) {}
console.warn('[Empfang] dm/conversation HTTP ' + r2.status + ' ' + errTxt);
allMessages = [];
_dmV2State.lastLoadCount = 0;
dmDiagWrite('LOAD_HTTP_' + r2.status);
renderView();
return;
}
d = await r2.json();
msgs = d.messages || [];
convKey = d.conversation_key || '';
_dmV2State.lastConvKey = convKey;
_dmV2State.lastLoadCount = msgs.length;
console.debug('[Empfang] AZA_CHAT_LOAD count=' + msgs.length + ' conv=' + convKey);
} else {
var audienceParam = '';
var meParam = (currentSession.display_name || '').trim();
var qs = 'audience=' + encodeURIComponent(audienceParam);
if (meParam) qs += '&me=' + encodeURIComponent(meParam);
if (meUid) qs += '&me_user_id=' + encodeURIComponent(meUid);
if (pidQ) qs += '&practice_id=' + encodeURIComponent(pidQ);
console.debug('[Empfang] loadConversation legacy mode=all (read-only legacy view)');
var r = await apiFetch(API_BASE + '/conversation?' + qs);
d = await r.json();
msgs = d.messages || [];
_dmV2State.lastConvKey = '';
_dmV2State.lastLoadCount = msgs.length;
}
dmDiagWrite('LOAD_OK');
if (mySeq !== _loadMessagesSeq) return;
var raw = JSON.stringify({
aud: audienceParam,
tick: d.tick || 0,
msgs: msgs
});
if (raw === lastDataHash) return;
lastDataHash = raw;
allMessages = msgs;
allMessages.forEach(function(m) { if (!m.thread_id) m.thread_id = m.id; });
buildThreads();
var currentIds = new Set(allThreads.map(function(t){ return t.tid; }));
var hasNewDesktop = false;
currentIds.forEach(function(tid) {
if (!previousThreadIds.has(tid)) {
var t = allThreads.find(function(th){ return th.tid === tid; });
if (t && !(t.root.absender || '').includes('Empfang')) hasNewDesktop = true;
}
});
if (previousThreadIds.size > 0 && hasNewDesktop && !justSentReply) {
playTone(currentToneIdx);
startRepeatTone();
}
justSentReply = false;
previousThreadIds = currentIds;
var openCount = countGeneralOpenThreads();
document.getElementById('count-badge').textContent = openCount + ' offen';
var ob = document.getElementById('sb-badge-open');
if (ob) ob.textContent = String(openCount || '');
renderView();
} catch (e) {
if (e.message === 'auth') return;
var cs = document.getElementById('conversation-scroll');
if (cs) cs.innerHTML =
'<div class="conv-empty">Backend nicht erreichbar.<br>Bitte Verbindung pr&uuml;fen.</div>';
}
}
function buildThreads() {
var threadMap = {};
allMessages.forEach(function(m) {
if (!threadMap[m.thread_id]) threadMap[m.thread_id] = [];
threadMap[m.thread_id].push(m);
});
allThreads = [];
Object.keys(threadMap).forEach(function(tid) {
var arr = threadMap[tid].slice().sort(function(a,b){
return (a.empfangen||'').localeCompare(b.empfangen||'');
});
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;
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'; });
}
function formatMessengerDayKey(iso) {
var raw = String(iso || '').replace(' ', 'T');
var d = Date.parse(raw);
if (!isFinite(d)) return '';
var dt = new Date(d);
var y = dt.getFullYear(), mo = dt.getMonth() + 1, da = dt.getDate();
return y + '-' + (mo < 10 ? '0' : '') + mo + '-' + (da < 10 ? '0' : '') + da;
}
function formatMessengerDateLabel(iso) {
var raw = String(iso || '').replace(' ', 'T');
var d = Date.parse(raw);
if (!isFinite(d)) return '';
var msgDate = new Date(d);
var today = new Date();
today.setHours(0, 0, 0, 0);
var dd = new Date(msgDate);
dd.setHours(0, 0, 0, 0);
var diffDays = Math.round((today.getTime() - dd.getTime()) / 86400000);
if (diffDays === 0) return 'Heute';
if (diffDays === 1) return 'Gestern';
return msgDate.getDate() + '.' + (msgDate.getMonth() + 1) + '.' + msgDate.getFullYear();
}
function messengerChronoSorted(msgs) {
return msgs.slice().sort(function(a, b) {
var ta = a.empfangen || a.zeitstempel || '';
var tb = b.empfangen || b.zeitstempel || '';
return ta.localeCompare(tb);
});
}
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) {
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 = 'Direkt: ' + activeDmPeer;
else titleEl.textContent = 'Bitte Benutzer w\u00e4hlen';
}
if (backBtn) {
backBtn.classList.toggle('visible', !!(activeDmPeer));
}
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: 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 timeShort = tsFull.slice(11, 16) || 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) {
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 {
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 + '" role="article" tabindex="-1">';
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 += '<button type="button" class="conv-msg-act-in" data-mid="' + escAttr(m.id || '') +
'" title="Aufgabe aus Nachricht">+ Aufgabe</button>';
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) backBtn.classList.add('visible');
if (titleEl) titleEl.textContent = 'Direkt: ' + 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">Direkt: ' + esc(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 time = (m.zeitstempel || m.empfangen || '').slice(11, 16) || '';
var date = (m.zeitstempel || m.empfangen || '').slice(0, 10) || '';
var today = new Date().toISOString().slice(0, 10);
var timeDisplay = date === today ? time : date.slice(5) + ' ' + time;
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(timeDisplay) + '</div>' +
'</div>' +
'<div class="tl-row2">' +
'<div class="tl-preview">' + esc(preview) + '</div>' +
(replyCount ? '<span class="tl-replies">' + replyCount + ' Antw.</span>' : '') +
'<span class="tl-status ' + (isDone ? 'erledigt' : 'offen') + '">' + (isDone ? 'Erledigt' : 'Offen') + '</span>' +
'</div>' +
'</div>';
}).join('');
}
function selectThread(tid) {
activeThreadId = tid;
dmWantsThreadList = false;
stopRepeatTone();
renderView();
}
function backToList() {
activeThreadId = null;
if (activeDmPeer) dmWantsThreadList = true;
renderView();
}
function smartBackChatTop() {
if (activeThreadId) {
backToList();
return;
}
if (activeDmPeer || activeConversation.mode === 'direct') {
applyActiveConversationFromSession();
activeConversation.mode = 'all';
activeConversation.targetUserId = '';
activeConversation.targetUserKey = '';
activeConversation.targetDisplayName = '';
syncLegacyDmFromActiveConversation();
activeThreadId = null;
dmWantsThreadList = false;
updateSidebarNavHighlight();
lastDataHash = '';
loadMessages();
}
}
function escAttr(str) {
return String(str || '').replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/'/g, '&#39;');
}
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 bindConversationMsgActionsOnce() {
var root = document.getElementById('conversation-scroll');
if (!root || root._azaMsgActs) return;
root._azaMsgActs = true;
root.addEventListener('click', function(ev) {
var btn = ev.target.closest('.conv-msg-act-in, .conv-doc-msg-act');
if (!btn) return;
var mid = btn.getAttribute('data-mid');
var rec = (_azaMsgLookup || {})[mid];
if (!rec) return;
ev.preventDefault();
ev.stopPropagation();
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.add('visible');
if (titleEl) {
if (activeDmPeer) titleEl.textContent = 'Direkt: ' + 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: (m.zeitstempel || m.empfangen || ''),
rawBody: rootBodyTxt,
assignGuess: asgR,
chatLabel: chatLineRoot
};
var html = '';
html += '<div class="conv-doc">';
html += '<button type="button" class="conv-doc-msg-act" data-mid="' + escAttr(m.id) + '" title="Aufgabe aus Nachricht">+ Aufgabe</button>';
html += '<div class="conv-doc-head">';
html += '<span class="conv-doc-sender">' + esc(m.absender || '\u2013') + '</span>';
html += '<span class="conv-doc-time">' + esc(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) {
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 {
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: (r.zeitstempel || r.empfangen || ''),
rawBody: msgBody,
assignGuess: asgRepl,
chatLabel: chatLineR
};
var isFromEmpfang = (r.absender || '').includes('Empfang');
var attHtml = '';
var ratt = r.extras && r.extras.attachments;
if (ratt && ratt.length) {
ratt.forEach(function(a) {
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>';
}
});
}
html += '<div class="conv-msg ' + (isFromEmpfang ? 'from-me' : 'from-other') + '" tabindex="-1">' +
'<button type="button" class="conv-msg-act-in" data-mid="' + escAttr(r.id) + '" title="Aufgabe aus Nachricht">+ Aufgabe</button>' +
'<div class="conv-msg-meta">' +
'<span class="conv-msg-sender">' + esc(r.absender || '') + '</span>' +
'<span>' + esc(r.zeitstempel || r.empfangen || '') + '</span>' +
'</div>' +
'<div class="conv-msg-text">' + esc(r.kommentar || '') + '</div>' + attHtml +
'</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;
el.textContent = pendingReplyAttachments.length
? pendingReplyAttachments.length + ' Bild(er) angeh\u00e4ngt'
: '';
}
function queueReplyImageFiles(files) {
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 || u.display_name === currentSession.display_name) return;
var o = document.createElement('option');
o.value = u.display_name;
o.textContent = u.display_name + (u.role ? ' (' + u.role + ')' : '');
if (u.user_id) o.setAttribute('data-peer-uid', String(u.user_id));
sel.appendChild(o);
});
var gr = document.querySelector('input[name="nc-mode"][value="general"]');
if (gr) gr.checked = true;
syncNcPeerSelect();
document.getElementById('new-chat-overlay').classList.remove('hidden');
}
function closeNewChatDialog() {
document.getElementById('new-chat-overlay').classList.add('hidden');
}
function syncNcPeerSelect() {
var mode = (document.querySelector('input[name="nc-mode"]:checked') || {}).value;
document.getElementById('nc-peer-select').disabled = mode !== 'dm';
}
function launchNewChatWindow() {
var mode = (document.querySelector('input[name="nc-mode"]:checked') || {}).value || 'general';
var selWin = document.getElementById('nc-peer-select');
var peer = (selWin.value || '').trim();
var opt0 = selWin.selectedOptions && selWin.selectedOptions[0];
var puid = (opt0 && opt0.getAttribute('data-peer-uid')) || '';
if (mode === 'dm' && !peer) {
alert('Bitte einen Benutzer ausw\u00e4hlen.');
return;
}
closeNewChatDialog();
var q = mode === 'general' ? 'mode=general' : ('mode=dm&peer=' + encodeURIComponent(peer) +
(puid ? '&peer_uid=' + encodeURIComponent(puid) : ''));
var url = window.location.origin + '/empfang/chatwin.html?' + q;
var wname = 'aza_empfang_chat_' + String(peer || 'allgemein').replace(/\W/g, '_') + '_' + Date.now();
var w = window.open(url, wname, 'width=560,height=720,menubar=no,toolbar=no,scrollbars=yes,resizable=yes');
if (!w) {
alert('Das Chat-Fenster wurde blockiert. Bitte Popups f\u00fcr diese Seite erlauben, oder die Adresse manuell \u00f6ffnen:\n\n' + url);
}
}
async function sendReply() {
if (!currentSession) return;
if (activeView === 'archiv' && !activeDmPeer) return;
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 = '38px';
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',
{ sent_msg_id: _dmV2State.lastSentMsgId, sent_conv: _dmV2State.lastConvKey,
loaded_count: allMessages.length });
dmDiagWrite('VERIFY_FAIL');
alert('Nachricht wurde gespeichert (msg=' + _dmV2State.lastSentMsgId + '),\n' +
'aber im aktuellen Verlauf nicht gefunden.\n' +
'Bitte Diagnose unten am Bildschirm pr\u00fcfen.');
} else {
dmDiagWrite('VERIFY_OK');
}
notifyOtherTabs();
return;
} catch (e) {}
}
function autoGrowTextarea(el) {
el.style.height = '38px';
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
}
/* ===================================================================
ACTIONS (markDone, delete, copy, save, print)
=================================================================== */
async function markDone(id) {
try { await apiFetch(API_BASE + '/messages/' + id + '/done', {method:'POST'}); } catch(e) { return; }
stopRepeatTone();
lastDataHash = '';
loadMessages();
notifyOtherTabs();
}
async function deleteMsg(id) {
try { await apiFetch(API_BASE + '/messages/' + id, {method:'DELETE'}); } catch(e) { return; }
activeThreadId = null;
lastDataHash = '';
loadMessages();
notifyOtherTabs();
}
function copyThread(tid) {
var t = allThreads.find(function(th){ return th.tid === tid; });
if (!t) return;
var m = t.root;
var parts = [];
if (m.patient) parts.push('Patient: ' + m.patient);
if (m.therapieplan) parts.push('Therapieplan:\n' + m.therapieplan);
if (m.procedere) parts.push('Procedere:\n' + m.procedere);
if (m.kommentar) parts.push('Kommentar:\n' + m.kommentar);
parts.push('Absender: ' + (m.absender||'\u2013') + ' \u00b7 ' + (m.zeitstempel||''));
if (t.replies.length) {
parts.push('\n--- Chat-Verlauf ---');
t.replies.forEach(function(r) {
parts.push((r.absender||'') + ' (' + (r.zeitstempel||'') + '):\n' + (r.kommentar||''));
});
}
navigator.clipboard.writeText(parts.join('\n\n')).catch(function(){});
stopRepeatTone();
}
function saveThread(tid) {
var t = allThreads.find(function(th){ return th.tid === tid; });
if (!t) return;
var m = t.root;
var lines = ['=== AzA-Empfang Chat-Verlauf ===',
'Patient/Nr.: ' + (m.patient || '-'),
'Absender: ' + (m.absender || '-'),
'Zeitpunkt: ' + (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||'') + '] ' + (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(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(r.zeitstempel||r.empfangen||'') + '</div><div class="fv">' + esc(r.kommentar||'') + '</div></div>';
}).join('') : '') +
'</body></html>';
w.document.write(h); w.document.close(); w.print();
}
async function doCleanup() {
var days = parseInt(document.getElementById('cleanup-days').value, 10) || 30;
var el = document.getElementById('cleanup-result');
el.textContent = 'Bereinige...';
try {
var r = await apiFetch(API_BASE + '/cleanup', {
method:'POST', headers:{'Content-Type':'application/json'},
body:JSON.stringify({max_age_days:days})
});
var d = await r.json();
el.textContent = d.removed + ' entfernt, ' + d.remaining + ' verbleibend';
if (d.removed > 0) { lastDataHash = ''; loadMessages(); }
} catch(e) {
if (e.message !== 'auth') el.textContent = 'Fehler';
}
}
/* ===================================================================
DESKTOP-HÜLLE (pywebview): immer im Vordergrund
=================================================================== */
var shellPinBusy = false;
function initShellPinUi() {
var b = document.getElementById('shell-pin-btn');
if (!b) return;
var api = window.pywebview && pywebview.api;
if (!api || typeof api.toggle_on_top !== 'function' || typeof api.get_on_top !== 'function') {
b.style.display = 'none';
return;
}
b.style.display = '';
updateShellPinBtn();
}
async function updateShellPinBtn() {
var b = document.getElementById('shell-pin-btn');
if (!b || b.style.display === 'none') return;
try {
var v = await pywebview.api.get_on_top();
if (v) {
b.classList.add('on');
b.title = 'Immer im Vordergrund (aktiv)';
b.setAttribute('aria-pressed', 'true');
} else {
b.classList.remove('on');
b.title = 'Immer im Vordergrund';
b.setAttribute('aria-pressed', 'false');
}
} catch (e) {}
}
async function toggleShellPin() {
if (shellPinBusy) return;
var b = document.getElementById('shell-pin-btn');
if (!b || b.style.display === 'none') return;
var api = window.pywebview && pywebview.api;
if (!api || typeof api.toggle_on_top !== 'function') return;
shellPinBusy = true;
try {
await api.toggle_on_top();
await updateShellPinBtn();
} catch (e) {}
shellPinBusy = false;
}
/* ===================================================================
SOUND
=================================================================== */
function updateSoundBtn() {
var btn = document.getElementById('sound-btn');
if (soundEnabled) { btn.innerHTML = '&#128276; Ton'; btn.classList.remove('muted'); }
else { btn.innerHTML = '&#128277; Aus'; btn.classList.add('muted'); }
}
function toggleSound() {
soundEnabled = !soundEnabled;
localStorage.setItem('empfang_sound', soundEnabled ? 'on' : 'off');
updateSoundBtn();
if (soundEnabled) playTone(currentToneIdx);
}
function testCurrentSound() {
var prev = soundEnabled; soundEnabled = true;
playTone(currentToneIdx); soundEnabled = prev;
}
function playTone(idx) {
if (!soundEnabled) return;
var preset = TONE_PRESETS[idx] || TONE_PRESETS[0];
var vol = preset.vol * Math.max(0, volume);
try {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume();
var t = audioCtx.currentTime;
preset.notes.forEach(function(note) {
var g = audioCtx.createGain();
g.connect(audioCtx.destination);
g.gain.setValueAtTime(vol, t);
g.gain.linearRampToValueAtTime(vol * 0.6, t + note.d * 0.4);
g.gain.exponentialRampToValueAtTime(0.001, t + note.d);
var o = audioCtx.createOscillator();
o.type = preset.wave; o.frequency.setValueAtTime(note.f, t);
o.connect(g); o.start(t); o.stop(t + note.d + 0.05);
t += note.d;
});
} catch(e) {}
}
function startRepeatTone() {
stopRepeatTone();
if (repeatMs > 0 && soundEnabled) {
repeatInterval = setInterval(function(){ playTone(currentToneIdx); }, repeatMs);
}
}
function stopRepeatTone() {
if (repeatInterval) { clearInterval(repeatInterval); repeatInterval = null; }
}
function setVolume(v) {
volume = parseInt(v, 10) / 100;
document.getElementById('vol-display').textContent = Math.round(volume * 100) + '%';
localStorage.setItem('empfang_volume', volume);
}
function saveSoundSettings() {
currentToneIdx = parseInt(document.getElementById('tone-select').value, 10);
localStorage.setItem('empfang_tone_idx', currentToneIdx);
onRepeatChange();
}
function initRepeatUI() {
var sel = document.getElementById('repeat-select');
var known = ['0','2000','5000','30000'];
if (known.indexOf(String(repeatMs)) >= 0) {
sel.value = String(repeatMs);
document.getElementById('custom-sec-wrap').style.display = 'none';
} else if (repeatMs > 0) {
sel.value = 'custom';
document.getElementById('custom-sec-wrap').style.display = 'inline';
document.getElementById('custom-sec').value = Math.round(repeatMs / 1000);
} else { sel.value = '0'; }
}
function onRepeatChange() {
var sel = document.getElementById('repeat-select');
var cw = document.getElementById('custom-sec-wrap');
if (sel.value === 'custom') {
cw.style.display = 'inline';
repeatMs = Math.max(1, parseInt(document.getElementById('custom-sec').value, 10)) * 1000;
} else { cw.style.display = 'none'; repeatMs = parseInt(sel.value, 10); }
localStorage.setItem('empfang_repeat_ms', repeatMs);
}
/* ===================================================================
FONT / UI
=================================================================== */
function applyFontSize() {
var fs = currentFontSize + 'pt';
/* rem bezieht sich auf <html>: nur body zu setzen skaliert die meisten Inhalte nicht */
document.documentElement.style.fontSize = fs;
document.body.style.fontSize = fs;
var pct = Math.round((currentFontSize / EMPFANG_FONT_BASE_PT) * 100);
var disp = document.getElementById('font-size-display');
if (disp) disp.textContent = pct + '%';
var wrap = document.getElementById('font-ctrl-wrap');
if (wrap) wrap.title = 'Schriftgröße: ' + pct + '% (' + currentFontSize + ' pt, 100% = ' + EMPFANG_FONT_BASE_PT + ' pt)';
localStorage.setItem('empfang_font_size', String(currentFontSize));
}
function changeFontSize(d) {
currentFontSize = Math.max(5, Math.min(20, currentFontSize + d));
applyFontSize();
}
function toggleSettings() {
document.getElementById('settings-panel').classList.toggle('open');
}
function toggleSort() {
sortNewestFirst = !sortNewestFirst;
localStorage.setItem('empfang_sort', sortNewestFirst ? 'newest' : 'asc');
updateSortBtn();
lastDataHash = '';
loadMessages();
}
function updateSortBtn() {
var btn = document.getElementById('sort-btn');
if (sortNewestFirst) { btn.innerHTML = '&#8593; Neu'; btn.classList.add('active-sort'); }
else { btn.innerHTML = '&#8595; Alt'; btn.classList.remove('active-sort'); }
}
/* ===================================================================
DICTATION
=================================================================== */
function toggleDictation() {
if (isRecording) { stopDictation(); return; }
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() { stopDictation(); };
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 renderTodos() {
var el = document.getElementById('todo-list');
if (!el) return;
syncTaskTabButtons();
var list = filteredTasksForUi();
if (!list.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 === 'team') hint = 'Keine offenen Team-Aufgaben.';
el.innerHTML =
'<div class="todo-empty-msg"><strong>' + esc(hint) + '</strong><br/>' +
'+ Aufgabe in einer Chatnachricht oder mit + hier oben kurz festhalten.</div>';
return;
}
el.innerHTML = list.map(function(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 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' : '') + '" data-task-id="' + escAttr(String(t.task_id || '')) + '">' +
'<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>';
}).join('');
}
async function blurTodoTitle(taskId, box) {
var task = serverTasks.find(function(tt) { return String(tt.task_id) === taskId; });
if (!task || task.done) return;
var nw = (box.textContent || '').trim();
if (!nw) { renderTodos(); return; }
var prev = todoDisplayTitle(task);
if (nw === prev) return;
try {
await apiFetch(API_BASE + '/tasks/' + taskId + '/update', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({title: nw})
});
await loadTasksFromServer();
} catch(e) {}
}
async function blurTodoDetail(taskId, box) {
var task = serverTasks.find(function(tt) { return String(tt.task_id) === taskId; });
if (!task || task.done) return;
var titleKept = todoDisplayTitle(task);
var nw = (box.textContent || '').trim();
/* Voller Kartentext: Titel optional getrennt */
var fullText = (titleKept + (nw ? '\n\n' + nw : '')).trim();
if (fullText === String(task.text || '').trim()) return;
try {
await apiFetch(API_BASE + '/tasks/' + taskId + '/update', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({text: fullText})
});
await loadTasksFromServer();
} catch(e) {}
}
function copyTodoBrief(taskId) {
var tk = serverTasks.find(function(tt) { return String(tt.task_id) === taskId; });
if (!tk) return;
var s = todoDisplayTitle(tk) + '\n\n' + String(tk.text || '').trim();
try { navigator.clipboard.writeText(s).catch(function(){}); } catch (ecl) {}
}
function toggleTodoInput() {
var row = document.getElementById('todo-input-row');
var inp = document.getElementById('todo-inline-input');
var isOpen = row.classList.toggle('open');
if (isOpen) { inp.value = ''; inp.focus(); }
}
async function addTodoInline() {
var inp = document.getElementById('todo-inline-input');
var text = (inp.value || '').trim();
if (!text) return;
try {
await apiFetch(API_BASE + '/tasks', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: firstLineTitle(text),
text: text,
assignee: ''
})
});
inp.value = '';
inp.focus();
await loadTasksFromServer();
notifyOtherTabs();
} catch(e) {}
}
async function toggleTodo(taskId) {
var task = serverTasks.find(function(t) { return String(t.task_id) === taskId; });
if (!task) return;
try {
await apiFetch(API_BASE + '/tasks/' + taskId + '/update', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({done: !task.done})
});
await loadTasksFromServer();
notifyOtherTabs();
} catch(e) {}
}
async function delTodo(taskId) {
try {
await apiFetch(API_BASE + '/tasks/' + taskId, {method: 'DELETE'});
await loadTasksFromServer();
notifyOtherTabs();
} catch(e) {}
}
async function assignTodo(taskId) {
var task = serverTasks.find(function(t) { return String(t.task_id) === taskId; });
if (!task) return;
var names = practiceUsers.map(function(u) { return u.display_name; }).join(', ');
var name = prompt('Zuweisen an (' + (names || 'keine bekannt') + '):', task.assignee || '');
if (name === null) return;
try {
await apiFetch(API_BASE + '/tasks/' + taskId + '/update', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({assignee: name.trim()})
});
await loadTasksFromServer();
notifyOtherTabs();
} catch(e) {}
}
/* ===================================================================
SETTINGS (Practice info)
=================================================================== */
function updateSbUsersPracticeHint(pid, pname) {
var h = document.getElementById('sb-users-hint');
if (!h) return;
if (!currentSession || !(pid || '').trim()) {
h.textContent = '';
return;
}
var label = ((pname || '').trim()) || 'Praxis-Chat';
h.textContent = 'Gleicher Datenraum wie in AzA («An Empfang senden»). Namen entsprechen dem gespeicherten Anzeigenamen — Praxis: ' + label + '.';
}
function applyHeaderPracticeSub(d) {
var sub = document.getElementById('header-practice-sub');
if (!sub) return;
var pname = String((d && d.practice_name) || '').trim() || 'Praxis-Chat';
var pid = String((d && d.practice_id) || '').trim();
var shortId = pid ? (pid.length > 14 ? pid.slice(0, 12) + '\u2026' : pid) : '';
if (!currentSession) {
sub.textContent = '';
return;
}
if (!pid) {
sub.textContent = pname + '\u2003·\u2003Praxiszuordnung pr\u00fcfen';
return;
}
sub.textContent = pname + (shortId ? '\u2003·\u2003ID ' + shortId : '') + '\u2003·\u2003mit Praxis-Chat verbunden';
}
async function loadPracticeInfo() {
try {
var r = await apiFetch(API_BASE + '/practice/info');
var d = await r.json();
var el = document.getElementById('practice-info-section');
if (!el) return;
var role = currentSession ? String(currentSession.role || '').toLowerCase() : '';
var isAdmin = role === 'admin';
var canSeeInvite = isAdmin || role === 'empfang';
var pname = String(d.practice_name || '').trim();
var showTitle = pname || 'Praxis-Chat';
var pidVal = String(d.practice_id || '').trim();
var codeVal = String(d.invite_code || '').trim();
lastPracticeInfoForHint = d;
var statusTxt = pidVal
? 'Mit Praxis-Chat verbunden. Konten entsprechen der Hauptsoftware («An Empfang senden», dieselbe Schnittstelle auf dem Server).'
: 'Praxiszuordnung unklar \u2014 bitte neu anmelden oder Ger\u00e4te-Verkn\u00fcpfung in den Einstellungen pr\u00fcfen.';
try {
if (sessionStorage.getItem('aza_practice_mismatch_cleared') === '1') {
statusTxt = 'Praxis wurde an den Einladungscode aus der Hauptinstallation angeglichen. Bitte erneut mit Ihrem Praxis-Benutzer anmelden.';
sessionStorage.removeItem('aza_practice_mismatch_cleared');
}
} catch(eM) {}
var bindSrc = '';
try { bindSrc = sessionStorage.getItem('aza_practice_bind') || ''; } catch(eB0) {}
var bindHuman = bindSrc === 'invite_code'
? '\u00dcber Einladungscode (URL oder Anmeldung) \u2014 ma\u00dfgeblich'
: (bindSrc === 'stored_practice_id'
? 'Gespeicherte Praxis-ID im Browser'
: (bindSrc === 'account'
? 'Benutzerkonto / Server-Session'
: 'Browser-Sitzung'));
var html = '';
html += '<div class="practice-identity-card">';
html += '<div class="pi-main">' + esc(showTitle) + '</div>';
html += '<div class="pi-row"><span class="pi-k">Practice-ID</span> <span class="pi-v pi-mono">' + esc(pidVal || '\u2014') + '</span></div>';
html += '<div class="pi-row"><span class="pi-k">Quelle</span> <span class="pi-v">' + esc(bindHuman) + '</span></div>';
if (codeVal && canSeeInvite && currentSession) {
html += '<div class="pi-row"><span class="pi-k">Einladung</span>';
html += '<span class="pi-code" id="invite-code-display">' + esc(codeVal) + '</span>';
html += '<button type="button" class="btn-test-small" onclick="copyInviteCode()">Kopieren</button>';
if (isAdmin) {
html += '<button type="button" class="btn-test-small" onclick="regenerateInviteCode()">Neuen Code</button>';
}
html += '</div>';
html += '<p class="pi-hint">F\u00fcr neue Mitarbeiter: Code weitergeben oder Link mit diesem Code verschicken.</p>';
}
html += '<p class="pi-status">' + esc(statusTxt) + '</p>';
html += '</div>';
el.innerHTML = html;
applyHeaderPracticeSub(d);
updateSbUsersPracticeHint(pidVal, pname);
var adminLink = document.getElementById('admin-panel-link');
if (adminLink) {
adminLink.style.display = isAdmin ? 'block' : 'none';
}
refreshSettingsUserSection();
} catch(e) {}
}
function copyInviteCode() {
var el = document.getElementById('invite-code-display');
if (el) navigator.clipboard.writeText(el.textContent.trim()).catch(function(){});
}
async function regenerateInviteCode() {
if (!confirm('Neuen Einladungscode erzeugen?\nDer alte Code wird ungueltig.')) return;
try {
var r = await apiFetch(API_BASE + '/auth/regenerate_invite', {method:'POST'});
var d = await r.json();
if (d.success) loadPracticeInfo();
} catch(e) {}
}
/* ===================================================================
ADMIN PANEL
=================================================================== */
function openAdmin() {
document.getElementById('admin-overlay').classList.add('open');
showAdminTab('praxis');
}
function closeAdmin() {
document.getElementById('admin-overlay').classList.remove('open');
}
function showAdminTab(tab) {
var tabs = document.querySelectorAll('.admin-tab');
var tabNames = ['praxis','users','devices','channels','federation'];
tabs.forEach(function(t, i) { t.classList.toggle('active', tabNames[i] === tab); });
if (tab === 'praxis') loadAdminPraxis();
else if (tab === 'users') loadAdminUsers();
else if (tab === 'devices') loadAdminDevices();
else if (tab === 'channels') loadAdminChannels();
else if (tab === 'federation') loadAdminFederation();
}
async function loadAdminPraxis() {
var body = document.getElementById('admin-body');
try {
var r = await apiFetch(API_BASE + '/practice/info');
var d = await r.json();
var html = '<div class="admin-section-title">Praxis-Informationen</div>';
html += '<div class="admin-info-row"><span class="admin-info-label">Praxisname:</span><span class="admin-info-value">' + esc(d.practice_name || '-') + '</span></div>';
html += '<div class="admin-info-row"><span class="admin-info-label">Benutzer:</span><span class="admin-info-value">' + d.user_count + '</span></div>';
html += '<div class="admin-info-row"><span class="admin-info-label">Nachrichten:</span><span class="admin-info-value">' + d.message_count + ' (' + d.open_count + ' offen)</span></div>';
if (d.admin_email) {
html += '<div class="admin-info-row"><span class="admin-info-label">Admin-E-Mail:</span><span class="admin-info-value">' + esc(d.admin_email) + '</span></div>';
}
if (d.invite_code) {
html += '<div class="admin-section-title" style="margin-top:20px">Einladungslink</div>';
html += '<div class="admin-info-row"><span class="admin-info-label">Code:</span><span class="admin-mono">' + esc(d.invite_code) + '</span>';
html += ' <button class="admin-btn" onclick="copyInviteCode()">Kopieren</button>';
html += ' <button class="admin-btn" onclick="regenerateInviteCode().then(function(){loadAdminPraxis()})">Neuen Code</button></div>';
var link = window.location.origin + '/empfang/?invite=' + encodeURIComponent(d.invite_code);
html += '<div class="admin-info-row"><span class="admin-info-label">Link:</span><span class="admin-mono" style="font-size:.75rem;word-break:break-all">' + esc(link) + '</span>';
html += ' <button class="admin-btn" onclick="navigator.clipboard.writeText(\'' + link.replace(/'/g, "\\'") + '\')">Link kopieren</button></div>';
html += '<div style="font-size:.78rem;color:#8a9aaa;margin-top:6px">Diesen Link an Mitarbeiter senden, damit sie sich registrieren k\u00f6nnen.</div>';
}
body.innerHTML = html;
} catch(e) { body.innerHTML = '<div class="admin-empty">Fehler beim Laden.</div>'; }
}
async function loadAdminUsers() {
var body = document.getElementById('admin-body');
body.innerHTML = '<div class="admin-empty">Laden...</div>';
try {
var r = await apiFetch(API_BASE + '/admin/users');
var d = await r.json();
var users = d.users || [];
var html = '<div class="admin-section-title">Benutzer (' + users.length + ')</div>';
if (!users.length) { html += '<div class="admin-empty">Keine Benutzer</div>'; body.innerHTML = html; return; }
html += '<table class="admin-table"><thead><tr><th>Name</th><th>Rolle</th><th>Status</th><th>Letzter Login</th><th>Aktionen</th></tr></thead><tbody>';
users.forEach(function(u) {
var roleClass = u.role || 'mpa';
var statusClass = u.status === 'deactivated' ? 'deactivated' : 'active';
html += '<tr>';
html += '<td><strong>' + esc(u.display_name) + '</strong>' + (u.email ? '<br><span style="font-size:.72rem;color:#8a9aaa">' + esc(u.email) + '</span>' : '') + '</td>';
html += '<td><span class="admin-badge ' + roleClass + '">' + esc(u.role) + '</span></td>';
html += '<td><span class="admin-badge ' + statusClass + '">' + (u.status === 'deactivated' ? 'Deaktiviert' : 'Aktiv') + '</span></td>';
html += '<td style="font-size:.75rem;color:#8a9aaa">' + esc(u.last_login || '-') + '</td>';
html += '<td style="white-space:nowrap">';
if (u.user_id !== currentSession.user_id) {
html += '<select onchange="changeUserRole(\'' + u.user_id + '\',this.value)" style="font-size:.75rem;padding:2px 4px;border:1px solid #ddd;border-radius:3px;margin-right:4px">';
['arzt','mpa','empfang'].forEach(function(ro) {
html += '<option value="' + ro + '"' + (u.role === ro ? ' selected' : '') + '>' + ro + '</option>';
});
html += '</select>';
if (u.status === 'deactivated') {
html += '<button class="admin-btn success" onclick="activateUser(\'' + u.user_id + '\')">Aktivieren</button> ';
} else {
html += '<button class="admin-btn" onclick="deactivateUser(\'' + u.user_id + '\')">Deaktivieren</button> ';
}
html += '<button class="admin-btn" onclick="resetPassword(\'' + u.user_id + '\',\'' + esc(u.display_name).replace(/'/g,"\\'") + '\')">PW Reset</button> ';
html += '<button class="admin-btn danger" onclick="deleteUser(\'' + u.user_id + '\',\'' + esc(u.display_name).replace(/'/g,"\\'") + '\')">L\u00f6schen</button>';
} else {
html += '<span style="font-size:.75rem;color:#8a9aaa">(Sie)</span>';
}
html += '</td></tr>';
});
html += '</tbody></table>';
body.innerHTML = html;
} catch(e) { body.innerHTML = '<div class="admin-empty">Fehler beim Laden.</div>'; }
}
async function changeUserRole(userId, newRole) {
try {
await apiFetch(API_BASE + '/admin/users/' + userId + '/role', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({role: newRole})
});
loadAdminUsers();
} catch(e) {}
}
async function deactivateUser(userId) {
if (!confirm('Benutzer wirklich deaktivieren?')) return;
try {
await apiFetch(API_BASE + '/admin/users/' + userId + '/deactivate', {method:'POST'});
loadAdminUsers();
} catch(e) {}
}
async function activateUser(userId) {
try {
await apiFetch(API_BASE + '/admin/users/' + userId + '/activate', {method:'POST'});
loadAdminUsers();
} catch(e) {}
}
async function resetPassword(userId, userName) {
if (!confirm('Passwort von "' + userName + '" zur\u00fccksetzen?')) return;
try {
var r = await apiFetch(API_BASE + '/admin/users/' + userId + '/reset_password', {method:'POST'});
var d = await r.json();
if (d.temp_password) {
alert('Neues tempor\u00e4res Passwort fuer ' + userName + ':\n\n' + d.temp_password + '\n\nBitte weitergeben. Muss beim n\u00e4chsten Login ge\u00e4ndert werden.');
}
loadAdminUsers();
} catch(e) {}
}
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 pulseTick() {
try {
var pulseUrl = API_BASE + '/pulse';
var ppid = currentSession && String(currentSession.practice_id || '').trim();
if (ppid) pulseUrl += '?practice_id=' + encodeURIComponent(ppid);
var r = await apiFetch(pulseUrl);
if (!r || !r.ok) return;
var d = await r.json();
var t = parseInt(d.tick || 0, 10);
if (t !== lastPulseTick) {
lastPulseTick = t;
loadMessages();
}
} catch(e) {}
}
function startPolling() {
stopPolling();
// Live-Pulse: <1 s Reaktion auf neue Nachrichten.
pollTimer = setInterval(pulseTick, 1000);
// Sicherheits-Refresh + Aufgaben/Benutzer.
fallbackTimer = setInterval(function() {
loadMessages();
loadTasksFromServer();
}, 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; }
}
document.addEventListener('visibilitychange', function() {
if (!document.hidden && currentSession) {
lastDataHash = '';
loadMessages();
loadUsersFromServer();
loadTasksFromServer();
}
});
var bc = null;
try {
bc = new BroadcastChannel('aza_empfang_sync');
bc.onmessage = function(ev) {
if (ev.data === 'reload' && currentSession) {
lastDataHash = '';
loadMessages();
loadTasksFromServer();
}
};
} catch(e) {}
function notifyOtherTabs() {
try { if (bc) bc.postMessage('reload'); } catch(e) {}
}
try {
window.addEventListener('pywebviewready', function() { initShellPinUi(); });
} catch (e) {}
</script>
</body>
</html>