Chat: Team-API und Laden des Chat-Bausteins vom Kundenportal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fce2f41463
commit
717a3323fa
3 files changed
+109
-2
No files matched your search
@@ -0,0 +1,28 @@
|
|||||||
|
// Chat-Baustein des Kundenportals (public/chat-composer.js + .css) zur Laufzeit laden: Eingabezeile mit Anhaengen,
|
||||||
|
// Kamera und Sprachnachricht sowie die Sprechblasen kommen aus einer Quelle fuer Portal und Ticketsystem.
|
||||||
|
// Rein (document/window werden injiziert), unter Node testbar.
|
||||||
|
|
||||||
|
const pending = new WeakMap()
|
||||||
|
|
||||||
|
/** -> Promise<window.ChatComposer>; je document nur ein Ladevorgang, nach einem Fehler neuer Versuch. */
|
||||||
|
export function loadChatComposer({ baseUrl, doc = globalThis.document, win = globalThis.window }) {
|
||||||
|
if (win.ChatComposer) return Promise.resolve(win.ChatComposer)
|
||||||
|
if (pending.has(doc)) return pending.get(doc)
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
const link = doc.createElement('link')
|
||||||
|
link.rel = 'stylesheet'
|
||||||
|
link.href = `${baseUrl}/chat-composer.css`
|
||||||
|
doc.head.appendChild(link)
|
||||||
|
const script = doc.createElement('script')
|
||||||
|
script.src = `${baseUrl}/chat-composer.js`
|
||||||
|
script.async = true
|
||||||
|
script.onload = () => (win.ChatComposer ? resolve(win.ChatComposer) : script.onerror())
|
||||||
|
script.onerror = () => {
|
||||||
|
pending.delete(doc)
|
||||||
|
reject(new Error('Der Chat-Baustein konnte nicht geladen werden.'))
|
||||||
|
}
|
||||||
|
doc.head.appendChild(script)
|
||||||
|
})
|
||||||
|
pending.set(doc, promise)
|
||||||
|
return promise
|
||||||
|
}
|
||||||
+32
-2
@@ -1,14 +1,18 @@
|
|||||||
import { account } from './appwrite'
|
import { account } from './appwrite'
|
||||||
import { createFinanceRequest } from './financeRequest'
|
import { createFinanceRequest } from './financeRequest'
|
||||||
|
import { createJwtCache } from './serverpulsAuth'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client fuer den Team-Eingang des Kundenportals (/api/team): alle Mitarbeiter, Appwrite-JWT (Bearer).
|
* Client fuer den Team-Eingang des Kundenportals (/api/team): alle Mitarbeiter, Appwrite-JWT (Bearer).
|
||||||
* Anfrage-Kern (JWT-Cache, Wiederholung bei 401, deutsche Fehlertexte) wie bei den Finanzen: financeRequest.js.
|
* Anfrage-Kern (JWT-Cache, Wiederholung bei 401, deutsche Fehlertexte) wie bei den Finanzen: financeRequest.js.
|
||||||
*/
|
*/
|
||||||
const PROJECT_ADMIN_URL =
|
export const PORTAL_URL =
|
||||||
import.meta.env.VITE_PROJECT_ADMIN_URL || 'https://project.webklar.com'
|
import.meta.env.VITE_PROJECT_ADMIN_URL || 'https://project.webklar.com'
|
||||||
|
|
||||||
const request = createFinanceRequest({ baseUrl: `${PROJECT_ADMIN_URL}/api/team`, createJWT: () => account.createJWT() })
|
const request = createFinanceRequest({ baseUrl: `${PORTAL_URL}/api/team`, createJWT: () => account.createJWT() })
|
||||||
|
|
||||||
|
// Eigenes JWT fuer Anfragen, die nicht ueber request laufen (Hochladen im Chat-Baustein), 10 Minuten gecacht
|
||||||
|
const jwtCache = createJwtCache({ createJWT: () => account.createJWT() })
|
||||||
|
|
||||||
const enc = encodeURIComponent
|
const enc = encodeURIComponent
|
||||||
|
|
||||||
@@ -29,3 +33,29 @@ export async function createPreviewLink({ subdomain, previewUrl }) {
|
|||||||
const { openUrl } = await request('/preview-link', { method: 'POST', body: { subdomain, previewUrl } })
|
const { openUrl } = await request('/preview-link', { method: 'POST', body: { subdomain, previewUrl } })
|
||||||
return openUrl
|
return openUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Chat (Messenger, Reiter "Chat", Zahl in der Sidebar) ---
|
||||||
|
|
||||||
|
/** { chats: [{ customerId, customerName, lastText, lastSender, lastAt, unreadFromCustomer }] } */
|
||||||
|
export const listChats = () => request('/chats')
|
||||||
|
|
||||||
|
/** { unread } */
|
||||||
|
export const getChatUnreadCount = () => request('/chats/unread-count')
|
||||||
|
|
||||||
|
/** { messages }; mit since nur neuere (Abfrage im offenen Gespraech) */
|
||||||
|
export const getChatMessages = (customerId, since = '') =>
|
||||||
|
request(`/chats/${enc(customerId)}/messages${since ? `?since=${enc(since)}` : ''}`)
|
||||||
|
|
||||||
|
/** { message, notice } */
|
||||||
|
export const sendChatMessage = (customerId, { text, attachmentIds }) =>
|
||||||
|
request(`/chats/${enc(customerId)}/messages`, { method: 'POST', body: { text, attachmentIds } })
|
||||||
|
|
||||||
|
export const markChatRead = (customerId) => request(`/chats/${enc(customerId)}/read`, { method: 'POST', body: {} })
|
||||||
|
|
||||||
|
/** Upload-Adresse fuer den Chat-Baustein (POST roh, DELETE <url>/<id>) */
|
||||||
|
export const chatUploadUrl = (customerId) => `${PORTAL_URL}/api/team/chats/${enc(customerId)}/attachments`
|
||||||
|
|
||||||
|
/** Header fuer den Chat-Baustein (Option requestHeaders) */
|
||||||
|
export async function teamAuthHeaders() {
|
||||||
|
return { Authorization: `Bearer ${await jwtCache.get()}` }
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { loadModule } from './load.mjs'
|
||||||
|
|
||||||
|
// Chat-Baustein (chat-composer.js/.css) wird zur Laufzeit vom Kundenportal geladen: eine Quelle fuer Portal und WOMS
|
||||||
|
const { loadChatComposer } = await loadModule('src/lib/loadChatComposer.js')
|
||||||
|
|
||||||
|
function fakeDoc() {
|
||||||
|
const appended = []
|
||||||
|
return {
|
||||||
|
appended,
|
||||||
|
head: { appendChild: (node) => { appended.push(node); return node } },
|
||||||
|
createElement: (tag) => ({ tag, attrs: {}, setAttribute(k, v) { this.attrs[k] = v } }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('haengt CSS und Skript einmal an und liefert window.ChatComposer', async () => {
|
||||||
|
const doc = fakeDoc()
|
||||||
|
const win = {}
|
||||||
|
const first = loadChatComposer({ baseUrl: 'https://project.webklar.com', doc, win })
|
||||||
|
const second = loadChatComposer({ baseUrl: 'https://project.webklar.com', doc, win })
|
||||||
|
assert.equal(doc.appended.length, 2)
|
||||||
|
const link = doc.appended.find((n) => n.tag === 'link')
|
||||||
|
const script = doc.appended.find((n) => n.tag === 'script')
|
||||||
|
assert.equal(link.href || link.attrs.href, 'https://project.webklar.com/chat-composer.css')
|
||||||
|
assert.equal(link.rel || link.attrs.rel, 'stylesheet')
|
||||||
|
assert.equal(script.src || script.attrs.src, 'https://project.webklar.com/chat-composer.js')
|
||||||
|
win.ChatComposer = { create() {} }
|
||||||
|
script.onload()
|
||||||
|
assert.equal(await first, win.ChatComposer)
|
||||||
|
assert.equal(await second, win.ChatComposer)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Skript laedt nicht -> deutsche Meldung, naechster Aufruf versucht es neu', async () => {
|
||||||
|
const doc = fakeDoc()
|
||||||
|
const win = {}
|
||||||
|
const p = loadChatComposer({ baseUrl: 'https://p', doc, win })
|
||||||
|
doc.appended.find((n) => n.tag === 'script').onerror()
|
||||||
|
await assert.rejects(p, { message: 'Der Chat-Baustein konnte nicht geladen werden.' })
|
||||||
|
loadChatComposer({ baseUrl: 'https://p', doc, win })
|
||||||
|
assert.equal(doc.appended.filter((n) => n.tag === 'script').length, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('schon geladen (win.ChatComposer vorhanden) -> nichts anhaengen', async () => {
|
||||||
|
const doc = fakeDoc()
|
||||||
|
const win = { ChatComposer: { create() {} } }
|
||||||
|
assert.equal(await loadChatComposer({ baseUrl: 'https://p', doc, win }), win.ChatComposer)
|
||||||
|
assert.equal(doc.appended.length, 0)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user