Files
aza/AzA march 2026 - Kopie (23)/web/empfang.html
2026-05-08 14:53:53 +02:00

3531 lines
158 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:center;justify-content:space-between;box-shadow:var(--aza-shadow-md);flex-shrink:0;gap:8px;z-index:50}
header h1{font-size:1.2rem;font-weight:600;letter-spacing:.3px;white-space:nowrap}
.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{
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}
.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: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}
.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}
.conv-msg-meta span:last-child{font-weight:500;color:#94aaba}
.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}
.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>
<h1>AzA-Empfang</h1>
<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-Information</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-inbox" onclick="setView('allgemein')" title="Posteingang">&#128172;</button>
<button type="button" class="nr-btn" id="nr-archiv" onclick="setView('archiv')" title="Archiv">&#128193;</button>
<button type="button" class="nr-btn" id="nr-tasks" onclick="toggleTasksPanel()" title="Aufgaben ein-/ausblenden">&#9745;</button>
<div class="nr-spacer"></div>
<button type="button" class="nr-btn" onclick="toggleSettings()" title="Einstellungen">&#9881;</button>
</nav>
<!-- Linksspalte: Kontext, Kanäle, Benutzer -->
<aside id="sidebar">
<div class="sb-me" id="sb-me">
<div class="sb-me-dot"></div>
<div style="flex:1;overflow:hidden">
<div class="sb-me-name" id="sb-me-name">Nicht angemeldet</div>
<div id="sb-me-role" style="font-size:.68rem;color:#8a9aaa"></div>
</div>
<button class="sb-me-change" onclick="doLogout()" title="Abmelden">&#x2192;</button>
</div>
<div id="sidebar-scroll">
<div class="nav-card-row" role="group" aria-label="Kanal">
<button type="button" class="nav-card active" data-view="allgemein" onclick="setView('allgemein')" title="Allgemeiner Posteingang">
<span class="nav-card-title">Allgemein</span>
<span class="nav-card-meta"><span id="sb-badge-open"></span> offen</span>
</button>
<button type="button" class="nav-card" data-view="archiv" onclick="setView('archiv')" title="Erledigte Nachrichten">
<span class="nav-card-title">Archiv</span>
<span class="nav-card-meta">Erledigt</span>
</button>
</div>
<div class="sb-add" onclick="openNewChatDialog()" style="margin:0 14px 10px">
<span>+</span> Neuer Chat (Fenster)
</div>
<div class="sb-section">Benutzer</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 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">
<div class="tp-header">
<div>
<span class="tp-sub">Aufgaben</span>
<span class="tp-hint">Aus Nachrichten oder hier anlegen.</span>
</div>
<button type="button" class="tp-add-btn" onclick="toggleTodoInput()" title="Neue Aufgabe">+</button>
</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.06.3</span>
</div>
<script>
/* ===================================================================
STATE
=================================================================== */
var API_BASE = window.location.origin + '/empfang';
function getPracticeIdOrEmpty() {
try { return localStorage.getItem('aza_practice_id') || ''; } catch (e) { return ''; }
}
var currentSession = null;
var practiceUsers = [];
var serverTasks = [];
var allMessages = [];
var allThreads = [];
var activeView = 'allgemein';
var activeDmPeer = null;
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 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 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'});
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;
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; }
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()
})
});
if (r.ok) {
try {
var ld = await r.json();
if (ld.practice_id) localStorage.setItem('aza_practice_id', ld.practice_id);
var saveUser = (ld.display_name || name).trim();
localStorage.setItem('aza_last_login_user', saveUser);
} 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);
} 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 = [];
serverTasks = [];
allMessages = [];
allThreads = [];
activeDmPeer = null;
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 = '';
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();
restoreTasksPanelState();
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 me = await checkAuth();
if (me && me.practice_id) {
try { localStorage.setItem('aza_practice_id', me.practice_id); } catch(e) {}
}
if (!me) {
var inviteParam = urlParams.get('invite');
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;
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 msgs = t.all || [];
for (var i = 0; i < msgs.length; i++) {
var m = msgs[i];
var ex = m.extras || {};
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 (!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 (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;
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('empfang_tasks_collapsed', p.classList.contains('collapsed') ? '1' : '0');
} catch (e) {}
updateSidebarNavHighlight();
}
function restoreTasksPanelState() {
try {
var c = localStorage.getItem('empfang_tasks_collapsed');
var p = document.getElementById('tasks-panel');
if (p && c === '1') p.classList.add('collapsed');
} catch (e) {}
}
function setView(view) {
activeView = view;
activeThreadId = null;
activeDmPeer = null;
dmWantsThreadList = false;
updateSidebarNavHighlight();
lastDataHash = '';
loadMessages();
}
function selectDmPeer(displayName) {
if (!currentSession) {
showLoginOverlay();
return;
}
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) {
activeDmPeer = null;
activeThreadId = null;
dmWantsThreadList = false;
updateSidebarNavHighlight();
lastDataHash = '';
loadMessages();
return;
}
activeDmPeer = peer;
activeThreadId = null;
dmWantsThreadList = false;
updateSidebarNavHighlight();
lastDataHash = '';
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;
selectDmPeer(row.getAttribute('data-peer'));
});
}
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;
}
el.innerHTML = practiceUsers.map(function(u) {
var name = u.display_name || '';
var isMe = currentSession && name === currentSession.display_name;
var dotCls = userPresenceDotClass(u, isMe);
return '<div class="sb-user' + (isMe ? ' me sb-user-self' : '') + '" data-peer="' + encodeURIComponent(name) + '" title="' +
(isMe ? 'Zur\u00fcck zur Hauptliste Allgemein' : ('Direktchat mit ' + esc(name).replace(/"/g, '&quot;'))) + '">' +
'<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>' : '') +
'</div>';
}).join('');
updateSidebarNavHighlight();
}
function updateSbMe() {
var nameEl = document.getElementById('sb-me-name');
var roleEl = document.getElementById('sb-me-role');
if (currentSession) {
nameEl.textContent = currentSession.display_name;
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();
} catch(e) {}
}
/* ===================================================================
MESSAGES
=================================================================== */
async function loadMessages() {
try {
if (!currentSession) return;
var audienceParam = activeDmPeer ? String(activeDmPeer).trim() : '';
var meParam = (currentSession.display_name || '').trim();
var qs = 'audience=' + encodeURIComponent(audienceParam);
if (meParam) qs += '&me=' + encodeURIComponent(meParam);
var r = await apiFetch(API_BASE + '/conversation?' + qs);
var d = await r.json();
var msgs = d.messages || [];
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';
document.getElementById('sb-badge-open').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);
});
}
/* ===================================================================
VIEW RENDERING
=================================================================== */
function renderView() {
maybeAutoPickSingleDmThread();
renderThreadList();
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) {
activeDmPeer = null;
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 + ')' : '');
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 peer = (document.getElementById('nc-peer-select').value || '').trim();
if (mode === 'dm' && !peer) {
alert('Bitte einen Benutzer ausw\u00e4hlen.');
return;
}
closeNewChatDialog();
var q = mode === 'general' ? 'mode=general' : ('mode=dm&peer=' + encodeURIComponent(peer));
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;
var ta = document.getElementById('reply-text');
if (!ta) return;
var text = ta.value.trim();
if (!text && !pendingReplyAttachments.length) return;
stopDictation();
try {
if (activeThreadId) {
var t = allThreads.find(function(th){ return th.tid === activeThreadId; });
var absenderName = currentSession.display_name + ' (Empfang)';
var replyToAbsender = t ? (t.root.absender || '') : '';
var extras = { reply_to: activeThreadId, reply_to_absender: replyToAbsender };
if (pendingReplyAttachments.length) {
extras.attachments = pendingReplyAttachments.slice();
pendingReplyAttachments = [];
updateReplyAttHint();
}
var payload = {
medikamente: '', therapieplan: '', procedere: '',
kommentar: text || (extras.attachments && extras.attachments.length ? '\u200b' : ''),
patient: '',
absender: absenderName,
zeitstempel: new Date().toISOString().slice(0,19).replace('T',' '),
extras: extras
};
var r = await apiFetch(API_BASE + '/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (r.ok) {
ta.value = '';
ta.style.height = '38px';
justSentReply = true;
lastDataHash = '';
loadMessages();
notifyOtherTabs();
}
return;
}
/* Erste Nachricht Direktchat: Thread wird vom Server vergeben */
if (!activeDmPeer) return;
var absenderDM = currentSession.display_name + ' (Empfang)';
var dmExtras = { recipient: String(activeDmPeer).trim() };
if (pendingReplyAttachments.length) {
dmExtras.attachments = pendingReplyAttachments.slice();
pendingReplyAttachments = [];
updateReplyAttHint();
}
var patientLine = 'Direkt: ' + String(activeDmPeer).trim();
var dmPayload = {
medikamente: '', therapieplan: '', procedere: '',
kommentar: text || (dmExtras.attachments && dmExtras.attachments.length ? '\u200b' : ''),
patient: patientLine,
absender: absenderDM,
zeitstempel: new Date().toISOString().slice(0,19).replace('T',' '),
extras: dmExtras
};
var r2 = await apiFetch(API_BASE + '/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dmPayload)
});
if (r2.ok) {
var dj = await r2.json().catch(function() { return {}; });
ta.value = '';
ta.style.height = '38px';
justSentReply = true;
if (dj.thread_id) activeThreadId = dj.thread_id;
lastDataHash = '';
loadMessages();
notifyOtherTabs();
}
} 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('empfang_tasks_collapsed', '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)
=================================================================== */
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 html = '<div class="setting-row"><label>Praxis:</label> <span>' + esc(d.practice_name || '-') + '</span></div>';
if (currentSession && currentSession.role === 'admin' && d.invite_code) {
html += '<div class="setting-row"><label>Einladungscode:</label> <span style="font-family:monospace;background:#f0f4f8;padding:2px 8px;border-radius:4px;user-select:all" id="invite-code-display">' + esc(d.invite_code) + '</span>' +
' <button class="btn-test-small" onclick="copyInviteCode()">Kopieren</button>' +
' <button class="btn-test-small" onclick="regenerateInviteCode()">Neuen Code</button></div>';
html += '<div class="setting-row" style="font-size:.78rem;color:#8a9aaa">Diesen Code an Mitarbeiter weitergeben, damit sie sich registrieren k\u00f6nnen.</div>';
}
el.innerHTML = html;
var adminLink = document.getElementById('admin-panel-link');
if (adminLink) {
adminLink.style.display = (currentSession && currentSession.role === 'admin') ? '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 r = await apiFetch(API_BASE + '/pulse');
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>