Chat im Ticketsystem: Messenger unter Kunden, Reiter Chat auf der Kundenseite

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-01 23:35:59 +00:00
1 parent 005875d970
commit eb0b889203
7 files changed
+299 -3

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
import { useEffect, useMemo, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { FaArrowLeft, FaRotateRight } from 'react-icons/fa6'
import { PageHeader, Card, Button, EmptyState, AutocompleteInput } from '../components/ui'
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 '../styles/chat.css'
const LIST_POLL_MS = 30 * 1000
// Messenger unter Kunden -> Chat: links alle Unterhaltungen, rechts das gewaehlte Gespraech (?kunde=<id>)
export default function CustomerChatPage() {
const [params, setParams] = useSearchParams()
const kunde = params.get('kunde') || ''
const { customers } = useCustomers()
const [chats, setChats] = useState(null)
const [error, setError] = useState(null)
const [query, setQuery] = useState('')
const [reload, setReload] = useState(0)
useEffect(() => {
let alive = true
const load = () => {
if (document.visibilityState === 'hidden') return
listChats()
.then(({ chats: list }) => { if (alive) { setChats(list || []); setError(null) } })
.catch((err) => { if (alive) setError(err.message || 'Unterhaltungen konnten nicht geladen werden.') })
}
load()
const iv = setInterval(load, LIST_POLL_MS)
return () => { alive = false; clearInterval(iv) }
}, [reload, kunde])
const sorted = useMemo(() => sortChats(chats || []), [chats])
const nameOf = (id) =>
sorted.find((c) => c.customerId === id)?.customerName || customers.find((c) => c.$id === id)?.name || 'Kunde'
const open = (id) => setParams({ kunde: id })
const suggestions = (text) => {
const q = String(text || '').trim().toLowerCase()
if (!q) return []
return customers
.filter((c) => (c.name || '').toLowerCase().includes(q))
.slice(0, 8)
.map((c) => ({ value: c.name, hint: c.code || c.email || '' }))
}
const onPick = (value) => {
setQuery(value)
const hit = customers.find((c) => c.name === value)
if (hit) {
setQuery('')
open(hit.$id)
}
}
return (
<div className="page">
<PageHeader title="Chat" subtitle="Unterhaltungen mit Kunden aus dem Kundenportal" />
<div className={`chat-page ${kunde ? 'has-thread' : ''}`}>
<div className="chat-side">
<AutocompleteInput value={query} onChange={onPick} getSuggestions={suggestions} placeholder="Neue Nachricht an Kunde…" />
<Card pad={false}>
{error && (
<div className="flex items-center gap-2 wrap" style={{ padding: 12 }}>
<span className="muted">{error}</span>
<Button variant="ghost" onClick={() => setReload((k) => k + 1)}><FaRotateRight /> Erneut versuchen</Button>
</div>
)}
{!error && chats && sorted.length === 0 && <EmptyState title="Noch keine Unterhaltungen" />}
<div className="chat-list">
{sorted.map((c) => (
<button
key={c.customerId}
type="button"
className={`chat-list-item ${c.customerId === kunde ? 'active' : ''}`}
onClick={() => open(c.customerId)}
>
<span className="chat-list-top">
<strong>{c.customerName}</strong>
{c.unreadFromCustomer > 0 && <span className="side-count">{c.unreadFromCustomer}</span>}
</span>
<span className="chat-list-last">{lastLine(c)}</span>
<span className="chat-list-time">{formatViewLogTime(c.lastAt)}</span>
</button>
))}
</div>
</Card>
</div>
<div className="chat-main">
{kunde ? (
<Card title={
<span className="flex items-center gap-2">
<Button variant="ghost" size="sm" className="chat-back" onClick={() => setParams({})}><FaArrowLeft /> Alle Unterhaltungen</Button>
{nameOf(kunde)}
</span>
}>
<ChatThread key={kunde} customerId={kunde} customerName={nameOf(kunde)} />
</Card>
) : (
<Card><EmptyState title="Unterhaltung links auswählen" hint="Oder oben eine neue Nachricht an einen Kunden beginnen." /></Card>
)}
</div>
</div>
</div>
)
}
+2
View File
@@ -13,6 +13,7 @@ import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
import { customerTabs, resolveCustomerTab } from '../lib/customerTabs'
import { customerStage, isArchived, isInvoiceReady, setCustomerArchived, upgradeToCustomer } from '../lib/leads'
import CustomerViewLog from '../components/customer/CustomerViewLog'
import ChatThread from '../components/chat/ChatThread'
import KundenansichtButton from '../components/customer/KundenansichtButton'
import { getCustomerPortalStatus } from '../lib/teamApi'
import { formatLastLogin } from '../lib/lastLogin'
@@ -104,6 +105,7 @@ export default function CustomerDetailPage() {
{activeTab === 'tickets' && <CustomerTickets customerId={customer.$id} />}
{activeTab === 'invoices' && <CustomerInvoices customer={customer} onShowOverview={() => setTab('overview')} />}
{activeTab === 'projects' && <CustomerProjects customerId={customer.$id} />}
{activeTab === 'chat' && <Card><ChatThread customerId={customer.$id} customerName={customer.name} /></Card>}
{activeTab === 'protokoll' && <CustomerViewLog customerId={customer.$id} />}
</div>
)