From 813c6a7e18caffd525872aee8b858a7e6b65548f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 23:46:51 +0000 Subject: [PATCH] Chat: Korrekturen aus dem Abschlussreview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Gespraech startet nicht neu, wenn der Kundenname nachlaedt (Entwurf bleibt, kein verwaister Timer) - Neue Nachricht an Kunde: erst die eindeutige Beschriftung Name · E-Mail · Kd.-Nr. oeffnet Co-Authored-By: Claude Opus 5.5 --- src/components/chat/ChatThread.jsx | 11 ++++++++--- src/lib/chatFormat.js | 10 ++++++++++ src/pages/CustomerChatPage.jsx | 10 ++++++---- tests/unit/chatFormat.test.mjs | 19 +++++++++++++++++++ 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/components/chat/ChatThread.jsx b/src/components/chat/ChatThread.jsx index d5eee50..609e32d 100644 --- a/src/components/chat/ChatThread.jsx +++ b/src/components/chat/ChatThread.jsx @@ -20,6 +20,9 @@ export default function ChatThread({ customerId, customerName }) { const [error, setError] = useState(null) const [notice, setNotice] = useState('') const [reloadKey, setReloadKey] = useState(0) + // Name nur ueber eine Referenz lesen: kommt er spaeter (Liste/Kunden laden noch), startet das Gespraech nicht neu + const nameRef = useRef(customerName) + nameRef.current = customerName useEffect(() => { let alive = true @@ -39,7 +42,7 @@ export default function ChatThread({ customerId, customerName }) { state.shown.add(msg.id) box.appendChild(chat.renderMessage(msg, { mine: msg.sender === 'admin', - meta: messageMeta(msg, customerName), + meta: messageMeta(msg, nameRef.current), fileUrl: (att) => att.url, })) if (msg.createdAt > state.last) state.last = msg.createdAt @@ -83,7 +86,7 @@ export default function ChatThread({ customerId, customerName }) { dropZone: zoneRef.current, uploadUrl: chatUploadUrl(customerId), requestHeaders: teamAuthHeaders, - placeholder: `Antwort an ${customerName || 'Kunde'}…`, + placeholder: `Antwort an ${nameRef.current || 'Kunde'}…`, placeholderShort: 'Antwort…', send: async ({ text, attachmentIds }) => { const { message, notice: result } = await sendChatMessage(customerId, { text, attachmentIds }) @@ -94,6 +97,8 @@ export default function ChatThread({ customerId, customerName }) { onSent: (message) => append([message]), }) await loadAll() + // waehrend des Ladens verlassen: keinen Timer mehr anlegen, den das Aufraeumen nicht mehr sieht + if (!alive) return timer = setInterval(poll, POLL_MS) } catch (err) { if (alive) setError(err.message || 'Die Unterhaltung konnte nicht geladen werden.') @@ -108,7 +113,7 @@ export default function ChatThread({ customerId, customerName }) { document.removeEventListener('visibilitychange', onVisible) composer?.destroy() } - }, [customerId, customerName, reloadKey]) + }, [customerId, reloadKey]) return (
diff --git a/src/lib/chatFormat.js b/src/lib/chatFormat.js index 14e9202..766f9e4 100644 --- a/src/lib/chatFormat.js +++ b/src/lib/chatFormat.js @@ -29,3 +29,13 @@ export function messageMeta(msg, customerName) { // Signierte Anhang-Links gelten 15 Minuten: den Verlauf nach 10 Minuten komplett neu laden const FULL_REFRESH_MS = 10 * 60 * 1000 export const needsFullRefresh = (lastFullAt, now) => now - lastFullAt >= FULL_REFRESH_MS + +/** Eindeutige Beschriftung fuer "Neue Nachricht an Kunde…" wie im Admin-Panel: Name · E-Mail · Kd.-Nr. */ +export function customerPickLabel(c) { + return [c.name, c.email, c.code ? `Kd.-Nr. ${c.code}` : ''].filter(Boolean).join(' · ') || c.$id +} + +/** Nur die volle Beschriftung oeffnet einen Kunden; Teil- oder reine Namens-Treffer liefern null. */ +export function findCustomerByLabel(customers, value) { + return customers.find((c) => customerPickLabel(c) === value) || null +} diff --git a/src/pages/CustomerChatPage.jsx b/src/pages/CustomerChatPage.jsx index 80bc8a0..3277e20 100644 --- a/src/pages/CustomerChatPage.jsx +++ b/src/pages/CustomerChatPage.jsx @@ -6,7 +6,7 @@ import ChatThread from '../components/chat/ChatThread' import { useCustomers } from '../hooks/useCustomers' import { listChats } from '../lib/teamApi' import { formatViewLogTime } from '../lib/viewLog' -import { lastLine, sortChats } from '../lib/chatFormat' +import { customerPickLabel, findCustomerByLabel, lastLine, sortChats } from '../lib/chatFormat' import '../styles/chat.css' const LIST_POLL_MS = 30 * 1000 @@ -43,13 +43,15 @@ export default function CustomerChatPage() { const q = String(text || '').trim().toLowerCase() if (!q) return [] return customers - .filter((c) => (c.name || '').toLowerCase().includes(q)) + .map((c) => ({ c, label: customerPickLabel(c) })) + .filter(({ label }) => label.toLowerCase().includes(q)) .slice(0, 8) - .map((c) => ({ value: c.name, hint: c.code || c.email || '' })) + .map(({ label }) => ({ value: label })) } + // Erst die eindeutige Beschriftung aus der Vorschlagsliste oeffnet das Gespraech (nicht schon ein Namens-Treffer) const onPick = (value) => { setQuery(value) - const hit = customers.find((c) => c.name === value) + const hit = findCustomerByLabel(customers, value) if (hit) { setQuery('') open(hit.$id) diff --git a/tests/unit/chatFormat.test.mjs b/tests/unit/chatFormat.test.mjs index faf1742..0c2f35d 100644 --- a/tests/unit/chatFormat.test.mjs +++ b/tests/unit/chatFormat.test.mjs @@ -35,3 +35,22 @@ test('needsFullRefresh ab 10 Minuten', () => { assert.equal(needsFullRefresh(0, 599999), false) assert.equal(needsFullRefresh(0, 600000), true) }) + +// Abschlussreview: die Auswahl "Neue Nachricht an Kunde…" darf nur eindeutig gewaehlte Kunden oeffnen +const { customerPickLabel, findCustomerByLabel } = await loadModule('src/lib/chatFormat.js') + +test('customerPickLabel: Name · E-Mail · Kd.-Nr. wie im Admin-Panel, ohne Name die E-Mail', () => { + assert.equal(customerPickLabel({ $id: 'c1', name: 'Amin', email: 'a@x.de', code: '5' }), 'Amin · a@x.de · Kd.-Nr. 5') + assert.equal(customerPickLabel({ $id: 'c3', name: '', email: 'leer@x.de' }), 'leer@x.de') +}) + +test('findCustomerByLabel: Teil- oder Namens-Treffer oeffnen nichts, nur die volle Beschriftung', () => { + const customers = [ + { $id: 'c1', name: 'Amin', email: 'a@x.de', code: '5' }, + { $id: 'c2', name: 'Amin Friseur', email: 'f@x.de', code: '6' }, + ] + assert.equal(findCustomerByLabel(customers, 'Amin'), null) + assert.equal(findCustomerByLabel(customers, 'Amin Friseur'), null) + assert.equal(findCustomerByLabel(customers, 'Amin Friseur · f@x.de · Kd.-Nr. 6')?.$id, 'c2') + assert.equal(findCustomerByLabel(customers, 'Amin · a@x.de · Kd.-Nr. 5')?.$id, 'c1') +})