This commit is contained in:
2026-05-08 14:53:53 +02:00
parent 01f05b23e9
commit 3ca2fea861
10597 changed files with 2474786 additions and 488 deletions

View File

@@ -10,8 +10,10 @@ html,body{height:100%;font-family:'Segoe UI',system-ui,sans-serif;background:#ea
:root{--aza-blue:#5B8DB3;--aza-deep:#356488;--aza-rail:linear-gradient(180deg,#3d6f92 0%,#2a5472 100%);--aza-sh:0 4px 18px rgba(28,62,94,.08)}
body{display:flex;flex-direction:column;min-height:100%;overflow:hidden}
header{background:linear-gradient(135deg,#5B8DB3,#3a6d93);color:#fff;padding:8px 14px;flex-shrink:0;z-index:30;box-shadow:0 2px 12px rgba(40,80,110,.2)}
.hdr-row1{display:flex;flex-wrap:wrap;align-items:center;gap:10px}
header h1{font-size:.95rem;font-weight:700;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.hdr-row1{display:flex;flex-wrap:wrap;align-items:flex-start;gap:10px}
.hdr-main-titles{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
header h1{font-size:.95rem;font-weight:700;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin:0}
.hdr-sub{font-size:.62rem;font-weight:500;opacity:.88;line-height:1.32;white-space:normal;word-wrap:break-word}
.hdr-tools{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-left:auto}
.hdr-tools .cw-ton-lbl{font-size:.62rem;opacity:.92;text-transform:uppercase;letter-spacing:.04em}
.hdr-tools input[type=range]{width:80px;accent-color:#fff;cursor:pointer}
@@ -38,7 +40,9 @@ header h1{font-size:.95rem;font-weight:700;flex:1;min-width:0;overflow:hidden;te
.pend-item{position:relative;border:1px solid #b8cce0;border-radius:12px;overflow:hidden;background:#fff;box-shadow:var(--aza-sh)}
.pend-item img{display:block;width:76px;height:76px;object-fit:cover}
.pend-item .rm{position:absolute;top:4px;right:4px;width:22px;height:22px;border:none;border-radius:50%;background:#c0392b;color:#fff;font-size:12px;cursor:pointer}
#log{flex:1;overflow-y:auto;padding:20px clamp(16px,4vw,40px);display:flex;flex-direction:column;gap:12px}
#log{flex:1;overflow-y:auto;padding:20px clamp(16px,4vw,40px);display:flex;flex-direction:column;gap:4px}
.msg-date-sep{text-align:center;margin:14px 0 8px}
.msg-date-sep span{display:inline-block;padding:4px 12px;border-radius:999px;background:rgba(255,255,255,.95);border:1px solid #d8e6f4;font-size:.65rem;font-weight:700;color:#5a7590}
#log::-webkit-scrollbar{width:10px}
#log::-webkit-scrollbar-thumb{background:#bfd5e8;border-radius:999px;border:2px solid #f0f6fb}
.msg{max-width:min(92%,640px);padding:13px 16px;border-radius:16px;font-size:.89rem;line-height:1.55;word-wrap:break-word;border:1px solid transparent;box-shadow:0 1px 3px rgba(28,62,94,.06)}
@@ -77,7 +81,10 @@ header h1{font-size:.95rem;font-weight:700;flex:1;min-width:0;overflow:hidden;te
</div>
<header>
<div class="hdr-row1">
<h1 id="hdr-title">Chat</h1>
<div class="hdr-main-titles">
<h1 id="hdr-title">Chat</h1>
<div class="hdr-sub" id="hdr-practice-sub" aria-live="polite"></div>
</div>
<div class="hdr-tools" title="Ton (Benachrichtigungen)">
<span class="cw-ton-lbl" aria-hidden="true">Ton</span>
<input type="range" id="cw-vol" min="0" max="300" step="5" title="Lautstärke" aria-label="Lautstärke Benachrichtigung">
@@ -128,11 +135,14 @@ var API_BASE = window.location.origin + '/empfang';
var MAX_B64 = 2 * 1024 * 1024;
var mode = 'general';
var peerName = '';
var peerUserId = '';
var threadId = null;
var currentSession = null;
var pollTimer = null;
var pendingAttach = [];
var lastPollSig = '';
var lastServerTick = -1;
var lastConvMessages = [];
var dictateRec = null;
var dictateListening = false;
@@ -243,6 +253,7 @@ function parseQs() {
} catch (e) {
peerName = (p.get('peer') || '').trim();
}
peerUserId = (p.get('peer_uid') || '').trim();
if (mode === 'dm' && !peerName) mode = 'general';
}
@@ -275,6 +286,24 @@ function setTitle() {
if (cs) cs.textContent = mode === 'dm' && peerName ? 'Direkt · ' + peerName : 'Praxis-Posteingang · Allgemein';
}
async function loadPracticeSubline() {
try {
var r = await fetch(API_BASE + '/practice/info', { credentials: 'include' });
if (!r.ok) return;
var d = await r.json();
var el = document.getElementById('hdr-practice-sub');
if (!el) return;
var pname = String(d.practice_name || '').trim() || 'Praxis-Chat';
var pid = String(d.practice_id || '').trim();
var sid = pid ? (pid.length > 14 ? pid.slice(0, 12) + '\u2026' : pid) : '';
if (!pid) {
el.textContent = pname + ' \u2013 Praxiszuordnung pr\u00fcfen';
return;
}
el.textContent = pname + ' \u00b7 ID ' + sid + ' \u00b7 mit Praxis-Chat verbunden';
} catch (e) {}
}
function mimeForAttachment(name, fallbackMime) {
var fm = (fallbackMime || '').toLowerCase();
if (fm.indexOf('image/') === 0) return fm;
@@ -408,6 +437,57 @@ function patientLine() {
pad(now.getHours()) + ':' + pad(now.getMinutes());
}
function conversationAudienceParam() {
if (mode === 'dm' && peerName) return peerName;
return '';
}
function conversationSortChrono(msgs) {
return msgs.slice().sort(function(a, b) {
return (a.empfangen || a.zeitstempel || '').localeCompare(b.empfangen || b.zeitstempel || '');
});
}
function conversationDayKey(iso) {
var raw = String(iso || '').replace(' ', 'T');
var d = Date.parse(raw);
if (!isFinite(d)) return '';
var dt = new Date(d);
var y = dt.getFullYear(), mo = dt.getMonth() + 1, da = dt.getDate();
return y + '-' + (mo < 10 ? '0' : '') + mo + '-' + (da < 10 ? '0' : '') + da;
}
function conversationDateLabel(iso) {
var raw = String(iso || '').replace(' ', 'T');
var d = Date.parse(raw);
if (!isFinite(d)) return '';
var msgDate = new Date(d);
var today = new Date();
today.setHours(0, 0, 0, 0);
var dd = new Date(msgDate);
dd.setHours(0, 0, 0, 0);
var diffDays = Math.round((today.getTime() - dd.getTime()) / 86400000);
if (diffDays === 0) return 'Heute';
if (diffDays === 1) return 'Gestern';
return msgDate.getDate() + '.' + (msgDate.getMonth() + 1) + '.' + msgDate.getFullYear();
}
function conversationAnchorLastId() {
var s = conversationSortChrono(lastConvMessages);
if (!s.length) return null;
return s[s.length - 1].id || null;
}
function messageBodyLines(m) {
var parts = [];
if (m.therapieplan && String(m.therapieplan).trim()) parts.push('Therapieplan:\n' + m.therapieplan);
if (m.procedere && String(m.procedere).trim()) parts.push('Procedere:\n' + m.procedere);
if (m.medikamente && String(m.medikamente).trim()) parts.push('Medikamente:\n' + m.medikamente);
var k = (m.kommentar || '').trim();
if (k && k !== '\u200b') parts.push(k);
return parts.join('\n\n');
}
async function doSend() {
stopDictateIfActive();
var tx = document.getElementById('tx');
@@ -416,11 +496,14 @@ async function doSend() {
if (!currentSession) return;
var extras = {};
if (threadId) {
extras.reply_to = threadId;
extras.reply_to_absender = '';
if (mode === 'dm' && peerName) {
extras.recipient = peerName;
if (peerUserId) extras.recipient_user_id = peerUserId;
}
if (mode === 'dm' && peerName) extras.recipient = peerName;
var selfUid = String((currentSession.user_id || '')).trim();
if (selfUid) extras.sender_user_id = selfUid;
var anchor = conversationAnchorLastId();
if (anchor) extras.reply_to = anchor;
if (pendingAttach.length) {
extras.attachments = pendingAttach.map(function(a) {
return { name: a.name, data: a.data };
@@ -452,7 +535,8 @@ async function doSend() {
renderPendingStrip();
document.getElementById('att-preview').textContent = '';
lastPollSig = '';
await loadThread();
lastServerTick = -1;
await loadConversationFromServer();
} else {
alert(d.detail || 'Senden fehlgeschlagen');
}
@@ -479,11 +563,14 @@ function renderOne(m, isMe) {
div.className = 'msg ' + (isMe ? 'me' : 'them');
var meta = document.createElement('div');
meta.className = 'meta';
meta.textContent = (m.absender || '') + ' · ' + (m.zeitstempel || m.empfangen || '');
var core = (m.absender || '').split('(')[0].trim();
meta.textContent = core + ' · ' + (m.zeitstempel || m.empfangen || '');
div.appendChild(meta);
if (m.kommentar && m.kommentar.trim() && m.kommentar !== '\u200b') {
var body = messageBodyLines(m);
if (body) {
var t = document.createElement('div');
t.textContent = m.kommentar;
t.style.whiteSpace = 'pre-wrap';
t.textContent = body;
div.appendChild(t);
}
var att = m.extras && m.extras.attachments;
@@ -591,11 +678,17 @@ function toggleSettings() {
if (g) g.classList.toggle('on', !p.classList.contains('hidden'));
}
async function loadThread() {
if (!threadId || !currentSession) return;
async function loadConversationFromServer() {
if (!currentSession) return;
var me = encodeURIComponent(currentSession.display_name || '');
var aud = encodeURIComponent(conversationAudienceParam());
var qs = 'audience=' + aud + '&me=' + me;
var mu = String((currentSession.user_id || '')).trim();
if (mu) qs += '&me_user_id=' + encodeURIComponent(mu);
if (mode === 'dm' && peerUserId) qs += '&peer_user_id=' + encodeURIComponent(peerUserId);
try {
var r = await fetch(
API_BASE + '/thread/' + encodeURIComponent(String(threadId)),
API_BASE + '/conversation?' + qs,
{ credentials: 'include' }
);
if (r.status === 401) {
@@ -603,10 +696,9 @@ async function loadThread() {
return;
}
var d = await r.json();
var msgs = (d.messages || []).slice();
msgs.sort(function(a, b) {
return (a.empfangen || '').localeCompare(b.empfangen || '');
});
var msgs = conversationSortChrono(d.messages || []);
lastConvMessages = msgs;
lastServerTick = parseInt(d.tick || 0, 10);
var sig = msgs.map(function(m) { return m.id; }).join(',');
if (lastPollSig && sig !== lastPollSig) {
@@ -627,7 +719,19 @@ async function loadThread() {
var log = document.getElementById('log');
log.innerHTML = '';
var myName = currentSession.display_name;
var lastDay = '';
msgs.forEach(function(m) {
var tsFull = m.empfangen || m.zeitstempel || '';
var dk = conversationDayKey(tsFull);
if (dk && dk !== lastDay) {
lastDay = dk;
var sep = document.createElement('div');
sep.className = 'msg-date-sep';
var span = document.createElement('span');
span.textContent = conversationDateLabel(tsFull);
sep.appendChild(span);
log.appendChild(sep);
}
var isMe = (m.absender || '').indexOf(myName) === 0 ||
(m.absender || '').split('(')[0].trim() === myName;
log.appendChild(renderOne(m, isMe));
@@ -637,7 +741,7 @@ async function loadThread() {
}
function reloadThread() {
loadThread();
loadConversationFromServer();
}
async function init() {
@@ -648,12 +752,43 @@ async function init() {
updateSoundBtn();
setupDropPaste();
var up = new URLSearchParams(window.location.search);
var invQ = (up.get('invite') || '').trim();
var resolvedMini = null;
if (invQ) {
try {
var ires = await fetch(API_BASE + '/auth/resolve_invite?code=' + encodeURIComponent(invQ));
resolvedMini = await ires.json().catch(function() { return null; });
if (resolvedMini && resolvedMini.valid && resolvedMini.practice_id) {
try { localStorage.setItem('aza_practice_id', resolvedMini.practice_id); } catch(e) {}
}
} catch(e) {}
}
currentSession = await checkAuth();
if (currentSession && invQ && resolvedMini && resolvedMini.valid && resolvedMini.practice_id &&
currentSession.practice_id && currentSession.practice_id !== resolvedMini.practice_id) {
try { await fetch(API_BASE + '/auth/logout', { method: 'POST', credentials: 'include' }); } catch(e) {}
currentSession = null;
try { localStorage.setItem('aza_practice_id', resolvedMini.practice_id); } catch(e) {}
}
if (!currentSession) {
showGate('Melden Sie sich im Empfang an, um zu chatten. Dieses Fenster nutzt dieselbe Anmeldung.');
return;
}
pollTimer = setInterval(loadThread, 5000);
await loadPracticeSubline();
await loadConversationFromServer();
pollTimer = setInterval(async function() {
try {
var r = await fetch(API_BASE + '/pulse', { credentials: 'include' });
if (r.status !== 200) return;
var d = await r.json();
var t = parseInt(d.tick || 0, 10);
if (t !== lastServerTick) {
lastServerTick = t;
await loadConversationFromServer();
}
} catch (e) {}
}, 1000);
}
init();