Files
aza/AzA march 2026/web/empfang.html

1161 lines
52 KiB
HTML
Raw Normal View History

2026-04-16 15:23:14 +02:00
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
2026-04-19 20:41:37 +02:00
<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='14' font-weight='bold' fill='white' text-anchor='middle'>AZA</text></svg>">
<title>AZA &ndash; Empfang</title>
2026-04-16 15:23:14 +02:00
<style>
*{box-sizing:border-box;margin:0;padding:0}
2026-04-19 20:41:37 +02:00
html,body{height:100%;overflow:hidden}
body{font-family:'Segoe UI',system-ui,sans-serif;background:#f0f4f8;color:#1a2a3a;font-size:9pt;display:flex;flex-direction:column}
/* === Header === */
header{background:linear-gradient(135deg,#5B8DB3,#3a6d93);color:#fff;padding:10px 20px;display:flex;align-items:center;justify-content:space-between;box-shadow:0 2px 8px rgba(0,0,0,.12);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.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}
/* === 3-Panel Layout === */
#app-layout{display:flex;flex:1;min-height:0;overflow:hidden}
/* === Sidebar (left) === */
#sidebar{width:210px;min-width:170px;background:#fff;border-right:1px solid #e0e4e8;display:flex;flex-direction:column;flex-shrink:0;overflow:hidden}
#sidebar-scroll{flex:1;overflow-y:auto;overflow-x:hidden}
.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-item{padding:7px 14px;font-size:.85rem;color:#1a3a5a;cursor:pointer;border-left:3px solid transparent;transition:all .12s;display:flex;align-items:center;gap:8px}
.sb-item:hover{background:#f4f8fc}
.sb-item.active{background:#e8f0f8;border-left-color:#5B8DB3;font-weight:600;color:#1a4d6d}
.sb-icon{font-size:.9rem;width:18px;text-align:center;flex-shrink:0}
.sb-badge{margin-left:auto;background:#5B8DB3;color:#fff;border-radius:10px;padding:1px 7px;font-size:.68rem;font-weight:600;min-width:18px;text-align:center}
.sb-badge:empty{display:none}
.sb-user{padding:5px 14px 5px 18px;font-size:.82rem;color:#3a5a7a;cursor:pointer;display:flex;align-items:center;gap:7px;transition:background .12s;border-left:3px solid transparent}
.sb-user:hover{background:#f4f8fc}
.sb-user.me{font-weight:600;color:#1a3a5a;background:#f0f7fe;border-left-color:#5B8DB3}
.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:.65rem;color:#8a9aaa;background:#f0f4f8;padding:1px 5px;border-radius:6px}
.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}
/* === Chat Area (middle) === */
#chat-area{flex:1;display:flex;flex-direction:column;min-width:0;background:#f0f4f8}
#chat-top-bar{padding:8px 16px;background:#fff;border-bottom:1px solid #e0e4e8;display:flex;align-items:center;gap:8px;flex-shrink:0;min-height:40px}
.ctb-back{background:none;border:none;color:#5B8DB3;cursor:pointer;font-size:.9rem;padding:4px 6px;border-radius:4px;display:none}
.ctb-back:hover{background:#e8f0f8}
.ctb-back.visible{display:inline-flex}
.ctb-title{font-weight:600;font-size:.95rem;color:#1a3a5a;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ctb-actions{display:flex;gap:4px}
.ctb-btn{background:#e8f0f8;color:#2a5a8a;border:1px solid #d0dce8;border-radius:5px;padding:3px 10px;font-size:.76rem;cursor:pointer;font-family:inherit;transition:all .12s;white-space:nowrap}
.ctb-btn:hover{background:#d4e4f0}
.ctb-btn.done{background:#d4edda;color:#155724;border-color:#c3e6cb}
.ctb-btn.done:hover{background:#c3e6cb}
.ctb-btn.del{color:#8a2a2a;background:#f8e8e8;border-color:#e8c8c8}
.ctb-btn.del:hover{background:#f0d4d4}
#chat-content{flex:1;overflow-y:auto;padding:12px 16px}
/* Thread list items */
.tl-item{background:#fff;border-radius:8px;padding:10px 14px;margin-bottom:8px;cursor:pointer;border-left:4px solid #5B8DB3;box-shadow:0 1px 3px rgba(0,0,0,.05);transition:all .12s}
.tl-item:hover{box-shadow:0 2px 8px rgba(0,0,0,.1);transform:translateY(-1px)}
.tl-item.done{border-left-color:#8bc49a;opacity:.7}
.tl-item.done:hover{opacity:.85}
.tl-row1{display:flex;align-items:baseline;gap:6px;margin-bottom:3px}
.tl-patient{font-weight:600;font-size:.92rem;color:#1a3a5a;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tl-time{font-size:.72rem;color:#8a9aaa;white-space:nowrap}
.tl-row2{display:flex;align-items:center;gap:6px}
.tl-sender{font-size:.78rem;color:#6a8a9a;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tl-replies{font-size:.7rem;color:#5B8DB3;white-space:nowrap}
.tl-status{font-size:.68rem;padding:2px 8px;border-radius:10px;font-weight:600;white-space:nowrap}
.tl-status.offen{background:#fff3cd;color:#856404}
.tl-status.erledigt{background:#d4edda;color:#155724}
/* Conversation view */
.conv-empty{text-align:center;padding:60px 20px;color:#8a9aaa;font-size:.95rem}
.conv-doc{background:#fff;border-radius:10px;padding:14px 18px;margin-bottom:14px;box-shadow:0 1px 4px rgba(0,0,0,.06);border-left:4px solid #5B8DB3}
.conv-doc-head{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.conv-doc-sender{font-weight:600;font-size:.88rem;color:#1a3a5a}
.conv-doc-time{font-size:.72rem;color:#8a9aaa}
.conv-field{margin-bottom:6px}
.conv-field-label{font-size:.72rem;font-weight:700;color:#5B8DB3;text-transform:uppercase;letter-spacing:.4px;margin-bottom:2px;cursor:pointer;user-select:none}
.conv-field-label .tri{font-size:.6rem;margin-right:3px;display:inline-block;transition:transform .15s}
.conv-field-label .tri.closed{transform:rotate(-90deg)}
.conv-field-value{white-space:pre-wrap;line-height:1.5;font-size:.88rem}
.conv-field-value.collapsed{display:none}
.conv-attach{margin-top:6px}
.conv-attach img{max-width:100%;max-height:260px;border-radius:6px;border:1px solid #e0e4e8}
.conv-msg{padding:10px 14px;margin-bottom:6px;border-radius:10px;max-width:78%;font-size:.88rem;position:relative}
.conv-msg.from-other{background:#fff;border:1px solid #e8ecf0;margin-right:auto;border-bottom-left-radius:3px}
.conv-msg.from-me{background:#e8f0fa;border:1px solid #d0dce8;margin-left:auto;border-bottom-right-radius:3px}
.conv-msg-meta{font-size:.7rem;color:#8a9aaa;margin-bottom:3px;display:flex;gap:6px}
.conv-msg-sender{font-weight:600;color:#5a7a9a}
.conv-msg-text{white-space:pre-wrap;line-height:1.45}
/* Input bar */
#chat-input-bar{padding:10px 16px;background:#fff;border-top:1px solid #e0e4e8;flex-shrink:0;display:none;gap:8px;align-items:flex-end}
#chat-input-bar.visible{display:flex}
#chat-input-bar textarea{flex:1;border:1px solid #d0dce8;border-radius:8px;padding:8px 12px;font-family:inherit;font-size:.9rem;resize:none;min-height:38px;max-height:120px;line-height:1.4;outline:none;transition:border-color .15s}
#chat-input-bar textarea:focus{border-color:#5B8DB3}
.input-btns{display:flex;gap:4px;flex-shrink:0}
.btn-send{background:#5B8DB3;color:#fff;border:none;border-radius:8px;padding:8px 16px;cursor:pointer;font-weight:600;font-size:.85rem;font-family:inherit;transition:background .15s;white-space:nowrap}
.btn-send:hover{background:#4A7A9E}
.btn-dictate{background:#f0e8ff;color:#6b3fa0;border:none;border-radius:8px;padding:8px 12px;cursor:pointer;font-size:.85rem;font-family:inherit;white-space:nowrap}
.btn-dictate:hover{background:#e0d0f8}
.btn-dictate.recording{background:#f8d7da;color:#721c24;animation:pulse 1.5s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.6}}
/* === Tasks Panel (right) === */
#tasks-panel{width:240px;min-width:170px;background:#fff;border-left:1px solid #e0e4e8;display:flex;flex-direction:column;flex-shrink:0;overflow:hidden}
.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:6px}
.todo-item{display:flex;align-items:flex-start;gap:4px;padding:4px 6px;border-radius:5px;margin-bottom:2px;background:#f8fafe;font-size:.8rem;transition:background .1s}
.todo-item:hover{background:#eef4fa}
.todo-item.todo-done{background:#f0f8f0}
.todo-item input[type=checkbox]{margin-top:3px;accent-color:#5B8DB3;cursor:pointer}
.todo-item .todo-text{flex:1;word-break:break-word;outline:none;min-height:1.2em}
.todo-item .todo-text.done{text-decoration:line-through;opacity:.5}
.todo-item .todo-assignee{font-size:.68rem;color:#8a9aaa;margin-top:1px}
.todo-item .todo-del{background:none;border:none;color:#ccc;cursor:pointer;font-size:.78rem;padding:2px;line-height:1}
.todo-item .todo-del:hover{color:#e05050}
.todo-item .todo-assign-btn{background:none;border:none;color:#aac;cursor:pointer;font-size:.7rem;padding:1px 3px}
.todo-item .todo-assign-btn:hover{color:#5B8DB3}
/* === 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}}
@media print{
header,.settings-panel,#sidebar,#tasks-panel,.status-bar,#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}
}
2026-04-16 15:23:14 +02:00
</style>
</head>
<body>
<header>
2026-04-19 20:41:37 +02:00
<h1>AZA &ndash; Empfang</h1>
2026-04-16 15:23:14 +02:00
<div class="header-right">
2026-04-19 20:41:37 +02:00
<div class="font-ctrl" title="Schriftgr&ouml;sse">
<span>Aa</span>
<span id="font-size-display">9</span>
<button onclick="changeFontSize(1)">&#9650;</button>
<button onclick="changeFontSize(-1)">&#9660;</button>
</div>
<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 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>
2026-04-16 15:23:14 +02:00
</div>
</header>
2026-04-19 20:41:37 +02:00
<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-Wiederholung:</label>
<select id="repeat-select" onchange="onRepeatChange()">
<option value="0">Aus</option>
<option value="30000">Standard (30 Sek.)</option>
<option value="5000">Alle 5 Sekunden</option>
<option value="2000">Alle 2 Sekunden</option>
<option value="custom">Eigene Eingabe</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">Benutzerverwaltung</h3>
<div id="user-mgmt-list" style="margin-bottom:6px"></div>
<div class="setting-row">
<input type="text" id="new-user-input" placeholder="Neuer Name..." style="border:1px solid #ccc;border-radius:4px;padding:3px 7px;font-size:.82rem;width:110px">
<select id="new-user-group" style="border:1px solid #ccc;border-radius:4px;padding:3px 5px;font-size:.8rem">
<option value="">Ohne Gruppe</option>
<option value="Empfang">Empfang</option>
<option value="Aerzte">&Auml;rzte</option>
<option value="Intern">Intern</option>
</select>
<button class="btn-test-small" onclick="addUserFromSettings()">Hinzuf&uuml;gen</button>
</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>
2026-04-16 15:23:14 +02:00
</div>
2026-04-19 20:41:37 +02:00
<div id="app-layout">
<!-- === SIDEBAR (left) === -->
<aside id="sidebar">
<div class="sb-me" id="sb-me">
<div class="sb-me-dot"></div>
<div class="sb-me-name" id="sb-me-name">Kein Benutzer</div>
<button class="sb-me-change" onclick="cycleUser()" title="Benutzer wechseln">&#8644;</button>
</div>
<div id="sidebar-scroll">
<div class="sb-section">Kan&auml;le</div>
<div class="sb-item active" data-view="allgemein" onclick="setView('allgemein')">
<span class="sb-icon">&#128172;</span> Allgemein
<span class="sb-badge" id="sb-badge-open"></span>
</div>
<div class="sb-item" data-view="archiv" onclick="setView('archiv')">
<span class="sb-icon">&#128193;</span> Archiv
</div>
<div class="sb-section">Benutzer</div>
<div id="sb-users"></div>
<div class="sb-add" onclick="toggleSbAddUser()">
<span>+</span> Neuer Benutzer
</div>
<div class="sb-add-input" id="sb-add-input">
<input type="text" id="sb-new-user" placeholder="Name eingeben..." onkeydown="if(event.key==='Enter'){addUserFromSidebar();event.preventDefault()}">
</div>
</div>
</aside>
<!-- === CHAT AREA (middle) === -->
<main id="chat-area">
<div id="chat-top-bar">
<button class="ctb-back" id="ctb-back" onclick="backToList()">&#9664; Zur&uuml;ck</button>
<div class="ctb-title" id="ctb-title">Allgemein</div>
<div class="ctb-actions" id="ctb-actions"></div>
</div>
<div id="chat-content">
<div class="conv-empty">Nachrichten werden geladen&hellip;</div>
</div>
<div id="chat-input-bar">
<textarea id="reply-text" placeholder="Nachricht schreiben..." rows="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){sendReply();event.preventDefault()}" oninput="autoGrowTextarea(this)"></textarea>
<div class="input-btns">
<button class="btn-dictate" id="dictate-btn" onclick="toggleDictation()">&#127908;</button>
<button class="btn-send" onclick="sendReply()">Senden</button>
</div>
</div>
</main>
<!-- === TASKS PANEL (right) === -->
<aside id="tasks-panel">
<div class="tp-header">
<span>Aufgaben</span>
<button class="tp-add-btn" onclick="toggleTodoInput()" title="Neue Aufgabe">+</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>
<div class="status-bar">
<span>Aktualisiert alle 10 Sek.</span>
<span style="opacity:.5" id="ui-version">v2026.04.18</span>
</div>
2026-04-16 15:23:14 +02:00
<script>
2026-04-19 20:41:37 +02:00
/* ===================================================================
STATE
=================================================================== */
var API_BASE = window.location.origin + '/empfang';
var allMessages = [];
var allThreads = [];
var activeView = 'allgemein';
var activeThreadId = null;
var lastDataHash = '';
var previousThreadIds = new Set();
var justSentReply = false;
2026-04-16 15:23:14 +02:00
2026-04-19 20:41:37 +02:00
var soundEnabled = localStorage.getItem('empfang_sound') !== 'off';
var audioCtx = null;
var currentFontSize = parseInt(localStorage.getItem('empfang_font_size') || '9', 10);
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 currentUser = localStorage.getItem('empfang_current_user') || '';
var knownUsers = JSON.parse(localStorage.getItem('empfang_known_users') || '[]');
var userGroups = JSON.parse(localStorage.getItem('empfang_user_groups') || '{}');
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);
/* ===================================================================
INIT
=================================================================== */
(function init() {
var sel = document.getElementById('tone-select');
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);
});
document.getElementById('vol-slider').value = Math.round(volume * 100);
document.getElementById('vol-display').textContent = Math.round(volume * 100) + '%';
initRepeatUI();
updateSortBtn();
updateSoundBtn();
applyFontSize();
renderSidebarUsers();
updateSbMe();
renderUserMgmt();
loadUserData();
})();
/* ===================================================================
SIDEBAR
=================================================================== */
function setView(view) {
activeView = view;
activeThreadId = null;
document.querySelectorAll('.sb-item').forEach(function(el) {
el.classList.toggle('active', el.getAttribute('data-view') === view);
});
renderView();
}
function renderSidebarUsers() {
var el = document.getElementById('sb-users');
if (!knownUsers.length) {
el.innerHTML = '<div style="padding:4px 18px;font-size:.78rem;color:#bbb">Keine Benutzer</div>';
return;
2026-04-16 15:23:14 +02:00
}
2026-04-19 20:41:37 +02:00
el.innerHTML = knownUsers.map(function(u) {
var g = userGroups[u] || '';
var isMe = u === currentUser;
return '<div class="sb-user' + (isMe ? ' me' : '') + '" onclick="switchUserSidebar(\'' + esc(u).replace(/'/g, "\\'") + '\')">' +
'<span class="sb-dot ' + (isMe ? 'online' : 'offline') + '"></span>' +
'<span class="sb-user-name">' + esc(u) + '</span>' +
(g ? '<span class="sb-group-tag">' + esc(g) + '</span>' : '') +
'</div>';
}).join('');
2026-04-16 15:23:14 +02:00
}
2026-04-19 20:41:37 +02:00
function updateSbMe() {
var el = document.getElementById('sb-me-name');
el.textContent = currentUser || 'Kein Benutzer';
2026-04-16 15:23:14 +02:00
}
2026-04-19 20:41:37 +02:00
function switchUserSidebar(name) {
currentUser = name;
localStorage.setItem('empfang_current_user', currentUser);
renderSidebarUsers();
updateSbMe();
loadUserData();
lastDataHash = '';
loadMessages();
}
function cycleUser() {
if (!knownUsers.length) { toggleSbAddUser(); return; }
var idx = knownUsers.indexOf(currentUser);
var next = knownUsers[(idx + 1) % knownUsers.length];
switchUserSidebar(next);
}
function toggleSbAddUser() {
var el = document.getElementById('sb-add-input');
var isOpen = el.classList.toggle('open');
if (isOpen) {
var inp = document.getElementById('sb-new-user');
inp.value = '';
inp.focus();
}
}
function addUserFromSidebar() {
var inp = document.getElementById('sb-new-user');
var name = (inp.value || '').trim();
if (!name) return;
if (!knownUsers.includes(name)) {
knownUsers.push(name);
localStorage.setItem('empfang_known_users', JSON.stringify(knownUsers));
}
fetch(API_BASE + '/users', {method:'POST', headers:{'Content-Type':'application/json'},
body:JSON.stringify({name:name})}).catch(function(){});
inp.value = '';
document.getElementById('sb-add-input').classList.remove('open');
switchUserSidebar(name);
renderUserMgmt();
2026-04-16 15:23:14 +02:00
}
2026-04-19 20:41:37 +02:00
/* ===================================================================
MESSAGES
=================================================================== */
2026-04-16 15:23:14 +02:00
async function loadMessages() {
try {
2026-04-19 20:41:37 +02:00
var r = await fetch(API_BASE + '/messages');
var d = await r.json();
var raw = JSON.stringify(d.messages || []);
if (raw === lastDataHash) return;
lastDataHash = raw;
allMessages = d.messages || [];
allMessages.forEach(function(m) { if (!m.thread_id) m.thread_id = m.id; });
if (currentUser) {
allMessages = allMessages.filter(function(m) {
var rcpt = m.extras && m.extras.recipient;
return !rcpt || rcpt === currentUser || rcpt === 'Alle';
});
}
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();
2026-04-16 15:23:14 +02:00
}
2026-04-19 20:41:37 +02:00
justSentReply = false;
previousThreadIds = currentIds;
var openCount = allThreads.filter(function(t){ return t.root.status === 'offen'; }).length;
document.getElementById('count-badge').textContent = openCount + ' offen';
document.getElementById('sb-badge-open').textContent = openCount || '';
renderView();
2026-04-16 15:23:14 +02:00
} catch (e) {
2026-04-19 20:41:37 +02:00
document.getElementById('chat-content').innerHTML =
'<div class="conv-empty">Backend nicht erreichbar.<br>Bitte Verbindung pr&uuml;fen.</div>';
2026-04-16 15:23:14 +02:00
}
}
2026-04-19 20:41:37 +02:00
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() {
if (activeThreadId) {
renderConversation(activeThreadId);
} else {
renderThreadList();
}
}
function renderThreadList() {
var c = document.getElementById('chat-content');
var backBtn = document.getElementById('ctb-back');
var titleEl = document.getElementById('ctb-title');
var actionsEl = document.getElementById('ctb-actions');
backBtn.classList.remove('visible');
actionsEl.innerHTML = '';
document.getElementById('chat-input-bar').classList.remove('visible');
var filtered;
if (activeView === 'archiv') {
filtered = allThreads.filter(function(t){ return t.root.status === 'erledigt'; });
titleEl.textContent = 'Archiv';
} else {
filtered = allThreads.filter(function(t){ return t.root.status === 'offen'; });
titleEl.textContent = 'Allgemein (' + filtered.length + ' offen)';
}
if (!filtered.length) {
var hint = activeView === 'archiv' ? 'Keine erledigten Nachrichten.' : 'Keine offenen Nachrichten.';
c.innerHTML = '<div class="conv-empty">' + hint + '</div>';
2026-04-16 15:23:14 +02:00
return;
}
2026-04-19 20:41:37 +02:00
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;
return '<div class="tl-item' + (isDone ? ' done' : '') + '" onclick="selectThread(\'' + t.tid + '\')">' +
'<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-sender">' + esc(m.absender || '\u2013') + '</div>' +
(replyCount ? '<span class="tl-replies">' + replyCount + ' Antw.</span>' : '') +
'<span class="tl-status ' + (isDone ? 'erledigt' : 'offen') + '">' + (isDone ? 'Erledigt' : 'Offen') + '</span>' +
'</div>' +
'</div>';
2026-04-16 15:23:14 +02:00
}).join('');
}
2026-04-19 20:41:37 +02:00
function selectThread(tid) {
activeThreadId = tid;
stopRepeatTone();
renderConversation(tid);
2026-04-16 15:23:14 +02:00
}
2026-04-19 20:41:37 +02:00
function backToList() {
activeThreadId = null;
renderView();
}
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('chat-content');
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');
backBtn.classList.add('visible');
titleEl.textContent = m.patient || 'Unterhaltung';
inputBar.classList.add('visible');
actionsEl.innerHTML =
(!isDone ? '<button class="ctb-btn done" onclick="markDone(\'' + m.id + '\')">&#10003; Erledigt</button>' : '') +
'<button class="ctb-btn" onclick="copyThread(\'' + tid + '\')">Kopieren</button>' +
'<button class="ctb-btn" onclick="saveThread(\'' + tid + '\')">Speichern</button>' +
'<button class="ctb-btn" onclick="printThread(\'' + tid + '\')">Drucken</button>' +
'<button class="ctb-btn del" onclick="deleteMsg(\'' + m.id + '\')">L&ouml;schen</button>';
var html = '';
html += '<div class="conv-doc">';
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 fields = [];
if (m.therapieplan) fields.push({l:'Therapieplan', v:m.therapieplan});
if (m.procedere) fields.push({l:'Procedere', v:m.procedere});
if (m.kommentar) fields.push({l:'Kommentar', v:m.kommentar});
if (m.medikamente) fields.push({l:'Medikamente', v:m.medikamente});
if (fields.length) {
fields.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>';
});
} else {
html += '<div style="color:#999;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;
sortedReplies.forEach(function(r) {
var isFromEmpfang = (r.absender || '').includes('Empfang');
html += '<div class="conv-msg ' + (isFromEmpfang ? 'from-me' : 'from-other') + '">' +
'<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>' +
'</div>';
});
}
c.innerHTML = html;
c.scrollTop = sortNewestFirst ? 0 : c.scrollHeight;
}
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
=================================================================== */
async function sendReply() {
if (!activeThreadId) return;
var ta = document.getElementById('reply-text');
var text = ta.value.trim();
if (!text) return;
stopDictation();
var t = allThreads.find(function(th){ return th.tid === activeThreadId; });
var absenderName = currentUser ? currentUser + ' (Empfang)' : 'Empfang (Antwort)';
var replyToAbsender = t ? (t.root.absender || '') : '';
try {
var payload = {
medikamente: '', therapieplan: '', procedere: '',
kommentar: text, patient: '',
absender: absenderName,
zeitstempel: new Date().toISOString().slice(0,19).replace('T',' '),
extras: { reply_to: activeThreadId, reply_to_absender: replyToAbsender }
};
var r = await fetch(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();
}
} 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) {
await fetch(API_BASE + '/messages/' + id + '/done', {method:'POST'});
stopRepeatTone();
lastDataHash = '';
loadMessages();
notifyOtherTabs();
}
async function deleteMsg(id) {
try { await fetch(API_BASE + '/messages/' + id, {method:'DELETE'}); } catch(e) {}
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 = [];
2026-04-16 15:23:14 +02:00
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);
2026-04-19 20:41:37 +02:00
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();
2026-04-16 15:23:14 +02:00
}
2026-04-19 20:41:37 +02:00
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);
2026-04-16 15:23:14 +02:00
}
2026-04-19 20:41:37 +02:00
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 fetch(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) { el.textContent = 'Fehler'; }
}
/* ===================================================================
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() {
document.body.style.fontSize = currentFontSize + 'pt';
document.getElementById('font-size-display').textContent = currentFontSize;
localStorage.setItem('empfang_font_size', 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 = '';
2026-04-16 15:23:14 +02:00
loadMessages();
}
2026-04-19 20:41:37 +02:00
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'); }
}
2026-04-16 15:23:14 +02:00
2026-04-19 20:41:37 +02:00
/* ===================================================================
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'; }
}
/* ===================================================================
USER MANAGEMENT (Settings panel)
=================================================================== */
function addUserFromSettings() {
var inp = document.getElementById('new-user-input');
var grpSel = document.getElementById('new-user-group');
var name = (inp.value || '').trim();
if (!name) return;
var grp = grpSel ? grpSel.value : '';
if (!knownUsers.includes(name)) {
knownUsers.push(name);
localStorage.setItem('empfang_known_users', JSON.stringify(knownUsers));
}
if (grp) { userGroups[name] = grp; localStorage.setItem('empfang_user_groups', JSON.stringify(userGroups)); }
fetch(API_BASE + '/users', {method:'POST', headers:{'Content-Type':'application/json'},
body:JSON.stringify({name:name})}).catch(function(){});
inp.value = '';
renderUserMgmt();
renderSidebarUsers();
}
function renameUser(oldName) {
var newName = prompt('Neuer Name fuer "' + oldName + '":', oldName);
if (!newName || !newName.trim() || newName.trim() === oldName) return;
newName = newName.trim();
var idx = knownUsers.indexOf(oldName);
if (idx >= 0) knownUsers[idx] = newName;
if (userGroups[oldName]) { userGroups[newName] = userGroups[oldName]; delete userGroups[oldName]; }
localStorage.setItem('empfang_known_users', JSON.stringify(knownUsers));
localStorage.setItem('empfang_user_groups', JSON.stringify(userGroups));
if (currentUser === oldName) { currentUser = newName; localStorage.setItem('empfang_current_user', newName); }
fetch(API_BASE + '/users', {method:'POST', headers:{'Content-Type':'application/json'},
body:JSON.stringify({name:oldName, action:'rename', new_name:newName})}).catch(function(){});
renderUserMgmt(); renderSidebarUsers(); updateSbMe();
}
function deleteUser(name) {
if (!confirm('Benutzer "' + name + '" entfernen?')) return;
knownUsers = knownUsers.filter(function(u){ return u !== name; });
delete userGroups[name];
localStorage.setItem('empfang_known_users', JSON.stringify(knownUsers));
localStorage.setItem('empfang_user_groups', JSON.stringify(userGroups));
if (currentUser === name) { currentUser = ''; localStorage.setItem('empfang_current_user', ''); }
fetch(API_BASE + '/users', {method:'POST', headers:{'Content-Type':'application/json'},
body:JSON.stringify({name:name, action:'delete'})}).catch(function(){});
renderUserMgmt(); renderSidebarUsers(); updateSbMe();
}
function renderUserMgmt() {
var el = document.getElementById('user-mgmt-list');
if (!el) return;
if (!knownUsers.length) { el.innerHTML = '<div style="font-size:.8rem;color:#aaa;padding:3px 0">Keine Benutzer</div>'; return; }
el.innerHTML = knownUsers.map(function(u) {
var g = userGroups[u] || '';
var gBadge = g ? '<span style="font-size:.68rem;background:#e8f0f8;color:#3a5a7a;padding:1px 5px;border-radius:6px;margin-left:4px">' + g + '</span>' : '';
return '<div style="display:flex;align-items:center;gap:5px;padding:2px 0;font-size:.82rem">' +
'<span style="flex:1;color:#1a2a3a">' + esc(u) + gBadge + '</span>' +
'<button style="background:none;border:none;color:#5B8DB3;cursor:pointer;font-size:.75rem" onclick="renameUser(\'' + u.replace(/'/g,"\\'") + '\')" title="Umbenennen">&#9998;</button>' +
'<button style="background:none;border:none;color:#ccc;cursor:pointer;font-size:.75rem" onclick="deleteUser(\'' + u.replace(/'/g,"\\'") + '\')" title="Entfernen">&times;</button>' +
'</div>';
}).join('');
}
/* ===================================================================
TODOS
=================================================================== */
function userKey(base) { return currentUser ? base + '_' + currentUser : base; }
var todos = JSON.parse(localStorage.getItem(userKey('empfang_todos')) || '[]');
function loadUserData() {
todos = JSON.parse(localStorage.getItem(userKey('empfang_todos')) || '[]');
renderTodos();
lastDataHash = '';
2026-04-16 15:23:14 +02:00
loadMessages();
}
2026-04-19 20:41:37 +02:00
function saveTodos() { localStorage.setItem(userKey('empfang_todos'), JSON.stringify(todos)); }
function renderTodos() {
var el = document.getElementById('todo-list');
if (!todos.length) { el.innerHTML = '<div style="color:#aaa;font-size:.8rem;padding:12px;text-align:center">Keine Aufgaben</div>'; return; }
el.innerHTML = todos.map(function(t, i) {
var assignee = t.assignee || '';
return '<div class="todo-item ' + (t.done ? 'todo-done' : '') + '">' +
'<input type="checkbox" ' + (t.done ? 'checked' : '') + ' onchange="toggleTodo(' + i + ')">' +
'<div style="flex:1;min-width:0">' +
'<span class="todo-text ' + (t.done ? 'done' : '') + '" contenteditable="true" onblur="editTodo(' + i + ',this)">' + esc(t.text) + '</span>' +
(assignee ? '<div class="todo-assignee">\u2192 ' + esc(assignee) + '</div>' : '') +
'</div>' +
'<button class="todo-assign-btn" onclick="assignTodo(' + i + ')" title="Zuweisen">\u270B</button>' +
'<button class="todo-del" onclick="delTodo(' + i + ')" title="Entfernen">&times;</button>' +
'</div>';
}).join('');
}
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(); }
}
function addTodoInline() {
var inp = document.getElementById('todo-inline-input');
var text = (inp.value || '').trim();
if (!text) return;
todos.push({text:text, done:false, assignee:currentUser||'', created:new Date().toISOString().slice(0,16)});
saveTodos(); renderTodos();
inp.value = ''; inp.focus();
}
function toggleTodo(i) { todos[i].done = !todos[i].done; saveTodos(); renderTodos(); }
function editTodo(i, el) { todos[i].text = (el.textContent || '').trim() || todos[i].text; saveTodos(); }
function delTodo(i) { todos.splice(i, 1); saveTodos(); renderTodos(); }
function assignTodo(i) {
var names = knownUsers.length ? knownUsers.join(', ') : '(keine bekannt)';
var name = prompt('Zuweisen an (bekannte: ' + names + '):', todos[i].assignee || '');
if (name !== null) { todos[i].assignee = name.trim(); saveTodos(); renderTodos(); }
}
/* ===================================================================
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
=================================================================== */
2026-04-16 15:23:14 +02:00
loadMessages();
setInterval(loadMessages, 10000);
2026-04-19 20:41:37 +02:00
document.addEventListener('visibilitychange', function() {
if (!document.hidden) { lastDataHash = ''; loadMessages(); }
});
var bc = null;
try {
bc = new BroadcastChannel('aza_empfang_sync');
bc.onmessage = function(ev) {
if (ev.data === 'reload') { lastDataHash = ''; loadMessages(); }
};
} catch(e) {}
function notifyOtherTabs() {
try { if (bc) bc.postMessage('reload'); } catch(e) {}
}
2026-04-16 15:23:14 +02:00
</script>
</body>
</html>