977 lines
44 KiB
HTML
977 lines
44 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>AZA – Empfang</title>
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{font-family:'Segoe UI',system-ui,sans-serif;background:#f0f4f8;color:#1a2a3a;min-height:100vh;font-size:9pt}
|
|
header{background:linear-gradient(135deg,#5B8DB3,#3a6d93);color:#fff;padding:14px 24px;display:flex;align-items:center;justify-content:space-between;box-shadow:0 2px 8px rgba(0,0,0,.12);flex-wrap:wrap;gap:8px}
|
|
header h1{font-size:1.35rem;font-weight:600;letter-spacing:.3px}
|
|
.header-right{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
.badge{background:rgba(255,255,255,.2);border-radius:20px;padding:4px 14px;font-size:.85rem}
|
|
.hbtn{background:none;border:1px solid rgba(255,255,255,.35);color:#fff;border-radius:6px;padding:4px 10px;font-size:.82rem;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)}
|
|
.font-ctrl{display:flex;align-items:center;gap:3px}
|
|
.font-ctrl span{font-size:.78rem;opacity:.8}
|
|
.font-ctrl button{background:none;border:1px solid rgba(255,255,255,.3);color:#fff;border-radius:4px;width:22px;height:22px;cursor:pointer;font-size:.7rem;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:4px}
|
|
.vol-ctrl input[type=range]{width:60px;accent-color:#fff;cursor:pointer}
|
|
.vol-ctrl span{font-size:.75rem;opacity:.85;min-width:28px;text-align:right}
|
|
.settings-panel{display:none;background:#fff;border-bottom:1px solid #e0e4e8;padding:14px 24px}
|
|
.settings-panel.open{display:block}
|
|
.settings-inner h3{font-size:.95rem;color:#1a4d6d;margin-bottom:10px}
|
|
.setting-row{display:flex;align-items:center;gap:10px;margin-bottom:8px;flex-wrap:wrap}
|
|
.setting-row label{font-size:.85rem;color:#3a5a7a}
|
|
.setting-row select,.setting-row input[type=number]{border:1px solid #ccc;border-radius:4px;padding:4px 8px;font-size:.85rem}
|
|
.setting-row input[type=number]{width:70px}
|
|
.btn-test-small{background:#e8f0f8;color:#2a5a8a;border:1px solid #c8d8e8;border-radius:4px;padding:3px 10px;font-size:.8rem;cursor:pointer}
|
|
.btn-test-small:hover{background:#d4e4f0}
|
|
.btn-cleanup{background:#f8e8e8;color:#8a2a2a;border:1px solid #e0c8c8;border-radius:4px;padding:3px 10px;font-size:.8rem;cursor:pointer}
|
|
.btn-cleanup:hover{background:#f0d4d4}
|
|
.main-split{display:flex;max-width:1200px;margin:0 auto;padding:16px;gap:0;min-height:calc(100vh - 120px)}
|
|
.container{flex:1;min-width:300px;padding:0 8px;overflow-y:auto}
|
|
.split-handle{width:6px;cursor:col-resize;background:#dde8f0;border-radius:3px;flex-shrink:0;transition:background .15s}
|
|
.split-handle:hover,.split-handle.dragging{background:#5B8DB3}
|
|
.todo-panel{width:260px;min-width:180px;max-width:400px;background:#fff;border-radius:10px;box-shadow:0 1px 6px rgba(0,0,0,.08);overflow:hidden;display:flex;flex-direction:column;flex-shrink:0}
|
|
.todo-header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:#fafcfe;border-bottom:1px solid #eef2f6;font-weight:600;color:#1a4d6d;font-size:.92rem}
|
|
.todo-add-btn{background:#5B8DB3;color:#fff;border:none;border-radius:50%;width:24px;height:24px;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1}
|
|
.todo-add-btn:hover{background:#4A7A9E}
|
|
.todo-list{flex:1;overflow-y:auto;padding:8px}
|
|
.todo-item{display:flex;align-items:flex-start;gap:6px;padding:6px 8px;border-radius:6px;margin-bottom:4px;background:#f8fafe;font-size:.85rem;transition:background .1s}
|
|
.todo-item:hover{background:#eef4fa}
|
|
.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-done{background:#f0f8f0}
|
|
.todo-item .todo-assignee{font-size:.7rem;color:#8a9aaa;margin-top:1px}
|
|
.todo-item .todo-del{background:none;border:none;color:#ccc;cursor:pointer;font-size:.8rem;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:.72rem;padding:1px 3px}
|
|
.todo-item .todo-assign-btn:hover{color:#5B8DB3}
|
|
.empty{text-align:center;padding:60px 20px;color:#6a8a9a;font-size:1.05rem}
|
|
.card{background:#fff;border-radius:10px;box-shadow:0 1px 6px rgba(0,0,0,.08);margin-bottom:16px;overflow:hidden;border-left:4px solid #5B8DB3;transition:border-color .2s}
|
|
.card.done{border-left-color:#8bc49a;background:#f6fbf6;opacity:.85}
|
|
.card.done .card-header{background:#f0f8f0}
|
|
.done-badge{display:inline-flex;align-items:center;gap:3px}
|
|
.done-badge::before{content:'\2713';font-weight:700;color:#3a8a4a;font-size:.85rem}
|
|
.card-header{display:flex;justify-content:space-between;align-items:center;padding:12px 18px;background:#fafcfe;border-bottom:1px solid #eef2f6;cursor:pointer;user-select:none}
|
|
.card-header .meta{font-size:.82rem;color:#6a8a9a}
|
|
.card-header .patient{font-weight:600;font-size:1rem;color:#1a3a5a;cursor:pointer;transition:color .15s}
|
|
.card-header .patient:hover{color:#5B8DB3}
|
|
.copied-flash{animation:copiedFl .6s ease}
|
|
@keyframes copiedFl{0%{background:#d4edda}100%{background:transparent}}
|
|
.card-header .status{font-size:.75rem;padding:3px 10px;border-radius:12px;font-weight:600}
|
|
.card-header .toggle-arrow{font-size:.7rem;color:#5B8DB3;margin-right:8px;transition:transform .15s}
|
|
.card-header .toggle-arrow.collapsed{transform:rotate(-90deg)}
|
|
.status-offen{background:#fff3cd;color:#856404}
|
|
.status-erledigt{background:#d4edda;color:#155724}
|
|
.card-body{padding:14px 18px}
|
|
.card-body.collapsed{display:none}
|
|
.field{margin-bottom:8px}
|
|
.field-label{font-size:.78rem;font-weight:600;color:#5B8DB3;text-transform:uppercase;letter-spacing:.5px;margin-bottom:2px;cursor:pointer;user-select:none}
|
|
.field-label .tri{font-size:.65rem;margin-right:4px;display:inline-block;transition:transform .15s}
|
|
.field-label .tri.closed{transform:rotate(-90deg)}
|
|
.field-value{white-space:pre-wrap;line-height:1.5}
|
|
.field-value.collapsed{display:none}
|
|
.thread-section{margin-top:12px;border-top:1px solid #e8ecf0;padding-top:10px}
|
|
.thread-title{font-size:.75rem;font-weight:600;color:#5B8DB3;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px}
|
|
.thread-msg{padding:8px 12px;margin-bottom:6px;border-radius:8px;font-size:.88rem}
|
|
.msg-empfang{background:#f3e8ff;border-left:3px solid #9b59b6;margin-right:40px}
|
|
.msg-desktop{background:#e8f4fd;border-left:3px solid #5B8DB3;margin-left:40px}
|
|
.thread-meta{font-size:.72rem;color:#8a9aaa;margin-bottom:3px}
|
|
.t-sender{font-weight:600;margin-right:6px}
|
|
.t-time{font-style:italic}
|
|
.t-text{white-space:pre-wrap;line-height:1.45}
|
|
.card-actions{display:flex;gap:8px;padding:10px 18px;background:#fafcfe;border-top:1px solid #eef2f6;flex-wrap:wrap}
|
|
.card-actions.collapsed{display:none}
|
|
.btn{border:none;border-radius:6px;padding:7px 16px;font-size:.82rem;cursor:pointer;font-weight:500;transition:background .15s}
|
|
.btn-chat{background:#e8e0f8;color:#4a1a6d}.btn-chat:hover{background:#dcd0f0}
|
|
.btn-copy{background:#e8f0f8;color:#2a5a8a}.btn-copy:hover{background:#d4e4f0}
|
|
.btn-print{background:#e8f0e8;color:#2a5a2a}.btn-print:hover{background:#d4e8d4}
|
|
.btn-done{background:#d4edda;color:#155724}.btn-done:hover{background:#c3e6cb}
|
|
.btn-delete{background:#f8e8e8;color:#8a2a2a}.btn-delete:hover{background:#f0d4d4}
|
|
.btn-save{background:#e8f0e8;color:#2a5a2a}.btn-save:hover{background:#d4e8d4}
|
|
.refresh-info{text-align:center;padding:8px;font-size:.78rem;color:#8a9aaa}
|
|
.chat-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.35);z-index:100;align-items:center;justify-content:center}
|
|
.chat-overlay.active{display:flex}
|
|
.chat-box{background:#fff;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.18);padding:24px;width:460px;max-width:92vw}
|
|
.chat-box h3{margin-bottom:12px;color:#1a4d6d}
|
|
.chat-box textarea{width:100%;height:100px;border:1px solid #ccc;border-radius:6px;padding:8px;font-family:inherit;font-size:.95rem;resize:vertical}
|
|
.chat-btns{display:flex;gap:8px;margin-top:12px;justify-content:flex-end}
|
|
.btn-send{background:#5B8DB3;color:#fff;border:none;border-radius:6px;padding:8px 20px;cursor:pointer;font-weight:600}
|
|
.btn-send:hover{background:#4A7A9E}
|
|
.btn-cancel{background:#dde8f0;color:#1a4d6d;border:none;border-radius:6px;padding:8px 20px;cursor:pointer}
|
|
.btn-dictate{background:#f0e8ff;color:#6b3fa0;border:none;border-radius:6px;padding:8px 16px;cursor:pointer;font-weight:500}
|
|
.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}}
|
|
@media print{header,.card-actions,.refresh-info,.chat-overlay,.settings-panel{display:none}.card{break-inside:avoid;border-left-width:2px;box-shadow:none}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<h1>AZA – Empfang</h1>
|
|
<div class="header-right">
|
|
<div class="font-ctrl">
|
|
<span>Aa</span>
|
|
<span id="font-size-display">9</span>
|
|
<button onclick="changeFontSize(1)" title="Schrift vergrössern">▲</button>
|
|
<button onclick="changeFontSize(-1)" title="Schrift verkleinern">▼</button>
|
|
</div>
|
|
<button class="hbtn" id="sound-btn" onclick="toggleSound()" title="Ton an/aus">🔔 Ton an</button>
|
|
<div class="vol-ctrl" title="Lautstärke">
|
|
<span>🔈</span>
|
|
<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" onclick="testCurrentSound()" title="Ton testen">▶ Testen</button>
|
|
<button class="hbtn" id="sort-btn" onclick="toggleSort()" title="Sortierung umkehren">⇅</button>
|
|
<span class="badge" id="count-badge">–</span>
|
|
<button class="hbtn" onclick="toggleSettings()" title="Einstellungen">⚙</button>
|
|
<select class="hbtn" id="user-select" onchange="switchUser()" title="Benutzer" style="max-width:120px;font-size:.78rem">
|
|
<option value="">Benutzer...</option>
|
|
</select>
|
|
</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-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:60px" onchange="onRepeatChange()"> Sek.
|
|
</span>
|
|
</div>
|
|
<h3 style="margin-top:14px">Aufbewahrung</h3>
|
|
<div class="setting-row">
|
|
<label for="cleanup-days">Nachrichten älter als</label>
|
|
<input type="number" id="cleanup-days" min="1" max="365" value="30" style="width:60px"> Tage
|
|
<button class="btn-cleanup" onclick="doCleanup()">Jetzt bereinigen</button>
|
|
<span id="cleanup-result" style="font-size:.8rem;color:#6a8a9a"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main-split" id="main-split">
|
|
<div class="container" id="messages-container">
|
|
<div class="empty">Nachrichten werden geladen…</div>
|
|
</div>
|
|
<div class="split-handle" id="split-handle" title="Ziehen zum Aendern der Breite"></div>
|
|
<div class="todo-panel" id="todo-panel">
|
|
<div class="todo-header">
|
|
<span>Aufgaben</span>
|
|
<div style="display:flex;gap:4px;align-items:center">
|
|
<button class="todo-add-btn" onclick="addTodo()" title="Neue Aufgabe" style="width:22px;height:22px;font-size:14px">+</button>
|
|
<button class="todo-add-btn" onclick="toggleTodoSide()" title="Panel-Seite wechseln" style="width:22px;height:22px;font-size:12px;background:#dde8f0;color:#5B8DB3">⇄</button>
|
|
</div>
|
|
</div>
|
|
<div class="todo-list" id="todo-list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="refresh-info">Aktualisiert automatisch alle 10 Sekunden · <span style="opacity:.5" id="ui-version">v2026.04.17c</span></div>
|
|
|
|
<div class="chat-overlay" id="chat-overlay">
|
|
<div class="chat-box">
|
|
<h3 id="chat-title">Antwort senden</h3>
|
|
<textarea id="chat-text" placeholder="Ihre Antwort..."></textarea>
|
|
<div class="chat-btns">
|
|
<button class="btn-dictate" id="dictate-btn" onclick="toggleDictation()">🎤 Diktieren</button>
|
|
<button class="btn-cancel" onclick="closeChat()">Abbrechen</button>
|
|
<button class="btn-send" onclick="sendChat()">Senden</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
var API_BASE = window.location.origin + '/empfang';
|
|
var lastData = '';
|
|
var previousThreadIds = new Set();
|
|
var soundEnabled = localStorage.getItem('empfang_sound') !== 'off';
|
|
var audioCtx = null;
|
|
var currentFontSize = parseInt(localStorage.getItem('empfang_font_size') || '9', 10);
|
|
var chatTargetThreadId = null;
|
|
var chatTargetAbsender = '';
|
|
var justSentReply = false;
|
|
var repeatInterval = null;
|
|
var recognition = null;
|
|
var isRecording = false;
|
|
var volume = parseFloat(localStorage.getItem('empfang_volume') || '1.0');
|
|
var repeatMs = parseInt(localStorage.getItem('empfang_repeat_ms') || '30000', 10);
|
|
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);
|
|
|
|
(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);
|
|
});
|
|
var vs = document.getElementById('vol-slider');
|
|
vs.value = Math.round(volume * 100);
|
|
document.getElementById('vol-display').textContent = Math.round(volume * 100) + '%';
|
|
initRepeatUI();
|
|
updateSortBtn();
|
|
})();
|
|
|
|
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';
|
|
var s = parseInt(document.getElementById('custom-sec').value, 10);
|
|
repeatMs = Math.max(1, s) * 1000;
|
|
} else {
|
|
cw.style.display = 'none';
|
|
repeatMs = parseInt(sel.value, 10);
|
|
}
|
|
localStorage.setItem('empfang_repeat_ms', repeatMs);
|
|
}
|
|
|
|
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 toggleSettings() {
|
|
document.getElementById('settings-panel').classList.toggle('open');
|
|
}
|
|
|
|
function applyFontSize() {
|
|
document.body.style.fontSize = currentFontSize + 'pt';
|
|
document.getElementById('font-size-display').textContent = currentFontSize;
|
|
localStorage.setItem('empfang_font_size', currentFontSize);
|
|
}
|
|
applyFontSize();
|
|
|
|
function changeFontSize(d) {
|
|
currentFontSize = Math.max(5, Math.min(20, currentFontSize + d));
|
|
applyFontSize();
|
|
}
|
|
|
|
function updateSoundBtn() {
|
|
var btn = document.getElementById('sound-btn');
|
|
if (soundEnabled) { btn.innerHTML = '🔔 Ton an'; btn.classList.remove('muted'); }
|
|
else { btn.innerHTML = '🔕 Ton aus'; btn.classList.add('muted'); }
|
|
}
|
|
updateSoundBtn();
|
|
|
|
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 toggleSort() {
|
|
sortNewestFirst = !sortNewestFirst;
|
|
localStorage.setItem('empfang_sort', sortNewestFirst ? 'newest' : 'asc');
|
|
updateSortBtn();
|
|
lastData = '';
|
|
loadMessages();
|
|
}
|
|
function updateSortBtn() {
|
|
var btn = document.getElementById('sort-btn');
|
|
if (sortNewestFirst) {
|
|
btn.innerHTML = '↑ Neueste oben';
|
|
btn.classList.add('active-sort');
|
|
} else {
|
|
btn.innerHTML = '↓ Neueste unten';
|
|
btn.classList.remove('active-sort');
|
|
}
|
|
}
|
|
|
|
/* --- Dictation (Web Speech API) --- */
|
|
function toggleDictation() {
|
|
if (isRecording) { stopDictation(); return; }
|
|
var SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
if (!SR) { alert('Spracherkennung wird von diesem Browser nicht unterstuetzt.\nBitte Chrome oder Edge verwenden.'); return; }
|
|
recognition = new SR();
|
|
recognition.lang = 'de-DE';
|
|
recognition.continuous = true;
|
|
recognition.interimResults = false;
|
|
recognition.onresult = function(ev) {
|
|
var ta = document.getElementById('chat-text');
|
|
for (var i = ev.resultIndex; i < ev.results.length; i++) {
|
|
if (ev.results[i].isFinal) ta.value += ev.results[i][0].transcript + ' ';
|
|
}
|
|
};
|
|
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 = '⏹ Stoppen'; btn.classList.add('recording'); }
|
|
else { btn.innerHTML = '🎤 Diktieren'; btn.classList.remove('recording'); }
|
|
}
|
|
|
|
/* --- Messages --- */
|
|
async function loadMessages() {
|
|
try {
|
|
var r = await fetch(API_BASE + '/messages');
|
|
var d = await r.json();
|
|
var raw = JSON.stringify(d.messages || []);
|
|
if (raw === lastData) return;
|
|
lastData = raw;
|
|
var msgs = d.messages || [];
|
|
|
|
msgs.forEach(function(m) { if (!m.thread_id) m.thread_id = m.id; });
|
|
|
|
if (currentUser) {
|
|
msgs = msgs.filter(function(m) {
|
|
var rcpt = m.extras && m.extras.recipient;
|
|
return !rcpt || rcpt === currentUser || rcpt === 'Alle';
|
|
});
|
|
}
|
|
|
|
var threadMap = {};
|
|
msgs.forEach(function(m) {
|
|
if (!threadMap[m.thread_id]) threadMap[m.thread_id] = [];
|
|
threadMap[m.thread_id].push(m);
|
|
});
|
|
|
|
var currentIds = new Set(Object.keys(threadMap));
|
|
var hasNewDesktop = false;
|
|
currentIds.forEach(function(tid) {
|
|
if (!previousThreadIds.has(tid)) {
|
|
var root = threadMap[tid].find(function(m){ return m.id === tid; }) || threadMap[tid][0];
|
|
if (root && !(root.absender || '').includes('Empfang')) hasNewDesktop = true;
|
|
}
|
|
});
|
|
|
|
if (previousThreadIds.size > 0 && hasNewDesktop && !justSentReply) {
|
|
playTone(currentToneIdx);
|
|
startRepeatTone();
|
|
}
|
|
justSentReply = false;
|
|
previousThreadIds = currentIds;
|
|
|
|
renderThreads(threadMap);
|
|
} catch (e) {
|
|
document.getElementById('messages-container').innerHTML =
|
|
'<div class="empty">Backend nicht erreichbar.<br>Bitte Verbindung prüfen.</div>';
|
|
}
|
|
}
|
|
|
|
function renderThreads(threadMap) {
|
|
var c = document.getElementById('messages-container');
|
|
var threads = [];
|
|
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 || '';
|
|
threads.push({ tid:tid, root:root, replies:replies, lastTime:lastTime });
|
|
});
|
|
|
|
threads.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);
|
|
});
|
|
|
|
var openCount = threads.filter(function(t){ return t.root.status === 'offen'; }).length;
|
|
document.getElementById('count-badge').textContent = openCount + ' offen';
|
|
|
|
if (!threads.length) { c.innerHTML = '<div class="empty">Keine Nachrichten vorhanden.</div>'; return; }
|
|
|
|
c.innerHTML = threads.map(function(t){ return renderOneThread(t); }).join('');
|
|
}
|
|
|
|
function renderOneThread(t) {
|
|
var tid = t.tid, m = t.root, replies = t.replies;
|
|
var isDone = m.status === 'erledigt';
|
|
var isTask = m.extras && m.extras.is_task;
|
|
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 / Chat', v:m.kommentar});
|
|
if (m.medikamente) fields.push({l:'Medikamente', v:m.medikamente});
|
|
|
|
var attachHtml = '';
|
|
if (m.extras && m.extras.attachments && m.extras.attachments.length) {
|
|
attachHtml = '<div class="field" style="margin-top:6px">' +
|
|
'<div class="field-label">Anh\u00e4nge</div>' +
|
|
m.extras.attachments.map(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');
|
|
return '<div style="margin:4px 0"><img src="data:' + mime + ';base64,' + a.data +
|
|
'" style="max-width:100%;max-height:300px;border-radius:6px;border:1px solid #e0e4e8" alt="' +
|
|
esc(a.name) + '"><div style="font-size:.75rem;color:#8a9aaa">' + esc(a.name) + '</div></div>';
|
|
}
|
|
return '<div style="margin:2px 0;font-size:.85rem">\uD83D\uDCCE ' + esc(a.name) + '</div>';
|
|
}).join('') + '</div>';
|
|
}
|
|
|
|
var sortedReplies = sortNewestFirst ? replies.slice().reverse() : replies;
|
|
|
|
var threadHtml = '';
|
|
if (replies.length) {
|
|
threadHtml = '<div class="thread-section"><div class="thread-title">Chat-Verlauf (' +
|
|
replies.length + ')</div>' +
|
|
sortedReplies.map(function(r) {
|
|
var isE = (r.absender||'').includes('Empfang');
|
|
return '<div class="thread-msg ' + (isE?'msg-empfang':'msg-desktop') + '">' +
|
|
'<div class="thread-meta"><span class="t-sender">' + esc(r.absender||'') +
|
|
'</span> <span class="t-time">' + esc(r.zeitstempel||r.empfangen||'') +
|
|
'</span></div><div class="t-text">' + esc(r.kommentar||'') + '</div></div>';
|
|
}).join('') + '</div>';
|
|
}
|
|
|
|
var replyInfo = replies.length ? ' · <b>' + replies.length + '</b> Antwort' + (replies.length>1?'en':'') : '';
|
|
|
|
return '<div class="card ' + (isDone?'done':'') + '" id="card-' + tid + '">' +
|
|
'<div class="card-header" onclick="toggleCard(\'' + tid + '\')">' +
|
|
'<div style="display:flex;align-items:center">' +
|
|
'<span class="toggle-arrow" id="arrow-' + tid + '">▼</span>' +
|
|
'<div><div class="patient" onclick="event.stopPropagation();copyText(this)" title="Klicken zum Kopieren">' + (isTask ? '\u2611 ' : '') + esc(m.patient || 'Ohne Patientenangabe') + '</div>' +
|
|
'<div class="meta">' + esc(m.absender||'\u2013') + ' · ' +
|
|
esc(m.zeitstempel||m.empfangen||'') + replyInfo + '</div></div>' +
|
|
'</div>' +
|
|
'<span class="status ' + (isDone?'status-erledigt done-badge':'status-offen') + '">' +
|
|
(isDone?'Erledigt':'Offen') + '</span>' +
|
|
'</div>' +
|
|
'<div class="card-body" id="body-' + tid + '">' +
|
|
fields.map(function(f) {
|
|
return '<div class="field"><div class="field-label" onclick="event.stopPropagation();toggleField(this)">' +
|
|
'<span class="tri">▼</span>' + esc(f.l) + '</div>' +
|
|
'<div class="field-value">' + esc(f.v) + '</div></div>';
|
|
}).join('') +
|
|
(!fields.length && !replies.length && !attachHtml ? '<div class="field"><div class="field-value" style="color:#999">Keine Inhalte</div></div>' : '') +
|
|
attachHtml +
|
|
threadHtml +
|
|
'</div>' +
|
|
'<div class="card-actions" id="actions-' + tid + '">' +
|
|
'<button class="btn btn-chat" onclick="openChat(\'' + tid + '\',\'' + esc(m.absender||'').replace(/'/g,'\\\'') + '\')">Chat antworten</button>' +
|
|
'<button class="btn btn-copy" onclick="copyThread(\'' + tid + '\')">Kopieren</button>' +
|
|
'<button class="btn btn-save" onclick="saveThread(\'' + tid + '\')">Speichern</button>' +
|
|
'<button class="btn btn-print" onclick="printThread(\'' + tid + '\')">Drucken</button>' +
|
|
(!isDone ? '<button class="btn btn-done" onclick="markDone(\'' + m.id + '\')">Erledigt</button>' : '') +
|
|
'<button class="btn btn-delete" onclick="deleteMsg(\'' + m.id + '\')">Löschen</button>' +
|
|
'</div></div>';
|
|
}
|
|
|
|
function toggleCard(id) {
|
|
var body = document.getElementById('body-' + id);
|
|
var actions = document.getElementById('actions-' + id);
|
|
var arrow = document.getElementById('arrow-' + id);
|
|
if (!body) return;
|
|
var isCollapsed = body.classList.toggle('collapsed');
|
|
if (actions) actions.classList.toggle('collapsed', isCollapsed);
|
|
if (arrow) arrow.classList.toggle('collapsed', isCollapsed);
|
|
stopRepeatTone();
|
|
}
|
|
|
|
function toggleField(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);
|
|
}
|
|
|
|
function esc(s) {
|
|
var d = document.createElement('div');
|
|
d.textContent = s || '';
|
|
return d.innerHTML;
|
|
}
|
|
|
|
/* --- Chat --- */
|
|
function openChat(threadId, absender) {
|
|
chatTargetThreadId = threadId;
|
|
chatTargetAbsender = absender;
|
|
document.getElementById('chat-title').textContent = 'Antwort an ' + (absender || 'Absender');
|
|
document.getElementById('chat-text').value = '';
|
|
document.getElementById('chat-overlay').classList.add('active');
|
|
document.getElementById('chat-text').focus();
|
|
stopRepeatTone();
|
|
}
|
|
|
|
function closeChat() {
|
|
document.getElementById('chat-overlay').classList.remove('active');
|
|
chatTargetThreadId = null;
|
|
stopDictation();
|
|
}
|
|
|
|
async function sendChat() {
|
|
var text = document.getElementById('chat-text').value.trim();
|
|
if (!text) return;
|
|
stopDictation();
|
|
try {
|
|
var payload = {
|
|
medikamente: '', therapieplan: '', procedere: '',
|
|
kommentar: text, patient: '',
|
|
absender: 'Empfang (Antwort)',
|
|
zeitstempel: new Date().toISOString().slice(0,19).replace('T',' '),
|
|
extras: { reply_to: chatTargetThreadId, reply_to_absender: chatTargetAbsender }
|
|
};
|
|
var r = await fetch(API_BASE + '/send', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
if (r.ok) { closeChat(); justSentReply = true; lastData = ''; loadMessages(); notifyOtherTabs(); }
|
|
else { alert('Senden fehlgeschlagen.'); }
|
|
} catch(e) { alert('Fehler: ' + e.message); }
|
|
}
|
|
|
|
/* --- Actions --- */
|
|
function saveThread(tid) {
|
|
var msgs = JSON.parse(lastData);
|
|
var threadMsgs = msgs.filter(function(m){ return (m.thread_id||m.id) === tid; });
|
|
var root = threadMsgs.find(function(m){ return m.id === tid; }) || threadMsgs[0];
|
|
if (!root) return;
|
|
var lines = [];
|
|
lines.push('=== AZA Empfang Chat-Verlauf ===');
|
|
lines.push('Patient/Nr.: ' + (root.patient || '-'));
|
|
lines.push('Absender: ' + (root.absender || '-'));
|
|
lines.push('Zeitpunkt: ' + (root.zeitstempel || root.empfangen || ''));
|
|
lines.push('');
|
|
if (root.therapieplan) lines.push('THERAPIEPLAN:\n' + root.therapieplan + '\n');
|
|
if (root.procedere) lines.push('PROCEDERE:\n' + root.procedere + '\n');
|
|
if (root.kommentar) lines.push('KOMMENTAR:\n' + root.kommentar + '\n');
|
|
var replies = threadMsgs.filter(function(m){ return m.id !== root.id; })
|
|
.sort(function(a,b){ return (a.empfangen||'').localeCompare(b.empfangen||''); });
|
|
if (replies.length) {
|
|
lines.push('--- Chat-Verlauf ---');
|
|
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_' + (root.patient || tid).replace(/[^a-zA-Z0-9]/g, '_') + '.txt';
|
|
a.click();
|
|
URL.revokeObjectURL(a.href);
|
|
}
|
|
|
|
function copyThread(tid) {
|
|
var msgs = JSON.parse(lastData);
|
|
var threadMsgs = msgs.filter(function(m){ return (m.thread_id||m.id) === tid; });
|
|
var root = threadMsgs.find(function(m){ return m.id === tid; }) || threadMsgs[0];
|
|
if (!root) return;
|
|
var parts = [];
|
|
if (root.patient) parts.push('Patient: ' + root.patient);
|
|
if (root.therapieplan) parts.push('Therapieplan:\n' + root.therapieplan);
|
|
if (root.procedere) parts.push('Procedere:\n' + root.procedere);
|
|
if (root.kommentar) parts.push('Kommentar:\n' + root.kommentar);
|
|
parts.push('Absender: ' + (root.absender||'\u2013') + ' \u00b7 ' + (root.zeitstempel||''));
|
|
var replies = threadMsgs.filter(function(m){ return m.id !== root.id; })
|
|
.sort(function(a,b){ return (a.empfangen||'').localeCompare(b.empfangen||''); });
|
|
if (replies.length) {
|
|
parts.push('\n--- Chat-Verlauf ---');
|
|
replies.forEach(function(r) {
|
|
parts.push((r.absender||'') + ' (' + (r.zeitstempel||'') + '):\n' + (r.kommentar||''));
|
|
});
|
|
}
|
|
navigator.clipboard.writeText(parts.join('\n\n')).then(function(){
|
|
var btn = document.querySelector('#card-' + tid + ' .btn-copy');
|
|
if (btn) { var o = btn.textContent; btn.textContent = '\u2713 Kopiert'; setTimeout(function(){ btn.textContent = o; }, 1500); }
|
|
});
|
|
stopRepeatTone();
|
|
}
|
|
|
|
function printThread(tid) {
|
|
var msgs = JSON.parse(lastData);
|
|
var threadMsgs = msgs.filter(function(m){ return (m.thread_id||m.id) === tid; });
|
|
var root = threadMsgs.find(function(m){ return m.id === tid; }) || threadMsgs[0];
|
|
if (!root) return;
|
|
var replies = threadMsgs.filter(function(m){ return m.id !== root.id; })
|
|
.sort(function(a,b){ return (a.empfangen||'').localeCompare(b.empfangen||''); });
|
|
var w = window.open('', '_blank', 'width=600,height=600');
|
|
var html = '<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(root.patient||'Empfangsnachricht') + '</h2>' +
|
|
(root.therapieplan ? '<div class="f"><div class="fl">Therapieplan</div><div class="fv">' + esc(root.therapieplan) + '</div></div>' : '') +
|
|
(root.procedere ? '<div class="f"><div class="fl">Procedere</div><div class="fv">' + esc(root.procedere) + '</div></div>' : '') +
|
|
(root.kommentar ? '<div class="f"><div class="fl">Kommentar</div><div class="fv">' + esc(root.kommentar) + '</div></div>' : '') +
|
|
'<div class="meta">Absender: ' + esc(root.absender||'\u2013') + ' \u00b7 ' + esc(root.zeitstempel||root.empfangen||'') + '</div>' +
|
|
(replies.length ? '<h3 style="margin-top:20px">Chat-Verlauf</h3>' +
|
|
replies.map(function(r){
|
|
return '<div class="reply"><div class="rs">' + esc(r.absender||'') + ' · ' +
|
|
esc(r.zeitstempel||r.empfangen||'') + '</div><div class="fv">' + esc(r.kommentar||'') + '</div></div>';
|
|
}).join('') : '') +
|
|
'</body></html>';
|
|
w.document.write(html);
|
|
w.document.close();
|
|
w.print();
|
|
}
|
|
|
|
async function markDone(id) {
|
|
await fetch(API_BASE + '/messages/' + id + '/done', {method:'POST'});
|
|
var card = document.getElementById('card-' + id);
|
|
if (!card) {
|
|
var msgs = JSON.parse(lastData || '[]');
|
|
var tgt = msgs.find(function(m){ return m.id === id; });
|
|
if (tgt) card = document.getElementById('card-' + (tgt.thread_id || id));
|
|
}
|
|
if (card) {
|
|
var body = card.querySelector('.card-body');
|
|
var actions = card.querySelector('.card-actions');
|
|
var arrow = card.querySelector('.toggle-arrow');
|
|
if (body) body.classList.add('collapsed');
|
|
if (actions) actions.classList.add('collapsed');
|
|
if (arrow) arrow.classList.add('collapsed');
|
|
}
|
|
lastData = '';
|
|
stopRepeatTone();
|
|
loadMessages();
|
|
notifyOtherTabs();
|
|
}
|
|
|
|
async function deleteMsg(id) {
|
|
if (!confirm('Nachricht und alle Antworten wirklich l\u00f6schen?')) return;
|
|
try {
|
|
var r = await fetch(API_BASE + '/messages/' + id, {method:'DELETE'});
|
|
if (!r.ok) { console.error('Delete failed:', r.status); }
|
|
} catch(e) { console.error('Delete error:', e); }
|
|
lastData = '';
|
|
loadMessages();
|
|
notifyOtherTabs();
|
|
}
|
|
|
|
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) { lastData = ''; loadMessages(); }
|
|
} catch(e) { el.textContent = 'Fehler'; }
|
|
}
|
|
|
|
/* --- Quick copy (click on patient/text) --- */
|
|
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);
|
|
});
|
|
}
|
|
|
|
/* --- Simple user model (no auth, just a name) --- */
|
|
var currentUser = localStorage.getItem('empfang_current_user') || '';
|
|
var knownUsers = JSON.parse(localStorage.getItem('empfang_known_users') || '[]');
|
|
|
|
function userKey(base) { return currentUser ? base + '_' + currentUser : base; }
|
|
|
|
(function initUserSelect() {
|
|
var sel = document.getElementById('user-select');
|
|
knownUsers.forEach(function(u) {
|
|
var o = document.createElement('option');
|
|
o.value = u; o.textContent = u;
|
|
if (u === currentUser) o.selected = true;
|
|
sel.appendChild(o);
|
|
});
|
|
var addOpt = document.createElement('option');
|
|
addOpt.value = '__new__'; addOpt.textContent = '+ Neuer Benutzer';
|
|
sel.appendChild(addOpt);
|
|
if (currentUser && !knownUsers.includes(currentUser)) {
|
|
var c = document.createElement('option');
|
|
c.value = currentUser; c.textContent = currentUser; c.selected = true;
|
|
sel.insertBefore(c, addOpt);
|
|
}
|
|
})();
|
|
|
|
function switchUser() {
|
|
var sel = document.getElementById('user-select');
|
|
if (sel.value === '__new__') {
|
|
var name = prompt('Benutzername eingeben:');
|
|
if (!name || !name.trim()) { sel.value = currentUser; return; }
|
|
name = name.trim();
|
|
if (!knownUsers.includes(name)) {
|
|
knownUsers.push(name);
|
|
localStorage.setItem('empfang_known_users', JSON.stringify(knownUsers));
|
|
var o = document.createElement('option');
|
|
o.value = name; o.textContent = name;
|
|
sel.insertBefore(o, sel.lastElementChild);
|
|
}
|
|
sel.value = name;
|
|
}
|
|
currentUser = sel.value;
|
|
localStorage.setItem('empfang_current_user', currentUser);
|
|
loadUserData();
|
|
}
|
|
|
|
function loadUserData() {
|
|
todos = JSON.parse(localStorage.getItem(userKey('empfang_todos')) || '[]');
|
|
renderTodos();
|
|
var savedW = parseInt(localStorage.getItem(userKey('empfang_todo_width')) || '260', 10);
|
|
document.getElementById('todo-panel').style.width = savedW + 'px';
|
|
var side = localStorage.getItem(userKey('empfang_todo_side'));
|
|
var split = document.getElementById('main-split');
|
|
var panel = document.getElementById('todo-panel');
|
|
var handle = document.getElementById('split-handle');
|
|
if (side === 'left') {
|
|
split.insertBefore(panel, split.firstElementChild);
|
|
split.insertBefore(handle, panel.nextSibling);
|
|
} else {
|
|
if (split.lastElementChild !== panel) {
|
|
split.appendChild(handle);
|
|
split.appendChild(panel);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* --- To-Do (user-scoped, with assignee) --- */
|
|
var todos = JSON.parse(localStorage.getItem(userKey('empfang_todos')) || '[]');
|
|
|
|
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:.82rem;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">×</button>' +
|
|
'</div>';
|
|
}).join('');
|
|
}
|
|
|
|
function addTodo() {
|
|
var text = prompt('Neue Aufgabe:');
|
|
if (text && text.trim()) {
|
|
todos.push({text: text.trim(), done: false, assignee: currentUser || '', created: new Date().toISOString().slice(0,16)});
|
|
saveTodos(); renderTodos();
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
renderTodos();
|
|
|
|
/* --- Layout: Todo panel side toggle (H3) --- */
|
|
function toggleTodoSide() {
|
|
var split = document.getElementById('main-split');
|
|
var panel = document.getElementById('todo-panel');
|
|
var handle = document.getElementById('split-handle');
|
|
var container = document.getElementById('messages-container');
|
|
var isLeft = split.firstElementChild === panel;
|
|
if (isLeft) {
|
|
split.appendChild(handle);
|
|
split.appendChild(panel);
|
|
} else {
|
|
split.insertBefore(panel, split.firstElementChild);
|
|
split.insertBefore(handle, panel.nextSibling);
|
|
}
|
|
localStorage.setItem(userKey('empfang_todo_side'), isLeft ? 'right' : 'left');
|
|
}
|
|
|
|
(function initTodoSide() {
|
|
var side = localStorage.getItem(userKey('empfang_todo_side'));
|
|
if (side === 'left') {
|
|
var split = document.getElementById('main-split');
|
|
var panel = document.getElementById('todo-panel');
|
|
var handle = document.getElementById('split-handle');
|
|
split.insertBefore(panel, split.firstElementChild);
|
|
split.insertBefore(handle, panel.nextSibling);
|
|
}
|
|
})();
|
|
|
|
/* --- Split handle --- */
|
|
(function initSplit() {
|
|
var handle = document.getElementById('split-handle');
|
|
var panel = document.getElementById('todo-panel');
|
|
var savedW = parseInt(localStorage.getItem(userKey('empfang_todo_width')) || '260', 10);
|
|
panel.style.width = savedW + 'px';
|
|
var dragging = false, startX = 0, startW = 0;
|
|
handle.addEventListener('mousedown', function(e) {
|
|
dragging = true; startX = e.clientX; startW = panel.offsetWidth;
|
|
handle.classList.add('dragging');
|
|
e.preventDefault();
|
|
});
|
|
document.addEventListener('mousemove', function(e) {
|
|
if (!dragging) return;
|
|
var newW = startW - (e.clientX - startX);
|
|
newW = Math.max(180, Math.min(400, newW));
|
|
panel.style.width = newW + 'px';
|
|
});
|
|
document.addEventListener('mouseup', function() {
|
|
if (!dragging) return;
|
|
dragging = false;
|
|
handle.classList.remove('dragging');
|
|
localStorage.setItem(userKey('empfang_todo_width'), panel.offsetWidth);
|
|
});
|
|
})();
|
|
|
|
loadMessages();
|
|
setInterval(loadMessages, 10000);
|
|
document.addEventListener('visibilitychange', function() {
|
|
if (!document.hidden) { lastData = ''; loadMessages(); }
|
|
});
|
|
|
|
/* --- Cross-tab sync via BroadcastChannel --- */
|
|
var bc = null;
|
|
try {
|
|
bc = new BroadcastChannel('aza_empfang_sync');
|
|
bc.onmessage = function(ev) {
|
|
if (ev.data === 'reload') { lastData = ''; loadMessages(); }
|
|
};
|
|
} catch(e) {}
|
|
function notifyOtherTabs() {
|
|
try { if (bc) bc.postMessage('reload'); } catch(e) {}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|