Chat: Bilder, Kamerafoto, Dateien und Sprachnachrichten
- Kundenbereich und Admin-Chat: Bueroklammer-Menue (Bild, Kamera, Datei), Mikrofon fuer Sprachnachrichten, Vorschau vor dem Senden mit Upload-Fortschritt, Einfuegen (Strg+V) und Hineinziehen von Dateien - Kamera: am Handy/Tablet die Kamera-App, am Rechner die Kamera im Browser mit Vorschau, Kamerawechsel und "Nochmal" - Sprachnachricht: Aufnahme mit Zeit und Pegel, vorher anhoerbar oder direkt senden, hoechstens 5 Minuten - Verlauf: Bilder (einzeln bzw. Raster, Grossansicht), Player fuer Sprachnachrichten, Datei-Kacheln mit Download - Server: jede Datei einzeln roh hochgeladen (max. 25 MB, 10 je Nachricht, max. 3 gleichzeitig), Typ aus dem Inhalt, nur erlaubte Endungen, keine Programme; Ablage unter upload-data/chat/<Kunde>/ ausserhalb von public/, Auslieferung nur mit Login; Bilder, Audio und PDF inline, alles andere als Download (nosniff, CSP sandbox) - Nicht gesendete Anhaenge werden nach 24 Stunden geloescht - Benachrichtigungs-Mail nennt nur Art und Namen der Anhaenge - Appwrite: neues Attribut portalMessages.attachments (scripts/setup-chat-collection.mjs, vor dem Deploy ausfuehren) - Datenschutzerklaerung Abschnitt 6 ergaenzt (Stand 26.09.2026), PDF neu Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6e18a70a5d
commit
35d98da977
15 files changed
+2086
-107
No files matched your search
+40
-37
@@ -269,8 +269,12 @@ const adminChat = {
|
||||
activeCustomerName: '',
|
||||
lastCreatedAt: '',
|
||||
threadTimer: null,
|
||||
shownIds: new Set(),
|
||||
composer: null,
|
||||
}
|
||||
|
||||
const adminChatUrl = (customerId) => `/api/portal-admin/chats/${encodeURIComponent(customerId)}`
|
||||
|
||||
function updateAdminChatBadge(unread) {
|
||||
const badge = document.getElementById('admin-chat-badge')
|
||||
if (!badge) return
|
||||
@@ -319,11 +323,16 @@ async function loadChatList() {
|
||||
function appendAdminChatMessages(messages) {
|
||||
const box = document.getElementById('admin-chat-messages')
|
||||
if (!box || !messages.length) return
|
||||
const base = adminChatUrl(adminChat.activeCustomerId)
|
||||
for (const msg of messages) {
|
||||
const div = document.createElement('div')
|
||||
div.className = `chat-bubble ${msg.sender === 'admin' ? 'mine' : 'theirs'}`
|
||||
div.innerHTML = `${escapeHtml(msg.text)}<span class="chat-meta">${msg.sender === 'admin' ? escapeHtml(msg.senderName || 'Admin') : escapeHtml(adminChat.activeCustomerName)} · ${fmtDateTime(msg.createdAt)}</span>`
|
||||
box.appendChild(div)
|
||||
if (adminChat.shownIds.has(msg.id)) continue
|
||||
adminChat.shownIds.add(msg.id)
|
||||
const mine = msg.sender === 'admin'
|
||||
box.appendChild(ChatComposer.renderMessage(msg, {
|
||||
mine,
|
||||
meta: `${mine ? msg.senderName || 'Admin' : adminChat.activeCustomerName} · ${fmtDateTime(msg.createdAt)}`,
|
||||
fileUrl: (att) => `${base}/attachments/${encodeURIComponent(att.id)}`,
|
||||
}))
|
||||
if (msg.createdAt > adminChat.lastCreatedAt) adminChat.lastCreatedAt = msg.createdAt
|
||||
}
|
||||
box.scrollTop = box.scrollHeight
|
||||
@@ -332,8 +341,10 @@ function appendAdminChatMessages(messages) {
|
||||
async function pollAdminThread() {
|
||||
if (!adminChat.activeCustomerId || document.hidden) return
|
||||
try {
|
||||
const customerId = adminChat.activeCustomerId
|
||||
const q = adminChat.lastCreatedAt ? `?since=${encodeURIComponent(adminChat.lastCreatedAt)}` : ''
|
||||
const { messages } = await api(`/api/portal-admin/chats/${encodeURIComponent(adminChat.activeCustomerId)}/messages${q}`)
|
||||
const { messages } = await api(`/api/portal-admin/chats/${encodeURIComponent(customerId)}/messages${q}`)
|
||||
if (customerId !== adminChat.activeCustomerId) return // inzwischen andere Unterhaltung geoeffnet
|
||||
if (messages.length) {
|
||||
appendAdminChatMessages(messages)
|
||||
await api(`/api/portal-admin/chats/${encodeURIComponent(adminChat.activeCustomerId)}/read`, { method: 'POST' }).catch(() => {})
|
||||
@@ -343,52 +354,44 @@ async function pollAdminThread() {
|
||||
}
|
||||
}
|
||||
|
||||
async function sendAdminChatMessage() {
|
||||
const input = document.getElementById('admin-chat-input')
|
||||
const btn = document.getElementById('admin-chat-send')
|
||||
const text = input.value.trim()
|
||||
if (!text || !adminChat.activeCustomerId) return
|
||||
btn.disabled = true
|
||||
try {
|
||||
const { message } = await api(`/api/portal-admin/chats/${encodeURIComponent(adminChat.activeCustomerId)}/messages`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ text }),
|
||||
})
|
||||
input.value = ''
|
||||
appendAdminChatMessages([message])
|
||||
loadChatList()
|
||||
} catch (err) {
|
||||
alert('Nachricht konnte nicht gesendet werden: ' + err.message)
|
||||
} finally {
|
||||
btn.disabled = false
|
||||
input.focus()
|
||||
}
|
||||
}
|
||||
|
||||
async function openChatThread(customerId, customerName) {
|
||||
adminChat.activeCustomerId = customerId
|
||||
adminChat.activeCustomerName = customerName || 'Kunde'
|
||||
adminChat.lastCreatedAt = ''
|
||||
adminChat.shownIds = new Set()
|
||||
if (adminChat.composer) adminChat.composer.destroy()
|
||||
|
||||
const thread = document.getElementById('admin-chat-thread')
|
||||
thread.innerHTML = `
|
||||
<div style="margin-bottom:8px"><strong>${escapeHtml(adminChat.activeCustomerName)}</strong></div>
|
||||
<div class="chat-messages" id="admin-chat-messages"></div>
|
||||
<div class="chat-input-row">
|
||||
<textarea id="admin-chat-input" maxlength="2000" placeholder="Antwort an ${escapeHtml(adminChat.activeCustomerName)}…"></textarea>
|
||||
<button type="button" class="mini-btn primary" id="admin-chat-send" style="padding:12px 18px">Senden</button>
|
||||
</div>`
|
||||
<div id="admin-chat-composer"></div>`
|
||||
ChatComposer.trackScroll(document.getElementById('admin-chat-messages'))
|
||||
|
||||
document.getElementById('admin-chat-send').addEventListener('click', sendAdminChatMessage)
|
||||
document.getElementById('admin-chat-input').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
sendAdminChatMessage()
|
||||
}
|
||||
// Text, Bilder, Kamerafoto, Dateien und Sprachnachrichten (public/chat-composer.js)
|
||||
adminChat.composer = ChatComposer.create({
|
||||
root: document.getElementById('admin-chat-composer'),
|
||||
dropZone: thread,
|
||||
uploadUrl: `${adminChatUrl(customerId)}/attachments`,
|
||||
placeholder: `Antwort an ${adminChat.activeCustomerName}…`,
|
||||
placeholderShort: 'Antwort…',
|
||||
send: async ({ text, attachmentIds }) => {
|
||||
const { message } = await api(`${adminChatUrl(customerId)}/messages`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ text, attachmentIds }),
|
||||
})
|
||||
return message
|
||||
},
|
||||
onSent: (message) => {
|
||||
if (adminChat.activeCustomerId !== customerId) return
|
||||
appendAdminChatMessages([message])
|
||||
loadChatList()
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
const { messages } = await api(`/api/portal-admin/chats/${encodeURIComponent(customerId)}/messages`)
|
||||
if (customerId !== adminChat.activeCustomerId) return // inzwischen andere Unterhaltung geoeffnet
|
||||
appendAdminChatMessages(messages)
|
||||
await api(`/api/portal-admin/chats/${encodeURIComponent(customerId)}/read`, { method: 'POST' }).catch(() => {})
|
||||
updateAdminChatBadge(0)
|
||||
|
||||
Reference in new issue
Block a user