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:
1 parent
005875d970
commit
eb0b889203
7 files changed
+299
-3
No files matched your search
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user