Files
aza/AzA march 2026/backups_empfang_shell_stop_browser_arc_2026-05-07/empfang.html
2026-05-08 14:53:53 +02:00

4403 lines
196 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<link rel="icon" href="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>