update
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user