9c6e2c76e8
Root cause of 'waiting for escalations': no queue/membership meant tickets were never assigned. Now: createTicket find-or-creates a default queue; new joinDefault endpoint + useGoOnline hook; agent-demo joins+goes-AVAILABLE on load (assignPending picks up any waiting ticket). smoke-support self-seeds (no seed script needed). Also: vitest singleFork to end cross-file DB races (45 tests deterministic). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
107 lines
3.5 KiB
TypeScript
107 lines
3.5 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import {
|
|
SupportProvider,
|
|
useAssignedTickets,
|
|
useGoOnline,
|
|
useThread,
|
|
useMessages,
|
|
type Ticket,
|
|
} from '@insignia/iios-support-web';
|
|
|
|
const SERVICE = 'http://localhost:3200';
|
|
const APP_ID = 'portal-demo';
|
|
const AGENT_ID = 'agent1';
|
|
|
|
async function devToken(userId: string): Promise<string> {
|
|
const r = await fetch(`${SERVICE}/v1/dev/token`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ appId: APP_ID, userId, name: userId }),
|
|
});
|
|
if (!r.ok) throw new Error(`devToken ${r.status} (service running with IIOS_DEV_TOKENS=1?)`);
|
|
return ((await r.json()) as { token: string }).token;
|
|
}
|
|
|
|
function AgentChat({ threadId }: { threadId: string }) {
|
|
const { open } = useThread();
|
|
useEffect(() => {
|
|
void open(threadId);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [threadId]);
|
|
const { messages, send } = useMessages(threadId);
|
|
const [text, setText] = useState('');
|
|
return (
|
|
<div style={{ flex: 1, padding: 12, fontFamily: 'sans-serif' }}>
|
|
<h3 style={{ marginTop: 0 }}>Thread {threadId.slice(0, 8)}</h3>
|
|
<div style={{ height: 280, overflow: 'auto', background: '#fafafa', padding: 8 }}>
|
|
{messages.map((m) => (
|
|
<div key={m.id} style={{ padding: '2px 0' }}>
|
|
<b>{m.senderActorId.slice(0, 6)}:</b> {m.content}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<input
|
|
value={text}
|
|
placeholder="reply as agent + Enter"
|
|
style={{ width: '100%', padding: 6, marginTop: 8 }}
|
|
onChange={(e) => setText(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' && text.trim()) {
|
|
void send(text.trim());
|
|
setText('');
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function AgentInner() {
|
|
const goOnline = useGoOnline();
|
|
const { tickets } = useAssignedTickets();
|
|
const [threadId, setThreadId] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
void goOnline(); // join default queue + go AVAILABLE
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
return (
|
|
<div style={{ display: 'flex', fontFamily: 'sans-serif' }}>
|
|
<div style={{ width: 240, borderRight: '1px solid #ddd', padding: 12 }}>
|
|
<b>Assigned tickets ({tickets.length})</b>
|
|
{tickets.length === 0 && <div style={{ color: '#999', marginTop: 6 }}>waiting for escalations…</div>}
|
|
{tickets.map((t: Ticket) => {
|
|
const tid = t.threadLinks?.[0]?.threadId ?? null;
|
|
return (
|
|
<div key={t.id} style={{ padding: '6px 0', borderBottom: '1px solid #eee' }}>
|
|
<div style={{ fontWeight: 600 }}>{t.subject}</div>
|
|
<div style={{ color: '#999' }}>{t.state}</div>
|
|
<button disabled={!tid} onClick={() => setThreadId(tid)}>
|
|
open thread
|
|
</button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
{threadId ? <AgentChat threadId={threadId} /> : <div style={{ flex: 1, padding: 12 }}>Select a ticket.</div>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function App() {
|
|
const [token, setToken] = useState<string | null>(null);
|
|
useEffect(() => {
|
|
void devToken(AGENT_ID).then(setToken);
|
|
}, []);
|
|
if (!token) return <div style={{ padding: 16, fontFamily: 'sans-serif' }}>loading agent…</div>;
|
|
return (
|
|
<div style={{ padding: 16 }}>
|
|
<h2 style={{ fontFamily: 'sans-serif' }}>IIOS P4 — Agent Dashboard ({AGENT_ID})</h2>
|
|
<SupportProvider serviceUrl={SERVICE} token={token}>
|
|
<AgentInner />
|
|
</SupportProvider>
|
|
</div>
|
|
);
|
|
}
|